--- 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. 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 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 --- # Cloudflare Email Routing **Status**: Production Ready ✅ | **Last Verified**: 2025-11-18 --- ## What Is Email Routing? Two capabilities: 1. **Email Workers** - Receive and process incoming emails (allowlists, forwarding, parsing) 2. **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:** 1. Dashboard → Domain → **Email** → **Email Routing** 2. **Enable Email Routing** → **Add records and enable** 3. Create destination address: - Custom: `hello@yourdomain.com` - Destination: Your email - Verify via email 4. ✅ Basic forwarding active ### Part 2: Receiving Emails (Email Workers) **Install dependencies:** ```bash bun 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", "node_compat": true // Required! } ``` **Deploy and connect:** ```bash bunx 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:** ```typescript 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 ✅ 1. **Enable node_compat: true** for postal-mime 2. **Verify destination addresses** before sending 3. **Parse with postal-mime** for email content 4. **Use mimetext** for creating emails 5. **Check message.from** for allowlists 6. **Forward with message.forward()** (not manual) 7. **Handle errors** (email delivery can fail) 8. **Test with real emails** (not just dashboard) 9. **Add MX records** (automatic via dashboard) 10. **Log email activity** for debugging ### Never Do ❌ 1. **Never skip node_compat** (postal-mime requires it) 2. **Never send without verification** (delivery fails) 3. **Never hardcode email addresses** in public code 4. **Never skip parsing** (raw email is hard to work with) 5. **Never ignore spam** (implement allowlists/blocklists) 6. **Never exceed Gmail limits** (500 emails/day to Gmail) 7. **Never skip error handling** (emails can fail) 8. **Never modify DNS manually** (use dashboard) 9. **Never expose email content** in logs (PII) 10. **Never assume instant delivery** (email is async) --- ## Common Patterns ### Allowlist ```typescript const allowlist = ['approved@domain.com']; if (!allowlist.includes(message.from)) { message.setReject('Not on allowlist'); return; } await message.forward('you@gmail.com'); ``` ### Blocklist ```typescript const blocklist = ['spam@bad.com']; if (blocklist.includes(message.from)) { message.setReject('Blocked'); return; } await message.forward('you@gmail.com'); ``` ### Reply to Email ```typescript 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 ```typescript 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 ```typescript async 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) ```typescript message.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) ```typescript email.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 1. **"Email Trigger not available"**: Enable node_compat: true 2. **Destination not verified**: Verify all send destinations 3. **Gmail rate limit**: Max 500 emails/day to Gmail 4. **SPF permerror**: Use dashboard to configure DNS 5. **Worker call failed**: Check logs for parsing errors --- ## Use Cases ### Use Case 1: Support Ticket System ```typescript async 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 ```typescript export 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: `