aboutsummaryrefslogtreecommitdiffstats
AgeCommit message (Collapse)AuthorFilesLines
13 daysfeat(rules): dry-run counts what a rule matchesDanilo M.1-0/+27
Two numbers per rule: what it matches across all mail, and what it would tag on the next sync. The first is what decides whether a rule is still earning its place.
13 daysfeat(rules): read-only rules list and showDanilo M.3-2/+68
Editing stays out of this tool: a rule edit affects every future sync and its gate is not designed yet. qtmaildir has the editor. The install step now copies mailrules.py beside mailctl.py. The import resolves from the script's own directory, so copying only mailctl.py would break every command, not just the new ones.
13 daystest(rules): verify the hook against a throwaway databaseDanilo M.1-0/+214
Covers what unit tests cannot: that a rule tags only what it matches, that stage order decides what a later rule can see, that a failed rule leaves tag:new in place, that a disjunction stays inside its scope, and that a rule removing a protected tag is skipped whole while the run continues and still consumes the marker. Three of those were confirmed to fail against a deliberately broken implementation: unparenthesised scoping, a guard that stripped only the protected removals, and a guard that aborted the run.
13 daysfeat(rules): post-new hook driven by the rule storeDanilo M.1-0/+137
Applies each enabled rule scoped to tag:new in stage order, then consumes the marker. Two properties are deliberate and must survive any edit: A rules file that will not load leaves tag:new alone and exits non-zero. Clearing the marker while the rules did not run would permanently orphan that mail, silently, and the gap would surface months later. The marker is consumed only after every rule succeeded, so a failure part way through leaves it set and a re-run finishes the work. Removing `unread` or `inbox` is refused. maildir.synchronize_flags is true, so removing `unread` rewrites Maildir filenames and reaches the server on the next mbsync, and `inbox` is what keeps mail visible at all; neither belongs in an unattended job. A rule that asks is skipped rather than aborting the run, which would both cost the other rules their tagging and strand tag:new forever. The restriction is expected to be lifted once there is a way to confirm such a rule before it runs.
13 daysfeat(rules): build the scoped query in one placeDanilo M.2-0/+48
A rule stores no scope. The hook supplies tag:new, a dry run supplies nothing. The parentheses around the rule's own query are what stop a disjunction of senders from escaping the scope and matching everything.
13 daysfeat(rules): save atomically, preserving unknown fieldsDanilo M.2-0/+101
Write to a temp file and rename, so the hook can never read a partial file. Fields this version does not understand round-trip untouched, which is what makes the format belong to neither tool.
13 daysfeat(rules): order rules by stage, ties by file positionDanilo M.2-0/+47
Account tags must run before topic rules, which is what the stage field is for. Disabled rules stay loaded so a UI can re-enable them.
13 daysfeat(rules): drop a malformed rule rather than the whole fileDanilo M.2-13/+167
One typo must not stop the other rules from tagging. A missing file and a file that will not parse are tracked separately: the hook may run on the first and must refuse on the second.
13 daysfeat(rules): load a shared tagging-rule storeDanilo M.2-0/+189
The rules that tag incoming mail live in a notmuch post-new hook as hand-written shell. This is the first piece of moving them into a JSON store that both this tool and qtmaildir read. A rule carries no scope: the hook supplies tag:new, a dry run supplies nothing. Unknown fields are kept per rule so the format belongs to neither tool.
2026-08-04deleted mailsync.sh as it now lives in **mailctl** project repository.Danilo M.3-71/+21
2026-08-03Initial commit: agent-safe notmuch wrapperDanilo M.9-0/+1588
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>