Files
archivmail/update.sh
T
sysopsandClaude Sonnet 5 a2e45b950b chore(PROJ-79): lucide-react Major-Upgrade + npm audit fix + Deploy-Warn-Gate
lucide-react 0.562.0 auf 1.28.0 (kein Codeumbau nötig, alle genutzten
Icon-Namen kanonisch unverändert). npm audit fix behebt alle 6 gefundenen
Vulnerabilities ohne Breaking Change (next minor 16.2.9->16.3.0 im
bestehenden ^16.1.1-Range). caniuse-lite/Browserslist-DB aktualisiert
(war 8 Monate alt).

update.sh: npm audit --audit-level=high läuft jetzt als nicht-blockierendes
Warn-Gate nach npm ci. Bewusst kein Auto-Fix während des laufenden Deploys
— ein npm audit fix live auf dem Produktivsystem könnte unbemerkt Versionen
ändern, ohne vorherige Verifikation. Bei Fund wird gewarnt und auf lokales
audit fix + Test + Commit verwiesen.

Restliche PROJ-79-Schritte (eslint, @types/node, tailwindcss v4 +
tailwind-merge v3, typescript) bleiben offen, siehe Feature-Spec.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019j28kGcaJAhBnrYX34hGdt
2026-08-05 17:37:40 +02:00

448 lines
21 KiB
Bash
Executable File

#!/bin/bash
# archivmail Updater
# Zieht die neueste Version aus Gitea, baut Frontend + Backend und startet Dienste neu.
#
# Aufruf (auf dem Server als root):
# bash /opt/archivmail/update.sh
#
# Oder direkt von Gitea laden und ausführen:
# curl -fsSL https://gitea.perlbach24.de/scripte/archivmail/raw/branch/main/update.sh | bash
set -euo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
log() { echo -e "${GREEN}[OK]${NC} $*"; }
info() { echo -e "${BLUE}[..]${NC} $*"; }
warn() { echo -e "${YELLOW}[!!]${NC} $*"; }
die() { echo -e "${RED}[ERR]${NC} $*" >&2; exit 1; }
[[ $EUID -eq 0 ]] || die "Bitte als root ausführen: sudo bash update.sh"
# PROJ-68: Backfill für Bestandsinstallationen, bei denen sudo-Rechte für die
# Admin-Dienststeuerung nie provisioniert wurden (siehe install.sh für den
# ausführlichen Kommentar zum "Warum"). Whitelist MUSS mit `allowedServices`
# in internal/api/admin_services_handlers.go synchron gehalten werden.
provision_archivmail_sudoers() {
local am_user="archivmail"
command -v sudo >/dev/null 2>&1 || apt-get install -y -qq sudo
local services=("archivmail" "archivmail-web" "manticore" "postgresql@17-main" "postfix" "nginx")
local tmpfile
tmpfile="$(mktemp)"
{
echo "# Verwaltet von install.sh/update.sh (PROJ-68) — nicht manuell editieren,"
echo "# Änderungen gehen beim nächsten Deploy verloren. Whitelist muss mit"
echo "# allowedServices in internal/api/admin_services_handlers.go übereinstimmen."
for svc in "${services[@]}"; do
echo "${am_user} ALL=(root) NOPASSWD: /usr/bin/systemctl start ${svc}.service"
echo "${am_user} ALL=(root) NOPASSWD: /usr/bin/systemctl stop ${svc}.service"
echo "${am_user} ALL=(root) NOPASSWD: /usr/bin/systemctl restart ${svc}.service"
echo "${am_user} ALL=(root) NOPASSWD: /usr/bin/systemctl enable ${svc}.service"
echo "${am_user} ALL=(root) NOPASSWD: /usr/bin/systemctl disable ${svc}.service"
done
} > "$tmpfile"
if visudo -c -f "$tmpfile" >/dev/null 2>&1; then
install -m 0440 -o root -g root "$tmpfile" /etc/sudoers.d/archivmail
log "sudo-Rechte für Dienststeuerung aktuell (/etc/sudoers.d/archivmail)"
else
warn "Generierte sudoers-Regel ist ungültig — /etc/sudoers.d/archivmail NICHT verändert"
fi
rm -f "$tmpfile"
}
provision_archivmail_sudoers
REPO_URL="${REPO_URL:-https://gitea.perlbach24.de/scripte/archivmail.git}"
INSTALL_DIR="/opt/archivmail"
BUILD_DIR="/opt/archivmail/_build"
FRONTEND_DIR="/opt/archivmail/web"
BIN_DIR="/opt/archivmail/bin"
echo ""
echo " ╔══════════════════════════════════════╗"
echo " ║ archivmail Updater ║"
echo " ╚══════════════════════════════════════╝"
echo ""
# ── Script selbst aktualisieren ──────────────────────────────────────────
SCRIPT_URL="https://gitea.perlbach24.de/scripte/archivmail/raw/branch/main/update.sh"
SELF="$INSTALL_DIR/update.sh"
if [[ -f "$SELF" ]] && command -v curl >/dev/null; then
curl -fsSL "$SCRIPT_URL" -o "$SELF.new" 2>/dev/null && mv "$SELF.new" "$SELF" && chmod +x "$SELF" \
&& info "Script aktualisiert" || rm -f "$SELF.new"
fi
# ── Go: Version sicherstellen (OS-versions-agnostisch) ───────────────────
# Go wird unter /usr/local/go installiert — unabhängig von apt und Debian-Version.
# Das gilt nach einem OS-Upgrade auf Debian 14+ genauso wie auf trixie.
GO_REQUIRED="1.24.4"
export PATH="$PATH:/usr/local/go/bin"
_go_ok=0
if command -v go >/dev/null 2>&1; then
_go_installed=$(go version 2>/dev/null | grep -oP 'go\K[0-9]+\.[0-9]+' | head -1)
_go_major=${_go_installed%%.*}
_go_minor=${_go_installed#*.}
# >= 1.24 gilt als ausreichend
if [[ "$_go_major" -gt 1 ]] || [[ "$_go_major" -eq 1 && "$_go_minor" -ge 24 ]]; then
_go_ok=1
fi
fi
if [[ $_go_ok -eq 0 ]]; then
info "Go < 1.24 oder nicht gefunden — installiere Go ${GO_REQUIRED} von upstream..."
_go_arch="amd64"
[[ "$(uname -m)" == "aarch64" ]] && _go_arch="arm64"
curl -fsSL "https://dl.google.com/go/go${GO_REQUIRED}.linux-${_go_arch}.tar.gz" \
| tar -C /usr/local -xz
ln -sf /usr/local/go/bin/go /usr/local/bin/go
ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt
log "Go $(go version | awk '{print $3}') installiert"
else
log "Go $(go version | awk '{print $3}') OK"
fi
# ── Sonstige Voraussetzungen ──────────────────────────────────────────────
command -v git >/dev/null || die "git nicht gefunden — apt-get install git"
command -v node >/dev/null || die "node nicht gefunden — apt-get install nodejs"
command -v npm >/dev/null || die "npm nicht gefunden — apt-get install npm"
# ── Manticore Search prüfen / installieren ────────────────────────────────
# Paket ist an bookworm gebunden — funktioniert auch auf trixie und Debian 14+,
# da Manticore kein OS-spezifisches ABI nutzt (repo.manticoresearch.com hat
# kein eigenes trixie-Repo, siehe Release-Notes ab v13.6.7 "Added support for
# Debian 13 Trixie" — das bezieht sich auf Lauffähigkeit, nicht auf ein neues
# Repo). Seit ~v25 ist "manticoresearch" ein einziges "manticore"-Paket
# (vorher mehrere manticore-server/-tools/-dev-Pakete) — Paketname und
# Pfadstruktur im Repo haben sich entsprechend geändert. GitHub-Releases
# liefern seit einiger Zeit keine .deb-Assets mehr (leeres assets[]), daher
# kein Fallback mehr dorthin — repo.manticoresearch.com ist der einzige Weg.
#
# Version wird dynamisch aus der Packages-Datei des Repos ermittelt (letzter
# "Package: manticore"-Block, dort steht Version+Filename explizit drin —
# kein Rätselraten über Namensschema). _MANTICORE_VERSION_FALLBACK ist nur
# ein Sicherheitsnetz, falls die Packages-Datei mal nicht erreichbar/nicht
# parsbar ist (Netzwerkfehler, Repo down, Format ändert sich erneut) — dann
# lieber ein bekannter, getesteter Stand als ein komplett fehlgeschlagenes
# Deploy. Bei jedem Update-Lauf ohnehin neu geprüft, kein manuelles Nachpflegen
# nötig, solange das Packages-Format stabil bleibt.
_MANTICORE_VERSION_FALLBACK="27.1.5-26061911-5a1cf9399"
_mc_installed_version="$(dpkg-query -W -f='${Version}' manticore 2>/dev/null || true)"
_mc_arch="amd64"
[[ "$(uname -m)" == "aarch64" ]] && _mc_arch="arm64"
# Liefert bei Erfolg "VERSION|FILENAME" (Filename ist der repo-relative Pfad
# aus der Packages-Datei, z.B. "dists/bookworm/main/binary-amd64/manticore_
# 27.1.5-...deb") auf stdout, sonst nichts (Exit 1).
_mc_latest_from_repo() {
local base="https://repo.manticoresearch.com/repository/manticoresearch_bookworm"
local packages block version filename
packages="$(curl -fsSL --max-time 15 "${base}/dists/bookworm/main/binary-${_mc_arch}/Packages" 2>/dev/null)" || return 1
[[ -n "$packages" ]] || return 1
# Packages-Einträge sind durch Leerzeilen getrennt; mehrere historische
# "Package: manticore"-Blöcke möglich — der letzte ist der aktuellste.
block="$(printf '%s\n' "$packages" | awk 'BEGIN{RS="\n\n"} {n=split($0,l,"\n"); if (l[1]=="Package: manticore") b=$0} END{print b}')"
[[ -n "$block" ]] || return 1
version="$(printf '%s\n' "$block" | awk -F': ' '/^Version:/{print $2; exit}')"
filename="$(printf '%s\n' "$block" | awk -F': ' '/^Filename:/{print $2; exit}')"
[[ -n "$version" && -n "$filename" ]] || return 1
printf '%s|%s\n' "$version" "$filename"
}
_mc_target_version="$_MANTICORE_VERSION_FALLBACK"
_mc_target_url="https://repo.manticoresearch.com/repository/manticoresearch_bookworm/dists/bookworm/main/binary-${_mc_arch}/manticore_${_MANTICORE_VERSION_FALLBACK}_${_mc_arch}.deb"
if _mc_latest="$(_mc_latest_from_repo)"; then
_mc_target_version="${_mc_latest%%|*}"
_mc_target_url="https://repo.manticoresearch.com/repository/manticoresearch_bookworm/${_mc_latest#*|}"
info "Manticore Search: aktuelle Repo-Version ${_mc_target_version} ermittelt"
else
warn "Manticore-Repo-Metadaten nicht abrufbar — falle auf bekannten Stand ${_MANTICORE_VERSION_FALLBACK} zurück"
fi
_mc_fetch_and_install() {
# $1 = human-readable action word for log ("installieren"/"aktualisieren")
wget -q -O /tmp/manticore.deb "$_mc_target_url" || true
if [[ ! -f /tmp/manticore.deb ]]; then
warn "Manticore Search konnte nicht heruntergeladen werden — bitte manuell $1"
warn "Siehe: https://manticoresearch.com/install/"
return 1
fi
dpkg -i /tmp/manticore.deb 2>/dev/null || apt-get install -f -y -qq 2>/dev/null || true
rm -f /tmp/manticore.deb
return 0
}
if [[ -z "$_mc_installed_version" ]]; then
info "Manticore Search nicht gefunden — installiere Version ${_mc_target_version}..."
_mc_fetch_and_install "installieren" && log "Manticore Search installiert"
elif dpkg --compare-versions "$_mc_installed_version" lt "$_mc_target_version"; then
# PROJ-67: bestehende Installation ist älter als der ermittelte Repo-Stand —
# Upgrade statt Skip, damit auch Bestandsinstallationen (nicht nur
# Neuinstallationen) beim nächsten Deploy automatisch mitgezogen werden.
# Downgrade-Schutz durch "lt"-Vergleich: eine bereits neuere installierte
# Version wird nie zurückgestuft (z.B. falls die Repo-Abfrage mal einen
# älteren Stand als den Fallback liefern sollte).
info "Manticore Search ${_mc_installed_version} gefunden, aktualisiere auf ${_mc_target_version}..."
systemctl stop manticore 2>/dev/null || true
if _mc_fetch_and_install "aktualisieren"; then
log "Manticore Search aktualisiert: ${_mc_installed_version}${_mc_target_version}"
else
warn "Upgrade fehlgeschlagen — vorherige Version bleibt installiert, Dienst wird trotzdem neu gestartet"
fi
else
log "Manticore Search ${_mc_installed_version} bereits aktuell (>= ${_mc_target_version})"
fi
if systemctl list-unit-files manticore.service >/dev/null 2>&1; then
systemctl enable manticore 2>/dev/null || true
systemctl is-active --quiet manticore || systemctl start manticore 2>/dev/null || warn "Manticore-Dienst konnte nicht gestartet werden"
systemctl is-active --quiet manticore && log "Manticore Search läuft"
fi
# ── OCR-Tools (PROJ-35: tesseract + poppler) ──────────────────────────────
# Optional: ohne diese Tools fällt OCR auf "deaktiviert" zurück, kein Abbruch.
if ! command -v tesseract >/dev/null 2>&1 || ! command -v pdftotext >/dev/null 2>&1; then
info "Installiere OCR-Tools (tesseract, poppler-utils)..."
apt-get install -y tesseract-ocr tesseract-ocr-deu poppler-utils 2>/dev/null || \
warn "OCR-Tools konnten nicht installiert werden — OCR wird deaktiviert"
fi
if command -v tesseract >/dev/null 2>&1 && command -v pdftotext >/dev/null 2>&1; then
log "OCR-Tools verfügbar (tesseract $(tesseract --version 2>&1 | head -1 | awk '{print $2}'))"
fi
# ── Quellcode holen ───────────────────────────────────────────────────────
if [[ -d "$BUILD_DIR/.git" ]]; then
info "Aktualisiere Quellcode aus Gitea..."
git config --global --add safe.directory "$BUILD_DIR" 2>/dev/null || true
git -C "$BUILD_DIR" fetch origin
git -C "$BUILD_DIR" reset --hard origin/main
log "Quellcode aktualisiert ($(git -C "$BUILD_DIR" log -1 --format='%h %s'))"
else
info "Lade Quellcode von $REPO_URL ..."
mkdir -p "$BUILD_DIR"
git clone "$REPO_URL" "$BUILD_DIR"
log "Quellcode geladen"
fi
# ── Go Backend bauen ──────────────────────────────────────────────────────
info "Baue Go Backend..."
cd "$BUILD_DIR"
go mod tidy && go mod download
CGO_ENABLED=0 go build -buildvcs=false -o "$BUILD_DIR/archivmail-new" ./cmd/archivmail/
log "Go Backend gebaut"
# ── Next.js Frontend bauen ────────────────────────────────────────────────
info "Installiere Node-Abhängigkeiten..."
npm ci --prefer-offline 2>/dev/null || npm ci
log "Node-Abhängigkeiten installiert"
if ! npm audit --audit-level=high >/tmp/npm-audit-report.txt 2>&1; then
warn "npm audit meldet High/Critical-Vulnerabilities — Details: /tmp/npm-audit-report.txt"
warn "Fix NICHT automatisch während des Deploys (Risiko: Breaking Change live) — 'npm audit fix' lokal ausführen, testen, committen."
fi
info "Baue Next.js Frontend..."
npm run build
log "Frontend gebaut"
# ── Dienste stoppen ───────────────────────────────────────────────────────
info "Stoppe Dienste..."
systemctl stop archivmail-web 2>/dev/null || warn "archivmail-web nicht aktiv"
systemctl stop archivmail 2>/dev/null || warn "archivmail nicht aktiv"
# ── Manticore als Standard-Backend in config.yml setzen ──────────────────
CONFIG_FILE="/etc/archivmail/config.yml"
if [[ -f "$CONFIG_FILE" ]]; then
# Backend auf manticore umstellen falls noch nicht gesetzt
if grep -q 'backend:' "$CONFIG_FILE"; then
if ! grep -q 'backend: manticore' "$CONFIG_FILE"; then
sed -i 's/^\([[:space:]]*\)backend:.*/\1backend: manticore/' "$CONFIG_FILE"
info "Index-Backend auf 'manticore' gesetzt"
fi
else
# backend: Zeile unter index: einfuegen
sed -i '/^index:/a\ backend: manticore' "$CONFIG_FILE"
info "Index-Backend 'manticore' hinzugefuegt"
fi
# manticore_dsn setzen falls nicht vorhanden
if ! grep -q 'manticore_dsn' "$CONFIG_FILE"; then
sed -i '/backend: manticore/a\ manticore_dsn: "manticore@tcp(127.0.0.1:9306)/"' "$CONFIG_FILE"
info "Manticore-DSN gesetzt"
fi
log "Manticore-Konfiguration aktualisiert"
fi
# ── Dateien einspielen ────────────────────────────────────────────────────
info "Spiele Backend ein..."
mkdir -p "$BIN_DIR"
cp "$BUILD_DIR/archivmail-new" "$BIN_DIR/archivmail"
chmod +x "$BIN_DIR/archivmail"
# Service-Binary direkt überschreiben (nach systemctl stop)
cp "$BIN_DIR/archivmail" "$INSTALL_DIR/archivmail"
ln -sf "$BIN_DIR/archivmail" /usr/local/bin/archivmail
log "Backend eingespielt"
info "Spiele Cron-Jobs ein..."
cp "$BUILD_DIR/deploy/cron.d/archivmail" /etc/cron.d/archivmail
chmod 644 /etc/cron.d/archivmail
for script in "$BUILD_DIR"/deploy/cron.d/*.sh; do
cp "$script" "/usr/local/bin/$(basename "$script")"
chmod 755 "/usr/local/bin/$(basename "$script")"
done
log "Cron-Jobs eingespielt (/etc/cron.d/archivmail + Wrapper-Skripte)"
info "Spiele Frontend ein (standalone)..."
# Next.js standalone mirrors the absolute build path — find the dir that contains server.js.
# The path is typically: .next/standalone/<build-dir>/server.js
STANDALONE_SERVER=$(find "$BUILD_DIR/.next/standalone" -maxdepth 3 -name "server.js" | head -1)
[[ -n "$STANDALONE_SERVER" ]] || die "server.js nicht im standalone-Build gefunden"
STANDALONE_ROOT=$(dirname "$STANDALONE_SERVER")
# Clean destination and copy fresh standalone output
rm -rf "$FRONTEND_DIR"
mkdir -p "$FRONTEND_DIR"
rsync -a "$STANDALONE_ROOT/" "$FRONTEND_DIR/"
# Copy static assets from the full build (standalone doesn't include them)
mkdir -p "$FRONTEND_DIR/.next/static"
rsync -a --delete "$BUILD_DIR/.next/static/" "$FRONTEND_DIR/.next/static/"
if [[ -d "$BUILD_DIR/public" ]]; then
rsync -a "$BUILD_DIR/public/" "$FRONTEND_DIR/public/"
fi
log "Frontend eingespielt"
# ── systemd Units synchronisieren ─────────────────────────────────────────
# install.sh schreibt die Unit-Dateien nur beim Erst-Setup. Ohne diesen
# Schritt bleiben bereits installierte Server auf dem Stand von damals
# eingefroren, selbst wenn sich die Unit-Definition im Repo ändert (z.B.
# CAP_NET_ADMIN für die Security-Tab-Firewall-Aktion, nachträglich ergänzt —
# Server die vor dieser Änderung installiert wurden, hatten die Capability
# bis zu diesem Fix hier gefehlt). Bei jedem Deploy neu schreiben + reload,
# damit Unit-Änderungen aus install.sh automatisch nachgezogen werden.
info "Synchronisiere systemd Units..."
STORE_DIR="/var/archivmail"
LOG_DIR="/var/log/archivmail"
CONFIG_DIR="/etc/archivmail"
SSL_DIR="/etc/ssl/archivmail"
AM_USER="archivmail"
cat > /etc/systemd/system/archivmail.service << UNIT
[Unit]
Description=archivmail Mail Archive Daemon
After=network.target postgresql.service manticore.service
Requires=postgresql.service manticore.service
[Service]
Type=simple
User=${AM_USER}
Group=${AM_USER}
# CAP_NET_ADMIN: required for the admin "enable firewall" action (nft -f /etc/nftables.conf)
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_ADMIN
# PROJ-68: CAP_SETUID/CAP_SETGID are NOT ambient (the Go daemon itself must
# never hold them) but must be in the bounding set so the setuid-root sudo
# child process (spawned by the admin "Dienste"-Tab for systemctl actions)
# can actually change to uid/gid 0. Without this, sudo aborts with
# "unable to change to root gid: Operation not permitted" even though the
# sudoers NOPASSWD rule is correct — verified 2026-07-05, see
# features/PROJ-68-sudo-provisionierung-dienststeuerung.md.
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_ADMIN CAP_SETUID CAP_SETGID
ExecStart=${INSTALL_DIR}/archivmail --config ${CONFIG_DIR}/config.yml
ExecReload=/bin/kill -HUP \$MAINPID
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=archivmail
NoNewPrivileges=false
ProtectSystem=strict
ReadWritePaths=${STORE_DIR} ${LOG_DIR}
ReadOnlyPaths=${CONFIG_DIR} ${SSL_DIR}
[Install]
WantedBy=multi-user.target
UNIT
cat > /etc/systemd/system/archivmail-web.service << UNIT
[Unit]
Description=archivmail Web Frontend
After=network.target archivmail.service
[Service]
Type=simple
User=${AM_USER}
Group=${AM_USER}
WorkingDirectory=${INSTALL_DIR}/web
ExecStart=/usr/bin/node server.js
Environment=NODE_ENV=production
Environment=PORT=3000
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=archivmail-web
[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable archivmail archivmail-web --quiet
log "systemd Units synchronisiert"
# ── Dienste starten ───────────────────────────────────────────────────────
info "Starte Dienste..."
systemctl start archivmail
systemctl start archivmail-web
log "Dienste gestartet"
# ── Status prüfen ─────────────────────────────────────────────────────────
# Hinweis: Kein automatischer Manticore-Reindex mehr bei jedem Update.
# Neu importierte Mails (IMAP/SMTP/POP3/EML) werden synchron indiziert
# (siehe internal/imap/importer.go IndexSync) — ein Reindex ist nur nach
# Schema-Änderungen am Index nötig: manuell mit
# archivmail reindex --config /etc/archivmail/config.yml
BACKEND_OK=0
FRONTEND_OK=0
# Health-Check statt nur systemd-Status: Backend muss tatsächlich auf
# HTTP antworten (Prozess kann laufen, aber z.B. an DB-Connect hängen).
info "Prüfe Backend-Health (/api/health)..."
for i in $(seq 1 15); do
if systemctl is-active --quiet archivmail && curl -fsS -o /dev/null --max-time 2 http://127.0.0.1:8080/api/health 2>/dev/null; then
BACKEND_OK=1
break
fi
sleep 1
done
info "Prüfe Frontend-Health (Port 3000)..."
for i in $(seq 1 15); do
if systemctl is-active --quiet archivmail-web && curl -fsS -o /dev/null --max-time 2 http://127.0.0.1:3000/ 2>/dev/null; then
FRONTEND_OK=1
break
fi
sleep 1
done
echo ""
echo " ┌──────────────────────────────────────┐"
[[ $BACKEND_OK -eq 1 ]] && echo " │ Backend ✓ läuft │" \
|| echo " │ Backend ✗ nicht aktiv │"
[[ $FRONTEND_OK -eq 1 ]] && echo " │ Frontend ✓ läuft │" \
|| echo " │ Frontend ✗ nicht aktiv │"
echo " └──────────────────────────────────────┘"
echo ""
[[ $BACKEND_OK -eq 1 && $FRONTEND_OK -eq 1 ]] && log "Update abgeschlossen." \
|| warn "Ein oder mehrere Dienste sind nicht aktiv. Prüfe: journalctl -u archivmail -u archivmail-web"