From 436efafb13079538f3ad5ab1b3df90e02311143c Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Wed, 12 Aug 2026 11:30:38 +0200 Subject: feat(rules): build the scoped query in one place 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. --- mailrules.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) (limited to 'mailrules.py') diff --git a/mailrules.py b/mailrules.py index 8c65708..dbb80a5 100755 --- a/mailrules.py +++ b/mailrules.py @@ -126,6 +126,25 @@ def load(path=None): return store +def scoped_query(rule, scope): + """The rule's query narrowed by `scope`, or the bare query when scope is + empty. + + The parentheses are load-bearing. notmuch's `and` binds tighter than + `or`, so `tag:new and a or b` means `(tag:new and a) or b`: a rule that + is a disjunction of senders would escape its scope and match the whole + corpus. Do not remove them, and do not build this string anywhere else. + """ + if not scope: + return rule.query + return f"{scope} and ({rule.query})" + + +def tag_arguments(rule): + """The +tag/-tag arguments for `notmuch tag`, adds before removes.""" + return [f"+{t}" for t in rule.add] + [f"-{t}" for t in rule.remove] + + def save(store, path=None): """Write the store atomically: a temp file in the same directory, then rename. Rename within a filesystem is atomic, so a concurrent reader sees -- cgit v1.2.3