Email agent·续
Forwarding emails
class MyAgent extends Agent {
async onEmail(email) {
await email.forward("admin@example.com");
}
}
class MyAgent extends Agent {
async onEmail(email: AgentEmail) {
await email.forward("admin@example.com");
}
}
Rejecting emails
class MyAgent extends Agent {
async onEmail(email) {
if (isSpam(email)) {
email.setReject("Message rejected as spam");
return;
}
// Process the email...
}
}
class MyAgent extends Agent {
async onEmail(email: AgentEmail) {
if (isSpam(email)) {
email.setReject("Message rejected as spam");
return;
}
// Process the email...
}
}
Error handling
When sending emails via sendEmail() or replyToEmail(), handle these common errors:
class MyAgent extends Agent {
async onEmail(email) {
try {
await this.replyToEmail(email, {
fromName: "Support Bot",
body: "Thanks for your email!",
});
} catch (error) {
switch (error.code) {
case "E_SENDER_NOT_VERIFIED":
console.error("Sender domain not verified. Verify in dashboard.");
break;
case "E_RATE_LIMIT_EXCEEDED":
console.error("Rate limit exceeded. Back off and retry.");
break;
case "E_DAILY_LIMIT_EXCEEDED":
console.error("Daily sending quota reached.");
break;
case "E_CONTENT_TOO_LARGE":
console.error("Email content exceeds size limit.");
break;
default:
console.error("Email sending failed:", error.message);
}
}
}
}
class MyAgent extends Agent {
async onEmail(email: AgentEmail) {
try {
await this.replyToEmail(email, {
fromName: "Support Bot",
body: "Thanks for your email!",
});
} catch (error) {
switch (error.code) {
case "E_SENDER_NOT_VERIFIED":
console.error("Sender domain not verified. Verify in dashboard.");
break;
case "E_RATE_LIMIT_EXCEEDED":
console.error("Rate limit exceeded. Back off and retry.");
break;
case "E_DAILY_LIMIT_EXCEEDED":
console.error("Daily sending quota reached.");
break;
case "E_CONTENT_TOO_LARGE":
console.error("Email content exceeds size limit.");
break;
default:
console.error("Email sending failed:", error.message);
}
}
}
}
Common error codes
| Error Code | Description | Solution |
|---|---|---|
E_SENDER_NOT_VERIFIED |
Sender domain/address not verified | Verify in Cloudflare dashboard |
E_RATE_LIMIT_EXCEEDED |
Sending rate limit reached | Implement exponential backoff |
E_DAILY_LIMIT_EXCEEDED |
Daily quota exceeded | Wait for quota reset or upgrade plan |
E_CONTENT_TOO_LARGE |
Email exceeds size limit | Reduce attachments or content |
E_RECIPIENT_NOT_ALLOWED |
Recipient not in allowed list | Check allowed destination addresses |
E_RECIPIENT_SUPPRESSED |
Recipient is on suppression list | Remove from suppression list |
E_VALIDATION_ERROR |
Invalid email format | Check email addresses |
E_TOO_MANY_RECIPIENTS |
More than 50 recipients | Split into multiple sends |
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
- Outbound: When you call
replyToEmail()orsendEmail()with asecret, the agent signs the routing headers (X-Agent-Name,X-Agent-ID) using HMAC-SHA256. - Inbound:
createSecureReplyEmailResolververifies the signature before routing. - Enforcement: If an email was routed via the secure resolver,
replyToEmail()requires a secret (or explicitnullto opt-out).
Setup
- Store the signing key as a Wrangler secret. Do not put it in
varsor commit it to source control:npmyarnpnpm
npx wrangler secret put EMAIL_SECRET
```
yarn wrangler secret put EMAIL_SECRET ```
pnpm wrangler secret put EMAIL_SECRET
```
2. Use the combined resolver pattern:
```js
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);
},
});
},
};
```
```ts
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);
},
});
},
} satisfies ExportedHandler<Env>;
```
3. Sign outbound emails:
```js
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
});
}
}
```
```ts
class MyAgent extends Agent {
async onEmail(email: AgentEmail) {
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` value | Behavior |
| --- | --- |
| `"my-secret"` | Signs headers (secure) |
| `undefined` (omitted) | **Throws error** - must provide secret or explicit opt-out |
| `null` | Allowed but not recommended - explicitly opts out of signing |
## Complete example
Here is a complete Email Service agent that sends outbound mail and handles secure replies:
```js
import { Agent, callable, routeAgentEmail } from "agents";
import {
createAddressBasedEmailResolver,
createSecureReplyEmailResolver,
} from "agents/email";
import PostalMime from "postal-mime";
export class EmailAgent extends Agent {
@callable()
async sendWelcome(to) {
return this.sendEmail({
binding: this.env.EMAIL,
to,
from: "support@yourdomain.com",
subject: "Welcome!",
text: "Thanks for signing up.",
secret: this.env.EMAIL_SECRET,
});
}
async onEmail(email) {
const raw = await email.getRaw();
const parsed = await PostalMime.parse(raw);
console.log(`Email from ${email.from}: ${parsed.subject}`);
const emails = this.state.emails || [];
emails.push({
from: email.from,
subject: parsed.subject,
receivedAt: new Date().toISOString(),
});
this.setState({ ...this.state, emails });
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) => {
const replyRouting = await secureReplyResolver(email, env);
if (replyRouting) return replyRouting;
return addressResolver(email, env);
},
onNoRoute: (email) => {
console.warn(`No route found for email from ${email.from}`);
email.setReject("Unknown recipient");
},
});
},
};
import { Agent, callable, routeAgentEmail } from "agents";
import {
createAddressBasedEmailResolver,
createSecureReplyEmailResolver,
type AgentEmail,
} from "agents/email";
import PostalMime from "postal-mime";
interface Env {
EmailAgent: DurableObjectNamespace<EmailAgent>;
EMAIL: SendEmail;
EMAIL_SECRET: string;
}
export class EmailAgent extends Agent<Env> {
@callable()
async sendWelcome(to: string) {
return this.sendEmail({
binding: this.env.EMAIL,
to,
from: "support@yourdomain.com",
subject: "Welcome!",
text: "Thanks for signing up.",
secret: this.env.EMAIL_SECRET,
});
}
async onEmail(email: AgentEmail) {
const raw = await email.getRaw();
const parsed = await PostalMime.parse(raw);
console.log(`Email from ${email.from}: ${parsed.subject}`);
const emails = this.state.emails || [];
emails.push({
from: email.from,
subject: parsed.subject,
receivedAt: new Date().toISOString(),
});
this.setState({ ...this.state, emails });
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: 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) => {
const replyRouting = await secureReplyResolver(email, env);
if (replyRouting) return replyRouting;
return addressResolver(email, env);
},
onNoRoute: (email) => {
console.warn(`No route found for email from ${email.from}`);
email.setReject("Unknown recipient");
},
});
},
} satisfies ExportedHandler<Env>;
API reference
EmailAddress
interface EmailAddress {
email: string;
name?: string;
}
sendEmail
async sendEmail(options: {
binding: EmailSendBinding;
to: string | EmailAddress | (string | EmailAddress)[];
from: string | EmailAddress;
subject: string;
text?: string;
html?: string;
replyTo?: string | EmailAddress;
cc?: string | EmailAddress | (string | EmailAddress)[];
bcc?: string | EmailAddress | (string | EmailAddress)[];
inReplyTo?: string;
headers?: Record<string, string>;
secret?: string;
}): Promise<EmailSendResult>;
Send an outbound email through the Email Service binding. Automatically injects X-Agent-Name and X-Agent-ID headers. When secret is provided, signs headers with HMAC-SHA256 for secure reply routing.
| Option | Description |
|---|---|
binding |
The send_email binding (for example, this.env.EMAIL). Required. |
to |
Recipient address, array of addresses, or EmailAddress object(s) |
from |
Sender address or EmailAddress object |
subject |
Email subject line |
text |
Plain text body (at least one of text/html required) |
html |
HTML body (at least one of text/html required) |
replyTo |
Reply-to address or EmailAddress object |
cc |
CC recipient address, array of addresses, or EmailAddress object(s) |
bcc |
BCC recipient address, array of addresses, or EmailAddress object(s) |
inReplyTo |
Message-ID for threading (sets the In-Reply-To header) |
headers |
Additional custom headers (agent headers take precedence if they collide) |
secret |
Secret for HMAC signing of agent routing headers |
routeAgentEmail
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.
| Option | Description |
|---|---|
resolver |
Function that determines which agent to route the email to |
onNoRoute |
Optional 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
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.
| Option | Description |
|---|---|
secret |
Secret key for HMAC verification (must match the key used to sign) |
maxAge |
Maximum age of signature in seconds (default: 30 days / 2592000 seconds) |
onInvalidSignature |
Optional callback for logging when signature verification fails |
signAgentHeaders
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
HTTP and SSE
Handle HTTP requests in your Agent.
Webhooks
Receive events from external services.
Agents API
Complete API reference for the Agents SDK.
Was this helpful?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/agents/examples/email-agent/#page","headline":"Email agent","description":"Build an agent that sends, receives, routes, and replies to email using Cloudflare Email Service and the Agents SDK.","url":"https://developers.cloudflare.com/agents/examples/email-agent/","inLanguage":"en","image":"https://developers.cloudflare.com/agents/examples/email-agent/og.png?v=ca77b2420210c79e","dateModified":"2026-08-17","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
description: Build and deploy an AI-powered Slack bot on Cloudflare Workers using the Agents SDK. title: Slack agent image: https://developers.cloudflare.com/agents/examples/slack-agent/og.png?v=6b68e1af54a972c3
Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/agents/llms.txt
Use this file to discover all available pages before exploring further.
Docs