中文文档 | English
A local-first email interface for AI agents, built on top of the mailboxes people already use.
MailCLI connects Gmail, Outlook / Microsoft 365, QQ Mail, 163 Mail, generic IMAP accounts, and local .eml fixtures to a stable CLI and JSON contract for agents.
It turns messy email into structured agent context:
- raw MIME becomes
StandardMessageJSON and clean Markdown - local mailboxes become searchable indexes and thread context
- deterministic parser output becomes triage evidence an agent can reason over
- outbound work uses
DraftMessage,ReplyDraft, prepare/confirm steps, and operation logs
The core is written in Go and keeps provider quirks, MIME parsing, local indexing, and transport details behind explicit commands. Model-backed enrichment can be connected around that boundary, but MailCLI does not require a hosted mailbox service or a bundled LLM to be useful.
MailCLI is:
- an AI-facing layer over existing email accounts
- a provider-neutral CLI and JSON contract for email automation
- a local-first toolkit for parsing, indexing, searching, threading, triage evidence, and safe outbound workflows
- an MCP server for exposing read/setup workflows to local agents
MailCLI is not:
- a hosted agent mailbox with a new address
- a replacement for a human mail client like Gmail, Outlook, or Apple Mail
- a service that grants agents automatic permission to send, delete, move, or mark mail
- a promise that a model has made a decision for you inside the Go core
By default, the MCP server exposes read and setup tools: parse, list, get, sync, search, threads, thread, and config diagnostics/capabilities. Mutating mailbox actions such as send, delete, move, and mark are not exposed through the default MCP server.
MailCLI is useful if you are building:
- AI agents that need reliable inbox context without pasting raw email into prompts
- workflow automation that searches and summarizes real mailboxes
- tools that need a stable email JSON contract across multiple providers
- local-first prototypes that should work before OAuth, API quotas, or hosted mail infrastructure exist
- safety-sensitive outbound flows where drafts, intents, confirmation, and audit logs matter
You do not need a real mailbox or network access to understand the agent boundary.
go build -o mailcli ./cmd/mailcli
./mailcli parse --format json testdata/emails/invoice.eml
./mailcli triage message testdata/emails/invoice.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 invoiceThose commands show the read and local-index side. The full MailCLI boundary also covers safe outbound work:
flowchart LR
A["Existing mailbox or .eml"] --> B["MailCLI"]
B --> C["StandardMessage / Triage Evidence"]
C --> D["Local Index / Thread Context"]
D --> E["Agent or Automation"]
E --> F["Draft / Intent"]
F --> G["Confirm / Operation Log"]
For a full local round trip, see Local Thread Demo. For fixed outbound JSON and MIME pairs, see Outbound Draft Patterns.
macOS / Linux:
curl -fsSL https://raw.githubusercontent.com/nonozone/MailCli/main/install.sh | shWindows PowerShell:
irm https://raw.githubusercontent.com/nonozone/MailCli/main/install.ps1 | iexThe installer detects your OS and CPU architecture, downloads the matching
GitHub Release asset, verifies checksums.txt when available, installs the
mailcli binary, and runs:
mailcli agent doctoragent doctor detects local agent CLIs such as Codex and Claude Code and prints
the MCP registration commands for mailcli mcp serve.
To install and register MailCLI with detected agents in one step:
curl -fsSL https://raw.githubusercontent.com/nonozone/MailCli/main/install.sh | sh -s -- --auto-configureWindows:
$env:MAILCLI_AGENT_AUTO_CONFIGURE = "1"
irm https://raw.githubusercontent.com/nonozone/MailCli/main/install.ps1 | iexIf you are maintaining the repository itself, the local demo artifacts now have a standard check entrypoint:
make demo-local-thread-checkMailCLI has a published v0.2.0 release with one-command installation and MCP agent setup. The current main branch adds existing-mailbox onboarding work.
Working today:
- parse local
.emlinput or stdin intoStandardMessage - add existing mailbox accounts with provider presets for Gmail, Outlook / Microsoft 365, QQ Mail, 163 Mail, and generic IMAP
- 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) - prepare and confirm send/reply intents with local operation logs for agent-safe outbound workflows
- 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)
- create, inspect, diagnose, test, and inspect account capabilities with
mailcli config init/mailcli config show/mailcli config doctor/mailcli config test/mailcli config capabilities - start an experimental local-only Mail Center with
mailcli webfor account status, sync, unified indexed inbox, message/thread reading, and operation queue inspection - integrate with Go, shell, and external agent workflows through stable JSON contracts
- LLM tool-use schemas for OpenAI and Anthropic (
tools/directory)
Stable enough to build against in current main:
mailcli parsemailcli listmailcli getmailcli syncmailcli searchmailcli threadsmailcli threadmailcli triage message|threadmailcli sendmailcli send prepare|confirmmailcli replymailcli reply prepare|confirmmailcli deletemailcli movemailcli markmailcli operations list|showmailcli exportmailcli watchmailcli account addmailcli config init|show|doctor|test|capabilitiesStandardMessageTriageRecord/TriageEnrichmentDraftMessageReplyDraftSendResult(now includesmessage_id)OperationIntentResultOperationLogEntryOperationResult(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
- local Web panel reply flows, native forwarding, and attachment download ergonomics
mailcli web starts a temporary browser control panel bound to 127.0.0.1.
It prints a per-run tokenized URL and serves embedded static assets from the
same Go binary:
mailcli web --config ~/.config/mailcli/config.yaml --index ~/.local/state/mailcli/index.dbBy default it uses 127.0.0.1:5566. Pass --port 0 if you want a random
available local port.
For fixture-only local testing:
mailcli web --config examples/config/fixtures-dir.yaml --index /tmp/mailcli-web.db --no-openThe v1 Web panel is local-only. It does not expose a LAN/public server, does not store secrets in the browser, and does not give AI automatic send/delete/move authority.
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, inbound MIME attachment metadata, 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, inbound attachment metadata, 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 triage message [--enrichment <path|->] <file|->mailcli triage thread [--index] [--account] [--mailbox] [--enrichment <path|->] <thread_id>mailcli export [query] [--index] [--account] [--mailbox] [--since] [--before] [--format jsonl|json|csv] [--output] [--limit]
mailcli send [--account] [--dry-run] <draft.json>for direct send or MIME previewmailcli send prepare [--account] [--operations] <draft.json>for agent-safe outbound intent creationmailcli send confirm [--account] [--operations] <intent-id>to execute a prepared send intentmailcli reply [--account] [--dry-run] <reply.json>for direct reply or MIME previewmailcli reply prepare [--account] [--operations] <reply.json>for agent-safe reply intent creationmailcli reply confirm [--account] [--operations] <intent-id>to execute a prepared reply intentmailcli operations list [--operations]mailcli operations show [--operations] <operation-id|intent-id>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 \ | go run ./examples/go/watch_reply_agent --from-address support@nono.im
mailcli account add [--provider gmail|outlook|microsoft365|qq|163|generic-imap] [--email <address>]— add an existing mailbox with provider defaults, human prompts, and environment-backed secret referencesmailcli config init [--config] --account <name> --driver imap --host <host> --username <email> --password-env <ENV>— create a starter config file that stores secret environment references such as${MAILCLI_IMAP_PASSWORD}, not raw passwordsmailcli config show [--config]— print accounts (passwords redacted)mailcli config doctor [--config]— run local static diagnostics without connecting to IMAP or SMTPmailcli config test [--config] [--account]— test live connectionmailcli config capabilities [--config] [--account]— print machine-readable account capabilities without connecting to the mailbox server
account add, config init, config doctor, and config capabilities are safe onboarding commands for agents and setup scripts. They do not print configured password values. config doctor distinguishes missing secret references from unset environment variables such as imap_password_env_unset. config test is the command that performs a live mailbox connection check.
Recommended human setup path:
mailcli account addRecommended agent/script setup path:
mailcli account add \
--provider gmail \
--email you@gmail.com \
--password-env MAILCLI_GMAIL_APP_PASSWORD \
--format jsonaccount add writes provider metadata and an auth_method, but it still stores
only secret references such as ${MAILCLI_GMAIL_APP_PASSWORD}. Gmail, QQ Mail,
and 163 Mail presets use app passwords or authorization codes. Outlook and
Microsoft 365 presets are read-first because many accounts require OAuth or
tenant policy changes that MailCLI does not implement yet.
- 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 prepare"]
C --> D["MailCLI derives From / To / thread headers"]
D --> E["Intent summary"]
E --> F["Confirmation"]
F --> G["mailcli reply confirm"]
G --> H["Operation log / Provider"]
Agent -> DraftMessage -> mailcli send prepare -> intent summary -> confirmation -> mailcli send confirm -> operation log -> Provider
Direct mailcli send <draft.json> remains available for trusted scripts and
interactive use. Agent automation should prefer send prepare / send confirm
so a user or controlling harness can inspect the intent before transport.
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: 465
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:
go run ./examples/go/agent_thread_assistant \
--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 example 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.jsongo run ./examples/go/agent_inbox_assistant \
--mailcli-bin ./mailcli \
--email testdata/emails/verification.emlgo run ./examples/go/agent_thread_assistant \
--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