1mail.dev
@1mail.dev

1mail CLI

The same reads as one curl command, without writing the curl: new, list, read, raw, upgrade.

Every inbox at 1mail.dev is public. Anyone who knows the address can read it. Never use these addresses for anything sensitive — or any account you care about.

Overview

The CLI is a client, not a new surface: it wraps the same three GETs as the API and the same tools as the MCP server, counts against the same read limits (60/min per inbox, 600/min per IP), and sees the same 24-hour retention. Nothing is stored between commands beyond a small update note (see Update).

Platforms: macOS and Linux on arm64 and x64, plus Windows x64 under Git Bash or WSL. An inbox argument accepts a bare local part (qa) or a full address (qa@1mail.dev); letter case never matters. Every inbox is public — the banner above is the contract.

Install

Installs to ~/.local/bin (override with ONEMAIL_INSTALL_DIR); the script verifies the sha256 published beside each binary before installing anything, and tells you what to add to PATH if needed.

curl -fsSL https://1mail.dev/install.sh | bash

Direct downloads, if you'd rather not pipe a script: the manifest lists every binary with its checksum — 1mail-darwin-arm64, 1mail-darwin-amd64, 1mail-linux-amd64, 1mail-linux-arm64, 1mail-windows-amd64.exe — under 1mail.dev/cli/latest/, with pinned versions under 1mail.dev/cli/v1.0.0/.

Package managers

npm i -g 1mail

One-off, no install — npx

npx 1mail <command>

Runs any command once with the latest release; nothing stays installed. Handy in scripts and CI: npx 1mail list <inbox> --json.

Every channel installs the same launcher and platform binary. Package-manager installs update through their manager — e.g. npm i -g 1mail@latest — and 1mail upgrade defers to them by design; only the curl install updates itself.

Update

1mail upgrade fetches the published manifest, downloads the build for your platform, verifies its sha256, and atomically replaces the running executable — any failure leaves your current binary untouched. Re-running the installer does the same thing.

$ 1mail upgrade
1mail 1.0.0 → 1.0.2
downloading 1mail-darwin-arm64…
verified 6.1 MiB — installed
upgraded: 1mail 1.0.2

1mail also checks for updates at most once per day and, when one exists, prints a single line to stderr — stdout and --json pipelines are never touched. Set NO_UPDATE_NOTIFIER=1 to silence it entirely.

Commands

1mail newLink to this command

Generate fresh, unguessable inbox addresses — locally, with no network call. One address per line, ready to hand to whatever sends mail.

1mail new                 # k7m2xq9vbt4w@1mail.dev
1mail new --count 3       # three of them

Twelve characters from an unambiguous alphabet (no i, l, o) ≈ 60 bits of randomness — anyone who knows the address can read its inbox, so it must not be guessable. The result is valid by construction (there is no creation step on the service) and never a reserved or operator local part.

FlagDefaultDescription
--count N1How many addresses to print (1–100).
--jsonoffn/a — new is offline; output is one address per line.

1mail listLink to this command

List an inbox, newest first: received, from, subject, id. An inbox with no messages yet is not an error — it materializes on its first mail.

1mail list qa
1mail list qa --limit 50
1mail list qa --since-id 0192a0c0-0000-7000-8000-000000000001 --json | jq '.messages[0]'
1mail list qa --since-id 0192a0c0-0000-7000-8000-000000000001 --wait 20000
FlagDefaultDescription
--limit N10Page size, 1–1000. (The API's own default is 100; the CLI narrows it for the table view.)
--since-id ID—Only messages newer than this id — your poll cursor.
--older-than-id ID—Page back from this id.
--wait MSoffHold up to MS ms (0..30000) for mail newer than --since-id — one held request instead of a poll loop. An empty answer means the window elapsed; run again to keep waiting. Not with --older-than-id.
--jsonoffPrint the API payload verbatim instead of the table.
--api URL—Override the API base (default https://api.1mail.dev/v1, or env ONEMAIL_API_BASE).

1mail readLink to this command

Print one parsed message: From/To/Subject, received and expiry times, the message id, attachment metadata, and the text part.

1mail read qa 0192a0c0-0000-7000-8000-000000000001
1mail read qa 0192a0c0-… --json | jq -r '.html'   # the html part

The id is the one list prints. The CLI never renders HTML: --json prints the API payload verbatim — including the unsanitized html field, which is untrusted by contract; treat it accordingly. If a message has no text part, read says so and points at --json or raw.

FlagDefaultDescription
--jsonoffPrint the API payload verbatim (headers, html, attachments).
--api URL—Override the API base (default https://api.1mail.dev/v1, or env ONEMAIL_API_BASE).

1mail rawLink to this command

Print the untouched original message bytes (message/rfc822) to stdout.

1mail raw qa 0192a0c0-… > message.eml

Exactly what the sender's server delivered — headers, MIME structure, attachments. Assert on this in tests when the parsed view is not enough.

1mail upgradeLink to this command

Update the 1mail binary in place. See Update, above, for the exact protocol.

1mail upgrade             # "1mail is up to date (1.0.0)" when it is

1mail help · versionLink to this command

Usage, per-command help, and the installed version.

1mail                     # the same as: 1mail help
1mail help list           # per-command help; flags work too: 1mail list --help
1mail version             # 1mail 1.0.0 — also: 1mail -v / 1mail --version

Scripting

The reliable wait-for-mail loop — generate an address, point the app at it, and let each call hold server-side for up to 20 s (--wait — one read per call, however long it holds):

addr=$(1mail new)
# …trigger the email to "$addr", then:
until 1mail list "$addr" --wait 20000 --json | jq -e '.messages | length > 0' >/dev/null; do
  : # each call held 20 s server-side — retry immediately
done
1mail list "$addr" --json | jq -r '.messages[0].id'

--since-id returns only newer messages, so steady-state waits stay cheap; messages expire after 24 hours, so long-running scripts should check expires_at rather than wait forever. Reads are rate limited — the same published limits as every other client.