| name | cloudflare-email-routing |
| description | Cloudflare Email Routing for receiving/sending emails via Workers. Use for email workers, forwarding, allowlists, or encountering Email Trigger errors, worker call failures, SPF issues. |
| license | MIT |
| metadata | {"version":"2.0.0","last_verified":"2025-11-18","production_tested":true,"token_savings":"~60%","errors_prevented":8,"templates_included":0,"references_included":1,"keywords":["Cloudflare Email Routing","Email Workers","send email","receive email","email forwarding","email allowlist","email blocklist","postal-mime","mimetext","cloudflare:email","EmailMessage","ForwardableEmailMessage","EmailEvent","MX records","SPF","DKIM","email worker binding","send_email binding","wrangler email","email handler","email routing worker","\"Email Trigger not available\"","\"failed to call worker\"","email delivery failed","email not forwarding","destination address not verified"]} |
Cloudflare Email Routing
Status: Production Ready ✅ | Last Verified: 2025-11-18
What Is Email Routing?
Two capabilities:
- Email Workers - Receive and process incoming emails (allowlists, forwarding, parsing)
- Send Email - Send emails from Workers to verified addresses
Both free and work together for complete email functionality.
Quick Start (10 Minutes)
Part 1: Enable Email Routing
Dashboard setup:
- Dashboard → Domain → Email → Email Routing
- Enable Email Routing → Add records and enable
- Create destination address:
- Custom:
hello@yourdomain.com
- Destination: Your email
- Verify via email
- ✅ Basic forwarding active
Part 2: Receiving Emails (Email Workers)
Install dependencies:
bun add postal-mime@2.5.0 mimetext@3.0.27
Create email worker:
import { EmailMessage } from 'cloudflare:email';
import PostalMime from 'postal-mime';
export default {
async email(message, env, ctx) {
const parser = new PostalMime.default();
const email = await parser.parse(await new Response(message.raw).arrayBuffer());
console.log('From:', message.from);
console.log('Subject:', email.subject);
await message.forward('you@gmail.com');
}
};
Configure wrangler.jsonc:
{
"name": "email-worker",
"main": "src/email.ts",
"compatibility_date": "2025-10-11",
"compatibility_flags": ["nodejs_compat"]
}
Deploy and connect:
bunx wrangler deploy
Dashboard → Email Workers → Create address → Select worker
Part 3: Sending Emails
Add send email binding:
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-10-11",
"send_email": [
{
"name": "SES",
"destination_address": "user@example.com"
}
]
}
Send from worker:
import { EmailMessage } from 'cloudflare:email';
import { createMimeMessage } from 'mimetext';
const msg = createMimeMessage();
msg.setSender({ name: 'App', addr: 'noreply@yourdomain.com' });
msg.setRecipient('user@example.com');
msg.setSubject('Hello!');
msg.addMessage({
contentType: 'text/plain',
data: 'Email body here'
});
const message = new EmailMessage(
'noreply@yourdomain.com',
'user@example.com',
msg.asRaw()
);
await env.SES.send(message);
Load references/setup-guide.md for complete walkthrough.
Critical Rules
Always Do ✅
- Enable
compatibility_flags: ["nodejs_compat"] for postal-mime (requires compatibility_date >= 2024-09-23)
- Verify destination addresses before sending
- Parse with postal-mime for email content
- Use mimetext for creating emails
- Check message.from for allowlists
- Forward with message.forward() (not manual)
- Handle errors (email delivery can fail)
- Test with real emails (not just dashboard)
- Add MX records (automatic via dashboard)
- Log email activity for debugging
Never Do ❌
- Never skip
nodejs_compat (postal-mime requires the Node.js compat flag)
- Never send without verification (delivery fails)
- Never hardcode email addresses in public code
- Never skip parsing (raw email is hard to work with)
- Never ignore spam (implement allowlists/blocklists)
- Never exceed Gmail limits (500 emails/day to Gmail)
- Never skip error handling (emails can fail)
- Never modify DNS manually (use dashboard)
- Never expose email content in logs (PII)
- Never assume instant delivery (email is async)
Common Patterns
Allowlist
const allowlist = ['approved@domain.com'];
if (!allowlist.includes(message.from)) {
message.setReject('Not on allowlist');
return;
}
await message.forward('you@gmail.com');
Blocklist
const blocklist = ['spam@bad.com'];
if (blocklist.includes(message.from)) {
message.setReject('Blocked');
return;
}
await message.forward('you@gmail.com');
Reply to Email
const msg = createMimeMessage();
msg.setSender({ addr: 'noreply@yourdomain.com' });
msg.setRecipient(message.from);
msg.setSubject(`Re: ${email.subject}`);
msg.addMessage({
contentType: 'text/plain',
data: 'Thanks for your email!'
});
const reply = new EmailMessage(
'noreply@yourdomain.com',
message.from,
msg.asRaw()
);
await env.SES.send(reply);
Parse Attachments
const parser = new PostalMime.default();
const email = await parser.parse(await new Response(message.raw).arrayBuffer());
for (const attachment of email.attachments) {
console.log('Filename:', attachment.filename);
console.log('Type:', attachment.mimeType);
console.log('Size:', attachment.content.byteLength);
}
Custom Routing Logic
async email(message, env, ctx) {
const parser = new PostalMime.default();
const email = await parser.parse(await new Response(message.raw).arrayBuffer());
if (email.subject.includes('[Support]')) {
await message.forward('support@yourdomain.com');
} else if (email.subject.includes('[Sales]')) {
await message.forward('sales@yourdomain.com');
} else {
await message.forward('general@yourdomain.com');
}
}
Email Message Properties
Incoming Messages (ForwardableEmailMessage)
message.from
message.to
message.headers
message.raw
message.rawSize
message.forward(address)
message.setReject(reason)
Parsed Email (PostalMime)
email.from
email.to
email.subject
email.text
email.html
email.attachments
email.headers
Top 5 Errors Prevented
- "Email Trigger not available": Enable
compatibility_flags: ["nodejs_compat"] (with compatibility_date >= 2024-09-23)
- Destination not verified: Verify all send destinations
- Gmail rate limit: Max 500 emails/day to Gmail
- SPF permerror: Use dashboard to configure DNS
- Worker call failed: Check logs for parsing errors
Use Cases
Use Case 1: Support Ticket System
async email(message, env, ctx) {
const parser = new PostalMime.default();
const email = await parser.parse(await new Response(message.raw).arrayBuffer());
await env.DB.prepare(
'INSERT INTO tickets (email, subject, body, created_at) VALUES (?, ?, ?, ?)'
).bind(message.from, email.subject, email.text, Date.now()).run();
const msg = createMimeMessage();
msg.setSender({ addr: 'support@yourdomain.com' });
msg.setRecipient(message.from);
msg.setSubject('Ticket Created');
msg.addMessage({
contentType: 'text/plain',
data: 'Your support ticket has been created.'
});
const confirmation = new EmailMessage(
'support@yourdomain.com',
message.from,
msg.asRaw()
);
env..(confirmation);
}
Use Case 2: Email Notifications
export default {
async fetch(request, env, ctx) {
const { email, name } = await request.json();
const msg = createMimeMessage();
msg.setSender({ name: 'App', addr: 'noreply@yourdomain.com' });
msg.setRecipient(email);
msg.setSubject('Welcome!');
msg.addMessage({
contentType: 'text/html',
data: `<h1>Welcome, ${name}!</h1>`
});
const message = new EmailMessage(
'noreply@yourdomain.com',
email,
msg.asRaw()
);
await env.SES.send(message);
return new Response('Welcome email sent!');
}
};
Use Case 3: Email Forwarding with Filtering
async email(message, env, ctx) {
const parser = new PostalMime.default();
const email = await parser.parse(await new Response(message.raw).arrayBuffer());
const spamKeywords = ['viagra', 'lottery', 'prince'];
const isSpam = spamKeywords.some(keyword =>
email.subject.toLowerCase().includes(keyword) ||
email.text.toLowerCase().includes(keyword)
);
if (isSpam) {
message.setReject('Spam detected');
return;
}
await message.forward('you@gmail.com');
}
When to Load References
Load references/setup-guide.md when:
- First-time Email Routing setup
- Configuring MX records
- Setting up email workers
- Configuring send email binding
- Complete walkthrough needed
Using Bundled Resources
References (references/):
setup-guide.md - Complete setup walkthrough (enabling routing, email workers, send email)
common-errors.md - All 8 documented errors with solutions and prevention
dns-setup.md - MX records, SPF, DKIM configuration guide
local-development.md - Local testing and development patterns
Templates (templates/):
receive-basic.ts - Basic email receiving worker
receive-allowlist.ts - Email allowlist implementation
receive-blocklist.ts - Email blocklist implementation
receive-reply.ts - Auto-reply email worker
send-basic.ts - Basic send email example
send-notification.ts - Notification email pattern
wrangler-email.jsonc - Wrangler configuration for email routing
Official Documentation
Questions? Issues?
- Check
references/setup-guide.md for complete setup
- Verify
compatibility_flags: ["nodejs_compat"] in wrangler.jsonc
- Confirm destination addresses verified
- Check logs for errors