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 } func (s *MaildirStore) FolderMap() (map[string]maildir.Dir, error) { folders := make(map[string]maildir.Dir) if s.maildirpp { // In Maildir++ layout, INBOX is the root folder folders["INBOX"] = maildir.Dir(s.root) } 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[dirPath] = maildir.Dir(path) // Since all mailboxes are stored in a single directory, don't // recurse into subdirectories return filepath.SkipDir } folders[dirPath] = maildir.Dir(path) 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.Flags{ maildir.FlagReplied: models.AnsweredFlag, maildir.FlagSeen: models.SeenFlag, maildir.FlagTrashed: models.DeletedFlag, maildir.FlagFlagged: models.FlaggedFlag, maildir.FlagDraft: models.DraftFlag, maildir.FlagPassed: models.ForwardedFlag, } var FlagToMaildir = map[models.Flags]maildir.Flag{ models.AnsweredFlag: maildir.FlagReplied, models.SeenFlag: maildir.FlagSeen, models.DeletedFlag: maildir.FlagTrashed, models.FlaggedFlag: maildir.FlagFlagged, models.DraftFlag: maildir.FlagDraft, models.ForwardedFlag: maildir.FlagPassed, } func FromMaildirFlags(maildirFlags []maildir.Flag) models.Flags { var flags models.Flags for _, maildirFlag := range maildirFlags { if flag, ok := MaildirToFlag[maildirFlag]; ok { flags |= flag } } return flags } func ToMaildirFlags(flags models.Flags) []maildir.Flag { var maildirFlags []maildir.Flag for flag, maildirFlag := range FlagToMaildir { if flags.Has(flag) { maildirFlags = append(maildirFlags, maildirFlag) } } return maildirFlags }