Skip to content
Cloudflare Docs

Email routing

Agents can receive and process emails using Cloudflare Email Routing. This guide covers how to route inbound emails to your Agents and handle replies securely.

Prerequisites

  1. A domain configured with Cloudflare Email Routing.
  2. An Email Worker configured to receive emails.
  3. An Agent to process emails.

Quick start

JavaScript
import { Agent, routeAgentEmail } from "agents";
import { createAddressBasedEmailResolver } from "agents/email";
// Your Agent that handles emails
export class EmailAgent extends Agent {
async onEmail(email) {
console.log("Received email from:", email.from);
console.log("Subject:", email.headers.get("subject"));
// Reply to the email
await this.replyToEmail(email, {
fromName: "My Agent",
body: "Thanks for your email!",
});
}
}
// Route emails to your Agent
export default {
async email(message, env) {
await routeAgentEmail(message, env, {
resolver: createAddressBasedEmailResolver("EmailAgent"),
});
},
};

Resolvers

Resolvers determine which Agent instance receives an incoming email. Choose the resolver that matches your use case.

createAddressBasedEmailResolver

Recommended for inbound mail. Routes emails based on the recipient address.

JavaScript
import { createAddressBasedEmailResolver } from "agents/email";
const resolver = createAddressBasedEmailResolver("EmailAgent");

Routing logic:

Recipient AddressAgent NameAgent ID
support@example.comEmailAgent (default)support
sales@example.comEmailAgent (default)sales
NotificationAgent+user123@example.comNotificationAgentuser123

The sub-address format (agent+id@domain) allows routing to different agent namespaces and instances from a single email domain.

createSecureReplyEmailResolver

For reply flows with signature verification. Verifies that incoming emails are authentic replies to your outbound emails, preventing attackers from routing emails to arbitrary agent instances.

JavaScript
import { createSecureReplyEmailResolver } from "agents/email";
const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET);

When your agent sends an email with replyToEmail() and a secret, it signs the routing headers with a timestamp. When a reply comes back, this resolver verifies the signature and checks that it has not expired before routing.

Options:

JavaScript
const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {
// Maximum age of signature in seconds (default: 30 days)
maxAge: 7 * 24 * 60 * 60, // 7 days
// Callback for logging/debugging signature failures
onInvalidSignature: (email, reason) => {
console.warn(`Invalid signature from ${email.from}: ${reason}`);
// reason can be: "missing_headers", "expired", "invalid", "malformed_timestamp"
},
});

When to use: If your agent initiates email conversations and you need replies to route back to the same agent instance securely.

createCatchAllEmailResolver

For single-instance routing. Routes all emails to a specific agent instance regardless of the recipient address.

JavaScript
import { createCatchAllEmailResolver } from "agents/email";
const resolver = createCatchAllEmailResolver("EmailAgent", "default");

When to use: When you have a single agent instance that handles all emails (for example, a shared inbox).

Combining resolvers

You can combine resolvers to handle different scenarios:

JavaScript
export default {
async email(message, env) {
const secureReplyResolver = createSecureReplyEmailResolver(
env.EMAIL_SECRET,
);
const addressResolver = createAddressBasedEmailResolver("EmailAgent");
await routeAgentEmail(message, env, {
resolver: async (email, env) => {
// First, check if this is a signed reply
const replyRouting = await secureReplyResolver(email, env);
if (replyRouting) return replyRouting;
// Otherwise, route based on recipient address
return addressResolver(email, env);
},
// Handle emails that do not match any routing rule
onNoRoute: (email) => {
console.warn(`No route found for email from ${email.from}`);
email.setReject("Unknown recipient");
},
});
},
};

Handling emails in your Agent

The AgentEmail interface

When your agent's onEmail method is called, it receives an AgentEmail object:

TypeScript
type AgentEmail = {
from: string; // Sender's email address
to: string; // Recipient's email address
headers: Headers; // Email headers (subject, message-id, etc.)
rawSize: number; // Size of the raw email in bytes
getRaw(): Promise<Uint8Array>; // Get the full raw email content
reply(options): Promise<void>; // Send a reply
forward(rcptTo, headers?): Promise<void>; // Forward the email
setReject(reason): void; // Reject the email with a reason
};

Parsing email content

Use a library like postal-mime to parse the raw email:

JavaScript
import PostalMime from "postal-mime";
class MyAgent extends Agent {
async onEmail(email) {
const raw = await email.getRaw();
const parsed = await PostalMime.parse(raw);
console.log("Subject:", parsed.subject);
console.log("Text body:", parsed.text);
console.log("HTML body:", parsed.html);
console.log("Attachments:", parsed.attachments);
}
}

Detecting auto-reply emails

Use isAutoReplyEmail() to detect auto-reply emails and avoid mail loops:

JavaScript
import { isAutoReplyEmail } from "agents/email";
import PostalMime from "postal-mime";
class MyAgent extends Agent {
async onEmail(email) {
const raw = await email.getRaw();
const parsed = await PostalMime.parse(raw);
// Detect auto-reply emails to avoid sending duplicate responses
if (isAutoReplyEmail(parsed.headers)) {
console.log("Skipping auto-reply email");
return;
}
// Process the email...
}
}

This checks for standard RFC 3834 headers (Auto-Submitted, X-Auto-Response-Suppress, Precedence) that indicate an email is an auto-reply.

Replying to emails

Use this.replyToEmail() to send a reply:

JavaScript
class MyAgent extends Agent {
async onEmail(email) {
await this.replyToEmail(email, {
fromName: "Support Bot", // Display name for the sender
subject: "Re: Your inquiry", // Optional, defaults to "Re: "
body: "Thanks for contacting us!", // Email body
contentType: "text/plain", // Optional, defaults to "text/plain"
headers: {
// Optional custom headers
"X-Custom-Header": "value",
},
secret: this.env.EMAIL_SECRET, // Optional, signs headers for secure reply routing
});
}
}

Forwarding emails

JavaScript
class MyAgent extends Agent {
async onEmail(email) {
await email.forward("admin@example.com");
}
}

Rejecting emails

JavaScript
class MyAgent extends Agent {
async onEmail(email) {
if (isSpam(email)) {
email.setReject("Message rejected as spam");
return;
}
// Process the email...
}
}

Secure reply routing

When your agent sends emails and expects replies, use secure reply routing to prevent attackers from forging headers to route emails to arbitrary agent instances.

How it works

  1. Outbound: When you call replyToEmail() with a secret, the agent signs the routing headers (X-Agent-Name, X-Agent-ID) using HMAC-SHA256.
  2. Inbound: createSecureReplyEmailResolver verifies the signature before routing.
  3. Enforcement: If an email was routed via the secure resolver, replyToEmail() requires a secret (or explicit null to opt-out).

Setup

  1. Add a secret to your wrangler.jsonc:

    {
    "vars": {
    "EMAIL_SECRET": "change-me-in-production",
    },
    }

    For production, use Wrangler secrets instead:

    Terminal window
    npx wrangler secret put EMAIL_SECRET
  2. Use the combined resolver pattern:

    JavaScript
    export default {
    async email(message, env) {
    const secureReplyResolver = createSecureReplyEmailResolver(
    env.EMAIL_SECRET,
    );
    const addressResolver = createAddressBasedEmailResolver("EmailAgent");
    await routeAgentEmail(message, env, {
    resolver: async (email, env) => {
    const replyRouting = await secureReplyResolver(email, env);
    if (replyRouting) return replyRouting;
    return addressResolver(email, env);
    },
    });
    },
    };
  3. Sign outbound emails:

    JavaScript
    class MyAgent extends Agent {
    async onEmail(email) {
    await this.replyToEmail(email, {
    fromName: "My Agent",
    body: "Thanks for your email!",
    secret: this.env.EMAIL_SECRET, // Signs the routing headers
    });
    }
    }

Enforcement behavior

When an email is routed via createSecureReplyEmailResolver, the replyToEmail() method enforces signing:

secret valueBehavior
"my-secret"Signs headers (secure)
undefined (omitted)Throws error - must provide secret or explicit opt-out
nullAllowed but not recommended - explicitly opts out of signing

Complete example

Here is a complete email agent with secure reply routing:

JavaScript
import { Agent, routeAgentEmail } from "agents";
import {
createAddressBasedEmailResolver,
createSecureReplyEmailResolver,
} from "agents/email";
import PostalMime from "postal-mime";
export class EmailAgent extends Agent {
async onEmail(email) {
const raw = await email.getRaw();
const parsed = await PostalMime.parse(raw);
console.log(`Email from ${email.from}: ${parsed.subject}`);
// Store the email in state
const emails = this.state.emails || [];
emails.push({
from: email.from,
subject: parsed.subject,
receivedAt: new Date().toISOString(),
});
this.setState({ ...this.state, emails });
// Send auto-reply with signed headers
await this.replyToEmail(email, {
fromName: "Support Bot",
body: `Thanks for your email! We received: "${parsed.subject}"`,
secret: this.env.EMAIL_SECRET,
});
}
}
export default {
async email(message, env) {
const secureReplyResolver = createSecureReplyEmailResolver(
env.EMAIL_SECRET,
{
maxAge: 7 * 24 * 60 * 60, // 7 days
onInvalidSignature: (email, reason) => {
console.warn(`Invalid signature from ${email.from}: ${reason}`);
},
},
);
const addressResolver = createAddressBasedEmailResolver("EmailAgent");
await routeAgentEmail(message, env, {
resolver: async (email, env) => {
// Try secure reply routing first
const replyRouting = await secureReplyResolver(email, env);
if (replyRouting) return replyRouting;
// Fall back to address-based routing
return addressResolver(email, env);
},
onNoRoute: (email) => {
console.warn(`No route found for email from ${email.from}`);
email.setReject("Unknown recipient");
},
});
},
};

API reference

routeAgentEmail

TypeScript
function routeAgentEmail<Env>(
email: ForwardableEmailMessage,
env: Env,
options: {
resolver: EmailResolver;
onNoRoute?: (email: ForwardableEmailMessage) => void | Promise<void>;
},
): Promise<void>;

Routes an incoming email to the appropriate Agent based on the resolver's decision.

OptionDescription
resolverFunction that determines which agent to route the email to
onNoRouteOptional callback invoked when no routing information is found. Use this to reject the email or perform custom handling. If not provided, a warning is logged and the email is dropped.

createSecureReplyEmailResolver

TypeScript
function createSecureReplyEmailResolver(
secret: string,
options?: {
maxAge?: number;
onInvalidSignature?: (
email: ForwardableEmailMessage,
reason: SignatureFailureReason,
) => void;
},
): EmailResolver;
type SignatureFailureReason =
| "missing_headers"
| "expired"
| "invalid"
| "malformed_timestamp";

Creates a resolver for routing email replies with signature verification.

OptionDescription
secretSecret key for HMAC verification (must match the key used to sign)
maxAgeMaximum age of signature in seconds (default: 30 days / 2592000 seconds)
onInvalidSignatureOptional callback for logging when signature verification fails

signAgentHeaders

TypeScript
function signAgentHeaders(
secret: string,
agentName: string,
agentId: string,
): Promise<Record<string, string>>;

Manually sign agent routing headers. Returns an object with X-Agent-Name, X-Agent-ID, X-Agent-Sig, and X-Agent-Sig-Ts headers.

Useful when sending emails through external services while maintaining secure reply routing. The signature includes a timestamp and will be valid for 30 days by default.

Next steps