feat(mail): capture Message-ID and a content hash at ingest #398

Merged
KofTwentyTwo merged 1 commits from story/message-id-capture into dev 2026-08-14 20:20:57 +00:00
1 Commits
Author SHA1 Message Date
james.maes 3de75493d5 feat(mail): capture Message-ID and a content hash at ingest
Identity cannot currently be reconstructed for a stored message. The parser
never reads Message-ID, the Email model has nowhere to put it, the messages
table has no column for it, and the raw octets are dropped once parsed. Any
later attempt to give a message a folder-independent identity therefore has
nothing to work from except a re-download of the entire mailbox.

Capture both now, as nullable columns nothing reads yet, so the data starts
accruing while the design that consumes it is still being built.

Normalization is fixed and shared rather than left to each call site: strip all
ASCII whitespace (a msg-id cannot legally contain any, so this unfolds a wrapped
header without knowing how the transport folded it), strip one pair of angle
brackets, and preserve case, since only the domain half is case-insensitive and
lowercasing wholesale would merge ids the origin considers distinct. Engines
that normalize differently would hold the same message under different bytes,
and no comparison between their caches would mean anything.

Captured at every ingest path, including the two that do not parse a full
message. The envelope-only path takes Message-ID from ENVELOPE rather than
skipping capture. The headers-only path keeps the id recovered from those
headers but discards the hash parse_mime computed over them, because
content_hash is defined over a message's full octets and publishing a
header-only digest under that name would quietly mean something else.

The hash exists to keep Message-ID honest: it is public, echoed in every
References header, and reused legitimately -- a mailing-list copy and a direct
copy share one, and a draft keeps one across every save -- so the pair
distinguishes messages that the id alone would merge.

No backfill. Legacy rows stay NULL, which reads as "not captured" rather than
"this message has no Message-ID"; inventing a value would be worse than
admitting the gap.

Refs #386
2026-08-10 14:48:37 -05:00