#!/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) # - 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 # - Klassifikation: OK / PARTIAL / DELAY / FAIL / UNKNOWN # - Optional: MX-Übersicht für -D/--domain mittels --mx # # 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 # # Exitcodes: 0=OK (Treffer evtl. 0), 1=Fehlerhafte Parameter/Umgebung # ----------------------------------------------------------------------------- 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] Zeitfenster (eine Option wählen): -H, --hours N Letzte N Stunden durchsuchen -DAYS, --days N Letzte N Tage durchsuchen -d, --date YYYY-MM-DD Exaktes Datum -r, --range A..B Datumsbereich (inkl.): YYYY-MM-DD..YYYY-MM-DD Filter (beliebig kombinierbar): -f, --from ADDR Envelope-From / "from <...>" -t, --to REGEX Empfänger (Regex, matcht ' for ' oder in '** rcpt') -D, --domain NAME Ziel-Domain (z. B. example.de) -m, --msgid ID Exim-Message-ID (z. B. 1uwRvz-000Fz2-CI) -G, --grep REGEX Zusätzlicher Regex auf Logzeilen Sonstiges: --mx Zeigt MX-Einträge (und A/AAAA) für --domain -n, --dry-run Zeigt nur, welche Logfiles untersucht würden -h, --help Diese Hilfe 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 USAGE } # --------------------------- Argumente parsen -------------------------------- 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:-}" 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 ;; -m|--msgid) MSGID="${2:-}"; shift 2 ;; -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 ;; esac done # ------------------------- Datums-Liste bauen -------------------------------- declare -a DATES=() if [[ -n "$HOURS" ]]; then mapfile -t DATES < <(gen_dates_hours_back "$HOURS") elif [[ -n "$DAYS" ]]; then mapfile -t DATES < <(gen_dates_days_back "$DAYS") elif [[ -n "$DATE_ONE" ]]; then DATES=("$DATE_ONE") elif [[ -n "$DATE_RANGE" ]]; then mapfile -t DATES < <(gen_dates_range "$DATE_RANGE") else DATES=("$TODAY") fi declare -a FILES=() for d in "${DATES[@]}"; do while read -r f; do FILES+=("$f"); done < <(files_for_date "$d" || true) done if (( DRYRUN )); then echo "Würde durchsuchen:" printf ' %s\n' "${FILES[@]}" exit 0 fi if [[ "${#FILES[@]}" -eq 0 ]]; then echo "Keine Logfiles gefunden unter ${LOGROOT}/YYYY/MM/DATE.log(.gz) fürs gewählte Zeitfenster." >&2 exit 0 fi # --------------------------- Vorfilter bauen --------------------------------- 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 -------------------------------------- cat_files() { # .log direkt lesen, .gz via zcat local f for f in "${FILES[@]}"; do if [[ "$f" == *.gz ]]; then zcat -- "$f" else cat -- "$f" fi done } 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 ------------------------------------ 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 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