Agent-friendly JMAP email CLI. Flat verbs, JSON by default, exit codes that mean something.
Project description
jmax
Agent-friendly JMAP email CLI. Flat verbs, JSON-by-default, exit codes that mean something.
It exists because every other "mail CLI" is either Fastmail-only, written for humans with a pager and a color scheme, or shipped as part of a mail client. None of them are good for scripting or for AI agents. jmax is.
Install
uv tool install jmax
# or
uvx jmax inbox
Python 3.10+. Only runtime dependency is httpx.
Configure
Two ways. Both work. Pick whichever fits.
Interactive (recommended for humans):
jmax auth login https://mail.example.com --user you@example.com
# prompts for password; writes ~/.config/jmax/auth.json at mode 0600
Non-interactive variants: --password-stdin, --token-stdin, --token, --account-id, or --verify to hit the server before saving.
Env vars (recommended for agents / CI):
export JMAP_URL=https://mail.example.com
export JMAP_USER=you@example.com
export JMAP_PASSWORD=... # or JMAP_TOKEN for bearer auth
# optional:
export JMAP_ACCOUNT_ID=... # auto-discovered if omitted
Precedence: CLI flag > env var > auth file. Overriding a single value per call with --url / --user / --password / --token / --account-id works as you'd expect.
Inspect what's active with jmax auth status. Clear it with jmax auth logout.
The auth file is plaintext JSON at 0600. No keyring, no obscuring, no false sense of security — if you don't like that, use env vars and keep the secret in your OS keyring (e.g. JMAP_PASSWORD=$(security find-generic-password -w -s jmax)).
Commands
jmax auth login <url> [--user U] [--password-stdin | --token-stdin | --token T]
jmax auth logout
jmax auth status
jmax inbox [--unread] [--limit N] [--since 7d|24h|ISO] [--count]
jmax mailboxes
jmax search <query> [--from X] [--in MAILBOX] [--unread] [--since ...]
jmax get <id> [--headers | --body | --attachments | --subject]
jmax read <id>...
jmax unread <id>...
jmax flag <id>...
jmax unflag <id>...
jmax move <id>... --to <mailbox>
jmax trash <id>...
jmax delete <id>... [--permanent] [--yes]
jmax send --to X --subject Y [--body-file F | stdin]
jmax reply <id> [--body-file F | stdin]
jmax attachment <email-id> <filename> [-o FILE]
All commands accept --table (where applicable), --dry-run (where mutating), and --yes (where destructive).
Output contract
- Scalars → plain stdout.
jmax inbox --countprints42.jmax get e1 --subjectprintsHello. - Lists/objects → JSON by default. Pipe into
jq, loop over it, assert on it. - Mutations → silent on success. Noise is a bug.
--table→ human-readable table for lists. Opt-in, never the default.
Exit codes
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Ran fine, returned no results or the ID wasn't there |
2 |
Something broke — auth, network, bad args, server error |
So jmax search urgent && echo 'got some' works the way you'd hope.
Safety
Destructive operations refuse to run without --yes:
jmax delete --permanentjmax delete/jmax trashon more than 10 IDs at once
Every mutating command supports --dry-run, which prints the raw JMAP methodCall instead of executing. Hand that to an agent when you want it to "show its work" before the real run.
Recipes for agents
# How many unread emails in the last day?
jmax inbox --unread --since 24h --count
# Dump every sender from this month into a CSV:
jmax search "" --since 30d --limit 1000 | jq -r '.[] | .from' | sort -u
# Archive every newsletter:
jmax search "" --from "newsletter@" --limit 500 \
| jq -r '.[].id' \
| xargs -n50 jmax move --to Archive
# Show subject, then reply:
jmax get $ID --subject
echo "Thanks!" | jmax reply $ID
Design principles
- Do one thing per command. No multiplexed "smart" verbs.
- Trust the server. JMAP queries are cheap; there's no local cache, no sync, no database. Every invocation re-queries. The agent's memory is the agent's problem.
- Don't pretty-print by default. The default consumer is a pipe.
- Env vars win, a plaintext file fills the gaps. Stealing from
llmandmc— a single managed JSON file at~/.config/jmax/auth.json, not a TOML you hand-edit. Env vars always override. - Fail loud. No silent retries on auth failure. Fix the env var.
Supported servers
Built against Stalwart and Fastmail. Any RFC 8620 / RFC 8621 server should work — file an issue if yours doesn't.
License
MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file jmax-0.2.0.tar.gz.
File metadata
- Download URL: jmax-0.2.0.tar.gz
- Upload date:
- Size: 34.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
31f2a4dc1b051b5f17ac7d80bef91abdcc579d60c7c818c359e29f39ffbdc550
|
|
| MD5 |
3c91efdec0f28e9a60b4ce759f029237
|
|
| BLAKE2b-256 |
c09a0658c36eb6f8d652a406363b8bffc3caf878718d44c0989f6b9e8009bbba
|
Provenance
The following attestation bundles were made for jmax-0.2.0.tar.gz:
Publisher:
release.yml on amarcin/jmax
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
jmax-0.2.0.tar.gz -
Subject digest:
31f2a4dc1b051b5f17ac7d80bef91abdcc579d60c7c818c359e29f39ffbdc550 - Sigstore transparency entry: 1354424659
- Sigstore integration time:
-
Permalink:
amarcin/jmax@5379cf2265ae21f11d45c285fffc403836b28ab6 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/amarcin
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5379cf2265ae21f11d45c285fffc403836b28ab6 -
Trigger Event:
push
-
Statement type:
File details
Details for the file jmax-0.2.0-py3-none-any.whl.
File metadata
- Download URL: jmax-0.2.0-py3-none-any.whl
- Upload date:
- Size: 19.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4587eaa55a472558b793804fd5480e06a941c75e32311b7a391a4b76f1a96a9c
|
|
| MD5 |
c539a74004a8646cd532ad85b1b22d27
|
|
| BLAKE2b-256 |
d08f3f89ad2542494c70b637f0e6c5cb6df84a15a4a0e583490f0209f3a11a6d
|
Provenance
The following attestation bundles were made for jmax-0.2.0-py3-none-any.whl:
Publisher:
release.yml on amarcin/jmax
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
jmax-0.2.0-py3-none-any.whl -
Subject digest:
4587eaa55a472558b793804fd5480e06a941c75e32311b7a391a4b76f1a96a9c - Sigstore transparency entry: 1354424841
- Sigstore integration time:
-
Permalink:
amarcin/jmax@5379cf2265ae21f11d45c285fffc403836b28ab6 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/amarcin
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5379cf2265ae21f11d45c285fffc403836b28ab6 -
Trigger Event:
push
-
Statement type: