Hello, thanks a lot for this thorough investigation! Would you like to make a PR for this?
-- Quentin Hivert Lead Dev R&D [email protected] Alinto 19, quai Perrache FR-69002 Lyon https://www.alinto.com Linkedin FR Le Vendredi, Juin 12, 2026 11:02 CEST, "\"Guido\"" ([email protected]) <[email protected]> a écrit: 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
