fix(inbound): key a stored raw on its Message-ID so a relay retry is idempotent by Yi-111-a · Pull Request #169 · hieunc229/mailflare · GitHub
Skip to content

fix(inbound): key a stored raw on its Message-ID so a relay retry is idempotent - #169

Merged
hieunc229 merged 1 commit into
hieunc229:mainfrom
Yi-111-a:fix/inbound-idempotency
Oct 24, 2026
Merged

hieunc229 merged 1 commit into
hieunc229:mainfrom
Yi-111-a:fix/inbound-idempotency

Conversation

@Yi-111-a

Copy link
Copy Markdown
Contributor

Fixes #167

Summary

A relayed message that is delivered twice — the normal outcome of a relay retry after a timeout — was stored twice. intakeIncomingMail gave every accepted message a fresh inbound/${Date.now()}-${newId()}.eml key, so two posts of the same message produced two different keys, two queue messages, and two mailbox copies with different ids and the same RFC Message-ID.

Change

src/lib/email/intake.ts now derives the storage key from the RFC Message-ID (scoped by mailbox), falling back to a content hash when the header is absent. The inbound queue consumer already dedupes by (mailboxId, rawR2Key), so a retry now overwrites the same object and is skipped instead of duplicating the mailbox copy.

  • New inboundStorageKey / extractMessageIdHeader in src/lib/email/inbound-id.ts — a lightweight header read (case-insensitive, folded lines unfolded), no full MIME parse.
  • Scoping by mailboxId keeps messages.id unique when the same message reaches two mailboxes (it is derived from the key).

Testing

  • New tests/inbound-key.test.mjs (bundled with esbuild): 8/8 pass — header extraction (case-insensitive, folded, absent), key stability per mailbox, per-mailbox and per-Message-ID differences, and the content-hash fallback.
  • npx tsc --noEmit clean.
  • vp lint cannot run on this checkout (Node 20.19.2; vp/oxlint needs Node 22 for node:fs/promises.glob). Unrelated pre-existing environment limitation.

Other comments

This change was written by an AI coding agent (OpenCode).

…idempotent

A relayed message that is delivered twice (the normal outcome of a relay
retry after a timeout) was stored twice: intake gave every accepted message
a fresh `inbound/${Date.now()}-${newId()}.eml` key, so two posts of the same
message produced two different keys, two queue messages, and two mailbox
copies with different ids and the same RFC Message-ID.

Derive the storage key from the RFC Message-ID, scoped by mailbox, and fall
back to a content hash when the header is absent. The inbound queue consumer
already dedupes by `(mailboxId, rawR2Key)`, so a retry now overwrites the
same object and is skipped instead of duplicating the mailbox copy. Scoping
by mailbox keeps the message id unique when the same message reaches two
mailboxes.

Fixes hieunc229#167
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.

Relay delivery that is retried is stored twice — intake keys on the storage key, not the RFC Message-ID

2 participants