Skip to content

fix(reply): send the parent's RFC msg-id in In-Reply-To, not its JMAP id - #35

Open
lucletoffe wants to merge 1 commit into
bulwarkmail:mainfrom
lucletoffe:fix/reply-threading-msgid
Open

fix(reply): send the parent's RFC msg-id in In-Reply-To, not its JMAP id#35
lucletoffe wants to merge 1 commit into
bulwarkmail:mainfrom
lucletoffe:fix/reply-threading-msgid

Conversation

@lucletoffe

Copy link
Copy Markdown
Contributor

The problem

Replying puts email.id — the server-local JMAP handle, e.g. gos2aabtux — into
In-Reply-To and References (EmailThreadScreen.tsx, navigateCompose). That id is
meaningless outside the account it came from: it is not an RFC 5322 msg-id, so no client
and no server can match it against the parent's Message-ID.

The conversation therefore splits in two: the original message sits alone in its thread,
while the reply — and everything that answers the reply — starts a fresh one.

Proof

Observed against a live Stalwart account (ids abbreviated):

# from threadId In-Reply-To
1 correspondent btux
2 reply sent from the app btuz gos2aabtux ← JMAP id of #1
3 correspondent replies to #2 btuz 18c72125…@mail-1

Server side this is correct behaviour: Stalwart hashes the referenced msg-ids at ingest
(crates/email/src/message/ingest.rs::find_thread_id), finds no match, and files the reply
under a new thread. The webmail builds these headers correctly, so the split only happens
to conversations replied to from mobile.

The fix

  • New src/lib/email-threading.ts: computeReplyThreadingHeaders() builds the pair from the
    parent's messageId and references — bracketed, ancestors first, parent closing the
    chain, de-duped (RFC 5322 §3.6.4).
  • messageId / inReplyTo / references added to the Email type and to
    EMAIL_FULL_PROPERTIES so they are actually fetched (JMAP returns them as bare ids,
    RFC 8621 §4.1.2.3).
  • navigateCompose uses it instead of email.id.

Replies to a message whose parent has no Message-ID now send no threading headers at all,
rather than a header that is actively wrong.

Tests

src/lib/__tests__/email-threading.test.ts — 5 cases: no msg-id, bracketing, ancestor chain,
de-dupe, ids that already carry angle brackets. npx tsc --noEmit clean; full suite 364 passed
(src/stores/__tests__/auth-store.test.ts fails to import on main too — pre-existing, unrelated).

Replying put `email.id` — the server-local JMAP handle, e.g. `gos2aabtux` —
into In-Reply-To/References. No recipient can match that against any
Message-ID, so the conversation splits in two: the original sits alone in
its thread while the reply (and everything answering it) starts a fresh one.

Proven against a live Stalwart account: the reply carried
`In-Reply-To: gos2aabtux` and landed in thread `btuz` while its parent stayed
in `btux` — Stalwart hashes the referenced msg-ids at ingest and found no
match.

Fetch `messageId`/`inReplyTo`/`references` with the full email and build the
headers from them (bracketed, ancestors first, parent closing the chain).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FTRyrFCR2N9qkpKK37GgWb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant