Compare commits

..
5 Commits
Author SHA1 Message Date
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 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
ljp ad6666d901 Merge pull request 'Added bashprofile for eign01' (#1) from peignerpausz/eign01 into main
Reviewed-on: https://vps.dominikus.one:3443/ljp/esn-support-bash-snippets/pulls/1
2025-09-11 15:08:08 +02:00
Patrick Eigner-Pausz 5877e71982 Added bashprofile for eign01 2025-09-11 14:23:30 +02:00
5 changed files with 610 additions and 437 deletions
+26
View File
@@ -0,0 +1,26 @@
### CUSTOM ALIASES BASH FILE ###
# Schutz vor versehentlichem Überschreiben
alias cp='cp -i'
alias mv='mv -i'
alias rm='rm -i'
# Navigation
alias ..='cd ..'
alias ...='cd ../..'
# Komfort
alias grep='grep --color=auto'
alias df='df -h'
alias free='free -m'
# Netzwerk-Shortcuts
alias ip4='ip -4 -brief addr'
alias ip6='ip -6 -brief addr'
alias myip='curl -s https://ifconfig.me || true'
# LS Aliase
alias ls='ls --color=auto'
alias ll='ls -alF'
alias la='ls -A'
alias l='ls -CF'
+14
View File
@@ -0,0 +1,14 @@
### CUSTOM ENV BASH FILE ###
# Standard-User für interne Tools
export LUSER="${LUSER:-peignerpausz}"
export WHUSER="${WHUSER:-peignerpausz}"
# sinnvolle Defaults
export EDITOR="${EDITOR:-vi}"
export PAGER="${PAGER:-less}"
export LESSHISTFILE=-
export GPG_TTY=$(tty 2>/dev/null || true)
# LS Optionen (wird von ls-Alias genutzt)
export LS_OPTIONS='--color=auto -h'
+18
View File
@@ -0,0 +1,18 @@
### CUSTOM PATH BASH FILE ###
# ~/bin und ~/.local/bin vorne anhängen, falls vorhanden
if [ -d "$HOME/bin" ]; then
case ":$PATH:" in
*":$HOME/bin:"*) : ;;
*) PATH="$HOME/bin:$PATH" ;;
esac
fi
if [ -d "$HOME/.local/bin" ]; then
case ":$PATH:" in
*":$HOME/.local/bin:"*) : ;;
*) PATH="$HOME/.local/bin:$PATH" ;;
esac
fi
export PATH
+66
View File
@@ -0,0 +1,66 @@
# CLEANED UPDATED BASHRC
# ~/.bashrc — interactive non-login shells
# Exit early if non-interactive
case $- in
*i*) ;;
*) return;;
esac
##### History #####
HISTCONTROL=ignoreboth:erasedups
shopt -s histappend
HISTSIZE=99999999
HISTFILESIZE=99999999
HISTTIMEFORMAT="%Y-%m-%d %T "
PROMPT_COMMAND='history -a; history -n'
shopt -s cmdhist
shopt -s lithist
shopt -s checkwinsize
##### Debian chroot prompt helper #####
if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then
debian_chroot=$(cat /etc/debian_chroot)
fi
##### Prompt #####
if [ -x /usr/bin/tput ] && tput setaf 1 >/dev/null 2>&1; then
color_prompt=yes
else
color_prompt=
fi
if [ "$color_prompt" = yes ]; then
PS1='\[\033[01;31m\]\u\[\033[01;33m\]@\[\033[01;36m\]\h \[\033[01;33m\]\w \[\033[01;35m\]\$ \[\033[00m\]'
else
PS1='\u@\h \w \$ '
fi
case "$TERM" in
xterm*|rxvt*)
PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@\h: \w\a\]$PS1"
;;
esac
##### LS-Farben & dircolors #####
if [ -x /usr/bin/dircolors ]; then
if [ -r "$HOME/.dircolors" ]; then
eval "$(dircolors -b "$HOME/.dircolors")"
else
eval "$(dircolors -b)"
fi
fi
##### Bash-Completion #####
if ! shopt -oq posix; then
if [ -f /usr/share/bash-completion/bash_completion ]; then
. /usr/share/bash-completion/bash_completion
elif [ -f /etc/bash_completion ]; then
. /etc/bash_completion
fi
fi
##### Ausgelagerte Dateien #####
[ -f "$HOME/.bash_aliases" ] && . "$HOME/.bash_aliases"
[ -f "$HOME/.bash_env" ] && . "$HOME/.bash_env"
[ -f "$HOME/.bash_paths" ] && . "$HOME/.bash_paths"
+456 -407
View File
@@ -1,27 +1,42 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# exitrace - Remote 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
# Führt Exim-Log-Analyse remote auf logger.in.nessus.at aus
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Features: # Features:
# - Remote-Ausführung auf logger.in.nessus.at (wo die Exim-Logs liegen) # - 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) auf logger # - 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
# Standardwerte
HOURS=""
DAYS=""
DATE=""
RANGE=""
FROM=""
TO=""
DOMAIN=""
MSGID=""
GREP=""
MX=0
DRY_RUN=0
usage() { usage() {
cat <<'USAGE' cat <<'USAGE'
Usage: exitrace [OPTS] Usage: exitrace [OPTS]
@@ -33,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):
-FROM, --from ADDR Envelope-From / "from <...>" -FROM, --from ADDR Envelope-From / "from <...>" (alias: -f)
-TO, --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)
-DOMAIN, --domain NAME Ziel-Domain (z. B. example.de) -DOMAIN, --domain NAME Ziel-Domain (z. B. example.de) (alias: -D)
-MSGID, --msgid ID Exim-Message-ID (z. B. 1uwRvz-000Fz2-CI) -MSGID, --msgid ID Exim-Message-ID (z. B. 1uwRvz-000Fz2-CI) (alias: -m)
-GREP, --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
@@ -49,470 +64,504 @@ Beispiele:
exitrace -MSGID 1uwRvz-000Fz2-CI exitrace -MSGID 1uwRvz-000Fz2-CI
exitrace -TO '.*@example.de' -r 2025-09-09..2025-09-11 --mx exitrace -TO '.*@example.de' -r 2025-09-09..2025-09-11 --mx
Note: Wird automatisch remote auf logger.in.nessus.at ausgeführt. Backwards Compatibility:
exitrace -f sender@domain.com -D example.de -H 48
exitrace -m 1uwRvz-000Fz2-CI
exitrace -t '.*@gmail\\.com' --mx
USAGE USAGE
} }
# Validate functions (inline for remote execution) # Parse Arguments
validate_positive_integer() {
local value="$1"
local param="$2"
if [[ ! "$value" =~ ^[1-9][0-9]*$ ]]; then
echo "Fehler: $param benötigt positive Ganzzahl, erhalten: $value" >&2
exit 1
fi
}
validate_date() {
local date_str="$1"
local param="$2"
if ! date -d "$date_str" >/dev/null 2>&1; then
echo "Fehler: $param ungültiges Datum: $date_str" >&2
exit 1
fi
if [[ ! "$date_str" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "Fehler: $param Format muss YYYY-MM-DD sein, erhalten: $date_str" >&2
exit 1
fi
}
validate_date_range() {
local range="$1"
if [[ ! "$range" =~ ^([0-9]{4}-[0-9]{2}-[0-9]{2})\.\.([0-9]{4}-[0-9]{2}-[0-9]{2})$ ]]; then
echo "Fehler: --range Format muss YYYY-MM-DD..YYYY-MM-DD sein, erhalten: $range" >&2
exit 1
fi
local start_date="${BASH_REMATCH[1]}"
local end_date="${BASH_REMATCH[2]}"
validate_date "$start_date" "--range start"
validate_date "$end_date" "--range end"
}
# --------------------------- Argumente parsen --------------------------------
HOURS=""; DAYS=""; DATE_ONE=""; DATE_RANGE=""
FROM=""; TO_REGEX=""; TARGET_DOMAIN=""; MSGID=""; GREPEXTRA=""
DRYRUN=0; WANT_MX=0
# Alle Argumente für Remote-Weiterleitung sammeln
REMOTE_ARGS=()
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case $1 in
-H|--hours) -H|--hours)
HOURS="${2:-}" HOURS="$2"
validate_positive_integer "$HOURS" "--hours" [[ "$HOURS" =~ ^[0-9]+$ ]] || { echo "Fehler: --hours benötigt eine Zahl" >&2; exit 1; }
REMOTE_ARGS+=("$1" "$2") shift 2
shift 2 ;; ;;
-DAYS|--days) -DAYS|--days)
DAYS="${2:-}" DAYS="$2"
validate_positive_integer "$DAYS" "--days" [[ "$DAYS" =~ ^[0-9]+$ ]] || { echo "Fehler: --days benötigt eine Zahl" >&2; exit 1; }
REMOTE_ARGS+=("$1" "$2") shift 2
shift 2 ;; ;;
-d|--date) -d|--date)
DATE_ONE="${2:-}" DATE="$2"
validate_date "$DATE_ONE" "--date" [[ "$DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || { echo "Fehler: --date benötigt YYYY-MM-DD" >&2; exit 1; }
REMOTE_ARGS+=("$1" "$2") shift 2
shift 2 ;; ;;
-r|--range) -r|--range)
DATE_RANGE="${2:-}" RANGE="$2"
validate_date_range "$DATE_RANGE" [[ "$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; }
REMOTE_ARGS+=("$1" "$2") shift 2
shift 2 ;; ;;
-FROM|--from|-f) -FROM|--from|-f)
FROM="${2:-}" FROM="$2"
REMOTE_ARGS+=("$1" "$2") shift 2
shift 2 ;; ;;
-TO|--to|-t) -TO|--to|-t)
TO_REGEX="${2:-}" TO="$2"
REMOTE_ARGS+=("$1" "$2") shift 2
shift 2 ;; ;;
-DOMAIN|--domain|-D) -DOMAIN|--domain|-D)
TARGET_DOMAIN="${2:-}" DOMAIN="$2"
REMOTE_ARGS+=("$1" "$2") shift 2
shift 2 ;; ;;
-MSGID|--msgid|-m) -MSGID|--msgid|-m)
MSGID="${2:-}" MSGID="$2"
REMOTE_ARGS+=("$1" "$2") shift 2
shift 2 ;; ;;
-GREP|--grep|-G) -GREP|--grep|-G)
GREPEXTRA="${2:-}" GREP="$2"
REMOTE_ARGS+=("$1" "$2") shift 2
shift 2 ;; ;;
--mx) --mx)
WANT_MX=1 MX=1
REMOTE_ARGS+=("$1") shift
shift ;; ;;
-n|--dry-run) -n|--dry-run)
DRYRUN=1 DRY_RUN=1
REMOTE_ARGS+=("$1") shift
shift ;; ;;
-h|--help) -h|--help)
usage; exit 0 ;; usage
exit 0
;;
*) *)
echo "Unbekannte Option: $1" >&2 echo "Unbekannte Option: $1" >&2
usage usage
exit 1 ;; exit 1
;;
esac esac
done done
# --- Remote-Kommando (wie checkms) --- # Validierung: Nur ein Zeitfenster
LUSER="${LUSER:-}" time_options=0
if [[ -z "$LUSER" ]] && type go >/dev/null 2>&1; then [[ -n "$HOURS" ]] && ((time_options++))
REMOTE="go logger" [[ -n "$DAYS" ]] && ((time_options++))
elif [[ -n "$LUSER" ]]; then [[ -n "$DATE" ]] && ((time_options++))
REMOTE="ssh ${LUSER}@logger.in.nessus.at" [[ -n "$RANGE" ]] && ((time_options++))
else
echo "Fehler: LUSER Variable nicht gesetzt und 'go' Tool nicht verfügbar" >&2 if [[ $time_options -gt 1 ]]; then
echo "Setze LUSER oder installiere das 'go' Tool für automatische Logger-Verbindung" >&2 echo "Fehler: Nur ein Zeitfenster (-H/-DAYS/-d/-r) erlaubt" >&2
exit 1 exit 1
fi fi
# --- Remote Ausführung --- # Default: Heutiges Datum
$REMOTE bash -s "${REMOTE_ARGS[@]}" <<'REMOTE_SCRIPT' if [[ $time_options -eq 0 ]]; then
#!/usr/bin/env bash DATE=$(date +%Y-%m-%d)
# Remote exitrace execution on logger.in.nessus.at fi
set -euo pipefail
# Date/validation functions
validate_positive_integer() {
local value="$1"
local param="$2"
if [[ ! "$value" =~ ^[1-9][0-9]*$ ]]; then
echo "Fehler: $param benötigt positive Ganzzahl, erhalten: $value" >&2
exit 1
fi
}
# Datum-Validierung für DATE
validate_date() { validate_date() {
local date_str="$1" local d="$1"
local param="$2" if ! date -d "$d" >/dev/null 2>&1; then
if ! date -d "$date_str" >/dev/null 2>&1; then echo "Fehler: Ungültiges Datum: $d" >&2
echo "Fehler: $param ungültiges Datum: $date_str" >&2
exit 1
fi
if [[ ! "$date_str" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "Fehler: $param Format muss YYYY-MM-DD sein, erhalten: $date_str" >&2
exit 1 exit 1
fi fi
} }
validate_date_range() { # Bestimme Logfiles basierend auf Zeitfenster
local range="$1" determine_logfiles() {
if [[ ! "$range" =~ ^([0-9]{4}-[0-9]{2}-[0-9]{2})\.\.([0-9]{4}-[0-9]{2}-[0-9]{2})$ ]]; then local logfiles=()
echo "Fehler: --range Format muss YYYY-MM-DD..YYYY-MM-DD sein, erhalten: $range" >&2
exit 1 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<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 fi
local start_date="${BASH_REMATCH[1]}"
local end_date="${BASH_REMATCH[2]}" printf '%s\n' "${logfiles[@]}"
validate_date "$start_date" "--range start"
validate_date "$end_date" "--range end"
} }
# Date generation functions # MX Record Lookup
gen_dates_hours_back() {
local hrs="$1" start_ts end_ts cur
local DATE_FMT="%Y-%m-%d"
end_ts="$(date +%s)"
start_ts="$(date -d "-${hrs} hours" +%s)"
cur="$(date -d "@$start_ts" +${DATE_FMT})"
while :; do
echo "$cur"
[[ "$(date -d "$cur +1 day" +%s)" -le "$end_ts" ]] || break
cur="$(date -d "$cur +1 day" +${DATE_FMT})"
done
}
gen_dates_days_back() {
local days="$1" i
for ((i=0;i<=days;i++)); do
date -d "-$i day" +%Y-%m-%d
done
}
gen_dates_range() {
local a="${1%%..*}" b="${1##*..}" cur="$a"
while :; do
echo "$cur"
[[ "$cur" == "$b" ]] && break
cur="$(date -d "$cur +1 day" +%Y-%m-%d)"
done
}
files_for_date() {
local d="$1" y="${d:0:4}" m="${d:5:2}"
local p="/var/log/easyname/${y}/${m}/${d}.log"
if [[ -f "$p" ]]; then
printf "%s\n" "$p"
fi
if [[ -f "${p}.gz" ]]; then
printf "%s\n" "${p}.gz"
fi
}
# MX lookup function
show_mx_records() { show_mx_records() {
local domain="$1" [[ -n "$DOMAIN" ]] || return 0
if command -v dig >/dev/null 2>&1; then [[ $MX -eq 1 ]] || return 0
echo "== MX für ${domain} =="
dig +short MX "$domain" | sort -n || true 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
echo "$mx_records"
# A/AAAA Records für MX-Hosts
echo "-- A/AAAA der MX:" echo "-- A/AAAA der MX:"
while read -r mx; do while read -r priority mx_host; do
local 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 "$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
echo "========================================================================"
} }
# Global settings # Log Analysis - der Hauptalgorithmus
LOGROOT="${LOGROOT:-/var/log/easyname}" analyze_logs() {
TODAY="$(date +%Y-%m-%d)" local logfiles=()
mapfile -t logfiles < <(determine_logfiles)
# Parse remote arguments if [[ $DRY_RUN -eq 1 ]]; then
HOURS=""; DAYS=""; DATE_ONE=""; DATE_RANGE="" echo "=== DRY-RUN: Würde folgende Logfiles untersuchen ==="
FROM=""; TO_REGEX=""; TARGET_DOMAIN=""; MSGID=""; GREPEXTRA="" for logfile in "${logfiles[@]}"; do
DRYRUN=0; WANT_MX=0 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
while [[ $# -gt 0 ]]; do # MX Records anzeigen falls gewünscht
case "$1" in show_mx_records
-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 ;;
-FROM|--from|-f) FROM="${2:-}"; shift 2 ;;
-TO|--to|-t) TO_REGEX="${2:-}"; shift 2 ;;
-DOMAIN|--domain|-D) TARGET_DOMAIN="${2:-}"; shift 2 ;;
-MSGID|--msgid|-m) MSGID="${2:-}"; shift 2 ;;
-GREP|--grep|-G) GREPEXTRA="${2:-}"; shift 2 ;;
--mx) WANT_MX=1; shift ;;
-n|--dry-run) DRYRUN=1; shift ;;
*) shift ;;
esac
done
# Generate date list # Sammle alle relevanten Logzeilen
declare -a DATES=() local temp_file
if [[ -n "$HOURS" ]]; then temp_file=$(mktemp)
mapfile -t DATES < <(gen_dates_hours_back "$HOURS") trap "rm -f '$temp_file'" EXIT
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
# Collect files for logfile in "${logfiles[@]}"; do
declare -a FILES=() local cat_cmd=""
for d in "${DATES[@]}"; do
while read -r f; do FILES+=("$f"); done < <(files_for_date "$d" || true)
done
if (( DRYRUN )); then if [[ -f "$logfile" ]]; then
echo "Würde durchsuchen:" cat_cmd="cat '$logfile'"
printf ' %s\n' "${FILES[@]}" elif [[ -f "$logfile.gz" ]]; then
exit 0 cat_cmd="zcat '$logfile.gz'"
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
# Build grep filters
declare -a PREF_GREP=()
[[ -n "$MSGID" ]] && PREF_GREP+=( "$MSGID" )
[[ -n "$FROM" ]] && PREF_GREP+=( "$FROM" )
[[ -n "$TARGET_DOMAIN" ]] && PREF_GREP+=( "$TARGET_DOMAIN" )
[[ -n "$GREPEXTRA" ]] && PREF_GREP+=( "$GREPEXTRA" )
# File reader
cat_files() {
local f
for f in "${FILES[@]}"; do
if [[ "$f" == *.gz ]]; then
zcat -- "$f"
else else
cat -- "$f" continue
fi 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 done
# Verarbeite gesammelte Logzeilen
process_log_entries "$temp_file"
} }
# Grep chain # Verarbeitung der Logeinträge und Gruppierung nach Message-ID
grep_chain() { process_log_entries() {
if [[ "${#PREF_GREP[@]}" -eq 0 ]]; then local input_file="$1"
cat_files
else
cat_files | grep -F -e "${PREF_GREP[0]}" $(for s in "${PREF_GREP[@]:1}"; do printf ' -e %q' "$s"; done)
fi
}
# MX Block [[ -s "$input_file" ]] || {
if (( WANT_MX )) && [[ -n "$TARGET_DOMAIN" ]]; then echo "Keine passenden Logeinträge gefunden."
show_mx_records "$TARGET_DOMAIN" return 0
fi }
# AWK Analysis # AWK-Script für Gruppierung und Status-Klassifikation
grep_chain | awk -v TO_RE="${TO_REGEX:-}" -v WANTDOM="${TARGET_DOMAIN:-}" -v MSGIDREQ="${MSGID:-}" -v FROMREQ="${FROM:-}" -v EXTRA="${GREPEXTRA:-}" ' awk '
function trim(s){ sub(/^[ \t\r\n]+/, "", s); sub(/[ \t\r\n]+$/, "", s); return s } BEGIN {
function add_rcpt(id, rc){ # Arrays für Message-ID Daten
if (!(id in rcpts)) rcpts[id][""] = "" # msgid_data[id]["first"] = erste Zeitstempel
if (!(rc in rcpts[id])) rcpts[id][rc]=1 # 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")
}
BEGIN { FS=" " } # 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)
}
return ""
}
{ # Extrahiere Zeitstempel (Format: 2025-09-11 08:32:15)
raw=$0 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 ""
}
# Zeit + ID detektieren # Hauptverarbeitung jeder Logzeile
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 msgid = extract_msgid($0)
split(t[0], p, " ") if (msgid == "") next
ts = p[1] " " p[2]
msgid = $1
if (!msgid) next
if (MSGIDREQ != "" && msgid != MSGIDREQ) next timestamp = extract_timestamp($0)
# Init Times # Initialisiere Message-ID falls neu
if (!(msgid in first_ts) || ts < first_ts[msgid]) first_ts[msgid]=ts if (!(msgid in msgid_data)) {
if (!(msgid in last_ts) || ts > last_ts[msgid]) last_ts[msgid]=ts 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"] = ""
}
# Sender extrahieren # Aktualisiere Zeitstempel
if (index($0, "<=") == 1 || $2 == "<=") { if (timestamp < msgid_data[msgid]["first"] || msgid_data[msgid]["first"] == "") {
if (match($0, / from <[^>]+>/, m)) { msgid_data[msgid]["first"] = timestamp
s=m[0]; gsub(/.* from </, "", s); gsub(/>.*/, "", s) }
from=trim(s) if (timestamp > msgid_data[msgid]["last"] || msgid_data[msgid]["last"] == "") {
} else if (match($0, /<= [^ ]+@[^ ]+/, m2)) { msgid_data[msgid]["last"] = timestamp
split(m2[0], a, " "); from=trim(a[2]) }
} else { from="" }
if (from != "") { # Füge Logzeile hinzu
from_addr[msgid]=from if (msgid_data[msgid]["logs"] == "") {
if (FROMREQ != "" && from != FROMREQ) from_mismatch[msgid]=1 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]
}
}
} }
} }
# Empfänger extrahieren END {
rcpt="" # Sortiere Message-IDs nach erstem Auftreten
if (match($0, / for <[^>]+>/, r)) { n = 0
rcpt=r[0]; gsub(/ for </, "", rcpt); gsub(/>/, "", rcpt); rcpt=trim(rcpt) for (msgid in msgid_data) {
} else if (match($0, /\*\* [^ ]+@[^ ]+/, r2)) { sorted_msgids[++n] = msgid
split(r2[0], b, " "); rcpt=trim(b[2])
}
if (rcpt != "") add_rcpt(msgid, rcpt)
# Klassify line types
is_delivery = index($0, " => ")>0
is_fail = index($0, " ** ")>0
is_remote = (index($0, " T=remote_smtp")>0 || index($0, " T=remote_smtp_dkim")>0)
# Success detection
is_success = 0
if (is_delivery && is_remote) {
if (match($0, /C="[^"]*250[^"]*"/)) is_success=1
}
# Deferred detection
is_defer = 0
if (is_remote && (index($0, "retry time not reached")>0 || index($0, "temporar")>0 || index($0, "timed out")>0 || match($0, /all hosts for.*have been failing/))) {
is_defer=1
}
# Fail detection
is_perm_fail = 0
if (is_fail) {
is_perm_fail=1
} else if (match($0, /(Unrouteable address|no such user|User unknown|550 )/)) {
is_perm_fail=1
}
# Counters
if (is_success) deliveries[msgid]++
if (is_defer) defers[msgid]++
if (is_perm_fail) fails[msgid]++
# Fail marker
if (match($0, /all hosts for.*have been failing/)) {
fail_marker[msgid]=1
}
# Full log sammeln
status[msgid]=(status[msgid] ? status[msgid] "\n" : "") ts " | " $0
}
END {
# Sort IDs
n=0
for (id in first_ts){ ids[++n]=id }
for (i=2;i<=n;i++){
key=ids[i]; j=i-1
while (j>=1 && first_ts[ids[j]] > first_ts[key]) { ids[j+1]=ids[j]; j-- }
ids[j+1]=key
}
if (n==0){
print "Keine passenden Einträge im gewählten Zeitfenster gefunden."
exit 0
}
sep="------------------------------------------------------------------------"
for (k=1;k<=n;k++){
id=ids[k]
# FROM filter
if (FROMREQ != "" && from_mismatch[id]==1) {
if (from_addr[id] != FROMREQ && from_addr[id] != "") continue
} }
d=(id in deliveries ? deliveries[id] : 0) # Einfache Sortierung nach first timestamp
e=(id in defers ? defers[id] : 0) for (i = 1; i <= n; i++) {
f=(id in fails ? fails[id] : 0) for (j = i + 1; j <= n; j++) {
if (msgid_data[sorted_msgids[i]]["first"] > msgid_data[sorted_msgids[j]]["first"]) {
# Status logic temp = sorted_msgids[i]
status_txt="UNKNOWN" sorted_msgids[i] = sorted_msgids[j]
if (d>0 && f==0 && e==0) status_txt="OK" sorted_msgids[j] = temp
else if (d>0 && (f>0 || e>0)) status_txt="PARTIAL"
else if (d==0 && f>0) status_txt="FAIL"
else if (d==0 && f==0 && e>0) status_txt="DELAY"
# Rcpts sammeln
out_rcpts=""
if (id in rcpts){
for (r in rcpts[id]) {
if (r != "") {
out_rcpts = (out_rcpts=="" ? r : out_rcpts ", " r)
} }
} }
} }
printf "[%s] ID=%s | First=%s | From=%s | Rcpts=%s | Delivered=%d Deferred=%d Failed=%d\n", # Ausgabe aller gefundenen Message-IDs
status_txt, id, first_ts[id], (from_addr[id] ? from_addr[id] : "(?)"), (out_rcpts=="" ? "(?)" : out_rcpts), d, e, f for (i = 1; i <= n; i++) {
msgid = sorted_msgids[i]
if (fail_marker[id]) { # Status-Klassifikation
printf " Note: DELIVERY STALL erkannt: '\''all hosts for domain failing (retry/backoff)'\''\n" 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 ""
} }
print sep if (n == 0) {
printf "ID: %s\n", id print "Keine passenden Exim-Message-IDs gefunden."
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
} }
' "$input_file"
} }
'
REMOTE_SCRIPT # Main Execution
main() {
analyze_logs
}
# Script ausführen
main "$@"
# Built with the help of Claude Code # Built with the help of Claude Code