diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..3e250ed --- /dev/null +++ b/deploy.sh @@ -0,0 +1,258 @@ +#!/usr/bin/env bash +# deploy.sh - Einfaches Deployment-Tool für esn-support-bash-snippets +# Maintainer: ljp +# +# Erkennt installierte Scripts in ~/bin und ermöglicht einfache Auswahl +set -euo pipefail + +# Prüfe LUSER Environment +LUSER="${LUSER:?Bitte LUSER setzen! (siehe go-Script)}" + +# Script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BIN_DIR="$HOME/bin" + +usage() { + cat <<'USAGE' +Usage: deploy.sh + +EINFACHES DEPLOYMENT: + - Erkennt automatisch installierte Scripts in ~/bin + - Einfache Auswahl mit bash select + - Einfache Server-Auswahl (web, mx, logger, support) + - Optional: bashrc deployment + +FEATURES: + - Auto-Detection von Scripts in ~/bin + - Symlink-basiertes Deployment + - PATH-Setup für target servers + - Dry-run verfügbar + +ENVIRONMENT: + LUSER Required: Username für SSH-Verbindungen +USAGE +} + +# Einfache Script-Detection +get_deployable_scripts() { + local scripts=() + + [[ ! -d "$BIN_DIR" ]] && { echo "Warnung: $BIN_DIR existiert nicht" >&2; return 0; } + + # Finde alle Symlinks die auf unser Repository zeigen + for symlink in "$BIN_DIR"/*; do + [[ -L "$symlink" ]] || continue + local target=$(readlink -f "$symlink" 2>/dev/null || continue) + [[ "$target" == "$SCRIPT_DIR"* ]] || continue + scripts+=("$(basename "$symlink")") + done + + printf '%s\n' "${scripts[@]}" | sort +} + +# Einfache Server-Auswahl (nur Target zurückgeben) +select_server() { + local servers=("web" "mx" "logger" "support") + local server + + select server in "${servers[@]}"; do + [[ -n "$server" ]] || { echo "Ungültige Auswahl" >&2; continue; } + + case "$server" in + web|mx) + echo -n "Server-Nummer eingeben: " >&2 + read -r number + [[ "$number" =~ ^[0-9]+$ ]] || { echo "Ungültige Nummer" >&2; continue; } + echo "${LUSER}@${server}${number}.in.easyname.com" + return 0 + ;; + logger) + echo "${LUSER}@logger.in.nessus.at" + return 0 + ;; + support) + echo "${LUSER}@support.in.easyname.com" + return 0 + ;; + esac + done +} + +# Einfaches Script-Deployment +deploy_script() { + local script_name="$1" + local ssh_target="$2" + local dry_run="$3" + + # Folge Symlink zum Original + local symlink_path="$BIN_DIR/$script_name" + [[ -L "$symlink_path" ]] || { echo "Fehler: $script_name ist kein Symlink" >&2; return 1; } + + local original_script=$(readlink -f "$symlink_path") + local script_dir=$(dirname "$original_script") + + echo "=== Deploying $script_name to $ssh_target ===" + + if (( dry_run )); then + echo " [DRY-RUN] Würde deployment ausführen:" + echo " Script: $original_script -> ~/$script_name" + echo " Symlink: ~/bin/$script_name -> ~/$script_name" + [[ -d "$script_dir/lib" ]] && echo " Lib: $script_dir/lib/ -> ~/lib_$script_name/" + return 0 + fi + + # Deployment ausführen - ordentlich in ~/scripts/ + ssh "$ssh_target" "mkdir -p ~/scripts ~/bin" + scp "$original_script" "$ssh_target:scripts/$script_name" + ssh "$ssh_target" "chmod +x ~/scripts/$script_name && ln -sf ~/scripts/$script_name ~/bin/$script_name" + + # Lib-Dependencies falls vorhanden + if [[ -d "$script_dir/lib" ]]; then + echo " Deploying lib dependencies..." + scp -r "$script_dir/lib" "$ssh_target:scripts/lib_$script_name" + ssh "$ssh_target" "sed -i 's|\$SCRIPT_DIR/lib/|~/scripts/lib_$script_name/|g' ~/scripts/$script_name" + fi + + echo " ✓ $script_name deployed successfully" +} + +# Einfaches bashrc-Deployment +deploy_bashrc() { + local ssh_target="$1" + local dry_run="$2" + + echo "=== Deploying bashrc to $ssh_target ===" + + if (( dry_run )); then + echo " [DRY-RUN] Würde default bashrc mit PATH-Setup erstellen" + return 0 + fi + + # Erstelle modulare .bashrc + config files + ssh "$ssh_target" 'cat > .bashrc << "EOF" +# .bashrc - Optimierte Bash-Konfiguration für easyname Support + +##### History ##### +HISTCONTROL=ignoredups:erasedups +HISTSIZE=50000 +HISTFILESIZE=100000 +HISTTIMEFORMAT="%Y-%m-%d %H:%M:%S " +shopt -s histappend +shopt -s cmdhist + +##### Prompt ##### +if [[ "$TERM" != "dumb" ]]; then + PS1="\[\e[01;32m\]\u@\h\[\e[00m\]:\[\e[01;34m\]\w\[\e[00m\]\\$ " +fi + +##### Modular configs ##### +[[ -f ~/.bash_aliases ]] && source ~/.bash_aliases +[[ -f ~/.bash_env ]] && source ~/.bash_env +[[ -f ~/.bash_paths ]] && source ~/.bash_paths + +##### Standard settings ##### +if [[ -f /etc/bash_completion ]]; then + source /etc/bash_completion +fi + +if [[ -x /usr/bin/dircolors ]]; then + eval "$(dircolors -b)" +fi + +# Built with the help of Claude Code +EOF' + + # Erstelle modulare Config-Files mit PATH-Check + ssh "$ssh_target" 'echo "# Add your custom aliases here" > .bash_aliases' + ssh "$ssh_target" 'echo "# Add your PATH extensions here" > .bash_paths' + + # Prüfe ob ~/bin bereits in PATH ist, wenn nicht -> zu .bash_env hinzufügen + ssh "$ssh_target" 'if [[ ":$PATH:" != *":$HOME/bin:"* ]]; then + echo "# Add ~/bin to PATH for deployed scripts" > .bash_env + echo "export PATH=\"\$HOME/bin:\$PATH\"" >> .bash_env + else + echo "# Add your environment variables here" > .bash_env + fi' + + echo " ✓ bashrc deployed successfully" +} + +# MAIN - Vereinfachte Ausführung +main() { + # Argument parsing + local dry_run=0 + while [[ $# -gt 0 ]]; do + case "$1" in + -n|--dry-run) dry_run=1; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "Unbekannte Option: $1" >&2; usage; exit 1 ;; + esac + done + + echo "=== easyname Support Scripts Deployment ===" + echo + + # Script-Auswahl + local -a available_scripts + mapfile -t available_scripts < <(get_deployable_scripts) + + [[ "${#available_scripts[@]}" -eq 0 ]] && { + echo "Keine deployable Scripts in $BIN_DIR gefunden" >&2 + exit 1 + } + + echo "Verfügbare Scripts:" + local script + select script in "${available_scripts[@]}" "ALL SCRIPTS"; do + [[ -n "$script" ]] && break + echo "Ungültige Auswahl" + done + + # Server-Auswahl + echo "=== Server-Auswahl ===" + local ssh_target + ssh_target=$(select_server) + + # Optionen + echo -n "Bashrc auch deployen? [y/N]: " + read -r deploy_bashrc_choice + local deploy_bashrc=0 + [[ "$deploy_bashrc_choice" =~ ^[yY]$ ]] && deploy_bashrc=1 + + # Bestätigung + echo + echo "=== Deployment-Zusammenfassung ===" + echo "Target: $ssh_target" + echo "Scripts: $script" + [[ "$deploy_bashrc" -eq 1 ]] && echo "Bashrc: Ja" + [[ "$dry_run" -eq 1 ]] && echo "Modus: DRY-RUN" + echo + echo -n "Deployment starten? [y/N]: " + read -r confirm + [[ "$confirm" =~ ^[yY]$ ]] || { echo "Abgebrochen."; exit 0; } + + # Deployment ausführen + echo + echo "=== Deployment wird ausgeführt ===" + + # Bashrc deployment + (( deploy_bashrc )) && deploy_bashrc "$ssh_target" "$dry_run" + + # Script deployment + if [[ "$script" == "ALL SCRIPTS" ]]; then + for s in "${available_scripts[@]}"; do + deploy_script "$s" "$ssh_target" "$dry_run" + done + else + deploy_script "$script" "$ssh_target" "$dry_run" + fi + + echo + echo "=== Deployment abgeschlossen ===" + [[ "$dry_run" -eq 1 ]] && echo "(DRY-RUN - nichts wurde tatsächlich deployed)" +} + +# Script starten +main "$@" + +# Built with the help of Claude Code \ No newline at end of file diff --git a/legacy/checkms/checkms.sh b/legacy/checkms/checkms.sh old mode 100644 new mode 100755 diff --git a/legacy/exitrace/README.md b/legacy/exitrace/README.md index f5536ed..1d4b68d 100644 --- a/legacy/exitrace/README.md +++ b/legacy/exitrace/README.md @@ -1,37 +1,57 @@ # exitrace -Exim-Log-Trace & Status für einzelne Mails/Adressen/Domain-Ziele: -Durchsucht Exim-Logs nach Message-IDs, gruppiert alle Ereignisse und klassifiziert den Delivery-Status +**Remote Exim-Log-Trace & Status für einzelne Mails/Adressen/Domain-Ziele** + +Führt Exim-Log-Analyse remote auf `logger.in.nessus.at` aus, wo sich die Exim-Logs befinden. +Durchsucht Logs nach Message-IDs, gruppiert alle Ereignisse und klassifiziert den Delivery-Status. --- ## Features -- Sucht in `/var/log/easyname/YYYY/MM/YYYY-MM-DD.log(.gz)` (konfigurierbar via `$LOGROOT`) -- Zeitfenster: letzte N Stunden/Tage ODER konkretes Datum/Datumsbereich -- Filter: Absender, Empfänger (Regex), Domain, Message-ID, zusätzliche Regex -- Gruppiert alle Treffer pro Exim-Message-ID und fasst Status zusammen +- **Remote-Ausführung**: Automatische SSH-Verbindung zu `logger.in.nessus.at` +- **Zeitfenster**: Letzte N Stunden/Tage ODER konkretes Datum/Datumsbereich +- **Filter**: Absender, Empfänger (Regex), Domain, Message-ID, zusätzliche Regex +- **Gruppierung**: Alle Treffer pro Exim-Message-ID mit Status-Zusammenfassung - **Status-Klassifikation**: OK / PARTIAL / DELAY / FAIL / UNKNOWN -- **MX-Records**: Optional DNS-Abfragen für Ziel-Domain (--mx) -- Markiert häufige Fehler (z. B. "all hosts for 'domain' have been failing") -- Sortierte chronologische Ausgabe pro Message-ID +- **MX-Records**: Optional DNS-Abfragen für Ziel-Domain (--mx) +- **Failure-Detection**: Markiert häufige Fehler (z.B. "all hosts failing") +- **Chronologische Ausgabe**: Sortiert nach Message-ID Erstkontakt - **Komprimierte Logs**: Automatischer Support für .gz-Archive +- **Smart Connection**: Verwendet `go logger` oder `ssh ${LUSER}@logger.in.nessus.at` --- ## Installation -1. Repository klonen oder Skript kopieren: - ```bash - git clone /esn-support-bash-snippets.git - cd esn-support-bash-snippets/legacy/exitrace - chmod +x exitrace.sh - ``` +Via Installer (empfohlen): +```bash +cd ~/.dev/esn-support-bash-snippets +./install.sh +# Dann: 3) Legacy Tools auswählen und exitrace installieren +``` -2. Abhängigkeiten: - - Standard Unix-Tools (awk, grep, date, zcat) - - `dig` (optional, für --mx Feature) - - Lesezugriff auf `/var/log/easyname/` (oder custom `$LOGROOT`) +Manuelle Installation: +```bash +git clone /esn-support-bash-snippets.git ~/.dev/esn-support-bash-snippets +cd ~/.dev/esn-support-bash-snippets/legacy/exitrace +chmod +x exitrace.sh +ln -sf ~/.dev/esn-support-bash-snippets/legacy/exitrace/exitrace.sh ~/bin/exitrace +``` + +## Voraussetzungen + +- **LUSER Environment Variable** ODER installiertes `go` Tool +- **SSH-Zugang** zu `logger.in.nessus.at` +- Standard Unix-Tools (awk, grep, date, ssh/scp) +- `dig` (optional, für --mx Feature) + +**Setup:** +```bash +export LUSER="dein-username" +# Oder in ~/.bash_env für permanente Konfiguration +echo 'export LUSER="dein-username"' >> ~/.bash_env +``` --- @@ -44,24 +64,43 @@ exitrace (durchsucht heutiges Log ohne Filter) ### Beispiele + +**Häufige Use-Cases:** ```bash -# Mail-Trace einer konkreten Message-ID -exitrace -m 1uwRvz-000Fz2-CI +# Mail-Trace für spezifischen Absender zu t-online.de (letzte 48h) +exitrace -FROM franziskus@adjuva.info -DOMAIN t-online.de -H 48 -# Alle Mails von bestimmtem Absender in den letzten 48 Stunden -exitrace -f sender@example.com -H 48 +# Alle Mails an bestimmte Domain in Zeitspanne +exitrace -DOMAIN gmx.de -r 2025-09-10..2025-09-11 -# Empfänger-Domain in Zeitspanne -exitrace -D example.de -r 2025-09-09..2025-09-11 +# Konkrete Message-ID verfolgen +exitrace -MSGID 1uwRvz-000Fz2-CI -# Regex-Filter auf Empfänger + konkretes Datum -exitrace -t '.*@example\.de' -d 2025-09-10 +# Regex-Filter auf Empfänger + MX-Records anzeigen +exitrace -TO '.*@gmail\.com' -d 2025-09-11 --mx -# Kombinierte Filter: Absender + Domain + Zeitfenster + MX-Info -exitrace -f sender@example.com -D example.de -H 48 --mx +# Kombinierte Filter: Absender + Domain + Zeitfenster +exitrace -FROM noreply@company.com -DOMAIN outlook.com -H 24 +``` -# Dry-Run: zeigt nur, welche Logfiles durchsucht würden +**Debugging & Analyse:** +```bash +# Dry-Run: zeigt nur welche Logfiles durchsucht würden exitrace -n -H 24 + +# Alle Mails von Domain in letzten 7 Tagen +exitrace -FROM '.*@problematic-domain\.com' -DAYS 7 + +# Zusätzlicher Regex-Filter +exitrace -DOMAIN example.com -GREP "550.*blocked" -H 12 +``` + +**Backwards Compatibility:** +```bash +# Alte Parameter funktionieren weiterhin +exitrace -f sender@domain.com -D example.de -H 48 +exitrace -m 1uwRvz-000Fz2-CI +exitrace -t '.*@gmail\.com' --mx ``` --- @@ -78,34 +117,55 @@ Zeitfenster (eine Option wählen): -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 nach " for " oder in "** ...") - -D, --domain NAME Ziel-Domain (z. B. example.de), prüft auf Zustellungen/Fehler dorthin - -m, --msgid ID Exim-Message-ID (z. B. 1uwRvz-000Fz2-CI) - -G, --grep REGEX Zusätzlicher Regex auf Logzeilen + -FROM, --from ADDR Envelope-From / "from <...>" (alias: -f) + -TO, --to REGEX Empfänger (Regex, matcht nach " for " oder in "** ...") (alias: -t) + -DOMAIN, --domain NAME Ziel-Domain (z. B. example.de), prüft auf Zustellungen/Fehler dorthin (alias: -D) + -MSGID, --msgid ID Exim-Message-ID (z. B. 1uwRvz-000Fz2-CI) (alias: -m) + -GREP, --grep REGEX Zusätzlicher Regex auf Logzeilen (alias: -G) Sonstiges: --mx Zeigt MX-Einträge (und A/AAAA) für --domain - -n, --dry-run Zeigt nur, welche Logfiles untersucht würden + -n, --dry-run Zeigt nur, welche Logfiles untersucht würden -h, --help Diese Hilfe + +Remote Execution: + Wird automatisch remote auf logger.in.nessus.at ausgeführt. + Verwendet 'go logger' oder 'ssh ${LUSER}@logger.in.nessus.at'. ``` --- ## Beispielausgabe +### Erfolgreiche Zustellung ``` -[OK] ID=1uwRvz-000Fz2-CI | First=2025-09-10 14:32:15 | From=sender@example.com | Rcpts=recipient@example.de | Delivered=1 Deferred=0 Failed=0 +[OK] ID=1v2K8X-0004Zm-BC | First=2025-09-11 08:32:15 | From=franziskus@adjuva.info | Rcpts=user@t-online.de | Delivered=1 Deferred=0 Failed=0 ------------------------------------------------------------------------ -ID: 1uwRvz-000Fz2-CI -First: 2025-09-10 14:32:15 -Last: 2025-09-10 14:32:47 -From: sender@example.com -Rcpts: recipient@example.de +ID: 1v2K8X-0004Zm-BC +First: 2025-09-11 08:32:15 +Last: 2025-09-11 08:32:47 +From: franziskus@adjuva.info +Rcpts: user@t-online.de Status: OK (Delivered=1, Deferred=0, Failed=0) Log: -2025-09-10 14:32:15 | <= sender@example.com H=mail.example.com [10.0.0.1] P=esmtp S=1234 -2025-09-10 14:32:47 | => recipient@example.de R=dnslookup T=remote_smtp H=mx.example.de [10.0.0.2] +2025-09-11 08:32:15 | <= franziskus@adjuva.info H=mail.adjuva.info [10.0.0.1] P=esmtp S=2156 +2025-09-11 08:32:47 | => user@t-online.de R=dnslookup T=remote_smtp H=mx01.t-online.de [217.5.206.120] +------------------------------------------------------------------------ +``` + +### Fehlgeschlagene Zustellung +``` +[FAIL] ID=1v2L9Y-0005Ap-DE | First=2025-09-11 09:15:32 | From=franziskus@adjuva.info | Rcpts=blocked@t-online.de | Delivered=0 Deferred=0 Failed=1 +------------------------------------------------------------------------ +ID: 1v2L9Y-0005Ap-DE +First: 2025-09-11 09:15:32 +Last: 2025-09-11 09:15:55 +From: franziskus@adjuva.info +Rcpts: blocked@t-online.de +Status: FAIL (Delivered=0, Deferred=0, Failed=1) +Log: +2025-09-11 09:15:32 | <= franziskus@adjuva.info H=mail.adjuva.info [10.0.0.1] P=esmtp S=1847 +2025-09-11 09:15:55 | ** blocked@t-online.de R=dnslookup T=remote_smtp H=mx01.t-online.de [217.5.206.120]: SMTP error from remote mail server after RCPT TO:: 550 requested action not taken: mailbox unavailable ------------------------------------------------------------------------ ``` @@ -138,32 +198,66 @@ Bei Delivery-Stalls: Nach Installation über `./install.sh` ist das Script direkt verfügbar: ```bash -exitrace -m 1uwRvz-000Fz2-CI +exitrace -FROM franziskus@adjuva.info -DOMAIN t-online.de -H 48 ``` -Das Installationsscript erstellt automatisch einen Symlink in `~/bin` (ohne `.sh` Extension). - -Umgebungsvariable für abweichenden Logpfad: +**LUSER Environment Variable setzen (wichtig!):** ```bash -export LOGROOT="/var/log/custom" +# Temporär +export LUSER="dein-username" + +# Permanent in ~/.bash_env (empfohlen) +echo 'export LUSER="dein-username"' >> ~/.bash_env +source ~/.bash_env ``` -Logger-spezifisch ggf. in `~/.profile` ergänzen: +**Verbindungstest:** ```bash -cd /var/log/easyname/$(date +%Y)/$(date +%m)/ +# Test ob go-Tool verfügbar +go logger + +# Test ob SSH funktioniert +ssh ${LUSER}@logger.in.nessus.at "hostname" ``` --- -## Notes +## Troubleshooting -- Das Skript analysiert Exim-Logs im Format `/var/log/easyname/YYYY/MM/YYYY-MM-DD.log(.gz)` -- **Komprimierte Archive**: Automatische Erkennung und Verarbeitung von .gz-Dateien -- **Status-Logik**: Basiert auf SMTP-Response-Codes und typischen Exim-Nachrichten -- Bei größeren Zeiträumen kann die Verarbeitung länger dauern -- Regex-Filter (-t, -G) sollten sorgfältig formuliert werden, um false positives zu vermeiden -- Die Ausgabe ist nach chronologischem Erstkontakt der Message-ID sortiert -- **MX-Feature**: Benötigt `dig` für DNS-Abfragen -- Exit-Codes: 0 = OK (auch bei 0 Treffern), 1 = Fehlerhafte Parameter/Umgebung +### Häufige Probleme + +**"LUSER Variable nicht gesetzt"** +```bash +export LUSER="dein-username" +``` + +**"Permission denied" oder SSH-Fehler** +- Kerberos-Login: `kinit ${LUSER}@NESSUS.AT` +- SSH-Keys prüfen oder `go` Tool installieren + +**"Keine Logfiles gefunden"** +- Zeitfenster zu eng: `-H 48` oder `-DAYS 3` probieren +- Dry-run verwenden: `exitrace -n -H 24` + +**AWK-Errors** +- Regex-Syntax prüfen bei `-t` und `-G` Optionen +- Sonderzeichen escapen: `.*@example\.com` + +### Performance-Tipps + +- **Kleine Zeitfenster**: `-H 24` statt `-DAYS 7` +- **Spezifische Filter**: `-f` oder `-m` sind schneller als `-t` +- **Dry-Run nutzen**: `-n` zeigt Suchbereich ohne Ausführung + +--- + +## Technical Notes + +- **Remote-Ausführung**: Komplettes Script wird via SSH Heredoc übertragen +- **Log-Format**: `/var/log/easyname/YYYY/MM/YYYY-MM-DD.log(.gz)` auf logger +- **Status-Logik**: Basiert auf SMTP-Response-Codes und Exim-Nachrichten +- **Komprimierte Archive**: Automatische .gz-Verarbeitung via `zcat` +- **Chronologische Sortierung**: Nach Message-ID Erstkontakt +- **Exit-Codes**: 0 = OK (auch bei 0 Treffern), 1 = Parameter-/Verbindungsfehler Built with the help of Claude Code \ No newline at end of file diff --git a/legacy/exitrace/exitrace.sh b/legacy/exitrace/exitrace.sh index 87ef40a..9dede6d 100755 --- a/legacy/exitrace/exitrace.sh +++ b/legacy/exitrace/exitrace.sh @@ -1,28 +1,41 @@ #!/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 # ----------------------------------------------------------------------------- # Features: -# - Sucht in /var/log/easyname/YYYY/MM/YYYY-MM-DD.log(.gz) (anpassbar mit $LOGROOT) +# - Läuft lokal auf logger.in.nessus.at (wo die Exim-Logs liegen) +# - Sucht in /var/log/easyname/YYYY/MM/YYYY-MM-DD.log(.gz) # - 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) +# - Filter: Absender (-FROM), Empfänger-Regex (-TO), Ziel-Domain (-DOMAIN), Message-ID (-MSGID), Extra-Regex (-GREP) # - 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 +# - Optional: MX-Übersicht für -DOMAIN mittels --mx +# +# Deployment: Mit deploy.sh auf logger.in.nessus.at installieren +# Usage: ssh logger.in.nessus.at exitrace [OPTS] # # 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 +# exitrace -FROM sender@example.com -DOMAIN example.de -H 48 +# exitrace -MSGID 1uwRvz-000Fz2-CI +# exitrace -TO '.*@example.de' -r 2025-09-09..2025-09-11 --mx # # Exitcodes: 0=OK (Treffer evtl. 0), 1=Fehlerhafte Parameter/Umgebung # ----------------------------------------------------------------------------- set -euo pipefail -LOGROOT="${LOGROOT:-/var/log/easyname}" -DATE_FMT="%Y-%m-%d" -TODAY="$(date +%Y-%m-%d)" +# Standardwerte +HOURS="" +DAYS="" +DATE="" +RANGE="" +FROM="" +TO="" +DOMAIN="" +MSGID="" +GREP="" +MX=0 +DRY_RUN=0 usage() { cat <<'USAGE' @@ -35,11 +48,11 @@ Zeitfenster (eine Option wählen): -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 + -FROM, --from ADDR Envelope-From / "from <...>" (alias: -f) + -TO, --to REGEX Empfänger (Regex, matcht ' for ' oder in '** rcpt') (alias: -t) + -DOMAIN, --domain NAME Ziel-Domain (z. B. example.de) (alias: -D) + -MSGID, --msgid ID Exim-Message-ID (z. B. 1uwRvz-000Fz2-CI) (alias: -m) + -GREP, --grep REGEX Zusätzlicher Regex auf Logzeilen (alias: -G) Sonstiges: --mx Zeigt MX-Einträge (und A/AAAA) für --domain @@ -47,322 +60,508 @@ Sonstiges: -h, --help Diese Hilfe Beispiele: - exitrace -f sender@example.com -D example.de -H 48 + exitrace -FROM sender@example.com -DOMAIN example.de -H 48 + exitrace -MSGID 1uwRvz-000Fz2-CI + exitrace -TO '.*@example.de' -r 2025-09-09..2025-09-11 --mx + +Backwards Compatibility: + exitrace -f sender@domain.com -D example.de -H 48 exitrace -m 1uwRvz-000Fz2-CI - exitrace -t '.*@example.de' -r 2025-09-09..2025-09-11 --mx + exitrace -t '.*@gmail\\.com' --mx USAGE } -# --------------------------- Argumente parsen -------------------------------- -HOURS=""; DAYS=""; DATE_ONE=""; DATE_RANGE="" -FROM=""; TO_REGEX=""; TARGET_DOMAIN=""; MSGID=""; GREPEXTRA="" -DRYRUN=0; WANT_MX=0 - +# Parse Arguments 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 ;; - -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 ;; + case $1 in + -H|--hours) + HOURS="$2" + [[ "$HOURS" =~ ^[0-9]+$ ]] || { echo "Fehler: --hours benötigt eine Zahl" >&2; exit 1; } + shift 2 + ;; + -DAYS|--days) + DAYS="$2" + [[ "$DAYS" =~ ^[0-9]+$ ]] || { echo "Fehler: --days benötigt eine Zahl" >&2; exit 1; } + shift 2 + ;; + -d|--date) + DATE="$2" + [[ "$DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || { echo "Fehler: --date benötigt YYYY-MM-DD" >&2; exit 1; } + shift 2 + ;; + -r|--range) + RANGE="$2" + [[ "$RANGE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}\.\.[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || { echo "Fehler: --range benötigt YYYY-MM-DD..YYYY-MM-DD" >&2; exit 1; } + shift 2 + ;; + -FROM|--from|-f) + FROM="$2" + shift 2 + ;; + -TO|--to|-t) + TO="$2" + shift 2 + ;; + -DOMAIN|--domain|-D) + DOMAIN="$2" + shift 2 + ;; + -MSGID|--msgid|-m) + MSGID="$2" + shift 2 + ;; + -GREP|--grep|-G) + GREP="$2" + shift 2 + ;; + --mx) + MX=1 + shift + ;; + -n|--dry-run) + DRY_RUN=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unbekannte Option: $1" >&2 + usage + exit 1 + ;; esac 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" +# Validierung: Nur ein Zeitfenster +time_options=0 +[[ -n "$HOURS" ]] && ((time_options++)) +[[ -n "$DAYS" ]] && ((time_options++)) +[[ -n "$DATE" ]] && ((time_options++)) +[[ -n "$RANGE" ]] && ((time_options++)) + +if [[ $time_options -gt 1 ]]; then + echo "Fehler: Nur ein Zeitfenster (-H/-DAYS/-d/-r) erlaubt" >&2 + exit 1 +fi + +# Default: Heutiges Datum +if [[ $time_options -eq 0 ]]; then + DATE=$(date +%Y-%m-%d) +fi + +# Datum-Validierung für DATE +validate_date() { + local d="$1" + if ! date -d "$d" >/dev/null 2>&1; then + echo "Fehler: Ungültiges Datum: $d" >&2 + exit 1 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 +# Bestimme Logfiles basierend auf Zeitfenster +determine_logfiles() { + local logfiles=() + + if [[ -n "$DATE" ]]; then + validate_date "$DATE" + local year month + year=$(date -d "$DATE" +%Y) + month=$(date -d "$DATE" +%m) + logfiles+=("/var/log/easyname/$year/$month/$DATE.log") + + elif [[ -n "$RANGE" ]]; then + local start_date end_date + start_date="${RANGE%%..*}" + end_date="${RANGE##*..}" + validate_date "$start_date" + validate_date "$end_date" + + local current_date="$start_date" + while [[ "$current_date" <= "$end_date" ]]; do + local year month + year=$(date -d "$current_date" +%Y) + month=$(date -d "$current_date" +%m) + logfiles+=("/var/log/easyname/$year/$month/$current_date.log") + current_date=$(date -d "$current_date + 1 day" +%Y-%m-%d) + done + + elif [[ -n "$HOURS" ]]; then + # Bestimme Datumsbereich für letzte N Stunden + local start_ts end_ts + end_ts=$(date +%s) + start_ts=$((end_ts - HOURS * 3600)) + + local current_ts="$start_ts" + local seen_dates=() + + while [[ $current_ts -le $end_ts ]]; do + local date_str + date_str=$(date -d "@$current_ts" +%Y-%m-%d) + + # Füge Datum nur einmal hinzu + local found=0 + for seen in "${seen_dates[@]}"; do + [[ "$seen" == "$date_str" ]] && found=1 && break + done + + if [[ $found -eq 0 ]]; then + seen_dates+=("$date_str") + local year month + year=$(date -d "$date_str" +%Y) + month=$(date -d "$date_str" +%m) + logfiles+=("/var/log/easyname/$year/$month/$date_str.log") + fi + + current_ts=$((current_ts + 86400)) # +1 Tag + done + + elif [[ -n "$DAYS" ]]; then + # Bestimme Datumsbereich für letzte N Tage + for ((i=0; i&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 +# MX Record Lookup +show_mx_records() { + [[ -n "$DOMAIN" ]] || return 0 + [[ $MX -eq 1 ]] || return 0 + + if ! command -v dig >/dev/null 2>&1; then + echo "Warnung: 'dig' nicht gefunden - MX-Records werden übersprungen" >&2 + return 0 + fi + + echo "== MX für $DOMAIN ==" + + # MX Records + local mx_records + mx_records=$(dig +short MX "$DOMAIN" | sort -n) + + if [[ -z "$mx_records" ]]; then + echo "Keine MX-Records gefunden" 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 - if command -v dig >/dev/null 2>&1; then - echo "== MX für ${TARGET_DOMAIN} ==" - dig +short MX "$TARGET_DOMAIN" | sort -n || true + echo "$mx_records" + + # A/AAAA Records für MX-Hosts 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." + while read -r priority mx_host; do + [[ -n "$mx_host" ]] || continue + mx_host="${mx_host%.}" # Trailing dot entfernen + + local a_records aaaa_records all_records + a_records=$(dig +short A "$mx_host" | paste -sd',' -) + aaaa_records=$(dig +short AAAA "$mx_host" | paste -sd',' -) + + all_records="" + [[ -n "$a_records" ]] && all_records="$a_records" + [[ -n "$aaaa_records" ]] && { + [[ -n "$all_records" ]] && all_records="$all_records," + all_records="$all_records$aaaa_records" + } + + printf "%-40s | %s\n" "$mx_host" "${all_records:-}" + done <<< "$mx_records" fi -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] } - -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 + + echo "========================================================================" } -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 +# Log Analysis - der Hauptalgorithmus +analyze_logs() { + local logfiles=() + mapfile -t logfiles < <(determine_logfiles) + + if [[ $DRY_RUN -eq 1 ]]; then + echo "=== DRY-RUN: Würde folgende Logfiles untersuchen ===" + for logfile in "${logfiles[@]}"; do + if [[ -f "$logfile" ]]; then + echo " [EXISTS] $logfile" + elif [[ -f "$logfile.gz" ]]; then + echo " [EXISTS] $logfile.gz (komprimiert)" + else + echo " [MISSING] $logfile(.gz)" + fi + done + return 0 + fi + + # MX Records anzeigen falls gewünscht + show_mx_records + + # Sammle alle relevanten Logzeilen + local temp_file + temp_file=$(mktemp) + trap "rm -f '$temp_file'" EXIT + + for logfile in "${logfiles[@]}"; do + local cat_cmd="" + + if [[ -f "$logfile" ]]; then + cat_cmd="cat '$logfile'" + elif [[ -f "$logfile.gz" ]]; then + cat_cmd="zcat '$logfile.gz'" + else + continue + fi + + # Zeitfilter für Stunden/Tage + local time_filter="" + if [[ -n "$HOURS" ]]; then + local start_time + start_time=$(date -d "-$HOURS hours" '+%b %_d %H:%M') + time_filter="awk -v start='$start_time' '\$0 ~ start,0'" + elif [[ -n "$DAYS" && $DAYS -lt 7 ]]; then # Nur für kleine Tageszahlen + local start_time + start_time=$(date -d "-$DAYS days" '+%b %_d') + time_filter="awk -v start='$start_time' '\$0 ~ start,0'" + fi + + # Baue Grep-Filter Pipeline + local filter_pipeline="" + + # Basis-Filter: Envelope-From + [[ -n "$FROM" ]] && filter_pipeline="$filter_pipeline | grep -F ' <= $FROM '" + + # TO-Filter: Empfänger-Regex + [[ -n "$TO" ]] && filter_pipeline="$filter_pipeline | grep -E '( for <.*$TO.*>|\\*\\* .*$TO)'" + + # Domain-Filter: Ziel-Domain + [[ -n "$DOMAIN" ]] && filter_pipeline="$filter_pipeline | grep -E '(@$DOMAIN>|to $DOMAIN|for $DOMAIN)'" + + # Message-ID Filter + [[ -n "$MSGID" ]] && filter_pipeline="$filter_pipeline | grep -F '$MSGID'" + + # Zusätzlicher Regex + [[ -n "$GREP" ]] && filter_pipeline="$filter_pipeline | grep -E '$GREP'" + + # Kommando zusammensetzen und ausführen + local full_cmd="$cat_cmd" + [[ -n "$time_filter" ]] && full_cmd="$full_cmd | $time_filter" + full_cmd="$full_cmd$filter_pipeline" + + eval "$full_cmd" >> "$temp_file" 2>/dev/null || true + done + + # Verarbeite gesammelte Logzeilen + process_log_entries "$temp_file" +} + +# Verarbeitung der Logeinträge und Gruppierung nach Message-ID +process_log_entries() { + local input_file="$1" + + [[ -s "$input_file" ]] || { + echo "Keine passenden Logeinträge gefunden." + return 0 } - - if (n==0){ - print "Keine passenden Einträge im gewählten Zeitfenster gefunden." - exit 0 + + # AWK-Script für Gruppierung und Status-Klassifikation + awk ' + BEGIN { + # Arrays für Message-ID Daten + # msgid_data[id]["first"] = erste Zeitstempel + # msgid_data[id]["last"] = letzte Zeitstempel + # msgid_data[id]["from"] = Absender + # msgid_data[id]["rcpts"] = Empfänger (getrennt durch ",") + # msgid_data[id]["delivered"] = Anzahl erfolgreich zugestellt + # msgid_data[id]["deferred"] = Anzahl wartend + # msgid_data[id]["failed"] = Anzahl fehlgeschlagen + # msgid_data[id]["logs"] = Alle Logzeilen (getrennt durch "\n") } - - 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 + + # Extrahiere Message-ID aus jeder Zeile + function extract_msgid(line) { + # Exim Message-ID Format: 1uwRvz-000Fz2-CI + match(line, /[0-9a-zA-Z]{6}-[0-9a-zA-Z]{6}-[0-9a-zA-Z]{2}/) + if (RSTART > 0) { + return substr(line, RSTART, RLENGTH) } - - 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) + return "" + } + + # Extrahiere Zeitstempel (Format: 2025-09-11 08:32:15) + function extract_timestamp(line) { + match(line, /^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/) + if (RSTART > 0) { + return substr(line, RSTART, RLENGTH) + } + return "" + } + + # Hauptverarbeitung jeder Logzeile + { + msgid = extract_msgid($0) + if (msgid == "") next + + timestamp = extract_timestamp($0) + + # Initialisiere Message-ID falls neu + if (!(msgid in msgid_data)) { + msgid_data[msgid]["first"] = timestamp + msgid_data[msgid]["last"] = timestamp + msgid_data[msgid]["from"] = "" + msgid_data[msgid]["rcpts"] = "" + msgid_data[msgid]["delivered"] = 0 + msgid_data[msgid]["deferred"] = 0 + msgid_data[msgid]["failed"] = 0 + msgid_data[msgid]["logs"] = "" + } + + # Aktualisiere Zeitstempel + if (timestamp < msgid_data[msgid]["first"] || msgid_data[msgid]["first"] == "") { + msgid_data[msgid]["first"] = timestamp + } + if (timestamp > msgid_data[msgid]["last"] || msgid_data[msgid]["last"] == "") { + msgid_data[msgid]["last"] = timestamp + } + + # Füge Logzeile hinzu + if (msgid_data[msgid]["logs"] == "") { + msgid_data[msgid]["logs"] = timestamp " | " $0 + } else { + msgid_data[msgid]["logs"] = msgid_data[msgid]["logs"] "\n" timestamp " | " $0 + } + + # Analysiere Logzeile für Status und Details + if ($0 ~ / <= /) { + # Eingehende Mail: Extract Sender + match($0, / <= ([^ ]+)/, arr) + if (arr[1] != "") { + msgid_data[msgid]["from"] = arr[1] } } - 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" + else if ($0 ~ / => /) { + # Erfolgreiche Zustellung + msgid_data[msgid]["delivered"]++ + + # Extrahiere Empfänger + match($0, / => ([^ ]+)/, arr) + if (arr[1] != "" && index(msgid_data[msgid]["rcpts"], arr[1]) == 0) { + if (msgid_data[msgid]["rcpts"] == "") { + msgid_data[msgid]["rcpts"] = arr[1] + } else { + msgid_data[msgid]["rcpts"] = msgid_data[msgid]["rcpts"] "," arr[1] + } + } + } + else if ($0 ~ / == /) { + # Verzögerte Zustellung (deferred) + msgid_data[msgid]["deferred"]++ + + # Extrahiere Empfänger + match($0, / == ([^ ]+)/, arr) + if (arr[1] != "" && index(msgid_data[msgid]["rcpts"], arr[1]) == 0) { + if (msgid_data[msgid]["rcpts"] == "") { + msgid_data[msgid]["rcpts"] = arr[1] + } else { + msgid_data[msgid]["rcpts"] = msgid_data[msgid]["rcpts"] "," arr[1] + } + } + } + else if ($0 ~ / \*\* /) { + # Fehlgeschlagene Zustellung + msgid_data[msgid]["failed"]++ + + # Extrahiere Empfänger + match($0, / \*\* ([^ ]+)/, arr) + if (arr[1] != "" && index(msgid_data[msgid]["rcpts"], arr[1]) == 0) { + if (msgid_data[msgid]["rcpts"] == "") { + msgid_data[msgid]["rcpts"] = arr[1] + } else { + msgid_data[msgid]["rcpts"] = msgid_data[msgid]["rcpts"] "," arr[1] + } + } } - - # 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 } + + END { + # Sortiere Message-IDs nach erstem Auftreten + n = 0 + for (msgid in msgid_data) { + sorted_msgids[++n] = msgid + } + + # Einfache Sortierung nach first timestamp + for (i = 1; i <= n; i++) { + for (j = i + 1; j <= n; j++) { + if (msgid_data[sorted_msgids[i]]["first"] > msgid_data[sorted_msgids[j]]["first"]) { + temp = sorted_msgids[i] + sorted_msgids[i] = sorted_msgids[j] + sorted_msgids[j] = temp + } + } + } + + # Ausgabe aller gefundenen Message-IDs + for (i = 1; i <= n; i++) { + msgid = sorted_msgids[i] + + # Status-Klassifikation + delivered = msgid_data[msgid]["delivered"] + deferred = msgid_data[msgid]["deferred"] + failed = msgid_data[msgid]["failed"] + + if (failed > 0 && delivered == 0 && deferred == 0) { + status = "FAIL" + } else if (delivered > 0 && failed == 0 && deferred == 0) { + status = "OK" + } else if (deferred > 0 && delivered == 0 && failed == 0) { + status = "DELAY" + } else if (delivered > 0 && (failed > 0 || deferred > 0)) { + status = "PARTIAL" + } else { + status = "UNKNOWN" + } + + # Kurze Übersicht + printf "[%s] ID=%s | First=%s | From=%s | Rcpts=%s | Delivered=%d Deferred=%d Failed=%d\n", + status, msgid, msgid_data[msgid]["first"], msgid_data[msgid]["from"], + msgid_data[msgid]["rcpts"], delivered, deferred, failed + + # Detaillierte Ausgabe + print "------------------------------------------------------------------------" + printf "ID: %s\n", msgid + printf "First: %s\n", msgid_data[msgid]["first"] + printf "Last: %s\n", msgid_data[msgid]["last"] + printf "From: %s\n", msgid_data[msgid]["from"] + printf "Rcpts: %s\n", msgid_data[msgid]["rcpts"] + printf "Status: %s (Delivered=%d, Deferred=%d, Failed=%d)\n", status, delivered, deferred, failed + print "Log:" + print msgid_data[msgid]["logs"] + + # Check für bekannte Delivery-Probleme + if (msgid_data[msgid]["logs"] ~ /all hosts for.*have been failing/) { + print " Note: DELIVERY STALL erkannt: '\''all hosts for domain failing (retry/backoff)'\''" + } + + print "------------------------------------------------------------------------" + print "" + } + + if (n == 0) { + print "Keine passenden Exim-Message-IDs gefunden." + } + } + ' "$input_file" } -' + +# Main Execution +main() { + analyze_logs +} + +# Script ausführen +main "$@" # Built with the help of Claude Code \ No newline at end of file diff --git a/legacy/go/go.sh b/legacy/go/go.sh old mode 100644 new mode 100755