Criteria-based export of Apple Mail messages to .eml files — without relying on smart folders.
Define jobs (a mailbox name, match rules, and an output folder). MailExporter scans ~/Library/Mail, copies matching messages, and keeps an incremental export so later runs only pick up new mail. It never deletes messages from Apple Mail.
An MCP server (mail-exporter) lets Cursor, Claude Desktop, and Claude Cowork list jobs, refresh exports, and read messages.
- macOS (reads Apple Mail’s on-disk store)
- Full Disk Access for whichever app actually scans Mail (see Permissions)
- Python 3.10+ for the CLI and MCP server
- Xcode command-line tools if you build the Mac app
macOS blocks ~/Library/Mail unless the running app has Full Disk Access:
System Settings → Privacy & Security → Full Disk Access
| How you run it | Grant access to |
|---|---|
| MailExporter.app | MailExporter |
python3 -m engine from Terminal |
Terminal (or iTerm) |
| MCP inside Cursor | Cursor |
| MCP inside Claude Desktop / Cowork | Claude |
Quit and reopen the app after toggling access.
apps/MailExporter/build.sh
open apps/MailExporter/MailExporter.appThe build produces a self-contained app: Swift UI plus a bundled MailExporterEngine (no Homebrew Python or rg at runtime).
- Config — add jobs, set the output folder, add match clauses (From / To / Subject / Body / Date), preview match count
- Run — export one job or all; one-line result plus a notification
Jobs are stored at:
~/Library/Application Support/MailExporter/jobs.json
Each export folder looks like this (MailExporter creates Drafts/ and Sent/):
<outputDir>/
*.eml exported messages
.exported-ids.json incremental state — do not delete unless a full re-export
_how_to_use.md notes for an AI assistant
Drafts/ Markdown the AI writes before the mail is known to be sent
Sent/ those Markdown files after a sent copy appears in the export
From the repository root:
python3 -m engine seed # example jobs (edit From addresses before a real export)
python3 -m engine list
python3 -m engine export --dry-run --job-name DHL
python3 -m engine export --job-name DHL--force-full wipes existing .eml files for that job and re-exports.
Hermetic unit tests (no Mail GUI or network). From the repository root:
.venv/bin/pip install -r requirements-dev.txt # once
.venv/bin/pytest -qClauses in a group with "conjunction": "all" are AND. Multiple values in one clause are OR.
{
"conjunction": "all",
"conditions": [
{ "field": "from", "op": "is", "values": ["xxx@dhl.com", "yyy@dhl.com"] },
{ "field": "date", "op": "after", "date": "2026-03-04" }
]
}Text fields: from, to, cc, recipient, subject, body, entire.
Text ops: contains, is, does_not_contain.
Date ops: after, before.
The local stdio server mailexporter_mcp talks to the same jobs.json as the Mac app. It does not send Mail to the internet. Keep it as a local process — do not register it as a public / remote Claude connector.
From the repository root (once):
If MailExporter.app is installed in /Applications, you can run the MCP server directly from the app bundle without Python or virtual environments:
{
"mcpServers": {
"mail-exporter": {
"command": "/Applications/MailExporter.app/Contents/Resources/MailExporterEngine/MailExporterEngine",
"args": ["mcp"]
}
}
}If developing from source:
python3 -m venv .venv
.venv/bin/pip install -r requirements-mcp.txtConfirm it starts (it will wait on stdin; Ctrl-C to quit):
PYTHONPATH="$(pwd)" .venv/bin/python -m mailexporter_mcp| Tool | Use when |
|---|---|
list_jobs |
See jobs (smart mailboxes) and their export folders |
list_messages |
List .eml files for a job (by name or id) |
read_message |
Read headers + body of one .eml |
compose_draft |
Open a Mail draft from Markdown (AppleScript) |
check_matches |
Dry-run: how many Mail messages currently match |
export_job |
Refresh the folder from Apple Mail (incremental unless force_full) |
clear_target |
Delete exported .eml files (debug / full redo) |
write_howto |
Refresh _how_to_use.md in the export folder |
Typical flow: list_jobs → list_messages / read_message → write a numbered .md in Drafts/ → compose_draft → export_job later and move the .md to Sent/ once a matching sent copy is in the export.
read_message only reads .eml files under a configured job outputDir. clear_target only clears folders that look like MailExporter exports (marker files present).
An assistant helping with this mailbox should keep outgoing mail as Markdown files next to the export, not in the .eml root.
Where
| Folder | Meaning |
|---|---|
<outputDir>/Drafts/ |
Composing, waiting to send, or not yet confirmed sent |
<outputDir>/Sent/ |
Confirmed sent because a matching .eml showed up after export_job |
Filenames: NNN_who_subject.md
NNN— three-digit sequence (001,002, …). Next number after the highest already used in bothDrafts/andSent/.who— who it is to (name or email local-part), lowercase, hyphens, no@.subject— enough of the subject to recognise the mail (stripRe:/Fwd:).
Examples: 001_supplier_inquiry.md, 014_contractor_quote.md.
Keep the same filename when moving Drafts/ → Sent/. Move (do not copy or delete) only when an exported .eml matches To / Subject / thread — the job must include Sent mail. If it is unclear, leave the file in Drafts/.
Details and the Markdown front-matter format are in _how_to_use.md inside each export folder.
Project config (this repo already has .cursor/mcp.json) or a global config at ~/.cursor/mcp.json.
Cursor Settings → MCP (or Customize → MCP), then add a server, or write:
{
"mcpServers": {
"mail-exporter": {
"command": "/path/to/mail-exporter/.venv/bin/python",
"args": ["-m", "mailexporter_mcp"],
"cwd": "/path/to/mail-exporter",
"env": {
"PYTHONPATH": "/path/to/mail-exporter"
}
}
}
}In a project file you can use Cursor interpolation instead of a hard-coded path:
{
"mcpServers": {
"mail-exporter": {
"command": "${workspaceFolder}/.venv/bin/python",
"args": ["-m", "mailexporter_mcp"],
"cwd": "${workspaceFolder}",
"env": {
"PYTHONPATH": "${workspaceFolder}"
}
}
}
}Reload the window (Developer: Reload Window) or restart Cursor. In Settings → MCP, mail-exporter should show as connected.
Grant Cursor Full Disk Access if export_job / check_matches cannot read Mail.
Ask Agent things like: “List MailExporter jobs” or “Read the latest messages in the DHL export.”
-
Install Claude Desktop and complete the venv setup above.
-
Open Claude → Settings → Developer → Edit Config. That creates or opens:
~/Library/Application Support/Claude/claude_desktop_config.json -
Merge this into the existing
mcpServersobject (keep any servers you already have):
{
"mcpServers": {
"mail-exporter": {
"command": "/path/to/mail-exporter/.venv/bin/python",
"args": ["-m", "mailexporter_mcp"],
"env": {
"PYTHONPATH": "/path/to/mail-exporter"
}
}
}
}- Fully quit Claude Desktop (Cmd-Q) and reopen it.
- Check Settings → Developer for a connected
mail-exporter, or click + in a chat → Connectors. - Grant Claude Full Disk Access if exports cannot read Mail.
Claude Desktop only understands local stdio servers in that JSON file. Use absolute paths; python3 on PATH often fails because the GUI app does not inherit your shell profile.
MailExporter must stay on your Mac (it reads ~/Library/Mail). Cowork can use it as a local connector, not as a cloud/remote custom connector.
- Install the server in Claude Desktop first (same
claude_desktop_config.jsonas above). Cowork does not have a separate MCP config file. - Open the latest Claude Desktop app. In the message box, choose Cowork.
- Click + → Connectors and enable mail-exporter for that session.
- Keep Claude Desktop running. Local connectors (including this server) are provided by the desktop app. If you start Cowork on the web or on a phone, the desktop app on this Mac must stay open or Cowork cannot reach local MCP.
- Grant Claude Full Disk Access (same as Desktop).
Do not add this server under Customize → Connectors → Add custom connector. That path is for remote MCP URLs that Anthropic’s cloud dials over the public internet. This server has no public URL and should not get one — it can list and export private mail.
Cloud Cowork sessions do not run local MCP inside Anthropic’s sandbox. They only reach this server through Claude Desktop on your machine.
This project only reads Mail data and writes .eml files to folders you choose. It never deletes messages from Apple Mail. Treat export folders as private mail.
python3 -m unittest tests.test_criteria -v| Path | Purpose |
|---|---|
apps/MailExporter/ |
Native Mac app (Config + Run) |
engine/ |
Python export engine (criteria matching, .emlx + attachments) |
mailexporter_mcp/ |
Local MCP server for Cursor / Claude / Cowork |