maildir: move common maildir code out of worker

This change moves code that could be common to both notmuch and maildir
workers in worker/lib.

Signed-off-by: Julian Pidancet <julian.pidancet@oracle.com>
Acked-by: Robin Jarry <robin@jarry.cc>
Acked-by: Tim Culverhouse <tim@timculverhouse.com>
This commit is contained in:
Julian Pidancet 2022-10-26 22:29:02 +02:00 committed by Robin Jarry
parent 19d16420de
commit f021bfd1c7
4 changed files with 172 additions and 143 deletions

153
worker/lib/maildir.go Normal file
View file

@ -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
}

View file

@ -4,108 +4,34 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"sort" "sort"
"strings"
"github.com/emersion/go-maildir" "github.com/emersion/go-maildir"
"git.sr.ht/~rjarry/aerc/lib/uidstore" "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 // A Container is a directory which contains other directories which adhere to
// the Maildir spec // the Maildir spec
type Container struct { type Container struct {
dir string Store *lib.MaildirStore
uids *uidstore.Store uids *uidstore.Store
recentUIDS map[uint32]struct{} // used to set the recent flag 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 // NewContainer creates a new container at the specified directory
func NewContainer(dir string, maildirpp bool) (*Container, error) { func NewContainer(dir string, maildirpp bool) (*Container, error) {
f, err := os.Open(dir) store, err := lib.NewMaildirStore(dir, maildirpp)
if err != nil { if err != nil {
return nil, err 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{ return &Container{
dir: dir, uids: uidstore.NewStore(), Store: store, uids: uidstore.NewStore(),
recentUIDS: make(map[uint32]struct{}), maildirpp: maildirpp, recentUIDS: make(map[uint32]struct{}),
}, nil }, 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 // SyncNewMail adds emails from new to cur, tracking them
func (c *Container) SyncNewMail(dir maildir.Dir) error { func (c *Container) SyncNewMail(dir maildir.Dir) error {
keys, err := dir.Unseen() 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 // OpenDirectory opens an existing maildir in the container by name, moves new
// messages into cur, and registers the new keys in the UIDStore. // messages into cur, and registers the new keys in the UIDStore.
func (c *Container) OpenDirectory(name string) (maildir.Dir, error) { 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 { if err := c.SyncNewMail(dir); err != nil {
return dir, err return dir, err
} }
return dir, nil 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 // IsRecent returns if a uid has the Recent flag set
func (c *Container) IsRecent(uid uint32) bool { func (c *Container) IsRecent(uid uint32) bool {
_, ok := c.recentUIDS[uid] _, 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) return fmt.Errorf("could not find path for message id %d", uid)
} }
// Remove encoded UID information from the key to prevent sync issues // 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) destPath := filepath.Join(string(dest), "cur", name)
return os.Rename(path, destPath) return os.Rename(path, destPath)
} }

View file

@ -33,7 +33,7 @@ func (m Message) ModelFlags() ([]models.Flag, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return translateMaildirFlags(flags), nil return lib.FromMaildirFlags(flags), nil
} }
// SetFlags replaces the message's flags with a new set. // 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) 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 { func (m Message) UID() uint32 {
return m.uid return m.uid
} }

View file

@ -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 var keyFlags map[string][]maildir.Flag
files, err := dirFiles(string(dir)) files, err := dirFiles(string(dir))
if err == nil { if err == nil {
@ -329,7 +329,7 @@ func (w *Worker) handleListDirectories(msg *types.ListDirectories) error {
if w.c == nil { if w.c == nil {
return errors.New("Incorrect maildir directory") return errors.New("Incorrect maildir directory")
} }
dirs, err := w.c.ListFolders() dirs, err := w.c.Store.ListFolders()
if err != nil { if err != nil {
logging.Errorf("failed listing directories: %v", err) logging.Errorf("failed listing directories: %v", err)
return 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 { 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 { if err := dir.Init(); err != nil {
logging.Errorf("could not create directory %s: %v", logging.Errorf("could not create directory %s: %v",
msg.Directory, err) msg.Directory, err)
@ -463,7 +463,7 @@ func (w *Worker) handleCreateDirectory(msg *types.CreateDirectory) error {
} }
func (w *Worker) handleRemoveDirectory(msg *types.RemoveDirectory) 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 { if err := os.RemoveAll(string(dir)); err != nil {
logging.Errorf("could not remove directory %s: %v", logging.Errorf("could not remove directory %s: %v",
msg.Directory, err) msg.Directory, err)
@ -604,7 +604,7 @@ func (w *Worker) handleFlagMessages(msg *types.FlagMessages) error {
w.err(msg, err) w.err(msg, err)
continue continue
} }
flag := flagToMaildir[msg.Flag] flag := lib.FlagToMaildir[msg.Flag]
if err := m.SetOneFlag(flag, msg.Enable); err != nil { if err := m.SetOneFlag(flag, msg.Enable); err != nil {
logging.Errorf("could change flag %v to %v on message: %v", flag, msg.Enable, err) logging.Errorf("could change flag %v to %v on message: %v", flag, msg.Enable, err)
w.err(msg, 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 { 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) err := w.c.CopyAll(dest, *w.selected, msg.Uids)
if err != nil { if err != nil {
return err return err
@ -645,7 +645,7 @@ func (w *Worker) handleCopyMessages(msg *types.CopyMessages) error {
} }
func (w *Worker) handleMoveMessages(msg *types.MoveMessages) 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) moved, err := w.c.MoveAll(dest, *w.selected, msg.Uids)
destInfo := w.getDirectoryInfo(msg.Destination) destInfo := w.getDirectoryInfo(msg.Destination)
w.worker.PostMessage(&types.DirectoryInfo{ 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 { func (w *Worker) handleAppendMessage(msg *types.AppendMessage) error {
// since we are the "master" maildir process, we can modify the maildir directly // since we are the "master" maildir process, we can modify the maildir directly
dest := w.c.Dir(msg.Destination) dest := w.c.Store.Dir(msg.Destination)
_, writer, err := dest.Create(translateFlags(msg.Flags)) _, writer, err := dest.Create(lib.ToMaildirFlags(msg.Flags))
if err != nil { if err != nil {
logging.Errorf("could not create message at %s: %v", msg.Destination, err) logging.Errorf("could not create message at %s: %v", msg.Destination, err)
return err return err
@ -733,12 +733,12 @@ func (w *Worker) handleCheckMail(msg *types.CheckMail) {
if err != nil { if err != nil {
w.err(msg, fmt.Errorf("checkmail: error running command: %w", err)) w.err(msg, fmt.Errorf("checkmail: error running command: %w", err))
} else { } else {
dirs, err := w.c.ListFolders() dirs, err := w.c.Store.ListFolders()
if err != nil { if err != nil {
w.err(msg, fmt.Errorf("failed listing directories: %w", err)) w.err(msg, fmt.Errorf("failed listing directories: %w", err))
} }
for _, name := range dirs { for _, name := range dirs {
err := w.c.SyncNewMail(w.c.Dir(name)) err := w.c.SyncNewMail(w.c.Store.Dir(name))
if err != nil { if err != nil {
w.err(msg, fmt.Errorf("could not sync new mail: %w", err)) w.err(msg, fmt.Errorf("could not sync new mail: %w", err))
} }