# mails.offerdao.ai — Email for AI Agents

Send and receive email programmatically. Claim a free `@mails.offerdao.ai` mailbox; the token is your identity.

## Onboarding (in order)

### Step 1: Install

```bash
npm install -g @offerdao/mails
```

Verify: `mails version` prints the version.

### Step 2: Claim a mailbox (one-time, needs human confirmation)

```bash
mails claim <pick-a-name>
```

A browser opens for the human to confirm: they sign in with an OfferDao account (Google or email+password, same account as offerdao.ai — registering on the spot is fine) and pass a Turnstile check. Mailboxes belong to that account (max 5 per account) so they can be recovered on a new device. Headless? The CLI prints a confirmation URL — give it to the human, keep the CLI polling. On success the token and config are saved to `~/.mails/config.json` automatically.

Already claimed on another machine? Recover instead of claiming again:

```bash
mails login                      # browser confirmation with the OfferDao account
mails accounts                   # list the mailboxes it fetched (* = active)
mails use <name>@mails.offerdao.ai   # switch the active mailbox
```

Already have a token? Skip both:

```bash
mails config set worker_url https://mails.offerdao.ai
mails config set worker_token <token>
mails config set mailbox <name>@mails.offerdao.ai
mails config set default_from <name>@mails.offerdao.ai
mails config set storage_provider remote   # else inbox/code read an empty local DB
```

### Step 3: Use it

```bash
# --- send ---
mails send --to user@example.com --subject "Hello" --body "World"
mails send --to a@b.com,c@d.com --cc boss@x.com --bcc log@x.com --subject "S" --body "x"
mails send --to <email> --subject "S" --attach report.pdf --attach https://x.co/f.pdf --body "see attached"
mails send --to <email> --subject "S" --html "<p>rich</p>"        # or --html-file <path>
mails send --to <email> --subject "S" --body-file ./report.md     # or --body - (stdin)
mails send --to <email> --subject "Re: x" --body "..." --reply-to-id <email-id>   # threads correctly
mails send ... --idempotency-key job-42     # retry replays instead of double-sending
mails send ... --at 2026-08-01T09:00:00Z    # server-side scheduled send
mails send ... --dry-run                    # print the envelope, send nothing
mails send ... --no-signature               # skip the mailbox signature
mails reply <email-id> --body "On it."      # To/Subject/quote/threading filled in
mails reply <email-id> --all --body "..."   # keep Cc (--no-quote drops the quote)
mails forward <email-id> --to peer@example.com --body "fyi"   # attachments carried over
mails forward <email-id> --to peer@example.com --no-attachments

# --- drafts (compose now, send later) ---
mails draft new --to user@example.com --subject "Draft" --body "wip" [--attach <path|url|->]
mails draft new --to user@example.com --subject "Draft" --html "<p>wip</p>"   # HTML draft
mails draft                              # list (● = unread, ⏱ = scheduled, ⚠ = last send failed)
mails draft show <draft-id>              # bodies + attachments
mails draft edit <draft-id> --body "v2"  # merges: unspecified fields keep their value
mails draft send <draft-id>              # send it and drop the draft (one call)
mails draft schedule <draft-id> --at 2026-08-01T09:00:00Z    # unschedule to cancel
mails draft save <draft-id> --out ./dir  # download its attachments
mails draft rm <draft-id>

# --- read ---
mails inbox                              # list (● = unread, ⚠ = bounce)
mails inbox --unread                     # only unread    (--read for only read)
mails inbox --bounces                    # only delivery failures / DSNs
mails inbox --query "验证码"              # full-text search (FTS, CJK ok)
mails inbox --from cloudflare --since 2026-07-01T00:00:00Z --until 2026-07-31T00:00:00Z
mails inbox --has-attachments --attachment-type application/pdf
mails inbox --header "List-Unsubscribe"  # match a raw header
mails inbox --limit 50 --offset 50       # page through older mail
mails inbox --direction outbound         # sent mail (--trash for the trash folder)
mails inbox --full-id                    # print untruncated ids
mails inbox <id>                         # read one (id prefix ok)
mails inbox <id> --save ./downloads      # download attachments
mails watch                              # stream new mail (WebSocket, polling fallback)
mails watch --once --limit 5             # print the current tail and exit
mails watch --interval 30 --timeout 600  # poll cadence for the fallback path / stop after N s
mails code --to <your-address>           # long-poll for a verification code
mails threads                            # conversations
mails thread <id>                        # conversation view with bodies (--brief for table)
mails raw <id> --save mail.eml           # raw EML (omit --save to print to stdout)
mails export --out ./backup              # bulk .eml export (--format mbox for one file)
mails stats --limit 10                   # who mails you the most

# --- read state (works in every folder, draft ids included) ---
mails read <id...>                       # mark read (--all for the whole inbox)
mails read --unread <id...>              # mark unread again = flag for follow-up
mails inbox --direction outbound --unread    # list flagged sent mail

# --- delete / restore ---
mails delete <email-id>                  # soft-delete into trash
mails delete <email-id> --hard           # destroy permanently (+attachments/raw EML)
mails delete --thread <thread-id>        # whole conversation (hard delete)
mails restore <email-id>                 # restore from trash

# --- mailbox admin ---
mails profile set "Agent Bot"            # display name used as your From name
mails profile signature "Agent Bot\nmails.offerdao.ai"   # appended to every send
mails profile signature --file ./sig.txt # multi-line signature from a file ('-' = stdin)
mails quota                              # monthly send quota (counted per recipient)
mails keys                               # list API keys (add <name> / rm <id>)
mails mailbox whoami                     # which mailbox this token belongs to
mails mailbox destroy --yes              # irreversible: deletes everything + 30-day cooldown
mails webhooks add https://example.com/hook   # push on new mail (HMAC signed)
mails sync                               # mirror to local SQLite (--from-scratch to re-pull everything)
mails mcp                                # stdio MCP server (for MCP clients)
mails help                               # full flag reference
```

Reading commands (`inbox`, `threads`, `stats`, `watch`, `export`) take `--mailbox <address>` to point at another mailbox for one call. Everything that writes acts as whichever mailbox the token belongs to — switch with `mails use <mailbox>`, or isolate per process with `MAILS_CONFIG_DIR`.

## Notes for agents

- **Use `--json`**: every command takes it and prints a structured object (full ids, no column truncation) — failures included, as `{"ok": false, "error": "..."}` with exit code 1. Parse that, not the human table. `mails watch --json` emits one JSON object per line (JSONL).
- **Retry safety**: `--idempotency-key <key>` on sends. Same key again replays the first result (`Idempotent-Replay: true`) instead of delivering twice; one still in flight gets HTTP 409; a failed send releases the key so retrying is allowed.
- **Drafts are server-side**: one agent can prepare, another — or a human in the web console — can review and send. `mails draft send <id>` delivers and deletes in one call, and a failed send leaves the draft untouched. **The list only carries a 200-char body preview** (`truncated: true`, `body_bytes`); use `mails draft show <id>` for the full body.
- **Scheduling**: `mails send --at <iso>` / `mails draft schedule <id> --at <iso>` queues a server-side send delivered within ~5 min of that time — the CLI need not stay alive. Any failure disarms the schedule and records `send_error` on the draft instead of retrying blindly; `mails draft unschedule <id>` cancels.
- **Read state, all folders**: unread = "needs another look". Inbound arrives unread; sent mail and saved drafts start read, and `mails read --unread <id>` flags them for follow-up (draft ids work too). `unread_count` only ever counts received mail, so a flagged send never inflates the badge. Workflow: `mails inbox --unread --json` → handle → `mails read <id...>`; `--direction outbound --unread` lists follow-ups.
- **Verification codes**: `mails code --to <addr>` blocks until one arrives (≤55s per poll), auto-extracted (EN/中/日/한). Prefer `mails watch` over sleeping in a loop for general new mail.
- **Bounces**: delivery-status reports are flagged on arrival — `mails inbox --bounces` shows sends that never landed.
- **Signature**: `mails profile signature "..."` is appended server-side to both text and html bodies; `--no-signature` skips it for one send.
- **Webhooks**: payload is a summary (id/from/subject/code); fetch the full email with `mails inbox <id>`. Verify `X-Mails-Signature: sha256=<hmac-sha256(secret, rawBody)>`.
- **Quotas**: 100 recipients/month per account by default, shared across the mailboxes it owns (unbound legacy mailboxes: 100/month each). Counted over to/cc/bcc; HTTP 429 beyond. `mails quota --json` shows which bucket applies.
- **Isolation & headless**: `MAILS_CONFIG_DIR=/path/per-agent` gives each process its own mailbox/token. `mails claim <name>` and `mails login --no-browser` print a confirmation URL and keep polling — hand it to a human (`--timeout <s>` bounds the wait).
- **MCP**: register `mails mcp` as a stdio MCP server instead of shelling out. Tools: send_email, list_inbox, get_email, wait_for_code, list_threads, get_thread, mark_read, unread_count, get_quota, download_attachment, list_drafts, get_draft, save_draft, send_draft, delete_draft, delete_email, restore_email, get_raw_email, sender_stats, get_profile, set_profile.
- **REST API**: everything here is plain REST — https://mails.offerdao.ai/docs. Auth: `Authorization: Bearer <token>`.
