package mboxer import ( "bytes" "errors" "fmt" "io" "net/url" "os" "path/filepath" "sort" "git.sr.ht/~rjarry/aerc/lib/log" "git.sr.ht/~rjarry/aerc/lib/rfc822" "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" ) func init() { handlers.RegisterWorkerFactory("mbox", NewWorker) } var errUnsupported = fmt.Errorf("unsupported command") type mboxWorker struct { data *mailboxContainer name string folder *container worker *types.Worker capabilities *models.Capabilities headers []string headersExclude []string } func NewWorker(worker *types.Worker) (types.Backend, error) { return &mboxWorker{ worker: worker, capabilities: &models.Capabilities{ Sort: true, Thread: false, }, }, 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 } var dir string 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.headers = msg.Config.Headers w.headersExclude = msg.Config.HeadersExclude 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.Debugf("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, }, }, 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.Debugf("%s opened", msg.Directory) case *types.FetchDirectoryContents: uids, err := filterUids(w.folder, w.folder.Uids(), msg.Filter) if err != nil { reterr = err break } uids, err = sortUids(w.folder, uids, 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 := messageInfo(m, true) if err != nil { w.worker.PostMessage(&types.MessageInfo{ Info: &models.MessageInfo{ Envelope: &models.Envelope{}, Flags: models.SeenFlag, Uid: uid, Error: err, }, Message: types.RespondTo(msg), }, nil) continue } else { switch { case len(w.headersExclude) > 0: msgInfo.RFC822Headers = lib.LimitHeaders(msgInfo.RFC822Headers, w.headersExclude, true) case len(w.headers) > 0: msgInfo.RFC822Headers = lib.LimitHeaders(msgInfo.RFC822Headers, w.headers, false) } 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.Errorf("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: %w", err) break } fullMsg, err := rfc822.ReadMessage(contentReader) if err != nil { reterr = fmt.Errorf("could not read message: %w", err) break } r, err := rfc822.FetchEntityPartReader(fullMsg, msg.Part) if err != nil { w.worker.Errorf( "could not get body part reader for message=%d, parts=%#v: %w", 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.Errorf("could not get message for uid %d: %v", uid, err) continue } r, err := m.NewReader() if err != nil { w.worker.Errorf("could not get message reader: %v", err) continue } defer r.Close() b, err := io.ReadAll(r) if err != nil { w.worker.Errorf("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.Errorf("could not get message: %v", err) continue } if err := m.(*message).SetFlag(msg.Flags, msg.Enable); err != nil { w.worker.Errorf("could not change flag %v to %t on message: %v", msg.Flags, msg.Enable, err) continue } info, err := rfc822.MessageInfo(m) if err != nil { w.worker.Errorf("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.MoveMessages: err := w.data.Copy(msg.Destination, w.name, msg.Uids) if err != nil { reterr = err break } 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(msg.Destination), }, nil) w.worker.PostMessage( &types.Done{Message: types.RespondTo(msg)}, nil) case *types.SearchDirectory: uids, err := filterUids(w.folder, w.folder.Uids(), msg.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 msg := range w.worker.Actions() { msg = w.worker.ProcessAction(msg) if err := w.handleMessage(msg); errors.Is(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) } } } func (w *mboxWorker) Capabilities() *models.Capabilities { return w.capabilities } func (w *mboxWorker) PathSeparator() string { return "/" } func filterUids(folder *container, uids []uint32, criteria *types.SearchCriteria) ([]uint32, error) { log.Debugf("Search with parsed criteria: %#v", criteria) m := make([]rfc822.RawMessage, 0, len(uids)) for _, uid := range uids { msg, err := folder.Message(uid) if err != nil { log.Errorf("failed to get message for uid: %d", uid) continue } m = append(m, msg) } return lib.Search(m, criteria) } func sortUids(folder *container, uids []uint32, criteria []*types.SortCriterion, ) ([]uint32, error) { var infos []*models.MessageInfo needSize := false for _, item := range criteria { if item.Field == types.SortSize { needSize = true } } for _, uid := range uids { m, err := folder.Message(uid) if err != nil { log.Errorf("could not get message %v", err) continue } info, err := messageInfo(m, needSize) if err != nil { log.Errorf("could not get message info %v", err) continue } infos = append(infos, info) } return lib.Sort(infos, criteria) } func messageInfo(m rfc822.RawMessage, needSize bool) (*models.MessageInfo, error) { info, err := rfc822.MessageInfo(m) if err != nil { return nil, err } if !needSize { return info, nil } r, err := m.NewReader() if err != nil { return nil, err } size, err := io.Copy(io.Discard, r) if err != nil { return nil, err } info.Size = uint32(size) return info, nil }