cloudflare / cloudflare/agentic-inbox

Route inbound mail by SMTP envelope recipient, not To: header

Open
#32 0 comments 1 reaction 0 assignees View on GitHub
Dominant language
TypeScript
Stars
7.5k
Forks
964
PR merge metrics
No merged PRs in 30d

Description

`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/.json` is absent in R2, and the handler logs `Ignoring email for : 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

1. Create a mailbox for `you@yourdomain.com`.
2. Subscribe to any list whose posts have a list `To:` (reproduced with `musl`, `kernel-hardening`, `lkrg-users`, `oss-security` on `lists.openwall.com`).
3. Confirm subscription — confirmation lands in the inbox.
4. 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.

```ts
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.

Contributor guide

Open the contributing guide

Research direction

Start at workers/index.ts:352-397 to trace how recipients select the mailbox and are stored, then inspect workers/app.ts:113 and the email handler type. Use the issue’s mailing-list reproduction to verify envelope-recipient routing, and confirm that header recipients remain available for display or threading while the correct mailbox receives the message.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
74/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.