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 /README.md | |
| download | mailctl-9f586a8e79c73cba5267aa1848c72123ded37f9b.tar.gz mailctl-9f586a8e79c73cba5267aa1848c72123ded37f9b.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 'README.md')
| -rw-r--r-- | README.md | 111 |
1 files changed, 111 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e1ef04 --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ +# mailctl + +A deliberately narrow, agent-safe CLI wrapper around [notmuch](https://notmuchmail.org/), meant for use by AI coding agents (Claude Code, opencode, etc.) to **search and organize** local mail. It has no ability to send: there is no SMTP, reply, or compose code path in the tool at all. + +## Why + +An agent given raw `notmuch`/`mbsync` access can do anything, including mangle tags across every account or trigger a sync at the wrong moment. `mailctl` closes that surface: + +- **Reads run freely**, mutations are gated. +- **Tag changes are dry-run by default**, and only touch the index with `--apply`. +- **Cross-account mutations require an explicit opt-in** (`--all-accounts`); the default forces a single `--account`. +- **Destructive changes** (adding `deleted`/`trash`/`spam`, removing `inbox`) need `--apply` **and** `--confirm-destructive`. +- **Every applied mutation is audited** to `~/.local/state/mailctl/audit.log`. +- **Drafting is possible, sending is not.** `mailctl draft` writes a local-only message into the account's Drafts folder for you to review and send yourself in neomutt. + +## Requirements + +- `notmuch` on `PATH`, with a synced Maildir at `~/Mail` (see Configuration). +- Python 3 (standard library only, no third-party packages). + +## Installation + +No installer, just copy files into place: + +```bash +cp mailctl.py mailsync.sh ~/bin/ # executables +cp -r mail-organize ~/.claude/skills/ # Claude Code skill, in its own dir +``` + +The skill invokes the tool at `~/bin/mailctl.py`; keep that path. + +## Configuration + +Your accounts are not in the source. `mailctl` reads them from `~/.config/mailctl/accounts.json` (override with the `MAILCTL_CONFIG` environment variable): + +```json +{ + "accounts": { + "work": { + "maildir": "work-mbsync-dir", + "address": "you@example.org", + "drafts": "Drafts" + }, + "personal": { + "maildir": "personal-mbsync-dir", + "address": "you@example.net", + "drafts": null + } + } +} +``` + +- `maildir` is the account's subdirectory under `~/Mail`, exactly as mbsync created it. +- `address` is the real From address, used only when writing a draft. +- `drafts` is the Drafts folder name inside that maildir, matching the account's mbsync `Patterns` (`Drafts`, `[Gmail]/Bozze`, ...). Use `null`, or omit it, if that account has no synced Drafts folder; `mailctl draft` then refuses that account. + +The account map stays a closed set. The config is checked against its schema **and** against the actual directories on disk when `mailctl` starts, and any problem is a hard exit before a query runs: a misspelled `maildir` can't silently produce a filter matching nothing, and a misspelled `drafts` can't put a draft somewhere mbsync never syncs. Set `MAILCTL_MAIL_ROOT` if your Maildir is not at `~/Mail`. + +## Usage + +```bash +mailctl search "<notmuch query>" [--account NAME] [--json] +mailctl show "<thread:id or id:msgid>" +mailctl tags # list all tags in use +mailctl count "<query>" [--account NAME] +mailctl senders "<query>" [--account NAME] [--top N] [--json] # ranked by count +mailctl subjects "<query>" [--account NAME] [--top N] [--json] # subject terms +mailctl tag "<query>" --account NAME [--add TAG]... [--remove TAG]... + [--apply] [--confirm-destructive] +mailctl draft --account NAME --to ADDR --subject TEXT + [--cc ADDR] [--body TEXT | --body-file PATH] +``` + +Reads default to global scope (all accounts) when `--account` is omitted. `tag` refuses to run without either `--account NAME` or `--all-accounts`. + +### Example + +```bash +# Preview: what would tagging these as 'newsletter' touch? +mailctl tag "from:substack.com" --account personal --add newsletter + +# Commit it after reviewing the preview +mailctl tag "from:substack.com" --account personal --add newsletter --apply +``` + +## Companion sync script + +`mailsync.sh` runs `mbsync -a` followed by `notmuch new`, with a `flock` guard and timestamped, rotated logging. It is meant for a cron/systemd timer and is **not** called by `mailctl.py`; sync and organization stay separate on purpose. + +## Claude Code skill + +`mail-organize/` is a Claude Code skill that makes `mailctl` the only sanctioned interface for an agent to touch your mail, and tightens the tool's gates into workflow rules (always dry-run first, never sync, never send, single-account scope). See `mail-organize/SKILL.md`. + +## License + +Copyright (C) 2026 Danilo M. <danix@danix.xyz> + +Released under the **GNU General Public License, version 2** (GPLv2-only). See +[`LICENSE`](LICENSE) for the full text. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. + +## Development Approach + +This project is developed using AI-assisted tools. Code is generated with the help of AI based on human-provided specifications, design decisions, and iterative feedback. + +All contributions are reviewed, tested, and curated by the maintainer before being included in the codebase. AI is used as a productivity and exploration tool, while human oversight remains central to all decisions. + +The goal is to combine the flexibility of AI-assisted development with standard open-source practices such as transparency, review, and accountability. |
