Skip to content

Add Cloudflare Email Sending as a sending provider - #2

Merged
sums001 merged 2 commits into
sums001:mainfrom
LABCAT:feat/cloudflare-email-sending
Nov 1, 2026
Merged

sums001 merged 2 commits into
sums001:mainfrom
LABCAT:feat/cloudflare-email-sending

Conversation

@LABCAT

@LABCAT LABCAT commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1. Adds Cloudflare Email Sending as an optional outbound provider, so a domain that already receives through Email Routing can also send from the same account without a second vendor.

How it sends. src/worker/providers/cloudflare.ts posts to POST /accounts/{account_id}/email/sending/send with the token and account id setup already stores, so connecting is one click and no key to paste. It uses the REST API rather than the send_email binding, which would mean adding a binding to wrangler.jsonc for every deploy and every existing instance.

A few things differ from the other adapters:

  • Cloudflare generates Message-ID and rejects it in headers, so the adapter drops it and relies on the References the send pipeline appends — the same path Resend already uses.
  • Error codes are mapped to messages instead of a bare status: 10105 says the account needs Workers Paid, 10203 says sending is not enabled for the domain, 10102 says the token is missing the Email Sending permission.
  • Cloudflare has no API for onboarding a sending domain (wrangler email sending enable and the dashboard only), so the provider is hasDomainApi: false, matching how SMTP already works. The Integrations card says to enable Email Sending per domain in the dashboard, and the connect flow no longer tries to walk every domain through a provider domain API. Domains that use it skip the "verified" check when picking a sender, same as SMTP.
  • Optional Email Sending permission added to TOKEN_PERMISSIONS and to the token link, with a probe in checkPermissions. 10105 counts as permission present, since that is an account state.
  • Migration 0003 widens the providers.type CHECK. SQLite cannot alter it, so the table is rebuilt; rows in provider_domains and quota_usage and the provider_id overrides on domains and messages are copied aside and restored in the same transaction, because dropping providers cascades.

The provider row defaults to a monthly cap of 3,000, which is what Workers Paid includes.

Testing. npm run lint, npm run typecheck, npm test (56 passed) and npm run test:e2e pass. test/integration/cloudflare-provider.test.ts covers connecting, a send, threading of the reply, the Workers Paid message, and retry after a failure, against a new mock for the send endpoint. I also ran the migration against a copy of the schema with rows in providers, provider_domains, quota_usage, domains and messages; everything survives and the new type inserts.

Also tested end to end on a Workers Paid account with a real domain: inbound via Email Routing, connecting the provider, a send through Cloudflare Email Sending that Gmail scored SPF/DKIM/DMARC pass with header.d aligned, and the reply threading into the same conversation. Details in the comment below.

Cloudflare Email Sending sends from the same Cloudflare account that
already handles inbound routing and DNS, so connecting it needs no second
credential: the adapter uses the stored API token and account id.

- providers/cloudflare.ts sends through POST /accounts/{id}/email/sending/send
  and maps Cloudflare's error codes (not entitled, sending disabled, missing
  permission) to readable messages. Message-ID is generated by Cloudflare and
  rejected in headers, so replies thread through References like Resend.
- Cloudflare exposes no API for onboarding a sending domain, so the provider
  has hasDomainApi: false, the same shape SMTP already uses; the UI says to
  enable Email Sending in the dashboard for each domain.
- Adds the optional Email Sending token permission (and its probe), a
  migration widening the provider type CHECK, a mock send endpoint and tests.

Requires the Workers Paid plan on the account, which the UI and README say.
Cloudflare and SMTP cannot verify a domain over an API, so they never get
a verified row in provider_domains. The Domains card read that as "Not
sending" and the health check added a "No sending provider set up"
warning, even though those providers send for any domain.

Both now treat a connected provider without a domain API as sending, and
the health item names it instead of warning.
@LABCAT

LABCAT commented Oct 31, 2026

Copy link
Copy Markdown
Contributor Author

End-to-end test completed on a fresh deploy from this branch, Workers Paid account, real domain on Cloudflare:

  • inbound mail via Email Routing into the OpenInbox inbox: works
  • connected Cloudflare under Integrations → Sending providers (no credentials, the stored token is reused): connected
  • sent from admin@daruma.nz to Gmail through Cloudflare Email Sending: delivered, Gmail reports spf=pass, dkim=pass header.d=daruma.nz, dmarc=pass
  • threading: the reply to that message lands in the same conversation

The test instance was deployed before the second commit (the "Not sending" badge fix), so that one is covered by tests only. The first bare test message was spam-filtered by Gmail, which is the expected reputation story for a brand-new domain; a normal message went through.

@sums001
sums001 merged commit 6d73264 into sums001:main Nov 1, 2026
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.

Cloudflare Email Sending as an outbound provider

2 participants