From f1890fc7b5fe1094445ce8b83c2d2f15258fceec Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Wed, 12 Aug 2026 11:42:14 +0200 Subject: feat(rules): read-only rules list and show 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. --- mailctl.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) (limited to 'mailctl.py') diff --git a/mailctl.py b/mailctl.py index be4cbcf..ea31286 100755 --- a/mailctl.py +++ b/mailctl.py @@ -59,6 +59,8 @@ from email.message import EmailMessage from email.utils import make_msgid, formatdate from pathlib import Path +import mailrules + AUDIT_LOG = Path.home() / ".local" / "state" / "mailctl" / "audit.log" MAIL_ROOT = Path(os.environ.get("MAILCTL_MAIL_ROOT", Path.home() / "Mail")) @@ -472,6 +474,41 @@ def log_mutation(scope, query, tag_expr, count): f"change={' '.join(tag_expr)}\tcount={count}\n") +def cmd_rules_list(args): + store = mailrules.load() + for warning in store.warnings: + print(f"warning: {warning}", file=sys.stderr) + if store.missing: + print(f"No rules file at {mailrules.default_path()}", file=sys.stderr) + return + for rule in mailrules.ordered(store.rules) if args.enabled_only \ + else store.rules: + state = " " if rule.enabled else "-" + tags = " ".join(mailrules.tag_arguments(rule)) + print(f"{state} {rule.stage:>4} {rule.id:<28.28} {tags}") + total = len(store.rules) + disabled = sum(1 for r in store.rules if not r.enabled) + print(f"\n{total} rule(s), {disabled} disabled", file=sys.stderr) + + +def cmd_rules_show(args): + store = mailrules.load() + for rule in store.rules: + if rule.id != args.id: + continue + print(f"id: {rule.id}") + print(f"stage: {rule.stage}") + print(f"enabled: {rule.enabled}") + print(f"add: {', '.join(rule.add) or '(none)'}") + print(f"remove: {', '.join(rule.remove) or '(none)'}") + print(f"query: {rule.query}") + if rule.note: + print(f"note: {rule.note}") + return + print(f"No rule with id '{args.id}'", file=sys.stderr) + sys.exit(1) + + def build_parser(): p = argparse.ArgumentParser( prog="mailctl", @@ -550,6 +587,19 @@ def build_parser(): sp.add_argument("--body-file", help="read body from a file instead") sp.set_defaults(func=cmd_draft) + sp = sub.add_parser("rules", help="inspect the shared tagging rules " + "(read-only)") + rules_sub = sp.add_subparsers(dest="rules_command", required=True) + + rp = rules_sub.add_parser("list", help="list every rule in stage order") + rp.add_argument("--enabled-only", action="store_true", + help="only the rules the hook would run") + rp.set_defaults(func=cmd_rules_list) + + rp = rules_sub.add_parser("show", help="show one rule in full") + rp.add_argument("id") + rp.set_defaults(func=cmd_rules_show) + return p -- cgit v1.2.3