Disclaimer up front: this analysis was done together with an AI assistant (Claude). It suggested the diagnostic steps, interpreted the logs and drew the conclusions; I ran the commands on my systems and provided the output. I am posting it because the findings look useful, not because the reasoning is mine.
Environment: NS8 cluster, sogo1 module (sogo-server 5.12.10, module image sogo:2.2.4) and mail1 on the same node, iOS Mail via ActiveSync, Thunderbird via IMAP.
The symptom
Read flags and deletions made in iOS Mail (ActiveSync via SOGo) never reached the IMAP backend. Changes in the other direction worked instantly. Thunderbird kept showing those mails as unread and present.
Root cause
With SOGoEASDebugEnabled on, a 90-second capture produced 436 KB of log. All 263 <Change> blocks in it referred to one single message: ServerId 45527 in the Trash collection.
Request from the device:
<SyncKey>43367-54383</SyncKey>
<CollectionId>mail%2F4289650282573b5b981e0000d4291c30</CollectionId>
<Commands><Change><ServerId>45527</ServerId>
<ApplicationData><Read xmlns="Email:">1</Read></ApplicationData>
</Change></Commands>
Response from SOGo:
<SyncKey>43367-54383</SyncKey>
<Status>1</Status>
<MoreAvailable/>
<Responses><Change><ServerId>45527</ServerId><Status>1</Status></Change></Responses>
The SyncKey comes back unchanged. Per MS-ASCMD the server must return an incremented SyncKey on a successful Sync, so the device treats the transaction as unfinished and resends — twice per second, indefinitely. <MoreAvailable/> with no data following makes it re-request immediately as well.
The change is processed server-side (EAS - Change - Process change for folder folderTrash easId 45527, Status 1). It is simply never acknowledged in a way the client can accept.
Also notable: immediately after processing the client’s change, SOGo logs Change detected during Sync, we push the content. — it appears to treat the client’s own change as a server-side change to push back.
The consequence is what made this so confusing: while that loop runs, other collections barely get a turn. In the capture, 263 requests went to Trash and 7 to INBOX. Read flags and deletions in the INBOX were queued behind an infinite loop — which looked exactly like “SOGo does not write back to IMAP”.
I could not determine how message 45527 got into that state; the capture starts with the loop already running.
The fix
sogo-tool manage-eas resetfolder <user> <deviceId>+folder<trashFolderId>
The loop stopped immediately and read/delete now propagate to IMAP within seconds. Note that resetfolder on the INBOX did not help — it had to be the collection where the stuck message actually lived. Get the folder key from manage-eas listfolders <user> <deviceId>.
Ruled out along the way
- Traefik / reverse proxy: Ping requests stay open 730 s and return 200; Sync completes in 0.3–0.5 s, all 200. Nothing is being cut.
- My own initial theory was wrong: the IMAP write-back never touches the reverse proxy at all, since SOGo reaches the mail module pod-internally. The IMAP STORE runs on a separate connection and commits before the HTTP response, so a dropped HTTP connection could not roll it back.
- Worker count: NS8 already ships the values from the SOGo tuning guide (
WOWORKERSCOUNT=30, ping/sync interval 3540, internal sync 30). Not the bottleneck for 4 push devices. - Trash folder mapping: correct, and no IMAP errors anywhere in the log.
Four things we tripped over on the way
Searching for the cause surfaced several unrelated issues. Listing them in case they are useful to others:
1. Possible template bug — LDAP bind against the groups source. Every authentication logs:
[LDAPSource] LDAPException REASON:operation bind failed: Invalid credentials (0x31)
INFO:{"error_code" = 49; login = "samaccountname=<user>,dc=ad,..."; }
The bind DN is constructed rather than searched. In the NS8-generated sogo.conf, the AD_Users source has bindFields = (sAMAccountName) while AD_Groups does not — yet it is still canAuthenticate = YES. So SOGo attempts a direct bind against the groups source on every login, which can only fail. Logins succeed via the users source, so the effect is cosmetic, but it is generated config and it fires on every request.
2. Stale EAS device registrations. manage-eas listdevices showed 14 device IDs for one user, one of them literally 0, the oldest last seen in December 2018. Across all users that was ~5700 rows in the sogo_cache_folder_* tables; pruning everything not seen since 2026 removed 4818 of them. manage-eas has no delete subcommand, so this means going into the database directly (with a dump taken first). Is there a supported way to do this, or would a deletedevice subcommand be worth requesting upstream?
Best regards,
Thorsten