From 7d5d4cb7b1eadbe96afc05f09d03c6e11c692b4e Mon Sep 17 00:00:00 2001 From: h0tznpl0tz Date: Thu, 11 Sep 2025 14:59:00 +0200 Subject: [PATCH] Refactor exitrace to remote execution model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Complete rewrite following checkms pattern - Remote execution on logger.in.nessus.at via SSH/go-tool - Inline all functions for remote heredoc execution - Remove lib/ directory - no longer needed - Maintains full API compatibility - Automatic fallback: go tool -> LUSER ssh -> error Fixes issue where exitrace could not access logs on logger. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- legacy/exitrace/exitrace.sh | 409 ++++++++++++++++++++++++--- legacy/exitrace/lib/date_utils.sh | 118 -------- legacy/exitrace/lib/log_analyzer.awk | 178 ------------ legacy/exitrace/lib/mx_lookup.sh | 43 --- 4 files changed, 370 insertions(+), 378 deletions(-) delete mode 100644 legacy/exitrace/lib/date_utils.sh delete mode 100755 legacy/exitrace/lib/log_analyzer.awk delete mode 100644 legacy/exitrace/lib/mx_lookup.sh diff --git a/legacy/exitrace/exitrace.sh b/legacy/exitrace/exitrace.sh index afb2f38..f3761cd 100755 --- a/legacy/exitrace/exitrace.sh +++ b/legacy/exitrace/exitrace.sh @@ -1,10 +1,11 @@ #!/usr/bin/env bash -# exitrace - Exim-Log-Trace & Status für einzelne Mails/Adressen/Domain-Ziele +# exitrace - Remote Exim-Log-Trace & Status für einzelne Mails/Adressen/Domain-Ziele # Maintainer: ljp -# Modular version - requires lib/ directory +# Führt Exim-Log-Analyse remote auf logger.in.nessus.at aus # ----------------------------------------------------------------------------- # Features: -# - Sucht in /var/log/easyname/YYYY/MM/YYYY-MM-DD.log(.gz) (anpassbar mit $LOGROOT) +# - Remote-Ausführung auf logger.in.nessus.at (wo die Exim-Logs liegen) +# - Sucht in /var/log/easyname/YYYY/MM/YYYY-MM-DD.log(.gz) auf logger # - Zeitfenster: letzte N Stunden/Tage ODER konkretes Datum/Spanne # - Filter: Absender (-f), Empfänger-Regex (-t), Ziel-Domain (-D), Message-ID (-m), Extra-Regex (-G) # - Gruppiert Treffer pro Exim-Message-ID und fasst Status zusammen @@ -21,28 +22,6 @@ set -euo pipefail -# Script directory (symlink-robust) -SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)" - -# Load modules -if [[ -f "$SCRIPT_DIR/lib/date_utils.sh" ]]; then - source "$SCRIPT_DIR/lib/date_utils.sh" -else - echo "Fehler: lib/date_utils.sh nicht gefunden in $SCRIPT_DIR" >&2 - exit 1 -fi - -if [[ -f "$SCRIPT_DIR/lib/mx_lookup.sh" ]]; then - source "$SCRIPT_DIR/lib/mx_lookup.sh" -else - echo "Fehler: lib/mx_lookup.sh nicht gefunden in $SCRIPT_DIR" >&2 - exit 1 -fi - -# Global settings -LOGROOT="${LOGROOT:-/var/log/easyname}" -TODAY="$(date +%Y-%m-%d)" - usage() { cat <<'USAGE' Usage: exitrace [OPTS] @@ -69,32 +48,242 @@ Beispiele: exitrace -f sender@example.com -D example.de -H 48 exitrace -m 1uwRvz-000Fz2-CI exitrace -t '.*@example.de' -r 2025-09-09..2025-09-11 --mx + +Note: Wird automatisch remote auf logger.in.nessus.at ausgeführt. USAGE } +# Validate functions (inline for remote execution) +validate_positive_integer() { + local value="$1" + local param="$2" + if [[ ! "$value" =~ ^[1-9][0-9]*$ ]]; then + echo "Fehler: $param benötigt positive Ganzzahl, erhalten: $value" >&2 + exit 1 + fi +} + +validate_date() { + local date_str="$1" + local param="$2" + if ! date -d "$date_str" >/dev/null 2>&1; then + echo "Fehler: $param ungültiges Datum: $date_str" >&2 + exit 1 + fi + if [[ ! "$date_str" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then + echo "Fehler: $param Format muss YYYY-MM-DD sein, erhalten: $date_str" >&2 + exit 1 + fi +} + +validate_date_range() { + local range="$1" + if [[ ! "$range" =~ ^([0-9]{4}-[0-9]{2}-[0-9]{2})\.\.([0-9]{4}-[0-9]{2}-[0-9]{2})$ ]]; then + echo "Fehler: --range Format muss YYYY-MM-DD..YYYY-MM-DD sein, erhalten: $range" >&2 + exit 1 + fi + local start_date="${BASH_REMATCH[1]}" + local end_date="${BASH_REMATCH[2]}" + validate_date "$start_date" "--range start" + validate_date "$end_date" "--range end" +} + # --------------------------- Argumente parsen -------------------------------- HOURS=""; DAYS=""; DATE_ONE=""; DATE_RANGE="" FROM=""; TO_REGEX=""; TARGET_DOMAIN=""; MSGID=""; GREPEXTRA="" DRYRUN=0; WANT_MX=0 +# Alle Argumente für Remote-Weiterleitung sammeln +REMOTE_ARGS=() + while [[ $# -gt 0 ]]; do case "$1" in -H|--hours) HOURS="${2:-}" validate_positive_integer "$HOURS" "--hours" + REMOTE_ARGS+=("$1" "$2") shift 2 ;; -DAYS|--days) DAYS="${2:-}" validate_positive_integer "$DAYS" "--days" + REMOTE_ARGS+=("$1" "$2") shift 2 ;; -d|--date) DATE_ONE="${2:-}" validate_date "$DATE_ONE" "--date" + REMOTE_ARGS+=("$1" "$2") shift 2 ;; -r|--range) DATE_RANGE="${2:-}" validate_date_range "$DATE_RANGE" + REMOTE_ARGS+=("$1" "$2") shift 2 ;; + -f|--from) + FROM="${2:-}" + REMOTE_ARGS+=("$1" "$2") + shift 2 ;; + -t|--to) + TO_REGEX="${2:-}" + REMOTE_ARGS+=("$1" "$2") + shift 2 ;; + -D|--domain) + TARGET_DOMAIN="${2:-}" + REMOTE_ARGS+=("$1" "$2") + shift 2 ;; + -m|--msgid) + MSGID="${2:-}" + REMOTE_ARGS+=("$1" "$2") + shift 2 ;; + -G|--grep) + GREPEXTRA="${2:-}" + REMOTE_ARGS+=("$1" "$2") + shift 2 ;; + --mx) + WANT_MX=1 + REMOTE_ARGS+=("$1") + shift ;; + -n|--dry-run) + DRYRUN=1 + REMOTE_ARGS+=("$1") + shift ;; + -h|--help) + usage; exit 0 ;; + *) + echo "Unbekannte Option: $1" >&2 + usage + exit 1 ;; + esac +done + +# --- Remote-Kommando (wie checkms) --- +LUSER="${LUSER:-}" +if [[ -z "$LUSER" ]] && type go >/dev/null 2>&1; then + REMOTE="go logger" +elif [[ -n "$LUSER" ]]; then + REMOTE="ssh ${LUSER}@logger.in.nessus.at" +else + echo "Fehler: LUSER Variable nicht gesetzt und 'go' Tool nicht verfügbar" >&2 + echo "Setze LUSER oder installiere das 'go' Tool für automatische Logger-Verbindung" >&2 + exit 1 +fi + +# --- Remote Ausführung --- +$REMOTE bash -s "${REMOTE_ARGS[@]}" <<'REMOTE_SCRIPT' +#!/usr/bin/env bash +# Remote exitrace execution on logger.in.nessus.at +set -euo pipefail + +# Date/validation functions +validate_positive_integer() { + local value="$1" + local param="$2" + if [[ ! "$value" =~ ^[1-9][0-9]*$ ]]; then + echo "Fehler: $param benötigt positive Ganzzahl, erhalten: $value" >&2 + exit 1 + fi +} + +validate_date() { + local date_str="$1" + local param="$2" + if ! date -d "$date_str" >/dev/null 2>&1; then + echo "Fehler: $param ungültiges Datum: $date_str" >&2 + exit 1 + fi + if [[ ! "$date_str" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then + echo "Fehler: $param Format muss YYYY-MM-DD sein, erhalten: $date_str" >&2 + exit 1 + fi +} + +validate_date_range() { + local range="$1" + if [[ ! "$range" =~ ^([0-9]{4}-[0-9]{2}-[0-9]{2})\.\.([0-9]{4}-[0-9]{2}-[0-9]{2})$ ]]; then + echo "Fehler: --range Format muss YYYY-MM-DD..YYYY-MM-DD sein, erhalten: $range" >&2 + exit 1 + fi + local start_date="${BASH_REMATCH[1]}" + local end_date="${BASH_REMATCH[2]}" + validate_date "$start_date" "--range start" + validate_date "$end_date" "--range end" +} + +# Date generation functions +gen_dates_hours_back() { + local hrs="$1" start_ts end_ts cur + local DATE_FMT="%Y-%m-%d" + end_ts="$(date +%s)" + start_ts="$(date -d "-${hrs} hours" +%s)" + cur="$(date -d "@$start_ts" +${DATE_FMT})" + while :; do + echo "$cur" + [[ "$(date -d "$cur +1 day" +%s)" -le "$end_ts" ]] || break + cur="$(date -d "$cur +1 day" +${DATE_FMT})" + done +} + +gen_dates_days_back() { + local days="$1" i + for ((i=0;i<=days;i++)); do + date -d "-$i day" +%Y-%m-%d + done +} + +gen_dates_range() { + local a="${1%%..*}" b="${1##*..}" cur="$a" + while :; do + echo "$cur" + [[ "$cur" == "$b" ]] && break + cur="$(date -d "$cur +1 day" +%Y-%m-%d)" + done +} + +files_for_date() { + local d="$1" y="${d:0:4}" m="${d:5:2}" + local p="/var/log/easyname/${y}/${m}/${d}.log" + if [[ -f "$p" ]]; then + printf "%s\n" "$p" + fi + if [[ -f "${p}.gz" ]]; then + printf "%s\n" "${p}.gz" + fi +} + +# MX lookup function +show_mx_records() { + local domain="$1" + if command -v dig >/dev/null 2>&1; then + echo "== MX für ${domain} ==" + dig +short MX "$domain" | sort -n || true + echo "-- A/AAAA der MX:" + while read -r mx; do + local host="${mx#* }" + printf "%-35s | " "$host" + dig +short A "$host" | paste -sd, - || true + printf "%-35s | " "" + dig +short AAAA "$host" | paste -sd, - || true + done < <(dig +short MX "$domain" | sort -n) + echo "========================================================================" + else + echo "(Hinweis) dig nicht gefunden, MX-Ausgabe übersprungen." + fi +} + +# Global settings +LOGROOT="${LOGROOT:-/var/log/easyname}" +TODAY="$(date +%Y-%m-%d)" + +# Parse remote arguments +HOURS=""; DAYS=""; DATE_ONE=""; DATE_RANGE="" +FROM=""; TO_REGEX=""; TARGET_DOMAIN=""; MSGID=""; GREPEXTRA="" +DRYRUN=0; WANT_MX=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + -H|--hours) HOURS="${2:-}"; shift 2 ;; + -DAYS|--days) DAYS="${2:-}"; shift 2 ;; + -d|--date) DATE_ONE="${2:-}"; shift 2 ;; + -r|--range) DATE_RANGE="${2:-}"; shift 2 ;; -f|--from) FROM="${2:-}"; shift 2 ;; -t|--to) TO_REGEX="${2:-}"; shift 2 ;; -D|--domain) TARGET_DOMAIN="${2:-}"; shift 2 ;; @@ -102,12 +291,11 @@ while [[ $# -gt 0 ]]; do -G|--grep) GREPEXTRA="${2:-}"; shift 2 ;; --mx) WANT_MX=1; shift ;; -n|--dry-run) DRYRUN=1; shift ;; - -h|--help) usage; exit 0 ;; - *) echo "Unbekannte Option: $1" >&2; usage; exit 1 ;; + *) shift ;; esac done -# ------------------------- Datums-Liste bauen -------------------------------- +# Generate date list declare -a DATES=() if [[ -n "$HOURS" ]]; then mapfile -t DATES < <(gen_dates_hours_back "$HOURS") @@ -121,6 +309,7 @@ else DATES=("$TODAY") fi +# Collect files declare -a FILES=() for d in "${DATES[@]}"; do while read -r f; do FILES+=("$f"); done < <(files_for_date "$d" || true) @@ -137,16 +326,15 @@ if [[ "${#FILES[@]}" -eq 0 ]]; then exit 0 fi -# --------------------------- Vorfilter bauen --------------------------------- +# Build grep filters declare -a PREF_GREP=() [[ -n "$MSGID" ]] && PREF_GREP+=( "$MSGID" ) [[ -n "$FROM" ]] && PREF_GREP+=( "$FROM" ) [[ -n "$TARGET_DOMAIN" ]] && PREF_GREP+=( "$TARGET_DOMAIN" ) [[ -n "$GREPEXTRA" ]] && PREF_GREP+=( "$GREPEXTRA" ) -# ------------------------------- Reader -------------------------------------- +# File reader cat_files() { - # .log direkt lesen, .gz via zcat local f for f in "${FILES[@]}"; do if [[ "$f" == *.gz ]]; then @@ -157,26 +345,169 @@ cat_files() { done } +# Grep chain grep_chain() { if [[ "${#PREF_GREP[@]}" -eq 0 ]]; then cat_files else - # Mehrere -F -e Muster cat_files | grep -F -e "${PREF_GREP[0]}" $(for s in "${PREF_GREP[@]:1}"; do printf ' -e %q' "$s"; done) fi } -# ------------------------------- MX Block ------------------------------------ +# MX Block if (( WANT_MX )) && [[ -n "$TARGET_DOMAIN" ]]; then show_mx_records "$TARGET_DOMAIN" fi -# ---------------------------- Auswertung (awk) -------------------------------- -if [[ ! -f "$SCRIPT_DIR/lib/log_analyzer.awk" ]]; then - echo "Fehler: lib/log_analyzer.awk nicht gefunden in $SCRIPT_DIR" >&2 - exit 1 -fi +# AWK Analysis +grep_chain | awk -v TO_RE="${TO_REGEX:-}" -v WANTDOM="${TARGET_DOMAIN:-}" -v MSGIDREQ="${MSGID:-}" -v FROMREQ="${FROM:-}" -v EXTRA="${GREPEXTRA:-}" ' +function trim(s){ sub(/^[ \t\r\n]+/, "", s); sub(/[ \t\r\n]+$/, "", s); return s } +function add_rcpt(id, rc){ if (!(rc in rcpts[id])) rcpts[id][rc]=1 } -grep_chain | awk -v TO_RE="${TO_REGEX:-}" -v WANTDOM="${TARGET_DOMAIN:-}" -v MSGIDREQ="${MSGID:-}" -v FROMREQ="${FROM:-}" -v EXTRA="${GREPEXTRA:-}" -f "$SCRIPT_DIR/lib/log_analyzer.awk" +BEGIN { FS=" " } + +{ + raw=$0 + + # Zeit + ID detektieren + match(raw, /[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} [A-Za-z0-9-]+/, t) + if (!t[0]) next + split(t[0], p, " ") + ts = p[1] " " p[2] + msgid = $1 + if (!msgid) next + + if (MSGIDREQ != "" && msgid != MSGIDREQ) next + + # Init Times + if (!(msgid in first_ts) || ts < first_ts[msgid]) first_ts[msgid]=ts + if (!(msgid in last_ts) || ts > last_ts[msgid]) last_ts[msgid]=ts + + # Sender extrahieren + if (index($0, "<=") == 1 || $2 == "<=") { + if (match($0, / from <[^>]+>/, m)) { + s=m[0]; gsub(/.* from .*/, "", s) + from=trim(s) + } else if (match($0, /<= [^ ]+@[^ ]+/, m2)) { + split(m2[0], a, " "); from=trim(a[2]) + } else { from="" } + if (from != "") { + from_addr[msgid]=from + if (FROMREQ != "" && from != FROMREQ) from_mismatch[msgid]=1 + } + } + + # Empfänger extrahieren + rcpt="" + if (match($0, / for <[^>]+>/, r)) { + rcpt=r[0]; gsub(/ for /, "", rcpt); rcpt=trim(rcpt) + } else if (match($0, /\*\* [^ ]+@[^ ]+/, r2)) { + split(r2[0], b, " "); rcpt=trim(b[2]) + } + if (rcpt != "") add_rcpt(msgid, rcpt) + + # Klassify line types + is_delivery = index($0, " => ")>0 + is_fail = index($0, " ** ")>0 + is_remote = (index($0, " T=remote_smtp")>0 || index($0, " T=remote_smtp_dkim")>0) + + # Success detection + is_success = 0 + if (is_delivery && is_remote) { + if (match($0, /C="[^"]*250[^"]*"/)) is_success=1 + } + + # Deferred detection + is_defer = 0 + if (is_remote && (index($0, "retry time not reached")>0 || index($0, "temporar")>0 || index($0, "timed out")>0 || match($0, /all hosts for '\''[^']+'\'' have been failing/))) { + is_defer=1 + } + + # Fail detection + is_perm_fail = 0 + if (is_fail) { + is_perm_fail=1 + } else if (match($0, /(Unrouteable address|no such user|User unknown|550 )/)) { + is_perm_fail=1 + } + + # Counters + if (is_success) deliveries[msgid]++ + if (is_defer) defers[msgid]++ + if (is_perm_fail) fails[msgid]++ + + # Fail marker + if (match($0, /all hosts for '\''[^']+'\'' have been failing/)) { + fail_marker[msgid]=1 + } + + # Full log sammeln + status[msgid]=(status[msgid] ? status[msgid] "\n" : "") ts " | " $0 +} + +END { + # Sort IDs + n=0 + for (id in first_ts){ ids[++n]=id } + for (i=2;i<=n;i++){ + key=ids[i]; j=i-1 + while (j>=1 && first_ts[ids[j]] > first_ts[key]) { ids[j+1]=ids[j]; j-- } + ids[j+1]=key + } + + if (n==0){ + print "Keine passenden Einträge im gewählten Zeitfenster gefunden." + exit 0 + } + + sep="------------------------------------------------------------------------" + for (k=1;k<=n;k++){ + id=ids[k] + + # FROM filter + if (FROMREQ != "" && from_mismatch[id]==1) { + if (from_addr[id] != FROMREQ && from_addr[id] != "") continue + } + + d=(id in deliveries ? deliveries[id] : 0) + e=(id in defers ? defers[id] : 0) + f=(id in fails ? fails[id] : 0) + + # Status logic + status_txt="UNKNOWN" + if (d>0 && f==0 && e==0) status_txt="OK" + else if (d>0 && (f>0 || e>0)) status_txt="PARTIAL" + else if (d==0 && f>0) status_txt="FAIL" + else if (d==0 && f==0 && e>0) status_txt="DELAY" + + # Rcpts sammeln + out_rcpts="" + if (id in rcpts){ + for (r in rcpts[id]) { + out_rcpts = (out_rcpts=="" ? r : out_rcpts ", " r) + } + } + + printf "[%s] ID=%s | First=%s | From=%s | Rcpts=%s | Delivered=%d Deferred=%d Failed=%d\n", + status_txt, id, first_ts[id], (from_addr[id] ? from_addr[id] : "(?)"), (out_rcpts=="" ? "(?)" : out_rcpts), d, e, f + + if (fail_marker[id]) { + printf " Note: DELIVERY STALL erkannt: '\''all hosts for domain failing (retry/backoff)'\''\n" + } + + print sep + printf "ID: %s\n", id + printf "First: %s\n", first_ts[id] + printf "Last: %s\n", last_ts[id] + printf "From: %s\n", (from_addr[id] ? from_addr[id] : "(unbekannt)") + printf "Rcpts: %s\n", (out_rcpts=="" ? "(unbekannt)" : out_rcpts) + printf "Status: %s (Delivered=%d, Deferred=%d, Failed=%d)\n", status_txt, d, e, f + print "Log:" + print status[id] + print sep + } +} +' +REMOTE_SCRIPT # Built with the help of Claude Code \ No newline at end of file diff --git a/legacy/exitrace/lib/date_utils.sh b/legacy/exitrace/lib/date_utils.sh deleted file mode 100644 index ee877be..0000000 --- a/legacy/exitrace/lib/date_utils.sh +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env bash -# date_utils.sh - Date utility functions for exitrace -# Maintainer: ljp - -# Constants -readonly DATE_FMT="%Y-%m-%d" -readonly MAX_DAYS_LIMIT=400 - -# Validation functions -validate_positive_integer() { - local value="$1" - local param="$2" - if [[ -z "$value" ]]; then - echo "Fehler: Parameter $param benötigt einen Wert" >&2 - exit 1 - fi - if ! [[ "$value" =~ ^[0-9]+$ ]]; then - echo "Fehler: $param muss eine positive Zahl sein (gegeben: '$value')" >&2 - exit 1 - fi - if [[ "$value" -eq 0 ]]; then - echo "Fehler: $param muss größer als 0 sein" >&2 - exit 1 - fi -} - -validate_date() { - local date_val="$1" - local param="$2" - if [[ -z "$date_val" ]]; then - echo "Fehler: Parameter $param benötigt einen Wert" >&2 - exit 1 - fi - if ! date -d "$date_val" >/dev/null 2>&1; then - echo "Fehler: Ungültiges Datumsformat für $param: '$date_val'" >&2 - echo "Erwartetes Format: YYYY-MM-DD" >&2 - exit 1 - fi -} - -validate_date_range() { - local range="$1" - if [[ -z "$range" ]]; then - echo "Fehler: --range benötigt einen Wert" >&2 - exit 1 - fi - if [[ "$range" != *".."* ]]; then - echo "Fehler: Ungültiges Range-Format: '$range'" >&2 - echo "Erwartetes Format: YYYY-MM-DD..YYYY-MM-DD" >&2 - exit 1 - fi - local start_date="${range%%..*}" - local end_date="${range##*..}" - validate_date "$start_date" "--range Startdatum" - validate_date "$end_date" "--range Enddatum" -} - -# Date generation functions -gen_dates_hours_back() { - local hrs="$1" start_ts end_ts cur next_ts - end_ts="$(date +%s)" || { echo "Fehler: Kann aktuellen Timestamp nicht ermitteln" >&2; exit 1; } - start_ts="$(date -d "-${hrs} hours" +%s)" || { echo "Fehler: Kann Start-Timestamp nicht berechnen" >&2; exit 1; } - cur="$(date -d "@$start_ts" +${DATE_FMT})" || { echo "Fehler: Kann Startdatum nicht formatieren" >&2; exit 1; } - - local iterations=0 - while :; do - echo "$cur" - next_ts="$(date -d "$cur +1 day" +%s 2>/dev/null)" || { echo "Fehler: Datumsberechnung fehlgeschlagen für $cur" >&2; exit 1; } - [[ "$next_ts" -le "$end_ts" ]] || break - cur="$(date -d "$cur +1 day" +${DATE_FMT} 2>/dev/null)" || { echo "Fehler: Datum-Inkrement fehlgeschlagen für $cur" >&2; exit 1; } - - # Sicherheit gegen Endlosschleifen - ((iterations++)) - if [[ $iterations -gt $MAX_DAYS_LIMIT ]]; then - echo "Fehler: Zu viele Tage im Zeitfenster (>$MAX_DAYS_LIMIT Tage)" >&2 - exit 1 - fi - done -} - -gen_dates_days_back() { - local days="$1" i - for ((i=0;i<=days;i++)); do - date -d "-$i day" +${DATE_FMT} || { echo "Fehler: Kann Datum für -$i day nicht berechnen" >&2; exit 1; } - done -} - -gen_dates_range() { - local a="${1%%..*}" b="${1##*..}" cur="$a" - local iterations=0 - - while :; do - echo "$cur" - [[ "$cur" == "$b" ]] && break - cur="$(date -d "$cur +1 day" +${DATE_FMT} 2>/dev/null)" || { echo "Fehler: Datum-Inkrement fehlgeschlagen für $cur" >&2; exit 1; } - - # Sicherheit gegen Endlosschleifen - ((iterations++)) - if [[ $iterations -gt $MAX_DAYS_LIMIT ]]; then - echo "Fehler: Zu großer Datumsbereich (>$MAX_DAYS_LIMIT Tage)" >&2 - exit 1 - fi - done -} - -files_for_date() { - local d="$1" y="${d:0:4}" m="${d:5:2}" - local logroot="${LOGROOT:-/var/log/easyname}" - local p="${logroot}/${y}/${m}/${d}.log" - if [[ -f "$p" ]]; then - printf "%s\n" "$p" - fi - if [[ -f "${p}.gz" ]]; then - printf "%s\n" "${p}.gz" - fi -} - -# Built with the help of Claude Code \ No newline at end of file diff --git a/legacy/exitrace/lib/log_analyzer.awk b/legacy/exitrace/lib/log_analyzer.awk deleted file mode 100755 index f6dd430..0000000 --- a/legacy/exitrace/lib/log_analyzer.awk +++ /dev/null @@ -1,178 +0,0 @@ -#!/usr/bin/awk -f -# log_analyzer.awk - Exim log analyzer for exitrace -# Maintainer: ljp -# Processes Exim logs and classifies delivery status per Message-ID - -function trim(s){ sub(/^[ \t\r\n]+/, "", s); sub(/[ \t\r\n]+$/, "", s); return s } -function add_rcpt(id, rc){ if (!(rc in rcpts[id])) rcpts[id][rc]=1 } -function incr(a, k){ a[k]++; return a[k] } - -BEGIN { FS=" " } - -{ - raw=$0 - - # Zeit + ID detektieren - match(raw, /[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} [A-Za-z0-9-]+/, t) - if (!t[0]) next - split(t[0], p, " ") - ts = p[1] " " p[2] - - # Extrahiere Rest der Zeile nach Timestamp für weitere Verarbeitung - raw_after = raw - sub(/^[^:]*:[^:]*:[^ ]+ /, "", raw_after) - msgid = $1 - if (!msgid) next - - if (MSGIDREQ != "" && msgid != MSGIDREQ) next - - # Init Times - if (!(msgid in first_ts) || ts < first_ts[msgid]) first_ts[msgid]=ts - if (!(msgid in last_ts) || ts > last_ts[msgid]) last_ts[msgid]=ts - - # Sender extrahieren - if (index($0, "<=") == 1 || $2 == "<=") { - if (match($0, / from <[^>]+>/, m)) { - s=m[0]; gsub(/.* from .*/, "", s) - from=trim(s) - } else if (match($0, /<= [^ ]+@[^ ]+/, m2)) { - split(m2[0], a, " "); from=trim(a[2]) - } else { from="" } - if (from != "") { - from_addr[msgid]=from - if (FROMREQ != "" && from != FROMREQ) from_mismatch[msgid]=1 - } - } - - # Empfänger extrahieren - rcpt="" - if (match($0, / for <[^>]+>/, r)) { - rcpt=r[0]; gsub(/ for /, "", rcpt); rcpt=trim(rcpt) - } else if (match($0, /\*\* [^ ]+@[^ ]+/, r2)) { - split(r2[0], b, " "); rcpt=trim(b[2]) - } - if (rcpt != "") add_rcpt(msgid, rcpt) - - # Klassify line types - is_delivery = index($0, " => ")>0 - is_fail = index($0, " ** ")>0 - is_remote = (index($0, " T=remote_smtp")>0 || index($0, " T=remote_smtp_dkim")>0) - - # Success detection: C="250 ..." oder "250 " in C=... - is_success = 0 - if (is_delivery && is_remote) { - if (match($0, /C="[^"]*250[^"]*"/)) is_success=1 - } - - # Deferred detection - # typische Phrasen: "retry time not reached", "temporarily", "Connection timed out", "have been failing" - is_defer = 0 - if (is_remote && (index($0, "retry time not reached")>0 || index($0, "temporar")>0 || index($0, "timed out")>0 || match($0, /all hosts for '\''[^']+'\'' have been failing/))) { - is_defer=1 - } - - # Fail detection (permanent) - # Zeilen mit "** rcpt ..." sind i. d. R. permanent; weitere Marker: "Unrouteable address", "rejected", "mailbox full" kann defer sein -> belassen - is_perm_fail = 0 - if (is_fail) { - is_perm_fail=1 - } else if (match($0, /(Unrouteable address|no such user|User unknown|550 )/)) { - is_perm_fail=1 - } - - # Domainfilter (weicher): wir verwerfen die ID nicht, zeigen aber non-matching Zeilen trotzdem mit - if (WANTDOM != "") { - # keine harte Filterung, nur spätere Auswertung nutzt WANTDOM, falls nötig - ; - } - - # Extra Regex? (nur für Zeilenfilterung, ID bleibt) - if (EXTRA != "" && $0 !~ EXTRA) { - ; # wir speichern die Zeile dennoch im Voll-Log, sonst verliert man Kontext - } - - # Counters - if (is_success) deliveries[msgid]++ - if (is_defer) defers[msgid]++ - if (is_perm_fail) fails[msgid]++ - - # „have been failing …" Marker - if (match($0, /all hosts for '\''[^']+'\'' have been failing/)) { - fail_marker[msgid]=1 - } - - # Full log sammeln - status[msgid]=(status[msgid] ? status[msgid] "\n" : "") ts " | " $0 -} - -END { - # IDs einsammeln & sortieren - n=0 - for (id in first_ts){ ids[++n]=id } - for (i=2;i<=n;i++){ - key=ids[i]; j=i-1 - while (j>=1 && first_ts[ids[j]] > first_ts[key]) { ids[j+1]=ids[j]; j-- } - ids[j+1]=key - } - - if (n==0){ - print "Keine passenden Einträge im gewählten Zeitfenster gefunden." - exit 0 - } - - sep="------------------------------------------------------------------------" - for (k=1;k<=n;k++){ - id=ids[k] - - # FROM Nachfilter - if (FROMREQ != "" && from_mismatch[id]==1) { - if (from_addr[id] != FROMREQ && from_addr[id] != "") continue - } - - d=(id in deliveries ? deliveries[id] : 0) - e=(id in defers ? defers[id] : 0) - f=(id in fails ? fails[id] : 0) - - # Status-Logik: - # - d>0 && f==0 && e==0 -> OK - # - d>0 && (f>0 || e>0) -> PARTIAL - # - d==0 && f>0 -> FAIL - # - d==0 && f==0 && e>0 -> DELAY - # - sonst -> UNKNOWN - status_txt="UNKNOWN" - if (d>0 && f==0 && e==0) status_txt="OK" - else if (d>0 && (f>0 || e>0)) status_txt="PARTIAL" - else if (d==0 && f>0) status_txt="FAIL" - else if (d==0 && f==0 && e>0) status_txt="DELAY" - - # Summary-Zeile - # Rcpts sammeln - out_rcpts="" - if (id in rcpts){ - for (r in rcpts[id]) { - out_rcpts = (out_rcpts=="" ? r : out_rcpts ", " r) - } - } - printf "[%s] ID=%s | First=%s | From=%s | Rcpts=%s | Delivered=%d Deferred=%d Failed=%d\n", - status_txt, id, first_ts[id], (from_addr[id] ? from_addr[id] : "(?)"), (out_rcpts=="" ? "(?)" : out_rcpts), d, e, f - - # Marker-Hinweis - if (fail_marker[id]) { - printf " Note: DELIVERY STALL erkannt: '\''all hosts for domain failing (retry/backoff)'\''\n" - } - - # Detailblock - print sep - printf "ID: %s\n", id - printf "First: %s\n", first_ts[id] - printf "Last: %s\n", last_ts[id] - printf "From: %s\n", (from_addr[id] ? from_addr[id] : "(unbekannt)") - printf "Rcpts: %s\n", (out_rcpts=="" ? "(unbekannt)" : out_rcpts) - printf "Status: %s (Delivered=%d, Deferred=%d, Failed=%d)\n", status_txt, d, e, f - print "Log:" - print status[id] - print sep - } -} - -# Built with the help of Claude Code \ No newline at end of file diff --git a/legacy/exitrace/lib/mx_lookup.sh b/legacy/exitrace/lib/mx_lookup.sh deleted file mode 100644 index 7fde1bc..0000000 --- a/legacy/exitrace/lib/mx_lookup.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bash -# mx_lookup.sh - MX record lookup functions for exitrace -# Maintainer: ljp - -# MX record lookup with A/AAAA resolution -show_mx_records() { - local domain="$1" - - if ! command -v dig >/dev/null 2>&1; then - echo "(Hinweis) dig nicht gefunden, MX-Ausgabe übersprungen." >&2 - return 1 - fi - - echo "== MX für ${domain} ==" - dig +short MX "$domain" | sort -n || { - echo "Fehler: MX-Abfrage für $domain fehlgeschlagen" >&2 - return 1 - } - - echo "-- A/AAAA der MX:" - while read -r mx; do - [[ -n "$mx" ]] || continue - local host="${mx#* }" - printf "%-35s | " "$host" - - # A records - local a_records - a_records="$(dig +short A "$host" 2>/dev/null | paste -sd, -)" - printf "%s" "${a_records:-"-"}" - - printf " | " - - # AAAA records - local aaaa_records - aaaa_records="$(dig +short AAAA "$host" 2>/dev/null | paste -sd, -)" - printf "%s\n" "${aaaa_records:-"-"}" - - done < <(dig +short MX "$domain" | sort -n) - - echo "========================================================================" -} - -# Built with the help of Claude Code \ No newline at end of file