#!/usr/bin/env bash set -euo pipefail UI_HOST=""; WEBMAIL_HOST=""; MAIL_HOST=""; SSL_AUTO=0 while [[ $# -gt 0 ]]; do case "$1" in --ui-host) UI_HOST="$2"; shift 2 ;; --webmail-host) WEBMAIL_HOST="$2"; shift 2 ;; --mail-host) MAIL_HOST="$2"; shift 2 ;; --ssl-auto) SSL_AUTO="$2"; shift 2 ;; *) shift ;; esac done PHPV=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;') PHP_FPM_SOCK="/run/php/php${PHPV}-fpm.sock" [ -S "$PHP_FPM_SOCK" ] || PHP_FPM_SOCK="/run/php/php-fpm.sock" APP_DIR="/var/www/mailwolt" NGINX_SITE="/etc/nginx/sites-available/mailwolt.conf" ACME_ROOT="/var/www/letsencrypt" mkdir -p "${ACME_ROOT}/.well-known/acme-challenge" # --- Phase 1: HTTP-only Vhosts mit ACME-Challenge --- cat > "${NGINX_SITE}" </dev/null | grep -q 'inet6' } cert_needs_action() { local domain="$1" local cert="/etc/letsencrypt/live/${domain}/fullchain.pem" [ ! -f "${cert}" ] && return 0 # 0 = gültig für >10 Tage → überspringen; 1 = läuft ab → erneuern openssl x509 -checkend 864000 -noout -in "${cert}" 2>/dev/null && return 1 return 0 } certbot_safe() { local domain="$1" local has_aaaa="" # dig ist nicht immer installiert — Fallback auf getent if command -v dig >/dev/null 2>&1; then has_aaaa=$(dig +short AAAA "${domain}" 2>/dev/null | head -1) || has_aaaa="" else has_aaaa=$(getent ahostsv6 "${domain}" 2>/dev/null | awk '{print $1}' | head -1) || has_aaaa="" fi if [ -n "${has_aaaa}" ] && ! has_global_ipv6; then echo "[!] ${domain}: AAAA-Record vorhanden aber kein IPv6 auf diesem Server — Let's Encrypt würde fehlschlagen. Self-signed wird verwendet." return 1 fi certbot certonly --webroot \ -w "${ACME_ROOT}" \ -d "${domain}" \ --non-interactive --agree-tos \ --email "webmaster@${domain}" \ --no-eff-email } if [ "${SSL_AUTO}" = "1" ]; then for DOMAIN in "${UI_HOST}" "${WEBMAIL_HOST}" "${MAIL_HOST}"; do [ -z "${DOMAIN}" ] && continue if cert_needs_action "${DOMAIN}"; then certbot_safe "${DOMAIN}" || true fi done fi # --- Phase 3: Finale Vhosts --- # Backup der aktuellen nginx-Config (Wiederherstellung bei Fehler) NGINX_BACKUP="${NGINX_SITE}.bak" [ -f "${NGINX_SITE}" ] && cp "${NGINX_SITE}" "${NGINX_BACKUP}" # Cert-Erkennung: fullchain.pem direkt, Renewal-Datei, oder umbenannter Cert-Ordner cert_exists() { local d="$1" [ -z "$d" ] && return 1 [ -f "/etc/letsencrypt/live/${d}/fullchain.pem" ] && return 0 [ -f "/etc/letsencrypt/renewal/${d}.conf" ] && return 0 ls "/etc/letsencrypt/live/${d}"-*/fullchain.pem 2>/dev/null | grep -q . && return 0 return 1 } UI_HAS_CERT=0; WM_HAS_CERT=0; MAIL_HAS_CERT=0 cert_exists "${UI_HOST}" && UI_HAS_CERT=1 cert_exists "${WEBMAIL_HOST}" && WM_HAS_CERT=1 cert_exists "${MAIL_HOST}" && MAIL_HAS_CERT=1 ( if [ "${UI_HAS_CERT}" = "1" ] || [ "${WM_HAS_CERT}" = "1" ]; then # Mindestens ein Web-Cert vorhanden → HTTP-Redirect Block (inkl. MAIL_HOST für ACME-Renewal) cat < "${NGINX_SITE}" # State-Dateien schreiben — korrekt pro Domain, nicht pauschal "done" STATE_DIR="/var/lib/mailwolt/wizard" if [ -d "${STATE_DIR}" ]; then # UI: done nur wenn LE-Cert tatsächlich vorhanden, sonst error if [ -f "${STATE_DIR}/ui" ]; then if [ "${UI_HAS_CERT}" = "1" ]; then printf "done" > "${STATE_DIR}/ui" else printf "error" > "${STATE_DIR}/ui" fi fi # Webmail: done nur wenn LE-Cert tatsächlich vorhanden, sonst error if [ -f "${STATE_DIR}/webmail" ]; then if [ "${WM_HAS_CERT}" = "1" ]; then printf "done" > "${STATE_DIR}/webmail" else printf "error" > "${STATE_DIR}/webmail" fi fi # Mail-Domain: LE-Cert für Postfix/Dovecot if [ -f "${STATE_DIR}/mail" ]; then if [ "${MAIL_HAS_CERT}" = "1" ]; then printf "done" > "${STATE_DIR}/mail" else printf "error" > "${STATE_DIR}/mail" fi fi # done-Signal: immer schreiben damit der 2s-Poll stoppt if [ "${UI_HAS_CERT}" = "1" ] || [ "${WM_HAS_CERT}" = "1" ]; then # Erst done schreiben, dann 6s warten bevor nginx auf HTTPS wechselt — # so hat der Browser Zeit den "Zum Login"-Button zu rendern bevor der Switch passiert printf "1" > "${STATE_DIR}/done" sleep 6 else printf "0" > "${STATE_DIR}/done" fi fi # --- Phase 4: Postfix + Dovecot mit Mail-Domain-Zertifikat konfigurieren --- if [ -n "${MAIL_HOST}" ]; then # Hostname immer setzen, unabhängig vom Zertifikat if command -v postconf >/dev/null 2>&1; then postconf -e "myhostname = ${MAIL_HOST}" postconf -e "myorigin = ${MAIL_HOST}" postconf -e "mydestination = ${MAIL_HOST}, localhost.localdomain, localhost" fi # /etc/aliases bereinigen: root → /dev/null verhindert Bounce-Schleife if [ -f /etc/aliases ]; then # Vorhandene root/clamav-Zeilen ersetzen oder am Ende ergänzen if grep -q '^root:' /etc/aliases; then sed -i 's|^root:.*|root: /dev/null|' /etc/aliases else echo 'root: /dev/null' >> /etc/aliases fi if grep -q '^clamav:' /etc/aliases; then sed -i 's|^clamav:.*|clamav: /dev/null|' /etc/aliases else echo 'clamav: /dev/null' >> /etc/aliases fi command -v newaliases >/dev/null 2>&1 && newaliases fi fi if [ -n "${MAIL_HOST}" ] && [ "${MAIL_HAS_CERT}" = "1" ]; then MAIL_CERT="/etc/letsencrypt/live/${MAIL_HOST}/fullchain.pem" MAIL_KEY="/etc/letsencrypt/live/${MAIL_HOST}/privkey.pem" # Postfix TLS if command -v postconf >/dev/null 2>&1; then postconf -e "smtpd_tls_cert_file = ${MAIL_CERT}" postconf -e "smtpd_tls_key_file = ${MAIL_KEY}" postconf -e "smtp_tls_cert_file = ${MAIL_CERT}" postconf -e "smtp_tls_key_file = ${MAIL_KEY}" systemctl reload postfix 2>/dev/null || true fi # Dovecot if [ -d /etc/dovecot/conf.d ]; then cat > /etc/dovecot/conf.d/10-ssl.conf </dev/null || true fi fi # --- Phase 5: .env aktualisieren + Assets neu bauen --- ENV_FILE="${APP_DIR}/.env" if [ -n "${UI_HOST}" ] && [ -f "${ENV_FILE}" ]; then APP_SCHEME="http" [ "${UI_HAS_CERT}" = "1" ] && APP_SCHEME="https" REVERB_PORT_VAL="443" [ "${UI_HAS_CERT}" != "1" ] && REVERB_PORT_VAL="80" _env_set() { local key="$1" val="$2" if grep -q "^${key}=" "${ENV_FILE}"; then sed -i "s|^${key}=.*|${key}=${val}|" "${ENV_FILE}" else echo "${key}=${val}" >> "${ENV_FILE}" fi } _env_set "APP_HOST" "${UI_HOST}" _env_set "APP_URL" "${APP_SCHEME}://\${APP_HOST}" # Reverb: Client-seitige Verbindung über nginx-Proxy (Domain + 443) _env_set "REVERB_HOST" "\${APP_HOST}" _env_set "REVERB_PORT" "${REVERB_PORT_VAL}" _env_set "REVERB_SCHEME" "${APP_SCHEME}" _env_set "REVERB_PATH" "/ws" # Reverb: Server-seitiger Prozess (lokal auf 8080) _env_set "REVERB_SERVER_HOST" "127.0.0.1" _env_set "REVERB_SERVER_PORT" "8080" _env_set "REVERB_SERVER_SCHEME" "http" _env_set "REVERB_SERVER_PATH" "" # Vite-Variablen (werden ins JS-Bundle eingebacken) _env_set "VITE_REVERB_HOST" "\${REVERB_HOST}" _env_set "VITE_REVERB_PORT" "\${REVERB_PORT}" _env_set "VITE_REVERB_SCHEME" "\${REVERB_SCHEME}" _env_set "VITE_REVERB_PATH" "\${REVERB_PATH}" # Assets neu bauen damit wsHost im JS den richtigen Domain-Namen hat if command -v npm >/dev/null 2>&1 && [ -f "${APP_DIR}/package.json" ]; then cd "${APP_DIR}" && npm run build --silent 2>/dev/null || true fi # Laravel-Caches leeren (als App-User, nicht root) APP_USER=$(stat -c '%U' "${APP_DIR}/artisan" 2>/dev/null || echo "www-data") su -s /bin/sh "${APP_USER}" -c "php '${APP_DIR}/artisan' optimize:clear --quiet" 2>/dev/null || true su -s /bin/sh "${APP_USER}" -c "php '${APP_DIR}/artisan' config:cache --quiet" 2>/dev/null || true su -s /bin/sh "${APP_USER}" -c "php '${APP_DIR}/artisan' route:cache --quiet" 2>/dev/null || true fi if nginx -t 2>/dev/null; then systemctl reload nginx echo "mailwolt-apply-domains fertig" else echo "[!] nginx -t fehlgeschlagen — alte Konfiguration wird wiederhergestellt" [ -f "${NGINX_BACKUP}" ] && cp "${NGINX_BACKUP}" "${NGINX_SITE}" nginx -t && systemctl reload nginx echo "[!] mailwolt-apply-domains mit Fehler beendet" exit 1 fi