DEV Community

Pinnadash
Pinnadash

Posted on

An inbox your AI agent can read: Cloudflare Email Routing -> Worker -> D1, plus the webmail we added this week

I run a one-person software company where AI agents do most of the work. That has one surprisingly annoying prerequisite: the agents need to receive email.

Signup verification codes, platform notices, payment receipts, customer replies — all of it arrives as email, and most of my automation was blocked on "a human has to open the inbox and copy the code out". IMAP polling from a script works, but it is flaky, stateful, and every agent needs its own credentials.

So I built an internal mail hub. This week it grew a webmail client, and that turned out to be the interesting part, because the whole thing runs on Cloudflare's free tier. Here is the architecture, the decisions that mattered, and the four pitfalls worth stealing.

The shape of it

MX (Cloudflare Email Routing)
   └─> Email Worker            one entry point for every address on the domain
         ├─ classify           verification / platform / spam / human
         └─ D1                 every message persisted = durable queue
                ├─ CLI         the agent-facing interface (drain, wait, cred, triage…)
                └─ daemon      callback per category
Enter fullscreen mode Exit fullscreen mode

Three properties did the heavy lifting:

1. D1 is the queue, not memory. The Worker writes the message, its classification, and its processing state into D1 before doing anything else. If my laptop is off, if the agent crashes mid-run, if I redeploy the Worker — the mail is still there. Every later step (CLI drain, daemon callback, done/ignore marking) is a state transition on a row, not an event that can be missed.

2. Classification at ingest, not at read time. The Worker tags each message as verification, platform, spam, or human before it is queued. That single decision is what makes the CLI usable: an agent asking for a login code never has to read marketing mail, and a spam sweep never touches a genuine customer reply.

3. A CLI is the interface an agent actually wants. Twenty commands, deterministic exit codes:

easy-mail drain              # what arrived since the last session
easy-mail wait --timeout 60  # block until a verification mail lands
easy-mail cred <selector>    # extract the code / signed link from it
easy-mail triage --apply     # apply the rule set to the queue
easy-mail done 12 15 19      # mark processed
Enter fullscreen mode Exit fullscreen mode

A tool call that returns "0 new messages, exit 0" is worth far more to an agent than an API with pagination and a half-page of JSON. Exit codes are the contract: 0 nothing to do / done, 1 needs attention, 2 usage error.

What we added this week: a webmail client on the free tier

Until now the hub was agent-only. I still had to open a mail client to read the human mail. So the hub got a web client — Pages for the SPA, a Worker for the API, D1 for storage. No paid Workers plan, no Access seat.

The access model is deliberately boring and deliberately not single-sign-on:

  • One mailbox = one account + one password. Not one login that can see every address. Each person gets access to exactly their own mailbox.
  • Passwords are PBKDF2-SHA256, 10,000 iterations, per-account salt, never stored in plaintext.
  • The session cookie is base64url(mailbox|expiry) + an HMAC signature — stateless, no session table to babysit.
  • Reading or writing another mailbox returns 404, not 403 — an attacker should not even learn that the row exists.
  • A separate /admin console (Bearer token, not a user session) opens accounts, resets passwords, disables accounts, and reads across mailboxes — with an audit table for every action.
  • The single global webmail password that the first version used was deleted, not deprecated.

I tested isolation the only way that counts: five real mailboxes with real mail (74 / 5 / 46 / 68 / 82 messages), logging in as each and confirming every other mailbox is unreadable, plus a direct URL probe that returns 404 rather than an auth error.

Four pitfalls if you build this yourself

1. Cookies are the cross-site trap. If your SPA is on mail.example.com and your API on api.example.com, your session cookie is third-party from the page's point of view, and browsers will quietly refuse to store it. Serving the API from a route on the same host fixed it in one line of routing instead of a week of SameSite debugging.

2. Turnstile will block your own automation. Once bot protection is on the signup form, your own programmatic self-tests stop working. Keep an authenticated API path for your agents and treat the browser form as a human-only door.

3. Attachments are only filenames unless you decide otherwise. Email Routing gives you the body; persisting attachment bytes means a second hop (back to IMAP, or straight into R2 at ingest). Decide that early — retrofitting "download the attachment" is a schema change plus a backfill.

4. Free tier can send to nobody. Cloudflare Email Routing is inbound. Outbound either needs a paid email-sending product or a transactional provider (Resend's free tier is 100/day). So the honest state of my hub today is: receive everything, send nothing. The UI has no compose button, and I am not going to ship one that pretends to work.

The part that made it usable

Not the database, not the UI — the CLI contract. When the interface is "list what is new, block until a code arrives, extract the code, mark it done", an agent can drive it in four tool calls with no reasoning about mailbox state. Everything else in this system is plumbing around that.

If you are wiring agents into real-world systems, my advice is to make the queue durable first, the classification early, and the interface a CLI with exit codes. The web UI can come last — it did here, and by then I already knew exactly what the UI had to show.


I build small Windows utilities and the automation behind them at pinnadash.com. The mail hub is internal tooling, not a product — the webmail client runs at mail.pinnadash.com, login-only, for our own mailboxes. If you want the "agents receive email" problem discussed from the other side (what breaks, what I would do differently), the comments are open.

Top comments (0)