diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-03 11:30:06 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-03 11:30:06 +0200 |
| commit | 9f586a8e79c73cba5267aa1848c72123ded37f9b (patch) | |
| tree | 011a0ea05a9858dd0759b9c267712fc218084b5c /CLAUDE.md | |
| download | mailctl-master.tar.gz mailctl-master.zip | |
mailctl is a deliberately narrow CLI wrapper around notmuch, built so an
AI coding agent can search and organize local mail with no ability to
send. There is no SMTP, reply, or compose code path in the tool.
Safety model:
- reads run freely, mutations are gated
- tag changes are dry-run until --apply
- cross-account mutations need an explicit --all-accounts
- destructive tags need --apply and --confirm-destructive
- bulk mutations are capped by --max-messages
- every applied mutation is audited to a local log
Accounts are not in the source. They load from
~/.config/mailctl/accounts.json and are validated against both the
schema and the actual maildirs on disk at import time, so a typo cannot
produce a query matching nothing or a draft under the wrong identity.
Ships with mailsync.sh (a separate mbsync + notmuch new driver, meant
for cron) and mail-organize, a Claude Code skill that makes mailctl the
only sanctioned interface to the user's mail.
Licensed GPLv2-only.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'CLAUDE.md')
| -rw-r--r-- | CLAUDE.md | 71 |
1 files changed, 71 insertions, 0 deletions
diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1cf9b0a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,71 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +`mailctl.py` is a deliberately narrow, agent-safe CLI wrapper around `notmuch`. It exists so an agent can search and organize local mail **without any ability to send**. There is no SMTP, reply, or compose code path in the tool. Adding one violates the core design; do not add send capability. + +`mailsync.sh` is the separate sync driver (mbsync + `notmuch new`), meant for cron/systemd, not called by `mailctl.py`. + +## Installation + +No installer, just copy files into place: + +```bash +cp mailctl.py mailsync.sh ~/bin/ # executables (skill calls ~/bin/mailctl.py) +cp -r mail-organize ~/.claude/skills/ # skill in its own directory +``` + +The skill's `allowed-tools` expects the tool at `~/bin/mailctl.py`; keep that path. + +## Running + +```bash +./mailctl.py search "from:foo and tag:inbox" # global read +./mailctl.py search QUERY --account NAME # scoped read +./mailctl.py count QUERY [--account NAME] +./mailctl.py show thread:0000... | id:... +./mailctl.py tags # list all tags in use +./mailctl.py senders QUERY [--account NAME] [--top N] # senders ranked by count +./mailctl.py subjects QUERY [--account NAME] [--top N] # subject terms ranked by thread count +./mailctl.py tag QUERY --account NAME --add work --remove inbox # dry-run +./mailctl.py tag QUERY --account NAME --add work --apply # commit +``` + +Requires `notmuch` on PATH and a synced Maildir at `~/Mail`. No build, no deps beyond the stdlib and the `notmuch` binary. The only test is `./test_mailctl.py` (plain asserts, no framework), covering the `senders` address-merge and `subjects` term-counting logic. + +## Safety model (the reason the code is shaped this way) + +These invariants are the point of the tool. Preserve them when editing: + +- **Reads are free, mutations are gated.** `search`/`show`/`tags`/`count`/`senders`/`subjects` take an optional `--account` (default = global across all mailboxes). `tag` refuses to run without either `--account NAME` or the explicit `--all-accounts`, so a cross-account mutation is never accidental. +- **Tag changes are dry-run by default.** `tag` prints what would change and only touches the index with `--apply`. +- **Destructive changes need a second gate.** Adding a tag in `DESTRUCTIVE_TAGS` (`deleted`/`trash`/`spam`) or removing one in `PROTECTED_REMOVALS` (`inbox`) requires `--apply` AND `--confirm-destructive`. +- **Bulk mutations are capped.** `tag --apply` aborts if the match count exceeds `--max-messages` (default `DEFAULT_MAX_MESSAGES`, 5000). Raise the flag to override for a deliberate large batch. +- **Every applied mutation is audited** to `~/.local/state/mailctl/audit.log` (tab-separated, timestamped) via `log_mutation`. + +## Key structures + +- `ACCOUNTS` / `DRAFTS_SUBDIR`: built at import time by `load_accounts()` from `~/.config/mailctl/accounts.json` (`MAILCTL_CONFIG` overrides the path, `MAILCTL_MAIL_ROOT` the maildir root). **The real addresses and maildir names are not in the repo**, which is what lets the source be published. Same shapes as before: account key → (`~/Mail` subdir, From address), and account key → Drafts folder name or `None`. +- `validate_accounts()` is the gate that replaces hardcoding. It checks schema (unknown/missing fields, key charset, address form, no absolute or `..` maildir, no duplicate maildirs) **and** the filesystem (the maildir and any Drafts dir must exist), then exits 2 with a message naming the account and field. It runs at import, before `build_parser()` reads `ACCOUNTS` for its `choices=`, so a bad config can never reach a query or a draft. Do not make this lazy or non-fatal. +- `scoped_query` turns an account key into a `path:"subdir/**" and (query)` filter. +- `cmd_draft` writes a **local-only** draft into the account's Drafts maildir using the atomic tmp/→rename→new/ pattern. It never sends; the draft reaches the server only on the next mbsync run. (Note: `draft` isn't listed in "Running" above because it's an outbound-adjacent path; it still writes nothing to the network.) + +Adding an account is a config edit, not a code edit: add an entry to `accounts.json` and run any command; validation reports a wrong maildir or Drafts name immediately. + +`test_mailctl.py` builds a throwaway maildir tree and config in a tempdir and sets `MAILCTL_CONFIG`/`MAILCTL_MAIL_ROOT` **before** importing `mailctl`, because the import validates. Keep that ordering when editing the tests. + +## Skill: mail-organize + +`mail-organize/SKILL.md` is a project skill governing **all** agent interaction with the user's mail. When a task involves searching, reviewing, tagging, archiving, or drafting the user's email, invoke this skill and follow it; it is the only sanctioned interface (via `~/bin/mailctl.py`), not raw `notmuch`/`mbsync`. Its hard rules mirror and tighten the tool's own gates: + +- Never send/deliver mail; draft instead (a draft is not a send). +- Never sync (`mbsync`/`mailsync.sh`/`notmuch new`) — that's the user's hourly cron; ask them if the index seems stale. +- Scope `tag` mutations to one `--account`; don't use `--all-accounts` unless the user explicitly asked this turn. +- Always dry-run and show the preview before `--apply`; never pass `--confirm-destructive` on your own judgment. +- Global search (no `--account`) is fine — it's read-only. + +## Known gaps (from the module docstring) + +- Audit log doesn't distinguish agent-run vs user-run (no `--actor`). |
