Skip to main content
This Worker gives one Xem installation a customer-owned inbound mailbox. Cloudflare Email Routing invokes the Worker, private R2 stores raw MIME, bodies, and attachments, and D1 stores the mailbox index, UID state, flags, and attachment metadata. Xem’s backend calls the signed Worker API. The browser never receives the shared secret or a Cloudflare API token. The implementation has a 10 MiB raw-message limit and accepts at most 32 attachments per message. It does not automatically delete received, pending, or quarantined mail. One deployment serves the exact address configured in MAILBOX_ADDRESS; use a separate Worker, D1 database, R2 bucket, and secret for each mailbox that needs isolation.

Account setup

The step-by-step guide, including what is free on Cloudflare and what is not, is Receive mail on your domain with Cloudflare. In short: Deploy to Cloudflare The button creates the D1 database and R2 bucket, asks for MAILBOX_ADDRESS and MAILBOX_API_SECRET (declared in .dev.vars.example), and runs pnpm run deploy, which deploys and then applies the D1 migration. Without the button, install Node.js 24 and pnpm 10.7.1, then from this directory:
  1. pnpm install --frozen-lockfile and pnpm wrangler login.
  2. cp .dev.vars.example .dev.vars, then set MAILBOX_ADDRESS to the complete mailbox address and MAILBOX_API_SECRET to a unique random value of 32–4096 characters (openssl rand -hex 32).
  3. pnpm wrangler deploy --secrets-file .dev.vars. Wrangler creates the xem-cloudflare-mailbox D1 database and R2 bucket if they don’t exist and writes the database ID back to wrangler.jsonc.
  4. pnpm wrangler d1 migrations apply MAILBOX_DB --remote.
  5. Create a Cloudflare Email Routing rule for that exact address whose action sends to this Worker. MAILBOX_ADDRESS must match it exactly, ignoring case.
  6. In Settings → SMTP senders → Connect Cloudflare inbox, enter the Worker URL, exact mailbox address, and the same shared secret. Xem confirms the signed /v1/mailbox/identity response before saving. It returns protocol version: 1 only after the D1 migration is available.
Do not make the R2 bucket public. Xem does not need an R2 or D1 API token; all content access stays behind the signed Worker API. For local development, .dev.vars also feeds pnpm wrangler dev. Apply the migration locally with pnpm wrangler d1 migrations apply MAILBOX_DB --local, then deliver a test message with curl -X POST "http://localhost:8787/cdn-cgi/handler/email?from=a@example.net&to=<MAILBOX_ADDRESS>" --data-binary @message.eml.

Storage and recovery

Ingestion first writes pending/<id>.eml, then parses MIME and writes deterministic message objects before atomically committing the D1 message and attachment rows. It deletes the pending object only after that commit. A per-minute scheduled handler retries one pending object per run and advances a private cursor so a transient failure cannot block the backlog; transient failures remain pending without an expiry. Malformed MIME and messages with more than 32 attachments move to quarantine/<id>.eml for operator inspection and manual recovery; they are never automatically deleted. Inspect the pending/ and quarantine/ prefixes through the authenticated Cloudflare dashboard or an account-scoped S3-compatible R2 client. Do not make the bucket public to inspect it. After correcting a parser or operational problem, copy the quarantined raw object and its from, to, and receivedAt custom metadata back to its original pending/<id>.eml key and let the scheduled handler retry it. Byte-identical messages with the same normalized envelope sender and recipient produce the same deterministic ID and deduplicate; distinct bytes remain separate messages. The mailbox exposes only INBOX, supports metadata search across subject, sender, recipients, and RFC Message-ID, and permits only independent read (\\Seen) and starred (\\Flagged) changes. It has no expunge or delete API.

Validation and limits

Cloudflare bills and limits Workers, Email Routing, R2, and D1 separately. Check the current account plan and product documentation before production use. D1 databases are documented at 500 MB each on the Free plan and 10 GB each on paid plans; Worker CPU and request quotas also constrain parsing and API access. This directory is a local implementation. Its tests and Wrangler dry run do not prove a live Cloudflare deployment, Email Routing delivery, DNS readiness, or receipt from external providers such as Google.