workers/index.ts receiveEmail picks the destination mailbox from parsedEmail.to, which is the message's To: header. For mailing-list traffic the To: header is the list address and the subscriber is only in the SMTP envelope, so the lookup misses, mailboxes/<list-address>.json is absent in R2, and the handler logs Ignoring email for <list-address>: mailbox does not exist and returns. Email Routing records Handled with pass/Safe, no exception is thrown, no retry fires, the message is lost. The subscription confirmation arrives normally (its To: is the subscriber), so the failure mode only shows up once list posts start flowing.
Repro
- Create a mailbox for
you@yourdomain.com.
- Subscribe to any list whose posts have a list
To: (reproduced with musl, kernel-hardening, lkrg-users, oss-security on lists.openwall.com).
- Confirm subscription — confirmation lands in the inbox.
- Wait for list traffic — Activity Log shows
Handled, inbox stays empty.
Sites
workers/index.ts:352 — if (!parsedEmail.to?.length …) throw
workers/index.ts:355 — allRecipients built from parsedEmail.to
workers/index.ts:359-364 — mailboxId chosen from allRecipients
workers/index.ts:367 — silent return when mailbox missing
workers/index.ts:397 — recipient: allRecipients.join(", ") stored in DB
workers/app.ts:113 — email handler typed as { raw: ReadableStream; rawSize: number }, hiding event.to/event.from/event.headers
Proposal
Use event.to (envelope RCPT TO, i.e. what Email Routing matched and what the Activity Log shows as Custom Address) as the mailbox id. Keep header parsing for display/threading only.
async function receiveEmail(
event: ForwardableEmailMessage,
env: Env,
ctx: ExecutionContext,
) {
const rawEmail = await streamToArrayBuffer(event.raw, event.rawSize);
const parsedEmail = await new PostalMime().parse(rawEmail);
const mailboxId = event.to.toLowerCase();
const allowed = ((env.EMAIL_ADDRESSES ?? []) as string[])
.map((a) => a.toLowerCase());
if (allowed.length > 0 && !allowed.includes(mailboxId)) {
console.log(`Ignoring email: ${mailboxId} not in EMAIL_ADDRESSES`);
return;
}
if (!(await env.BUCKET.head(`mailboxes/${mailboxId}.json`))) {
console.log(`Ignoring email for ${mailboxId}: mailbox does not exist`);
return;
}
// …existing storage logic, with `recipient` set from envelope + header
// so list mail still shows the list address for context.
}
Also update the handler signature in workers/app.ts:113 to ForwardableEmailMessage so event.to/event.from/event.headers are visible to TS.
Why
The SMTP envelope is authoritative — it's what Email Routing matched the rule against and the only field that survives BCC, alias expansion, and list distribution. Parsing To: is a re-derivation that disagrees with the envelope in the most common "third-party drives mail to me" cases: mailing lists, BCC-only delivery, and forwarders that don't rewrite headers. Right now those all get silently dropped with Handled in the Activity Log, which is invisible to operators and unrecoverable.
Happy to send a PR.
workers/index.tsreceiveEmailpicks the destination mailbox fromparsedEmail.to, which is the message'sTo:header. For mailing-list traffic theTo:header is the list address and the subscriber is only in the SMTP envelope, so the lookup misses,mailboxes/<list-address>.jsonis absent in R2, and the handler logsIgnoring email for <list-address>: mailbox does not existand returns. Email Routing recordsHandledwithpass/Safe, no exception is thrown, no retry fires, the message is lost. The subscription confirmation arrives normally (itsTo:is the subscriber), so the failure mode only shows up once list posts start flowing.Repro
you@yourdomain.com.To:(reproduced withmusl,kernel-hardening,lkrg-users,oss-securityonlists.openwall.com).Handled, inbox stays empty.Sites
workers/index.ts:352—if (!parsedEmail.to?.length …) throwworkers/index.ts:355—allRecipientsbuilt fromparsedEmail.toworkers/index.ts:359-364—mailboxIdchosen fromallRecipientsworkers/index.ts:367— silent return when mailbox missingworkers/index.ts:397—recipient: allRecipients.join(", ")stored in DBworkers/app.ts:113—emailhandler typed as{ raw: ReadableStream; rawSize: number }, hidingevent.to/event.from/event.headersProposal
Use
event.to(envelopeRCPT TO, i.e. what Email Routing matched and what the Activity Log shows asCustom Address) as the mailbox id. Keep header parsing for display/threading only.Also update the handler signature in
workers/app.ts:113toForwardableEmailMessagesoevent.to/event.from/event.headersare visible to TS.Why
The SMTP envelope is authoritative — it's what Email Routing matched the rule against and the only field that survives BCC, alias expansion, and list distribution. Parsing
To:is a re-derivation that disagrees with the envelope in the most common "third-party drives mail to me" cases: mailing lists, BCC-only delivery, and forwarders that don't rewrite headers. Right now those all get silently dropped withHandledin the Activity Log, which is invisible to operators and unrecoverable.Happy to send a PR.