Compare commits

...

8 Commits

4 changed files with 486 additions and 110 deletions
+98 -40
View File
@@ -26,7 +26,7 @@ set -euo pipefail
# * --oneline: 1 Zeile TSV (kein Leerzeilentrenner) # * --oneline: 1 Zeile TSV (kein Leerzeilentrenner)
# * --stats: Statistik statt Events (inkl. Matrix/Toplisten) # * --stats: Statistik statt Events (inkl. Matrix/Toplisten)
# - Live-Modus: # - Live-Modus:
# * --follow: tail -F (gz-Dateien werden dafür übersprungen) # * --follow: tail -F, sudo nur falls nötig (gz-Dateien werden dafür übersprungen)
# #
# Anforderungen/Annahmen: # Anforderungen/Annahmen:
# - Timestamp steht als erstes Feld in ISO-Format (z.B. 2026-01-17T12:34:56+01:00) # - Timestamp steht als erstes Feld in ISO-Format (z.B. 2026-01-17T12:34:56+01:00)
@@ -53,6 +53,10 @@ FOLLOW=0
ALL=0 ALL=0
ONELINE=0 ONELINE=0
STATS=0 STATS=0
SHOW_PROGRESS=0
if [[ -t 2 ]]; then
SHOW_PROGRESS=1
fi
# Service-/Event-Filter (OR-verknüpft, wenn mehrere gesetzt sind) # Service-/Event-Filter (OR-verknüpft, wenn mehrere gesetzt sind)
ONLY_LOGIN=0 ONLY_LOGIN=0
@@ -111,7 +115,7 @@ INPUT OPTIONS
You can combine --all and --file to add extra files. You can combine --all and --file to add extra files.
--follow --follow
Follow the active logfile(s) (tail -F). Follow the active logfile(s) (tail -F; sudo only if needed).
.gz files cannot be followed and will be skipped in follow mode. .gz files cannot be followed and will be skipped in follow mode.
TIME FILTERS (ISO8601 recommended) TIME FILTERS (ISO8601 recommended)
@@ -336,6 +340,24 @@ if [[ $FOLLOW -eq 1 ]]; then
[[ ${#FOLLOW_FILES[@]} -gt 0 ]] || die "--follow requested, but only .gz files selected. Add an active log with --file." [[ ${#FOLLOW_FILES[@]} -gt 0 ]] || die "--follow requested, but only .gz files selected. Add an active log with --file."
fi fi
NEED_SUDO=0
declare -a UNREADABLE_FILES=()
for f in "${LOGFILES[@]}"; do
if ! { : < "$f"; } 2>/dev/null; then
NEED_SUDO=1
UNREADABLE_FILES+=("$f")
fi
done
if [[ $NEED_SUDO -eq 1 ]]; then
{
echo "Using sudo because these selected log file(s) cannot be opened for reading by $(id -un):"
echo "Current groups: $(id -nG)"
printf " %s\n" "${UNREADABLE_FILES[@]}"
} >&2
sudo -v || die "sudo authentication failed; cannot read Dovecot log files."
fi
# ------------------------------------------------------------------- # -------------------------------------------------------------------
# POSIX-AWK Parser # POSIX-AWK Parser
# - kompatibel zu mawk/nawk/busybox awk # - kompatibel zu mawk/nawk/busybox awk
@@ -388,7 +410,7 @@ function extract_type(line, arr, t) {
# Tries to extract a user/mailbox from common patterns. # Tries to extract a user/mailbox from common patterns.
function extract_user(line, arr, u) { function extract_user(line, arr, u) {
if (match(line, /user=<[^>]+>/, arr)) { if (match(line, /user=<[^>]+>/, arr)) {
return substr(arr[0], 7, length(arr[0])-7-1) return substr(arr[0], 7, length(arr[0])-7)
} }
if (match(line, /: (imap|pop3|lmtp)\([^)]*\)/, arr)) { if (match(line, /: (imap|pop3|lmtp)\([^)]*\)/, arr)) {
@@ -468,42 +490,49 @@ function type_regex_match(type, typere) {
return 0 return 0
} }
# ---- STATS helper: sort and print top N ---- # ---- progress spinner on stderr ----
function print_top_sorted(title, arr, n, k,i,j,keys,tmp) { function progress_tick( now, ch) {
print title if (show_progress != 1) return
i=0
for (k in arr) { i++; keys[i]=k }
for (i=1; i<=length(keys); i++) { now = systime()
for (j=i+1; j<=length(keys); j++) { if (PROGRESS_LAST != "" && now - PROGRESS_LAST < 2) return
if (arr[keys[j]] > arr[keys[i]]) {
tmp=keys[i]; keys[i]=keys[j]; keys[j]=tmp
}
}
}
for (i=1; i<=length(keys) && i<=n; i++) { PROGRESS_LAST = now
printf " %7d %s\n", arr[keys[i]], keys[i] PROGRESS_IDX = (PROGRESS_IDX % 4) + 1
} ch = substr("|/-\\", PROGRESS_IDX, 1)
print "" printf "\r%s", ch > "/dev/stderr"
fflush("/dev/stderr")
PROGRESS_DRAWN = 1
} }
# ---- STATS helper: sort and print top N for matrix-like keys ---- function progress_clear() {
function print_matrix(title, arr, n, k,i,j,keys,tmp) { if (show_progress != 1 || PROGRESS_DRAWN != 1) return
printf "\r \r" > "/dev/stderr"
fflush("/dev/stderr")
PROGRESS_DRAWN = 0
}
# ---- STATS helper: keep only Top N while scanning the array ----
function print_top_sorted(title, arr, n, k,i,pos,count,topk,topv) {
print title print title
i=0
for (k in arr) { i++; keys[i]=k }
for (i=1; i<=length(keys); i++) { count=0
for (j=i+1; j<=length(keys); j++) { for (k in arr) {
if (arr[keys[j]] > arr[keys[i]]) { pos=1
tmp=keys[i]; keys[i]=keys[j]; keys[j]=tmp while (pos<=count && arr[k] <= topv[pos]) pos++
} if (pos > n) continue
if (count < n) count++
for (i=count; i>pos; i--) {
topk[i]=topk[i-1]
topv[i]=topv[i-1]
} }
topk[pos]=k
topv[pos]=arr[k]
} }
for (i=1; i<=length(keys) && i<=n; i++) { for (i=1; i<=count; i++) {
printf " %7d %s\n", arr[keys[i]], keys[i] printf " %7d %s\n", topv[i], topk[i]
} }
print "" print ""
} }
@@ -524,10 +553,14 @@ BEGIN {
onlypop3 = ENVIRON["ONLY_POP3"] + 0 onlypop3 = ENVIRON["ONLY_POP3"] + 0
typere = ENVIRON["TYPE_RE"] typere = ENVIRON["TYPE_RE"]
show_progress = ENVIRON["SHOW_PROGRESS"] + 0
total=0 total=0
progress_tick()
} }
{ {
progress_tick()
line=$0 line=$0
if (line !~ /dovecot:/) next if (line !~ /dovecot:/) next
@@ -566,10 +599,10 @@ BEGIN {
if (emailneedle != "" && index(tolower(user), emailneedle) == 0) next if (emailneedle != "" && index(tolower(user), emailneedle) == 0) next
total++ total++
c_res[res]++
# Stats sammeln # Stats sammeln
if (stats == 1) { if (stats == 1) {
c_res[res]++
c_type[type]++ c_type[type]++
c_res_type[res "@" type]++ c_res_type[res "@" type]++
@@ -592,10 +625,12 @@ BEGIN {
# Eventausgabe # Eventausgabe
if (oneline == 1) { if (oneline == 1) {
progress_clear()
printf "%s\t%s\t%s\t%s\t%s\t%s\n", ts, type, res, ip, user, msg printf "%s\t%s\t%s\t%s\t%s\t%s\n", ts, type, res, ip, user, msg
next next
} }
progress_clear()
print "time: " ts print "time: " ts
print "type: " type print "type: " type
print "client-ip: " ip print "client-ip: " ip
@@ -606,7 +641,22 @@ BEGIN {
} }
END { END {
if (stats != 1) exit progress_clear()
if (stats != 1) {
if (oneline == 1) {
printf "stats\ttotal=%d\tsuccess=%d\tfail=%d\terror=%d\tinfo=%d\n", total+0, c_res["success"]+0, c_res["fail"]+0, c_res["error"]+0, c_res["info"]+0
exit
}
print ""
print "Statistics:"
printf " total: %7d\n", total+0
printf " success: %7d\n", c_res["success"]+0
printf " fail: %7d\n", c_res["fail"]+0
printf " error: %7d\n", c_res["error"]+0
printf " info: %7d\n", c_res["info"]+0
exit
}
print "Events: " total print "Events: " total
print "" print ""
@@ -626,7 +676,7 @@ END {
print_top_sorted("Top users (overall, Top 10)", c_user, 10) print_top_sorted("Top users (overall, Top 10)", c_user, 10)
print_top_sorted("Top users (fail only, Top 10)", c_user_fail, 10) print_top_sorted("Top users (fail only, Top 10)", c_user_fail, 10)
print_matrix("Matrix result@type (Top 30)", c_res_type, 30) print_top_sorted("Matrix result@type (Top 30)", c_res_type, 30)
print_top_sorted("Fail combos IP -> user (Top 20)", c_fail_ip_user, 20) print_top_sorted("Fail combos IP -> user (Top 20)", c_fail_ip_user, 20)
} }
@@ -635,16 +685,25 @@ END {
# ------------------------------------------------------------------- # -------------------------------------------------------------------
# run_pipeline(): # run_pipeline():
# Liest die ausgewählten Files (inkl. .gz über zcat -f) und pipe't # Liest die ausgewählten Files (inkl. .gz über zcat -f) und pipe't
# in awk. In --follow Mode: tail -F auf nicht-gz Dateien. # in awk. sudo wird nur verwendet, wenn mindestens eine Logdatei nicht
# direkt lesbar ist. In --follow Mode: tail -F auf nicht-gz Dateien.
# ------------------------------------------------------------------- # -------------------------------------------------------------------
run_read() {
if [[ $NEED_SUDO -eq 1 ]]; then
sudo "$@"
else
"$@"
fi
}
run_pipeline() { run_pipeline() {
local f local f
if [[ $FOLLOW -eq 1 ]]; then if [[ $FOLLOW -eq 1 ]]; then
tail -F "${FOLLOW_FILES[@]}" | \ run_read tail -F "${FOLLOW_FILES[@]}" | \
env SINCE="$SINCE" UNTIL="$UNTIL" ADDR_RE="$ADDR_RE" EMAIL_NEEDLE="$EMAIL_NEEDLE" TYPE_RE="$TYPE_RE" \ env SINCE="$SINCE" UNTIL="$UNTIL" ADDR_RE="$ADDR_RE" EMAIL_NEEDLE="$EMAIL_NEEDLE" TYPE_RE="$TYPE_RE" \
ONLY_LOGIN="$ONLY_LOGIN" ONLY_LMTP="$ONLY_LMTP" ONLY_IMAP="$ONLY_IMAP" ONLY_POP3="$ONLY_POP3" \ ONLY_LOGIN="$ONLY_LOGIN" ONLY_LMTP="$ONLY_LMTP" ONLY_IMAP="$ONLY_IMAP" ONLY_POP3="$ONLY_POP3" \
STATUS_FILTER="$STATUS_FILTER" ONELINE="$ONELINE" STATS="$STATS" \ STATUS_FILTER="$STATUS_FILTER" ONELINE="$ONELINE" STATS="$STATS" SHOW_PROGRESS="$SHOW_PROGRESS" \
awk "$AWK_SCRIPT" awk "$AWK_SCRIPT"
return return
fi fi
@@ -652,16 +711,15 @@ run_pipeline() {
{ {
for f in "${LOGFILES[@]}"; do for f in "${LOGFILES[@]}"; do
if [[ "$f" =~ \.gz$ ]]; then if [[ "$f" =~ \.gz$ ]]; then
zcat -f -- "$f" 2>/dev/null || true run_read zcat -f -- "$f" || true
else else
cat -- "$f" 2>/dev/null || true run_read cat -- "$f" || true
fi fi
done done
} | env SINCE="$SINCE" UNTIL="$UNTIL" ADDR_RE="$ADDR_RE" EMAIL_NEEDLE="$EMAIL_NEEDLE" TYPE_RE="$TYPE_RE" \ } | env SINCE="$SINCE" UNTIL="$UNTIL" ADDR_RE="$ADDR_RE" EMAIL_NEEDLE="$EMAIL_NEEDLE" TYPE_RE="$TYPE_RE" \
ONLY_LOGIN="$ONLY_LOGIN" ONLY_LMTP="$ONLY_LMTP" ONLY_IMAP="$ONLY_IMAP" ONLY_POP3="$ONLY_POP3" \ ONLY_LOGIN="$ONLY_LOGIN" ONLY_LMTP="$ONLY_LMTP" ONLY_IMAP="$ONLY_IMAP" ONLY_POP3="$ONLY_POP3" \
STATUS_FILTER="$STATUS_FILTER" ONELINE="$ONELINE" STATS="$STATS" \ STATUS_FILTER="$STATUS_FILTER" ONELINE="$ONELINE" STATS="$STATS" SHOW_PROGRESS="$SHOW_PROGRESS" \
awk "$AWK_SCRIPT" awk "$AWK_SCRIPT"
} }
run_pipeline run_pipeline
+12 -11
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
script_dir="$(dirname $(realpath $0))" script_dir="$(dirname "$(realpath "$0")")"
conf_dir="${script_dir}/conf" conf_dir="${script_dir}/conf"
conf_file="${conf_dir}/get_mail_domains.conf" conf_file="${conf_dir}/get_mail_domains.conf"
@@ -30,12 +30,12 @@ DEFAULT_db_name="postfix"
clean_up() { clean_up() {
# Perform program exit housekeeping # Perform program exit housekeeping
rm -rf $tmp_dir rm -rf "$tmp_dir"
exit $1 exit $1
} }
echononl(){ echononl(){
echo X\\c > /tmp/shprompt$$ echo X\\c > /tmp/shprompt$$
if [ `wc -c /tmp/shprompt$$ | awk '{print $1}'` -eq 1 ]; then if [ "$(wc -c /tmp/shprompt$$ | awk '{print $1}')" -eq 1 ]; then
echo "$*\\c" 1>&2 echo "$*\\c" 1>&2
else else
echo -e -n "$*" 1>&2 echo -e -n "$*" 1>&2
@@ -77,14 +77,15 @@ trap clean_up SIGHUP SIGINT SIGTERM
clear clear
echo "" echo ""
echo -e "\033[32mRunning script \033[1m"$(basename $0)"\033[m .." echo -e "\033[32mRunning script \033[1m$(basename "$0")\033[m .."
echo "" echo ""
echo "" echo ""
echononl " Loading default Configuration values from $(basename ${conf_file}).." echononl " Loading default Configuration values from $(basename "${conf_file}").."
if [[ ! -f "$conf_file" ]]; then if [[ ! -f "$conf_file" ]]; then
echo_skipped echo_skipped
else else
# shellcheck source=/dev/null
source "${conf_file}" > /dev/null 2>&1 source "${conf_file}" > /dev/null 2>&1
if [[ $? -eq 0 ]]; then if [[ $? -eq 0 ]]; then
echo_ok echo_ok
@@ -131,7 +132,7 @@ fi
echo "" echo ""
echononl " Collect supported domains at this server.." echononl " Collect supported domains at this server.."
if [[ "$db_type" = "mysql" ]] ; then if [[ "$db_type" = "mysql" ]] ; then
domains=$(mysql $mysql_credential_args "$db_name" -N -s -e \ domains=$(mysql "$mysql_credential_args" "$db_name" -N -s -e \
"SELECT domain FROM domain ORDER by domain" 2> "$log_messages") "SELECT domain FROM domain ORDER by domain" 2> "$log_messages")
else else
domains=$(su - postgres -c"psql \"$db_name\" -t -q -c\"SELECT domain FROM domain ORDER by domain\"") domains=$(su - postgres -c"psql \"$db_name\" -t -q -c\"SELECT domain FROM domain ORDER by domain\"")
@@ -144,16 +145,16 @@ else
clean_up 1 clean_up 1
fi fi
echo -e "\n\n \033[37m\033[1mMail Domains supported by this server ("$(hostname -f)"):\033[m\n" echo -e "\n\n \033[37m\033[1mMail Domains supported by this server ($(hostname -f)):\033[m\n"
echo "Mail Domains supported by this server ("$(hostname -f)"):" > $out_file echo "Mail Domains supported by this server ($(hostname -f)):" > "$out_file"
echo "" >> $out_file echo "" >> "$out_file"
for domain in $domains ; do for domain in $domains ; do
[[ "$domain" = "ALL" ]] && continue [[ "$domain" = "ALL" ]] && continue
echo -e " \033[32m$domain\033[m" echo -e " \033[32m$domain\033[m"
echo " $domain" >> $out_file echo " $domain" >> "$out_file"
done done
echo "" >> $out_file echo "" >> "$out_file"
echo "" echo ""
clean_up 0 clean_up 0
+368 -58
View File
@@ -57,8 +57,10 @@ set -euo pipefail
# - Das sind Regeln “für die Praxis”, keine 100% perfekte Klassifikation. # - Das sind Regeln “für die Praxis”, keine 100% perfekte Klassifikation.
# #
# 6) Erfolg/Fehlschlag: # 6) Erfolg/Fehlschlag:
# - --success zeigt nur erfolgreiche LMTP Outcomes # - --success zeigt nur Outcomes mit status=success
# - --fail zeigt nur nicht-erfolgreiche Outcomes # - --fail zeigt nur Outcomes mit status=failed
# - Handoff-Einträge (Übergabe an Amavis) werden von beiden Filtern
# ausgeblendet, da sie keine endgültige Zustellung darstellen.
# - Erfolgreich definieren wir hier pragmatisch als status in: # - Erfolgreich definieren wir hier pragmatisch als status in:
# sent | delivered | deliverable # sent | delivered | deliverable
# #
@@ -71,6 +73,7 @@ set -euo pipefail
FOLLOW=0 FOLLOW=0
ALL_LOGS=0 ALL_LOGS=0
LOG_FILE=""
FAIL_ONLY=0 FAIL_ONLY=0
SUCCESS_ONLY=0 SUCCESS_ONLY=0
@@ -80,6 +83,8 @@ FROM_FILTER=""
TO_FILTER="" TO_FILTER=""
QID_FILTER="" QID_FILTER=""
MSGID_FILTER="" MSGID_FILTER=""
SENDER_HOST_FILTER=""
SENDER_IP_FILTER=""
SINCE_PREFIX="" SINCE_PREFIX=""
UNTIL_PREFIX="" UNTIL_PREFIX=""
@@ -89,7 +94,7 @@ NO_NOTE=0 # suppress note lines in block output when TLS unknown
DEDUP=0 DEDUP=0
DEDUP_PREFER="final" # final|amavis|first|last DEDUP_PREFER="final" # final|amavis|first|last
FINAL_ONLY=0 FINAL_ONLY=1
SASL_ONLY=0 SASL_ONLY=0
NO_SASL=0 NO_SASL=0
@@ -97,6 +102,21 @@ INBOUND_ONLY=0
OUTBOUND_ONLY=0 OUTBOUND_ONLY=0
DEBUG=0 DEBUG=0
SHOW_PROGRESS=0
if [[ -t 2 ]]; then
SHOW_PROGRESS=1
fi
AWK_BIN="${AWK_BIN:-}"
if [[ -z "$AWK_BIN" ]]; then
if command -v mawk >/dev/null 2>&1; then
AWK_BIN="mawk"
elif command -v gawk >/dev/null 2>&1; then
AWK_BIN="gawk"
else
AWK_BIN="awk"
fi
fi
usage() { usage() {
cat <<'USAGE' cat <<'USAGE'
@@ -113,10 +133,12 @@ Filter:
--to-addr <substr> match nur in to --to-addr <substr> match nur in to
--qid <substr> match Queue-ID (Postfix Queue-ID / "qid") --qid <substr> match Queue-ID (Postfix Queue-ID / "qid")
--message-id <substr> match Header Message-ID (cleanup: message-id=<...>) --message-id <substr> match Header Message-ID (cleanup: message-id=<...>)
--sender-host <substr> match absendenden Client-Host (client/orig_client)
--sender-ip <substr> match absendende Client-IP (clientip/orig_clientip; IPv4/IPv6)
Status-Filter: Status-Filter:
--success nur erfolgreiche Endzustellungen (status=success) --success nur erfolgreiche Endzustellungen (status=success)
--fail nur fehlgeschlagene Endzustellungen (status=failed oder handoff) --fail nur fehlgeschlagene Endzustellungen (status=failed, kein handoff)
AUTH / Richtung: AUTH / Richtung:
--sasl-only nur Einlieferungen mit SMTP AUTH (sasl_username vorhanden) --sasl-only nur Einlieferungen mit SMTP AUTH (sasl_username vorhanden)
@@ -127,7 +149,11 @@ AUTH / Richtung:
Dedup (ein Eintrag pro Empfänger): Dedup (ein Eintrag pro Empfänger):
--dedup dedupliziere pro (message-id,to). Fallback: (qid,to) falls msgid fehlt --dedup dedupliziere pro (message-id,to). Fallback: (qid,to) falls msgid fehlt
--dedup-prefer <mode> final|amavis|first|last (Default: final) --dedup-prefer <mode> final|amavis|first|last (Default: final)
--final-only nur endgültige Zustellung ausgeben (remote SMTP oder lokales LMTP), keine Amavis-Handoffs
Umfang:
(default) nur endgültige Zustellungen ausgeben (remote SMTP oder lokales LMTP), keine Amavis-Handoffs
--final-only wie Default; nur finale Zustellungen anzeigen
--all-outcomes alle SMTP/LMTP-Outcomes anzeigen, inkl. Amavis-Handoffs
TLS/Notizen: TLS/Notizen:
--no-note im Blockformat keine Hinweiszeile ausgeben, wenn TLS nicht korrelierbar ist --no-note im Blockformat keine Hinweiszeile ausgeben, wenn TLS nicht korrelierbar ist
@@ -135,11 +161,38 @@ TLS/Notizen:
Debug: Debug:
--debug ergänzt Debug-Details zur Heuristik/Klassifikation --debug ergänzt Debug-Details zur Heuristik/Klassifikation
Fortschritt:
Bei interaktivem stderr zeigt mailtrace.sh waehrend der Verarbeitung einen
kleinen Spinner an. Der Spinner wird vor jeder Ausgabezeile entfernt.
Input / Zeitraum: Input / Zeitraum:
--follow tail -F /var/log/mail.log (live) --follow tail -F /var/log/mail.log (live)
--all-logs /var/log/mail.log* inkl. .gz (historisch) --all-logs /var/log/mail.log* inkl. .gz (historisch)
--log-file <pfad> einzelne Logdatei (auch .gz); nicht kombinierbar mit --follow/--all-logs
--since <prefix> Timestamp prefix-match (z.B. 2026-01-13T14:47) --since <prefix> Timestamp prefix-match (z.B. 2026-01-13T14:47)
--until <prefix> stoppe sobald ts >= prefix (ISO8601 string compare) --until <prefix> stoppe sobald ts >= prefix (ISO8601 string compare)
Beispiele:
# Finale Zustellungen eines absendenden Hosts (Default)
mailtrace.sh --sender-host web0.warenform.de
# Finale Zustellungen einer IPv4-Adresse
mailtrace.sh --sender-ip 83.223.85.164
# Finale Zustellungen einer IPv6-Adresse
mailtrace.sh --sender-ip 2a01:30:0:13:286:96ff:fe4a:6eee
# Nur erfolgreiche finale Zustellungen dieses Hosts
mailtrace.sh --sender-host web0.warenform.de --success
# Nur fehlgeschlagene Zustellungen einer IP in alten und rotierten Logs
mailtrace.sh --all-logs --sender-ip 83.223.85.164 --fail
# Voller Verlauf inkl. Amavis-Handoffs und Reinject-Zustellungen
mailtrace.sh --sender-host web0.warenform.de --all-outcomes
# Kompakte Ausgabe fuer einen Zeitraum
mailtrace.sh --sender-host web0.warenform.de --since 2026-01-13T14 --one-line
USAGE USAGE
} }
@@ -148,6 +201,7 @@ while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--follow) FOLLOW=1 ;; --follow) FOLLOW=1 ;;
--all-logs) ALL_LOGS=1 ;; --all-logs) ALL_LOGS=1 ;;
--log-file) LOG_FILE="${2:-}"; shift ;;
--success) SUCCESS_ONLY=1 ;; --success) SUCCESS_ONLY=1 ;;
--fail) FAIL_ONLY=1 ;; --fail) FAIL_ONLY=1 ;;
--addr) ADDR_FILTER="${2:-}"; shift ;; --addr) ADDR_FILTER="${2:-}"; shift ;;
@@ -155,6 +209,8 @@ while [[ $# -gt 0 ]]; do
--to-addr) TO_FILTER="${2:-}"; shift ;; --to-addr) TO_FILTER="${2:-}"; shift ;;
--qid) QID_FILTER="${2:-}"; shift ;; --qid) QID_FILTER="${2:-}"; shift ;;
--message-id) MSGID_FILTER="${2:-}"; shift ;; --message-id) MSGID_FILTER="${2:-}"; shift ;;
--sender-host) SENDER_HOST_FILTER="${2:-}"; shift ;;
--sender-ip) SENDER_IP_FILTER="${2:-}"; shift ;;
--since) SINCE_PREFIX="${2:-}"; shift ;; --since) SINCE_PREFIX="${2:-}"; shift ;;
--until) UNTIL_PREFIX="${2:-}"; shift ;; --until) UNTIL_PREFIX="${2:-}"; shift ;;
--one-line) FORMAT="one-line" ;; --one-line) FORMAT="one-line" ;;
@@ -163,6 +219,7 @@ while [[ $# -gt 0 ]]; do
--dedup) DEDUP=1 ;; --dedup) DEDUP=1 ;;
--dedup-prefer) DEDUP_PREFER="${2:-}"; shift ;; --dedup-prefer) DEDUP_PREFER="${2:-}"; shift ;;
--final-only) FINAL_ONLY=1 ;; --final-only) FINAL_ONLY=1 ;;
--all-outcomes) FINAL_ONLY=0 ;;
--sasl-only) SASL_ONLY=1 ;; --sasl-only) SASL_ONLY=1 ;;
--no-sasl) NO_SASL=1 ;; --no-sasl) NO_SASL=1 ;;
--inbound-only) INBOUND_ONLY=1 ;; --inbound-only) INBOUND_ONLY=1 ;;
@@ -183,6 +240,14 @@ if [[ "$DEDUP_PREFER" != "final" && "$DEDUP_PREFER" != "amavis" && "$DEDUP_PREFE
echo "Ungültiges --dedup-prefer: $DEDUP_PREFER (final|amavis|first|last)" >&2 echo "Ungültiges --dedup-prefer: $DEDUP_PREFER (final|amavis|first|last)" >&2
exit 2 exit 2
fi fi
if [[ -n "$LOG_FILE" && ($FOLLOW -eq 1 || $ALL_LOGS -eq 1) ]]; then
echo "--log-file kann nicht zusammen mit --follow oder --all-logs verwendet werden." >&2
exit 2
fi
if [[ -n "$LOG_FILE" && ! -e "$LOG_FILE" ]]; then
echo "Datei nicht gefunden: $LOG_FILE" >&2
exit 2
fi
if [[ $SUCCESS_ONLY -eq 1 && $FAIL_ONLY -eq 1 ]]; then if [[ $SUCCESS_ONLY -eq 1 && $FAIL_ONLY -eq 1 ]]; then
echo "Bitte nur eines von --success oder --fail verwenden." >&2 echo "Bitte nur eines von --success oder --fail verwenden." >&2
exit 2 exit 2
@@ -197,44 +262,120 @@ if [[ $INBOUND_ONLY -eq 1 && $OUTBOUND_ONLY -eq 1 ]]; then
fi fi
# --- Input selection --------------------------------------------------------- # --- Input selection ---------------------------------------------------------
if [[ $FOLLOW -eq 1 ]]; then declare -a LOGFILES=()
INPUT_CMD=(sudo tail -F /var/log/mail.log)
if [[ -n "$LOG_FILE" ]]; then
LOGFILES=("$LOG_FILE")
elif [[ $ALL_LOGS -eq 1 ]]; then
for f in /var/log/mail.log*; do
[[ -e "$f" ]] && LOGFILES+=("$f")
done
else else
if [[ $ALL_LOGS -eq 1 ]]; then LOGFILES=(/var/log/mail.log)
INPUT_CMD=(sudo zcat -f /var/log/mail.log*) fi
else
INPUT_CMD=(sudo cat /var/log/mail.log) if [[ ${#LOGFILES[@]} -eq 0 ]]; then
echo "Keine Logdateien gefunden." >&2
exit 2
fi
NEED_SUDO=0
declare -a UNREADABLE_FILES=()
for f in "${LOGFILES[@]}"; do
if ! { : < "$f"; } 2>/dev/null; then
NEED_SUDO=1
UNREADABLE_FILES+=("$f")
fi fi
done
if [[ $NEED_SUDO -eq 1 ]]; then
{
echo "Using sudo because these selected log file(s) cannot be opened for reading by $(id -un):"
echo "Current groups: $(id -nG)"
for f in "${UNREADABLE_FILES[@]}"; do
if [[ -e "$f" ]]; then
ls -ld -- "$f"
else
printf " missing: %s\n" "$f"
fi
done
} >&2
sudo -v || { echo "sudo authentication failed; cannot read mail log files." >&2; exit 2; }
fi
run_read() {
if [[ $NEED_SUDO -eq 1 ]]; then
sudo "$@"
else
"$@"
fi
}
if [[ $FOLLOW -eq 1 ]]; then
INPUT_CMD=(run_read tail -F /var/log/mail.log)
else
INPUT_CMD=(run_read zcat -f "${LOGFILES[@]}")
fi fi
############################################################################### ###############################################################################
# gawk core # gawk core
############################################################################### ###############################################################################
"${INPUT_CMD[@]}" | gawk \ AWK_PROGRAM="$(mktemp)"
-v success_only="$SUCCESS_ONLY" \ AWK_FIFO="$(mktemp -u)"
-v fail_only="$FAIL_ONLY" \ GAWK_ERR="$(mktemp)"
-v addr_filter="$ADDR_FILTER" \ mkfifo "$AWK_FIFO"
-v from_filter="$FROM_FILTER" \ trap 'rm -f "$AWK_PROGRAM" "$AWK_FIFO" "$GAWK_ERR"' EXIT
-v to_filter="$TO_FILTER" \ cat > "$AWK_PROGRAM" <<'AWK'
-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 final_only="$FINAL_ONLY" \
-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 # Helferfunktionen
############################################################################### ###############################################################################
function grab(re, s, m) { return match(s, re, m) ? m[1] : "" } function token_after(s, prefix, n, r) {
n = index(s, prefix)
if (n == 0) return ""
r = substr(s, n + length(prefix))
sub(/[ ,;].*/, "", r)
return r
}
function angle_after(s, prefix, n, r, e) {
n = index(s, prefix)
if (n == 0) return ""
r = substr(s, n + length(prefix))
e = index(r, ">")
if (e == 0) return ""
return substr(r, 1, e - 1)
}
function bracket_after(s, prefix, n, r, b, e) {
n = index(s, prefix)
if (n == 0) return ""
r = substr(s, n + length(prefix))
b = index(r, "[")
if (b == 0) return ""
r = substr(r, b + 1)
e = index(r, "]")
if (e == 0) return ""
return substr(r, 1, e - 1)
}
function host_before_bracket_after(s, prefix, n, r, b) {
n = index(s, prefix)
if (n == 0) return ""
r = substr(s, n + length(prefix))
b = index(r, "[")
if (b > 0) return substr(r, 1, b - 1)
sub(/[ ,;].*/, "", r)
return r
}
function qid_from_line(s) {
if (!match(s, / [A-F0-9]{7,20}:/)) return ""
return substr(s, RSTART + 1, RLENGTH - 2)
}
function status_message(s, n, r, p) {
n = index(s, " status=")
if (n == 0) return ""
r = substr(s, n + length(" status="))
p = index(r, " (")
if (p == 0 || substr(r, length(r), 1) != ")") return ""
return substr(r, p + 2, length(r) - p - 2)
}
function is_amavis_handoff(relay) { function is_amavis_handoff(relay) {
return (relay ~ /(127\.0\.0\.1|localhost).*:1002[456]$/ || relay ~ /\[127\.0\.0\.1\]:1002[456]$/) return (relay ~ /(127\.0\.0\.1|localhost).*:1002[456]$/ || relay ~ /\[127\.0\.0\.1\]:1002[456]$/)
} }
@@ -260,6 +401,28 @@ function annotate_sender(from, qid, reason) {
return from return from
} }
function delete_qid_state(qid) {
delete FROM[qid]
delete TO[qid]
delete MID[qid]
delete CLIENT[qid]
delete CLIENTIP[qid]
delete ORIGCLIENT[qid]
delete ORIGCLIENTIP[qid]
delete HELO[qid]
delete SASL[qid]
delete UID[qid]
delete TLS[qid]
delete TLS_P[qid]
delete TLS_C[qid]
delete TLS_B[qid]
delete TMP_EXTCLIENT[qid]
delete TMP_EXTCLIENTIP[qid]
delete DSN_KIND[qid]
delete DSN_REASON[qid]
delete LAST_FAILURE[qid]
}
function jesc(s) { function jesc(s) {
gsub(/\\/,"\\\\",s); gsub(/"/,"\\\"",s) gsub(/\\/,"\\\\",s); gsub(/"/,"\\\"",s)
gsub(/\t/,"\\t",s); gsub(/\r/,"\\r",s); gsub(/\n/,"\\n",s) gsub(/\t/,"\\t",s); gsub(/\r/,"\\r",s); gsub(/\n/,"\\n",s)
@@ -278,6 +441,7 @@ BEGIN {
maxw = 0 maxw = 0
for (i=1; i in labels; i++) { w = length(labels[i] ":"); if (w > maxw) maxw = w } for (i=1; i in labels; i++) { w = length(labels[i] ":"); if (w > maxw) maxw = w }
LABEL_W = maxw LABEL_W = maxw
progress_tick()
} }
function pl(label, value) { printf "%-*s %s\n", LABEL_W, label ":", value } function pl(label, value) { printf "%-*s %s\n", LABEL_W, label ":", value }
@@ -296,12 +460,23 @@ function classify_source(qid, mid, relay, extip) {
############################################################################### ###############################################################################
# TLS Korrelation # TLS Korrelation
############################################################################### ###############################################################################
function remember_tls(line, ip, proto, cipher, bits) { function remember_tls(line, ip, proto, cipher, bits, n, r, p) {
ip = grab(" from [^\\[]+\\[([^\\]]+)\\]", line) ip = bracket_after(line, " from ")
if (ip == "") return if (ip == "") return
proto = grab(": (TLS[^ ]+) with cipher", line)
cipher = grab(" with cipher ([^ ]+)", line) n = index(line, ": TLS")
bits = grab(" \\(([0-9]+/[0-9]+) bits\\)", line) if (n > 0) {
r = substr(line, n + 2)
p = index(r, " with cipher")
if (p > 0) proto = substr(r, 1, p - 1)
}
cipher = token_after(line, " with cipher ")
n = index(line, " (")
if (n > 0) {
r = substr(line, n + 2)
p = index(r, " bits)")
if (p > 0) bits = substr(r, 1, p - 1)
}
TLS_SEEN[ip] = 1 TLS_SEEN[ip] = 1
TLS_PROTO[ip] = (proto != "" ? proto : "-") TLS_PROTO[ip] = (proto != "" ? proto : "-")
@@ -355,10 +530,67 @@ function is_inbound(sasl, orig_ip, src) {
return 0 return 0
} }
###############################################################################
# Fortschrittsanzeige auf stderr
###############################################################################
function progress_tick( now, ch) {
if (show_progress != 1) return
now = systime()
if (PROGRESS_LAST != "" && now - PROGRESS_LAST < 2) return
PROGRESS_LAST = now
PROGRESS_IDX = (PROGRESS_IDX % 4) + 1
ch = substr("|/-\\", PROGRESS_IDX, 1)
printf "\r%s", ch > "/dev/stderr"
fflush("/dev/stderr")
PROGRESS_DRAWN = 1
}
function progress_clear() {
if (show_progress != 1 || PROGRESS_DRAWN != 1) return
printf "\r \r" > "/dev/stderr"
fflush("/dev/stderr")
PROGRESS_DRAWN = 0
}
############################################################################### ###############################################################################
# Ausgabe # Ausgabe
############################################################################### ###############################################################################
function count_record(k) {
STAT_TOTAL++
if (REC_status[k] == "success") STAT_SUCCESS++
if (REC_status[k] == "failed") STAT_FAILED++
}
function emit_stats( width) {
progress_clear()
if (out_format == "json") {
printf "{\"type\":\"stats\",\"total\":%d,\"success\":%d,\"failed\":%d}\n", STAT_TOTAL+0, STAT_SUCCESS+0, STAT_FAILED+0
return
}
if (out_format == "one-line") {
printf "stats\ttotal=%d\tsuccess=%d\tfailed=%d\n", STAT_TOTAL+0, STAT_SUCCESS+0, STAT_FAILED+0
return
}
width = length(STAT_TOTAL+0)
if (length(STAT_SUCCESS+0) > width) width = length(STAT_SUCCESS+0)
if (length(STAT_FAILED+0) > width) width = length(STAT_FAILED+0)
print ""
print "Statistics:"
printf " total: %*d\n", width, STAT_TOTAL+0
printf " success: %*d\n", width, STAT_SUCCESS+0
printf " failed: %*d\n", width, STAT_FAILED+0
}
function emit_record(k, tls_part) { function emit_record(k, tls_part) {
progress_clear()
count_record(k)
if (out_format == "one-line") { if (out_format == "one-line") {
tls_part = "" tls_part = ""
if (REC_tls[k] != "" && REC_tls[k] != "-") { if (REC_tls[k] != "" && REC_tls[k] != "-") {
@@ -485,12 +717,15 @@ END {
} }
for (i=1; i<=n; i++) emit_record(KEYS[i]) for (i=1; i<=n; i++) emit_record(KEYS[i])
} }
emit_stats()
} }
############################################################################### ###############################################################################
# Hauptparser: pro Logzeile State sammeln, bei Zustell-Outcome ausgeben # Hauptparser: pro Logzeile State sammeln, bei Zustell-Outcome ausgeben
############################################################################### ###############################################################################
{ {
progress_tick()
ts = $1 ts = $1
# Zeitraumfilter (prefix match; ISO8601) # Zeitraumfilter (prefix match; ISO8601)
@@ -501,17 +736,22 @@ END {
if ($0 ~ /TLS connection established from /) { remember_tls($0); next } if ($0 ~ /TLS connection established from /) { remember_tls($0); next }
# Postfix Queue-ID (qid) aus Zeile ziehen # Postfix Queue-ID (qid) aus Zeile ziehen
qid = grab(" ([A-F0-9]{7,20}):", $0) qid = qid_from_line($0)
if (qid == "") next if (qid == "") next
if (qid_filter != "" && index(qid, qid_filter) == 0) next if (qid_filter != "" && index(qid, qid_filter) == 0) next
if ($0 ~ / postfix\/qmgr\[/ && $0 ~ / removed$/) {
delete_qid_state(qid)
next
}
# Envelope from/to sammeln # Envelope from/to sammeln
if ($0 ~ / from=<[^>]*>/) FROM[qid] = grab(" from=<([^>]*)>", $0) if ($0 ~ / from=<[^>]*>/) FROM[qid] = angle_after($0, " from=<")
if ($0 ~ / to=<[^>]*>/) TO[qid] = grab(" to=<([^>]*)>", $0) if ($0 ~ / to=<[^>]*>/) TO[qid] = angle_after($0, " to=<")
# cleanup: Header Message-ID sammeln + Ursprung/TLS an msgid binden # cleanup: Header Message-ID sammeln + Ursprung/TLS an msgid binden
if ($0 ~ / postfix\/cleanup\[/ && $0 ~ / message-id=<[^>]+>/) { if ($0 ~ / postfix\/cleanup\[/ && $0 ~ / message-id=<[^>]+>/) {
MID[qid] = grab(" message-id=<([^>]+)>", $0) MID[qid] = angle_after($0, " message-id=<")
# Externen Ursprung puffern und an msgid binden (für spätere qids) # Externen Ursprung puffern und an msgid binden (für spätere qids)
if (MID[qid] != "" && TMP_EXTCLIENT[qid] != "") { if (MID[qid] != "" && TMP_EXTCLIENT[qid] != "") {
@@ -530,11 +770,11 @@ END {
} }
# pickup uid: lokal erzeugt # pickup uid: lokal erzeugt
if ($0 ~ / postfix\/pickup\[/ && $0 ~ / uid=/) UID[qid] = grab(" uid=([0-9]+)", $0) if ($0 ~ / postfix\/pickup\[/ && $0 ~ / uid=/) UID[qid] = token_after($0, " uid=")
# postfix/bounce: neue DSN-Queue-ID einer fehlgeschlagenen Ursprungszustellung zuordnen # postfix/bounce: neue DSN-Queue-ID einer fehlgeschlagenen Ursprungszustellung zuordnen
if ($0 ~ / postfix\/bounce\[/ && $0 ~ / sender non-delivery notification: /) { if ($0 ~ / postfix\/bounce\[/ && $0 ~ / sender non-delivery notification: /) {
newqid = grab(" sender non-delivery notification: ([A-F0-9]{7,20})", $0) newqid = token_after($0, " sender non-delivery notification: ")
if (newqid != "") { if (newqid != "") {
DSN_KIND[newqid] = "non-delivery notification" DSN_KIND[newqid] = "non-delivery notification"
if (LAST_FAILURE[qid] != "") DSN_REASON[newqid] = LAST_FAILURE[qid] if (LAST_FAILURE[qid] != "") DSN_REASON[newqid] = LAST_FAILURE[qid]
@@ -543,12 +783,12 @@ END {
# smtpd: client/orig_client, HELO fallback, TLS map via clientip # smtpd: client/orig_client, HELO fallback, TLS map via clientip
if ($0 ~ / postfix\/smtpd\[/ && $0 ~ / client=/) { if ($0 ~ / postfix\/smtpd\[/ && $0 ~ / client=/) {
CLIENT[qid] = grab(" client=([^\\[]+)", $0) CLIENT[qid] = host_before_bracket_after($0, " client=")
CLIENTIP[qid] = grab(" client=[^\\[]+\\[([^\\]]+)\\]", $0) CLIENTIP[qid] = bracket_after($0, " client=")
if ($0 ~ / orig_client=/) { if ($0 ~ / orig_client=/) {
ORIGCLIENT[qid] = grab(" orig_client=([^\\[]+)", $0) ORIGCLIENT[qid] = host_before_bracket_after($0, " orig_client=")
ORIGCLIENTIP[qid] = grab(" orig_client=[^\\[]+\\[([^\\]]+)\\]", $0) ORIGCLIENTIP[qid] = bracket_after($0, " orig_client=")
} }
# externen Ursprung puffern bis cleanup (msgid) kommt # externen Ursprung puffern bis cleanup (msgid) kommt
@@ -559,8 +799,8 @@ END {
# HELO auslesen (falls vorhanden), sonst fallback auf client host # HELO auslesen (falls vorhanden), sonst fallback auf client host
if ($0 ~ / helo=/) { if ($0 ~ / helo=/) {
he = grab(" helo=<([^>]+)>", $0) he = angle_after($0, " helo=<")
if (he == "") he = grab(" helo=([^ ,;]+)", $0) if (he == "") he = token_after($0, " helo=")
if (he != "") HELO[qid] = he if (he != "") HELO[qid] = he
} }
if (!HELO[qid] && CLIENT[qid] != "") HELO[qid] = CLIENT[qid] if (!HELO[qid] && CLIENT[qid] != "") HELO[qid] = CLIENT[qid]
@@ -575,11 +815,11 @@ END {
} }
# SMTP AUTH User # SMTP AUTH User
if ($0 ~ / postfix\/smtpd\[/ && $0 ~ / sasl_username=/) SASL[qid] = grab(" sasl_username=([^, ]+)", $0) if ($0 ~ / postfix\/smtpd\[/ && $0 ~ / sasl_username=/) SASL[qid] = token_after($0, " sasl_username=")
# ===================== Delivery Outcome (Ausgabezeitpunkt) ===================== # ===================== Delivery Outcome (Ausgabezeitpunkt) =====================
if ($0 ~ / postfix\/(lmtp|smtp)\[/ && $0 ~ / status=/) { if ($0 ~ / postfix\/(lmtp|smtp)\[/ && $0 ~ / status=/) {
raw_status = grab(" status=([^ ]+)", $0) raw_status = token_after($0, " status=")
transport = ($0 ~ / postfix\/smtp\[/ ? "smtp" : "lmtp") transport = ($0 ~ / postfix\/smtp\[/ ? "smtp" : "lmtp")
from = (FROM[qid] ? FROM[qid] : "-") from = (FROM[qid] ? FROM[qid] : "-")
@@ -592,16 +832,16 @@ END {
if (from_filter != "" && index(from, from_filter)==0) next if (from_filter != "" && index(from, from_filter)==0) next
if (to_filter != "" && index(to, to_filter)==0) next if (to_filter != "" && index(to, to_filter)==0) next
dsn = grab(" dsn=([^, ]+)", $0) dsn = token_after($0, " dsn=")
relay = grab(" relay=([^, ]+)", $0) relay = token_after($0, " relay=")
if (final_only == 1 && !is_final_relay(relay, transport)) next if (final_only == 1 && !is_final_relay(relay, transport)) next
status = outcome_status(raw_status, relay) status = outcome_status(raw_status, relay)
msg = grab(" status=[^ ]+ \\((.*)\\)$", $0) msg = status_message($0)
if (status == "failed" && msg != "") LAST_FAILURE[qid] = msg if (status == "failed" && msg != "") LAST_FAILURE[qid] = msg
# Statusfilter # Statusfilter
if (success_only == 1 && !is_success(status)) next if (success_only == 1 && status != "success") next
if (fail_only == 1 && is_success(status)) next if (fail_only == 1 && status != "failed") next
client = (CLIENT[qid] ? CLIENT[qid] : "-") client = (CLIENT[qid] ? CLIENT[qid] : "-")
clientip = (CLIENTIP[qid] ? CLIENTIP[qid] : "-") clientip = (CLIENTIP[qid] ? CLIENTIP[qid] : "-")
@@ -618,6 +858,14 @@ END {
orig_client = (mid != "" && EXTCLIENT_BY_MSGID[mid] ? EXTCLIENT_BY_MSGID[mid] : (ORIGCLIENT[qid]?ORIGCLIENT[qid]:"-")) 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]:"-")) orig_clientip = (mid != "" && EXTCLIENTIP_BY_MSGID[mid] ? EXTCLIENTIP_BY_MSGID[mid] : (ORIGCLIENTIP[qid]?ORIGCLIENTIP[qid]:"-"))
# Absender-Host/IP Filter: direkter client plus rekonstruierter Ursprung.
if (sender_host_filter != "" &&
index(client, sender_host_filter)==0 &&
index(orig_client, sender_host_filter)==0) next
if (sender_ip_filter != "" &&
index(clientip, sender_ip_filter)==0 &&
index(orig_clientip, sender_ip_filter)==0) next
# TLS: qid oder msgid-Vererbung # TLS: qid oder msgid-Vererbung
tls = (TLS[qid] ? TLS[qid] : "") tls = (TLS[qid] ? TLS[qid] : "")
tls_proto = (TLS_P[qid] ? TLS_P[qid] : "") tls_proto = (TLS_P[qid] ? TLS_P[qid] : "")
@@ -667,4 +915,66 @@ END {
emit_record(k) emit_record(k)
} }
} }
' AWK
set +e
"${INPUT_CMD[@]}" > "$AWK_FIFO" &
input_pid=$!
bash -c '
awk_bin=$1
shift
"$awk_bin" "$@"
st=$?
exit "$st"
' _ "$AWK_BIN" \
-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 sender_host_filter="$SENDER_HOST_FILTER" \
-v sender_ip_filter="$SENDER_IP_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 final_only="$FINAL_ONLY" \
-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" \
-v show_progress="$SHOW_PROGRESS" \
-f "$AWK_PROGRAM" < "$AWK_FIFO" 2>"$GAWK_ERR"
gawk_status=$?
wait "$input_pid"
input_status=$?
set -e
if [[ $gawk_status -eq 139 ]]; then
grep -v -E '(Segmentation fault|Speicherzugriffsfehler).*(gawk|awk)' "$GAWK_ERR" >&2 || true
echo "ERROR: $AWK_BIN crashed with a segmentation fault while parsing the mail log." >&2
echo " Try narrowing the input with --since/--until or --log-file, or set AWK_BIN=gawk/mawk explicitly." >&2
exit 1
fi
if [[ $gawk_status -ne 0 ]]; then
cat "$GAWK_ERR" >&2
echo "ERROR: $AWK_BIN failed while parsing the mail log (exit status: $gawk_status)." >&2
exit "$gawk_status"
fi
# 141 = SIGPIPE. This can happen when awk exits intentionally, for example
# because --until stopped parsing before zcat/cat consumed all input.
if [[ $input_status -ne 0 && $input_status -ne 141 ]]; then
echo "ERROR: input command failed while reading the mail log (exit status: $input_status)." >&2
exit "$input_status"
fi
+7
View File
@@ -0,0 +1,7 @@
{
"folders": [
{
"path": "."
}
]
}