Read this first
Three properties of this channel differ from the IMAP and Gmail adapters, and all three are easier to learn here than in production.
There is no backfill
The channel sees mail from the moment its Email Routing rule goes live, forward. There is no history import, and fetchHistory / importHistory are deliberately not implemented — Cloudflare delivers on receipt and there is no mailbox to page backwards through. POST /channels/{id}/import-history returns a clean "feature not supported".
Consequence: if you intend to accumulate a corpus, create the routing rule as early as possible. Every week of delay is a week of mail that cannot be recovered later.
Inbound is 25 MiB; outbound is 5 MiB
These are different limits and conflating them is easy. Cloudflare rejects inbound messages over 25 MiB at the edge, before the Worker runs — unrecoverable, no retry. Outbound total message size (body plus encoded attachments) is 5 MiB; the 25 MiB figure applies to outbound only when sending to verified destination addresses.
The adapter enforces the outbound ceiling before calling the API, so an oversize send fails with a message naming the file rather than a bare 400.
It is the first email channel here with working outbound attachments
channel_imap returns a failed result when handed attachments; channel_gmail does not stitch attachment URLs into its MIME body. This adapter fetches, validates and base64-encodes them, so capabilities.fileSharing is true.
That flag is a persisted snapshot — it is written into the channel row at connect time — so it ships true from 0.1.0 rather than being flipped later, which would strand already-connected channels with a stale value.
Architecture
sender ─→ your-address@your-domain.com
│
Cloudflare Email Routing
↓
Email Worker (worker/)
├─ buffers message.raw ONCE, parses with postal-mime
├─ raw MIME + attachment blobs ──→ R2
└─ HMAC-signed envelope ──→ POST
↓
/api/communication_channels/webhook/cloudflare_email (Open Mercato core)
↓
adapter.verifyWebhook() ← HMAC, fail-closed
adapter.normalizeInbound()
↓
core: dedup → thread match → contact resolution → Message
MessageChannelLink.channelPayload = the full envelope
↓
event: communication_channels.message.received
Outbound is the mirror: convertOutbound fetches and encodes attachments, sendMessage posts to the Email Sending REST API, and a Queues event subscription feeds delivery status back through the Worker to this module's own endpoint.
Installation
yarn mercato module add @northbound-run/channel-cloudflare-email --allow-third-party
yarn generate
--allow-third-party is required because the package is outside the @open-mercato/* scope. The module ships no entities and no migrations, so there is nothing to migrate.
Requires communication_channels and integrations to be enabled — both are declared in requires, so yarn generate fails with a clear message naming the missing module rather than dying at runtime.
To take local ownership of the source:
yarn mercato module add @northbound-run/channel-cloudflare-email --allow-third-party --eject
Cloudflare setup
Deploy the Worker before connecting the channel — the connect form asks for values the Worker deployment produces.