diff --git a/worker/lib/maildir.go b/worker/lib/maildir.go new file mode 100644 index 0000000..f6199f9 --- /dev/null +++ b/worker/lib/maildir.go @@ -0,0 +1,153 @@ +package lib + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "git.sr.ht/~rjarry/aerc/models" + "github.com/emersion/go-maildir" +) + +type MaildirStore struct { + root string + maildirpp bool // whether to use Maildir++ directory layout +} + +func NewMaildirStore(root string, maildirpp bool) (*MaildirStore, error) { + f, err := os.Open(root) + if err != nil { + return nil, err + } + defer f.Close() + s, err := f.Stat() + if err != nil { + return nil, err + } + if !s.IsDir() { + return nil, fmt.Errorf("Given maildir '%s' not a directory", root) + } + return &MaildirStore{ + root: root, maildirpp: maildirpp, + }, nil +} + +// ListFolders returns a list of maildir folders in the container +func (s *MaildirStore) ListFolders() ([]string, error) { + folders := []string{} + if s.maildirpp { + // In Maildir++ layout, INBOX is the root folder + folders = append(folders, "INBOX") + } + err := filepath.Walk(s.root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("Invalid path '%s': error: %w", path, err) + } + if !info.IsDir() { + return nil + } + + // Skip maildir's default directories + n := info.Name() + if n == "new" || n == "tmp" || n == "cur" { + return filepath.SkipDir + } + + // Get the relative path from the parent directory + dirPath, err := filepath.Rel(s.root, path) + if err != nil { + return err + } + + // Skip the parent directory + if dirPath == "." { + return nil + } + + // Drop dirs that lack {new,tmp,cur} subdirs + for _, sub := range []string{"new", "tmp", "cur"} { + if _, err := os.Stat(filepath.Join(path, sub)); os.IsNotExist(err) { + return nil + } + } + + if s.maildirpp { + // In Maildir++ layout, mailboxes are stored in a single directory + // and prefixed with a dot, and subfolders are separated by dots. + if !strings.HasPrefix(dirPath, ".") { + return filepath.SkipDir + } + dirPath = strings.TrimPrefix(dirPath, ".") + dirPath = strings.ReplaceAll(dirPath, ".", "/") + folders = append(folders, dirPath) + + // Since all mailboxes are stored in a single directory, don't + // recurse into subdirectories + return filepath.SkipDir + } + + folders = append(folders, dirPath) + return nil + }) + return folders, err +} + +// Folder returns a maildir.Dir with the specified name inside the Store +func (s *MaildirStore) Dir(name string) maildir.Dir { + if s.maildirpp { + // Use Maildir++ layout + if name == "INBOX" { + return maildir.Dir(s.root) + } + return maildir.Dir(filepath.Join(s.root, "."+strings.ReplaceAll(name, "/", "."))) + } + return maildir.Dir(filepath.Join(s.root, name)) +} + +// uidReg matches filename encoded UIDs in maildirs synched with mbsync or +// OfflineIMAP +var uidReg = regexp.MustCompile(`,U=\d+`) + +func StripUIDFromMessageFilename(basename string) string { + return uidReg.ReplaceAllString(basename, "") +} + +var MaildirToFlag = map[maildir.Flag]models.Flag{ + maildir.FlagReplied: models.AnsweredFlag, + maildir.FlagSeen: models.SeenFlag, + maildir.FlagTrashed: models.DeletedFlag, + maildir.FlagFlagged: models.FlaggedFlag, + // maildir.FlagDraft Flag = 'D' + // maildir.FlagPassed Flag = 'P' +} + +var FlagToMaildir = map[models.Flag]maildir.Flag{ + models.AnsweredFlag: maildir.FlagReplied, + models.SeenFlag: maildir.FlagSeen, + models.DeletedFlag: maildir.FlagTrashed, + models.FlaggedFlag: maildir.FlagFlagged, + // maildir.FlagDraft Flag = 'D' + // maildir.FlagPassed Flag = 'P' +} + +func FromMaildirFlags(maildirFlags []maildir.Flag) []models.Flag { + var flags []models.Flag + for _, maildirFlag := range maildirFlags { + if flag, ok := MaildirToFlag[maildirFlag]; ok { + flags = append(flags, flag) + } + } + return flags +} + +func ToMaildirFlags(flags []models.Flag) []maildir.Flag { + var maildirFlags []maildir.Flag + for _, flag := range flags { + if maildirFlag, ok := FlagToMaildir[flag]; ok { + maildirFlags = append(maildirFlags, maildirFlag) + } + } + return maildirFlags +} diff --git a/worker/maildir/container.go b/worker/maildir/container.go index 4181648..f9fb507 100644 --- a/worker/maildir/container.go +++ b/worker/maildir/container.go @@ -4,108 +4,34 @@ import ( "fmt" "os" "path/filepath" - "regexp" "sort" - "strings" "github.com/emersion/go-maildir" "git.sr.ht/~rjarry/aerc/lib/uidstore" + "git.sr.ht/~rjarry/aerc/worker/lib" ) -// uidReg matches filename encoded UIDs in maildirs synched with mbsync or -// OfflineIMAP -var uidReg = regexp.MustCompile(`,U=\d+`) - // A Container is a directory which contains other directories which adhere to // the Maildir spec type Container struct { - dir string + Store *lib.MaildirStore uids *uidstore.Store recentUIDS map[uint32]struct{} // used to set the recent flag - maildirpp bool // whether to use Maildir++ directory layout } // NewContainer creates a new container at the specified directory func NewContainer(dir string, maildirpp bool) (*Container, error) { - f, err := os.Open(dir) + store, err := lib.NewMaildirStore(dir, maildirpp) if err != nil { return nil, err } - defer f.Close() - s, err := f.Stat() - if err != nil { - return nil, err - } - if !s.IsDir() { - return nil, fmt.Errorf("Given maildir '%s' not a directory", dir) - } return &Container{ - dir: dir, uids: uidstore.NewStore(), - recentUIDS: make(map[uint32]struct{}), maildirpp: maildirpp, + Store: store, uids: uidstore.NewStore(), + recentUIDS: make(map[uint32]struct{}), }, nil } -// ListFolders returns a list of maildir folders in the container -func (c *Container) ListFolders() ([]string, error) { - folders := []string{} - if c.maildirpp { - // In Maildir++ layout, INBOX is the root folder - folders = append(folders, "INBOX") - } - err := filepath.Walk(c.dir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return fmt.Errorf("Invalid path '%s': error: %w", path, err) - } - if !info.IsDir() { - return nil - } - - // Skip maildir's default directories - n := info.Name() - if n == "new" || n == "tmp" || n == "cur" { - return filepath.SkipDir - } - - // Get the relative path from the parent directory - dirPath, err := filepath.Rel(c.dir, path) - if err != nil { - return err - } - - // Skip the parent directory - if dirPath == "." { - return nil - } - - // Drop dirs that lack {new,tmp,cur} subdirs - for _, sub := range []string{"new", "tmp", "cur"} { - if _, err := os.Stat(filepath.Join(path, sub)); os.IsNotExist(err) { - return nil - } - } - - if c.maildirpp { - // In Maildir++ layout, mailboxes are stored in a single directory - // and prefixed with a dot, and subfolders are separated by dots. - if !strings.HasPrefix(dirPath, ".") { - return filepath.SkipDir - } - dirPath = strings.TrimPrefix(dirPath, ".") - dirPath = strings.ReplaceAll(dirPath, ".", "/") - folders = append(folders, dirPath) - - // Since all mailboxes are stored in a single directory, don't - // recurse into subdirectories - return filepath.SkipDir - } - - folders = append(folders, dirPath) - return nil - }) - return folders, err -} - // SyncNewMail adds emails from new to cur, tracking them func (c *Container) SyncNewMail(dir maildir.Dir) error { keys, err := dir.Unseen() @@ -122,25 +48,13 @@ func (c *Container) SyncNewMail(dir maildir.Dir) error { // OpenDirectory opens an existing maildir in the container by name, moves new // messages into cur, and registers the new keys in the UIDStore. func (c *Container) OpenDirectory(name string) (maildir.Dir, error) { - dir := c.Dir(name) + dir := c.Store.Dir(name) if err := c.SyncNewMail(dir); err != nil { return dir, err } return dir, nil } -// Dir returns a maildir.Dir with the specified name inside the container -func (c *Container) Dir(name string) maildir.Dir { - if c.maildirpp { - // Use Maildir++ layout - if name == "INBOX" { - return maildir.Dir(c.dir) - } - return maildir.Dir(filepath.Join(c.dir, "."+strings.ReplaceAll(name, "/", "."))) - } - return maildir.Dir(filepath.Join(c.dir, name)) -} - // IsRecent returns if a uid has the Recent flag set func (c *Container) IsRecent(uid uint32) bool { _, ok := c.recentUIDS[uid] @@ -239,7 +153,7 @@ func (c *Container) moveMessage(dest maildir.Dir, src maildir.Dir, uid uint32) e return fmt.Errorf("could not find path for message id %d", uid) } // Remove encoded UID information from the key to prevent sync issues - name := uidReg.ReplaceAllString(filepath.Base(path), "") + name := lib.StripUIDFromMessageFilename(filepath.Base(path)) destPath := filepath.Join(string(dest), "cur", name) return os.Rename(path, destPath) } diff --git a/worker/maildir/message.go b/worker/maildir/message.go index 9cd1331..3c8ce9e 100644 --- a/worker/maildir/message.go +++ b/worker/maildir/message.go @@ -33,7 +33,7 @@ func (m Message) ModelFlags() ([]models.Flag, error) { if err != nil { return nil, err } - return translateMaildirFlags(flags), nil + return lib.FromMaildirFlags(flags), nil } // SetFlags replaces the message's flags with a new set. @@ -91,44 +91,6 @@ func (m Message) NewBodyPartReader(requestedParts []int) (io.Reader, error) { return lib.FetchEntityPartReader(msg, requestedParts) } -var maildirToFlag = map[maildir.Flag]models.Flag{ - maildir.FlagReplied: models.AnsweredFlag, - maildir.FlagSeen: models.SeenFlag, - maildir.FlagTrashed: models.DeletedFlag, - maildir.FlagFlagged: models.FlaggedFlag, - // maildir.FlagDraft Flag = 'D' - // maildir.FlagPassed Flag = 'P' -} - -var flagToMaildir = map[models.Flag]maildir.Flag{ - models.AnsweredFlag: maildir.FlagReplied, - models.SeenFlag: maildir.FlagSeen, - models.DeletedFlag: maildir.FlagTrashed, - models.FlaggedFlag: maildir.FlagFlagged, - // maildir.FlagDraft Flag = 'D' - // maildir.FlagPassed Flag = 'P' -} - -func translateMaildirFlags(maildirFlags []maildir.Flag) []models.Flag { - var flags []models.Flag - for _, maildirFlag := range maildirFlags { - if flag, ok := maildirToFlag[maildirFlag]; ok { - flags = append(flags, flag) - } - } - return flags -} - -func translateFlags(flags []models.Flag) []maildir.Flag { - var maildirFlags []maildir.Flag - for _, flag := range flags { - if maildirFlag, ok := flagToMaildir[flag]; ok { - maildirFlags = append(maildirFlags, maildirFlag) - } - } - return maildirFlags -} - func (m Message) UID() uint32 { return m.uid } diff --git a/worker/maildir/worker.go b/worker/maildir/worker.go index ddb1bf6..d2bb646 100644 --- a/worker/maildir/worker.go +++ b/worker/maildir/worker.go @@ -182,7 +182,7 @@ func (w *Worker) getDirectoryInfo(name string) *models.DirectoryInfo { }, } - dir := w.c.Dir(name) + dir := w.c.Store.Dir(name) var keyFlags map[string][]maildir.Flag files, err := dirFiles(string(dir)) if err == nil { @@ -329,7 +329,7 @@ func (w *Worker) handleListDirectories(msg *types.ListDirectories) error { if w.c == nil { return errors.New("Incorrect maildir directory") } - dirs, err := w.c.ListFolders() + dirs, err := w.c.Store.ListFolders() if err != nil { logging.Errorf("failed listing directories: %v", err) return err @@ -453,7 +453,7 @@ func (w *Worker) sort(uids []uint32, criteria []*types.SortCriterion) ([]uint32, } func (w *Worker) handleCreateDirectory(msg *types.CreateDirectory) error { - dir := w.c.Dir(msg.Directory) + dir := w.c.Store.Dir(msg.Directory) if err := dir.Init(); err != nil { logging.Errorf("could not create directory %s: %v", msg.Directory, err) @@ -463,7 +463,7 @@ func (w *Worker) handleCreateDirectory(msg *types.CreateDirectory) error { } func (w *Worker) handleRemoveDirectory(msg *types.RemoveDirectory) error { - dir := w.c.Dir(msg.Directory) + dir := w.c.Store.Dir(msg.Directory) if err := os.RemoveAll(string(dir)); err != nil { logging.Errorf("could not remove directory %s: %v", msg.Directory, err) @@ -604,7 +604,7 @@ func (w *Worker) handleFlagMessages(msg *types.FlagMessages) error { w.err(msg, err) continue } - flag := flagToMaildir[msg.Flag] + flag := lib.FlagToMaildir[msg.Flag] if err := m.SetOneFlag(flag, msg.Enable); err != nil { logging.Errorf("could change flag %v to %v on message: %v", flag, msg.Enable, err) w.err(msg, err) @@ -631,7 +631,7 @@ func (w *Worker) handleFlagMessages(msg *types.FlagMessages) error { } func (w *Worker) handleCopyMessages(msg *types.CopyMessages) error { - dest := w.c.Dir(msg.Destination) + dest := w.c.Store.Dir(msg.Destination) err := w.c.CopyAll(dest, *w.selected, msg.Uids) if err != nil { return err @@ -645,7 +645,7 @@ func (w *Worker) handleCopyMessages(msg *types.CopyMessages) error { } func (w *Worker) handleMoveMessages(msg *types.MoveMessages) error { - dest := w.c.Dir(msg.Destination) + dest := w.c.Store.Dir(msg.Destination) moved, err := w.c.MoveAll(dest, *w.selected, msg.Uids) destInfo := w.getDirectoryInfo(msg.Destination) w.worker.PostMessage(&types.DirectoryInfo{ @@ -660,8 +660,8 @@ func (w *Worker) handleMoveMessages(msg *types.MoveMessages) error { func (w *Worker) handleAppendMessage(msg *types.AppendMessage) error { // since we are the "master" maildir process, we can modify the maildir directly - dest := w.c.Dir(msg.Destination) - _, writer, err := dest.Create(translateFlags(msg.Flags)) + dest := w.c.Store.Dir(msg.Destination) + _, 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 @@ -733,12 +733,12 @@ func (w *Worker) handleCheckMail(msg *types.CheckMail) { if err != nil { w.err(msg, fmt.Errorf("checkmail: error running command: %w", err)) } else { - dirs, err := w.c.ListFolders() + dirs, err := w.c.Store.ListFolders() if err != nil { w.err(msg, fmt.Errorf("failed listing directories: %w", err)) } for _, name := range dirs { - err := w.c.SyncNewMail(w.c.Dir(name)) + err := w.c.SyncNewMail(w.c.Store.Dir(name)) if err != nil { w.err(msg, fmt.Errorf("could not sync new mail: %w", err)) }