aboutsummaryrefslogtreecommitdiffstats
path: root/CLAUDE.md
diff options
context:
space:
mode:
Diffstat (limited to 'CLAUDE.md')
-rw-r--r--CLAUDE.md71
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`).