Skip to content

Workers API

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.

Email handler syntax

Add the email handler function to your Worker's exported handlers:

TypeScript
export default {
async email(message, env, ctx): Promise<void> {
// Process incoming email
await message.forward("destination@example.com");
},
} satisfies ExportedHandler<Env>;

Parameters

ParameterTypeDescription
messageForwardableEmailMessageThe incoming email message
envobjectWorker environment bindings (KV, EMAIL, etc.)
ctxobjectExecution context with waitUntil function

ForwardableEmailMessage interface

The message parameter provides access to the incoming email:

TypeScript
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>;
}

Properties

TypeScript
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}`);
},
};

Email actions

Forward emails

Forward incoming emails to verified destination addresses:

TypeScript
export default {
async email(message, env, ctx): Promise<void> {
// Forward to a single address
await message.forward("team@example.com");
},
};

Forward with custom headers

Add custom headers when forwarding. Only headers with an X- prefix can be added through forward(). Other headers are removed.

TypeScript
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);
},
};

Reply to emails

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:

  • The incoming email must have a valid DMARC result.
  • An email can only be replied to once per EmailMessage event.
  • The recipient in the reply must match the sender of the incoming email.
  • The outgoing sender domain must match the domain that received the email.
  • The reply is rejected if the incoming email has more than 100 entries in its 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.

TypeScript
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");
},
};

Reject emails

Reject emails with a permanent SMTP error:

TypeScript
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");
},
};

Error handling

Handle errors gracefully in email processing:

TypeScript
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");
}
}

Next steps