From 63db4c1317b68dfa87d70c56938e0f55e3cfcaf0 Mon Sep 17 00:00:00 2001 From: Christoph Date: Thu, 26 Mar 2026 14:35:21 +0100 Subject: [PATCH] Enhance mailtrace script: add final delivery filtering and improve status output descriptions --- mailtrace.sh | 67 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/mailtrace.sh b/mailtrace.sh index dcaa1d8..4e90569 100755 --- a/mailtrace.sh +++ b/mailtrace.sh @@ -89,6 +89,7 @@ NO_NOTE=0 # suppress note lines in block output when TLS unknown DEDUP=0 DEDUP_PREFER="final" # final|amavis|first|last +FINAL_ONLY=0 SASL_ONLY=0 NO_SASL=0 @@ -99,7 +100,7 @@ DEBUG=0 usage() { cat <<'USAGE' -mailtrace – Postfix/Dovecot LMTP Zustellungen aus /var/log/mail.log korrelieren +mailtrace – Postfix SMTP/LMTP Zustellungen aus /var/log/mail.log korrelieren Ausgabeformate: (default) Blockformat (lesbar, mit Leerzeile davor) @@ -114,8 +115,8 @@ Filter: --message-id match Header Message-ID (cleanup: message-id=<...>) Status-Filter: - --success nur erfolgreiche Outcomes (sent|delivered|deliverable) - --fail nur nicht erfolgreiche Outcomes (alles andere) + --success nur erfolgreiche Endzustellungen (status=success) + --fail nur fehlgeschlagene Endzustellungen (status=failed oder handoff) AUTH / Richtung: --sasl-only nur Einlieferungen mit SMTP AUTH (sasl_username vorhanden) @@ -126,6 +127,7 @@ AUTH / Richtung: Dedup (ein Eintrag pro Empfänger): --dedup dedupliziere pro (message-id,to). Fallback: (qid,to) falls msgid fehlt --dedup-prefer final|amavis|first|last (Default: final) + --final-only nur endgültige Zustellung ausgeben (remote SMTP oder lokales LMTP), keine Amavis-Handoffs TLS/Notizen: --no-note im Blockformat keine Hinweiszeile ausgeben, wenn TLS nicht korrelierbar ist @@ -160,6 +162,7 @@ while [[ $# -gt 0 ]]; do --no-note) NO_NOTE=1 ;; --dedup) DEDUP=1 ;; --dedup-prefer) DEDUP_PREFER="${2:-}"; shift ;; + --final-only) FINAL_ONLY=1 ;; --sasl-only) SASL_ONLY=1 ;; --no-sasl) NO_SASL=1 ;; --inbound-only) INBOUND_ONLY=1 ;; @@ -221,6 +224,7 @@ fi -v no_note="$NO_NOTE" \ -v dedup="$DEDUP" \ -v dedup_prefer="$DEDUP_PREFER" \ + -v final_only="$FINAL_ONLY" \ -v follow_mode="$FOLLOW" \ -v sasl_only="$SASL_ONLY" \ -v no_sasl="$NO_SASL" \ @@ -231,7 +235,30 @@ fi # Helferfunktionen ############################################################################### function grab(re, s, m) { return match(s, re, m) ? m[1] : "" } -function is_success(st) { return (st=="sent" || st=="delivered" || st=="deliverable") } +function is_amavis_handoff(relay) { + return (relay ~ /(127\.0\.0\.1|localhost).*:1002[456]$/ || relay ~ /\[127\.0\.0\.1\]:1002[456]$/) +} +function is_success_raw(st) { return (st=="sent" || st=="delivered" || st=="deliverable") } +function outcome_status(st, relay) { + if (is_amavis_handoff(relay)) return "handoff" + return is_success_raw(st) ? "success" : "failed" +} +function is_success(st) { return (st=="success") } +function is_final_relay(relay, transport) { + if (is_amavis_handoff(relay)) return 0 + if (transport == "lmtp" && relay ~ /private\/dovecot-lmtp/) return 1 + if (transport == "smtp") return 1 + return 0 +} +function annotate_sender(from, qid, reason) { + if (from != "") return from + if (DSN_KIND[qid] != "") { + reason = DSN_REASON[qid] + if (reason != "") return "MAILER-DAEMON (" DSN_KIND[qid] ": " reason ")" + return "MAILER-DAEMON (" DSN_KIND[qid] ")" + } + return from +} function jesc(s) { gsub(/\\/,"\\\\",s); gsub(/"/,"\\\"",s) @@ -288,7 +315,7 @@ function remember_tls(line, ip, proto, cipher, bits) { function dedup_score(relay, idx) { if (dedup_prefer == "first") return -idx if (dedup_prefer == "last") return idx - if (dedup_prefer == "final") { if (relay ~ /private\/dovecot-lmtp/) return 1000000000 + idx; return idx } + if (dedup_prefer == "final") { if (!is_amavis_handoff(relay)) return 1000000000 + idx; return idx } if (dedup_prefer == "amavis") { if (relay ~ /127\.0\.0\.1:10024/ || relay ~ /\[127\.0\.0\.1\]:10024/) return 1000000000 + idx; return idx } return idx } @@ -461,7 +488,7 @@ END { } ############################################################################### -# Hauptparser: pro Logzeile State sammeln, bei LMTP status= ausgeben +# Hauptparser: pro Logzeile State sammeln, bei Zustell-Outcome ausgeben ############################################################################### { ts = $1 @@ -505,6 +532,15 @@ END { # pickup uid: lokal erzeugt if ($0 ~ / postfix\/pickup\[/ && $0 ~ / uid=/) UID[qid] = grab(" uid=([0-9]+)", $0) + # postfix/bounce: neue DSN-Queue-ID einer fehlgeschlagenen Ursprungszustellung zuordnen + if ($0 ~ / postfix\/bounce\[/ && $0 ~ / sender non-delivery notification: /) { + newqid = grab(" sender non-delivery notification: ([A-F0-9]{7,20})", $0) + if (newqid != "") { + DSN_KIND[newqid] = "non-delivery notification" + if (LAST_FAILURE[qid] != "") DSN_REASON[newqid] = LAST_FAILURE[qid] + } + } + # smtpd: client/orig_client, HELO fallback, TLS map via clientip if ($0 ~ / postfix\/smtpd\[/ && $0 ~ / client=/) { CLIENT[qid] = grab(" client=([^\\[]+)", $0) @@ -541,13 +577,10 @@ END { # SMTP AUTH User if ($0 ~ / postfix\/smtpd\[/ && $0 ~ / sasl_username=/) SASL[qid] = grab(" sasl_username=([^, ]+)", $0) - # ===================== LMTP Outcome (Ausgabezeitpunkt) ===================== - if ($0 ~ / postfix\/lmtp\[/ && $0 ~ / status=/) { - status = grab(" status=([^ ]+)", $0) - - # Statusfilter - if (success_only == 1 && !is_success(status)) next - if (fail_only == 1 && is_success(status)) next + # ===================== Delivery Outcome (Ausgabezeitpunkt) ===================== + if ($0 ~ / postfix\/(lmtp|smtp)\[/ && $0 ~ / status=/) { + raw_status = grab(" status=([^ ]+)", $0) + transport = ($0 ~ / postfix\/smtp\[/ ? "smtp" : "lmtp") from = (FROM[qid] ? FROM[qid] : "-") to = (TO[qid] ? TO[qid] : "-") @@ -561,13 +594,21 @@ END { dsn = grab(" dsn=([^, ]+)", $0) relay = grab(" relay=([^, ]+)", $0) + if (final_only == 1 && !is_final_relay(relay, transport)) next + status = outcome_status(raw_status, relay) msg = grab(" status=[^ ]+ \\((.*)\\)$", $0) + if (status == "failed" && msg != "") LAST_FAILURE[qid] = msg + + # Statusfilter + if (success_only == 1 && !is_success(status)) next + if (fail_only == 1 && is_success(status)) next client = (CLIENT[qid] ? CLIENT[qid] : "-") clientip = (CLIENTIP[qid] ? CLIENTIP[qid] : "-") helo = (HELO[qid] ? HELO[qid] : "-") sasl = (SASL[qid] ? SASL[qid] : "-") uid = (UID[qid] ? UID[qid] : "") + if (from == "-") from = annotate_sender("", qid) # SASL Filter if (sasl_only == 1 && (sasl == "" || sasl == "-")) next