Run this in Codex or your terminal on a Mac:
codex plugin marketplace add KeystoneScience/mac-mail-codex-plugin --ref mainThen restart Codex, open the plugin picker, and install Mac Mail from the new marketplace. If Codex reports missing macOS permissions, grant Full Disk Access to Codex so it can read Apple Mail's local index. Mail.app Automation is only needed when you first create, open, or send a visible Mail draft.
Paste this into Codex on a Mac:
Install the Mac Mail Codex plugin from https://github.com/KeystoneScience/mac-mail-codex-plugin and make it ready to use.
Please run:
mkdir -p ~/plugins
if [ -d ~/plugins/mac-mail/.git ]; then
git -C ~/plugins/mac-mail pull --ff-only origin main
elif [ -e ~/plugins/mac-mail ]; then
echo "~/plugins/mac-mail already exists but is not a git checkout. Move it aside before installing the self-updating copy." >&2
exit 1
else
git clone https://github.com/KeystoneScience/mac-mail-codex-plugin.git ~/plugins/mac-mail
fi
python3 ~/plugins/mac-mail/scripts/bootstrap_install.py
~/plugins/mac-mail/scripts/run_doctor.sh
Restart Codex after install/update so the native Mac Mail tool schema reloads.
Then ask me to grant the required macOS permissions:
- Full Disk Access for Codex so the plugin can read Apple Mail's local index and downloaded messages.
- Full Disk Access for Terminal or iTerm too if you tested the plugin from a shell.
- Mail.app Automation permission when I first ask you to create/open/send a Mail draft.
If the plugin reports missing permissions, call mail_permissions_check. If Full Disk Access is blocked, call it with open_full_disk_access=true so System Settings opens to the right page. If Mail Automation is blocked, call it with include_mail_app=true and open_automation=true.
Do not send email during setup. Verify read-only setup by listing mailboxes and searching one exact mailbox_id.
Local-first Codex plugin for Apple Mail on macOS. It exposes Mail.app data through a stdio MCP server with fast read-only SQLite metadata search, bounded .emlx body reads, optional private FTS body search, safe attachment handling, visible draft preparation, and a strict send gate.
- No cloud email API or credentials.
- Reads Apple Mail's local
Envelope Indexin SQLite read-only mode. - Searches by exact
mailbox_id, multiplemailbox_ids, account UUID, role, mailbox name/path, sender, recipient, subject, dates, unread/flagged state, and attachments. - Permission diagnostics can explain missing Full Disk Access or Automation and optionally open the right macOS System Settings pane.
- Git-backed installs can check GitHub for updates and pull the latest plugin code in the background.
- Excludes Junk/Spam from broad metadata search by default.
- Reads downloaded
.emlxbodies only when needed and caps body output. - Optional private FTS body index under
~/Library/Application Support/Codex Mac Mail/body-search.sqlite3. - Broad body-index builds skip Sent, Drafts, Junk, and Trash unless explicitly included.
- Can purge the private body index with
mail_purge_body_index. - Creates visible Apple Mail drafts and local
X-Unsent.emlfiles. - Sending requires
ALLOW_MAC_MAIL_SEND=1, explicit approval fields, and a matching inspecteddraft_sha256.
- macOS with Apple Mail configured.
- Python 3.10+.
- Full Disk Access for the app that launches Codex or the MCP server if macOS blocks
~/Library/Mailreads. - Mail.app Automation permission only for draft/open/send operations. Read-only SQLite search does not need to control Mail.app.
No third-party Python packages are required.
The MCP launcher uses python3.12, python3.11, python3.10, then python3, or the MAC_MAIL_PYTHON environment variable if set.
For the easiest self-updating install, clone the repository directly into Codex's home-local plugin path and run the bootstrap script:
mkdir -p ~/plugins
git clone https://github.com/KeystoneScience/mac-mail-codex-plugin.git ~/plugins/mac-mail
python3 ~/plugins/mac-mail/scripts/bootstrap_install.pyThe bootstrap script:
- keeps the plugin at
~/plugins/mac-mail; - creates or updates
~/.agents/plugins/marketplace.json; - enables
mac-mail@<local-marketplace>in~/.codex/config.toml; - refreshes any existing Git-backed Codex plugin cache checkout for this plugin;
- runs a non-destructive doctor check;
- prints the exact permission steps to finish setup.
If you are developing from another checkout, you can sync it as a home-local plugin:
./scripts/install_local_plugin.shBy default that syncs to:
~/plugins/mac-mail
You can pass a different target:
./scripts/install_local_plugin.sh /path/to/plugins/mac-mailThe development sync script is useful while editing, but it intentionally excludes .git. Use bootstrap_install.py or clone directly into ~/plugins/mac-mail for automatic GitHub update checks.
If you use a local Codex marketplace manually, add or keep an entry that points at the installed plugin directory. The plugin manifest is:
.codex-plugin/plugin.json
The MCP config is:
.mcp.json
Run a non-destructive doctor check:
scripts/run_doctor.shIf macOS permissions are blocked, open the relevant pane:
scripts/run_doctor.sh --open-full-disk-access
scripts/run_doctor.sh --open-automationFrom Codex, use mail_permissions_check. It can open the same settings panes with open_full_disk_access=true or open_automation=true.
List tools over JSON-RPC:
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}\n' | bash scripts/run_mcp.shSearch metadata without launching Mail.app:
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"mail_search_messages","arguments":{"query":"invoice","limit":5}}}\n' | bash scripts/run_mcp.shFind a mailbox, then search only that exact mailbox:
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"mail_list_mailboxes","arguments":{"query":"inbox","include_empty":false,"limit":10}}}\n' | bash scripts/run_mcp.sh
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"mail_search_messages","arguments":{"mailbox_id":123,"limit":5}}}\n' | bash scripts/run_mcp.shMetadata search is always the fastest path. Use body search only when message body text matters.
Build a narrow private body index:
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"mail_rebuild_body_index","arguments":{"mailbox_role":"inbox","date_from":"2026-04-01","max_messages":100}}}\n' | bash scripts/run_mcp.shSearch the private body index:
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"mail_search_bodies","arguments":{"query":"contract deadline","mailbox_role":"inbox","limit":5}}}\n' | bash scripts/run_mcp.shRemove the private body cache:
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"mail_purge_body_index","arguments":{"confirm_purge":true}}}\n' | bash scripts/run_mcp.shPrepare a local unsent rich draft file without creating a Mail draft:
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"mail_prepare_eml_draft","arguments":{"to":["person@example.com"],"subject":"Hello","text_body":"Plain fallback","html_body":"<p>Plain <strong>fallback</strong></p>","open_in_mail":false}}}\n' | bash scripts/run_mcp.shVisible Apple Mail drafts can be created with mail_create_draft, mail_create_reply_draft, and mail_create_forward_draft.
Sending is intentionally hard to trigger:
- Create or open a visible outgoing draft.
- Inspect it with
mail_inspect_outgoing_draft. - Get explicit user approval for the exact draft.
- Set
ALLOW_MAC_MAIL_SEND=1in the MCP server environment. - Call
mail_send_draftwithconfirm_send=true,approval_note, and the matchingdraft_sha256.
The server re-inspects the outgoing draft immediately before sending and refuses to send if the hash changed.
Git-backed installs expose two update tools:
mail_plugin_update_statuschecks the current checkout against GitHub.mail_plugin_update_installpulls a fast-forward update afterconfirm_update=true; by default it runs in the background and writes a local update log under~/Library/Application Support/Codex Mac Mail/update.log.
Restart Codex after an update so the MCP server reloads the new code.
mail_get_statemail_permissions_checkmail_plugin_update_statusmail_plugin_update_installmail_list_accountsmail_list_mailboxesmail_inbox_overviewmail_search_messagesmail_index_statusmail_purge_body_indexmail_rebuild_body_indexmail_search_bodiesmail_read_messagemail_read_threadmail_list_attachmentsmail_export_attachmentsmail_prepare_eml_draftmail_create_draftmail_create_reply_draftmail_create_forward_draftmail_inspect_outgoing_draftmail_send_draft
Unit tests:
python3 -m unittest discover -s tests -vLive read-only smoke checks:
python3 scripts/live_smoke_test.pyBenchmarks that print timings and counts only:
python3 scripts/benchmark_mail.pyRelease verification:
scripts/verify_release.sh
scripts/verify_release.sh --liveCreate a distributable archive in dist/:
scripts/package_plugin.shThe package excludes caches, private local data, and generated artifacts.
- This plugin does not request or store email credentials.
- It does not write to Apple Mail's SQLite database.
- It does not implement delete, archive, move, mark-read/unread, rules, signatures, or account mutation tools.
- Attachment export requires reviewed names unless
export_all=true. - Outbound attachments must be regular files, are size/count capped, and block common executable/package/script extensions.
- Logs are redacted and live under
~/Library/Application Support/Codex Mac Mail/operation-log.jsonl. - Local Mail data may lag remote providers until Mail.app syncs.
See docs/ARCHITECTURE.md for the data-flow model and non-goals.
MIT. See LICENSE.
