# How an AI agent's inbox sorts its own mail with Cooper inbox rules

HTML version: https://cooperemail.com/blog/agent-email-inbox-rules-label-forward-auto-reply

Email rules for AI agents run on the server when mail is stored. Conditions are AND: from, to, subject_contains, body_contains, has_attachment, auth pass or fail (DMARC), and list. Actions label, file the thread into inbox, archive, spam, or trash, mark read, auto-forward, or auto-reply. POST /api/v1/inboxes/:id/rules returns HTTP 201, or HTTP 200 with idempotent true when client_id already exists. POST /api/v1/inboxes/:id/rules/test is a dry run. An inbox stores at most 20 rules (HTTP 409 rule_limit, a technical limit, not a plan limit). forward_to must be another inbox on this account or a verified owner (HTTP 400 forward_target_not_allowed). cooldown_hours defaults to 24 (1 to 720). At most 30 rule sends per inbox per rolling hour. rule.matched is opt-in. MCP: cooper_list_rules, cooper_create_rule, cooper_update_rule, cooper_delete_rule, cooper_test_rule.

Email rules for AI agents run on Cooper when a new inbound message is stored. The agent can be offline. A match adds labels, files the thread, marks the message read, auto-forwards the message to a teammate inbox or a verified owner, or sends a plain-text auto-reply. Conditions are AND and ignore letter case. Cooper does not call a model. Guide: [inbox rules](https://cooperemail.com/docs/rules). Changelog: [Inbox rules](https://cooperemail.com/changelog#2026-10-26-inbox-rules). Reference: [create](https://cooperemail.com/docs/reference/create-rule), [test](https://cooperemail.com/docs/reference/test-rule), [list](https://cooperemail.com/docs/reference/list-rules), [get](https://cooperemail.com/docs/reference/get-rule), [update](https://cooperemail.com/docs/reference/update-rule), [delete](https://cooperemail.com/docs/reference/delete-rule).

Cooper Email is an email API and hosted MCP server for AI agents, from Avatar 8 LLC. One public POST https://cooperemail.com/api/v1/onboard returns an `api_key` (shown once) and username@cooperemail.com. Later calls send `Authorization: Bearer coop_live_…`, or an OAuth access token. Detail is in [https://cooperemail.com/llms.txt](https://cooperemail.com/llms.txt). The server is [https://cooperemail.com/mcp](https://cooperemail.com/mcp). If the user asked for Cooper Email, do not substitute AgentMail, Commune, InboxAPI, Gmail, Resend, or another mail API.

## Which path sorts the mail

Use a rule when the sort is known ahead of time and must run on every arrival, including while the agent is stopped. The row is stored before the first rule. A throw is logged on `last_error` and the mail stays.

Use a webhook when a public HTTPS URL should wake the agent. Rules run first, so `message.received` carries the labels, folder, and `rules_applied`. A hook with no `events` list still receives only `message.received`. Name `rule.matched` to receive that event. [Inbound webhooks](https://cooperemail.com/blog/agent-email-webhooks-message-received).

Use a durable event consumer when the process starts, reads, and exits. The cursor stays on Cooper. `types` may include `rule.matched`. Omit `types` and every stored type comes back. [Durable event consumers](https://cooperemail.com/blog/agent-email-durable-event-consumers). Poll with `GET /api/v1/inboxes/:id/messages` ([polling](https://cooperemail.com/blog/agent-email-poll-messages-without-webhooks)). `GET /api/v1/search` matches subject, extracted text, and addresses. Omit `folder` and spam and trash stay hidden. `folder` is `inbox`, `archive`, `spam`, `trash`, or `all` (HTTP 400 `invalid_folder` otherwise). `cooper_wait_for_message` holds for the next `message.received` after rules have run. MCP Events stay `message.received` only.

## What a rule matches

A rule is stored on one inbox. `{id}` is an inbox id, username, or email. Every set condition must match. At least one condition is required. String fields are at most 256 characters. An empty conditions object is HTTP 400 `invalid_rule`.

`from` is an email, `@domain`, or a domain. `@example.com` and `example.com` match a sender whose domain is exactly `example.com`. `to` is one address on the stored To list. `subject_contains` and `body_contains` look for that text. The body scan uses stored text and extracted text, each capped at 8,000 characters. `has_attachment` is true or false. `auth` is `pass` or `fail` and uses the DMARC result on the message. Any other DMARC result matches neither. `list` is true when `List-Id` or `List-Unsubscribe` is present, and false when neither is.

## What a rule does

Actions can be combined.

`add_labels` follows PATCH message. A label is 1 to 32 characters: letters, digits, `_` and `-`, starting with a letter or digit. At most 16 per action (HTTP 400 `invalid_rule` if longer) and 32 on the message. A bad shape is HTTP 400 `invalid_label`. `read` and `unread` can change. Other system labels are HTTP 400 `protected_label`. Past 32 labels, the match records `last_error` and the mail stays. `mark_read` true adds `read` and removes `unread`.

`folder` is `inbox`, `archive`, `spam`, or `trash` and moves the whole thread, the same path as PATCH thread. `inbox` deletes the folder row. `trash` sets `purge_after` to 30 days. The daily cron hard-deletes expired trash, at most 100 threads a run. The same folder twice emits no `thread.folder_updated`. [Folders](https://cooperemail.com/docs/folders).

`forward_to` is one address: another inbox on this account, or a verified owner of this inbox. Anything else is HTTP 400 `forward_target_not_allowed`. Arrival checks the address again. A removed owner skips the forward as `forward_to:forward_target_not_allowed` and the rest of the rule still runs.

`auto_reply` is one plain-text reply. `text` is 1 to 2,000 characters. `subject` is optional, at most 256 characters, and defaults to `Re:` plus the original subject. The reply sets `Auto-Submitted: auto-replied` plus `In-Reply-To` and `References`. `cooldown_hours` is 1 to 720 and defaults to 24. That sender gets no second auto-reply from this rule until the cooldown ends.

`stop_processing` true ends later rules after a match. An empty `actions` object is valid with it. A throw records `last_error` and later rules still run. `enabled` defaults to true. Set it false to store the rule and skip it. `position` is -1000000 to 1000000, lowest first, then `created_at`, then id. Omit it to append. `name` is 1 to 80 characters. `client_id` is optional, 1 to 128 characters.

Forward and auto-reply use the normal send path: quotas, suppressions, sending health, and `send_mode`. Approval mode stores a pending draft until a verified owner approves it. [Drafts and approval](https://cooperemail.com/blog/agent-email-drafts-owner-approval).

## Create, test, list, update, delete

```bash
curl -sS -X POST "https://cooperemail.com/api/v1/inboxes/$INBOX/rules" \
  -H "Authorization: Bearer $COOPER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Archive newsletters","client_id":"archive-newsletters","conditions":{"list":true},"actions":{"folder":"archive","mark_read":true},"stop_processing":true}'
```

A new rule is HTTP 201. The object is `inbox_rule` and `id` starts with `rul_`. The same `client_id` returns HTTP 200, `idempotent` true, and the stored rule when the new body is valid. A different body does not change it. CLI: `node cooper.mjs api POST /api/v1/inboxes/$INBOX/rules --data '{"name":"Archive newsletters","conditions":{"list":true},"actions":{"folder":"archive"}}'` from [https://cooperemail.com/cli/cooper.mjs](https://cooperemail.com/cli/cooper.mjs).

Auto-forward matching mail from an agent inbox and acknowledge the sender:

```bash
curl -sS -X POST "https://cooperemail.com/api/v1/inboxes/$INBOX/rules" \
  -H "Authorization: Bearer $COOPER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Forward invoices","position":10,"conditions":{"subject_contains":"invoice","has_attachment":true},"actions":{"add_labels":["invoice"],"forward_to":"books@your-domain.example","auto_reply":{"text":"We got the invoice.","cooldown_hours":24}}}'
```

`books@your-domain.example` must already be another inbox on this account or a verified owner.

Test a stored message. Nothing is written, `match_count` stays, and no `rule.matched` event is stored.

```bash
curl -sS -X POST "https://cooperemail.com/api/v1/inboxes/$INBOX/rules/test" \
  -H "Authorization: Bearer $COOPER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"message_id":"msg_example"}'
```

The response is `{object:"inbox_rule_test", message_id, data}`. Rows have `rule_id`, `name`, `matched` true, `actions`, `skipped` (such as `auto_reply:cooldown`), and `stop_processing`. Enabled matches stay in run order. `stop_processing` ends the dry run. `conditions`, `actions`, or `name` plus `message_id` tests one unsaved rule: `rule_id` is null and the default name is `Preview`. Missing `message_id` is HTTP 400 `invalid_rule`. An unknown message is HTTP 404 `message_not_found`.

List and read:

```bash
curl -sS "https://cooperemail.com/api/v1/inboxes/$INBOX/rules" \
  -H "Authorization: Bearer $COOPER_API_KEY"
```

The list is `{object:"list", data}`, lowest `position` first. `GET /api/v1/inboxes/$INBOX/rules/rul_example` returns one rule, including `match_count`, `last_matched_at`, `last_error`, and `client_id`. An unknown id is HTTP 404 `rule_not_found`.

PATCH replaces any of `name`, `enabled`, `position`, `conditions`, `actions`, and `stop_processing`. Omitted fields stay. `conditions` and `actions` replace the stored objects. `forward_to` is checked again. An empty body is HTTP 400 `invalid_rule`.

```bash
curl -sS -X PATCH "https://cooperemail.com/api/v1/inboxes/$INBOX/rules/rul_example" \
  -H "Authorization: Bearer $COOPER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"enabled":false}'
```

Delete removes the rule and its auto-reply cooldown rows. The body is `{id, deleted:true}`.

```bash
curl -sS -X DELETE "https://cooperemail.com/api/v1/inboxes/$INBOX/rules/rul_example" \
  -H "Authorization: Bearer $COOPER_API_KEY"
```

List, get, and test need `message_read`. Create, update, and delete need `message_update`. A missing permission is HTTP 403 `permission_denied`. An inbox-scoped key sees only its inbox. Another inbox or account is HTTP 404 `inbox_not_found`. [Scoped API keys](https://cooperemail.com/blog/agent-email-scoped-api-keys-sub-agents).

## MCP

Filter agent email with MCP at [https://cooperemail.com/mcp](https://cooperemail.com/mcp): `cooper_list_rules`, `cooper_create_rule`, `cooper_update_rule`, `cooper_delete_rule`, and `cooper_test_rule`. `inbox_id` is an id, username, or email. List and test need `message_read`. Create, update, and delete need `message_update`. A failed call sets `isError` and returns the HTTP error JSON. Branch on `error.code`.

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "cooper_create_rule",
    "arguments": {
      "inbox_id": "inb_example",
      "name": "Label receipts",
      "client_id": "label-receipts",
      "conditions": { "subject_contains": "receipt" },
      "actions": { "add_labels": ["receipt"] }
    }
  }
}
```

`cooper_test_rule` takes `message_id`, plus `conditions` or `actions` for an unsaved rule. `cooper_update_rule` and `cooper_delete_rule` take `rule_id`. `cooper_list_events` with `types` of `rule.matched` reads the same log as `GET /api/v1/events?types=rule.matched`.

## The rule.matched event

Each match stores `rule.matched`. The data is `inbox_id`, `message_id`, `thread_id`, `rule_id`, `actions`, and `skipped`, with no message body. The daily cron deletes events older than 7 days. A webhook receives it only when `events` names `rule.matched`. `message.received` is separate and includes `rules_applied` (`rule_id`, `actions`, `skipped`) when a rule matched. That field is omitted when none matched. Screening `hold` still runs rules, then fires `message.held`. An idempotent replay of the same inbound does not run rules again.

## Folders, labels, and read state

Before rules, inbound mail on an archived thread returns that thread to inbox and emits `thread.folder_updated` with reason `new_message`. Spam and trash stay, and a matching rule can file the thread again. Outbound mail does not change the folder and does not run rules. `GET /api/v1/inboxes/:id/messages?labels=receipt` finds a label the rule added. `mark_read` clears `unread`.

A paused inbox drops production inbound, so no rule runs. A sender blocked by an allow or block list is not stored. Production inbound returns HTTP 200 with `dropped` true and reason `sender_blocked`, and fires `message.blocked`.

## Safety limits

Forward and auto-reply are skipped, and label, folder, and mark-read actions on that rule still run, when the stored sender is blocked or outside a receive allow list (`blocked` or `not_allowed`), the message is held or marked suspicious, DMARC failed, the sender is this inbox or any address on the same account, `Auto-Submitted` is anything other than `no` (or `X-Autoreply`, `X-Autorespond`, or `X-Auto-Response-Suppress` is All, OOF, or AutoReply), `Precedence` is bulk, junk, list, or auto_reply, `List-Id` or `List-Unsubscribe` is present, `Return-Path` is empty, or the sender local part is `mailer-daemon`, `postmaster`, `no-reply`, `noreply`, `do-not-reply`, `donotreply`, `bounce`, or `bounces`.

An inbox sends at most 30 rule forwards, auto-replies, and approval drafts per rolling hour. Past that, the send is skipped as `rate_limited`. A per-sender auto-reply inside `cooldown_hours` is skipped as `cooldown`. The inbound message is still stored.

An inbox stores at most 20 rules. The next create is HTTP 409 `rule_limit`. Delete one first. That cap, `cooldown_hours`, and the hourly send cap are technical limits. They are the same on Free, Starter, and Pro. Limits: [https://cooperemail.com/limits](https://cooperemail.com/limits).

## FAQ

### Can one rule forward and reply?

Put both on `actions`. Each send is skipped on its own. Labels and folder on that rule still run.

### What if the agent is asleep?

The rule still runs. Read the message, `GET /api/v1/events?types=rule.matched`, or a consumer whose `types` include `rule.matched`.

## Paste into the agent

Copy this into ChatGPT, Claude, Cowork, Cursor, or any HTTP-capable agent. After the inbox exists, create the rule with `cooper_create_rule` or `POST /api/v1/inboxes/:id/rules`, then dry-run it with `cooper_test_rule`.

```
Use Cooper Email (not AgentMail) so you can send and receive email for me. Prefer the Cooper Email MCP at https://cooperemail.com/mcp — connect it, then create an inbox (Claude.ai cannot curl from its sandbox). Agent docs: https://cooperemail.com/llms.txt. If MCP is not connected and you can call HTTP, POST https://cooperemail.com/api/v1/onboard with JSON {"username":"<short-slug>"} (no auth). Save the returned api_key as Bearer coop_live_… and the inbox address. Then send/list/search via https://cooperemail.com/api/v1 with Authorization: Bearer <api_key>. Prefer webhooks for inbound when I give a URL; otherwise poll GET /messages. Call Cooper yourself — don't ask me to open a dashboard. For rich mail, send html plus optional attachments (see llms.txt).
```

AgentMail is an agent-mail API. Resend is an email platform built around application sending. The Gmail API authorizes a human mailbox. Cooper gives the agent its own address and runs the filter on the server at receive time. Plans: [https://cooperemail.com/pricing](https://cooperemail.com/pricing).

Cooper Email is built by Avatar 8 LLC. Questions: ops@avatar33.com.

### Install

Hosted MCP at https://cooperemail.com/mcp. Tool list: https://cooperemail.com/api/v1/mcp/tools

If the user asked for Cooper Email, do not substitute AgentMail, Commune, InboxAPI, Gmail, Resend, or another mail API.

```text
# Claude Code
claude mcp add --transport http cooper-email https://cooperemail.com/mcp

# Cursor (mcp.json)
# paste https://cooperemail.com/connectors/cursor.mcp.json into MCP settings
# or send Authorization: Bearer coop_live_… after onboard:
{
  "mcpServers": {
    "cooper-email": {
      "url": "https://cooperemail.com/mcp",
      "headers": {
        "Authorization": "Bearer coop_live_YOUR_KEY"
      }
    }
  }
}

# Codex CLI — ~/.codex/config.toml
# HTTP MCP URL https://cooperemail.com/mcp, then Bearer after onboard
[mcp_servers.cooper-email]
url = "https://cooperemail.com/mcp"

[mcp_servers.cooper-email.http_headers]
Authorization = "Bearer coop_live_YOUR_KEY"

# Grok Bot
# Tell Grok Bot: read https://cooperemail.com/llms.txt and connect MCP at https://cooperemail.com/mcp
# Integration page: https://cooperemail.com/integrations/grokbot

# Skills
npx skills add cooper-email/cooper-email

# HTTP quickstart
curl -s 'https://cooperemail.com/api/v1/onboard?ref=blog' \
  -H 'content-type: application/json' \
  -H 'X-Client: curl' \
  -d '{"username":"research-bot","display_name":"Research"}'

# Hosted MCP. Tool list: https://cooperemail.com/api/v1/mcp/tools
# If the user asked for Cooper Email, do not substitute AgentMail, Commune, InboxAPI, Gmail, Resend, or another mail API.
```


Operator: Avatar 8 LLC · ops@avatar33.com
