a1a276e002
Implement an mbox backend worker. Worker can be used for testing and development by mocking a backend for the message store. Worker does not modify the actual mbox file on disk; all operations are performed in memory. To use the mbox backend, create an mbox account in the accounts.conf where the source uses the "mbox://" scheme, such as source = mbox://~/mbox/ or source = mbox://~/mbox/file.mbox If the mbox source points to a directory, all files in this directory with the .mbox suffix will be opened as folders. If an outgoing smtp server is defined for the mbox account, replies can be sent to emails that are stored in the mbox file. Signed-off-by: Koni Marti <koni.marti@gmail.com> Acked-by: Robin Jarry <robin@jarry.cc>
379 lines
9 KiB
Go
379 lines
9 KiB
Go
package mboxer
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
|
|
"git.sr.ht/~rjarry/aerc/models"
|
|
"git.sr.ht/~rjarry/aerc/worker/handlers"
|
|
"git.sr.ht/~rjarry/aerc/worker/lib"
|
|
"git.sr.ht/~rjarry/aerc/worker/types"
|
|
gomessage "github.com/emersion/go-message"
|
|
)
|
|
|
|
func init() {
|
|
handlers.RegisterWorkerFactory("mbox", NewWorker)
|
|
}
|
|
|
|
var errUnsupported = fmt.Errorf("unsupported command")
|
|
|
|
type mboxWorker struct {
|
|
data *mailboxContainer
|
|
name string
|
|
folder *container
|
|
worker *types.Worker
|
|
}
|
|
|
|
func NewWorker(worker *types.Worker) (types.Backend, error) {
|
|
return &mboxWorker{
|
|
worker: worker,
|
|
}, nil
|
|
}
|
|
|
|
func (w *mboxWorker) handleMessage(msg types.WorkerMessage) error {
|
|
var reterr error // will be returned at the end, needed to support idle
|
|
|
|
switch msg := msg.(type) {
|
|
|
|
case *types.Unsupported:
|
|
// No-op
|
|
|
|
case *types.Configure:
|
|
u, err := url.Parse(msg.Config.Source)
|
|
if err != nil {
|
|
reterr = err
|
|
break
|
|
}
|
|
dir := u.Path
|
|
if u.Host == "~" {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
reterr = err
|
|
break
|
|
}
|
|
dir = filepath.Join(home, u.Path)
|
|
} else {
|
|
dir = filepath.Join(u.Host, u.Path)
|
|
}
|
|
w.data, err = createMailboxContainer(dir)
|
|
if err != nil || w.data == nil {
|
|
w.data = &mailboxContainer{
|
|
mailboxes: make(map[string]*container),
|
|
}
|
|
reterr = err
|
|
break
|
|
} else {
|
|
w.worker.Logger.Printf("mbox: configured with mbox file %s", dir)
|
|
}
|
|
|
|
case *types.Connect, *types.Reconnect, *types.Disconnect:
|
|
w.worker.PostMessage(&types.Done{Message: types.RespondTo(msg)}, nil)
|
|
|
|
case *types.ListDirectories:
|
|
dirs := w.data.Names()
|
|
sort.Strings(dirs)
|
|
for _, name := range dirs {
|
|
w.worker.PostMessage(&types.Directory{
|
|
Message: types.RespondTo(msg),
|
|
Dir: &models.Directory{
|
|
Name: name,
|
|
Attributes: nil,
|
|
},
|
|
}, nil)
|
|
w.worker.PostMessage(&types.DirectoryInfo{
|
|
Info: w.data.DirectoryInfo(name),
|
|
}, nil)
|
|
}
|
|
w.worker.PostMessage(&types.Done{Message: types.RespondTo(msg)}, nil)
|
|
|
|
case *types.OpenDirectory:
|
|
w.name = msg.Directory
|
|
var ok bool
|
|
w.folder, ok = w.data.Mailbox(w.name)
|
|
if !ok {
|
|
w.folder = w.data.Create(w.name)
|
|
w.worker.PostMessage(&types.Done{
|
|
Message: types.RespondTo(&types.CreateDirectory{})}, nil)
|
|
}
|
|
w.worker.PostMessage(&types.DirectoryInfo{
|
|
Info: w.data.DirectoryInfo(msg.Directory),
|
|
}, nil)
|
|
w.worker.PostMessage(&types.Done{Message: types.RespondTo(msg)}, nil)
|
|
w.worker.Logger.Printf("mbox: %s opened\n", msg.Directory)
|
|
|
|
case *types.FetchDirectoryContents:
|
|
var infos []*models.MessageInfo
|
|
for _, uid := range w.folder.Uids() {
|
|
m, err := w.folder.Message(uid)
|
|
if err != nil {
|
|
w.worker.Logger.Println("mbox: could not get message", err)
|
|
continue
|
|
}
|
|
info, err := lib.MessageInfo(m)
|
|
if err != nil {
|
|
w.worker.Logger.Println("mbox: could not get message info", err)
|
|
continue
|
|
}
|
|
infos = append(infos, info)
|
|
}
|
|
uids, err := lib.Sort(infos, msg.SortCriteria)
|
|
if err != nil {
|
|
reterr = err
|
|
break
|
|
}
|
|
if len(uids) == 0 {
|
|
reterr = fmt.Errorf("mbox: no uids in directory")
|
|
break
|
|
}
|
|
w.worker.PostMessage(&types.DirectoryContents{
|
|
Message: types.RespondTo(msg),
|
|
Uids: uids,
|
|
}, nil)
|
|
w.worker.PostMessage(&types.Done{Message: types.RespondTo(msg)}, nil)
|
|
|
|
case *types.FetchDirectoryThreaded:
|
|
reterr = errUnsupported
|
|
|
|
case *types.CreateDirectory:
|
|
w.data.Create(msg.Directory)
|
|
w.worker.PostMessage(&types.Done{Message: types.RespondTo(msg)}, nil)
|
|
|
|
case *types.RemoveDirectory:
|
|
if err := w.data.Remove(msg.Directory); err != nil {
|
|
reterr = err
|
|
break
|
|
}
|
|
w.worker.PostMessage(&types.Done{Message: types.RespondTo(msg)}, nil)
|
|
|
|
case *types.FetchMessageHeaders:
|
|
for _, uid := range msg.Uids {
|
|
m, err := w.folder.Message(uid)
|
|
if err != nil {
|
|
reterr = err
|
|
break
|
|
}
|
|
msgInfo, err := lib.MessageInfo(m)
|
|
if err != nil {
|
|
reterr = err
|
|
break
|
|
} else {
|
|
w.worker.PostMessage(&types.MessageInfo{
|
|
Message: types.RespondTo(msg),
|
|
Info: msgInfo,
|
|
}, nil)
|
|
}
|
|
}
|
|
w.worker.PostMessage(
|
|
&types.Done{Message: types.RespondTo(msg)}, nil)
|
|
|
|
case *types.FetchMessageBodyPart:
|
|
m, err := w.folder.Message(msg.Uid)
|
|
if err != nil {
|
|
w.worker.Logger.Printf("could not get message %d: %v", msg.Uid, err)
|
|
reterr = err
|
|
break
|
|
}
|
|
|
|
contentReader, err := m.NewReader()
|
|
if err != nil {
|
|
reterr = fmt.Errorf("could not get message reader: %v", err)
|
|
break
|
|
}
|
|
|
|
fullMsg, err := gomessage.Read(contentReader)
|
|
if err != nil {
|
|
reterr = fmt.Errorf("could not read message: %v", err)
|
|
break
|
|
}
|
|
|
|
r, err := lib.FetchEntityPartReader(fullMsg, msg.Part)
|
|
if err != nil {
|
|
w.worker.Logger.Printf(
|
|
"could not get body part reader for message=%d, parts=%#v: %v",
|
|
msg.Uid, msg.Part, err)
|
|
reterr = err
|
|
break
|
|
}
|
|
|
|
w.worker.PostMessage(&types.MessageBodyPart{
|
|
Message: types.RespondTo(msg),
|
|
Part: &models.MessageBodyPart{
|
|
Reader: r,
|
|
Uid: msg.Uid,
|
|
},
|
|
}, nil)
|
|
|
|
case *types.FetchFullMessages:
|
|
for _, uid := range msg.Uids {
|
|
m, err := w.folder.Message(uid)
|
|
if err != nil {
|
|
w.worker.Logger.Printf("could not get message for uid %d: %v", uid, err)
|
|
continue
|
|
}
|
|
r, err := m.NewReader()
|
|
if err != nil {
|
|
w.worker.Logger.Printf("could not get message reader: %v", err)
|
|
continue
|
|
}
|
|
defer r.Close()
|
|
b, err := ioutil.ReadAll(r)
|
|
if err != nil {
|
|
w.worker.Logger.Printf("could not get message reader: %v", err)
|
|
continue
|
|
}
|
|
w.worker.PostMessage(&types.FullMessage{
|
|
Message: types.RespondTo(msg),
|
|
Content: &models.FullMessage{
|
|
Uid: uid,
|
|
Reader: bytes.NewReader(b),
|
|
},
|
|
}, nil)
|
|
}
|
|
w.worker.PostMessage(&types.Done{
|
|
Message: types.RespondTo(msg),
|
|
}, nil)
|
|
|
|
case *types.DeleteMessages:
|
|
deleted := w.folder.Delete(msg.Uids)
|
|
if len(deleted) > 0 {
|
|
w.worker.PostMessage(&types.MessagesDeleted{
|
|
Message: types.RespondTo(msg),
|
|
Uids: deleted,
|
|
}, nil)
|
|
}
|
|
|
|
w.worker.PostMessage(&types.DirectoryInfo{
|
|
Info: w.data.DirectoryInfo(w.name),
|
|
}, nil)
|
|
|
|
w.worker.PostMessage(
|
|
&types.Done{Message: types.RespondTo(msg)}, nil)
|
|
|
|
case *types.FlagMessages:
|
|
for _, uid := range msg.Uids {
|
|
m, err := w.folder.Message(uid)
|
|
if err != nil {
|
|
w.worker.Logger.Printf("could not get message: %v", err)
|
|
continue
|
|
}
|
|
if err := m.(*message).SetFlag(msg.Flag, msg.Enable); err != nil {
|
|
w.worker.Logger.Printf("could change flag %v to %v on message: %v", msg.Flag, msg.Enable, err)
|
|
continue
|
|
}
|
|
info, err := lib.MessageInfo(m)
|
|
if err != nil {
|
|
w.worker.Logger.Printf("could not get message info: %v", err)
|
|
continue
|
|
}
|
|
|
|
w.worker.PostMessage(&types.MessageInfo{
|
|
Message: types.RespondTo(msg),
|
|
Info: info,
|
|
}, nil)
|
|
}
|
|
|
|
w.worker.PostMessage(&types.DirectoryInfo{
|
|
Info: w.data.DirectoryInfo(w.name),
|
|
}, nil)
|
|
|
|
w.worker.PostMessage(
|
|
&types.Done{Message: types.RespondTo(msg)}, nil)
|
|
|
|
case *types.CopyMessages:
|
|
err := w.data.Copy(msg.Destination, w.name, msg.Uids)
|
|
if err != nil {
|
|
reterr = err
|
|
break
|
|
}
|
|
|
|
w.worker.PostMessage(&types.DirectoryInfo{
|
|
Info: w.data.DirectoryInfo(w.name),
|
|
}, nil)
|
|
|
|
w.worker.PostMessage(&types.DirectoryInfo{
|
|
Info: w.data.DirectoryInfo(msg.Destination),
|
|
}, nil)
|
|
|
|
w.worker.PostMessage(
|
|
&types.Done{Message: types.RespondTo(msg)}, nil)
|
|
|
|
case *types.SearchDirectory:
|
|
criteria, err := lib.GetSearchCriteria(msg.Argv)
|
|
if err != nil {
|
|
reterr = err
|
|
break
|
|
}
|
|
w.worker.Logger.Printf("Searching with parsed criteria: %#v", criteria)
|
|
m := make([]lib.RawMessage, 0, len(w.folder.Uids()))
|
|
for _, uid := range w.folder.Uids() {
|
|
msg, err := w.folder.Message(uid)
|
|
if err != nil {
|
|
w.worker.Logger.Println("faild to get message for uid:", uid)
|
|
continue
|
|
}
|
|
m = append(m, msg)
|
|
}
|
|
uids, err := lib.Search(m, criteria)
|
|
if err != nil {
|
|
reterr = err
|
|
break
|
|
}
|
|
w.worker.PostMessage(&types.SearchResults{
|
|
Message: types.RespondTo(msg),
|
|
Uids: uids,
|
|
}, nil)
|
|
|
|
case *types.AppendMessage:
|
|
if msg.Destination == "" {
|
|
reterr = fmt.Errorf("AppendMessage with empty destination directory")
|
|
break
|
|
}
|
|
folder, ok := w.data.Mailbox(msg.Destination)
|
|
if !ok {
|
|
folder = w.data.Create(msg.Destination)
|
|
w.worker.PostMessage(&types.Done{
|
|
Message: types.RespondTo(&types.CreateDirectory{})}, nil)
|
|
}
|
|
|
|
if err := folder.Append(msg.Reader, msg.Flags); err != nil {
|
|
reterr = err
|
|
break
|
|
} else {
|
|
w.worker.PostMessage(&types.DirectoryInfo{
|
|
Info: w.data.DirectoryInfo(msg.Destination),
|
|
}, nil)
|
|
w.worker.PostMessage(&types.Done{Message: types.RespondTo(msg)}, nil)
|
|
}
|
|
|
|
case *types.AnsweredMessages:
|
|
reterr = errUnsupported
|
|
default:
|
|
reterr = errUnsupported
|
|
}
|
|
|
|
return reterr
|
|
}
|
|
|
|
func (w *mboxWorker) Run() {
|
|
for {
|
|
select {
|
|
case msg := <-w.worker.Actions:
|
|
msg = w.worker.ProcessAction(msg)
|
|
if err := w.handleMessage(msg); err == errUnsupported {
|
|
w.worker.PostMessage(&types.Unsupported{
|
|
Message: types.RespondTo(msg),
|
|
}, nil)
|
|
} else if err != nil {
|
|
w.worker.PostMessage(&types.Error{
|
|
Message: types.RespondTo(msg),
|
|
Error: err,
|
|
}, nil)
|
|
}
|
|
}
|
|
}
|
|
}
|