{
  "markdown": "# openagent.email\n\n**Self-hosted email for AI agents. The open-source alternative to AgentMail.**\n\n**[openagent.email](https://openagent.email)** · website: [openagentemail/website](https://github.com/openagentemail/website)\n\n![Web dashboard: a message with its extracted verification code](docs/images/message-detail.png)\n\n[![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE)\n[![npm](https://img.shields.io/npm/v/@openagentemail/mcp.svg)](https://www.npmjs.com/package/@openagentemail/mcp)\n[![release](https://github.com/openagentemail/openagentemail/actions/workflows/release.yml/badge.svg)](https://github.com/openagentemail/openagentemail/actions/workflows/release.yml)\n[![Glama](https://glama.ai/mcp/servers/openagentemail/openagentemail/badges/score.svg)](https://glama.ai/mcp/servers/openagentemail/openagentemail)\n[![Smithery](https://smithery.ai/badge/tizerluo/openagentemail)](https://smithery.ai/servers/tizerluo/openagentemail)\n[![GitHub stars](https://img.shields.io/github/stars/openagentemail/openagentemail.svg?style=social)](https://github.com/openagentemail/openagentemail)\n\nOne `docker compose up` on your own VPS gives every agent you run unlimited real\nmailboxes on your own domain — over REST and MCP — with OTP and verification-link\nextraction built in. No per-inbox pricing, no third party ever seeing your mail.\n\n## Quickstart\n\n```bash\nnpx -y @openagentemail/setup\n```\n\nA guided wizard: it checks what you already have, helps you pick a VPS and a\ndomain if you're missing either, and connects your agent clients (Claude Code,\nCursor, Kimi Code…) once the server is up.\n\nThe manual path needs a VPS with outbound/inbound port 25 open and a domain\nyou control:\n\n```bash\ngit clone https://github.com/openagentemail/openagentemail.git && cd openagentemail\ncp .env.example .env   # set DOMAIN, API_KEYS, mailbox password, and NTFY_ADMIN_PASSWORD\ndocker compose up -d\nsudo ./deploy/dns-records.sh   # prints the exact DNS records to create\n```\n\nThen verify everything end to end:\n\n```bash\nsudo ./deploy/doctor.sh\n```\n\n`doctor.sh` checks `.env` permissions; MX, A, SPF, DKIM, and DMARC; PTR;\noutbound port 25; DNS blocklists; TLS certificates on 465 and 993; and the\nserver-side ntfy verification endpoint. It does not log in over IMAP/SMTP or\nsend a round-trip test. Docker Mailserver can create `docker-data/` as root,\nso use `sudo` for these two scripts; an EACCES failure prints the same retry\ninstruction instead of pretending the DKIM key is missing.\n\nIf no SMTP relay is configured and outbound port 25 is blocked, API\n`queued:true` only means the local mailserver accepted the message. It does\n**not** mean the recipient received it: Postfix can retain the message in its\nqueue. Treat doctor's outbound-port-25 result as the delivery prerequisite, or\nconfigure a relay before relying on direct delivery.\n\n## Public TLS with Let's Encrypt (opt-in)\n\nThe default `docker compose up -d` path remains self-signed: it does not start\nor pull Certbot and does not publish TCP 80. To use a publicly trusted mail\ncertificate, opt in only after `mail.$DOMAIN` has an A (and, if used, AAAA)\nrecord pointing at this host and the firewall permits inbound TCP 80. HTTP-01\ncannot create those DNS or firewall prerequisites for you.\n\nIn `.env`, set the following (use a reachable contact address outside this\nmailserver when possible):\n\n```dotenv\nSSL_TYPE=letsencrypt\nSSL_DOMAIN=mail.example.com       # exactly mail.$DOMAIN\nLETSENCRYPT_EMAIL=admin@example.net  # optional, but recommended\n```\n\nFirst issue the certificate with the explicitly enabled sidecar; do not start\nthe mailserver in `letsencrypt` mode before this succeeds:\n\n```bash\ndocker compose --profile letsencrypt-bootstrap up -d certbot-bootstrap\ndocker compose logs -f certbot-bootstrap\n# Wait for “Successfully received certificate”, then confirm:\ndocker compose --profile letsencrypt-bootstrap run --rm --no-deps \\\n  --entrypoint ls certbot-bootstrap -l \\\n  /etc/letsencrypt/live/mail.example.com/fullchain.pem \\\n  /etc/letsencrypt/live/mail.example.com/privkey.pem\n```\n\nThis temporary container reads the shared certificate volume, so confirmation\nstill works after the one-shot bootstrap has stopped.\n\nIf first issuance fails, Certbot stops instead of retrying the ACME request in\na tight loop. Correct the DNS/port-80/domain prerequisite, then explicitly run\nthe same `docker compose --profile letsencrypt-bootstrap up -d certbot-bootstrap`\ncommand again.\n\nThe entire `/etc/letsencrypt` tree is a persistent named volume shared with\nthe mailserver read-only: Certbot's `live/` files are symlinks into `archive/`,\nso mounting only `live/` is incorrect. Once the first certificate exists,\nstart the full opt-in stack and verify the public endpoints. Do not enable\n`letsencrypt-bootstrap` and `letsencrypt` together: both publish host TCP 80.\n\n```bash\ndocker compose --profile letsencrypt up -d\n./deploy/doctor.sh\nopenssl s_client -connect mail.example.com:465 -servername mail.example.com </dev/null \\\n  2>/dev/null | openssl x509 -noout -issuer -subject -dates\nopenssl s_client -connect mail.example.com:993 -servername mail.example.com </dev/null \\\n  2>/dev/null | openssl x509 -noout -issuer -subject -dates\n```\n\nAfter bootstrap, the renewal sidecar runs `renew` every 12 hours and restarts\nwith Docker. docker-mailserver's change-detection service watches\n`SSL_TYPE=letsencrypt` certificate updates and reloads Postfix and Dovecot, so\nrenewed certificates take effect on 465/993 without a manual container restart.\nKeep the `letsencrypt` profile enabled for normal operation. If it is omitted,\nthe sidecars and TCP 80 are absent and the original self-signed path is unchanged.\n\nCreate an identity and hand your agent its scoped token (shown once):\n\n```bash\ncurl -X POST http://localhost:3100/v1/identities \\\n  -H \"Authorization: Bearer $API_KEY\" -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"signup-bot\",\"scopes\":[\"read:messages\"]}'\n# → 201 {\"address\":\"fox-k7d2@example.com\",\"name\":\"signup-bot\",\"token\":\"oa_…\",\"scopes\":[\"read:messages\"]}\n```\n\nThe API binds to `127.0.0.1` by default — reach it from other hosts over an\nSSH tunnel or a TLS proxy: [docs/security.md](https://openagent.email/docs/guides/security/).\n\nPaths are exact: call `/v1/notify`, not `/v1/notify/` — a trailing slash\nreturns a plain 404 rather than the API error format.\n\n## Using your own mail server\n\nAlready have a mail provider for your domain? Run the API by itself with\n[`compose.api-only.yaml`](compose.api-only.yaml), connected to that provider's\ncatch-all mailbox. The [external mail server guide](https://openagent.email/docs/guides/external-mailserver/)\ncovers the required catch-all setup, Portainer deployment, SMTP sender limits,\nand TLS certificate verification.\n\nThe standalone default project name is `openagentemail`. If the full\n`compose.yaml` stack also runs on the same host, the API-only stack must not\nshare that default project: give it an explicitly different `-p` value or\n`COMPOSE_PROJECT_NAME` so the two stacks cannot adopt each other's resources.\n\nTo run multiple API-only instances on one host, give every instance its own\nenvironment file, unique Compose project, and host `API_PORT`. The API always\nlistens on port 3100 inside its container; `API_PORT` changes only the host-side\nmapping. For example:\n\n```bash\nmkdir -p ../oae-api-only-env\ncp .env.api-only.example ../oae-api-only-env/alpha.env\ncp .env.api-only.example ../oae-api-only-env/beta.env\nchmod 600 ../oae-api-only-env/*.env\n# Set API_PORT=3100 in alpha.env and API_PORT=3101 in beta.env.\n# Generate separate API_KEYS and TASK_SIGNING_SECRET values in each file.\n\ndocker compose -p oae-alpha --env-file ../oae-api-only-env/alpha.env -f compose.api-only.yaml up -d\ndocker compose -p oae-beta  --env-file ../oae-api-only-env/beta.env  -f compose.api-only.yaml up -d\n```\n\nYou may set a unique `COMPOSE_PROJECT_NAME` for each command instead of using\n`-p`. The project names make Compose generate distinct container names and\nproject-scoped named volumes (for example `oae-alpha_api-data` and\n`oae-beta_api-data`). Do not reuse a project name or `API_PORT` between the two\ninstances. Each instance must have independently generated `API_KEYS` and\n`TASK_SIGNING_SECRET` values; configure its IMAP and SMTP credentials for that\ninstance's intended mailbox/provider boundary as well. Keep these populated\nenvironment files outside the repository, as in the example above.\n\n## Read mail in a browser\n\nOpen [`http://localhost:3100/ui`](http://localhost:3100/ui) and paste an admin\nor identity API token. The built-in dashboard lists the addresses the token\nmay access, shows Inbox / All Mail (IMAP) and Sent (API/MCP send audit\nfor 30 days; direct SMTP is not listed) with cursor paging, extracts\nverification codes and links at the top of a message, offers Rendered\n(isolated HTML iframe), Plain text, or Source views, and can mark messages\nread or unread. Source is fetched on demand from a size-capped `no-store`\nendpoint and never injected as HTML. Scheduled and Trash are omitted until\nthe backend can serve them.\nAdmin sessions can also create identities (with custom address prefixes),\nrotate tokens, and delete identities directly from the overview table.\n\nThe browser exchanges the token once for an `HttpOnly` session cookie; manually\npasted tokens never enter the URL or browser storage. You can also bookmark\n`https://myinstance:3100/ui?token=<admin-token>` for direct login. The server\nautomatically issues a single-use exchange code (TTL ≤ 10 min) via a 302 redirect\nand sanitizes the URL with `Cache-Control: no-store` and `Referrer-Policy: no-referrer`,\nfollowed by client `history.replaceState` cleanup. Tokens passed in URLs can still linger\nin upstream reverse proxy access logs before redirection — see `docs/security.md` for\nreverse-proxy scrubbing guidance.\nPercent-encode the token if it contains URL-reserved characters like `+`, `&`, or `#`\n(e.g. `a+b` → `a%2Bb`), as `+` decodes to a space and `&` truncates the value.\nOnly open `?token=` links you generated yourself; if you suspect a link has leaked,\nrotate the token from the UI immediately. Do not open `?token=` links sent by\nothers — the link signs you into the sender's session (the app displays a\nvisible \"Signed in via link\" banner across the interface for the session as the tell).\nSessions live only in API process memory, so restarting the API signs every\nbrowser out. They expire after 12 idle hours or 24 hours total — or tick **Trust\nthis device** at login to keep a sliding 30-day session on that browser. Each\ntoken holds at most five sessions; a sixth login evicts that token's\nleast-recently-used one instead of locking you out.\n\nFor another computer, use the same SSH tunnel recommended for the API or put a\nTLS reverse proxy in front. The login form refuses non-local plain HTTP, and\nsession cookies are `Secure` away from localhost. Set `UI_ENABLED=false` in\n`.env` to make every `/ui` route return 404.\n\nHTML email is treated as hostile input. The UI removes scripts, images, forms,\nlinks, sender CSS, and all attributes except numeric table spans, then loads the\nresult in a separately sandboxed frame with a restrictive CSP. The\n`sanitize-html` 2.x dependency is deliberately pinned to an exact version;\nupgrade it in a dedicated change and rerun the full poison-message corpus.\n\n### Admin overview\n\nAn admin session lands on **Overview**: every identity in one table with the\nmessage count, unseen count, last delivery, and creation day, plus totals across\nthe top. Each row also shows whether the identity has a token (green dot) and\nhas **Rotate** and **Delete** action buttons. A **Create Identity** button\nabove the table opens a form where you can set a custom address prefix (e.g.\n`qa-bot`) or leave it blank for a random one; the new token is shown once in a\ncopy-to-clipboard modal. Identity sessions never see the overview or management\ncontrols — they go straight to their own inbox.\nThe page is served from the same in-process API as the rest of `/ui`; there is no\nnew public endpoint outside `/ui/api`.\n\nWhat the numbers mean, and where they stop:\n\n- **Counts are a window, not a lifetime total.** One scan reads the newest 500\n  messages in the catch-all mailbox and attributes each one to the identities it\n  was delivered to. The header says `newest N of M in the mailbox` so the window\n  is never mistaken for history. A message addressed to two identities counts\n  once for each row and once — not twice — in the totals.\n- **Honest instead of round.** Messages with enormous recipient lists can exceed\n  the scanner's per-message and global memory bounds. Rows the scanner could not\n  fully account for show `≥N` or `Unknown` rather than a confident wrong number,\n  the page explains why, and `unmatchedInWindow` is reported as `null` instead of\n  a made-up zero. The same applies to an identity created after the last scan:\n  it reads `Unknown` until the next one, never a false `0`.\n- **Snapshots are cached in memory for 15 seconds** and reused for up to\n  10 minutes while a refresh runs in the background, so opening Overview or\n  walking in and out of inboxes does not hammer IMAP. Refresh is floored at\n  5 seconds. Restarting the API drops the cache — the first request afterwards\n  pays for a fresh scan.\n- **Failures cool down and never lie.** If a scan fails, the next attempt waits\n  5 seconds (the API sends `Retry-After`), the table keeps showing the previous\n  numbers, and the header says the last refresh failed. Once a snapshot is older\n  than 10 minutes it is not revived by a failed refresh: the page reports the\n  counts as unavailable instead of showing stale data as current. While a cold\n  scan is still running the endpoint answers `202` and the address list renders\n  immediately with `Loading…` in the count columns.\n- **`GET /ui/api/overview`** (browser session only, admin only) returns exactly\n  the fields the page renders — never message content. `?refresh=1` asks for a\n  new scan, subject to the 5-second floor and the failure cooldown.\n\nDeliberate limits, so nothing here is a surprise later:\n\n- Overview shows counts and timestamps only. Subjects, senders, and verification\n  codes need per-message parsing, which is what the inbox view is for.\n- New mail can be up to 15 seconds late on the page; `Refresh` fetches sooner.\n  There is no steady-state polling: the page only schedules a follow-up while\n  counts are loading or a refresh is pending, capped at 15 attempts over 20\n  seconds and paced by the server's own retry hint.\n- A scan that misses its deadline is abandoned even if IMAP answers a moment\n  later, and the next request scans again. The deadline covers connecting as well\n  as fetching, so a hung server does not park a request behind IMAP's own\n  30-second socket timeout.\n- Up to 200 identities render in one pass. Beyond that, expect to want paging or\n  virtual scrolling; filtering and sorting happen in the browser today.\n- The dashboard self-hosts the Satoshi webfont (`/ui/fonts/*`, the same\n  typeface as the website) so it renders identically on every machine;\n  `font-src` is `'self'`. The favicon is an SVG (`/ui/favicon.svg`) so it\n  needs no build step, and `/ui/favicon.ico` keeps returning 204 as before.\n- Form controls use a dedicated `--line-control` border token so their outlines\n  stay above 3:1 contrast. It is the one intentional deviation from the website's\n  palette and is a one-line revert.\n\n## Features\n\n- **Unlimited identities** — one catch-all mailbox, unlimited `anything@yourdomain`\n  addresses. No provisioning, no per-inbox cost.\n- **Scoped tokens** — every identity gets its own token that can only read and\n  send as that address. The admin key never has to touch your agents.\n- **REST + MCP** — the same operations over a plain HTTP API and a first-class\n  MCP server your agents can call directly.\n- **Server-side notifications** — private ntfy transport for human alerts and\n  managed-agent wake-ups, with OTP-only mail notifications by default. Topics\n  and ntfy credentials stay on the server; phone setup is deliberately a later\n  v0.3.1 step. Per-identity **push content tiers** control how much of each\n  mail-arrival alert leaves the server: tier 1 interrupt only (default),\n  tier 2 adds subject/from, tier 3 (admin + explicit risk confirm) adds a\n  short body preview and extracted OTP codes/links.\n- **`mail_wait_for` / `POST /v1/messages/wait`** — long-poll an inbox until a\n  matching message arrives, with OTP codes and verification links already extracted.\n  Built for automated signups.\n- **Read/unread state** — `mail_mark_seen` / `POST /v1/messages/:id/seen` lets an\n  agent (or the human in the dashboard) mark a message handled, so the unseen\n  count means \"still needs attention\". An identity may flag mail it received\n  **or that this server actually sent** (TO ∨ trusted Sent: From match and\n  Message-ID in the outbound registry), matching the Sent folder (#26 PR 2);\n  a forged From does not count. It cannot flag another identity's mail. Reading\n  a message never changes the flag by itself.\n- **Web dashboard for humans** — inspect identities and messages at `/ui` (Inbox\n  is the default landing for every session). Inbox is a three-pane mail client\n  (identity/folder, list, detail) with Rendered / Plain text / Source; HTML\n  stays in a sandboxed iframe. Real `/ui/*` History routes cover Overview,\n  Tasks, Notifications, and Configure; the shell stays a zero-bundler\n  `/ui/styles.css` + `/ui/app.js` pair.\n- **Safety rails built in** — per-identity send rate limits (20/hour default),\n  automatic mail retention (30 days default), localhost-only API binding.\n- **Bring your own relay** — send directly from the VPS, or route outbound through\n  Amazon SES / SMTP2GO / any SMTP relay with one env var.\n- **DNS wizard + doctor** — `deploy/dns-records.sh` generates your exact DNS records;\n  `deploy/doctor.sh` diagnoses deliverability before your agents depend on it.\n- **Single dependency: Docker.** The stack is the API,\n  [docker-mailserver](https://github.com/docker-mailserver/docker-mailserver),\n  and a private ntfy container. Nothing else.\n\n## How it works\n\n```\n┌─────────────┐   MCP (stdio)    ┌──────────────────┐\n│  AI agents   ├──────────────────▶                  │\n│ (Claude Code,│                  │   openagent api  │\n│  Cursor, …)  │   REST /v1/*     │  (Node, imapflow │\n└─────────────┘──────────────────▶ │   + nodemailer)  │\n                                   └────────┬─────────┘\n                                            │ IMAP + SMTP (localhost)\n                                   ┌────────▼─────────┐      SMTP 25\n                                   │ docker-mailserver│ ◀──────────▶  the world\n                                   │ catch-all mailbox│  (or your relay: SES, …)\n                                   └──────────────────┘\n```\n\nOne catch-all account on your domain receives everything. The API logs into it over\nIMAP, matches messages to identities by the `To`/`Delivered-To` header, and sends\nvia SMTP with the `From` rewritten to the chosen identity. Polling + IMAP IDLE for\nlow-latency waits.\n\n### Multi-domain support\n\nYou can configure secondary domains alongside the primary `DOMAIN` by setting `EXTRA_DOMAINS=domain2.com,domain3.org` in `.env`.\n\n- **Identities**: Agents can be created on the primary domain or any configured secondary domain via the API (`POST /v1/identities` with `domain`), MCP tools (`mail_new_identity`), or the Web Dashboard modal. Note that creating the same localpart across different domains is not allowed for now and will be revisited in Part 2.\n- **Inbound & Outbound**: Catch-all inbound matching and internal mail stamping (`X-OA-Mail-Stamp`) recognize all configured domains.\n- **Tasks & Alerts**: Cross-domain tasks and notifications seamlessly support agents across all configured domains, with identical localparts across domains currently not allowed (409) pending a design revision, and agent-card endpoints still publishing the primary domain.\n- **Pending Deploy Automation**: Automated deploy scripts (`deploy/dns-records.sh`, `deploy/doctor.sh`, Certbot SANs, and multi-domain DKIM key generation) currently configure the primary `DOMAIN`. Automated multi-domain provisioning scripts will follow in Part 2.\n\n### Optional compliance archive\n\nSet `ALWAYS_BCC=archive@example.net` only when your compliance policy permits\nan additional external delivery copy. It is off by default and adds the archive\nonce to the SMTP envelope for API, MCP, and task sends, preserving visible\nrecipient order and matching an existing recipient only by exact local-part and\ncase-insensitive domain—without adding a MIME `Bcc` header or\nchanging visible `To`, header From, envelope MAIL FROM, SPF, DKIM content, or\nDMARC alignment. Archive mailbox access, privacy, retention, aliases, and\nforwarding are operator/MTA responsibilities.\n\nThe archive is an independent, off-domain compliance destination—not an\nordinary untrusted recipient. For all-local visible recipients it receives the\nexact MIME, including the `X-OA-Mail-Stamp` that preserves local `internal`\nclassification. Same-domain `ALWAYS_BCC` values are rejected at startup: on a\nshared catch-all deployment they are not an independent archive destination,\nand duplicate suppression is not a substitute for that boundary. Configure a\ncontrolled external compliance mailbox or leave `ALWAYS_BCC` unset.\n\nEvery enabled archive requires an explicit `TASK_SIGNING_SECRET` of at least\n32 characters. The application enforces only presence and that length minimum;\nit does not prove entropy, so operators must generate a high-entropy secret\n(the Compose examples use `openssl rand -hex 32`). The historical SMTP-password\nfallback remains only when the archive is absent or blank. Both Compose\ndeployments already require the dedicated secret.\n\nIf at least one original recipient is accepted and the configured archive RCPT\nis rejected, the API preserves Nodemailer's partial-success semantics and logs\na content-free warning, even when another original recipient was rejected. If\nthe archive is accepted while no original recipient is reported accepted, the\nAPI fails instead. After an upstream SMTP server accepts or queues the archive\nRCPT, later delivery failures appear in SMTP/Postfix/relay logs or DSNs rather\nthan synchronously in the API response.\n\nSMTP result matching deliberately keeps local-part spelling exact. A relay that\nrewrites accepted RCPT local-part case can therefore cause a conservative\nfailure/retry; configure relays to preserve RCPT spelling. Case-distinct archive\nand original local-parts are intentionally separate SMTP mailboxes and can\nproduce two copies with a case-folding provider, so use consistent spelling.\n\n## Use it from your agent (MCP)\n\nRequires Node.js 18+ on the machine running the MCP client — no install step,\n`npx` downloads and runs the package on first use.\n\n```bash\nclaude mcp add openagentemail \\\n  --env OPENAGENTEMAIL_API_URL=http://localhost:3100 \\\n  --env OPENAGENTEMAIL_API_KEY=oa_your-identity-token \\\n  -- npx -y @openagentemail/mcp\n```\n\nOr the raw JSON config (Claude Desktop, Cursor, Kimi Code):\n\n```json\n{\n  \"mcpServers\": {\n    \"openagentemail\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@openagentemail/mcp\"],\n      \"env\": {\n        \"OPENAGENTEMAIL_API_URL\": \"http://localhost:3100\",\n        \"OPENAGENTEMAIL_API_KEY\": \"oa_your-identity-token\"\n      }\n    }\n  }\n}\n```\n\n## Tools\n\n| Tool | Description |\n| --- | --- |\n| `mail_new_identity(name?, localpart?, scopes?)` | Admin only: create an identity and one-time token; pass `scopes: [\"read:messages\"]` for read-only own-mailbox access, `scopes: []` for no API operation permissions, or omit for legacy full identity permissions |\n| `mail_list_identities()` | List all identities |\n| `mail_list_messages(address, limit?)` | List messages for an address (id/from/to/subject/date/seen/snippet) |\n| `mail_read_message(address, id)` | Full message: text, html?, and `otp:{codes:[],links:[]}` |\n| `mail_mark_seen(address, id, seen?)` | Mark a message read (default) or unread — reading never changes the flag by itself |\n| `mail_wait_for(address, fromContains?, subjectContains?, timeoutSec?)` | Block until a matching message arrives (default 120s, max 600s) |\n| `mail_send(from, to, subject, text, html?)` | Send mail; `from` must be an existing identity |\n| `notify_user(title, message, level?, tags?)` | Send a human alert (needs the server-side `can_notify_user` grant) |\n| `notify_agent(name, title, message, level?, tags?)` | Wake a named agent without exposing an ntfy topic or token |\n| `notify_check(since?)` | Read recent notifications for the calling identity only |\n| `notify_verify()` | Publish and poll a harmless server-side notification self-check |\n| `task_create(to, subject, body?, kind?, approval?, wait?, parentTaskId?)` | Assign an ordinary email-backed task (required `body`) or typed approval (`kind: \"approval\"` with required `approval:{action,expiresAt}`); `parentTaskId` selects a durable readable parent |\n| `task_list_children(parentTaskId, limit?, cursor?)` | List only direct children readable by the current viewer, with 20/50/100 pagination, a parent/viewer/limit-scoped cursor, and no totals or descendants |\n| `task_decide(id, decision)` | Stored reviewer approves or rejects a pending typed approval |\n| `task_claim(id, leaseSec?)` | Claim a recipient task for an optional lease duration |\n| `task_renew(id, leaseToken, leaseSec?)` | Renew a claimed task using its opaque bearer |\n| `task_release(id, leaseToken, reason?)` | Release a claimed task using its opaque bearer |\n| `task_list(state?)` | List task threads visible to the calling identity |\n| `task_get(id, wait?)` | Read a task thread and its stamped state history; optionally wait up to 10 minutes |\n| `task_update(id, state, body?, result?, leaseToken?)` | Advance a participating task; structured output goes in `result` |\n\nTyped approval actions are JSON-only and limited to 65,536 canonical UTF-8 bytes, root-inclusive depth 10, and a server-clock lifetime of 30 days. Exact limits pass; REST and MCP surface `approval_action_too_large`, `approval_action_too_deep`, or `approval_expiry_too_far` as stable client errors when a bound is exceeded.\n\nThe exact v1 action-digest recipe and public cross-runtime vectors are in [docs/approval-digest.md](docs/approval-digest.md).\n\nTask leases are opt-in; `TASK_LEASES_ENABLED` defaults to `false`, so existing clients remain compatible. If the flag is turned off after a lease exists, list/detail responses retain its safe timing and generation fields and add `leaseStatus: \"disabled\"`; no lease is silently cleaned up. A lease generation is capped at 24 hours from its initial claim, and no claim or renewal is allowed at or after seven days from the task's first claim; renewals cap their deadline rather than resetting either anchor, and equality is rejected. During an active recipient lease, omitting the optional credential retains `task_already_terminal`; a supplied wrong, malformed, expired, or reclaimed-generation credential returns `task_lease_required` at HTTP 409. The opaque `leaseToken` is only the claim bearer and is never listed, rendered, logged, or emailed. When `TASK_LEASES_ENABLED=true`, each mailbox requires exactly one API process (single-replica deploy). Multi-process sharing of a mailbox is an unsupported boundary: concurrent claims can assign the same generation, and durable rebuild fail-closes on same-generation different-body claims (the task is hidden; this is not silent corruption and has no cross-task impact). Future cross-process coordination, if introduced, will be derived from the durable stream under the already-decided redesign in issues #80 and #84.\n\nFull per-client setup (Claude Code, Claude Desktop, Cursor, Kimi Code, generic):\n[docs/mcp-clients.md](https://openagent.email/docs/reference/mcp-clients/) · server details:\n[packages/mcp/README.md](packages/mcp/README.md)\n\n## Why self-host?\n\n- **Privacy** — OTP codes and verification links are credentials. Self-hosted, they\n  never leave a machine you own. No third party reads, stores, or trains on your mail.\n- **Cost** — a $5 VPS and a domain you already have vs. per-inbox/per-message SaaS\n  pricing that scales linearly with your agent fleet.\n- **Control** — your IPs, your reputation, your retention. No rate limits, no\n  account suspensions, no sudden API deprecations.\n\n## Server requirements\n\nMeasured on our own production instance, idle: **~190 MB RAM total, ~0% CPU**,\nand ~2 GB of disk for the Docker images. Mail itself is a rounding error —\nretention auto-deletes after 30 days.\n\n| Tier | Spec | Notes |\n|---|---|---|\n| Minimum | 1 vCPU / 1 GB RAM / 10 GB disk | works with the defaults (ClamAV and SpamAssassin off) |\n| Comfortable | 1 vCPU / 2 GB RAM / 20 GB disk | headroom to enable SpamAssassin |\n| With antivirus | 4 GB RAM | ClamAV alone needs ~1 GB extra |\n\nThat's a $5/mo VPS — or a $10–15/**year** deal box. The real prerequisite isn't\nsize, it's **port 25**: AWS, GCP, Azure, DigitalOcean and Vultr block it by\ndefault (some unblock on request). Check before you buy — or route outbound\nthrough a [relay](https://openagent.email/docs/guides/deliverability/) and you don't need port 25 out at all.\n\n## Comparison\n\n| | **openagent.email** | AgentMail.to | MailSlurp |\n|---|---|---|---|\n| Open source | ✅ Apache-2.0 | ❌ | ❌ |\n| Deployment | ✅ Any VPS (true self-host) | SaaS, or enterprise BYOC (Outposts on AWS) | SaaS only |\n| Price | Flat VPS cost | Per-inbox subscription | Usage-based subscription |\n| Unlimited inboxes | ✅ (catch-all) | Paid tiers | Paid tiers |\n| MCP-native | ✅ | ✅ | ❌ (REST/SDKs) |\n| OTP/link extraction | ✅ | ✅ | ✅ |\n| Mail data residency | Your box* | SaaS: theirs; Outposts: your AWS† | Always theirs |\n| Vendor control plane | None | Yes (incl. Outposts) | Yes |\n| You run a server | Yes — that's the point | No (BYOC still vendor-operated) | No |\n\n\\* Push tiers 2/3 may relay subject/from or body/OTP via ntfy (off by default).\n† [AgentMail Outposts](https://www.agentmail.to/blog/agentmail-outposts-byoc): email content stays in your AWS account; AgentMail still runs dashboard, auth, billing, and upgrades. BYOC ≠ open-source self-host on any VPS.\n\n## Roadmap\n\n- **v0.1** — REST + MCP, catch-all identities, `wait_for` with OTP/link\n  extraction, DNS wizard + doctor, optional SMTP relay.\n- **v0.2** — scoped per-identity tokens, send rate limits, automatic retention,\n  localhost-safe defaults, expanded OTP corpus.\n- **v0.3 (current)** — built-in private ntfy notifications, OTP-aware IMAP\n  watcher, server-side agent wake-ups and notification ACLs. Phone delivery and\n  webhooks are intentionally out of this first release.\n- **Distribution** — planned one-click app in the\n  [OpenShip](https://github.com/oblien/openship) catalog.\n\n## Docs\n\n- [docs/api.md](https://openagent.email/docs/reference/api/) — REST API reference with curl examples\n- [docs/security.md](https://openagent.email/docs/guides/security/) — tokens, exposure, rate limits, retention\n- [docs/mcp-clients.md](https://openagent.email/docs/reference/mcp-clients/) — MCP setup for every major client\n- [docs/cloudflare-zero-trust.md](https://openagent.email/docs/guides/cloudflare-zero-trust/) — Cloudflare Tunnel and Access for the Dashboard without blocking MCP\n- [docs/agent-signup.md](https://openagent.email/docs/guides/agent-signup/) — let agents finish sign-ups that email a code, with a verified OKX wallet example\n- [docs/dns-setup.md](https://openagent.email/docs/guides/dns-setup/) — DNS records, explained one by one\n- [docs/deliverability.md](https://openagent.email/docs/guides/deliverability/) — the field guide to actually\n  landing in the inbox\n\n## Contributing\n\nIssues and PRs welcome — see [CONTRIBUTING.md](CONTRIBUTING.md).\n\n## License\n\n[Apache-2.0](LICENSE)\n",
  "bytes": 31902,
  "sha": "8cf7cfe3744f4ad0dc7960297bda589bae2eed2ab9ca3050c4a1e4805b81bc64",
  "repo_slug": "openagentemail/openagentemail",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_openagentemail_mcp_e1bef5a6/readme"
}