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
+179
View File
@@ -0,0 +1,179 @@
package middleware
import (
"fmt"
"strings"
"sync"
"git.sr.ht/~rjarry/aerc/worker/types"
)
type folderMapper struct {
sync.Mutex
types.WorkerInteractor
fm folderMap
table map[string]string
}
func NewFolderMapper(base types.WorkerInteractor, mapping map[string]string,
order []string,
) types.WorkerInteractor {
base.Infof("loading worker middleware: foldermapper")
return &folderMapper{
WorkerInteractor: base,
fm: folderMap{mapping, order},
table: make(map[string]string),
}
}
func (f *folderMapper) Unwrap() types.WorkerInteractor {
return f.WorkerInteractor
}
func (f *folderMapper) incoming(msg types.WorkerMessage, dir string) string {
f.Lock()
defer f.Unlock()
mapped, ok := f.table[dir]
if !ok {
return dir
}
return mapped
}
func (f *folderMapper) outgoing(msg types.WorkerMessage, dir string) string {
f.Lock()
defer f.Unlock()
for k, v := range f.table {
if v == dir {
mapped := k
return mapped
}
}
return dir
}
func (f *folderMapper) store(s string) {
f.Lock()
defer f.Unlock()
display := f.fm.Apply(s)
f.table[display] = s
f.Tracef("store display folder '%s' to '%s'", display, s)
}
func (f *folderMapper) create(s string) (string, error) {
f.Lock()
defer f.Unlock()
backend := createFolder(f.table, s)
if _, exists := f.table[s]; exists {
return s, fmt.Errorf("folder already exists: %s", s)
}
f.table[s] = backend
f.Tracef("create display folder '%s' as '%s'", s, backend)
return backend, nil
}
func (f *folderMapper) ProcessAction(msg types.WorkerMessage) types.WorkerMessage {
switch msg := msg.(type) {
case *types.CheckMail:
for i := range msg.Directories {
msg.Directories[i] = f.incoming(msg, msg.Directories[i])
}
case *types.CopyMessages:
msg.Destination = f.incoming(msg, msg.Destination)
case *types.AppendMessage:
msg.Destination = f.incoming(msg, msg.Destination)
case *types.MoveMessages:
msg.Destination = f.incoming(msg, msg.Destination)
case *types.CreateDirectory:
var err error
msg.Directory, err = f.create(msg.Directory)
if err != nil {
f.Errorf("error creating new directory: %v", err)
}
case *types.RemoveDirectory:
msg.Directory = f.incoming(msg, msg.Directory)
case *types.OpenDirectory:
msg.Directory = f.incoming(msg, msg.Directory)
}
return f.WorkerInteractor.ProcessAction(msg)
}
func (f *folderMapper) PostMessage(msg types.WorkerMessage, cb func(m types.WorkerMessage)) {
switch msg := msg.(type) {
case *types.Done:
switch msg := msg.InResponseTo().(type) {
case *types.CheckMail:
for i := range msg.Directories {
msg.Directories[i] = f.outgoing(msg, msg.Directories[i])
}
case *types.CopyMessages:
msg.Destination = f.outgoing(msg, msg.Destination)
case *types.AppendMessage:
msg.Destination = f.outgoing(msg, msg.Destination)
case *types.MoveMessages:
msg.Destination = f.outgoing(msg, msg.Destination)
case *types.CreateDirectory:
msg.Directory = f.outgoing(msg, msg.Directory)
case *types.RemoveDirectory:
msg.Directory = f.outgoing(msg, msg.Directory)
case *types.OpenDirectory:
msg.Directory = f.outgoing(msg, msg.Directory)
}
case *types.CheckMailDirectories:
for i := range msg.Directories {
msg.Directories[i] = f.outgoing(msg, msg.Directories[i])
}
case *types.Directory:
f.store(msg.Dir.Name)
msg.Dir.Name = f.outgoing(msg, msg.Dir.Name)
case *types.DirectoryInfo:
msg.Info.Name = f.outgoing(msg, msg.Info.Name)
}
f.WorkerInteractor.PostMessage(msg, cb)
}
// folderMap contains the mapping between the ui and backend folder names
type folderMap struct {
mapping map[string]string
order []string
}
// Apply applies the mapping from the folder map to the backend folder
func (f *folderMap) Apply(s string) string {
for _, k := range f.order {
v := f.mapping[k]
strict := true
if strings.HasSuffix(v, "*") {
v = strings.TrimSuffix(v, "*")
strict = false
}
if (strings.HasPrefix(s, v) && !strict) || (s == v && strict) {
term := strings.TrimPrefix(s, v)
if strings.Contains(k, "*") && !strict {
prefix := k
for strings.Contains(prefix, "**") {
prefix = strings.ReplaceAll(prefix, "**", "*")
}
s = strings.Replace(prefix, "*", term, 1)
} else {
s = k + term
}
}
}
return s
}
// createFolder reverses the mapping of a new folder name
func createFolder(table map[string]string, s string) string {
max, key := 0, ""
for k := range table {
if strings.HasPrefix(s, k) && len(k) > max {
max, key = len(k), k
}
}
if max > 0 && key != "" {
s = table[key] + strings.TrimPrefix(s, key)
}
return s
}
+103
View File
@@ -0,0 +1,103 @@
package middleware
import (
"reflect"
"testing"
)
func TestFolderMap_Apply(t *testing.T) {
tests := []struct {
name string
mapping map[string]string
order []string
input []string
want []string
}{
{
name: "strict single folder mapping",
mapping: map[string]string{"Drafts": "INBOX/Drafts"},
order: []string{"Drafts"},
input: []string{"INBOX/Drafts"},
want: []string{"Drafts"},
},
{
name: "prefix mapping with * suffix",
mapping: map[string]string{"Prefix/": "INBOX/*"},
order: []string{"Prefix/"},
input: []string{"INBOX", "INBOX/Test1", "INBOX/Test2", "Archive"},
want: []string{"INBOX", "Prefix/Test1", "Prefix/Test2", "Archive"},
},
{
name: "remove prefix with * in key",
mapping: map[string]string{"*": "INBOX/*"},
order: []string{"*"},
input: []string{"INBOX", "INBOX/Test1", "INBOX/Test2", "Archive"},
want: []string{"INBOX", "Test1", "Test2", "Archive"},
},
{
name: "remove two prefixes with * in keys",
mapping: map[string]string{
"*": "INBOX/*",
"**": "PROJECT/*",
},
order: []string{"*", "**"},
input: []string{"INBOX", "INBOX/Test1", "INBOX/Test2", "Archive", "PROJECT/sub1", "PROJECT/sub2"},
want: []string{"INBOX", "Test1", "Test2", "Archive", "sub1", "sub2"},
},
{
name: "multiple, sequential mappings",
mapping: map[string]string{
"Archive/existing": "Archive*",
"Archive": "Archivum*",
},
order: []string{"Archive/existing", "Archive"},
input: []string{"Archive", "Archive/sub", "Archivum", "Archivum/year1"},
want: []string{"Archive/existing", "Archive/existing/sub", "Archive", "Archive/year1"},
},
}
for i, test := range tests {
fm := &folderMap{
mapping: test.mapping,
order: test.order,
}
var result []string
for _, in := range test.input {
result = append(result, fm.Apply(in))
}
if !reflect.DeepEqual(result, test.want) {
t.Errorf("test (%d: %s) failed: want '%v' but got '%v'",
i, test.name, test.want, result)
}
}
}
func TestFolderMap_createFolder(t *testing.T) {
tests := []struct {
name string
table map[string]string
input string
want string
}{
{
name: "create normal folder",
table: map[string]string{"Drafts": "INBOX/Drafts"},
input: "INBOX/Drafts2",
want: "INBOX/Drafts2",
},
{
name: "create mapped folder",
table: map[string]string{"Drafts": "INBOX/Drafts"},
input: "Drafts/Sub",
want: "INBOX/Drafts/Sub",
},
}
for i, test := range tests {
result := createFolder(test.table, test.input)
if result != test.want {
t.Errorf("test (%d: %s) failed: want '%v' but got '%v'",
i, test.name, test.want, result)
}
}
}
+133
View File
@@ -0,0 +1,133 @@
package middleware
import (
"strconv"
"strings"
"sync"
"git.sr.ht/~rjarry/aerc/worker/imap/extensions/xgmext"
"git.sr.ht/~rjarry/aerc/worker/types"
"github.com/emersion/go-imap/client"
)
type gmailWorker struct {
types.WorkerInteractor
mu sync.Mutex
client *client.Client
}
// NewGmailWorker returns an IMAP middleware for the X-GM-EXT-1 extension
func NewGmailWorker(base types.WorkerInteractor, c *client.Client,
) types.WorkerInteractor {
base.Infof("loading worker middleware: X-GM-EXT-1")
// avoid double wrapping; unwrap and check for another gmail handler
for iter := base; iter != nil; iter = iter.Unwrap() {
if g, ok := iter.(*gmailWorker); ok {
base.Infof("already loaded; resetting")
err := g.reset(c)
if err != nil {
base.Errorf("reset failed: %v", err)
}
return base
}
}
return &gmailWorker{WorkerInteractor: base, client: c}
}
func (g *gmailWorker) Unwrap() types.WorkerInteractor {
return g.WorkerInteractor
}
func (g *gmailWorker) reset(c *client.Client) error {
g.mu.Lock()
defer g.mu.Unlock()
g.client = c
return nil
}
func (g *gmailWorker) ProcessAction(msg types.WorkerMessage) types.WorkerMessage {
switch msg := msg.(type) {
case *types.FetchMessageHeaders:
handler := xgmext.NewHandler(g.client)
g.mu.Lock()
uids, err := handler.FetchEntireThreads(msg.Uids)
g.mu.Unlock()
if err != nil {
g.Warnf("failed to fetch entire threads: %v", err)
}
if len(uids) > 0 {
msg.Uids = uids
}
case *types.FetchDirectoryContents:
if msg.Filter == nil || (msg.Filter != nil &&
len(msg.Filter.Terms) == 0) {
break
}
if !msg.Filter.UseExtension {
g.Debugf("use regular imap filter instead of X-GM-EXT1: " +
"extension flag not set")
break
}
search := strings.Join(msg.Filter.Terms, " ")
g.Debugf("X-GM-EXT1 filter term: '%s'", search)
handler := xgmext.NewHandler(g.client)
g.mu.Lock()
uids, err := handler.RawSearch(strconv.Quote(search))
g.mu.Unlock()
if err != nil {
g.Errorf("X-GM-EXT1 filter failed: %v", err)
g.Warnf("falling back to imap filtering")
break
}
g.PostMessage(&types.DirectoryContents{
Message: types.RespondTo(msg),
Uids: uids,
}, nil)
g.PostMessage(&types.Done{Message: types.RespondTo(msg)}, nil)
return &types.Unsupported{}
case *types.SearchDirectory:
if msg.Criteria == nil || (msg.Criteria != nil &&
len(msg.Criteria.Terms) == 0) {
break
}
if !msg.Criteria.UseExtension {
g.Debugf("use regular imap search instead of X-GM-EXT1: " +
"extension flag not set")
break
}
search := strings.Join(msg.Criteria.Terms, " ")
g.Debugf("X-GM-EXT1 search term: '%s'", search)
handler := xgmext.NewHandler(g.client)
g.mu.Lock()
uids, err := handler.RawSearch(strconv.Quote(search))
g.mu.Unlock()
if err != nil {
g.Errorf("X-GM-EXT1 search failed: %v", err)
g.Warnf("falling back to regular imap search.")
break
}
g.PostMessage(&types.SearchResults{
Message: types.RespondTo(msg),
Uids: uids,
}, nil)
g.PostMessage(&types.Done{Message: types.RespondTo(msg)}, nil)
return &types.Unsupported{}
}
return g.WorkerInteractor.ProcessAction(msg)
}