diff options
| -rwxr-xr-x | mail-overview/mail-notify.sh | 129 |
1 files changed, 128 insertions, 1 deletions
diff --git a/mail-overview/mail-notify.sh b/mail-overview/mail-notify.sh index 3e28bab..8309b51 100755 --- a/mail-overview/mail-notify.sh +++ b/mail-overview/mail-notify.sh @@ -147,8 +147,135 @@ write_state() { return 0 } +# Sends one notification for one account. +# +# dunstify rather than notify-send because actions need it. A stack tag per +# account means a second batch for the same account replaces the first rather +# than stacking, which is what "one notification per account" has to mean when +# mail keeps arriving. +# +# Normal urgency and an explicit 10s timeout, deliberately not -u critical: +# on most dunst configurations critical notifications never expire, which +# would leave mail popups on screen until clicked. +# +# The click cannot open the account it belongs to. qtmaildir accepts no +# command line arguments and startup_account is a static config setting, not +# a flag, which is the same limitation the drawer's thread rows already have. +notify_account() { + local label="$1" key="$2" count="$3" body="$4" + + if ! command -v dunstify >/dev/null 2>&1; then + # No actions available, but a notification without a click is still + # worth having. + notify-send -a mail-overview -u normal -t 10000 \ + "$label · $count new" "$body" + return 0 + fi + + # Backgrounded because -b blocks until the notification is dismissed or + # clicked. Without this the loop would stall for the full timeout on + # every account, and a five-account batch would take most of a minute. + ( + if [[ "$(dunstify -a mail-overview -u normal -t 10000 -b \ + -h "string:x-dunst-stack-tag:mail-$key" \ + -A "default,open" \ + "$label · $count new" "$body")" == "default" ]]; then + "$HOME/bin/qtmaildir" & + fi + ) >/dev/null 2>&1 & +} + +# One pass: what has arrived since the revision we last saw. +tick() { + local lastmod uuid cur prev + + # Three tab-separated fields: count, database UUID, revision. Verified on + # notmuch 0.39. + lastmod="$(notmuch count --lastmod "$SCOPE" 2>/dev/null)" || return 0 + uuid="$(printf '%s' "$lastmod" | cut -f2)" + cur="$(printf '%s' "$lastmod" | cut -f3)" + + # The output is the test, not the exit status. notmuch fails two ways and + # only one is detectable: a rejected query prints nothing and exits 1, + # while a query Xapian merely misparses returns a plausible wrong number + # and exits 0. The defence against the second is that SCOPE is a fixed + # string and is never built from anything. + [[ "$cur" =~ ^[0-9]+$ ]] || return 0 + [[ -n "$uuid" ]] || return 0 + + prev="$(read_prev_rev "$uuid")" + + # No trustworthy floor: record where we are and say nothing. This is the + # first run, a rebuilt database, or a corrupt state file. + if [[ -z "$prev" ]]; then + write_state "$uuid" "$cur" + return 0 + fi + + # Nothing committed since last time, or the counter went backwards. + if [[ "$cur" -le "$prev" ]]; then + return 0 + fi + + local key label query count rows + while IFS=$'\t' read -r key label; do + [[ -n "$key" ]] || continue + + query="$SCOPE and tag:account-$key and lastmod:$prev..$cur" + + count="$(notmuch count "$query" 2>/dev/null)" + # Same validation, same reason: an empty string must not become a + # zero, and a failure here skips this account rather than the batch. + [[ "$count" =~ ^[0-9]+$ ]] || continue + [[ "$count" -gt 0 ]] || continue + + rows="$(notmuch search --format=json --limit="$ROWS" \ + --sort=newest-first "$query" 2>/dev/null)" || rows="[]" + + notify_account "$label" "$key" "$count" "$(build_body "$rows" "$count")" + done < <(parse_accounts < "$CONFIG") + + # Written only after every account is done, so a failure part way through + # leaves prev unchanged and the next tick retries rather than dropping a + # batch silently. + write_state "$uuid" "$cur" +} + main() { - echo "not implemented" + local db + db="$(notmuch config get database.path 2>/dev/null)/xapian" + + if [[ ! -d "$db" ]]; then + echo "mail-notify: no notmuch database at $db" >&2 + exit 1 + fi + + if [[ ! -r "$CONFIG" ]]; then + echo "mail-notify: cannot read $CONFIG" >&2 + exit 1 + fi + + if [[ "${1:-}" == "--once" ]]; then + tick + return 0 + fi + + # Seed before watching, so a first run never notifies the backlog. + tick + + # The watch is on the xapian DIRECTORY, not a file inside it: a commit + # replaces files, and a watch held on a filename dies with the file. + while inotifywait -qq -e close_write,moved_to "$db" 2>/dev/null; do + # One commit touches several files. Without this, a single sync fires + # three or four ticks. + sleep 0.3 + tick + done + + # Falling out means inotifywait itself failed. Say so rather than exiting + # silently, which is indistinguishable from no mail arriving. + echo "mail-notify: inotify watch stopped" >&2 + exit 1 } # Sourced by the test with MAIL_NOTIFY_LIB set, which must not start a watch |
