Files
postfix/mailtrace.sh
2026-01-14 11:21:25 +01:00

630 lines
25 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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)
}
}
'