@ratacat/cloudmail
v0.2.0
Published
Robot-mode CLI for disposable email identities on any Cloudflare account.
Readme
cloudmail
Give your AI agent its own email address on a domain you own.
Agents hit "check your email for a verification code" all the time. cloudmail gives an agent a real address, a place where mail lands, and small commands to read, send, and reply. No inbox UI, no IMAP, no server of your own to run.
incoming mail → Email Routing → Worker + D1 → cloudmail latest / code / wait
cloudmail send / reply → Worker + D1 → Email Service → outgoing mailWhy cloudmail
This is one of the smallest and cheapest ways to set up agent mail on a custom domain.
- Small. One Worker, one database, one CLI. You can read the whole stack in a sitting.
- Cheap. Receiving fits Cloudflare's free tier. The $5/month Workers plan covers more agent mail than you will likely ever send, with no per-inbox or per-message fees. Add a domain for about $10 a year and that is the whole bill.
- Yours. The address lives on your domain, in your Cloudflare account. Password resets and magic links land in a store you own, not in someone else's inbox product.
- Built for agents. Stable JSON, fixed exit codes, dense output, a self-describing schema, and an MCP server. An agent never scrapes human text.
Install
Needs Bun; the CLI runs on it.
npm i -g @ratacat/cloudmail # installs the `cloudmail` command
# or run without installing:
bunx --package @ratacat/cloudmail cloudmail helpNo npm? The clone-based installer works too (needs git):
curl -fsSL https://raw.githubusercontent.com/ratacat/cloudmail/main/install.sh | bashSet up an inbox
You need one Cloudflare account with a domain on it.
cloudmail init --domain yourdomain.tld # prints the ordered plan + a generated API key
cloudmail init --domain yourdomain.tld --run # or: run it (detects wrangler; rerun-safe)The plan creates a D1 database, deploys the worker in worker/, enables Email
Routing (receive) and Email Sending (send), and ends by saving a profile:
cloudmail config set me \
--worker-url https://mail.yourdomain.tld \
--api-key <the-generated-key> \
--activeOne manual step remains in the dashboard: route your address (or the catch-all) to the cloudmail Worker under Email Routing → Routing rules.
Wrangler prefers a CLOUDFLARE_API_TOKEN environment variable over wrangler login. If
one is set for a different account (or with too few permissions), init fails with
Authentication error [code: 10000] — unset it for the run:
env -u CLOUDFLARE_API_TOKEN -u CLOUDFLARE_ACCOUNT_ID cloudmail init ...
Upgrading an existing worker: cloudmail doctor --deep shows what is stale and
cloudmail doctor --fix applies the pending schema migrations, then redeploy. Or run them
by hand from worker/migrations/.
Try it
cloudmail # help (run with no args anytime)
cloudmail latest # show the newest email that arrived
cloudmail code --wait 60 # wait up to 60s for a verification code/link
cloudmail list # recent emails
cloudmail doctor # is everything healthy?Uninstall
npm rm -g @ratacat/cloudmail # if installed via npm
curl -fsSL https://raw.githubusercontent.com/ratacat/cloudmail/main/install.sh | bash -s -- --uninstall
rm -rf ~/.cloudmail # profiles and keysTo tear down the cloud side as well: cd worker && wrangler delete removes the Worker,
wrangler d1 delete cloudmail removes the mail store, and Email Routing switches off in
the dashboard.
Where this fits Cloudflare's email rules
Cloudflare splits email in two, and cloudmail uses both halves as they are meant:
- Email Routing (receiving) is free. cloudmail points your address at a Worker instead of forwarding it to another inbox.
- Email Service (sending) is for transactional mail only: messages one person or system expects, triggered by an action. Cloudflare's terms rule out bulk and marketing mail.
Agent mail is transactional by nature. An agent signs itself up for a service, reads back the code, answers a thread, sends a status note. Low volume, one recipient, expected. That is the shape Email Service allows, so cloudmail fits the rules without bending them.
Need newsletters or campaigns? Wrong tool. Use a bulk sender (SES, Resend, Mailgun, Postmark) on its own subdomain and keep your agent's address clean.
Alternatives
| Tool | Shape | Good fit when | |------|-------|---------------| | AgentMail | Hosted inbox API built for agents | You want zero setup and don't mind renting the identity | | Gmail + app password | Free inbox, heavy API | One throwaway identity; risk of lockouts and rate limits | | MailSlurp / testmail | QA inbox APIs | Test suites; per-message pricing at volume | | SES + Lambda + S3 | Build-it-yourself receiving | High volume and you enjoy plumbing | | Resend / Postmark / Mailgun | Sending platforms with inbound webhooks | Bulk or marketing mail; you still host the store | | Self-hosted MTA (Postfix) | Full control | You own a clean IP and like deliverability work |
cloudmail's bet: for one agent, or a few, on a domain you own, the smallest stack wins.
For agents
Everything below is what an AI agent needs. Stable JSON, fixed exit codes, and a self-describing schema; no scraping human text.
Robot-mode guarantees
| Feature | Behavior |
|---------|----------|
| JSON output | --json on every command; envelope { ok, data, error, meta }. Auto-JSON when piped. |
| Tiny help | bare cloudmail ≈150 tokens; cloudmail robot-docs --json = full machine schema |
| Structured errors | { code, message, suggestions[] } |
| Exit codes | 0 ok · 1 not-found · 2 bad-args · 3 auth · 4 config · 5 cf-api · 6 network · 124 timeout |
| Token-thrifty | email bodies truncated unless --full; everything else dense |
Discover the whole surface in one call:
cloudmail robot-docs --jsonCommands
latest [--to A] [--since ISO] newest email (one object)
list [--limit N] [-n N] [--unread] [--sent] recent emails, newest first
get <id> one email by id; marks it read
code [--to A] [--since ISO] [--wait S] extract verification code/link (long-poll w/ --wait)
wait [--to A] [--timeout S] block until a new email arrives
send --to A --subject S (--text T | --html H) [--from F] [--from-name N] [--reply-to R] send email
reply <id> (--text T | --html H) [--subject S] reply to an inbound email (targets its Reply-To, else From)
init [--domain D] [--api-key K] [--run] guided Cloudflare setup; --run converges (rerun-safe)
config get|set|list|use manage profiles (multi-account)
status resolved profile + worker target
doctor [--deep] [--fix] diagnose; --deep checks provisioning, --fix repairs it
robot-docs machine schema of commands + codes
mcp run as a stdio MCP serverTypical agent flow: sign up somewhere, then
cloudmail code --wait 90 --json # poll for the code the signup just triggeredMCP mode
cloudmail mcp # stdio MCP server exposing: cloudmail_latest, cloudmail_list, cloudmail_get, cloudmail_code, cloudmail_wait, cloudmail_send, cloudmail_replyMulti-account / config
Profiles live in ~/.cloudmail/profiles.json (mode 0600). Switch with --profile/-p, or
set CLOUDMAIL_WORKER_URL + CLOUDMAIL_API_KEY for an ephemeral env profile (wins over saved).
cloudmail config set work --worker-url https://mail.acme.dev --api-key $KEY
cloudmail -p work latestArchitecture
Layout and conventions live in AGENTS.md; the frozen per-module contracts in
INTERFACES.md.
Develop
bun test # unit suite (never touches the network)
bun run test:live # opt-in e2e against your real account
bun run typecheck
bun run cli -- latest --jsonSecurity
This address is an identity root: password resets and magic links arrive here. Keep the
worker API_KEY secret and require human approval before an agent creates accounts, sends
mail, or clicks sensitive links. Sending is enabled per worker with a Cloudflare Email
Service send_email binding and SEND_FROM variable. A worker deployed without the
binding stays receive-only; POST /send returns 501 (SEND_DISABLED). To deploy
receive-only on purpose, delete the send_email entry from worker/wrangler.jsonc first.
License
MIT
