Apple Mail Mcp — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Apple Mail Mcp (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
A minimal, read-only MCP (Model Context Protocol) server that lets Claude Desktop interact with Apple Mail on macOS. Uses AppleScript via subprocess — no third-party email libraries, no network calls.
Current: 1.3.0
Versioning follows Semantic Versioning:
| Tool | Description |
|---|---|
mail_list_mailboxes | List every account and mailbox configured in Apple Mail |
mail_search_emails | Search emails by keyword and/or date; scans subject+sender via AppleScript whose predicates; results are merged across accounts, deduped, and sorted newest-first; optional account, mailbox_name, before_days, and include_all_mailboxes filters |
mail_read_email | Read the full content of a specific email by its opaque ID |
If you have uv installed, you can skip cloning and the virtual environment entirely — add this to ~/Library/Application Support/Claude/claude_desktop_config.json under "mcpServers":
{
"mcpServers": {
"apple_mail": {
"command": "uvx",
"args": ["--from", "git+https://github.com/androidua/apple-mail-mcp", "apple-mail-mcp"]
}
}
}uvx fetches the package, resolves its pinned dependencies, and runs the apple-mail-mcp console entry point. Restart Claude Desktop after saving. The manual venv setup below remains available as an alternative.
git clone https://github.com/androidua/apple-mail-mcp.git
cd apple-mail-mcppython3 -m venv venv
venv/bin/pip install -r requirements.txtvenv/bin/python apple_mail_mcp.pyPress Ctrl-C to stop. If no errors appear, the server is ready.
The first time the server runs, macOS will ask whether this process may control Apple Mail. Click OK. You can manage this later in:
System Settings → Privacy & Security → Automation
Open (or create) ~/Library/Application Support/Claude/claude_desktop_config.json and add the block shown below under "mcpServers".
{
"mcpServers": {
"apple_mail": {
"command": "/path/to/apple-mail-mcp/venv/bin/python",
"args": [
"/path/to/apple-mail-mcp/apple_mail_mcp.py"
]
}
}
}Replace /path/to/apple-mail-mcp with the absolute path to the directory where you cloned the repository (e.g. /Users/yourname/projects/apple-mail-mcp).
Restart Claude Desktop after saving the file.
Once connected, you can ask Claude things like:
mail_search_emails uses AppleScript's whose clause — a declarative predicate evaluated by Mail's Objective-C runtime. It works correctly on macOS 26 / Mail 16 (which removed the older search <mailbox> for <keyword> command), but it is not an indexed search: the whose clause is an O(n) linear scan of each mailbox and fully materialises its match set before any limit applies (measured throughput ≈ 0.5–1.5k messages/second). A low limit reduces output size, not scan cost.
Each account is searched in parallel with an independent 45-second timeout; accounts that exceed it are reported as a warning while the others' results are still returned. Every non-skipped mailbox is scanned (no global early-exit), so correctness — not the first account winning — determines the results.
Typical timings on a 6-account setup (iCloud/Yahoo/Google/Hotmail, largest mailboxes ~10–11k messages), measured on this machine:
| Query | Time |
|---|---|
All accounts, since_days=7, limit=10 | ~40 s |
Keyword + since_days=90 | ~12 s |
| Read one email (10k-message mailbox) | ~18 s |
mail_list_mailboxes(include_counts=true), 66 mailboxes | ~10 s |
Date-only searches over wide windows are the slow case — a bare since_days=90 with no keyword scans every message in every mailbox. Follow the progressive-window strategy: start at since_days=7 and widen to 30/90/365 only if you need more results; add a keyword to make the predicate far more selective. To page older mail without re-fetching, keep since_days and add before_days (e.g. since_days=90, before_days=30 = 30–90 days ago).
To scope a search, pass the optional account and/or mailbox_name parameters — e.g. restrict to account="Yahoo" and mailbox_name="INBOX" to avoid scanning all accounts. Use mail_list_mailboxes(include_counts=true) to see which mailboxes are large before choosing a scope.
Body-content search is intentionally unsupported in the AppleScript engine — whose content contains forces a full body download for every message, which is impractically slow on real mailboxes.
| Symptom | Fix |
|---|---|
AppleScript failed … not allowed to send Apple events | Go to System Settings → Privacy & Security → Automation and enable Mail for your Python process. |
No mailboxes found | Open Apple Mail and ensure at least one account is signed in. |
| Tool times out | Use account and/or mailbox_name to scope the search, or reduce limit. |
Invalid email_id | Always pass the email_id back exactly as returned by mail_search_emails. |
apple-mail-mcp/
├── apple_mail_mcp.py # MCP server — single file, all tools
├── requirements.txt # Pinned dependencies
├── README.md # This file
└── venv/ # Local virtual environment (not committed)since_days=7 returned 10/10 results from one account, hiding five others with no warning)include_all_mailboxes=true opts back inmail_read_email timeout raised to 60 s (reads on 10k+ mailboxes ran up against the old 30 s limit); recipients without display names no longer render as "missing value"before_days bounds the search window's near edge for paging older mail without re-fetchingmail_list_mailboxes accepts include_counts=true for per-mailbox message counts (search-scoping metadata)uvx one-line install), pytest suite, GitHub Actions CI, server-level MCP instructions, correct advertised server versionwhose clause is an unindexed O(n) scan, ~0.5–1.5k msgs/sec); prompt-injection warning for email contentproc.stdout.close() / proc.stderr.close() — asyncio.StreamReader has no .close() method; calling it on timeout caused AttributeError that crashed the tool and surfaced as the "StreamReader object has no attribute 'close'" error users saw for slow IMAP accountsasyncio.gather(return_exceptions=True) — a slow or offline account (Yahoo!, Hotmail, etc.) can no longer block or crash results from other accounts; each account gets an independent 45-second timeoutaccount is provided the original single-script path is preserved (60 s timeout); parallel path is used only for cross-account searchesmail_search_emails now accepts since_days (integer, 1–365) to filter emails by date received — supports natural queries like "last 7 days", "yesterday", "past month"keyword is now optional in mail_search_emails — browse recent mail without a search term (e.g. since_days=1 returns today's mail)keyword="invoice" + since_days=30 returns invoice emails from the past monthwhose content contains forces a full body download for every message, making it impractically slow on real mailboxes_script_read_email to proven account/mailbox iteration — direct AppleScript addressing (mailbox X of account Y) was unreliable for non-standard account types (Gmail, Exchange, shared accounts)mail_search_emails JSON output to flat array [...] — the {"results": [...]} wrapper introduced in v1.1.3 broke Claude AI's ability to extract email_id values from resultsawait proc.wait() on timeout — prevents file descriptor accumulation under repeated Mail.app timeouts---BODY_START--- split to a leading newline — prevents a subject line containing that exact string from corrupting header parsing in mail_read_email\x1f byte in a subject no longer shifts sender/date/is_read columns_CTRL_STRIP_RE to cover C1 controls U+0080–U+009F (including U+0085 NEL which Python's splitlines() treats as a line terminator)osascript stderr internally; return a generic error string to callers instead of forwarding script fragments_script_read_email with direct AppleScript object addressing (mailbox X of account Y) — O(1) lookup instead of O(accounts × mailboxes) name scanwhose docstring — it is O(n) per mailbox, not indexed; low limit does not reduce scan cost\) in the_script_read_email AppleScript template with sequential assignments — AppleScript uses ¬ for continuation, not \; the invalid characters caused all mail_read_email calls to fail with AppleScript syntax error -2741
search <mailbox> for <keyword> AppleScript command with awhose clause filter — the search command was removed in Mail 16 (macOS 26) and caused all mail_search_emails calls to fail with an AppleScript syntax error
mail_search_emails now uses Apple Mail's native indexedsearch (search <mailbox> for <keyword>) instead of brute-force message iteration — dramatically faster on large mailboxes (e.g. Yahoo with 20+ years of email)
account and mailbox_name parameters tomail_search_emails for scoped searches (e.g. search only Yahoo / INBOX)
skipped automatically unless explicitly targeted via mailbox_name
mail_list_mailboxes, mail_search_emails, mail_read_email~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.