fix: update version.txt for 1.26.2 release
This commit is contained in:
parent
6d5adeef9c
commit
fb68a1b011
8 changed files with 691 additions and 0 deletions
332
check_mail_deliverability.sh
Executable file
332
check_mail_deliverability.sh
Executable file
|
|
@ -0,0 +1,332 @@
|
|||
#!/usr/bin/env bash
|
||||
# check_mail_deliverability.sh — Taler mail-plane deliverability (ops-checklist)
|
||||
#
|
||||
# SUT domains (NOT cyon): taler.net, gnunet.org, firefly.gnunet.org,
|
||||
# taler-systems.com, anastasis.lu, taler-ops.ch (from mail-catalog.conf)
|
||||
#
|
||||
# Based on admin-log/firefly/ops-checklist.md (2026-03 MT/ED scores):
|
||||
# dig SPF + DMARC + DKIM selector(s); SMTP RCPT smoke via monitord helper;
|
||||
# optional IMAP sink poll (mon001–003@hacktivism.ch on cyon = sink only).
|
||||
#
|
||||
# Explicit phase (not in all/full):
|
||||
# ./taler-monitoring.sh mail-deliverability
|
||||
# ./taler-monitoring.sh maildeliv
|
||||
#
|
||||
# Env:
|
||||
# MAIL_CATALOG default mail-catalog.conf
|
||||
# MAIL_DKIM_SELECTORS default "mail-2026-3 mail" (space-separated)
|
||||
# MAIL_DELIV_IMAP=1 enable sink IMAP login+INBOX check (needs pass)
|
||||
# MAIL_DELIV_IMAP_USERS default mon001@hacktivism.ch mon002@… mon003@…
|
||||
# MAIL_DELIV_IMAP_HOST default mail.cyon.ch (sink host — not SUT)
|
||||
# MAIL_DELIV_IMAP_PASS shared password for mon* (prefer secrets.env)
|
||||
# MAIL_DELIV_SEND=0 1 = authenticated send (Freigabe; needs SMTP_*)
|
||||
# MAIL_DELIV_ALERT=0 1 = on ERROR, send alert via sink SMTP (Freigabe)
|
||||
# MAIL_DELIV_ALERT_FROM default mon001@hacktivism.ch
|
||||
# MAIL_DELIV_ALERT_TO default hernani+mon@taler-systems.com
|
||||
# hernani+mon@vecirex.net vecirex+mon@icloud.com
|
||||
# fdold+mon@taler.net
|
||||
# MAIL_DELIV_ALERT_SMTP default mail.cyon.ch:587 (sink submission — not SUT)
|
||||
# MAIL_DELIV_ALERT_PASS default MAIL_DELIV_IMAP_PASS
|
||||
# MAIL_DELIV_ALERT_COOLDOWN_S default 21600 (6h); stamp under monitord state
|
||||
# TALER_MONITORD_DELIVER_* see site-gen smtp_deliverability_probe
|
||||
#
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=lib.sh
|
||||
source "$ROOT/lib.sh"
|
||||
|
||||
set_area maildeliv
|
||||
section "maildeliv · Taler mail domains (SPF/DMARC/DKIM + SMTP RCPT; cyon=sink only)"
|
||||
|
||||
CATALOG="${MAIL_CATALOG:-$ROOT/mail-catalog.conf}"
|
||||
SELECTORS="${MAIL_DKIM_SELECTORS:-mail-2026-3 mail}"
|
||||
: "${MAIL_DELIV_IMAP:=0}"
|
||||
: "${MAIL_DELIV_SEND:=0}"
|
||||
: "${MAIL_DELIV_ALERT:=0}"
|
||||
: "${MAIL_DELIV_PROBE:=0}"
|
||||
IMAP_HOST="${MAIL_DELIV_IMAP_HOST:-mail.cyon.ch}"
|
||||
IMAP_USERS="${MAIL_DELIV_IMAP_USERS:-mon001@hacktivism.ch mon002@hacktivism.ch mon003@hacktivism.ch}"
|
||||
ALERT_FROM="${MAIL_DELIV_ALERT_FROM:-mon001@hacktivism.ch}"
|
||||
ALERT_TO="${MAIL_DELIV_ALERT_TO:-hernani+mon@taler-systems.com hernani+mon@vecirex.net vecirex+mon@icloud.com fdold+mon@taler.net}"
|
||||
ALERT_SMTP="${MAIL_DELIV_ALERT_SMTP:-mail.cyon.ch:587}"
|
||||
ALERT_PASS="${MAIL_DELIV_ALERT_PASS:-${MAIL_DELIV_IMAP_PASS:-}}"
|
||||
ALERT_COOLDOWN_S="${MAIL_DELIV_ALERT_COOLDOWN_S:-21600}"
|
||||
STATE_DIR="${TALER_MONITORD_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/taler-monitord}"
|
||||
ALERT_STAMP="${STATE_DIR}/mail-deliv-alert.stamp"
|
||||
|
||||
if [ ! -f "$CATALOG" ]; then
|
||||
fail "catalog" "missing $CATALOG"
|
||||
summary
|
||||
exit 1
|
||||
fi
|
||||
|
||||
info "scope" "SUT = catalog Taler domains · sink IMAP host=$IMAP_HOST (not under test)"
|
||||
|
||||
dns_txt() {
|
||||
local name="$1"
|
||||
if command -v dig >/dev/null 2>&1; then
|
||||
dig +short +time=3 +tries=2 TXT "$name" 2>/dev/null | tr -d '"'
|
||||
else
|
||||
host -t TXT "$name" 2>/dev/null | sed 's/.*"\(.*\)"/\1/'
|
||||
fi
|
||||
}
|
||||
|
||||
dns_mx() {
|
||||
local d="$1"
|
||||
dig +short +time=3 +tries=2 MX "$d" 2>/dev/null | awk '{print tolower($2)}' | sed 's/\.$//'
|
||||
}
|
||||
|
||||
# --- per catalog domain ---
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
line=${line%%#*}
|
||||
line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
[ -z "$line" ] && continue
|
||||
domain=$(echo "$line" | awk '{print $1}')
|
||||
expect_mx=$(echo "$line" | awk '{print $2}')
|
||||
label=$(echo "$line" | awk '{print $6}')
|
||||
[ -n "$domain" ] || continue
|
||||
|
||||
set_group "${label:-$domain}"
|
||||
|
||||
# SPF
|
||||
spf=$(dns_txt "$domain" | tr '\n' ' ' | grep -oE 'v=spf1[^"]*' | head -1 || true)
|
||||
if [ -n "$spf" ]; then
|
||||
ok "spf" "$domain has SPF" "$spf"
|
||||
else
|
||||
fail "spf" "$domain missing SPF TXT"
|
||||
fi
|
||||
|
||||
# DMARC
|
||||
dmarc=$(dns_txt "_dmarc.$domain" | tr '\n' ' ' | grep -oE 'v=DMARC1.*' | head -1 || true)
|
||||
if [ -n "$dmarc" ]; then
|
||||
case "$dmarc" in
|
||||
*p=none*)
|
||||
if [ "$domain" = "taler-ops.ch" ] || [ "$domain" = "firefly.gnunet.org" ]; then
|
||||
warn "dmarc" "$domain DMARC p=none (known weak · ops-checklist)" "$dmarc"
|
||||
else
|
||||
warn "dmarc" "$domain DMARC p=none" "$dmarc"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
ok "dmarc" "$domain DMARC present" "$dmarc"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
fail "dmarc" "$domain missing _dmarc TXT"
|
||||
fi
|
||||
|
||||
# DKIM selectors (ops-checklist / 2026-03)
|
||||
dkim_ok=0
|
||||
dkim_hit=""
|
||||
for sel in $SELECTORS; do
|
||||
[ -n "$sel" ] || continue
|
||||
txt=$(dns_txt "${sel}._domainkey.${domain}" | tr '\n' ' ')
|
||||
if echo "$txt" | grep -qiE 'v=DKIM1|p='; then
|
||||
dkim_ok=1
|
||||
dkim_hit="$sel"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ "$dkim_ok" = "1" ]; then
|
||||
ok "dkim" "$domain DKIM selector $dkim_hit"
|
||||
else
|
||||
warn "dkim" "$domain no DKIM TXT for selectors [$SELECTORS] (mail-tester often -1)"
|
||||
fi
|
||||
|
||||
# MX matches catalog expectation (first token)
|
||||
mx_got=$(dns_mx "$domain" | head -1)
|
||||
exp_first=${expect_mx%%,*}
|
||||
exp_first=${exp_first%% *}
|
||||
if [ -n "$mx_got" ]; then
|
||||
if [ -z "$exp_first" ] || [ "$exp_first" = "-" ] || [ "$mx_got" = "$exp_first" ]; then
|
||||
ok "mx" "$domain MX=$mx_got"
|
||||
else
|
||||
# allow alternate listed in expect_mx comma list
|
||||
case ",${expect_mx}," in
|
||||
*",${mx_got},"*) ok "mx" "$domain MX=$mx_got (listed)" ;;
|
||||
*) warn "mx" "$domain MX=$mx_got want~$exp_first" ;;
|
||||
esac
|
||||
fi
|
||||
else
|
||||
fail "mx" "$domain no MX"
|
||||
continue
|
||||
fi
|
||||
|
||||
# SMTP RCPT smoke (no DATA) — reuse site-gen helper
|
||||
if [ -f "$ROOT/site-gen/stack_status_lib.py" ]; then
|
||||
out=$(
|
||||
ROOT="$ROOT" DOMAIN="$domain" MX="$mx_got" python3 - <<'PY' 2>/dev/null || echo "err=import"
|
||||
import os, sys
|
||||
sys.path.insert(0, os.path.join(os.environ["ROOT"], "site-gen"))
|
||||
from stack_status_lib import smtp_deliverability_probe
|
||||
r = smtp_deliverability_probe(os.environ["MX"], domain=os.environ["DOMAIN"])
|
||||
print(f"ok={r.get('ok')} light={r.get('light')} reason={r.get('reason')} rcpt={r.get('rcpt')}")
|
||||
PY
|
||||
)
|
||||
case "$out" in
|
||||
ok=True*)
|
||||
ok "smtp-rcpt" "$domain $out"
|
||||
;;
|
||||
ok=False*light=yellow*|ok=False*rcpt_reject*|ok=False*reason=rcpt_reject*)
|
||||
# Many MTAs reject postmaster@ from random clients; still useful signal (ops-checklist uses mail-tester for full score).
|
||||
warn "smtp-rcpt" "$domain $out"
|
||||
;;
|
||||
ok=False*)
|
||||
fail "smtp-rcpt" "$domain $out"
|
||||
;;
|
||||
*)
|
||||
warn "smtp-rcpt" "$domain probe unavailable · $out"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
info "smtp-rcpt" "skipped (no site-gen/stack_status_lib.py)"
|
||||
fi
|
||||
done <"$CATALOG"
|
||||
|
||||
# --- optional IMAP sinks (cyon hosts mon*; not SUT) ---
|
||||
if [ "$MAIL_DELIV_IMAP" = "1" ]; then
|
||||
set_group sink-imap
|
||||
if [ -z "${MAIL_DELIV_IMAP_PASS:-}" ]; then
|
||||
warn "sink-imap" "MAIL_DELIV_IMAP=1 but MAIL_DELIV_IMAP_PASS unset — skip"
|
||||
else
|
||||
info "sink-imap" "probing sinks on $IMAP_HOST (infrastructure only · not Taler MTA)"
|
||||
for user in $IMAP_USERS; do
|
||||
res=$(
|
||||
MAIL_DELIV_IMAP_HOST="$IMAP_HOST" MAIL_DELIV_IMAP_USER="$user" \
|
||||
MAIL_DELIV_IMAP_PASS="$MAIL_DELIV_IMAP_PASS" python3 - <<'PY' 2>/dev/null || echo FAIL
|
||||
import imaplib, os, ssl
|
||||
host=os.environ["MAIL_DELIV_IMAP_HOST"]
|
||||
user=os.environ["MAIL_DELIV_IMAP_USER"]
|
||||
pw=os.environ["MAIL_DELIV_IMAP_PASS"]
|
||||
ctx=ssl.create_default_context()
|
||||
M=imaplib.IMAP4_SSL(host, 993, ssl_context=ctx)
|
||||
M.login(user, pw)
|
||||
typ, data=M.select("INBOX")
|
||||
n=data[0].decode() if data and data[0] else "?"
|
||||
# also see junk/spam exists
|
||||
boxes=[]
|
||||
t, bl=M.list()
|
||||
for b in bl or []:
|
||||
s=b.decode(errors="replace")
|
||||
if "Junk" in s or "spam" in s.lower():
|
||||
boxes.append("junk/spam")
|
||||
M.logout()
|
||||
print(f"OK inbox={n} extra={','.join(boxes) or 'none'}")
|
||||
PY
|
||||
)
|
||||
case "$res" in
|
||||
OK*) ok "sink" "$user · $res" ;;
|
||||
*) fail "sink" "$user · $res" ;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
else
|
||||
info "sink-imap" "skipped (MAIL_DELIV_IMAP=0) · sinks mon001–003 for Freigabe send tests"
|
||||
fi
|
||||
|
||||
if [ "$MAIL_DELIV_SEND" = "1" ]; then
|
||||
warn "send" "MAIL_DELIV_SEND=1 deprecated here — use MAIL_DELIV_PROBE=1 (inbox vs spam)"
|
||||
fi
|
||||
|
||||
if [ "$MAIL_DELIV_PROBE" = "1" ]; then
|
||||
set_group probe
|
||||
if [ -z "${MAIL_DELIV_IMAP_PASS:-}${MAIL_DELIV_ALERT_PASS:-}" ]; then
|
||||
warn "probe" "MAIL_DELIV_PROBE=1 but no IMAP pass"
|
||||
else
|
||||
info "probe" "mon* send + IMAP inbox-vs-spam (regular path · host-agent timer 6h)"
|
||||
if out=$(python3 "$ROOT/mail_deliv_probe.py" 2>&1); then
|
||||
ok "probe" "all sinks inbox"
|
||||
info "probe-detail" "$(echo "$out" | tr '\n' ' ' | head -c 240)"
|
||||
else
|
||||
ec=$?
|
||||
echo "$out" | while IFS= read -r line; do
|
||||
case "$line" in
|
||||
*result=spam*|*result=both*)
|
||||
fail "probe-spam" "$line"
|
||||
;;
|
||||
*result=missing*|*IMAP_FAIL*|*SEND_FAIL*)
|
||||
fail "probe-miss" "$line"
|
||||
;;
|
||||
SENT*|ALERT*)
|
||||
info "probe" "$line"
|
||||
;;
|
||||
*)
|
||||
info "probe" "$line"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
[ "$ec" -eq 0 ] || true
|
||||
fi
|
||||
fi
|
||||
else
|
||||
info "probe" "skipped (MAIL_DELIV_PROBE=0) · enable for inbox-vs-spam; timer: taler-monitoring-mail-deliv.timer"
|
||||
fi
|
||||
|
||||
# --- alert on ERROR via sink SMTP (cyon submission; recipients = hernani+mon / icloud) ---
|
||||
maildeliv_alert_if_needed() {
|
||||
set_group alert
|
||||
if [ "$MAIL_DELIV_ALERT" != "1" ]; then
|
||||
info "alert" "skipped (MAIL_DELIV_ALERT=0) · would To: $ALERT_TO"
|
||||
return 0
|
||||
fi
|
||||
if [ "${#ERRORS[@]}" -eq 0 ]; then
|
||||
ok "alert" "no ERRORs — no alert mail"
|
||||
return 0
|
||||
fi
|
||||
if [ -z "$ALERT_PASS" ]; then
|
||||
warn "alert" "MAIL_DELIV_ALERT=1 but no MAIL_DELIV_ALERT_PASS / MAIL_DELIV_IMAP_PASS"
|
||||
return 0
|
||||
fi
|
||||
if ! command -v swaks >/dev/null 2>&1; then
|
||||
warn "alert" "swaks missing — apt/brew install swaks"
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$STATE_DIR" 2>/dev/null || true
|
||||
now=$(date +%s)
|
||||
if [ -f "$ALERT_STAMP" ]; then
|
||||
prev=$(cat "$ALERT_STAMP" 2>/dev/null || echo 0)
|
||||
if [ "$prev" -gt 0 ] 2>/dev/null; then
|
||||
age=$((now - prev))
|
||||
if [ "$age" -lt "$ALERT_COOLDOWN_S" ]; then
|
||||
info "alert" "cooldown ${age}s < ${ALERT_COOLDOWN_S}s — skip send"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
smtp_host=${ALERT_SMTP%%:*}
|
||||
smtp_port=${ALERT_SMTP##*:}
|
||||
[ "$smtp_port" = "$ALERT_SMTP" ] && smtp_port=587
|
||||
body=$(mktemp)
|
||||
{
|
||||
echo "taler-monitoring mail-deliverability ALERT"
|
||||
echo "host=$(hostname 2>/dev/null || echo ?)"
|
||||
echo "when=$(date -Iseconds 2>/dev/null || date)"
|
||||
echo "errors=${#ERRORS[@]}"
|
||||
echo ""
|
||||
for e in "${ERRORS[@]}"; do
|
||||
echo "- $e"
|
||||
done
|
||||
echo ""
|
||||
echo "SUT = Taler mail domains (not cyon). From=$ALERT_FROM via $ALERT_SMTP"
|
||||
} >"$body"
|
||||
subj="[taler-mon maildeliv] ${#ERRORS[@]} ERROR(s)"
|
||||
send_fail=0
|
||||
for to in $ALERT_TO; do
|
||||
if swaks --server "$smtp_host" --port "$smtp_port" \
|
||||
--auth LOGIN --auth-user "$ALERT_FROM" --auth-password "$ALERT_PASS" \
|
||||
--tls --from "$ALERT_FROM" --to "$to" \
|
||||
--header "Subject: $subj" --body "$body" >/dev/null 2>&1; then
|
||||
ok "alert" "sent → $to"
|
||||
else
|
||||
warn "alert" "send failed → $to"
|
||||
send_fail=1
|
||||
fi
|
||||
done
|
||||
rm -f "$body"
|
||||
if [ "$send_fail" = "0" ]; then
|
||||
echo "$now" >"$ALERT_STAMP" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
maildeliv_alert_if_needed
|
||||
|
||||
summary
|
||||
Reference in a new issue