diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-13 17:23:49 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-13 17:23:49 +0200 |
| commit | 3df934d85aee0d0db20c0fc522d60b0178f4018a (patch) | |
| tree | 7bd68b75481e1ec88473832702d62e6251767082 /mail-overview/mail-notify.sh | |
| parent | 18735f3771ae335b147b604e25b805f6bb7afd62 (diff) | |
| download | quickshell-3df934d85aee0d0db20c0fc522d60b0178f4018a.tar.gz quickshell-3df934d85aee0d0db20c0fc522d60b0178f4018a.zip | |
feat(mail-overview): notify per account when mail arrives
Asks notmuch what changed since the revision last seen, then sends
one notification per account with mail in that window. Counts are
validated as integers because a rejected notmuch query prints nothing
and exits 1, and an empty string must not read as zero.
dunstify is backgrounded because -b blocks until the notification is
dismissed, which would otherwise stall the loop for the full timeout
on every account. The watch is on the xapian directory rather than a
file inside it, because a commit replaces files and a watch held on a
filename dies with it.
Diffstat (limited to 'mail-overview/mail-notify.sh')
| -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 |
