| [ Web Proxy ] |
| Viewing: https://developers.cloudflare.com/email-routing/email-workers/ | [Back] [Original] |
Process incoming emails with the email() handler in Cloudflare Workers. Forward, reply, reject, or process emails programmatically.
Process incoming emails using the email() handler in your Cloudflare Workers. This allows you to programmatically handle email routing with custom logic.
Add the email handler function to your Worker's exported handlers:
export default {
async email(message, env, ctx): Promise<void> {
// Process incoming email
await message.forward("destination@example.com");
},
} satisfies ExportedHandler<Env>;from workers import WorkerEntrypoint
class Default(WorkerEntrypoint):
async def email(self, message, env, ctx):
await message.forward("destination@example.com")| Parameter | Type | Description |
|---|---|---|
message |
ForwardableEmailMessage |
The incoming email message |
env |
object |
Worker environment bindings (KV, EMAIL, etc.) |
ctx |
object |
Execution context with waitUntil function |
The message parameter provides access to the incoming email:
interface ForwardableEmailMessage {
readonly from: string; // Sender email address (envelope MAIL FROM)
readonly to: string; // Recipient email address (envelope RCPT TO)
readonly headers: Headers; // Email headers (Subject, Message-ID, etc.)
readonly raw: ReadableStream; // Raw MIME email content stream
readonly rawSize: number; // Size of raw email in bytes
readonly canBeForwarded: boolean; // Whether the message can be forwarded
// Actions
setReject(reason: string): void;
forward(rcptTo: string, headers?: Headers): Promise<EmailSendResult>;
reply(message: EmailMessage): Promise<EmailSendResult>;
}
export default {
async email(message, env, ctx): Promise<void> {
// Access email metadata
console.log(`From: ${message.from}`);
console.log(`To: ${message.to}`);
console.log(`Size: ${message.rawSize} bytes`);
// Access headers
const subject = message.headers.get("subject");
const date = message.headers.get("date");
const messageId = message.headers.get("message-id");
console.log(`Subject: ${subject}`);
console.log(`Date: ${date}`);
console.log(`Message-ID: ${messageId}`);
},
};Use postal-mime to parse the MIME structure of an incoming email. The parser handles multipart boundaries, transfer encodings, and character sets correctly.
import PostalMime from "postal-mime";
export default {
async email(message, env, ctx): Promise<void> {
const email = await PostalMime.parse(message.raw);
console.log(`Subject: ${email.subject}`);
console.log(`Text: ${email.text}`);
console.log(`HTML: ${email.html}`);
},
};Forward incoming emails to verified destination addresses:
export default {
async email(message, env, ctx): Promise<void> {
// Forward to a single address
await message.forward("team@example.com");
},
};export default {
async email(message, env, ctx): Promise<void> {
const recipient = message.to;
const subject = message.headers.get("subject") || "";
// Route based on recipient
if (recipient.includes("support@")) {
await message.forward("support-team@example.com");
} else if (recipient.includes("sales@")) {
await message.forward("sales-team@example.com");
} else if (subject.toLowerCase().includes("urgent")) {
await message.forward("urgent@example.com");
} else {
// Default routing
await message.forward("general@example.com");
}
},
};export default {
async email(message, env, ctx): Promise<void> {
const subject = message.headers.get("subject") || "";
if (subject.toLowerCase().includes("security")) {
// Forward to multiple addresses for security issues
await Promise.all([
message.forward("security@example.com"),
message.forward("admin@example.com"),
message.forward("ciso@example.com"),
]);
} else {
await message.forward("general@example.com");
}
},
};Add custom headers when forwarding. Only headers with an X- prefix can be added through forward(). Other headers are removed.
export default {
async email(message, env, ctx): Promise<void> {
// Create custom headers
const customHeaders = new Headers();
customHeaders.set("X-Processed-By", "Email-Worker");
customHeaders.set("X-Processing-Time", new Date().toISOString());
customHeaders.set("X-Original-Recipient", message.to);
customHeaders.set("X-Spam-Score", "0.1"); // Example spam score
// Forward with custom headers
await message.forward("processed@example.com", customHeaders);
},
};
Send automatic replies with message.reply(). Replies built this way are threaded with the original message and pass through the same SMTP session, so they preserve the original Message-ID chain.
Replies through the Workers API must satisfy the following requirements, otherwise reply() throws an exception:
EmailMessage event.References header, to prevent reply loops and abuse.The reply payload is an EmailMessage built from a raw MIME string. The examples below use mimetext to build the MIME body. The mimetext package requires the nodejs_compat compatibility flag.
import { EmailMessage } from "cloudflare:email";
import { createMimeMessage } from "mimetext";
export default {
async email(message, env, ctx): Promise<void> {
const subject = message.headers.get("subject") || "";
const messageId = message.headers.get("Message-ID");
const reply = createMimeMessage();
if (messageId) {
reply.setHeader("In-Reply-To", messageId);
reply.setHeader("References", messageId);
}
reply.setSender(message.to);
reply.setRecipient(message.from);
reply.setSubject(`Re: ${subject}`);
reply.addMessage({
contentType: "text/plain",
data: "Thank you for your message. We have received your email and will respond shortly.",
});
reply.addMessage({
contentType: "text/html",
data: "<h1>Thank you for your message</h1><p>We have received your email and will respond shortly.</p>",
});
await message.reply(
new EmailMessage(message.to, message.from, reply.asRaw()),
);
// Also forward to human team
await message.forward("team@example.com");
},
};import { EmailMessage } from "cloudflare:email";
import { createMimeMessage } from "mimetext";
export default {
async email(message, env, ctx): Promise<void> {
const sender = message.from;
const recipient = message.to;
const subject = message.headers.get("subject") || "";
const messageId = message.headers.get("Message-ID");
// Don't reply to automated emails
if (
sender.includes("noreply") ||
sender.includes("no-reply") ||
subject.toLowerCase().includes("automated")
) {
await message.forward("team@example.com");
return;
}
// Customized auto-reply based on recipient
let html = "";
if (recipient.includes("support@")) {
html = `
<h1>Support Request Received</h1>
<p>Thank you for contacting support. Your request has been assigned ticket #${Date.now()}.</p>
<p>Expected response time: 2-4 hours during business hours.</p>
`;
} else if (recipient.includes("sales@")) {
html = `
<h1>Sales Inquiry Received</h1>
<p>Thank you for your interest in our products.</p>
<p>A sales representative will contact you within 24 hours.</p>
`;
} else {
html = `
<h1>Message Received</h1>
<p>Thank you for your message. We will respond within 2 business days.</p>
`;
}
const reply = createMimeMessage();
if (messageId) {
reply.setHeader("In-Reply-To", messageId);
reply.setHeader("References", messageId);
}
reply.setSender(recipient);
reply.setRecipient(sender);
reply.setSubject(`Re: ${subject}`);
reply.addMessage({
contentType: "text/plain",
data: html.replace(/<[^>]*>/g, ""),
});
reply.addMessage({ contentType: "text/html", data: html });
await message.reply(new EmailMessage(recipient, sender, reply.asRaw()));
// Forward to appropriate team
await message.forward("team@example.com");
},
};Reject emails with a permanent SMTP error:
export default {
async email(message, env, ctx): Promise<void> {
const sender = message.from;
// Block specific senders
const blockedDomains = ["spam.com", "unwanted.net"];
const senderDomain = sender.split("@")[1];
if (blockedDomains.includes(senderDomain)) {
message.setReject("Sender domain not allowed");
return;
}
// Continue processing
await message.forward("inbox@example.com");
},
};export default {
async email(message, env, ctx): Promise<void> {
const subject = message.headers.get("subject") || "";
// Reject based on subject content
const spamKeywords = ["buy now", "limited time", "act fast", "urgent"];
const containsSpam = spamKeywords.some((keyword) =>
subject.toLowerCase().includes(keyword),
);
if (containsSpam) {
message.setReject("Message appears to be spam");
return;
}
// Check message size
if (message.rawSize > 25 * 1024 * 1024) {
// 25 MiB limit (inbound message size)
message.setReject("Message too large");
return;
}
// Continue processing
await message.forward("inbox@example.com");
},
};Handle errors gracefully in email processing:
export default {
async email(message, env, ctx): Promise<void> {
try {
// Main email processing logic
await processEmail(message, env);
} catch (error) {
console.error("Email processing failed:", error);
// Log error for monitoring
if (env.ERROR_LOGS) {
await env.ERROR_LOGS.put(
`error-${Date.now()}`,
JSON.stringify({
error: error.message,
stack: error.stack,
from: message.from,
to: message.to,
timestamp: new Date().toISOString(),
}),
);
}
// Fallback: forward to admin
try {
await message.forward("admin@example.com");
} catch (fallbackError) {
console.error("Fallback forwarding failed:", fallbackError);
// Last resort: reject the email
message.setReject("Internal processing error");
}
}
},
};
async function processEmail(message, env) {
// Your main email processing logic here
const recipient = message.to;
if (recipient.includes("support@")) {
await message.forward("support@example.com");
} else if (recipient.includes("sales@")) {
await message.forward("sales@example.com");
} else {
await message.forward("general@example.com");
}
}
| Web Proxy Viewer | New URL | Original Page |