Files
aerc-fork-mirror/worker/middleware/foldermapper.go
Robin Jarry e324fa248f worker: add directory and filter metadata to response messages
Add Directory and Filter fields to DirectoryContents, DirectoryThreaded,
SearchResults, MessageInfo, and FullMessage response types. Workers now
populate these fields so the UI knows which directory and filter context
each response belongs to.

This metadata allows the message store to correctly associate incoming
messages with their source context, which is essential for the offline
worker to cache messages by directory and for proper handling of
concurrent operations across multiple folders.

Signed-off-by: Robin Jarry <robin@jarry.cc>
Reviewed-by: Simon Martin <simon@nasilyan.com>
2026-02-09 14:46:27 +01:00

246 lines
7.1 KiB
Go

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.Source = f.incoming(msg, msg.Source)
msg.Destination = f.incoming(msg, msg.Destination)
case *types.AppendMessage:
msg.Destination = f.incoming(msg, msg.Destination)
case *types.MoveMessages:
msg.Source = f.incoming(msg, msg.Source)
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)
case *types.FetchDirectoryContents:
msg.Directory = f.incoming(msg, msg.Directory)
case *types.FetchDirectoryThreaded:
msg.Directory = f.incoming(msg, msg.Directory)
case *types.SearchDirectory:
msg.Directory = f.incoming(msg, msg.Directory)
case *types.FetchMessageHeaders:
msg.Directory = f.incoming(msg, msg.Directory)
case *types.FetchFullMessages:
msg.Directory = f.incoming(msg, msg.Directory)
case *types.FetchMessageBodyPart:
msg.Directory = f.incoming(msg, msg.Directory)
case *types.FetchMessageFlags:
msg.Directory = f.incoming(msg, msg.Directory)
case *types.DeleteMessages:
msg.Directory = f.incoming(msg, msg.Directory)
case *types.FlagMessages:
msg.Directory = f.incoming(msg, msg.Directory)
case *types.AnsweredMessages:
msg.Directory = f.incoming(msg, msg.Directory)
case *types.ForwardedMessages:
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.Source = f.outgoing(msg, msg.Source)
msg.Destination = f.outgoing(msg, msg.Destination)
case *types.AppendMessage:
msg.Destination = f.outgoing(msg, msg.Destination)
case *types.MoveMessages:
msg.Source = f.outgoing(msg, msg.Source)
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.FetchDirectoryContents:
msg.Directory = f.outgoing(msg, msg.Directory)
case *types.FetchDirectoryThreaded:
msg.Directory = f.outgoing(msg, msg.Directory)
case *types.SearchDirectory:
msg.Directory = f.outgoing(msg, msg.Directory)
case *types.FetchMessageHeaders:
msg.Directory = f.outgoing(msg, msg.Directory)
case *types.FetchFullMessages:
msg.Directory = f.outgoing(msg, msg.Directory)
case *types.FetchMessageBodyPart:
msg.Directory = f.outgoing(msg, msg.Directory)
case *types.FetchMessageFlags:
msg.Directory = f.outgoing(msg, msg.Directory)
case *types.DeleteMessages:
msg.Directory = f.outgoing(msg, msg.Directory)
case *types.FlagMessages:
msg.Directory = f.outgoing(msg, msg.Directory)
case *types.AnsweredMessages:
msg.Directory = f.outgoing(msg, msg.Directory)
case *types.ForwardedMessages:
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)
case *types.MessagesMoved:
msg.Destination = f.outgoing(msg, msg.Destination)
case *types.MessagesCopied:
msg.Destination = f.outgoing(msg, msg.Destination)
case *types.RemoveDirectory:
msg.Directory = f.outgoing(msg, msg.Directory)
case *types.DirectoryContents:
msg.Directory = f.outgoing(msg, msg.Directory)
case *types.DirectoryThreaded:
msg.Directory = f.outgoing(msg, msg.Directory)
case *types.SearchResults:
msg.Directory = f.outgoing(msg, msg.Directory)
case *types.MessageInfo:
if msg.Info != nil {
msg.Info.Directory = f.outgoing(msg, msg.Info.Directory)
}
case *types.MessagesDeleted:
msg.Directory = f.outgoing(msg, msg.Directory)
}
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 before, ok := strings.CutSuffix(v, "*"); ok {
v = before
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
}