v0.5.0  ·  MIT  ·  Node ≥ 20

mmmail

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.

$ npm install -g @0xmmo/mmmail
Linux also needs libsecret-1-dev for keychain support.
Quickstart· Setup· Commands· Attachments· JSON / agents· Development

Quickstart

# 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 setup

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 · OAuth (one-time per machine, ~3 min)

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).

  1. Create a Google Cloud project — console.cloud.google.com/projectcreate
  2. Enable the Gmail API — console.cloud.google.com/apis/library/gmail
  3. Configure the OAuth consent screen (Audience: External) — console.cloud.google.com/auth/overview
  4. Add yourself as a test user — console.cloud.google.com/auth/audience
  5. Create OAuth credentials of type Desktop appconsole.cloud.google.com/auth/clients
  6. Copy the client ID + secret (or download the JSON), then run:
mmm add google --email [email protected] --credentials-file ./client_secret.json

Subsequent Gmail accounts on the same machine skip the project setup and only run the consent flow.

Microsoft 365 / Outlook.com · OAuth (zero Azure setup)

mmm ships with a built-in Microsoft Entra app (multi-tenant + personal accounts, public client + PKCE, no secret).

mmm add microsoft --email [email protected]

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:

mmm add microsoft --email [email protected] --client-id <your-id>
IMAP / SMTP · App password (Fastmail, iCloud, Yahoo, generic)

App password stored in your OS keychain; never written to the config file.

mmm add imap --email [email protected] --preset fastmail --password-stdin

Presets 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.

Commands

Every command takes -a, --account <email> to override the default account, and --json for machine-readable output.

Reading

mmm list20 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 foldersList available IMAP folders.

Writing

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>.

Mailbox actions

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.

Accounts & health

mmmInteractive dashboard (TTY); non-TTY prints help and exits 2.
mmm accountsList configured accounts.
mmm default <email>Set the active account.
mmm remove <email>Drop an account + its keychain entries.
mmm add google|microsoft|imapNon-interactive add. See Setup.
mmm doctorProbe every account; exits 1 on any failure — drop into CI as a tripwire.

Attachments

--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.

JSON output & agent notes

--json everywhere

Every data command — list, read, search, send, reply, mark, move, delete, folders, doctor, accounts, add — supports --json.

Fail-fast bare invocation

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.

Health gate

mmm doctor --json exits 1 if any account fails to connect.

Secrets off argv

--body-stdin, --password-stdin, --client-secret-stdin keep secrets out of argv and shell history.

Pagination

--uid-after <n> for incremental sync; --since / --before for time windows. Both list and search.

HTML off by default

read --json drops the HTML body (often 50× the text). Pass --include-html when you need it.

Development

$ 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