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:
parent
19d16420de
commit
f021bfd1c7
4 changed files with 172 additions and 143 deletions
153
worker/lib/maildir.go
Normal file
153
worker/lib/maildir.go
Normal 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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue