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
- Custom:
- ✅ Basic forwarding active
Part 2: Receiving Emails (Email Workers)
Install dependencies:
bashbun add postal-mime@2.5.0 mimetext@3.0.27
Create email worker:
typescript// src/email.ts 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); // Forward to destination await message.forward('you@gmail.com'); } };
Configure wrangler.jsonc:
jsonc{ "name": "email-worker", "main": "src/email.ts", "compatibility_date": "2025-10-11", // must be >= 2024-09-23 for nodejs_compat "compatibility_flags": ["nodejs_compat"] // Required! postal-mime needs Node.js compat }
Deploy and connect:
bashbunx wrangler deploy
Dashboard → Email Workers → Create address → Select worker
Part 3: Sending Emails
Add send email binding:
jsonc{ "name": "my-worker", "main": "src/index.ts", "compatibility_date": "2025-10-11", "send_email": [ { "name": "SES", "destination_address": "user@example.com" } ] }
Send from worker:
typescriptimport { 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 (requirescompatibility_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
typescriptconst allowlist = ['approved@domain.com']; if (!allowlist.includes(message.from)) { message.setReject('Not on allowlist'); return; } await message.forward('you@gmail.com');
Blocklist
typescriptconst blocklist = ['spam@bad.com']; if (blocklist.includes(message.from)) { message.setReject('Blocked'); return; } await message.forward('you@gmail.com');
Reply to Email
typescriptconst 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
typescriptconst 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
typescriptasync email(message, env, ctx) { const parser = new PostalMime.default(); const email = await parser.parse(await new Response(message.raw).arrayBuffer()); // Route based on subject 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)
typescriptmessage.from // Sender email message.to // Recipient email message.headers // Email headers message.raw // Raw email stream message.rawSize // Size in bytes // Methods message.forward(address) // Forward to address message.setReject(reason) // Reject email
Parsed Email (PostalMime)
typescriptemail.from // { name, address } email.to // [{ name, address }] email.subject // Subject line email.text // Plain text body email.html // HTML body email.attachments // Array of attachments email.headers // All headers
Top 5 Errors Prevented
- "Email Trigger not available": Enable
compatibility_flags: ["nodejs_compat"](withcompatibility_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
typescriptasync email(message, env, ctx) { const parser = new PostalMime.default(); const email = await parser.parse(await new Response(message.raw).arrayBuffer()); // Create ticket in database await env.DB.prepare( 'INSERT INTO tickets (email, subject, body, created_at) VALUES (?, ?, ?, ?)' ).bind(message.from, email.subject, email.text, Date.now()).run(); // Send confirmation 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() ); await env.SES.send(confirmation); }
Use Case 2: Email Notifications
typescriptexport default { async fetch(request, env, ctx) { // User signup 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
typescriptasync email(message, env, ctx) { const parser = new PostalMime.default(); const email = await parser.parse(await new Response(message.raw).arrayBuffer()); // Filter spam keywords 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 preventiondns-setup.md- MX records, SPF, DKIM configuration guidelocal-development.md- Local testing and development patterns
Templates (templates/):
receive-basic.ts- Basic email receiving workerreceive-allowlist.ts- Email allowlist implementationreceive-blocklist.ts- Email blocklist implementationreceive-reply.ts- Auto-reply email workersend-basic.ts- Basic send email examplesend-notification.ts- Notification email patternwrangler-email.jsonc- Wrangler configuration for email routing
Official Documentation
- Email Routing: https://developers.cloudflare.com/email-routing/
- Email Workers: https://developers.cloudflare.com/email-routing/email-workers/
- Send Email: https://developers.cloudflare.com/email-routing/email-workers/send-email-workers/
Questions? Issues?
- Check
references/setup-guide.mdfor complete setup - Verify
compatibility_flags: ["nodejs_compat"]in wrangler.jsonc - Confirm destination addresses verified
- Check logs for errors

