aboutsummaryrefslogtreecommitdiffstats
path: root/assets/hooks
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-31 11:42:24 +0100
committerDanilo M. <danix@danix.xyz>2026-10-31 11:42:24 +0100
commita8d84d57b7f3b43fdc0460eb252da45c76ab2baa (patch)
tree9268fce7cce66f624af1d256f391a209d38859b3 /assets/hooks
parent8804a75ae561a7e504fdce8aea58d3ddc8670b71 (diff)
downloadqtmaildir-a8d84d57b7f3b43fdc0460eb252da45c76ab2baa.tar.gz
qtmaildir-a8d84d57b7f3b43fdc0460eb252da45c76ab2baa.zip
feat: an optional allow_archive field on a tagging rule
Both readers learn it together, as the rule format requires. No version bump: the field is optional, and each writer emits it only on a rule that removes inbox, so a file without one saves unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'assets/hooks')
-rwxr-xr-xassets/hooks/mailrules.py9
-rwxr-xr-xassets/hooks/test_mailrules.py35
2 files changed, 43 insertions, 1 deletions
diff --git a/assets/hooks/mailrules.py b/assets/hooks/mailrules.py
index dbb80a5..fbd615b 100755
--- a/assets/hooks/mailrules.py
+++ b/assets/hooks/mailrules.py
@@ -41,7 +41,8 @@ DEFAULT_STAGE = 50
# Fields this version understands. Anything else in a rule object is kept in
# `unknown` and written back untouched, which is what makes the file neutral
# rather than this tool's file that another program may read.
-KNOWN_KEYS = {"id", "stage", "enabled", "add", "remove", "query", "note"}
+KNOWN_KEYS = {"id", "stage", "enabled", "add", "remove", "query", "note",
+ "allow_archive"}
# An id is a handle, not a display name: a UI selects on it and a diff tracks
# it. Tags may contain '/' and may be renamed; ids may not.
@@ -57,6 +58,7 @@ class Rule:
stage: int = DEFAULT_STAGE
enabled: bool = True
note: str = ""
+ allow_archive: bool = False
unknown: dict = field(default_factory=dict)
@@ -194,6 +196,10 @@ def _rule_to_dict(rule):
"query": rule.query,
"note": rule.note,
}
+ # Written only where it does something, matching src/tagrules.cpp, so a
+ # file without an archiving rule round-trips unchanged (item 213).
+ if rule.allow_archive and "inbox" in rule.remove:
+ out["allow_archive"] = True
out.update(rule.unknown)
return out
@@ -257,5 +263,6 @@ def _parse_rule(obj, index, seen, warnings):
stage=stage,
enabled=bool(obj.get("enabled", True)),
note=obj.get("note", "") if isinstance(obj.get("note", ""), str) else "",
+ allow_archive=obj.get("allow_archive") is True,
unknown={k: v for k, v in obj.items() if k not in KNOWN_KEYS},
)
diff --git a/assets/hooks/test_mailrules.py b/assets/hooks/test_mailrules.py
index b1f31c9..ae8fe04 100755
--- a/assets/hooks/test_mailrules.py
+++ b/assets/hooks/test_mailrules.py
@@ -266,6 +266,41 @@ def test_tag_arguments():
assert mailrules.tag_arguments(rule) == ["+one", "+two", "-three"]
+def test_allow_archive_round_trips_and_is_omitted_when_false():
+ """Item 213. Known, so not carried as unknown; written only when true."""
+ with tempfile.TemporaryDirectory() as tmp:
+ path = write_rules(tmp, {
+ "version": 1,
+ "rules": [
+ {"id": "promo", "add": ["promo"], "remove": ["inbox"],
+ "query": "from:a@example.com", "allow_archive": True},
+ {"id": "plain", "add": ["x"], "query": "from:b@example.com"},
+ ],
+ })
+ store = mailrules.load(path)
+ assert store.rules[0].allow_archive is True
+ assert store.rules[1].allow_archive is False
+ assert "allow_archive" not in store.rules[0].unknown
+
+ mailrules.save(store, path)
+ raw = json.loads(path.read_text())
+ assert raw["rules"][0]["allow_archive"] is True
+ assert "allow_archive" not in raw["rules"][1]
+
+
+def test_allow_archive_on_a_rule_keeping_inbox_is_accepted_and_ignored():
+ """Never a reason to drop a rule: the hook runs unattended."""
+ with tempfile.TemporaryDirectory() as tmp:
+ path = write_rules(tmp, {
+ "version": 1,
+ "rules": [{"id": "x", "add": ["x"], "query": "from:a@example.com",
+ "allow_archive": True}],
+ })
+ store = mailrules.load(path)
+ assert len(store.rules) == 1
+ assert store.warnings == []
+
+
def run_all():
for name, fn in sorted(globals().items()):
if name.startswith("test_") and callable(fn):