#!/bin/bash # archivmail Installer # Unterstützte Systeme: Debian 12 / 13 # # Aufruf: # bash install.sh # Interaktiv (Modus-Auswahl) # INSTALL_MODE=native bash install.sh # Nativ (systemd, direkter Build) # INSTALL_MODE=docker bash install.sh # Docker (alle Abhängigkeiten im Container) set -euo pipefail export DEBIAN_FRONTEND=noninteractive 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 install.sh" # ── Gemeinsame Variablen ────────────────────────────────────────────────────── DB_PASSWORD="${DB_PASSWORD:-$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32)}" API_SECRET="${API_SECRET:-$(openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c 64)}" WEBHOOK_SECRET="${WEBHOOK_SECRET:-$(openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c 40)}" INSTALL_DIR="/opt/archivmail" STORE_DIR="/var/archivmail" LOG_DIR="/var/log/archivmail" CONFIG_DIR="/etc/archivmail" SSL_DIR="/etc/ssl/archivmail" AM_USER="archivmail" FQDN="$(hostname -f 2>/dev/null || hostname)" REPO_URL="${REPO_URL:-https://github.com/yourorg/archivmail.git}" # ── Banner ──────────────────────────────────────────────────────────────────── echo "" echo " ╔══════════════════════════════════════════╗" echo " ║ archivmail Installer v2.0 ║" echo " ╚══════════════════════════════════════════╝" echo "" info "Hostname: $FQDN" echo "" # ── Modus-Auswahl ───────────────────────────────────────────────────────────── INSTALL_MODE="${INSTALL_MODE:-}" if [[ -z "$INSTALL_MODE" ]]; then echo " Installations-Modus wählen:" echo "" echo " 1) Nativ — Go/Node.js direkt auf dem Server kompiliert und gebaut" echo " (systemd-Dienste, PostgreSQL lokal, kein Docker nötig)" echo "" echo " 2) Docker — Alle Abhängigkeiten (Go, Xapian, Node.js, PostgreSQL)" echo " laufen in Containern. Kein Build-Toolchain auf dem Host." echo " Deployment via GitHub Webhook (git pull → docker compose up)" echo "" read -rp " Auswahl [1/2]: " _choice case "$_choice" in 1) INSTALL_MODE="native" ;; 2) INSTALL_MODE="docker" ;; *) die "Ungültige Auswahl: $_choice" ;; esac fi [[ "$INSTALL_MODE" == "native" || "$INSTALL_MODE" == "docker" ]] \ || die "INSTALL_MODE muss 'native' oder 'docker' sein (aktuell: $INSTALL_MODE)" echo "" info "Modus: $INSTALL_MODE" echo "" # ══════════════════════════════════════════════════════════════════════════════ # DOCKER-INSTALLATION # ══════════════════════════════════════════════════════════════════════════════ install_docker() { # GitHub-Repo abfragen wenn nicht gesetzt if [[ "$REPO_URL" == *"yourorg"* ]]; then read -rp " GitHub-Repository URL (z.B. https://github.com/org/archivmail.git): " REPO_URL [[ -n "$REPO_URL" ]] || die "Kein Repository angegeben." fi # ── Docker installieren ─────────────────────────────────────────────────── info "Installiere Docker..." if command -v docker &>/dev/null; then log "Docker bereits installiert: $(docker --version)" else apt-get update -qq apt-get install -y -qq ca-certificates curl gnupg install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/debian/gpg \ | gpg --dearmor -o /etc/apt/keyrings/docker.gpg chmod a+r /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \ > /etc/apt/sources.list.d/docker.list apt-get update -qq apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin systemctl enable --now docker log "Docker $(docker --version) installiert" fi # ── Hilfspakete ─────────────────────────────────────────────────────────── info "Installiere Hilfspakete..." apt-get install -y -qq git curl nginx openssl logrotate log "Pakete installiert" # ── webhook-Binary (adnanh/webhook) ────────────────────────────────────── info "Installiere webhook-Empfänger..." if ! command -v webhook &>/dev/null; then WEBHOOK_BIN_VERSION="2.8.2" _arch="$(dpkg --print-architecture)" [[ "$_arch" == "amd64" ]] && _arch="linux_amd64" [[ "$_arch" == "arm64" ]] && _arch="linux_arm64" curl -fsSL \ "https://github.com/adnanh/webhook/releases/download/${WEBHOOK_BIN_VERSION}/webhook-${_arch}.tar.gz" \ | tar -xz -C /usr/local/bin --strip-components=1 "webhook-${_arch}/webhook" chmod +x /usr/local/bin/webhook log "webhook ${WEBHOOK_BIN_VERSION} installiert" else log "webhook bereits installiert" fi # ── Verzeichnisse ───────────────────────────────────────────────────────── info "Erstelle Verzeichnisse..." mkdir -p "$CONFIG_DIR" "$LOG_DIR" log "Verzeichnisse erstellt" # ── Repository klonen ───────────────────────────────────────────────────── info "Klone Repository..." if [[ -d "$INSTALL_DIR/.git" ]]; then log "Repository existiert bereits — git pull" git -C "$INSTALL_DIR" pull origin main else git clone "$REPO_URL" "$INSTALL_DIR" log "Repository geklont: $INSTALL_DIR" fi # ── Keyfile ─────────────────────────────────────────────────────────────── info "Generiere Verschlüsselungs-Keyfile..." if [[ ! -f "$CONFIG_DIR/keyfile" ]]; then openssl rand -base64 32 > "$CONFIG_DIR/keyfile" chmod 400 "$CONFIG_DIR/keyfile" log "Keyfile generiert: $CONFIG_DIR/keyfile" else log "Keyfile existiert bereits – wird nicht überschrieben" fi # ── TLS-Zertifikat ──────────────────────────────────────────────────────── info "Erstelle selbstsigniertes TLS-Zertifikat..." mkdir -p "$SSL_DIR" if [[ ! -f "$SSL_DIR/archivmail.crt" ]]; then SERVER_IP="$(hostname -I | awk '{print $1}')" openssl req -x509 -nodes -days 3650 -newkey rsa:4096 \ -keyout "$SSL_DIR/archivmail.key" \ -out "$SSL_DIR/archivmail.crt" \ -subj "/CN=${FQDN}/O=archivmail/C=DE" \ -addext "subjectAltName=DNS:${FQDN},DNS:$(hostname -s),IP:${SERVER_IP}" \ 2>/dev/null chmod 640 "$SSL_DIR/archivmail.key" chmod 644 "$SSL_DIR/archivmail.crt" log "TLS-Zertifikat erstellt" else log "TLS-Zertifikat existiert bereits – wird nicht überschrieben" fi # ── config.yml aus Vorlage ──────────────────────────────────────────────── info "Erstelle Konfigurationsdatei..." if [[ ! -f "$CONFIG_DIR/config.yml" ]]; then sed \ -e "s/CHANGE_ME_DB_PASSWORD/$DB_PASSWORD/g" \ -e "s/CHANGE_ME_64_CHAR_SECRET/$API_SECRET/g" \ -e "s/mail\.example\.com/$FQDN/g" \ "$INSTALL_DIR/config/config.docker.yml.example" \ > "$CONFIG_DIR/config.yml" chmod 640 "$CONFIG_DIR/config.yml" log "config.yml erstellt: $CONFIG_DIR/config.yml" else log "config.yml existiert bereits – wird nicht überschrieben" fi # ── .env für docker-compose ─────────────────────────────────────────────── info "Erstelle .env..." if [[ ! -f "$INSTALL_DIR/.env" ]]; then printf 'DB_PASSWORD=%s\n' "$DB_PASSWORD" > "$INSTALL_DIR/.env" chmod 600 "$INSTALL_DIR/.env" log ".env erstellt" else log ".env existiert bereits – wird nicht überschrieben" fi # ── deploy.sh ausführbar ────────────────────────────────────────────────── chmod +x "$INSTALL_DIR/scripts/deploy.sh" # ── logrotate ───────────────────────────────────────────────────────────── cat > /etc/logrotate.d/archivmail < /etc/systemd/system/archivmail-webhook.service < /etc/nginx/sites-available/archivmail < "$summary_file" </dev/null 2>&1; then GO_BIN=$(find /usr/lib/go-*/bin /usr/local/go/bin -name "go" 2>/dev/null | head -1) [[ -n "$GO_BIN" ]] || die "go binary nicht gefunden nach Installation" ln -sf "$GO_BIN" /usr/local/bin/go log "go binary verlinkt: $GO_BIN → /usr/local/bin/go" fi log "go $(go version | awk '{print $3}')" # ── 2. Systembenutzer ───────────────────────────────────────────────────── info "Lege Systembenutzer '$AM_USER' an..." id "$AM_USER" &>/dev/null \ && log "Benutzer '$AM_USER' existiert bereits" \ || { useradd --system --shell /bin/false --home "$STORE_DIR" --create-home "$AM_USER"; log "Benutzer angelegt"; } # ── 3. Verzeichnisstruktur ──────────────────────────────────────────────── info "Erstelle Verzeichnisstruktur..." mkdir -p "$STORE_DIR/store" "$STORE_DIR/astore" "$STORE_DIR/xapian" mkdir -p "$CONFIG_DIR" "$LOG_DIR" "$INSTALL_DIR" "$INSTALL_DIR/web" "$SSL_DIR" chown -R "$AM_USER:$AM_USER" "$STORE_DIR" "$LOG_DIR" chmod 755 "$STORE_DIR" chmod 700 "$STORE_DIR/store" "$STORE_DIR/astore" "$STORE_DIR/xapian" log "Verzeichnisse erstellt" # ── 4. Keyfile ──────────────────────────────────────────────────────────── info "Generiere Verschlüsselungs-Keyfile..." if [ ! -f "$CONFIG_DIR/keyfile" ]; then openssl rand -base64 32 > "$CONFIG_DIR/keyfile" chmod 400 "$CONFIG_DIR/keyfile" chown "$AM_USER:$AM_USER" "$CONFIG_DIR/keyfile" log "Keyfile generiert: $CONFIG_DIR/keyfile" else log "Keyfile existiert bereits – wird nicht überschrieben" fi # ── 5. TLS-Zertifikat ───────────────────────────────────────────────────── info "Erstelle selbstsigniertes TLS-Zertifikat..." if [ ! -f "$SSL_DIR/archivmail.crt" ]; then SERVER_IP="$(hostname -I | awk '{print $1}')" openssl req -x509 -nodes -days 3650 -newkey rsa:4096 \ -keyout "$SSL_DIR/archivmail.key" \ -out "$SSL_DIR/archivmail.crt" \ -subj "/CN=${FQDN}/O=archivmail/C=DE" \ -addext "subjectAltName=DNS:${FQDN},DNS:$(hostname -s),IP:${SERVER_IP}" \ 2>/dev/null chmod 640 "$SSL_DIR/archivmail.key" chmod 644 "$SSL_DIR/archivmail.crt" chown "root:$AM_USER" "$SSL_DIR/archivmail.key" log "TLS-Zertifikat erstellt: $SSL_DIR/archivmail.crt (FQDN: $FQDN, IP: $SERVER_IP)" else log "TLS-Zertifikat existiert bereits – wird nicht überschrieben" fi # ── 6. PostgreSQL ───────────────────────────────────────────────────────── info "Richte PostgreSQL ein..." systemctl enable postgresql --quiet systemctl start postgresql # Falls config.yml schon existiert, DB-Passwort daraus lesen (verhindert Passwort-Mismatch bei Re-Install) if [ -f "$CONFIG_DIR/config.yml" ]; then EXISTING_PW=$(grep -A5 '^database:' "$CONFIG_DIR/config.yml" | awk '/password:/{print $2}' | head -1) [[ -n "$EXISTING_PW" ]] && DB_PASSWORD="$EXISTING_PW" && info "DB-Passwort aus vorhandener config.yml übernommen" fi su -c "psql -tc \"SELECT 1 FROM pg_roles WHERE rolname='archivmail'\" | grep -q 1 \ && psql -c \"ALTER USER archivmail WITH PASSWORD '$DB_PASSWORD'\" \ || psql -c \"CREATE USER archivmail WITH PASSWORD '$DB_PASSWORD'\"" postgres su -c "psql -tc \"SELECT 1 FROM pg_database WHERE datname='archivmail'\" | grep -q 1 || \ psql -c \"CREATE DATABASE archivmail OWNER archivmail\"" postgres su -c "psql archivmail -c \"GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO archivmail;\"" postgres su -c "psql archivmail -c \"GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO archivmail;\"" postgres su -c "psql archivmail -c \"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO archivmail;\"" postgres su -c "psql archivmail -c \"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO archivmail;\"" postgres log "PostgreSQL eingerichtet" # ── 7. Konfiguration ────────────────────────────────────────────────────── info "Erstelle Konfigurationsdatei..." if [ ! -f "$CONFIG_DIR/config.yml" ]; then cat > "$CONFIG_DIR/config.yml" << CONFIG # archivmail Konfiguration – generiert am $(date -u +%Y-%m-%dT%H:%M:%SZ) server: api_port: 8080 smtp_port: 2525 database: host: 127.0.0.1 port: 5432 name: archivmail user: archivmail password: ${DB_PASSWORD} sslmode: disable storage: store_path: ${STORE_DIR}/store astore_path: ${STORE_DIR}/astore xapian_path: ${STORE_DIR}/xapian keyfile: ${CONFIG_DIR}/keyfile api: bind: ":8080" secret: ${API_SECRET} trusted_proxies: - 127.0.0.1 secure_cookies: true index: path: ${STORE_DIR}/xapian backend: xapian batch_size: 100 audit: log_path: ${LOG_DIR}/audit.log retention_days: 0 smtp: enabled: true bind: ":2525" domain: "${FQDN}" tls_cert: ${SSL_DIR}/archivmail.crt tls_key: ${SSL_DIR}/archivmail.key allowed_ips: - 127.0.0.1 imap_server: enabled: true bind: ":993" tls_cert: ${SSL_DIR}/archivmail.crt tls_key: ${SSL_DIR}/archivmail.key CONFIG chmod 640 "$CONFIG_DIR/config.yml" chown "root:$AM_USER" "$CONFIG_DIR/config.yml" log "Konfiguration erstellt: $CONFIG_DIR/config.yml" else log "config.yml existiert bereits – wird nicht überschrieben" fi # ── 8. Nginx ────────────────────────────────────────────────────────────── info "Konfiguriere nginx (HTTP → HTTPS + TLS)..." cat > /etc/nginx/sites-available/archivmail << NGINX # HTTP → HTTPS Redirect server { listen 80; server_name ${FQDN} _; return 301 https://\$host\$request_uri; } # HTTPS server { listen 443 ssl; http2 on; server_name ${FQDN} _; ssl_certificate ${SSL_DIR}/archivmail.crt; ssl_certificate_key ${SSL_DIR}/archivmail.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; add_header Strict-Transport-Security "max-age=31536000" always; add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options SAMEORIGIN always; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; client_max_body_size 512M; } location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; proxy_cache_bypass \$http_upgrade; } access_log /var/log/nginx/archivmail.access.log; error_log /var/log/nginx/archivmail.error.log; } NGINX ln -sf /etc/nginx/sites-available/archivmail /etc/nginx/sites-enabled/archivmail rm -f /etc/nginx/sites-enabled/default nginx -t systemctl enable nginx --quiet systemctl restart nginx log "nginx konfiguriert" # ── 9. logrotate ────────────────────────────────────────────────────────── cat > /etc/logrotate.d/archivmail << LOGROTATE ${LOG_DIR}/audit.log { daily rotate 365 compress delaycompress missingok notifempty create 640 ${AM_USER} ${AM_USER} } LOGROTATE log "logrotate konfiguriert" # ── 10. systemd Units ───────────────────────────────────────────────────── info "Erstelle systemd Units..." cat > /etc/systemd/system/archivmail.service << UNIT [Unit] Description=archivmail Mail Archive Daemon After=network.target postgresql.service Requires=postgresql.service [Service] Type=simple User=${AM_USER} Group=${AM_USER} AmbientCapabilities=CAP_NET_BIND_SERVICE CapabilityBoundingSet=CAP_NET_BIND_SERVICE ExecStart=${INSTALL_DIR}/archivmail --config ${CONFIG_DIR}/config.yml 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 Environment=NEXT_PUBLIC_API_URL=http://127.0.0.1:8080 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 erstellt und aktiviert" # ── 11. Erstes Deployment via update.sh ─────────────────────────────────── info "Installiere update.sh und führe erstes Deployment durch..." _update_url="${REPO_URL/\.git/}/raw/branch/main/update.sh" # GitHub-URLs anpassen (raw.githubusercontent.com statt github.com/raw/...) if [[ "$REPO_URL" == *"github.com"* ]]; then _repo_path="${REPO_URL#*github.com/}" _repo_path="${_repo_path%.git}" _update_url="https://raw.githubusercontent.com/${_repo_path}/main/update.sh" fi curl -fsSL "$_update_url" -o "$INSTALL_DIR/update.sh" 2>/dev/null \ || { warn "update.sh konnte nicht geladen werden – überspringe Deployment"; } if [ -f "$INSTALL_DIR/update.sh" ]; then chmod +x "$INSTALL_DIR/update.sh" log "update.sh installiert" info "Führe erstes Deployment durch..." bash "$INSTALL_DIR/update.sh" for _i in $(seq 1 15); do _pw=$(journalctl -u archivmail --no-pager -n 100 | grep 'admin' | grep -oP ':\s+\K[0-9a-f]{16,}' | tail -1) [[ -n "$_pw" ]] && break sleep 1 done PW_SUPERADMIN=$(journalctl -u archivmail --no-pager -n 100 | grep 'superadmin' | grep -oP ':\s+\K\S+' | tail -1) PW_ADMIN=$(journalctl -u archivmail --no-pager -n 100 | grep ' admin ' | grep -v superadmin | grep -oP ':\s+\K\S+' | tail -1) PW_AUDITOR=$(journalctl -u archivmail --no-pager -n 100 | grep 'auditor' | grep -oP ':\s+\K\S+' | tail -1) [[ -n "$PW_ADMIN" ]] || PW_ADMIN="(nicht gefunden — journalctl -u archivmail | grep admin)" [[ -n "$PW_AUDITOR" ]] || PW_AUDITOR="(nicht gefunden — journalctl -u archivmail | grep auditor)" [[ -n "$PW_SUPERADMIN" ]] || PW_SUPERADMIN="(nicht gefunden — journalctl -u archivmail | grep superadmin)" else warn "update.sh fehlt — manuell: bash $INSTALL_DIR/update.sh" PW_SUPERADMIN="(noch nicht generiert)"; PW_ADMIN="(noch nicht generiert)"; PW_AUDITOR="(noch nicht generiert)" fi # ── Zusammenfassung ─────────────────────────────────────────────────────── SUMMARY_FILE="$CONFIG_DIR/install-summary.txt" cat > "$SUMMARY_FILE" << SUMMARY archivmail Native-Installation — $(date '+%d.%m.%Y %H:%M:%S') Server: $FQDN === ZUGANGSDATEN === Datenbank: archivmail / $DB_PASSWORD API-Secret: $API_SECRET Web-Logins (UNBEDINGT ÄNDERN!): superadmin@archivmail / $PW_SUPERADMIN admin@archivmail / $PW_ADMIN auditor@archivmail / $PW_AUDITOR === DIENSTE === Web (HTTPS): https://$FQDN IMAP (TLS): $FQDN:993 SMTP: $FQDN:2525 === DATEIPFADE === Konfiguration: $CONFIG_DIR/config.yml Keyfile: $CONFIG_DIR/keyfile TLS-Zertifikat: $SSL_DIR/archivmail.crt Mail-Speicher: $STORE_DIR/ Logs: $LOG_DIR/ Updater: $INSTALL_DIR/update.sh SUMMARY chmod 600 "$SUMMARY_FILE" log "Zusammenfassung: $SUMMARY_FILE" # ── Abschluss ───────────────────────────────────────────────────────────── echo "" echo " ╔══════════════════════════════════════════════════════════╗" echo " ║ Installation abgeschlossen! ║" echo " ╚══════════════════════════════════════════════════════════╝" echo "" echo " Web (HTTPS): https://$FQDN" echo " IMAP (TLS): $FQDN:993" echo " SMTP: $FQDN:2525" echo "" echo " ┌─────────────────────────────────────────────────────────┐" echo " │ ANGELEGTE BENUTZER & PASSWÖRTER │" echo " ├─────────────────────────────────────────────────────────┤" printf " │ DB archivmail: %-38s │\n" "$DB_PASSWORD" echo " ├─────────────────────────────────────────────────────────┤" echo " │ Web-Logins (UNBEDINGT ÄNDERN!): │" printf " │ superadmin / %-38s│\n" "$PW_SUPERADMIN" printf " │ admin / %-38s│\n" "$PW_ADMIN" printf " │ auditor / %-38s│\n" "$PW_AUDITOR" echo " └─────────────────────────────────────────────────────────┘" echo "" warn "Zusammenfassung mit Passwörtern: $SUMMARY_FILE" warn "Standardpasswörter unbedingt nach dem ersten Login ändern!" echo "" } # ══════════════════════════════════════════════════════════════════════════════ # MODUS AUSFÜHREN # ══════════════════════════════════════════════════════════════════════════════ case "$INSTALL_MODE" in docker) install_docker ;; native) install_native ;; esac