A Cloudflare Worker that gives any AI agent its own email address. Receive, read, and send email — with proper threading, attachment handling, and multi-agent isolation — all on Cloudflare's free tier.
Built for the era of AI agents that need to communicate beyond chat. Your agent gets a real email address like agent@yourdomain.com, can receive messages with attachments, and reply in threaded conversations — just like a human colleague.
Someone emails agent@yourdomain.com
→ Cloudflare Email Routing catches it
→ Worker parses the MIME, stores the email in KV, attachments in R2
→ Optionally forwards a webhook to your agent framework
Your agent calls POST /send
→ Worker builds a MIME message with proper threading headers
→ Delivers via Cloudflare Email Routing to your verified inbox
- Inbound email — parsed with PostalMime, stored in KV, attachments in R2
- Outbound email — build and send via Cloudflare's native
send_emailbinding - Threading —
In-Reply-To/Referencesheaders are automatically stitched so replies land in the right thread in your email client - Attachments — inbound attachments stored in R2, downloadable via API
- Multi-mailbox — one Worker serves many agents; storage is partitioned per mailbox so agents can't see each other's email
- MCP server included — plug into Claude Code or any MCP-compatible client; your agent gets
send_email,list_emails,read_email, andget_attachmenttools - Webhook forwarding — optionally POST parsed emails to any HTTP endpoint (NanoClaw, n8n, Make, custom agents)
┌─────────────────────────────────────────────────────┐
│ Cloudflare Email Worker │
│ │
│ Inbound: agent@yourdomain → parse → KV + R2 │
│ Outbound: POST /send → MIME → Email Routing │
│ Read: GET /emails, /email/:id │
│ Attach: GET /email/:id/attachment/:filename │
└──────────┬──────────────────────┬───────────────────┘
│ webhook (optional) │ HTTP API
│ │
┌─────▼──────┐ ┌────────▼────────────────┐
│ Your agent │ │ MCP Server (included) │
│ framework │ │ Claude Code / any MCP │
└────────────┘ └─────────────────────────┘
- A domain on Cloudflare (free plan works)
- Email Routing enabled on that domain
- A verified destination address (your personal inbox)
- Node.js 18+ and Wrangler (
npm i -g wrangler)
git clone https://github.com/YOUR_USERNAME/cloudflare-email-agent.git
cd cloudflare-email-agent
npm installwrangler kv namespace create THREADS
wrangler kv namespace create EMAILS
wrangler r2 bucket create email-attachmentsEdit wrangler.jsonc:
wrangler secret put SEND_TOKEN # token agents use to call the API
wrangler secret put WEBHOOK_URL # optional: where to forward inbound email
wrangler secret put WEBHOOK_TOKEN # optional: auth for webhookwrangler deployIn the Cloudflare dashboard: Email Routing → Routing Rules → Create address
- Custom address:
agent@yourdomain.com - Action: Send to Worker →
cloudflare-email-agent
# Send an email from your agent
curl -X POST https://cloudflare-email-agent.<you>.workers.dev/send \
-H "Authorization: Bearer $SEND_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"mailbox": "agent@yourdomain.com",
"subject": "Hello from my agent",
"text": "The mailbox is working."
}'
# Check your inbox, then reply to the email.
# After a moment, list inbound emails:
curl https://cloudflare-email-agent.<you>.workers.dev/emails?mailbox=agent@yourdomain.com \
-H "Authorization: Bearer $SEND_TOKEN"All endpoints except /health require Authorization: Bearer $SEND_TOKEN.
All read endpoints require ?mailbox=agent@yourdomain.com (or X-Mailbox header).
| Method | Path | Description |
|---|---|---|
GET |
/health |
Health check (no auth) |
GET |
/emails |
List recent emails (paginated: ?limit=20&offset=0) |
GET |
/email/:id |
Read full email by Message-ID |
GET |
/email/:id/attachments |
List attachments for an email |
GET |
/email/:id/attachment/:filename |
Download an attachment |
POST |
/send |
Send an email |
{
"mailbox": "agent@yourdomain.com",
"subject": "Required",
"text": "Plain text body",
"html": "<p>HTML body</p>",
"from_name": "Agent Name",
"in_reply_to": "<message-id@domain>"
}mailboxandsubjectare required- At least one of
textorhtmlis required in_reply_tois optional; when set, the email threads correctly in the recipient's mail client
The mcp/ directory contains an MCP server that wraps the Worker API. It exposes four tools: send_email, list_emails, read_email, and get_attachment.
cd mcp && npm install && npx tscAdd to ~/.claude/settings.json (global) or your project's .mcp.json:
{
"mcpServers": {
"cloudflare-email": {
"command": "node",
"args": ["/path/to/cloudflare-email-agent/mcp/dist/index.js"],
"env": {
"WORKER_URL": "https://cloudflare-email-agent.<you>.workers.dev",
"SEND_TOKEN": "your-send-token",
"MAILBOX": "agent@yourdomain.com"
}
}
}
}Now in any Claude Code session:
"Check my email" "Send Shaheed a summary of today's changes" "Download the spreadsheet from that last email"
Each agent gets its own MCP instance with a different MAILBOX. Add a route per address in Cloudflare Email Routing, then add another entry in your MCP config:
{
"mcpServers": {
"email-agent-1": {
"command": "node",
"args": ["/path/to/mcp/dist/index.js"],
"env": {
"WORKER_URL": "https://cloudflare-email-agent.<you>.workers.dev",
"SEND_TOKEN": "same-token",
"MAILBOX": "agent-1@yourdomain.com"
}
},
"email-agent-2": {
"command": "node",
"args": ["/path/to/mcp/dist/index.js"],
"env": {
"WORKER_URL": "https://cloudflare-email-agent.<you>.workers.dev",
"SEND_TOKEN": "same-token",
"MAILBOX": "agent-2@yourdomain.com"
}
}
}
}Agents are isolated — agent-1 cannot read agent-2's email.
Storage is partitioned by mailbox address:
- KV keys:
agent@domain:__index__,agent@domain:<message-id> - R2 keys:
agent@domain/<message-id>/<filename> - Thread cache:
agent@domain:<message-id>
One Worker, one KV namespace, one R2 bucket — many isolated mailboxes.
- Outbound recipients must be verified in Cloudflare Email Routing. This is by design for personal agent setups. For sending to arbitrary external addresses, swap
send_emailfor Resend, Postmark, or AWS SES. - Attachment bytes are not included in webhook payloads — only metadata. Retrieve them via the
/attachmentendpoint or add inline forwarding in theemail()handler. - KV has eventual consistency — there's a small window after receiving an email where it may not appear in
list_emails. In practice this is under a second. - Email storage expires after 90 days (configurable via
EMAIL_TTL_SECONDS).
cp .dev.vars.example .dev.vars # fill in test values
wrangler dev # local HTTP server on :8787wrangler dev does not simulate inbound email. Test inbound by deploying and sending real mail.
Everything fits within Cloudflare's free tier:
| Resource | Free tier | Typical agent usage |
|---|---|---|
| Workers | 100,000 req/day | Well under |
| KV | 100,000 reads/day, 1,000 writes/day | Well under |
| R2 | 10GB storage, 10M reads/month | Well under |
| Email Routing | Unlimited | - |
MIT
{ "send_email": [{ "name": "SEND_EMAIL", "destination_address": "you@gmail.com" // your verified inbox }], "kv_namespaces": [ { "binding": "THREADS", "id": "<from step 2>" }, { "binding": "EMAILS", "id": "<from step 2>" } ], "r2_buckets": [{ "bucket_name": "email-attachments", "binding": "ATTACHMENTS" }], "vars": { "AGENT_ADDRESS": "you@gmail.com", // same as destination "FROM_ADDRESS": "agent@yourdomain.com", // on your CF domain "FROM_NAME": "My Agent" } }