Messages received between imapsync import and first JMAP client connection are missing in JMAP (visible in IMAP)

Issue Description

Emails received via standard transport after a bulk imapsync migration, but before connecting a JMAP client for the first time, are accessible via IMAP but completely missing in JMAP.

Expected Behavior

JMAP should reflect all stored messages in the mailbox, or running an AccountMaintenance reindex task should bring JMAP into parity with IMAP.

Actual Behavior

The affected messages remain invisible in JMAP clients. Executing an AccountMaintenance search reindex task completes, but does not resolve the discrepancy.

Reproduction Steps

  1. Perform a bulk email import using imapsync.
  2. Receive new emails via normal transport before connecting any JMAP client.
  3. Connect a JMAP client for the first time.
  4. Compare mailbox contents between IMAP and JMAP: emails arriving between the imapsync operation and the first JMAP connection appear in IMAP, but are missing in JMAP.
  5. Execute an AccountMaintenance (Reindex search data) task for the account; the messages remain missing in JMAP.
  6. Move an affected email to another folder and back via IMAP; it immediately becomes visible in JMAP.

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

Internal

I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.

on

I have searched this support forum (open and closed topics) and confirm this is not a duplicate.

on

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

Based on the task-manager code, this looks like a cache-consistency issue rather than something a reindex can fix, and it explains exactly the symptoms you saw.

JMAP mailbox contents come from an in-memory account cache (MessageStoreCache) that is built once and then kept in sync by consulting the per-account change log, not by re-scanning the store. The AccountMaintenance “Reindex” task only rebuilds full-text/search index documents for messages that are already present in that cache (get_cached_messages() → schedule IndexDocument tasks for each cached id). It does not rebuild the cache itself or discover items missing from it, which is why running a reindex completes successfully but the missing messages never show up in JMAP.

Moving the message via IMAP works because that operation writes a normal mailbox change-log entry, which the cache-refresh logic picks up on the next JMAP access and adds the message to the cache.

The underlying cause therefore appears to be that messages delivered via normal transport between the imapsync bulk import and the first JMAP cache build are not correctly reflected in the account’s change log/cache state (a race between bulk import initialization of the cache and concurrent delivery), rather than anything a search reindex can address. This is a server-behavior bug, not a documented configuration option, so I can’t point you to a docs page that covers it, and a human maintainer should look at the change-log/cache interaction you’ve described.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

Thank you! Is there anything I should do now? Or just wait for a human to have a look?

Reindexing search data wouldn’t change this. IMAP and JMAP take mailbox membership from the same place, not from the search index, and a message missing from the index only sorts last. An IMAP move keeps the message’s id and just records new changes, so a move making it show up suggests the JMAP client’s sync state skipped those messages, not that the server lost them. I can’t confirm that from here yet, though.

I need the exact version and which JMAP client this is. Then pick one affected message, get its ids over IMAP with UID FETCH <uid> (EMAILID THREADID), and send a raw Email/get for that id plus an Email/query filtered with inMailbox on its mailbox. If both return it, the server has it and the client never picked it up. If the query leaves it out, post the request and response here.

I may be doing something wrong or misunderstanding. I can’t even get the EMAILID:

A1 ENABLE OBJECTID+

* ENABLED OBJECTID+

A1 OK ENABLE successful.

A2 BAD No mailbox is selected.

. select INBOX

* 11121 EXISTS

* FLAGS (\Answered \Flagged \Deleted \Seen \Draft)

* 0 RECENT

* OK [PERMANENTFLAGS (\Deleted \Seen \Answered \Flagged \Draft \*)] All allowed

* OK [UIDVALIDITY 3637003549] UIDs valid

* OK [UIDNEXT 11123] Next predicted UID

* OK [OBJECTID (ACCOUNTID c MAILBOXID a)] Object identifiers

. OK [READ-WRITE] SELECT completed

A2 UID FETCH 11067 (EMAILID THREADID)

A2 BAD Invalid attribute "EMAILID"

I just realized, I didn’t put any other information.

I’m running stalwart 0.16.21.

The IMAP client that successfully got all email was FairEmail, which I’m currently running 1.2333a of. I cannot say with certainty that it hasn’t updated sometime in between.

For JMAP, I used Sterna (1.5.4) and Mailtemi (3.3.3).

My fault on the command. This server implements the newer objectid draft and advertises OBJECTID+ only, so the standalone EMAILID and THREADID attributes do not exist here. It is:

UID FETCH (OBJECTID)

which comes back as OBJECTID (EMAILID THREADID ). The EMAILID value is the JMAP Email id verbatim, so the rest of what I asked for still works.

Before you go through that, one request will probably settle it. Send an Email/query with “calculateTotal”: true and no limit argument, and look at what comes back. If the response carries “limit”: 5000 and “total”: 11121, that is the whole story: queryMaxResults defaults to 5000, your INBOX has 11121 messages, and with no sort argument we return them in internal id order, oldest first. A client that does not page then loses exactly the newest messages, and IMAP is unaffected because it does not go through that path.

That would also explain why moving a message and moving it back appears to fix it. The move writes a changelog entry your client’s incremental sync picks up; the message id and thread id do not change, so nothing was repaired server-side.