A CLI email client made for agents. Stateless commands — list, read, send, reply, search, mark, move, delete, folders, doctor — plus attachments, for Gmail (OAuth), Microsoft 365 (OAuth + PKCE), and any IMAP/SMTP host. Installs the mmm binary. --json on every data command.
libsecret-1-dev for keychain support.
# add an account — Gmail, Microsoft, or any IMAP host $ mmm init # list, read, search $ mmm list # 20 most recent in INBOX $ mmm list -n 50 --uid-after 12000 # incremental sync $ mmm read 1234 # headers + text body $ mmm search "invoice" # subject + body + from # send + attachments $ mmm send -t [email protected] -s "hi" -b "hello" $ mmm send -t [email protected] -s "q3" -b "see attached" \ --attach ./report.pdf --attach ./numbers.xlsx $ mmm reply 1234 -b "thanks" --attach ./signed.pdf # incoming attachments $ mmm read 1234 --save-attachments ./out # mailbox actions $ mmm mark 1234 --read --flagged $ mmm move 1234 "[Gmail]/All Mail" $ mmm delete 1234 # \Deleted + EXPUNGE # agent-friendly $ mmm doctor --json # exits 1 on any failure $ mmm list --json | jq '.[] | .from'
Account metadata lives in ~/.config/mmmail/config.json (mode 0600). Passwords, OAuth secrets, and refresh tokens go to your OS keychain via @napi-rs/keyring and never touch disk in plain.
Gmail's https://mail.google.com/ scope is restricted, so a shared OAuth client isn't possible. You'll create your own desktop OAuth client — mmm init walks you through it. Stays in testing mode (no Google verification, up to 100 test users).
Subsequent Gmail accounts on the same machine skip the project setup and only run the consent flow.
mmm ships with a built-in Microsoft Entra app (multi-tenant + personal accounts, public client + PKCE, no secret).
You'll see "mmmail" on the consent screen, approve, done.
Work/school accounts: many M365 tenants disable SMTP AUTH by default. If mmm send fails with SmtpClientAuthentication is disabled, ask your admin to enable authenticated client SMTP submission for your mailbox.
Prefer your own Entra app? Register one (any organizational directory + personal accounts), add http://localhost as a Mobile and desktop applications redirect URI, enable Allow public client flows, then:
App password stored in your OS keychain; never written to the config file.
mmm add imap --email [email protected] --preset fastmail --password-stdinPresets cover fastmail, icloud, yahoo. Without a preset, pass --imap-host, --imap-port, --smtp-host, --smtp-port, --imap-tls, --smtp-tls explicitly. Use --smtp-password-stdin if your provider requires a different password for SMTP.
Every command takes -a, --account <email> to override the default account, and --json for machine-readable output.
| mmm list | 20 most recent in a folder. -f <name>, -n <limit>, -u (unread only), --since, --before, --uid-after. |
| mmm read <uid> | Headers + text body. -f <folder>, --include-html (large), --save-attachments <dir>. |
| mmm search <q> | Full-text across subject / body / from. Same time + pagination flags as list. |
| mmm folders | List available IMAP folders. |
| mmm send | -t <addr...> -s <subj>. Body: -b <text>, --body-stdin, or omit for $EDITOR. -c <cc...>, -B <bcc...>, --attach <path...>. |
| mmm reply <uid> | Auto-threads (sets In-Reply-To, References). -b / --body-stdin, --all, --attach, -f <folder>. |
| mmm mark <uid> | --read / --unread, --flagged / --unflagged. |
| mmm move <uid> <dest> | Move to another folder, e.g. "[Gmail]/All Mail". |
| mmm delete <uid> | Permanent — sets \Deleted, EXPUNGEs. |
| mmm | Interactive dashboard (TTY); non-TTY prints help and exits 2. |
| mmm accounts | List configured accounts. |
| mmm default <email> | Set the active account. |
| mmm remove <email> | Drop an account + its keychain entries. |
| mmm add google|microsoft|imap | Non-interactive add. See Setup. |
| mmm doctor | Probe every account; exits 1 on any failure — drop into CI as a tripwire. |
--attach <path...> on send and reply (repeatable). On read, attachments are listed in the rendered output and in --json. --save-attachments <dir> writes the bytes to disk.
# attach one or more files (repeatable) $ mmm send -t [email protected] -s "Q3 numbers" -b "see attached" \ --attach ./report.pdf --attach ./spreadsheet.xlsx $ mmm reply 1234 --attach ./signed.pdf # attachments show up automatically when reading $ mmm read 1234 Subject: Q3 numbers Attachments: 1. report.pdf (application/pdf, 124.2 KB) 2. spreadsheet.xlsx (application/vnd.openxmlformats-…, 38.4 KB) # save them to disk — collisions get -1, -2, … suffixes $ mmm read 1234 --save-attachments ./out # JSON for agents — savedPath populated when --save-attachments is set $ mmm read 1234 --save-attachments ./out --json | jq .attachments
Filenames are sanitised (path separators stripped, leading dots removed); empty or invalid names fall back to attachment-N. JSON output includes filename, contentType, size, contentId, inline, and (when saved) savedPath.
Every data command — list, read, search, send, reply, mark, move, delete, folders, doctor, accounts, add — supports --json.
mmm with no subcommand in a non-TTY exits status 2, so scripts that forgot a command fail loudly instead of hanging on a dashboard.
mmm doctor --json exits 1 if any account fails to connect.
--body-stdin, --password-stdin, --client-secret-stdin keep secrets out of argv and shell history.
--uid-after <n> for incremental sync; --since / --before for time windows. Both list and search.
read --json drops the HTML body (often 50× the text). Pass --include-html when you need it.
$ git clone https://github.com/0xmmo/mmmail && cd mmmail $ npm install $ npm run cli -- list -n 5 # run from TS via tsx $ npm test $ npm run typecheck $ npm run build
To release: bump the version, push tags, then publish manually.
$ npm version minor $ git push --follow-tags $ npm login && npm publish