diff options
Diffstat (limited to 'worker/notmuch')
-rw-r--r-- | worker/notmuch/eventhandlers.go | 22 | ||||
-rw-r--r-- | worker/notmuch/message.go | 109 | ||||
-rw-r--r-- | worker/notmuch/worker.go | 214 |
3 files changed, 337 insertions, 8 deletions
diff --git a/worker/notmuch/eventhandlers.go b/worker/notmuch/eventhandlers.go index 7b3c5174..764add0c 100644 --- a/worker/notmuch/eventhandlers.go +++ b/worker/notmuch/eventhandlers.go @@ -3,7 +3,12 @@ package notmuch -import "git.sr.ht/~rjarry/aerc/logging" +import ( + "fmt" + "strconv" + + "git.sr.ht/~rjarry/aerc/logging" +) func (w *worker) handleNotmuchEvent(et eventType) error { switch ev := et.(type) { @@ -15,6 +20,21 @@ func (w *worker) handleNotmuchEvent(et eventType) error { } func (w *worker) handleUpdateDirCounts(ev eventType) error { + folders, err := w.store.FolderMap() + if err != nil { + logging.Errorf("failed listing directories: %v", err) + return err + } + for name := range folders { + query := fmt.Sprintf("folder:%s", strconv.Quote(name)) + info, err := w.buildDirInfo(name, query, true) + if err != nil { + logging.Errorf("could not gather DirectoryInfo: %v", err) + continue + } + w.w.PostMessage(info, nil) + } + for name, query := range w.nameQueryMap { info, err := w.buildDirInfo(name, query, true) if err != nil { diff --git a/worker/notmuch/message.go b/worker/notmuch/message.go index 59367fd1..8f8deffd 100644 --- a/worker/notmuch/message.go +++ b/worker/notmuch/message.go @@ -7,6 +7,10 @@ import ( "fmt" "io" "os" + "path/filepath" + "strings" + + "github.com/emersion/go-maildir" "git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/worker/lib" @@ -175,3 +179,108 @@ func (m *Message) RemoveTag(tag string) error { func (m *Message) ModifyTags(add, remove []string) error { return m.db.MsgModifyTags(m.key, add, remove) } + +func (m *Message) Remove(dir maildir.Dir) error { + filenames, err := m.db.MsgFilenames(m.key) + if err != nil { + return err + } + for _, filename := range filenames { + if dirContains(dir, filename) { + err := m.db.DeleteMessage(filename) + if err != nil { + return err + } + + if err := os.Remove(filename); err != nil { + return err + } + + return nil + } + } + + return fmt.Errorf("no matching message file found in %s", string(dir)) +} + +func (m *Message) Copy(target maildir.Dir) error { + filename, err := m.Filename() + if err != nil { + return err + } + + source, key := parseFilename(filename) + if key == "" { + return fmt.Errorf("failed to parse message filename: %s", filename) + } + + newKey, err := source.Copy(target, key) + if err != nil { + return err + } + newFilename, err := target.Filename(newKey) + if err != nil { + return err + } + _, err = m.db.IndexFile(newFilename) + return err +} + +func (m *Message) Move(srcDir, destDir maildir.Dir) error { + var src string + + filenames, err := m.db.MsgFilenames(m.key) + if err != nil { + return err + } + for _, filename := range filenames { + if dirContains(srcDir, filename) { + src = filename + break + } + } + + if src == "" { + return fmt.Errorf("no matching message file found in %s", string(srcDir)) + } + + // Remove encoded UID information from the key to prevent sync issues + name := lib.StripUIDFromMessageFilename(filepath.Base(src)) + dest := filepath.Join(string(destDir), "cur", name) + + if err := m.db.DeleteMessage(src); err != nil { + return err + } + + if err := os.Rename(src, dest); err != nil { + return err + } + + _, err = m.db.IndexFile(dest) + return err +} + +func parseFilename(filename string) (maildir.Dir, string) { + base := filepath.Base(filename) + dir := filepath.Dir(filename) + dir, curdir := filepath.Split(dir) + if curdir != "cur" { + return "", "" + } + split := strings.Split(base, ":") + if len(split) < 2 { + return maildir.Dir(dir), "" + } + key := split[0] + return maildir.Dir(dir), key +} + +func dirContains(dir maildir.Dir, filename string) bool { + for _, sub := range []string{"cur", "new"} { + match, _ := filepath.Match(filepath.Join(string(dir), sub, "*"), filename) + if match { + return true + } + } + return false +} diff --git a/worker/notmuch/worker.go b/worker/notmuch/worker.go index 38539df5..c5947113 100644 --- a/worker/notmuch/worker.go +++ b/worker/notmuch/worker.go @@ -14,6 +14,7 @@ import ( "os" "os/exec" "path/filepath" + "strconv" "strings" "time" @@ -42,6 +43,7 @@ type worker struct { currentQueryName string queryMapOrder []string nameQueryMap map[string]string + store *lib.MaildirStore db *notmuch.DB setupErr error currentSortCriteria []*types.SortCriterion @@ -135,13 +137,18 @@ func (w *worker) handleMessage(msg types.WorkerMessage) error { case *types.CheckMail: go w.handleCheckMail(msg) return nil - // not implemented, they are generally not used - // in a notmuch based workflow - // case *types.DeleteMessages: - // case *types.CopyMessages: - // case *types.AppendMessage: - // case *types.CreateDirectory: - // case *types.RemoveDirectory: + case *types.DeleteMessages: + return w.handleDeleteMessages(msg) + case *types.CopyMessages: + return w.handleCopyMessages(msg) + case *types.MoveMessages: + return w.handleMoveMessages(msg) + case *types.AppendMessage: + return w.handleAppendMessage(msg) + case *types.CreateDirectory: + return w.handleCreateDirectory(msg) + case *types.RemoveDirectory: + return w.handleRemoveDirectory(msg) } return errUnsupported } @@ -172,6 +179,12 @@ func (w *worker) handleConfigure(msg *types.Configure) error { } excludedTags := w.loadExcludeTags(msg.Config) w.db = notmuch.NewDB(pathToDB, excludedTags) + store, err := lib.NewMaildirStore(pathToDB, false) + if err != nil { + return fmt.Errorf("Cannot initialize maildir store: %w", err) + } + w.store = store + return nil } @@ -194,6 +207,21 @@ func (w *worker) handleConnect(msg *types.Connect) error { } func (w *worker) handleListDirectories(msg *types.ListDirectories) error { + folders, err := w.store.FolderMap() + if err != nil { + logging.Errorf("failed listing directories: %v", err) + return err + } + for name := range folders { + w.w.PostMessage(&types.Directory{ + Message: types.RespondTo(msg), + Dir: &models.Directory{ + Name: name, + Attributes: []string{}, + }, + }, nil) + } + for _, name := range w.queryMapOrder { w.w.PostMessage(&types.Directory{ Message: types.RespondTo(msg), @@ -259,6 +287,10 @@ func (w *worker) queryFromName(name string) (string, bool) { // try the friendly name first, if that fails assume it's a query q, ok := w.nameQueryMap[name] if !ok { + folders, _ := w.store.FolderMap() + if _, ok := folders[name]; ok { + return fmt.Sprintf("folder:%s", strconv.Quote(name)), true + } return name, true } return q, false @@ -679,3 +711,171 @@ func (w *worker) handleCheckMail(msg *types.CheckMail) { } } } + +func (w *worker) handleDeleteMessages(msg *types.DeleteMessages) error { + var deleted []uint32 + + // With notmuch, two identical files can be referenced under + // the same index key, even if they exist in two different + // folders. So in order to remove the message from the right + // maildir folder we need to pass a hint to Remove() so it + // can purge the right file. + folders, _ := w.store.FolderMap() + path, ok := folders[w.currentQueryName] + if !ok { + w.err(msg, fmt.Errorf("Can only delete file from a maildir folder")) + w.done(msg) + return nil + } + + for _, uid := range msg.Uids { + m, err := w.msgFromUid(uid) + if err != nil { + logging.Errorf("could not get message: %v", err) + w.err(msg, err) + continue + } + if err := m.Remove(path); err != nil { + logging.Errorf("could not remove message: %v", err) + w.err(msg, err) + continue + } + deleted = append(deleted, uid) + } + if len(deleted) > 0 { + w.w.PostMessage(&types.MessagesDeleted{ + Message: types.RespondTo(msg), + Uids: deleted, + }, nil) + } + w.done(msg) + return nil +} + +func (w *worker) handleCopyMessages(msg *types.CopyMessages) error { + // Only allow file to be copied to a maildir folder + folders, _ := w.store.FolderMap() + dest, ok := folders[msg.Destination] + if !ok { + return fmt.Errorf("Can only move file to a maildir folder") + } + + for _, uid := range msg.Uids { + m, err := w.msgFromUid(uid) + if err != nil { + logging.Errorf("could not get message: %v", err) + return err + } + if err := m.Copy(dest); err != nil { + logging.Errorf("could not copy message: %v", err) + return err + } + } + w.w.PostMessage(&types.MessagesCopied{ + Message: types.RespondTo(msg), + Destination: msg.Destination, + Uids: msg.Uids, + }, nil) + w.done(msg) + return nil +} + +func (w *worker) handleMoveMessages(msg *types.MoveMessages) error { + var moved []uint32 + + // With notmuch, two identical files can be referenced under + // the same index key, even if they exist in two different + // folders. So in order to remove the message from the right + // maildir folder we need to pass a hint to Move() so it + // can act on the right file. + folders, _ := w.store.FolderMap() + source, ok := folders[w.currentQueryName] + if !ok { + return fmt.Errorf("Can only move file from a maildir folder") + } + + // Only allow file to be moved to a maildir folder + dest, ok := folders[msg.Destination] + if !ok { + return fmt.Errorf("Can only move file to a maildir folder") + } + + var err error + for _, uid := range msg.Uids { + m, err := w.msgFromUid(uid) + if err != nil { + logging.Errorf("could not get message: %v", err) + break + } + if err := m.Move(source, dest); err != nil { + logging.Errorf("could not copy message: %v", err) + break + } + moved = append(moved, uid) + } + w.w.PostMessage(&types.MessagesDeleted{ + Message: types.RespondTo(msg), + Uids: moved, + }, nil) + if err == nil { + w.done(msg) + } + return err +} + +func (w *worker) handleAppendMessage(msg *types.AppendMessage) error { + // Only allow file to be created in a maildir folder + // since we are the "master" maildir process, we can modify the maildir directly + folders, _ := w.store.FolderMap() + dest, ok := folders[msg.Destination] + if !ok { + return fmt.Errorf("Can only create file in a maildir folder") + } + key, writer, err := dest.Create(lib.ToMaildirFlags(msg.Flags)) + if err != nil { + logging.Errorf("could not create message at %s: %v", msg.Destination, err) + return err + } + filename, err := dest.Filename(key) + if err != nil { + writer.Close() + return err + } + if _, err := io.Copy(writer, msg.Reader); err != nil { + logging.Errorf("could not write message to destination: %v", err) + writer.Close() + os.Remove(filename) + return err + } + writer.Close() + if _, err := w.db.IndexFile(filename); err != nil { + return err + } + if err := w.emitDirectoryInfo(w.currentQueryName); err != nil { + logging.Errorf("could not emit directory info: %v", err) + } + w.done(msg) + return nil +} + +func (w *worker) handleCreateDirectory(msg *types.CreateDirectory) error { + dir := w.store.Dir(msg.Directory) + if err := dir.Init(); err != nil { + logging.Errorf("could not create directory %s: %v", + msg.Directory, err) + return err + } + w.done(msg) + return nil +} + +func (w *worker) handleRemoveDirectory(msg *types.RemoveDirectory) error { + dir := w.store.Dir(msg.Directory) + if err := os.RemoveAll(string(dir)); err != nil { + logging.Errorf("could not remove directory %s: %v", + msg.Directory, err) + return err + } + w.done(msg) + return nil +} |