A fast, scriptable command-line tool for Gmail and iCloud mail, built on
Effect. List, search, read, send, reply, forward,
download attachments, archive, trash, mark read, and one-click unsubscribe —
across as many accounts as you like, from one mail command.
Accounts are user-defined in a config file, so nothing about the tool is tied
to a particular person's setup. Add a Gmail or iCloud account by adding an entry
to ~/.mail-control/config.json.
mail list -a all --max 20 # every configured inbox at once
mail search -a work "invoice" # search one account
mail recent --since 24h --json # machine-readable recent mail
mail send -a personal -t a@b.com -s "Hi" -b "Hello"
mail tui # interactive unified inboxbun install
bun run buildThe CLI entrypoint is packages/cli/bin/mail. Link it globally from the CLI
package so mail works from every directory:
cd packages/cli
bun link
mail --helpUse bun run mail ... only as a repo-local diagnostic fallback.
mail-control reads ~/.mail-control/config.json. Each entry under accounts has
a type (gmail or icloud) and the account's non-secret settings. The map key
is the account id you pass to -a/--account.
Copy config.example.json to ~/.mail-control/config.json
and edit. Set the home directory elsewhere with MAIL_CONTROL_DIR.
For a Gmail account, credentialsPath / tokenPath default to
~/.mail-control/<id>-credentials.json and ~/.mail-control/<id>-token.json;
override them to point at existing files.
Then authorize each account with mail auth <id> and check setup at any time with
mail accounts:
mail accounts # per-account: ready, or what's still needed
mail auth personal # walks you through Gmail OAuth / iCloud passwordRule of thumb: config.json is identity (safe to share); secrets live
elsewhere. Secrets are resolved through an ordered chain so you are never
forced to use environment variables:
- Environment variable — an override, ideal for CI/headless.
~/.mail-control/secrets.json— a0600file, the default local store.- (pluggable) an OS keychain provider can slot in as a later step.
Gmail uses OAuth, and mail auth <id> runs the whole flow:
- Create an OAuth Desktop client in Google Cloud and download the client
secret JSON. Running
mail auth <id>before that file exists prints the exact console steps and the path to save it to. - Run
mail auth <id>— it opens your browser, captures the grant, writes the token to the account'stokenPath, and verifies by reading one message.
On a headless machine (no browser), use mail auth <id> --manual to print a URL
and paste the resulting code.
iCloud uses an app-specific password. The easiest path is:
mail auth <id> # prompts (hidden) and writes secrets.json (0600), then verifiesOn a headless machine, pipe it instead: printf '%s' "$APP_PW" | mail auth <id>.
You can also provide it without the command — via the MAIL_<ID>_APP_PASSWORD
environment variable (override the name per account with "appPasswordEnv"), or a
~/.mail-control/secrets.json entry (0600):
{ "accounts": { "icloud": { "appPassword": "abcd-efgh-ijkl-mnop" } } }IMAP/SMTP hosts default to iCloud's and can be overridden per account
(imapHost, imapPort, imapSecure, smtpHost, smtpPort, smtpSecure,
mailbox).
# Set up
mail accounts # list configured accounts and their setup status
mail auth <id> # authorize an account (Gmail OAuth / iCloud password)
mail auth <id> --manual # headless: paste a code instead of opening a browser
mail tui # launch the interactive terminal inbox
# Read
mail list [-a <id>|all] [--unread|--read] [-q query] [--max N] [--mailbox M]
mail search [-a <id>|all] <query> [--max N] # searches beyond the inbox
mail recent [-a <id>|all] --since 24h [--max N] # 24h, 2d, 6w, 12mo, 1y
mail read -a <id> <message-id> [--mailbox M]
mail download -a <id> <message-id> -o ./dir
# Write (Gmail supports all; iCloud supports send)
mail send -a <id> -t to@x.com -s "Subject" -b "Body" [-A file]
mail reply -a <id> <message-id> -b "Body"
mail forward -a <id> <message-id> -t to@x.com
# Mutate (Gmail; iCloud supports archive/trash/unsubscribe)
mail archive -a <id> <message-id>
mail trash -a <id> <message-id>
mail mark-read -a <id> <message-id>
mail unsubscribe -a <id> <message-id>-a/--account accepts any id from your config, or all (default for
list/search/recent). Add --json to any read/mutation command for
machine-readable output. Use -f/--body-file for long message bodies.
Capabilities are determined by account type: Gmail supports every command;
iCloud supports read, send, archive, trash, and unsubscribe (for messages that
expose standards-based List-Unsubscribe headers).
config.jsonandsecrets.jsonare written0600.- The TUI keeps a private
0600SQLite cache at~/.mail-control/cache.sqliteso mailbox views and opened message bodies restore immediately after restart. Delete that file to clear cached mail. Entries are pruned automatically. - Secrets never appear in
config.json, so it is safe to commit or share. .envand credential/token files are gitignored.- Tokens and passwords are held as Effect
Redactedvalues and never logged.
An agent skill for driving this CLI lives in
skills/mail-control/SKILL.md.
bun run check-types
bun run test
bun run lintThe repo is a small workspace: packages/gmail is a standalone Effect-based Gmail
client, and packages/cli is the multi-account CLI that unifies Gmail and iCloud.
{ "accounts": { "personal": { "type": "gmail" }, "work": { "type": "gmail", "credentialsPath": "~/.mail-control/work-credentials.json", "tokenPath": "~/.mail-control/work-token.json" }, "icloud": { "type": "icloud", "email": "you@icloud.com" } } }