xiaohongshu
xiaohongshu
cloudflare-email-routing
Complete guide for Cloudflare Email Routing covering both Email Workers (receiving emails) and Send Email bindings (sending emails from Workers). Use when: setting up email routing, creating email workers, processing incoming emails, sending emails from Workers, implementing email allowlists/blo...
Full skill instructions
Status: Production Ready ✅ Last Updated: 2025-10-23 Latest Versions: [email protected], [email protected]
Cloudflare Email Routing provides two complementary capabilities:
Both capabilities are free and work together to enable complete email functionality in Cloudflare Workers.
Prerequisites: Domain must be on Cloudflare DNS
[email protected][email protected])What you just did: Configured DNS and basic forwarding. Now let's add Workers for custom logic.
npm install [email protected] [email protected]
Why these packages:
postal-mime - Parse incoming email messages (headers, body, attachments)mimetext - Create email messages for sending/replyingCreate src/email.ts:
import { EmailMessage } from 'cloudflare:email';
import PostalMime from 'postal-mime';
export default {
async email(message, env, ctx) {
// Parse the incoming message
const parser = new PostalMime.default();
const email = await parser.parse(await new Response(message.raw).arrayBuffer());
console.log('From:', message.from);
console.log('To:', message.to);
console.log('Subject:', email.subject);
// Forward to verified destination
await message.forward('[email protected]');
},
};
Update wrangler.jsonc:
{
"name": "email-worker",
"main": "src/email.ts",
"compatibility_date": "2025-10-11"
}
npx wrangler deploy
# In Cloudflare Dashboard:
# Email > Email Routing > Email Workers
# Select your worker → Create route → Enter address (e.g., [email protected])
What you just did: Created a Worker that logs and forwards emails.
Update wrangler.jsonc:
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-10-11",
"send_email": [
{
"name": "EMAIL",
"destination_address": "[email protected]"
}
]
}
CRITICAL: destination_address must be:
import { EmailMessage } from 'cloudflare:email';
import { createMimeMessage } from 'mimetext';
export default {
async fetch(request, env, ctx) {
// Create email message
const msg = createMimeMessage();
msg.setSender({ name: 'My App', addr: '[email protected]' });
msg.setRecipient('[email protected]');
msg.setSubject('Welcome to My App');
msg.addMessage({
contentType: 'text/plain',
data: 'Thank you for signing up!',
});
// Send via binding
const message = new EmailMessage(
'[email protected]',
'[email protected]',
msg.asRaw()
);
await env.EMAIL.send(message);
return new Response('Email sent!');
},
};
npx wrangler deploy
What you just did: Configured your Worker to send emails to verified addresses.
export default {
async email(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext) {
// Process email here
},
};
Parameters:
message - ForwardableEmailMessage objectenv - Environment bindings (KV, D1, secrets, etc.)ctx - Execution context (waitUntil for async operations)interface ForwardableEmailMessage {
readonly from: string; // Sender email
readonly to: string; // Recipient email
readonly headers: Headers; // Email headers
readonly raw: ReadableStream; // Raw email message
readonly rawSize: number; // Size in bytes
// Methods
setReject(reason: string): void;
forward(rcptTo: string, headers?: Headers): Promise<void>;
reply(message: EmailMessage): Promise<void>;
}
Only accept emails from approved senders:
export default {
async email(message, env, ctx) {
const allowList = [
'[email protected]',
'[email protected]',
'[email protected]',
];
if (!allowList.includes(message.from)) {
message.setReject('Address not on allowlist');
return;
}
await message.forward('[email protected]');
},
};
When to use: Contact forms, private email addresses, team inboxes
Reject emails from specific senders or domains:
export default {
async email(message, env, ctx) {
const blockList = [
'[email protected]',
'@suspicious-domain.com', // Block entire domain
];
const isBlocked = blockList.some(pattern =>
message.from.includes(pattern)
);
if (isBlocked) {
message.setReject('Sender blocked');
return;
}
await message.forward('[email protected]');
},
};
When to use: Spam filtering, blocking harassers, domain-level blocks
Extract email content and store in D1 or KV:
import PostalMime from 'postal-mime';
export default {
async email(message, env, ctx) {
// Parse email
const parser = new PostalMime.default();
const rawEmail = new Response(message.raw);
const email = await parser.parse(await rawEmail.arrayBuffer());
// Store in D1
await env.DB.prepare(
'INSERT INTO emails (from_addr, subject, text, received_at) VALUES (?, ?, ?, ?)'
).bind(
message.from,
email.subject,
email.text,
new Date().toISOString()
).run();
// Forward to inbox
await message.forward('[email protected]');
},
};
When to use: Email archiving, ticket systems, support inboxes, audit logs
Send automatic replies with custom logic:
import PostalMime from 'postal-mime';
import { createMimeMessage } from 'mimetext';
import { EmailMessage } from 'cloudflare:email';
export default {
async email(message, env, ctx) {
// Parse incoming email
const parser = new PostalMime.default();
const email = await parser.parse(await new Response(message.raw).arrayBuffer());
// Create reply
const msg = createMimeMessage();
msg.setSender({ name: 'Support Team', addr: '[email protected]' });
msg.setRecipient(message.from);
msg.setHeader('In-Reply-To', message.headers.get('Message-ID'));
msg.setSubject(`Re: ${email.subject}`);
msg.addMessage({
contentType: 'text/plain',
data: `Thank you for your message about "${email.subject}". We'll respond within 24 hours.`,
});
// Send reply
const replyMessage = new EmailMessage(
'[email protected]',
message.from,
msg.asRaw()
);
await message.reply(replyMessage);
// Also forward to team inbox
await message.forward('[email protected]');
},
};
When to use: Out-of-office replies, support ticket acknowledgments, automated responses
Route emails to different destinations based on content:
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());
const subject = email.subject.toLowerCase();
// Route based on subject keywords
if (subject.includes('urgent') || subject.includes('critical')) {
await message.forward('[email protected]');
} else if (subject.includes('invoice') || subject.includes('payment')) {
await message.forward('[email protected]');
} else if (subject.includes('support') || subject.includes('help')) {
await message.forward('[email protected]');
} else {
await message.forward('[email protected]');
}
},
};
When to use: Department routing, priority filtering, category-based inboxes
{
"send_email": [
{
"name": "EMAIL",
"destination_address": "[email protected]"
}
]
}
Behavior: All emails sent via env.EMAIL go to this address.
{
"send_email": [
{
"name": "EMAIL",
"allowed_destination_addresses": [
"[email protected]",
"[email protected]",
"[email protected]"
]
}
]
}
Behavior: Can send to any address in the list.
{
"send_email": [
{
"name": "NOTIFICATIONS",
"destination_address": "[email protected]"
},
{
"name": "ALERTS",
"destination_address": "[email protected]"
}
]
}
Behavior: Use different bindings for different purposes.
import { EmailMessage } from 'cloudflare:email';
import { createMimeMessage } from 'mimetext';
const msg = createMimeMessage();
msg.setSender({ name: 'My App', addr: '[email protected]' });
msg.setRecipient('[email protected]');
msg.setSubject('Welcome!');
msg.addMessage({
contentType: 'text/plain',
data: 'Welcome to our service!',
});
const email = new EmailMessage(
'[email protected]',
'[email protected]',
msg.asRaw()
);
await env.EMAIL.send(email);
import { EmailMessage } from 'cloudflare:email';
import { createMimeMessage } from 'mimetext';
const msg = createMimeMessage();
msg.setSender({ name: 'My App', addr: '[email protected]' });
msg.setRecipient('[email protected]');
msg.setSubject('Welcome!');
// Add both plain text and HTML versions
msg.addMessage({
contentType: 'text/plain',
data: 'Welcome to our service!',
});
msg.addMessage({
contentType: 'text/html',
data: '<h1>Welcome!</h1><p>Thanks for joining us.</p>',
});
const email = new EmailMessage(
'[email protected]',
'[email protected]',
msg.asRaw()
);
await env.EMAIL.send(email);
import { EmailMessage } from 'cloudflare:email';
import { createMimeMessage } from 'mimetext';
const msg = createMimeMessage();
msg.setSender({ name: 'My App', addr: '[email protected]' });
msg.setRecipient('[email protected]');
msg.setSubject('Password Reset');
// Add custom headers
msg.setHeader('X-Priority', '1');
msg.setHeader('X-Application-ID', 'my-app-123');
msg.addMessage({
contentType: 'text/plain',
data: 'Click here to reset your password...',
});
const email = new EmailMessage(
'[email protected]',
'[email protected]',
msg.asRaw()
);
await env.EMAIL.send(email);
When you enable Email Routing in the dashboard, Cloudflare automatically adds:
MX Records - Direct email to Cloudflare's servers
yourdomain.com. 300 IN MX 13 amir.mx.cloudflare.net.
yourdomain.com. 300 IN MX 86 linda.mx.cloudflare.net.
yourdomain.com. 300 IN MX 24 isaac.mx.cloudflare.net.
SPF Record - Authorize Cloudflare to send on your behalf
yourdomain.com. 300 IN TXT "v=spf1 include:_spf.mx.cloudflare.net ~all"
DKIM Records - Sign outgoing emails
Automatically configured per domain
If you need to migrate from another provider:
WARNING: Changing MX records will break Email Routing. Only do this if migrating providers.
This skill prevents 8 documented issues:
Error: Testing email workers fails with "Email Trigger not available to this workers"
Source: workers-sdk #3751
Why It Happens: Wrangler dev doesn't fully support email triggers; testing must be done via deployed Workers
Prevention:
wrangler tail for live debuggingError: Verified destination addresses show as "unverified" in Email Worker forwarding
Source: Community reports (Cloudflare Community)
Why It Happens: Bug in dashboard where addresses only show verified if also used in regular routing rules
Prevention:
Error: "421: Our system has detected an unusual rate of unsolicited mail originating from your IP address"
Source: Community reports
Why It Happens: Gmail may flag Cloudflare's IP ranges as suspicious due to shared infrastructure
Prevention:
Error: SPF permerror when routing through MailChannels
Source: Community discussion
Why It Happens: SPF record chain breaks when forwarding through multiple services
Prevention:
Error: Cannot see worker logs or email processing details
Source: Community reports
Why It Happens: Free plan has limited log retention and streaming
Prevention:
wrangler tail during development for live logsconsole.log() extensively in email workersError: Emails show as "Dropped" in Activity Log even when successfully forwarded
Source: Community reports
Why It Happens: Dashboard bug showing incorrect status
Prevention:
wrangler tail to verify processingError: Dashboard "Test Email Event" button remains in loading state forever
Source: workers-sdk #9195
Why It Happens: Bug in dashboard testing interface (unresolved as of 2025-10)
Prevention:
curl with local development instead (see Local Development section)wrangler tail to monitor processingError: "Rejected reason: Unknown error: failed to call worker: Worker call failed for 3 times, aborting…"
Source: workers-sdk #9069, Community reports
Why It Happens: Worker crashes due to runtime errors, timeouts, or memory issues
Prevention:
ctx.waitUntil() for non-critical operationsWrangler simulates email reception via HTTP POST:
# Start dev server
npx wrangler dev
# In another terminal, send test email
curl http://localhost:8787 -X POST \
--data-binary @- << EOF
From: [email protected]
To: [email protected]
Subject: Test Email
This is a test email body.
EOF
What happens: Wrangler logs the email processing and shows where forwarded emails would go.
Wrangler writes sent emails to local .eml files:
// Your worker code
await env.EMAIL.send(message);
Output in terminal:
[wrangler:inf] send_email binding called with the following message:
/tmp/miniflare-abc123/files/email/message-123.eml
View the email:
cat /tmp/miniflare-abc123/files/email/message-123.eml
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "email-worker",
"main": "src/email.ts",
"account_id": "YOUR_ACCOUNT_ID",
"compatibility_date": "2025-10-11",
"observability": {
"enabled": true
},
// Send email binding
"send_email": [
{
"name": "NOTIFICATIONS",
"destination_address": "[email protected]"
},
{
"name": "ALERTS",
"allowed_destination_addresses": [
"[email protected]",
"[email protected]"
]
}
],
// Optional: Add other bindings
"d1_databases": [
{
"binding": "DB",
"database_name": "email-archive",
"database_id": "YOUR_DATABASE_ID"
}
],
"kv_namespaces": [
{
"binding": "EMAIL_CACHE",
"id": "YOUR_KV_ID"
}
]
}
interface Env {
// Send email bindings
EMAIL: SendEmail;
NOTIFICATIONS: SendEmail;
ALERTS: SendEmail;
// Other bindings
DB: D1Database;
EMAIL_CACHE: KVNamespace;
}
interface SendEmail {
send(message: EmailMessage): Promise<void>;
}
import { EmailMessage } from 'cloudflare:email';
interface ForwardableEmailMessage {
readonly from: string;
readonly to: string;
readonly headers: Headers;
readonly raw: ReadableStream;
readonly rawSize: number;
setReject(reason: string): void;
forward(rcptTo: string, headers?: Headers): Promise<void>;
reply(message: EmailMessage): Promise<void>;
}
declare module 'cloudflare:email' {
export class EmailMessage {
constructor(from: string, to: string, raw: string | ReadableStream);
}
}
[email protected] installed[email protected] installedasync email() handlernpx wrangler deploywrangler tailsend_email binding configured in wrangler.jsoncdestination_address or allowed_destination_addresses specifiedenv.EMAIL.send()npx wrangler deploySolution:
npx wrangler deploywrangler tail to monitor processingSolution:
Solution:
Solution:
wrangler tail to see processing logsSolution:
wrangler tail --format pretty for live logsconsole.log() statements in workerSolution:
ctx.waitUntil() for non-blocking operationsimport 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());
// Access attachments
if (email.attachments && email.attachments.length > 0) {
for (const attachment of email.attachments) {
console.log('Attachment:', attachment.filename);
console.log('Type:', attachment.mimeType);
console.log('Size:', attachment.content.length);
// Store in R2
await env.BUCKET.put(
`emails/${Date.now()}-${attachment.filename}`,
attachment.content
);
}
}
await message.forward('[email protected]');
},
};
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());
// Extract task from email subject
const taskMatch = email.subject.match(/\[TASK\](.*)/i);
if (taskMatch) {
const taskDescription = taskMatch[1].trim();
// Create task in D1
await env.DB.prepare(
'INSERT INTO tasks (description, created_by, created_at) VALUES (?, ?, ?)'
).bind(
taskDescription,
message.from,
new Date().toISOString()
).run();
// Send confirmation
await message.reply(new EmailMessage(
'[email protected]',
message.from,
`Task created: ${taskDescription}`
));
}
},
};
export default {
async email(message, env, ctx) {
// Trigger Cloudflare Workflow based on email
if (message.from.endsWith('@trusted-domain.com')) {
await env.WORKFLOW.create({
params: {
emailFrom: message.from,
emailTo: message.to,
receivedAt: new Date().toISOString(),
},
});
}
await message.forward('[email protected]');
},
};
Required:
[email protected] - Parse incoming email messages[email protected] - Create email messages for sendingBuilt-in:
cloudflare:email - EmailMessage class (no installation needed)Optional:
@cloudflare/workers-types - TypeScript type definitions{
"dependencies": {
"postal-mime": "^2.5.0",
"mimetext": "^3.0.27"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20251014.0",
"wrangler": "^4.44.0"
}
}
Questions? Issues?
references/common-errors.md for detailed troubleshootingreferences/dns-setup.md for DNS configuration helpreferences/local-development.md for testing patternswrangler tail for live debuggingxiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience