#!/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). # # Generiert pro Domain: # - Wildcard-Cert Block (*.domain + domain) # - Webmail Block (reverse_proxy zu Roundcube) # - Autodiscover/Autoconfig Block (importiert email_settings Snippet) # - Email-Setup Block (QR-Code Seite für iPhone) # # 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) 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 + Webmail + Autodiscover für DMS-Domains.\n" OUTPUT="${OUTPUT}# Einbinden im Caddyfile: import mail_certs\n" OUTPUT="${OUTPUT}# Generiert: $(date)\n" OUTPUT="${OUTPUT}\n" # ===================================================================== # Autodiscover/Autoconfig Snippet (dynamisch) # {labels.1}.{labels.0} = Basisdomain aus Hostname # ===================================================================== OUTPUT="${OUTPUT}# ═══════════════════════════════════════════════\n" OUTPUT="${OUTPUT}# Autodiscover/Autoconfig Snippet (dynamisch)\n" OUTPUT="${OUTPUT}# {labels.1}.{labels.0} = Basisdomain aus Hostname\n" OUTPUT="${OUTPUT}# ═══════════════════════════════════════════════\n" OUTPUT="${OUTPUT}(email_settings) {\n" # --- 1. Outlook Classic Autodiscover (POST + GET XML) --- OUTPUT="${OUTPUT} # Outlook Autodiscover (XML) - POST und GET\n" OUTPUT="${OUTPUT} route /autodiscover/autodiscover.xml {\n" OUTPUT="${OUTPUT} header Content-Type \"application/xml\"\n" OUTPUT="${OUTPUT} respond \`\n" OUTPUT="${OUTPUT}\n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} email\n" OUTPUT="${OUTPUT} settings\n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} IMAP\n" OUTPUT="${OUTPUT} imap.{labels.1}.{labels.0}\n" OUTPUT="${OUTPUT} 993\n" OUTPUT="${OUTPUT} off\n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} off\n" OUTPUT="${OUTPUT} on\n" OUTPUT="${OUTPUT} on\n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} SMTP\n" OUTPUT="${OUTPUT} smtp.{labels.1}.{labels.0}\n" OUTPUT="${OUTPUT} 465\n" OUTPUT="${OUTPUT} off\n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} off\n" OUTPUT="${OUTPUT} on\n" OUTPUT="${OUTPUT} on\n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT}\` 200\n" OUTPUT="${OUTPUT} }\n" OUTPUT="${OUTPUT}\n" # --- 2. Outlook New / Microsoft 365 (JSON v2) --- # Outlook New sendet GET auf /autodiscover/autodiscover.json?Protocol=AutodiscoverV1&... # Antwort muss den XML-Endpoint zurückgeben OUTPUT="${OUTPUT} # Outlook New/365 (JSON → Redirect zu XML)\n" OUTPUT="${OUTPUT} route /autodiscover/autodiscover.json {\n" OUTPUT="${OUTPUT} header Content-Type \"application/json\"\n" OUTPUT="${OUTPUT} respond \`{\"Protocol\":\"AutodiscoverV1\",\"Url\":\"https://autodiscover.{labels.1}.{labels.0}/autodiscover/autodiscover.xml\"}\` 200\n" OUTPUT="${OUTPUT} }\n" OUTPUT="${OUTPUT}\n" # --- 3. Thunderbird Autoconfig --- OUTPUT="${OUTPUT} # Thunderbird Autoconfig\n" OUTPUT="${OUTPUT} route /mail/config-v1.1.xml {\n" OUTPUT="${OUTPUT} header Content-Type \"application/xml\"\n" OUTPUT="${OUTPUT} respond \`\n" OUTPUT="${OUTPUT}\n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} {labels.1}.{labels.0} Mail\n" OUTPUT="${OUTPUT} {labels.1}.{labels.0}\n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} imap.{labels.1}.{labels.0}\n" OUTPUT="${OUTPUT} 993\n" OUTPUT="${OUTPUT} SSL\n" OUTPUT="${OUTPUT} password-cleartext\n" OUTPUT="${OUTPUT} %%EMAILADDRESS%%\n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} smtp.{labels.1}.{labels.0}\n" OUTPUT="${OUTPUT} 465\n" OUTPUT="${OUTPUT} SSL\n" OUTPUT="${OUTPUT} password-cleartext\n" OUTPUT="${OUTPUT} %%EMAILADDRESS%%\n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT}\` 200\n" OUTPUT="${OUTPUT} }\n" OUTPUT="${OUTPUT}\n" # --- 4. Apple MobileConfig (inline, wie Autodiscover/Autoconfig) --- OUTPUT="${OUTPUT} # Apple MobileConfig (inline respond)\n" OUTPUT="${OUTPUT} route /apple {\n" OUTPUT="${OUTPUT} header Content-Type \"application/x-apple-aspen-config; charset=utf-8\"\n" OUTPUT="${OUTPUT} header Content-Disposition \"attachment; filename=email.mobileconfig\"\n" OUTPUT="${OUTPUT} respond \`\n" OUTPUT="${OUTPUT}\n" OUTPUT="${OUTPUT}\n" OUTPUT="${OUTPUT}\n" OUTPUT="${OUTPUT} PayloadContent\n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} EmailAccountDescription\n" OUTPUT="${OUTPUT} {query.email}\n" OUTPUT="${OUTPUT} EmailAccountName\n" OUTPUT="${OUTPUT} {query.email}\n" OUTPUT="${OUTPUT} EmailAccountType\n" OUTPUT="${OUTPUT} EmailTypeIMAP\n" OUTPUT="${OUTPUT} EmailAddress\n" OUTPUT="${OUTPUT} {query.email}\n" OUTPUT="${OUTPUT} IncomingMailServerAuthentication\n" OUTPUT="${OUTPUT} EmailAuthPassword\n" OUTPUT="${OUTPUT} IncomingMailServerHostName\n" OUTPUT="${OUTPUT} imap.{labels.1}.{labels.0}\n" OUTPUT="${OUTPUT} IncomingMailServerPortNumber\n" OUTPUT="${OUTPUT} 993\n" OUTPUT="${OUTPUT} IncomingMailServerUseSSL\n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} IncomingMailServerUsername\n" OUTPUT="${OUTPUT} {query.email}\n" OUTPUT="${OUTPUT} OutgoingMailServerAuthentication\n" OUTPUT="${OUTPUT} EmailAuthPassword\n" OUTPUT="${OUTPUT} OutgoingMailServerHostName\n" OUTPUT="${OUTPUT} smtp.{labels.1}.{labels.0}\n" OUTPUT="${OUTPUT} OutgoingMailServerPortNumber\n" OUTPUT="${OUTPUT} 465\n" OUTPUT="${OUTPUT} OutgoingMailServerUseSSL\n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} OutgoingMailServerUsername\n" OUTPUT="${OUTPUT} {query.email}\n" OUTPUT="${OUTPUT} OutgoingPasswordRequired\n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} PayloadDescription\n" OUTPUT="${OUTPUT} E-Mail Konfiguration\n" OUTPUT="${OUTPUT} PayloadDisplayName\n" OUTPUT="${OUTPUT} {labels.1}.{labels.0} E-Mail\n" OUTPUT="${OUTPUT} PayloadIdentifier\n" OUTPUT="${OUTPUT} com.{labels.1}.{labels.0}.email.account\n" OUTPUT="${OUTPUT} PayloadType\n" OUTPUT="${OUTPUT} com.apple.mail.managed\n" OUTPUT="${OUTPUT} PayloadUUID\n" OUTPUT="${OUTPUT} A1B2C3D4-E5F6-7890-ABCD-EF1234567890\n" OUTPUT="${OUTPUT} PayloadVersion\n" OUTPUT="${OUTPUT} 1\n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} PayloadDescription\n" OUTPUT="${OUTPUT} E-Mail Einrichtung\n" OUTPUT="${OUTPUT} PayloadDisplayName\n" OUTPUT="${OUTPUT} {labels.1}.{labels.0} E-Mail\n" OUTPUT="${OUTPUT} PayloadIdentifier\n" OUTPUT="${OUTPUT} com.{labels.1}.{labels.0}.email.profile\n" OUTPUT="${OUTPUT} PayloadOrganization\n" OUTPUT="${OUTPUT} Bay Area Affiliates, Inc.\n" OUTPUT="${OUTPUT} PayloadRemovalDisallowed\n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} PayloadType\n" OUTPUT="${OUTPUT} Configuration\n" OUTPUT="${OUTPUT} PayloadUUID\n" OUTPUT="${OUTPUT} F0E1D2C3-B4A5-6789-0FED-CBA987654321\n" OUTPUT="${OUTPUT} PayloadVersion\n" OUTPUT="${OUTPUT} 1\n" OUTPUT="${OUTPUT}\n" OUTPUT="${OUTPUT}\` 200\n" OUTPUT="${OUTPUT} }\n" # --- 5. Samsung Email (nutzt ebenfalls autoconfig, kein extra Block nötig) --- # Samsung Email-App versucht: # 1. https://autoconfig./mail/config-v1.1.xml (= Thunderbird-Format, schon abgedeckt) # 2. Alternativ: Outlook Autodiscover XML # → Kein separater Block erforderlich. OUTPUT="${OUTPUT}}\n\n" # ===================================================================== # Email-Setup Snippet (QR-Code Seite für iPhone) # ===================================================================== OUTPUT="${OUTPUT}# ═══════════════════════════════════════════════\n" OUTPUT="${OUTPUT}# Email-Setup Snippet (QR-Code Seite)\n" OUTPUT="${OUTPUT}# ═══════════════════════════════════════════════\n" OUTPUT="${OUTPUT}(email_setup_page) {\n" OUTPUT="${OUTPUT} route /email-setup* {\n" OUTPUT="${OUTPUT} uri strip_prefix /email-setup\n" OUTPUT="${OUTPUT} root * /var/www/email-setup\n" OUTPUT="${OUTPUT} try_files {path} /setup.html\n" OUTPUT="${OUTPUT} file_server\n" OUTPUT="${OUTPUT} }\n" OUTPUT="${OUTPUT}}\n\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 + Webmail + Autodiscover pro Kundendomain for domain in $DOMAINS; do echo " → Wildcard Block: *.${domain}" echo " → Webmail Block: webmail.${domain}" echo " → Autodiscover Block: autodiscover.${domain}, autoconfig.${domain}" echo " → Email-Setup Block: webmail.${domain}/email-setup" # Wildcard-Cert Block (für Cert-Generierung + Fallback) OUTPUT="${OUTPUT}# ═══════════════════════════════════════════════\n" OUTPUT="${OUTPUT}# ${domain}\n" OUTPUT="${OUTPUT}# ═══════════════════════════════════════════════\n\n" 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" # Webmail Block (Roundcube + Email-Setup) OUTPUT="${OUTPUT}# Roundcube Webmail + Email-Setup für $domain\n" OUTPUT="${OUTPUT}webmail.${domain} {\n" OUTPUT="${OUTPUT} import email_setup_page\n" OUTPUT="${OUTPUT} reverse_proxy roundcube:80\n" OUTPUT="${OUTPUT} encode gzip\n" OUTPUT="${OUTPUT} log {\n" OUTPUT="${OUTPUT} output stderr\n" OUTPUT="${OUTPUT} format console\n" OUTPUT="${OUTPUT} }\n" OUTPUT="${OUTPUT}}\n\n" # Autodiscover / Autoconfig Block OUTPUT="${OUTPUT}# Autodiscover/Autoconfig für $domain\n" OUTPUT="${OUTPUT}autodiscover.${domain}, autoconfig.${domain} {\n" OUTPUT="${OUTPUT} import email_settings\n" OUTPUT="${OUTPUT} respond \"Autodiscover Service Online\" 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" fi # Prüfe ob alte email_autodiscover Referenz entfernt werden kann if grep -q "import email_autodiscover" "$CADDYFILE"; then echo "" echo "⚠️ AUFRÄUMEN: 'import email_autodiscover' im Caddyfile gefunden!" echo " Das Snippet (email_settings) ist jetzt in mail_certs eingebettet." echo " Bitte 'import email_autodiscover' aus dem Caddyfile entfernen." fi fi # --- Prüfe ob alte Dateien noch existieren --- if [ -f "$SCRIPT_DIR/email_autodiscover" ]; then echo "" echo "⚠️ AUFRÄUMEN: Datei 'email_autodiscover' kann entfernt werden!" echo " Das Snippet ist jetzt in mail_certs eingebettet." fi if [ -f "$SCRIPT_DIR/email-setup/autodiscover.xml" ]; then echo "" echo "⚠️ AUFRÄUMEN: 'email-setup/autodiscover.xml' kann entfernt werden!" echo " Statische XML wird nicht mehr benötigt (dynamisch über Caddy)." 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. Autodiscover testen:" for domain in $DOMAINS; do echo " # Thunderbird:" echo " curl -s https://autoconfig.${domain}/mail/config-v1.1.xml | head -10" echo " # Outlook:" echo " curl -s https://autodiscover.${domain}/autodiscover/autodiscover.xml | head -10" echo " # Apple (sollte .mobileconfig liefern):" echo " curl -sI \"https://autodiscover.${domain}/apple?email=test@${domain}\"" echo "" done echo "5. iPhone Email-Setup QR-Code Seite:" for domain in $DOMAINS; do echo " https://webmail.${domain}/email-setup" done echo "============================================================"