Enhance mailtrace script: add final delivery filtering and improve status output descriptions

This commit is contained in:
2026-03-26 14:35:21 +01:00
parent 159d1fb8ac
commit 63db4c1317

View File

@@ -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 <substr> 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 <mode> 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