rspamd_report.sh wertet /var/log/mail/rspamd.log lesbar aus: Uebersicht als Tabelle (Score, Aktion, Scanzeit, Von, An) und Detailansicht einer Mail mit nach Gewicht sortierter Symbolaufschluesselung samt Summe. Getestet gegen echte Logzeilen, die Summe deckt sich mit dem Score im Log. train_bayes.sh: --only akzeptiert jetzt mehrere Muster (mehrfach angebbar oder kommasepariert), damit sich das Training auf kuratierte Postfaecher begrenzen laesst. Ohne Filter wird das jetzt explizit ausgegeben. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
223 lines
8.3 KiB
Bash
Executable File
223 lines
8.3 KiB
Bash
Executable File
#!/bin/bash
|
|
# ===========================================================================
|
|
# train_bayes.sh - Bayes-Klassifikator von rspamd aus den vorhandenen
|
|
# Postfaechern anlernen.
|
|
#
|
|
# Hintergrund: rspamd traegt mit BAYES_* erst dann zum Score bei, wenn genug
|
|
# gelernt wurde (Default: 200 Spam UND 200 Ham). Bis dahin liefert der
|
|
# Klassifikator gar nichts - genau das war im Log zu sehen, dort tauchte kein
|
|
# einziges BAYES_-Symbol auf. Statt monatelang auf manuelles Verschieben in
|
|
# den Junk-Ordner zu warten (das macht RSPAMD_LEARN=1 bereits automatisch),
|
|
# lernt dieses Skript einmalig aus dem, was ohnehin auf der Platte liegt.
|
|
#
|
|
# AUSFUEHRUNG - immer IM Container:
|
|
#
|
|
# docker exec mailserver bash /tmp/docker-mailserver/train_bayes.sh
|
|
# -> Trockenlauf: zaehlt nur, lernt nichts. Das ist der Default.
|
|
#
|
|
# docker exec mailserver bash /tmp/docker-mailserver/train_bayes.sh --apply
|
|
# -> lernt tatsaechlich
|
|
#
|
|
# Optionen:
|
|
# --apply Wirklich lernen (ohne das passiert nichts)
|
|
# --max N Max. Nachrichten je Postfach und Klasse (Default 400)
|
|
# --include-sent Gesendete Mails zusaetzlich als Ham lernen
|
|
# --only MUSTER Nur Postfaecher, deren Adresse eines der Muster
|
|
# enthaelt. Mehrfach angebbar oder kommasepariert:
|
|
# --only support@,andreas.knuth@
|
|
# WICHTIG: Ohne diese Option laeuft das Skript ueber
|
|
# ALLE Postfaecher aller Domains unter /var/mail.
|
|
# --host HOST:PORT rspamd-Controller (Default 127.0.0.1:11334)
|
|
#
|
|
# Passwort: Falls der Controller eines verlangt (bei euch in
|
|
# rspamd/override.d/worker-controller.inc gesetzt), vorweg setzen:
|
|
# docker exec -e RSPAMC_PASSWORD='...' mailserver bash /tmp/.../train_bayes.sh
|
|
#
|
|
# WICHTIG zur Datenqualitaet:
|
|
# - Ham kommt aus der INBOX. Liegt dort unerkannter Spam, wird er als Ham
|
|
# gelernt und verschlechtert die Erkennung. Vorher grob durchsehen lohnt.
|
|
# - Spam kommt aus Junk. Liegt dort faelschlich einsortierte legitime Mail,
|
|
# gilt dasselbe umgekehrt.
|
|
# - Ein sehr schiefes Verhaeltnis (z.B. 8000 Ham zu 50 Spam) macht den
|
|
# Klassifikator einseitig. Der Trockenlauf zeigt das Verhaeltnis an -
|
|
# danach ggf. mit --max nachsteuern.
|
|
# ===========================================================================
|
|
|
|
set -uo pipefail
|
|
|
|
MAILBASE="${MAILBASE:-/var/mail}"
|
|
RSPAMC_HOST="127.0.0.1:11334"
|
|
MAX_PER_CLASS=400
|
|
MAX_BYTES=5242880 # 5 MB - groessere Mails bringen fuers Lernen nichts
|
|
BATCH=50 # Dateien pro rspamc-Aufruf
|
|
APPLY=0
|
|
INCLUDE_SENT=0
|
|
ONLY_PATTERNS=()
|
|
|
|
usage() { sed -n '2,40p' "$0" | sed 's/^# \{0,1\}//' ; }
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--apply) APPLY=1; shift ;;
|
|
--max) MAX_PER_CLASS="${2:-}"; shift 2 ;;
|
|
--include-sent) INCLUDE_SENT=1; shift ;;
|
|
--only) IFS=',' read -ra _pat <<<"${2:-}"
|
|
ONLY_PATTERNS+=("${_pat[@]}"); shift 2 ;;
|
|
--host) RSPAMC_HOST="${2:-}"; shift 2 ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) echo "Unbekannte Option: $1" >&2; echo "Hilfe: $0 --help" >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
if [[ ! -d ${MAILBASE} ]]; then
|
|
echo "FEHLER: ${MAILBASE} existiert nicht. Laeuft das Skript wirklich IM Container?" >&2
|
|
exit 1
|
|
fi
|
|
|
|
RSPAMC=(rspamc -h "${RSPAMC_HOST}")
|
|
[[ -n ${RSPAMC_PASSWORD:-} ]] && RSPAMC+=(-P "${RSPAMC_PASSWORD}")
|
|
|
|
if ! command -v rspamc >/dev/null 2>&1; then
|
|
echo "FEHLER: rspamc nicht gefunden." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! "${RSPAMC[@]}" stat >/dev/null 2>&1; then
|
|
echo "FEHLER: Controller unter ${RSPAMC_HOST} nicht erreichbar oder Passwort noetig." >&2
|
|
echo " Versuche es mit: docker exec -e RSPAMC_PASSWORD='...' mailserver bash $0 ..." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Dateien einsammeln: bis LIMIT regulaere Dateien unterhalb der uebergebenen
|
|
# Maildir-Verzeichnisse, groessenbegrenzt. Neueste zuerst waere schoener, ist
|
|
# aber bei zehntausenden Dateien zu teuer - Maildir-Namen beginnen mit dem
|
|
# Timestamp, daher liefert die Sortierung eine brauchbare Naeherung.
|
|
# ---------------------------------------------------------------------------
|
|
gather() {
|
|
local limit="$1"; shift
|
|
local dir
|
|
for dir in "$@"; do
|
|
[[ -d ${dir} ]] || continue
|
|
find "${dir}" -maxdepth 1 -type f -size -"${MAX_BYTES}"c -print 2>/dev/null
|
|
done | sort -r | head -n "${limit}"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Lernen in Batches. Gibt "gelernt fehlgeschlagen" zurueck.
|
|
# ---------------------------------------------------------------------------
|
|
learn() {
|
|
local cmd="$1"; shift
|
|
local -a files=("$@")
|
|
local total=${#files[@]}
|
|
local ok=0 fail=0 idx=0 out
|
|
|
|
while (( idx < total )); do
|
|
out=$("${RSPAMC[@]}" "${cmd}" "${files[@]:idx:BATCH}" 2>&1)
|
|
ok=$(( ok + $(grep -c 'success = true' <<<"${out}") ))
|
|
fail=$(( fail + $(grep -ci 'error' <<<"${out}") ))
|
|
idx=$(( idx + BATCH ))
|
|
done
|
|
|
|
echo "${ok} ${fail}"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Hauptlauf
|
|
# ---------------------------------------------------------------------------
|
|
if (( APPLY )); then
|
|
echo "=== Bayes-Training (SCHREIBEND) ==="
|
|
else
|
|
echo "=== Bayes-Training (Trockenlauf - es wird nichts gelernt) ==="
|
|
fi
|
|
echo "Basis: ${MAILBASE} | max ${MAX_PER_CLASS} je Postfach und Klasse"
|
|
if (( ${#ONLY_PATTERNS[@]} > 0 )); then
|
|
echo "Filter: nur Postfaecher mit '${ONLY_PATTERNS[*]}'"
|
|
else
|
|
echo "Filter: KEINER - es werden ALLE Postfaecher unter ${MAILBASE} erfasst"
|
|
fi
|
|
printf '\n%-42s %8s %8s\n' "POSTFACH" "SPAM" "HAM"
|
|
printf -- '---------------------------------------------------------------\n'
|
|
|
|
total_spam=0
|
|
total_ham=0
|
|
learned_spam=0
|
|
learned_ham=0
|
|
failed=0
|
|
|
|
for domain_dir in "${MAILBASE}"/*/; do
|
|
[[ -d ${domain_dir} ]] || continue
|
|
domain=$(basename "${domain_dir}")
|
|
|
|
for user_dir in "${domain_dir}"*/; do
|
|
user_dir="${user_dir%/}"
|
|
[[ -d ${user_dir}/cur ]] || continue # keine Maildir -> ueberspringen
|
|
|
|
mailbox="$(basename "${user_dir}")@${domain}"
|
|
|
|
if (( ${#ONLY_PATTERNS[@]} > 0 )); then
|
|
matched=0
|
|
for pat in "${ONLY_PATTERNS[@]}"; do
|
|
[[ ${mailbox} == *"${pat}"* ]] && { matched=1; break; }
|
|
done
|
|
(( matched )) || continue
|
|
fi
|
|
|
|
mapfile -t spam_files < <(gather "${MAX_PER_CLASS}" \
|
|
"${user_dir}/.Junk/cur" "${user_dir}/.Junk/new" \
|
|
"${user_dir}/.Spam/cur" "${user_dir}/.Spam/new")
|
|
|
|
ham_dirs=("${user_dir}/cur" "${user_dir}/new")
|
|
if (( INCLUDE_SENT )); then
|
|
ham_dirs+=("${user_dir}/.Sent/cur" "${user_dir}/.Sent Messages/cur")
|
|
fi
|
|
mapfile -t ham_files < <(gather "${MAX_PER_CLASS}" "${ham_dirs[@]}")
|
|
|
|
n_spam=${#spam_files[@]}
|
|
n_ham=${#ham_files[@]}
|
|
(( n_spam == 0 && n_ham == 0 )) && continue
|
|
|
|
printf '%-42s %8d %8d\n' "${mailbox}" "${n_spam}" "${n_ham}"
|
|
total_spam=$(( total_spam + n_spam ))
|
|
total_ham=$(( total_ham + n_ham ))
|
|
|
|
if (( APPLY )); then
|
|
if (( n_spam > 0 )); then
|
|
read -r ok fail <<<"$(learn learn_spam "${spam_files[@]}")"
|
|
learned_spam=$(( learned_spam + ok )); failed=$(( failed + fail ))
|
|
fi
|
|
if (( n_ham > 0 )); then
|
|
read -r ok fail <<<"$(learn learn_ham "${ham_files[@]}")"
|
|
learned_ham=$(( learned_ham + ok )); failed=$(( failed + fail ))
|
|
fi
|
|
fi
|
|
done
|
|
done
|
|
|
|
printf -- '---------------------------------------------------------------\n'
|
|
printf '%-42s %8d %8d\n\n' "SUMME" "${total_spam}" "${total_ham}"
|
|
|
|
if (( total_spam > 0 )); then
|
|
ratio=$(( total_ham / (total_spam > 0 ? total_spam : 1) ))
|
|
if (( ratio >= 5 )); then
|
|
echo "WARNUNG: Ham/Spam-Verhaeltnis etwa ${ratio}:1 - sehr einseitig."
|
|
echo " Besser mit kleinerem --max fahren, z.B. --max $(( total_spam / 4 + 10 ))."
|
|
echo
|
|
fi
|
|
fi
|
|
|
|
if (( ! APPLY )); then
|
|
echo "Trockenlauf beendet. Zum echten Lernen dasselbe Kommando mit --apply."
|
|
echo "rspamd braucht je Klasse mindestens 200 Nachrichten, bevor BAYES_* feuert."
|
|
exit 0
|
|
fi
|
|
|
|
echo "Gelernt: ${learned_spam} Spam, ${learned_ham} Ham, ${failed} Fehler/Duplikate."
|
|
echo
|
|
echo "=== rspamc stat ==="
|
|
"${RSPAMC[@]}" stat 2>/dev/null | grep -iE 'learn|classifier|statfile' || true
|
|
echo
|
|
echo "Kontrolle an der naechsten eingehenden Mail:"
|
|
echo " docker exec mailserver sh -c 'grep rspamd_task_write_log /var/log/mail/rspamd.log | tail -3'"
|
|
echo "In der Symbolliste sollte jetzt BAYES_HAM oder BAYES_SPAM auftauchen."
|