Hello,
I'd like to report a bug in SOGo's ActiveSync implementation that causes
messages deleted (or moved to another folder) via IMAP or the SOGo web UI to
remain permanently on EAS clients (tested with iOS/iPadOS Mail). I have fully
root-caused the issue down to the exact line of code and have a tested one-line
fix.
Environment:
- SOGo 5.x nightly (Ubuntu 24.04, packages.sogo.nu/nightly/5)
- Backend: Cyrus IMAP 3.8.2 (behind a transparent Dovecot proxy used only for
XOAUTH2 authentication; all commands are passed through 1:1)
- Backend: Cyrus IMAP 3.8.2 — same problem when connected directly to Cyrus
(without the Dovecot proxy)
- EAS client: iOS/iPadOS Mail
Symptom:
When a message is expunged from a folder outside of EAS (IMAP client, webmail),
the deletion is not synced to the ActiveSync device. Crucially, this happens
exactly when the expunged message holds the HIGHEST UID in the mailbox (i.e.
the newest message — which on a phone is the one most likely to be deleted).
Deleting older messages syncs fine, which made the issue look intermittent for
a long time.
Root cause:
In syncTokenFieldsWithProperties (SoObjects/Mailer/SOGoMailFolder.m), SOGo
detects external expunges via SOPE's fetchVanished:
(sope-mime/NGImap4/NGImap4Client.m, ~line 1257), which issues:
UID FETCH 1:* (UID) (CHANGEDSINCE <modseq> VANISHED)
Cyrus resolves '*' to the highest VISIBLE (non-expunged) UID (imap/index.c,
_parse_sequence(): maxval = index_getuid(state, state->exists)). In
index_vanished(), expunged records are then filtered with seqset_ismember()
against that sequence. If the expunged message had the highest UID, it now lies
ABOVE the resolved upper bound of '1:*' and is silently excluded — Cyrus
returns no VANISHED response at all.
SOGo consequently never emits a <Delete> command, but still advances the
SyncKey past the expunge modseq. The deletion is permanently lost for that
device; the ghost message stays until a full folder resync.
Wire trace (after deleting the newest message, UID 15626; M0 = HIGHESTMODSEQ
before deletion):
C: a005 UID FETCH 1:* (UID) (CHANGEDSINCE 181806 VANISHED)
S: a005 OK Completed (0.002 sec) <- no VANISHED response
C: a004 SELECT "INBOX" (QRESYNC (1146603129 181806))
S: * VANISHED (EARLIER) 15626 <- Cyrus DOES have the expunge
history
C: a004 UID FETCH 1:4294967295 (UID) (CHANGEDSINCE 181806 VANISHED)
S: * VANISHED (EARLIER) 15626 <- explicit upper bound works
Fix (tested):
In sope-mime/NGImap4/NGImap4Client.m, fetchVanished:, replace the '1:*' with an
explicit upper bound:
- @"UID FETCH 1:* (UID) (CHANGEDSINCE %llu VANISHED)"
+ @"UID FETCH 1:4294967295 (UID) (CHANGEDSINCE %llu VANISHED)"
With this single change, deletions AND moves performed via IMAP/webmail are now
reliably propagated to the EAS device in my setup. Dovecot is unaffected by the
change (it resolves '*' generously, which is why this bug never shows up on
Dovecot backends — the combination SOGo+Cyrus is rare).
One could argue Cyrus' interpretation of '*' is the underlying problem (I will
report it there as well), but since RFC 3501 defines '*' as the highest UID "in
the mailbox" (which after the expunge no longer covers the vanished UID),
SOGo/SOPE should be robust against this strictly conformant reading. The
canonical QRESYNC resync idiom breaks otherwise.
Best regards,
Guido