aboutsummaryrefslogtreecommitdiffstats
path: root/mailrules.py
diff options
context:
space:
mode:
Diffstat (limited to 'mailrules.py')
-rwxr-xr-xmailrules.py54
1 files changed, 54 insertions, 0 deletions
diff --git a/mailrules.py b/mailrules.py
index 7df1b42..8c65708 100755
--- a/mailrules.py
+++ b/mailrules.py
@@ -31,6 +31,7 @@ runs on every sync, and mailctl has no dependencies to inherit.
import json
import os
import re
+import tempfile
from dataclasses import dataclass, field
from pathlib import Path
@@ -125,6 +126,59 @@ def load(path=None):
return store
+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
+ either the old file or the new one and never a partial write.
+
+ There is no locking. Last writer wins on a true collision, which is
+ accepted for a single-user setup; the failure that would actually hurt is
+ a truncated read by the hook, and rename eliminates it.
+ """
+ path = Path(path) if path else default_path()
+ path.parent.mkdir(parents=True, exist_ok=True)
+
+ payload = dict(store.unknown)
+ payload["version"] = FORMAT_VERSION
+ payload["rules"] = [_rule_to_dict(r) for r in store.rules]
+
+ # delete=False plus an explicit replace: NamedTemporaryFile would unlink
+ # the file on close, and the rename is the whole point.
+ handle = tempfile.NamedTemporaryFile(
+ mode="w", dir=path.parent, prefix=".rules-", suffix=".tmp",
+ delete=False)
+ try:
+ with handle:
+ json.dump(payload, handle, indent=2, ensure_ascii=False)
+ handle.write("\n")
+ handle.flush()
+ os.fsync(handle.fileno())
+ os.replace(handle.name, path)
+ except BaseException:
+ # A failed write must not leave the temp file beside the real one.
+ try:
+ os.unlink(handle.name)
+ except OSError:
+ pass
+ raise
+
+
+def _rule_to_dict(rule):
+ """Known fields first in a stable order, then anything this version did
+ not understand. Stable ordering keeps a diff of this file readable."""
+ out = {
+ "id": rule.id,
+ "stage": rule.stage,
+ "enabled": rule.enabled,
+ "add": list(rule.add),
+ "remove": list(rule.remove),
+ "query": rule.query,
+ "note": rule.note,
+ }
+ out.update(rule.unknown)
+ return out
+
+
def ordered(rules):
"""Enabled rules in execution order: by stage ascending, ties by position.