Compare commits

...
16 Commits
Author SHA1 Message Date
ljp 1befa79366 Merge pull request 'feature/deploy-system' (#2) from feature/deploy-system into main
Reviewed-on: https://vps.dominikus.one:3443/ljp/esn-support-bash-snippets/pulls/2
2025-09-11 17:56:20 +02:00
Philipp Ljubeko 6c0a9749d0 Merge branch 'main' of https://vps.dominikus.one:3443/ljp/esn-support-bash-snippets into feature/deploy-system 2025-09-11 17:38:37 +02:00
Philipp Ljubeko f1f7babfd7 Merge branch 'feature/exitrace-modular' of https://vps.dominikus.one:3443/ljp/esn-support-bash-snippets into feature/deploy-system 2025-09-11 17:09:20 +02:00
h0tznpl0tzandClaude 06647a1bf1 Improve deployment organization and bashrc structure
Script Organization:
- Deploy scripts to ~/scripts/ instead of ~/ directly
- Create proper symlinks ~/bin/script -> ~/scripts/script
- Libraries go to ~/scripts/lib_scriptname/ for better organization

Bashrc Improvements:
- Add modular .bashrc with .bash_env, .bash_aliases, .bash_paths
- Include HISTTIMEFORMAT and enhanced history settings
- Smart PATH detection - only adds ~/bin if not already present
- Proper bash completion and dircolors setup
- Maintains all original functionality from complex version

Deployment now creates clean, organized structure on target servers.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 16:25:37 +02:00
h0tznpl0tzandClaude 034018de87 Fix syntax error in select statement
Remove invalid stderr redirection from select statement.
Select cannot be redirected with >&2 syntax.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 16:21:53 +02:00
h0tznpl0tzandClaude 012d3158a0 Fix SSH target parsing bug in deploy.sh
- Redirect select UI output to stderr to prevent multiline capture
- Only SSH target goes to stdout for clean variable assignment
- Fixes 'invalid user name' error from corrupted ssh_target variable
- SSH connection now works properly like manual ssh command

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 16:19:28 +02:00
h0tznpl0tzandClaude 63121c71f8 Simplify deploy.sh following KISS principle
- Reduce from 419 to 230 lines (45% reduction)
- Replace complex find-based script detection with simple for loop
- Use bash select builtin instead of manual state management
- Consolidate server selection into single function
- Simplify bashrc deployment with direct PATH setup
- Remove unnecessary abstractions while preserving all features

Features preserved:
- Auto-detection of scripts via ~/bin symlinks
- Interactive script and server selection
- Support for web N, mx N, logger, support servers
- Bashrc deployment with PATH configuration
- Lib dependencies handling
- Dry-run functionality
- Symlink creation in ~/bin

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 15:48:14 +02:00
h0tznpl0tzandClaude 248b2f9c1a Simplify exitrace.sh following KISS principle
- Eliminate 345 lines of duplicated HERE-document code
- Replace complex remote execution with local deployment approach
- Remove AWK array context errors from shell escaping
- Preserve all features: -FROM/-TO/-DOMAIN/-MSGID/-GREP, time filters, MX records, status classification
- Maintain backwards compatibility with -f/-t/-D/-m/-G parameters
- Use deploy.sh for installation on logger.in.nessus.at

Reduces complexity while maintaining full functionality.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 15:46:04 +02:00
h0tznpl0tzandClaude fb66a3a014 Add interactive deployment system for easyname server infrastructure
- Auto-detects scripts via symlinks in ~/bin
- Interactive script and server selection
- Supports bashrc deployment
- Handles lib/ dependencies
- Includes security improvements

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 15:14:57 +02:00
h0tznpl0tzandClaude 9241cc3d14 Improve exitrace parameter naming for better usability
- Add descriptive parameter names: -FROM, -TO, -DOMAIN, -MSGID, -GREP
- Maintain backwards compatibility with old short forms (-f, -t, -D, -m, -G)
- Update README with new examples and backwards compatibility section
- More intuitive and self-documenting command line interface

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 15:06:40 +02:00
h0tznpl0tzandClaude 0774ed0729 Fix AWK array initialization error
- Initialize rcpts array properly to avoid scalar/array error
- Skip empty recipient strings in output formatting
- Ensures proper 2D array handling in AWK

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 15:02:08 +02:00
h0tznpl0tzandClaude aa6a470b0d Fix AWK regex syntax error in exitrace
- Fix unterminated regexp in deferred detection
- Simplify regex pattern for 'all hosts failing' detection
- Removes problematic single quote escaping in AWK

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 15:00:48 +02:00
h0tznpl0tzandClaude 7d5d4cb7b1 Refactor exitrace to remote execution model
- 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 <noreply@anthropic.com>
2025-09-11 14:59:00 +02:00
h0tznpl0tzandClaude 124337e0a4 Fix symlink resolution in exitrace
- Make SCRIPT_DIR robust for symlink execution
- Resolves lib/ dependency loading when called via ~/bin symlink
- Maintains compatibility with direct execution

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 14:29:52 +02:00
h0tznpl0tzandClaude 72537749c5 Fix file permissions for legacy scripts
- Make checkms.sh and go.sh executable
- Required for proper script execution

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 14:16:57 +02:00
h0tznpl0tzandClaude d1e28599c9 Refactor exitrace to modular architecture
- Split monolithic exitrace.sh into focused lib/ modules
- date_utils.sh: Date validation and generation functions
- mx_lookup.sh: MX record lookup functionality
- log_analyzer.awk: Core log analysis logic extracted from main script
- Maintains full backward compatibility with existing interface
- Improves maintainability, testability, and code organization

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 14:04:12 +02:00
5 changed files with 917 additions and 366 deletions
Executable
+258
View File
@@ -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
Regular → Executable
View File
+152 -58
View File
@@ -1,37 +1,57 @@
# exitrace # exitrace
Exim-Log-Trace & Status für einzelne Mails/Adressen/Domain-Ziele: **Remote 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
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 ## Features
- Sucht in `/var/log/easyname/YYYY/MM/YYYY-MM-DD.log(.gz)` (konfigurierbar via `$LOGROOT`) - **Remote-Ausführung**: Automatische SSH-Verbindung zu `logger.in.nessus.at`
- Zeitfenster: letzte N Stunden/Tage ODER konkretes Datum/Datumsbereich - **Zeitfenster**: Letzte N Stunden/Tage ODER konkretes Datum/Datumsbereich
- Filter: Absender, Empfänger (Regex), Domain, Message-ID, zusätzliche Regex - **Filter**: Absender, Empfänger (Regex), Domain, Message-ID, zusätzliche Regex
- Gruppiert alle Treffer pro Exim-Message-ID und fasst Status zusammen - **Gruppierung**: Alle Treffer pro Exim-Message-ID mit Status-Zusammenfassung
- **Status-Klassifikation**: OK / PARTIAL / DELAY / FAIL / UNKNOWN - **Status-Klassifikation**: OK / PARTIAL / DELAY / FAIL / UNKNOWN
- **MX-Records**: Optional DNS-Abfragen für Ziel-Domain (--mx) - **MX-Records**: Optional DNS-Abfragen für Ziel-Domain (--mx)
- Markiert häufige Fehler (z. B. "all hosts for 'domain' have been failing") - **Failure-Detection**: Markiert häufige Fehler (z.B. "all hosts failing")
- Sortierte chronologische Ausgabe pro Message-ID - **Chronologische Ausgabe**: Sortiert nach Message-ID Erstkontakt
- **Komprimierte Logs**: Automatischer Support für .gz-Archive - **Komprimierte Logs**: Automatischer Support für .gz-Archive
- **Smart Connection**: Verwendet `go logger` oder `ssh ${LUSER}@logger.in.nessus.at`
--- ---
## Installation ## Installation
1. Repository klonen oder Skript kopieren: Via Installer (empfohlen):
```bash ```bash
git clone <gitea-url>/esn-support-bash-snippets.git cd ~/.dev/esn-support-bash-snippets
cd esn-support-bash-snippets/legacy/exitrace ./install.sh
chmod +x exitrace.sh # Dann: 3) Legacy Tools auswählen und exitrace installieren
``` ```
2. Abhängigkeiten: Manuelle Installation:
- Standard Unix-Tools (awk, grep, date, zcat) ```bash
- `dig` (optional, für --mx Feature) git clone <repo-url>/esn-support-bash-snippets.git ~/.dev/esn-support-bash-snippets
- Lesezugriff auf `/var/log/easyname/` (oder custom `$LOGROOT`) 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) (durchsucht heutiges Log ohne Filter)
### Beispiele ### Beispiele
**Häufige Use-Cases:**
```bash ```bash
# Mail-Trace einer konkreten Message-ID # Mail-Trace für spezifischen Absender zu t-online.de (letzte 48h)
exitrace -m 1uwRvz-000Fz2-CI exitrace -FROM franziskus@adjuva.info -DOMAIN t-online.de -H 48
# Alle Mails von bestimmtem Absender in den letzten 48 Stunden # Alle Mails an bestimmte Domain in Zeitspanne
exitrace -f sender@example.com -H 48 exitrace -DOMAIN gmx.de -r 2025-09-10..2025-09-11
# Empfänger-Domain in Zeitspanne # Konkrete Message-ID verfolgen
exitrace -D example.de -r 2025-09-09..2025-09-11 exitrace -MSGID 1uwRvz-000Fz2-CI
# Regex-Filter auf Empfänger + konkretes Datum # Regex-Filter auf Empfänger + MX-Records anzeigen
exitrace -t '.*@example\.de' -d 2025-09-10 exitrace -TO '.*@gmail\.com' -d 2025-09-11 --mx
# Kombinierte Filter: Absender + Domain + Zeitfenster + MX-Info # Kombinierte Filter: Absender + Domain + Zeitfenster
exitrace -f sender@example.com -D example.de -H 48 --mx 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 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 -r, --range A..B Datumsbereich (inkl.): YYYY-MM-DD..YYYY-MM-DD
Filter (beliebig kombinierbar): Filter (beliebig kombinierbar):
-f, --from ADDR Envelope-From / "from <...>" -FROM, --from ADDR Envelope-From / "from <...>" (alias: -f)
-t, --to REGEX Empfänger (Regex, matcht nach " for <rcpt>" oder in "** ...") -TO, --to REGEX Empfänger (Regex, matcht nach " for <rcpt>" oder in "** ...") (alias: -t)
-D, --domain NAME Ziel-Domain (z. B. example.de), prüft auf Zustellungen/Fehler dorthin -DOMAIN, --domain NAME Ziel-Domain (z. B. example.de), prüft auf Zustellungen/Fehler dorthin (alias: -D)
-m, --msgid ID Exim-Message-ID (z. B. 1uwRvz-000Fz2-CI) -MSGID, --msgid ID Exim-Message-ID (z. B. 1uwRvz-000Fz2-CI) (alias: -m)
-G, --grep REGEX Zusätzlicher Regex auf Logzeilen -GREP, --grep REGEX Zusätzlicher Regex auf Logzeilen (alias: -G)
Sonstiges: Sonstiges:
--mx Zeigt MX-Einträge (und A/AAAA) für --domain --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 -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 ## 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 ID: 1v2K8X-0004Zm-BC
First: 2025-09-10 14:32:15 First: 2025-09-11 08:32:15
Last: 2025-09-10 14:32:47 Last: 2025-09-11 08:32:47
From: sender@example.com From: franziskus@adjuva.info
Rcpts: recipient@example.de Rcpts: user@t-online.de
Status: OK (Delivered=1, Deferred=0, Failed=0) Status: OK (Delivered=1, Deferred=0, Failed=0)
Log: Log:
2025-09-10 14:32:15 | <= sender@example.com H=mail.example.com [10.0.0.1] P=esmtp S=1234 2025-09-11 08:32:15 | <= franziskus@adjuva.info H=mail.adjuva.info [10.0.0.1] P=esmtp S=2156
2025-09-10 14:32:47 | => recipient@example.de <recipient@example.de> R=dnslookup T=remote_smtp H=mx.example.de [10.0.0.2] 2025-09-11 08:32:47 | => user@t-online.de <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 <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:<blocked@t-online.de>: 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: Nach Installation über `./install.sh` ist das Script direkt verfügbar:
```bash ```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). **LUSER Environment Variable setzen (wichtig!):**
Umgebungsvariable für abweichenden Logpfad:
```bash ```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 ```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)` ### Häufige Probleme
- **Komprimierte Archive**: Automatische Erkennung und Verarbeitung von .gz-Dateien
- **Status-Logik**: Basiert auf SMTP-Response-Codes und typischen Exim-Nachrichten **"LUSER Variable nicht gesetzt"**
- Bei größeren Zeiträumen kann die Verarbeitung länger dauern ```bash
- Regex-Filter (-t, -G) sollten sorgfältig formuliert werden, um false positives zu vermeiden export LUSER="dein-username"
- 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 **"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 Built with the help of Claude Code
+491 -292
View File
@@ -1,28 +1,41 @@
#!/usr/bin/env bash #!/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 # Maintainer: ljp
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Features: # 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 # - 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 # - Gruppiert Treffer pro Exim-Message-ID und fasst Status zusammen
# - Klassifikation: OK / PARTIAL / DELAY / FAIL / UNKNOWN # - 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: # Beispiele:
# exitrace -f sender@example.com -D example.de -H 48 # exitrace -FROM sender@example.com -DOMAIN example.de -H 48
# exitrace -m 1uwRvz-000Fz2-CI # exitrace -MSGID 1uwRvz-000Fz2-CI
# exitrace -t '.*@example.de' -r 2025-09-09..2025-09-11 --mx # exitrace -TO '.*@example.de' -r 2025-09-09..2025-09-11 --mx
# #
# Exitcodes: 0=OK (Treffer evtl. 0), 1=Fehlerhafte Parameter/Umgebung # Exitcodes: 0=OK (Treffer evtl. 0), 1=Fehlerhafte Parameter/Umgebung
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
set -euo pipefail set -euo pipefail
LOGROOT="${LOGROOT:-/var/log/easyname}" # Standardwerte
DATE_FMT="%Y-%m-%d" HOURS=""
TODAY="$(date +%Y-%m-%d)" DAYS=""
DATE=""
RANGE=""
FROM=""
TO=""
DOMAIN=""
MSGID=""
GREP=""
MX=0
DRY_RUN=0
usage() { usage() {
cat <<'USAGE' cat <<'USAGE'
@@ -35,11 +48,11 @@ Zeitfenster (eine Option wählen):
-r, --range A..B Datumsbereich (inkl.): YYYY-MM-DD..YYYY-MM-DD -r, --range A..B Datumsbereich (inkl.): YYYY-MM-DD..YYYY-MM-DD
Filter (beliebig kombinierbar): Filter (beliebig kombinierbar):
-f, --from ADDR Envelope-From / "from <...>" -FROM, --from ADDR Envelope-From / "from <...>" (alias: -f)
-t, --to REGEX Empfänger (Regex, matcht ' for <rcpt>' oder in '** rcpt') -TO, --to REGEX Empfänger (Regex, matcht ' for <rcpt>' oder in '** rcpt') (alias: -t)
-D, --domain NAME Ziel-Domain (z. B. example.de) -DOMAIN, --domain NAME Ziel-Domain (z. B. example.de) (alias: -D)
-m, --msgid ID Exim-Message-ID (z. B. 1uwRvz-000Fz2-CI) -MSGID, --msgid ID Exim-Message-ID (z. B. 1uwRvz-000Fz2-CI) (alias: -m)
-G, --grep REGEX Zusätzlicher Regex auf Logzeilen -GREP, --grep REGEX Zusätzlicher Regex auf Logzeilen (alias: -G)
Sonstiges: Sonstiges:
--mx Zeigt MX-Einträge (und A/AAAA) für --domain --mx Zeigt MX-Einträge (und A/AAAA) für --domain
@@ -47,322 +60,508 @@ Sonstiges:
-h, --help Diese Hilfe -h, --help Diese Hilfe
Beispiele: 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 -m 1uwRvz-000Fz2-CI
exitrace -t '.*@example.de' -r 2025-09-09..2025-09-11 --mx exitrace -t '.*@gmail\\.com' --mx
USAGE USAGE
} }
# --------------------------- Argumente parsen -------------------------------- # Parse Arguments
HOURS=""; DAYS=""; DATE_ONE=""; DATE_RANGE=""
FROM=""; TO_REGEX=""; TARGET_DOMAIN=""; MSGID=""; GREPEXTRA=""
DRYRUN=0; WANT_MX=0
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case $1 in
-H|--hours) HOURS="${2:-}"; shift 2 ;; -H|--hours)
-DAYS|--days) DAYS="${2:-}"; shift 2 ;; HOURS="$2"
-d|--date) DATE_ONE="${2:-}"; shift 2 ;; [[ "$HOURS" =~ ^[0-9]+$ ]] || { echo "Fehler: --hours benötigt eine Zahl" >&2; exit 1; }
-r|--range) DATE_RANGE="${2:-}"; shift 2 ;; shift 2
-f|--from) FROM="${2:-}"; shift 2 ;; ;;
-t|--to) TO_REGEX="${2:-}"; shift 2 ;; -DAYS|--days)
-D|--domain) TARGET_DOMAIN="${2:-}"; shift 2 ;; DAYS="$2"
-m|--msgid) MSGID="${2:-}"; shift 2 ;; [[ "$DAYS" =~ ^[0-9]+$ ]] || { echo "Fehler: --days benötigt eine Zahl" >&2; exit 1; }
-G|--grep) GREPEXTRA="${2:-}"; shift 2 ;; shift 2
--mx) WANT_MX=1; shift ;; ;;
-n|--dry-run) DRYRUN=1; shift ;; -d|--date)
-h|--help) usage; exit 0 ;; DATE="$2"
*) echo "Unbekannte Option: $1" >&2; usage; exit 1 ;; [[ "$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 esac
done done
# ------------------------- Datums-Liste bauen -------------------------------- # Validierung: Nur ein Zeitfenster
files_for_date() { time_options=0
local d="$1" y="${d:0:4}" m="${d:5:2}" [[ -n "$HOURS" ]] && ((time_options++))
local p="${LOGROOT}/${y}/${m}/${d}.log" [[ -n "$DAYS" ]] && ((time_options++))
if [[ -f "$p" ]]; then [[ -n "$DATE" ]] && ((time_options++))
printf "%s\n" "$p" [[ -n "$RANGE" ]] && ((time_options++))
fi
if [[ -f "${p}.gz" ]]; then if [[ $time_options -gt 1 ]]; then
printf "%s\n" "${p}.gz" 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 fi
} }
gen_dates_hours_back() { # Bestimme Logfiles basierend auf Zeitfenster
local hrs="$1" start_ts end_ts cur determine_logfiles() {
end_ts="$(date +%s)" local logfiles=()
start_ts="$(date -d "-${hrs} hours" +%s)"
cur="$(date -d "@$start_ts" +${DATE_FMT})" if [[ -n "$DATE" ]]; then
while :; do validate_date "$DATE"
echo "$cur" local year month
[[ "$(date -d "$cur +1 day" +%s)" -le "$end_ts" ]] || break year=$(date -d "$DATE" +%Y)
cur="$(date -d "$cur +1 day" +${DATE_FMT})" 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 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<DAYS; i++)); do
local date_str
date_str=$(date -d "-$i days" +%Y-%m-%d)
local year month
year=$(date -d "$date_str" +%Y)
month=$(date -d "$date_str" +%m)
logfiles+=("/var/log/easyname/$year/$month/$date_str.log")
done
fi
printf '%s\n' "${logfiles[@]}"
} }
gen_dates_days_back() { # MX Record Lookup
local days="$1" i show_mx_records() {
for ((i=0;i<=days;i++)); do [[ -n "$DOMAIN" ]] || return 0
date -d "-$i day" +${DATE_FMT} [[ $MX -eq 1 ]] || return 0
done
}
gen_dates_range() { if ! command -v dig >/dev/null 2>&1; then
local a="${1%%..*}" b="${1##*..}" cur="$a" echo "Warnung: 'dig' nicht gefunden - MX-Records werden übersprungen" >&2
while :; do return 0
echo "$cur" fi
[[ "$cur" == "$b" ]] && break
cur="$(date -d "$cur +1 day" +${DATE_FMT})"
done
}
declare -a DATES=() echo "== MX für $DOMAIN =="
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=() # MX Records
for d in "${DATES[@]}"; do local mx_records
while read -r f; do FILES+=("$f"); done < <(files_for_date "$d" || true) mx_records=$(dig +short MX "$DOMAIN" | sort -n)
done
if (( DRYRUN )); then if [[ -z "$mx_records" ]]; then
echo "Würde durchsuchen:" echo "Keine MX-Records gefunden"
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 else
cat -- "$f" echo "$mx_records"
fi
done
}
grep_chain() { # A/AAAA Records für MX-Hosts
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
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:" echo "-- A/AAAA der MX:"
while read -r mx; do while read -r priority mx_host; do
host="${mx#* }" [[ -n "$mx_host" ]] || continue
printf "%-35s | " "$host" mx_host="${mx_host%.}" # Trailing dot entfernen
dig +short A "$host" | paste -sd, - || true
printf "%-35s | " "" local a_records aaaa_records all_records
dig +short AAAA "$host" | paste -sd, - || true a_records=$(dig +short A "$mx_host" | paste -sd',' -)
done < <(dig +short MX "$TARGET_DOMAIN" | sort -n) aaaa_records=$(dig +short AAAA "$mx_host" | paste -sd',' -)
echo "========================================================================"
else all_records=""
echo "(Hinweis) dig nicht gefunden, MX-Ausgabe übersprungen." [[ -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:-<keine A/AAAA Records>}"
done <<< "$mx_records"
fi fi
fi
# ---------------------------- Auswertung (awk) -------------------------------- echo "========================================================================"
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); gsub(/>.*/, "", 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); gsub(/>/, "", 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 { # Log Analysis - der Hauptalgorithmus
# IDs einsammeln & sortieren analyze_logs() {
n=0 local logfiles=()
for (id in first_ts){ ids[++n]=id } mapfile -t logfiles < <(determine_logfiles)
for (i=2;i<=n;i++){
key=ids[i]; j=i-1 if [[ $DRY_RUN -eq 1 ]]; then
while (j>=1 && first_ts[ids[j]] > first_ts[key]) { ids[j+1]=ids[j]; j-- } echo "=== DRY-RUN: Würde folgende Logfiles untersuchen ==="
ids[j+1]=key 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){ # AWK-Script für Gruppierung und Status-Klassifikation
print "Keine passenden Einträge im gewählten Zeitfenster gefunden." awk '
exit 0 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="------------------------------------------------------------------------" # Extrahiere Message-ID aus jeder Zeile
for (k=1;k<=n;k++){ function extract_msgid(line) {
id=ids[k] # Exim Message-ID Format: 1uwRvz-000Fz2-CI
match(line, /[0-9a-zA-Z]{6}-[0-9a-zA-Z]{6}-[0-9a-zA-Z]{2}/)
# FROM Nachfilter if (RSTART > 0) {
if (FROMREQ != "" && from_mismatch[id]==1) { return substr(line, RSTART, RLENGTH)
if (from_addr[id] != FROMREQ && from_addr[id] != "") continue }
return ""
} }
d=(id in deliveries ? deliveries[id] : 0) # Extrahiere Zeitstempel (Format: 2025-09-11 08:32:15)
e=(id in defers ? defers[id] : 0) function extract_timestamp(line) {
f=(id in fails ? fails[id] : 0) 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 ""
}
# Status-Logik: # Hauptverarbeitung jeder Logzeile
# - d>0 && f==0 && e==0 -> OK {
# - d>0 && (f>0 || e>0) -> PARTIAL msgid = extract_msgid($0)
# - d==0 && f>0 -> FAIL if (msgid == "") next
# - 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 timestamp = extract_timestamp($0)
# Rcpts sammeln
out_rcpts="" # Initialisiere Message-ID falls neu
if (id in rcpts){ if (!(msgid in msgid_data)) {
for (r in rcpts[id]) { msgid_data[msgid]["first"] = timestamp
out_rcpts = (out_rcpts=="" ? r : out_rcpts ", " r) 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]
} }
} }
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]
}
}
}
}
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", 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 status, msgid, msgid_data[msgid]["first"], msgid_data[msgid]["from"],
msgid_data[msgid]["rcpts"], delivered, deferred, failed
# Marker-Hinweis # Detaillierte Ausgabe
if (fail_marker[id]) { print "------------------------------------------------------------------------"
printf " Note: DELIVERY STALL erkannt: '\''all hosts for domain failing (retry/backoff)'\''\n" printf "ID: %s\n", msgid
} printf "First: %s\n", msgid_data[msgid]["first"]
printf "Last: %s\n", msgid_data[msgid]["last"]
# Detailblock printf "From: %s\n", msgid_data[msgid]["from"]
print sep printf "Rcpts: %s\n", msgid_data[msgid]["rcpts"]
printf "ID: %s\n", id printf "Status: %s (Delivered=%d, Deferred=%d, Failed=%d)\n", status, delivered, deferred, failed
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 "Log:"
print status[id] print msgid_data[msgid]["logs"]
print sep
# 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 # Built with the help of Claude Code
Regular → Executable
View File