mirror of
https://git.sr.ht/~rjarry/aerc
synced 2025-06-30 19:00:21 +02:00

To delete N messages, aerc issues a single EXPUNGE command to the IMAP
server. The server sends back one ExpungeUpdate for each of those N
messages, and aerc reacts to each to update the folder contents.
Unfortunately, the RFC does not specify the order in which a conforming
server should send those "individual replies". aerc currently implicitly
assumes that it deletes messages in increasing sequence number order
(what GMail or FastMail do), and deleting N>1 messages will generally
not work for servers that use a different policy (e.g. Office 365 or
WorkMail, among others).
This patch implements an automatic detection of the policy used by the
server and adapts to it. Since some servers use a policy that can
confuse the automatic detection (e.g. WorkMail that deletes in random
order), it's also possible to statically configure that policy in
accounts.conf if needed.
Fixes: 80408384
("handle outdated sequence numbers from server [...]")
Signed-off-by: Simon Martin <simon@nasilyan.com>
Tested-by: Karel Balej <balejk@matfyz.cz>
Acked-by: Robin Jarry <robin@jarry.cc>
115 lines
2.7 KiB
Go
115 lines
2.7 KiB
Go
package imap
|
|
|
|
import (
|
|
"slices"
|
|
"sort"
|
|
"sync"
|
|
|
|
"git.sr.ht/~rjarry/aerc/lib/log"
|
|
)
|
|
|
|
type SeqMap struct {
|
|
lock sync.Mutex
|
|
// map of IMAP sequence numbers to message UIDs
|
|
m []uint32
|
|
}
|
|
|
|
// Initialize sets the initial seqmap of the mailbox
|
|
func (s *SeqMap) Initialize(uids []uint32) {
|
|
s.lock.Lock()
|
|
s.m = make([]uint32, len(uids))
|
|
copy(s.m, uids)
|
|
s.sort()
|
|
s.lock.Unlock()
|
|
}
|
|
|
|
func (s *SeqMap) Size() int {
|
|
s.lock.Lock()
|
|
size := len(s.m)
|
|
s.lock.Unlock()
|
|
return size
|
|
}
|
|
|
|
// Get returns the UID of the given seqnum
|
|
func (s *SeqMap) Get(seqnum uint32) (uint32, bool) {
|
|
if int(seqnum) > s.Size() || seqnum < 1 {
|
|
return 0, false
|
|
}
|
|
s.lock.Lock()
|
|
uid := s.m[seqnum-1]
|
|
s.lock.Unlock()
|
|
return uid, true
|
|
}
|
|
|
|
// Put adds a UID to the slice. Put should only be used to add new messages
|
|
// into the slice
|
|
func (s *SeqMap) Put(uid uint32) {
|
|
s.lock.Lock()
|
|
for _, n := range s.m {
|
|
if n == uid {
|
|
// We already have this UID, don't insert it.
|
|
s.lock.Unlock()
|
|
return
|
|
}
|
|
}
|
|
s.m = append(s.m, uid)
|
|
s.sort()
|
|
s.lock.Unlock()
|
|
}
|
|
|
|
// Take a snapshot of the SequenceNumber=>UID mappings for the given UIDs,
|
|
// remove those UIDs from the SeqMap, and return the snapshot it to the caller,
|
|
// as well as the loweest sequence number it contains.
|
|
func (s *SeqMap) Snapshot(uids []uint32) (map[uint32]uint32, uint32) {
|
|
// Take the snapshot.
|
|
snapshot := make(map[uint32]uint32)
|
|
var minSequenceNum uint32 = 0
|
|
var snapshotSeqNums []uint32
|
|
s.lock.Lock()
|
|
for num, uid := range s.m {
|
|
if slices.Contains(uids, uid) {
|
|
// IMAP sequence numbers start at 1
|
|
seqNum := uint32(num) + 1
|
|
snapshotSeqNums = append(snapshotSeqNums, seqNum)
|
|
if minSequenceNum == 0 {
|
|
minSequenceNum = seqNum
|
|
}
|
|
snapshot[seqNum] = uid
|
|
}
|
|
}
|
|
s.lock.Unlock()
|
|
|
|
// Remove the snapshotted mappings from the sequence; we need to do it from
|
|
// the highest to the lowest key since a SeqMap.Pop moves all the items on
|
|
// the right of the popped sequence number by one position to the left.
|
|
for i := len(snapshotSeqNums) - 1; i >= 0; i-- {
|
|
_, ok := s.Pop(snapshotSeqNums[i])
|
|
if !ok {
|
|
log.Errorf("Unable to pop %d from SeqMap", snapshotSeqNums[i])
|
|
}
|
|
}
|
|
|
|
return snapshot, minSequenceNum
|
|
}
|
|
|
|
// Pop removes seqnum from the SeqMap. seqnum must be a valid seqnum, ie
|
|
// [1:size of mailbox]
|
|
func (s *SeqMap) Pop(seqnum uint32) (uint32, bool) {
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
if int(seqnum) > len(s.m) || seqnum < 1 {
|
|
return 0, false
|
|
}
|
|
uid := s.m[seqnum-1]
|
|
s.m = append(s.m[:seqnum-1], s.m[seqnum:]...)
|
|
return uid, true
|
|
}
|
|
|
|
// sort sorts the slice in ascending UID order. See:
|
|
// https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.1.2
|
|
func (s *SeqMap) sort() {
|
|
// Always be sure the SeqMap is sorted
|
|
sort.Slice(s.m, func(i, j int) bool {
|
|
return s.m[i] < s.m[j]
|
|
})
|
|
}
|