Skip to content

Repository files navigation

中文文档 | English

MailCLI

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 StandardMessage JSON 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.

What It Is

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.

Who It Is For

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

Try It Locally In 2 Minutes

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 invoice

Those 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"]
Loading

For a full local round trip, see Local Thread Demo. For fixed outbound JSON and MIME pairs, see Outbound Draft Patterns.

Install

macOS / Linux:

curl -fsSL https://raw.githubusercontent.com/nonozone/MailCli/main/install.sh | sh

Windows PowerShell:

irm https://raw.githubusercontent.com/nonozone/MailCli/main/install.ps1 | iex

The 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 doctor

agent 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-configure

Windows:

$env:MAILCLI_AGENT_AUTO_CONFIGURE = "1"
irm https://raw.githubusercontent.com/nonozone/MailCli/main/install.ps1 | iex

If you are maintaining the repository itself, the local demo artifacts now have a standard check entrypoint:

make demo-local-thread-check

Project Status

MailCLI 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 .eml input or stdin into StandardMessage
  • 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/--before date 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 web for 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 parse
  • mailcli list
  • mailcli get
  • mailcli sync
  • mailcli search
  • mailcli threads
  • mailcli thread
  • mailcli triage message|thread
  • mailcli send
  • mailcli send prepare|confirm
  • mailcli reply
  • mailcli reply prepare|confirm
  • mailcli delete
  • mailcli move
  • mailcli mark
  • mailcli operations list|show
  • mailcli export
  • mailcli watch
  • mailcli account add
  • mailcli config init|show|doctor|test|capabilities
  • StandardMessage
  • TriageRecord / TriageEnrichment
  • DraftMessage
  • ReplyDraft
  • SendResult (now includes message_id)
  • OperationIntentResult
  • OperationLogEntry
  • 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
  • local Web panel reply flows, native forwarding, and attachment download ergonomics

Experimental local Web panel

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.db

By 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-open

The 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:

  • imap for real mailbox access
  • dir for local .eml directories and zero-network agent workflows
  • stub for 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:

Vision

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.

Key Features

  • 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_seconds when the mail states a relative expiry.
  • Developer-friendly CLI Support json, yaml, and table output formats, stdin pipelines, and scriptable commands.
  • Bidirectional workflow Read mail with list/get/parse, then produce DraftMessage and ReplyDraft flows for outbound delivery.
  • Provider-agnostic architecture Designed to support IMAP, SMTP, APIs, and future ecosystem integrations without redefining the core model.

Why MailCLI Exists

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 DraftMessage or ReplyDraft
  • transport stays behind drivers

Current Capabilities

Read path

  • 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]

Write path

  • mailcli send [--account] [--dry-run] <draft.json> for direct send or MIME preview
  • mailcli send prepare [--account] [--operations] <draft.json> for agent-safe outbound intent creation
  • mailcli send confirm [--account] [--operations] <intent-id> to execute a prepared send intent
  • mailcli reply [--account] [--dry-run] <reply.json> for direct reply or MIME preview
  • mailcli reply prepare [--account] [--operations] <reply.json> for agent-safe reply intent creation
  • mailcli reply confirm [--account] [--operations] <intent-id> to execute a prepared reply intent
  • mailcli 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>

Watch (streaming events)

  • mailcli watch [--account] [--mailbox ...] [--poll 30s] [--since] [--auto-sync] [--index] [--heartbeat 5m]

    Streams JSONL events to stdout: watching, new_message (full StandardMessage), heartbeat, error. Uses IMAP IDLE when available; falls back to polling otherwise.

    When --index is 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

Config management

  • 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 references
  • mailcli 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 passwords
  • mailcli config show [--config] — print accounts (passwords redacted)
  • mailcli config doctor [--config] — run local static diagnostics without connecting to IMAP or SMTP
  • mailcli config test [--config] [--account] — test live connection
  • mailcli 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 add

Recommended agent/script setup path:

mailcli account add \
  --provider gmail \
  --email you@gmail.com \
  --password-env MAILCLI_GMAIL_APP_PASSWORD \
  --format json

account 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.

Outbound Markdown baseline

  • 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 support

  • reply_to_message_id is supported
  • reply_to_id is supported
  • when reply_to_id is used, MailCLI can fetch the original message and derive:
    • In-Reply-To
    • References
    • default reply subject
    • default reply recipient when to is omitted
  • for non-dry-run outbound commands, MailCLI can also derive from.address from configured smtp_username or username

Architecture

MailCLI follows a layered architecture so contributors can work on clear boundaries:

  1. Driver Layer Fetch raw messages and send raw bytes.
  2. Parser Engine Decode MIME, normalize charsets, clean HTML, convert to Markdown, and extract actions.
  3. Composer Compile DraftMessage and ReplyDraft into standards-compliant outbound MIME.
  4. 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

Agent Collaboration Model

MailCLI is not just a parser. It is the bridge between agents and email systems.

Read loop

Agent -> mailcli list/get/parse -> Driver -> Raw Email -> Parser -> StandardMessage -> Agent

Local retrieval loop

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.

Local thread loop

Agent -> mailcli sync -> mailcli threads -> choose thread -> mailcli search/get/reply

Reply loop

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"]
Loading

New outbound message loop

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:

Build From Source

go build -o mailcli ./cmd/mailcli
./mailcli --help

Minimal Config Example

current_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}

Development Config Example

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: INBOX

Use 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: INBOX

The repository already ships a ready-to-run zero-network config:

examples/config/fixtures-dir.yaml

Secret fields currently support environment-variable expansion:

  • password
  • smtp_password

Recommended usage:

  • use app passwords or provider-issued tokens
  • inject them through environment variables
  • avoid committing real mailbox secrets into config files

Command Cookbook

Recommended Paths

  • Zero-network first path: Start with examples/config/fixtures-dir.yaml, then see Local Thread Demo.
  • Single-message agent path: Start with mailcli parse or mailcli get, then see Agent Inbox Example.
  • Thread-aware agent path: Start with mailcli sync, mailcli threads, and mailcli thread, then see Agent Thread Example.
  • Outbound draft path: Start with Outbound Draft Patterns when you want concrete ReplyDraft and DraftMessage objects 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.

Parse a local email

cat test.eml | mailcli parse --format json -

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
./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 invoice

If 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.json

List messages from a configured account

mailcli list --config ~/.config/mailcli/config.yaml --format table

Fetch and parse a message by id

mailcli get --config ~/.config/mailcli/config.yaml "<message-id>"

Sync recent messages into the local index

mailcli sync --config ~/.config/mailcli/config.yaml --limit 10

By 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.

Search the local index

mailcli search invoice

Use --full when an agent wants the full indexed message payload instead of the compact search summary:

mailcli search --full invoice

Use --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.

Inspect local threads

mailcli threads
mailcli threads invoice

Thread 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_in

Search within a selected thread

mailcli search --thread "<root@example.com>" update

Read a full local thread

mailcli thread "<root@example.com>"

Dry-run an outbound draft

mailcli send --dry-run draft.json

Dry-run a reply

mailcli reply --dry-run reply.json

Run the agent example

go run ./examples/go/agent_inbox_assistant \
  --mailcli-bin ./mailcli \
  --email testdata/emails/verification.eml

Run the thread agent example

go 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:

Current Priorities

  • 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:

Contributing

MailCLI is still early, but the direction is intentional.

We want contributors in these areas:

  1. Parser quality Better MIME handling, HTML cleanup, charset handling, and Markdown fidelity.
  2. Semantic contracts Better shared specs for agent-facing email workflows.
  3. Drivers More providers, safer transport behavior, and better compatibility layers.
  4. 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

Docs Map

License

Apache-2.0

About

A local-first email interface for AI agents — structured inbox context, search, triage, and safe actions over your existing mailbox.

Topics

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages