Email routing
The email-routing extension configures incoming domain email using Cloudflare
Email Routing. Messages are forwarded to an existing, verified inbox such as
Gmail. It provisions no VM, Worker or D1 database and needs no paid Workers plan.
It does not provide a mailbox, IMAP, POP3 or outbound SMTP.
Cloudflare's pricing allows unlimited inbound routing on Free. Sending to arbitrary recipients requires Workers Paid; sending to your own verified destinations is free. To reply from your domain while keeping Cloudflare Free, use a separate SMTP provider. Configure that provider's DKIM/SPF as part of its setup; do not add a second SPF record.
Configure an instance
2srv init app mail --template email-routing -o platform/mail.yamlEdit the generated App file:
apiVersion: 2server.app/v1
kind: App
metadata: {name: mail}
template: email-routing
spec:
zone: example.com
routes:
- address: [email protected]
destination: [email protected]
- address: [email protected]
destination: [email protected]Replace the domain and addresses. The CLI resolves the account from the active
Cloudflare zone. Optional accountId pins the expected account and rejects a
mismatch. This version manages exact addresses on
the zone apex; subdomain addresses and catch-all changes are not supported.
Destinations must be external inboxes to avoid forwarding loops.
Keep the instance name stable: each rule is owned by
2server:<instance>:<address>. Different instances cannot take over an address.
There are no VM secrets, HTTP domains, requires or webhooks on this App.
API token
Follow Create a Cloudflare token for dashboard setup, Account/Zone resource selection and verification. Use a scoped bearer token, with these permissions on the selected account/zone:
| Scope | Permission | Purpose |
|---|---|---|
| Account | Email Routing Addresses: Edit | Register and inspect destination inboxes |
| Zone | Zone: Read | Resolve the active zone and verify its account |
| Zone | DNS: Read | Inspect existing mail DNS |
| Zone | Zone Settings: Edit | Enable Email Routing and its Cloudflare-managed DNS |
| Zone | Email Routing Rules: Edit | Inspect and reconcile exact-address rules |
| Zone | DNS: Edit, only with spec.replaceMx | Delete explicitly approved obsolete MX records |
DNS Edit covers DNS Read when the token already deploys public domains. Optional account-rule and token-policy diagnostic probes can be denied independently of ordinary zone routing; their rights are not required for normal deployment.
Set CLOUDFLARE_API_TOKEN privately in the local process environment, CI secret
store, or ignored 2server/.env (mode 0600) when running from the product root.
Never put its value in an App file, command argument or Git. If the token is
already on a 2server VM, pass --connection FILE (or --ssh user@host) to
plan/deploy/get. The CLI reads the VM's configured cloudflare.tokenEnv through
a read-only control session and keeps the token in memory. It does not change VM
configuration, install containers or fall back to local values if the VM secret
is missing. See the
Cloudflare API.
Account-wide permissions do not imply zone-level Email Routing rule permissions.
For an account-owned token with Account API Tokens Read/Edit, optional
spec.manageTokenPermissions: true can append Email Routing Rules Write to
one existing allow policy scoped to exactly the declared zone. plan reports
the proposed addition; apply preserves other policies and token restrictions.
It refuses broader or ambiguous zone policies, deny policies and concurrent
policy changes. This option defaults to false and never rotates the credential
or adds resource scopes.
For a newly requested zone without an existing exact-zone policy, the separate
spec.createZoneTokenPolicy: true opt-in allows creation of one such policy.
It requires manageTokenPermissions: true and a pinned accountId; the CLI
checks the zone belongs to that account before any grant. The new policy grants
only Zone Read, DNS Read, Zone Settings Write and Email Routing Rules Write on
that exact zone. Existing policies and token restrictions remain intact. A plan
reports the grant and defers protected mail reads; apply creates the policy,
then checks mail DNS/settings/rules and destination verification. It may leave
the granted policy in place if later mail preflight fails. It never grants rights
to all zones or all accounts. This option defaults to false.
Plan, verify, deploy
2srv validate -f platform/mail.yaml
2srv plan -f platform/mail.yaml
2srv deploy -f platform/mail.yaml --apply
# When the credential is owned by a connected VM:
2srv deploy -f platform/mail.yaml --connection .2server/connection.json --applyvalidate is offline. plan reads Cloudflare without mutation and checks account
ownership, conflicting MX/SPF, routing-rule ownership and destination verification.
get -f FILE diagnoses settings, DNS, destination verification and token policy
metadata, including separate account/zone API responses. Neither proves write
permission or delivery.
If a destination is new, deploy --apply registers it and Cloudflare sends a
verification message. The result is status: pending-verification; no mail DNS
or routing rules change yet. An opted-in token permission addition may already
have been applied. Open that message in the destination inbox and
verify the address, then run the same deploy command again. Existing pending
destinations are reused without repeatedly sending verification messages.
Once all destinations are verified, the CLI enables routing using Cloudflare's
DNS API and creates or updates only its owned rules. It checks live settings,
required DNS records and rule contents before reporting status: ready.
Repeated deployment keeps unchanged rules. An API failure can leave partial
changes: inspect the state with plan before retrying. The CLI reports permission
failures without printing token values or provider response bodies.
Existing mail-provider MX or a different SPF causes deployment to stop. Review
the mail migration and sender authentication explicitly before changing those
records. To replace a confirmed obsolete apex MX, explicitly list its exact
hostname and priority in spec.replaceMx, for example:
replaceMx:
- content: old.mx.example.net
priority: 1000The plan reports matching records. They are deleted only after destinations are verified and DNS/rule ownership has been rechecked. Other conflicting MX and SPF still stop deployment; no SPF replacement is supported. Foreign rules, catch-all behavior, account-wide destinations and unrelated DNS remain intact.
For end-to-end verification, send to each configured alias from a separate external inbox and confirm it arrives in the destination inbox. Also send to an unconfigured alias and check the intended existing catch-all/drop behavior. Cloudflare readiness checks alone do not prove inbox delivery.
Disable or retire
To disable an owned address, keep its route in the file with enabled: false
and deploy. Removing a row leaves its existing rule in place and reports it in
retainedRules; it does not silently retire mail delivery.
Deletion and rollback from the CLI are refused. Retire rules explicitly in
Cloudflare after reviewing delivery needs. Do not disable zone-wide routing,
remove shared MX/SPF or delete account-wide destinations just to retire one
instance. app NAME logs|restart does not apply: this extension has no container
and is not recorded as a VM installation.
