The Cloudflare Email Routing alternative for AI agents
Cloudflare Email Routing is free inbound forwarding and Email Workers hand your agent the raw MIME stream: great plumbing if you're all-in on Cloudflare and happy to parse MIME, derive trust, and wire the reply constraints yourself. MailKite (which we build) hands an agent parsed JSON with an auth verdict and a receive→reply loop in a few lines. For anyone giving an autonomous agent its own inbox.
Here’s that split in one picture: the same inbound email landing at an autonomous agent, and everything each side makes you operate between the SMTP edge and the model loop. The rest of this post is the honest version of the diagram, where Email Routing genuinely wins for an agent, the real Cloudflare-native code, and the handful of lines that are the whole MailKite side.
Here’s the whole receive → think → reply loop on MailKite. It runs as pasted on Node 18+ (npm install express mailkite), and it’s the entire agent-side integration:
import express from "express";
import { MailKite } from "mailkite";
const app = express();
const mk = new MailKite(process.env.MAILKITE_API_KEY);
const SECRET = process.env.MAILKITE_WEBHOOK_SECRET;
app.post("/hooks/agent", express.raw({ type: "application/json" }), async (req, res) => {
if (!MailKite.verifyWebhook(req.headers["x-mailkite-signature"], req.body, SECRET)) {
return res.sendStatus(401); // signature + replay window, one call
}
res.sendStatus(200); // ack fast; run the agent out of band
const event = JSON.parse(req.body);
if (event.type !== "email.received") return;
// The body is untrusted INPUT, never instructions. Weight it by the auth verdict.
const answer = await runAgent({
task: event.text,
from: event.from.address,
trusted: event.auth.spf === "pass" && event.auth.dmarc === "pass",
});
await mk.send({
from: event.to[0].address, // reply from the address it was sent to
to: event.from.address, // to anyone, not just verified addresses
subject: `Re: ${event.subject}`,
inReplyTo: event.id, // threads the reply
html: answer.html,
});
});
app.listen(3000);
That’s a fully autonomous email agent: it hears, it thinks, it answers. The same handler shape exists for Python, Ruby, Go, PHP, and Java; see the receiving docs and sending docs. The companion repo is demo-cloudflare-email-routing-ai-agent, and you can open it in StackBlitz (real Node in a browser tab) to fire the exact signed payload our delivery worker sends and watch a parsed email land.
Where Cloudflare wins for agents, honestly
Email Routing is a genuinely good inbound primitive, and free at the point where most alternatives start metering:
If you want to own the whole edge and you’re happy writing the parsing, trust, and reply plumbing yourself, Cloudflare is a legitimate choice. We build MailKite on Cloudflare, for exactly these reasons. The rest of this post is about the plumbing between “an Email Worker fired” and “my agent answered safely.”
What Cloudflare asks of an agent builder
The email() handler hands you message.raw: a ReadableStream of raw MIME. There is no parsed body, and no auth verdict object. You run a MIME parser (Cloudflare documents postal-mime), you derive trust yourself, and you construct the reply by hand under a set of constraints. Here’s the honest version, top to bottom:
mk.send().Here’s the Cloudflare version of the loop up top: a real email() handler that parses the raw MIME and replies (cloudflare-worker/index.mjs in the demo repo).
// Cloudflare Email Worker: raw MIME in, you parse, you build the reply by hand.
import PostalMime from "postal-mime";
import { EmailMessage } from "cloudflare:email";
import { createMimeMessage } from "mimetext";
export default {
async email(message, env, ctx) {
// No parsed fields, no auth verdict — message.raw is a ReadableStream of MIME.
const email = await PostalMime.parse(message.raw);
const from = message.from; // envelope sender, trivially spoofable
const text = email.text ?? ""; // you decoded this yourself
// There's no auth block: derive trust from the headers/DMARC before acting.
const answer = await runAgent({ task: text, from });
// reply() is constrained: DMARC must pass, once per message, and the
// recipient must equal the original sender. To email anyone else you'd
// move to the separate Email Sending product (beta).
const msg = createMimeMessage();
msg.setHeader("In-Reply-To", message.headers.get("Message-ID"));
msg.setSender({ addr: message.to });
msg.setRecipient(message.from);
msg.setSubject("Re: " + message.headers.get("Subject"));
msg.addMessage({ contentType: "text/html", data: answer.html });
await message.reply(new EmailMessage(message.to, message.from, msg.asRaw()));
},
};
None of this is exotic if you already live in Workers. But four things are on you that a purpose-built inbound platform hands you decoded: the MIME parse, the trust decision (there’s no auth object, so an agent following a spoofed From: is your problem to prevent), the by-hand reply construction, and the recipient constraint. Email Routing’s send_email binding only reaches verified destination addresses; free-form “send to any recipient” is the newer Email Sending product, in public beta as of April 2026, which needs a sending-verified domain. Cloudflare’s Agents SDK adds onEmail() and replyToEmail() helpers on top, but they don’t parse the MIME or hand you an auth verdict either. You still build the loop.
The comparison, agent-relevant rows
| Cloudflare Email Routing | MailKite | |
|---|---|---|
| Inbound to the agent | Raw MIME stream in an Email Worker (parse yourself) | One parsed JSON webhook |
| Auth verdict | None — derive SPF/DKIM/DMARC yourself | auth{spf,dkim,dmarc,spam} in the payload |
| Reply to the sender | reply(), DMARC-gated, once, sender-only | mk.send(), threaded via inReplyTo |
| Send to any recipient | Separate Email Sending product (beta), sending-verified domain | Included: DNS-verify, then send to anyone |
| Attachments | Decode from MIME yourself | Signed URLs in the payload |
| DNS requirement | Domain’s DNS must be on Cloudflare | Any DNS host; add MX + SPF + DKIM |
| Managed agent loop | None (build it in the Worker) | Optional route action: 'agent' on a queue |
| Free inbound | Yes, forwarding is free | Free tier: 3,000 msgs/mo, in + out |
The through-line: Cloudflare gives you a free, low-latency inbound primitive and the raw material to do anything with it. MailKite gives an agent the message already decoded, already authenticated, with a reply path that isn’t scoped to verified recipients.
What actually hits your agent’s webhook
The same inbound email, delivered parsed. No message.raw stream, no postal-mime, and the auth block means the agent never re-derives SPF/DKIM/DMARC to decide whether to trust a sender:
{
"id": "msg_2Hk9…",
"type": "email.received",
"from": { "address": "ada@example.com" },
"to": [{ "address": "agent@myapp.ai" }],
"subject": "Re: invoice #1042",
"text": "Looks good — approved!",
"html": "<p>Looks good — approved!</p>",
"threadId": "<a1b2c3@mail.example.com>",
"auth": { "spf": "pass", "dkim": "pass", "dmarc": "pass", "spam": "ham" },
"attachments": [
{ "id": "msg_2Hk9…:0", "filename": "po.pdf", "contentType": "application/pdf",
"size": 18213, "url": "https://api.mailkite.dev/att/2Hk9…/0?exp=…&sig=…" }
]
}
Treat the body as untrusted input, always. From: is plain text and trivially forged, so an email body is a prompt-injection vector the moment an agent follows it. The auth block is there so the agent can check whether SPF and DKIM passed before it weights a sender’s instructions; that check is necessary, not sufficient. Bound the agent’s authority regardless. See webhook security and the agent security post.
One MailKite arc
MailKite, which we build, is the layer we put on top of the exact Cloudflare primitives above: an MX edge that parses and authenticates, a signed and retried JSON webhook with the auth verdict, and a Send API that replies to anyone with inReplyTo threading. If you’d rather not host the model loop at all, point a route’s action at agent with a free-text agentPrompt, and MailKite runs the turns for you on a Cloudflare Queue with a per-run transcript, capped and reaped so a slow model call never wedges the pipeline. Bring your own loop, or hand it over; same parsed inbound edge either way. Details in the receiving docs and quickstart.
FAQ
Can Cloudflare Email Routing reply to emails?
Yes, as of March 2025. An Email Worker can call message.reply(), and a send_email binding can send to verified destination addresses for free. The reply is constrained: the incoming message must pass DMARC, a message can be replied to only once, and the recipient must match the original sender. To email an arbitrary recipient you move to Cloudflare’s separate Email Sending product (public beta, April 2026), which needs a sending-verified domain. MailKite replies to anyone with one mk.send() call once your domain passes SPF + DKIM.
Can an AI agent read email with Cloudflare Email Workers?
Yes, but you do the decoding. The email() handler gives you message.raw, a raw MIME stream; you run a parser (Cloudflare documents postal-mime) to get subject, body, and attachments. There’s no normalized JSON and no SPF/DKIM/DMARC verdict object. MailKite delivers the message already parsed to JSON with an auth block, so the agent reads plain fields.
Does Cloudflare Email Routing require my DNS on Cloudflare? Yes. Email Routing needs the domain’s DNS managed by Cloudflare (nameservers pointed at Cloudflare); it then auto-adds the MX and SPF records. MailKite works with any DNS host: you add MX to receive and SPF + DKIM to send, wherever your DNS lives.
Can Cloudflare send email to any address for an agent?
Not through Email Routing alone. The routing send_email binding is scoped to verified destination addresses, and reply() only answers the original sender. Sending to arbitrary recipients requires the newer Email Sending product (beta) with a sending-verified domain. On MailKite, DNS-verify the domain and you can send to anyone, no verified-recipient list.
How is MailKite different from Cloudflare Email Workers for an agent?
Cloudflare hands your Worker the raw MIME and the reply constraints; you build the parse, the trust decision, and the reply plumbing. MailKite hands the agent parsed JSON with an auth verdict, a resolved threadId, attachments as signed URLs, and a send() that replies to anyone, plus an optional action: 'agent' route that runs the loop for you. It’s the assembled version of the Cloudflare stack.
If Cloudflare has your agent parsing raw MIME in a Worker and juggling reply constraints just to answer one email, there’s a simpler shape. Clone the demo repo (or run it in your browser), then point a domain at MailKite and your agent’s next inbound email arrives as parsed JSON with an auth verdict attached.
Related: the pillar on giving your AI agent its own inbox, agent inbox security by design, the full MailKite vs Cloudflare Email Routing comparison, and why Cloudflare Email Routing can’t reply (and how to send from your domain).