Skip to content

Repository files navigation

mailgrep

Read-only search and extraction for the local Apple Mail store on macOS.

Reads the files Mail already keeps on disk — the Envelope Index SQLite database for metadata, .emlx files for bodies. No AppleScript, no IMAP, no dependencies. There is no code path that can send, delete, move or flag a message.

Install

As a Claude Code skill:

git clone https://github.com/hexrw/mailgrep ~/.claude/skills/mailgrep
ln -s ~/.claude/skills/mailgrep/bin/mailgrep ~/.local/bin/mailgrep

The second line puts mailgrep on your PATH. Skip it and the CLI still works at ~/.claude/skills/mailgrep/bin/mailgrep.

As a Claude Code plugin:

/plugin marketplace add hexrw/mailgrep
/plugin install mailgrep@mailgrep

Standalone, anywhere:

git clone https://github.com/hexrw/mailgrep
ln -s "$PWD/mailgrep/bin/mailgrep" ~/.local/bin/mailgrep

pip install . also works, but managed Python installs (Homebrew) refuse it under PEP 668. The shim avoids that entirely — there are no dependencies to install, and it runs on macOS's own /usr/bin/python3.

Grant Full Disk Access

~/Library/Mail is protected by macOS privacy controls. Grant Full Disk Access to the application that launches your shell — your terminal or editor, not mailgrep itself. macOS attributes file access to the launching app, so granting it to the executable does nothing.

System Settings → Privacy & Security → Full Disk Access → add your terminal, then quit and reopen it.

mailgrep doctor detects a missing grant and names the exact app that needs it.

Use

mailgrep doctor                                        # access, schema, coverage, freshness
mailgrep search --from alec@example.com --since 2026-06-01
mailgrep search --subject invoice --body "signed copy"
mailgrep search --to me@example.com --mailbox INBOX --unread
mailgrep read 12345
mailgrep attachments 12345 --extract ./out
mailgrep accounts
mailgrep mailboxes

Run doctor first — it answers most "why is this empty" questions on its own. --from matches address or display name; --subject, --mailbox and --to are substring matches; --body decodes every candidate message and is much slower than metadata filters. Add --json to any command.

Every command reports what it examined, what it could not read, and what it skipped. If a result set is truncated or some messages were unreadable, the output says so — silence means complete.

Exit Meaning
0 success
1 usage or lookup error
2 Full Disk Access missing, no Mail store, or access undetermined
3 envelope index schema not recognised
4 coverage incomplete (doctor only)
5 an attachment could not be extracted

Things that will surprise you

Attachments have four states. Mail strips attachment bytes out of the .emlx and stores them in a sibling directory, so a naive parser gets an empty payload and writes a 0-byte file while reporting success. Worse, not_downloaded attachments were never fetched from the server and do not exist locally at all — on one real mailbox that was 42% of them. mailgrep refuses to write empty output and tells you which state applies.

Exchange/EWS accounts store nothing locally. For those, this returns nothing rather than stale data. Not a bug.

Freshness cannot be measured, only bounded. Mail does not fetch while it is closed, and macOS exposes no last-sync timestamp anywhere. doctor reports whether Mail is running and when it last wrote locally — treat that as a lower bound, never as proof anything synced.

Message ids are SQLite ROWIDs and Mail reassigns them when it rebuilds its index. Use them within a session; don't store them.

doctor can report coverage as INCOMPLETE. That usually means drafts and outbox, which Mail stores differently. search reads those directly from disk anyway, labelled (unindexed), so they still turn up.

Development

just test          # or: python3 -m unittest discover -s tests -t tests

118 tests, run entirely against synthetic fixtures in a temp directory — no real mail, no Full Disk Access, no network.

docs/store-format.md documents what was reverse-engineered about the store: schema resolution, timestamp epochs, the attachment layout, flag bits, and the TCC behaviour. Worth reading before changing anything.

License

MIT

About

Read-only search and extraction for the local Apple Mail store on macOS. Zero dependencies, no AppleScript, cannot send or modify mail.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages