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
Env vars only — no dotfiles.
export JMAP_URL=https://mail.example.com/jmap # or https://api.fastmail.com/jmap
export JMAP_USER=you@example.com
export JMAP_PASSWORD=... # or JMAP_TOKEN for bearer auth
# optional:
export JMAP_ACCOUNT_ID=... # auto-discovered if omitted
Any of those can be overridden per-call with --url, --user, --password, --token, --account-id.
Commands
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-var config, no files. Like
gh, likeaws. Simpler to reason about. - 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.1.0.tar.gz.
File metadata
- Download URL: jmax-0.1.0.tar.gz
- Upload date:
- Size: 29.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bcf38db4ee72444ce21e6dae7587df31b1e6a0b246f0347add8742f5de0f9406
|
|
| MD5 |
6b2c8fc7b34f5515b550938d5144dc3a
|
|
| BLAKE2b-256 |
dffe9fe1e0785554579dad37e256e6c02f4e0f917a312f7eae094333b02b9a9b
|
Provenance
The following attestation bundles were made for jmax-0.1.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.1.0.tar.gz -
Subject digest:
bcf38db4ee72444ce21e6dae7587df31b1e6a0b246f0347add8742f5de0f9406 - Sigstore transparency entry: 1352322904
- Sigstore integration time:
-
Permalink:
amarcin/jmax@99be5fe48fb8bc7aea5c5ae24bbb22aec895e64a -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/amarcin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@99be5fe48fb8bc7aea5c5ae24bbb22aec895e64a -
Trigger Event:
push
-
Statement type:
File details
Details for the file jmax-0.1.0-py3-none-any.whl.
File metadata
- Download URL: jmax-0.1.0-py3-none-any.whl
- Upload date:
- Size: 15.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 |
23d3e409f8c37215d1b5775cc170e028789a7a5577356e98093c80b67814af94
|
|
| MD5 |
16e068dc84633a2b4bec3f0bd38c056f
|
|
| BLAKE2b-256 |
bcaaecfe112064129b9615ab4ec49146a891e17485f76f294111b79953cdfb35
|
Provenance
The following attestation bundles were made for jmax-0.1.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.1.0-py3-none-any.whl -
Subject digest:
23d3e409f8c37215d1b5775cc170e028789a7a5577356e98093c80b67814af94 - Sigstore transparency entry: 1352323042
- Sigstore integration time:
-
Permalink:
amarcin/jmax@99be5fe48fb8bc7aea5c5ae24bbb22aec895e64a -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/amarcin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@99be5fe48fb8bc7aea5c5ae24bbb22aec895e64a -
Trigger Event:
push
-
Statement type: