Skip to content

References on frames and threading headers in the list projection - #12

Merged
TheGreatAxios merged 12 commits into
mainfrom
cl-7446-references-and-threading-headers
Sep 4, 2026
Merged

References on frames and threading headers in the list projection#12
TheGreatAxios merged 12 commits into
mainfrom
cl-7446-references-and-threading-headers

Conversation

@TheGreatAxios

Copy link
Copy Markdown
Contributor

An inbox page never loads raw — the list projection is deliberately
built from cached columns only — so a client had no way to thread it:
Message-ID and In-Reply-To lived nowhere but inside the frame.

Frame. buildMailFrame accepts references (the thread's ancestry,
oldest first, as RFC 2822 §3.6.4 defines it) and emits it as a folded
References: header inside the 78-character line limit. In-Reply-To
stays the single immediate parent. Every threading value — messageId,
inReplyTo, and each references entry — must be a bracketed msg-id
(<local@domain>), and a RangeError refuses anything else: past the
builder the bytes are frozen, and an unthreadable header written today is
unthreadable forever.

Decode. decodeMailFrame now returns messageId, inReplyTo
(string | null) and references (string[], oldest first) parsed
alongside the header map, so no reader re-derives them.

Projection. principal_mail gains message_id and in_reply_to,
nullable text, cached exactly the way subject and from_address are.
All three durable paths write them — writeMailboxMessage,
deliverInboxItems (both accept inReplyTo and references on their
inputs) and createMailboxPersist, which takes them off the decode of a
frame it did not build. MailboxMessage gains an optional inReplyTo;
messageId now resolves header → cached column → row id, so the row id
is the last resort rather than the list's only answer.

Migration 0002_mail_threading_headers adds both columns and
backfills them from each existing row's raw, so threading does not
silently begin at the upgrade. The backfill searches only the header
section and decodes raw as LATIN1 — a single non-UTF8 byte in one frame
would otherwise abort the whole statement.

Tests: three-deep References round trip and folding, msg-id rejection,
list and detail agreeing on the headers, the persist path caching from an
inbound frame, and the backfill against a table seeded with legacy rows
(raw present, columns null, one of them invalid UTF-8). bun run test,
bun run typecheck, bun run build and the reference-host acceptance
suite are all green locally.

Part of CL-7445
Fixes CL-7446

A frame is frozen at rest, so the properties worth pinning are the ones
nobody can fix later: a three-deep References chain round-trips oldest
first and folds inside the 78-character line limit, a value that is not
a bracketed msg-id is refused, the list projection serves both headers
without loading raw, the persist path caches them off a frame it did not
build, and 0002 backfills legacy rows whose columns are null - including
one whose raw is not valid UTF-8, which must not abort the statement.
An inbox page never loads raw, so a client could not thread it: the only
place Message-ID and In-Reply-To lived was inside the frame. They join
subject and from_address as cached columns, written on all three durable
paths and backfilled from raw by 0002 so threading does not begin at the
upgrade.

References is emitted folded and oldest first, the order RFC 2822 defines
and every threading client walks; In-Reply-To stays the single immediate
parent. Every threading value is validated as a bracketed msg-id at the
builder, which is the last point a caller can still be blamed - past it
the bytes are frozen and an unthreadable header is unthreadable forever.
The data model block listed every principal_mail column, so it now lists
the two cached threading columns as well.
@TheGreatAxios
TheGreatAxios merged commit 55cca1e into main Sep 4, 2026
1 check passed
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