中文文档 | English
AI-Native Email Interface: turning messy MIME into clean, structured context for agents.
MailCLI is an open-source email interface built for AI agents, LLM workflows, and automation developers.
It is not trying to be a traditional mail client for humans browsing inboxes.
It is trying to be the stable boundary between agents and email systems:
- agents consume structured message context instead of raw MIME
- agents produce
DraftMessageorReplyDraftinstead of hand-written MIME - mailbox and transport details stay behind drivers and CLI contracts
Instead of pushing raw MIME, bloated HTML, and provider-specific quirks into prompts, MailCLI turns email into structured JSON, clean Markdown, and machine-facing workflows.
If you only want to understand the agent boundary, start here:
# 1. build mailcli
go build -o mailcli ./cmd/mailcli
# 2. inspect one local message as structured JSON
./mailcli parse --format json testdata/emails/invoice.eml
# 3. run the local thread loop
./mailcli sync --config examples/config/fixtures-dir.yaml --account fixtures --index /tmp/mailcli-fixtures-index.json --limit 0
./mailcli threads --index /tmp/mailcli-fixtures-index.json invoice
# 4. compile the smallest useful reply boundary
./mailcli reply --config examples/config/fixtures-dir.yaml --account fixtures --dry-run examples/artifacts/outbound-patterns/minimal-reply.reply.jsonMinimal agent handoff:
{
"account": "fixtures",
"body_text": "Thanks, we received the invoice notification and queued it for processing.",
"reply_to_id": "invoice.eml"
}MailCLI fills in the rest:
from.addressfrom account config- default reply recipient from the source message
In-Reply-ToReferences- default reply subject
# 1. build mailcli
go build -o mailcli ./cmd/mailcli
# 2. run the zero-network local thread loop
./mailcli sync --config examples/config/fixtures-dir.yaml --account fixtures --index /tmp/mailcli-fixtures-index.db --limit 0
./mailcli threads --index /tmp/mailcli-fixtures-index.db invoice
# 3. inspect the full agent boundary
python3 examples/python/agent_thread_assistant.py \
--mailcli-bin ./mailcli \
--config examples/config/fixtures-dir.yaml \
--account fixtures \
--index /tmp/mailcli-fixtures-index.db \
--sync-limit 0 \
--query invoiceflowchart LR
A["Raw Email"] --> B["MailCLI"]
B --> C["StandardMessage / Thread Context"]
C --> D["Agent"]
D --> E["Minimal ReplyDraft JSON"]
E --> F["MailCLI"]
F --> G["Derived MIME + Transport"]
The fastest way to understand MailCLI is to avoid mailbox setup entirely.
The repository already includes:
- a local fixture corpus under
testdata/emails - a zero-network config at
examples/config/fixtures-dir.yaml - runnable Python examples
- a full local round-trip demo at Local Thread Demo
- fixed outbound JSON and MIME pairs at Outbound Draft Patterns
Recommended first commands:
go build -o mailcli ./cmd/mailcli
./mailcli parse --format json testdata/emails/verification.eml
./mailcli sync --config examples/config/fixtures-dir.yaml --account fixtures --index /tmp/mailcli-fixtures-index.db --limit 0
./mailcli threads --index /tmp/mailcli-fixtures-index.db invoiceIf you are maintaining the repository itself, the local demo artifacts now have a standard check entrypoint:
make demo-local-thread-checkMailCLI is currently in pre-v0.1 release candidate stage.
Working today:
- parse local
.emlinput or stdin intoStandardMessage - list messages from configured IMAP accounts (with
--since/--beforedate filters) - fetch and parse messages by sequence number, UID, or
Message-ID - sync recent messages into a local searchable index (BulkFetcher, BulkUpsert single-transaction, error isolation)
- search a local message index with FTS5 full-text search and field-weighted ranking
- inspect local conversation/thread summaries from indexed messages
- compile outbound drafts and replies (RFC 2047 encoded headers)
- send through SMTP-backed IMAP-style accounts (returns
message_id) - delete, move, mark-read/unread on remote mailboxes
- export the local index as JSONL, JSON, or CSV
- watch one or more mailboxes with IMAP IDLE push (streaming JSONL event feed, persistent seen state across restarts)
- manage config with
mailcli config show/mailcli config test - integrate with Python or shell agent workflows through stable JSON contracts
- LLM tool-use schemas for OpenAI and Anthropic (
tools/directory)
Stable enough to build against for v0.1 RC:
mailcli parsemailcli listmailcli getmailcli syncmailcli searchmailcli threadsmailcli threadmailcli sendmailcli replymailcli deletemailcli movemailcli markmailcli exportmailcli watchmailcli config show|testStandardMessageDraftMessageReplyDraftSendResult(now includesmessage_id)OperationResult(delete / move / mark)
Still evolving:
- HTML cleanup and URL normalization heuristics
- richer list/search semantics for inbox workflows
- richer outbound HTML rendering and attachment ergonomics
- broader provider coverage and extension guidance
Current built-in driver types:
imapfor real mailbox accessdirfor local.emldirectories and zero-network agent workflowsstubfor local development, tests, and driver-extension examples
Current parser fixture coverage now includes:
- plaintext mail
- newsletter/promo mail
- subscription/unsubscribe mail
- delivery failure mail
- postfix-style DSN / bounce mail
- verification mail
- multilingual verification mail with full-width digits
- quoted-reply verification mail
- invoice/payment mail
- security reset mail
- security reset mail with corporate safe-link wrappers
- attachment-entry mail
- multipart/related inline-image mail
Heuristic areas to treat as evolving:
- action extraction coverage and classification
- verification-code extraction beyond common OTP layouts
- HTML body selection and cleanup for unusual templates
- token estimates
Detailed next-step planning:
In the AI era, email should not be treated as a pile of HTML and transport headers.
It should behave like a structured API resource.
MailCLI exists to make email as easy for agents to consume and produce as a JSON document.
- AI-first parser Convert noisy raw email into normalized JSON and Markdown suitable for agent reasoning.
- Protocol/content separation Drivers handle transport, parsers handle content, composers handle outbound MIME.
- Action extraction
Extract unsubscribe links, security entry points, verification codes, invoice/payment entry points, attachment entry points, bounce/error context, and thread-related metadata.
Verification-code extraction is conservative but now handles common multilingual and next-line layouts, and can expose
expires_in_secondswhen the mail states a relative expiry. - Developer-friendly CLI
Support
json,yaml, andtableoutput formats, stdin pipelines, and scriptable commands. - Bidirectional workflow
Read mail with
list/get/parse, then produceDraftMessageandReplyDraftflows for outbound delivery. - Provider-agnostic architecture Designed to support IMAP, SMTP, APIs, and future ecosystem integrations without redefining the core model.
Raw email is a poor interface for agents:
- MIME trees are noisy
- HTML templates are token-expensive
- reply threading is easy to break
- provider APIs differ too much
MailCLI solves that by providing a stable boundary:
- inbound email becomes
StandardMessage - machine-usable artifacts such as actions, codes, and bounce context are extracted
- outbound intent becomes
DraftMessageorReplyDraft - transport stays behind drivers
mailcli parse --format json|yaml|table <file|->mailcli list [--account] [--mailbox] [--limit] [--since] [--before] [--format json|table]mailcli get [--account] [--mailbox] <id>mailcli sync [--account] [--mailbox] [--limit] [--since] [--before] [--refresh] [--index]mailcli search [--index] [--account] [--mailbox] [--since] [--before] [--thread] [--limit] [--full] <query>mailcli threads [query] [--index] [--account] [--mailbox] [--since] [--before] [--category] [--action] [--has-codes] [--limit]mailcli thread <thread_id> [--index] [--account] [--mailbox] [--limit]mailcli export [query] [--index] [--account] [--mailbox] [--since] [--before] [--format jsonl|json|csv] [--output] [--limit]
mailcli send [--account] [--dry-run] <draft.json>mailcli reply [--account] [--dry-run] <reply.json>mailcli delete [--account] [--mailbox] <id>mailcli move [--account] [--mailbox] <id> <dest-mailbox>mailcli mark [--account] [--mailbox] [--unread] <id>
-
mailcli watch [--account] [--mailbox ...] [--poll 30s] [--since] [--auto-sync] [--index] [--heartbeat 5m]Streams JSONL events to stdout:
watching,new_message(fullStandardMessage),heartbeat,error. Uses IMAP IDLE when available; falls back to polling otherwise.When
--indexis provided, maintains a persistent seen set in SQLite so restarts never re-emit already-processed messages.# Pipe to AI agent with persistent deduplication: mailcli watch --account work --index ~/.config/mailcli/index.db \ | python3 tools/agent_example.py
mailcli config show [--config]— print accounts (passwords redacted)mailcli config test [--config] [--account]— test live connection
- headings
- Markdown links rendered as clickable HTML anchors with readable plain-text fallbacks
- unordered lists as
ul/li - ordered lists as
ol/li - blockquotes
- simple Markdown tables
reply_to_message_idis supportedreply_to_idis supported- when
reply_to_idis used, MailCLI can fetch the original message and derive:In-Reply-ToReferences- default reply subject
- default reply recipient when
tois omitted
- for non-dry-run outbound commands, MailCLI can also derive
from.addressfrom configuredsmtp_usernameorusername
MailCLI follows a layered architecture so contributors can work on clear boundaries:
- Driver Layer Fetch raw messages and send raw bytes.
- Parser Engine Decode MIME, normalize charsets, clean HTML, convert to Markdown, and extract actions.
- Composer
Compile
DraftMessageandReplyDraftinto standards-compliant outbound MIME. - CLI Core Handle account selection, command routing, output formatting, and workflow orchestration.
Core rule:
protocol belongs to drivers, content belongs to parsers, composition belongs to composers, orchestration belongs to the CLI core
MailCLI is not just a parser. It is the bridge between agents and email systems.
Agent -> mailcli list/get/parse -> Driver -> Raw Email -> Parser -> StandardMessage -> Agent
Agent -> mailcli sync -> Local Index -> mailcli search -> Indexed Message Context -> Agent
Compact mailcli search results now expose thread_id, so an agent can narrow subsequent retrieval to a single conversation without reconstructing thread membership itself.
Agent -> mailcli sync -> mailcli threads -> choose thread -> mailcli search/get/reply
flowchart LR
A["Agent"] --> B["Minimal ReplyDraft JSON"]
B --> C["mailcli reply"]
C --> D["MailCLI derives From / To / thread headers"]
D --> E["Composer"]
E --> F["Raw MIME"]
F --> G["Driver"]
G --> H["Provider"]
Agent -> DraftMessage -> mailcli send -> Composer -> Raw MIME -> Driver -> Provider
Detailed workflow docs:
go build -o mailcli ./cmd/mailcli
./mailcli --helpcurrent_account: work
accounts:
- name: work
driver: imap
host: imap.example.com
port: 993
username: you@example.com
password: ${MAILCLI_IMAP_PASSWORD}
tls: true
mailbox: INBOX
smtp_host: smtp.example.com
smtp_port: 587
smtp_username: you@example.com
smtp_password: ${MAILCLI_SMTP_PASSWORD}Use the built-in stub driver when you want to validate agent flows, CLI output, or parser integration without connecting a real mailbox:
current_account: demo
accounts:
- name: demo
driver: stub
mailbox: INBOXUse the built-in dir driver when you want to point MailCLI at a local corpus of .eml fixtures or archived messages:
current_account: fixtures
accounts:
- name: fixtures
driver: dir
path: ./testdata/emails
mailbox: INBOXThe repository already ships a ready-to-run zero-network config:
examples/config/fixtures-dir.yaml
Secret fields currently support environment-variable expansion:
passwordsmtp_password
Recommended usage:
- use app passwords or provider-issued tokens
- inject them through environment variables
- avoid committing real mailbox secrets into config files
- Zero-network first path:
Start with
examples/config/fixtures-dir.yaml, then see Local Thread Demo. - Single-message agent path:
Start with
mailcli parseormailcli get, then see Agent Inbox Example. - Thread-aware agent path:
Start with
mailcli sync,mailcli threads, andmailcli thread, then see Agent Thread Example. - Outbound draft path:
Start with Outbound Draft Patterns when you want concrete
ReplyDraftandDraftMessageobjects before wiring a real provider. - Model-backed analysis path: Keep MailCLI as the boundary and delegate reasoning to an external subprocess provider, then see OpenAI External Provider Example and Examples Index.
cat test.eml | mailcli parse --format json -./mailcli sync --config examples/config/fixtures-dir.yaml --account fixtures --index /tmp/mailcli-fixtures-index.db --limit 0
./mailcli threads --index /tmp/mailcli-fixtures-index.db invoice
./mailcli thread --index /tmp/mailcli-fixtures-index.db "<invoice-123@example.com>"If you want the full agent-side JSON and reply boundary, use:
python3 examples/python/agent_thread_assistant.py \
--mailcli-bin ./mailcli \
--config examples/config/fixtures-dir.yaml \
--account fixtures \
--index /tmp/mailcli-fixtures-index.db \
--sync-limit 0 \
--query invoiceIf you want fixed JSON and MIME pairs for outbound composition without reading Python code, use:
./mailcli reply --config examples/config/fixtures-dir.yaml --account fixtures --dry-run examples/artifacts/outbound-patterns/ack-reply.draft.json
./mailcli send --dry-run examples/artifacts/outbound-patterns/release-update.draft.jsonmailcli list --config ~/.config/mailcli/config.yaml --format tablemailcli get --config ~/.config/mailcli/config.yaml "<message-id>"mailcli sync --config ~/.config/mailcli/config.yaml --limit 10By default, sync skips messages that are already indexed for the same account and message id. Use --refresh when you want to re-fetch and overwrite local records.
Current sync output also exposes listed_count, fetched_count, indexed_count, skipped_count, refreshed_count, and index_path so an agent can reason about cache state without reading the index file.
mailcli search invoiceUse --full when an agent wants the full indexed message payload instead of the compact search summary:
mailcli search --full invoiceUse --account and --mailbox to filter local results in multi-account setups.
Compact search results now include a deterministic score field, and results are ordered by relevance before recency.
mailcli threads
mailcli threads invoiceThread summaries now include the latest message preview and sender, so agents can often choose the right conversation before loading the full thread.
They also aggregate deterministic triage signals from indexed messages, including thread-level labels, categories, action_types, has_codes, code_count, action_count, and participant_count.
You can filter threads directly:
mailcli threads --has-codes
mailcli threads --category verification
mailcli threads --action verify_sign_inmailcli search --thread "<root@example.com>" updatemailcli thread "<root@example.com>"mailcli send --dry-run draft.jsonmailcli reply --dry-run reply.jsonpython3 examples/python/agent_inbox_assistant.py \
--mailcli-bin ./mailcli \
--email testdata/emails/verification.emlpython3 examples/python/agent_thread_assistant.py \
--mailcli-bin ./mailcli \
--config ~/.config/mailcli/config.yaml \
--account work \
--index /tmp/mailcli-index.db \
--query invoice \
--from-address support@nono.im \
--reply-text "Thanks for your email."More runnable entry points:
- keep the current machine-facing contracts stable for agent developers
- continue improving parser quality through fixture-driven regression work
- make local search and thread workflows more reliable
- keep contribution paths explicit for drivers, parser work, and contract changes
Detailed planning lives in:
MailCLI is still early, but the direction is intentional.
We want contributors in these areas:
- Parser quality Better MIME handling, HTML cleanup, charset handling, and Markdown fidelity.
- Semantic contracts Better shared specs for agent-facing email workflows.
- Drivers More providers, safer transport behavior, and better compatibility layers.
- Agent tooling Better examples, prompt patterns, and workflow integrations.
Major changes should be discussed first.
Start here:
The project is community-open, but it is still directionally curated to stay focused on:
- AI-native workflows
- clean separation of concerns
- stable machine-facing contracts
- Start here: Examples Index, Local Thread Demo, Outbound Draft Patterns, Agent Workflows
- Specs: Outbound Message Spec, Agent Provider Contract, Driver Extension Spec, Config Spec, Local Index Spec, Watch Spec
- Contribution: Contribution Guide, Parser Contributor Guide, Adding a Driver
- Release and planning: v0.1 RC Release Notes, Announcement Kit, Next Development Roadmap
Apache-2.0