> ## 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 Agent Example

> Build an email-processing agent with secure reply routing, auto-reply, loop prevention, and comprehensive security tests

An email-processing agent using Cloudflare Email Routing. Demonstrates email parsing, auto-reply with HMAC-signed headers for secure routing, loop prevention, and comprehensive security testing.

## What it demonstrates

* **Email Routing** - Routes emails to agents based on email addresses (e.g., `agent+id@domain.com`)
* **Secure Reply Routing** - HMAC-signed headers for secure reply flows
* **Email Parsing** - Uses PostalMime to parse incoming emails
* **Auto-Reply** - Automatically responds to incoming emails with loop prevention
* **State Management** - Tracks email count and stores recent emails
* **Security Tests** - Comprehensive test suite including attack bypass attempts

## Server Implementation

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

interface EmailData {
  from: string;
  subject: string;
  text?: string;
  html?: string;
  to: string;
  timestamp: Date;
  messageId?: string;
}

interface EmailAgentState {
  emailCount: number;
  lastUpdated: Date;
  emails: EmailData[];
  autoReplyEnabled: boolean;
}

export class EmailAgent extends Agent<Env, EmailAgentState> {
  initialState = {
    autoReplyEnabled: true,
    emailCount: 0,
    emails: [],
    lastUpdated: new Date()
  };

  async onEmail(email: AgentEmail) {
    console.log("📧 Received email from:", email.from, "to:", email.to);

    const raw = await email.getRaw();
    const parsed = await PostalMime.parse(raw);

    const emailData: EmailData = {
      from: parsed.from?.address || email.from,
      html: parsed.html,
      messageId: parsed.messageId,
      subject: parsed.subject || "No Subject",
      text: parsed.text,
      timestamp: new Date(),
      to: email.to
    };

    const newState = {
      autoReplyEnabled: this.state.autoReplyEnabled,
      emailCount: this.state.emailCount + 1,
      emails: [...this.state.emails.slice(-9), emailData],
      lastUpdated: new Date()
    };

    this.setState(newState);

    if (newState.autoReplyEnabled && !this.isAutoReply(parsed)) {
      await this.replyToEmail(email, {
        fromName: "My Email Agent",
        body: `Thank you for your email!

I received your message with subject: "${email.headers.get("subject")}"

Current stats:
- Total emails processed: ${newState.emailCount}
- Last updated: ${newState.lastUpdated.toISOString()}

Best regards,
Email Agent`,
        secret: this.env.EMAIL_SECRET
      });
    }
  }

  private isAutoReply(
    parsed: Awaited<ReturnType<typeof PostalMime.parse>>
  ): boolean {
    // Check headers for auto-reply indicators
    for (const h of parsed.headers) {
      const header = h as Record<string, string | undefined>;

      // auto-submitted header (RFC 3834)
      const autoSubmitted = header["auto-submitted"];
      if (autoSubmitted && autoSubmitted.toLowerCase() !== "no") {
        return true;
      }

      // x-auto-response-suppress header (Microsoft)
      if (header["x-auto-response-suppress"]) {
        return true;
      }

      // precedence header
      const precedence = header.precedence;
      if (
        precedence &&
        ["bulk", "junk", "list", "auto_reply"].includes(
          precedence.toLowerCase()
        )
      ) {
        return true;
      }
    }

    // Check subject line for common auto-reply patterns
    const subject = (parsed.subject || "").toLowerCase();
    return (
      subject.includes("auto-reply") ||
      subject.includes("out of office") ||
      subject.includes("automatic reply")
    );
  }
}

export default {
  async email(email, env: Env) {
    console.log("📮 Email received via email handler");

    const secureReplyResolver = createSecureReplyEmailResolver(
      env.EMAIL_SECRET
    );
    const addressResolver = createAddressBasedEmailResolver("EmailAgent");

    await routeAgentEmail(email, env, {
      resolver: async (email, env) => {
        // Check if this is a reply to one of our outbound emails
        const replyRouting = await secureReplyResolver(email, env);
        if (replyRouting) return replyRouting;
        // Otherwise route based on recipient address
        return addressResolver(email, env);
      }
    });
  },
  async fetch(request: Request, env: Env) {
    return (
      (await routeAgentRequest(request, env)) ||
      new Response("Not found", { status: 404 })
    );
  }
} satisfies ExportedHandler<Env>;
```

## Email Routing Strategies

### 1. Secure Reply Routing (Recommended for Replies)

Uses HMAC-signed headers to securely route email replies:

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

**Security features:**

* HMAC-SHA256 signatures prevent header forgery
* Timestamp validation prevents replay attacks
* Constant-time comparison prevents timing attacks

### 2. Address-Based Routing

Routes based on email address patterns:

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

// EmailAgent+user123@domain.com → { agentName: "EmailAgent", agentId: "user123" }
// john.doe@domain.com → { agentName: "EmailAgent", agentId: "john.doe" }
```

**Routing rules:**

* With sub-addresses: `localpart+subaddress@domain.com` → `agentName: "localpart", agentId: "subaddress"`
* Without sub-addresses: `localpart@domain.com` → `agentName: defaultAgentName, agentId: "localpart"`

### 3. Catch-All Routing

Routes all emails to a single agent:

```typescript theme={null}
const resolver = createCatchAllEmailResolver("EmailAgent", "main");
// All emails route to EmailAgent:main
```

### Composing Resolvers

```typescript theme={null}
await routeAgentEmail(email, env, {
  resolver: async (email, env) => {
    // Try secure reply routing first (for replies)
    const replyRouting = await secureReplyResolver(email, env);
    if (replyRouting) return replyRouting;

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

## Auto-Reply with Loop Prevention

The agent detects auto-replies to prevent infinite loops:

```typescript theme={null}
private isAutoReply(parsed: ParsedEmail): boolean {
  // Check headers
  for (const h of parsed.headers) {
    // Auto-Submitted header (RFC 3834)
    if (h["auto-submitted"] && h["auto-submitted"] !== "no") {
      return true;
    }
    // X-Auto-Response-Suppress header (Microsoft)
    if (h["x-auto-response-suppress"]) {
      return true;
    }
    // Precedence header
    if (["bulk", "junk", "list", "auto_reply"].includes(h.precedence)) {
      return true;
    }
  }

  // Check subject line
  const subject = parsed.subject.toLowerCase();
  return subject.includes("auto-reply") ||
         subject.includes("out of office") ||
         subject.includes("automatic reply");
}
```

## Secure Reply Flow

When sending outbound emails, the agent signs headers:

```typescript theme={null}
await this.replyToEmail(email, {
  fromName: "My Email Agent",
  body: "Thank you for your email!",
  secret: this.env.EMAIL_SECRET
});
```

The reply includes signed headers:

```
X-Agent-Name: EmailAgent
X-Agent-ID: customer123
X-Agent-Sig: <HMAC signature>
X-Agent-Sig-Ts: <Unix timestamp>
```

When a reply comes back, the signature is verified before routing:

| Attack           | Protection               |
| ---------------- | ------------------------ |
| Forged headers   | Signature verification   |
| Replay attacks   | Timestamp expiration     |
| Future timestamp | Clock skew limit (5 min) |
| Timing attacks   | Constant-time comparison |

## Testing

### Automated Test Suite

```bash theme={null}
# Run all tests (functional + security)
npm test

# Run with verbose output
npm run test:verbose

# Run only security tests
npm run test:security
```

Sample output:

```
═════════════════════════════════════════════════════════════════
FUNCTIONAL TESTS
═════════════════════════════════════════════════════════════════
  ✅ PASS  Basic Email                    (12ms)
  ✅ PASS  Unicode Content                (6ms)
  ✅ PASS  Long Subject                   (6ms)
  ✅ PASS  Multiline Body                 (7ms)
  ✅ PASS  Special Characters             (11ms)
  Subtotal: 5/5 passed

═════════════════════════════════════════════════════════════════
SECURITY TESTS (Attack Bypass Attempts)
═════════════════════════════════════════════════════════════════
  🛡️ BLOCKED  Forged headers (no signature)       (4ms)
  🛡️ BLOCKED  Fake signature (random)             (4ms)
  🛡️ BLOCKED  Expired signature (31 days)         (2ms)
  🛡️ BLOCKED  SQL injection in agent ID           (3ms)
  🛡️ BLOCKED  Path traversal in agent ID          (2ms)
  Subtotal: 15/15 attacks blocked

🎉 All tests passed! Security defenses are working.
```

### Manual Testing

```bash theme={null}
# Run all test scenarios
npm run test-email

# Run specific scenario
npm run test-email -- --scenario basic

# Use custom agent ID
npm run test-email -- --scenario unicode --id my-custom-id
```

Available scenarios: `basic`, `unicode`, `long-subject`, `multiline`, `special-chars`

## Running the Example

<Steps>
  <Step title="Install dependencies">
    ```bash theme={null}
    npm install
    ```
  </Step>

  <Step title="Configure secret">
    Update `wrangler.jsonc` with a unique secret:

    ```jsonc theme={null}
    "vars": {
      "EMAIL_SECRET": "your-unique-secret-here"
    }
    ```

    For production, use Wrangler secrets:

    ```bash theme={null}
    wrangler secret put EMAIL_SECRET
    ```
  </Step>

  <Step title="Start development server">
    ```bash theme={null}
    npm start
    ```
  </Step>

  <Step title="Run tests">
    ```bash theme={null}
    npm test
    ```
  </Step>
</Steps>

## Deployment

<Steps>
  <Step title="Set production secret">
    ```bash theme={null}
    wrangler secret put EMAIL_SECRET
    ```
  </Step>

  <Step title="Deploy">
    ```bash theme={null}
    npm run deploy
    ```
  </Step>

  <Step title="Configure email routing">
    In Cloudflare Dashboard:

    1. Go to `https://dash.cloudflare.com/<account-id>/<domain>/email/routing/routes`
    2. Add routing rules to point to your worker
  </Step>

  <Step title="Send emails">
    Send emails to addresses like:

    * `support@yourdomain.com` → EmailAgent with ID "support"
    * `EmailAgent+urgent@yourdomain.com` → EmailAgent with ID "urgent"
  </Step>
</Steps>

## Security Tests

The test suite includes 15 attack scenarios:

* Forged headers without signature
* Random/fake signatures
* Expired signatures
* Future timestamps
* Malformed timestamps
* SQL injection payloads
* Path traversal attempts
* Header injection (newlines)
* Unicode normalization attacks
* Case manipulation
* Long payload DoS attempts
* Null byte injection

Run: `npm run test:security`

## Related Examples

<CardGroup cols={2}>
  <Card title="GitHub Webhook" icon="webhook" href="/examples/github-webhook">
    Handle webhooks with signature verification
  </Card>

  <Card title="x402 Payments" icon="dollar-sign" href="/examples/x402-payments">
    HTTP payment gating with automatic payment
  </Card>

  <Card title="Workflows" icon="diagram-project" href="/examples/workflows">
    Multi-step workflows with approval gates
  </Card>

  <Card title="Email Guide" icon="book" href="/guides/email">
    In-depth guide to email routing
  </Card>
</CardGroup>
