Skip to main content

imail — Apple Mail CLI (+ agent schema)

imail is an agent-friendly command line for Apple Mail.app — list your inbox, send mail, and auto-organize messages into folders, all without IMAP, himalaya, or any mail server config. It drives Mail.app directly through macOS Automation (AppleScript/osascript), so it needs nothing but Mail.app itself and the accounts already configured in it.

imail doctor
imail list --limit 20
imail organize --account work --limit 100

For agent-friendly discovery, imail -h lists commands, imail help prints examples, and imail <command> -h shows command options. Root options are -V/--version, -j/--json, and -q/--quiet; shell completion is intentionally not exposed as framework-generated root flags.

macOS only. imail automates Mail.app via AppleScript, which only exists on macOS, and requires the OS-level Automation permission grant described below. There is no Linux/Windows build and none is planned — the tool has nothing to talk to on those platforms.


Install

# Homebrew
brew install ml-lubich/tap/imail

# pipx (or uv, or pip)
pipx install imail-mcp          # or: uv tool install imail-mcp
                                 # or: pip install imail-mcp

imail -h

The PyPI package is named imail-mcp (an older release also exists as mac-imail); the installed command is always imail.

Developing on imail itself
git clone https://github.com/ml-lubich/imail-mcp
cd imail-mcp
uv tool install .          # or: pip install -e ".[dev]"
imail -h

Run the test suite with uv run pytest -q (or install the dev extra with pip install -e ".[dev]"). Tests use a sanitized account-wall fixture and do not require the ignored, personal accounts.json file or Mail.app access. CI sets NO_COLOR=1 so Rich help output stays stable in noninteractive runs.


Setup: grant Mail Automation permission

The first time imail talks to Mail.app, macOS will prompt you to allow your terminal to control it. If it doesn't prompt, or you denied it by mistake, grant it manually:

System Settings → Privacy & Security → Automation → find your terminal app (Terminal, iTerm, etc.) → enable Mail.

Verify everything is wired up:

imail doctor

This confirms Mail.app is reachable and prints your configured accounts. If it fails, re-check the Automation permission above and make sure Mail.app is running with at least one account configured.


Quick start

imail doctor                                     # check Mail.app is reachable
imail accounts                                    # list configured Mail.app accounts
imail list --limit 20                             # list INBOX messages
imail list --account Work --mailbox INBOX --json  # JSON output for scripts/agents
imail organize --account work --limit 100         # file INBOX into folders (move only)
imail send --from you@example.com --to a@b.com \
  --subject "Hi" --body "Hello"
imail version

organize only moves messages between mailboxes — it never deletes anything. On each run it creates any of these folders that don't already exist: Action, Waiting, Meetings, IT, Releases, Security, FYI, Personal, Archive (plus Job Applications if you've created it yourself), then files messages into them by subject-line pattern matching.


Commands

Command What it does
imail doctor Verify Mail.app is reachable via Automation
imail accounts List Mail.app accounts (name + user)
imail walls Print work/personal account walls from accounts.json
imail list List inbox messages (--account, --mailbox, --limit, --query/-q, --json)
imail search Search messages by query (subject or sender) (<query>, -a/--account, -n/--limit, -j/--json)
imail read Read full message details, headers, attachments, and content (<index>, -a/--account, -j/--json)
imail reply Reply to email by index with auto Reply-To and attachments (<index>, -b/--body, -f/--body-file, -A/--attach, -F/--from, -t/--to)
imail organize Classify and move INBOX messages into folders (--account/-a, --limit/-n) — never deletes
imail autodraft Scan personal inboxes; silent Mail.app drafts (or auto-send only low-stakes known follow-ups). --dry-run, --account, --limit. Scheduled by brain reply, not a second daemon.
imail autodraft-eval Run the labeled eval (src/imail/autodraft_eval.json) against the real LLM; prints a scorecard, exits non-zero on any unsafe send
imail autodraft-log Pretty-print the last N autodraft decisions from the log (-n/--limit, default 20)
imail outreach draft Draft a source-grounded outreach email from JSON config and supplied company/candidate evidence; never sends
imail send Send a message via Mail.app (--from, --to, --subject, --body, -a/--attach, --cc, --markdown)
imail draft Save a draft silently in Mail.app without opening GUI windows (--to, --subject, --body/--body-file, --from, -a/--attach)
imail status Show status of queued and sent batch emails
imail batch Queue, manage, and dispatch batch emails with randomized intervals (--file, --run, --limit, --clear, --dry-run)
imail version Print the installed version
imail prompt Inspect or export system prompts and schemas (--list, <name>, --json)
imail mcp Run the built-in MCP server over stdio (doctor, list_accounts, list_messages, send_message, read_message, reply_message tools)
imail agent schema Print the JSON command catalog and work/personal wall keys; private account values are omitted
imail agent guide Print a plain-text usage guide for humans and agents

Help is available everywhere: imail -h, imail <command> -h.


Autodraft

imail autodraft scans your personal inboxes for messages that need a reply, grounds a reply decision in the email body plus your brain knowledge store via an LLM, and creates a silent, unsent Mail.app draft — or, only in narrow cases, sends the reply outright.

Guardrails:

  • A regex prefilter (matches_skip_patterns) drops obvious spam/newsletter/ mass-blast senders before the LLM is ever called.
  • The LLM's JSON decision is validated with exact types — any malformed field (wrong type, out-of-range confidence, unknown stakes value) is treated as an error, not silently coerced. This is fail-closed by design.
  • The email body is untrusted input: the LLM prompt explicitly frames it as data to react to, never instructions to follow, since a message can try to talk the model into a fake high-confidence/low-stakes claim.
  • Auto-send only fires when all of these hold (auto_send_allowed): confidence ≥ 0.95, stakes is "low", the recipient is a known Contacts correspondent, the reply is ≤ 400 characters, the incoming message has no attachments, and the intent isn't recruiter.
  • Recruiter emails are never auto-sent — always drafted, regardless of confidence.
  • Every draft is created silently (no Mail.app GUI popup) and stays an unsent draft until you review and send it yourself.

Commands:

  • imail autodraft [--dry-run] [--account] [--limit] — run the pipeline.
  • imail autodraft-eval — run ~14 hand-labeled synthetic cases through the real LLM + gate and print a PASS/FAIL scorecard; exits non-zero if any case would have produced an unsafe auto-send.
  • imail autodraft-log [-n 20] — pretty-print the last N logged decisions (~/.config/imail/autodraft-log.jsonl).

The OpenAI key (used for the primary gpt-5-nano backend, falling back to claude haiku) is read from the environment (OPENAI_API_KEY) or, if unset, the macOS keychain item named OPENAI_API_KEY.

Autodraft is entirely config-driven — no accounts, names, or resume paths are hardcoded. It reads an optional autodraft block from your accounts.json (see below):

{
  "autodraft": {
    "accounts": ["you@gmail.com"],
    "owner_name": "jamie",
    "sign_as": "jamie",
    "resume_dir": "~/dev/resumes/resumes",
    "resume_default_variant": "base",
    "resume_variants": {
      "ai": ["ai", "ml", "llm", "agent"],
      "fullstack_ai": ["fullstack", "full stack"]
    }
  }
}
  • autodraft.accounts — which addresses autodraft scans. Preferred over walls.personal.emails when you want autodraft scoped narrower than your general personal wall (e.g. to exclude an address you never want auto-drafted). If neither is set, imail autodraft raises a clear error.
  • owner_name / sign_as — who the LLM triages on behalf of and what name it signs replies with. Both default to a neutral "the account owner" / no name if unset.
  • resume_dir / resume_default_variant / resume_variants — only used for recruiter-intent replies. With no resume_dir set, autodraft never attaches a resume. resume_variants maps a folder name (under resume_dir) to the keywords that select it from the job text; resume_default_variant is the fallback folder when no keyword matches.

Grounded outreach drafts

The imail.outreach.draft_outreach API prepares a company-specific referral draft from role evidence and candidate facts supplied by the caller. The structured LLM selects IDs from those inputs; imail builds the final factual sentences from the selected records. It does not browse, send, or save a message. The caller supplies source URLs and observation dates so stale or unsupported company details can be screened before drafting.

from imail.outreach import draft_outreach

draft = draft_outreach(
    company="Acme",
    contact_name="Taylor",
    role="Applied AI Engineer",
    role_description="Build agent workflows and evaluate their reliability.",
    candidate_name="Jordan",
    candidate_facts=[{"id": "f1", "text": "Jordan has shipped production AI agents."}],
    company_evidence=[{
        "id": "e1",
        "text": "AI agents for customer operations",
        "source_url": "https://example.com/careers",
        "observed_at": "2026-10-23",
    }],
    config={"enabled": True, "max_company_points": 1, "sign_as": "Referral Sender"},
    candidate_email="jordan@example.com",
)

config.enabled is an explicit opt-in; missing or false means no model call. max_company_points is capped at three, and max_candidate_points controls how many profile facts are selected. Both default to at most three. Company evidence must include source_url and observed_at unless require_company_sources is explicitly false. A custom body_template may use only the supplied fields: company, contact_name, role, candidate_name, company_points, candidate_points, ask, sign_as, candidate_contact, and website. Model output is validated against known IDs and configured limits; it cannot supply factual body text. The result includes the draft body, selected points, and evidence references for review.

Evidence dates default to strict YYYY-MM-DD ISO format and a maximum age of 90 days. Set max_evidence_age_days to a non-negative integer to change the freshness window. Future dates and unparseable dates fail closed. If a source uses another date format, list its exact strptime pattern in accepted_date_formats; no alternate format is inferred. Selected evidence and candidate facts are normalized to plain text; markup, control characters, and instruction-like content are rejected before they enter the draft.

The CLI exposes the same function without requiring a script:

imail outreach draft --config outreach-config.json --input outreach-evidence.json

The config JSON contains the personalization settings (including enabled, point limits, ask, signoff, and optional body template). The input JSON contains company, contact_name, role, role_description, candidate_name, candidate_facts, company_evidence, and optional candidate contact fields. The command prints a structured JSON result with the draft and evidence IDs; it does not browse, send, or save mail. Use official company and current role sources as evidence, and keep the source URL and observation date with each company fact. Any downstream send still uses the regular imail send or guarded imail reply flow and its account, recipient, and delivery checks.

The same structured ID-selection pattern can support other constrained tasks such as niche dropdown choices: provide the allowed options and grounded records as data, validate returned IDs against that list, and keep the action that uses the selection in a separate reviewed step.


Reliable sends and guarded replies

imail read includes the message's To and CC recipients. imail list --mailbox Outbox and imail read INDEX --mailbox Outbox read Mail.app's global Outbox; when an account is supplied, the read verifies the actual sender account before returning a message.

imail reply pins the reviewed message by RFC Message-ID, re-reads that exact message, and requires --expected-recipient to match its Reply-To address before sending. --expected-sender and --expected-message-id provide extra guards when the caller has recorded them. The MCP reply tool exposes the same guards and CC support. Re-list and re-read a thread immediately before a reply; do not reuse a mailbox index from an earlier scan.

After a network error, imail send and imail reply reconcile the exact sender, recipient, CC, subject, body, and attachment set against Sent and the global Outbox. They retry at most once by default, only for a recognized connectivity failure and only after three checks confirm the exact message is absent. A match in Sent is returned as provider-verified with its Message-ID. A queued, ambiguous, or inaccessible result is delivery_unknown; the command does not retry that message. Check Sent and Outbox before any manual retry. Outbox count changes alone are never treated as delivery proof.


Account walls (accounts.json)

If you juggle multiple Mail.app accounts (e.g. a work account and personal accounts) and want imail to help keep them separate, create an accounts.json. imail looks for it, in order, in the current directory, the repo root (source checkouts), and ~/.config/imail/accounts.json (the stable location for an installed package):

{
  "walls": {
    "work": { "accounts": ["Work"], "emails": ["you@work.example.com"] },
    "personal": { "accounts": ["Google"], "emails": ["you@gmail.com"] }
  },
  "aliases": { "work": "Work", "google": "Google" },
  "rules": ["Auto-send MUST set --from matching the correct wall."]
}
  • walls / rules are surfaced by imail walls — informational, not enforced by send itself. The agent schema and guide only identify the available work/personal wall keys; they never print private account values.
  • aliases lets imail organize --account <alias> accept short names instead of the exact Mail.app account name.
  • accounts.json is only required for walls, autodraft, and organize --account <alias>; doctor, accounts, list, send, and a plain imail organize (all accounts) work without it.

Using imail from a coding agent

imail agent schema prints a JSON catalog of every command, its parameters, and the available work/personal wall keys — enough for an agent to construct valid imail invocations without exposing private account values or parsing --help output. Schema and guide discovery work without accounts.json:

imail agent schema   # machine-readable command catalog
imail agent guide     # short usage guide (CLI-first, wall rules) for humans/agents

imail is a CLI meant to be called directly from agent shells, which is cheaper on tokens than a tool-calling round trip — prefer that where the agent's harness allows arbitrary shell commands.

For harnesses that only speak MCP (Model Context Protocol), imail also ships a built-in stdio MCP server exposing four tools (doctor, list_accounts, list_messages, send_message):

imail mcp   # runs an MCP server over stdio; exits on EOF/SIGTERM

Point an MCP client at it, e.g. in a Claude Desktop / Claude Code mcp_servers (or .mcp.json) config:

{
  "mcpServers": {
    "imail": {
      "command": "imail",
      "args": ["mcp"]
    }
  }
}

If you need a broader MCP server for Mail.app (more tools than the four above), patrickfreyer/apple-mail-mcp is a community option.


MIT License · Copyright (c) 2026 Misha Lubich (ml-lubich)

Metadata

Release files for imail-mcp 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for imail-mcp 0.2.1
File Size Uploaded Provenance
imail_mcp-0.2.1.tar.gz 52.7 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for imail-mcp 0.2.1
File Interpreter ABI Platform
imail_mcp-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 112.7 kB

Release files / imail_mcp-0.2.1.tar.gz

Download URL imail_mcp-0.2.1.tar.gz
Size 52.7 kB
Tags Source
SHA-256 checksum
How to use checksums
c1d6fe8da6b8e84ddf27d46b3f96941a2d7fee5866f395f27d8859b55523ac66
BLAKE2b-256 checksum
How to use checksums
bc08b12605323821f48561439306ff1b2f3a9953c4b5ad256f92b0a661eae09f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.9

Release files / imail_mcp-0.2.1-py3-none-any.whl

Download URL imail_mcp-0.2.1-py3-none-any.whl
Size 60.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
41ea90b17995984c5f33f19a1228c489748c6088eb1cda07de73ceff8d0f7f5d
BLAKE2b-256 checksum
How to use checksums
fca975a7aa19267c344e233341eac201acf0763aca4fdd3251a6ba5c39fafe46
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.9

Security for imail-mcp 0.2.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page