aboutsummaryrefslogtreecommitdiffstats

mailctl

A deliberately narrow, agent-safe CLI wrapper around notmuch, 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:

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):

{
  "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

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

# 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 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.