docker-mailserver LXC für Proxmox: Stack + Admin-UI + Webmail + Hardening

- dms-lxc.sh: Proxmox-Host-Installer (unprivilegierter LXC, Debian 13, Docker),
  curl-Self-Download, Multi-Domain-DKIM, SnappyMail-Provisionierung, PVE-Firewall
- Stack: docker-mailserver, Node-Admin-API (Supabase-Auth), React-Admin-UI
  (OPENBUREAU-Look), SnappyMail (Shibui-Theme), Rspamd-Web-UI, docker-socket-proxy
- Admin: Postfächer/Aliase/Catch-all/Quota, editierbare Domains+Settings,
  Server (Quota/Queue über abgesicherte Bridge), Status & DNS
- Hardening: no-new-privileges, Whitelisted exec-Bridge, Rspamd-Passwort,
  .env chmod 600, PVE-CT-Firewall, generisch/teilbar (keine festen Domains)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 02:26:28 +02:00
commit 1d3818e725
36 changed files with 5523 additions and 0 deletions
+463
View File
@@ -0,0 +1,463 @@
#!/usr/bin/env bash
# =============================================================================
# docker-mailserver (DMS) als LXC auf Proxmox VE
# -----------------------------------------------------------------------------
# Dieses Skript wird AUF DEM PROXMOX-HOST als root ausgeführt.
# Es legt einen UNPRIVILEGIERTEN LXC-Container (Debian 13) mit Nesting an,
# installiert Docker und deployt den kompletten Stack via docker compose:
# docker-mailserver + Admin-API + React-Admin-UI + SnappyMail.
#
# Der Ordner stack/ muss neben diesem Skript liegen.
#
# Aufruf: bash dms-lxc.sh
# oder: CTID=110 MAIL_FQDN=mail.example.com bash dms-lxc.sh
# =============================================================================
set -Eeuo pipefail
# ---------------------------------------------------------------------------
# Standard-Einstellungen (per ENV oder interaktiv überschreibbar)
# ---------------------------------------------------------------------------
CTID="${CTID:-}" # leer => nächste freie ID
HOSTNAME="${HOSTNAME:-mailserver}" # LXC-Hostname (Anzeigename)
MAIL_FQDN="${MAIL_FQDN:-}" # z.B. mail.example.com (PFLICHT)
MAIL_DOMAIN="${MAIL_DOMAIN:-}" # primäre Domain, z.B. kgva.ch (leer => aus FQDN abgeleitet)
MAIL_DOMAINS="${MAIL_DOMAINS:-}" # ALLE Mail-Domains (Leerzeichen-getrennt); leer => nur MAIL_DOMAIN
FIRST_EMAIL="${FIRST_EMAIL:-}" # erstes Postfach, z.B. admin@example.com
FIRST_PASSWORD="${FIRST_PASSWORD:-}" # Passwort für das erste Postfach
# Admin-UI / Webmail / Supabase
ADMIN_PORT="${ADMIN_PORT:-8080}" # Port der Admin-UI (NPM zeigt hierauf)
WEBMAIL_PORT="${WEBMAIL_PORT:-8888}" # Port von SnappyMail (NPM zeigt hierauf)
RSPAMD_PORT="${RSPAMD_PORT:-11334}" # Rspamd Web-UI (NPM zeigt hierauf)
ADMIN_ALLOWED_EMAILS="${ADMIN_ALLOWED_EMAILS:-}" # Komma-getrennte Admin-E-Mails (Supabase-Login)
SUPABASE_URL="${SUPABASE_URL:-}" # https://xxxx.supabase.co
SUPABASE_ANON_KEY="${SUPABASE_ANON_KEY:-}" # Supabase anon/public key
# Branding / Web-Domains (Erst-Befüllung, später in der Admin-UI editierbar)
BRAND="${BRAND:-}" # Anzeigename im Dashboard
WEBMAIL_FQDN="${WEBMAIL_FQDN:-}" # Webmail-Domain (NPM)
ADMIN_FQDN="${ADMIN_FQDN:-}" # Admin-UI-Domain (NPM)
# Hardening
NPM_IP="${NPM_IP:-}" # IP des Nginx Proxy Manager; schränkt Web-Ports per PVE-Firewall darauf ein (leer = offen, mit Warnung)
HARDEN_FIREWALL="${HARDEN_FIREWALL:-1}" # 1 = Proxmox-CT-Firewall-Regeln schreiben
RSPAMD_PASSWORD="${RSPAMD_PASSWORD:-}" # leer => wird generiert
# Container-Ressourcen (5 Container + Rspamd -> 4 GB empfohlen)
CORES="${CORES:-2}"
RAM_MB="${RAM_MB:-4096}" # 4 GB; mit ClamAV besser 6144+
DISK_GB="${DISK_GB:-20}" # nach erwartetem Mailvolumen wählen
SWAP_MB="${SWAP_MB:-512}"
# Storage / Template / Netz
OS_TEMPLATE="${OS_TEMPLATE:-debian-13-standard}" # Debian 13 (Trixie); Fallback auf 12 automatisch
STORAGE="${STORAGE:-local-lvm}" # Storage für rootfs
TEMPLATE_STORAGE="${TEMPLATE_STORAGE:-local}" # Storage für CT-Template
BRIDGE="${BRIDGE:-vmbr0}"
NET_IP="${NET_IP:-dhcp}" # "dhcp" oder z.B. "192.168.1.50/24"
NET_GW="${NET_GW:-}" # Gateway, nur bei statischer IP
TIMEZONE="${TIMEZONE:-Europe/Zurich}"
# Funktionen / Optionen
ENABLE_CLAMAV="${ENABLE_CLAMAV:-0}" # 1 = Virenscan (braucht ~1GB RAM extra)
ENABLE_FAIL2BAN="${ENABLE_FAIL2BAN:-1}"
DMS_IMAGE="${DMS_IMAGE:-ghcr.io/docker-mailserver/docker-mailserver:latest}"
# ---------------------------------------------------------------------------
# Hübsche Ausgabe
# ---------------------------------------------------------------------------
RD=$'\033[01;31m'; GN=$'\033[1;92m'; YW=$'\033[33m'; BL=$'\033[1;34m'; CL=$'\033[m'
msg_info() { echo -e " ${YW}${CL} $1"; }
msg_ok() { echo -e " ${GN}${CL} $1"; }
msg_err() { echo -e " ${RD}${CL} $1" >&2; }
die() { msg_err "$1"; exit 1; }
trap 'msg_err "Fehler in Zeile $LINENO. Abbruch."' ERR
# ---------------------------------------------------------------------------
# Pre-Flight Checks
# ---------------------------------------------------------------------------
[[ $EUID -eq 0 ]] || die "Bitte als root auf dem Proxmox-Host ausführen."
command -v pct >/dev/null 2>&1 || die "'pct' nicht gefunden läuft das Skript wirklich auf einem Proxmox-Host?"
command -v pveam >/dev/null 2>&1 || die "'pveam' nicht gefunden kein Proxmox VE?"
command -v openssl >/dev/null 2>&1 || die "'openssl' nicht gefunden (für Passwort-Hash) 'apt install openssl'."
command -v curl >/dev/null 2>&1 || die "'curl' nicht gefunden 'apt install curl'."
# ---------------------------------------------------------------------------
# Stack-Ordner: neben dem Skript ODER per curl-Einzeiler selbst herunterladen
# curl-Einzeiler: bash <(curl -fsSL <RAW-URL>/dms-lxc.sh)
# Privates Repo: GIT_TOKEN=<token> bash <(curl ... )
# ---------------------------------------------------------------------------
REPO_ARCHIVE="${REPO_ARCHIVE:-https://git.kgva.ch/karim/DOCKERMAILSERVER-LXC/archive/main.tar.gz}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd || echo /tmp)"
STACK_DIR="${STACK_DIR:-$SCRIPT_DIR/stack}"
if [[ ! -f "$STACK_DIR/docker-compose.yml" ]]; then
msg_info "stack/ nicht gefunden lade Repo von $REPO_ARCHIVE ..."
DL="$(mktemp -d)"
AUTH=(); [[ -n "${GIT_TOKEN:-}" ]] && AUTH=(-H "Authorization: token ${GIT_TOKEN}")
curl -fsSL "${AUTH[@]}" "$REPO_ARCHIVE" -o "$DL/repo.tar.gz" || die "Download fehlgeschlagen (privates Repo? GIT_TOKEN setzen)."
tar -xzf "$DL/repo.tar.gz" -C "$DL" || die "Entpacken fehlgeschlagen."
FOUND="$(find "$DL" -maxdepth 4 -type f -name docker-compose.yml -path '*/stack/*' 2>/dev/null | head -1)"
[[ -n "$FOUND" ]] || die "stack/ im heruntergeladenen Archiv nicht gefunden."
STACK_DIR="$(dirname "$FOUND")"
msg_ok "Repo geladen, stack/ unter $STACK_DIR"
fi
[[ -f "$STACK_DIR/docker-compose.yml" ]] || die "stack/docker-compose.yml nicht gefunden unter $STACK_DIR"
# ---------------------------------------------------------------------------
# Interaktive Abfragen (nur wenn nicht per ENV gesetzt)
# ---------------------------------------------------------------------------
ask() { local p="$1" d="${2:-}" v; read -rp "$(echo -e "${BL}?${CL} $p ${d:+[$d] }")" v; echo "${v:-$d}"; }
if [[ -z "$CTID" ]]; then CTID="$(pvesh get /cluster/nextid)"; fi
CTID="$(ask "Container-ID (CTID)" "$CTID")"
[[ -z "$MAIL_FQDN" ]] && MAIL_FQDN="$(ask "Mailserver-FQDN (z.B. mail.example.com)")"
[[ -n "$MAIL_FQDN" ]] || die "MAIL_FQDN ist Pflicht."
# Domain aus FQDN ableiten (alles nach dem ersten Punkt) falls nicht gesetzt
[[ -z "$MAIL_DOMAIN" ]] && MAIL_DOMAIN="${MAIL_FQDN#*.}"
MAIL_DOMAIN="$(ask "Primäre Mail-Domain" "$MAIL_DOMAIN")"
[[ -z "$MAIL_DOMAINS" ]] && MAIL_DOMAINS="$(ask "Alle Mail-Domains (Leerzeichen-getrennt)" "$MAIL_DOMAIN")"
# Primäre Domain sicher enthalten, Duplikate raus
MAIL_DOMAINS="$(echo "$MAIL_DOMAIN $MAIL_DOMAINS" | tr ' ' '\n' | awk 'NF && !seen[$0]++' | tr '\n' ' ' | sed 's/ *$//')"
# Branding + Web-Domains (Defaults aus der primären Domain abgeleitet)
[[ -z "$BRAND" ]] && BRAND="$(ask "Anzeigename / Brand (Dashboard)" "$MAIL_DOMAIN")"
[[ -z "$WEBMAIL_FQDN" ]] && WEBMAIL_FQDN="$(ask "Webmail-Domain (NPM-Proxy-Host)" "mail.${MAIL_DOMAIN}")"
[[ -z "$ADMIN_FQDN" ]] && ADMIN_FQDN="$(ask "Admin-UI-Domain (NPM-Proxy-Host)" "admin.${MAIL_DOMAIN}")"
[[ -z "$FIRST_EMAIL" ]] && FIRST_EMAIL="$(ask "Erstes Postfach (E-Mail)" "admin@${MAIL_DOMAIN}")"
if [[ -z "$FIRST_PASSWORD" ]]; then
read -rsp "$(echo -e "${BL}?${CL} Passwort für ${FIRST_EMAIL}: ")" FIRST_PASSWORD; echo
[[ -n "$FIRST_PASSWORD" ]] || die "Passwort darf nicht leer sein."
fi
NET_IP="$(ask "IP (dhcp oder CIDR z.B. 192.168.1.50/24)" "$NET_IP")"
if [[ "$NET_IP" != "dhcp" && -z "$NET_GW" ]]; then
NET_GW="$(ask "Gateway (für statische IP)")"
fi
# --- Admin-UI / Supabase ---
[[ -z "$ADMIN_ALLOWED_EMAILS" ]] && ADMIN_ALLOWED_EMAILS="$(ask "Admin-E-Mail(s) für UI-Login (Supabase, Komma-getrennt)" "$FIRST_EMAIL")"
[[ -z "$SUPABASE_URL" ]] && SUPABASE_URL="$(ask "Supabase URL (leer = später in .env eintragen)")"
[[ -z "$SUPABASE_ANON_KEY" ]] && SUPABASE_ANON_KEY="$(ask "Supabase anon key (leer = später in .env eintragen)")"
# --- Hardening ---
[[ -z "$NPM_IP" ]] && NPM_IP="$(ask "IP des Nginx Proxy Manager (Web-Ports nur von dort; leer = offen)")"
# Rspamd-Controller-Passwort generieren, falls nicht gesetzt
[[ -z "$RSPAMD_PASSWORD" ]] && RSPAMD_PASSWORD="$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 24)"
cat <<EOF
${GN}── Zusammenfassung ─────────────────────────────${CL}
CTID .............. $CTID
Hostname .......... $HOSTNAME
Mail-FQDN ......... $MAIL_FQDN
Mail-Domains ...... $MAIL_DOMAINS
Erstes Postfach ... $FIRST_EMAIL
Ressourcen ........ ${CORES} vCPU / ${RAM_MB}MB RAM / ${DISK_GB}GB Disk
Storage ........... $STORAGE (Template: $TEMPLATE_STORAGE)
Netz .............. $BRIDGE / $NET_IP ${NET_GW:+gw $NET_GW}
Admin-UI .......... Port $ADMIN_PORT (Login: $ADMIN_ALLOWED_EMAILS)
Webmail ........... Port $WEBMAIL_PORT (SnappyMail)
Rspamd-UI ......... Port $RSPAMD_PORT (Passwort wird generiert)
Supabase .......... ${SUPABASE_URL:-<später in .env eintragen>}
Firewall .......... $([[ "$HARDEN_FIREWALL" == 1 ]] && echo "PVE-CT-Firewall an${NPM_IP:+, Web nur von $NPM_IP}" || echo aus)
ClamAV ............ $([[ "$ENABLE_CLAMAV" == 1 ]] && echo an || echo aus)
Fail2ban .......... $([[ "$ENABLE_FAIL2BAN" == 1 ]] && echo an || echo aus)
${GN}────────────────────────────────────────────────${CL}
EOF
read -rp "$(echo -e "${BL}?${CL} Fortfahren? [J/n] ")" go; [[ "${go:-J}" =~ ^[JjYy]?$ ]] || die "Abgebrochen."
# ---------------------------------------------------------------------------
# CT-Template sicherstellen
# ---------------------------------------------------------------------------
msg_info "Suche $OS_TEMPLATE Template ..."
pveam update >/dev/null 2>&1 || true
TEMPLATE="$(pveam available --section system | awk -v t="$OS_TEMPLATE" '$2 ~ t {print $2}' | sort -V | tail -1)"
if [[ -z "$TEMPLATE" ]]; then
msg_err "$OS_TEMPLATE nicht verfügbar fällt auf debian-12-standard zurück."
TEMPLATE="$(pveam available --section system | awk '/debian-12-standard/{print $2}' | sort -V | tail -1)"
fi
[[ -n "$TEMPLATE" ]] || die "Kein passendes Debian-Template in 'pveam available' gefunden."
if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then
msg_info "Lade Template $TEMPLATE herunter ..."
pveam download "$TEMPLATE_STORAGE" "$TEMPLATE"
fi
TEMPLATE_REF="${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}"
msg_ok "Template bereit: $TEMPLATE"
# ---------------------------------------------------------------------------
# Netzwerk-String bauen
# ---------------------------------------------------------------------------
FW_FLAG=""
[[ "$HARDEN_FIREWALL" == 1 ]] && FW_FLAG=",firewall=1"
if [[ "$NET_IP" == "dhcp" ]]; then
NET="name=eth0,bridge=${BRIDGE},ip=dhcp${FW_FLAG}"
else
NET="name=eth0,bridge=${BRIDGE},ip=${NET_IP}${NET_GW:+,gw=${NET_GW}}${FW_FLAG}"
fi
# ---------------------------------------------------------------------------
# LXC erstellen (unprivilegiert) + Nesting/keyctl für Docker
# ---------------------------------------------------------------------------
msg_info "Erstelle LXC $CTID ..."
pct create "$CTID" "$TEMPLATE_REF" \
--hostname "$HOSTNAME" \
--cores "$CORES" \
--memory "$RAM_MB" \
--swap "$SWAP_MB" \
--rootfs "${STORAGE}:${DISK_GB}" \
--net0 "$NET" \
--features nesting=1,keyctl=1 \
--unprivileged 1 \
--onboot 1 \
--ostype debian \
--timezone "$TIMEZONE" \
--description "docker-mailserver (DMS) — ${MAIL_FQDN}"
msg_ok "LXC $CTID erstellt."
# ---------------------------------------------------------------------------
# Proxmox-CT-Firewall: Mail-Ports offen, Web/Rspamd nur von NPM (falls gesetzt)
# (greift nur, wenn die Datacenter/Node-Firewall aktiviert ist — sonst inert)
# ---------------------------------------------------------------------------
if [[ "$HARDEN_FIREWALL" == 1 ]]; then
msg_info "Schreibe Proxmox-CT-Firewall-Regeln ..."
WEBSRC=""; [[ -n "$NPM_IP" ]] && WEBSRC=" -source ${NPM_IP}"
mkdir -p /etc/pve/firewall
cat > "/etc/pve/firewall/${CTID}.fw" <<EOF
[OPTIONS]
enable: 1
policy_in: DROP
policy_out: ACCEPT
[RULES]
# SSH/Console
IN ACCEPT -p tcp -dport 22
# Mail (von überall erreichbar)
IN ACCEPT -p tcp -dport 25
IN ACCEPT -p tcp -dport 465
IN ACCEPT -p tcp -dport 587
IN ACCEPT -p tcp -dport 143
IN ACCEPT -p tcp -dport 993
# Web/Admin/Webmail/Rspamd — nur vom Nginx Proxy Manager (falls NPM_IP gesetzt)
IN ACCEPT -p tcp -dport ${ADMIN_PORT}${WEBSRC}
IN ACCEPT -p tcp -dport ${WEBMAIL_PORT}${WEBSRC}
IN ACCEPT -p tcp -dport ${RSPAMD_PORT}${WEBSRC}
EOF
msg_ok "Firewall-Regeln geschrieben (${NPM_IP:+Web nur von $NPM_IP}${NPM_IP:-Web offen NPM_IP setzen für Einschränkung})."
fi
msg_info "Starte Container ..."
pct start "$CTID"
# Auf Netzwerk warten
for i in {1..30}; do
pct exec "$CTID" -- getent hosts deb.debian.org >/dev/null 2>&1 && break
sleep 2
done
msg_ok "Container läuft."
# ---------------------------------------------------------------------------
# Basis-Setup + Docker im Container installieren
# ---------------------------------------------------------------------------
msg_info "Installiere Updates & Docker im Container ..."
pct exec "$CTID" -- bash -c '
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq ca-certificates curl gnupg openssl jq >/dev/null
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
. /etc/os-release
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian ${VERSION_CODENAME} stable" \
> /etc/apt/sources.list.d/docker.list
apt-get update -qq
apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null
systemctl enable --now docker >/dev/null 2>&1
'
msg_ok "Docker installiert."
# ---------------------------------------------------------------------------
# Stack-Ordner vorbereiten (.env + mailserver.env patchen) und übertragen
# ---------------------------------------------------------------------------
DEPLOY_DIR="/opt/dms-stack"
DMS_TAG="${DMS_IMAGE##*:}"
msg_info "Bereite Stack vor ..."
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
cp -r "$STACK_DIR" "$TMP/stack"
# mailserver.env an Optionen anpassen
sed -i "s|^TZ=.*|TZ=${TIMEZONE}|" "$TMP/stack/mailserver.env"
sed -i "s|^ENABLE_CLAMAV=.*|ENABLE_CLAMAV=${ENABLE_CLAMAV}|" "$TMP/stack/mailserver.env"
sed -i "s|^ENABLE_FAIL2BAN=.*|ENABLE_FAIL2BAN=${ENABLE_FAIL2BAN}|" "$TMP/stack/mailserver.env"
# .env für docker compose schreiben
cat > "$TMP/stack/.env" <<EOF
MAIL_FQDN=${MAIL_FQDN}
MAIL_DOMAIN=${MAIL_DOMAIN}
MAIL_DOMAINS=${MAIL_DOMAINS}
BRAND=${BRAND}
WEBMAIL_FQDN=${WEBMAIL_FQDN}
ADMIN_FQDN=${ADMIN_FQDN}
DMS_TAG=${DMS_TAG}
ADMIN_PORT=${ADMIN_PORT}
WEBMAIL_PORT=${WEBMAIL_PORT}
RSPAMD_PORT=${RSPAMD_PORT}
ADMIN_ALLOWED_EMAILS=${ADMIN_ALLOWED_EMAILS}
SUPABASE_URL=${SUPABASE_URL}
SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY}
EOF
# Rspamd-Controller mit Passwort schützen (Web-UI)
mkdir -p "$TMP/stack/docker-data/dms/config/rspamd/override.d"
cat > "$TMP/stack/docker-data/dms/config/rspamd/override.d/worker-controller.inc" <<EOF
bind_socket = "*:11334";
password = "${RSPAMD_PASSWORD}";
enable_password = "${RSPAMD_PASSWORD}";
EOF
# Verzeichnis per tar in den Container übertragen (pct push kann nur Dateien)
tar -C "$TMP/stack" -czf "$TMP/stack.tar.gz" .
pct exec "$CTID" -- mkdir -p "$DEPLOY_DIR"
pct push "$CTID" "$TMP/stack.tar.gz" /tmp/stack.tar.gz
pct exec "$CTID" -- tar -xzf /tmp/stack.tar.gz -C "$DEPLOY_DIR"
pct exec "$CTID" -- rm -f /tmp/stack.tar.gz
# Hardening: .env (Supabase-Keys) nur für root lesbar
pct exec "$CTID" -- chmod 600 "$DEPLOY_DIR/.env"
msg_ok "Stack übertragen nach $DEPLOY_DIR"
# ---------------------------------------------------------------------------
# TLS-Cert + erstes Postfach VOR dem Start vor-seeden
# (DMS bricht sonst ab: ohne Cert -> TLS-Fehler, ohne Konto -> Shutdown nach 120s,
# und Port 25 öffnet erst NACH dem ersten Konto -> sonst Deadlock beim Warten.)
# ---------------------------------------------------------------------------
msg_info "Erzeuge Self-signed-Zertifikat (Fallback bis NPM/Let's Encrypt) ..."
pct exec "$CTID" -- bash -c "
set -e
mkdir -p $DEPLOY_DIR/docker-data/certs $DEPLOY_DIR/docker-data/dms/config
if [ ! -f $DEPLOY_DIR/docker-data/certs/cert.pem ]; then
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
-keyout $DEPLOY_DIR/docker-data/certs/key.pem \
-out $DEPLOY_DIR/docker-data/certs/cert.pem \
-subj '/CN=$MAIL_FQDN' -addext 'subjectAltName=DNS:$MAIL_FQDN' >/dev/null 2>&1
fi
"
msg_info "Lege erstes Postfach an: $FIRST_EMAIL"
# Passwort-Hash auf dem Host erzeugen (Passwort geht nie in den Container).
PW_HASH="$(printf '%s' "$FIRST_PASSWORD" | openssl passwd -6 -stdin)"
pct exec "$CTID" -- bash -c "grep -q '^${FIRST_EMAIL}|' $DEPLOY_DIR/docker-data/dms/config/postfix-accounts.cf 2>/dev/null || \
printf '%s\n' '${FIRST_EMAIL}|{SHA512-CRYPT}${PW_HASH}' >> $DEPLOY_DIR/docker-data/dms/config/postfix-accounts.cf"
# ---------------------------------------------------------------------------
# Stack bauen & starten + DKIM
# ---------------------------------------------------------------------------
msg_info "Baue & starte Stack (Image-Pull/Build kann einige Minuten dauern) ..."
pct exec "$CTID" -- bash -c "cd $DEPLOY_DIR && docker compose up -d --build"
# Warten bis Mailserver auf Port 25 lauscht (kommt jetzt, da Konto+Cert vorhanden)
msg_info "Warte auf Mailserver-Start ..."
for i in {1..60}; do
pct exec "$CTID" -- bash -c 'docker exec mailserver ss -lnt 2>/dev/null | grep -q ":25 "' && break
sleep 3
done
msg_info "Erzeuge DKIM-Schlüssel (RSA 2048) pro Domain ..."
for d in $MAIL_DOMAINS; do
pct exec "$CTID" -- bash -c "docker exec mailserver setup config dkim keysize 2048 domain '$d'" \
|| msg_err "DKIM für $d fehlgeschlagen später manuell nachholen."
done
# alle DKIM-DNS-Records einsammeln (rspamd legt je Domain eine .dns.txt an)
DKIM_TXT="$(pct exec "$CTID" -- bash -c "for f in $DEPLOY_DIR/docker-data/dms/config/rspamd/dkim/*.dns.txt; do [ -f \"\$f\" ] && echo \"; --- \$(basename \"\$f\" .dns.txt) ---\" && cat \"\$f\" && echo; done 2>/dev/null" || true)"
# --- SnappyMail provisionieren: Wildcard-Domain -> mailserver-Container, Shibui als Default ---
msg_info "Provisioniere SnappyMail (alle Domains -> mailserver, Shibui-Theme) ..."
SM_DEF="$DEPLOY_DIR/docker-data/snappymail/_data_/_default_"
for i in {1..30}; do
pct exec "$CTID" -- bash -c "[ -f '$SM_DEF/domains/default.json' ]" && break
sleep 2
done
pct exec "$CTID" -- bash -c '
set -e
D="$1"; STACK="$2"
[ -f "$D/domains/default.json" ] || exit 0
jq ".IMAP.host=\"mailserver\" | .IMAP.port=143 | .IMAP.type=2 | .IMAP.ssl.allow_self_signed=true
| .SMTP.host=\"mailserver\" | .SMTP.port=587 | .SMTP.type=2 | .SMTP.useAuth=true | .SMTP.ssl.allow_self_signed=true
| .Sieve.host=\"mailserver\"" "$D/domains/default.json" > "$D/domains/default.json.tmp"
mv "$D/domains/default.json.tmp" "$D/domains/default.json"
sed -i "s/^theme = .*/theme = \"Shibui@custom\"/" "$D/configs/application.ini" 2>/dev/null || true
cd "$STACK" && docker compose restart snappymail >/dev/null 2>&1
' _ "$SM_DEF" "$DEPLOY_DIR" || msg_err "SnappyMail-Provisionierung übersprungen (kann manuell erfolgen)."
# Container-IP ermitteln
CT_IP="$(pct exec "$CTID" -- bash -c "hostname -I | awk '{print \$1}'" 2>/dev/null || true)"
# ---------------------------------------------------------------------------
# Abschluss / DNS-Hinweise
# ---------------------------------------------------------------------------
cat <<EOF
${GN}╔══════════════════════════════════════════════════════════════╗
║ docker-mailserver ist eingerichtet 🎉 ║
╚══════════════════════════════════════════════════════════════╝${CL}
Container .......... CTID $CTID (IP: ${CT_IP:-unbekannt})
Mailserver ......... $MAIL_FQDN
Erstes Postfach .... $FIRST_EMAIL
Admin-UI ........... http://${CT_IP:-<ip>}:${ADMIN_PORT} (Supabase-Login)
Webmail ............ http://${CT_IP:-<ip>}:${WEBMAIL_PORT} (SnappyMail)
Rspamd-UI .......... http://${CT_IP:-<ip>}:${RSPAMD_PORT} (Passwort unten)
Verwaltung ......... pct enter $CTID → cd ${DEPLOY_DIR}
${BL}Tipp:${CL} Im Nginx Proxy Manager als Proxy-Hosts anlegen (HTTPS):
admin.${MAIL_DOMAIN} -> ${CT_IP:-<ip>}:${ADMIN_PORT}
mail.${MAIL_DOMAIN} -> ${CT_IP:-<ip>}:${WEBMAIL_PORT}
rspamd.${MAIL_DOMAIN} -> ${CT_IP:-<ip>}:${RSPAMD_PORT}
${RD}Rspamd-Web-UI-Passwort:${CL} ${RSPAMD_PASSWORD}
(steht auch in ${DEPLOY_DIR}/docker-data/dms/config/rspamd/override.d/worker-controller.inc)
${YW}── DNS: einmal für den Mailhost ──${CL}
A-Record: ${MAIL_FQDN}. IN A <öffentliche IP>
PTR/rDNS: <öffentliche IP> -> ${MAIL_FQDN} (beim Hoster/ISP setzen!)
${YW}── DNS: pro Mail-Domain (MX/SPF/DMARC) ──${CL}
EOF
for d in $MAIL_DOMAINS; do
cat <<EOF
[${d}]
MX ${d}. IN MX 10 ${MAIL_FQDN}.
SPF ${d}. IN TXT "v=spf1 mx ~all"
DMARC _dmarc.${d}. IN TXT "v=DMARC1; p=quarantine; rua=mailto:postmaster@${d}"
EOF
done
cat <<EOF
${YW}── DKIM (TXT) je Domain ──${CL}
EOF
if [[ -n "$DKIM_TXT" ]]; then
echo "$DKIM_TXT"
else
echo " (DKIM-Records noch nicht erzeugt siehe README, Abschnitt DKIM.)"
fi
cat <<EOF
${BL}Wichtig:${CL} Port 25 muss von deinem ISP/Hoster freigegeben sein und
per Portweiterleitung (25,465,587,143,993) zum Container ${CT_IP:-…} zeigen.
${YW}── Hardening-Checkliste ──${CL}
[ ] Echtes TLS: NPM-Zertifikat (DNS-Challenge) nach docker-data/certs/cert.pem|key.pem, dann
'docker compose restart mailserver' (ersetzt das Self-signed).
[ ] PVE-Firewall: $([[ "$HARDEN_FIREWALL" == 1 ]] && echo "CT-Regeln geschrieben${NPM_IP:+ (Web nur von $NPM_IP)}" || echo "AUS").
Wirkt nur, wenn die Datacenter/Node-Firewall aktiv ist (Datacenter → Firewall → Options → Enable).
[ ] Web-Ports (Admin/Webmail/Rspamd) nur über NPM/HTTPS erreichbar machen${NPM_IP:+ — NPM_IP gesetzt ✔}.
[ ] Backups: vzdump des CT + ${DEPLOY_DIR}/docker-data (Mail!).
[ ] DMARC nach Testphase auf p=reject verschärfen.
Details, TLS, Backup & Troubleshooting: siehe README.md
EOF
msg_ok "Fertig."