AgentBox
README.md
# AgentBox
**Email inboxes for AI agents** — an open-source, self-hosted inbox service that
gives any AI agent its own email address: create inboxes by API, receive mail
with verification codes and confirmation links extracted as structured JSON,
and get notified by signed webhook or long-poll. Runs on Cloudflare Workers +
KV at near-zero cost: the Cloudflare free tier (Email Routing included) covers
typical self-hosted use — up to 150 inboxes by default (see Limits).
Agents don't read raw MIME. AgentBox parses every incoming message for them:
OTP / verification codes (with confidence levels), confirmation links, all
links, and attachment metadata — then serves the result over a REST API or a
native MCP server, so a tool-using agent can work its inbox directly.
## Why AgentBox (vs. hosted agent-mail SaaS such as AgentMail)
| | AgentBox | Hosted SaaS (e.g. AgentMail) |
|---|---|---|
| Source | Open source (MIT), self-hosted | Closed, hosted only |
| Cost | Your Cloudflare account; free tier covers typical use | Per-inbox subscription |
| Data | Parsed messages live in your own KV namespace (raw MIME and attachment files are not stored) | On the vendor's servers |
| Scope | Receive-only by design (codes/links/notifications) | Send + receive |
| Integration | REST API + MCP server + machine-readable `install.md` | API / SDK |
AgentBox deliberately does **one thing**: receiving. Sending is out of scope —
pair it with any send provider if you need outbound mail.
## Quick start
> ⚠️ Keep the Worker name `agent-mail` (the default in
> `wrangler.toml.example`), or update the Worker name hard-coded in
> `src/index.js` (`cfCreateRule`) to match — routing rules are created
> pointing at that Worker by name. Renaming only one side silently breaks
> inbox creation.
You need:
- A Cloudflare account (free tier is fine).
- A domain (or subdomain) on that account with **Email Routing enabled** —
this is the domain your inboxes will live on, e.g. `mail.example.com`.
To enable it: Cloudflare dashboard → your domain → *Email → Email Routing*
→ *Enable*; Cloudflare adds the required MX/SPF records for you. (AgentBox
creates a literal routing rule per inbox automatically at inbox creation;
you do not need to pre-create address rules.)
- Node.js + `wrangler` (`npm i -g wrangler`, or use `npx wrangler`).
- A Cloudflare API token for rule creation: **Zone → Email Routing → Write**
(plus Zone Read), scoped to your one zone. Create it in the Cloudflare
dashboard under *My Profile → API Tokens*.
Steps:
```bash
git clone <this-repo> agentbox && cd agentbox
npm install
# 1. Create the KV namespace that will store inboxes and messages
npx wrangler kv namespace create agentbox
# 2. Configure
cp wrangler.toml.example wrangler.toml
# edit wrangler.toml: paste the KV id into [[kv_namespaces]] (keep
# binding = "KV" — the code reads env.KV), set DOMAIN, and set
# CF_ZONE_ID (find it in the dashboard: your domain → Overview →
# right-hand sidebar → Zone ID)
# 3. Set the two secrets (values are yours; never commit them)
npx wrangler secret put ADMIN_TOKEN # operator token; guards inbox creation
npx wrangler secret put CF_API_TOKEN # the Email Routing token from above
# 4. Deploy
npx wrangler deploy
```
Then follow the machine-readable **[install.md](install.md)**: it contains the
full quickstart (create an inbox → send a test email → read back the extracted
code), an executable self-check script, an MCP client configuration example,
and a troubleshooting table. `llms.txt` is the short index for LLM tools.
## How it works
- One Worker (`src/index.js`): HTTP API + MCP endpoint + Cloudflare Email
Routing `email` handler.
- Storage is a single KV namespace:
`inbox|<addr>` inbox record (API key stored as SHA-256 hash only, plus a
per-inbox `webhook_secret`), `idx|<addr>` message id index,
`msg|<addr>|<id>` message records, `sink|log` webhook test log.
- Creating an inbox automatically creates the literal Email Routing rule for
that address (pointing at this Worker) through the Cloudflare API and reads
it back to verify (`rule_status` in the create response).
- Extraction is local and free — no paid AI APIs: links from text + HTML
anchors; confirmation links by confirm/verify/activate semantics; codes by
keyword proximity (high), standalone 6-digit (medium), other 5–8 digit
(low), each with a context snippet.
## API summary
Full details in [install.md](install.md).
- `GET /health` → `{ok, service, domain, inboxes}`
- `POST /inboxes` (ADMIN token) `{name, webhook_url?}` →
`{address, api_key, webhook_secret, rule_status}` — `api_key` and
`webhook_secret` are returned **exactly once**. Past `MAX_INBOXES`
(default in the example config: 150) creation returns `429`.
- Read mail (per-inbox API key as `Authorization: Bearer <api_key>`; a key
only ever opens its own inbox — no key → 401, another inbox's key → 403):
- `GET /inboxes/{addr}/messages?limit=50` — newest first, with extractions
- `GET /inboxes/{addr}/messages/{id}` — full message
- `GET /inboxes/{addr}/wait?timeout=30&after={message_id}` — long-poll;
`after` is your cursor, only newer messages are returned
- `GET /install.md`, `GET /llms.txt` — machine-readable docs, served live
(source of truth: `src/docs.js`, dual-written with the repo-root files).
### MCP
`POST /mcp` — stateless Streamable HTTP, JSON-RPC 2.0, one request → one JSON
response (no SSE, no sessions). Tools:
- `create_inbox` (ADMIN token) `{name, webhook_url?}`
- `list_messages` `{address, limit?}`
- `get_message` `{address, message_id}`
- `wait_for_message` `{address, timeout_seconds? (default 20, max 50), after?}`
The token travels in the MCP request's `Authorization: Bearer` header — the
ADMIN token for `create_inbox`, the inbox's own API key for the other tools.
There is no anonymous access.
### Webhooks
If `webhook_url` was set at creation, every new message is POSTed as
structured JSON `{event: "message.received", event_id, timestamp, inbox,
message}` with headers `X-Agent-Mail-Signature`
(`sha256=` + HMAC-SHA256(`webhook_secret`, `timestamp + "." + body`)),
`X-Agent-Mail-Timestamp`, `X-Agent-Mail-Event-Id`. Delivery is attempted up
to 3 times (backoff 1 s, then 3 s); the final status is recorded on the
message record. Dedupe retries by `event_id`. Test sink: `POST /sink` records callbacks,
`GET /sink` (ADMIN token) reads them back.
## Limits
- **Receive-only.** No sending in this version.
- **500 messages per inbox per day**; excess is dropped and counted.
- **Attachments are metadata only** (name, MIME type, size) — bodies are not
stored.
- **200 Email Routing rules per zone** (Cloudflare platform limit), shared
with all other rules in the zone; each inbox consumes one literal rule, so
plan `MAX_INBOXES` with headroom.
- **KV is eventually consistent**: a new message can take a few seconds to
become visible; re-read after ~10 s before concluding mail is missing.
- Wait caps: REST `wait` max 55 s, MCP `wait_for_message` max 50 s — loop for
longer waits.
## Security notes
- `api_key` and `webhook_secret` are shown **exactly once**, in the create
response. The server stores only a hash of the API key — a lost key cannot
be recovered; the operator must create a new inbox. Store both values in a
secret store immediately.
- `ADMIN_TOKEN` can create inboxes. Keep it server-side
(`wrangler secret put`), never in client code, prompts, or logs.
- `CF_API_TOKEN` should be scoped to the single zone with only Email Routing
write + zone read permissions.
- Verify webhook signatures before trusting payloads, and dedupe by
`event_id` — the same message may be delivered by the sender more than
once, and `event_id` identifies a delivery event, not the message itself;
use a content hash as your idempotency key if you need message-level
deduping.
- **Webhook targets must be external URLs.** Cloudflare Workers cannot call
back their own `workers.dev` address. Point `webhook_url` at an external
endpoint or a custom domain, not at this Worker's own address.
- Inbox creation is admin-only by design. Do not expose `ADMIN_TOKEN` to
end-user agents — hand each agent only its own inbox key.
## Development
```bash
npm test # node tests/v2-harness.mjs && node tests/mcp-harness.mjs
```
Both harnesses run the Worker against an in-memory KV stub — no Cloudflare
account needed.
## License
MIT — see [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo recent community issues