← Tools

tmail

Tags

RustCLIEmailAgents

Agent-first disposable email CLI in Rust: mint throwaway inboxes, block until a verification code arrives, and send outbound mail over SMTP. JSON-only output, stable exit codes, never prompts.

A single-binary CLI for disposable email, built for agents rather than humans. Mint a real throwaway inbox, block until the verification code lands, and pull it out as JSON, without a browser or a polling loop.

#Features

  • Mint — Fresh, real disposable inboxes via mail.tm
  • Blocking readswait and otp hold until a matching message arrives, so no polling loop in your agent
  • Code extractionotp renders HTML bodies to text, collapses separators (481-920481920), and pulls the code
  • Send — Outbound mail through your own SMTP account, with CC/BCC, attachments, and HTML bodies
  • Stateless mode--stateless returns a portable handle instead of writing local state
  • Agent contract — JSON-only stdout, stable exit codes, never prompts for input

#Installation

curl --proto '=https' --tlsv1.2 -LsSf https://raw.githubusercontent.com/raymond-UI/tmail/main/install.sh | sh

Set TMAIL_VERSION to pin a release, or TMAIL_INSTALL_DIR to change the install location.

From source:

cargo install --git https://github.com/raymond-UI/tmail

Prebuilt binaries for macOS (arm64/x86_64), Linux (x86_64/aarch64/musl), and Windows (x86_64) are on the releases page.

#Usage

#Mint an inbox

bash
tmail new
json
{
  "id": "a1b2c3",
  "address": "k7f2x9@punkproof.com",
  "provider": "mail.tm",
  "createdAt": "2026-06-27T18:40:00Z"
}

#Sign up and grab the code

otp blocks until a message matching the filters arrives, then extracts the verification code:

bash
addr=$(tmail new | jq -r .address)
# ... drive the signup flow with $addr ...
code=$(tmail otp "$addr" --from noreply@target.com --timeout 180 | jq -r .code)
json
{
  "code": "481920",
  "msgId": "msg_01",
  "from": "noreply@github.com",
  "matchedBy": "default-digits"
}

Extraction scans the text body for the first \b(\d{4,8})\b run near keywords like code, otp, verify, or pin, then falls back to the first standalone 4–8 digit run. Override with --pattern <regex> or --len <n>. If a message matches the filter but yields no code, you get exit 9 NO_MATCH rather than a silent empty string.

#Wait for any message

Same filters as otp, but returns the full message instead of a code:

bash
tmail wait "$addr" --subject "Reset your password" --timeout 120

Baseline is the inbox creation time (minus a clock-skew guard). For handles created before the wait starts, tmail snapshots existing message IDs and resolves on the first new one.

#Read and fetch

bash
tmail read "$addr" --unread --limit 20 | jq '.[] | {from, subject}'
tmail get "$addr" msg_01 --text

read is non-blocking and newest-first. get prefers HTML and returns a text rendering; --html gives you the raw body, --text forces plain.

#Send

Body comes from a flag, a file, or stdin:

bash
echo "$body" | tmail send --to user@example.com --subject "Re: request" --html
bash
tmail send \
  --to dest@example.com \
  --cc team@example.com \
  --subject "Report" \
  --body-file ./report.md \
  --attach ./report.pdf \
  --reply-to me@example.com

Requires SMTP credentials in config; --from defaults to [smtp].from.

#Stateless handles

Skip local state entirely, which suits ephemeral agent sandboxes:

bash
out=$(tmail new --stateless)
h=$(echo "$out" | jq -r .handle)
tmail wait --handle "$h" --timeout 120

With --handle (or TMAIL_HANDLE), the <id|address> positional becomes optional on receive commands. If you pass both, the handle wins and the positional must match, or you get exit 7 CONFIG.

#Clean up

bash
tmail rm "$addr"

Deletes upstream, then forgets locally, and is idempotent.

#Commands

CommandBlockingNotes
newnoMint an inbox; --stateless returns a handle
lsnoLocal inboxes, newest first
read <id|address>noMessage list; --unread, --since, --limit
get <id|address> <msgId>noFull body; --html, --text
wait <id|address>yesFirst matching new message
otp <id|address>yesExtracts verification code
rm <id|address>noDelete upstream + local
sendnoSMTP send; body from flag, file, or stdin

Global flags: --json (default), --pretty, -v, --handle <blob>, --config <path>, --timeout <secs>. Command-level --timeout beats the global flag, which beats [defaults].wait_timeout_secs.

#Exit codes

Every failure prints exactly one JSON error to stdout and a human-readable line to stderr:

json
{
  "error": {
    "code": "RATE_LIMITED",
    "message": "mail.tm is cooling down",
    "retryAfterMs": 30000
  }
}
ExitCodeMeaning
0Success
1GENERICUnexpected / unclassified
2NOT_FOUNDUnknown inbox or message id
3RATE_LIMITEDProvider 429; includes retryAfterMs
4ALL_PROVIDERS_DOWNNo inbox could be minted
5AUTHBad or missing SMTP / provider credentials
6TIMEOUTwait / otp deadline passed
7CONFIGMalformed config or missing setting
8NETWORKConnection, DNS, or TLS failure
9NO_MATCHotp matched a message but found no code
View on GitHub·raymond-UI/tmail
Actions
Open