aboutsummaryrefslogtreecommitdiffstats
path: root/mail-overview/mail-notify.sh
diff options
context:
space:
mode:
Diffstat (limited to 'mail-overview/mail-notify.sh')
-rwxr-xr-xmail-overview/mail-notify.sh129
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