> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/cloudflare/agents/llms.txt
> Use this file to discover all available pages before exploring further.

# Email Routing

> Route incoming emails to Agents with secure reply flows

## Overview

Cloudflare Agents provides utilities for routing incoming emails to Agent instances with support for address-based routing, secure reply flows, and catch-all patterns.

```typescript theme={null}
import { routeAgentEmail, createAddressBasedEmailResolver } from "agents";

export default {
  async email(message, env) {
    await routeAgentEmail(message, env, {
      resolver: createAddressBasedEmailResolver("EmailAgent")
    });
  }
};
```

## routeAgentEmail()

Route an email to the appropriate Agent.

<ParamField path="email" type="ForwardableEmailMessage" required>
  The email to route (from Email Workers)
</ParamField>

<ParamField path="env" type="Env" required>
  Environment containing Agent bindings
</ParamField>

<ParamField path="options" type="EmailRoutingOptions<Env>" required>
  <ParamField path="resolver" type="EmailResolver<Env>" required>
    Function that determines which Agent to route to
  </ParamField>

  <ParamField path="onNoRoute" type="(email: ForwardableEmailMessage) => void | Promise<void>">
    Called when no routing information is found. If not provided, a warning is logged and the email is dropped.
  </ParamField>
</ParamField>

```typescript theme={null}
export default {
  async email(message: ForwardableEmailMessage, env: Env) {
    await routeAgentEmail(message, env, {
      resolver: createAddressBasedEmailResolver("SupportAgent"),
      onNoRoute: async (email) => {
        console.warn("No route for:", email.from, "→", email.to);
        email.setReject("Invalid recipient");
      }
    });
  }
};
```

**Returns:** `Promise<void>`

## Email Resolvers

### createAddressBasedEmailResolver()

Route based on email address (sub-address or local part).

<ParamField path="defaultAgentName" type="string" required>
  Default agent name to use if email doesn't contain sub-address
</ParamField>

```typescript theme={null}
const resolver = createAddressBasedEmailResolver("SupportAgent");
```

**Routing patterns:**

* `support+ticket123@example.com` → `SupportAgent` instance `ticket123`
* `support@example.com` → `SupportAgent` instance `support`
* `agent+room@example.com` → `agent` instance `room`

**Returns:** `EmailResolver<Env>`

### createSecureReplyEmailResolver()

Route secure reply emails with signature verification.

<ParamField path="secret" type="string" required>
  Secret key for HMAC verification (must match signAgentHeaders)
</ParamField>

<ParamField path="options" type="SecureReplyResolverOptions">
  <ParamField path="maxAge" type="number" default="2592000">
    Maximum signature age in seconds (default: 30 days)
  </ParamField>

  <ParamField path="onInvalidSignature" type="(email, reason) => void">
    Called when signature verification fails
  </ParamField>
</ParamField>

```typescript theme={null}
const secureResolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {
  maxAge: 7 * 24 * 60 * 60, // 7 days
  onInvalidSignature: (email, reason) => {
    console.warn(`Invalid signature from ${email.from}: ${reason}`);
  }
});
```

**Returns:** `EmailResolver<Env>`

<Note>
  Use `signAgentHeaders()` when sending outbound emails to enable secure reply routing.
</Note>

### createCatchAllEmailResolver()

Route all emails to a single Agent instance.

<ParamField path="agentName" type="string" required>
  Agent class name
</ParamField>

<ParamField path="agentId" type="string" required>
  Agent instance name
</ParamField>

```typescript theme={null}
const resolver = createCatchAllEmailResolver("InboxAgent", "main");
```

**Returns:** `EmailResolver<Env>`

## Combining Resolvers

Try multiple resolvers in sequence:

```typescript theme={null}
export default {
  async email(message: ForwardableEmailMessage, env: Env) {
    const secureResolver = createSecureReplyEmailResolver(env.EMAIL_SECRET);
    const addressResolver = createAddressBasedEmailResolver("SupportAgent");

    await routeAgentEmail(message, env, {
      resolver: async (email, env) => {
        // Try secure reply routing first
        const replyRouting = await secureResolver(email, env);
        if (replyRouting) return replyRouting;

        // Fall back to address-based routing
        return addressResolver(email, env);
      }
    });
  }
};
```

## Secure Reply Flow

### Signing Outbound Emails

Use `signAgentHeaders()` to sign emails for secure reply routing:

<ParamField path="secret" type="string" required>
  Secret key for HMAC signing (store in environment variables)
</ParamField>

<ParamField path="agentName" type="string" required>
  Agent class name (kebab-case)
</ParamField>

<ParamField path="agentId" type="string" required>
  Agent instance name
</ParamField>

```typescript theme={null}
import { signAgentHeaders } from "agents/email";

const headers = await signAgentHeaders(
  env.EMAIL_SECRET,
  "support-agent",
  this.name
);

// Use headers when sending email
// Headers: X-Agent-Name, X-Agent-ID, X-Agent-Sig, X-Agent-Sig-Ts
```

**Returns:** `Promise<Record<string, string>>`

### replyToEmail()

Reply to an email from within an Agent:

<ParamField path="email" type="AgentEmail" required>
  The email to reply to
</ParamField>

<ParamField path="options" type="ReplyOptions" required>
  <ParamField path="fromName" type="string" required>
    Sender name
  </ParamField>

  <ParamField path="subject" type="string">
    Email subject (defaults to "Re: original subject")
  </ParamField>

  <ParamField path="body" type="string" required>
    Email body
  </ParamField>

  <ParamField path="contentType" type="string" default="text/plain">
    MIME content type
  </ParamField>

  <ParamField path="headers" type="Record<string, string>">
    Additional headers
  </ParamField>

  <ParamField path="secret" type="string | null">
    Secret for signing headers. Required if email was routed via createSecureReplyEmailResolver. Pass `null` to opt out.
  </ParamField>
</ParamField>

```typescript theme={null}
class SupportAgent extends Agent {
  async onEmail(email: AgentEmail) {
    await this.replyToEmail(email, {
      fromName: "Support Team",
      body: "Thank you for your message!",
      secret: this.env.EMAIL_SECRET
    });
  }
}
```

**Returns:** `Promise<void>`

<Warning>
  If the email was routed via `createSecureReplyEmailResolver`, you **must** pass a `secret` to sign replies. Otherwise, replies cannot be routed back securely.
</Warning>

## Email Utilities

### isAutoReplyEmail()

Check if an email is an auto-reply (to avoid reply loops).

<ParamField path="headers" type="EmailHeader[]" required>
  Headers array from postal-mime or similar
</ParamField>

```typescript theme={null}
import { isAutoReplyEmail } from "agents/email";
import PostalMime from "postal-mime";

class EmailAgent extends Agent {
  async onEmail(email: AgentEmail) {
    const raw = await email.getRaw();
    const parser = new PostalMime();
    const parsed = await parser.parse(raw);

    if (isAutoReplyEmail(parsed.headers)) {
      console.log("Skipping auto-reply");
      return;
    }

    // Process email
  }
}
```

**Returns:** `boolean`

**Checks for:**

* `Auto-Submitted` header (RFC 3834)
* `X-Auto-Response-Suppress` header
* `Precedence: bulk/junk/list` header

## Email Handler

### onEmail()

Override to handle incoming emails in your Agent:

```typescript theme={null}
import type { AgentEmail } from "agents/email";
import PostalMime from "postal-mime";

class SupportAgent extends Agent {
  async onEmail(email: AgentEmail) {
    // Parse email
    const raw = await email.getRaw();
    const parser = new PostalMime();
    const parsed = await parser.parse(raw);

    // Skip auto-replies
    if (isAutoReplyEmail(parsed.headers)) {
      return;
    }

    // Extract content
    const subject = email.headers.get("subject") ?? "No subject";
    const text = parsed.text ?? "";

    console.log(`Email from ${email.from}: ${subject}`);
    console.log(`Body: ${text}`);

    // Reply
    await this.replyToEmail(email, {
      fromName: "Support Bot",
      body: `Thanks for your message about: ${subject}`,
      secret: this.env.EMAIL_SECRET
    });
  }
}
```

## AgentEmail Type

The `AgentEmail` object passed to `onEmail()`:

<ResponseField name="from" type="string" required>
  Sender email address
</ResponseField>

<ResponseField name="to" type="string" required>
  Recipient email address
</ResponseField>

<ResponseField name="headers" type="Headers" required>
  Email headers

  ```typescript theme={null}
  const subject = email.headers.get("subject");
  const messageId = email.headers.get("message-id");
  ```
</ResponseField>

<ResponseField name="rawSize" type="number" required>
  Size of the raw email in bytes
</ResponseField>

<ResponseField name="getRaw" type="() => Promise<Uint8Array>" required>
  Get the raw email content

  ```typescript theme={null}
  const raw = await email.getRaw();
  const parser = new PostalMime();
  const parsed = await parser.parse(raw);
  ```
</ResponseField>

<ResponseField name="reply" type="(options) => Promise<void>" required>
  Send a reply (use `replyToEmail()` instead for automatic header signing)
</ResponseField>

<ResponseField name="forward" type="(rcptTo: string, headers?: Headers) => Promise<void>" required>
  Forward the email to another address
</ResponseField>

<ResponseField name="setReject" type="(reason: string) => void" required>
  Reject the email with a reason
</ResponseField>

## Full Example

```typescript theme={null}
// src/index.ts
import {
  routeAgentEmail,
  createSecureReplyEmailResolver,
  createAddressBasedEmailResolver,
  type AgentEmail,
  isAutoReplyEmail
} from "agents";
import PostalMime from "postal-mime";

export class SupportAgent extends Agent {
  async onEmail(email: AgentEmail) {
    // Parse email
    const raw = await email.getRaw();
    const parser = new PostalMime();
    const parsed = await parser.parse(raw);

    // Skip auto-replies
    if (isAutoReplyEmail(parsed.headers)) {
      return;
    }

    const subject = email.headers.get("subject") ?? "No subject";
    const text = parsed.text ?? "";

    console.log(`Ticket from ${email.from}: ${subject}`);

    // Store in state
    this.setState({
      ...this.state,
      lastEmail: {
        from: email.from,
        subject,
        body: text,
        receivedAt: Date.now()
      }
    });

    // Reply
    await this.replyToEmail(email, {
      fromName: "Support Team",
      subject: `Re: ${subject}`,
      body: `Thank you for contacting support. Your ticket has been created.\n\nOriginal message:\n${text}`,
      secret: this.env.EMAIL_SECRET
    });
  }
}

export default {
  async email(message: ForwardableEmailMessage, env: Env) {
    const secureResolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {
      onInvalidSignature: (email, reason) => {
        console.warn(`Invalid signature: ${reason}`);
      }
    });

    const addressResolver = createAddressBasedEmailResolver("SupportAgent");

    await routeAgentEmail(message, env, {
      resolver: async (email, env) => {
        // Try secure reply first
        const routing = await secureResolver(email, env);
        if (routing) return routing;

        // Fall back to address-based
        return addressResolver(email, env);
      },
      onNoRoute: (email) => {
        console.warn("No route for:", email.to);
        email.setReject("Invalid recipient");
      }
    });
  }
};
```

## wrangler.jsonc Configuration

```jsonc theme={null}
{
  "name": "email-agent",
  "main": "src/index.ts",
  "compatibility_date": "2026-01-28",
  "compatibility_flags": ["nodejs_compat"],
  "durable_objects": {
    "bindings": [
      {
        "name": "SUPPORT_AGENT",
        "class_name": "SupportAgent",
        "script_name": "email-agent"
      }
    ]
  },
  "vars": {
    "EMAIL_SECRET": "your-secret-here"
  }
}
```

## Security

### Signature Verification

Signatures prevent attackers from spoofing email headers to route emails to arbitrary agents:

```typescript theme={null}
// ✅ Secure - verifies HMAC signature
const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET);

// ❌ Insecure - trusts attacker-controlled headers (REMOVED)
const resolver = createHeaderBasedEmailResolver(); // Error: removed
```

### Signature Expiration

Signatures expire after `maxAge` (default: 30 days):

```typescript theme={null}
const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {
  maxAge: 7 * 24 * 60 * 60 // 7 days
});
```

### Secret Management

Store secrets in environment variables:

```bash theme={null}
wrangler secret put EMAIL_SECRET
```

Or in `.dev.vars` for local development:

```
EMAIL_SECRET=your-secret-here
```

## Best Practices

### Use Secure Resolvers

```typescript theme={null}
// ✅ Good - secure reply routing
const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET);

// ❌ Bad - address-based only (no signature verification)
const resolver = createAddressBasedEmailResolver("Agent");
```

### Check Auto-Replies

```typescript theme={null}
// ✅ Good - prevent reply loops
if (isAutoReplyEmail(parsed.headers)) {
  return;
}

// ❌ Bad - might create reply loops
// (no auto-reply check)
```

### Handle No Route

```typescript theme={null}
// ✅ Good - reject invalid emails
await routeAgentEmail(message, env, {
  resolver,
  onNoRoute: (email) => {
    email.setReject("Invalid recipient");
  }
});

// ❌ Bad - silently drop emails
await routeAgentEmail(message, env, { resolver });
```

### Sign Replies

```typescript theme={null}
// ✅ Good - sign for secure routing
await this.replyToEmail(email, {
  fromName: "Bot",
  body: "Reply",
  secret: this.env.EMAIL_SECRET
});

// ❌ Bad - no signature (replies can't be securely routed)
await this.replyToEmail(email, {
  fromName: "Bot",
  body: "Reply",
  secret: null
});
```

## Related

* [Agent Class](/api/agent-class) - Email lifecycle hooks
* [Email Workers](https://developers.cloudflare.com/email-routing/email-workers/) - Cloudflare Email Workers
* [Routing](/api/routing) - HTTP request routing
