diff options
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`). |
