diff --git a/mailwolt-installer b/mailwolt-installer
deleted file mode 160000
index c443c5a..0000000
--- a/mailwolt-installer
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit c443c5a42641f725bc3794fcad88b62a2d48f56a
diff --git a/mailwolt-installer/.idea/.gitignore b/mailwolt-installer/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/mailwolt-installer/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/mailwolt-installer/.idea/copilot.data.migration.agent.xml b/mailwolt-installer/.idea/copilot.data.migration.agent.xml
new file mode 100644
index 0000000..4ea72a9
--- /dev/null
+++ b/mailwolt-installer/.idea/copilot.data.migration.agent.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mailwolt-installer/.idea/copilot.data.migration.ask.xml b/mailwolt-installer/.idea/copilot.data.migration.ask.xml
new file mode 100644
index 0000000..7ef04e2
--- /dev/null
+++ b/mailwolt-installer/.idea/copilot.data.migration.ask.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mailwolt-installer/.idea/copilot.data.migration.ask2agent.xml b/mailwolt-installer/.idea/copilot.data.migration.ask2agent.xml
new file mode 100644
index 0000000..1f2ea11
--- /dev/null
+++ b/mailwolt-installer/.idea/copilot.data.migration.ask2agent.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mailwolt-installer/.idea/copilot.data.migration.edit.xml b/mailwolt-installer/.idea/copilot.data.migration.edit.xml
new file mode 100644
index 0000000..8648f94
--- /dev/null
+++ b/mailwolt-installer/.idea/copilot.data.migration.edit.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mailwolt-installer/.idea/mailwolt-installer.iml b/mailwolt-installer/.idea/mailwolt-installer.iml
new file mode 100644
index 0000000..c956989
--- /dev/null
+++ b/mailwolt-installer/.idea/mailwolt-installer.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mailwolt-installer/.idea/misc.xml b/mailwolt-installer/.idea/misc.xml
new file mode 100644
index 0000000..3ce3588
--- /dev/null
+++ b/mailwolt-installer/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mailwolt-installer/.idea/modules.xml b/mailwolt-installer/.idea/modules.xml
new file mode 100644
index 0000000..2492f5c
--- /dev/null
+++ b/mailwolt-installer/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mailwolt-installer/.idea/php.xml b/mailwolt-installer/.idea/php.xml
new file mode 100644
index 0000000..f324872
--- /dev/null
+++ b/mailwolt-installer/.idea/php.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mailwolt-installer/.idea/vcs.xml b/mailwolt-installer/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/mailwolt-installer/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mailwolt-installer/config/nginx/site.conf.tmpl b/mailwolt-installer/config/nginx/site.conf.tmpl
new file mode 100644
index 0000000..935dfe0
--- /dev/null
+++ b/mailwolt-installer/config/nginx/site.conf.tmpl
@@ -0,0 +1,17 @@
+# ===================== HTTP (Port 80) =====================
+server {
+ listen 80 default_server;
+ listen [::]:80 default_server;
+ server_name _;
+
+ # ACME HTTP-01
+ location ^~ /.well-known/acme-challenge/ {
+ root /var/www/letsencrypt;
+ allow all;
+ }
+
+ __HTTP_BODY__
+}
+
+# ===================== HTTPS (Port 443) ====================
+__SSL_SERVER_BLOCK__
\ No newline at end of file
diff --git a/mailwolt-installer/install.sh b/mailwolt-installer/install.sh
new file mode 100755
index 0000000..d1e7429
--- /dev/null
+++ b/mailwolt-installer/install.sh
@@ -0,0 +1,909 @@
+#!/usr/bin/env bash
+(
+export HISTFILE=
+set +o history
+set -euo pipefail
+
+##############################################
+# MailWolt #
+# Bootstrap Installer v1.0 #
+##############################################
+
+# ===== CLI-Flags (-dev / -stag) =====
+APP_ENV="${APP_ENV:-production}"
+APP_DEBUG="${APP_DEBUG:-false}"
+
+DEV_MODE=0
+STAG_MODE=0
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ -dev)
+ DEV_MODE=1
+ APP_ENV="local"
+ APP_DEBUG="true"
+ ;;
+ -stag|-staging)
+ STAG_MODE=1
+ APP_ENV="staging"
+ APP_DEBUG="false"
+ ;;
+ esac
+ shift
+done
+
+# ===== Branding & Pfade =====
+APP_NAME="${APP_NAME:-MailWolt}"
+
+APP_USER="${APP_USER:-mailwolt}"
+APP_GROUP="${APP_GROUP:-www-data}"
+
+APP_DIR="/var/www/${APP_USER}"
+
+ADMIN_USER="${APP_USER}"
+ADMIN_EMAIL="admin@localhost"
+ADMIN_PASS="ChangeMe"
+
+CONF_BASE="/etc/${APP_USER}"
+CERT_DIR="${CONF_BASE}/ssl"
+CERT="${CERT_DIR}/cert.pem"
+KEY="${CERT_DIR}/key.pem"
+
+NGINX_SITE="/etc/nginx/sites-available/${APP_USER}.conf"
+NGINX_SITE_LINK="/etc/nginx/sites-enabled/${APP_USER}.conf"
+
+DB_NAME="${DB_NAME:-${APP_USER}}"
+DB_USER="${DB_USER:-${APP_USER}}"
+DB_PASS="${DB_PASS:-$(openssl rand -hex 16)}"
+
+GIT_REPO="${GIT_REPO:-http://10.10.20.81:3000/boban/mailwolt.git}"
+GIT_BRANCH="${GIT_BRANCH:-main}"
+
+NODE_SETUP="${NODE_SETUP:-deb}"
+
+# ===== Styling =====
+GREEN="\033[1;32m"; YELLOW="\033[1;33m"; RED="\033[1;31m"; CYAN="\033[1;36m"; GREY="\033[0;90m"; NC="\033[0m"
+BAR="──────────────────────────────────────────────────────────────────────────────"
+
+header() {
+ echo -e "${CYAN}${BAR}${NC}"
+ echo -e "${CYAN} 888b d888 d8b 888 888 888 888 888 ${NC}"
+ echo -e "${CYAN} 8888b d8888 Y8P 888 888 o 888 888 888 ${NC}"
+ echo -e "${CYAN} 88888b.d88888 888 888 d8b 888 888 888 ${NC}"
+ echo -e "${CYAN} 888Y88888P888 8888b. 888 888 888 d888b 888 .d88b. 888 888888 ${NC}"
+ echo -e "${CYAN} 888 Y888P 888 '88b 888 888 888d88888b888 d88''88b 888 888 ${NC}"
+ echo -e "${CYAN} 888 Y8P 888 .d888888 888 888 88888P Y88888 888 888 888 888 ${NC}"
+ echo -e "${CYAN} 888 ' 888 888 888 888 888 8888P Y8888 Y88..88P 888 Y88b. ${NC}"
+ echo -e "${CYAN} 888 888 'Y888888 888 888 888P Y888 'Y88P' 888 'Y888 ${NC}"
+ echo -e "${CYAN}${BAR}${NC}"
+ echo
+}
+
+print_bootstrap_summary() {
+ local ip="$1"
+ local admin_user="$2"
+ local admin_pass="$3"
+ local GREEN="\033[1;32m"
+ local CYAN="\033[1;36m"
+ local GREY="\033[0;90m"
+ local YELLOW="\033[1;33m"
+ local RED="\033[1;31m"
+ local NC="\033[0m"
+ local BAR="${BAR:-──────────────────────────────────────────────────────────────────────────────}"
+ local scheme="http"
+ if [ -s "${CERT}" ] && [ -s "${KEY}" ]; then scheme="https"; fi
+
+ echo
+ echo -e "${GREEN}${BAR}${NC}"
+ echo -e "${GREEN} ✔ ${APP_NAME} Bootstrap erfolgreich abgeschlossen${NC}"
+ echo -e "${GREEN}${BAR}${NC}"
+ echo -e " Bootstrap-Login (nur für ERSTEN Login & Wizard):"
+ echo -e " User: ${YELLOW}${admin_user}${NC}"
+ echo -e " Passwort: ${RED}${admin_pass}${NC}"
+ echo
+ echo -e " Aufruf: ${CYAN}${scheme}://${ip}${NC}"
+ echo -e " Laravel Root: ${GREY}${APP_DIR}${NC}"
+ echo -e " Nginx Site: ${GREY}${NGINX_SITE}${NC}"
+ echo -e " Self-signed Cert: ${GREY}${CERT_DIR}/{cert.pem,key.pem}${NC}"
+ echo -e " Postfix/Dovecot Ports aktiv: ${GREY}25, 465, 587, 110, 995, 143, 993${NC}"
+ echo -e " Rspamd/OpenDKIM: ${GREY}aktiv (DKIM-Keys später im Wizard)${NC}"
+ echo -e " Monit (Watchdog): ${GREY}installiert, NICHT aktiviert${NC}"
+ echo -e "${GREEN}${BAR}${NC}"
+ echo
+}
+
+log() { echo -e "${GREEN}[+]${NC} $*"; }
+warn() { echo -e "${YELLOW}[!]${NC} $*"; }
+err() { echo -e "${RED}[x]${NC} $*"; }
+require_root() { [ "$(id -u)" -eq 0 ] || { err "Bitte als root ausführen."; exit 1; }; }
+
+detect_ip() {
+ local ip
+ ip="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for (i=1;i<=NF;i++) if ($i=="src") {print $(i+1); exit}}')" || true
+ [[ -n "${ip:-}" ]] || ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
+ [[ -n "${ip:-}" ]] || { err "Konnte Server-IP nicht ermitteln."; exit 1; }
+ echo "$ip"
+}
+
+gen() { head -c 512 /dev/urandom | tr -dc 'A-Za-z0-9' | head -c "${1:-28}" || true; }
+pw() { gen 28; }
+short() { gen 16; }
+
+# ===== Start =====
+require_root
+header
+
+SERVER_IP="$(detect_ip)"
+MAIL_HOSTNAME="${MAIL_HOSTNAME:-"bootstrap.local"}"
+TZ="${TZ:-""}"
+
+echo -e "${GREY}Server-IP erkannt: ${SERVER_IP}${NC}"
+[ -n "$TZ" ] && { ln -fs "/usr/share/zoneinfo/${TZ}" /etc/localtime || true; }
+
+log "Paketquellen aktualisieren…"
+export DEBIAN_FRONTEND=noninteractive
+apt-get update -y
+
+# ---- MariaDB-Workaround ----
+log "MariaDB-Workaround vorbereiten…"
+mkdir -p /etc/mysql /etc/mysql/mariadb.conf.d
+[ -f /etc/mysql/mariadb.cnf ] || echo '!include /etc/mysql/mariadb.conf.d/*.cnf' > /etc/mysql/mariadb.cnf
+
+# ---- Basis-Pakete installieren ----
+log "Pakete installieren… (dies kann einige Minuten dauern)"
+export DEBIAN_FRONTEND=noninteractive
+apt-get -y -o Dpkg::Options::="--force-confdef" \
+ -o Dpkg::Options::="--force-confold" install \
+ postfix postfix-mysql \
+ dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-mysql \
+ mariadb-server mariadb-client \
+ redis-server \
+ rspamd \
+ opendkim opendkim-tools \
+ nginx \
+ php php-fpm php-cli php-mbstring php-xml php-curl php-zip php-mysql php-redis php-gd unzip curl \
+ composer git \
+ certbot python3-certbot-nginx \
+ fail2ban \
+ ca-certificates rsyslog sudo openssl netcat-openbsd monit acl
+
+NGINX_HTTP2_SUPPORTED=0
+if nginx -V 2>&1 | grep -q http_v2; then
+ NGINX_HTTP2_SUPPORTED=1
+ log "Nginx: HTTP/2-Unterstützung vorhanden ✅"
+else
+ warn "Nginx: HTTP/2-Modul nicht gefunden – wechsle auf 'nginx-full'…"
+ apt-get install -y nginx-full || true
+ systemctl restart nginx || true
+ if nginx -V 2>&1 | grep -q http_v2; then
+ NGINX_HTTP2_SUPPORTED=1
+ log "Nginx: HTTP/2 jetzt verfügbar ✅"
+ else
+ warn "HTTP/2 weiterhin nicht verfügbar (verwende SSL ohne http2)."
+ fi
+fi
+
+if [ "$NGINX_HTTP2_SUPPORTED" = "1" ]; then
+ NGINX_HTTP2_SUFFIX=" http2"
+else
+ NGINX_HTTP2_SUFFIX=""
+fi
+
+# ===== Verzeichnisse / User =====
+log "Verzeichnisse und Benutzer anlegen…"
+mkdir -p "${CERT_DIR}" /etc/postfix/sql /etc/dovecot/conf.d /etc/rspamd/local.d /var/mail/vhosts
+id vmail >/dev/null 2>&1 || adduser --system --group --home /var/mail vmail
+chown -R vmail:vmail /var/mail
+
+id "$APP_USER" >/dev/null 2>&1 || adduser --disabled-password --gecos "" "$APP_USER"
+usermod -a -G "$APP_GROUP" "$APP_USER"
+
+# ===== Self-signed TLS (SAN = IP) =====
+OSSL_CFG="${CERT_DIR}/openssl.cnf"
+if [ ! -s "$CERT" ] || [ ! -s "$KEY" ]; then
+ log "Erzeuge Self-Signed TLS Zertifikat (SAN=IP:${SERVER_IP})…"
+ install -d -m 0750 -o root -g "${APP_USER}" "${CERT_DIR}"
+ cat > "$OSSL_CFG" </dev/null 2>&1; then
+ setfacl -m u:${DEV_USER}:x "${CONF_BASE}" "${CERT_DIR}" || true
+ setfacl -m u:${DEV_USER}:r "$CERT" "$KEY" || true
+fi
+
+# ===== MariaDB =====
+log "MariaDB vorbereiten…"
+systemctl enable --now mariadb
+mysql -uroot < /etc/postfix/sql/mysql-virtual-mailbox-maps.cf < /etc/postfix/sql/mysql-virtual-alias-maps.cf < /etc/dovecot/dovecot.conf <<'CONF'
+!include_try /etc/dovecot/conf.d/*.conf
+CONF
+
+cat > /etc/dovecot/conf.d/10-mail.conf <<'CONF'
+protocols = imap pop3 lmtp
+mail_location = maildir:/var/mail/vhosts/%d/%n
+namespace inbox { inbox = yes }
+mail_privileged_group = mail
+CONF
+
+cat > /etc/dovecot/conf.d/10-auth.conf <<'CONF'
+disable_plaintext_auth = yes
+auth_mechanisms = plain login
+!include_try auth-sql.conf.ext
+CONF
+
+cat > /etc/dovecot/dovecot-sql.conf.ext < /etc/dovecot/conf.d/auth-sql.conf.ext <<'CONF'
+passdb { driver = sql args = /etc/dovecot/dovecot-sql.conf.ext }
+userdb { driver = static args = uid=vmail gid=vmail home=/var/mail/vhosts/%d/%n }
+CONF
+sudo chown root:dovecot /etc/dovecot/conf.d/auth-sql.conf.ext
+sudo chmod 640 /etc/dovecot/conf.d/auth-sql.conf.ext
+
+cat > /etc/dovecot/conf.d/10-master.conf <<'CONF'
+service lmtp {
+ unix_listener /var/spool/postfix/private/dovecot-lmtp { mode = 0600 user = postfix group = postfix }
+}
+service auth {
+ unix_listener /var/spool/postfix/private/auth { mode = 0660 user = postfix group = postfix }
+}
+service imap-login {
+ inet_listener imap { port = 143 }
+ inet_listener imaps { port = 993 ssl = yes }
+}
+service pop3-login {
+ inet_listener pop3 { port = 110 }
+ inet_listener pop3s { port = 995 ssl = yes }
+}
+CONF
+
+cat > /etc/dovecot/conf.d/10-ssl.conf < /etc/rspamd/local.d/worker-controller.inc <<'CONF'
+password = "admin";
+bind_socket = "127.0.0.1:11334";
+CONF
+systemctl enable --now rspamd || true
+
+cat > /etc/opendkim.conf <<'CONF'
+Syslog yes
+UMask 002
+Mode sv
+Socket inet:8891@127.0.0.1
+Canonicalization relaxed/simple
+On-BadSignature accept
+On-Default accept
+On-KeyNotFound accept
+On-NoSignature accept
+LogWhy yes
+OversignHeaders From
+# KeyTable / SigningTable werden nach dem Wizard gesetzt
+CONF
+systemctl enable --now opendkim || true
+
+# ===== Redis =====
+log "Redis absichern (Passwort setzen & nur localhost)…"
+REDIS_CONF="/etc/redis/redis.conf"
+REDIS_PASS="${REDIS_PASS:-$(openssl rand -hex 16)}"
+sed -i 's/^\s*#\?\s*bind .*/bind 127.0.0.1/' "$REDIS_CONF"
+sed -i 's/^\s*#\?\s*protected-mode .*/protected-mode yes/' "$REDIS_CONF"
+if grep -qE '^\s*#?\s*requirepass ' "$REDIS_CONF"; then
+ sed -i "s/^\s*#\?\s*requirepass .*/requirepass ${REDIS_PASS}/" "$REDIS_CONF"
+else
+ printf "\nrequirepass %s\n" "${REDIS_PASS}" >> "$REDIS_CONF"
+fi
+systemctl enable --now redis-server
+systemctl restart redis-server
+
+# ===== Nginx =====
+log "Nginx konfigurieren…"
+rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default || true
+
+detect_php_fpm_sock() {
+ for v in 8.3 8.2 8.1 8.0 7.4; do s="/run/php/php${v}-fpm.sock"; [ -S "$s" ] && { echo "$s"; return; }; done
+ [ -S "/run/php/php-fpm.sock" ] && { echo "/run/php/php-fpm.sock"; return; }
+ echo "127.0.0.1:9000"
+}
+PHP_FPM_SOCK="$(detect_php_fpm_sock)"
+install -d -m 0755 /var/www/letsencrypt
+
+if [ -s "${CERT}" ] && [ -s "${KEY}" ]; then
+ cat > "${NGINX_SITE}" < "${NGINX_SITE}" </dev/null || true)" ]; then
+ sudo -u "$APP_USER" -H bash -lc "cd /var/www && COMPOSER_ALLOW_SUPERUSER=0 composer create-project laravel/laravel ${APP_USER} --no-interaction"
+ fi
+else
+ if [ ! -d "${APP_DIR}/.git" ]; then
+ sudo -u "$APP_USER" -H bash -lc "git clone --depth=1 -b ${GIT_BRANCH} ${GIT_REPO} ${APP_DIR}"
+ else
+ sudo -u "$APP_USER" -H bash -lc "
+ set -e
+ cd ${APP_DIR}
+ git checkout ${GIT_BRANCH} 2>/dev/null || git checkout -B ${GIT_BRANCH}
+ git fetch --depth=1 origin ${GIT_BRANCH}
+ if git merge-base --is-ancestor HEAD origin/${GIT_BRANCH}; then
+ git pull --ff-only
+ else
+ echo '[i] Non-fast-forward erkannt – setze hart auf origin/${GIT_BRANCH}.' >&2
+ git reset --hard origin/${GIT_BRANCH}
+ git clean -fd
+ fi
+ "
+ fi
+ if [ -f "${APP_DIR}/composer.json" ]; then
+ if [ "${DEV_MODE}" = "1" ]; then
+ sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && composer install --no-interaction --prefer-dist"
+ else
+ sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && composer install --no-interaction --prefer-dist --no-dev"
+ fi
+ fi
+fi
+
+# ===== Node / Frontend =====
+if [ -f "${APP_DIR}/package.json" ]; then
+ log "Node/NPM installieren…"
+ if command -v node >/dev/null 2>&1; then
+ NODE_MAJ=$(node -v | sed 's/^v//' | cut -d. -f1)
+ NODE_MIN=$(node -v | sed 's/^v//' | cut -d. -f2)
+ if [ "$NODE_MAJ" -lt 20 ] || { [ "$NODE_MAJ" -eq 20 ] && [ "$NODE_MIN" -lt 19 ]; }; then
+ curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
+ apt-get install -y nodejs
+ fi
+ else
+ curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
+ apt-get install -y nodejs
+ fi
+
+ # .env anlegen & APP_KEY
+ sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && cp -n .env.example .env || true"
+ if ! grep -q '^APP_KEY=' "${APP_DIR}/.env"; then echo "APP_KEY=" >> "${APP_DIR}/.env"; fi
+ sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan key:generate --force || true"
+fi
+
+# ===== .env füllen =====
+ENV_FILE="${APP_DIR}/.env"
+upsert_env () {
+ local key="$1" val="$2"
+ local esc_key esc_val
+ esc_key="$(printf '%s' "$key" | sed -e 's/[.[\*^$(){}+?|/]/\\&/g')"
+ esc_val="$(printf '%s' "$val" | sed -e 's/[&/]/\\&/g')"
+ if grep -qE "^[#[:space:]]*${esc_key}=" "$ENV_FILE"; then
+ sed -Ei "s|^[#[:space:]]*${esc_key}=.*|${key}=${esc_val}|g" "$ENV_FILE"
+ else
+ printf '%s=%s\n' "$key" "$val" >> "$ENV_FILE"
+ fi
+}
+
+if [ -s "${CERT}" ] && [ -s "${KEY}" ]; then
+ upsert_env APP_URL "\"https://\${APP_HOST}\""
+else
+ upsert_env APP_URL "\"http://\${APP_HOST}\""
+fi
+
+upsert_env APP_HOST "${SERVER_IP}"
+upsert_env APP_ADMIN_USER "${ADMIN_USER}"
+upsert_env APP_ADMIN_EMAIL "${ADMIN_EMAIL}"
+upsert_env APP_ADMIN_PASS "${ADMIN_PASS}"
+upsert_env APP_NAME "${APP_NAME}"
+upsert_env APP_ENV "${APP_ENV}"
+upsert_env APP_DEBUG "${APP_DEBUG}"
+
+upsert_env DB_CONNECTION "mysql"
+upsert_env DB_HOST "127.0.0.1"
+upsert_env DB_PORT "3306"
+upsert_env DB_DATABASE "${DB_NAME}"
+upsert_env DB_USERNAME "${DB_USER}"
+upsert_env DB_PASSWORD "${DB_PASS}"
+
+# -------- WICHTIG: Cache-Store auf REDIS setzen (verhindert DB-Tabellenfehler) --------
+upsert_env CACHE_SETTINGS_STORE "redis"
+upsert_env CACHE_STORE "redis" # <- HIER geändert (vorher: database)
+upsert_env CACHE_DRIVER "redis"
+upsert_env CACHE_PREFIX "${APP_USER}_cache"
+
+upsert_env SESSION_DRIVER "redis"
+upsert_env REDIS_CLIENT "phpredis"
+upsert_env REDIS_HOST "127.0.0.1"
+upsert_env REDIS_PORT "6379"
+upsert_env REDIS_PASSWORD "${REDIS_PASS}"
+upsert_env REDIS_DB "0"
+upsert_env REDIS_CACHE_DB "1"
+upsert_env REDIS_CACHE_CONNECTION "cache"
+upsert_env REDIS_CACHE_LOCK_CONNECTION "default"
+
+# Reverb / Vite
+upsert_env BROADCAST_DRIVER "reverb"
+upsert_env QUEUE_CONNECTION "redis"
+upsert_env REVERB_APP_ID "${APP_USER}"
+upsert_env REVERB_APP_KEY "${APP_USER}-yhp47tbt1aebhr1fgvgj"
+upsert_env REVERB_APP_SECRET "${APP_USER}-ulrdt9agwzkqwqsunbnb"
+upsert_env REVERB_HOST "127.0.0.1"
+upsert_env REVERB_PORT "443"
+upsert_env REVERB_SCHEME "https"
+upsert_env REVERB_PATH "/ws"
+upsert_env VITE_REVERB_APP_KEY "\${REVERB_APP_KEY}"
+upsert_env VITE_REVERB_PORT "\${REVERB_PORT}"
+upsert_env VITE_REVERB_SCHEME "\${REVERB_SCHEME}"
+upsert_env VITE_REVERB_PATH "\${REVERB_PATH}"
+
+if [ "${DEV_MODE}" = "1" ]; then
+ sed -i '/^# --- MailWolt DEV/,/^# --- \/MailWolt DEV/d' "${ENV_FILE}"
+ cat >> "${ENV_FILE}" < "${APP_DIR}/vite.config.js" <<'JS'
+import { defineConfig, loadEnv } from 'vite'
+import laravel from 'laravel-vite-plugin'
+import tailwindcss from '@tailwindcss/vite'
+export default ({ mode }) => {
+ const env = loadEnv(mode, process.cwd(), '')
+ const host = env.VITE_DEV_HOST || '127.0.0.1'
+ const port = Number(env.VITE_DEV_PORT || 5173)
+ const origin = env.VITE_DEV_ORIGIN || env.APP_URL || 'https://localhost'
+ const hmrHost = env.VITE_HMR_HOST || (new URL(origin)).hostname
+ return defineConfig({
+ plugins: [laravel({ input: ['resources/css/app.css','resources/js/app.js'], refresh: true }), tailwindcss()],
+ server: { host, port, https:false, strictPort:true,
+ hmr:{ protocol: env.VITE_HMR_PROTOCOL || 'wss', host: hmrHost, clientPort:Number(env.VITE_HMR_CLIENT_PORT||443) },
+ origin
+ }
+ })
+}
+JS
+ chown "${APP_USER}:${APP_GROUP}" "${APP_DIR}/vite.config.js"
+fi
+
+# ===== Frontend Build =====
+if [ -f "${APP_DIR}/package.json" ]; then
+ log "Frontend Build…"
+ if [ -f "${APP_DIR}/package-lock.json" ] || [ -f "${APP_DIR}/npm-shrinkwrap.json" ]; then
+ sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm ci --no-audit --no-fund || npm install"
+ else
+ sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm install"
+ fi
+ sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm run build || true"
+ rm -f ${APP_DIR}/bootstrap/cache/*.php
+fi
+
+# ===== Rechte / PHP-FPM =====
+APP_PW="${APP_PW:-changeme}"
+if ! id -u "$APP_USER" >/dev/null 2>&1; then
+ adduser --disabled-password --gecos "" "$APP_USER"
+ echo "${APP_USER}:${APP_PW}" | chpasswd
+fi
+usermod -a -G "$APP_GROUP" "$APP_USER"
+
+# Sichert, dass alle nötigen Ordner existieren (idempotent)
+install -d -m 0775 "${APP_DIR}/storage" \
+ "${APP_DIR}/storage/framework" \
+ "${APP_DIR}/storage/framework/cache" \
+ "${APP_DIR}/storage/framework/cache/data" \
+ "${APP_DIR}/storage/framework/sessions" \
+ "${APP_DIR}/storage/framework/views" \
+ "${APP_DIR}/bootstrap/cache"
+
+# Besitz & Rechte
+chown -R "$APP_USER":"$APP_GROUP" "$APP_DIR"
+find "$APP_DIR" -type d -exec chmod 775 {} \;
+find "$APP_DIR" -type f -exec chmod 664 {} \;
+
+[ -f "$APP_DIR/artisan" ] && chmod 755 "$APP_DIR/artisan"
+[ -d "$APP_DIR/vendor/bin" ] && chmod -R 755 "$APP_DIR/vendor/bin"
+[ -d "$APP_DIR/node_modules/.bin" ] && chmod -R 755 "$APP_DIR/node_modules/.bin"
+[ -f "$APP_DIR/node_modules/vite/bin/vite.js" ] && chmod 755 "$APP_DIR/node_modules/vite/bin/vite.js"
+find "$APP_DIR" -type f -name "*.sh" -exec chmod 755 {} \;
+
+chmod -R 775 "$APP_DIR"/storage "$APP_DIR"/bootstrap/cache
+
+if command -v setfacl >/dev/null 2>&1; then
+ setfacl -R -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX "${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
+ setfacl -dR -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX "${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
+fi
+
+grep -q 'umask 002' /home/${APP_USER}/.profile 2>/dev/null || echo 'umask 002' >> /home/${APP_USER}/.profile
+grep -q 'umask 002' /home/${APP_USER}/.bashrc 2>/dev/null || echo 'umask 002' >> /home/${APP_USER}/.bashrc
+sudo -u "$APP_USER" -H bash -lc "npm config set umask 0002" >/dev/null 2>&1 || true
+
+PHPV=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')
+FPM_POOL="/etc/php/${PHPV}/fpm/pool.d/www.conf"
+if [ -f "$FPM_POOL" ]; then
+ sed -i 's/^;*listen\.owner.*/listen.owner = www-data/' "$FPM_POOL"
+ sed -i 's/^;*listen\.group.*/listen.group = www-data/' "$FPM_POOL"
+ sed -i 's/^;*listen\.mode.*/listen.mode = 0660/' "$FPM_POOL"
+ systemctl restart php${PHPV}-fpm || true
+fi
+
+IDE_USER="${SUDO_USER:-}"
+if [ -n "$IDE_USER" ] && id "$IDE_USER" >/dev/null 2>&1 && command -v setfacl >/dev/null 2>&1; then
+ usermod -a -G "$APP_GROUP" "$IDE_USER" || true
+ setfacl -R -m u:${IDE_USER}:rwX "$APP_DIR"
+ setfacl -dR -m u:${IDE_USER}:rwX "$APP_DIR"
+ echo -e "${GREEN}[i]${NC} Benutzer '${IDE_USER}' wurde für Schreibzugriff freigeschaltet (ACL + Gruppe ${APP_GROUP})."
+fi
+
+# Webstack neu laden
+systemctl reload nginx || true
+systemctl restart php*-fpm || true
+
+# ===== Reverb systemd =====
+cat > /etc/systemd/system/mailwolt-ws.service </dev/null | grep -qE '(^| )reverb:start( |$)'"; then
+ systemctl enable --now mailwolt-ws
+else
+ systemctl disable --now mailwolt-ws >/dev/null 2>&1 || true
+fi
+
+# ===== Monit =====
+log "Monit konfigurieren & starten…"
+cat > /etc/monit/monitrc <<'EOF'
+set daemon 60
+set logfile syslog facility log_daemon
+
+check process postfix with pidfile /var/spool/postfix/pid/master.pid
+ start program = "/bin/systemctl start postfix"
+ stop program = "/bin/systemctl stop postfix"
+ if failed port 25 protocol smtp then restart
+ if failed port 465 type tcp ssl then restart
+ if failed port 587 type tcp then restart
+
+check process dovecot with pidfile /var/run/dovecot/master.pid
+ start program = "/bin/systemctl start dovecot"
+ stop program = "/bin/systemctl stop dovecot"
+ if failed port 143 type tcp then restart
+ if failed port 993 type tcp ssl then restart
+ if failed port 110 type tcp then restart
+ if failed port 995 type tcp ssl then restart
+
+check process mariadb with pidfile /var/run/mysqld/mysqld.pid
+ start program = "/bin/systemctl start mariadb"
+ stop program = "/bin/systemctl stop mariadb"
+ if failed port 3306 type tcp then restart
+
+check process redis-server with pidfile /run/redis/redis-server.pid
+ start program = "/bin/systemctl start redis-server"
+ stop program = "/bin/systemctl stop redis-server"
+ if failed port 6379 type tcp then restart
+
+check process rspamd with pidfile /run/rspamd/rspamd.pid
+ start program = "/bin/systemctl start rspamd"
+ stop program = "/bin/systemctl stop rspamd"
+ if failed port 11332 type tcp then restart
+
+check process opendkim with pidfile /run/opendkim/opendkim.pid
+ start program = "/bin/systemctl start opendkim"
+ stop program = "/bin/systemctl stop opendkim"
+ if failed port 8891 type tcp then restart
+
+check process nginx with pidfile /run/nginx.pid
+ start program = "/bin/systemctl start nginx"
+ stop program = "/bin/systemctl stop nginx"
+ if failed port 80 type tcp then restart
+ if failed port 443 type tcp ssl then restart
+
+check process mailwolt-ws matching "reverb:start"
+ start program = "/bin/systemctl start mailwolt-ws"
+ stop program = "/bin/systemctl stop mailwolt-ws"
+ if failed host 127.0.0.1 port 8080 type tcp for 2 cycles then restart
+ if 5 restarts within 5 cycles then timeout
+EOF
+chmod 600 /etc/monit/monitrc
+monit -t && systemctl enable --now monit
+monit reload
+monit summary || true
+
+# ===== Healthchecks =====
+GREEN="\033[1;32m"; RED="\033[1;31m"; GREY="\033[0;90m"; NC="\033[0m"
+ok(){ echo -e " [${GREEN}OK${NC}]"; }
+fail(){ echo -e " [${RED}FAIL${NC}]"; }
+
+echo "[+] Quick-Healthchecks…"
+printf " • MariaDB … " ; mysqladmin ping --silent >/dev/null 2>&1 && ok || fail
+printf " • Redis … " ; if command -v redis-cli >/dev/null 2>&1; then
+ if [ -n "${REDIS_PASS:-}" ] && [ "${REDIS_PASS}" != "null" ]; then
+ redis-cli -a "${REDIS_PASS}" ping 2>/dev/null | grep -q PONG && ok || fail
+ else
+ redis-cli ping 2>/dev/null | grep -q PONG && ok || fail
+ fi
+else fail; fi
+printf " • PHP-FPM … " ; if [[ "$PHP_FPM_SOCK" == 127.0.0.1:9000 ]]; then ss -ltn | grep -q ":9000 " && ok || fail; else [ -S "$PHP_FPM_SOCK" ] && ok || fail; fi
+printf " • App … " ; if command -v curl >/dev/null 2>&1; then
+ if [ -s "${CERT}" ] && [ -s "${KEY}" ]; then curl -skI "https://127.0.0.1" >/dev/null 2>&1 && ok || fail; else curl -sI "http://127.0.0.1" >/dev/null 2>&1 && ok || fail; fi
+else echo -e " ${GREY}(curl fehlt)${NC}"; fi
+check_port(){ local label="$1" cmd="$2"; printf " • %-5s … " "$label"; timeout 8s bash -lc "$cmd" >/dev/null 2>&1 && ok || fail; }
+check_port "25" 'printf "QUIT\r\n" | nc -w 3 127.0.0.1 25'
+check_port "465" 'printf "QUIT\r\n" | openssl s_client -connect 127.0.0.1:465 -quiet -ign_eof'
+check_port "587" 'printf "EHLO x\r\nSTARTTLS\r\nQUIT\r\n" | openssl s_client -starttls smtp -connect 127.0.0.1:587 -quiet -ign_eof'
+check_port "110" 'printf "QUIT\r\n" | nc -w 3 127.0.0.1 110'
+check_port "995" 'printf "QUIT\r\n" | openssl s_client -connect 127.0.0.1:995 -quiet -ign_eof'
+check_port "143" 'printf ". LOGOUT\r\n" | nc -w 3 127.0.0.1 143'
+check_port "993" 'printf ". LOGOUT\r\n" | openssl s_client -connect 127.0.0.1:993 -quiet -ign_eof'
+
+print_bootstrap_summary "$SERVER_IP" "$ADMIN_USER" "$ADMIN_PASS"
+
+# ===== MOTD =====
+install -d /usr/local/bin
+cat >/usr/local/bin/mw-motd <<'SH'
+#!/usr/bin/env bash
+set -euo pipefail
+NC="\033[0m"; CY="\033[1;36m"; GR="\033[1;32m"; YE="\033[1;33m"; RD="\033[1;31m"; GY="\033[0;90m"
+printf "\033[1;36m"
+cat <<'ASCII'
+:::: :::: ::: ::::::::::: ::: ::: ::: :::::::: ::: :::::::::::
++:+:+: :+:+:+ :+: :+: :+: :+: :+: :+: :+: :+: :+: :+:
++:+ +:+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+
++#+ +:+ +#+ +#++:++#++: +#+ +#+ +#+ +:+ +#+ +#+ +:+ +#+ +#+
++#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+#+ +#+ +#+ +#+ +#+ +#+
+#+# #+# #+# #+# #+# #+# #+#+# #+#+# #+# #+# #+# #+#
+### ### ### ### ########### ########## ### ### ######## ########## ###
+ASCII
+printf "\033[0m\n"
+now="$(date '+%Y-%m-%d %H:%M:%S %Z')"
+fqdn="$(hostname -f 2>/dev/null || hostname)"
+ip_int="$(hostname -I 2>/dev/null | awk '{print $1}')"
+ip_ext=""; command -v curl >/dev/null 2>&1 && ip_ext="$(curl -s --max-time 1 https://ifconfig.me || true)"
+upt="$(uptime -p 2>/dev/null || true)"
+cores="$(nproc 2>/dev/null || echo -n '?')"
+mhz="$(LC_ALL=C lscpu 2>/dev/null | awk -F: '/MHz/{gsub(/ /,"",$2); printf("%.0f MHz",$2); exit}')"
+[ -z "$mhz" ] && mhz="$(awk -F: '/cpu MHz/{printf("%.0f MHz",$2); exit}' /proc/cpuinfo 2>/dev/null)"
+load="$(awk '{print $1" / "$2" / "$3}' /proc/loadavg 2>/dev/null)"
+mem_total="$(awk '/MemTotal/{printf "%.2f GB",$2/1024/1024}' /proc/meminfo)"
+mem_free="$(awk '/MemAvailable/{printf "%.2f GB",$2/1024/1024}' /proc/meminfo)"
+svc_status(){ systemctl is-active --quiet "$1" && echo -e "${GR}OK${NC}" || echo -e "${RD}FAIL${NC}"; }
+printf "${CY}Information as of:${NC} ${YE}%s${NC}\n" "$now"
+printf "${GY}FQDN :${NC} %s\n" "$fqdn"
+if [ -n "$ip_ext" ]; then printf "${GY}IP :${NC} %s ${GY}(external:${NC} %s${GY})${NC}\n" "${ip_int:-?}" "$ip_ext"; else printf "${GY}IP :${NC} %s\n" "${ip_int:-?}"; fi
+printf "${GY}Uptime :${NC} %s\n" "${upt:-?}"
+printf "${GY}Core(s) :${NC} %s core(s) at ${CY}%s${NC}\n" "$cores" "${mhz:-?}"
+printf "${GY}Load :${NC} %s (1 / 5 / 15)\n" "${load:-?}"
+printf "${GY}Memory :${NC} ${RD}%s${NC} ${GY}(free)${NC} / ${CY}%s${NC} ${GY}(total)${NC}\n" "${mem_free:-?}" "${mem_total:-?}"
+echo
+printf "${GY}Services :${NC} postfix: $(svc_status postfix) dovecot: $(svc_status dovecot) nginx: $(svc_status nginx) mariadb: $(svc_status mariadb) redis: $(svc_status redis)\n"
+SH
+chmod +x /usr/local/bin/mw-motd
+
+if [ -d /etc/update-motd.d ]; then
+ cat >/etc/update-motd.d/10-mailwolt <<'SH'
+#!/usr/bin/env bash
+/usr/local/bin/mw-motd
+SH
+ chmod +x /etc/update-motd.d/10-mailwolt
+ [ -f /etc/update-motd.d/50-motd-news ] && chmod -x /etc/update-motd.d/50-motd-news || true
+ [ -f /etc/update-motd.d/80-livepatch ] && chmod -x /etc/update-motd.d/80-livepatch || true
+else
+ cat >/etc/profile.d/10-mailwolt-motd.sh <<'SH'
+case "$-" in *i*) /usr/local/bin/mw-motd ;; esac
+SH
+fi
+: > /etc/motd 2>/dev/null || true
+
+)
\ No newline at end of file
diff --git a/mailwolt-installer/scripts/10-provision.sh b/mailwolt-installer/scripts/10-provision.sh
new file mode 100644
index 0000000..1697460
--- /dev/null
+++ b/mailwolt-installer/scripts/10-provision.sh
@@ -0,0 +1,125 @@
+#!/usr/bin/env bash
+set -euo pipefail
+source ./lib.sh
+
+if [ -r /etc/mailwolt/installer.env ]; then
+ . /etc/mailwolt/installer.env
+fi
+
+REDIS_PASS="${REDIS_PASS:-}"
+
+SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+find "$SCRIPTS_DIR/.." -type f -name "*.sh" -exec sed -i 's/\r$//' {} \; || true
+
+log "Pakete installieren …"
+export DEBIAN_FRONTEND=noninteractive
+apt-get update -y
+# Minimal aber vollständig
+apt-get -y -o Dpkg::Options::="--force-confdef" \
+ -o Dpkg::Options::="--force-confold" install \
+ postfix postfix-mysql dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-mysql \
+ mariadb-server mariadb-client redis-server rspamd opendkim opendkim-tools opendmarc clamav \
+ clamav-daemon nginx php php-fpm php-cli php-mbstring php-xml php-curl php-zip php-mysql \
+ php-redis php-gd unzip curl composer git certbot python3-certbot-nginx fail2ban ca-certificates \
+ rsyslog sudo openssl monit acl netcat-openbsd jq sqlite3
+
+# <<< Apache konsequent entfernen >>>
+systemctl disable --now apache2 >/dev/null 2>&1 || true
+apt-get -y purge 'apache2*' >/dev/null 2>&1 || true
+apt-get -y autoremove >/dev/null 2>&1 || true
+
+log "Systemuser/Dirs …"
+id vmail >/dev/null 2>&1 || adduser --system --group --home /var/mail vmail
+id "$APP_USER" >/dev/null 2>&1 || adduser --disabled-password --gecos "" "$APP_USER"
+# Systemuser/Dirs …
+id vmail >/dev/null 2>&1 || adduser --system --group --home /var/mail vmail
+id "$APP_USER" >/dev/null 2>&1 || adduser --disabled-password --gecos "" "$APP_USER"
+
+# --- FIX: Gruppen und Berechtigungen für Maildir und Dovecot-Zugriff ---
+# vmail soll primär der Gruppe "mail" angehören, zusätzlich dovecot
+usermod -g mail -a -G dovecot vmail || true
+
+# App-User in relevante Gruppen
+usermod -a -G "$APP_GROUP" "$APP_USER" || true
+usermod -a -G mail,dovecot "$APP_USER" || true
+
+# Maildir-Baum für Gruppe mail lesbar
+chgrp -R mail /var/mail/vhosts || true
+chmod -R g+rx /var/mail/vhosts || true
+
+# ACLs setzen, damit neue Verzeichnisse automatisch passende Rechte bekommen
+setfacl -R -m g:mail:rx /var/mail/vhosts || true
+setfacl -dR -m g:mail:rx /var/mail/vhosts || true
+usermod -a -G "$APP_GROUP" "$APP_USER" || true
+install -d -m 0755 -o root -g root /var/www
+install -d -m 0775 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR"
+
+SUDOERS_DKIM="/etc/sudoers.d/mailwolt-dkim"
+cat > "${SUDOERS_DKIM}" <<'EOF'
+Defaults!/usr/local/sbin/mailwolt-install-dkim !requiretty
+Defaults!/usr/local/sbin/mailwolt-remove-dkim !requiretty
+Defaults!/usr/bin/systemctl !requiretty
+Defaults!/usr/bin/test !requiretty
+
+www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install-dkim *
+www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-remove-dkim *
+www-data ALL=(root) NOPASSWD: /usr/bin/systemctl reload opendkim
+www-data ALL=(root) NOPASSWD: /usr/bin/test *
+
+mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install-dkim *
+mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-remove-dkim *
+mailwolt ALL=(root) NOPASSWD: /usr/bin/systemctl reload opendkim
+mailwolt ALL=(root) NOPASSWD: /usr/bin/test *
+EOF
+chown root:root "${SUDOERS_DKIM}"
+chmod 440 "${SUDOERS_DKIM}"
+
+if ! visudo -c -f "${SUDOERS_DKIM}" >/dev/null 2>&1; then
+ echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_DKIM} – entferne Datei."
+ rm -f "${SUDOERS_DKIM}"
+fi
+
+SUDOERS_DOVEADM="/etc/sudoers.d/mailwolt-doveadm"
+cat > "${SUDOERS_DOVEADM}" <<'EOF'
+Cmnd_Alias MW_DOVEADM_STATUS = /usr/bin/doveadm -f tab mailbox status -u * messages INBOX, \
+ /usr/bin/doveadm mailbox status -u * messages INBOX
+www-data ALL=(vmail) NOPASSWD: MW_DOVEADM_STATUS
+mailwolt ALL=(vmail) NOPASSWD: MW_DOVEADM_STATUS
+EOF
+chown root:root "${SUDOERS_DOVEADM}"
+chmod 440 "${SUDOERS_DOVEADM}"
+visudo -c -f "${SUDOERS_DOVEADM}" || rm -f "${SUDOERS_DOVEADM}"
+
+log "MariaDB include-fix …"
+mkdir -p /etc/mysql/mariadb.conf.d
+[[ -f /etc/mysql/mariadb.cnf ]] || echo '!include /etc/mysql/mariadb.conf.d/*.cnf' > /etc/mysql/mariadb.cnf
+
+log "Redis absichern …"
+if [[ -z "${REDIS_PASS:-}" || "${REDIS_PASS}" == "changeme" ]]; then
+ REDIS_PASS="$(openssl rand -hex 16)"
+ export REDIS_PASS
+ log "Neues Redis-Passwort generiert."
+fi
+# Aktiven Redis-Config-Pfad aus systemd holen (Fallback: Standard)
+REDIS_CONF="$(systemctl show -p ExecStart redis-server \
+ | sed -n 's/^ExecStart=.*redis-server[[:space:]]\+\([^[:space:]]\+\).*/\1/p')"
+REDIS_CONF="${REDIS_CONF:-/etc/redis/redis.conf}"
+
+# Bind + protected-mode hart setzen
+sed -i 's/^[[:space:]]*#\?[[:space:]]*bind .*/bind 127.0.0.1/' "$REDIS_CONF"
+sed -i 's/^[[:space:]]*#\?[[:space:]]*protected-mode .*/protected-mode yes/' "$REDIS_CONF"
+
+# Vorherige requirepass-Zeilen entfernen (kommentiert/unkommentiert), dann neu schreiben
+sed -i '/^[[:space:]]*#\?[[:space:]]*requirepass[[:space:]]\+/d' "$REDIS_CONF"
+printf '\nrequirepass %s\n' "${REDIS_PASS}" >> "$REDIS_CONF"
+
+# Dienst aktivieren & neu starten
+systemctl enable --now redis-server
+systemctl restart redis-server || true
+
+# Sanity-Check (kein harter Exit, nur Log)
+if redis-cli -a "${REDIS_PASS}" ping 2>/dev/null | grep -q PONG; then
+ log "Redis mit Passwort OK."
+else
+ warn "Redis PING mit Passwort fehlgeschlagen – bitte /etc/redis/redis.conf prüfen."
+fi
\ No newline at end of file
diff --git a/mailwolt-installer/scripts/20-ssl.sh b/mailwolt-installer/scripts/20-ssl.sh
new file mode 100644
index 0000000..1dceed2
--- /dev/null
+++ b/mailwolt-installer/scripts/20-ssl.sh
@@ -0,0 +1,60 @@
+#!/usr/bin/env bash
+set -euo pipefail
+source ./lib.sh
+
+CONF_BASE="/etc/${APP_USER}"
+CERT_DIR="${CONF_BASE}/ssl"
+UI_SSL_DIR="/etc/ssl/ui"; WEBMAIL_SSL_DIR="/etc/ssl/webmail"; MAIL_SSL_DIR="/etc/ssl/mail"
+UI_CERT="${UI_SSL_DIR}/fullchain.pem"; UI_KEY="${UI_SSL_DIR}/privkey.pem"
+WEBMAIL_CERT="${WEBMAIL_SSL_DIR}/fullchain.pem"; WEBMAIL_KEY="${WEBMAIL_SSL_DIR}/privkey.pem"
+MAIL_CERT="${MAIL_SSL_DIR}/fullchain.pem"; MAIL_KEY="${MAIL_SSL_DIR}/privkey.pem"
+
+install -d -m 0750 "$CERT_DIR"
+CERT="${CERT_DIR}/cert.pem"; KEY="${CERT_DIR}/key.pem"
+
+if [[ ! -s "$CERT" || ! -s "$KEY" ]]; then
+ log "Self-signed Zertifikat erzeugen …"
+ OSSL_CFG="${CERT_DIR}/openssl.cnf"
+ cat > "$OSSL_CFG" <&2
+fi
+
+# Optional: kurze Info, wohin verlinkt wurde
+echo "[i] Mail TLS: $MAIL_CERT -> $CERT ; $MAIL_KEY -> $KEY"
\ No newline at end of file
diff --git a/mailwolt-installer/scripts/21-le-deploy-hook.sh b/mailwolt-installer/scripts/21-le-deploy-hook.sh
new file mode 100644
index 0000000..fb37804
--- /dev/null
+++ b/mailwolt-installer/scripts/21-le-deploy-hook.sh
@@ -0,0 +1,588 @@
+#!/usr/bin/env bash
+set -euo pipefail
+source ./lib.sh
+
+log "Let's Encrypt Deploy-Hooks und Wrapper anlegen …"
+
+# -------------------------------------------------------------------
+# 2) POSIX-kompatibler Deploy-Wrapper (von Certbot aufgerufen)
+# -------------------------------------------------------------------
+cat >/usr/local/sbin/mailwolt-deploy.sh <<'WRAP'
+#!/bin/sh
+# POSIX-safe Certbot deploy-hook (ohne bashisms)
+set -eu
+
+# Installer-ENV laden (liefert UI_HOST/WEBMAIL_HOST/MAIL_HOSTNAME etc.)
+if [ -r /etc/mailwolt/installer.env ]; then
+ . /etc/mailwolt/installer.env
+fi
+
+UI_HOST="${UI_HOST:-}"
+WEBMAIL_HOST="${WEBMAIL_HOST:-}"
+MAIL_HOSTNAME="${MAIL_HOSTNAME:-}"
+ACME_BASE="/etc/letsencrypt/live"
+
+copy_cert() {
+ le_base="$1" # z.B. /etc/letsencrypt/live/ui.example.com
+ target_dir="$2" # z.B. /etc/ssl/ui
+
+ cert="${le_base}/fullchain.pem"
+ key="${le_base}/privkey.pem"
+
+ [ -s "$cert" ] || { echo "[deploy] missing $cert"; return 1; }
+ [ -s "$key" ] || { echo "[deploy] missing $key"; return 1; }
+
+ mkdir -p "$target_dir"
+
+ # echte Dateien (keine Symlinks), feste Rechte
+ install -m 0644 "$cert" "${target_dir}/fullchain.pem"
+ install -m 0600 "$key" "${target_dir}/privkey.pem"
+
+ echo "[+] Copied ${target_dir}/fullchain.pem und privkey.pem ← ${le_base}"
+}
+
+reload_services() {
+ kind="$1" # ui | mail
+ if command -v systemctl >/dev/null 2>&1; then
+ if [ "$kind" = "mail" ]; then
+ systemctl reload postfix 2>/dev/null || true
+ systemctl reload dovecot 2>/dev/null || true
+ else
+ systemctl reload nginx 2>/dev/null || true
+ fi
+ fi
+}
+
+# Certbot-Kontext
+LINEAGE="${RENEWED_LINEAGE:-}"
+HOST=""
+if [ -n "$LINEAGE" ]; then
+ HOST="$(basename "$LINEAGE")"
+fi
+
+did_any=0
+
+maybe_copy_for_host() {
+ host="$1"
+ dir="$2"
+ [ -n "$host" ] || return 0
+
+ # Fall A: Certbot liefert RENEWED_DOMAINS (Space-getrennt)
+ if [ -n "${RENEWED_DOMAINS:-}" ]; then
+ case " ${RENEWED_DOMAINS} " in
+ *" ${host} "*) copy_cert "${ACME_BASE}/${host}" "${dir}" && did_any=1 ;;
+ esac
+ return 0
+ fi
+
+ # Fall B: Erst-issue / kein RENEWED_DOMAINS → über LINEAGE matchen
+ if [ -n "$HOST" ] && [ "$HOST" = "$host" ]; then
+ copy_cert "${ACME_BASE}/${host}" "${dir}" && did_any=1
+ fi
+}
+
+# Gezieltes Kopieren
+maybe_copy_for_host "$UI_HOST" "/etc/ssl/ui"
+maybe_copy_for_host "$WEBMAIL_HOST" "/etc/ssl/webmail"
+maybe_copy_for_host "$MAIL_HOSTNAME" "/etc/ssl/mail"
+
+# Fallback (Erstlauf): kopiere vorhandene Lineages
+if [ "$did_any" -eq 0 ]; then
+ [ -n "$UI_HOST" ] && [ -d "${ACME_BASE}/${UI_HOST}" ] && copy_cert "${ACME_BASE}/${UI_HOST}" "/etc/ssl/ui"
+ [ -n "$WEBMAIL_HOST" ] && [ -d "${ACME_BASE}/${WEBMAIL_HOST}" ] && copy_cert "${ACME_BASE}/${WEBMAIL_HOST}" "/etc/ssl/webmail"
+ [ -n "$MAIL_HOSTNAME" ] && [ -d "${ACME_BASE}/${MAIL_HOSTNAME}" ] && copy_cert "${ACME_BASE}/${MAIL_HOSTNAME}" "/etc/ssl/mail"
+fi
+
+# TLSA-Refresh (tolerant falls App noch nicht ready)
+if command -v php >/dev/null 2>&1 && [ -f /var/www/mailwolt/artisan ]; then
+ (cd /var/www/mailwolt && php artisan dns:tlsa:refresh) || true
+fi
+
+# Services neu laden
+if [ -n "$HOST" ]; then
+ if [ -n "$MAIL_HOSTNAME" ] && [ "$HOST" = "$MAIL_HOSTNAME" ]; then
+ reload_services mail
+ else
+ reload_services ui
+ fi
+else
+ reload_services ui
+fi
+
+exit 0
+WRAP
+chmod +x /usr/local/sbin/mailwolt-deploy.sh
+
+# -------------------------------------------------------------------
+# 3) Certbot deploy-hook, der den Wrapper aufruft
+# -------------------------------------------------------------------
+install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
+cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-certs.sh <<'HOOK'
+#!/bin/sh
+exec /usr/local/sbin/mailwolt-deploy.sh
+HOOK
+chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-certs.sh
+
+
+log "[✓] MailWolt Deploy-Hook eingerichtet"
+
+##!/usr/bin/env bash
+#set -euo pipefail
+#source ./lib.sh
+#
+## Persistente Installer-Variablen (werden vom Wrapper gelesen)
+#install -d -m 0755 /etc/mailwolt
+#cat >/etc/mailwolt/installer.env </usr/local/sbin/mw-deploy.sh <<'WRAP'
+##!/usr/bin/env bash
+#set -euo pipefail
+#
+## Installer-Variablen laden
+#set +u
+#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
+#set -u
+#
+#UI_HOST="${UI_HOST:-}"
+#WEBMAIL_HOST="${WEBMAIL_HOST:-}"
+#MAIL_HOSTNAME="${MAIL_HOSTNAME:-}"
+#
+## --- Kopieren statt Symlinks (damit Laravel lesen kann) ---------------------
+#copy_cert() {
+# local le_base="$1" target_dir="$2"
+# local cert="${le_base}/fullchain.pem"
+# local key="${le_base}/privkey.pem"
+#
+# [[ -s "$cert" && -s "$key" ]] || return 0
+#
+# install -d -m 0755 "$target_dir"
+#
+# # Vorhandene Symlinks entfernen, sonst kopierst du in die LE-Datei hinein
+# [ -L "${target_dir}/fullchain.pem" ] && rm -f "${target_dir}/fullchain.pem"
+# [ -L "${target_dir}/privkey.pem" ] && rm -f "${target_dir}/privkey.pem"
+#
+# # Echte Dateien ablegen
+# install -m 0644 "$cert" "${target_dir}/fullchain.pem"
+# install -m 0600 "$key" "${target_dir}/privkey.pem"
+#
+# echo "[+] Copied ${target_dir}/fullchain.pem und privkey.pem ← ${le_base}"
+#}
+#
+## Nur Domains bearbeiten, die in diesem Lauf betroffen sind.
+## Bei manchen Distros ist RENEWED_DOMAINS auf Erst-issue leer -> Fallback nutzen.
+#RDOMS=" ${RENEWED_DOMAINS:-} "
+#did_any=0
+#
+#maybe_copy_for() {
+# local host="$1" dir="$2"
+# [[ -z "$host" ]] && return 0
+# if [[ "$RDOMS" == *" ${host} "* ]]; then
+# copy_cert "/etc/letsencrypt/live/${host}" "${dir}"
+# did_any=1
+# fi
+#}
+#
+## 1) Normalfall: nur die vom Certbot gemeldeten Hosts kopieren
+#maybe_copy_for "$UI_HOST" "/etc/ssl/ui"
+#maybe_copy_for "$WEBMAIL_HOST" "/etc/ssl/webmail"
+#maybe_copy_for "$MAIL_HOSTNAME" "/etc/ssl/mail"
+#
+## 2) Fallback: Beim Erstlauf/Edge-Cases alles kopieren, was bereits existiert
+#if [[ "$did_any" -eq 0 ]]; then
+# [[ -n "$UI_HOST" && -d "/etc/letsencrypt/live/${UI_HOST}" ]] && copy_cert "/etc/letsencrypt/live/${UI_HOST}" "/etc/ssl/ui"
+# [[ -n "$WEBMAIL_HOST" && -d "/etc/letsencrypt/live/${WEBMAIL_HOST}" ]] && copy_cert "/etc/letsencrypt/live/${WEBMAIL_HOST}" "/etc/ssl/webmail"
+# [[ -n "$MAIL_HOSTNAME" && -d "/etc/letsencrypt/live/${MAIL_HOSTNAME}"]] && copy_cert "/etc/letsencrypt/live/${MAIL_HOSTNAME}"/etc/ssl/mail
+#fi
+#
+## Optional: TLSA via Laravel (tolerant, falls App noch nicht gebaut)
+#if command -v php >/dev/null 2>&1 && [ -d /var/www/mailwolt ] && [ -f /var/www/mailwolt/artisan ]; then
+# (cd /var/www/mailwolt && php artisan dns:tlsa:refresh) || true
+#fi
+#
+## Nginx nur neu laden, wenn aktiv
+#if systemctl is-active --quiet nginx; then
+# systemctl reload nginx || true
+#fi
+#WRAP
+#chmod +x /usr/local/sbin/mw-deploy.sh
+#
+## 2) Certbot-Deploy-Hook: ruft den Wrapper bei jeder erfolgreichen Ausstellung/Renew auf
+#install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
+#cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-certs.sh <<'HOOK'
+##!/usr/bin/env bash
+#exec /usr/local/sbin/mw-deploy.sh
+#HOOK
+#chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-certs.sh
+#
+#log "[✓] MailWolt Deploy-Hook eingerichtet"
+
+##!/usr/bin/env bash
+#set -euo pipefail
+#source ./lib.sh
+#
+## Persistente Installer-Variablen (werden vom Wrapper gelesen)
+#install -d -m 0755 /etc/mailwolt
+#cat >/etc/mailwolt/installer.env </usr/local/sbin/mw-deploy.sh <<'WRAP'
+##!/usr/bin/env bash
+#set -euo pipefail
+#
+## Installer-Variablen laden
+#set +u
+#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
+#set -u
+#
+#UI_HOST="${UI_HOST:-}"
+#WEBMAIL_HOST="${WEBMAIL_HOST:-}"
+#MAIL_HOSTNAME="${MAIL_HOSTNAME:-}"
+#
+## --- Kopieren statt Symlinks (damit Laravel lesen kann) ---------------------
+#copy_cert() {
+# local le_base="$1" target_dir="$2"
+# local cert="${le_base}/fullchain.pem"
+# local key="${le_base}/privkey.pem"
+#
+# [[ -s "$cert" && -s "$key" ]] || return 0
+#
+# # Zielordner sicherstellen
+# install -d -m 0755 "$target_dir"
+#
+# # Falls vorher Symlinks existieren → entfernen, sonst würde "install" das Ziel des Links überschreiben
+# [ -L "${target_dir}/fullchain.pem" ] && rm -f "${target_dir}/fullchain.pem"
+# [ -L "${target_dir}/privkey.pem" ] && rm -f "${target_dir}/privkey.pem"
+#
+# # KOPIEREN mit sauberen Rechten (Chain world-readable, Key nur root)
+# install -m 0644 "$cert" "${target_dir}/fullchain.pem"
+# install -m 0600 "$key" "${target_dir}/privkey.pem"
+#
+# echo "[+] Copied ${target_dir}/fullchain.pem und privkey.pem ← ${le_base}"
+#}
+#
+## Nur für Domains arbeiten, die in diesem Lauf betroffen sind
+#RDOMS=" ${RENEWED_DOMAINS:-} "
+#
+## UI
+#if [[ -n "$UI_HOST" && "$RDOMS" == *" ${UI_HOST} "* ]]; then
+# copy_cert "/etc/letsencrypt/live/${UI_HOST}" "/etc/ssl/ui"
+#fi
+## Webmail
+#if [[ -n "$WEBMAIL_HOST" && "$RDOMS" == *" ${WEBMAIL_HOST} "* ]]; then
+# copy_cert "/etc/letsencrypt/live/${WEBMAIL_HOST}" "/etc/ssl/webmail"
+#fi
+## MX
+#if [[ -n "$MAIL_HOSTNAME" && "$RDOMS" == *" ${MAIL_HOSTNAME} "* ]]; then
+# copy_cert "/etc/letsencrypt/live/${MAIL_HOSTNAME}" "/etc/ssl/mail"
+#fi
+#
+## Optional: TLSA via Laravel (still tolerant, falls App noch nicht gebaut)
+#if command -v php >/dev/null 2>&1 && [ -d /var/www/mailwolt ] && [ -f /var/www/mailwolt/artisan ]; then
+# (cd /var/www/mailwolt && php artisan dns:tlsa:refresh) || true
+#fi
+#
+## Nginx nur neu laden, wenn aktiv
+#if systemctl is-active --quiet nginx; then
+# systemctl reload nginx || true
+#fi
+#WRAP
+#chmod +x /usr/local/sbin/mw-deploy.sh
+#
+## 2) Certbot-Deploy-Hook: ruft den Wrapper bei jeder erfolgreichen Ausstellung/Renew auf
+#install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
+#cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh <<'HOOK'
+##!/usr/bin/env bash
+#exec /usr/local/sbin/mw-deploy.sh
+#HOOK
+#chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh
+#
+#log "[✓] MailWolt Deploy-Hook eingerichtet"
+
+##!/usr/bin/env bash
+#set -euo pipefail
+#source ./lib.sh
+#
+#install -d -m 0755 /etc/mailwolt
+#cat >/etc/mailwolt/installer.env </usr/local/sbin/mw-deploy.sh <<'WRAP'
+##!/usr/bin/env bash
+#set -euo pipefail
+#
+## Installer-Variablen laden (UI_HOST, WEBMAIL_HOST, MAIL_HOSTNAME, optional LE_EMAIL etc.)
+#set +u
+#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
+#set -u
+#
+#UI_HOST="${UI_HOST:-}"
+#WEBMAIL_HOST="${WEBMAIL_HOST:-}"
+#MAIL_HOSTNAME="${MAIL_HOSTNAME:-}"
+#
+#link_if() {
+# local le_base="$1" target_dir="$2"
+# local cert="${le_base}/fullchain.pem"
+# local key="${le_base}/privkey.pem"
+# [[ -s "$cert" && -s "$key" ]] || return 0
+# install -d -m 0755 "$target_dir"
+# ln -sf "$cert" "${target_dir}/fullchain.pem"
+# ln -sf "$key" "${target_dir}/privkey.pem"
+# chmod 644 "${target_dir}/fullchain.pem" 2>/dev/null || true
+# chmod 600 "${target_dir}/privkey.pem" 2>/dev/null || true
+# echo "[+] Linked ${target_dir} -> ${le_base}"
+#}
+#
+## Nur für Domains arbeiten, die im aktuellen Lauf erneuert/ausgestellt wurden
+#RDOMS=" ${RENEWED_DOMAINS:-} "
+#
+## UI
+#if [[ -n "$UI_HOST" && "$RDOMS" == *" ${UI_HOST} "* ]]; then
+# link_if "/etc/letsencrypt/live/${UI_HOST}" "/etc/ssl/ui"
+#fi
+## Webmail
+#if [[ -n "$WEBMAIL_HOST" && "$RDOMS" == *" ${WEBMAIL_HOST} "* ]]; then
+# link_if "/etc/letsencrypt/live/${WEBMAIL_HOST}" "/etc/ssl/webmail"
+#fi
+## MX
+#if [[ -n "$MAIL_HOSTNAME" && "$RDOMS" == *" ${MAIL_HOSTNAME} "* ]]; then
+# link_if "/etc/letsencrypt/live/${MAIL_HOSTNAME}" "/etc/ssl/mail"
+#fi
+#
+## Optional: TLSA via Laravel, falls App schon vorhanden (sonst still überspringen)
+#if command -v php >/dev/null 2>&1 && [ -d /var/www/mailwolt ]; then
+# (cd /var/www/mailwolt && php artisan dns:tlsa:refresh) || true
+#fi
+#
+## Nginx nur neu laden, wenn aktiv
+#if systemctl is-active --quiet nginx; then
+# systemctl reload nginx || true
+#fi
+#WRAP
+#chmod +x /usr/local/sbin/mw-deploy.sh
+#
+## 2) Certbot-Deploy-Hooks einrichten (ruft nur den Wrapper auf)
+#install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
+#cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh <<'HOOK'
+##!/usr/bin/env bash
+#exec /usr/local/sbin/mw-deploy.sh
+#HOOK
+#chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh
+#
+#log "[✓] MailWolt Deploy-Hook eingerichtet"
+
+##!/usr/bin/env bash
+#set -euo pipefail
+#source ./lib.sh
+#
+#log "Let's Encrypt Deploy-Hooks und Wrapper anlegen …"
+#
+## 1) Wrapper-Skript, das Symlinks setzt und Nginx reloaded
+#cat >/usr/local/sbin/mw-deploy.sh <<'WRAP'
+##!/usr/bin/env bash
+#set -euo pipefail
+#
+#link_if() {
+# local le_base="$1" target_dir="$2"
+# local cert="${le_base}/fullchain.pem"
+# local key="${le_base}/privkey.pem"
+# [[ -s "$cert" && -s "$key" ]] || return 0
+# install -d -m 0755 "$target_dir"
+# ln -sf "$cert" "${target_dir}/fullchain.pem"
+# ln -sf "$key" "${target_dir}/privkey.pem"
+# chmod 644 "${target_dir}/fullchain.pem" 2>/dev/null || true
+# chmod 600 "${target_dir}/privkey.pem" 2>/dev/null || true
+# echo "[+] Linked ${target_dir} -> ${le_base}"
+#}
+#
+#UI_HOST="${UI_HOST:-}"
+#WEBMAIL_HOST="${WEBMAIL_HOST:-}"
+#MAIL_HOSTNAME="${MAIL_HOSTNAME:-}"
+#
+#[[ -n "$UI_HOST" ]] && link_if "/etc/letsencrypt/live/${UI_HOST}" "/etc/ssl/ui"
+#[[ -n "$WEBMAIL_HOST" ]] && link_if "/etc/letsencrypt/live/${WEBMAIL_HOST}" "/etc/ssl/webmail"
+#[[ -n "$MAIL_HOSTNAME" ]] && link_if "/etc/letsencrypt/live/${MAIL_HOSTNAME}" "/etc/ssl/mail"
+#
+#if systemctl is-active --quiet nginx; then
+# systemctl reload nginx || true
+#fi
+#WRAP
+#
+#chmod +x /usr/local/sbin/mw-deploy.sh
+#
+## 2) Certbot Deploy-Hook-Verzeichnis + Symlink für Renewals
+#install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
+#cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh <<'HOOK'
+##!/usr/bin/env bash
+#exec /usr/local/sbin/mw-deploy.sh
+#HOOK
+#chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh
+#
+#log "[✓] MailWolt Deploy-Hook eingerichtet"
+#
+###!/usr/bin/env bash
+##set -euo pipefail
+##source ./lib.sh
+##
+### ────────────────────────────────────────────────────────────────────────────
+### 21-le-deploy-hook.sh
+### • legt /etc/mailwolt/installer.env an (falls fehlt)
+### • erzeugt Deploy-Hooks:
+### - 50-mailwolt-symlinks.sh → verlinkt LE-Zerts nach /etc/ssl/{ui,webmail,mail}
+### - 60-mailwolt-tlsa.sh → aktualisiert TLSA (3 1 1) für MX bei jedem Renew
+### • KEIN Reload von Postfix/Dovecot (kommt später im Installer)
+### ────────────────────────────────────────────────────────────────────────────
+##
+### 0) Hostnamen persistent speichern (für spätere Deploys)
+##install -d -m 0755 /etc/mailwolt
+##if [[ ! -f /etc/mailwolt/installer.env ]]; then
+## cat >/etc/mailwolt/installer.env </etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh </dev/null || true
+## chmod 600 "\${target_dir}/privkey.pem" 2>/dev/null || true
+## echo "[+] Linked \${target_dir} -> \${le_base}"
+##}
+##
+### Verlinken (nur wenn Host konfiguriert)
+##[[ -n "${UI_HOST}" ]] && link_if "\$UI_LE" "\$UI_SSL_DIR"
+##[[ -n "${WEBMAIL_HOST}" ]] && link_if "\$WEBMAIL_LE" "\$WEBMAIL_SSL_DIR"
+##[[ -n "${MAIL_HOSTNAME}" ]] && link_if "\$MX_LE" "\$MAIL_SSL_DIR"
+##
+### Nur reloaden, wenn Nginx aktiv ist (Installer startet ihn später erst)
+##if systemctl is-active --quiet nginx; then
+## systemctl reload nginx || true
+##fi
+##HOOK
+##chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh
+##
+### ────────────────────────────────────────────────────────────────────────────
+### 3) 60-mailwolt-tlsa.sh
+### → nutzt Laravel, falls vorhanden; sonst Fallback mit OpenSSL.
+### → schreibt nur, wenn sich der Hash geändert hat (idempotent)
+### ────────────────────────────────────────────────────────────────────────────
+##cat >/etc/letsencrypt/renewal-hooks/deploy/60-mailwolt-tlsa.sh <<'HOOK'
+###!/usr/bin/env bash
+##set -euo pipefail
+##
+### installer.env lesen
+##set +u
+##[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
+##set -u
+##
+##APP_ENV_VAL="${APP_ENV:-production}"
+##BASE_DOMAIN_VAL="${BASE_DOMAIN:-example.com}"
+##
+##case "$APP_ENV_VAL" in
+## local|dev|development) exit 0 ;;
+##esac
+##[ "$BASE_DOMAIN_VAL" = "example.com" ] && exit 0
+##
+##MX_HOST="${MAIL_HOSTNAME:-}"
+##SERVICE="_25._tcp"
+##DNS_DIR="/etc/mailwolt/dns"
+##OUT_FILE="${DNS_DIR}/${MX_HOST}.tlsa.txt"
+##
+### Nur reagieren, wenn MX-Zertifikat betroffen war
+##case " ${RENEWED_DOMAINS:-} " in
+## *" ${MX_HOST} "*) ;;
+## *) exit 0 ;;
+##esac
+##
+##CERT="${RENEWED_LINEAGE}/fullchain.pem"
+##[ -s "$CERT" ] || exit 0
+##
+### Wenn Laravel vorhanden ist → interner Command (DB + Datei idempotent)
+##if command -v php >/dev/null 2>&1 && [ -d /var/www/mailwolt ]; then
+## cd /var/www/mailwolt || exit 0
+## php artisan dns:tlsa:refresh || true
+## exit 0
+##fi
+##
+### Fallback: nur Datei aktualisieren, wenn Hash sich ändert
+##HASH="$(openssl x509 -in "$CERT" -noout -pubkey \
+## | openssl pkey -pubin -outform DER \
+## | openssl dgst -sha256 | sed 's/^.*= //')"
+##NEW_LINE="${SERVICE}.${MX_HOST}. IN TLSA 3 1 1 ${HASH}"
+##
+##mkdir -p "$DNS_DIR"
+##
+##if [ -r "$OUT_FILE" ] && grep -q "IN TLSA" "$OUT_FILE"; then
+## if grep -q "$HASH" "$OUT_FILE"; then
+## echo "[TLSA] Unverändert – kein Update nötig."
+## exit 0
+## fi
+##fi
+##
+##echo "$NEW_LINE" > "$OUT_FILE"
+##echo "[TLSA] Aktualisiert: $NEW_LINE"
+##HOOK
+##chmod +x /etc/letsencrypt/renewal-hooks/deploy/60-mailwolt-tlsa.sh
+##
+### ────────────────────────────────────────────────────────────────────────────
+##echo "[✓] Deploy-Hooks installiert."
\ No newline at end of file
diff --git a/mailwolt-installer/scripts/22-dkim-helper.sh b/mailwolt-installer/scripts/22-dkim-helper.sh
new file mode 100644
index 0000000..2ef8c7d
--- /dev/null
+++ b/mailwolt-installer/scripts/22-dkim-helper.sh
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+set -euo pipefail
+source ./lib.sh
+
+log "Installiere DKIM-Helper …"
+
+install -d -m 0755 /usr/local/sbin
+
+cat >/usr/local/sbin/mailwolt-install-dkim <<'EOF'
+#!/usr/bin/env bash
+set -euo pipefail
+
+DOMAIN="$1" # z.B. sysmail.toastra.com
+SELECTOR="${2:-mwl1}"
+
+[[ -n "$DOMAIN" ]] || { echo "Usage: $0 [selector]"; exit 2; }
+
+KEYDIR="/etc/opendkim/keys/${DOMAIN}"
+PRIV="${KEYDIR}/${SELECTOR}.private"
+TXT="${KEYDIR}/${SELECTOR}.txt"
+
+install -d -m 0750 -o opendkim -g opendkim "$KEYDIR"
+
+if [[ ! -s "$PRIV" ]]; then
+ opendkim-genkey -b 2048 -s "$SELECTOR" -d "$DOMAIN" -D "$KEYDIR"
+ chown opendkim:opendkim "$PRIV"
+ chmod 600 "$PRIV"
+fi
+
+grep -q "^${SELECTOR}\._domainkey\.${DOMAIN} " /etc/opendkim/KeyTable 2>/dev/null \
+ || echo "${SELECTOR}._domainkey.${DOMAIN} ${DOMAIN}:${SELECTOR}:${PRIV}" >> /etc/opendkim/KeyTable
+
+grep -q "^\*@${DOMAIN} " /etc/opendkim/SigningTable 2>/dev/null \
+ || echo "*@${DOMAIN} ${SELECTOR}._domainkey.${DOMAIN}" >> /etc/opendkim/SigningTable
+
+install -d -m 0755 /etc/mailwolt/dns
+[[ -s "$TXT" ]] && cp -f "$TXT" "/etc/mailwolt/dns/dkim-${DOMAIN}.txt" || true
+
+systemctl restart opendkim
+EOF
+
+log "[✓] DKIM-Helper installiert: /usr/local/sbin/mailwolt-install-dkim"
\ No newline at end of file
diff --git a/mailwolt-installer/scripts/30-db.sh b/mailwolt-installer/scripts/30-db.sh
new file mode 100644
index 0000000..9781562
--- /dev/null
+++ b/mailwolt-installer/scripts/30-db.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+set -euo pipefail
+source ./lib.sh
+
+log "MariaDB vorbereiten …"
+systemctl enable --now mariadb
+mysql -uroot < /etc/postfix/sql/mysql-virtual-domains.cf < /etc/postfix/sql/mysql-virtual-mailbox-maps.cf < /etc/postfix/sql/mysql-virtual-alias-maps.cf </dev/null 2>&1 || true
diff --git a/mailwolt-installer/scripts/50-dovecot.sh b/mailwolt-installer/scripts/50-dovecot.sh
new file mode 100644
index 0000000..bd2922f
--- /dev/null
+++ b/mailwolt-installer/scripts/50-dovecot.sh
@@ -0,0 +1,247 @@
+#!/usr/bin/env bash
+set -euo pipefail
+source ./lib.sh
+
+MAIL_SSL_DIR="/etc/ssl/mail"
+MAIL_CERT="${MAIL_SSL_DIR}/fullchain.pem"
+MAIL_KEY="${MAIL_SSL_DIR}/privkey.pem"
+
+log "Dovecot konfigurieren …"
+
+# ──────────────────────────────────────────────────────────────────────────────
+# 1) vmail-Benutzer/Gruppe & Mailspool vorbereiten (DYNAMIC UID!)
+# ──────────────────────────────────────────────────────────────────────────────
+
+# Sicherstellen, dass die Gruppe 'mail' existiert (auf Debian/Ubuntu idR vorhanden)
+getent group mail >/dev/null || groupadd -g 8 mail || true
+
+# vmail anlegen, wenn er fehlt. Bevorzugt UID 109, falls frei – sonst automatisch.
+if ! getent passwd vmail >/dev/null; then
+ if ! getent passwd 109 >/dev/null; then
+ useradd -u 109 -g mail -d /var/mail -M -s /usr/sbin/nologin vmail
+ else
+ useradd -g mail -d /var/mail -M -s /usr/sbin/nologin vmail
+ fi
+fi
+
+# Tatsächliche vmail-UID ermitteln (wird unten in die Dovecot-Config geschrieben)
+VMAIL_UID="$(id -u vmail)"
+
+# Mailspool-Basis
+install -d -m 0770 -o vmail -g mail /var/mail/vhosts
+
+# ──────────────────────────────────────────────────────────────────────────────
+# 2) Dovecot Grundgerüst
+# ──────────────────────────────────────────────────────────────────────────────
+
+# Hauptdatei
+install -d -m 0755 /etc/dovecot/conf.d
+cat > /etc/dovecot/dovecot.conf <<'CONF'
+!include_try /etc/dovecot/conf.d/*.conf
+CONF
+
+# Mail-Location & Namespace + UID-Grenzen
+cat > /etc/dovecot/conf.d/10-mail.conf < /etc/dovecot/conf.d/15-mailboxes.conf <<'CONF'
+namespace inbox {
+ inbox = yes
+
+ mailbox Drafts {
+ special_use = \Drafts
+ auto = subscribe
+ }
+ mailbox Junk {
+ special_use = \Junk
+ auto = subscribe
+ }
+ mailbox Trash {
+ special_use = \Trash
+ auto = subscribe
+ }
+ mailbox Sent {
+ special_use = \Sent
+ auto = subscribe
+ }
+ mailbox Archive {
+ special_use = \Archive
+ auto = create
+ }
+}
+CONF
+
+# Auth
+cat > /etc/dovecot/conf.d/10-auth.conf <<'CONF'
+disable_plaintext_auth = yes
+auth_mechanisms = plain login
+!include_try auth-sql.conf.ext
+
+auth_cache_size = 10M
+auth_cache_ttl = 1 hour
+CONF
+
+# SQL-Anbindung (Passwörter aus App-DB)
+cat > /etc/dovecot/dovecot-sql.conf.ext < /etc/dovecot/conf.d/auth-sql.conf.ext <<'CONF'
+passdb {
+ driver = sql
+ args = /etc/dovecot/dovecot-sql.conf.ext
+}
+userdb {
+ driver = static
+ args = uid=vmail gid=mail home=/var/mail/vhosts/%d/%n
+}
+CONF
+chown root:dovecot /etc/dovecot/conf.d/auth-sql.conf.ext
+chmod 640 /etc/dovecot/conf.d/auth-sql.conf.ext
+
+# ──────────────────────────────────────────────────────────────────────────────
+# 3) IMAP Optimierung (iOS/IDLE-freundlich)
+# ──────────────────────────────────────────────────────────────────────────────
+
+cat > /etc/dovecot/conf.d/20-imap.conf <<'CONF'
+# IMAP-spezifische Einstellungen
+
+imap_idle_notify_interval = 2 mins
+imap_hibernate_timeout = 0
+
+protocol imap {
+ mail_max_userip_connections = 20
+ imap_logout_format = in=%i out=%o deleted=%{deleted} expunged=%{expunged}
+}
+CONF
+
+# ──────────────────────────────────────────────────────────────────────────────
+# 4) Master Services (LMTP, AUTH, IMAP, POP3, STATS)
+# ──────────────────────────────────────────────────────────────────────────────
+
+cat > /etc/dovecot/conf.d/10-master.conf <<'CONF'
+service lmtp {
+ unix_listener /var/spool/postfix/private/dovecot-lmtp {
+ mode = 0600
+ user = postfix
+ group = postfix
+ }
+}
+service auth {
+ unix_listener /var/spool/postfix/private/auth {
+ mode = 0660
+ user = postfix
+ group = postfix
+ }
+ unix_listener auth-userdb {
+ mode = 0660
+ user = vmail
+ group = mail
+ }
+ process_limit = 1
+}
+service imap-login {
+ inet_listener imap {
+ port = 143
+ }
+ inet_listener imaps {
+ port = 993
+ ssl = yes
+ }
+ process_limit = 128
+ process_min_avail = 10
+ service_count = 0
+ vsz_limit = 512M
+}
+service pop3-login {
+ inet_listener pop3 {
+ port = 110
+ }
+ inet_listener pop3s {
+ port = 995
+ ssl = yes
+ }
+ process_limit = 50
+ service_count = 0
+}
+CONF
+
+# --- Dovecot: doveadm-server für App-Zugriff ---
+cat >/etc/dovecot/conf.d/99-mailwolt-perms.conf <<'CONF'
+service auth {
+ unix_listener auth-userdb {
+ mode = 0660
+ user = vmail
+ group = mail
+ }
+}
+
+service stats {
+ unix_listener stats-reader {
+ mode = 0660
+ user = vmail
+ group = mail
+ }
+ unix_listener stats-writer {
+ mode = 0660
+ user = vmail
+ group = mail
+ }
+}
+CONF
+
+# ──────────────────────────────────────────────────────────────────────────────
+# 5) SSL-Konfiguration (ohne DH-Param-Erzeugung)
+# ──────────────────────────────────────────────────────────────────────────────
+
+DOVECOT_SSL_CONF="/etc/dovecot/conf.d/10-ssl.conf"
+touch "$DOVECOT_SSL_CONF"
+grep -q '^ssl\s*=' "$DOVECOT_SSL_CONF" 2>/dev/null || echo "ssl = required" >> "$DOVECOT_SSL_CONF"
+if grep -q '^\s*ssl_cert\s*=' "$DOVECOT_SSL_CONF"; then
+ sed -i "s|^\s*ssl_cert\s*=.*|ssl_cert = <${MAIL_CERT}|" "$DOVECOT_SSL_CONF"
+else
+ echo "ssl_cert = <${MAIL_CERT}" >> "$DOVECOT_SSL_CONF"
+fi
+if grep -q '^\s*ssl_key\s*=' "$DOVECOT_SSL_CONF"; then
+ sed -i "s|^\s*ssl_key\s*=.*|ssl_key = <${MAIL_KEY}|" "$DOVECOT_SSL_CONF"
+else
+ echo "ssl_key = <${MAIL_KEY}" >> "$DOVECOT_SSL_CONF"
+fi
+grep -q '^ssl_min_protocol' "$DOVECOT_SSL_CONF" || echo "ssl_min_protocol = TLSv1.2" >> "$DOVECOT_SSL_CONF"
+grep -q '^ssl_prefer_server_ciphers' "$DOVECOT_SSL_CONF" || echo "ssl_prefer_server_ciphers = yes" >> "$DOVECOT_SSL_CONF"
+
+grep -q '^ssl_dh' "$DOVECOT_SSL_CONF" || echo "ssl_dh = > "$DOVECOT_SSL_CONF"
+
+# ──────────────────────────────────────────────────────────────────────────────
+# 6) Verzeichnisse & Rechte prüfen
+# ──────────────────────────────────────────────────────────────────────────────
+
+mkdir -p /var/spool/postfix/private
+chown root:root /var/spool/postfix
+chmod 0755 /var/spool/postfix
+chown postfix:postfix /var/spool/postfix/private
+chmod 0755 /var/spool/postfix/private
+
+
+# ──────────────────────────────────────────────────────────────────────────────
+# 7) Abschluss
+# ──────────────────────────────────────────────────────────────────────────────
+
+log "Dovecot-Konfiguration abgeschlossen."
diff --git a/mailwolt-installer/scripts/60-rspamd-opendkim.sh b/mailwolt-installer/scripts/60-rspamd-opendkim.sh
new file mode 100644
index 0000000..9d7166a
--- /dev/null
+++ b/mailwolt-installer/scripts/60-rspamd-opendkim.sh
@@ -0,0 +1,319 @@
+#!/usr/bin/env bash
+set -euo pipefail
+source ./lib.sh
+
+log "Rspamd + OpenDKIM einrichten …"
+
+# ──────────────────────────────────────────────────────────────
+# ENV laden
+# ──────────────────────────────────────────────────────────────
+set +u
+[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
+set -u
+
+BASE_DOMAIN="${BASE_DOMAIN:-example.com}"
+SYSMAIL_DOMAIN="${SYSMAIL_DOMAIN:-sysmail.${BASE_DOMAIN}}" # z.B. sysmail.example.com
+DKIM_ENABLE="${DKIM_ENABLE:-1}" # 1=OpenDKIM aktiv
+DKIM_SELECTOR="${DKIM_SELECTOR:-mwl1}" # z.B. mwl1
+DKIM_GENERATE="${DKIM_GENERATE:-0}" # 1=Key generieren, falls fehlt
+RSPAMD_CONTROLLER_PASSWORD="${RSPAMD_CONTROLLER_PASSWORD:-admin}"
+
+# ──────────────────────────────────────────────────────────────
+# Rspamd (Controller + Milter)
+# ──────────────────────────────────────────────────────────────
+install -d -m 0750 /etc/rspamd/local.d
+
+if command -v rspamadm >/dev/null 2>&1; then
+ RSPAMD_HASH="$(rspamadm pw -p "${RSPAMD_CONTROLLER_PASSWORD}")"
+else
+ RSPAMD_HASH="${RSPAMD_CONTROLLER_PASSWORD}"
+fi
+
+cat >/etc/rspamd/local.d/worker-controller.inc </etc/rspamd/local.d/statistic.conf </etc/rspamd/local.d/worker-proxy.inc <<'CONF'
+worker "proxy" {
+ bind_socket = "127.0.0.1:11332";
+ milter = yes;
+ timeout = 120s;
+
+ upstream "scan" {
+ default = yes;
+ self_scan = yes;
+ servers = "127.0.0.1:11333";
+ }
+}
+CONF
+
+cat >/etc/rspamd/local.d/worker-normal.inc <<'CONF'
+worker "normal" {
+ bind_socket = "127.0.0.1:11333";
+}
+CONF
+
+cat >/etc/rspamd/local.d/milter_headers.conf <<'CONF'
+use = ["authentication-results"];
+header = "Authentication-Results";
+CONF
+
+cat >/etc/rspamd/local.d/options.inc <<'CONF'
+dns {
+ servers = ["9.9.9.9:53", "1.1.1.1:53"];
+ timeout = 5s;
+ retransmits = 2;
+}
+CONF
+
+# ──────────────────────────────────────────────────────────────
+# Rspamd Redis-Konfiguration
+# ──────────────────────────────────────────────────────────────
+log "Rspamd Redis konfigurieren …"
+
+: "${REDIS_PASS:=}"
+
+cat >/etc/rspamd/local.d/redis.conf </dev/null 2>&1; then
+ if [[ -n "${REDIS_PASS}" ]]; then
+ if redis-cli -h 127.0.0.1 -p 6379 -a "${REDIS_PASS}" ping >/dev/null 2>&1; then
+ log "[✓] Redis erreichbar und Passwort akzeptiert."
+ else
+ log "[!] Warnung: Redis antwortet nicht oder Passwort falsch."
+ fi
+ else
+ if redis-cli -h 127.0.0.1 -p 6379 ping >/dev/null 2>&1; then
+ log "[✓] Redis erreichbar (ohne Passwort)."
+ else
+ log "[!] Warnung: Redis antwortet nicht."
+ fi
+ fi
+fi
+
+systemctl enable --now rspamd || true
+
+# ──────────────────────────────────────────────────────────────
+# OpenDKIM – nur wenn DKIM_ENABLE=1
+# ──────────────────────────────────────────────────────────────
+
+if [[ "${DKIM_ENABLE}" != "1" ]]; then
+ log "DKIM_ENABLE=0 → OpenDKIM wird übersprungen."
+ /usr/sbin/postconf -e "milter_default_action = accept"
+ /usr/sbin/postconf -e "milter_protocol = 6"
+ /usr/sbin/postconf -e "smtpd_milters = inet:127.0.0.1:11332"
+ /usr/sbin/postconf -e "non_smtpd_milters = inet:127.0.0.1:11332"
+ exit 0
+fi
+
+
+install -d -m 0755 /etc/opendkim
+install -d -m 0750 /etc/opendkim/keys
+chown -R opendkim:opendkim /etc/opendkim
+chmod 750 /etc/opendkim/keys
+
+# TrustedHosts
+cat >/etc/opendkim/TrustedHosts <<'CONF'
+127.0.0.1
+::1
+localhost
+CONF
+chown opendkim:opendkim /etc/opendkim/TrustedHosts
+chmod 640 /etc/opendkim/TrustedHosts
+
+# ── Key-Verzeichnis für SYSMAIL_DOMAIN vorbereiten ───────────────────────────
+KEY_DIR="/etc/opendkim/keys/${SYSMAIL_DOMAIN}"
+KEY_PRIV="${KEY_DIR}/${DKIM_SELECTOR}.private"
+KEY_DNSTXT="${KEY_DIR}/${DKIM_SELECTOR}.txt"
+install -d -m 0750 -o opendkim -g opendkim "${KEY_DIR}"
+
+# ── Key optional generieren (nur wenn gewünscht) ─────────────────────────────
+if [[ ! -s "${KEY_PRIV}" && "${DKIM_GENERATE}" = "1" ]]; then
+ if command -v opendkim-genkey >/dev/null 2>&1; then
+ opendkim-genkey -b 2048 -s "${DKIM_SELECTOR}" -d "${SYSMAIL_DOMAIN}" -D "${KEY_DIR}"
+ chown opendkim:opendkim "${KEY_DIR}/${DKIM_SELECTOR}.private" || true
+ chmod 600 "${KEY_DIR}/${DKIM_SELECTOR}.private" || true
+ else
+ echo "[!] opendkim-genkey fehlt – kann DKIM-Key nicht generieren."
+ fi
+fi
+
+# ── Key-/SigningTable nur anlegen, nicht leeren ───────────────────────────────
+touch /etc/opendkim/KeyTable /etc/opendkim/SigningTable
+chown opendkim:opendkim /etc/opendkim/KeyTable /etc/opendkim/SigningTable
+chmod 640 /etc/opendkim/KeyTable /etc/opendkim/SigningTable
+
+if [[ -s "${KEY_PRIV}" && "${BASE_DOMAIN}" != "example.com" ]]; then
+ LINE_KT="${DKIM_SELECTOR}._domainkey.${SYSMAIL_DOMAIN} ${SYSMAIL_DOMAIN}:${DKIM_SELECTOR}:${KEY_PRIV}"
+ LINE_ST="*@${SYSMAIL_DOMAIN} ${DKIM_SELECTOR}._domainkey.${SYSMAIL_DOMAIN}"
+ grep -Fqx "$LINE_KT" /etc/opendkim/KeyTable || echo "$LINE_KT" >> /etc/opendkim/KeyTable
+ grep -Fqx "$LINE_ST" /etc/opendkim/SigningTable || echo "$LINE_ST" >> /etc/opendkim/SigningTable
+else
+ echo "[i] Kein Private Key unter ${KEY_PRIV} – App-Helper trägt später ein."
+fi
+
+# ── Hauptkonfiguration ───────────────────────────────────────────────────────
+cat >/etc/opendkim.conf <<'CONF'
+Syslog yes
+UMask 002
+Mode sv
+Socket inet:8891@127.0.0.1
+PidFile /run/opendkim/opendkim.pid
+Canonicalization relaxed/simple
+
+On-BadSignature accept
+On-Default accept
+On-KeyNotFound accept
+On-NoSignature accept
+
+LogWhy yes
+OversignHeaders From
+
+KeyTable /etc/opendkim/KeyTable
+SigningTable refile:/etc/opendkim/SigningTable
+ExternalIgnoreList /etc/opendkim/TrustedHosts
+InternalHosts /etc/opendkim/TrustedHosts
+
+UserID opendkim:opendkim
+AutoRestart yes
+AutoRestartRate 10/1h
+Background yes
+DNSTimeout 5
+SignatureAlgorithm rsa-sha256
+SyslogSuccess yes
+CONF
+
+# ── systemd Drop-in: /run/opendkim sicherstellen ─────────────────────────────
+install -d -m 0755 /etc/systemd/system/opendkim.service.d
+cat >/etc/systemd/system/opendkim.service.d/override.conf <<'EOF'
+[Service]
+RuntimeDirectory=opendkim
+RuntimeDirectoryMode=0755
+EOF
+
+install -d -o opendkim -g opendkim -m 0755 /run/opendkim
+
+# ──────────────────────────────────────────────────────────────
+# Root-Helper: DKIM installieren / entfernen + sudoers-Regel
+# ──────────────────────────────────────────────────────────────
+install -d -m 0750 /usr/local/sbin
+
+# --- mailwolt-install-dkim ------------------------------------
+cat > /usr/local/sbin/mailwolt-install-dkim <<'EOSH'
+#!/usr/bin/env bash
+set -euo pipefail
+
+DOMAIN="$1"
+SELECTOR="$2"
+SRC_PRIV="$3"
+SRC_TXT="${4:-}"
+
+OKDIR="/etc/opendkim"
+KEYDIR="${OKDIR}/keys/${DOMAIN}"
+KEYPRI="${KEYDIR}/${SELECTOR}.private"
+
+install -d -m 0750 -o opendkim -g opendkim "${KEYDIR}"
+install -m 0600 -o opendkim -g opendkim "${SRC_PRIV}" "${KEYPRI}"
+
+KT="${OKDIR}/KeyTable"
+ST="${OKDIR}/SigningTable"
+touch "$KT" "$ST"
+chown opendkim:opendkim "$KT" "$ST"
+chmod 0640 "$KT" "$ST"
+
+LINE_KT="${SELECTOR}._domainkey.${DOMAIN} ${DOMAIN}:${SELECTOR}:${KEYPRI}"
+LINE_ST="*@${DOMAIN} ${SELECTOR}._domainkey.${DOMAIN}"
+
+grep -Fqx "$LINE_KT" "$KT" || echo "$LINE_KT" >> "$KT"
+grep -Fqx "$LINE_ST" "$ST" || echo "$LINE_ST" >> "$ST"
+
+if [[ -n "${SRC_TXT}" && -s "${SRC_TXT}" ]]; then
+ install -d -m 0755 /etc/mailwolt/dns
+ cp -f "${SRC_TXT}" "/etc/mailwolt/dns/dkim-${DOMAIN}.txt"
+fi
+
+systemctl is-active --quiet opendkim && systemctl reload opendkim || true
+echo "OK"
+EOSH
+chmod 0750 /usr/local/sbin/mailwolt-install-dkim
+chown root:root /usr/local/sbin/mailwolt-install-dkim
+
+# --- 2) mailwolt-remove-dkim ----------------------------------
+cat >/usr/local/sbin/mailwolt-remove-dkim <<'EOSH'
+#!/usr/bin/env bash
+set -euo pipefail
+
+DOMAIN="$1" # z.B. kunden.tld oder sysmail.example.com
+SELECTOR="$2" # z.B. mwl1
+
+OKDIR="/etc/opendkim"
+KEYDIR="${OKDIR}/keys/${DOMAIN}"
+KEYPRI="${KEYDIR}/${SELECTOR}.private"
+KT="${OKDIR}/KeyTable"
+ST="${OKDIR}/SigningTable"
+
+# Key-Datei löschen (falls vorhanden)
+[[ -f "${KEYPRI}" ]] && rm -f "${KEYPRI}"
+
+# Zeilen aus KeyTable und SigningTable entfernen
+if [[ -f "$KT" ]]; then
+ tmp="$(mktemp)"; grep -v -F "${SELECTOR}._domainkey.${DOMAIN} ${DOMAIN}:${SELECTOR}:" "$KT" >"$tmp" && mv "$tmp" "$KT"
+ chown opendkim:opendkim "$KT"; chmod 0640 "$KT"
+fi
+if [[ -f "$ST" ]]; then
+ tmp="$(mktemp)"; grep -v -F "*@${DOMAIN} ${SELECTOR}._domainkey.${DOMAIN}" "$ST" >"$tmp" && mv "$tmp" "$ST"
+ chown opendkim:opendkim "$ST"; chmod 0640 "$ST"
+fi
+
+# Verzeichnis ggf. aufräumen
+rmdir "${KEYDIR}" 2>/dev/null || true
+
+# Dienst neu laden, falls aktiv
+if systemctl is-active --quiet opendkim; then
+ systemctl reload opendkim || true
+fi
+
+echo "OK"
+EOSH
+chown root:root /usr/local/sbin/mailwolt-remove-dkim
+chmod 0750 /usr/local/sbin/mailwolt-remove-dkim
+
+# ── Dienst + Postfix-Milter aktivieren ─────────────────────────
+systemctl daemon-reload
+systemctl enable opendkim || true
+
+touch /run/mailwolt.need-apply-milters || true
+
+chgrp _rspamd /etc/rspamd/local.d/*.inc /etc/rspamd/local.d/*.conf || true
+chmod 0640 /etc/rspamd/local.d/*.inc /etc/rspamd/local.d/*.conf || true
+
+#/usr/sbin/postconf -e "smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891"
+#/usr/sbin/postconf -e "non_smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891"
+
+log "[✓] Rspamd + OpenDKIM eingerichtet (läuft; signiert, sobald Keys vorhanden sind)."
diff --git a/mailwolt-installer/scripts/61-opendmarc.sh b/mailwolt-installer/scripts/61-opendmarc.sh
new file mode 100644
index 0000000..5f48521
--- /dev/null
+++ b/mailwolt-installer/scripts/61-opendmarc.sh
@@ -0,0 +1,63 @@
+#!/usr/bin/env bash
+set -euo pipefail
+source ./lib.sh
+
+log "OpenDMARC installieren/konfigurieren …"
+
+# Flags laden
+set +u
+[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
+set -u
+OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}"
+
+# Paket sicherstellen
+if ! dpkg -s opendmarc >/dev/null 2>&1; then
+ apt-get update -qq
+ apt-get install -y opendmarc
+fi
+
+# Config-Verzeichnisse
+install -d -m 0755 /etc/opendmarc
+install -d -m 0755 /run/opendmarc
+
+# IgnoreHosts
+cat >/etc/opendmarc/ignore.hosts <<'EOF'
+127.0.0.1
+::1
+localhost
+EOF
+chmod 0644 /etc/opendmarc/ignore.hosts
+
+# Hauptkonfiguration
+cat >/etc/opendmarc.conf <<'EOF'
+AuthservID mailwolt
+TrustedAuthservIDs mailwolt
+IgnoreHosts /etc/opendmarc/ignore.hosts
+Syslog true
+SoftwareHeader true
+Socket local:/run/opendmarc/opendmarc.sock
+RejectFailures false
+EOF
+chmod 0644 /etc/opendmarc.conf
+
+# systemd Drop-in für RuntimeDirectory (robust nach Reboot)
+install -d -m 0755 /etc/systemd/system/opendmarc.service.d
+cat >/etc/systemd/system/opendmarc.service.d/override.conf <<'EOF'
+[Service]
+RuntimeDirectory=opendmarc
+RuntimeDirectoryMode=0755
+EOF
+
+systemctl daemon-reload
+
+# Dienst nach Flag
+if [[ "$OPENDMARC_ENABLE" = "1" ]]; then
+ systemctl enable --now opendmarc
+else
+ systemctl disable --now opendmarc || true
+fi
+
+# Postfix-Milter-Kette konsistent setzen (Rspamd + OpenDKIM + optional OpenDMARC)
+touch /run/mailwolt.need-apply-milters || true
+
+log "[✓] OpenDMARC (ENABLE=${OPENDMARC_ENABLE}) bereit."
\ No newline at end of file
diff --git a/mailwolt-installer/scripts/62-clamav.sh b/mailwolt-installer/scripts/62-clamav.sh
new file mode 100644
index 0000000..fd3cb07
--- /dev/null
+++ b/mailwolt-installer/scripts/62-clamav.sh
@@ -0,0 +1,59 @@
+#!/usr/bin/env bash
+set -euo pipefail
+source ./lib.sh
+
+log "ClamAV (clamav-daemon) installieren/konfigurieren …"
+
+# Flags laden
+set +u
+[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
+set -u
+CLAMAV_ENABLE="${CLAMAV_ENABLE:-0}"
+
+# Pakete
+if ! dpkg -s clamav-daemon >/dev/null 2>&1; then
+ apt-get update -qq
+ apt-get install -y clamav clamav-daemon
+fi
+
+# Signaturen aktualisieren (erst Freshclam starten)
+systemctl stop clamav-freshclam 2>/dev/null || true
+freshclam || true
+systemctl start clamav-freshclam || true
+
+# clamd LocalSocket setzen
+sed -i 's|^#\?LocalSocket .*|LocalSocket /run/clamav/clamd.ctl|' /etc/clamav/clamd.conf || true
+install -d -m 0755 /run/clamav
+chown clamav:clamav /run/clamav
+
+# Dienst nach Flag
+if [[ "$CLAMAV_ENABLE" = "1" ]]; then
+ systemctl enable --now clamav-daemon
+else
+ systemctl disable --now clamav-daemon || true
+fi
+
+# Rspamd-Integration (nur wenn aktiv)
+AV_CONF="/etc/rspamd/local.d/antivirus.conf"
+if [[ "$CLAMAV_ENABLE" = "1" ]]; then
+ cat >"$AV_CONF" <<'EOF'
+clamav {
+ symbol = "CLAM_VIRUS";
+ type = "clamav";
+ servers = "/run/clamav/clamd.ctl";
+ scan_mime_parts = true;
+ scan_text_mime = true;
+ max_size = 50mb;
+ log_clean = false;
+ action = "reject";
+}
+EOF
+ chown root:_rspamd "$AV_CONF" || true
+ chmod 0640 "$AV_CONF" || true
+ systemctl reload rspamd || systemctl restart rspamd
+else
+ rm -f "$AV_CONF" || true
+ systemctl reload rspamd || true
+fi
+
+log "[✓] ClamAV (ENABLE=${CLAMAV_ENABLE}) konfiguriert."
\ No newline at end of file
diff --git a/mailwolt-installer/scripts/63-fail2ban.sh b/mailwolt-installer/scripts/63-fail2ban.sh
new file mode 100644
index 0000000..454b24f
--- /dev/null
+++ b/mailwolt-installer/scripts/63-fail2ban.sh
@@ -0,0 +1,230 @@
+#!/usr/bin/env bash
+set -euo pipefail
+source ./lib.sh
+
+log "Fail2Ban installieren/konfigurieren …"
+
+# Flags laden
+set +u
+[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
+set -u
+FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
+
+# Paket
+if ! dpkg -s fail2ban >/dev/null 2>&1; then
+ apt-get update -qq
+ apt-get install -y fail2ban sqlite3
+fi
+
+install -d -m 0755 /etc/fail2ban/jail.d
+
+# ---------------------------------------------------------------
+# Basis-Jails (praxisnah)
+# ---------------------------------------------------------------
+cat >/etc/fail2ban/jail.d/mailwolt.conf <<'EOF'
+[sshd]
+enabled = true
+port = ssh
+logpath = /var/log/auth.log
+
+[postfix]
+enabled = true
+logpath = /var/log/mail.log
+port = smtp,ssmtp,submission,465
+
+[dovecot]
+enabled = true
+logpath = /var/log/mail.log
+port = pop3,pop3s,imap,imaps,submission,465,587,993
+
+[rspamd-controller]
+enabled = true
+port = 11334
+filter = rspamd
+logpath = /var/log/rspamd/rspamd.log
+maxretry = 5
+EOF
+
+# einfacher Filter für Rspamd-Controller
+if [ ! -f /etc/fail2ban/filter.d/rspamd.conf ]; then
+ cat >/etc/fail2ban/filter.d/rspamd.conf <<'EOF'
+[Definition]
+failregex = .*Authentication failed for user.* from
+ignoreregex =
+EOF
+fi
+
+# ---------------------------------------------------------------
+# Fail2Ban-Backend auf SQLite umstellen
+# ---------------------------------------------------------------
+log "SQLite-Backend aktivieren …"
+
+cat >/etc/fail2ban/fail2ban.local <<'EOF'
+[Definition]
+loglevel = INFO
+logtarget = /var/log/fail2ban.log
+dbfile = /var/lib/fail2ban/fail2ban.sqlite3
+dbpurgeage = 86400
+EOF
+
+# Datenbankverzeichnis sicherstellen
+install -d -o fail2ban -g fail2ban -m 0750 /var/lib/fail2ban
+
+# Falls DB nicht existiert, Dummy anlegen (wird vom Dienst erweitert)
+if [ ! -f /var/lib/fail2ban/fail2ban.sqlite3 ]; then
+ sqlite3 /var/lib/fail2ban/fail2ban.sqlite3 "VACUUM;"
+fi
+chown fail2ban:fail2ban /var/lib/fail2ban/fail2ban.sqlite3
+chmod 0640 /var/lib/fail2ban/fail2ban.sqlite3
+
+# ---------------------------------------------------------------
+# sudoers für Web-UI
+# ---------------------------------------------------------------
+# Fail2Ban Blacklist-Jail
+cat >/etc/fail2ban/jail.d/mailwolt-blacklist.local <<'EOF'
+[mailwolt-blacklist]
+enabled = true
+filter = none
+port = anyport
+bantime = -1
+findtime = 1
+maxretry = 1
+EOF
+
+cat >/etc/fail2ban/filter.d/none.conf <<'EOF'
+[Definition]
+failregex =
+ignoreregex =
+EOF
+
+chmod 0640 /etc/fail2ban/filter.d/none.conf
+
+
+SUDOERS_F2B="/etc/sudoers.d/mailwolt-fail2ban"
+cat > "${SUDOERS_F2B}" <<'EOF'
+Defaults:www-data !requiretty
+www-data ALL=(root) NOPASSWD: \
+ /usr/bin/fail2ban-client, \
+ /usr/bin/fail2ban-client ping, \
+ /usr/bin/fail2ban-client status, \
+ /usr/bin/fail2ban-client status *, \
+ /usr/bin/fail2ban-client get *, \
+ /usr/bin/fail2ban-client set * banip *, \
+ /usr/bin/fail2ban-client set * unbanip *, \
+ /usr/bin/fail2ban-client reload, \
+ /usr/bin/journalctl, \
+ /bin/journalctl, \
+ /usr/bin/zgrep, \
+ /bin/zgrep, \
+ /usr/bin/grep, \
+ /bin/grep, \
+ /usr/bin/tail, \
+ /bin/tail, \
+ /usr/bin/sqlite3, \
+ /usr/bin/tee /etc/fail2ban/jail.d/*
+EOF
+chown root:root "${SUDOERS_F2B}"
+chmod 440 "${SUDOERS_F2B}"
+
+if ! visudo -c -f "${SUDOERS_F2B}" >/dev/null 2>&1; then
+ echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_F2B} – entferne Datei."
+ rm -f "${SUDOERS_F2B}"
+fi
+
+# ---------------------------------------------------------------
+# Dienst aktivieren/deaktivieren
+# ---------------------------------------------------------------
+if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then
+ systemctl enable --now fail2ban
+else
+ systemctl disable --now fail2ban || true
+fi
+
+log "[✓] Fail2Ban (ENABLE=${FAIL2BAN_ENABLE}) bereit."
+
+
+##!/usr/bin/env bash
+#set -euo pipefail
+#source ./lib.sh
+#
+#log "Fail2Ban installieren/konfigurieren …"
+#
+## Flags laden
+#set +u
+#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
+#set -u
+#FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
+#
+## Paket
+#if ! dpkg -s fail2ban >/dev/null 2>&1; then
+# apt-get update -qq
+# apt-get install -y fail2ban
+#fi
+#
+#install -d -m 0755 /etc/fail2ban/jail.d
+#
+## Basis-Jails (praxisnah)
+#cat >/etc/fail2ban/jail.d/mailwolt.conf <<'EOF'
+#[DEFAULT]
+#bantime = 1h
+#findtime = 10m
+#maxretry = 5
+#backend = auto
+#
+#[sshd]
+#enabled = true
+#port = ssh
+#logpath = /var/log/auth.log
+#
+#[postfix]
+#enabled = true
+#logpath = /var/log/mail.log
+#port = smtp,ssmtp,submission,465
+#
+#[dovecot]
+#enabled = true
+#logpath = /var/log/mail.log
+#port = pop3,pop3s,imap,imaps,submission,465,587,993
+#
+#[rspamd-controller]
+#enabled = true
+#port = 11334
+#filter = rspamd
+#logpath = /var/log/rspamd/rspamd.log
+#maxretry = 5
+#EOF
+#
+## einfacher Filter für Rspamd-Controller
+#if [ ! -f /etc/fail2ban/filter.d/rspamd.conf ]; then
+# cat >/etc/fail2ban/filter.d/rspamd.conf <<'EOF'
+#[Definition]
+#failregex = .*Authentication failed for user.* from
+#ignoreregex =
+#EOF
+#fi
+#
+#SUDOERS_F2B="/etc/sudoers.d/mailwolt-fail2ban"
+#cat > "${SUDOERS_F2B}" <<'EOF'
+#www-data ALL=(root) NOPASSWD: /usr/bin/fail2ban-client status, /usr/bin/fail2ban-client status *
+#EOF
+#chown root:root "${SUDOERS_F2B}"
+#chmod 440 "${SUDOERS_F2B}"
+#
+#if ! visudo -c -f "${SUDOERS_F2B}" >/dev/null 2>&1; then
+# echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_F2B} – entferne Datei."
+# rm -f "${SUDOERS_F2B}"
+#fi
+#
+#sudo tee /etc/sudoers.d/mailwolt-fail2ban >/dev/null <<'EOF'
+#www-data ALL=(root) NOPASSWD: /usr/bin/fail2ban-client status, /usr/bin/fail2ban-client status *
+#EOF
+#sudo visudo -cf /etc/sudoers.d/mailwolt-fail2ban
+#
+## Dienst nach Flag
+#if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then
+# systemctl enable --now fail2ban
+#else
+# systemctl disable --now fail2ban || true
+#fi
+#
+#log "[✓] Fail2Ban (ENABLE=${FAIL2BAN_ENABLE}) bereit."
\ No newline at end of file
diff --git a/mailwolt-installer/scripts/64-apply-milters.sh b/mailwolt-installer/scripts/64-apply-milters.sh
new file mode 100644
index 0000000..0927324
--- /dev/null
+++ b/mailwolt-installer/scripts/64-apply-milters.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+set -euo pipefail
+source ./lib.sh
+
+# nur ausführen, wenn vorherige Schritte das Flag gesetzt haben
+if [[ -f /run/mailwolt.need-apply-milters ]]; then
+ if command -v /usr/local/sbin/mailwolt-apply-milters >/dev/null 2>&1; then
+ log "Setze Postfix-Milter-Kette (Rspamd/OpenDKIM[/OpenDMARC]) …"
+ /usr/local/sbin/mailwolt-apply-milters || true
+ else
+ # Fallback (ident wie im Tool)
+ /usr/sbin/postconf -e "milter_default_action = accept"
+ /usr/sbin/postconf -e "milter_protocol = 6"
+ CHAIN="inet:127.0.0.1:11333, inet:127.0.0.1:8891"
+ systemctl is-active --quiet opendmarc && CHAIN="$CHAIN, inet:127.0.0.1:8893" || true
+ /usr/sbin/postconf -e "smtpd_milters = $CHAIN"
+ /usr/sbin/postconf -e "non_smtpd_milters = $CHAIN"
+ systemctl reload postfix || true
+ fi
+ rm -f /run/mailwolt.need-apply-milters || true
+ log "[✓] Milter-Kette angewandt."
+else
+ log "Milter-Kette: kein Bedarf (Flag nicht gesetzt) – überspringe."
+fi
\ No newline at end of file
diff --git a/mailwolt-installer/scripts/70-nginx.sh b/mailwolt-installer/scripts/70-nginx.sh
new file mode 100644
index 0000000..56f4590
--- /dev/null
+++ b/mailwolt-installer/scripts/70-nginx.sh
@@ -0,0 +1,465 @@
+#!/usr/bin/env bash
+set -euo pipefail
+source ./lib.sh
+
+log "Nginx konfigurieren …"
+
+# ── Flags/Umgebung (vom Bootstrap gesetzt; hier Fallbacks) ────────────────
+DEV_MODE="${DEV_MODE:-0}" # 1 = DEV (Vite-Proxy aktiv), 0 = PROD
+PROXY_MODE="${PROXY_MODE:-0}" # 1 = NPM/Proxy davor, Backend spricht nur HTTP:80
+NPM_IP="${NPM_IP:-}" # z.B. 10.10.20.20
+
+# Erwartet vom Bootstrap/Installer exportiert:
+: "${UI_HOST:?UI_HOST fehlt}"
+: "${WEBMAIL_HOST:?WEBMAIL_HOST fehlt}"
+: "${APP_DIR:?APP_DIR fehlt}"
+
+ACME_ROOT="/var/www/letsencrypt"
+install -d -m 0755 "$ACME_ROOT"
+
+# Default-Sites entfernen (verhindert doppelten default_server)
+rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default || true
+
+# HTTP/2-Unterstützung erkennen
+NGINX_HTTP2_SUFFIX=""
+if nginx -V 2>&1 | grep -q http_v2; then
+ NGINX_HTTP2_SUFFIX=" http2"
+fi
+
+# PHP-FPM Socket/TCP finden → fastcgi_pass bauen
+detect_php_fpm_sock(){
+ for v in 8.3 8.2 8.1 8.0 7.4; do
+ s="/run/php/php${v}-fpm.sock"
+ [[ -S "$s" ]] && { echo "unix:${s}"; return; }
+ done
+ [[ -S "/run/php/php-fpm.sock" ]] && { echo "unix:/run/php/php-fpm.sock"; return; }
+ echo "127.0.0.1:9000"
+}
+PHP_FPM_TARGET="$(detect_php_fpm_sock)"
+if [[ "$PHP_FPM_TARGET" == unix:* ]]; then
+ FASTCGI_PASS="fastcgi_pass ${PHP_FPM_TARGET};"
+else
+ FASTCGI_PASS="fastcgi_pass ${PHP_FPM_TARGET};"
+fi
+
+# ── Builder 1: HTTP-only (Proxy-Mode: TLS endet im NPM) ───────────────────
+## $1=host, $2=outfile
+#build_site_http_only(){
+# local host="$1" outfile="$2"
+#
+# local def=""
+# [[ "${DEV_MODE}" = "1" ]] && def=" default_server"
+# [[ -z "${host}" || "${host}" = "_" ]] && host="_"
+#
+# cat > "$outfile" <> "$outfile" <<'CONF'
+# # DEV: Vite-Proxy (HMR)
+# location ^~ /@vite/ { proxy_pass http://127.0.0.1:5173/@vite/; proxy_set_header Host $host; }
+# location ^~ /node_modules/ { proxy_pass http://127.0.0.1:5173/node_modules/; proxy_set_header Host $host; }
+# location ^~ /resources/ { proxy_pass http://127.0.0.1:5173/resources/; proxy_set_header Host $host; }
+#CONF
+# fi
+#
+# echo "}" >> "$outfile"
+#}
+
+#build_site_http_only(){
+# local host="$1" outfile="$2"
+#
+# # DEV: IP-Zugriff ohne Hostname → default_server + server_name _
+# local def=""
+# if [[ "${DEV_MODE}" = "1" ]]; then
+# def=" default_server"
+# host="_"
+# fi
+# [[ -z "${host}" || "${host}" = "_" ]] && host="_"
+#
+# cat > "$outfile" <> "$outfile" <<'CONF'
+# # DEV: Vite-Proxy (HMR)
+# location ^~ /@vite/ { proxy_pass http://127.0.0.1:5173/@vite/; proxy_set_header Host $host; }
+# location ^~ /node_modules/ { proxy_pass http://127.0.0.1:5173/node_modules/; proxy_set_header Host $host; }
+# location ^~ /resources/ { proxy_pass http://127.0.0.1:5173/resources/; proxy_set_header Host $host; }
+#CONF
+# fi
+#
+# echo "}" >> "$outfile"
+#}
+
+# $1=host, $2=outfile, $3=default_flag (default|nodefault)
+build_site_http_only(){
+ local host="$1" outfile="$2" def_flag="${3:-default}"
+
+ local def=""
+ if [[ "${DEV_MODE}" = "1" && "${def_flag}" = "default" ]]; then
+ def=" default_server"
+ fi
+ [[ -z "${host}" || "${host}" = "_" ]] && host="_"
+
+ cat > "$outfile" <> "$outfile" <<'CONF'
+ # DEV: Vite-Proxy (HMR)
+ location ^~ /@vite/ { proxy_pass http://127.0.0.1:5173/@vite/; proxy_set_header Host $host; }
+ location ^~ /node_modules/ { proxy_pass http://127.0.0.1:5173/node_modules/; proxy_set_header Host $host; }
+ location ^~ /resources/ { proxy_pass http://127.0.0.1:5173/resources/; proxy_set_header Host $host; }
+CONF
+ fi
+
+ echo "}" >> "$outfile"
+}
+
+# ── Builder 2: 80→443 Redirect + 443/TLS (Live-Server) ────────────────────
+# $1=host, $2=cert_dir (/etc/ssl/ui | /etc/ssl/webmail), $3=outfile
+build_site_tls(){
+ local host="$1" cert_dir="$2" outfile="$3"
+ local cert="${cert_dir}/fullchain.pem"
+ local key="${cert_dir}/privkey.pem"
+
+ cat > "$outfile" <> "$outfile" <<'CONF'
+ # DEV: Vite-Proxy
+ location ^~ /@vite/ { proxy_pass http://127.0.0.1:5173/@vite/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; }
+ location ^~ /node_modules/ { proxy_pass http://127.0.0.1:5173/node_modules/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; }
+ location ^~ /resources/ { proxy_pass http://127.0.0.1:5173/resources/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; }
+CONF
+ fi
+
+ echo "}" >> "$outfile"
+}
+
+build_site_acme_only(){
+ local host="$1" outfile="$2"
+
+ cat > "$outfile" < /etc/nginx/conf.d/realip.conf </dev/null 2>&1 || true
+ systemctl reload nginx || true
+else
+ die "nginx -t fehlgeschlagen – siehe /var/log/nginx/*.log"
+fi
diff --git a/mailwolt-installer/scripts/75-le-issue.sh b/mailwolt-installer/scripts/75-le-issue.sh
new file mode 100644
index 0000000..02ae118
--- /dev/null
+++ b/mailwolt-installer/scripts/75-le-issue.sh
@@ -0,0 +1,121 @@
+#!/usr/bin/env bash
+set -euo pipefail
+source ./lib.sh
+
+ACME_WEBROOT="/var/www/letsencrypt"
+install -d -m 0755 "${ACME_WEBROOT}/.well-known/acme-challenge"
+
+# Staging optional (verbraucht kein Live-Limit)
+CERTBOT_EXTRA=()
+LE_STAGING="${LE_STAGING:-0}"
+[[ "$LE_STAGING" = "1" ]] && CERTBOT_EXTRA+=(--test-cert)
+
+# Einheitliche LE-Mail (Fallback)
+LE_MAIL="${LE_EMAIL:-admin@${BASE_DOMAIN}}"
+
+resolve_ok() {
+ local host="$1"
+ local pats=()
+ [[ -n "${SERVER_PUBLIC_IPV4:-}" ]] && pats+=("${SERVER_PUBLIC_IPV4//./\\.}")
+ [[ -n "${SERVER_PUBLIC_IPV6:-}" ]] && pats+=("${SERVER_PUBLIC_IPV6//:/\\:}")
+ [[ ${#pats[@]} -eq 0 ]] && return 0
+ getent ahosts "$host" | awk '{print $1}' | sort -u \
+ | grep -Eq "^($(IFS='|'; echo "${pats[*]}"))$"
+}
+
+probe_http() {
+ local host="$1"
+ echo test > "${ACME_WEBROOT}/.well-known/acme-challenge/_probe"
+ curl -fsS --max-time 5 -4 "http://${host}/.well-known/acme-challenge/_probe" >/dev/null \
+ || curl -fsS --max-time 5 -6 "http://${host}/.well-known/acme-challenge/_probe" >/dev/null
+}
+
+issue() {
+ local host="${1:-}"
+ [[ -z "$host" ]] && return 0
+
+ echo "[i] Versuche LE für ${host} …"
+
+ if ! resolve_ok "$host"; then
+ echo "[!] DNS zeigt (noch) nicht hierher – überspringe: ${host}"
+ return 0
+ fi
+
+ if ! probe_http "$host"; then
+ echo "[!] ACME-HTTP-Check für ${host} fehlgeschlagen (Port 80/IPv6/Firewall/Nginx prüfen)."
+ # wir versuchen trotzdem – Certbot meldet sich, falls es scheitert
+ fi
+
+ EXTRA_ARGS=()
+ # Für MX den Key wiederverwenden → stabiler TLSA (3 1 1)
+ [[ "$host" == "${MAIL_HOSTNAME}" ]] && EXTRA_ARGS+=(--reuse-key)
+
+ # WICHTIG: Deploy-Wrapper anhängen, damit Symlinks/Nginx gesetzt werden
+ certbot certonly \
+ --agree-tos -m "${LE_MAIL}" --non-interactive \
+ --webroot -w "${ACME_WEBROOT}" -d "${host}" \
+ --deploy-hook /usr/local/sbin/mailwolt-deploy.sh \
+ "${EXTRA_ARGS[@]}" "${CERTBOT_EXTRA[@]}" || true
+}
+
+if [[ "${BASE_DOMAIN}" != "example.com" ]]; then
+ issue "${UI_HOST:-}"
+ issue "${WEBMAIL_HOST:-}"
+ issue "${MAIL_HOSTNAME:-}"
+
+ # Nginx nur neu laden, wenn aktiv
+ if systemctl is-active --quiet nginx; then
+ systemctl reload nginx || true
+ fi
+else
+ echo "[i] BASE_DOMAIN=example.com – LE wird übersprungen."
+fi
+
+# ──────────────────────────────────────────────────────────────────────────────
+# FIX: Validierung & Reparatur des Mail-Zertifikats
+# ──────────────────────────────────────────────────────────────────────────────
+MAIL_SSL_DIR="/etc/ssl/mail"
+install -d -m 0755 "$MAIL_SSL_DIR"
+
+MAIL_CERT="${MAIL_SSL_DIR}/fullchain.pem"
+MAIL_KEY="${MAIL_SSL_DIR}/privkey.pem"
+
+HOST="${MAIL_HOSTNAME:-}"
+LE_DIR=""
+[[ -n "$HOST" ]] && LE_DIR="/etc/letsencrypt/live/${HOST}"
+
+need_fix=0
+
+# Ist der vorhandene Key gültig? (leer/nicht vorhanden/ungültig -> fix)
+if [[ ! -s "$MAIL_KEY" ]] || ! openssl pkey -in "$MAIL_KEY" -noout >/dev/null 2>&1; then
+ need_fix=1
+fi
+
+# Wenn Fix nötig: aus Let's Encrypt Live kopieren
+if [[ $need_fix -eq 1 ]]; then
+ echo "[!] Ungültiger oder fehlender Mail-Private-Key – versuche Reparatur …"
+ if [[ -n "$LE_DIR" && -r "${LE_DIR}/privkey.pem" && -r "${LE_DIR}/fullchain.pem" ]]; then
+ cp -f "${LE_DIR}/privkey.pem" "$MAIL_KEY"
+ cp -f "${LE_DIR}/fullchain.pem" "$MAIL_CERT"
+ chown root:root "$MAIL_CERT" "$MAIL_KEY"
+ chmod 600 "$MAIL_KEY"
+ chmod 644 "$MAIL_CERT"
+ echo "[+] Zertifikate neu kopiert aus ${LE_DIR}."
+ # Reload NICHT sofort – flaggen für 90-services
+ touch /run/mailwolt.need-dovecot-reload
+ else
+ echo "[!] Konnte ${LE_DIR}/privkey.pem oder fullchain.pem nicht lesen – bitte prüfen."
+ fi
+else
+ echo "[✓] Mail-Zertifikat & -Key sind gültig."
+fi
+
+# Optionaler Live-Check (nur wenn Host gesetzt)
+if [[ -n "$HOST" ]]; then
+ if openssl s_client -connect "${HOST}:993" -servername "${HOST}" /dev/null \
+ | grep -q "Verify return code: 0"; then
+ echo "[✓] TLS-Handshake erfolgreich auf imaps://${HOST}:993."
+ else
+ echo "[!] TLS-Handshake auf imaps://${HOST}:993 fehlgeschlagen (Dovecot Reload folgt in 90-services, falls Flag gesetzt)."
+ fi
+fi
\ No newline at end of file
diff --git a/mailwolt-installer/scripts/80-app.sh b/mailwolt-installer/scripts/80-app.sh
new file mode 100644
index 0000000..1ac72c0
--- /dev/null
+++ b/mailwolt-installer/scripts/80-app.sh
@@ -0,0 +1,343 @@
+#!/usr/bin/env bash
+set -euo pipefail
+source ./lib.sh
+
+# --- Helper: sicherer Frontend-Build als APP_USER ---------------------------
+safe_frontend_build() {
+ echo "[i] Frontend build …"
+
+ # Verzeichnisse & Rechte vorbereiten (Gruppen-sticky & ACL)
+ install -d -m 2775 -o "$APP_USER" -g "$APP_GROUP" \
+ "${APP_DIR}/public/build" "${APP_DIR}/node_modules" "${APP_DIR}/.npm-cache"
+
+ chown -R "$APP_USER":"$APP_GROUP" "${APP_DIR}"
+ find "${APP_DIR}" -type d -exec chmod 2775 {} \;
+ find "${APP_DIR}" -type f -exec chmod 664 {} \;
+ setfacl -R -m g:"$APP_GROUP":rwX -m d:g:"$APP_GROUP":rwX "${APP_DIR}" || true
+
+ # Vite-/Build-Reste bereinigen (falls mal root dort gebaut hat)
+ rm -rf "${APP_DIR}/node_modules/.vite" "${APP_DIR}/public/build/"* 2>/dev/null || true
+
+ # npm auf projektlokales Cache konfigurieren
+ sudo -u "$APP_USER" -H bash -lc "cat > ~/.npmrc <<'RC'
+fund=false
+audit=false
+prefer-offline=true
+cache=${APP_DIR}/.npm-cache
+RC"
+
+ # Node ggf. installieren
+ if ! command -v node >/dev/null 2>&1; then
+ curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
+ apt-get install -y nodejs
+ fi
+
+ # Dependencies + Build (als App-User)
+ if sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && (npm ci --no-audit --no-fund || npm install --no-audit --no-fund) && npm run build"; then
+ return 0
+ fi
+
+ echo "[!] Build fehlgeschlagen – Rechtefix + Clean + Retry …"
+ rm -rf "${APP_DIR}/node_modules/.vite" "${APP_DIR}/public/build/"* 2>/dev/null || true
+ chown -R "$APP_USER":"$APP_GROUP" "${APP_DIR}"
+ find "${APP_DIR}" -type d -exec chmod 2775 {} \;
+ find "${APP_DIR}" -type f -exec chmod 664 {} \;
+
+ sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm run build"
+}
+
+relink_and_reload() {
+ if [[ -d /etc/letsencrypt/renewal-hooks/deploy ]]; then
+ run-parts /etc/letsencrypt/renewal-hooks/deploy || true
+ fi
+ if systemctl is-active --quiet nginx; then
+ systemctl reload nginx || true
+ fi
+}
+
+log "App bereitstellen …"
+mkdir -p "$(dirname "$APP_DIR")"
+chown -R "$APP_USER":"$APP_GROUP" "$(dirname "$APP_DIR")"
+
+# Repo holen oder Laravel anlegen – passe GIT_REPO/GIT_BRANCH bei Bedarf an
+GIT_REPO="${GIT_REPO:-https://git.nexlab.at/boban/mailwolt.git}"
+GIT_BRANCH="${GIT_BRANCH:-main}"
+
+if [[ "${GIT_REPO}" == "https://example.com/your-repo-placeholder.git" ]]; then
+ [[ -d "$APP_DIR" && -n "$(ls -A "$APP_DIR" 2>/dev/null || true)" ]] || \
+ sudo -u "$APP_USER" -H bash -lc "cd /var/www && composer create-project laravel/laravel ${APP_USER} --no-interaction"
+else
+ if [[ ! -d "${APP_DIR}/.git" ]]; then
+ sudo -u "$APP_USER" -H bash -lc "git clone --depth=1 -b ${GIT_BRANCH} ${GIT_REPO} ${APP_DIR}"
+ else
+ sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && git fetch --depth=1 origin ${GIT_BRANCH} && git reset --hard origin/${GIT_BRANCH}"
+ fi
+ [[ -f "${APP_DIR}/composer.json" ]] && sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && composer install --no-interaction --prefer-dist"
+fi
+
+ENV_FILE="${APP_DIR}/.env"
+sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && cp -n .env.example .env || true"
+grep -q '^APP_KEY=' "$ENV_FILE" || echo "APP_KEY=" >> "$ENV_FILE"
+sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan key:generate --force || true"
+
+# --- App-URL/Hosts ----------------------------------------------------------
+#SERVER_PUBLIC_IPV4="${SERVER_PUBLIC_IPV4:-}"
+#if [[ -z "$SERVER_PUBLIC_IPV4" ]] && command -v curl >/dev/null 2>&1; then
+# SERVER_PUBLIC_IPV4="$(curl -fsS --max-time 2 https://ifconfig.me 2>/dev/null || true)"
+# [[ "$SERVER_PUBLIC_IPV4" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || SERVER_PUBLIC_IPV4=""
+#fi
+#[[ -n "$SERVER_PUBLIC_IPV4" ]] || SERVER_PUBLIC_IPV4="$(detect_ip)"
+#
+#UI_CERT="/etc/ssl/ui/fullchain.pem"
+#UI_KEY="/etc/ssl/ui/privkey.pem"
+#
+#if [[ -n "${UI_HOST:-}" ]]; then
+# APP_HOST_VAL="$UI_HOST"
+# APP_URL_VAL="https://${UI_HOST}"
+#else
+# APP_HOST_VAL="$SERVER_PUBLIC_IPV4"
+# SCHEME="http"
+# [[ -s "$UI_CERT" && -s "$UI_KEY" ]] && SCHEME="https"
+# APP_URL_VAL="${SCHEME}://${SERVER_PUBLIC_IPV4}"
+#fi
+
+SERVER_PUBLIC_IPV4="${SERVER_PUBLIC_IPV4:-}"
+if [[ -z "$SERVER_PUBLIC_IPV4" ]] && command -v curl >/dev/null 2>&1; then
+ SERVER_PUBLIC_IPV4="$(curl -fsS --max-time 2 https://ifconfig.me 2>/dev/null || true)"
+ [[ "$SERVER_PUBLIC_IPV4" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || SERVER_PUBLIC_IPV4=""
+fi
+[[ -n "$SERVER_PUBLIC_IPV4" ]] || SERVER_PUBLIC_IPV4="$(detect_ip)"
+
+UI_CERT="/etc/ssl/ui/fullchain.pem"
+UI_KEY="/etc/ssl/ui/privkey.pem"
+
+# DEV-Modus: immer IP als Host, http (kein example.com / keine Fake-Domain)
+if [[ "${DEV_MODE:-0}" = "1" || "${APP_ENV:-production}" = "local" ]]; then
+ APP_HOST_VAL="$SERVER_PUBLIC_IPV4"
+ APP_URL_VAL="http://${APP_HOST_VAL}"
+else
+ # PROD/normal: wenn UI_HOST gesetzt → benutzen, sonst IP
+ if [[ -n "${UI_HOST:-}" ]]; then
+ APP_HOST_VAL="$UI_HOST"
+ APP_URL_VAL="https://${UI_HOST}"
+ else
+ APP_HOST_VAL="$SERVER_PUBLIC_IPV4"
+ SCHEME="http"
+ [[ -s "$UI_CERT" && -s "$UI_KEY" ]] && SCHEME="https"
+ APP_URL_VAL="${SCHEME}://${APP_HOST_VAL}"
+ fi
+fi
+
+SECURE=$([[ "${APP_ENV}" = "production" ]] && echo true || echo false)
+
+# --- .env schreiben ---------------------------------------------------------
+upsert_env APP_URL "${APP_URL_VAL}"
+
+if [[ "${PROXY_MODE:-0}" -eq 1 ]]; then
+ TP_LIST="127.0.0.1,::1"
+ [[ -n "${NPM_IP:-}" ]] && TP_LIST="${TP_LIST},${NPM_IP}"
+ upsert_env TRUSTED_PROXIES "$TP_LIST"
+ upsert_env TRUSTED_HEADERS "x-forwarded-all"
+else
+ upsert_env TRUSTED_PROXIES ""
+ upsert_env TRUSTED_HEADERS "x-forwarded-all"
+fi
+
+upsert_env APP_HOST "${APP_HOST_VAL}"
+upsert_env APP_NAME "${APP_NAME}"
+upsert_env APP_ENV "${APP_ENV:-production}"
+upsert_env APP_DEBUG "${APP_DEBUG:-false}"
+upsert_env APP_TIMEZONE "${APP_TZ:-UTC}"
+
+upsert_env APP_LOCALE "${APP_LOCALE:-de}"
+upsert_env APP_FALLBACK_LOCALE "en"
+
+upsert_env SERVER_PUBLIC_IPV4 "${SERVER_PUBLIC_IPV4}"
+upsert_env SERVER_PUBLIC_IPV6 "${SERVER_PUBLIC_IPV6:-}"
+
+upsert_env SYSMAIL_SUB "${SYSMAIL_SUB}"
+upsert_env SYSMAIL_DOMAIN "${SYSMAIL_DOMAIN}"
+upsert_env DKIM_ENABLE "${DKIM_ENABLE}"
+upsert_env DKIM_SELECTOR "${DKIM_SELECTOR}"
+upsert_env DKIM_GENERATE "${DKIM_GENERATE}"
+
+upsert_env BASE_DOMAIN "${BASE_DOMAIN}"
+upsert_env UI_SUB "${UI_SUB}"
+upsert_env WEBMAIL_SUB "${WEBMAIL_SUB}"
+upsert_env MTA_SUB "${MTA_SUB}"
+upsert_env LE_EMAIL "${LE_EMAIL:-admin@${BASE_DOMAIN}}"
+
+upsert_env DB_CONNECTION "mysql"
+upsert_env DB_HOST "127.0.0.1"
+upsert_env DB_PORT "3306"
+upsert_env DB_DATABASE "${DB_NAME}"
+upsert_env DB_USERNAME "${DB_USER}"
+upsert_env DB_PASSWORD "${DB_PASS}"
+
+upsert_env CACHE_SETTINGS_STORE "redis"
+upsert_env CACHE_STORE "redis"
+upsert_env CACHE_DRIVER "redis"
+upsert_env CACHE_PREFIX "${APP_USER_PREFIX}_cache:"
+upsert_env SESSION_DRIVER "redis"
+upsert_env SESSION_SECURE_COOKIE "${SECURE}" # DEV=false, PROD=true
+upsert_env SESSION_SAMESITE "lax"
+upsert_env REDIS_CLIENT "phpredis"
+upsert_env REDIS_HOST "127.0.0.1"
+upsert_env REDIS_PORT "6379"
+upsert_env REDIS_PASSWORD "${REDIS_PASS}"
+upsert_env REDIS_DB "0"
+upsert_env REDIS_CACHE_DB "1"
+upsert_env REDIS_CACHE_CONNECTION "cache"
+upsert_env REDIS_CACHE_LOCK_CONNECTION "default"
+
+upsert_env BROADCAST_DRIVER "reverb"
+upsert_env QUEUE_CONNECTION "redis"
+upsert_env LOG_CHANNEL "daily"
+
+upsert_env REVERB_APP_ID "${APP_USER_PREFIX}"
+grep -q '^REVERB_APP_KEY=' "$ENV_FILE" || upsert_env REVERB_APP_KEY "${APP_USER_PREFIX}_$(openssl rand -hex 16)"
+grep -q '^REVERB_APP_SECRET=' "$ENV_FILE" || upsert_env REVERB_APP_SECRET "${APP_USER_PREFIX}_$(openssl rand -hex 32)"
+upsert_env REVERB_HOST "\${APP_HOST}"
+upsert_env REVERB_PORT "443"
+upsert_env REVERB_SCHEME "https"
+upsert_env REVERB_PATH "/ws"
+upsert_env REVERB_SCALING_ENABLED "true"
+upsert_env REVERB_SCALING_CHANNEL "reverb"
+
+upsert_env VITE_REVERB_APP_KEY "\${REVERB_APP_KEY}"
+upsert_env VITE_REVERB_HOST "\${REVERB_HOST}"
+upsert_env VITE_REVERB_PORT "\${REVERB_PORT}"
+upsert_env VITE_REVERB_SCHEME "\${REVERB_SCHEME}"
+upsert_env VITE_REVERB_PATH "\${REVERB_PATH}"
+
+upsert_env REVERB_SERVER_APP_KEY "\${REVERB_APP_KEY}"
+upsert_env REVERB_SERVER_HOST "127.0.0.1"
+upsert_env REVERB_SERVER_PORT "8080"
+upsert_env REVERB_SERVER_PATH ""
+upsert_env REVERB_SERVER_SCHEME "http"
+
+# --- DEV Block (optional) ---------------------------------------------------
+DEV_MODE="${DEV_MODE:-0}"
+if [[ "$DEV_MODE" = "1" ]]; then
+ sed -i '/^# --- MailWolt DEV/,/^# --- \/MailWolt DEV/d' "${ENV_FILE}"
+ cat >> "${ENV_FILE}" <make(Illuminate\\Contracts\\Console\\Kernel::class)->bootstrap();
+ \$d = App\\Models\\Domain::firstOrCreate([\"domain\"=>\"${SYSMAIL_DOMAIN}\"],[\"is_active\"=>1,\"is_system\"=>1]);
+ \$r = app(App\\Services\\DkimService::class)->generateForDomain(\$d, 2048, \"${DKIM_SELECTOR}\");
+ echo \$r[\"priv_path\"], \"\\n\";
+ echo \$r[\"dns_txt\"], \"\\n\";
+ '
+ ")"
+
+ PRIV_PATH="$(printf '%s\n' "$OUT" | sed -n '1p')"
+ DNS_TXT="$(printf '%s\n' "$OUT" | sed -n '2,$p')"
+
+ if [[ -z "$PRIV_PATH" || ! -s "$PRIV_PATH" ]]; then
+ echo "[!] DKIM priv_path fehlt oder Datei leer: $PRIV_PATH" >&2
+ exit 1
+ fi
+
+ TMP_TXT="$(mktemp /tmp/dkim_txt_XXXXXX.txt)"
+ printf '%s' "$DNS_TXT" >"$TMP_TXT"
+
+ # 2) Root-Helper ausführen (hängt Key ein, pflegt Key/SigningTable, kopiert TXT)
+ if [[ -x /usr/local/sbin/mailwolt-install-dkim ]]; then
+ /usr/local/sbin/mailwolt-install-dkim "${SYSMAIL_DOMAIN}" "${DKIM_SELECTOR}" "${PRIV_PATH}" "${TMP_TXT}"
+ else
+ echo "[!] Helper /usr/local/sbin/mailwolt-install-dkim fehlt oder ist nicht ausführbar." >&2
+ fi
+
+ rm -f "$TMP_TXT" || true
+
+ # 3) OpenDKIM neu laden
+ touch /run/mailwolt.need-opendkim-reload || true
+else
+ log "DKIM übersprungen (DKIM_ENABLE=${DKIM_ENABLE}, SYSMAIL_DOMAIN='${SYSMAIL_DOMAIN}')."
+fi
+
+
+# --- TLSA aus App heraus (idempotent; läuft, wenn Zert lesbar ist) ----------
+sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan dns:tlsa:refresh || true"
+
+# --- Build Frontend (nur wenn nötig) ----------------------------------------
+if [[ -f "${APP_DIR}/package.json" && ! -f "${APP_DIR}/public/build/manifest.json" ]]; then
+ safe_frontend_build
+fi
+
+# --- Abschluss: Caches + Rechte + Reloads -----------------------------------
+sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan optimize:clear && php artisan config:cache && php artisan optimize:clear"
+
+# Konsistente Rechte/ACL für das gesamte App-Verzeichnis
+chown -R "$APP_USER":"$APP_GROUP" "$APP_DIR"
+find "$APP_DIR" -type d -exec chmod 2775 {} \;
+find "$APP_DIR" -type f -exec chmod 664 {} \;
+setfacl -R -m g:"$APP_GROUP":rwX -m d:g:"$APP_GROUP":rwX "$APP_DIR" || true
+
+# Laravel-Write-Dirs sicherstellen (mit setgid & ACL)
+install -d -m 2775 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR/storage" "$APP_DIR/bootstrap/cache"
+chgrp -R www-data "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" || true
+find "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" -type d -exec chmod 2775 {} \; || true
+find "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" -type f -exec chmod 0664 {} \; || true
+setfacl -R -m u:www-data:rwx,u:${APP_USER}:rwx "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" || true
+setfacl -dR -m u:www-data:rwx,u:${APP_USER}:rwx "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" || true
\ No newline at end of file
diff --git a/mailwolt-installer/scripts/88-update-wrapper.sh b/mailwolt-installer/scripts/88-update-wrapper.sh
new file mode 100644
index 0000000..6852861
--- /dev/null
+++ b/mailwolt-installer/scripts/88-update-wrapper.sh
@@ -0,0 +1,264 @@
+#!/usr/bin/env bash
+set -euo pipefail
+source ./lib.sh
+
+log "Update-Wrapper & Sudoers …"
+
+WRAPPER="/usr/local/sbin/mailwolt-update"
+LOGFILE="/var/log/mailwolt-update.log"
+STATEDIR="/var/lib/mailwolt/update"
+SUDOERS="/etc/sudoers.d/mailwolt-update"
+VERSION_FILE="/var/lib/mailwolt/version"
+SUDOERS_SERVICES="/etc/sudoers.d/mailwolt-services"
+SUDOERS_ARTISAN="/etc/sudoers.d/mailwolt-artisan"
+
+# Kandidaten: wo liegt update.sh?
+CANDIDATES=(
+ /opt/mailwolt-installer/scripts/update.sh
+ /mailwolt-installer/scripts/update.sh
+ /usr/local/lib/mailwolt/update.sh
+)
+
+# State/Log vorbereiten
+install -d -m 0755 "$(dirname "$LOGFILE")"
+install -d -m 0755 "$STATEDIR"
+: > "$LOGFILE" || true
+chmod 0644 "$LOGFILE"
+
+# Wrapper erzeugen
+cat > "$WRAPPER" <<'EOF'
+#!/usr/bin/env bash
+set -euo pipefail
+
+LOG="/var/log/mailwolt-update.log"
+STATE_DIR="/var/lib/mailwolt/update"
+APP_DIR="/var/www/mailwolt"
+WEB_USER="www-data"
+
+CANDIDATES=(
+ /opt/mailwolt-installer/scripts/update.sh
+ /mailwolt-installer/scripts/update.sh
+ /usr/local/lib/mailwolt/update.sh
+)
+
+install -d -m 0755 "$(dirname "$LOG")" "$STATE_DIR" /var/lib/mailwolt
+: > "$LOG" || true
+chmod 0644 "$LOG"
+
+echo "running" > "$STATE_DIR/state"
+
+{
+ echo "===== $(date -Is) :: Update gestartet ====="
+
+ # --- Update-Script finden --------------------------------------------------
+ SCRIPT=""
+ for p in "${CANDIDATES[@]}"; do
+ if [[ -x "$p" ]]; then SCRIPT="$p"; break; fi
+ if [[ -f "$p" && -r "$p" ]]; then SCRIPT="$p"; break; fi
+ done
+
+ if [[ -z "$SCRIPT" ]]; then
+ echo "[!] update.sh nicht gefunden (versucht: ${CANDIDATES[*]})"
+ rc=127
+ else
+ echo "[i] benutze: $SCRIPT"
+ if [[ "$(id -u)" -ne 0 ]]; then
+ echo "[!] Bitte als root ausführen"
+ rc=1
+ else
+ if [[ -x "$SCRIPT" ]]; then
+ ALLOW_DIRTY=1 "$SCRIPT"
+ else
+ ALLOW_DIRTY=1 bash "$SCRIPT"
+ fi
+ rc=$?
+ fi
+ fi
+
+ echo "===== $(date -Is) :: Update-Script beendet (rc=$rc) ====="
+
+ # --- Nach dem Update: Assets neu bauen & Laravel optimieren ---------------
+ if [ -d "$APP_DIR" ]; then
+ cd "$APP_DIR" || exit 1
+
+ echo "[i] Führe Composer aus (falls vorhanden) ..."
+ if [ -f composer.json ]; then
+ sudo -u "$WEB_USER" composer install --no-dev --prefer-dist --no-interaction -q || true
+ fi
+
+ echo "[i] Baue Frontend-Assets neu ..."
+ if command -v npm >/dev/null 2>&1 && [ -f package.json ]; then
+ sudo -u "$WEB_USER" npm ci --silent || true
+ sudo -u "$WEB_USER" npm run build --silent || true
+ fi
+
+ echo "[i] Führe Migrationen & Cache-Optimierungen durch ..."
+ sudo -u "$WEB_USER" php artisan migrate --force || true
+ sudo -u "$WEB_USER" php artisan config:cache || true
+ sudo -u "$WEB_USER" php artisan optimize:clear || true
+ sudo -u "$WEB_USER" php artisan route:cache || true
+ sudo -u "$WEB_USER" php artisan view:cache || true
+
+ echo "[i] Hebe Wartungsmodus auf ..."
+ sudo -u "$WEB_USER" php artisan up >/dev/null 2>&1 || true
+ fi
+
+ # --- Version aktualisieren -------------------------------------------------
+ echo "[i] Aktualisiere Version ..."
+ if command -v git >/dev/null 2>&1; then
+ SRC="/var/www/mailwolt"
+ if [ ! -d "$SRC/.git" ]; then
+ SRC="/opt/mailwolt-installer"
+ fi
+
+ git config --global --add safe.directory "$SRC" || true
+
+ if [ -f "$SRC/.git/shallow" ]; then
+ git -C "$SRC" fetch --unshallow --quiet || true
+ fi
+ git -C "$SRC" fetch --tags --quiet origin || true
+
+ raw="$(git -C "$SRC" describe --tags --always --dirty 2>/dev/null || echo "unknown")"
+ norm="$(printf '%s' "$raw" | sed -E 's/^[vV]//; s/-.*$//')"
+
+ printf '%s\n' "$raw" > /var/lib/mailwolt/version_raw
+ printf '%s\n' "$norm" > /var/lib/mailwolt/version
+ chmod 0644 /var/lib/mailwolt/version_raw /var/lib/mailwolt/version
+
+ echo "[i] Version aktualisiert: raw=$raw norm=$norm (Quelle: $SRC)"
+ else
+ echo "unknown" > /var/lib/mailwolt/version_raw
+ echo "0.0.0" > /var/lib/mailwolt/version
+ chmod 0644 /var/lib/mailwolt/version_raw /var/lib/mailwolt/version
+ fi
+
+ # --- Services neu starten --------------------------------------------------
+ echo "[i] Starte MailWolt-Dienste neu ..."
+ sudo -u "$WEB_USER" php artisan mailwolt:restart-services || true
+
+ # --- Abschluss -------------------------------------------------------------
+ printf '%s\n' "$rc" > "$STATE_DIR/rc"
+ echo "done" > "$STATE_DIR/state"
+ echo "===== $(date -Is) :: Update beendet ====="
+ exit "$rc"
+
+} | tee -a "$LOG"
+EOF
+
+chmod 0755 "$WRAPPER"
+chown root:root "$WRAPPER"
+
+# Sudoers: www-data (Laravel) & mailwolt dürfen den Wrapper laufen lassen
+cat > "$SUDOERS" <<'EOF'
+Defaults!/usr/local/sbin/mailwolt-update !requiretty
+www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-update
+mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-update
+EOF
+
+chown root:root "$SUDOERS"
+chmod 440 "$SUDOERS"
+
+if ! visudo -c -f "$SUDOERS" >/dev/null 2>&1; then
+ echo "[!] Ungültiger sudoers-Eintrag in $SUDOERS – entferne Datei."
+ rm -f "$SUDOERS"
+fi
+
+cat > "$SUDOERS_SERVICES" <<'EOF'
+Defaults!/usr/bin/systemctl !requiretty
+
+Cmnd_Alias MW_SERVICES = \
+ /usr/bin/systemctl reload nginx.service, \
+ /usr/bin/systemctl try-reload-or-restart nginx.service, \
+ /usr/bin/systemctl try-reload-or-restart postfix.service, \
+ /usr/bin/systemctl try-reload-or-restart dovecot.service, \
+ /usr/bin/systemctl try-reload-or-restart rspamd.service, \
+ /usr/bin/systemctl try-reload-or-restart opendkim.service, \
+ /usr/bin/systemctl try-reload-or-restart opendmarc.service, \
+ /usr/bin/systemctl try-reload-or-restart clamav-daemon.service, \
+ /usr/bin/systemctl try-reload-or-restart redis-server.service
+
+www-data ALL=(root) NOPASSWD: MW_SERVICES
+EOF
+
+chmod 440 "$SUDOERS_SERVICES"
+chown root:root "$SUDOERS_SERVICES"
+
+# Prüfen, ob Syntax gültig ist
+if ! visudo -c -f "$SUDOERS_SERVICES" >/dev/null 2>&1; then
+ echo "[!] Ungültiger sudoers-Eintrag in $SUDOERS_SERVICES – entferne Datei."
+ rm -f "$SUDOERS_SERVICES"
+else
+ echo "[✓] Sudoers für Dienststeuerung angelegt: $SUDOERS_SERVICES"
+fi
+
+# Version-File initial anlegen, falls nicht existiert
+if [[ ! -f "$VERSION_FILE" ]]; then
+ echo "unknown" > "$VERSION_FILE"
+ chmod 0644 "$VERSION_FILE"
+fi
+
+cat > "$SUDOERS_ARTISAN" <<'EOF'
+# mailwolt darf artisan im App-Verzeichnis als www-data ausführen (ohne Passwort)
+mailwolt ALL=(www-data) NOPASSWD: /usr/bin/php /var/www/mailwolt/artisan *
+EOF
+
+chown root:root "$SUDOERS_ARTISAN"
+chmod 440 "$SUDOERS_ARTISAN"
+
+if ! visudo -c -f "$SUDOERS_ARTISAN" >/dev/null 2>&1; then
+ echo "[!] Ungültiger sudoers-Eintrag in $SUDOERS_ARTISAN – entferne Datei."
+ rm -f "$SUDOERS_ARTISAN"
+else
+ echo "[✓] Sudoers für Artisan-Kommandos angelegt: $SUDOERS_ARTISAN"
+fi
+
+log "[✓] Update-Wrapper bereit: $WRAPPER"
+log "[✓] Version wird unter $VERSION_FILE gespeichert"
+
+# ─── Installer-Wrapper ────────────────────────────────────────────────────────
+INSTALL_WRAPPER="/usr/local/sbin/mailwolt-install"
+INSTALL_SUDOERS="/etc/sudoers.d/mailwolt-install"
+INSTALL_STATE_DIR="/var/lib/mailwolt/install"
+INSTALL_LOG="/var/log/mailwolt-install.log"
+
+# State/Log vorbereiten
+install -d -m 0755 "$INSTALL_STATE_DIR"
+: > "$INSTALL_LOG" || true
+chmod 0644 "$INSTALL_LOG"
+
+# Installer-Wrapper aus scripts/install-wrapper.sh kopieren
+INSTALL_SRC=""
+for candidate in \
+ "$(dirname "$0")/install-wrapper.sh" \
+ /opt/mailwolt-installer/scripts/install-wrapper.sh \
+ /var/www/mailwolt/mailwolt-installer/scripts/install-wrapper.sh; do
+ [[ -f "$candidate" ]] && INSTALL_SRC="$candidate" && break
+done
+
+if [[ -n "$INSTALL_SRC" ]]; then
+ cp "$INSTALL_SRC" "$INSTALL_WRAPPER"
+ chmod 0755 "$INSTALL_WRAPPER"
+ chown root:root "$INSTALL_WRAPPER"
+ echo "[✓] Installer-Wrapper angelegt: $INSTALL_WRAPPER"
+else
+ echo "[!] install-wrapper.sh nicht gefunden – Installer-Wrapper wird übersprungen."
+fi
+
+# Sudoers: www-data & mailwolt dürfen den Installer-Wrapper laufen lassen
+cat > "$INSTALL_SUDOERS" <<'SUDOEOF'
+Defaults!/usr/local/sbin/mailwolt-install !requiretty
+www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install
+www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install *
+mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install
+mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install *
+SUDOEOF
+
+chown root:root "$INSTALL_SUDOERS"
+chmod 440 "$INSTALL_SUDOERS"
+
+if ! visudo -c -f "$INSTALL_SUDOERS" >/dev/null 2>&1; then
+ echo "[!] Ungültiger sudoers-Eintrag in $INSTALL_SUDOERS – entferne Datei."
+ rm -f "$INSTALL_SUDOERS"
+else
+ echo "[✓] Sudoers für Installer angelegt: $INSTALL_SUDOERS"
+fi
\ No newline at end of file
diff --git a/mailwolt-installer/scripts/90-services.sh b/mailwolt-installer/scripts/90-services.sh
new file mode 100644
index 0000000..c28b982
--- /dev/null
+++ b/mailwolt-installer/scripts/90-services.sh
@@ -0,0 +1,134 @@
+#!/usr/bin/env bash
+set -euo pipefail
+source ./lib.sh
+
+log "systemd Units (Reverb / Scheduler / Queue / Mail) …"
+
+cat > /etc/systemd/system/${APP_USER}-ws.service < /etc/systemd/system/${APP_USER}-schedule.service < /etc/systemd/system/${APP_USER}-queue.service </dev/null 2>&1 || true
+fi
+systemctl enable --now ${APP_USER}-schedule
+systemctl enable --now ${APP_USER}-queue
+
+# Mail-Dienste starten
+systemctl enable --now rspamd opendkim postfix dovecot || true
+
+# PHP-FPM: Unit erkennen, enable + (re)load
+enable_and_touch_php_fpm() {
+ for u in php8.3-fpm php8.2-fpm php8.1-fpm php8.0-fpm php7.4-fpm php-fpm; do
+ if systemctl list-unit-files | grep -q "^${u}\.service"; then
+ systemctl enable --now "$u" || true
+ systemctl reload "$u" || systemctl restart "$u" || true
+ echo "[i] PHP-FPM unit: $u"
+ return 0
+ fi
+ done
+ echo "[!] Keine passende php-fpm Unit gefunden."
+}
+enable_and_touch_php_fpm
+
+# Falls in 80-app.sh DKIM installiert wurde: jetzt einmal reloaden
+if [[ -e /run/mailwolt.need-opendkim-reload ]]; then
+ systemctl reload opendkim || true
+ rm -f /run/mailwolt.need-opendkim-reload || true
+fi
+
+# Falls Zert-Fix markiert ist: Dovecot neu laden
+if [[ -e /run/mailwolt.need-dovecot-reload ]]; then
+ systemctl reload dovecot || true
+ rm -f /run/mailwolt.need-dovecot-reload || true
+fi
+
+# Falls DB-Migration schon durch: einmal reload
+db_ready(){ mysql -u"${DB_USER}" -p"${DB_PASS}" -h 127.0.0.1 -D "${DB_NAME}" -e "SHOW TABLES LIKE 'migrations'\G" >/dev/null 2>&1; }
+if db_ready; then
+ systemctl reload postfix || true
+fi
+
+# Mini-Portcheck (hilft beim Installer-Output)
+echo "Listening (25/465/587):"
+ss -ltnp | awk '$4 ~ /:(25|465|587)$/ {print " " $0}'
\ No newline at end of file
diff --git a/mailwolt-installer/scripts/92-sudoers-npm.sh b/mailwolt-installer/scripts/92-sudoers-npm.sh
new file mode 100644
index 0000000..6f07a93
--- /dev/null
+++ b/mailwolt-installer/scripts/92-sudoers-npm.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+set -euo pipefail
+source ./lib.sh
+
+log "Sudoers: npm-Build ohne Passwort für user 'mailwolt' …"
+
+# 1) npm-Binary ermitteln (normal: /usr/bin/npm)
+NPM_BIN="$(command -v npm || true)"
+
+if [[ -z "$NPM_BIN" ]]; then
+ warn "npm wurde nicht gefunden – sudoers wird vorbereitet, aber ohne Validierung. Stelle sicher, dass Node/npm installiert ist."
+ # Fallback – die meisten Distros legen hier an
+ NPM_BIN="/usr/bin/npm"
+fi
+
+SUDOERS_FILE="/etc/sudoers.d/mailwolt-npm"
+
+# 2) Sudoers-Datei schreiben
+cat > "$SUDOERS_FILE" </dev/null 2>&1; then
+ log "[✓] sudoers OK: ${SUDOERS_FILE} erlaubt 'mailwolt' → ${NPM_BIN} ohne Passwort."
+else
+ echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_FILE} – entferne Datei."
+ rm -f "$SUDOERS_FILE"
+fi
\ No newline at end of file
diff --git a/mailwolt-installer/scripts/93-backup-tools.sh b/mailwolt-installer/scripts/93-backup-tools.sh
new file mode 100644
index 0000000..ebfd7c4
--- /dev/null
+++ b/mailwolt-installer/scripts/93-backup-tools.sh
@@ -0,0 +1,284 @@
+#!/usr/bin/env bash
+set -euo pipefail
+source ./lib.sh
+
+log "Backup/Restore – Tools, Config & Timer (installer.env) …"
+
+# ─────────────────────────────────────────────────────────────
+# 1) installer.env laden (ENV > installer.env > Defaults)
+# ─────────────────────────────────────────────────────────────
+if [[ -f /etc/mailwolt/installer.env ]]; then
+ # automatisch exportieren, damit ${VAR} später überall wirkt
+ set -a
+ # shellcheck disable=SC1091
+ source /etc/mailwolt/installer.env
+ set +a
+else
+ log "[i] /etc/mailwolt/installer.env nicht gefunden – nutze Defaults."
+fi
+
+# ─────────────────────────────────────────────────────────────
+# 2) Pfade & Defaults (werden durch ENV/installer.env überschrieben)
+# ─────────────────────────────────────────────────────────────
+CONF_DIR="/etc/mailwolt"
+CONF_FILE="${CONF_DIR}/backup.conf"
+BIN_DIR="/usr/local/sbin"
+UNIT_DIR="/etc/systemd/system"
+
+APP_DIR="${APP_DIR:-/var/www/mailwolt}"
+
+# DB-Parameter aus installer.env (bzw. ENV) oder Fallbacks
+DB_HOST="${DB_HOST:-127.0.0.1}"
+DB_NAME="${DB_NAME:-mailwolt}"
+DB_USER="${DB_USER:-mailwolt}"
+DB_PASS="${DB_PASS:-}"
+
+# Backup-Settings aus installer.env (bzw. ENV)
+BACKUP_DIR="${BACKUP_DIR:-/var/backups/mailwolt}"
+BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-7}"
+BACKUP_USE_ZSTD="${BACKUP_USE_ZSTD:-1}"
+BACKUP_ENABLED="${BACKUP_ENABLED:-0}" # 0|1
+BACKUP_INTERVAL="${BACKUP_INTERVAL:-daily}" # daily|weekly|monthly
+
+install -d -m 0755 "$CONF_DIR" "$BACKUP_DIR"
+
+
+SUDOERS_BACKUP_FILE="/etc/sudoers.d/mailwolt-backup"
+# 2) Sudoers-Datei schreiben
+cat > "${SUDOERS_BACKUP_FILE} " </dev/null 2>&1; then
+ echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_BACKUP_FILE} – entferne Datei."
+ rm -f "${SUDOERS_BACKUP_FILE}"
+fi
+
+# ─────────────────────────────────────────────────────────────
+# 3) /etc/mailwolt/backup.conf (von UI/APP überschreibbar)
+# ─────────────────────────────────────────────────────────────
+cat > "$CONF_FILE" < "${BIN_DIR}/mailwolt-backup" <<'EOSH'
+#!/usr/bin/env bash
+set -euo pipefail
+
+log(){ echo "[$(date -Is)] $*"; }
+
+# Konfiguration laden (ENV > Datei)
+CONF="/etc/mailwolt/backup.conf"
+[[ -f "$CONF" ]] && # shellcheck disable=SC1090
+source "$CONF"
+
+APP_DIR="${APP_DIR:-/var/www/mailwolt}"
+BACKUP_DIR="${BACKUP_DIR:-/var/backups/mailwolt}"
+RETENTION_DAYS="${RETENTION_DAYS:-7}"
+USE_ZSTD="${USE_ZSTD:-1}"
+
+MYSQL_DB="${MYSQL_DB:-mailwolt}"
+MYSQL_USER="${MYSQL_USER:-mailwolt}"
+MYSQL_PASS="${MYSQL_PASS:-}"
+MYSQL_HOST="${MYSQL_HOST:-127.0.0.1}"
+MYSQL_PORT="${MYSQL_PORT:-3306}"
+
+STATE_DIR="/var/lib/mailwolt"
+STATUS_FILE="${STATE_DIR}/backup.status"
+install -d -m 0755 "$STATE_DIR" "$BACKUP_DIR"
+
+START_TS="$(date +%s)"
+TS="$(date -u +%Y%m%dT%H%M%SZ)"
+TMP="$(mktemp -d /tmp/mwbackup.XXXXXX)"
+trap 'rm -rf "$TMP"' EXIT
+
+fail(){
+ local msg="${1:-backup failed}"
+ local now="$(date -Is)"
+ {
+ echo "time=${now}"
+ echo "size=0"
+ echo "dur=$(( $(date +%s) - START_TS ))s"
+ echo "ok=0"
+ echo "error=${msg}"
+ } > "$STATUS_FILE"
+ echo "[$now] ${msg}" >&2
+ exit 1
+}
+trap 'fail "unexpected error (exit $?)"' ERR
+
+OUT="${BACKUP_DIR}/mailwolt-${TS}.tar"
+log "⇒ starte Backup in $OUT …"
+
+# 1) DB
+log " • mysqldump …"
+MYSQL_PWD="$MYSQL_PASS" mysqldump \
+ -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" \
+ --single-transaction --routines --events --triggers \
+ "$MYSQL_DB" > "$TMP/mysql.sql"
+
+# 2) Maildir
+log " • Maildir …"
+tar -C / -cf "$TMP/mail.tar" var/mail/vhosts 2>/dev/null || true
+
+# 3) App (ohne heavy dirs)
+log " • App …"
+tar -C / -cf "$TMP/app.tar" \
+ --exclude='var/www/mailwolt/vendor' \
+ --exclude='var/www/mailwolt/node_modules' \
+ --exclude='var/www/mailwolt/public/build' \
+ var/www/mailwolt
+
+# 4) Configs
+log " • Configs …"
+mkdir -p "$TMP/files"
+cp -a /etc/mailwolt "$TMP/files/" 2>/dev/null || true
+cp -a /etc/postfix "$TMP/files/" 2>/dev/null || true
+cp -a /etc/dovecot "$TMP/files/" 2>/dev/null || true
+cp -a /etc/opendkim "$TMP/files/" 2>/dev/null || true
+cp -a /etc/opendmarc "$TMP/files/" 2>/dev/null || true
+cp -a /etc/rspamd "$TMP/files/" 2>/dev/null || true
+cp -a /etc/ssl/ui "$TMP/files/" 2>/dev/null || true
+tar -C "$TMP" -cf "$TMP/files.tar" files
+
+# 5) Paket
+log " • Archiviere …"
+tar -C "$TMP" -cf "$OUT" mysql.sql mail.tar app.tar files.tar
+
+# 6) Komprimieren (optional)
+if [[ "${USE_ZSTD:-1}" = "1" ]] && command -v zstd >/dev/null 2>&1; then
+ log " • komprimiere (zstd) …"
+ zstd -f --rm -19 "$OUT"
+ OUT="${OUT}.zst"
+fi
+
+# 7) Retention
+if [[ "$RETENTION_DAYS" =~ ^[0-9]+$ ]]; then
+ log " • Retention: lösche älter als ${RETENTION_DAYS} Tage …"
+ find "$BACKUP_DIR" -type f -mtime +"$RETENTION_DAYS" -name 'mailwolt-*' -delete || true
+fi
+
+# 8) Statusfile fürs UI
+SIZE_BYTES="$(stat -c '%s' "$OUT" 2>/dev/null || echo 0)"
+{
+ echo "time=$(date -Is)"
+ echo "size=${SIZE_BYTES}"
+ echo "dur=$(( $(date +%s) - START_TS ))s"
+ echo "ok=1"
+ echo "file=${OUT}"
+} > "$STATUS_FILE"
+chmod 0644 "$STATUS_FILE" 2>/dev/null || true
+
+log "[✓] Backup fertig: $OUT"
+EOSH
+chmod 0755 "${BIN_DIR}/mailwolt-backup"
+
+# ─────────────────────────────────────────────────────────────
+# 5) /usr/local/sbin/mailwolt-restore
+# ─────────────────────────────────────────────────────────────
+cat > "${BIN_DIR}/mailwolt-restore" <<'EOSH'
+#!/usr/bin/env bash
+set -euo pipefail
+log(){ echo "[$(date -Is)] $*"; }
+
+ARCHIVE="${1:-}"
+[[ -n "$ARCHIVE" ]] || { echo "Usage: mailwolt-restore "; exit 1; }
+[[ -f "$ARCHIVE" ]] || { echo "Backup nicht gefunden: $ARCHIVE"; exit 1; }
+
+TMP="$(mktemp -d /tmp/mwrestore.XXXXXX)"
+trap 'rm -rf "$TMP"' EXIT
+
+case "$ARCHIVE" in
+ *.zst) zstd -d -c "$ARCHIVE" > "$TMP/backup.tar" ;;
+ *) cp -a "$ARCHIVE" "$TMP/backup.tar" ;;
+esac
+
+log "⇒ entpacke …"
+tar -C "$TMP" -xf "$TMP/backup.tar"
+
+# Reihenfolge: DB → App → Mail → Config
+if [[ -f "$TMP/mysql.sql" ]]; then
+ log " • MySQL wiederherstellen …"
+ mysql < "$TMP/mysql.sql"
+fi
+
+if [[ -f "$TMP/app.tar" ]]; then
+ log " • App → /var/www/mailwolt …"
+ tar -C / -xf "$TMP/app.tar"
+fi
+
+if [[ -f "$TMP/mail.tar" ]]; then
+ log " • Maildir → /var/mail/vhosts …"
+ tar -C / -xf "$TMP/mail.tar"
+fi
+
+if [[ -f "$TMP/files.tar" ]]; then
+ log " • Configs → /etc/* …"
+ tar -C / -xf "$TMP/files.tar"
+fi
+
+log "[✓] Restore abgeschlossen."
+EOSH
+chmod 0755 "${BIN_DIR}/mailwolt-restore"
+
+log "[✓] Tools installiert: ${BIN_DIR}/mailwolt-backup, mailwolt-restore"
+
+# ─────────────────────────────────────────────────────────────
+# 6) systemd Service + Timer (Timer default via installer.env)
+# ─────────────────────────────────────────────────────────────
+cat > "${UNIT_DIR}/mailwolt-backup.service" <<'EOSVC'
+[Unit]
+Description=MailWolt Backup
+
+[Service]
+Type=oneshot
+ExecStart=/usr/local/sbin/mailwolt-backup
+Nice=10
+IOSchedulingClass=best-effort
+IOSchedulingPriority=7
+EOSVC
+
+cat > "${UNIT_DIR}/mailwolt-backup.timer" </dev/null 2>&1 || true
+fi
+
+log "[✓] Backup-Setup abgeschlossen."
\ No newline at end of file
diff --git a/mailwolt-installer/scripts/95-monit.sh b/mailwolt-installer/scripts/95-monit.sh
new file mode 100644
index 0000000..6dca86e
--- /dev/null
+++ b/mailwolt-installer/scripts/95-monit.sh
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+set -euo pipefail
+source ./lib.sh
+
+log "Monit konfigurieren …"
+cat > /etc/monit/monitrc <<'EOF'
+set daemon 60
+set logfile syslog facility log_daemon
+
+check process postfix with pidfile /var/spool/postfix/pid/master.pid
+ start program = "/bin/systemctl start postfix"
+ stop program = "/bin/systemctl stop postfix"
+ if failed port 25 protocol smtp then restart
+ if failed port 465 type tcp ssl then restart
+ if failed port 587 type tcp then restart
+
+check process dovecot with pidfile /run/dovecot/master.pid
+ start program = "/bin/systemctl start dovecot"
+ stop program = "/bin/systemctl stop dovecot"
+ if failed port 143 type tcp then restart
+ if failed port 993 type tcp ssl then restart
+
+check process mariadb with pidfile /run/mysqld/mysqld.pid
+ start program = "/bin/systemctl start mariadb"
+ stop program = "/bin/systemctl stop mariadb"
+ if failed port 3306 type tcp then restart
+
+check process redis-server with pidfile /run/redis/redis-server.pid
+ start program = "/bin/systemctl start redis-server"
+ stop program = "/bin/systemctl stop redis-server"
+ if failed port 6379 type tcp then restart
+
+check process nginx with pidfile /run/nginx.pid
+ start program = "/bin/systemctl start nginx"
+ stop program = "/bin/systemctl stop nginx"
+ if failed port 80 type tcp then restart
+ if failed port 443 type tcp ssl then restart
+EOF
+chmod 600 /etc/monit/monitrc
+monit -t && systemctl enable --now monit
+monit reload || true
+
+log "[✓] Monit konfiguriert und gestartet"
+
+# ── mailwolt-update ins System kopieren ─────────────────────────────
+install -m 0750 -o root -g root scripts/update.sh /usr/local/sbin/mailwolt-update
+log "[✓] mailwolt-update installiert → ausführbar via 'sudo mailwolt-update'"
\ No newline at end of file
diff --git a/mailwolt-installer/scripts/95-woltguard.bak.sh b/mailwolt-installer/scripts/95-woltguard.bak.sh
new file mode 100644
index 0000000..d727918
--- /dev/null
+++ b/mailwolt-installer/scripts/95-woltguard.bak.sh
@@ -0,0 +1,491 @@
+#!/usr/bin/env bash
+set -euo pipefail
+source ./lib.sh
+
+log "WoltGuard (Monit + Self-Heal) einrichten …"
+
+# ─────────────────────────────────────────────────────────────
+# Env nur nachladen, wenn Flags nicht bereits exportiert sind
+# ─────────────────────────────────────────────────────────────
+INSTALLER_ENV="/etc/mailwolt/installer.env"
+: "${CLAMAV_ENABLE:=}" ; : "${OPENDMARC_ENABLE:=}" ; : "${FAIL2BAN_ENABLE:=}"
+if [[ -z "${CLAMAV_ENABLE}${OPENDMARC_ENABLE}${FAIL2BAN_ENABLE}" && -r "$INSTALLER_ENV" ]]; then
+ # shellcheck disable=SC1090
+ . "$INSTALLER_ENV"
+fi
+CLAMAV_ENABLE="${CLAMAV_ENABLE:-0}"
+OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-0}"
+FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
+
+# ─────────────────────────────────────────────────────────────
+# Monit installieren & aktivieren
+# ─────────────────────────────────────────────────────────────
+command -v monit >/dev/null || { apt-get update -qq; apt-get install -y monit; }
+systemctl enable --now monit
+
+# ─────────────────────────────────────────────────────────────
+# Helper-Skripte (laufen später eigenständig → Env selbst laden)
+# ─────────────────────────────────────────────────────────────
+install -d -m 0755 /usr/local/sbin
+
+# Redis-Ping (nimmt REDIS_PASSWORD aus installer.env oder .env)
+cat >/usr/local/sbin/mailwolt-redis-ping.sh <<'EOSH'
+#!/usr/bin/env bash
+set -euo pipefail
+
+INSTALLER_ENV="/etc/mailwolt/installer.env"
+APP_ENV="/var/www/mailwolt/.env"
+
+# Defaults
+REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
+REDIS_PORT="${REDIS_PORT:-6379}"
+REDIS_PASSWORD="${REDIS_PASSWORD:-}"
+REDIS_PASS="${REDIS_PASS:-}" # Legacy
+
+# Installer-Env (falls vorhanden)
+[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV" || true
+
+# Falls .env existiert: Werte ergänzen, die noch leer sind
+if [[ -r "$APP_ENV" ]]; then
+ [[ -z "${REDIS_HOST}" ]] && REDIS_HOST="$(grep -m1 -E '^REDIS_HOST=' "$APP_ENV" | cut -d= -f2- || true)"
+ [[ -z "${REDIS_PORT}" ]] && REDIS_PORT="$(grep -m1 -E '^REDIS_PORT=' "$APP_ENV" | cut -d= -f2- || true)"
+ [[ -z "${REDIS_PASSWORD}" ]] && REDIS_PASSWORD="$(grep -m1 -E '^REDIS_PASSWORD=' "$APP_ENV" | cut -d= -f2- || true)"
+fi
+
+# Legacy-Fallback: wenn PASSWORD leer, aber PASS gesetzt → übernehmen
+[[ -z "${REDIS_PASSWORD}" && -n "${REDIS_PASS}" ]] && REDIS_PASSWORD="$REDIS_PASS"
+
+# Quotes strippen
+strip(){ printf '%s' "$1" | sed -E 's/^"(.*)"$/\1/; s/^'\''(.*)'\''$/\1/'; }
+REDIS_HOST="$(strip "${REDIS_HOST:-}")"
+REDIS_PORT="$(strip "${REDIS_PORT:-}")"
+REDIS_PASSWORD="$(strip "${REDIS_PASSWORD:-}")"
+
+# redis-cli muss vorhanden sein
+command -v redis-cli >/dev/null 2>&1 || exit 1
+
+BASE=(timeout 2 redis-cli --no-auth-warning --raw -h "$REDIS_HOST" -p "$REDIS_PORT")
+if [[ -n "$REDIS_PASSWORD" ]]; then
+ CMD=("${BASE[@]}" -a "$REDIS_PASSWORD" ping)
+else
+ CMD=("${BASE[@]}" ping)
+fi
+
+# Erfolgreich nur bei exakt "PONG"
+[[ "$("${CMD[@]}" 2>/dev/null || true)" == "PONG" ]]
+EOSH
+chmod 0755 /usr/local/sbin/mailwolt-redis-ping.sh
+
+# Rspamd-Heal (setzt Laufzeitverzeichnis, leert alte Socke, restarts rspamd)
+cat >/usr/local/sbin/mailwolt-rspamd-heal.sh <<'EOSH'
+#!/usr/bin/env bash
+set -euo pipefail
+
+INSTALLER_ENV="/etc/mailwolt/installer.env"
+APP_ENV="/var/www/mailwolt/.env"
+
+REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
+REDIS_PORT="${REDIS_PORT:-6379}"
+REDIS_PASSWORD="${REDIS_PASSWORD:-}"
+
+[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV"
+if [[ -z "${REDIS_PASSWORD}" && -r "$APP_ENV" ]]; then
+ REDIS_PASSWORD="$(grep -E '^REDIS_PASSWORD=' "$APP_ENV" | head -n1 | cut -d= -f2- || true)"
+fi
+
+# Rspamd Runtime fixen
+install -d -m 0755 -o _rspamd -g _rspamd /run/rspamd || true
+[[ -S /var/lib/rspamd/rspamd.sock ]] && rm -f /var/lib/rspamd/rspamd.sock || true
+
+# Neustart
+systemctl restart rspamd
+
+# Mini-Healthcheck
+sleep 2
+ss -tln | grep -q ':11334' || echo "[WARN] Rspamd Controller Port 11334 nicht sichtbar"
+
+exit 0
+EOSH
+chmod 0755 /usr/local/sbin/mailwolt-rspamd-heal.sh
+
+# ─────────────────────────────────────────────────────────────
+# WoltGuard Wrapper + Unit
+# ─────────────────────────────────────────────────────────────
+cat >/usr/local/bin/woltguard <<'EOSH'
+#!/usr/bin/env bash
+set -euo pipefail
+case "${1:-status}" in
+ start) systemctl enable --now monit ;;
+ stop) systemctl stop monit ;;
+ status) monit summary || systemctl status monit || true ;;
+ heal) monit reload || true; sleep 1; monit restart all || true ;;
+ monitor) monit monitor all || true ;;
+ unmonitor) monit unmonitor all || true ;;
+ *) echo "Usage: woltguard {start|stop|status|heal|monitor|unmonitor}"; exit 2;;
+esac
+EOSH
+chmod 0755 /usr/local/bin/woltguard
+
+cat >/etc/systemd/system/woltguard.service <<'EOF'
+[Unit]
+Description=WoltGuard – Self-Healing Monitor for MailWolt
+After=network.target
+[Service]
+Type=oneshot
+ExecStart=/usr/local/bin/woltguard start
+ExecStop=/usr/local/bin/woltguard stop
+RemainAfterExit=yes
+[Install]
+WantedBy=multi-user.target
+EOF
+systemctl daemon-reload
+systemctl enable --now woltguard
+
+# ─────────────────────────────────────────────────────────────
+# Monit Basis + includes
+# ─────────────────────────────────────────────────────────────
+sed -i 's/^set daemon .*/set daemon 30/' /etc/monit/monitrc || true
+grep -q 'include /etc/monit/conf.d/*' /etc/monit/monitrc || echo 'include /etc/monit/conf.d/*' >>/etc/monit/monitrc
+install -d -m 0755 /etc/monit/conf.d
+
+# ─────────────────────────────────────────────────────────────
+# Monit Checks
+# ─────────────────────────────────────────────────────────────
+# 10 – Redis zuerst (abhängig für rspamd)
+cat >/etc/monit/conf.d/10-redis.conf <<'EOF'
+check process redis with pidfile /run/redis/redis-server.pid
+ start program = "/bin/systemctl start redis-server"
+ stop program = "/bin/systemctl stop redis-server"
+ if failed host 127.0.0.1 port 6379 for 2 cycles then restart
+ if 5 restarts within 5 cycles then alert
+
+check program redis_ping path "/usr/local/sbin/mailwolt-redis-ping.sh"
+ if status != 0 for 2 cycles then exec "/bin/systemctl restart redis-server"
+EOF
+
+# 20 – Rspamd (hängt von Redis ab), robust über process-matching
+cat >/etc/monit/conf.d/20-rspamd.conf <<'EOF'
+check process rspamd matching "/usr/bin/rspamd"
+ start program = "/bin/systemctl start rspamd"
+ stop program = "/bin/systemctl stop rspamd"
+ depends on redis
+ if failed host 127.0.0.1 port 11333 for 2 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
+ if failed host 127.0.0.1 port 11334 for 2 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
+ if 5 restarts within 5 cycles then alert
+EOF
+
+# 30 – Maildienste
+cat >/etc/monit/conf.d/30-postfix.conf <<'EOF'
+check process postfix with pidfile /var/spool/postfix/pid/master.pid
+ start program = "/bin/systemctl start postfix"
+ stop program = "/bin/systemctl stop postfix"
+ if failed port 25 protocol smtp then restart
+ if failed port 465 type tcpssl then restart
+ if failed port 587 type tcp then restart
+ if 5 restarts within 5 cycles then alert
+EOF
+
+cat >/etc/monit/conf.d/30-dovecot.conf <<'EOF'
+check process dovecot with pidfile /run/dovecot/master.pid
+ start program = "/bin/systemctl start dovecot"
+ stop program = "/bin/systemctl stop dovecot"
+ if failed port 993 type tcpssl for 2 cycles then restart
+ if failed port 24 protocol lmtp for 2 cycles then restart
+ if 5 restarts within 5 cycles then alert
+EOF
+
+# 40 – Web/PHP
+cat >/etc/monit/conf.d/40-nginx.conf <<'EOF'
+check process nginx with pidfile /run/nginx.pid
+ start program = "/bin/systemctl start nginx"
+ stop program = "/bin/systemctl stop nginx"
+ if failed port 80 type tcp then restart
+ if failed port 443 type tcpssl then restart
+ if 5 restarts within 5 cycles then alert
+EOF
+
+# 50 – DKIM/DMARC
+cat >/etc/monit/conf.d/50-opendkim.conf <<'EOF'
+check process opendkim with pidfile /run/opendkim/opendkim.pid
+ start program = "/bin/systemctl start opendkim"
+ stop program = "/bin/systemctl stop opendkim"
+ if failed host 127.0.0.1 port 8891 type tcp for 2 cycles then restart
+ if 5 restarts within 5 cycles then alert
+EOF
+
+# optional: OpenDMARC
+if [[ "$OPENDMARC_ENABLE" = "1" ]]; then
+ cat >/etc/monit/conf.d/55-opendmarc.conf <<'EOF'
+check process opendmarc with pidfile /run/opendmarc/opendmarc.pid
+ start program = "/bin/systemctl start opendmarc"
+ stop program = "/bin/systemctl stop opendmarc"
+ if 5 restarts within 5 cycles then alert
+EOF
+else
+ rm -f /etc/monit/conf.d/55-opendmarc.conf || true
+fi
+
+# 60 – optional: ClamAV
+if [[ "$CLAMAV_ENABLE" = "1" ]]; then
+ cat >/etc/monit/conf.d/60-clamav.conf <<'EOF'
+check process clamd with pidfile /run/clamav/clamd.pid
+ start program = "/bin/systemctl start clamav-daemon"
+ stop program = "/bin/systemctl stop clamav-daemon"
+ if failed unixsocket /run/clamav/clamd.ctl for 3 cycles then restart
+ if 5 restarts within 5 cycles then timeout
+EOF
+else
+ rm -f /etc/monit/conf.d/60-clamav.conf || true
+fi
+
+# 70 – Fail2Ban (optional, standardmäßig aktiv)
+if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then
+ cat >/etc/monit/conf.d/70-fail2ban.conf <<'EOF'
+check process fail2ban with pidfile /run/fail2ban/fail2ban.pid
+ start program = "/bin/systemctl start fail2ban"
+ stop program = "/bin/systemctl stop fail2ban"
+ if 5 restarts within 5 cycles then alert
+EOF
+else
+ rm -f /etc/monit/conf.d/70-fail2ban.conf || true
+fi
+# ─────────────────────────────────────────────────────────────
+# Monit neu laden
+# ─────────────────────────────────────────────────────────────
+monit -t
+systemctl reload monit || systemctl restart monit
+systemctl status monit --no-pager || true
+log "[✓] WoltGuard aktiv."
+
+##!/usr/bin/env bash
+#set -euo pipefail
+#source ./lib.sh
+#
+#log "WoltGuard (Monit + Self-Heal) einrichten …"
+#
+#set +u
+#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
+#set -u
+#CLAMAV_ENABLE="${CLAMAV_ENABLE:-0}"
+#OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-0}"
+#FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
+#
+## Pakete sicherstellen
+#command -v monit >/dev/null || { apt-get update -qq; apt-get install -y monit; }
+#systemctl enable --now monit
+#
+## Helper-Skripte
+#install -d -m 0755 /usr/local/sbin
+#cat >/usr/local/sbin/mailwolt-redis-ping.sh <<'EOSH'
+##!/usr/bin/env bash
+#set -euo pipefail
+#PASS=""
+#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env || true
+#if command -v redis-cli >/dev/null 2>&1; then
+# [[ -n "${REDIS_PASS:-}" ]] \
+# && redis-cli -h 127.0.0.1 -p 6379 -a "$REDIS_PASS" ping | grep -q PONG \
+# || redis-cli -h 127.0.0.1 -p 6379 ping | grep -q PONG
+#else
+# exit 1
+#fi
+#EOSH
+#chmod 0755 /usr/local/sbin/mailwolt-redis-ping.sh
+#
+#cat >/usr/local/sbin/mailwolt-rspamd-heal.sh <<'EOSH'
+##!/usr/bin/env bash
+#set -euo pipefail
+#
+#REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
+#REDIS_PORT="${REDIS_PORT:-6379}"
+#REDIS_PASSWORD="${REDIS_PASSWORD:-}"
+#
+#INSTALLER_ENV="/etc/mailwolt/installer.env"
+#APP_ENV="/var/www/mailwolt/.env"
+#REDIS_CLI="$(command -v redis-cli || true)"
+#SYSTEMCTL="$(command -v systemctl || true)"
+#RSPAMD_SERVICE="rspamd"
+#
+#if [ -r "$INSTALLER_ENV" ]; then . "$INSTALLER_ENV"; fi
+#if [ -z "${REDIS_PASSWORD}" ] && [ -r "$APP_ENV" ]; then
+# REDIS_PASSWORD="$(grep -E '^REDIS_PASSWORD=' "$APP_ENV" | head -n1 | cut -d= -f2- || true)"
+#fi
+#
+#if [ -n "$REDIS_CLI" ]; then
+# echo "[INFO] Prüfe Redis Verbindung..."
+# if [ -n "${REDIS_PASSWORD}" ]; then
+# if ! "$REDIS_CLI" -h "$REDIS_HOST" -p "$REDIS_PORT" -a "$REDIS_PASSWORD" ping | grep -q '^PONG$'; then
+# echo "[WARN] Redis antwortet nicht oder Passwort falsch!"
+# else
+# echo "[OK] Redis antwortet (auth ok)."
+# fi
+# else
+# if ! "$REDIS_CLI" -h "$REDIS_HOST" -p "$REDIS_PORT" ping | grep -q '^PONG$'; then
+# echo "[WARN] Redis antwortet nicht (ohne Passwort)."
+# else
+# echo "[OK] Redis antwortet (kein Passwort)."
+# fi
+# fi
+#else
+# echo "[WARN] redis-cli nicht gefunden – überspringe Test."
+#fi
+#
+#echo "[INFO] Prüfe Rspamd Socket & Verzeichnis..."
+#install -d -m 0755 -o _rspamd -g _rspamd /run/rspamd || true
+#[ -S /var/lib/rspamd/rspamd.sock ] && rm -f /var/lib/rspamd/rspamd.sock || true
+#
+#echo "[INFO] Starte Rspamd neu..."
+#if [ -n "$SYSTEMCTL" ]; then
+# "$SYSTEMCTL" restart "$RSPAMD_SERVICE"
+# echo "[OK] Rspamd erfolgreich neu gestartet."
+#else
+# echo "[ERROR] systemctl nicht gefunden – kein Neustart möglich."
+# exit 1
+#fi
+#
+#echo "[INFO] Healthcheck (Port 11334)..."
+#sleep 3
+#if ss -tln | grep -q ':11334'; then
+# echo "[OK] Rspamd Controller läuft auf Port 11334."
+#else
+# echo "[WARN] Rspamd Controller Port 11334 nicht erreichbar."
+#fi
+#
+#echo "[DONE] Mailwolt Rspamd-Heal abgeschlossen."
+#exit 0
+#EOSH
+#chmod 0755 /usr/local/sbin/mailwolt-rspamd-heal.sh
+#
+## WoltGuard Wrapper + Unit
+#cat >/usr/local/bin/woltguard <<'EOSH'
+##!/usr/bin/env bash
+#set -euo pipefail
+#case "${1:-status}" in
+# start) systemctl enable --now monit ;;
+# stop) systemctl stop monit ;;
+# status) monit summary || systemctl status monit || true ;;
+# heal) monit reload || true; sleep 1; monit restart all || true ;;
+# monitor) monit monitor all || true ;;
+# unmonitor) monit unmonitor all || true ;;
+# *) echo "Usage: woltguard {start|stop|status|heal|monitor|unmonitor}"; exit 2;;
+#esac
+#EOSH
+#chmod 0755 /usr/local/bin/woltguard
+#
+#cat >/etc/systemd/system/woltguard.service <<'EOF'
+#[Unit]
+#Description=WoltGuard – Self-Healing Monitor for MailWolt
+#After=network.target
+#[Service]
+#Type=oneshot
+#ExecStart=/usr/local/bin/woltguard start
+#ExecStop=/usr/local/bin/woltguard stop
+#RemainAfterExit=yes
+#[Install]
+#WantedBy=multi-user.target
+#EOF
+#systemctl daemon-reload
+#systemctl enable --now woltguard
+#
+## Monit Basis + include
+#sed -i 's/^set daemon .*/set daemon 30/' /etc/monit/monitrc || true
+#grep -q 'include /etc/monit/conf.d/*' /etc/monit/monitrc || echo 'include /etc/monit/conf.d/*' >>/etc/monit/monitrc
+#install -d -m 0755 /etc/monit/conf.d
+#
+## Checks
+#cat >/etc/monit/conf.d/postfix.conf <<'EOF'
+#check process postfix with pidfile /var/spool/postfix/pid/master.pid
+# start program = "/bin/systemctl start postfix"
+# stop program = "/bin/systemctl stop postfix"
+# if failed port 25 protocol smtp then restart
+# if failed port 465 type tcpssl then restart
+# if failed port 587 type tcp then restart
+# if 5 restarts within 5 cycles then alert
+#EOF
+#
+#cat >/etc/monit/conf.d/dovecot.conf <<'EOF'
+#check process dovecot with pidfile /run/dovecot/master.pid
+# start program = "/bin/systemctl start dovecot"
+# stop program = "/bin/systemctl stop dovecot"
+# if failed port 993 type tcpssl for 2 cycles then restart
+# if failed port 24 protocol lmtp for 2 cycles then restart
+# if 5 restarts within 5 cycles then alert
+#EOF
+#
+#cat >/etc/monit/conf.d/nginx.conf <<'EOF'
+#check process nginx with pidfile /run/nginx.pid
+# start program = "/bin/systemctl start nginx"
+# stop program = "/bin/systemctl stop nginx"
+# if failed port 80 type tcp then restart
+# if failed port 443 type tcpssl then restart
+# if 5 restarts within 5 cycles then alert
+#EOF
+#
+#cat >/etc/monit/conf.d/redis.conf <<'EOF'
+#check process redis with pidfile /run/redis/redis-server.pid
+# start program = "/bin/systemctl start redis-server"
+# stop program = "/bin/systemctl stop redis-server"
+# if failed host 127.0.0.1 port 6379 for 2 cycles then restart
+# if 5 restarts within 5 cycles then alert
+#
+#check program redis_ping path "/usr/local/sbin/mailwolt-redis-ping.sh"
+# if status != 0 for 2 cycles then exec "/bin/systemctl restart redis-server"
+#EOF
+#
+#cat >/etc/monit/conf.d/rspamd.conf <<'EOF'
+#check process rspamd with pidfile /run/rspamd/rspamd.pid
+# start program = "/bin/systemctl start rspamd"
+# stop program = "/bin/systemctl stop rspamd"
+# if failed port 11333 for 2 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
+# if failed port 11334 for 2 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
+# if 5 restarts within 5 cycles then alert
+#EOF
+#
+#cat >/etc/monit/conf.d/opendkim.conf <<'EOF'
+#check process opendkim with pidfile /run/opendkim/opendkim.pid
+# start program = "/bin/systemctl start opendkim"
+# stop program = "/bin/systemctl stop opendkim"
+# if failed host 127.0.0.1 port 8891 type tcp for 2 cycles then restart
+# if 5 restarts within 5 cycles then alert
+#EOF
+#
+## optional: OpenDMARC
+#if [[ "$OPENDMARC_ENABLE" = "1" ]]; then
+# cat >/etc/monit/conf.d/opendmarc.conf <<'EOF'
+#check process opendmarc with pidfile /run/opendmarc/opendmarc.pid
+# start program = "/bin/systemctl start opendmarc"
+# stop program = "/bin/systemctl stop opendmarc"
+# if 5 restarts within 5 cycles then alert
+#EOF
+#else
+# rm -f /etc/monit/conf.d/opendmarc.conf || true
+#fi
+#
+## optional: ClamAV
+#if [[ "$CLAMAV_ENABLE" = "1" ]]; then
+# cat >/etc/monit/conf.d/clamav.conf <<'EOF'
+#check process clamd with pidfile /run/clamav/clamd.pid
+# start program = "/bin/systemctl start clamav-daemon"
+# stop program = "/bin/systemctl stop clamav-daemon"
+# if failed unixsocket /run/clamav/clamd.ctl then restart
+# if 5 restarts within 5 cycles then alert
+#EOF
+#else
+# rm -f /etc/monit/conf.d/clamav.conf || true
+#fi
+#
+## optional: Fail2Ban
+#if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then
+# cat >/etc/monit/conf.d/fail2ban.conf <<'EOF'
+#check process fail2ban with pidfile /run/fail2ban/fail2ban.pid
+# start program = "/bin/systemctl start fail2ban"
+# stop program = "/bin/systemctl stop fail2ban"
+# if 5 restarts within 5 cycles then alert
+#EOF
+#else
+# rm -f /etc/monit/conf.d/fail2ban.conf || true
+#fi
+#
+#monit -t
+#systemctl reload monit || systemctl restart monit
+#systemctl status monit --no-pager || true
+#log "[✓] WoltGuard aktiv."
\ No newline at end of file
diff --git a/mailwolt-installer/scripts/95-woltguard.sh b/mailwolt-installer/scripts/95-woltguard.sh
new file mode 100644
index 0000000..b3f31f4
--- /dev/null
+++ b/mailwolt-installer/scripts/95-woltguard.sh
@@ -0,0 +1,439 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Flags laden (falls vorhanden)
+INSTALLER_ENV="/etc/mailwolt/installer.env"
+: "${CLAMAV_ENABLE:=}"; : "${OPENDMARC_ENABLE:=}"; : "${FAIL2BAN_ENABLE:=}"; : "${MONIT_HTTP:=}"
+if [[ -z "${CLAMAV_ENABLE}${OPENDMARC_ENABLE}${FAIL2BAN_ENABLE}" && -r "$INSTALLER_ENV" ]]; then
+ . "$INSTALLER_ENV"
+fi
+CLAMAV_ENABLE="${CLAMAV_ENABLE:-1}"
+OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}"
+FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
+MONIT_HTTP="${MONIT_HTTP:-1}"
+
+# ── Monit so konfigurieren, dass NUR monitrc.d/* geladen wird ────────────────
+install -d -m 0755 /etc/monit/monitrc.d
+install -d -m 0755 /etc/monit/conf.d # passiver Ablageort (NICHT includiert)
+
+# Poll-Intervall (30s)
+sed -i 's/^set daemon .*/set daemon 30/' /etc/monit/monitrc || true
+# alle alten include-Zeilen raus und monitrc.d setzen
+sed -i 's|^#\?\s*include .*$||g' /etc/monit/monitrc
+grep -q '^include /etc/monit/monitrc.d/\*' /etc/monit/monitrc \
+ || echo 'include /etc/monit/monitrc.d/*' >> /etc/monit/monitrc
+
+# Optional: HTTP-UI nur einschalten, wenn explizit gewünscht
+if [[ "$MONIT_HTTP" = "1" ]]; then
+ grep -q '^set httpd port 2812' /etc/monit/monitrc || cat >>/etc/monit/monitrc <<'HTTP'
+set httpd port 2812 and
+ use address localhost
+ allow localhost
+HTTP
+fi
+
+# KEIN Löschen mehr der Dateien – wir verschieben je nach Status
+# (vorher stand hier rm -rf /etc/monit/monitrc.d/* und rm -f /etc/monit/conf.d/*.conf)
+
+# ── Helper-Skripte ──────────────────────────────────────────────────────────
+install -d -m 0755 /usr/local/sbin
+
+# Redis-Ping (Password: REDIS_PASSWORD aus installer.env oder .env)
+cat >/usr/local/sbin/mailwolt-redis-ping.sh <<'EOSH'
+#!/usr/bin/env bash
+set -euo pipefail
+INSTALLER_ENV="/etc/mailwolt/installer.env"
+APP_ENV="/var/www/mailwolt/.env"
+REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
+REDIS_PORT="${REDIS_PORT:-6379}"
+REDIS_PASSWORD="${REDIS_PASSWORD:-}"
+REDIS_PASS="${REDIS_PASS:-}"
+
+[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV" || true
+if [[ -r "$APP_ENV" ]]; then
+ [[ -z "${REDIS_HOST}" ]] && REDIS_HOST="$(grep -m1 '^REDIS_HOST=' "$APP_ENV" | cut -d= -f2- || true)"
+ [[ -z "${REDIS_PORT}" ]] && REDIS_PORT="$(grep -m1 '^REDIS_PORT=' "$APP_ENV" | cut -d= -f2- || true)"
+ [[ -z "${REDIS_PASSWORD}" ]] && REDIS_PASSWORD="$(grep -m1 '^REDIS_PASSWORD=' "$APP_ENV" | cut -d= -f2- || true)"
+fi
+[[ -z "${REDIS_PASSWORD}" && -n "${REDIS_PASS}" ]] && REDIS_PASSWORD="$REDIS_PASS"
+
+strip(){ printf '%s' "$1" | sed -E 's/^"(.*)"$/\1/; s/^'"'"'(.*)'"'"'$/\1/'; }
+REDIS_HOST="$(strip "${REDIS_HOST:-}")"
+REDIS_PORT="$(strip "${REDIS_PORT:-}")"
+REDIS_PASSWORD="$(strip "${REDIS_PASSWORD:-}")"
+
+command -v redis-cli >/dev/null 2>&1 || exit 1
+BASE=(timeout 2 redis-cli --no-auth-warning --raw -h "$REDIS_HOST" -p "$REDIS_PORT")
+[[ -n "$REDIS_PASSWORD" ]] && CMD=("${BASE[@]}" -a "$REDIS_PASSWORD" ping) || CMD=("${BASE[@]}" ping)
+[[ "$("${CMD[@]}" 2>/dev/null || true)" == "PONG" ]]
+EOSH
+chmod 0755 /usr/local/sbin/mailwolt-redis-ping.sh
+
+# Rspamd-Heal (Socke aufräumen, restart, Mini-Port-Check)
+cat >/usr/local/sbin/mailwolt-rspamd-heal.sh <<'EOSH'
+#!/usr/bin/env bash
+set -euo pipefail
+
+INSTALLER_ENV="/etc/mailwolt/installer.env"
+APP_ENV="/var/www/mailwolt/.env"
+
+REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
+REDIS_PORT="${REDIS_PORT:-6379}"
+REDIS_PASS="${REDIS_PASS:-}"
+
+[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV"
+if [[ -z "${REDIS_PASS}" && -r "$APP_ENV" ]]; then
+ REDIS_PASS="$(grep -E '^REDIS_PASS=' "$APP_ENV" | head -n1 | cut -d= -f2- || true)"
+fi
+
+# Rspamd Runtime fixen
+install -d -m 0755 -o _rspamd -g _rspamd /run/rspamd || true
+[[ -S /var/lib/rspamd/rspamd.sock ]] && rm -f /var/lib/rspamd/rspamd.sock || true
+
+echo "$(date '+%F %T') heal run" >> /var/log/rspamd-heal.log
+
+# Neustart
+systemctl restart rspamd
+
+# Mini-Healthcheck
+sleep 2
+ss -tln | grep -q ':11334' || echo "[WARN] Rspamd Controller Port 11334 nicht sichtbar"
+
+exit 0
+EOSH
+chmod 0755 /usr/local/sbin/mailwolt-rspamd-heal.sh
+
+# ── Monit-Checks (nummeriert) – fixe Dienste immer aktiv ────────────────────
+# 10 – Redis
+cat >/etc/monit/monitrc.d/10-redis.conf <<'EOF'
+check process redis with pidfile /run/redis/redis-server.pid
+ start program = "/bin/systemctl start redis-server"
+ stop program = "/bin/systemctl stop redis-server"
+ if failed host 127.0.0.1 port 6379 for 2 cycles then restart
+ if 5 restarts within 5 cycles then alert
+
+check program redis_ping path "/usr/local/sbin/mailwolt-redis-ping.sh"
+ if status != 0 for 2 cycles then exec "/bin/systemctl restart redis-server"
+EOF
+
+# 20 – Rspamd (robust via process-matching + Heal)
+cat >/etc/monit/monitrc.d/20-rspamd.conf <<'EOF'
+check process rspamd matching "rspamd: main process"
+ start program = "/bin/systemctl start rspamd" with timeout 120 seconds
+ stop program = "/bin/systemctl stop rspamd"
+ depends on redis
+ if failed host 127.0.0.1 port 11333 for 3 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
+ if failed host 127.0.0.1 port 11334 for 3 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
+ if does not exist for 2 cycles then restart
+ if 5 restarts within 10 cycles then unmonitor
+EOF
+
+# 30 – Postfix
+cat >/etc/monit/monitrc.d/30-postfix.conf <<'EOF'
+check process postfix with pidfile /var/spool/postfix/pid/master.pid
+ start program = "/bin/systemctl start postfix"
+ stop program = "/bin/systemctl stop postfix"
+ if failed host 127.0.0.1 port 25 type tcp with timeout 15 seconds for 3 cycles then restart
+ if failed host 127.0.0.1 port 465 type tcpssl with timeout 10 seconds then restart
+ if failed host 127.0.0.1 port 587 type tcp with timeout 10 seconds then restart
+ if 5 restarts within 5 cycles then alert
+EOF
+
+# 30 – Dovecot (IMAPS; LMTP oft Unix-Socket → kein TCP-Fehlalarm)
+cat >/etc/monit/monitrc.d/30-dovecot.conf <<'EOF'
+check process dovecot with pidfile /run/dovecot/master.pid
+ start program = "/bin/systemctl start dovecot"
+ stop program = "/bin/systemctl stop dovecot"
+ if failed port 993 type tcpssl for 3 cycles then restart
+ if 5 restarts within 10 cycles then alert
+EOF
+
+# 40 – Nginx
+cat >/etc/monit/monitrc.d/40-nginx.conf <<'EOF'
+check process nginx with pidfile /run/nginx.pid
+ start program = "/bin/systemctl start nginx"
+ stop program = "/bin/systemctl stop nginx"
+ if failed port 80 type tcp then restart
+ if failed port 443 type tcpssl then restart
+ if 5 restarts within 5 cycles then alert
+EOF
+
+# 50 – OpenDKIM
+cat >/etc/monit/monitrc.d/50-opendkim.conf <<'EOF'
+check process opendkim with pidfile /run/opendkim/opendkim.pid
+ start program = "/bin/systemctl start opendkim"
+ stop program = "/bin/systemctl stop opendkim"
+ if failed host 127.0.0.1 port 8891 type tcp for 2 cycles then restart
+ if 5 restarts within 5 cycles then alert
+EOF
+
+move_monit_conf() {
+ local name="$1" # z.B. 55-opendmarc
+ local enabled="$2" # "0" oder "1"
+ local src="/etc/monit/conf.d/${name}.conf"
+ local dst="/etc/monit/monitrc.d/${name}.conf"
+
+ mkdir -p /etc/monit/conf.d /etc/monit/monitrc.d
+
+ # Falls Datei nirgends existiert → in conf.d anlegen (lesbare Quelle)
+ if [[ ! -f "$src" && ! -f "$dst" ]]; then
+ cat >"$src" <<'EOF_PAYLOAD'
+__PAYLOAD__
+EOF_PAYLOAD
+ fi
+
+ if [[ "$enabled" = "1" ]]; then
+ # Aktiv: in monitrc.d haben
+ if [[ -f "$src" && ! -f "$dst" ]]; then
+ mv -f "$src" "$dst"
+ fi
+ else
+ # Inaktiv: in conf.d haben
+ if [[ -f "$dst" && ! -f "$src" ]]; then
+ mv -f "$dst" "$src"
+ fi
+ fi
+}
+
+move_monit_conf "55-opendmarc" "${OPENDMARC_ENABLE:-0}" <<'EOF'
+check process opendmarc with pidfile /run/opendmarc/opendmarc.pid
+ start program = "/bin/systemctl start opendmarc"
+ stop program = "/bin/systemctl stop opendmarc"
+ if 5 restarts within 5 cycles then alert
+EOF
+
+move_monit_conf "60-clamav" "${CLAMAV_ENABLE:-0}" <<'EOF'
+check process clamd matching "clamd"
+ start program = "/bin/systemctl start clamav-daemon"
+ stop program = "/bin/systemctl stop clamav-daemon"
+ if failed unixsocket /run/clamav/clamd.ctl for 3 cycles then restart
+ if 5 restarts within 10 cycles then unmonitor
+EOF
+
+move_monit_conf "70-fail2ban" "${FAIL2BAN_ENABLE:-0}" <<'EOF'
+check process fail2ban with pidfile /run/fail2ban/fail2ban.pid
+ start program = "/bin/systemctl start fail2ban"
+ stop program = "/bin/systemctl stop fail2ban"
+ if 5 restarts within 5 cycles then alert
+EOF
+
+# ── Monit neu laden ─────────────────────────────────────────────────────────
+monit -t
+systemctl reload monit || systemctl restart monit
+
+# Optionaler Sichttest (CLI funktioniert auch ohne HTTP-UI)
+# sleep 2
+# monit summary || true
+
+##!/usr/bin/env bash
+#set -euo pipefail
+#
+## Flags laden (falls vorhanden)
+#INSTALLER_ENV="/etc/mailwolt/installer.env"
+#: "${CLAMAV_ENABLE:=}"; : "${OPENDMARC_ENABLE:=}"; : "${FAIL2BAN_ENABLE:=}"; : "${MONIT_HTTP:=}"
+#if [[ -z "${CLAMAV_ENABLE}${OPENDMARC_ENABLE}${FAIL2BAN_ENABLE}" && -r "$INSTALLER_ENV" ]]; then
+# . "$INSTALLER_ENV"
+#fi
+#CLAMAV_ENABLE="${CLAMAV_ENABLE:-1}"
+#OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}"
+#FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
+#MONIT_HTTP="${MONIT_HTTP:-1}"
+#
+## ── Monit so konfigurieren, dass NUR monitrc.d/* geladen wird ────────────────
+#install -d -m 0755 /etc/monit/monitrc.d
+## Poll-Intervall (30s)
+#sed -i 's/^set daemon .*/set daemon 30/' /etc/monit/monitrc || true
+## alle alten include-Zeilen raus und monitrc.d setzen
+#sed -i 's|^#\?\s*include .*$||g' /etc/monit/monitrc
+#grep -q '^include /etc/monit/monitrc.d/\*' /etc/monit/monitrc \
+# || echo 'include /etc/monit/monitrc.d/*' >> /etc/monit/monitrc
+#
+## Optional: HTTP-UI nur einschalten, wenn explizit gewünscht
+#if [[ "$MONIT_HTTP" = "1" ]]; then
+# grep -q '^set httpd port 2812' /etc/monit/monitrc || cat >>/etc/monit/monitrc <<'HTTP'
+#set httpd port 2812 and
+# use address localhost
+# allow localhost
+#HTTP
+#fi
+#
+#sudo mkdir -p /etc/monit/monitrc.d
+#sudo rm -rf /etc/monit/monitrc.d/* 2>/dev/null || true
+#sudo rm -f /etc/monit/conf.d/*.conf 2>/dev/null || true
+#
+## ── Helper-Skripte ──────────────────────────────────────────────────────────
+#install -d -m 0755 /usr/local/sbin
+#
+## Redis-Ping (Password: REDIS_PASSWORD aus installer.env oder .env)
+#cat >/usr/local/sbin/mailwolt-redis-ping.sh <<'EOSH'
+##!/usr/bin/env bash
+#set -euo pipefail
+#INSTALLER_ENV="/etc/mailwolt/installer.env"
+#APP_ENV="/var/www/mailwolt/.env"
+#REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
+#REDIS_PORT="${REDIS_PORT:-6379}"
+#REDIS_PASSWORD="${REDIS_PASSWORD:-}"
+#REDIS_PASS="${REDIS_PASS:-}"
+#
+#[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV" || true
+#if [[ -r "$APP_ENV" ]]; then
+# [[ -z "${REDIS_HOST}" ]] && REDIS_HOST="$(grep -m1 '^REDIS_HOST=' "$APP_ENV" | cut -d= -f2- || true)"
+# [[ -z "${REDIS_PORT}" ]] && REDIS_PORT="$(grep -m1 '^REDIS_PORT=' "$APP_ENV" | cut -d= -f2- || true)"
+# [[ -z "${REDIS_PASSWORD}" ]] && REDIS_PASSWORD="$(grep -m1 '^REDIS_PASSWORD=' "$APP_ENV" | cut -d= -f2- || true)"
+#fi
+#[[ -z "${REDIS_PASSWORD}" && -n "${REDIS_PASS}" ]] && REDIS_PASSWORD="$REDIS_PASS"
+#
+#strip(){ printf '%s' "$1" | sed -E 's/^"(.*)"$/\1/; s/^'"'"'(.*)'"'"'$/\1/'; }
+#REDIS_HOST="$(strip "${REDIS_HOST:-}")"
+#REDIS_PORT="$(strip "${REDIS_PORT:-}")"
+#REDIS_PASSWORD="$(strip "${REDIS_PASSWORD:-}")"
+#
+#command -v redis-cli >/dev/null 2>&1 || exit 1
+#BASE=(timeout 2 redis-cli --no-auth-warning --raw -h "$REDIS_HOST" -p "$REDIS_PORT")
+#[[ -n "$REDIS_PASSWORD" ]] && CMD=("${BASE[@]}" -a "$REDIS_PASSWORD" ping) || CMD=("${BASE[@]}" ping)
+#[[ "$("${CMD[@]}" 2>/dev/null || true)" == "PONG" ]]
+#EOSH
+#chmod 0755 /usr/local/sbin/mailwolt-redis-ping.sh
+#
+## Rspamd-Heal (Socke aufräumen, restart, Mini-Port-Check)
+#cat >/usr/local/sbin/mailwolt-rspamd-heal.sh <<'EOSH'
+##!/usr/bin/env bash
+#set -euo pipefail
+#
+#INSTALLER_ENV="/etc/mailwolt/installer.env"
+#APP_ENV="/var/www/mailwolt/.env"
+#
+#REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
+#REDIS_PORT="${REDIS_PORT:-6379}"
+#REDIS_PASS="${REDIS_PASS:-}"
+#
+#[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV"
+#if [[ -z "${REDIS_PASS}" && -r "$APP_ENV" ]]; then
+# REDIS_PASS="$(grep -E '^REDIS_PASS=' "$APP_ENV" | head -n1 | cut -d= -f2- || true)"
+#fi
+#
+## Rspamd Runtime fixen
+#install -d -m 0755 -o _rspamd -g _rspamd /run/rspamd || true
+#[[ -S /var/lib/rspamd/rspamd.sock ]] && rm -f /var/lib/rspamd/rspamd.sock || true
+#
+#echo "$(date '+%F %T') heal run" >> /var/log/rspamd-heal.log
+#
+## Neustart
+#systemctl restart rspamd
+#
+## Mini-Healthcheck
+#sleep 2
+#ss -tln | grep -q ':11334' || echo "[WARN] Rspamd Controller Port 11334 nicht sichtbar"
+#
+#exit 0
+#EOSH
+#chmod 0755 /usr/local/sbin/mailwolt-rspamd-heal.sh
+#
+## ── Monit-Checks (nummeriert) ───────────────────────────────────────────────
+## 10 – Redis
+#cat >/etc/monit/monitrc.d/10-redis.conf <<'EOF'
+#check process redis with pidfile /run/redis/redis-server.pid
+# start program = "/bin/systemctl start redis-server"
+# stop program = "/bin/systemctl stop redis-server"
+# if failed host 127.0.0.1 port 6379 for 2 cycles then restart
+# if 5 restarts within 5 cycles then alert
+#
+#check program redis_ping path "/usr/local/sbin/mailwolt-redis-ping.sh"
+# if status != 0 for 2 cycles then exec "/bin/systemctl restart redis-server"
+#EOF
+#
+## 20 – Rspamd (robust via process-matching + Heal)
+#cat >/etc/monit/monitrc.d/20-rspamd.conf <<'EOF'
+#check process rspamd matching "rspamd: main process"
+# start program = "/bin/systemctl start rspamd" with timeout 120 seconds
+# stop program = "/bin/systemctl stop rspamd"
+# depends on redis
+# if failed host 127.0.0.1 port 11333 for 3 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
+# if failed host 127.0.0.1 port 11334 for 3 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
+# if does not exist for 2 cycles then restart
+# if 5 restarts within 10 cycles then unmonitor
+#EOF
+#
+## 30 – Postfix
+#cat >/etc/monit/monitrc.d/30-postfix.conf <<'EOF'
+#check process postfix with pidfile /var/spool/postfix/pid/master.pid
+# start program = "/bin/systemctl start postfix"
+# stop program = "/bin/systemctl stop postfix"
+# if failed host 127.0.0.1 port 25 type tcp with timeout 15 seconds for 3 cycles then restart
+# if failed host 127.0.0.1 port 465 type tcpssl with timeout 10 seconds then restart
+# if failed host 127.0.0.1 port 587 type tcp with timeout 10 seconds then restart
+# if 5 restarts within 5 cycles then alert
+#EOF
+#
+## 30 – Dovecot (IMAPS; LMTP oft Unix-Socket → kein TCP-Fehlalarm)
+#cat >/etc/monit/monitrc.d/30-dovecot.conf <<'EOF'
+#check process dovecot with pidfile /run/dovecot/master.pid
+# start program = "/bin/systemctl start dovecot"
+# stop program = "/bin/systemctl stop dovecot"
+# if failed port 993 type tcpssl for 3 cycles then restart
+# if 5 restarts within 10 cycles then alert
+#EOF
+#
+## 40 – Nginx
+#cat >/etc/monit/monitrc.d/40-nginx.conf <<'EOF'
+#check process nginx with pidfile /run/nginx.pid
+# start program = "/bin/systemctl start nginx"
+# stop program = "/bin/systemctl stop nginx"
+# if failed port 80 type tcp then restart
+# if failed port 443 type tcpssl then restart
+# if 5 restarts within 5 cycles then alert
+#EOF
+#
+## 50 – OpenDKIM
+#cat >/etc/monit/monitrc.d/50-opendkim.conf <<'EOF'
+#check process opendkim with pidfile /run/opendkim/opendkim.pid
+# start program = "/bin/systemctl start opendkim"
+# stop program = "/bin/systemctl stop opendkim"
+# if failed host 127.0.0.1 port 8891 type tcp for 2 cycles then restart
+# if 5 restarts within 5 cycles then alert
+#EOF
+#
+## 55 – OpenDMARC (optional)
+#if [[ "$OPENDMARC_ENABLE" = "1" ]]; then
+# cat >/etc/monit/monitrc.d/55-opendmarc.conf <<'EOF'
+#check process opendmarc with pidfile /run/opendmarc/opendmarc.pid
+# start program = "/bin/systemctl start opendmarc"
+# stop program = "/bin/systemctl stop opendmarc"
+# if 5 restarts within 5 cycles then alert
+#EOF
+#else
+# rm -f /etc/monit/monitrc.d/55-opendmarc.conf || true
+#fi
+#
+## 60 – ClamAV (über Socket)
+#if [[ "$CLAMAV_ENABLE" = "1" ]]; then
+# cat >/etc/monit/monitrc.d/60-clamav.conf <<'EOF'
+#check process clamd matching "clamd"
+# start program = "/bin/systemctl start clamav-daemon"
+# stop program = "/bin/systemctl stop clamav-daemon"
+# if failed unixsocket /run/clamav/clamd.ctl for 3 cycles then restart
+# if 5 restarts within 10 cycles then unmonitor
+#EOF
+#else
+# rm -f /etc/monit/monitrc.d/60-clamav.conf || true
+#fi
+#
+## 70 – Fail2Ban (optional)
+#if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then
+# cat >/etc/monit/monitrc.d/70-fail2ban.conf <<'EOF'
+#check process fail2ban with pidfile /run/fail2ban/fail2ban.pid
+# start program = "/bin/systemctl start fail2ban"
+# stop program = "/bin/systemctl stop fail2ban"
+# if 5 restarts within 5 cycles then alert
+#EOF
+#else
+# rm -f /etc/monit/monitrc.d/70-fail2ban.conf || true
+#fi
+#
+## ── Monit neu laden ─────────────────────────────────────────────────────────
+#monit -t
+#systemctl reload monit || systemctl restart monit
+#
+## Optionaler Sichttest (CLI funktioniert auch ohne HTTP-UI)
+##sleep 2
+##monit summary || true
\ No newline at end of file
diff --git a/mailwolt-installer/scripts/98-motd.sh b/mailwolt-installer/scripts/98-motd.sh
new file mode 100644
index 0000000..f5a7736
--- /dev/null
+++ b/mailwolt-installer/scripts/98-motd.sh
@@ -0,0 +1,150 @@
+#!/usr/bin/env bash
+set -euo pipefail
+source ./lib.sh
+
+log "MOTD installieren …"
+install -d /usr/local/bin
+
+cat >/usr/local/bin/mw-motd <<'SH'
+#!/usr/bin/env bash
+# MOTD – MailWolt
+# bewusst KEIN "set -e" und KEIN pipefail; MOTD darf nie hart abbrechen
+set -u
+
+# ---------- Farben ----------
+NC="\033[0m"; WH="\033[1;37m"; CY="\033[1;36m"; GY="\033[0;90m"
+GR="\033[1;32m"; YE="\033[1;33m"; RD="\033[1;31m"
+
+# ---------- Breite / Zentrierung ----------
+W=110
+term_cols=$(tput cols 2>/dev/null || echo $W)
+[ "$term_cols" -gt "$W" ] && pad=$(( (term_cols - W)/2 )) || pad=0
+sp(){ [ "$1" -gt 0 ] && printf "%${1}s" " " || true; }
+center() { local s="$1"; local n=$(( (W - ${#s})/2 )); sp $((pad+n)); printf "%s\n" "$s"; }
+rule(){ sp "$pad"; printf "%0.s=" $(seq 1 "$W"); printf "\n"; }
+title(){ sp "$pad"; local t="$1"; local lf=$(( (W - ${#t} - 2)/2 )); local rf=$(( W - ${#t} - 2 - lf )); \
+ printf "%s" "$(printf '─%.0s' $(seq 1 $lf))"; printf " %s " "$t"; printf "%s\n" "$(printf '─%.0s' $(seq 1 $rf))"; }
+kv(){ sp "$pad"; printf "%-12s: %s\n" "$1" "$2"; }
+
+# ---------- Installer-/App-Variablen ----------
+UI_HOST=""; WEBMAIL_HOST=""; MAIL_HOSTNAME=""
+[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env || true
+
+# ---------- Systemdaten ----------
+now="$(date '+%Y-%m-%d %H:%M:%S %Z' 2>/dev/null || echo '-')"
+upt="$(uptime -p 2>/dev/null || echo '-')"
+cores="$(nproc 2>/dev/null || echo 1)"
+load_raw="$(awk '{printf "%s / %s / %s",$1,$2,$3}' /proc/loadavg 2>/dev/null || echo '0.00 / 0.00 / 0.00')"
+load1="$(awk '{print $1}' /proc/loadavg 2>/dev/null || echo 0)"
+
+# RAM/SWAP
+mem_total="$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)"
+mem_avail="$(awk '/MemAvailable/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)"
+mem_used=$(( mem_total - mem_avail ))
+swap_total="$(awk '/SwapTotal/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)"
+swap_free="$(awk '/SwapFree/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)"
+swap_used=$(( swap_total - swap_free ))
+
+pct(){ local u="$1" t="$2"; [ "$t" -gt 0 ] || { echo 0; return; }; awk -v u="$u" -v t="$t" 'BEGIN{printf "%d",(u*100)/t}' ; }
+ram_pct=$(pct "$mem_used" "$mem_total")
+swap_pct=$(pct "$swap_used" "$swap_total")
+
+# Disks
+df_line(){ df -hP "$1" 2>/dev/null | awk 'NR==2{printf "%s / %s (%s)",$3,$2,$5}'; }
+df_pct(){ df -P "$1" 2>/dev/null | awk 'NR==2{gsub("%","",$5);print $5+0}'; }
+disk_root="$(df_line /)"; pct_root="$(df_pct /)"
+disk_var="$(df_line /var 2>/dev/null)"; [ -n "$disk_var" ] || disk_var="-"
+pct_var="$(df_pct /var 2>/dev/null)"; [ -n "$pct_var" ] || pct_var=0
+
+# IPs (int/ext)
+ipv4_int="$(hostname -I 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i!~/:/){print $i;exit}}')"
+ipv6_int="$(hostname -I 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i~/:/){print $i;exit}}')"
+ipv4_ext="$(curl -4fsS --max-time 1 https://ifconfig.me 2>/dev/null || true)"
+ipv6_ext="$(curl -6fsS --max-time 1 https://ifconfig.me 2>/dev/null || true)"
+
+# ---------- Status-Farben ----------
+mark(){ # value thresholdY thresholdR
+ local v="$1" y="$2" r="$3"
+ if [ "$v" -ge "$r" ]; then printf "${RD}[HIGH]${NC}"
+ elif [ "$v" -ge "$y" ]; then printf "${YE}[WARN]${NC}"
+ else printf "${GR}[OK]${NC}"
+ fi
+}
+# Load/CPU-Schwellen (pro Core)
+load_pct=$(awk -v l="$load1" -v c="$cores" 'BEGIN{if(c<1)c=1; printf "%d", (l/c)*100}')
+m_load="$(mark "$load_pct" 70 100)"
+m_ram="$(mark "$ram_pct" 75 90)"
+m_swap="$(mark "$swap_pct" 10 50)"
+m_root="$(mark "$pct_root" 75 90)"
+m_var="$(mark "$pct_var" 75 90)"
+
+# ---------- Header ----------
+rule
+center ""
+center ":::: :::: ::: ::::::::::: ::: ::: ::: :::::::: ::: :::::::::::"
+center ":+:+:+ :+:+:+ :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: "
+center ":+: +:+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ "
+center "+#+ +:+ +#+ +#++:++#++: +#+ +#+ +#+ +:+ +#+ +#+ +:+ +#+ +#+ "
+center "+#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+#+ +#+ +#+ +#+ +#+ +#+ "
+center "#+# #+# #+# #+# #+# #+# #+#+# #+#+# #+# #+# #+# #+# "
+center "### ### ### ### ########### ########## ### ### ######## ########## ### "
+center ""
+rule
+
+# ---------- System ----------
+kv "Date / Time" "${YE}${now}${NC}"
+sp "$pad"; printf "%-12s: int %-40s ext %s\n" "IPv4" "${ipv4_int:--}" "${ipv4_ext:--}"
+sp "$pad"; printf "%-12s: int %-40s ext %s\n" "IPv6" "${ipv6_int:--}" "${ipv6_ext:--}"
+kv "Uptime" "$upt"
+sp "$pad"; printf "%-12s: %s cores, load %s %b\n" "CPU" "$cores" "$load_raw" "$m_load"
+sp "$pad"; printf "%-12s: %s MiB / %s MiB (%d%%) %b %-5s %s MiB / %s MiB (%d%%) %b\n" \
+ "RAM" "$mem_used" "$mem_total" "$ram_pct" "$m_ram" "SWAP:" "$swap_used" "$swap_total" "$swap_pct" "$m_swap"
+sp "$pad"; printf "%-12s: / %s %b %-5s %s %b\n" \
+ "Disk" "$disk_root" "$m_root" "/var:" "$disk_var" "$m_var"
+echo
+
+# ---------- Domains ----------
+title "Domains"
+[ -n "${UI_HOST:-}" ] && kv "UI" "${UI_HOST}"
+[ -n "${WEBMAIL_HOST:-}" ] && kv "Webmail" "${WEBMAIL_HOST}"
+[ -n "${MAIL_HOSTNAME:-}" ]&& kv "MX" "${MAIL_HOSTNAME}"
+echo
+
+# ---------- Services (4 Spalten, bündig) ----------
+title "Services"
+svc_state(){ systemctl is-active --quiet "$1" && printf "${GR}[OK]${NC}" || printf "${RD}[FAIL]${NC}"; }
+SVC=( nginx mariadb redis-server postfix dovecot rspamd opendkim opendmarc clamav-daemon fail2ban mailwolt-ws mailwolt-queue mailwolt-schedule )
+
+i=0; line=""
+for s in "${SVC[@]}"; do
+ st="$(svc_state "$s")"
+ seg="$(printf "%-18s %-7s" "$s" "$st")"
+ line="$line$seg"
+ i=$((i+1))
+ if [ $((i%4)) -eq 0 ]; then sp "$pad"; echo "$line"; line=""; else line="$line "; fi
+done
+[ -n "$line" ] && { sp "$pad"; echo "$line"; }
+echo
+
+exit 0
+SH
+
+chmod 755 /usr/local/bin/mw-motd
+
+# update-motd Hook
+if [[ -d /etc/update-motd.d ]]; then
+ cat >/etc/update-motd.d/10-mailwolt <<'SH'
+#!/usr/bin/env bash
+/usr/local/bin/mw-motd
+SH
+ chmod +x /etc/update-motd.d/10-mailwolt
+ [[ -f /etc/update-motd.d/50-motd-news ]] && chmod -x /etc/update-motd.d/50-motd-news || true
+else
+ # Fallback für Systeme ohne dynamic MOTD
+ cat >/etc/profile.d/10-mailwolt-motd.sh <<'SH'
+case "$-" in *i*) /usr/local/bin/mw-motd ;; esac
+SH
+fi
+
+: > /etc/motd 2>/dev/null || true
+log "[✓] MOTD installiert."
\ No newline at end of file
diff --git a/mailwolt-installer/scripts/99-summary.sh b/mailwolt-installer/scripts/99-summary.sh
new file mode 100644
index 0000000..bf08778
--- /dev/null
+++ b/mailwolt-installer/scripts/99-summary.sh
@@ -0,0 +1,212 @@
+#!/usr/bin/env bash
+set -euo pipefail
+source ./lib.sh
+
+# ─────────────────────────────────────────────────────────────
+# MailWolt – Abschluss / Summary (Dienste, Zertifikate, Smoke-Test)
+# ─────────────────────────────────────────────────────────────
+
+# Farben & Deko
+NC="\033[0m"; BOLD="\033[1m"; DIM="\033[2m"
+GREEN="\033[1;32m"; RED="\033[1;31m"; YELLOW="\033[1;33m"; CYAN="\033[1;36m"; GREY="\033[0;90m"
+OKS="${GREEN}OK${NC}"; FAILS="${RED}FAIL${NC}"
+bar(){ printf "${CYAN}%s${NC}\n" "──────────────────────────────────────────────────────────────────────────────"; }
+ok(){ printf " [${OKS}]\n"; }
+fail(){ printf " [${FAILS}]\n"; }
+
+# Installer-Variablen laden (falls vorhanden)
+set +u
+[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
+set -u
+
+# Defaults / Umgebung
+APP_USER="${APP_USER:-mailwolt}"
+APP_GROUP="${APP_GROUP:-www-data}"
+APP_DIR="${APP_DIR:-/var/www/${APP_USER}}"
+
+BASE_DOMAIN="${BASE_DOMAIN:-example.com}"
+UI_HOST="${UI_HOST:-}"
+WEBMAIL_HOST="${WEBMAIL_HOST:-}"
+MAIL_HOSTNAME="${MAIL_HOSTNAME:-}"
+
+APP_ENV="${APP_ENV:-production}"
+PROXY_MODE="${PROXY_MODE:-}" # leer = nicht anzeigen; "1"=Proxy, "dev"=Dev, sonst "nein"
+NPM_IP="${NPM_IP:-}"
+
+LE_EMAIL="${LE_EMAIL:-admin@${BASE_DOMAIN}}"
+ACME_WEBROOT="/var/www/letsencrypt"
+
+# Zert-Pfade (werden via Hook nach /etc/ssl/* verlinkt)
+UI_CERT="/etc/ssl/ui/fullchain.pem"
+UI_KEY="/etc/ssl/ui/privkey.pem"
+WEBMAIL_CERT="/etc/ssl/webmail/fullchain.pem"
+MAIL_CERT="/etc/ssl/mail/fullchain.pem"
+
+# IPs (aus lib.sh)
+SERVER_PUBLIC_IPV4="${SERVER_PUBLIC_IPV4:-$(detect_ip)}"
+SERVER_PUBLIC_IPV6="${SERVER_PUBLIC_IPV6:-$(detect_ipv6)}"
+
+# URLs (https nur, wenn UI-Cert+Key vorhanden)
+SCHEME="http"
+[[ -s "$UI_CERT" && -s "$UI_KEY" ]] && SCHEME="https"
+APP_URL="${SCHEME}://${UI_HOST:-$SERVER_PUBLIC_IPV4}"
+WEBMAIL_URL="${SCHEME}://${WEBMAIL_HOST:-$SERVER_PUBLIC_IPV4}"
+
+# Ziel eines Symlinks auflösen
+real_target(){ readlink -f -- "$1" 2>/dev/null || true; }
+
+# "LE" werten, wenn live/* ODER archive/* (auch fullchainN.pem) getroffen wird
+is_le_path(){
+ local p="$1"
+ [[ "$p" == /etc/letsencrypt/live/*/fullchain.pem || "$p" == /etc/letsencrypt/archive/*/fullchain*.pem ]]
+}
+
+UI_CERT_TARGET="$(real_target "$UI_CERT")"
+WEBMAIL_CERT_TARGET="$(real_target "$WEBMAIL_CERT")"
+MAIL_CERT_TARGET="$(real_target "$MAIL_CERT")"
+
+is_le_path() {
+ case "$1" in
+ /etc/letsencrypt/live/*) return 0 ;;
+ *) return 1 ;;
+ esac
+}
+
+# robust gegen set -u: immer ${var:-}
+UI_LE="self-signed/none"
+if [ -s "${UI_CERT:-}" ] && [ -n "${UI_CERT_TARGET:-}" ] && is_le_path "${UI_CERT_TARGET:-}"; then
+ UI_LE="LE"
+fi
+
+WEBMAIL_LE="self-signed/none"
+if [ -s "${WEBMAIL_CERT:-}" ] && [ -n "${WEBMAIL_CERT_TARGET:-}" ] && is_le_path "${WEBMAIL_CERT_TARGET:-}"; then
+ WEBMAIL_LE="LE"
+fi
+
+MAIL_LE="self-signed/none"
+if [ -s "${MAIL_CERT:-}" ] && [ -n "${MAIL_CERT_TARGET:-}" ] && is_le_path "${MAIL_CERT_TARGET:-}"; then
+ MAIL_LE="LE"
+fi
+
+echo
+bar
+printf " %s\n" "✔ MailWolt Bootstrap fertig"
+bar
+
+# Kopf-Infos
+printf " %-14s %s\n" "Aufruf UI:" "${APP_URL}"
+printf " %-14s %s\n" "Webmail:" "${WEBMAIL_URL}"
+printf " %-14s %s\n" "App Root:" "${APP_DIR}"
+printf " %-14s %s\n" "Mail-FQDN:" "${MAIL_HOSTNAME:-$SERVER_PUBLIC_IPV4}"
+printf " %-14s %s\n" "BASE_DOMAIN:" "${BASE_DOMAIN}"
+printf " %-14s %s\n" "LE-Email:" "${LE_EMAIL}"
+printf " %-14s %s\n" "APP_ENV:" "${APP_ENV}"
+# Proxy-Block nur anzeigen, wenn Variable gesetzt ist
+if [[ -n "$PROXY_MODE" ]]; then
+ if [[ "$PROXY_MODE" == "1" ]]; then
+ printf " %-14s %s\n" "Proxy-Mode:" "ja (NPM: ${NPM_IP:-unbekannt})"
+ elif [[ "$PROXY_MODE" == "dev" ]]; then
+ printf " %-14s %s\n" "Proxy-Mode:" "Entwicklungsmodus"
+ else
+ printf " %-14s %s\n" "Proxy-Mode:" "nein"
+ fi
+fi
+printf " %-14s %s\n" "Server IPv6:" "${SERVER_PUBLIC_IPV6:-–}"
+printf " %-14s %s\n" "ACME Webroot:" "${ACME_WEBROOT}"
+
+echo
+printf " %-14s UI=%s, Webmail=%s, MX=%s\n" "Zertifikate:" "$UI_LE" "$WEBMAIL_LE" "$MAIL_LE"
+echo
+
+echo " Anmeldung: Keine vordefinierten Admin-Daten."
+echo " Bitte zuerst registrieren (Erst-User wird Admin, danach"
+echo " wird die Registrierung automatisch gesperrt)."
+echo
+
+# ── Dienste ────────────────────────────────────────────────────────────────
+bar
+echo " Services"
+bar
+
+OK_LIST=()
+FAIL_LIST=()
+
+svc(){
+ local unit="$1" label="${2:-$1}"
+ printf " • %-18s … " "$label"
+ if systemctl is-active --quiet "$unit"; then
+ ok
+ OK_LIST+=("$label")
+ else
+ fail
+ FAIL_LIST+=("$label")
+ fi
+}
+
+# Kern-Services
+svc nginx
+svc mariadb
+svc redis-server
+svc postfix
+svc dovecot
+# App-Worker (tolerant)
+svc "${APP_USER}-ws" "mailwolt-ws" || true
+svc "${APP_USER}-schedule" "mailwolt-schedule" || true
+svc "${APP_USER}-queue" "mailwolt-queue" || true
+
+echo
+if ((${#OK_LIST[@]})); then
+ printf " ${GREEN}OK:${NC} %s\n" "$(IFS=', '; echo "${OK_LIST[*]}")"
+fi
+if ((${#FAIL_LIST[@]})); then
+ printf " ${RED}FAIL:${NC} %s\n" "$(IFS=', '; echo "${FAIL_LIST[*]}")"
+ echo " ${YELLOW}Hinweis:${NC} Details mit: journalctl -u -b --no-pager"
+fi
+echo
+
+# ── Smoke-Test ─────────────────────────────────────────────────────────────
+bar
+echo " Smoke-Test (SMTP/IMAP/POP3 mit/ohne TLS)"
+bar
+
+check_port(){
+ local tag="$1" cmd="$2" desc="$3"
+ printf " [%-3s] %-35s … " "$tag" "$desc"
+ if timeout 8s bash -lc "$cmd" >/dev/null 2>&1; then ok; else fail; fi
+}
+
+# kleines Delay nach Erststart
+sleep 6 || true
+
+# SMTP
+check_port "25" 'printf "EHLO x\r\nQUIT\r\n" | nc -w 3 127.0.0.1 25' \
+ "SMTP (EHLO)"
+check_port "465" 'printf "QUIT\r\n" | openssl s_client -connect 127.0.0.1:465 -quiet -ign_eof' \
+ "SMTPS (TLS + EHLO)"
+check_port "587" 'printf "EHLO x\r\nSTARTTLS\r\nQUIT\r\n" | openssl s_client -starttls smtp -connect 127.0.0.1:587 -quiet -ign_eof' \
+ "Submission (STARTTLS)"
+
+# POP/IMAP
+check_port "110" 'printf "QUIT\r\n" | nc -w 3 127.0.0.1 110' \
+ "POP3 (QUIT)"
+check_port "995" 'printf "QUIT\r\n" | openssl s_client -connect 127.0.0.1:995 -quiet -ign_eof' \
+ "POP3S (TLS + QUIT)"
+check_port "143" 'printf ". CAPABILITY\r\n. LOGOUT\r\n" | nc -w 3 127.0.0.1 143' \
+ "IMAP (CAPABILITY/LOGOUT)"
+check_port "993" 'printf ". CAPABILITY\r\n. LOGOUT\r\n" | openssl s_client -connect 127.0.0.1:993 -quiet -ign_eof' \
+ "IMAPS (TLS + CAPABILITY/LOGOUT)"
+
+echo
+
+# Hinweise nur ausgeben, wenn wirklich kein LE für UI/Webmail
+if [[ "$UI_LE" != "LE" || "$WEBMAIL_LE" != "LE" ]]; then
+ echo -e " ${YELLOW}Hinweis:${NC} UI/Webmail verwenden noch kein Let's-Encrypt-Zertifikat."
+ echo -e " Prüfe Symlinks unter /etc/ssl/{ui,webmail} und den LE-Hook (21/75-Skripte)."
+ echo
+fi
+
+# Proxy-Info (optional)
+if [[ "$PROXY_MODE" == "1" ]]; then
+ echo -e " ${GREY}Proxy-Hinweis:${NC} App erwartet TLS am Proxy (Backend ohne https-Redirects)."
+ echo
+fi
\ No newline at end of file
diff --git a/mailwolt-installer/scripts/bootstrap.sh b/mailwolt-installer/scripts/bootstrap.sh
new file mode 100644
index 0000000..2bfaf20
--- /dev/null
+++ b/mailwolt-installer/scripts/bootstrap.sh
@@ -0,0 +1,633 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# --- Farbschema für whiptail (libnewt) – hohe Lesbarkeit (dunkler Input, schwarze Schrift) ---
+export NEWT_COLORS='
+root=,blue
+border=black,lightgray
+window=black,lightgray
+textbox=black,lightgray
+label=black,lightgray
+entry=black,cyan
+button=black,cyan
+actlistbox=black,cyan
+actsellistbox=black,cyan
+'
+
+# optionales Backtitle (erscheint oben)
+export DIALOGOPTS="--backtitle MailWolt Setup"
+
+# ──────────────────────────────────────────────────────────────
+# MailWolt – Interaktiver Bootstrap (whiptail + Fallback)
+# ──────────────────────────────────────────────────────────────
+
+DEV_MODE=0
+PROXY_MODE=0
+NPM_IP=""
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ -dev) DEV_MODE=1 ;;
+ -proxy) PROXY_MODE=1; NPM_IP="${2:-}"; shift ;;
+ esac
+ shift
+done
+
+APP_ENV="${APP_ENV:-$([[ $DEV_MODE -eq 1 ]] && echo local || echo production)}"
+APP_DEBUG="${APP_DEBUG:-$([[ $DEV_MODE -eq 1 ]] && echo true || echo false)}"
+export DEV_MODE PROXY_MODE NPM_IP APP_ENV APP_DEBUG
+
+DB_PASS="${DB_PASS:-$(openssl rand -hex 16)}"
+REDIS_PASS="${REDIS_PASS:-$(openssl rand -hex 16)}"
+export DB_PASS REDIS_PASS
+
+cd "$(dirname "$0")"
+source ./lib.sh
+require_root
+header
+
+# ── Defaults ──────────────────────────────────────────────────
+APP_NAME="${APP_NAME:-MailWolt}"
+APP_USER="${APP_USER:-mailwolt}"
+APP_GROUP="${APP_GROUP:-www-data}"
+APP_USER_PREFIX="${APP_USER_PREFIX:-mw}"
+APP_DIR="${APP_DIR:-/var/www/${APP_USER}}"
+
+BASE_DOMAIN="${BASE_DOMAIN:-example.com}"
+UI_SUB="${UI_SUB:-ui}"
+WEBMAIL_SUB="${WEBMAIL_SUB:-webmail}"
+MTA_SUB="${MTA_SUB:-mx}"
+
+DB_NAME="${DB_NAME:-${APP_USER}}"
+DB_USER="${DB_USER:-${APP_USER}}"
+
+SERVER_PUBLIC_IPV4="$(detect_ip)"
+SERVER_PUBLIC_IPV6="$(detect_ipv6)"
+DEFAULT_TZ="$(detect_timezone)"
+DEFAULT_LOCALE="$(guess_locale_from_tz "$DEFAULT_TZ")"
+
+echo -e "${GREY}Erkannte IP (v4): ${SERVER_PUBLIC_IPV4} v6: ${SERVER_PUBLIC_IPV6:-–}${NC}"
+
+# ── Helpers ───────────────────────────────────────────────────
+have_whiptail(){ command -v whiptail >/dev/null 2>&1; }
+
+#valid_fqdn(){
+# [[ "$1" =~ ^([a-z0-9]([-a-z0-9]*[a-z0-9])?\.)+[a-z]{2,}$ ]]
+#}
+
+# ── Host-Validierung & DEV-Erkennung ────────────────────────────────────────
+valid_fqdn_prod(){ [[ "$1" =~ ^([a-z0-9]([-a-z0-9]*[a-z0-9])?\.)+[a-z]{2,}$ ]]; }
+valid_host_dev(){
+ # erlaubt: single-label (ui, webmail), FQDNs, IPv4
+ [[ "$1" =~ ^([a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9-]+)*$ ]] || [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]
+}
+is_local_like(){
+ local h="$(echo "$1" | tr '[:upper:]' '[:lower:]')"
+ [[ "$h" =~ \.local$ || "$h" =~ \.loc$ || "$h" =~ \.dev$ || "$h" =~ \.test$ || "$h" = "localhost" ]] && return 0
+ [[ "$h" =~ ^10\. || "$h" =~ ^192\.168\. || "$h" =~ ^172\.(1[6-9]|2[0-9]|3[0-1])\. || "$h" =~ ^127\. ]] && return 0
+ return 1
+}
+normalize_host(){
+ # $1=input $2=default (nutzt DEV_MODE für die passende Prüflogik)
+ local inp="$1" def="$2"
+ if [[ "${DEV_MODE}" = "1" ]]; then
+ valid_host_dev "$inp" && { echo "$inp"; return; }
+ else
+ valid_fqdn_prod "$inp" && { echo "$inp"; return; }
+ fi
+ echo "$def"
+}
+
+ask_tty_domain(){
+ local label="$1" example="$2" def="$3" outvar="$4" inp
+ echo -e "${CYAN}${label}${NC}"
+ echo -e " z.B. ${YELLOW}${example}${NC}"
+ echo -e " Default: ${GREY}${def}${NC}"
+ read -r -p " Eingabe (Enter=Default): " inp || true
+ inp="${inp:-$def}"
+ if ! valid_fqdn "$inp"; then
+ echo -e "${YELLOW}[!] Ungültiger FQDN, nehme Default: ${def}${NC}"
+ inp="$def"
+ fi
+ eval "$outvar='$inp'"
+}
+
+# ── Interaktive Eingaben (whiptail oder Fallback) ─────────────
+MTA_DEFAULT="${MTA_SUB}.${BASE_DOMAIN}"
+UI_DEFAULT="${UI_SUB}.${BASE_DOMAIN}"
+WEBMAIL_DEFAULT="${WEBMAIL_SUB}.${BASE_DOMAIN}"
+
+CLAMAV_ENABLE=1
+OPENDMARC_ENABLE=1
+FAIL2BAN_ENABLE=1
+
+if command -v whiptail >/dev/null 2>&1; then
+ TITLE="MailWolt Setup"
+
+ # Hinweise zu erlaubten DEV-Hosts
+ MSG_SUFFIX="\n\nHinweis: Im DEV-Modus sind auch single-label Hosts (z.B. ui, webmail), *.local/*.dev und IPs erlaubt."
+
+ _mta_in="$(whiptail --title "$TITLE" --inputbox "Mailserver-Host (MX)\nBeispiele: mx.domain.tld | mx.local | 10.0.0.10${MSG_SUFFIX}" 13 70 "$MTA_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1
+ _ui_in="$(whiptail --title "$TITLE" --inputbox "UI / Admin-Panel Host\nBeispiele: ui.domain.tld | ui.local | 10.0.0.10${MSG_SUFFIX}" 13 70 "$UI_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1
+ _wm_in="$(whiptail --title "$TITLE" --inputbox "Webmail Host\nBeispiele: webmail.domain.tld | web.local | 10.0.0.10${MSG_SUFFIX}" 13 70 "$WEBMAIL_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1
+
+ # ZUERST provisorisch prüfen, ob „lokal“ → DEV erzwingen
+ if is_local_like "$_mta_in" || is_local_like "$_ui_in" || is_local_like "$_wm_in"; then
+ DEV_MODE=1; APP_ENV="local"; APP_DEBUG="true"
+ fi
+ export DEV_MODE APP_ENV APP_DEBUG
+
+ # Jetzt mit passender Logik normalisieren
+ MTA_FQDN="$(normalize_host "$_mta_in" "$MTA_DEFAULT")"
+ UI_FQDN="$(normalize_host "$_ui_in" "$UI_DEFAULT")"
+ WEBMAIL_FQDN="$(normalize_host "$_wm_in" "$WEBMAIL_DEFAULT")"
+
+ CHOICES="$(whiptail --title "$TITLE" --checklist "Optionale Dienste aktivieren" 15 70 6 \
+ "ClamAV" "Virenscan (clamd/clamav-daemon)" ON \
+ "OpenDMARC" "DMARC-Auswertung" ON \
+ "Fail2Ban" "Brute-Force-Schutz" ON \
+ 3>&1 1>&2 2>&3)" || true
+ CLAMAV_ENABLE=0; [[ "$CHOICES" == *"ClamAV"* ]] && CLAMAV_ENABLE=1
+ OPENDMARC_ENABLE=0; [[ "$CHOICES" == *"OpenDMARC"* ]] && OPENDMARC_ENABLE=1
+ FAIL2BAN_ENABLE=0; [[ "$CHOICES" == *"Fail2Ban"* ]] && FAIL2BAN_ENABLE=1
+
+else
+ echo -e "${GREY}[i] whiptail nicht gefunden – TTY-Fallback.${NC}\n"
+ read -r -p "Mailserver-Host (MX) [${MTA_DEFAULT}]: " _mta_in; _mta_in="${_mta_in:-$MTA_DEFAULT}"
+ read -r -p "UI / Admin-Panel Host [${UI_DEFAULT}]: " _ui_in; _ui_in="${_ui_in:-$UI_DEFAULT}"
+ read -r -p "Webmail Host [${WEBMAIL_DEFAULT}]: " _wm_in; _wm_in="${_wm_in:-$WEBMAIL_DEFAULT}"
+
+ if is_local_like "$_mta_in" || is_local_like "$_ui_in" || is_local_like "$_wm_in"; then
+ DEV_MODE=1; APP_ENV="local"; APP_DEBUG="true"
+ fi
+ export DEV_MODE APP_ENV APP_DEBUG
+
+ MTA_FQDN="$(normalize_host "$_mta_in" "$MTA_DEFAULT")"
+ UI_FQDN="$(normalize_host "$_ui_in" "$UI_DEFAULT")"
+ WEBMAIL_FQDN="$(normalize_host "$_wm_in" "$WEBMAIL_DEFAULT")"
+
+ read -r -p "ClamAV aktivieren? (1/0, Enter=1): " CLAMAV_ENABLE; CLAMAV_ENABLE="${CLAMAV_ENABLE:-1}"
+ read -r -p "OpenDMARC aktivieren? (1/0, Enter=1): " OPENDMARC_ENABLE; OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}"
+ read -r -p "Fail2Ban aktivieren? (1/0, Enter=1): " FAIL2BAN_ENABLE; FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
+fi
+
+#if have_whiptail; then
+# TITLE="MailWolt Setup"
+#
+# MTA_FQDN="$(whiptail --title "$TITLE" --inputbox "Mailserver-FQDN (MX)\nBeispiel: mx.domain.tld" 11 70 "$MTA_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1
+# valid_fqdn "$MTA_FQDN" || MTA_FQDN="$MTA_DEFAULT"
+#
+# UI_FQDN="$(whiptail --title "$TITLE" --inputbox "UI / Admin-Panel FQDN\nBeispiel: ui.domain.tld" 11 70 "$UI_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1
+# valid_fqdn "$UI_FQDN" || UI_FQDN="$UI_DEFAULT"
+#
+# WEBMAIL_FQDN="$(whiptail --title "$TITLE" --inputbox "Webmail FQDN\nBeispiel: webmail.domain.tld" 11 70 "$WEBMAIL_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1
+# valid_fqdn "$WEBMAIL_FQDN" || WEBMAIL_FQDN="$WEBMAIL_DEFAULT"
+#
+# CHOICES="$(whiptail --title "$TITLE" --checklist "Optionale Dienste aktivieren" 15 70 6 \
+# "ClamAV" "Virenscan (clamd/clamav-daemon)" ON \
+# "OpenDMARC" "DMARC-Auswertung" ON \
+# "Fail2Ban" "Brute-Force-Schutz" ON \
+# 3>&1 1>&2 2>&3)" || true
+#
+# CLAMAV_ENABLE=0; [[ "$CHOICES" == *"ClamAV"* ]] && CLAMAV_ENABLE=1
+# OPENDMARC_ENABLE=0; [[ "$CHOICES" == *"OpenDMARC"* ]] && OPENDMARC_ENABLE=1
+# FAIL2BAN_ENABLE=0; [[ "$CHOICES" == *"Fail2Ban"* ]] && FAIL2BAN_ENABLE=1
+#
+# whiptail --title "$TITLE" --msgbox "Zusammenfassung:
+#
+#MX : $MTA_FQDN
+#UI : $UI_FQDN
+#Webmail : $WEBMAIL_FQDN
+#
+#ClamAV : $([[ $CLAMAV_ENABLE -eq 1 ]] && echo Aktiv || echo Deaktiv)
+#OpenDMARC : $([[ $OPENDMARC_ENABLE -eq 1 ]] && echo Aktiv || echo Deaktiv)
+#Fail2Ban : $([[ $FAIL2BAN_ENABLE -eq 1 ]] && echo Aktiv || echo Deaktiv)
+#" 16 70
+#
+#else
+# echo -e "${GREY}[i] whiptail nicht gefunden – nutze TTY-Prompts.${NC}\n"
+# ask_tty_domain "Mailserver-FQDN (MX)" "mx.domain.tld" "$MTA_DEFAULT" MTA_FQDN
+# ask_tty_domain "UI / Admin-Panel FQDN" "ui.domain.tld" "$UI_DEFAULT" UI_FQDN
+# ask_tty_domain "Webmail FQDN" "webmail.domain.tld" "$WEBMAIL_DEFAULT" WEBMAIL_FQDN
+#
+# read -r -p "ClamAV aktivieren? (1/0, Enter=1): " CLAMAV_ENABLE; CLAMAV_ENABLE="${CLAMAV_ENABLE:-1}"
+# read -r -p "OpenDMARC aktivieren? (1/0, Enter=1): " OPENDMARC_ENABLE; OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}"
+# read -r -p "Fail2Ban aktivieren? (1/0, Enter=1): " FAIL2BAN_ENABLE; FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
+#fi
+
+# ── Defaults/Kompatibilität ──────────────────────────────────
+MTA_FQDN="${MTA_FQDN:-${MTA_DEFAULT}}"
+UI_FQDN="${UI_FQDN:-${UI_DEFAULT}}"
+WEBMAIL_FQDN="${WEBMAIL_FQDN:-${WEBMAIL_DEFAULT}}"
+DKIM_ENABLE="${DKIM_ENABLE:-1}"
+DKIM_SELECTOR="${DKIM_SELECTOR:-mwl1}"
+DKIM_GENERATE="${DKIM_GENERATE:-1}"
+
+# BASE_DOMAIN/Subs aus FQDNs ableiten
+if [[ "$MTA_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then MTA_SUB="${BASH_REMATCH[1]}"; BASE_DOMAIN="${BASH_REMATCH[2]}"; fi
+if [[ "$UI_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then UI_SUB="${BASH_REMATCH[1]}"; fi
+if [[ "$WEBMAIL_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then WEBMAIL_SUB="${BASH_REMATCH[1]}"; fi
+
+SYSMAIL_SUB="${SYSMAIL_SUB:-sysmail}"
+SYSMAIL_DOMAIN="${SYSMAIL_SUB}.${BASE_DOMAIN}"
+
+MAIL_HOSTNAME="${MTA_FQDN}"
+UI_HOST="${UI_FQDN}"
+WEBMAIL_HOST="${WEBMAIL_FQDN}"
+
+APP_TZ="${APP_TZ:-$DEFAULT_TZ}"
+APP_LOCALE="${APP_LOCALE:-$DEFAULT_LOCALE}"
+
+# ── Export & persist ─────────────────────────────────────────
+export APP_NAME APP_USER APP_GROUP APP_USER_PREFIX APP_DIR
+export BASE_DOMAIN UI_SUB WEBMAIL_SUB MTA_SUB
+export SYSMAIL_SUB SYSMAIL_DOMAIN DKIM_ENABLE DKIM_SELECTOR DKIM_GENERATE
+export UI_HOST WEBMAIL_HOST MAIL_HOSTNAME
+export DB_NAME DB_USER
+export SERVER_PUBLIC_IPV4 SERVER_PUBLIC_IPV6 APP_TZ APP_LOCALE
+export CLAMAV_ENABLE OPENDMARC_ENABLE FAIL2BAN_ENABLE
+
+install -d -m 0755 /etc/mailwolt
+cat >/etc/mailwolt/installer.env <>> Running ${STEP}.sh"
+ bash "./${STEP}.sh"
+done
+
+##!/usr/bin/env bash
+#set -euo pipefail
+#
+## --- Flags / Modi ---
+#DEV_MODE=0
+#PROXY_MODE=0
+#NPM_IP=""
+#
+#while [[ $# -gt 0 ]]; do
+# case "$1" in
+# -dev) DEV_MODE=1 ;;
+# -proxy) PROXY_MODE=1; NPM_IP="${2:-}"; shift ;;
+# esac
+# shift
+#done
+#
+#APP_ENV="${APP_ENV:-$([[ $DEV_MODE -eq 1 ]] && echo local || echo production)}"
+#APP_DEBUG="${APP_DEBUG:-$([[ $DEV_MODE -eq 1 ]] && echo true || echo false)}"
+#export DEV_MODE PROXY_MODE NPM_IP APP_ENV APP_DEBUG
+#
+#DB_PASS="${DB_PASS:-$(openssl rand -hex 16)}"
+#REDIS_PASS="${REDIS_PASS:-$(openssl rand -hex 16)}"
+#
+#export DB_PASS REDIS_PASS
+#
+#cd "$(dirname "$0")"
+#source ./lib.sh
+#require_root
+#header
+#
+## ── Defaults ────────────────────────────────────────────────────────────────
+#APP_NAME="${APP_NAME:-MailWolt}"
+#APP_USER="${APP_USER:-mailwolt}"
+#APP_GROUP="${APP_GROUP:-www-data}"
+#APP_USER_PREFIX="${APP_USER_PREFIX:-mw}"
+#APP_DIR="${APP_DIR:-/var/www/${APP_USER}}"
+#
+#BASE_DOMAIN="${BASE_DOMAIN:-example.com}"
+#UI_SUB="${UI_SUB:-ui}"
+#WEBMAIL_SUB="${WEBMAIL_SUB:-webmail}"
+#MTA_SUB="${MTA_SUB:-mx}"
+#
+#DB_NAME="${DB_NAME:-${APP_USER}}"
+#DB_USER="${DB_USER:-${APP_USER}}"
+#
+#SERVER_PUBLIC_IPV4="$(detect_ip)"
+#SERVER_PUBLIC_IPV6="$(detect_ipv6)"
+#DEFAULT_TZ="$(detect_timezone)"
+#DEFAULT_LOCALE="$(guess_locale_from_tz "$DEFAULT_TZ")"
+#
+#echo -e "${GREY}Erkannte IP (v4): ${SERVER_PUBLIC_IPV4} v6: ${SERVER_PUBLIC_IPV6:-–}${NC}"
+#
+## ── Schöne, farbige Abfragen ────────────────────────────────────────────────
+#echo -e "${CYAN}"
+#echo "──────────────────────────────────────────────"
+#echo -e " 📧 MailWolt Setup – Domain Konfiguration"
+#echo "──────────────────────────────────────────────"
+#echo -e "${NC}"
+#
+#MTA_DEFAULT="${MTA_SUB}.${BASE_DOMAIN}"
+#UI_DEFAULT="${UI_SUB}.${BASE_DOMAIN}"
+#WEBMAIL_DEFAULT="${WEBMAIL_SUB}.${BASE_DOMAIN}"
+#
+#ask_domain() {
+# local __outvar="$1" label="$2" example="$3" defval="$4" input=""
+# echo -e "${GREEN}[?]${NC} ${label}"
+# echo -e " z.B. ${YELLOW}${example}${NC}"
+# echo -e " Default: ${CYAN}${defval}${NC}"
+# echo -ne " → Eingabe: ${CYAN}"
+# read -r input
+# echo -e "${NC}"
+# if [[ -z "$input" ]]; then
+# eval "$__outvar='$defval'"
+# else
+# eval "$__outvar='$input'"
+# fi
+#}
+#
+#ask_toggle() {
+# local __outvar="$1" label="$2" defval="${3:-1}" input=""
+# echo -ne "${GREEN}[?]${NC} ${label} (${CYAN}1${NC}=Ja / ${YELLOW}0${NC}=Nein) [Enter=${defval}]: "
+# read -r input
+# input="${input:-$defval}"
+# case "$input" in
+# 1|0) ;;
+# *) echo -e "${YELLOW}Ungültig, nehme Default=${defval}.${NC}"; input="$defval" ;;
+# esac
+# eval "$__outvar='$input'"
+#}
+#
+#ask_domain "MTA_FQDN" "Mailserver-FQDN (MX)" "mx.domain.tld" "$MTA_DEFAULT"
+#ask_domain "UI_FQDN" "UI / Admin-Panel" "ui.domain.tld" "$UI_DEFAULT"
+#ask_domain "WEBMAIL_FQDN" "Webmail-FQDN" "webmail.domain.tld" "$WEBMAIL_DEFAULT"
+#
+#echo -e "${CYAN}"
+#echo "──────────────────────────────────────────────"
+#echo -e " 🛡 Optionale Dienste"
+#echo "──────────────────────────────────────────────"
+#echo -e "${NC}"
+#
+#ask_toggle "CLAMAV_ENABLE" "ClamAV Virenscan aktivieren?" 1
+#ask_toggle "OPENDMARC_ENABLE" "OpenDMARC auswerten?" 1
+#ask_toggle "FAIL2BAN_ENABLE" "Fail2Ban aktivieren?" 1
+#echo
+#
+## Defaults, wenn Enter gedrückt (Abwärtskompatibilität)
+#MTA_FQDN="${MTA_FQDN:-${MTA_SUB}.${BASE_DOMAIN}}"
+#UI_FQDN="${UI_FQDN:-${UI_SUB}.${BASE_DOMAIN}}"
+#WEBMAIL_FQDN="${WEBMAIL_FQDN:-${WEBMAIL_SUB}.${BASE_DOMAIN}}"
+#DKIM_ENABLE="${DKIM_ENABLE:-1}"
+#DKIM_SELECTOR="${DKIM_SELECTOR:-mwl1}"
+#DKIM_GENERATE="${DKIM_GENERATE:-1}"
+#
+## BASE_DOMAIN und Sub-Labels aus MTA/UI/WEBMAIL ableiten (robust)
+#if [[ "$MTA_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then
+# MTA_SUB="${BASH_REMATCH[1]}"
+# BASE_DOMAIN="${BASH_REMATCH[2]}"
+#fi
+#if [[ "$UI_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then
+# UI_SUB="${BASH_REMATCH[1]}"
+#fi
+#if [[ "$WEBMAIL_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then
+# WEBMAIL_SUB="${BASH_REMATCH[1]}"
+#fi
+#
+#SYSMAIL_SUB="${SYSMAIL_SUB:-sysmail}"
+#SYSMAIL_DOMAIN="${SYSMAIL_SUB}.${BASE_DOMAIN}"
+## Kanonische Host-Variablen (NIE wieder zusammenbauen – nimm die FQDNs)
+#MAIL_HOSTNAME="${MTA_FQDN}"
+#UI_HOST="${UI_FQDN}"
+#WEBMAIL_HOST="${WEBMAIL_FQDN}"
+#
+## Zeitzone/Locale sinnvoll setzen
+#APP_TZ="${APP_TZ:-$DEFAULT_TZ}"
+#APP_LOCALE="${APP_LOCALE:-$DEFAULT_LOCALE}"
+#
+## ── Variablen exportieren ───────────────────────────────────────────────────
+#export APP_NAME APP_USER APP_GROUP APP_USER_PREFIX APP_DIR
+#export BASE_DOMAIN UI_SUB WEBMAIL_SUB MTA_SUB
+#export SYSMAIL_SUB SYSMAIL_DOMAIN DKIM_ENABLE DKIM_SELECTOR DKIM_GENERATE
+#export UI_HOST WEBMAIL_HOST MAIL_HOSTNAME
+#export DB_NAME DB_USER
+#export SERVER_PUBLIC_IPV4 SERVER_PUBLIC_IPV6 APP_TZ APP_LOCALE
+#export CLAMAV_ENABLE OPENDMARC_ENABLE FAIL2BAN_ENABLE
+#
+#install -d -m 0755 /etc/mailwolt
+#cat >/etc/mailwolt/installer.env <>> Running ${STEP}.sh"
+# bash "./${STEP}.sh"
+#done
+###!/usr/bin/env bash
+##set -euo pipefail
+##
+### --- Flags / Modi ---
+##DEV_MODE=0
+##PROXY_MODE=0
+##NPM_IP=""
+##
+##while [[ $# -gt 0 ]]; do
+## case "$1" in
+## -dev) DEV_MODE=1 ;;
+## -proxy) PROXY_MODE=1; NPM_IP="${2:-}"; shift ;;
+## esac
+## shift
+##done
+##
+##APP_ENV="${APP_ENV:-$([[ $DEV_MODE -eq 1 ]] && echo local || echo production)}"
+##APP_DEBUG="${APP_DEBUG:-$([[ $DEV_MODE -eq 1 ]] && echo true || echo false)}"
+##export DEV_MODE PROXY_MODE NPM_IP APP_ENV APP_DEBUG
+##
+##DB_PASS="${DB_PASS:-$(openssl rand -hex 16)}"
+##REDIS_PASS="${REDIS_PASS:-$(openssl rand -hex 16)}"
+##
+##export DB_PASS REDIS_PASS
+##
+##cd "$(dirname "$0")"
+##source ./lib.sh
+##require_root
+##header
+##
+### ── Defaults ────────────────────────────────────────────────────────────────
+##APP_NAME="${APP_NAME:-MailWolt}"
+##APP_USER="${APP_USER:-mailwolt}"
+##APP_GROUP="${APP_GROUP:-www-data}"
+##APP_USER_PREFIX="${APP_USER_PREFIX:-mw}"
+##APP_DIR="${APP_DIR:-/var/www/${APP_USER}}"
+##
+##BASE_DOMAIN="${BASE_DOMAIN:-example.com}"
+##UI_SUB="${UI_SUB:-ui}"
+##WEBMAIL_SUB="${WEBMAIL_SUB:-webmail}"
+##MTA_SUB="${MTA_SUB:-mx}"
+##
+##DB_NAME="${DB_NAME:-${APP_USER}}"
+##DB_USER="${DB_USER:-${APP_USER}}"
+##
+##SERVER_PUBLIC_IPV4="$(detect_ip)"
+##SERVER_PUBLIC_IPV6="$(detect_ipv6)"
+##DEFAULT_TZ="$(detect_timezone)"
+##DEFAULT_LOCALE="$(guess_locale_from_tz "$DEFAULT_TZ")"
+##
+##echo -e "${GREY}Erkannte IP (v4): ${SERVER_PUBLIC_IPV4} v6: ${SERVER_PUBLIC_IPV6:-–}${NC}"
+##
+### ── FQDNs abfragen ───────────────────────────────────────────────────────────
+##read -r -p "Mailserver FQDN (MX, z.B. mx.domain.tld) [Enter=${MTA_SUB}.${BASE_DOMAIN}]: " MTA_FQDN
+##read -r -p "UI / Admin-Panel FQDN (z.B. ui.domain.tld) [Enter=${UI_SUB}.${BASE_DOMAIN}]: " UI_FQDN
+##read -r -p "Webmail FQDN (z.B. webmail.domain.tld) [Enter=${WEBMAIL_SUB}.${BASE_DOMAIN}]: " WEBMAIL_FQDN
+##
+### Defaults, wenn Enter gedrückt
+##MTA_FQDN="${MTA_FQDN:-${MTA_SUB}.${BASE_DOMAIN}}"
+##UI_FQDN="${UI_FQDN:-${UI_SUB}.${BASE_DOMAIN}}"
+##WEBMAIL_FQDN="${WEBMAIL_FQDN:-${WEBMAIL_SUB}.${BASE_DOMAIN}}"
+##DKIM_ENABLE="${DKIM_ENABLE:-1}"
+##DKIM_SELECTOR="${DKIM_SELECTOR:-mwl1}"
+##DKIM_GENERATE="${DKIM_GENERATE:-1}"
+##
+### BASE_DOMAIN und Sub-Labels aus MTA/UI/WEBMAIL ableiten (robust)
+##if [[ "$MTA_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then
+## MTA_SUB="${BASH_REMATCH[1]}"
+## BASE_DOMAIN="${BASH_REMATCH[2]}"
+##fi
+##if [[ "$UI_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then
+## UI_SUB="${BASH_REMATCH[1]}"
+##fi
+##if [[ "$WEBMAIL_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then
+## WEBMAIL_SUB="${BASH_REMATCH[1]}"
+##fi
+##
+##SYSMAIL_SUB="${SYSMAIL_SUB:-sysmail}"
+##SYSMAIL_DOMAIN="${SYSMAIL_SUB}.${BASE_DOMAIN}"
+### Kanonische Host-Variablen (NIE wieder zusammenbauen – nimm die FQDNs)
+##MAIL_HOSTNAME="${MTA_FQDN}"
+##UI_HOST="${UI_FQDN}"
+##WEBMAIL_HOST="${WEBMAIL_FQDN}"
+##
+### Zeitzone/Locale sinnvoll setzen (könntest du auch noch abfragen)
+##APP_TZ="${APP_TZ:-$DEFAULT_TZ}"
+##APP_LOCALE="${APP_LOCALE:-$DEFAULT_LOCALE}"
+##
+### ── Variablen exportieren ───────────────────────────────────────────────────
+##export APP_NAME APP_USER APP_GROUP APP_USER_PREFIX APP_DIR
+##export BASE_DOMAIN UI_SUB WEBMAIL_SUB MTA_SUB
+##export SYSMAIL_SUB SYSMAIL_DOMAIN DKIM_ENABLE DKIM_SELECTOR DKIM_GENERATE
+##export UI_HOST WEBMAIL_HOST MAIL_HOSTNAME
+##export DB_NAME DB_USER
+##export SERVER_PUBLIC_IPV4 SERVER_PUBLIC_IPV6 APP_TZ APP_LOCALE
+##
+##install -d -m 0755 /etc/mailwolt
+##cat >/etc/mailwolt/installer.env <>> Running ${STEP}.sh"
+## bash "./${STEP}.sh"
+##done
\ No newline at end of file
diff --git a/mailwolt-installer/scripts/install-wrapper.sh b/mailwolt-installer/scripts/install-wrapper.sh
new file mode 100644
index 0000000..a575ab3
--- /dev/null
+++ b/mailwolt-installer/scripts/install-wrapper.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+# Mailwolt Installer-Wrapper
+# Deploy to: /usr/local/sbin/mailwolt-install
+# Permissions: chmod 0755, chown root:root
+set -euo pipefail
+
+LOG="/var/log/mailwolt-install.log"
+STATE_DIR="/var/lib/mailwolt/install"
+INSTALLER_SCRIPT="/var/www/mailwolt/mailwolt-installer/install.sh"
+APP_DIR="/var/www/mailwolt"
+
+install -d -m 0755 "$STATE_DIR" /var/lib/mailwolt
+: > "$LOG"
+chmod 0644 "$LOG"
+
+echo "running" > "$STATE_DIR/state"
+: > "$STATE_DIR/rc"
+
+{
+ echo "===== $(date -Is) :: Installation gestartet ====="
+
+ if [[ "$(id -u)" -ne 0 ]]; then
+ echo "[!] Muss als root laufen"
+ printf '1\n' > "$STATE_DIR/rc"
+ echo "done" > "$STATE_DIR/state"
+ exit 1
+ fi
+
+ # Komponente aus $1, falls übergeben (z.B. "nginx", "postfix", "dovecot", "all")
+ COMPONENT="${1:-all}"
+ echo "[i] Komponente: $COMPONENT"
+
+ RC=0
+ if [[ -f "$INSTALLER_SCRIPT" ]]; then
+ APP_DIR="$APP_DIR" COMPONENT="$COMPONENT" bash "$INSTALLER_SCRIPT" || RC=$?
+ else
+ echo "[!] installer script nicht gefunden: $INSTALLER_SCRIPT"
+ RC=127
+ fi
+
+ echo "===== $(date -Is) :: Installation beendet (rc=$RC) ====="
+ printf '%s\n' "$RC" > "$STATE_DIR/rc"
+ echo "done" > "$STATE_DIR/state"
+ exit "$RC"
+} 2>&1 | tee -a "$LOG"
diff --git a/mailwolt-installer/scripts/lib.sh b/mailwolt-installer/scripts/lib.sh
new file mode 100644
index 0000000..cfa30c4
--- /dev/null
+++ b/mailwolt-installer/scripts/lib.sh
@@ -0,0 +1,189 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if [ -f /etc/mailwolt/installer.env ]; then
+ set -a
+ . /etc/mailwolt/installer.env
+ set +a
+fi
+# ── Styling ────────────────────────────────────────────────────────────────
+GREEN="$(printf '\033[1;32m')"; YELLOW="$(printf '\033[1;33m')"
+RED="$(printf '\033[1;31m')"; CYAN="$(printf '\033[1;36m')"
+GREY="$(printf '\033[0;90m')"; NC="$(printf '\033[0m')"
+BAR="──────────────────────────────────────────────────────────────────────────────"
+log(){ echo -e "${GREEN}[+]${NC} $*"; }
+warn(){ echo -e "${YELLOW}[!]${NC} $*"; }
+err(){ echo -e "${RED}[x]${NC} $*"; }
+die(){ err "$*"; exit 1; }
+require_root(){ [[ "$(id -u)" -eq 0 ]] || die "Bitte als root ausführen."; }
+
+# --- Defaults, nur wenn noch nicht gesetzt ---------------------------------
+: "${APP_USER:=mailwolt}"
+: "${APP_GROUP:=www-data}"
+: "${APP_DIR:=/var/www/${APP_USER}}"
+
+: "${APP_NAME:=MailWolt}"
+
+: "${BASE_DOMAIN:=example.com}"
+: "${UI_SUB:=ui}"
+: "${WEBMAIL_SUB:=webmail}"
+: "${MTA_SUB:=mx}"
+
+# DB / Redis (werden später durch .env überschrieben)
+: "${DB_NAME:=${APP_USER}}"
+: "${DB_USER:=${APP_USER}}"
+: "${DB_PASS:=}"
+: "${REDIS_PASS:=}"
+
+# Stabile Zert-Pfade (UI/WEBMAIL/MX → symlinked via 20-ssl.sh)
+: "${MAIL_SSL_DIR:=/etc/ssl/mail}"
+: "${UI_SSL_DIR:=/etc/ssl/ui}"
+: "${WEBMAIL_SSL_DIR:=/etc/ssl/webmail}"
+: "${UI_CERT:=${UI_SSL_DIR}/fullchain.pem}"
+: "${UI_KEY:=${UI_SSL_DIR}/privkey.pem}"
+
+# Optional: E-Mail für LE
+: "${LE_EMAIL:=admin@${BASE_DOMAIN}}"
+
+load_env_file(){
+ local f="$1"
+ [[ -f "$f" ]] || return 0
+ while IFS='=' read -r k v; do
+ [[ "$k" =~ ^[A-Z0-9_]+$ ]] || continue
+ export "$k=$v"
+ done < <(grep -E '^[A-Z0-9_]+=' "$f")
+}
+
+header(){ echo -e "${CYAN}${BAR}${NC}
+${CYAN} :::: :::: ::: ::::::::::: ::: ::: ::: :::::::: ::: :::::::::::
+${CYAN} +:+:+: :+:+:+ :+: :+: :+: :+: :+: :+: :+: :+: :+: :+:
+${CYAN} +:+ +:+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+
+${CYAN} +#+ +:+ +#+ +#++:++#++: +#+ +#+ +#+ +:+ +#+ +#+ +:+ +#+ +#+
+${CYAN} +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+#+ +#+ +#+ +#+ +#+ +#+
+${CYAN} #+# #+# #+# #+# #+# #+# #+#+# #+#+# #+# #+# #+# #+#
+${CYAN} ### ### ### ### ########### ########## ### ### ######## ########## ###
+${CYAN} ${CYAN}${BAR}${NC}\n"; }
+
+#header(){ echo -e "${CYAN}${BAR}${NC}
+#${CYAN} 888b d888 d8b 888 888 888 888 888
+#${CYAN} 8888b d8888 Y8P 888 888 o 888 888 888
+#${CYAN} 88888b.d88888 888 888 d8b 888 888 888
+#${CYAN} 888Y88888P888 8888b. 888 888 888 d888b 888 .d88b. 888 888888
+#${CYAN} 888 Y888P 888 '88b 888 888 888d88888b888 d88''88b 888 888
+#${CYAN} 888 Y8P 888 .d888888 888 888 88888P Y88888 888 888 888 888
+#${CYAN} 888 ' 888 888 888 888 888 8888P Y8888 Y88..88P 888 Y88b.
+#${CYAN} 888 888 'Y888888 888 888 888P Y888 'Y88P' 888 'Y888
+#${CYAN}${BAR}${NC}\n"; }
+
+detect_ip(){
+ local ip
+ ip="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1); exit}}')" || true
+ [[ -n "${ip:-}" ]] || ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
+ [[ -n "${ip:-}" ]] || die "Konnte Server-IP nicht ermitteln."
+ echo "$ip"
+}
+detect_ipv4() {
+ local ext=""
+ if command -v curl >/dev/null 2>&1; then
+ ext="$(curl -fsS --max-time 2 https://ifconfig.me 2>/dev/null || true)"
+ [[ "$ext" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || ext=""
+ fi
+ echo "$ext"
+}
+detect_ipv6(){
+ local ip6
+ ip6="$(ip -6 addr show scope global 2>/dev/null | awk '/inet6/{print $2}' | cut -d/ -f1 | head -n1)" || true
+ [[ -n "${ip6:-}" ]] || ip6="$(hostname -I 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i ~ /:/){print $i; exit}}')" || true
+ echo "${ip6:-}"
+}
+detect_timezone(){
+ local tz
+ if command -v timedatectl >/dev/null 2>&1; then
+ tz="$(timedatectl show -p Timezone --value 2>/dev/null | tr -d '[:space:]')" || true
+ [[ -n "${tz:-}" && "$tz" == */* ]] && { echo "$tz"; return; }
+ fi
+ [[ -r /etc/timezone ]] && { tz="$(sed -n '1p' /etc/timezone | tr -d '[:space:]')" || true; [[ "$tz" == */* ]] && { echo "$tz"; return; }; }
+ if [[ -L /etc/localtime ]]; then
+ tz="$(readlink -f /etc/localtime 2>/dev/null || true)"; tz="${tz#/usr/share/zoneinfo/}"
+ [[ "$tz" == */* ]] && { echo "$tz"; return; }
+ fi
+ if command -v curl >/dev/null 2>&1; then
+ tz="$(curl -fsSL --max-time 3 https://ipapi.co/timezone 2>/dev/null || true)"; [[ "$tz" == */* ]] && { echo "$tz"; return; }
+ fi
+ echo "UTC"
+}
+guess_locale_from_tz(){ case "${1:-UTC}" in
+ Europe/Berlin|Europe/Vienna|Europe/Zurich|Europe/Luxembourg|Europe/Brussels|Europe/Amsterdam) echo "de";;
+ *) echo "en";; esac; }
+
+resolve_ok(){ local host="$1"; getent ahosts "$host" | awk '{print $1}' | sort -u | grep -q -F "${SERVER_PUBLIC_IPV4:-}" ; }
+join_host(){ local sub="$1" base="$2"; [[ -z "$sub" ]] && echo "$base" || echo "$sub.$base"; }
+
+# dns_preflight HOST [HOST2 ...]
+# Prüft: A-Record → SERVER_PUBLIC_IPV4, MX (nur wenn HOST == MAIL_HOSTNAME), PTR.
+# Gibt strukturierte Zeilen aus: OK|WARN|FAIL
+# Rückgabe 0 = alles OK; 1 = mind. ein FAIL.
+dns_preflight(){
+ local overall=0
+ local server_ip="${SERVER_PUBLIC_IPV4:-}"
+
+ _dns_line(){ local level="$1" host="$2" check="$3" detail="$4"
+ case "$level" in
+ OK) echo -e "${GREEN}[DNS OK ]${NC} ${host} ${GREY}${check}${NC} → ${detail}" ;;
+ WARN) echo -e "${YELLOW}[DNS WARN]${NC} ${host} ${GREY}${check}${NC} → ${detail}" ;;
+ FAIL) echo -e "${RED}[DNS FAIL]${NC} ${host} ${GREY}${check}${NC} → ${detail}"; overall=1 ;;
+ esac
+ }
+
+ for host in "$@"; do
+ [[ -z "$host" || "$host" == "example.com" ]] && continue
+
+ # A-Record
+ local a_ip
+ a_ip="$(dig +short A "$host" @1.1.1.1 2>/dev/null | grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -n1)"
+ if [[ -z "$a_ip" ]]; then
+ _dns_line FAIL "$host" "A-Record" "kein Eintrag gefunden"
+ elif [[ -n "$server_ip" && "$a_ip" != "$server_ip" ]]; then
+ _dns_line FAIL "$host" "A-Record" "${a_ip} ≠ ${server_ip} (Server-IP)"
+ else
+ _dns_line OK "$host" "A-Record" "${a_ip}"
+ fi
+
+ # MX (nur für MAIL_HOSTNAME)
+ if [[ "$host" == "${MAIL_HOSTNAME:-}" ]]; then
+ local mx
+ mx="$(dig +short MX "$host" @1.1.1.1 2>/dev/null | awk '{print $2}' | head -n1)"
+ if [[ -z "$mx" ]]; then
+ _dns_line WARN "$host" "MX-Record" "kein Eintrag – ausgehende Mail ggf. eingeschränkt"
+ else
+ _dns_line OK "$host" "MX-Record" "${mx}"
+ fi
+ fi
+
+ # PTR (nur wenn IP bekannt)
+ if [[ -n "$server_ip" && -n "$a_ip" && "$a_ip" == "$server_ip" ]]; then
+ local ptr
+ ptr="$(dig +short -x "$server_ip" @1.1.1.1 2>/dev/null | head -n1 | sed 's/\.$//')"
+ if [[ -z "$ptr" ]]; then
+ _dns_line WARN "$host" "PTR-Record" "kein Reverse-DNS – kann Spam-Score erhöhen"
+ elif [[ "$ptr" != "$host" && "$ptr" != "${MAIL_HOSTNAME:-}" ]]; then
+ _dns_line WARN "$host" "PTR-Record" "${ptr} (zeigt nicht auf ${host})"
+ else
+ _dns_line OK "$host" "PTR-Record" "${ptr}"
+ fi
+ fi
+ done
+
+ return $overall
+}
+
+upsert_env(){ # upsert in $ENV_FILE
+ local k="$1" v="$2" ek ev
+ ek="$(printf '%s' "$k" | sed -e 's/[.[\*^$(){}+?|/]/\\&/g')"
+ ev="$(printf '%s' "$v" | sed -e 's/[&/]/\\&/g')"
+ if grep -qE "^[#[:space:]]*${ek}=" "$ENV_FILE" 2>/dev/null; then
+ sed -Ei "s|^[#[:space:]]*${ek}=.*|${k}=${ev}|g" "$ENV_FILE"
+ else
+ printf '%s=%s\n' "$k" "$v" >> "$ENV_FILE"
+ fi
+}
diff --git a/mailwolt-installer/scripts/phase2-go-live.sh b/mailwolt-installer/scripts/phase2-go-live.sh
new file mode 100755
index 0000000..af9692c
--- /dev/null
+++ b/mailwolt-installer/scripts/phase2-go-live.sh
@@ -0,0 +1,102 @@
+#!/usr/bin/env bash
+# Phase 2: Go-Live — DNS-Check → LE-Zertifikate → Nginx → Postfix/Dovecot aktualisieren
+# Muss als root ausgeführt werden, NACHDEM die DNS-Einträge gesetzt wurden.
+# Liest Konfiguration aus /etc/mailwolt/installer.env (durch Phase 1 geschrieben).
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "${SCRIPT_DIR}/lib.sh"
+
+require_root
+
+ENV_FILE="/etc/mailwolt/installer.env"
+[[ -f "$ENV_FILE" ]] || die "Phase 1 noch nicht abgeschlossen – ${ENV_FILE} fehlt."
+
+set -a; . "$ENV_FILE"; set +a
+
+# ── Pflichtfelder ─────────────────────────────────────────────────────────────
+: "${BASE_DOMAIN:?BASE_DOMAIN fehlt in ${ENV_FILE}}"
+: "${UI_HOST:?UI_HOST fehlt in ${ENV_FILE}}"
+: "${WEBMAIL_HOST:?WEBMAIL_HOST fehlt in ${ENV_FILE}}"
+: "${MAIL_HOSTNAME:?MAIL_HOSTNAME fehlt in ${ENV_FILE}}"
+: "${SERVER_PUBLIC_IPV4:?SERVER_PUBLIC_IPV4 fehlt in ${ENV_FILE}}"
+
+header
+echo -e "${CYAN} Phase 2 – Go-Live${NC}"
+echo -e "${CYAN}${BAR}${NC}"
+echo
+
+# ── Schritt 1: DNS Preflight ──────────────────────────────────────────────────
+log "DNS-Vorab-Check …"
+
+SKIP_DNS="${SKIP_DNS:-0}"
+dns_ok=0
+
+if [[ "$SKIP_DNS" != "1" ]]; then
+ if dns_preflight "$UI_HOST" "$WEBMAIL_HOST" "$MAIL_HOSTNAME"; then
+ dns_ok=1
+ else
+ warn "Einige DNS-Einträge zeigen noch nicht auf diesen Server."
+ echo
+ echo -e " Möglichkeiten:"
+ echo -e " a) DNS reparieren und dieses Skript erneut ausführen."
+ echo -e " b) Trotzdem fortfahren: SKIP_DNS=1 bash phase2-go-live.sh"
+ echo
+ read -rp "Trotzdem fortfahren? [j/N] " _ans
+ [[ "${_ans,,}" == "j" ]] || die "Abgebrochen."
+ dns_ok=1
+ fi
+else
+ warn "DNS-Check übersprungen (SKIP_DNS=1)."
+ dns_ok=1
+fi
+
+echo
+
+# ── Schritt 2: Let's Encrypt Zertifikate ─────────────────────────────────────
+log "Let's Encrypt Zertifikate ausstellen …"
+bash "${SCRIPT_DIR}/75-le-issue.sh"
+echo
+
+# ── Schritt 3: Nginx-Konfiguration neu schreiben (TLS) ───────────────────────
+log "Nginx-Konfiguration aktualisieren (TLS) …"
+# Nginx-Builder aus 70-nginx.sh wiederverwenden
+source "${SCRIPT_DIR}/70-nginx.sh" || true # sourcing setzt Variablen und führt aus
+echo
+
+# ── Schritt 4: Postfix hostname + TLS-Zertifikate aktualisieren ──────────────
+log "Postfix: myhostname = ${MAIL_HOSTNAME} …"
+postconf -e "myhostname = ${MAIL_HOSTNAME}"
+postconf -e "myorigin = \$myhostname"
+postconf -e "smtpd_tls_cert_file = /etc/ssl/mail/fullchain.pem"
+postconf -e "smtpd_tls_key_file = /etc/ssl/mail/privkey.pem"
+systemctl reload postfix || true
+
+# ── Schritt 5: Dovecot TLS ───────────────────────────────────────────────────
+log "Dovecot: TLS-Zertifikate aktualisieren …"
+if [[ -f /etc/dovecot/conf.d/10-ssl.conf ]]; then
+ sed -i "s|^ssl_cert =.*|ssl_cert = /dev/null || systemctl restart "$u"
+ return 0
+ fi
+ done
+}
+
+artisan_down(){
+ as_app "cd ${APP_DIR} && php artisan down --retry=10 --render=errors.503" 2>/dev/null || true
+ echo "[i] Wartungsmodus aktiviert"
+}
+
+artisan_up(){
+ as_app "cd ${APP_DIR} && php artisan up" 2>/dev/null || true
+ echo "[i] Wartungsmodus beendet"
+}
+
+git_safe(){
+ # Falls nötig: Repo als safe markieren (z.B. wenn Root den Wrapper aufruft)
+ as_app "git -C ${APP_DIR} config --global --add safe.directory ${APP_DIR} >/dev/null 2>&1 || true"
+}
+
+git_dirty_check(){
+ if [[ "$ALLOW_DIRTY" != "1" ]]; then
+ local dirty
+ dirty="$(as_app "git -C ${APP_DIR} status --porcelain")"
+ if [[ -n "$dirty" ]]; then
+ echo "[!] Arbeitsbaum hat uncommitted Änderungen. Abbruch (ALLOW_DIRTY=1 zum Überschreiben)."
+ exit 2
+ fi
+ fi
+}
+
+get_version(){
+ as_app "cd ${APP_DIR} && (git describe --tags --always 2>/dev/null || git rev-parse --short=7 HEAD)"
+}
+
+write_build_info(){
+ local ver="$1" rev="$2"
+ install -d -m 0755 /etc/mailwolt || true
+ printf "version=%s\nrev=%s\nupdated=%s\n" "$ver" "$rev" "$(date -Is)" > /etc/mailwolt/build.info || true
+}
+
+# --- Frontend build (quiet & robust) ------------------------------------------
+frontend_build_quiet() {
+ local LOG="/var/log/mailwolt-frontend-build.log"
+ local NPM_ENV="CI=1 NPM_CONFIG_FUND=false NPM_CONFIG_AUDIT=false npm_config_loglevel=warn"
+
+ # Logfile vorbereiten
+ install -d -m 0755 "$(dirname "$LOG")"
+ : > "$LOG" || true
+ chmod 0644 "$LOG"
+
+ echo "[i] Frontend: vorbereiten …"
+ # Build-Ziele & Temp besitzbar machen
+ as_app "mkdir -p '${APP_DIR}/public/build' '${APP_DIR}/node_modules' '${APP_DIR}/.vite' '${APP_DIR}/.npm-cache'"
+ chown -R "$APP_USER":"$APP_GROUP" "${APP_DIR}/public/build" "${APP_DIR}/node_modules" "${APP_DIR}/.vite" "${APP_DIR}/.npm-cache" || true
+ chmod -R g+rwX "${APP_DIR}/public/build" "${APP_DIR}/node_modules" "${APP_DIR}/.vite" || true
+
+ # evtl. störrische Reste entfernen (vermeidet EACCES beim Unlink/Rimraf)
+ rm -rf "${APP_DIR}/node_modules/.vite" "${APP_DIR}/public/build/"* 2>/dev/null || true
+
+ # npm Config (kein Fund/Audit, eigener Cache unter App-User)
+ as_app "printf 'fund=false\naudit=false\ncache=${APP_DIR}/.npm-cache\n' > ~/.npmrc" >>"$LOG" 2>&1 || true
+
+ echo "[i] Frontend: Dependencies … (Details: $LOG)"
+ if ! as_app "cd '${APP_DIR}' && ${NPM_ENV} npm ci --no-audit --no-fund --no-progress" >>"$LOG" 2>&1; then
+ if ! as_app "cd '${APP_DIR}' && ${NPM_ENV} npm install --no-audit --no-fund --no-progress" >>"$LOG" 2>&1; then
+ echo "[!] npm install fehlgeschlagen. Letzte 60 Zeilen:"
+ tail -n 60 "$LOG" || true
+ return 1
+ fi
+ fi
+
+ echo "[i] Frontend: Build … (Details: $LOG)"
+ if ! as_app "cd '${APP_DIR}' && ${NPM_ENV} npm run build --silent --loglevel=warn" >>"$LOG" 2>&1; then
+ echo "[!] Build fehlgeschlagen. Letzte 80 Zeilen:"
+ tail -n 80 "$LOG" || true
+ return 1
+ fi
+
+ # Nach dem Build noch einmal Rechte begradigen
+ chown -R "$APP_USER":"$APP_GROUP" "${APP_DIR}/public/build" || true
+ find "${APP_DIR}/public/build" -type d -exec chmod 2775 {} \; 2>/dev/null || true
+ find "${APP_DIR}/public/build" -type f -exec chmod 0664 {} \; 2>/dev/null || true
+
+ echo "[✓] Frontend gebaut."
+}
+
+# -------- Guards --------------------------------------------------------------
+[[ "$(id -u)" -eq 0 ]] || { echo "[!] Bitte als root ausführen"; exit 1; }
+[[ -d "$APP_DIR/.git" ]] || { echo "[!] $APP_DIR scheint kein Git-Repo zu sein"; exit 1; }
+
+git_safe
+git_dirty_check
+
+# -------- Git: neuen Stand holen ---------------------------------------------
+echo "[i] Prüfe Repository …"
+OLD_REV="$(as_app "git -C ${APP_DIR} rev-parse HEAD")"
+OLD_VER="$(get_version)"
+NEW_REV="$OLD_REV"
+
+if [[ "$MODE" = "tags" ]]; then
+ # → Neueste Tags holen
+ as_app "git -C ${APP_DIR} fetch --quiet origin && git -C ${APP_DIR} fetch --tags --quiet origin || true"
+ LATEST_TAG="$(as_app "git -C ${APP_DIR} tag --list | sort -V | tail -n1")"
+ if [[ -z "$LATEST_TAG" ]]; then
+ echo "[!] Keine Tags gefunden – falle auf origin/${BRANCH} zurück"
+ as_app "git -C ${APP_DIR} checkout -q ${BRANCH} && git -C ${APP_DIR} pull --ff-only origin ${BRANCH}"
+ else
+ TARGET_REV="$(as_app "git -C ${APP_DIR} rev-list -n1 ${LATEST_TAG}")"
+ if [[ "$TARGET_REV" = "$OLD_REV" ]]; then
+ echo "[✓] Bereits auf neuestem Release (${LATEST_TAG}) – nichts zu tun."
+ write_build_info "$(get_version)" "$OLD_REV"
+ exit 0
+ fi
+ echo "[i] Checkout auf Release ${LATEST_TAG} (${TARGET_REV:0:7}) …"
+ as_app "git -C ${APP_DIR} checkout -q ${LATEST_TAG}"
+ fi
+ NEW_REV="$(as_app "git -C ${APP_DIR} rev-parse HEAD")"
+else
+ # Rolling: branch folgen
+ as_app "git -C ${APP_DIR} fetch --quiet origin ${BRANCH}"
+ BEHIND="$(as_app "git -C ${APP_DIR} rev-list --count HEAD..origin/${BRANCH} || echo 0")"
+ if [[ "$BEHIND" -eq 0 ]]; then
+ echo "[✓] Branch origin/${BRANCH} ist bereits aktuell – nichts zu tun."
+ write_build_info "$(get_version)" "$OLD_REV"
+ exit 0
+ fi
+ echo "[i] Es gibt ${BEHIND} neue Commit(s) – ziehe Änderungen …"
+ as_app "git -C ${APP_DIR} checkout -q ${BRANCH} && git -C ${APP_DIR} pull --ff-only origin ${BRANCH}"
+ NEW_REV="$(as_app "git -C ${APP_DIR} rev-parse HEAD")"
+fi
+
+# -------- Änderungstypen ermitteln -------------------------------------------
+CHANGED_FILES="$(as_app "git -C ${APP_DIR} diff --name-only ${OLD_REV}..${NEW_REV}")"
+
+NEED_COMPOSER=0
+NEED_MIGRATIONS=0
+NEED_FRONTEND=0
+NEED_PHP_RESTART=0
+
+echo "$CHANGED_FILES" | grep -qE '(^|/)composer\.(json|lock)$' && NEED_COMPOSER=1
+echo "$CHANGED_FILES" | grep -qE '^database/migrations/' && NEED_MIGRATIONS=1
+echo "$CHANGED_FILES" | grep -qE '^(package(-lock)?\.json|vite\.config(\.ts|\.js)?|resources/|public/.*\.(js|css))' && NEED_FRONTEND=1
+echo "$CHANGED_FILES" | grep -qE '^(app/|routes/|config/|resources/views/)' && NEED_PHP_RESTART=1
+
+echo "[i] Zusammenfassung:"
+echo " Composer : $([[ $NEED_COMPOSER -eq 1 ]] && echo JA || echo nein)"
+echo " Migrations : $([[ $NEED_MIGRATIONS -eq 1 ]] && echo JA || echo nein)"
+echo " Frontend : $([[ $NEED_FRONTEND -eq 1 ]] && echo JA || echo nein)"
+echo " PHP restart : $([[ $NEED_PHP_RESTART -eq 1 ]] && echo JA || echo nein)"
+
+# Wenn gar nichts relevantes geändert wurde → sauber beenden
+if [[ $NEED_COMPOSER -eq 0 && $NEED_MIGRATIONS -eq 0 && $NEED_FRONTEND -eq 0 && $NEED_PHP_RESTART -eq 0 ]]; then
+ echo "[✓] Code-Stand aktualisiert, aber keine Build/Runtime-Änderungen – keine Neustarts nötig."
+ NEW_VER="$(get_version)"
+ write_build_info "$NEW_VER" "$NEW_REV"
+ echo "[i] Version: ${OLD_VER} → ${NEW_VER}"
+ exit 0
+fi
+
+# -------- Composer (ohne Downtime) -------------------------------------------
+if [[ $NEED_COMPOSER -eq 1 ]]; then
+ echo "[i] Composer …"
+ as_app "cd ${APP_DIR} && composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev"
+fi
+
+# -------- Frontend (ohne Downtime — Assets sind statisch) --------------------
+if [[ $NEED_FRONTEND -eq 1 ]]; then
+ echo "[i] Frontend-Änderungen erkannt – baue Assets …"
+ if ! frontend_build_quiet; then
+ echo "[!] Frontend-Build ist fehlgeschlagen (siehe /var/log/mailwolt-frontend-build.log)."
+ exit 1
+ fi
+fi
+
+# -------- Wartungsmodus: Migrations + Cache + PHP-FPM reload -----------------
+# Trap stellt sicher dass artisan up auch bei Fehler läuft
+MAINTENANCE_ACTIVE=0
+cleanup_maintenance(){
+ if [[ "$MAINTENANCE_ACTIVE" -eq 1 ]]; then
+ artisan_up
+ fi
+}
+trap cleanup_maintenance EXIT INT TERM
+
+if [[ $NEED_MIGRATIONS -eq 1 || $NEED_PHP_RESTART -eq 1 || $NEED_COMPOSER -eq 1 ]]; then
+ artisan_down
+ MAINTENANCE_ACTIVE=1
+fi
+
+if [[ $NEED_MIGRATIONS -eq 1 ]]; then
+ echo "[i] DB-Migrationen …"
+ as_app "cd ${APP_DIR} && php artisan migrate --force"
+fi
+
+if [[ $NEED_PHP_RESTART -eq 1 || $NEED_COMPOSER -eq 1 || $NEED_MIGRATIONS -eq 1 ]]; then
+ echo "[i] Cache/Optimierungen …"
+ as_app "cd ${APP_DIR} && php artisan optimize:clear || true"
+ as_app "cd ${APP_DIR} && php artisan config:cache || true"
+ as_app "cd ${APP_DIR} && php artisan route:cache || true"
+ as_app "cd ${APP_DIR} && php artisan queue:restart || true"
+fi
+
+# -------- Dienste neu laden (gezielt) ----------------------------------------
+echo "[i] Dienste neu laden …"
+if [[ $NEED_PHP_RESTART -eq 1 || $NEED_COMPOSER -eq 1 || $NEED_MIGRATIONS -eq 1 ]]; then
+ restart_php_fpm
+ restart_if_exists "${APP_USER}-queue"
+ restart_if_exists "${APP_USER}-schedule"
+ restart_if_exists "${APP_USER}-ws"
+fi
+if [[ $NEED_FRONTEND -eq 1 || $NEED_PHP_RESTART -eq 1 ]]; then
+ reload_if_active nginx
+fi
+
+# -------- Wartungsmodus beenden ----------------------------------------------
+if [[ "$MAINTENANCE_ACTIVE" -eq 1 ]]; then
+ artisan_up
+ MAINTENANCE_ACTIVE=0
+fi
+
+# -------- Build-Info ablegen --------------------------------------------------
+NEW_VER="$(get_version)"
+write_build_info "$NEW_VER" "$NEW_REV"
+
+echo "[✓] Update abgeschlossen: ${OLD_REV:0:7} → ${NEW_REV:0:7} (Version: ${NEW_VER})"
\ No newline at end of file
diff --git a/mailwolt-installer/tools/push.sh b/mailwolt-installer/tools/push.sh
new file mode 100644
index 0000000..67456ba
--- /dev/null
+++ b/mailwolt-installer/tools/push.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+MSG=""
+BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo main)"
+REMOTE="${REMOTE:-origin}"
+
+usage(){ echo "Usage: $0 -m \"commit message\" [-b branch]"; exit 1; }
+
+while getopts ":m:b:" opt; do
+ case "$opt" in
+ m) MSG="$OPTARG" ;;
+ b) BRANCH="$OPTARG" ;;
+ *) usage ;;
+ esac
+done
+shift $((OPTIND-1))
+
+[[ -n "$MSG" ]] || usage
+git rev-parse --git-dir >/dev/null 2>&1 || { echo "[x] kein Git-Repo"; exit 1; }
+
+echo "[i] Add/Commit → $BRANCH"
+git add -A
+# Kein Commit, wenn nichts geändert ist
+if ! git diff --cached --quiet; then
+ git commit -m "$MSG"
+else
+ echo "[i] Nichts zu committen (Index leer) – pushe nur."
+fi
+
+echo "[i] Push → $REMOTE/$BRANCH"
+git push "$REMOTE" "$BRANCH"
+echo "[✓] Done."
\ No newline at end of file
diff --git a/mailwolt-installer/tools/release.sh b/mailwolt-installer/tools/release.sh
new file mode 100644
index 0000000..75869cb
--- /dev/null
+++ b/mailwolt-installer/tools/release.sh
@@ -0,0 +1,80 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+REMOTE="${REMOTE:-origin}"
+BRANCH="${BRANCH:-main}"
+ALLOW_DIRTY=0
+VERSION=""
+MSG=""
+
+usage(){
+ echo "Usage: $0 [-m \"release notes\"] [-b branch] [--allow-dirty]"
+ exit 1
+}
+
+# Args
+[[ $# -ge 1 ]] || usage
+VERSION="$1"; shift || true
+[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "[x] Ungültige Version: $VERSION (erwarte SemVer X.Y.Z)"; exit 1; }
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ -m) MSG="$2"; shift 2 ;;
+ -b) BRANCH="$2"; shift 2 ;;
+ --allow-dirty) ALLOW_DIRTY=1; shift ;;
+ *) usage ;;
+ esac
+done
+
+git rev-parse --git-dir >/dev/null 2>&1 || { echo "[x] kein Git-Repo"; exit 1; }
+
+# Clean tree?
+if [[ $ALLOW_DIRTY -eq 0 ]]; then
+ if ! git diff --quiet || ! git diff --cached --quiet; then
+ echo "[x] Arbeitsbaum nicht sauber. Committe oder nutze --allow-dirty."
+ exit 1
+ fi
+fi
+
+# Aktuellen Stand holen
+git fetch --quiet "$REMOTE" --tags
+# Branch check-out/sync
+git checkout -q "$BRANCH"
+git pull --ff-only "$REMOTE" "$BRANCH"
+
+TAG="v${VERSION}"
+if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
+ echo "[x] Tag ${TAG} existiert bereits."
+ exit 1
+fi
+
+LAST_TAG="$(git describe --tags --abbrev=0 2>/dev/null || true)"
+if [[ -n "$LAST_TAG" ]]; then
+ CHANGELOG="$(git log --pretty=format:'- %s (%h)' "${LAST_TAG}..HEAD" || true)"
+else
+ CHANGELOG="$(git log --pretty=format:'- %s (%h)' || true)"
+fi
+[[ -n "$CHANGELOG" ]] || CHANGELOG="- initial release content"
+
+# VERSION bumpen
+echo -n "$VERSION" > VERSION
+git add VERSION
+git commit -m "chore(release): v${VERSION}"
+
+# Tag-Message bauen
+if [[ -z "$MSG" ]]; then
+read -r -d '' MSG <