MailKite
Start free
All posts
Gabe 19 min read

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.

Cloudflare vs MailKite
Cloudflare vs MailKite — the same job (an inbox for your AI agent), two approaches.

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.

Cloudflare Email Routing + Email Workers sender Routingfree forward Email Workerraw MIME postal-mimeyou parse + trust agentyour model reply()verified only …plus your DNS must live on Cloudflare, there's no normalized SPF/DKIM/DMARC verdict object, and reply()/send is DMARC-gated and scoped to verified recipients — all yours to wire ← yours to write and operate MailKite sender MX edgeparse + auth JSON webhooksigned + auth block agentmk.send() the loop below is the whole agent-side integration
One inbound email reaching an agent: the Cloudflare assembly (Routing + Email Worker + postal-mime + constrained reply) vs MailKite's loop (parsed webhook with an auth block in, one send call out).

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:

  • Free inbound forwarding. Receiving mail for a domain on Cloudflare costs nothing on every plan. Cloudflare even writes the MX and SPF records for you when you enable it. If your agent just needs to see mail, that's a hard price to beat.
  • Code at the SMTP edge. An Email Worker runs your code on the message itself, before it's stored anywhere. No polling, no round-trip through a bucket, low latency, and the compute lives next to your other Workers.
  • Reply and send do exist now. Since March 2025 an Email Worker can call message.reply(), and a send_email binding lets it send to verified destination addresses for free. This is not the old "Routing can't send" world.
  • All-in on Cloudflare. If your agent already lives in Workers with KV, D1, Queues, and Workers AI beside it, keeping the inbox in the same runtime is a real architectural win.

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:

email inhits your routing address email() handlermessage.raw = raw MIME stream postal-mime parseheaders, body, attachments derive trustno auth verdict handed to you run agentyour model, Worker CPU budget reply() / send_emailDMARC-gated, verified recipients Every box above the blue one is yours to build. And the reply is constrained: to reach an arbitrary recipient you step up to the separate Email Sending product (beta).
The Cloudflare Email Worker agent pipeline, stage by stage. MailKite collapses the parse and trust boxes into the payload, and the reply into one 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 RoutingMailKite
Inbound to the agentRaw MIME stream in an Email Worker (parse yourself)One parsed JSON webhook
Auth verdictNone — derive SPF/DKIM/DMARC yourselfauth{spf,dkim,dmarc,spam} in the payload
Reply to the senderreply(), DMARC-gated, once, sender-onlymk.send(), threaded via inReplyTo
Send to any recipientSeparate Email Sending product (beta), sending-verified domainIncluded: DNS-verify, then send to anyone
AttachmentsDecode from MIME yourselfSigned URLs in the payload
DNS requirementDomain’s DNS must be on CloudflareAny DNS host; add MX + SPF + DKIM
Managed agent loopNone (build it in the Worker)Optional route action: 'agent' on a queue
Free inboundYes, forwarding is freeFree 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).

Discuss this post: Hacker News Share on X

Related posts