diff --git a/legacy/exitrace/exitrace.sh b/legacy/exitrace/exitrace.sh index 87ef40a..0b78107 100755 --- a/legacy/exitrace/exitrace.sh +++ b/legacy/exitrace/exitrace.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash # exitrace - Exim-Log-Trace & Status für einzelne Mails/Adressen/Domain-Ziele # Maintainer: ljp +# Modular version - requires lib/ directory # ----------------------------------------------------------------------------- # Features: # - Sucht in /var/log/easyname/YYYY/MM/YYYY-MM-DD.log(.gz) (anpassbar mit $LOGROOT) @@ -20,8 +21,26 @@ set -euo pipefail +# Script directory +SCRIPT_DIR="$(cd "$(dirname "${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}" -DATE_FMT="%Y-%m-%d" TODAY="$(date +%Y-%m-%d)" usage() { @@ -60,10 +79,22 @@ 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 ;; + -H|--hours) + HOURS="${2:-}" + validate_positive_integer "$HOURS" "--hours" + shift 2 ;; + -DAYS|--days) + DAYS="${2:-}" + validate_positive_integer "$DAYS" "--days" + shift 2 ;; + -d|--date) + DATE_ONE="${2:-}" + validate_date "$DATE_ONE" "--date" + shift 2 ;; + -r|--range) + DATE_RANGE="${2:-}" + validate_date_range "$DATE_RANGE" + shift 2 ;; -f|--from) FROM="${2:-}"; shift 2 ;; -t|--to) TO_REGEX="${2:-}"; shift 2 ;; -D|--domain) TARGET_DOMAIN="${2:-}"; shift 2 ;; @@ -77,45 +108,6 @@ while [[ $# -gt 0 ]]; do done # ------------------------- Datums-Liste bauen -------------------------------- -files_for_date() { - local d="$1" y="${d:0:4}" m="${d:5:2}" - 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 -} - -gen_dates_hours_back() { - local hrs="$1" start_ts end_ts cur - 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" +${DATE_FMT} - done -} - -gen_dates_range() { - local a="${1%%..*}" b="${1##*..}" cur="$a" - while :; do - echo "$cur" - [[ "$cur" == "$b" ]] && break - cur="$(date -d "$cur +1 day" +${DATE_FMT})" - done -} - declare -a DATES=() if [[ -n "$HOURS" ]]; then mapfile -t DATES < <(gen_dates_hours_back "$HOURS") @@ -176,193 +168,15 @@ grep_chain() { # ------------------------------- MX Block ------------------------------------ if (( WANT_MX )) && [[ -n "$TARGET_DOMAIN" ]]; then - if command -v dig >/dev/null 2>&1; then - echo "== MX für ${TARGET_DOMAIN} ==" - dig +short MX "$TARGET_DOMAIN" | sort -n || true - echo "-- A/AAAA der MX:" - while read -r mx; do - 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 "$TARGET_DOMAIN" | sort -n) - echo "========================================================================" - else - echo "(Hinweis) dig nicht gefunden, MX-Ausgabe übersprungen." - fi + show_mx_records "$TARGET_DOMAIN" fi # ---------------------------- Auswertung (awk) -------------------------------- -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 } -function incr(a, k){ a[k]++; return a[k] } +if [[ ! -f "$SCRIPT_DIR/lib/log_analyzer.awk" ]]; then + echo "Fehler: lib/log_analyzer.awk nicht gefunden in $SCRIPT_DIR" >&2 + exit 1 +fi -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] - sub(/^[0-9-: ]+ /, "", raw_after) # restline - 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 - } -} -' +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" # 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 new file mode 100644 index 0000000..ee877be --- /dev/null +++ b/legacy/exitrace/lib/date_utils.sh @@ -0,0 +1,118 @@ +#!/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 new file mode 100755 index 0000000..f6dd430 --- /dev/null +++ b/legacy/exitrace/lib/log_analyzer.awk @@ -0,0 +1,178 @@ +#!/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 new file mode 100644 index 0000000..7fde1bc --- /dev/null +++ b/legacy/exitrace/lib/mx_lookup.sh @@ -0,0 +1,43 @@ +#!/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