Search, inspect, or mutate Apple Mail on macOS using local files and Mail.app. Use for Apple Mail, Mac Mail, local mail, email bodies, downloaded mail, .emlx files, Mail's Envelope Index, sqlite3, rg, Mail.app search diagnostics, drafting, sending, moving, archiving, flagging, or marking messages read/unread.
How this skill is triggered — by the user, by Claude, or both
Slash command
/jtennant-agent-config:apple-mailThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Use local Mail files. Avoid AppleScript for broad search: it is slow, timeout-prone, and hides permission/indexing problems.
Use local Mail files. Avoid AppleScript for broad search: it is slow, timeout-prone, and hides permission/indexing problems.
Requires Full Disk Access for the terminal/agent app. If ~/Library/Mail is empty, unreadable, or SQLite says unable to open database file, stop and have the user grant Full Disk Access:
open "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"
bin/mail-find wraps SQLite or rg and returns compact rowid records. bin/mail-read consumes rowids for metadata, normalized bodies, excerpts, packets, and coverage. Keep search strategy in the skill/agent; use helpers for stable rowid IO.
Before any Apple Mail operation — search, read, verify, draft, send, delete, or move — force a background Mail sync. Do not foreground Mail just to sync.
osascript <<'APPLESCRIPT'
tell application "Mail"
repeat with acct in accounts
try
synchronize with acct
end try
end repeat
end tell
APPLESCRIPT
After syncing, query Mail's Envelope Index or re-run the relevant helper. If waiting for a just-sent external confirmation email, allow a short delay after sync before checking the index.
~/Library/Mail/V*/MailData/Envelope Index
~/Library/Mail/V*/**/*.emlx
.partial.emlx files. Partial files can contain useful text, but are best effort.Thin wrappers preserve the native search shape and add the metadata users usually fetch next:
MAIL_ROOT="$HOME/Library/Mail/$(ls "$HOME/Library/Mail" | grep '^V[0-9]' | sort -V | tail -1)"
"{{SKILL_DIR}}/bin/mail-find" describe
"{{SKILL_DIR}}/bin/mail-find" sql "s.subject like '%invoice%' collate nocase" --order-by "m.date_received desc" --limit 50
"{{SKILL_DIR}}/bin/mail-find" rg 'contract term' --limit 50
rg -l 'project name' "$MAIL_ROOT" -g '*.emlx' | "{{SKILL_DIR}}/bin/mail-find" paths
mail-find describe shows SQL aliases, output shape, and common columns. mail-find rg returns raw rg path/line/text plus rowid, dates, sender, subject, mailbox, path, downloaded, and partial. mail-find sql runs a transparent metadata WHERE clause with optional raw --order-by and adds path/download status. For anything unusual, use SQLite/rg directly.
Common metadata filters:
"{{SKILL_DIR}}/bin/mail-find" sql "m.date_received >= strftime('%s','now','-7 days')" --limit 20
"{{SKILL_DIR}}/bin/mail-find" sql "m.read = 0" --limit 20
"{{SKILL_DIR}}/bin/mail-find" sql "m.read = 0 and m.date_received >= strftime('%s','now','-7 days')" --limit 20
Newer than a last-seen row id:
LAST_SEEN=12345
"{{SKILL_DIR}}/bin/mail-find" sql "m.rowid > $LAST_SEEN" --order-by "m.rowid asc" --limit 50
For polling, keep the largest returned id as the next last_seen.
Use these after metadata/body search identifies relevant row ids:
"{{SKILL_DIR}}/bin/mail-read" meta 12345 12346
"{{SKILL_DIR}}/bin/mail-read" show 12345 --limit 8000
"{{SKILL_DIR}}/bin/mail-read" excerpt 'contract term' 12345 12346 --context 500
"{{SKILL_DIR}}/bin/mail-read" packet 12345 12346 --term 'contract term' > source-packet.md
"{{SKILL_DIR}}/bin/mail-read" coverage
mail-read handles .emlx byte-count lines, MIME parts, transfer encodings, HTML noise, rowid-to-path mapping, and JSON/markdown output. It accepts --ids-from - for pipelines:
"{{SKILL_DIR}}/bin/mail-find" sql "s.subject like '%invoice%' collate nocase" \
| jq -r '.[].id' \
| "{{SKILL_DIR}}/bin/mail-read" packet --ids-from -
For evidence work:
mail-read only on selected row ids.rg snippets alone.Useful targeted query for a date window plus people/subjects:
MAIL_ROOT="$HOME/Library/Mail/$(ls "$HOME/Library/Mail" | grep '^V[0-9]' | sort -V | tail -1)"
MAIL_DB="$MAIL_ROOT/MailData/Envelope Index"
sqlite3 -readonly -header -csv "$MAIL_DB" <<'SQL'
select m.rowid id,
datetime(m.date_sent, 'unixepoch') sent,
coalesce(a.address, '') sender_email,
coalesce(a.comment, '') sender_name,
s.subject
from messages m
left join subjects s on s.rowid = m.subject
left join addresses a on a.rowid = m.sender
where m.deleted = 0
and m.date_sent between strftime('%s','2022-01-01') and strftime('%s','2023-01-01')
and (s.subject like '%project%' collate nocase
or a.address like '%example.com%' collate nocase
or a.comment like '%Example Name%' collate nocase)
order by m.date_sent;
SQL
Check indexed mail versus downloaded bodies before claiming body-search completeness:
python3 - <<'PY'
import sqlite3
from pathlib import Path
root = Path.home() / 'Library/Mail'
mail_root = root / sorted(p.name for p in root.iterdir() if p.name.startswith('V'))[-1]
con = sqlite3.connect(f'file:{mail_root / "MailData/Envelope Index"}?mode=ro', uri=True)
indexed = con.execute('select count(*) from messages where deleted=0').fetchone()[0]
full = partial = 0
for p in mail_root.rglob('*.emlx'):
partial += '.partial.' in p.name
full += '.partial.' not in p.name
print({'indexed': indexed, 'local_total': full + partial, 'full': full, 'partial': partial})
PY
For Apple Mail mutations, use Mail.app APIs, never mutate Mail SQLite or .emlx files. Use AppleScript only for exact object deletion or inspection when no Mail AppIntent covers the operation.
Before running, verify the original message's row id, account/mailbox, sender, subject, and RFC Message-ID. Tell the user that Mail will open a compose window.
python3 {{SKILL_DIR}}/bin/mailkit.py \
reply-composer ORIGINAL_ROW_ID --body-file /path/to/reply.txt
This opens a verified native reply composer and leaves it open for review and Mail autosave. Do not run it when another composer with the same reply subject is open.
After the user explicitly approves sending, target the exact subject, recipient, and expected body:
python3 {{SKILL_DIR}}/bin/mailkit.py send-composer \
--subject 'Re: EXACT SUBJECT' \
--to '[email protected]' \
--body-file /path/to/reply.txt
This clicks the unique enabled Send button in the matching Mail composer, then verifies that the composer closed, the draft disappeared, and a matching sent message was indexed.
Replace the composer rather than updating it in place:
reply-composer.python3 {{SKILL_DIR}}/bin/mailkit.py \
reply-composer ORIGINAL_ROW_ID \
--body-file /path/to/revised-reply.txt \
--replace-draft-message-id '<STALE-DRAFT-RFC-MESSAGE-ID>'
Use only when the user wants an immediately persisted draft and accepts Mail's save confirmation UI:
python3 {{SKILL_DIR}}/bin/mailkit.py \
reply-draft ORIGINAL_ROW_ID --body-file /path/to/reply.txt
date_sent and date_received are Unix timestamps. Do not add the Apple epoch unless verified.document_id may be binary-ish and useless for locating files. Use rowid -> **/Messages/<rowid>.emlx..partial.emlx can be enough for evidence, but absence from body files is not absence from Mail.npx claudepluginhub johnmatthewtennant/jtennant-agent-configGuides collaborative design exploration before implementation: explores context, asks clarifying questions, proposes approaches, and writes a design doc for user approval.
Creates structured, bite-sized implementation plans from specs or requirements before writing code. Useful for breaking down multi-step tasks into testable steps with file structure and task boundaries.
Resolves in-progress git merge or rebase conflicts by analyzing history, understanding intent, and preserving both changes where possible. Runs automated checks after resolution.