Mail Control
Use the local mail CLI instead of browser automation when it can complete the email task.
Command Resolution
- Use the globally linked
mail command. Verify it with command -v mail and mail --help; the check passes when help lists the list, search, send, and accounts subcommands.
- If
mail resolves to /usr/bin/mail or shows mail [-dEiInv], repair the global link from packages/cli with bun link, then verify mail --help again. A stale ~/.bun/bin/mail symlink may point to the retired @hub/mail-cli; remove that obsolete global package with bun remove --global @hub/mail-cli before relinking.
- Treat
bun run mail ... as a temporary diagnostic fallback only. Do not leave the environment using the repo-local command when the requested global CLI can be repaired.
- Confirm subcommand flags with
<resolved-command> <subcommand> --help before relying on remembered flags.
Accounts
Accounts are defined by the user in ~/.mail-control/config.json (override the
directory with MAIL_CONTROL_DIR). Each account has an id (the map key) and a
type of gmail or icloud.
- Pass
-a <id> / --account <id> whenever the account matters.
all is the default for list, search, and recent; use a concrete id for single-message mutations.
- Discover configured ids by reading
config.json or running a command and seeing the [id] tags in output.
- Capabilities depend on type: Gmail supports every command; iCloud supports read, send, archive, and trash.
- For iCloud mailbox-specific reads/lists/searches, pass
--mailbox <name> (e.g. "Sent Messages", Archive).
Credentials
Do not print credential, token, or password contents.
config.json holds identity only (safe to share). Secrets never live there.
- Run
mail accounts to see, per account, whether it is ready or what setup it still needs.
- Run
mail auth <id> to set an account up: for Gmail it runs the OAuth browser flow (or --manual for headless) after guiding credential creation; for iCloud it prompts for the app-specific password and writes ~/.mail-control/secrets.json (0600). Both verify by reading one message.
- Under the hood: Gmail uses OAuth credential + token JSON (
credentialsPath / tokenPath, default ~/.mail-control/<id>-credentials.json and <id>-token.json); iCloud resolves its password from MAIL_<ID>_APP_PASSWORD (or the account's appPasswordEnv) then secrets.json.
Workflows
Finding mail:
mail list # inbox-scoped listing, all accounts
mail list -a personal --unread
mail list -a work -q "from:someone@example.com"
mail search "invoice" # broader search, not inbox-only
mail recent --since 24h
mail recent --since 48h --json
mail read -a personal <message-id>
mail read -a icloud --mailbox "Sent Messages" <message-id>
Use --json for machine-readable output from list, search, recent, read, download, and mutation commands. Prefer JSON for scripts instead of parsing pretty terminal output.
Daily email review:
The goal is inbox zero across all accounts: a clear picture of every inbox, focused on unread mail, with explicit recommendations for what to archive, what to unsubscribe from, and what needs a response. Work across all inboxes at once with mail list -a all unless the user narrows the account.
- Start with simple listings, covering all inboxes.
mail list -a all --max 80 --json # every inbox at once (inbox-scoped)
mail recent --since 24h --max 80 --json # what's new
mail recent --since 7d --unread --max 80 --json
- Summarize every inbox into buckets, prioritizing unread but covering all messages:
- Needs response or decision.
- Security/account/billing notices worth reading.
- Safe archive candidates: already-handled notifications, receipts, calendar cancellations, no-action-required product notices.
- Possible unsubscribe candidates: recurring marketing/newsletters/promos the user is unlikely to want.
- Leave alone: personal, transactional, legal, medical, financial, or ambiguous messages.
State, per message, whether to archive, unsubscribe, respond, or leave it — the point is to drive the inbox toward zero, not just list mail.
- Propose exact actions before mutating mail:
Archive: account:id subject...
Unsubscribe: account:id sender subject...
Read/respond: account:id subject...
Leave: account:id subject...
- Only after explicit approval, run
mail archive, mail unsubscribe, mail mark-read, or mail trash for the specific approved messages/senders.
Sending mail:
mail send -a personal -t recipient@example.com -s "Subject" -b "Body"
mail send -a personal -t one@example.com -t two@example.com -s "Subject" -f body.txt
mail send -a personal -t recipient@example.com -s "Subject" -b "Body" -A file.pdf
Application-managed inbox smoke tests:
- Resolve the exact recipient from the application's provider configuration or provider API. Do not guess an address from product branding or treat a mailbox search as authoritative.
- Send from one concrete configured account with a unique marker and an explicit no-action/no-reply body.
- Verify one provider intake by that marker before concluding the mail path works. For cursor-based consumers, establish a new-only baseline first; never rewind a production cursor merely to manufacture a test message.
Replying and forwarding:
mail reply -a personal <message-id> -b "Reply body"
mail forward -a personal -t recipient@example.com <message-id>
Attachments and mutations:
mail download -a personal -o /tmp/mail-attachments <message-id>
mail archive -a personal <message-id>
mail archive -a icloud <message-id>
mail trash -a personal <message-id>
mail mark-read -a personal <message-id>
mail unsubscribe -a personal <message-id>
Safety
- For sending, replying, forwarding, archiving, trashing, marking read, or unsubscribing, make sure the user's intent is clear before executing the command.
- Prefer
list, search, and read before acting on a message unless the user gives an exact account and message ID.
- Never run
trash or other destructive mail commands unless the user clearly approved the specific sender/message/action.
- Use
-f <body-file> for longer drafted emails so the body is inspectable and shell quoting is not fragile.
- Never expose secrets from
~/.mail-control, .env, Gmail tokens, OAuth credentials, or iCloud app passwords.
Self-Iteration
If the CLI fails or appears stale, do not stop at the first error:
- Inspect
packages/cli/src for CLI behavior and packages/gmail/src for Gmail auth/API behavior.
- Re-run with a narrower account, mailbox, query, or message ID to isolate the failure.
- For code changes, verify with
bun run check-types and targeted tests; use bun run test when relevant.
Prefer small local fixes when they unblock the requested email task and are low risk.
1---2name: mail-control3description: Use the local mail-control CLI for Gmail and iCloud email tasks. Use when asked to list, search, read, inspect recent mail, unsubscribe, trash/delete mail, send, reply, forward, download attachments, archive, mark read, set up mail accounts, debug mail-control, or operate email from the terminal instead of a browser.4---56# Mail Control78Use the local `mail` CLI instead of browser automation when it can complete the email task.910## Command Resolution1112- Use the globally linked `mail` command. Verify it with `command -v mail` and `mail --help`; the check passes when help lists the `list`, `search`, `send`, and `accounts` subcommands.13- If `mail` resolves to `/usr/bin/mail` or shows `mail [-dEiInv]`, repair the global link from `packages/cli` with `bun link`, then verify `mail --help` again. A stale `~/.bun/bin/mail` symlink may point to the retired `@hub/mail-cli`; remove that obsolete global package with `bun remove --global @hub/mail-cli` before relinking.14- Treat `bun run mail ...` as a temporary diagnostic fallback only. Do not leave the environment using the repo-local command when the requested global CLI can be repaired.15- Confirm subcommand flags with `<resolved-command> <subcommand> --help` before relying on remembered flags.1617## Accounts1819Accounts are defined by the user in `~/.mail-control/config.json` (override the20directory with `MAIL_CONTROL_DIR`). Each account has an id (the map key) and a21`type` of `gmail` or `icloud`.2223- Pass `-a <id>` / `--account <id>` whenever the account matters.24- `all` is the default for `list`, `search`, and `recent`; use a concrete id for single-message mutations.25- Discover configured ids by reading `config.json` or running a command and seeing the `[id]` tags in output.26- Capabilities depend on type: Gmail supports every command; iCloud supports read, send, archive, and trash.27- For iCloud mailbox-specific reads/lists/searches, pass `--mailbox <name>` (e.g. `"Sent Messages"`, `Archive`).2829## Credentials3031Do not print credential, token, or password contents.3233- `config.json` holds identity only (safe to share). Secrets never live there.34- Run `mail accounts` to see, per account, whether it is `ready` or what setup it still needs.35- Run `mail auth <id>` to set an account up: for Gmail it runs the OAuth browser flow (or `--manual` for headless) after guiding credential creation; for iCloud it prompts for the app-specific password and writes `~/.mail-control/secrets.json` (0600). Both verify by reading one message.36- Under the hood: Gmail uses OAuth credential + token JSON (`credentialsPath` / `tokenPath`, default `~/.mail-control/<id>-credentials.json` and `<id>-token.json`); iCloud resolves its password from `MAIL_<ID>_APP_PASSWORD` (or the account's `appPasswordEnv`) then `secrets.json`.3738## Workflows3940Finding mail:4142```bash43mail list # inbox-scoped listing, all accounts44mail list -a personal --unread45mail list -a work -q "from:someone@example.com"46mail search "invoice" # broader search, not inbox-only47mail recent --since 24h48mail recent --since 48h --json49mail read -a personal <message-id>50mail read -a icloud --mailbox "Sent Messages" <message-id>51```5253Use `--json` for machine-readable output from `list`, `search`, `recent`, `read`, `download`, and mutation commands. Prefer JSON for scripts instead of parsing pretty terminal output.5455Daily email review:5657The goal is inbox zero across all accounts: a clear picture of every inbox, focused on unread mail, with explicit recommendations for what to archive, what to unsubscribe from, and what needs a response. Work across all inboxes at once with `mail list -a all` unless the user narrows the account.58591. Start with simple listings, covering all inboxes.6061```bash62mail list -a all --max 80 --json # every inbox at once (inbox-scoped)63mail recent --since 24h --max 80 --json # what's new64mail recent --since 7d --unread --max 80 --json65```66672. Summarize every inbox into buckets, prioritizing unread but covering all messages:6869- Needs response or decision.70- Security/account/billing notices worth reading.71- Safe archive candidates: already-handled notifications, receipts, calendar cancellations, no-action-required product notices.72- Possible unsubscribe candidates: recurring marketing/newsletters/promos the user is unlikely to want.73- Leave alone: personal, transactional, legal, medical, financial, or ambiguous messages.7475State, per message, whether to archive, unsubscribe, respond, or leave it — the point is to drive the inbox toward zero, not just list mail.76773. Propose exact actions before mutating mail:7879```text80Archive: account:id subject...81Unsubscribe: account:id sender subject...82Read/respond: account:id subject...83Leave: account:id subject...84```85864. Only after explicit approval, run `mail archive`, `mail unsubscribe`, `mail mark-read`, or `mail trash` for the specific approved messages/senders.8788Sending mail:8990```bash91mail send -a personal -t recipient@example.com -s "Subject" -b "Body"92mail send -a personal -t one@example.com -t two@example.com -s "Subject" -f body.txt93mail send -a personal -t recipient@example.com -s "Subject" -b "Body" -A file.pdf94```9596Application-managed inbox smoke tests:97981. Resolve the exact recipient from the application's provider configuration or provider API. Do not guess an address from product branding or treat a mailbox search as authoritative.992. Send from one concrete configured account with a unique marker and an explicit no-action/no-reply body.1003. Verify one provider intake by that marker before concluding the mail path works. For cursor-based consumers, establish a new-only baseline first; never rewind a production cursor merely to manufacture a test message.101102Replying and forwarding:103104```bash105mail reply -a personal <message-id> -b "Reply body"106mail forward -a personal -t recipient@example.com <message-id>107```108109Attachments and mutations:110111```bash112mail download -a personal -o /tmp/mail-attachments <message-id>113mail archive -a personal <message-id>114mail archive -a icloud <message-id>115mail trash -a personal <message-id>116mail mark-read -a personal <message-id>117mail unsubscribe -a personal <message-id>118```119120## Safety121122- For sending, replying, forwarding, archiving, trashing, marking read, or unsubscribing, make sure the user's intent is clear before executing the command.123- Prefer `list`, `search`, and `read` before acting on a message unless the user gives an exact account and message ID.124- Never run `trash` or other destructive mail commands unless the user clearly approved the specific sender/message/action.125- Use `-f <body-file>` for longer drafted emails so the body is inspectable and shell quoting is not fragile.126- Never expose secrets from `~/.mail-control`, `.env`, Gmail tokens, OAuth credentials, or iCloud app passwords.127128## Self-Iteration129130If the CLI fails or appears stale, do not stop at the first error:131132- Inspect `packages/cli/src` for CLI behavior and `packages/gmail/src` for Gmail auth/API behavior.133- Re-run with a narrower account, mailbox, query, or message ID to isolate the failure.134- For code changes, verify with `bun run check-types` and targeted tests; use `bun run test` when relevant.135136Prefer small local fixes when they unblock the requested email task and are low risk.