init: pristine aerc 0.20.0 source
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user