Email CLI
Send, receive, and manage email from your terminal. Built for AI agents.
A single binary that gives your AI agent a real email address. Send, receive, reply, draft, sync, and manage contacts, broadcasts, segments, and topics through Resend -- all from the command line. No IMAP. No browser inbox. No MCP server. Just a local SQLite mailbox and 70+ commands.
Why This Exists | Install | How It Works | Commands | Mailing-list Workflows | Configuration | Contributing
Why This Exists
AI agents need to send and receive email. The existing options are bad:
- IMAP/SMTP requires complex server configuration, credential management, and connection handling. Agents struggle with it.
- Email APIs work for sending, but agents need a local mailbox to track threads, drafts, and read status.
- Browser-based inboxes are not scriptable. Agents cannot use them.
Email CLI solves this by wrapping the Resend API in a local-first CLI. Your agent gets a verified email address, a local SQLite mailbox, and structured JSON output with semantic exit codes. It calls agent-info once to learn every command, then works from memory.
You can also use it as a human. It works the same way.
Install
One-line install (pre-built binary, no Rust required)
|
Homebrew
Cargo
Update
Quick Start
# 1. Add your Resend API key
# 2. Verify domain connectivity
# 3. Register a sending identity
# 4. Send an email
# 5. Sync and read incoming mail
# 6. Reply (threads correctly with In-Reply-To + References)
# 7. Reply All (preserves CC recipients)
# 8. Forward
# 9. Thread a new send into an existing conversation
How It Works
Three concepts:
- Profile -- a Resend API key. You can have multiple profiles for different Resend accounts.
- Account -- a sender/receiver identity (
agent@yourdomain.com). Each account belongs to a profile. - Local mailbox -- a SQLite database that stores messages, drafts, attachments, and sync cursors.
Resend handles delivery. Email CLI handles the local operating model that agents need: read tracking, threading, drafts, batch sends, and structured output.
┌────────────────────────────────┐
│ Your Agent / You │
│ (Claude, Codex, Gemini) │
└──────────────┬─────────────────┘
│ CLI commands
▼
┌────────────────────────────────┐
│ email-cli │
│ 50+ commands, JSON output, │
│ semantic exit codes │
└──────────┬─────────┬───────────┘
│ │
┌─────▼──┐ ┌───▼────────┐
│ SQLite │ │ Resend API │
│ local │ │ (send, │
│ store │ │ receive, │
│ │ │ domains) │
└────────┘ └────────────┘
Commands
Core Email
| Command | What it does |
|---|---|
send |
Send email (--reply-to-msg for threading, --cc, --bcc, --attach) |
reply <id> |
Reply with proper threading headers (--all for Reply All) |
forward <id> |
Forward a message (--to, --cc, --text for preamble) |
sync |
Pull sent and received messages into local store |
inbox ls |
List messages (filter by account, unread status) |
inbox read <id> |
Read a message, optionally mark as read |
inbox search <q> |
Search messages by keyword |
inbox delete <id> |
Delete a message |
inbox archive <id> |
Archive a message |
draft create |
Save a local draft with attachment snapshots |
draft list |
List all drafts |
draft show <id> |
View draft details |
draft edit <id> |
Edit a draft |
draft send <id> |
Send a draft and delete it |
draft delete <id> |
Delete a draft |
attachments list <id> |
List attachments on a message |
attachments get <id> <att> |
Download an attachment to disk |
Account Management
| Command | What it does |
|---|---|
profile add <name> |
Add or update a Resend API profile |
profile list |
List all profiles |
profile test <name> |
Test API connectivity |
account add <email> |
Register an email identity |
account list |
List all accounts |
account use <email> |
Set the default sending account |
signature set <account> |
Set per-account signature |
signature show <account> |
Show signature |
Resend Management
| Command | What it does |
|---|---|
domain list |
List your Resend domains |
domain get <id> |
Get domain details with DNS records |
domain create --name <d> |
Register a new domain |
domain verify <id> |
Trigger domain verification |
domain delete <id> |
Delete a domain |
domain update <id> |
Update tracking settings |
batch send --file <path> |
Send batch emails from a JSON file |
email list |
List sent emails via Resend GET /emails (each row includes last_event for polling) |
api-key list |
List API keys |
api-key create --name <n> |
Create an API key |
api-key delete <id> |
Delete an API key |
The Audience Section
In the Resend dashboard, "Audience" is the section name in the left sidebar. It groups four primitives — Contacts, Properties, Segments, and Topics — each with its own CLI command in email-cli:
| Resend dashboard tab | email-cli command | Purpose |
|---|---|---|
| Contacts | contact |
Individual email addresses |
| Properties | contact-property |
Typed custom-field schemas (e.g. company, plan) |
| Segments | segment |
Groupings of contacts (a contact can be in multiple segments) |
| Topics | topic |
User-facing subscription preferences for broadcasts |
The pre-November-2025 "Audiences as a grouping primitive" model has been replaced. Contacts are now flat — each contact can belong to zero, one, or multiple segments. There is no
audiencenoun in this CLI; usesegmentinstead.
Contacts
| Command | What it does |
|---|---|
contact list |
List contacts (--limit 1-100, --after <id> for cursor pagination) |
contact get <id_or_email> |
Get a contact by id or email (surfaces custom properties) |
contact create --email <addr> |
Create a contact (--first-name, --last-name, --unsubscribed, --properties '{"k":"v"}', --segments seg1,seg2, --topics top_xxx:opt_in,top_yyy:opt_out) |
contact update <id_or_email> |
Update contact fields (--first-name, --last-name, --unsubscribed, --properties) |
contact delete <id_or_email> |
Delete a contact |
Segments
| Command | What it does |
|---|---|
segment list |
List segments |
segment get <id> |
Get segment details |
segment create --name <n> |
Create a segment |
segment delete <id> |
Delete a segment |
segment contacts <id> |
List the contacts in a segment |
segment contact-add --contact <id_or_email> --segment <id> |
Add a contact to a segment |
segment contact-remove --contact <id_or_email> --segment <id> |
Remove a contact from a segment |
segment contact-list --contact <id_or_email> |
List the segments a contact belongs to |
Custom Contact Properties
Resend's contact properties are typed key/value fields you attach to contacts. Use them for merge tags in broadcasts and templates. You must define a property's schema before assigning values to a contact.
| Command | What it does |
|---|---|
contact-property list |
List defined contact-property schemas |
contact-property get <id> |
Get a contact-property schema |
contact-property create --key company --property-type string |
Define a property (--fallback for default value, --property-type string/number) |
contact-property update <id> --fallback "N/A" |
Update a property's fallback value (add --as-number if the property is numeric) |
contact-property delete <id> |
Delete a contact-property schema |
contact create --properties '{"company":"Acme","plan":"pro"}' |
Assign property values when creating a contact |
Topics (Granular Subscription Preferences)
Topics power Resend's granular unsubscribe flow on broadcasts. Each contact can be opted in or out of any topic, and broadcasts wired to a topic auto-substitute the right unsubscribe URL.
| Command | What it does |
|---|---|
topic list |
List topics |
topic get <id> |
Get topic details |
topic create --name "Weekly Digest" |
Create a topic (--description, --default-subscription opt_in/opt_out, --visibility public/private) |
topic update <id> |
Update topic fields (--name, --description, --default-subscription, --visibility) |
topic delete <id> |
Delete a topic |
topic contact-set --contact <id> --topic <id> --subscription opt_in |
Subscribe or unsubscribe a contact |
topic contact-list --contact <id> |
List a contact's topic subscriptions |
Broadcasts
Broadcasts are Resend's native campaign sending. Use these instead of batch send when you want auto unsubscribe wiring ({{{RESEND_UNSUBSCRIBE_URL}}}), server-side scheduling, and Resend's internal queue throttling.
| Command | What it does |
|---|---|
broadcast list |
List broadcasts |
broadcast get <id> |
Get broadcast details |
broadcast create --segment-id <id> --from <addr> --subject <subj> --html <body> |
Create a broadcast (--text, --name, --reply-to, --topic-id, --scheduled-at, --send for inline send). --audience-id is accepted as a legacy alias. |
broadcast update <id> |
Update a draft broadcast (--segment-id, --from, --subject, --html, --text, --name, --reply-to, --topic-id) |
broadcast send <id> |
Send (or schedule via --scheduled-at) |
broadcast delete <id> |
Delete (cancels scheduled broadcasts) |
Agent Tooling
| Command | What it does |
|---|---|
agent-info |
Machine-readable JSON capability manifest |
skill install |
Install skill file for Claude/Codex/Gemini |
skill status |
Check skill installation status |
completions <shell> |
Generate shell completions (bash/zsh/fish) |
Mailing-list Workflows
Email CLI is the Resend client for mailing-list-cli and any other tool that needs to wrap a complete mailing-list workflow on top of Resend. Three primitives matter:
- Segments group contacts. (Audiences, in pre-November-2025 terms.)
- Contact properties are typed custom fields on each contact. Define the schema, then assign values.
- Topics drive granular per-contact subscription preferences for broadcasts.
End-to-end example
# 1. Define a custom field schema (do this once per property)
# 2. Create a topic for granular unsubscribes
# → returns topic id, e.g. top_xyz
# 3. Create a segment to group contacts
# → returns segment id, e.g. seg_abc123
# 4. Add a contact with custom properties and add them to the segment in one shot
# 5. Subscribe the contact to the topic
# 6. Create and send a broadcast (auto unsubscribe URL via {{{RESEND_UNSUBSCRIBE_URL}}})
# → returns broadcast id, e.g. brd_456
# 7. Poll delivery status (replaces a webhook listener for the basic case)
Note: Wiring
--topic-idon the broadcast tells Resend which topic the unsubscribe link should opt the recipient out of. Without--topic-id, the unsubscribe URL still works but uses an account-wide unsubscribe.
Why polling beats running a second webhook listener
email list returns each email's last_event (sent, delivered, bounced, complained, opened, clicked). For the suppression-driving path -- delivered/bounced/complained -- polling is enough and avoids the operational cost of a second webhook URL pointed at your tool. For full engagement history (open → click → unsubscribe), keep using webhook listen -- it stores every event into the local SQLite events table, which events list queries.
Email Threading
Every outgoing email gets a unique Message-ID header (<uuid@yourdomain.com>). Replies set In-Reply-To and References per RFC 5322, so threads display correctly in Gmail, Outlook, and Apple Mail.
| Action | Headers Set | Threading |
|---|---|---|
send |
Message-ID | New conversation |
send --reply-to-msg <id> |
Message-ID, In-Reply-To, References | Threads into existing conversation |
reply <id> |
Message-ID, In-Reply-To, References | Threads into existing conversation |
reply <id> --all |
Same + preserves CC | Threads with all recipients |
forward <id> |
Message-ID only | New conversation (per RFC) |
Agent Integration
Email CLI follows the agent-cli-framework patterns. Any agent that speaks structured JSON can use it.
Capability Discovery
Returns a JSON manifest of every command, flag, exit code, and output format. An agent calls this once and works from memory.
Structured Output
All commands produce JSON when piped or when you pass --json:
Errors include actionable suggestions:
Semantic Exit Codes
| Code | Meaning | Agent action |
|---|---|---|
| 0 | Success | Continue |
| 1 | Transient error (network) | Retry |
| 2 | Configuration error | Fix setup |
| 3 | Bad input | Fix arguments |
| 4 | Rate limited | Wait and retry |
Skill Self-Install
Writes a skill file to ~/.claude/skills/email-cli/, ~/.codex/skills/email-cli/, and ~/.gemini/skills/email-cli/. The skill tells agents the CLI exists and to run agent-info for full details.
Configuration
Local State
All data lives in ~/.local/share/email-cli/email-cli.db (override with --db <path>). Sibling directories:
draft-attachments/-- snapshots of files attached to draftsdownloads/-- fetched attachments (configurable via--output)
Database Tables
| Table | Purpose |
|---|---|
profiles |
API key storage |
accounts |
Email identities with signatures |
messages |
Sent and received email with full metadata |
drafts |
Local drafts with attachment snapshots |
attachments |
Attachment metadata and local file paths |
sync_state |
Per-account sync cursor positions |
SQLite runs with WAL mode, busy timeout, and foreign keys enabled.
Security
- API keys live in the local SQLite database. Treat
email-cli.dbas sensitive. - Use
--api-key-env VAR_NAMEor--api-key-file pathinstead of passing keys directly. - Attachment filenames are sanitized before writing to disk.
- Each send includes a UUID
Idempotency-Keyheader.
Requirements
- A Resend API key with sending enabled
- A verified Resend domain (enable receiving on the domain for inbox sync)
- Rust 1.85+ if building from source (edition 2024)
Contributing
Contributions are welcome. See CONTRIBUTING.md for guidelines.
License
MIT -- see LICENSE.
Built by Boris Djordjevic at 199 Biotechnologies | Paperfoot AI
If this is useful to you: