Compare commits

..

No commits in common. "8808d811135b5e8812f489dc6af72fbc5dcd4686" and "b321e6d2ecc1ae62718e243e46639865b48c37d9" have entirely different histories.

12 changed files with 130 additions and 875 deletions

View File

@ -1,26 +1,23 @@
services:
mailserver:
# image: docker.io/mailserver/docker-mailserver:latest # AUSKOMMENTIERT
build:
context: .
dockerfile: Dockerfile
image: dms-custom:latest
container_name: mailserver
# Node-spezifischer Hostname - A-Record zeigt auf DIESEN Server.
# email-srvr.com selbst zeigt auf einen anderen Server und wird hier NICHT verwendet.
hostname: node1.email-srvr.com
hostname: mail.email-srvr.com
domainname: email-srvr.com
ports:
- "25:25"
- "587:587"
- "465:465"
- "143:143"
- "993:993"
- "110:110"
- "995:995"
- "127.0.0.1:11334:11334"
- "25:25" # SMTP (parallel zu MailCow auf Port 25)
- "587:587" # SMTP Submission
- "465:465" # SMTP SSL
- "143:143" # IMAP
- "993:993" # IMAP SSL
- "110:110" # POP3
- "995:995" # POP3 SSL
- "127.0.0.1:11334:11334" # Bindet nur an Localhost!
volumes:
- ./docker-data/dms/mail-data/:/var/mail/
- ./docker-data/dms/mail-state/:/var/mail-state/
@ -30,88 +27,67 @@ services:
- /etc/localtime:/etc/localtime:ro
- ./sync_dynamodb_to_sieve.py:/scripts/sync.py:ro
- ./sieve-cron:/etc/cron.d/sieve-sync:ro
# -------------------------------------------------------
# Caddy Zertifikate: gesamtes Cert-Verzeichnis mounten.
#
# Caddy legt Wildcard-Certs so ab:
# *.andreasknuth.de/
# *.andreasknuth.de.crt
# *.andreasknuth.de.key
# node1.email-srvr.com/
# node1.email-srvr.com.crt
# node1.email-srvr.com.key
#
# setup-dms-tls.sh referenziert per:
# /etc/mail/certs/*.domain/*.domain.crt|.key
# -------------------------------------------------------
- /var/lib/docker/volumes/caddy_data/_data/caddy/certificates/acme-v02.api.letsencrypt.org-directory:/etc/mail/certs:ro
# -------------------------------------------------------
# Dovecot SNI Konfiguration (generiert von setup-dms-tls.sh)
# DMS lädt /tmp/docker-mailserver/dovecot-sni.cf automatisch.
# -------------------------------------------------------
- ./docker-data/dms/config/dovecot-sni.cf:/tmp/docker-mailserver/dovecot-sni.cf:ro
- /var/lib/docker/volumes/caddy_data/_data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.email-srvr.com:/etc/mail/certs:ro
environment:
# -------------------------------------------------------
# SSL Default-Cert: node1.email-srvr.com
# Das ist das Fallback-Cert wenn kein SNI-Match gefunden wird
# (z.B. bei direktem IP-Connect ohne Hostname).
# Kundendomain-SNI wird über postfix-main.cf + dovecot-sni.cf gesteuert.
# -------------------------------------------------------
# Wichtig: Rspamd und andere Services deaktivieren für ersten Test
- SSL_TYPE=manual
- SSL_CERT_PATH=/etc/mail/certs/node1.email-srvr.com/node1.email-srvr.com.crt
- SSL_KEY_PATH=/etc/mail/certs/node1.email-srvr.com/node1.email-srvr.com.key
# SPAM / Rspamd
# Diese Pfade beziehen sich auf das INNERE des Containers (wo wir hin mounten)
- SSL_CERT_PATH=/etc/mail/certs/mail.email-srvr.com.crt
- SSL_KEY_PATH=/etc/mail/certs/mail.email-srvr.com.key
- ENABLE_OPENDKIM=1
- ENABLE_OPENDMARC=0
- ENABLE_POLICYD_SPF=0
# #### SPAM SECTION #####
# SPAM Rspamd aktivieren
- ENABLE_RSPAMD=1
# Greylisting AUS (vermeidet Verzögerungen)
- RSPAMD_GREYLISTING=0
# Eigene Mails NICHT scannen (vermeidet Probleme beim Senden)
- RSPAMD_CHECK_AUTHENTICATED=0
# Hostname Check AN (filtert Botnets, sehr sicher)
- RSPAMD_HFILTER=1
# Spam sortieren statt löschen (Sieve Magic)
- MOVE_SPAM_TO_JUNK=1
# Alte Dienste aus
- ENABLE_AMAVIS=0
- ENABLE_SPAMASSASSIN=0
- ENABLE_POSTGREY=0
# 2. ClamAV deaktivieren (Anti-Virus)
- ENABLE_CLAMAV=0
# Sicherheit
# HACKERSCHUTZ (Pflicht!)
- ENABLE_FAIL2BAN=1
- ENABLE_UNBOUND=1
# Sonstige
# DNS Resolver (verhindert Spamhaus-Probleme)
- ENABLE_UNBOUND=1
# #### END SPAM SECTION #####
# END SPAM SECTION
- ENABLE_MANAGESIEVE=0
- ENABLE_POP3=1
- RSPAMD_LEARN=1
- ONE_DIR=1
- ENABLE_UPDATE_CHECK=0
- PERMIT_DOCKER=network
- SPOOF_PROTECTION=0
- ENABLE_SRS=0
- LOG_LEVEL=info
# Amazon SES Relay
# - PERMIT_DOCKER=empty
- SSL_TYPE=manual
- SSL_CERT_PATH=/tmp/docker-mailserver/ssl/cert.pem
- SSL_KEY_PATH=/tmp/docker-mailserver/ssl/key.pem
# Amazon SES SMTP Relay
- RELAY_HOST=email-smtp.us-east-2.amazonaws.com
- RELAY_PORT=587
- RELAY_USER=${SES_SMTP_USER}
- RELAY_PASSWORD=${SES_SMTP_PASSWORD}
# AWS Credentials
# Content Filter AWS Credentials
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
- AWS_REGION=us-east-2
# Postfix
# POSTFIX_OVERRIDE_HOSTNAME: Was Postfix im EHLO/HELO Banner sendet.
# node1.email-srvr.com passt zum TLS-Cert und ist der echte Hostname.
- POSTFIX_OVERRIDE_HOSTNAME=node1.email-srvr.com
- AWS_REGION=us-east-2
# Weitere Einstellungen
- POSTFIX_OVERRIDE_HOSTNAME=email-srvr.com
- POSTFIX_MYNETWORKS=172.16.0.0/12 172.17.0.0/12 172.18.0.0/12 [::1]/128 [fe80::]/64
- POSTFIX_MAILBOX_SIZE_LIMIT=0
- POSTFIX_MESSAGE_SIZE_LIMIT=0
- SPOOF_PROTECTION=0
- ENABLE_SRS=0
# Debug-Einstellungen
- LOG_LEVEL=info
cap_add:
- NET_ADMIN
- SYS_PTRACE
@ -119,6 +95,7 @@ services:
networks:
mail_network:
aliases:
- mail.email-srvr.com
- mailserver
roundcube:
@ -134,14 +111,16 @@ services:
- ROUNDCUBEMAIL_DB_NAME=roundcube
- ROUNDCUBEMAIL_DB_USER=roundcube
- ROUNDCUBEMAIL_DB_PASSWORD=${ROUNDCUBE_DB_PASSWORD}
# Roundcube verbindet intern über den Docker-Alias
- ROUNDCUBEMAIL_DEFAULT_HOST=ssl://mailserver
# Einfache Konfiguration ohne SSL-Probleme (für ersten Test)
- ROUNDCUBEMAIL_DEFAULT_HOST=ssl://mail.email-srvr.com
- ROUNDCUBEMAIL_DEFAULT_PORT=993
- ROUNDCUBEMAIL_SMTP_SERVER=tls://mailserver
- ROUNDCUBEMAIL_SMTP_SERVER=tls://mail.email-srvr.com
- ROUNDCUBEMAIL_SMTP_PORT=587
#- ROUNDCUBEMAIL_PLUGINS=password,email_config,managesieve
- ROUNDCUBEMAIL_PLUGINS=password,email_config
# In docker-compose.yml bei roundcube hinzufügen:
ports:
- "8888:80"
- "8888:80" # Host:Container
volumes:
- ./docker-data/roundcube/config:/var/roundcube/config
- ./docker-data/roundcube/plugins/email_config:/var/www/html/plugins/email_config:ro
@ -166,4 +145,4 @@ services:
networks:
mail_network:
external: true
external: true

View File

@ -1,205 +0,0 @@
#!/bin/bash
# setup-dms-tls.sh
# Gehört ins Root-Verzeichnis des DMS (neben docker-compose.yml).
#
# Generiert Dovecot- und Postfix-SNI-Konfigurationen für Multi-Domain TLS.
# Liest Domains aus dem laufenden DMS und erstellt:
# - docker-data/dms/config/dovecot-sni.cf
# - docker-data/dms/config/postfix-main.cf
#
# Cert-Konvention (Caddy Wildcard):
# /etc/mail/certs/*.domain.tld/*.domain.tld.crt
# /etc/mail/certs/*.domain.tld/*.domain.tld.key
#
# Usage:
# ./setup-dms-tls.sh
# DMS_CONTAINER=mailserver NODE_HOSTNAME=node1.email-srvr.com ./setup-dms-tls.sh
set -e
DMS_CONTAINER=${DMS_CONTAINER:-"mailserver"}
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CONFIG_DIR="$SCRIPT_DIR/docker-data/dms/config"
CERTS_BASE_PATH=${CERTS_BASE_PATH:-"/etc/mail/certs"}
# Node-Hostname: Fallback-Cert für DMS (kein Wildcard, direktes Cert)
# Muss mit dem 'hostname' in docker-compose.yml übereinstimmen.
NODE_HOSTNAME=${NODE_HOSTNAME:-"node1.email-srvr.com"}
echo "============================================================"
echo " 🔐 DMS TLS SNI Setup (Multi-Domain)"
echo " DMS Container: $DMS_CONTAINER"
echo " Config Dir: $CONFIG_DIR"
echo " Certs Base: $CERTS_BASE_PATH"
echo " Node Hostname: $NODE_HOSTNAME"
echo "============================================================"
# --- Domains aus DMS lesen ---
echo ""
echo "📋 Lese Domains aus DMS..."
DOMAINS=$(docker exec "$DMS_CONTAINER" setup email list 2>/dev/null \
| grep -oP '(?<=@)[^\s]+' \
| sort -u)
if [ -z "$DOMAINS" ]; then
echo "❌ Keine Accounts im DMS gefunden!"
echo " Bitte zuerst anlegen: ./manage_mail_user.sh add user@domain.com PW"
exit 1
fi
echo " Gefundene Domains:"
for d in $DOMAINS; do echo " - $d"; done
# --- Cert-Verfügbarkeit im Container prüfen ---
echo ""
echo "🔍 Prüfe Zertifikat-Verfügbarkeit..."
DOMAINS_OK=""
DOMAINS_MISSING=""
for domain in $DOMAINS; do
CERT_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.crt"
KEY_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.key"
if docker exec "$DMS_CONTAINER" test -f "$CERT_PATH" 2>/dev/null; then
echo "$domain → Cert vorhanden"
DOMAINS_OK="$DOMAINS_OK $domain"
else
echo " ⚠️ $domain → KEIN Cert unter $CERT_PATH"
echo " → update-caddy-certs.sh ausführen + caddy reload!"
DOMAINS_MISSING="$DOMAINS_MISSING $domain"
fi
done
# Node-Hostname Cert prüfen (direktes Cert, kein Wildcard)
NODE_CERT_PATH="$CERTS_BASE_PATH/$NODE_HOSTNAME/$NODE_HOSTNAME.crt"
NODE_KEY_PATH="$CERTS_BASE_PATH/$NODE_HOSTNAME/$NODE_HOSTNAME.key"
if docker exec "$DMS_CONTAINER" test -f "$NODE_CERT_PATH" 2>/dev/null; then
echo "$NODE_HOSTNAME → Cert vorhanden (Node Default)"
NODE_CERT_OK=true
else
echo " ⚠️ $NODE_HOSTNAME → KEIN Cert! Caddy-Block im Caddyfile prüfen."
NODE_CERT_OK=false
fi
if [ -n "$DOMAINS_MISSING" ]; then
echo ""
echo " ⚠️ Fehlende Certs:$DOMAINS_MISSING"
echo " Diese Domains werden NICHT in SNI-Config eingetragen."
fi
if [ -z "$DOMAINS_OK" ]; then
echo "❌ Kein einziges Kundendomain-Cert gefunden!"
echo " Bitte zuerst update-caddy-certs.sh ausführen + caddy reload abwarten."
exit 1
fi
# ================================================================
# DOVECOT SNI Konfiguration
# ================================================================
DOVECOT_CFG="$CONFIG_DIR/dovecot-sni.cf"
echo ""
echo "📝 Generiere: $DOVECOT_CFG"
cat > "$DOVECOT_CFG" << 'HEADER'
# dovecot-sni.cf - Automatisch generiert von setup-dms-tls.sh
# SNI-basierte Zertifikat-Auswahl für Dovecot (IMAP/POP3).
# Dovecot liest dieses File über den Volume-Mount in /tmp/docker-mailserver/
# und wendet es automatisch an.
#
# Volume-Mount in docker-compose.yml:
# - ./docker-data/dms/config/dovecot-sni.cf:/tmp/docker-mailserver/dovecot-sni.cf:ro
HEADER
for domain in $DOMAINS_OK; do
CERT_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.crt"
KEY_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.key"
cat >> "$DOVECOT_CFG" << EOF
# $domain
local_name mail.$domain {
ssl_cert = <$CERT_PATH
ssl_key = <$KEY_PATH
}
local_name imap.$domain {
ssl_cert = <$CERT_PATH
ssl_key = <$KEY_PATH
}
local_name smtp.$domain {
ssl_cert = <$CERT_PATH
ssl_key = <$KEY_PATH
}
local_name pop.$domain {
ssl_cert = <$CERT_PATH
ssl_key = <$KEY_PATH
}
EOF
done
echo " ✅ Dovecot SNI: $(echo $DOMAINS_OK | wc -w) Domain(s)"
# ================================================================
# POSTFIX SNI Konfiguration
# ================================================================
POSTFIX_CFG="$CONFIG_DIR/postfix-main.cf"
echo ""
echo "📝 Generiere: $POSTFIX_CFG"
# Backup falls vorhanden
if [ -f "$POSTFIX_CFG" ]; then
cp "$POSTFIX_CFG" "${POSTFIX_CFG}.bak.$(date +%Y%m%d%H%M%S)"
echo " Backup: ${POSTFIX_CFG}.bak.*"
fi
# smtpd_tls_chain_files aufbauen: Key + Cert Paar pro Domain
# Postfix wählt automatisch per SNI das passende Paar
CHAIN_LINES=""
for domain in $DOMAINS_OK; do
KEY_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.key"
CERT_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.crt"
if [ -z "$CHAIN_LINES" ]; then
CHAIN_LINES=" $KEY_PATH, $CERT_PATH"
else
CHAIN_LINES="$CHAIN_LINES,\n $KEY_PATH, $CERT_PATH"
fi
done
cat > "$POSTFIX_CFG" << POSTFIX_EOF
# postfix-main.cf - Automatisch generiert von setup-dms-tls.sh
# Postfix SNI-Konfiguration: pro Kundendomain ein Key/Cert-Paar.
# Postfix wählt beim TLS-Handshake das passende Paar per SNI.
# DMS lädt dieses File automatisch beim Start.
# TLS Chain: Key + Cert Paare (Postfix >= 3.4)
smtpd_tls_chain_files =
$(printf '%b' "$CHAIN_LINES")
POSTFIX_EOF
echo " ✅ Postfix SNI: $(echo $DOMAINS_OK | wc -w) Domain(s)"
# ================================================================
# Zusammenfassung
# ================================================================
echo ""
echo "============================================================"
echo "✅ Konfigurationen generiert."
echo ""
echo "📋 Nächste Schritte:"
echo ""
echo "1. DMS neu starten:"
echo " docker compose restart mailserver"
echo ""
echo "2. TLS testen (SNI):"
for domain in $DOMAINS_OK; do
echo " openssl s_client -connect mail.$domain:993 -servername mail.$domain 2>/dev/null | grep 'subject\|issuer'"
done
echo ""
echo "3. Bei neuen Domains:"
echo " a) Accounts anlegen: ./manage_mail_user.sh add user@newdomain.com PW"
echo " b) Im Caddy-Dir: ./update-caddy-certs.sh && docker exec caddy caddy reload ..."
echo " c) Warten bis Cert generiert (~30s)"
echo " d) Dieses Script erneut ausführen"
echo " e) docker compose restart mailserver"
echo "============================================================"

View File

@ -1,8 +1,7 @@
#!/bin/bash
# cloudflareMigrationDns.sh
# Setzt DNS Records für Amazon SES Migration + Cloudflare
# Unterstützt: DKIM, SPF (Merge), DMARC, MX, Autodiscover
# NEU: Setzt mail/imap/smtp/pop Subdomains für domain-spezifischen Mailserver-Zugang
# Unterstützt: DKIM, SPF (Merge), DMARC, MX (Safety Check), Autodiscover
set -e
@ -10,13 +9,8 @@ set -e
AWS_REGION=${AWS_REGION:-"us-east-2"}
DRY_RUN=${DRY_RUN:-"false"}
# IP des Mailservers - PFLICHT wenn keine CNAME-Kette gewünscht
# export MAIL_SERVER_IP="1.2.3.4"
MAIL_SERVER_IP=${MAIL_SERVER_IP:-""}
# Ziel-Server für Mailclients. Standard: mail.<kundendomain>
# Wenn MAIL_SERVER_IP gesetzt ist, bekommt mail.<domain> einen A-Record
# und imap/smtp/pop/webmail zeigen per CNAME auf mail.<domain>
# Ziel für Autodiscover/IMAP (wohin sollen Mail-Clients verbinden?)
# Standard: mail.deinedomain.tld. Kann überschrieben werden.
TARGET_MAIL_SERVER=${TARGET_MAIL_SERVER:-"mail.${DOMAIN_NAME}"}
# --- CHECKS ---
@ -25,18 +19,9 @@ if [ -z "$CF_API_TOKEN" ]; then echo "❌ Fehler: CF_API_TOKEN fehlt."; exit 1;
if ! command -v jq &> /dev/null; then echo "❌ Fehler: 'jq' fehlt."; exit 1; fi
if ! command -v aws &> /dev/null; then echo "❌ Fehler: 'aws' CLI fehlt."; exit 1; fi
if [ -z "$MAIL_SERVER_IP" ] && [ "$TARGET_MAIL_SERVER" == "mail.$DOMAIN_NAME" ]; then
echo "⚠️ WARNUNG: MAIL_SERVER_IP ist nicht gesetzt!"
echo " mail.$DOMAIN_NAME braucht einen A-Record."
echo " Bitte setzen: export MAIL_SERVER_IP=<IP deines Servers>"
exit 1
fi
echo "============================================================"
echo " 🛡️ DNS Migration Setup für: $DOMAIN_NAME"
echo " 🌍 Region: $AWS_REGION"
echo " 📬 Mail-Server Target: $TARGET_MAIL_SERVER"
[ -n "$MAIL_SERVER_IP" ] && echo " 🖥️ Server IP: $MAIL_SERVER_IP"
[ "$DRY_RUN" = "true" ] && echo " ⚠️ DRY RUN MODE - Keine Änderungen!"
echo "============================================================"
@ -53,31 +38,41 @@ echo " ✅ Zone ID: $ZONE_ID"
# ------------------------------------------------------------------
# FUNKTION: ensure_record
# Prüft Existenz -> Create oder Update (je nach Typ)
# ------------------------------------------------------------------
ensure_record() {
local type=$1
local name=$2
local content=$3
local proxied=${4:-false}
local priority=$5
local priority=$5 # Optional für MX
echo " ⚙️ Prüfe $type $name..."
# Bestehenden Record suchen
# Hinweis: Wir suchen exakt nach Name und Typ
local search_res=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=$type&name=$name" \
-H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json")
local rec_id=$(echo "$search_res" | jq -r '.result[0].id')
local rec_content=$(echo "$search_res" | jq -r '.result[0].content')
# JSON Body bauen
if [ "$type" == "MX" ]; then
json_data=$(jq -n --arg t "$type" --arg n "$name" --arg c "$content" --argjson p "$proxied" --argjson prio "$priority" \
'{type: $t, name: $n, content: $c, ttl: 3600, proxied: $p, priority: $prio}')
elif [ "$type" == "TXT" ]; then
# Bei TXT Quotes escapen falls nötig, aber jq macht das meist gut
json_data=$(jq -n --arg t "$type" --arg n "$name" --arg c "$content" --argjson p "$proxied" \
'{type: $t, name: $n, content: $c, ttl: 3600, proxied: $p}')
else
json_data=$(jq -n --arg t "$type" --arg n "$name" --arg c "$content" --argjson p "$proxied" \
'{type: $t, name: $n, content: $c, ttl: 3600, proxied: $p}')
fi
# LOGIK
if [ "$rec_id" == "null" ]; then
# --- CREATE ---
if [ "$DRY_RUN" = "true" ]; then
echo " [DRY] Würde ERSTELLEN: $content"
else
@ -86,27 +81,34 @@ ensure_record() {
if [ "$(echo $res | jq -r .success)" == "true" ]; then
echo " ✅ Erstellt."
else
echo " ❌ Fehler: $(echo $res | jq -r .errors[0].message)"
echo " ❌ Fehler beim Erstellen: $(echo $res | jq -r .errors[0].message)"
fi
fi
else
# --- EXISTS ---
if [ "$rec_content" == "$content" ]; then
echo " 🆗 Identisch. Überspringe."
echo " 🆗 Identisch vorhanden. Überspringe."
else
# Inhalt anders -> Update oder Error?
if [ "$type" == "MX" ] && [ "$name" == "$DOMAIN_NAME" ]; then
echo " ⛔ MX existiert aber anders! Gefunden: $rec_content / Erwartet: $content"
echo " Bitte Record ID $rec_id manuell löschen."
echo " ⛔ MX Record existiert aber ist anders!"
echo " Gefunden: $rec_content"
echo " Erwartet: $content"
echo " ABBRUCH: Bitte alten MX Record ID $rec_id manuell löschen."
# Wir brechen hier nicht das ganze Script ab, aber setzen den neuen nicht.
return
fi
# Für TXT (SPF/DMARC) oder CNAME machen wir ein UPDATE (Overwrite)
if [ "$DRY_RUN" = "true" ]; then
echo " [DRY] Würde UPDATEN: '$rec_content' → '$content'"
echo " [DRY] Würde UPDATEN von '$rec_content' auf '$content'"
else
res=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$rec_id" \
-H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" --data "$json_data")
if [ "$(echo $res | jq -r .success)" == "true" ]; then
echo " 🔄 Aktualisiert."
else
echo " ❌ Fehler: $(echo $res | jq -r .errors[0].message)"
echo " ❌ Fehler beim Update: $(echo $res | jq -r .errors[0].message)"
fi
fi
fi
@ -118,36 +120,37 @@ ensure_record() {
# ------------------------------------------------------------------
echo ""
echo "--- 1. MAIL FROM Domain ---"
# Wenn von außen nicht gesetzt, versuche via AWS
if [ -z "$MAIL_FROM_DOMAIN" ]; then
echo " Variable MAIL_FROM_DOMAIN leer, frage AWS SES..."
SES_JSON=$(aws sesv2 get-email-identity --email-identity $DOMAIN_NAME --region $AWS_REGION 2>/dev/null)
MAIL_FROM_DOMAIN=$(echo "$SES_JSON" | jq -r '.MailFromAttributes.MailFromDomain')
if [ "$MAIL_FROM_DOMAIN" == "null" ] || [ -z "$MAIL_FROM_DOMAIN" ]; then
MAIL_FROM_DOMAIN="mail.$DOMAIN_NAME"
echo " ⚠️ Kein MAIL FROM in SES. Fallback: $MAIL_FROM_DOMAIN"
echo " ⚠️ Keine MAIL FROM in SES gefunden. Fallback auf: $MAIL_FROM_DOMAIN"
fi
else
echo " Nutze: $MAIL_FROM_DOMAIN"
echo " Nutze vorgegebene MAIL FROM: $MAIL_FROM_DOMAIN"
fi
# ------------------------------------------------------------------
# SCHRITT 2: DKIM Records
# SCHRITT 2: DKIM Records (CNAME)
# ------------------------------------------------------------------
echo ""
echo "--- 2. DKIM Records ---"
TOKENS=$(aws ses get-identity-dkim-attributes --identities $DOMAIN_NAME --region $AWS_REGION \
--query "DkimAttributes.\"$DOMAIN_NAME\".DkimTokens" --output text)
TOKENS=$(aws ses get-identity-dkim-attributes --identities $DOMAIN_NAME --region $AWS_REGION --query "DkimAttributes.\"$DOMAIN_NAME\".DkimTokens" --output text)
for token in $TOKENS; do
ensure_record "CNAME" "${token}._domainkey.$DOMAIN_NAME" "${token}.dkim.amazonses.com" false
done
# ------------------------------------------------------------------
# SCHRITT 3: SES Verification
# SCHRITT 3: SES Verification (_amazonses)
# ------------------------------------------------------------------
echo ""
echo "--- 3. SES Verification TXT ---"
VERIF_TOKEN=$(aws ses get-identity-verification-attributes --identities $DOMAIN_NAME \
--region $AWS_REGION --query "VerificationAttributes.\"$DOMAIN_NAME\".VerificationToken" --output text)
if [ "$VERIF_TOKEN" != "None" ] && [ -n "$VERIF_TOKEN" ]; then
VERIF_TOKEN=$(aws ses get-identity-verification-attributes --identities $DOMAIN_NAME --region $AWS_REGION --query "VerificationAttributes.\"$DOMAIN_NAME\".VerificationToken" --output text)
if [ "$VERIF_TOKEN" != "None" ]; then
ensure_record "TXT" "_amazonses.$DOMAIN_NAME" "$VERIF_TOKEN" false
fi
@ -156,27 +159,47 @@ fi
# ------------------------------------------------------------------
echo ""
echo "--- 4. MAIL FROM Subdomain ($MAIL_FROM_DOMAIN) ---"
# MX für die Subdomain (feedback loop)
ensure_record "MX" "$MAIL_FROM_DOMAIN" "feedback-smtp.$AWS_REGION.amazonses.com" false 10
# SPF für die Subdomain (strikte SES Regel)
ensure_record "TXT" "$MAIL_FROM_DOMAIN" "v=spf1 include:amazonses.com ~all" false
# ------------------------------------------------------------------
# SCHRITT 5: Root Domain SPF
# SCHRITT 5: Root Domain SPF (Merge Logic)
# ------------------------------------------------------------------
echo ""
echo "--- 5. Root Domain SPF ---"
if [ -n "$OLD_PROVIDER_SPF" ]; then
# Merge: SES + Alter Provider
FINAL_SPF="v=spf1 include:amazonses.com $OLD_PROVIDER_SPF ~all"
echo " Modus: Migration (SES + Alt)"
else
# Nur SES
FINAL_SPF="v=spf1 include:amazonses.com ~all"
echo " Modus: SES only"
fi
ensure_record "TXT" "$DOMAIN_NAME" "$FINAL_SPF" false
# ------------------------------------------------------------------
# SCHRITT 6: Root Domain MX
# SCHRITT 6: Root Domain MX (Safety First)
# ------------------------------------------------------------------
echo ""
echo "--- 6. Root Domain MX ---"
ensure_record "MX" "$DOMAIN_NAME" "mail.$DOMAIN_NAME" false 10
# Hier wollen wir den Inbound SMTP von AWS (falls man AWS WorkMail nutzt oder DMS via AWS ingress)
# WARTE: Du nutzt DMS. Dein DMS hat vermutlich eine eigene IP/Hostname (z.B. mail.buddelectric.net).
# Wenn du SES NUR ZUM SENDEN nutzt, darfst du den Root MX NICHT auf Amazon ändern!
#
# Annahme: Du willst den MX für den Empfang setzen.
# Da du oben "feedback-smtp" erwähnt hast, geht es wohl um den SES Return-Path.
# Aber der "echte MX" für die Domain ($DOMAIN_NAME) zeigt auf DEINEN Mailserver (DMS).
#
# Falls du den MX auf deinen DMS Server zeigen lassen willst:
TARGET_MX=${TARGET_MX:-"mail.$DOMAIN_NAME"}
echo " Ziel-MX ist: $TARGET_MX"
# HINWEIS: MX Records brauchen oft einen Hostnamen, keine IP.
# Wir prüfen, ob ein MX existiert.
ensure_record "MX" "$DOMAIN_NAME" "$TARGET_MX" false 10
# ------------------------------------------------------------------
# SCHRITT 7: DMARC
@ -186,51 +209,34 @@ echo "--- 7. DMARC ---"
ensure_record "TXT" "_dmarc.$DOMAIN_NAME" "v=DMARC1; p=none; rua=mailto:postmaster@$DOMAIN_NAME" false
# ------------------------------------------------------------------
# SCHRITT 8 (NEU): Mailclient Subdomains
# SCHRITT 8: Autodiscover / Autoconfig
# ------------------------------------------------------------------
echo ""
echo "--- 8. Mailclient Subdomains (A + CNAME) ---"
echo "--- 8. Autodiscover / Autoconfig ---"
# Ziel ist meist der IMAP/SMTP Server
echo " Ziel für Clients: $TARGET_MAIL_SERVER"
if [ -n "$MAIL_SERVER_IP" ]; then
# A-Record für mail.<domain> direkt auf Server-IP
ensure_record "A" "mail.$DOMAIN_NAME" "$MAIL_SERVER_IP" false
else
# CNAME auf externen Ziel-Host (nur wenn verschieden)
if [ "$TARGET_MAIL_SERVER" != "mail.$DOMAIN_NAME" ]; then
ensure_record "CNAME" "mail.$DOMAIN_NAME" "$TARGET_MAIL_SERVER" false
fi
fi
ensure_record "CNAME" "autodiscover.$DOMAIN_NAME" "$TARGET_MAIL_SERVER" false
ensure_record "CNAME" "autoconfig.$DOMAIN_NAME" "$TARGET_MAIL_SERVER" false
# imap, smtp, pop, webmail → CNAME auf mail.<domain>
ensure_record "CNAME" "imap.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false
ensure_record "CNAME" "smtp.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false
ensure_record "CNAME" "pop.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false
ensure_record "CNAME" "webmail.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false
# Füge das zu deinem Skript hinzu (Schritt 9 optional):
# ------------------------------------------------------------------
# SCHRITT 9: Autodiscover / Autoconfig
# SCHRITT 9: SRV Records (Service Discovery)
# ------------------------------------------------------------------
echo ""
echo "--- 9. Autodiscover / Autoconfig ---"
ensure_record "CNAME" "autodiscover.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false
ensure_record "CNAME" "autoconfig.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false
echo "--- 9. SRV Records (Service Discovery) ---"
# Das hilft Outlook, direkt "email-srvr.com" zu nutzen statt "mail.domain.tld"
# Format: _service._proto.name TTL class SRV priority weight port target
# ------------------------------------------------------------------
# SCHRITT 10: SRV Records
# ------------------------------------------------------------------
echo ""
echo "--- 10. SRV Records ---"
ensure_record "SRV" "_imap._tcp.$DOMAIN_NAME" "0 5 143 mail.$DOMAIN_NAME" false
ensure_record "SRV" "_imaps._tcp.$DOMAIN_NAME" "0 5 993 mail.$DOMAIN_NAME" false
ensure_record "SRV" "_submission._tcp.$DOMAIN_NAME" "0 5 587 mail.$DOMAIN_NAME" false
# IMAP SRV
ensure_record "SRV" "_imap._tcp.$DOMAIN_NAME" "0 5 143 $TARGET_MAIL_SERVER" false
# IMAPS SRV (Port 993)
ensure_record "SRV" "_imaps._tcp.$DOMAIN_NAME" "0 5 993 $TARGET_MAIL_SERVER" false
# SUBMISSION SRV (Port 587)
ensure_record "SRV" "_submission._tcp.$DOMAIN_NAME" "0 5 587 $TARGET_MAIL_SERVER" false
echo " ✅ SRV Records gesetzt (Server: $TARGET_MAIL_SERVER)"
echo ""
echo "============================================================"
echo "✅ Fertig für Domain: $DOMAIN_NAME"
echo ""
echo " Mailclient-Konfiguration für Kunden:"
echo " IMAP: imap.$DOMAIN_NAME Port 993 (SSL)"
echo " SMTP: smtp.$DOMAIN_NAME Port 587 (STARTTLS) oder 465 (SSL)"
echo " POP3: pop.$DOMAIN_NAME Port 995 (SSL)"
echo " Webmail: webmail.$DOMAIN_NAME"
echo "============================================================"
echo "✅ Fertig."

View File

@ -1,21 +0,0 @@
{
email {env.CLOUDFLARE_EMAIL}
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
acme_ca https://acme-v02.api.letsencrypt.org/directory
debug
}
import email_autodiscover
# ---------------------------------------------------------
# Block A: Die dedizierten Autodiscover Domains
# ---------------------------------------------------------
autodiscover.bayarea-cc.com, autodiscover.bizmatch.net,
autodiscover.ruehrgedoens.de, autoconfig.ruehrgedoens.de,
autoconfig.bayarea-cc.com, autoconfig.bizmatch.net {
# Hier rufen wir das Snippet auf
import email_settings
# Fallback für Aufrufe auf Root dieser Subdomains
respond "Autodiscover Service Online" 200
}

View File

@ -1,13 +0,0 @@
# Dockerfile.caddy
ARG CADDY_VERSION=2.9.1
FROM caddy:${CADDY_VERSION}-builder AS builder
# Caddy in exakt dieser Version + Plugins bauen
RUN xcaddy build ${CADDY_VERSION} \
--with github.com/caddy-dns/cloudflare \
--with github.com/caddyserver/replace-response
FROM caddy:${CADDY_VERSION}
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
RUN mkdir -p /var/log/caddy

View File

@ -1,44 +0,0 @@
services:
caddy:
image: custom-caddy:2.9.1-rr1
container_name: caddy
build:
context: .
dockerfile: Dockerfile.caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
extra_hosts:
- 'host.docker.internal:host-gateway'
networks:
- bizmatch
- keycloak
- gitea
- mail_network
volumes:
- $PWD/Caddyfile:/etc/caddy/Caddyfile
- $PWD/email_autodiscover:/etc/caddy/email_autodiscover
- $PWD/email.mobileconfig.tpl:/etc/caddy/email.mobileconfig.tpl
- $PWD/email-setup:/var/www/email-setup
- caddy_data:/data
- caddy_config:/config
- /home/aknuth/log/caddy:/var/log/caddy
environment:
- CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}
- CLOUDFLARE_EMAIL=${CLOUDFLARE_EMAIL}
networks:
bizmatch:
external: true
keycloak:
external: true
gitea:
external: true
mail_network:
external: true
volumes:
caddy_data:
external: true
caddy_config:

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
<Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
<Account>
<AccountType>email</AccountType>
<Action>settings</Action>
<Protocol>
<Type>IMAP</Type>
<Server>mail.email-srvr.com</Server>
<Port>993</Port>
<DomainRequired>off</DomainRequired>
<LoginName></LoginName>
<SPA>off</SPA>
<SSL>on</SSL>
<AuthRequired>on</AuthRequired>
</Protocol>
<Protocol>
<Type>SMTP</Type>
<Server>mail.email-srvr.com</Server>
<Port>465</Port>
<DomainRequired>off</DomainRequired>
<LoginName></LoginName>
<SPA>off</SPA>
<SSL>on</SSL>
<AuthRequired>on</AuthRequired>
</Protocol>
</Account>
</Response>
</Autodiscover>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,122 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email Setup</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: #f2f2f7; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; padding: 20px; box-sizing: border-box; }
.card { background: white; padding: 2.5rem; border-radius: 24px; box-shadow: 0 12px 30px rgba(0,0,0,0.1); width: 100%; max-width: 420px; text-align: center; transition: all 0.3s ease; }
.logo { width: 80px; height: 80px; margin-bottom: 1.5rem; }
h1 { margin: 0 0 1rem 0; color: #1a1a1a; font-size: 1.8rem; }
p { color: #666; line-height: 1.5; margin-bottom: 2rem; }
/* Input Section */
#input-section { transition: opacity 0.3s ease; }
input { width: 100%; padding: 16px; margin-bottom: 16px; border: 2px solid #eee; border-radius: 14px; font-size: 16px; box-sizing: border-box; transition: border-color 0.2s; outline: none; }
input:focus { border-color: #007AFF; }
button { width: 100%; padding: 16px; background: #007AFF; color: white; border: none; border-radius: 14px; font-size: 18px; font-weight: 600; cursor: pointer; transition: background 0.2s, transform 0.1s; }
button:hover { background: #0062cc; }
button:active { transform: scale(0.98); }
/* QR Section (initially hidden) */
#qr-section { display: none; opacity: 0; transition: opacity 0.5s ease; }
#qrcode { margin: 2rem auto; padding: 15px; background: white; border-radius: 16px; box-shadow: 0 4px 12px rgba(0,0,0,0.08); display: inline-block; }
#qrcode img { margin: auto; } /* Centers the generated QR code */
.hint { font-size: 0.9rem; color: #888; margin-top: 1.5rem; }
.hint strong { color: #333; }
.error { color: #d32f2f; background: #fde8e8; padding: 10px; border-radius: 8px; font-size: 0.9rem; display: none; margin-bottom: 16px; }
.back-btn { background: transparent; color: #007AFF; margin-top: 1rem; font-size: 16px; }
.back-btn:hover { background: #f0f8ff; }
</style>
</head>
<body>
<div class="card">
<img src="/logo.png" alt="Logo" class="logo">
<div id="input-section">
<h1>Email Setup</h1>
<p>Enter your email address to automatically configure your iPhone or iPad.</p>
<div id="error-msg" class="error">Please enter a valid email address.</div>
<input type="email" id="email" placeholder="name@company.com" required autocomplete="email">
<button onclick="generateQR()">Generate QR Code</button>
</div>
<div id="qr-section">
<h1>Scan me!</h1>
<p>Open the <strong>Camera app</strong> on your iPhone and point it at this code.</p>
<div id="qrcode"></div>
<p class="hint">
Tap the banner that appears at the top.<br>
Click <strong>"Allow"</strong> and then go to <strong>Settings</strong> to install the profile.
</p>
<button class="back-btn" onclick="resetForm()">Back</button>
</div>
</div>
<script>
const inputSection = document.getElementById('input-section');
const qrSection = document.getElementById('qr-section');
const emailInput = document.getElementById('email');
const errorMsg = document.getElementById('error-msg');
let qrcode = null;
function generateQR() {
const email = emailInput.value.trim();
if (!email || !email.includes('@') || email.split('@')[1].length < 3) {
errorMsg.style.display = 'block';
emailInput.focus();
return;
}
errorMsg.style.display = 'none';
const domain = email.split('@')[1];
// The magic link
const targetUrl = `https://autodiscover.${domain}/apple?email=${email}`;
// Hide input, show QR
inputSection.style.display = 'none';
qrSection.style.display = 'block';
setTimeout(() => qrSection.style.opacity = '1', 50);
// Generate (or update) QR Code
if (qrcode === null) {
qrcode = new QRCode(document.getElementById("qrcode"), {
text: targetUrl,
width: 200,
height: 200,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRCode.CorrectLevel.H
});
} else {
qrcode.clear();
qrcode.makeCode(targetUrl);
}
}
function resetForm() {
qrSection.style.opacity = '0';
setTimeout(() => {
qrSection.style.display = 'none';
inputSection.style.display = 'block';
emailInput.value = '';
emailInput.focus();
}, 300);
}
emailInput.addEventListener("keypress", function(event) {
if (event.key === "Enter") generateQR();
});
</script>
</body>
</html>

View File

@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadContent</key>
<array>
<dict>
<key>EmailAccountDescription</key>
<string>{{.Req.URL.Query.Get "email"}}</string>
<key>EmailAccountName</key>
<string>{{.Req.URL.Query.Get "email"}}</string>
<key>EmailAccountType</key>
<string>EmailTypeIMAP</string>
<key>EmailAddress</key>
<string>{{.Req.URL.Query.Get "email"}}</string>
<key>IncomingMailServerAuthentication</key>
<string>EmailAuthPassword</string>
<key>IncomingMailServerHostName</key>
<string>mail.email-srvr.com</string>
<key>IncomingMailServerPortNumber</key>
<integer>993</integer>
<key>IncomingMailServerUseSSL</key>
<true/>
<key>IncomingMailServerUsername</key>
<string>{{.Req.URL.Query.Get "email"}}</string>
<key>OutgoingMailServerAuthentication</key>
<string>EmailAuthPassword</string>
<key>OutgoingMailServerHostName</key>
<string>mail.email-srvr.com</string>
<key>OutgoingMailServerPortNumber</key>
<integer>465</integer>
<key>OutgoingMailServerUseSSL</key>
<true/>
<key>OutgoingMailServerUsername</key>
<string>{{.Req.URL.Query.Get "email"}}</string>
<key>PayloadDescription</key>
<string>E-Mail Konfiguration für {{.Req.URL.Query.Get "email"}}</string>
<key>PayloadDisplayName</key>
<string>{{.Req.URL.Query.Get "email"}}</string>
<key>PayloadIdentifier</key>
<string>com.email-srvr.profile.{{.Req.URL.Query.Get "email"}}</string>
<key>PayloadType</key>
<string>com.apple.mail.managed</string>
<key>PayloadUUID</key>
<string>{{uuidv4}}</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</array>
<key>PayloadDescription</key>
<string>Automatische E-Mail Einrichtung für {{.Req.URL.Query.Get "email"}}</string>
<key>PayloadDisplayName</key>
<string>E-Mail Einstellungen</string>
<key>PayloadIdentifier</key>
<string>com.email-srvr.profile.root</string>
<key>PayloadOrganization</key>
<string>IT Support</string>
<key>PayloadRemovalDisallowed</key>
<false/>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>{{uuidv4}}</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>

View File

@ -1,97 +0,0 @@
(email_settings) {
# 1. Autodiscover für Outlook
route /autodiscover/autodiscover.xml {
header Content-Type "application/xml"
# Wir nutzen {header.X-Anchormailbox} um die Email dynamisch einzufügen
respond `<?xml version="1.0" encoding="utf-8"?>
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
<Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
<Account>
<AccountType>email</AccountType>
<Action>settings</Action>
<Protocol>
<Type>IMAP</Type>
<Server>mail.email-srvr.com</Server>
<Port>993</Port>
<DomainRequired>on</DomainRequired>
<LoginName>{header.X-Anchormailbox}</LoginName>
<SPA>off</SPA>
<SSL>on</SSL>
<AuthRequired>on</AuthRequired>
</Protocol>
<Protocol>
<Type>POP3</Type>
<Server>mail.email-srvr.com</Server>
<Port>995</Port>
<DomainRequired>on</DomainRequired>
<LoginName>{header.X-Anchormailbox}</LoginName>
<SPA>off</SPA>
<SSL>on</SSL>
<AuthRequired>on</AuthRequired>
</Protocol>
<Protocol>
<Type>SMTP</Type>
<Server>mail.email-srvr.com</Server>
<Port>465</Port>
<DomainRequired>on</DomainRequired>
<LoginName>{header.X-Anchormailbox}</LoginName>
<SPA>off</SPA>
<SSL>on</SSL>
<AuthRequired>on</AuthRequired>
</Protocol>
</Account>
</Response>
</Autodiscover>` 200
}
# 2. JSON Autodiscover (Modern Outlook) - bleibt gleich
route /autodiscover/autodiscover.json {
header Content-Type "application/json"
respond `{
"Protocol": "AutodiscoverV1",
"Url": "https://autodiscover.bayarea-cc.com/autodiscover/autodiscover.xml"
}` 200
}
# 3. Thunderbird Autoconfig - bleibt gleich (dort funktioniert %EMAILADDRESS% ja nativ)
route /mail/config-v1.1.xml {
header Content-Type "application/xml"
respond `<?xml version="1.0"?>
<clientConfig version="1.1">
<emailProvider id="email-srvr.com">
<displayName>Rackspace Email</displayName>
<incomingServer type="imap">
<hostname>mail.email-srvr.com</hostname>
<port>993</port>
<socketType>SSL</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</incomingServer>
<outgoingServer type="smtp">
<hostname>mail.email-srvr.com</hostname>
<port>465</port>
<socketType>SSL</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</outgoingServer>
</emailProvider>
</clientConfig>` 200
}
# NEU: Apple MobileConfig Route
# Aufrufbar über: /apple?email=kunde@domain.de
route /apple {
# KORREKTUR: Wir müssen Caddy sagen, dass er diesen MIME-Type bearbeiten soll!
templates {
mime "application/x-apple-aspen-config"
}
# Den richtigen MIME-Type setzen
header Content-Type "application/x-apple-aspen-config; charset=utf-8"
# Pfad zur Datei im Container
root * /etc/caddy
rewrite * /email.mobileconfig.tpl
file_server
}
}

View File

@ -1,132 +0,0 @@
#!/bin/bash
# update-caddy-certs.sh
# Gehört ins Caddy-Verzeichnis (neben dem Caddyfile).
#
# Liest alle Domains aus dem DMS und generiert die Wildcard-Cert-Blöcke
# für Caddy in die Datei "mail_certs" (per "import mail_certs" im Caddyfile).
#
# Bei neuen Domains: Script erneut laufen lassen + caddy reload.
#
# Usage:
# ./update-caddy-certs.sh
# DRY_RUN=true ./update-caddy-certs.sh
# DMS_CONTAINER=mailserver CADDY_CONTAINER=caddy ./update-caddy-certs.sh
set -e
DMS_CONTAINER=${DMS_CONTAINER:-"mailserver"}
CADDY_CONTAINER=${CADDY_CONTAINER:-"caddy"}
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
OUTPUT_FILE="$SCRIPT_DIR/mail_certs"
DRY_RUN=${DRY_RUN:-"false"}
# Node-Hostname des Mailservers (für Default-Cert Block)
# Wird immer mit eingetragen, auch wenn keine DMS-Accounts existieren.
NODE_HOSTNAME=${NODE_HOSTNAME:-"node1.email-srvr.com"}
echo "============================================================"
echo " 📜 Caddy Wildcard-Cert Konfig Generator"
echo " DMS Container: $DMS_CONTAINER"
echo " Caddy Container: $CADDY_CONTAINER"
echo " Output: $OUTPUT_FILE"
echo " Node Hostname: $NODE_HOSTNAME"
[ "$DRY_RUN" = "true" ] && echo " ⚠️ DRY RUN - Keine Dateien werden geschrieben"
echo "============================================================"
# --- Domains aus DMS lesen ---
echo ""
echo "📋 Lese Domains aus DMS..."
DOMAINS=$(docker exec "$DMS_CONTAINER" setup email list 2>/dev/null \
| grep -oP '(?<=@)[^\s]+' \
| sort -u)
if [ -z "$DOMAINS" ]; then
echo "⚠️ Keine DMS-Accounts gefunden. Nur Node-Hostname wird eingetragen."
fi
if [ -n "$DOMAINS" ]; then
echo " Gefundene Domains:"
for d in $DOMAINS; do echo " - $d"; done
fi
# --- Konfig generieren ---
echo ""
echo "📝 Generiere Caddy-Konfiguration..."
OUTPUT=""
OUTPUT="${OUTPUT}# mail_certs - Automatisch generiert von update-caddy-certs.sh\n"
OUTPUT="${OUTPUT}# Wildcard-Zertifikate für DMS-Domains + Node-Hostname.\n"
OUTPUT="${OUTPUT}# Einbinden im Caddyfile: import mail_certs\n"
OUTPUT="${OUTPUT}# Generiert: $(date)\n"
OUTPUT="${OUTPUT}\n"
# Node-Hostname immer als erstes (Default-Cert des DMS)
echo " → Node-Hostname Block: $NODE_HOSTNAME"
OUTPUT="${OUTPUT}# Node-Hostname (Default-Cert für DMS Fallback)\n"
OUTPUT="${OUTPUT}${NODE_HOSTNAME} {\n"
OUTPUT="${OUTPUT} tls {\n"
OUTPUT="${OUTPUT} dns cloudflare {env.CLOUDFLARE_API_TOKEN}\n"
OUTPUT="${OUTPUT} }\n"
OUTPUT="${OUTPUT} respond \"OK\" 200\n"
OUTPUT="${OUTPUT}}\n\n"
# Wildcard-Blocks pro Kundendomain
for domain in $DOMAINS; do
echo " → Wildcard Block: *.${domain}"
OUTPUT="${OUTPUT}# Wildcard-Cert für $domain\n"
OUTPUT="${OUTPUT}*.${domain}, ${domain} {\n"
OUTPUT="${OUTPUT} tls {\n"
OUTPUT="${OUTPUT} dns cloudflare {env.CLOUDFLARE_API_TOKEN}\n"
OUTPUT="${OUTPUT} }\n"
OUTPUT="${OUTPUT} respond \"OK\" 200\n"
OUTPUT="${OUTPUT}}\n\n"
done
# --- Ausgabe ---
if [ "$DRY_RUN" = "true" ]; then
echo ""
echo "--- VORSCHAU ---"
printf '%b' "$OUTPUT"
echo "--- ENDE ---"
else
printf '%b' "$OUTPUT" > "$OUTPUT_FILE"
echo " ✅ Geschrieben: $OUTPUT_FILE"
fi
# --- Import im Caddyfile prüfen ---
CADDYFILE="$SCRIPT_DIR/Caddyfile"
if [ -f "$CADDYFILE" ]; then
if grep -q "import mail_certs" "$CADDYFILE"; then
echo " ✅ 'import mail_certs' bereits im Caddyfile vorhanden."
else
echo ""
echo "⚠️ AKTION: 'import mail_certs' fehlt noch im Caddyfile!"
echo " Bitte nach dem globalen {} Block eintragen:"
echo ""
echo " { ← globaler Block"
echo " email {env.CLOUDFLARE_EMAIL}"
echo " ..."
echo " }"
echo " import mail_certs ← hier einfügen"
echo " import email_autodiscover"
echo " ..."
fi
fi
echo ""
echo "============================================================"
echo "🔄 Nächste Schritte:"
echo ""
echo "1. Caddy Konfiguration validieren:"
echo " docker exec $CADDY_CONTAINER caddy validate --config /etc/caddy/Caddyfile"
echo ""
echo "2. Caddy neu laden (kein Downtime):"
echo " docker exec $CADDY_CONTAINER caddy reload --config /etc/caddy/Caddyfile"
echo ""
echo "3. Cert-Generierung verfolgen (~30s pro Domain):"
echo " docker logs -f $CADDY_CONTAINER 2>&1 | grep -i 'certificate\|acme\|tls\|error'"
echo ""
echo "4. Cert-Pfade kontrollieren:"
echo " ls /var/lib/docker/volumes/caddy_data/_data/caddy/certificates/"
echo " acme-v02.api.letsencrypt.org-directory/"
echo "============================================================"