Skip to content

Support multiple domains per inbox - #49

Open
PierreFouquet wants to merge 2 commits into
cloudflare:mainfrom
PierreFouquet:multi-domain-support
Open

PierreFouquet wants to merge 2 commits into
cloudflare:mainfrom
PierreFouquet:multi-domain-support

Conversation

@PierreFouquet

Copy link
Copy Markdown

Summary

Resolves #8 ("Connect Multiple domain in 1 worker?").

While investigating, I found that a single instance can already serve multiple domains — it was just undocumented, which is why it was assumed unsupported:

  • GET /api/v1/config already splits the DOMAINS var on commas (workers/index.ts).
  • The New Mailbox dialog already renders a domain Select when more than one domain is configured (app/routes/home.tsx).
  • Sending (validateSender) and receiving (receiveEmail) are domain-agnostic, and EMAIL_ADDRESSES can already span domains.

This PR makes multi-domain a documented, first-class feature and adds a matching backend guard.

Changes

  • workers/index.ts — add a parseDomains() helper (reused by /api/v1/config), and reject mailbox creation on domains outside the configured DOMAINS, mirroring the front-end picker. Explicit EMAIL_ADDRESSES entries bypass the check (they are the authoritative allow-list and may legitimately span domains), and an empty DOMAINS imposes no restriction — so existing behavior is preserved.
  • README.md — new "Using multiple domains" section; clarified the singular "your domain" wording.
  • wrangler.jsonc — comments documenting that DOMAINS accepts a comma-separated list and that EMAIL_ADDRESSES may span domains.
  • package.json — expanded the deploy-time DOMAINS binding description.

No change to send/receive behavior.

How to use

Set DOMAINS to a comma-separated list and add a catch-all Email Routing rule (and verified sending) per domain:

"DOMAINS": "example.com,another.com"

Testing

Verified locally with a multi-domain config (DOMAINS="example.com,another.com") injected via a local-only Wrangler config (per the approach in #44), running npm run dev:

Check Result
GET /api/v1/config {"domains":["example.com","another.com"],"emailAddresses":[]}
Create hello@example.com (1st domain) 201
Create hi@another.com (2nd domain) 201
Create x@notconfigured.com 400 "Mailbox domain must be one of the configured DOMAINS"
npm run typecheck passes ✅

🤖 Generated with Claude Code

A single instance can already serve multiple domains by setting DOMAINS to a
comma-separated list: the /api/v1/config endpoint splits it, and the New Mailbox
dialog renders a domain picker when more than one is configured. This was
undocumented, so users assumed it was unsupported (issue cloudflare#8).

Make it a documented, first-class feature and add a matching backend guard:

- Add a parseDomains() helper and reuse it in /api/v1/config.
- Reject mailbox creation on domains outside the configured DOMAINS, mirroring
  the front-end picker. Explicit EMAIL_ADDRESSES entries bypass the check, so the
  auto-create flow (which may span domains) is unaffected. No DOMAINS configured
  means no restriction, preserving existing behavior.
- Document multi-domain setup in the README ("Using multiple domains"),
  wrangler.jsonc, and the deploy-time DOMAINS binding description.

Closes cloudflare#8

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Serving more than one domain from one instance does not work reliably on the
receive side. receiveEmail picks parsedEmail.to[0] -- the first address in the
To: header -- as the mailbox. That is not the address the message was actually
delivered to, so with several domains configured:

- a message addressed to an external recipient first is filed against that
  address, finds no mailbox, and is silently dropped
- a Bcc'd message, whose recipient never appears in the headers, is dropped
- a forged To: header can steer a message at another mailbox

Resolve the mailbox from the SMTP envelope recipient (event.to) instead, which
is the address Email Routing delivered to and the only signal that separates
domains correctly. Header addresses (To/Cc/Bcc) remain a fallback for events
that carry no envelope, such as local dev or a replayed message, and that
fallback now prefers an address on a configured domain over header order.

The envelope path is deliberately not filtered by DOMAINS: a stale DOMAINS
value must not silently drop mail that Email Routing was configured to deliver
to the Worker. Mailbox existence in R2 remains the real gate.

The decision is extracted into resolveMailboxId() in lib/email-helpers so the
rule sits in one readable place, and IncomingEmailEvent widens the handler's
event type to carry the envelope fields.

Single-domain deployments are unaffected: with one configured domain there is
only one address the envelope can name.

Verified: typecheck and build pass; resolveMailboxId exercised against 15
cases covering two domains, Bcc-only delivery, forged headers, the
EMAIL_ADDRESSES allow-list, stale DOMAINS, and the no-envelope fallback.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UJmpmeXN1r87S3c2rJ7V8Y
PierreFouquet pushed a commit to PierreFouquet/agentic-inbox that referenced this pull request Sep 3, 2026
…uting

Brings in the full contents of cloudflare#49 as it now stands:

- parseDomains() over the comma-separated DOMAINS var, mailbox creation
  restricted to the configured domains, and multi-domain setup documented in
  the README, package.json binding description, and wrangler.jsonc
- inbound mail resolved by SMTP envelope recipient rather than the first To:
  header address, which is what actually keeps domains apart

Conflict in wrangler.jsonc resolved by keeping our DOMAINS value
("pierrefouquet.co.uk") and our custom route block, while taking the upstream
explanatory comments. No domain was added and no route changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UJmpmeXN1r87S3c2rJ7V8Y
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.

Connect Multiple domain in 1 worker?

2 participants