Back to Skills
Cloudflare Email Routing
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.
cloudflareai
By secondsky
Skill Content
# 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: `<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
```typescript
async 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 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
- **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?**
1. Check `references/setup-guide.md` for complete setup
2. Verify node_compat: true in wrangler.jsonc
3. Confirm destination addresses verified
4. Check logs for errorsHow to use
- Copy the skill content above
- Create a .claude/skills directory in your project
- Save as .claude/skills/claude-skills-cloudflare-email-routing.md
- Use /claude-skills-cloudflare-email-routing in Claude Code to invoke this skill