mirror of
https://git.sr.ht/~rjarry/aerc
synced 2026-10-10 21:50:18 +02:00
Recently, locking was added to the maildir Container struct. Since both
DeleteAll() and Message() take the container lock, calling Message() from
DeleteAll() deadlocks. Deleting any message in a maildir-backed account
triggers this code path.
Fix the issue by adding a helper function message(), that relies on the
caller to take the lock and use it in DeleteAll(). Convert Message() to
a thin wrapper around message() handling synchronization.
Fixes: ba1d070b ("maildir: protect container maps against concurrent modifications")
Signed-off-by: Folker Schwesinger <dev@folker-schwesinger.de>
Acked-by: Simon Martin <simon@nasilyan.com>
206 lines
5.2 KiB
Go
206 lines
5.2 KiB
Go
package maildir
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"sync"
|
|
|
|
"github.com/emersion/go-maildir"
|
|
|
|
"git.sr.ht/~rjarry/aerc/lib/log"
|
|
"git.sr.ht/~rjarry/aerc/models"
|
|
"git.sr.ht/~rjarry/aerc/worker/lib"
|
|
)
|
|
|
|
// A Container is a directory which contains other directories which adhere to
|
|
// the Maildir spec
|
|
type Container struct {
|
|
sync.RWMutex
|
|
Store *lib.MaildirStore
|
|
recentUIDS map[models.UID]struct{} // used to set the recent flag
|
|
msgCache map[models.UID]*Message
|
|
}
|
|
|
|
// NewContainer creates a new container at the specified directory
|
|
func NewContainer(dir string, maildirpp bool) (*Container, error) {
|
|
store, err := lib.NewMaildirStore(dir, maildirpp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Container{
|
|
Store: store,
|
|
recentUIDS: make(map[models.UID]struct{}),
|
|
msgCache: make(map[models.UID]*Message),
|
|
}, nil
|
|
}
|
|
|
|
// SyncNewMail adds emails from new to cur, tracking them
|
|
func (c *Container) SyncNewMail(dir maildir.Dir) error {
|
|
unseen, err := dir.Unseen()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.Lock()
|
|
for _, msg := range unseen {
|
|
c.recentUIDS[models.UID(msg.Key())] = struct{}{}
|
|
}
|
|
c.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// 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.Store.Dir(name)
|
|
if err := c.SyncNewMail(dir); err != nil {
|
|
return dir, err
|
|
}
|
|
return dir, nil
|
|
}
|
|
|
|
// IsRecent returns if a uid has the Recent flag set
|
|
func (c *Container) IsRecent(uid models.UID) bool {
|
|
c.RLock()
|
|
_, ok := c.recentUIDS[uid]
|
|
c.RUnlock()
|
|
return ok
|
|
}
|
|
|
|
// ClearRecentFlag removes the Recent flag from the message with the given uid
|
|
func (c *Container) ClearRecentFlag(uid models.UID) {
|
|
c.Lock()
|
|
delete(c.recentUIDS, uid)
|
|
c.Unlock()
|
|
}
|
|
|
|
// UIDs fetches the unique message identifiers for the maildir
|
|
func (c *Container) UIDs(d maildir.Dir) ([]models.UID, error) {
|
|
// messages, err := d.Keys()
|
|
messages, err := d.Messages()
|
|
if err != nil && len(messages) == 0 {
|
|
return nil, fmt.Errorf("could not get keys for %s: %w", d, err)
|
|
}
|
|
if err != nil {
|
|
log.Errorf("could not get all keys for %s: %s", d, err.Error())
|
|
}
|
|
|
|
// Reconstruct the cache rather than modifying it, so any stale entries
|
|
// are purged.
|
|
newCache := make(map[models.UID]*Message)
|
|
var uids []models.UID
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
for _, msg := range messages {
|
|
key := msg.Key()
|
|
uid := models.UID(key)
|
|
if m, ok := c.msgCache[uid]; ok {
|
|
// If cached message is already present, update msg so
|
|
// any path (flag) changes by external programs are
|
|
// picked up.
|
|
m.msg = msg
|
|
newCache[uid] = m
|
|
} else {
|
|
newCache[uid] = &Message{
|
|
dir: d,
|
|
uid: uid,
|
|
key: key,
|
|
msg: msg,
|
|
}
|
|
}
|
|
uids = append(uids, uid)
|
|
}
|
|
c.msgCache = newCache
|
|
|
|
slices.Sort(uids)
|
|
return uids, err
|
|
}
|
|
|
|
// message returns a Message struct for the given UID and maildir.
|
|
// Callers should hold the container lock.
|
|
func (c *Container) message(d maildir.Dir, uid models.UID) (*Message, error) {
|
|
if m, ok := c.msgCache[uid]; ok && m.dir == d {
|
|
return m, nil
|
|
}
|
|
m := &Message{
|
|
dir: d,
|
|
uid: uid,
|
|
key: string(uid),
|
|
}
|
|
c.msgCache[uid] = m
|
|
return m, nil
|
|
}
|
|
|
|
// Message returns a Message struct for the given UID and maildir
|
|
func (c *Container) Message(d maildir.Dir, uid models.UID) (*Message, error) {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
return c.message(d, uid)
|
|
}
|
|
|
|
// 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 []models.UID) ([]models.UID, error) {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
var success []models.UID
|
|
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)
|
|
delete(c.msgCache, uid)
|
|
}
|
|
return success, nil
|
|
}
|
|
|
|
func (c *Container) CopyAll(
|
|
dest maildir.Dir, src maildir.Dir, uids []models.UID,
|
|
) error {
|
|
for _, uid := range uids {
|
|
if err := c.copyMessage(dest, src, uid); err != nil {
|
|
return fmt.Errorf("could not copy message %s: %w", uid, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Container) copyMessage(
|
|
dest maildir.Dir, src maildir.Dir, uid models.UID,
|
|
) error {
|
|
msg, err := src.MessageByKey(string(uid))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to retrieve message %q: %w", uid, err)
|
|
}
|
|
_, err = msg.CopyTo(dest)
|
|
return err
|
|
}
|
|
|
|
func (c *Container) MoveAll(dest maildir.Dir, src maildir.Dir, uids []models.UID) ([]models.UID, error) {
|
|
var success []models.UID
|
|
for _, uid := range uids {
|
|
if err := c.moveMessage(dest, src, uid); err != nil {
|
|
return success, fmt.Errorf("could not move message %s: %w", uid, err)
|
|
}
|
|
success = append(success, uid)
|
|
}
|
|
return success, nil
|
|
}
|
|
|
|
func (c *Container) moveMessage(dest maildir.Dir, src maildir.Dir, uid models.UID) error {
|
|
msg, err := src.MessageByKey(string(uid))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to retrieve message %q: %w", uid, err)
|
|
}
|
|
path := msg.Filename()
|
|
// Remove encoded UID information from the key to prevent sync issues
|
|
name := lib.StripUIDFromMessageFilename(filepath.Base(path))
|
|
destPath := filepath.Join(string(dest), "cur", name)
|
|
return os.Rename(path, destPath)
|
|
}
|