Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
248b2f9c1a | ||
|
|
9241cc3d14 |
+149
-55
@@ -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
|
||||||
|
git clone <repo-url>/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)
|
- `dig` (optional, für --mx Feature)
|
||||||
- Lesezugriff auf `/var/log/easyname/` (oder custom `$LOGROOT`)
|
|
||||||
|
**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
|
||||||
+449
-400
@@ -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):
|
||||||
-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
|
||||||
@@ -45,474 +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 -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
|
||||||
|
|
||||||
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 ;;
|
;;
|
||||||
-f|--from)
|
-FROM|--from|-f)
|
||||||
FROM="${2:-}"
|
FROM="$2"
|
||||||
REMOTE_ARGS+=("$1" "$2")
|
shift 2
|
||||||
shift 2 ;;
|
;;
|
||||||
-t|--to)
|
-TO|--to|-t)
|
||||||
TO_REGEX="${2:-}"
|
TO="$2"
|
||||||
REMOTE_ARGS+=("$1" "$2")
|
shift 2
|
||||||
shift 2 ;;
|
;;
|
||||||
-D|--domain)
|
-DOMAIN|--domain|-D)
|
||||||
TARGET_DOMAIN="${2:-}"
|
DOMAIN="$2"
|
||||||
REMOTE_ARGS+=("$1" "$2")
|
shift 2
|
||||||
shift 2 ;;
|
;;
|
||||||
-m|--msgid)
|
-MSGID|--msgid|-m)
|
||||||
MSGID="${2:-}"
|
MSGID="$2"
|
||||||
REMOTE_ARGS+=("$1" "$2")
|
shift 2
|
||||||
shift 2 ;;
|
;;
|
||||||
-G|--grep)
|
-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
|
|
||||||
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
|
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
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Date generation functions
|
if [[ -n "$DATE" ]]; then
|
||||||
gen_dates_hours_back() {
|
validate_date "$DATE"
|
||||||
local hrs="$1" start_ts end_ts cur
|
local year month
|
||||||
local DATE_FMT="%Y-%m-%d"
|
year=$(date -d "$DATE" +%Y)
|
||||||
end_ts="$(date +%s)"
|
month=$(date -d "$DATE" +%m)
|
||||||
start_ts="$(date -d "-${hrs} hours" +%s)"
|
logfiles+=("/var/log/easyname/$year/$month/$DATE.log")
|
||||||
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() {
|
elif [[ -n "$RANGE" ]]; then
|
||||||
local days="$1" i
|
local start_date end_date
|
||||||
for ((i=0;i<=days;i++)); do
|
start_date="${RANGE%%..*}"
|
||||||
date -d "-$i day" +%Y-%m-%d
|
end_date="${RANGE##*..}"
|
||||||
done
|
validate_date "$start_date"
|
||||||
}
|
validate_date "$end_date"
|
||||||
|
|
||||||
gen_dates_range() {
|
local current_date="$start_date"
|
||||||
local a="${1%%..*}" b="${1##*..}" cur="$a"
|
while [[ "$current_date" <= "$end_date" ]]; do
|
||||||
while :; do
|
local year month
|
||||||
echo "$cur"
|
year=$(date -d "$current_date" +%Y)
|
||||||
[[ "$cur" == "$b" ]] && break
|
month=$(date -d "$current_date" +%m)
|
||||||
cur="$(date -d "$cur +1 day" +%Y-%m-%d)"
|
logfiles+=("/var/log/easyname/$year/$month/$current_date.log")
|
||||||
|
current_date=$(date -d "$current_date + 1 day" +%Y-%m-%d)
|
||||||
done
|
done
|
||||||
}
|
|
||||||
|
|
||||||
files_for_date() {
|
elif [[ -n "$HOURS" ]]; then
|
||||||
local d="$1" y="${d:0:4}" m="${d:5:2}"
|
# Bestimme Datumsbereich für letzte N Stunden
|
||||||
local p="/var/log/easyname/${y}/${m}/${d}.log"
|
local start_ts end_ts
|
||||||
if [[ -f "$p" ]]; then
|
end_ts=$(date +%s)
|
||||||
printf "%s\n" "$p"
|
start_ts=$((end_ts - HOURS * 3600))
|
||||||
fi
|
|
||||||
if [[ -f "${p}.gz" ]]; then
|
local current_ts="$start_ts"
|
||||||
printf "%s\n" "${p}.gz"
|
local seen_dates=()
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# MX lookup function
|
while [[ $current_ts -le $end_ts ]]; do
|
||||||
show_mx_records() {
|
local date_str
|
||||||
local domain="$1"
|
date_str=$(date -d "@$current_ts" +%Y-%m-%d)
|
||||||
if command -v dig >/dev/null 2>&1; then
|
|
||||||
echo "== MX für ${domain} =="
|
|
||||||
dig +short MX "$domain" | sort -n || true
|
|
||||||
echo "-- A/AAAA der MX:"
|
|
||||||
while read -r mx; do
|
|
||||||
local 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 "$domain" | sort -n)
|
|
||||||
echo "========================================================================"
|
|
||||||
else
|
|
||||||
echo "(Hinweis) dig nicht gefunden, MX-Ausgabe übersprungen."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Global settings
|
# Füge Datum nur einmal hinzu
|
||||||
LOGROOT="${LOGROOT:-/var/log/easyname}"
|
local found=0
|
||||||
TODAY="$(date +%Y-%m-%d)"
|
for seen in "${seen_dates[@]}"; do
|
||||||
|
[[ "$seen" == "$date_str" ]] && found=1 && break
|
||||||
|
done
|
||||||
|
|
||||||
# Parse remote arguments
|
if [[ $found -eq 0 ]]; then
|
||||||
HOURS=""; DAYS=""; DATE_ONE=""; DATE_RANGE=""
|
seen_dates+=("$date_str")
|
||||||
FROM=""; TO_REGEX=""; TARGET_DOMAIN=""; MSGID=""; GREPEXTRA=""
|
local year month
|
||||||
DRYRUN=0; WANT_MX=0
|
year=$(date -d "$date_str" +%Y)
|
||||||
|
month=$(date -d "$date_str" +%m)
|
||||||
|
logfiles+=("/var/log/easyname/$year/$month/$date_str.log")
|
||||||
|
fi
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
current_ts=$((current_ts + 86400)) # +1 Tag
|
||||||
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 ;;
|
|
||||||
*) shift ;;
|
|
||||||
esac
|
|
||||||
done
|
done
|
||||||
|
|
||||||
# Generate date list
|
|
||||||
declare -a DATES=()
|
|
||||||
if [[ -n "$HOURS" ]]; then
|
|
||||||
mapfile -t DATES < <(gen_dates_hours_back "$HOURS")
|
|
||||||
elif [[ -n "$DAYS" ]]; then
|
elif [[ -n "$DAYS" ]]; then
|
||||||
mapfile -t DATES < <(gen_dates_days_back "$DAYS")
|
# Bestimme Datumsbereich für letzte N Tage
|
||||||
elif [[ -n "$DATE_ONE" ]]; then
|
for ((i=0; i<DAYS; i++)); do
|
||||||
DATES=("$DATE_ONE")
|
local date_str
|
||||||
elif [[ -n "$DATE_RANGE" ]]; then
|
date_str=$(date -d "-$i days" +%Y-%m-%d)
|
||||||
mapfile -t DATES < <(gen_dates_range "$DATE_RANGE")
|
local year month
|
||||||
else
|
year=$(date -d "$date_str" +%Y)
|
||||||
DATES=("$TODAY")
|
month=$(date -d "$date_str" +%m)
|
||||||
fi
|
logfiles+=("/var/log/easyname/$year/$month/$date_str.log")
|
||||||
|
|
||||||
# Collect files
|
|
||||||
declare -a FILES=()
|
|
||||||
for d in "${DATES[@]}"; do
|
|
||||||
while read -r f; do FILES+=("$f"); done < <(files_for_date "$d" || true)
|
|
||||||
done
|
done
|
||||||
|
|
||||||
if (( DRYRUN )); then
|
|
||||||
echo "Würde durchsuchen:"
|
|
||||||
printf ' %s\n' "${FILES[@]}"
|
|
||||||
exit 0
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "${#FILES[@]}" -eq 0 ]]; then
|
printf '%s\n' "${logfiles[@]}"
|
||||||
echo "Keine Logfiles gefunden unter ${LOGROOT}/YYYY/MM/DATE.log(.gz) fürs gewählte Zeitfenster." >&2
|
}
|
||||||
exit 0
|
|
||||||
|
# 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
|
fi
|
||||||
|
|
||||||
# Build grep filters
|
echo "== MX für $DOMAIN =="
|
||||||
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
|
# MX Records
|
||||||
cat_files() {
|
local mx_records
|
||||||
local f
|
mx_records=$(dig +short MX "$DOMAIN" | sort -n)
|
||||||
for f in "${FILES[@]}"; do
|
|
||||||
if [[ "$f" == *.gz ]]; then
|
if [[ -z "$mx_records" ]]; then
|
||||||
zcat -- "$f"
|
echo "Keine MX-Records gefunden"
|
||||||
else
|
else
|
||||||
cat -- "$f"
|
echo "$mx_records"
|
||||||
|
|
||||||
|
# A/AAAA Records für MX-Hosts
|
||||||
|
echo "-- A/AAAA der MX:"
|
||||||
|
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:-<keine A/AAAA Records>}"
|
||||||
|
done <<< "$mx_records"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "========================================================================"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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
|
fi
|
||||||
done
|
done
|
||||||
}
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
# Grep chain
|
# MX Records anzeigen falls gewünscht
|
||||||
grep_chain() {
|
show_mx_records
|
||||||
if [[ "${#PREF_GREP[@]}" -eq 0 ]]; then
|
|
||||||
cat_files
|
# 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
|
else
|
||||||
cat_files | grep -F -e "${PREF_GREP[0]}" $(for s in "${PREF_GREP[@]:1}"; do printf ' -e %q' "$s"; done)
|
continue
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# MX Block
|
|
||||||
if (( WANT_MX )) && [[ -n "$TARGET_DOMAIN" ]]; then
|
|
||||||
show_mx_records "$TARGET_DOMAIN"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# AWK Analysis
|
# Zeitfilter für Stunden/Tage
|
||||||
grep_chain | awk -v TO_RE="${TO_REGEX:-}" -v WANTDOM="${TARGET_DOMAIN:-}" -v MSGIDREQ="${MSGID:-}" -v FROMREQ="${FROM:-}" -v EXTRA="${GREPEXTRA:-}" '
|
local time_filter=""
|
||||||
function trim(s){ sub(/^[ \t\r\n]+/, "", s); sub(/[ \t\r\n]+$/, "", s); return s }
|
if [[ -n "$HOURS" ]]; then
|
||||||
function add_rcpt(id, rc){
|
local start_time
|
||||||
if (!(id in rcpts)) rcpts[id][""] = ""
|
start_time=$(date -d "-$HOURS hours" '+%b %_d %H:%M')
|
||||||
if (!(rc in rcpts[id])) rcpts[id][rc]=1
|
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"
|
||||||
}
|
}
|
||||||
|
|
||||||
BEGIN { FS=" " }
|
# 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
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
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
|
||||||
{
|
{
|
||||||
raw=$0
|
msgid = extract_msgid($0)
|
||||||
|
if (msgid == "") next
|
||||||
|
|
||||||
# Zeit + ID detektieren
|
timestamp = extract_timestamp($0)
|
||||||
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]
|
|
||||||
msgid = $1
|
|
||||||
if (!msgid) next
|
|
||||||
|
|
||||||
if (MSGIDREQ != "" && msgid != MSGIDREQ) next
|
# Initialisiere Message-ID falls neu
|
||||||
|
if (!(msgid in msgid_data)) {
|
||||||
# Init Times
|
msgid_data[msgid]["first"] = timestamp
|
||||||
if (!(msgid in first_ts) || ts < first_ts[msgid]) first_ts[msgid]=ts
|
msgid_data[msgid]["last"] = timestamp
|
||||||
if (!(msgid in last_ts) || ts > last_ts[msgid]) last_ts[msgid]=ts
|
msgid_data[msgid]["from"] = ""
|
||||||
|
msgid_data[msgid]["rcpts"] = ""
|
||||||
# Sender extrahieren
|
msgid_data[msgid]["delivered"] = 0
|
||||||
if (index($0, "<=") == 1 || $2 == "<=") {
|
msgid_data[msgid]["deferred"] = 0
|
||||||
if (match($0, / from <[^>]+>/, m)) {
|
msgid_data[msgid]["failed"] = 0
|
||||||
s=m[0]; gsub(/.* from </, "", s); gsub(/>.*/, "", s)
|
msgid_data[msgid]["logs"] = ""
|
||||||
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
|
# Aktualisiere Zeitstempel
|
||||||
rcpt=""
|
if (timestamp < msgid_data[msgid]["first"] || msgid_data[msgid]["first"] == "") {
|
||||||
if (match($0, / for <[^>]+>/, r)) {
|
msgid_data[msgid]["first"] = timestamp
|
||||||
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)
|
if (timestamp > msgid_data[msgid]["last"] || msgid_data[msgid]["last"] == "") {
|
||||||
|
msgid_data[msgid]["last"] = timestamp
|
||||||
# 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
|
# Füge Logzeile hinzu
|
||||||
is_defer = 0
|
if (msgid_data[msgid]["logs"] == "") {
|
||||||
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/))) {
|
msgid_data[msgid]["logs"] = timestamp " | " $0
|
||||||
is_defer=1
|
} else {
|
||||||
|
msgid_data[msgid]["logs"] = msgid_data[msgid]["logs"] "\n" timestamp " | " $0
|
||||||
}
|
}
|
||||||
|
|
||||||
# Fail detection
|
# Analysiere Logzeile für Status und Details
|
||||||
is_perm_fail = 0
|
if ($0 ~ / <= /) {
|
||||||
if (is_fail) {
|
# Eingehende Mail: Extract Sender
|
||||||
is_perm_fail=1
|
match($0, / <= ([^ ]+)/, arr)
|
||||||
} else if (match($0, /(Unrouteable address|no such user|User unknown|550 )/)) {
|
if (arr[1] != "") {
|
||||||
is_perm_fail=1
|
msgid_data[msgid]["from"] = arr[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
|
|
||||||
}
|
}
|
||||||
|
else if ($0 ~ / => /) {
|
||||||
|
# Erfolgreiche Zustellung
|
||||||
|
msgid_data[msgid]["delivered"]++
|
||||||
|
|
||||||
# Full log sammeln
|
# Extrahiere Empfänger
|
||||||
status[msgid]=(status[msgid] ? status[msgid] "\n" : "") ts " | " $0
|
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 {
|
END {
|
||||||
# Sort IDs
|
# Sortiere Message-IDs nach erstem Auftreten
|
||||||
n = 0
|
n = 0
|
||||||
for (id in first_ts){ ids[++n]=id }
|
for (msgid in msgid_data) {
|
||||||
for (i=2;i<=n;i++){
|
sorted_msgids[++n] = msgid
|
||||||
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
|
# 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) {
|
if (n == 0) {
|
||||||
print "Keine passenden Einträge im gewählten Zeitfenster gefunden."
|
print "Keine passenden Exim-Message-IDs 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)
|
|
||||||
e=(id in defers ? defers[id] : 0)
|
|
||||||
f=(id in fails ? fails[id] : 0)
|
|
||||||
|
|
||||||
# Status logic
|
|
||||||
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"
|
|
||||||
|
|
||||||
# Rcpts sammeln
|
|
||||||
out_rcpts=""
|
|
||||||
if (id in rcpts){
|
|
||||||
for (r in rcpts[id]) {
|
|
||||||
if (r != "") {
|
|
||||||
out_rcpts = (out_rcpts=="" ? r : out_rcpts ", " r)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
' "$input_file"
|
||||||
}
|
}
|
||||||
|
|
||||||
printf "[%s] ID=%s | First=%s | From=%s | Rcpts=%s | Delivered=%d Deferred=%d Failed=%d\n",
|
# Main Execution
|
||||||
status_txt, id, first_ts[id], (from_addr[id] ? from_addr[id] : "(?)"), (out_rcpts=="" ? "(?)" : out_rcpts), d, e, f
|
main() {
|
||||||
|
analyze_logs
|
||||||
if (fail_marker[id]) {
|
|
||||||
printf " Note: DELIVERY STALL erkannt: '\''all hosts for domain failing (retry/backoff)'\''\n"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
print sep
|
# Script ausführen
|
||||||
printf "ID: %s\n", id
|
main "$@"
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'
|
|
||||||
REMOTE_SCRIPT
|
|
||||||
|
|
||||||
# Built with the help of Claude Code
|
# Built with the help of Claude Code
|
||||||
Reference in New Issue
Block a user