2019-07-11 15:44:51 +02:00
|
|
|
package maildir
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2020-05-05 10:16:02 +02:00
|
|
|
"os"
|
2019-07-11 15:44:51 +02:00
|
|
|
"path/filepath"
|
2022-08-24 16:49:32 +02:00
|
|
|
"regexp"
|
2019-07-11 15:44:51 +02:00
|
|
|
"sort"
|
2022-07-04 21:21:48 +02:00
|
|
|
"strings"
|
2019-07-11 15:44:51 +02:00
|
|
|
|
|
|
|
"github.com/emersion/go-maildir"
|
|
|
|
|
2021-11-05 10:19:46 +01:00
|
|
|
"git.sr.ht/~rjarry/aerc/lib/uidstore"
|
2019-07-11 15:44:51 +02:00
|
|
|
)
|
|
|
|
|
2022-08-24 16:49:32 +02:00
|
|
|
// uidReg matches filename encoded UIDs in maildirs synched with mbsync or
|
|
|
|
// OfflineIMAP
|
|
|
|
var uidReg = regexp.MustCompile(`,U=\d+`)
|
|
|
|
|
2019-07-11 15:44:51 +02:00
|
|
|
// A Container is a directory which contains other directories which adhere to
|
|
|
|
// the Maildir spec
|
|
|
|
type Container struct {
|
2020-09-13 10:42:40 +02:00
|
|
|
dir string
|
|
|
|
uids *uidstore.Store
|
|
|
|
recentUIDS map[uint32]struct{} // used to set the recent flag
|
2022-07-04 21:21:48 +02:00
|
|
|
maildirpp bool // whether to use Maildir++ directory layout
|
2019-07-11 15:44:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewContainer creates a new container at the specified directory
|
2022-07-19 22:31:51 +02:00
|
|
|
func NewContainer(dir string, maildirpp bool) (*Container, error) {
|
2020-07-25 10:11:13 +02:00
|
|
|
f, err := os.Open(dir)
|
|
|
|
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)
|
|
|
|
}
|
2022-07-31 22:16:40 +02:00
|
|
|
return &Container{
|
|
|
|
dir: dir, uids: uidstore.NewStore(),
|
|
|
|
recentUIDS: make(map[uint32]struct{}), maildirpp: maildirpp,
|
|
|
|
}, nil
|
2019-07-11 15:44:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// ListFolders returns a list of maildir folders in the container
|
|
|
|
func (c *Container) ListFolders() ([]string, error) {
|
2020-05-05 10:16:02 +02:00
|
|
|
folders := []string{}
|
2022-07-04 21:21:48 +02:00
|
|
|
if c.maildirpp {
|
|
|
|
// In Maildir++ layout, INBOX is the root folder
|
|
|
|
folders = append(folders, "INBOX")
|
|
|
|
}
|
2020-05-05 10:16:02 +02:00
|
|
|
err := filepath.Walk(c.dir, func(path string, info os.FileInfo, err error) error {
|
2020-07-25 10:11:13 +02:00
|
|
|
if err != nil {
|
2022-07-31 15:15:27 +02:00
|
|
|
return fmt.Errorf("Invalid path '%s': error: %w", path, err)
|
2020-07-25 10:11:13 +02:00
|
|
|
}
|
2020-05-05 10:16:02 +02:00
|
|
|
if !info.IsDir() {
|
|
|
|
return nil
|
2019-07-11 15:44:51 +02:00
|
|
|
}
|
2020-05-05 10:16:02 +02:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2022-10-03 19:16:26 +02:00
|
|
|
// 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) {
|
2022-10-21 21:15:07 +02:00
|
|
|
return nil
|
2022-10-03 19:16:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-04 21:21:48 +02:00
|
|
|
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, ".")
|
2022-07-31 14:32:48 +02:00
|
|
|
dirPath = strings.ReplaceAll(dirPath, ".", "/")
|
2022-07-04 21:21:48 +02:00
|
|
|
folders = append(folders, dirPath)
|
|
|
|
|
|
|
|
// Since all mailboxes are stored in a single directory, don't
|
|
|
|
// recurse into subdirectories
|
|
|
|
return filepath.SkipDir
|
|
|
|
}
|
|
|
|
|
2020-05-05 10:16:02 +02:00
|
|
|
folders = append(folders, dirPath)
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
return folders, err
|
2019-07-11 15:44:51 +02:00
|
|
|
}
|
|
|
|
|
2020-09-13 10:42:40 +02:00
|
|
|
// SyncNewMail adds emails from new to cur, tracking them
|
|
|
|
func (c *Container) SyncNewMail(dir maildir.Dir) error {
|
|
|
|
keys, err := dir.Unseen()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
for _, key := range keys {
|
|
|
|
uid := c.uids.GetOrInsert(key)
|
|
|
|
c.recentUIDS[uid] = struct{}{}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-07-11 15:44:51 +02:00
|
|
|
// 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)
|
2020-09-13 10:42:40 +02:00
|
|
|
if err := c.SyncNewMail(dir); err != nil {
|
2019-07-11 15:44:51 +02:00
|
|
|
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 {
|
2022-07-04 21:21:48 +02:00
|
|
|
if c.maildirpp {
|
|
|
|
// Use Maildir++ layout
|
|
|
|
if name == "INBOX" {
|
|
|
|
return maildir.Dir(c.dir)
|
|
|
|
}
|
2022-07-31 14:32:48 +02:00
|
|
|
return maildir.Dir(filepath.Join(c.dir, "."+strings.ReplaceAll(name, "/", ".")))
|
2022-07-04 21:21:48 +02:00
|
|
|
}
|
2019-07-11 15:44:51 +02:00
|
|
|
return maildir.Dir(filepath.Join(c.dir, name))
|
|
|
|
}
|
|
|
|
|
2020-09-13 10:42:40 +02:00
|
|
|
// IsRecent returns if a uid has the Recent flag set
|
|
|
|
func (c *Container) IsRecent(uid uint32) bool {
|
|
|
|
_, ok := c.recentUIDS[uid]
|
|
|
|
return ok
|
|
|
|
}
|
|
|
|
|
|
|
|
// ClearRecentFlag removes the Recent flag from the message with the given uid
|
|
|
|
func (c *Container) ClearRecentFlag(uid uint32) {
|
|
|
|
delete(c.recentUIDS, uid)
|
|
|
|
}
|
|
|
|
|
2019-07-11 15:44:51 +02:00
|
|
|
// UIDs fetches the unique message identifiers for the maildir
|
|
|
|
func (c *Container) UIDs(d maildir.Dir) ([]uint32, error) {
|
|
|
|
keys, err := d.Keys()
|
|
|
|
if err != nil {
|
2022-07-31 15:15:27 +02:00
|
|
|
return nil, fmt.Errorf("could not get keys for %s: %w", d, err)
|
2019-07-11 15:44:51 +02:00
|
|
|
}
|
|
|
|
sort.Strings(keys)
|
|
|
|
var uids []uint32
|
|
|
|
for _, key := range keys {
|
|
|
|
uids = append(uids, c.uids.GetOrInsert(key))
|
|
|
|
}
|
|
|
|
return uids, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Message returns a Message struct for the given UID and maildir
|
|
|
|
func (c *Container) Message(d maildir.Dir, uid uint32) (*Message, error) {
|
|
|
|
if key, ok := c.uids.GetKey(uid); ok {
|
|
|
|
return &Message{
|
|
|
|
dir: d,
|
|
|
|
uid: uid,
|
|
|
|
key: key,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
return nil, fmt.Errorf("could not find message with uid %d in maildir %s",
|
|
|
|
uid, d)
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteAll deletes a set of messages by UID and returns the subset of UIDs
|
|
|
|
// which were successfully deleted, stopping upon the first error.
|
|
|
|
func (c *Container) DeleteAll(d maildir.Dir, uids []uint32) ([]uint32, error) {
|
|
|
|
var success []uint32
|
|
|
|
for _, uid := range uids {
|
|
|
|
msg, err := c.Message(d, uid)
|
|
|
|
if err != nil {
|
|
|
|
return success, err
|
|
|
|
}
|
|
|
|
if err := msg.Remove(); err != nil {
|
|
|
|
return success, err
|
|
|
|
}
|
|
|
|
success = append(success, uid)
|
|
|
|
}
|
|
|
|
return success, nil
|
|
|
|
}
|
2019-07-11 15:44:53 +02:00
|
|
|
|
|
|
|
func (c *Container) CopyAll(
|
2022-07-31 22:16:40 +02:00
|
|
|
dest maildir.Dir, src maildir.Dir, uids []uint32,
|
|
|
|
) error {
|
2019-07-11 15:44:53 +02:00
|
|
|
for _, uid := range uids {
|
|
|
|
if err := c.copyMessage(dest, src, uid); err != nil {
|
2022-07-31 15:15:27 +02:00
|
|
|
return fmt.Errorf("could not copy message %d: %w", uid, err)
|
2019-07-11 15:44:53 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Container) copyMessage(
|
2022-07-31 22:16:40 +02:00
|
|
|
dest maildir.Dir, src maildir.Dir, uid uint32,
|
|
|
|
) error {
|
2019-07-11 15:44:53 +02:00
|
|
|
key, ok := c.uids.GetKey(uid)
|
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("could not find key for message id %d", uid)
|
|
|
|
}
|
2019-08-08 04:21:19 +02:00
|
|
|
_, err := src.Copy(dest, key)
|
|
|
|
return err
|
2019-07-11 15:44:53 +02:00
|
|
|
}
|
2022-08-16 23:23:40 +02:00
|
|
|
|
|
|
|
func (c *Container) MoveAll(dest maildir.Dir, src maildir.Dir, uids []uint32) ([]uint32, error) {
|
|
|
|
var success []uint32
|
|
|
|
for _, uid := range uids {
|
|
|
|
if err := c.moveMessage(dest, src, uid); err != nil {
|
|
|
|
return success, fmt.Errorf("could not move message %d: %w", uid, err)
|
|
|
|
}
|
|
|
|
success = append(success, uid)
|
|
|
|
}
|
|
|
|
return success, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Container) moveMessage(dest maildir.Dir, src maildir.Dir, uid uint32) error {
|
|
|
|
key, ok := c.uids.GetKey(uid)
|
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("could not find key for message id %d", uid)
|
|
|
|
}
|
2022-08-24 16:49:32 +02:00
|
|
|
path, err := src.Filename(key)
|
|
|
|
if err != nil {
|
|
|
|
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), "")
|
|
|
|
destPath := filepath.Join(string(dest), "cur", name)
|
|
|
|
return os.Rename(path, destPath)
|
2022-08-16 23:23:40 +02:00
|
|
|
}
|