Mailspring inbox archive collapse issue

On a generic (non-Gmail) IMAP account, Mailspring’s mailsync engine treats a message as a single identity shared across all folders. If the same message (identical envelope / Message-ID) genuinely exists as separate copies in two different IMAP folders on the server — e.g. INBOX and a secondary Archive_ folder that the server itself populates independently (not something the client moved) — mailsync repeatedly reassigns that message’s single local Message row to whichever folder it happens to sync last in its loop. In practice this means every message also present in Archive_ gets silently evicted from the local INBOX view on every sync cycle, even though the message is still present in INBOX on the server.
The result: the Mailspring UI shows the inbox as empty (triggering the “Hooray! You’re done.” / Inbox Zero empty-state animation) while the account’s INBOX folder actually has hundreds of unread messages on the server. Removing and re-adding the account does not fix it — the very next sync loop re-triggers the same collapse.

Description

On a generic (non-Gmail) IMAP account, Mailspring’s mailsync engine treats a message as a single identity shared across all folders. If the same message (identical envelope / Message-ID) genuinely exists as separate copies in two different IMAP folders on the server — e.g. INBOX and a secondary Archive_ folder that the server itself populates independently (not something the client moved) — mailsync repeatedly reassigns that message’s single local Message row to whichever folder it happens to sync last in its loop. In practice this means every message also present in Archive_ gets silently evicted from the local INBOX view on every sync cycle, even though the message is still present in INBOX on the server.
The result: the Mailspring UI shows the inbox as empty (triggering the “Hooray! You’re done.” / Inbox Zero empty-state animation) while the account’s INBOX folder actually has hundreds of unread messages on the server. Removing and re-adding the account does not fix it — the very next sync loop re-triggers the same collapse.

To Reproduce…

Steps to reproduce the behavior:

    1. Configure a generic IMAP account whose server keeps a secondary folder (in our case literally named Archive_, no special role) that contains copies of messages that are also currently present in INBOX — i.e., not moved out of INBOX, genuinely duplicated with a different UID in each folder.
    2. Let Mailspring complete an initial sync.
    3. Watch ~/.config/Mailspring/mailsync-<accountId>.log across two consecutive sync loops.

Expected Behavior

A message that is independently present in two different real IMAP folders (not moved, both copies co-existing with distinct UIDs) should be represented locally in a way that preserves its membership in both folders — or at minimum, sync engine folder-processing order should not cause a message legitimately present in INBOX to be silently dropped from INBOX’s local view because a later-processed folder also contains a copy.
This effectively makes any generic IMAP account unusable in Mailspring if the server has any folder whose contents overlap with INBOX by design (auto-archiving/fileinto+keep style server rules, common on Zimbra and similar platforms) — a configuration Thunderbird and other IMAP clients handle fine because they key messages by (folder, UID) rather than by a single cross-folder identity.

Screenshots

Setup

  • OS and Version: Ubuntu 24.04 LTS, Linux 6.17
    • Installation Method: apt
  • Mailspring Version: 1.23.0
  • Server: a self-hosted / enterprise IMAP server (Zimbra-family), unrelated Gmail accounts on the same install are unaffected
    • Confirmed not a server-side problem: Thunderbird against the same IMAP/SMTP server shows INBOX correctly and has for years.

Additional Context

Initial per-folder UID range sync correctly finds all real INBOX messages:

syncFolderUIDRange for INBOX, UIDs: 1 - 8499, Heavy: false
- INBOX: remote=720, local=0, remoteUID=<inboxFolderId>
- Fetching full headers for 720 (of 720 needed)

Immediately afterward, in the same sync loop, Archive_ is processed and reassigns many of those same rows away from INBOX:

syncFolderUIDRange for Archive_, UIDs: 4073 - 4823, Heavy: true
- Archive_: remote=683, local=0, remoteUID=<archiveFolderId>
- Updating message <messageRowId>
-- UID (8498 to 4822)
-- FolderID (<inboxFolderId> to <archiveFolderId>)
- Updating message <messageRowId>
-- UID (8497 to 4821)
-- FolderID (<inboxFolderId> to <archiveFolderId>)
... (repeats for dozens of messages per loop)

Querying the local edgehill.db directly confirms the effect: shortly after a fresh full resync, INBOX’s message count is already lower than the server-reported remote=720, and stalls/shrinks further on subsequent loops as more rows get reassigned to Archive_. The folder’s own localStatus (syncedMinUID: 1, uidnext matching the server) claims the folder is fully and correctly synced, while the actual Message table row count for that remoteFolderId does not reflect that.
Because the folder-sync-state bookkeeping (syncedMinUID) is per-folder and considers the deep scan “complete” once it has walked the UID range once, there is no mechanism that re-detects or re-corrects the earlier eviction on subsequent loops — the message is just gone from the INBOX folder’s local message set until the next full resync temporarily repopulates it, only for the next Archive_ pass to reclaim it again.
User-visible symptom: the message list for INBOX intermittently/persistently renders as empty, which triggers EmptyListStateEmptyInboxState (the “Hooray! You’re done.” Inbox Zero congratulations screen) even though the mailbox is far from empty.