init: pristine aerc 0.20.0 source

This commit is contained in:
Mortdecai
2026-04-07 19:54:54 -04:00
commit 083402a548
502 changed files with 68722 additions and 0 deletions
+308
View File
@@ -0,0 +1,308 @@
package types
import (
"context"
"io"
"time"
"git.sr.ht/~rjarry/aerc/config"
"git.sr.ht/~rjarry/aerc/models"
"github.com/emersion/go-message/mail"
)
type WorkerMessage interface {
InResponseTo() WorkerMessage
getId() int64
setId(id int64)
Account() string
setAccount(string)
}
type Message struct {
inResponseTo WorkerMessage
id int64
acct string
}
func RespondTo(msg WorkerMessage) Message {
return Message{
inResponseTo: msg,
}
}
func (m Message) InResponseTo() WorkerMessage {
return m.inResponseTo
}
func (m Message) getId() int64 {
return m.id
}
func (m *Message) setId(id int64) {
m.id = id
}
func (m *Message) Account() string {
return m.acct
}
func (m *Message) setAccount(name string) {
m.acct = name
}
// Meta-messages
type Done struct {
Message
}
type Error struct {
Message
Error error
}
type Cancelled struct {
Message
}
type ConnError struct {
Message
Error error
}
type Unsupported struct {
Message
}
// Actions
type Configure struct {
Message
Config *config.AccountConfig
}
type Connect struct {
Message
}
type Reconnect struct {
Message
}
type Disconnect struct {
Message
}
type ListDirectories struct {
Message
}
type OpenDirectory struct {
Message
Context context.Context
Directory string
Query string
Force bool
}
type FetchDirectoryContents struct {
Message
Context context.Context
SortCriteria []*SortCriterion
Filter *SearchCriteria
}
type FetchDirectoryThreaded struct {
Message
Context context.Context
SortCriteria []*SortCriterion
Filter *SearchCriteria
ThreadContext bool
}
type SearchDirectory struct {
Message
Context context.Context
Criteria *SearchCriteria
}
type DirectoryThreaded struct {
Message
Threads []*Thread
}
type CreateDirectory struct {
Message
Directory string
Quiet bool
}
type RemoveDirectory struct {
Message
Directory string
Quiet bool
}
type FetchMessageHeaders struct {
Message
Context context.Context
Uids []models.UID
}
type FetchFullMessages struct {
Message
Uids []models.UID
}
type FetchMessageBodyPart struct {
Message
Uid models.UID
Part []int
}
type FetchMessageFlags struct {
Message
Context context.Context
Uids []models.UID
}
type DeleteMessages struct {
Message
Uids []models.UID
MultiFileStrategy *MultiFileStrategy
}
// Flag messages with different mail types
type FlagMessages struct {
Message
Enable bool
Flags models.Flags
Uids []models.UID
}
type AnsweredMessages struct {
Message
Answered bool
Uids []models.UID
}
type ForwardedMessages struct {
Message
Forwarded bool
Uids []models.UID
}
type CopyMessages struct {
Message
Destination string
Uids []models.UID
MultiFileStrategy *MultiFileStrategy
}
type MoveMessages struct {
Message
Destination string
Uids []models.UID
MultiFileStrategy *MultiFileStrategy
}
type AppendMessage struct {
Message
Destination string
Flags models.Flags
Date time.Time
Reader io.Reader
Length int
}
type CheckMail struct {
Message
Directories []string
Command string
Timeout time.Duration
}
type StartSendingMessage struct {
Message
From *mail.Address
Rcpts []*mail.Address
CopyTo []string
}
// Messages
type Directory struct {
Message
Dir *models.Directory
}
type DirectoryInfo struct {
Message
Info *models.DirectoryInfo
Refetch bool
}
type DirectoryContents struct {
Message
Uids []models.UID
}
type SearchResults struct {
Message
Uids []models.UID
}
type MessageInfo struct {
Message
Info *models.MessageInfo
NeedsFlags bool
}
type FullMessage struct {
Message
Content *models.FullMessage
}
type MessageBodyPart struct {
Message
Part *models.MessageBodyPart
}
type MessagesDeleted struct {
Message
Uids []models.UID
}
type MessagesCopied struct {
Message
Destination string
Uids []models.UID
}
type MessagesMoved struct {
Message
Destination string
Uids []models.UID
}
type ModifyLabels struct {
Message
Uids []models.UID
Add []string
Remove []string
}
type LabelList struct {
Message
Labels []string
}
type CheckMailDirectories struct {
Message
Directories []string
}
type MessageWriter struct {
Message
Writer io.WriteCloser
}
+33
View File
@@ -0,0 +1,33 @@
package types
// MultiFileStrategy represents a strategy for taking file-based actions (e.g.,
// move, copy, delete) on messages that are represented by more than one file.
// These strategies are only used by the notmuch backend but are defined in this
// package to prevent import cycles.
type MultiFileStrategy uint
const (
Refuse MultiFileStrategy = iota
ActAll
ActOne
ActOneDelRest
ActDir
ActDirDelRest
)
var StrToStrategy = map[string]MultiFileStrategy{
"refuse": Refuse,
"act-all": ActAll,
"act-one": ActOne,
"act-one-delete-rest": ActOneDelRest,
"act-dir": ActDir,
"act-dir-delete-rest": ActDirDelRest,
}
func StrategyStrs() []string {
strs := make([]string, 0, len(StrToStrategy))
for s := range StrToStrategy {
strs = append(strs, s)
}
return strs
}
+84
View File
@@ -0,0 +1,84 @@
package types
import (
"net/textproto"
"time"
"git.sr.ht/~rjarry/aerc/models"
)
type SearchCriteria struct {
WithFlags models.Flags
WithoutFlags models.Flags
From []string
To []string
Cc []string
Headers textproto.MIMEHeader
StartDate time.Time
EndDate time.Time
SearchBody bool
SearchAll bool
Terms []string
UseExtension bool
}
func (c *SearchCriteria) PrepareHeader() {
if c == nil {
return
}
if c.Headers == nil {
c.Headers = make(textproto.MIMEHeader)
}
for _, from := range c.From {
c.Headers.Add("From", from)
}
for _, to := range c.To {
c.Headers.Add("To", to)
}
for _, cc := range c.Cc {
c.Headers.Add("Cc", cc)
}
}
func (c *SearchCriteria) Combine(other *SearchCriteria) *SearchCriteria {
if c == nil {
return other
}
headers := make(textproto.MIMEHeader)
for k, v := range c.Headers {
headers[k] = v
}
for k, v := range other.Headers {
headers[k] = v
}
start := c.StartDate
if !other.StartDate.IsZero() {
start = other.StartDate
}
end := c.EndDate
if !other.EndDate.IsZero() {
end = other.EndDate
}
from := make([]string, len(c.From)+len(other.From))
copy(from[:len(c.From)], c.From)
copy(from[len(c.From):], other.From)
to := make([]string, len(c.To)+len(other.To))
copy(to[:len(c.To)], c.To)
copy(to[len(c.To):], other.To)
cc := make([]string, len(c.Cc)+len(other.Cc))
copy(cc[:len(c.Cc)], c.Cc)
copy(cc[len(c.Cc):], other.Cc)
return &SearchCriteria{
WithFlags: c.WithFlags | other.WithFlags,
WithoutFlags: c.WithoutFlags | other.WithoutFlags,
From: from,
To: to,
Cc: cc,
Headers: headers,
StartDate: start,
EndDate: end,
SearchBody: c.SearchBody || other.SearchBody,
SearchAll: c.SearchAll || other.SearchAll,
Terms: append(c.Terms, other.Terms...),
}
}
+20
View File
@@ -0,0 +1,20 @@
package types
type SortField int
const (
SortArrival SortField = iota
SortCc
SortDate
SortFrom
SortRead
SortSize
SortSubject
SortTo
SortFlagged
)
type SortCriterion struct {
Field SortField
Reverse bool
}
+185
View File
@@ -0,0 +1,185 @@
package types
import (
"errors"
"fmt"
"sort"
"git.sr.ht/~rjarry/aerc/lib/log"
"git.sr.ht/~rjarry/aerc/models"
)
type Thread struct {
Uid models.UID
Parent *Thread
PrevSibling *Thread
NextSibling *Thread
FirstChild *Thread
Hidden int // if this flag is not zero the message isn't rendered in the UI
Deleted bool // if this flag is set the message was deleted
// if this flag is set the message is the root of an incomplete thread
Dummy bool
// Context indicates the message doesn't match the mailbox / query but
// is displayed for context
Context bool
}
// AddChild appends the child node at the end of the existing children of t.
func (t *Thread) AddChild(child *Thread) {
t.InsertCmp(child, func(_, _ *Thread) bool { return true })
}
// OrderedInsert inserts the child node in ascending order among the existing
// children based on their respective UIDs.
func (t *Thread) OrderedInsert(child *Thread) {
t.InsertCmp(child, func(child, iter *Thread) bool { return child.Uid > iter.Uid })
}
// InsertCmp inserts child as a child node into t in ascending order. The
// ascending order is determined by the bigger function that compares the child
// with the existing children. It should return true when the child is bigger
// than the other, and false otherwise.
func (t *Thread) InsertCmp(child *Thread, bigger func(*Thread, *Thread) bool) {
if t.FirstChild == nil {
t.FirstChild = child
} else {
start := &Thread{NextSibling: t.FirstChild}
var iter *Thread
for iter = start; iter.NextSibling != nil &&
bigger(child, iter.NextSibling); iter = iter.NextSibling {
}
child.NextSibling = iter.NextSibling
iter.NextSibling = child
t.FirstChild = start.NextSibling
}
child.Parent = t
}
func (t *Thread) Walk(walkFn NewThreadWalkFn) error {
err := newWalk(t, walkFn, 0, nil)
if errors.Is(err, ErrSkipThread) {
return nil
}
return err
}
// Root returns the root thread of the thread tree
func (t *Thread) Root() *Thread {
if t == nil {
return nil
}
var iter *Thread
for iter = t; iter.Parent != nil; iter = iter.Parent {
}
return iter
}
// Uids returns all associated uids for the given thread and its children
func (t *Thread) Uids() []models.UID {
if t == nil {
return nil
}
uids := make([]models.UID, 0)
err := t.Walk(func(node *Thread, _ int, _ error) error {
uids = append(uids, node.Uid)
return nil
})
if err != nil {
log.Errorf("walk to collect uids failed: %v", err)
}
return uids
}
func (t *Thread) String() string {
if t == nil {
return "<nil>"
}
var parent models.UID
if t.Parent != nil {
parent = t.Parent.Uid
}
var next models.UID
if t.NextSibling != nil {
next = t.NextSibling.Uid
}
var child models.UID
if t.FirstChild != nil {
child = t.FirstChild.Uid
}
return fmt.Sprintf(
"[%s] (parent:%s, next:%s, child:%s)",
t.Uid, parent, next, child,
)
}
func newWalk(node *Thread, walkFn NewThreadWalkFn, lvl int, ce error) error {
if node == nil {
return nil
}
err := walkFn(node, lvl, ce)
if err != nil {
return err
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
err = newWalk(child, walkFn, lvl+1, err)
if errors.Is(err, ErrSkipThread) {
err = nil
continue
} else if err != nil {
return err
}
}
return nil
}
var ErrSkipThread = errors.New("skip this Thread")
type NewThreadWalkFn func(t *Thread, level int, currentErr error) error
// Implement interface to be able to sort threads by newest (max UID)
type ByUID []*Thread
func getMaxUID(thread *Thread) models.UID {
// TODO: should we make this part of the Thread type to avoid recomputation?
var Uid models.UID
_ = thread.Walk(func(t *Thread, _ int, currentErr error) error {
if t.Deleted || t.Hidden > 0 {
return nil
}
if t.Uid > Uid {
Uid = t.Uid
}
return nil
})
return Uid
}
func (s ByUID) Len() int {
return len(s)
}
func (s ByUID) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s ByUID) Less(i, j int) bool {
maxUID_i := getMaxUID(s[i])
maxUID_j := getMaxUID(s[j])
return maxUID_i < maxUID_j
}
func SortThreadsBy(toSort []*Thread, sortBy []models.UID) {
// build a map from sortBy
uidMap := make(map[models.UID]int)
for i, uid := range sortBy {
uidMap[uid] = i
}
// sortslice of toSort with less function of indexing the map sortBy
sort.Slice(toSort, func(i, j int) bool {
return uidMap[getMaxUID(toSort[i])] < uidMap[getMaxUID(toSort[j])]
})
}
+232
View File
@@ -0,0 +1,232 @@
package types
import (
"fmt"
"strings"
"testing"
"git.sr.ht/~rjarry/aerc/models"
)
func genFakeTree() *Thread {
tree := new(Thread)
var prevChild *Thread
for i := uint32(1); i < uint32(3); i++ {
child := &Thread{
Uid: models.Uint32ToUid(i * 10),
Parent: tree,
PrevSibling: prevChild,
}
if prevChild != nil {
prevChild.NextSibling = child
} else if tree.FirstChild == nil {
tree.FirstChild = child
} else {
panic("unreachable")
}
prevChild = child
var prevSecond *Thread
for j := uint32(1); j < uint32(3); j++ {
second := &Thread{
Uid: models.Uint32ToUid(models.UidToUint32(child.Uid) + j),
Parent: child,
PrevSibling: prevSecond,
}
if prevSecond != nil {
prevSecond.NextSibling = second
} else if child.FirstChild == nil {
child.FirstChild = second
} else {
panic("unreachable")
}
prevSecond = second
var prevThird *Thread
limit := uint32(3)
if j == 2 {
limit = 8
}
for k := uint32(1); k < limit; k++ {
third := &Thread{
Uid: models.Uint32ToUid(models.UidToUint32(second.Uid)*10 + j),
Parent: second,
PrevSibling: prevThird,
}
if prevThird != nil {
prevThird.NextSibling = third
} else if second.FirstChild == nil {
second.FirstChild = third
} else {
panic("unreachable")
}
prevThird = third
}
}
}
return tree
}
func TestNewWalk(t *testing.T) {
tree := genFakeTree()
var prefix []string
lastLevel := 0
tree.Walk(func(t *Thread, lvl int, e error) error {
if e != nil {
fmt.Printf("ERROR: %v\n", e)
}
if lvl > lastLevel && lvl > 1 {
// we actually just descended... so figure out what connector we need
// level 1 is flush to the root, so we avoid the indentation there
if t.Parent.NextSibling != nil {
prefix = append(prefix, "│ ")
} else {
prefix = append(prefix, " ")
}
} else if lvl < lastLevel {
// ascended, need to trim the prefix layers
diff := lastLevel - lvl
prefix = prefix[:len(prefix)-diff]
}
var arrow string
if t.Parent != nil {
if t.NextSibling != nil {
arrow = "├─>"
} else {
arrow = "└─>"
}
}
// format
fmt.Printf("%s%s%s\n", strings.Join(prefix, ""), arrow, t)
lastLevel = lvl
return nil
})
}
func uidSeq(tree *Thread) string {
var seq []string
tree.Walk(func(t *Thread, _ int, _ error) error {
seq = append(seq, string(t.Uid))
return nil
})
return strings.Join(seq, ".")
}
func TestThread_AddChild(t *testing.T) {
tests := []struct {
name string
seq []models.UID
want string
}{
{
name: "ascending",
seq: []models.UID{"1", "2", "3", "4", "5", "6"},
want: ".1.2.3.4.5.6",
},
{
name: "descending",
seq: []models.UID{"6", "5", "4", "3", "2", "1"},
want: ".6.5.4.3.2.1",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
tree := new(Thread)
for _, i := range test.seq {
tree.AddChild(&Thread{Uid: i})
}
if got := uidSeq(tree); got != test.want {
t.Errorf("got: %s, but wanted: %s", got,
test.want)
}
})
}
}
func TestThread_OrderedInsert(t *testing.T) {
tests := []struct {
name string
seq []models.UID
want string
}{
{
name: "ascending",
seq: []models.UID{"1", "2", "3", "4", "5", "6"},
want: ".1.2.3.4.5.6",
},
{
name: "descending",
seq: []models.UID{"6", "5", "4", "3", "2", "1"},
want: ".1.2.3.4.5.6",
},
{
name: "mixed",
seq: []models.UID{"2", "1", "6", "3", "4", "5"},
want: ".1.2.3.4.5.6",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
tree := new(Thread)
for _, i := range test.seq {
tree.OrderedInsert(&Thread{Uid: i})
}
if got := uidSeq(tree); got != test.want {
t.Errorf("got: %s, but wanted: %s", got,
test.want)
}
})
}
}
func TestThread_InsertCmd(t *testing.T) {
tests := []struct {
name string
seq []models.UID
want string
}{
{
name: "ascending",
seq: []models.UID{"1", "2", "3", "4", "5", "6"},
want: ".6.4.2.1.3.5",
},
{
name: "descending",
seq: []models.UID{"6", "5", "4", "3", "2", "1"},
want: ".6.4.2.1.3.5",
},
{
name: "mixed",
seq: []models.UID{"2", "1", "6", "3", "4", "5"},
want: ".6.4.2.1.3.5",
},
}
sortMap := map[models.UID]int{
"6": 1,
"4": 2,
"2": 3,
"1": 4,
"3": 5,
"5": 6,
}
// bigger compares the new child with the next node and returns true if
// the child node is bigger and false otherwise.
bigger := func(newNode, nextChild *Thread) bool {
return sortMap[newNode.Uid] > sortMap[nextChild.Uid]
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
tree := new(Thread)
for _, i := range test.seq {
tree.InsertCmp(&Thread{Uid: i}, bigger)
}
if got := uidSeq(tree); got != test.want {
t.Errorf("got: %s, but wanted: %s", got,
test.want)
}
})
}
}
+172
View File
@@ -0,0 +1,172 @@
package types
import (
"container/list"
"sync"
"sync/atomic"
"git.sr.ht/~rjarry/aerc/lib/log"
"git.sr.ht/~rjarry/aerc/models"
)
type WorkerInteractor interface {
log.Logger
Actions() chan WorkerMessage
ProcessAction(WorkerMessage) WorkerMessage
PostAction(WorkerMessage, func(msg WorkerMessage))
PostMessage(WorkerMessage, func(msg WorkerMessage))
Unwrap() WorkerInteractor
}
var lastId int64 = 1 // access via atomic
type Backend interface {
Run()
Capabilities() *models.Capabilities
PathSeparator() string
}
type Worker struct {
Backend Backend
actions chan WorkerMessage
actionCallbacks map[int64]func(msg WorkerMessage)
messageCallbacks map[int64]func(msg WorkerMessage)
actionQueue *list.List
status int32
name string
sync.Mutex
log.Logger
}
func NewWorker(name string) *Worker {
return &Worker{
Logger: log.NewLogger(name, 2),
actions: make(chan WorkerMessage),
actionCallbacks: make(map[int64]func(msg WorkerMessage)),
messageCallbacks: make(map[int64]func(msg WorkerMessage)),
actionQueue: list.New(),
name: name,
}
}
func (worker *Worker) Unwrap() WorkerInteractor {
return nil
}
func (worker *Worker) Actions() chan WorkerMessage {
return worker.actions
}
func (worker *Worker) setId(msg WorkerMessage) {
id := atomic.AddInt64(&lastId, 1)
msg.setId(id)
}
const (
idle int32 = iota
busy
)
// Add a new task to the action queue without blocking. Start processing the
// queue in the background if needed.
func (worker *Worker) queue(msg WorkerMessage) {
worker.Lock()
defer worker.Unlock()
worker.actionQueue.PushBack(msg)
if atomic.LoadInt32(&worker.status) == idle {
atomic.StoreInt32(&worker.status, busy)
go worker.processQueue()
}
}
// Start processing the action queue and write all messages to the actions
// channel, one by one. Stop when the action queue is empty.
func (worker *Worker) processQueue() {
defer log.PanicHandler()
for {
worker.Lock()
e := worker.actionQueue.Front()
if e == nil {
atomic.StoreInt32(&worker.status, idle)
worker.Unlock()
return
}
msg := worker.actionQueue.Remove(e).(WorkerMessage)
worker.Unlock()
worker.actions <- msg
}
}
// PostAction posts an action to the worker. This method should not be called
// from the same goroutine that the worker runs in or deadlocks may occur
func (worker *Worker) PostAction(msg WorkerMessage, cb func(msg WorkerMessage)) {
worker.setId(msg)
// write to actions channel without blocking
worker.queue(msg)
if cb != nil {
worker.Lock()
worker.actionCallbacks[msg.getId()] = cb
worker.Unlock()
}
}
var WorkerMessages = make(chan WorkerMessage, 50)
// PostMessage posts an message to the UI. This method should not be called
// from the same goroutine that the UI runs in or deadlocks may occur
func (worker *Worker) PostMessage(msg WorkerMessage,
cb func(msg WorkerMessage),
) {
worker.setId(msg)
msg.setAccount(worker.name)
WorkerMessages <- msg
if cb != nil {
worker.Lock()
worker.messageCallbacks[msg.getId()] = cb
worker.Unlock()
}
}
func (worker *Worker) ProcessMessage(msg WorkerMessage) WorkerMessage {
if inResponseTo := msg.InResponseTo(); inResponseTo != nil {
worker.Lock()
f, ok := worker.actionCallbacks[inResponseTo.getId()]
worker.Unlock()
if ok {
f(msg)
switch msg.(type) {
case *Cancelled, *Done:
worker.Lock()
delete(worker.actionCallbacks, inResponseTo.getId())
worker.Unlock()
}
}
}
return msg
}
func (worker *Worker) ProcessAction(msg WorkerMessage) WorkerMessage {
if inResponseTo := msg.InResponseTo(); inResponseTo != nil {
worker.Lock()
f, ok := worker.messageCallbacks[inResponseTo.getId()]
worker.Unlock()
if ok {
f(msg)
if _, ok := msg.(*Done); ok {
worker.Lock()
delete(worker.messageCallbacks, inResponseTo.getId())
worker.Unlock()
}
}
}
return msg
}
func (worker *Worker) PathSeparator() string {
return worker.Backend.PathSeparator()
}