#!/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//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} ${SSL_DIR} ReadOnlyPaths=${CONFIG_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"