Email Routing with Workers -- Custom Processing
In this tutorial, you'll learn about Email Routing with Workers. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Cloudflare Email Routing with Workers enables Serverless email processing -- you can receive emails at any custom domain, Process them with a Worker, and forward, store, or reject messages based on custom logic without running an email server.
Why Email Routing with Workers Matters
Managing email infrastructure is notoriously complex. You need MX records, SMTP servers, spam filtering, virus scanning, and storage -- all while handling variable volumes of incoming mail. Cloudflare Email Routing replaces this with a simple model: emails sent to your domain are caught by Cloudflare's email infrastructure and delivered to a Worker function you write. The Worker receives the email as a structured object with headers, sender, recipients, and body. You can inspect the content, run spam checks, extract attachments, store messages in R2 or D1, forward to external addresses, or trigger other business logic. Unlike traditional email processing which requires always-on servers, Workers run only when an email arrives. Combined with Cloudflare's security products, email processing happens entirely within Cloudflare's edge network. This feature is distinct from REST APIs -- it handles the SMTP-to-HTTP translation so your Worker never deals with raw SMTP protocols.
Real-world use: A support team receives customer emails at support@example.com. Each email is processed by a Worker that scans for keywords, routes urgent messages to a Slack Webhook, archives the original to R2, and stores metadata in D1 for reporting -- all without any email server software.
Email Routing Architecture
flowchart TD
S[Sender] --> MTA[MTA over SMTP]
MTA --> CF[Cloudflare Email Routing]
CF --> W[Email Worker]
W --> R2[Archive to R2]
W --> D1[Metadata to D1]
W --> FWD[Forward to External]
W --> SL[Slack/Webhook]
W --> AUTO[Auto-Reply]
subgraph Worker_Processing
H[Headers] --> P[Parse]
P --> FIL[Filter Rules]
FIL --> ACT[Action]
end
style CF fill:#f90,color:#fff
style W fill:#3498db,color:#fff
style R2 fill:#2ecc71,color:#fff
style D1 fill:#2ecc71,color:#fff
Cloudflare intercepts inbound emails at the MX level, converts them to HTTP requests, and delivers them to your Worker. The Worker receives an EmailMessage object and calls methods to forward, reject, or Process the message.
Setting Up Email Routing
# Step 1: Configure MX records in Cloudflare DNS
# Priority 10: mx.cloudflare.com
# Step 2: Enable Email Routing in the Cloudflare Dashboard
# Email -> Email Routing -> Get Started
# Step 3: Create a catch-all or route-specific email address
# Route: support@example.com -> Worker: email-processor
# Step 4: Write the worker and deploy
npx wrangler deploy src/email-worker.js
// wrangler.toml
name = "email-processor"
main = "src/email-worker.js"
[env.production]
# No special bindings required for basic Email Routing
# Environment variables for forwarding rules:
vars = { FORWARD_TO = "team@company.com", ARCHIVE_BUCKET = "email-archive" }
Email Routing requires MX record changes which can take 30-60 minutes to propagate. Once configured, you define routes in the Cloudflare Dashboard or API that map email addresses to Workers.
Basic Email Forwarding with Filtering
export default {
async email(message, env, ctx) {
const from = message.from;
const to = message.to;
const subject = message.headers.get('subject') || 'No Subject';
// Filter spam senders
const blockedDomains = ['spam.com', 'mailer.example.com'];
const senderDomain = from.split('@')[1];
if (blockedDomains.includes(senderDomain)) {
console.log(`Blocked email from ${from}`);
// Reject the email -- it will not be delivered
return;
}
// Auto-reply to specific senders
if (subject.includes('Unsubscribe')) {
await message.reply('You have been unsubscribed successfully.');
console.log(`Sent unsubscribe confirmation to ${from}`);
return;
}
// Forward to the team inbox
await message.forward(env.FORWARD_TO);
console.log(`Forwarded email from ${from} about "${subject}" to ${env.FORWARD_TO}`);
}
};
// Expected behavior:
// Email from spam.com -> Blocked, rejected, not forwarded
// Email with "Unsubscribe" subject -> Auto-reply sent, not forwarded
// Normal email from legitimate sender -> Forwarded to team@company.com
The email handler is the entry point for Email Routing Workers. It receives an EmailMessage object with from, to, headers, and raw properties. The message.forward() method sends the email to another address. The message.reply() method sends an automatic reply.
Processing Attachments and Archiving
export default {
async email(message, env, ctx) {
const emailId = crypto.randomUUID();
const timestamp = new Date().toISOString();
// Store the raw email in R2 for archival
const rawEmail = await new Response(message.raw).arrayBuffer();
await env.ARCHIVE.put(
`emails/${timestamp}-${emailId}.eml`,
rawEmail,
{ httpMetadata: { contentType: 'message/rfc822' } }
);
// Store metadata in D1 for searchability
await env.DB.prepare(`
INSERT INTO email_log (id, from_addr, to_addr, subject, received_at, has_attachments)
VALUES (?, ?, ?, ?, ?, ?)
`).bind(
emailId,
message.from,
message.to,
message.headers.get('subject') || '',
timestamp,
message.attachments.length > 0 ? 1 : 0
).run();
// Forward the original email
await message.forward(env.FORWARD_TO);
console.log(`Archived email ${emailId} from ${message.from}`);
}
};
// Expected behavior:
// 1. Email arrives for support@example.com
// 2. Raw .eml file saved to R2 at emails/2026-06-23T10:30:00Z-uuid.eml
// 3. Metadata row inserted into D1 email_log table
// 4. Original email forwarded to the team inbox
// 5. All processing completes within a few hundred milliseconds
This pattern archives every email for Compliance and enables full-text search of email metadata via D1. The raw .eml file in R2 can be served to authorized users for review. Attachments are accessible through the message.attachments array.
Multi-Route Email Processing
const routes = [
{
pattern: /^support@/,
handler: async (message, env) => {
await message.forward('support-team"@company".com');
await env.DB.prepare('INSERT INTO ticket_queue (from_addr, subject) VALUES (?, ?)')
.bind(message.from, message.headers.get('subject'))
.run();
}
},
{
pattern: /^billing@/,
handler: async (message, env) => {
const body = await new Response(message.raw).text();
if (body.includes('REFUND')) {
await message.forward('billing-manager"@company".com');
} else {
await message.forward('billing-team"@company".com');
}
}
},
{
pattern: /^noreply@/,
handler: async (message) => {
// Silently discard
console.log(`Discarded noreply email from ${message.from}`);
}
}
];
export default {
async email(message, env, ctx) {
for (const route of routes) {
if (route.pattern.test(message.to)) {
await route.handler(message, env);
return;
}
}
// Default: forward to general inbox
await message.forward('general@company.com');
}
};
// Expected behavior:
// support@example.com -> Forwarded to support-team, logged to D1
// billing@example.com with REFUND -> Forwarded to billing-manager
// billing@example.com without REFUND -> Forwarded to billing-team
// noreply@example.com -> Silently discarded
// unknown@example.com -> Forwarded to general@company.com
By routing emails based on the recipient address, a single Worker can handle multiple email roles. Each route pattern matches against the message.to value and applies custom logic.
Common Errors
| Error | Cause | Fix |
|---|---|---|
Email Routing not enabled for domain |
MX records not configured or routing not active | Verify MX records point to mx.<a href="/web-servers-hosting/cloudflare/">Cloudflare</a>.com and Email Routing is toggled on in the dashboard |
Worker handler not found |
Worker does not export an email function |
Export a named email function from your Worker module (not fetch) |
Message too large |
Email exceeds 10MB size limit | Cloudflare Email Routing supports up to 10MB messages; reject oversized messages at the MX level |
Failed to forward message |
Destination mail server rejects the email | Verify the forwarding address exists and is not blocking Cloudflare's sending IPs |
Attachment processing failed |
Attachment exceeds per-file limits | Check size limits; Process large attachments asynchronously and store in R2 |
Practice Questions
- What function must a Worker export to handle incoming emails?
- How do you archive a raw email to R2 for Compliance purposes?
- How can you route emails differently based on the recipient address?
FAQ
Summary
Cloudflare Email Routing with Workers lets you receive, process, and forward emails using Serverless functions. The Worker receives an EmailMessage with headers, body, and attachments. Use message.forward() to relay emails, message.reply() to auto-respond, or Process and archive to R2 and D1. Multi-route pattern matching enables sophisticated email workflows from a single Worker. Email Routing replaces traditional mail server infrastructure with edge functions. DodaTech uses Email Routing Workers to Process customer support tickets and archive all correspondence for Compliance.
This guide is brought to you by the developers of Cloudflare, Docker containers, and Durga Antivirus Pro at DodaTech.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro