Add script mailtrace.sh.

This commit is contained in:
2026-01-14 11:21:25 +01:00
parent 07b45ba272
commit cf41d3c712

629
mailtrace.sh Executable file
View File

@@ -0,0 +1,629 @@
#!/usr/bin/env bash
set -euo pipefail
###############################################################################
# mailtrace
#
# Ziel:
# Postfix-/Amavis-/Dovecot-LMTP-Logzeilen aus /var/log/mail.log korrelieren,
# damit du pro Zustell-Outcome (LMTP) sauber siehst:
# - from / to
# - status / dsn
# - Queue-ID (qid) und Header Message-ID
# - relay (Amavis-Hop 10024 / Dovecot private/dovecot-lmtp / etc.)
# - client / clientip / orig_clientip (extern vs. reinject)
# - sasl user (SMTP AUTH)
# - TLS Daten (wenn im Log korrelierbar)
#
# Admin-Notizen (wichtig, wenn du dich über "fehlende Felder" wunderst):
#
# 1) "qid" = Postfix Queue-ID, nicht Header-Message-ID:
# - qid ist ein interner Bezeichner für einen Queue-Entry in Postfix.
# - Eine Mail kann mehrere qids bekommen (z.B. durch Content-Filter/Amavis
# oder durch Re-Queueing). Daher korrelieren wir zusätzlich über
# die Header Message-ID (cleanup: message-id=<...>).
#
# 2) Warum "helo=" oft leer/nie auftaucht:
# - Postfix loggt das HELO/EHLO nicht standardmäßig bei jeder Mail.
# - Darum nutzt mailtrace als Fallback den Client-Hostname aus
# "client=host[ip]" (hilft für Herkunft/Debugging zuverlässig).
#
# 3) TLS-Parameter sind manchmal nicht zuordenbar:
# - Die Zeile "TLS connection established ..." hat KEINE Queue-ID.
# - TLS ist "pro Verbindung", qid entsteht oft erst später im SMTP-Dialog.
# - Bei Amavis-Reinject entstehen neue qids (localhost), TLS gehört aber
# zur externen Session.
# - Wir mappen TLS zunächst per clientip und vererben TLS zusätzlich über
# die Header-Message-ID (damit nach Reinject/Mehrfachzustellung möglichst
# viel wieder sichtbar wird).
#
# 4) TLS auf 25/465/587 (dein Setup):
# - Je nach master.cf siehst du TLS-Zeilen in:
# postfix/smtpd[...] (Port 25)
# postfix/submission/smtpd[...] (Port 587, Submission)
# postfix/smtps/smtpd[...] (Port 465, SMTPS)
# - Dieses Script sucht die generische Zeile:
# "TLS connection established from ..."
# unabhängig davon, welcher Service-Name davor steht. D.h. TLS von
# 25/465/587 wird grundsätzlich erkannt wenn die Zeile im Log ist.
# - Wie viele TLS-Details im Log landen, steuert u.a. smtpd_tls_loglevel.
# Höherer Loglevel => mehr Details, aber auch deutlich mehr Logvolumen.
#
# 5) inbound-only / outbound-only (Heuristik!):
# - outbound-only: typischerweise SMTP AUTH (sasl_username) oder lokal
# erzeugt (pickup uid=...), ggf. rein lokal/localhost.
# - inbound-only : typischerweise von extern (orig_clientip extern),
# ohne SMTP AUTH.
# - Das sind Regeln “für die Praxis”, keine 100% perfekte Klassifikation.
#
# 6) Erfolg/Fehlschlag:
# - --success zeigt nur erfolgreiche LMTP Outcomes
# - --fail zeigt nur nicht-erfolgreiche Outcomes
# - Erfolgreich definieren wir hier pragmatisch als status in:
# sent | delivered | deliverable
#
# 7) Debug-Modus:
# - --debug ergänzt im Output eine Zeile "debug:" mit Gründen, z.B.:
# outbound: sasl set / pickup uid / local / no external origin
# inbound : external origin + no sasl
# Das hilft beim Nachjustieren der Heuristik, ohne den Parser umzubauen.
###############################################################################
FOLLOW=0
ALL_LOGS=0
FAIL_ONLY=0
SUCCESS_ONLY=0
ADDR_FILTER=""
FROM_FILTER=""
TO_FILTER=""
QID_FILTER=""
MSGID_FILTER=""
SINCE_PREFIX=""
UNTIL_PREFIX=""
FORMAT="block" # block|one-line|json
NO_NOTE=0 # suppress note lines in block output when TLS unknown
DEDUP=0
DEDUP_PREFER="final" # final|amavis|first|last
SASL_ONLY=0
NO_SASL=0
INBOUND_ONLY=0
OUTBOUND_ONLY=0
DEBUG=0
usage() {
cat <<'USAGE'
mailtrace Postfix/Dovecot LMTP Zustellungen aus /var/log/mail.log korrelieren
Ausgabeformate:
(default) Blockformat (lesbar, mit Leerzeile davor)
--one-line Eine Zeile pro Zustell-Outcome
--format json JSON Lines (eine Zeile pro Outcome)
Filter:
--addr <substr> match in from ODER to
--from-addr <substr> match nur in from
--to-addr <substr> match nur in to
--qid <substr> match Queue-ID (Postfix Queue-ID / "qid")
--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)
AUTH / Richtung:
--sasl-only nur Einlieferungen mit SMTP AUTH (sasl_username vorhanden)
--no-sasl nur Einlieferungen ohne SMTP AUTH (sasl_username fehlt)
--inbound-only heuristisch: nur eingehend (extern, i.d.R. ohne AUTH)
--outbound-only heuristisch: nur ausgehend (AUTH oder lokal erzeugt)
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)
TLS/Notizen:
--no-note im Blockformat keine Hinweiszeile ausgeben, wenn TLS nicht korrelierbar ist
Debug:
--debug ergänzt Debug-Details zur Heuristik/Klassifikation
Input / Zeitraum:
--follow tail -F /var/log/mail.log (live)
--all-logs /var/log/mail.log* inkl. .gz (historisch)
--since <prefix> Timestamp prefix-match (z.B. 2026-01-13T14:47)
--until <prefix> stoppe sobald ts >= prefix (ISO8601 string compare)
USAGE
}
# --- CLI Parser --------------------------------------------------------------
while [[ $# -gt 0 ]]; do
case "$1" in
--follow) FOLLOW=1 ;;
--all-logs) ALL_LOGS=1 ;;
--success) SUCCESS_ONLY=1 ;;
--fail) FAIL_ONLY=1 ;;
--addr) ADDR_FILTER="${2:-}"; shift ;;
--from-addr) FROM_FILTER="${2:-}"; shift ;;
--to-addr) TO_FILTER="${2:-}"; shift ;;
--qid) QID_FILTER="${2:-}"; shift ;;
--message-id) MSGID_FILTER="${2:-}"; shift ;;
--since) SINCE_PREFIX="${2:-}"; shift ;;
--until) UNTIL_PREFIX="${2:-}"; shift ;;
--one-line) FORMAT="one-line" ;;
--format) FORMAT="${2:-}"; shift ;;
--no-note) NO_NOTE=1 ;;
--dedup) DEDUP=1 ;;
--dedup-prefer) DEDUP_PREFER="${2:-}"; shift ;;
--sasl-only) SASL_ONLY=1 ;;
--no-sasl) NO_SASL=1 ;;
--inbound-only) INBOUND_ONLY=1 ;;
--outbound-only) OUTBOUND_ONLY=1 ;;
--debug) DEBUG=1 ;;
--help|-h) usage; exit 0 ;;
*) echo "Unbekannte Option: $1" >&2; usage; exit 2 ;;
esac
shift
done
# --- Validation --------------------------------------------------------------
if [[ "$FORMAT" != "block" && "$FORMAT" != "one-line" && "$FORMAT" != "json" ]]; then
echo "Ungültiges --format: $FORMAT (erlaubt: block|one-line|json)" >&2
exit 2
fi
if [[ "$DEDUP_PREFER" != "final" && "$DEDUP_PREFER" != "amavis" && "$DEDUP_PREFER" != "first" && "$DEDUP_PREFER" != "last" ]]; then
echo "Ungültiges --dedup-prefer: $DEDUP_PREFER (final|amavis|first|last)" >&2
exit 2
fi
if [[ $SUCCESS_ONLY -eq 1 && $FAIL_ONLY -eq 1 ]]; then
echo "Bitte nur eines von --success oder --fail verwenden." >&2
exit 2
fi
if [[ $SASL_ONLY -eq 1 && $NO_SASL -eq 1 ]]; then
echo "Bitte nur eines von --sasl-only oder --no-sasl verwenden." >&2
exit 2
fi
if [[ $INBOUND_ONLY -eq 1 && $OUTBOUND_ONLY -eq 1 ]]; then
echo "Bitte nur eines von --inbound-only oder --outbound-only verwenden." >&2
exit 2
fi
# --- Input selection ---------------------------------------------------------
if [[ $FOLLOW -eq 1 ]]; then
INPUT_CMD=(sudo tail -F /var/log/mail.log)
else
if [[ $ALL_LOGS -eq 1 ]]; then
INPUT_CMD=(sudo zcat -f /var/log/mail.log*)
else
INPUT_CMD=(sudo cat /var/log/mail.log)
fi
fi
###############################################################################
# gawk core
###############################################################################
"${INPUT_CMD[@]}" | gawk \
-v success_only="$SUCCESS_ONLY" \
-v fail_only="$FAIL_ONLY" \
-v addr_filter="$ADDR_FILTER" \
-v from_filter="$FROM_FILTER" \
-v to_filter="$TO_FILTER" \
-v qid_filter="$QID_FILTER" \
-v msgid_filter="$MSGID_FILTER" \
-v since_prefix="$SINCE_PREFIX" \
-v until_prefix="$UNTIL_PREFIX" \
-v out_format="$FORMAT" \
-v no_note="$NO_NOTE" \
-v dedup="$DEDUP" \
-v dedup_prefer="$DEDUP_PREFER" \
-v follow_mode="$FOLLOW" \
-v sasl_only="$SASL_ONLY" \
-v no_sasl="$NO_SASL" \
-v inbound_only="$INBOUND_ONLY" \
-v outbound_only="$OUTBOUND_ONLY" \
-v debug_mode="$DEBUG" '
###############################################################################
# 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 jesc(s) {
gsub(/\\/,"\\\\",s); gsub(/"/,"\\\"",s)
gsub(/\t/,"\\t",s); gsub(/\r/,"\\r",s); gsub(/\n/,"\\n",s)
return s
}
function is_local_ip(ip) { return (ip=="" || ip=="-" || ip=="127.0.0.1" || ip=="::1") }
###############################################################################
# Pretty printing (Blockausgabe)
###############################################################################
BEGIN {
labels[1]="time"; labels[2]="from"; labels[3]="to"; labels[4]="status"; labels[5]="dsn"
labels[6]="qid"; labels[7]="msgid"; labels[8]="relay"; labels[9]="client"; labels[10]="clientip"
labels[11]="orig_client"; labels[12]="orig_clientip"; labels[13]="helo"; labels[14]="sasl"; labels[15]="source"
labels[16]="tls"; labels[17]="tls_proto"; labels[18]="tls_cipher"; labels[19]="tls_bits"; labels[20]="note"; labels[21]="debug"
maxw = 0
for (i=1; i in labels; i++) { w = length(labels[i] ":"); if (w > maxw) maxw = w }
LABEL_W = maxw
}
function pl(label, value) { printf "%-*s %s\n", LABEL_W, label ":", value }
###############################################################################
# Quelle klassifizieren (grob)
###############################################################################
function classify_source(qid, mid, relay, extip) {
extip = (mid != "" && EXTCLIENTIP_BY_MSGID[mid] ? EXTCLIENTIP_BY_MSGID[mid] : "")
if (relay ~ /127\.0\.0\.1:10024/ || relay ~ /\[127\.0\.0\.1\]:10024/) return "amavis"
if (extip != "" && !is_local_ip(extip)) return "smtpd"
if (is_local_ip(CLIENTIP[qid]) && extip != "") return "amavis"
if (UID[qid] != "") return "local"
return "local"
}
###############################################################################
# TLS Korrelation
###############################################################################
function remember_tls(line, ip, proto, cipher, bits) {
ip = grab(" from [^\\[]+\\[([^\\]]+)\\]", line)
if (ip == "") return
proto = grab(": (TLS[^ ]+) with cipher", line)
cipher = grab(" with cipher ([^ ]+)", line)
bits = grab(" \\(([0-9]+/[0-9]+) bits\\)", line)
TLS_SEEN[ip] = 1
TLS_PROTO[ip] = (proto != "" ? proto : "-")
TLS_CIPHER[ip] = (cipher != "" ? cipher : "-")
TLS_BITS[ip] = (bits != "" ? bits : "-")
}
###############################################################################
# Dedup: Ein Eintrag pro (message-id,to) (fallback: (qid,to))
###############################################################################
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 == "amavis") { if (relay ~ /127\.0\.0\.1:10024/ || relay ~ /\[127\.0\.0\.1\]:10024/) return 1000000000 + idx; return idx }
return idx
}
###############################################################################
# inbound/outbound Heuristik + Debug-Gründe
###############################################################################
function outbound_reason(sasl, uid, orig_ip, src, r) {
r=""
if (sasl != "" && sasl != "-") r = r (r?"; ":"") "sasl set"
if (uid != "" && uid != "-") r = r (r?"; ":"") "pickup uid"
if (src == "local") r = r (r?"; ":"") "source=local"
if (is_local_ip(orig_ip)) r = r (r?"; ":"") "no external origin"
if (r=="") r="(none)"
return r
}
function inbound_reason(sasl, orig_ip, src, r) {
r=""
if (sasl != "" && sasl != "-") r = r (r?"; ":"") "sasl set (blocks inbound)"
if (!is_local_ip(orig_ip)) r = r (r?"; ":"") "external origin"
if (src == "smtpd") r = r (r?"; ":"") "source=smtpd"
if (r=="") r="(none)"
return r
}
function is_outbound(sasl, uid, orig_ip, src) {
if (sasl != "" && sasl != "-") return 1
if (uid != "" && uid != "-") return 1
if (src == "local") return 1
if (is_local_ip(orig_ip)) return 1
return 0
}
function is_inbound(sasl, orig_ip, src) {
if (sasl != "" && sasl != "-") return 0
if (!is_local_ip(orig_ip)) return 1
if (src == "smtpd") return 1
return 0
}
###############################################################################
# Ausgabe
###############################################################################
function emit_record(k, tls_part) {
if (out_format == "one-line") {
tls_part = ""
if (REC_tls[k] != "" && REC_tls[k] != "-") {
tls_part = sprintf("\ttls=%s\tproto=%s\tcipher=%s\tbits=%s",
REC_tls[k],
(REC_tls_proto[k]?REC_tls_proto[k]:"-"),
(REC_tls_cipher[k]?REC_tls_cipher[k]:"-"),
(REC_tls_bits[k]?REC_tls_bits[k]:"-"))
}
if (debug_mode == 1 && REC_debug[k] != "") {
tls_part = tls_part sprintf("\tdebug=%s", REC_debug[k])
}
printf "%s\tfrom=%s\tto=%s\tstatus=%s\tdsn=%s\tqid=%s\tmsgid=<%s>\tsource=%s%s\trelay=%s\tclient=%s\tclientip=%s\torig_client=%s\torig_clientip=%s\thelo=%s\tsasl=%s\t%s\n",
REC_time[k], REC_from[k], REC_to[k], REC_status[k], REC_dsn[k], REC_qid[k], (REC_msgid[k]?REC_msgid[k]:"-"),
REC_source[k], tls_part, REC_relay[k], REC_client[k], REC_clientip[k], REC_orig_client[k], REC_orig_clientip[k],
REC_helo[k], REC_sasl[k], REC_msg[k]
return
}
if (out_format == "json") {
printf "{"
printf "\"time\":\"%s\",", jesc(REC_time[k])
printf "\"from\":\"%s\",", jesc(REC_from[k])
printf "\"to\":\"%s\",", jesc(REC_to[k])
printf "\"status\":\"%s\",", jesc(REC_status[k])
printf "\"dsn\":\"%s\",", jesc(REC_dsn[k])
printf "\"qid\":\"%s\",", jesc(REC_qid[k])
printf "\"message_id\":\"%s\",", jesc(REC_msgid[k]?REC_msgid[k]:"-")
printf "\"source\":\"%s\",", jesc(REC_source[k])
if (REC_tls[k] != "" && REC_tls[k] != "-") {
printf "\"tls\":\"%s\",", jesc(REC_tls[k])
printf "\"tls_proto\":\"%s\",", jesc(REC_tls_proto[k]?REC_tls_proto[k]:"-")
printf "\"tls_cipher\":\"%s\",", jesc(REC_tls_cipher[k]?REC_tls_cipher[k]:"-")
printf "\"tls_bits\":\"%s\",", jesc(REC_tls_bits[k]?REC_tls_bits[k]:"-")
}
if (debug_mode == 1 && REC_debug[k] != "") {
printf "\"debug\":\"%s\",", jesc(REC_debug[k])
}
printf "\"relay\":\"%s\",", jesc(REC_relay[k])
printf "\"client\":\"%s\",", jesc(REC_client[k])
printf "\"clientip\":\"%s\",", jesc(REC_clientip[k])
printf "\"orig_client\":\"%s\",", jesc(REC_orig_client[k])
printf "\"orig_clientip\":\"%s\",", jesc(REC_orig_clientip[k])
printf "\"helo\":\"%s\",", jesc(REC_helo[k])
printf "\"sasl\":\"%s\",", jesc(REC_sasl[k])
printf "\"message\":\"%s\"", jesc(REC_msg[k])
printf "}\n"
return
}
# Blockformat
print ""
pl("time", REC_time[k]); pl("from", REC_from[k]); pl("to", REC_to[k]); pl("status", REC_status[k]); pl("dsn", REC_dsn[k])
pl("qid", REC_qid[k]); pl("msgid", "<" (REC_msgid[k]?REC_msgid[k]:"-") ">"); pl("relay", REC_relay[k])
pl("client", REC_client[k]); pl("clientip", REC_clientip[k])
pl("orig_client", REC_orig_client[k]); pl("orig_clientip", REC_orig_clientip[k])
pl("helo", REC_helo[k]); pl("sasl", REC_sasl[k]); pl("source", REC_source[k])
if (REC_tls[k] != "" && REC_tls[k] != "-") {
pl("tls", REC_tls[k])
pl("tls_proto", (REC_tls_proto[k]?REC_tls_proto[k]:"-"))
pl("tls_cipher", (REC_tls_cipher[k]?REC_tls_cipher[k]:"-"))
pl("tls_bits", (REC_tls_bits[k]?REC_tls_bits[k]:"-"))
} else if (no_note != 1) {
pl("note", "TLS-Info im Log nicht eindeutig korrelierbar")
}
if (debug_mode == 1 && REC_debug[k] != "") {
pl("debug", REC_debug[k])
}
if (REC_msg[k] != "") print "\n" REC_msg[k]
}
###############################################################################
# Dedup Speicherlogik
###############################################################################
function store_record(key, score, idx,
time, from, to, status, dsn, qid, msgid, relay,
client, clientip, orig_client, orig_clientip,
helo, sasl, source,
tls, tls_proto, tls_cipher, tls_bits, msg, debug) {
if (follow_mode == 1) {
# Live: first wins, direkt emit
if (!(key in BEST_score)) {
BEST_score[key] = score
BEST_idx[key] = idx
REC_time[key]=time; REC_from[key]=from; REC_to[key]=to; REC_status[key]=status; REC_dsn[key]=dsn
REC_qid[key]=qid; REC_msgid[key]=msgid; REC_relay[key]=relay; REC_client[key]=client; REC_clientip[key]=clientip
REC_orig_client[key]=orig_client; REC_orig_clientip[key]=orig_clientip; REC_helo[key]=helo; REC_sasl[key]=sasl
REC_source[key]=source; REC_tls[key]=tls; REC_tls_proto[key]=tls_proto; REC_tls_cipher[key]=tls_cipher; REC_tls_bits[key]=tls_bits
REC_msg[key]=msg; REC_debug[key]=debug
emit_record(key)
}
return
}
# Historisch: best-of nach score
if (!(key in BEST_score) || score > BEST_score[key]) {
BEST_score[key] = score
BEST_idx[key] = idx
REC_time[key]=time; REC_from[key]=from; REC_to[key]=to; REC_status[key]=status; REC_dsn[key]=dsn
REC_qid[key]=qid; REC_msgid[key]=msgid; REC_relay[key]=relay; REC_client[key]=client; REC_clientip[key]=clientip
REC_orig_client[key]=orig_client; REC_orig_clientip[key]=orig_clientip; REC_helo[key]=helo; REC_sasl[key]=sasl
REC_source[key]=source; REC_tls[key]=tls; REC_tls_proto[key]=tls_proto; REC_tls_cipher[key]=tls_cipher; REC_tls_bits[key]=tls_bits
REC_msg[key]=msg; REC_debug[key]=debug
}
}
END {
if (dedup == 1 && follow_mode == 0) {
# Ausgabe in Einlesereihenfolge: sortiere keys nach BEST_idx (einfaches insertion sort)
n=0
for (k in BEST_idx) { n++; KEYS[n]=k }
for (i=2; i<=n; i++) {
k=KEYS[i]; j=i-1
while (j>=1 && BEST_idx[KEYS[j]] > BEST_idx[k]) { KEYS[j+1]=KEYS[j]; j-- }
KEYS[j+1]=k
}
for (i=1; i<=n; i++) emit_record(KEYS[i])
}
}
###############################################################################
# Hauptparser: pro Logzeile State sammeln, bei LMTP status= ausgeben
###############################################################################
{
ts = $1
# Zeitraumfilter (prefix match; ISO8601)
if (since_prefix != "" && index(ts, since_prefix) != 1) next
if (until_prefix != "" && ts >= until_prefix) exit
# TLS: keine qid -> per IP merken
if ($0 ~ /TLS connection established from /) { remember_tls($0); next }
# Postfix Queue-ID (qid) aus Zeile ziehen
qid = grab(" ([A-F0-9]{7,20}):", $0)
if (qid == "") next
if (qid_filter != "" && index(qid, qid_filter) == 0) next
# Envelope from/to sammeln
if ($0 ~ / from=<[^>]*>/) FROM[qid] = grab(" from=<([^>]*)>", $0)
if ($0 ~ / to=<[^>]*>/) TO[qid] = grab(" to=<([^>]*)>", $0)
# cleanup: Header Message-ID sammeln + Ursprung/TLS an msgid binden
if ($0 ~ / postfix\/cleanup\[/ && $0 ~ / message-id=<[^>]+>/) {
MID[qid] = grab(" message-id=<([^>]+)>", $0)
# Externen Ursprung puffern und an msgid binden (für spätere qids)
if (MID[qid] != "" && TMP_EXTCLIENT[qid] != "") {
EXTCLIENT_BY_MSGID[MID[qid]] = TMP_EXTCLIENT[qid]
EXTCLIENTIP_BY_MSGID[MID[qid]] = TMP_EXTCLIENTIP[qid]
TMP_EXTCLIENT[qid] = ""; TMP_EXTCLIENTIP[qid] = ""
}
# TLS an msgid binden (wenn TLS schon auf qid bekannt ist)
if (MID[qid] != "" && TLS[qid] != "" && TLS[qid] != "-") {
TLS_BY_MSGID[MID[qid]] = TLS[qid]
TLS_P_BY_MSGID[MID[qid]] = (TLS_P[qid] ? TLS_P[qid] : "-")
TLS_C_BY_MSGID[MID[qid]] = (TLS_C[qid] ? TLS_C[qid] : "-")
TLS_B_BY_MSGID[MID[qid]] = (TLS_B[qid] ? TLS_B[qid] : "-")
}
}
# pickup uid: lokal erzeugt
if ($0 ~ / postfix\/pickup\[/ && $0 ~ / uid=/) UID[qid] = grab(" uid=([0-9]+)", $0)
# smtpd: client/orig_client, HELO fallback, TLS map via clientip
if ($0 ~ / postfix\/smtpd\[/ && $0 ~ / client=/) {
CLIENT[qid] = grab(" client=([^\\[]+)", $0)
CLIENTIP[qid] = grab(" client=[^\\[]+\\[([^\\]]+)\\]", $0)
if ($0 ~ / orig_client=/) {
ORIGCLIENT[qid] = grab(" orig_client=([^\\[]+)", $0)
ORIGCLIENTIP[qid] = grab(" orig_client=[^\\[]+\\[([^\\]]+)\\]", $0)
}
# externen Ursprung puffern bis cleanup (msgid) kommt
if (!is_local_ip(CLIENTIP[qid])) {
TMP_EXTCLIENT[qid] = CLIENT[qid]
TMP_EXTCLIENTIP[qid] = CLIENTIP[qid]
}
# HELO auslesen (falls vorhanden), sonst fallback auf client host
if ($0 ~ / helo=/) {
he = grab(" helo=<([^>]+)>", $0)
if (he == "") he = grab(" helo=([^ ,;]+)", $0)
if (he != "") HELO[qid] = he
}
if (!HELO[qid] && CLIENT[qid] != "") HELO[qid] = CLIENT[qid]
# TLS pro clientip übernehmen
ip = CLIENTIP[qid]
if (ip != "" && TLS_SEEN[ip]) {
TLS[qid]="yes"; TLS_P[qid]=TLS_PROTO[ip]; TLS_C[qid]=TLS_CIPHER[ip]; TLS_B[qid]=TLS_BITS[ip]
} else {
if (!is_local_ip(ip)) TLS[qid]="no"
}
}
# 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
from = (FROM[qid] ? FROM[qid] : "-")
to = (TO[qid] ? TO[qid] : "-")
mid = (MID[qid] ? MID[qid] : "")
# Allgemeine Filter
if (msgid_filter != "" && (mid == "" || index(mid, msgid_filter) == 0)) next
if (addr_filter != "" && index(from, addr_filter)==0 && index(to, addr_filter)==0) next
if (from_filter != "" && index(from, from_filter)==0) next
if (to_filter != "" && index(to, to_filter)==0) next
dsn = grab(" dsn=([^, ]+)", $0)
relay = grab(" relay=([^, ]+)", $0)
msg = grab(" status=[^ ]+ \\((.*)\\)$", $0)
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] : "")
# SASL Filter
if (sasl_only == 1 && (sasl == "" || sasl == "-")) next
if (no_sasl == 1 && (sasl != "" && sasl != "-")) next
# Ursprung via msgid zurückführen (Amavis)
orig_client = (mid != "" && EXTCLIENT_BY_MSGID[mid] ? EXTCLIENT_BY_MSGID[mid] : (ORIGCLIENT[qid]?ORIGCLIENT[qid]:"-"))
orig_clientip = (mid != "" && EXTCLIENTIP_BY_MSGID[mid] ? EXTCLIENTIP_BY_MSGID[mid] : (ORIGCLIENTIP[qid]?ORIGCLIENTIP[qid]:"-"))
# TLS: qid oder msgid-Vererbung
tls = (TLS[qid] ? TLS[qid] : "")
tls_proto = (TLS_P[qid] ? TLS_P[qid] : "")
tls_cipher= (TLS_C[qid] ? TLS_C[qid] : "")
tls_bits = (TLS_B[qid] ? TLS_B[qid] : "")
if (tls == "" && mid != "" && TLS_BY_MSGID[mid] != "") {
tls = TLS_BY_MSGID[mid]
tls_proto = TLS_P_BY_MSGID[mid]
tls_cipher= TLS_C_BY_MSGID[mid]
tls_bits = TLS_B_BY_MSGID[mid]
}
source = classify_source(qid, mid, relay)
# inbound/outbound Filter + Debug-Grund
debug = ""
if (debug_mode == 1) {
debug = "outbound_reason=" outbound_reason(sasl, uid, orig_clientip, source) \
" | inbound_reason=" inbound_reason(sasl, orig_clientip, source)
}
if (inbound_only == 1 && !is_inbound(sasl, orig_clientip, source)) next
if (outbound_only == 1 && !is_outbound(sasl, uid, orig_clientip, source)) next
# Dedup oder Direktausgabe
if (dedup == 1) {
idx = ++SEQ
key = (mid != "" ? mid : qid) SUBSEP to
score = dedup_score(relay, idx)
store_record(key, score, idx,
ts, from, to, status, (dsn?dsn:"-"), qid, (mid?mid:"-"), (relay?relay:"-"),
client, clientip, orig_client, orig_clientip,
helo, sasl, source,
(tls!=""?tls:""), (tls_proto!=""?tls_proto:""), (tls_cipher!=""?tls_cipher:""), (tls_bits!=""?tls_bits:""),
msg, debug)
next
}
k="__direct__"
REC_time[k]=ts; REC_from[k]=from; REC_to[k]=to; REC_status[k]=status; REC_dsn[k]=(dsn?dsn:"-")
REC_qid[k]=qid; REC_msgid[k]=(mid?mid:"-"); REC_relay[k]=(relay?relay:"-")
REC_client[k]=client; REC_clientip[k]=clientip
REC_orig_client[k]=orig_client; REC_orig_clientip[k]=orig_clientip
REC_helo[k]=helo; REC_sasl[k]=sasl; REC_source[k]=source
REC_tls[k]=(tls!=""?tls:""); REC_tls_proto[k]=(tls_proto!=""?tls_proto:""); REC_tls_cipher[k]=(tls_cipher!=""?tls_cipher:""); REC_tls_bits[k]=(tls_bits!=""?tls_bits:"")
REC_msg[k]=msg; REC_debug[k]=debug
emit_record(k)
}
}
'