From b79f34725fca2eb43bf1377e43b9aba4ef29afeb Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Sun, 13 Sep 2026 17:21:31 +0200 Subject: feat(mail-overview): track the notified revision Stores the database UUID beside the revision, because notmuch revisions only compare within one database and a rebuild restarts the counter. Missing, corrupt or mismatched state reports nothing, which the caller treats as seed-silently: with no floor, lastmod:0.. matches every unread message and startup becomes a wall of popups. --- mail-overview/mail-notify.sh | 37 +++++++++++++++++++++++++++++++++++++ mail-overview/test-mail-notify.sh | 25 +++++++++++++++++++++++++ 2 files changed, 62 insertions(+) (limited to 'mail-overview') diff --git a/mail-overview/mail-notify.sh b/mail-overview/mail-notify.sh index ed0c3e3..3e28bab 100755 --- a/mail-overview/mail-notify.sh +++ b/mail-overview/mail-notify.sh @@ -110,6 +110,43 @@ build_body() { printf '\n' } +# The last revision this script notified up to, or empty when there is none +# to trust. Empty means "seed silently": record where we are now and notify +# nothing. +# +# The stored UUID is checked because notmuch revisions are only comparable +# within one database. A rebuilt database restarts the counter, so an old +# revision would be meaningless, and treating it as a floor would either +# notify nothing forever or notify everything at once. +read_prev_rev() { + local want_uuid="$1" got_uuid rev + + [[ -f "$STATE" ]] || return 0 + read -r got_uuid rev < "$STATE" 2>/dev/null || return 0 + + [[ "$got_uuid" == "$want_uuid" ]] || return 0 + [[ "$rev" =~ ^[0-9]+$ ]] || return 0 + + printf '%s' "$rev" +} + +# Written by atomic replace, the same idiom mail-watcher uses for its +# heartbeat: a reader must never see a half-written file, and mv within a +# directory is atomic where a redirect into the final path is not. +# +# Failure to write is deliberately not fatal. The notifications have already +# been sent; taking the watcher down over a failure to record that would turn +# a bookkeeping problem into a no-mail-notifications problem. +write_state() { + local uuid="$1" rev="$2" tmp + + mkdir -p "$(dirname "$STATE")" 2>/dev/null || return 0 + tmp="$(mktemp "${STATE}.XXXXXX")" || return 0 + printf '%s %s\n' "$uuid" "$rev" > "$tmp" || { rm -f "$tmp"; return 0; } + mv -f "$tmp" "$STATE" 2>/dev/null || rm -f "$tmp" + return 0 +} + main() { echo "not implemented" } diff --git a/mail-overview/test-mail-notify.sh b/mail-overview/test-mail-notify.sh index fccf4b3..95e971d 100755 --- a/mail-overview/test-mail-notify.sh +++ b/mail-overview/test-mail-notify.sh @@ -111,5 +111,30 @@ check "malformed json yields an empty body rather than an error" \ "" \ "$(build_body 'not json at all' 1 2>/dev/null)" +# A scratch state file, removed at exit. Never the real one: that is the +# user's live notification position, and a test must not move it. +tmpstate="$(mktemp)" +trap 'rm -f "$tmpstate"' EXIT +STATE="$tmpstate" + +rm -f "$tmpstate" +check "a missing state file reports no previous revision" \ + "" "$(read_prev_rev "uuid-a")" + +write_state "uuid-a" 500 +check "a revision written is a revision read back" \ + "500" "$(read_prev_rev "uuid-a")" + +check "a different database UUID discards the revision" \ + "" "$(read_prev_rev "uuid-b")" + +printf 'garbage not a state file\n' > "$tmpstate" +check "an unparseable state file reports no previous revision" \ + "" "$(read_prev_rev "uuid-a")" + +write_state "uuid-a" 600 +check "a rewrite replaces rather than appends" \ + "1" "$(wc -l < "$tmpstate")" + printf '\n%d passed, %d failed\n' "$pass" "$fail" [[ "$fail" -eq 0 ]] -- cgit v1.2.3