Compare commits
58 Commits
a70ae78a93
...
3ab46f163a
| Author | SHA1 | Date |
|---|---|---|
|
|
3ab46f163a | |
|
|
56c7b51e35 | |
|
|
c826d4c299 | |
|
|
908bb76c3a | |
|
|
41514a7f51 | |
|
|
4324a5785f | |
|
|
f1b2c33996 | |
|
|
726df19a76 | |
|
|
f6601501c0 | |
|
|
22d937ddfd | |
|
|
c56cae16d6 | |
|
|
a090e940f1 | |
|
|
282298c361 | |
|
|
d91152c035 | |
|
|
80596ab347 | |
|
|
7173da31d4 | |
|
|
8995cede7d | |
|
|
a077b38998 | |
|
|
73dd442596 | |
|
|
7920ab07b8 | |
|
|
98c78d8dce | |
|
|
3381fd68c2 | |
|
|
3f91936098 | |
|
|
ee02d505c6 | |
|
|
eea0fcc35d | |
|
|
7bc8cbb9f7 | |
|
|
69fbb670f1 | |
|
|
39e862cdd5 | |
|
|
b2d41e2baa | |
|
|
552dd73f0a | |
|
|
51405a3ec5 | |
|
|
bd3b2db235 | |
|
|
bbc24cbb63 | |
|
|
06e25b33e0 | |
|
|
a5a7096cc7 | |
|
|
c20d471036 | |
|
|
0b0b7ddb82 | |
|
|
42d16063a1 | |
|
|
bf96810d09 | |
|
|
4452dae34c | |
|
|
b1a295df85 | |
|
|
7956d2d6f5 | |
|
|
915b0e59be | |
|
|
b90c8aec9e | |
|
|
dd41497f0b | |
|
|
8f0a899b66 | |
|
|
4ac32f43d0 | |
|
|
a1c7fecc27 | |
|
|
173b3f382f | |
|
|
a84bb23af0 | |
|
|
3e656dacfa | |
|
|
ce26d864b5 | |
|
|
f9723b2b68 | |
|
|
956214f8c9 | |
|
|
aee2335c48 | |
|
|
8808d81113 | |
|
|
ee19b5b659 | |
|
|
b072083318 |
|
|
@ -1 +1,2 @@
|
||||||
.env
|
.env
|
||||||
|
node_modules
|
||||||
|
|
@ -1,23 +1,26 @@
|
||||||
services:
|
services:
|
||||||
|
|
||||||
mailserver:
|
mailserver:
|
||||||
# image: docker.io/mailserver/docker-mailserver:latest # AUSKOMMENTIERT
|
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
image: dms-custom:latest
|
image: dms-custom:latest
|
||||||
container_name: mailserver
|
container_name: mailserver
|
||||||
hostname: mail.email-srvr.com
|
|
||||||
domainname: email-srvr.com
|
# 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
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- "25:25" # SMTP (parallel zu MailCow auf Port 25)
|
- "25:25"
|
||||||
- "587:587" # SMTP Submission
|
- "587:587"
|
||||||
- "465:465" # SMTP SSL
|
- "465:465"
|
||||||
- "143:143" # IMAP
|
- "143:143"
|
||||||
- "993:993" # IMAP SSL
|
- "993:993"
|
||||||
- "110:110" # POP3
|
- "110:110"
|
||||||
- "995:995" # POP3 SSL
|
- "995:995"
|
||||||
- "127.0.0.1:11334:11334" # Bindet nur an Localhost!
|
- "127.0.0.1:11334:11334"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./docker-data/dms/mail-data/:/var/mail/
|
- ./docker-data/dms/mail-data/:/var/mail/
|
||||||
- ./docker-data/dms/mail-state/:/var/mail-state/
|
- ./docker-data/dms/mail-state/:/var/mail-state/
|
||||||
|
|
@ -27,67 +30,87 @@ services:
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
- ./sync_dynamodb_to_sieve.py:/scripts/sync.py:ro
|
- ./sync_dynamodb_to_sieve.py:/scripts/sync.py:ro
|
||||||
- ./sieve-cron:/etc/cron.d/sieve-sync:ro
|
- ./sieve-cron:/etc/cron.d/sieve-sync:ro
|
||||||
- /var/lib/docker/volumes/caddy_data/_data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.email-srvr.com:/etc/mail/certs: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:/etc/dovecot/conf.d/99-sni.conf:ro
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
# Wichtig: Rspamd und andere Services deaktivieren für ersten Test
|
# -------------------------------------------------------
|
||||||
|
# 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.
|
||||||
|
# -------------------------------------------------------
|
||||||
- SSL_TYPE=manual
|
- SSL_TYPE=manual
|
||||||
# Diese Pfade beziehen sich auf das INNERE des Containers (wo wir hin mounten)
|
- SSL_CERT_PATH=/etc/mail/certs/node1.email-srvr.com/node1.email-srvr.com.crt
|
||||||
- SSL_CERT_PATH=/etc/mail/certs/mail.email-srvr.com.crt
|
- SSL_KEY_PATH=/etc/mail/certs/node1.email-srvr.com/node1.email-srvr.com.key
|
||||||
- SSL_KEY_PATH=/etc/mail/certs/mail.email-srvr.com.key
|
|
||||||
|
# SPAM / Rspamd
|
||||||
- ENABLE_OPENDKIM=1
|
- ENABLE_OPENDKIM=1
|
||||||
- ENABLE_OPENDMARC=0
|
- ENABLE_OPENDMARC=0
|
||||||
- ENABLE_POLICYD_SPF=0
|
- ENABLE_POLICYD_SPF=0
|
||||||
# #### SPAM SECTION #####
|
|
||||||
# SPAM Rspamd aktivieren
|
|
||||||
- ENABLE_RSPAMD=1
|
- ENABLE_RSPAMD=1
|
||||||
# Greylisting AUS (vermeidet Verzögerungen)
|
|
||||||
- RSPAMD_GREYLISTING=0
|
- RSPAMD_GREYLISTING=0
|
||||||
# Eigene Mails NICHT scannen (vermeidet Probleme beim Senden)
|
|
||||||
- RSPAMD_CHECK_AUTHENTICATED=0
|
- RSPAMD_CHECK_AUTHENTICATED=0
|
||||||
# Hostname Check AN (filtert Botnets, sehr sicher)
|
|
||||||
- RSPAMD_HFILTER=1
|
- RSPAMD_HFILTER=1
|
||||||
# Spam sortieren statt löschen (Sieve Magic)
|
|
||||||
- MOVE_SPAM_TO_JUNK=1
|
- MOVE_SPAM_TO_JUNK=1
|
||||||
# Alte Dienste aus
|
|
||||||
- ENABLE_AMAVIS=0
|
- ENABLE_AMAVIS=0
|
||||||
- ENABLE_SPAMASSASSIN=0
|
- ENABLE_SPAMASSASSIN=0
|
||||||
- ENABLE_POSTGREY=0
|
- ENABLE_POSTGREY=0
|
||||||
# 2. ClamAV deaktivieren (Anti-Virus)
|
|
||||||
- ENABLE_CLAMAV=0
|
- ENABLE_CLAMAV=0
|
||||||
# HACKERSCHUTZ (Pflicht!)
|
|
||||||
|
# Sicherheit
|
||||||
- ENABLE_FAIL2BAN=1
|
- ENABLE_FAIL2BAN=1
|
||||||
# DNS Resolver (verhindert Spamhaus-Probleme)
|
|
||||||
- ENABLE_UNBOUND=1
|
- ENABLE_UNBOUND=1
|
||||||
# #### END SPAM SECTION #####
|
|
||||||
# END SPAM SECTION
|
# Sonstige
|
||||||
- ENABLE_MANAGESIEVE=0
|
- ENABLE_MANAGESIEVE=0
|
||||||
- ENABLE_POP3=1
|
- ENABLE_POP3=1
|
||||||
- RSPAMD_LEARN=1
|
- RSPAMD_LEARN=1
|
||||||
- ONE_DIR=1
|
- ONE_DIR=1
|
||||||
- ENABLE_UPDATE_CHECK=0
|
- ENABLE_UPDATE_CHECK=0
|
||||||
- PERMIT_DOCKER=network
|
- PERMIT_DOCKER=network
|
||||||
# - PERMIT_DOCKER=empty
|
- SPOOF_PROTECTION=0
|
||||||
- SSL_TYPE=manual
|
- ENABLE_SRS=0
|
||||||
- SSL_CERT_PATH=/tmp/docker-mailserver/ssl/cert.pem
|
- LOG_LEVEL=info
|
||||||
- SSL_KEY_PATH=/tmp/docker-mailserver/ssl/key.pem
|
|
||||||
# Amazon SES SMTP Relay
|
# Amazon SES Relay
|
||||||
- RELAY_HOST=email-smtp.us-east-2.amazonaws.com
|
- RELAY_HOST=email-smtp.us-east-2.amazonaws.com
|
||||||
- RELAY_PORT=587
|
- RELAY_PORT=587
|
||||||
- RELAY_USER=${SES_SMTP_USER}
|
- RELAY_USER=${SES_SMTP_USER}
|
||||||
- RELAY_PASSWORD=${SES_SMTP_PASSWORD}
|
- RELAY_PASSWORD=${SES_SMTP_PASSWORD}
|
||||||
# Content Filter AWS Credentials
|
|
||||||
|
# AWS Credentials
|
||||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
|
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
|
||||||
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
|
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
|
||||||
- AWS_REGION=us-east-2
|
- AWS_REGION=us-east-2
|
||||||
# Weitere Einstellungen
|
|
||||||
- POSTFIX_OVERRIDE_HOSTNAME=email-srvr.com
|
# 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
|
||||||
- POSTFIX_MYNETWORKS=172.16.0.0/12 172.17.0.0/12 172.18.0.0/12 [::1]/128 [fe80::]/64
|
- 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_MAILBOX_SIZE_LIMIT=0
|
||||||
- POSTFIX_MESSAGE_SIZE_LIMIT=0
|
- POSTFIX_MESSAGE_SIZE_LIMIT=0
|
||||||
- SPOOF_PROTECTION=0
|
|
||||||
- ENABLE_SRS=0
|
|
||||||
# Debug-Einstellungen
|
|
||||||
- LOG_LEVEL=info
|
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
- SYS_PTRACE
|
- SYS_PTRACE
|
||||||
|
|
@ -95,8 +118,8 @@ services:
|
||||||
networks:
|
networks:
|
||||||
mail_network:
|
mail_network:
|
||||||
aliases:
|
aliases:
|
||||||
- mail.email-srvr.com
|
|
||||||
- mailserver
|
- mailserver
|
||||||
|
- node1.email-srvr.com
|
||||||
|
|
||||||
roundcube:
|
roundcube:
|
||||||
image: roundcube/roundcubemail:latest
|
image: roundcube/roundcubemail:latest
|
||||||
|
|
@ -111,18 +134,24 @@ services:
|
||||||
- ROUNDCUBEMAIL_DB_NAME=roundcube
|
- ROUNDCUBEMAIL_DB_NAME=roundcube
|
||||||
- ROUNDCUBEMAIL_DB_USER=roundcube
|
- ROUNDCUBEMAIL_DB_USER=roundcube
|
||||||
- ROUNDCUBEMAIL_DB_PASSWORD=${ROUNDCUBE_DB_PASSWORD}
|
- ROUNDCUBEMAIL_DB_PASSWORD=${ROUNDCUBE_DB_PASSWORD}
|
||||||
# Einfache Konfiguration ohne SSL-Probleme (für ersten Test)
|
# Roundcube verbindet intern über den Docker-Alias
|
||||||
- ROUNDCUBEMAIL_DEFAULT_HOST=ssl://mail.email-srvr.com
|
- ROUNDCUBEMAIL_DEFAULT_HOST=ssl://node1.email-srvr.com
|
||||||
- ROUNDCUBEMAIL_DEFAULT_PORT=993
|
- ROUNDCUBEMAIL_DEFAULT_PORT=993
|
||||||
- ROUNDCUBEMAIL_SMTP_SERVER=tls://mail.email-srvr.com
|
# Interner Traffic ohne TLS
|
||||||
- ROUNDCUBEMAIL_SMTP_PORT=587
|
- ROUNDCUBEMAIL_SMTP_SERVER=ssl://node1.email-srvr.com
|
||||||
#- ROUNDCUBEMAIL_PLUGINS=password,email_config,managesieve
|
- ROUNDCUBEMAIL_SMTP_PORT=465
|
||||||
|
|
||||||
|
# WICHTIG: Variablen LEER lassen, damit Roundcube keine Authentifizierung versucht!
|
||||||
|
- ROUNDCUBEMAIL_SMTP_USER=
|
||||||
|
- ROUNDCUBEMAIL_SMTP_PASSWORD=
|
||||||
- ROUNDCUBEMAIL_PLUGINS=password,email_config
|
- ROUNDCUBEMAIL_PLUGINS=password,email_config
|
||||||
# In docker-compose.yml bei roundcube hinzufügen:
|
# NEU: Schaltet die strikte PHP-Zertifikatsprüfung für interne Verbindungen ab
|
||||||
|
- ROUNDCUBEMAIL_IMAP_CONN_OPTIONS={"ssl":{"verify_peer":false,"verify_peer_name":false}}
|
||||||
|
- ROUNDCUBEMAIL_SMTP_CONN_OPTIONS={"ssl":{"verify_peer":false,"verify_peer_name":false}}
|
||||||
ports:
|
ports:
|
||||||
- "8888:80" # Host:Container
|
- "8888:80"
|
||||||
volumes:
|
volumes:
|
||||||
- ./docker-data/roundcube/config:/var/roundcube/config
|
# - ./docker-data/roundcube/config:/var/www/html/config
|
||||||
- ./docker-data/roundcube/plugins/email_config:/var/www/html/plugins/email_config:ro
|
- ./docker-data/roundcube/plugins/email_config:/var/www/html/plugins/email_config:ro
|
||||||
networks:
|
networks:
|
||||||
- mail_network
|
- mail_network
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Eigene Dovecot-Optimierungen für Outlook
|
||||||
|
mail_max_userip_connections = 50
|
||||||
|
imap_client_workarounds = delay-newmail tb-extra-mailbox-sep tb-lsub-flags
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[DEFAULT]
|
[DEFAULT]
|
||||||
# Whitelist: Localhost, private Docker-Netze und die Budd Electric Office-IP
|
# Whitelist: Localhost, private Docker-Netze und die Budd Electric Office-IP
|
||||||
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 24.155.193.233
|
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 24.155.193.233 69.223.70.143
|
||||||
|
|
||||||
[dovecot]
|
[dovecot]
|
||||||
# Erhöht die Anzahl der erlaubten Fehlversuche auf 20
|
# Erhöht die Anzahl der erlaubten Fehlversuche auf 20
|
||||||
|
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDljCCAxugAwIBAgISBjozmCOOzvH/aTFaP5JdZIt8MAoGCCqGSM49BAMDMDIx
|
|
||||||
CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF
|
|
||||||
NzAeFw0yNTExMTcyMDU0NDFaFw0yNjAyMTUyMDU0NDBaMB4xHDAaBgNVBAMTE21h
|
|
||||||
aWwuZW1haWwtc3J2ci5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQzcUl7
|
|
||||||
crboIHaausaf+PKcQ9Q1YnitEYptUCnmLXV4rrBL8wJuqK2nXziFFL/TIoquuJV5
|
|
||||||
N+BuJaoGppdFJCmqo4ICIzCCAh8wDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQG
|
|
||||||
CCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBRF9u/n
|
|
||||||
S60FfiVi+hzhYw+caKfkjDAfBgNVHSMEGDAWgBSuSJ7chx1EoG/aouVgdAR4wpwA
|
|
||||||
gDAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly9lNy5pLmxlbmNy
|
|
||||||
Lm9yZy8wHgYDVR0RBBcwFYITbWFpbC5lbWFpbC1zcnZyLmNvbTATBgNVHSAEDDAK
|
|
||||||
MAgGBmeBDAECATAuBgNVHR8EJzAlMCOgIaAfhh1odHRwOi8vZTcuYy5sZW5jci5v
|
|
||||||
cmcvMTI1LmNybDCCAQUGCisGAQQB1nkCBAIEgfYEgfMA8QB3AEmcm2neHXzs/Dbe
|
|
||||||
zYdkprhbrwqHgBnRVVL76esp3fjDAAABmpPOvpoAAAQDAEgwRgIhAP/5ucrprAoN
|
|
||||||
1yatL9NMD2g6lz5APNoj0tUPCPrCuCRXAiEA0GaG6fEcQfNnfpAbu/owF7llP8E9
|
|
||||||
0RXRi7HAdeZxEAQAdgAOV5S8866pPjMbLJkHs/eQ35vCPXEyJd0hqSWsYcVOIQAA
|
|
||||||
AZqTzr6aAAAEAwBHMEUCIQCMbarF0Pg8Keb3aMua184bxbQcKOGAn4OVjv61fdp8
|
|
||||||
hgIgVT30nW0H2VJwIK7LVJoCVKCAvBLBkvs9/DwyHwaF7SgwCgYIKoZIzj0EAwMD
|
|
||||||
aQAwZgIxAPpXnIr1uy/hUpYVDh3BTOzt6kA50/CBWMqXUHM+V4zSSy7L7zSMueEF
|
|
||||||
FQBbqlqpfgIxAOncbLTJKRIixUPQ0tpDrpZzcrrqkHlsAVTkfrhVaWx8NE91wdvk
|
|
||||||
e3KIaDlcBV+1KQ==
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIEVzCCAj+gAwIBAgIRAKp18eYrjwoiCWbTi7/UuqEwDQYJKoZIhvcNAQELBQAw
|
|
||||||
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
|
||||||
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw
|
|
||||||
WhcNMjcwMzEyMjM1OTU5WjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
|
|
||||||
RW5jcnlwdDELMAkGA1UEAxMCRTcwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARB6AST
|
|
||||||
CFh/vjcwDMCgQer+VtqEkz7JANurZxLP+U9TCeioL6sp5Z8VRvRbYk4P1INBmbef
|
|
||||||
QHJFHCxcSjKmwtvGBWpl/9ra8HW0QDsUaJW2qOJqceJ0ZVFT3hbUHifBM/2jgfgw
|
|
||||||
gfUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD
|
|
||||||
ATASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSuSJ7chx1EoG/aouVgdAR4
|
|
||||||
wpwAgDAfBgNVHSMEGDAWgBR5tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcB
|
|
||||||
AQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0g
|
|
||||||
BAwwCjAIBgZngQwBAgEwJwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVu
|
|
||||||
Y3Iub3JnLzANBgkqhkiG9w0BAQsFAAOCAgEAjx66fDdLk5ywFn3CzA1w1qfylHUD
|
|
||||||
aEf0QZpXcJseddJGSfbUUOvbNR9N/QQ16K1lXl4VFyhmGXDT5Kdfcr0RvIIVrNxF
|
|
||||||
h4lqHtRRCP6RBRstqbZ2zURgqakn/Xip0iaQL0IdfHBZr396FgknniRYFckKORPG
|
|
||||||
yM3QKnd66gtMst8I5nkRQlAg/Jb+Gc3egIvuGKWboE1G89NTsN9LTDD3PLj0dUMr
|
|
||||||
OIuqVjLB8pEC6yk9enrlrqjXQgkLEYhXzq7dLafv5Vkig6Gl0nuuqjqfp0Q1bi1o
|
|
||||||
yVNAlXe6aUXw92CcghC9bNsKEO1+M52YY5+ofIXlS/SEQbvVYYBLZ5yeiglV6t3S
|
|
||||||
M6H+vTG0aP9YHzLn/KVOHzGQfXDP7qM5tkf+7diZe7o2fw6O7IvN6fsQXEQQj8TJ
|
|
||||||
UXJxv2/uJhcuy/tSDgXwHM8Uk34WNbRT7zGTGkQRX0gsbjAea/jYAoWv0ZvQRwpq
|
|
||||||
Pe79D/i7Cep8qWnA+7AE/3B3S/3dEEYmc0lpe1366A/6GEgk3ktr9PEoQrLChs6I
|
|
||||||
tu3wnNLB2euC8IKGLQFpGtOO/2/hiAKjyajaBP25w1jF0Wl8Bbqne3uZ2q1GyPFJ
|
|
||||||
YRmT7/OXpmOH/FVLtwS+8ng1cAmpCujPwteJZNcDG0sF2n/sc0+SQf49fdyUK0ty
|
|
||||||
+VUwFj9tmWxyR/M=
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
-----BEGIN EC PRIVATE KEY-----
|
|
||||||
MHcCAQEEIFvBg5uuw4K36qMR6CYx09cfDcSPJOsCtQi/M/HKSYN1oAoGCCqGSM49
|
|
||||||
AwEHoUQDQgAEM3FJe3K26CB2mrrGn/jynEPUNWJ4rRGKbVAp5i11eK6wS/MCbqit
|
|
||||||
p184hRS/0yKKrriVeTfgbiWqBqaXRSQpqg==
|
|
||||||
-----END EC PRIVATE KEY-----
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// WICHTIG: Zuerst die vom Docker-Container generierte Config einbinden!
|
||||||
|
// Deine Overrides (hier wird alles überschrieben, was wir brauchen)
|
||||||
|
$config['smtp_server'] = 'ssl://mailserver';
|
||||||
|
$config['smtp_port'] = 465;
|
||||||
|
|
||||||
|
$config['smtp_conn_options'] = array(
|
||||||
|
'ssl' => array(
|
||||||
|
'verify_peer' => false,
|
||||||
|
'verify_peer_name' => false,
|
||||||
|
'allow_self_signed' => true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$config['language'] = 'en_US';
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 1. Prüfen, ob die Umgebungsvariablen HOST1 und HOST2 gesetzt sind
|
||||||
|
if [ -z "$HOST1" ] || [ -z "$HOST2" ]; then
|
||||||
|
echo "Fehler: Die Umgebungsvariablen HOST1 und/oder HOST2 sind nicht gesetzt."
|
||||||
|
echo "Bitte setze diese vor dem Ausführen des Skripts, zum Beispiel mit:"
|
||||||
|
echo 'export HOST1="65.254.254.50"'
|
||||||
|
echo 'export HOST2="147.93.132.244"'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. E-Mail-Adresse interaktiv abfragen
|
||||||
|
read -p "Bitte E-Mail-Adresse eingeben: " EMAIL
|
||||||
|
|
||||||
|
# 3. Passwort interaktiv und unsichtbar (-s) abfragen
|
||||||
|
read -s -p "Bitte Passwort eingeben: " PASSWORD
|
||||||
|
echo "" # Zeilenumbruch für eine saubere Darstellung nach der Passworteingabe
|
||||||
|
|
||||||
|
# 4. Log-Datei mit Zeitstempel und E-Mail definieren
|
||||||
|
LOGFILE="imapsync_${EMAIL}_$(date +%Y%m%d_%H%M%S).log"
|
||||||
|
|
||||||
|
echo "Starte imapsync für $EMAIL..."
|
||||||
|
echo "Quell-Host (HOST1): $HOST1"
|
||||||
|
echo "Ziel-Host (HOST2): $HOST2"
|
||||||
|
echo "Logs werden gespeichert in: $LOGFILE"
|
||||||
|
echo "---------------------------------------------------"
|
||||||
|
|
||||||
|
# 5. Docker-Container ausführen und Output mit 'tee' loggen
|
||||||
|
docker run --rm -i gilleslamiral/imapsync imapsync \
|
||||||
|
--host1 "$HOST1" \
|
||||||
|
--user1 "$EMAIL" \
|
||||||
|
--password1 "$PASSWORD" \
|
||||||
|
--ssl1 \
|
||||||
|
--host2 "$HOST2" \
|
||||||
|
--user2 "$EMAIL" \
|
||||||
|
--password2 "$PASSWORD" \
|
||||||
|
--ssl2 \
|
||||||
|
--automap 2>&1 | tee "$LOGFILE"
|
||||||
|
|
||||||
|
echo "---------------------------------------------------"
|
||||||
|
echo "Sync abgeschlossen. Das vollständige Log findest du in: $LOGFILE"
|
||||||
|
|
@ -0,0 +1,207 @@
|
||||||
|
#!/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
|
||||||
|
# - docker-data/dms/config/postfix-sni.map (NEU für Postfix SNI)
|
||||||
|
#
|
||||||
|
# Cert-Konvention (Caddy Wildcard):
|
||||||
|
# Caddy speichert *.domain.tld unter: wildcard_.domain.tld/wildcard_.domain.tld.crt
|
||||||
|
# Im Container (gemountet unter /etc/mail/certs):
|
||||||
|
# /etc/mail/certs/wildcard_.domain.tld/wildcard_.domain.tld.crt
|
||||||
|
# /etc/mail/certs/wildcard_.domain.tld/wildcard_.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-Pfad Hilfsfunktionen ---
|
||||||
|
wildcard_cert_path() {
|
||||||
|
echo "$CERTS_BASE_PATH/wildcard_.${1}/wildcard_.${1}.crt"
|
||||||
|
}
|
||||||
|
wildcard_key_path() {
|
||||||
|
echo "$CERTS_BASE_PATH/wildcard_.${1}/wildcard_.${1}.key"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 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=$(wildcard_cert_path "$domain")
|
||||||
|
KEY_PATH=$(wildcard_key_path "$domain")
|
||||||
|
|
||||||
|
if docker exec "$DMS_CONTAINER" test -f "$CERT_PATH" 2>/dev/null; then
|
||||||
|
echo " ✅ $domain → $CERT_PATH"
|
||||||
|
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)"
|
||||||
|
else
|
||||||
|
echo " ⚠️ $NODE_HOSTNAME → KEIN Cert! Caddy-Block im Caddyfile prüfen."
|
||||||
|
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.
|
||||||
|
HEADER
|
||||||
|
|
||||||
|
for domain in $DOMAINS_OK; do
|
||||||
|
CERT_PATH=$(wildcard_cert_path "$domain")
|
||||||
|
KEY_PATH=$(wildcard_key_path "$domain")
|
||||||
|
|
||||||
|
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 (Neu geschrieben für echte SNI Maps)
|
||||||
|
# ================================================================
|
||||||
|
POSTFIX_CFG="$CONFIG_DIR/postfix-main.cf"
|
||||||
|
POSTFIX_MAP="$CONFIG_DIR/postfix-sni.map"
|
||||||
|
echo ""
|
||||||
|
echo "📝 Generiere: $POSTFIX_CFG und $POSTFIX_MAP"
|
||||||
|
|
||||||
|
if [ -f "$POSTFIX_CFG" ]; then
|
||||||
|
cp "$POSTFIX_CFG" "${POSTFIX_CFG}.bak.$(date +%Y%m%d%H%M%S)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 1. postfix-main.cf erstellen
|
||||||
|
cat > "$POSTFIX_CFG" << POSTFIX_EOF
|
||||||
|
# postfix-main.cf - Automatisch generiert von setup-dms-tls.sh
|
||||||
|
#
|
||||||
|
# 1. Fallback-Zertifikat (Wird genutzt, wenn kein SNI-Match gefunden wird)
|
||||||
|
smtpd_tls_chain_files = ${NODE_KEY_PATH}, ${NODE_CERT_PATH}
|
||||||
|
|
||||||
|
# 2. SNI-Mapping aktivieren
|
||||||
|
# Wir nutzen 'texthash', damit Postfix die Map direkt lesen kann,
|
||||||
|
# ohne dass 'postmap' ausgeführt werden muss!
|
||||||
|
tls_server_sni_maps = texthash:/tmp/docker-mailserver/postfix-sni.map
|
||||||
|
POSTFIX_EOF
|
||||||
|
|
||||||
|
# 2. postfix-sni.map erstellen
|
||||||
|
echo "# postfix-sni.map - Automatisch generiert (Format: host key_pfad cert_pfad)" > "$POSTFIX_MAP"
|
||||||
|
|
||||||
|
for domain in $DOMAINS_OK; do
|
||||||
|
KEY_PATH=$(wildcard_key_path "$domain")
|
||||||
|
CERT_PATH=$(wildcard_cert_path "$domain")
|
||||||
|
|
||||||
|
cat >> "$POSTFIX_MAP" << EOF
|
||||||
|
mail.${domain} ${KEY_PATH} ${CERT_PATH}
|
||||||
|
smtp.${domain} ${KEY_PATH} ${CERT_PATH}
|
||||||
|
imap.${domain} ${KEY_PATH} ${CERT_PATH}
|
||||||
|
pop.${domain} ${KEY_PATH} ${CERT_PATH}
|
||||||
|
${domain} ${KEY_PATH} ${CERT_PATH}
|
||||||
|
EOF
|
||||||
|
done
|
||||||
|
|
||||||
|
echo " ✅ Postfix SNI: $(echo $DOMAINS_OK | wc -w) Domain(s) konfiguriert"
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# Zusammenfassung
|
||||||
|
# ================================================================
|
||||||
|
echo ""
|
||||||
|
echo "============================================================"
|
||||||
|
echo "✅ Konfigurationen generiert."
|
||||||
|
echo ""
|
||||||
|
echo "🔄 Lade Postfix und Dovecot neu (ohne Downtime)..."
|
||||||
|
docker exec "$DMS_CONTAINER" postfix reload || echo "⚠️ Postfix Reload fehlgeschlagen"
|
||||||
|
docker exec "$DMS_CONTAINER" dovecot reload || echo "⚠️ Dovecot Reload fehlgeschlagen"
|
||||||
|
echo ""
|
||||||
|
echo "📋 Nächste Schritte:"
|
||||||
|
echo ""
|
||||||
|
echo "1. 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 "============================================================"
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# update_dms_config.sh
|
|
||||||
# Fügt eine neue Domain zur lokalen DMS Konfiguration hinzu:
|
|
||||||
# 1. Ergänzt SRS_EXCLUDE_DOMAINS in docker-compose.yml
|
|
||||||
# 2. Ergänzt Whitelist in smtp_header_checks
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
DOMAIN=$1
|
|
||||||
DOCKER_COMPOSE_FILE="./docker-compose.yml"
|
|
||||||
HEADER_CHECKS_FILE="./docker-data/dms/config/postfix/smtp_header_checks"
|
|
||||||
|
|
||||||
if [ -z "$DOMAIN" ]; then
|
|
||||||
echo "Usage: $0 <domain>"
|
|
||||||
echo "Example: $0 cielectrical.com"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "=== Aktualisiere lokale Konfiguration für $DOMAIN ==="
|
|
||||||
|
|
||||||
# ---------------------------------------------
|
|
||||||
# 1. Update docker-compose.yml (SRS Exclude)
|
|
||||||
# ---------------------------------------------
|
|
||||||
if [ -f "$DOCKER_COMPOSE_FILE" ]; then
|
|
||||||
echo "-> Prüfe docker-compose.yml..."
|
|
||||||
|
|
||||||
# Prüfen, ob Domain schon in der Zeile steht
|
|
||||||
if grep -q "SRS_EXCLUDE_DOMAINS=.*$DOMAIN" "$DOCKER_COMPOSE_FILE"; then
|
|
||||||
echo " Domain bereits in SRS_EXCLUDE_DOMAINS vorhanden."
|
|
||||||
else
|
|
||||||
# Backup erstellen
|
|
||||||
cp "$DOCKER_COMPOSE_FILE" "${DOCKER_COMPOSE_FILE}.bak"
|
|
||||||
|
|
||||||
# sed Magie: Suche Zeile mit SRS_EXCLUDE_DOMAINS, hänge ",domain" am Ende an
|
|
||||||
# Wir nutzen ein Komma als Trenner vor der neuen Domain
|
|
||||||
sed -i "s/SRS_EXCLUDE_DOMAINS=.*/&,$DOMAIN/" "$DOCKER_COMPOSE_FILE"
|
|
||||||
echo " ✅ $DOMAIN zu SRS_EXCLUDE_DOMAINS hinzugefügt."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "❌ Fehler: $DOCKER_COMPOSE_FILE nicht gefunden!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ---------------------------------------------
|
|
||||||
# 2. Update smtp_header_checks (PCRE Whitelist)
|
|
||||||
# ---------------------------------------------
|
|
||||||
if [ -f "$HEADER_CHECKS_FILE" ]; then
|
|
||||||
echo "-> Prüfe smtp_header_checks..."
|
|
||||||
|
|
||||||
# Domain für Regex escapen (der Punkt muss \. sein)
|
|
||||||
ESCAPED_DOMAIN="${DOMAIN//./\\.}"
|
|
||||||
NEW_LINE="/.*@${ESCAPED_DOMAIN}/ DUNNO"
|
|
||||||
|
|
||||||
# Prüfen, ob Eintrag existiert
|
|
||||||
if grep -Fq "@$ESCAPED_DOMAIN/" "$HEADER_CHECKS_FILE"; then
|
|
||||||
echo " Domain bereits in smtp_header_checks vorhanden."
|
|
||||||
else
|
|
||||||
# Backup erstellen
|
|
||||||
cp "$HEADER_CHECKS_FILE" "${HEADER_CHECKS_FILE}.bak"
|
|
||||||
|
|
||||||
# Wir fügen die Zeile oben bei den Whitelists ein (nach dem Kommentar "# 1. EIGENE...")
|
|
||||||
# Oder einfach am Anfang der Datei, falls die Reihenfolge egal ist.
|
|
||||||
# Aber bei PCRE ist Reihenfolge wichtig! Whitelist muss VOR Rewrite stehen.
|
|
||||||
|
|
||||||
# Strategie: Wir suchen die erste Zeile, die mit /.*@ anfängt und fügen davor ein
|
|
||||||
# Oder wir hängen es einfach oben an einen definierten Marker an.
|
|
||||||
|
|
||||||
# Einfachste sichere Methode für dein File: Nach dem Kommentarblock einfügen
|
|
||||||
# Wir suchen nach der Zeile mit "1. EIGENE DOMAINS" und fügen 3 Zeilen später ein
|
|
||||||
# Aber sed insert ist tricky.
|
|
||||||
|
|
||||||
# Bessere Methode: Wir wissen, dass Whitelists ganz oben stehen sollen.
|
|
||||||
# Wir erstellen eine temporäre Datei.
|
|
||||||
|
|
||||||
# 1. Header (Kommentare) behalten oder neu schreiben?
|
|
||||||
# Wir hängen es einfach GANZ OBEN in die Datei ein (vor alle anderen Regeln),
|
|
||||||
# das ist bei "DUNNO" (Whitelist) immer sicherste Variante.
|
|
||||||
|
|
||||||
sed -i "1i $NEW_LINE" "$HEADER_CHECKS_FILE"
|
|
||||||
|
|
||||||
echo " ✅ $DOMAIN zu smtp_header_checks hinzugefügt (ganz oben)."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "⚠️ Warnung: $HEADER_CHECKS_FILE nicht gefunden. Überspringe."
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "========================================================"
|
|
||||||
echo "Konfiguration aktualisiert."
|
|
||||||
echo "HINWEIS: Damit die Änderungen wirksam werden, führen Sie bitte aus:"
|
|
||||||
echo " docker compose up -d --force-recreate"
|
|
||||||
echo "========================================================"
|
|
||||||
|
|
@ -1,7 +1,19 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# cloudflareMigrationDns.sh
|
# cloudflareMigrationDns.sh
|
||||||
# Setzt DNS Records für Amazon SES Migration + Cloudflare
|
# Setzt DNS Records für Amazon SES Migration + Cloudflare
|
||||||
# Unterstützt: DKIM, SPF (Merge), DMARC, MX (Safety Check), Autodiscover
|
# Unterstützt: DKIM, SPF (Merge), DMARC, MX, Autodiscover
|
||||||
|
# Setzt mail/imap/smtp/pop Subdomains für domain-spezifischen Mailserver-Zugang
|
||||||
|
#
|
||||||
|
# MIGRATIONS-FLAGS:
|
||||||
|
# SKIP_CLIENT_DNS=true → Abschnitt 8 (imap/smtp/pop/webmail) + 10 (SRV) überspringen
|
||||||
|
# Nutzen: Client-Subdomains bleiben beim alten Provider
|
||||||
|
# SKIP_DMARC=true → Abschnitt 7 (DMARC) überspringen
|
||||||
|
# Nutzen: Bestehenden DMARC-Record nicht anfassen
|
||||||
|
#
|
||||||
|
# Typischer Migrations-Ablauf:
|
||||||
|
# Phase 0 (Vorbereitung): SKIP_CLIENT_DNS=true SKIP_DMARC=true → nur SES + SPF
|
||||||
|
# Phase 1 (MX Cutover): MX umstellen (manuell)
|
||||||
|
# Phase 2 (Client Switch): ohne SKIP Flags → alle Records setzen
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
|
@ -9,8 +21,14 @@ set -e
|
||||||
AWS_REGION=${AWS_REGION:-"us-east-2"}
|
AWS_REGION=${AWS_REGION:-"us-east-2"}
|
||||||
DRY_RUN=${DRY_RUN:-"false"}
|
DRY_RUN=${DRY_RUN:-"false"}
|
||||||
|
|
||||||
# Ziel für Autodiscover/IMAP (wohin sollen Mail-Clients verbinden?)
|
# Migrations-Flags (NEU)
|
||||||
# Standard: mail.deinedomain.tld. Kann überschrieben werden.
|
SKIP_CLIENT_DNS=${SKIP_CLIENT_DNS:-"false"}
|
||||||
|
SKIP_DMARC=${SKIP_DMARC:-"false"}
|
||||||
|
|
||||||
|
# IP des Mailservers - PFLICHT wenn keine CNAME-Kette gewünscht
|
||||||
|
MAIL_SERVER_IP=${MAIL_SERVER_IP:-""}
|
||||||
|
|
||||||
|
# Ziel-Server für Mailclients. Standard: mail.<kundendomain>
|
||||||
TARGET_MAIL_SERVER=${TARGET_MAIL_SERVER:-"mail.${DOMAIN_NAME}"}
|
TARGET_MAIL_SERVER=${TARGET_MAIL_SERVER:-"mail.${DOMAIN_NAME}"}
|
||||||
|
|
||||||
# --- CHECKS ---
|
# --- CHECKS ---
|
||||||
|
|
@ -19,10 +37,21 @@ 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 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 ! 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 " Setze: export MAIL_SERVER_IP=<deine-server-ip>"
|
||||||
|
# Kein exit - Abschnitt 8 wird ggf. übersprungen
|
||||||
|
fi
|
||||||
|
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
echo " 🛡️ DNS Migration Setup für: $DOMAIN_NAME"
|
echo " 🛡️ DNS Migration Setup für: $DOMAIN_NAME"
|
||||||
echo " 🌍 Region: $AWS_REGION"
|
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!"
|
[ "$DRY_RUN" = "true" ] && echo " ⚠️ DRY RUN MODE - Keine Änderungen!"
|
||||||
|
[ "$SKIP_CLIENT_DNS" = "true" ] && echo " ⏭️ SKIP: Client-Subdomains (imap/smtp/pop/webmail/SRV)"
|
||||||
|
[ "$SKIP_DMARC" = "true" ] && echo " ⏭️ SKIP: DMARC Record"
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
|
|
||||||
# 1. ZONE ID HOLEN
|
# 1. ZONE ID HOLEN
|
||||||
|
|
@ -49,30 +78,21 @@ ensure_record() {
|
||||||
|
|
||||||
echo " ⚙️ Prüfe $type $name..."
|
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" \
|
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")
|
-H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json")
|
||||||
|
|
||||||
local rec_id=$(echo "$search_res" | jq -r '.result[0].id')
|
local rec_id=$(echo "$search_res" | jq -r '.result[0].id')
|
||||||
local rec_content=$(echo "$search_res" | jq -r '.result[0].content')
|
local rec_content=$(echo "$search_res" | jq -r '.result[0].content')
|
||||||
|
|
||||||
# JSON Body bauen
|
|
||||||
if [ "$type" == "MX" ]; then
|
if [ "$type" == "MX" ]; then
|
||||||
json_data=$(jq -n --arg t "$type" --arg n "$name" --arg c "$content" --argjson p "$proxied" --argjson prio "$priority" \
|
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}')
|
'{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
|
else
|
||||||
json_data=$(jq -n --arg t "$type" --arg n "$name" --arg c "$content" --argjson p "$proxied" \
|
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}')
|
'{type: $t, name: $n, content: $c, ttl: 3600, proxied: $p}')
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# LOGIK
|
|
||||||
if [ "$rec_id" == "null" ]; then
|
if [ "$rec_id" == "null" ]; then
|
||||||
# --- CREATE ---
|
|
||||||
if [ "$DRY_RUN" = "true" ]; then
|
if [ "$DRY_RUN" = "true" ]; then
|
||||||
echo " [DRY] Würde ERSTELLEN: $content"
|
echo " [DRY] Würde ERSTELLEN: $content"
|
||||||
else
|
else
|
||||||
|
|
@ -81,34 +101,27 @@ ensure_record() {
|
||||||
if [ "$(echo $res | jq -r .success)" == "true" ]; then
|
if [ "$(echo $res | jq -r .success)" == "true" ]; then
|
||||||
echo " ✅ Erstellt."
|
echo " ✅ Erstellt."
|
||||||
else
|
else
|
||||||
echo " ❌ Fehler beim Erstellen: $(echo $res | jq -r .errors[0].message)"
|
echo " ❌ Fehler beim Erstellen: $(echo $res | jq -r '.errors[0].message')"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# --- EXISTS ---
|
|
||||||
if [ "$rec_content" == "$content" ]; then
|
if [ "$rec_content" == "$content" ]; then
|
||||||
echo " 🆗 Identisch vorhanden. Überspringe."
|
echo " 🆗 Identisch. Überspringe."
|
||||||
else
|
else
|
||||||
# Inhalt anders -> Update oder Error?
|
|
||||||
if [ "$type" == "MX" ] && [ "$name" == "$DOMAIN_NAME" ]; then
|
if [ "$type" == "MX" ] && [ "$name" == "$DOMAIN_NAME" ]; then
|
||||||
echo " ⛔ MX Record existiert aber ist anders!"
|
echo " ⛔ Root-MX existiert aber ist anders: $rec_content"
|
||||||
echo " Gefunden: $rec_content"
|
echo " → Wird NICHT automatisch geändert (Migrations-Schutz)"
|
||||||
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
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Für TXT (SPF/DMARC) oder CNAME machen wir ein UPDATE (Overwrite)
|
|
||||||
if [ "$DRY_RUN" = "true" ]; then
|
if [ "$DRY_RUN" = "true" ]; then
|
||||||
echo " [DRY] Würde UPDATEN von '$rec_content' auf '$content'"
|
echo " [DRY] Würde UPDATEN: '$rec_content' → '$content'"
|
||||||
else
|
else
|
||||||
res=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$rec_id" \
|
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")
|
-H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" --data "$json_data")
|
||||||
if [ "$(echo $res | jq -r .success)" == "true" ]; then
|
if [ "$(echo $res | jq -r .success)" == "true" ]; then
|
||||||
echo " 🔄 Aktualisiert."
|
echo " ✅ Aktualisiert."
|
||||||
else
|
else
|
||||||
echo " ❌ Fehler beim Update: $(echo $res | jq -r .errors[0].message)"
|
echo " ❌ Fehler beim Updaten: $(echo $res | jq -r '.errors[0].message')"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
@ -116,127 +129,199 @@ ensure_record() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# SCHRITT 1: MAIL FROM ermitteln
|
# SCHRITT 1: MAIL FROM Domain (aus SES lesen)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
echo ""
|
echo ""
|
||||||
echo "--- 1. MAIL FROM Domain ---"
|
echo "--- 1. MAIL FROM Domain ---"
|
||||||
# Wenn von außen nicht gesetzt, versuche via AWS
|
MAIL_FROM_DOMAIN=$(aws sesv2 get-email-identity \
|
||||||
if [ -z "$MAIL_FROM_DOMAIN" ]; then
|
--email-identity "$DOMAIN_NAME" \
|
||||||
echo " Variable MAIL_FROM_DOMAIN leer, frage AWS SES..."
|
--region "$AWS_REGION" \
|
||||||
SES_JSON=$(aws sesv2 get-email-identity --email-identity $DOMAIN_NAME --region $AWS_REGION 2>/dev/null)
|
--query 'MailFromAttributes.MailFromDomain' \
|
||||||
MAIL_FROM_DOMAIN=$(echo "$SES_JSON" | jq -r '.MailFromAttributes.MailFromDomain')
|
--output text 2>/dev/null || echo "NONE")
|
||||||
|
|
||||||
if [ "$MAIL_FROM_DOMAIN" == "null" ] || [ -z "$MAIL_FROM_DOMAIN" ]; then
|
if [ "$MAIL_FROM_DOMAIN" == "NONE" ] || [ "$MAIL_FROM_DOMAIN" == "None" ] || [ -z "$MAIL_FROM_DOMAIN" ]; then
|
||||||
MAIL_FROM_DOMAIN="mail.$DOMAIN_NAME"
|
echo " ℹ️ Keine MAIL FROM Domain in SES konfiguriert."
|
||||||
echo " ⚠️ Keine MAIL FROM in SES gefunden. Fallback auf: $MAIL_FROM_DOMAIN"
|
echo " → Überspringe MAIL FROM DNS Setup."
|
||||||
fi
|
MAIL_FROM_DOMAIN=""
|
||||||
else
|
|
||||||
echo " Nutze vorgegebene MAIL FROM: $MAIL_FROM_DOMAIN"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# SCHRITT 2: DKIM Records (CNAME)
|
# SCHRITT 2: DKIM Records
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
echo ""
|
echo ""
|
||||||
echo "--- 2. DKIM Records ---"
|
echo "--- 2. DKIM Records ---"
|
||||||
TOKENS=$(aws ses get-identity-dkim-attributes --identities $DOMAIN_NAME --region $AWS_REGION --query "DkimAttributes.\"$DOMAIN_NAME\".DkimTokens" --output text)
|
DKIM_TOKENS=$(aws sesv2 get-email-identity \
|
||||||
for token in $TOKENS; do
|
--email-identity "$DOMAIN_NAME" \
|
||||||
ensure_record "CNAME" "${token}._domainkey.$DOMAIN_NAME" "${token}.dkim.amazonses.com" false
|
--region "$AWS_REGION" \
|
||||||
|
--query 'DkimAttributes.Tokens' \
|
||||||
|
--output text 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ -n "$DKIM_TOKENS" ] && [ "$DKIM_TOKENS" != "None" ]; then
|
||||||
|
for TOKEN in $DKIM_TOKENS; do
|
||||||
|
ensure_record "CNAME" "${TOKEN}._domainkey.${DOMAIN_NAME}" "${TOKEN}.dkim.amazonses.com" false
|
||||||
done
|
done
|
||||||
|
else
|
||||||
|
echo " ⚠️ Keine DKIM Tokens gefunden. SES Identity angelegt?"
|
||||||
|
fi
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# SCHRITT 3: SES Verification (_amazonses)
|
# SCHRITT 3: SES Verification TXT
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
echo ""
|
echo ""
|
||||||
echo "--- 3. SES Verification TXT ---"
|
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)
|
VERIFICATION_TOKEN=$(aws ses get-identity-verification-attributes \
|
||||||
if [ "$VERIF_TOKEN" != "None" ]; then
|
--identities "$DOMAIN_NAME" \
|
||||||
ensure_record "TXT" "_amazonses.$DOMAIN_NAME" "$VERIF_TOKEN" false
|
--region "$AWS_REGION" \
|
||||||
|
--query "VerificationAttributes.\"${DOMAIN_NAME}\".VerificationToken" \
|
||||||
|
--output text 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ -n "$VERIFICATION_TOKEN" ] && [ "$VERIFICATION_TOKEN" != "None" ]; then
|
||||||
|
ensure_record "TXT" "_amazonses.${DOMAIN_NAME}" "$VERIFICATION_TOKEN" false
|
||||||
|
else
|
||||||
|
echo " ⚠️ Kein Verification Token. SES Identity angelegt?"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# SCHRITT 4: MAIL FROM Subdomain (MX + SPF)
|
# SCHRITT 4: MAIL FROM Subdomain (MX + SPF)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
echo ""
|
echo ""
|
||||||
echo "--- 4. MAIL FROM Subdomain ($MAIL_FROM_DOMAIN) ---"
|
echo "--- 4. MAIL FROM Subdomain (${MAIL_FROM_DOMAIN:-'nicht konfiguriert'}) ---"
|
||||||
# MX für die Subdomain (feedback loop)
|
|
||||||
ensure_record "MX" "$MAIL_FROM_DOMAIN" "feedback-smtp.$AWS_REGION.amazonses.com" false 10
|
if [ -n "$MAIL_FROM_DOMAIN" ]; then
|
||||||
# SPF für die Subdomain (strikte SES Regel)
|
# Prüfe ob CNAME-Konflikt auf der MAIL FROM Subdomain existiert
|
||||||
|
CNAME_CHECK=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=CNAME&name=$MAIL_FROM_DOMAIN" \
|
||||||
|
-H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" | jq -r '.result[0].content')
|
||||||
|
|
||||||
|
if [ "$CNAME_CHECK" != "null" ] && [ -n "$CNAME_CHECK" ]; then
|
||||||
|
echo " ⛔ CNAME-Konflikt! $MAIL_FROM_DOMAIN hat CNAME → $CNAME_CHECK"
|
||||||
|
echo " MX + TXT können nicht neben CNAME existieren."
|
||||||
|
echo " → awsses.sh mit anderem MAIL_FROM_SUBDOMAIN erneut ausführen"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ensure_record "MX" "$MAIL_FROM_DOMAIN" "feedback-smtp.${AWS_REGION}.amazonses.com" false 10
|
||||||
ensure_record "TXT" "$MAIL_FROM_DOMAIN" "v=spf1 include:amazonses.com ~all" false
|
ensure_record "TXT" "$MAIL_FROM_DOMAIN" "v=spf1 include:amazonses.com ~all" false
|
||||||
|
else
|
||||||
|
echo " ℹ️ Übersprungen (keine MAIL FROM Domain konfiguriert)."
|
||||||
|
fi
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# SCHRITT 5: Root Domain SPF (Merge Logic)
|
# SCHRITT 5: Root Domain SPF (Merge mit altem Provider)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
echo ""
|
echo ""
|
||||||
echo "--- 5. Root Domain SPF ---"
|
echo "--- 5. Root Domain SPF ---"
|
||||||
if [ -n "$OLD_PROVIDER_SPF" ]; then
|
|
||||||
# Merge: SES + Alter Provider
|
# Aktuellen SPF-Record lesen
|
||||||
FINAL_SPF="v=spf1 include:amazonses.com $OLD_PROVIDER_SPF ~all"
|
# Cloudflare liefert TXT-Content manchmal mit Anführungszeichen,
|
||||||
echo " ℹ️ Modus: Migration (SES + Alt)"
|
# daher erst alle TXT-Records holen und dann filtern
|
||||||
|
CURRENT_SPF=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=TXT&name=$DOMAIN_NAME" \
|
||||||
|
-H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" \
|
||||||
|
| jq -r '[.result[] | select(.content | gsub("^\"|\"$";"") | startswith("v=spf1"))][0].content // ""')
|
||||||
|
|
||||||
|
# Anführungszeichen sofort entfernen
|
||||||
|
CURRENT_SPF=$(echo "$CURRENT_SPF" | tr -d '"')
|
||||||
|
|
||||||
|
if [ -n "$CURRENT_SPF" ]; then
|
||||||
|
echo " 📋 Aktueller SPF: $CURRENT_SPF"
|
||||||
|
# Prüfe ob amazonses.com schon drin ist
|
||||||
|
if echo "$CURRENT_SPF" | grep -q "include:amazonses.com"; then
|
||||||
|
echo " 🆗 SPF enthält bereits include:amazonses.com"
|
||||||
else
|
else
|
||||||
# Nur SES
|
# amazonses.com einfügen direkt nach v=spf1
|
||||||
FINAL_SPF="v=spf1 include:amazonses.com ~all"
|
NEW_SPF=$(echo "$CURRENT_SPF" | sed 's/v=spf1 /v=spf1 include:amazonses.com /')
|
||||||
echo " ℹ️ Modus: SES only"
|
# ?all → ~all upgraden
|
||||||
|
NEW_SPF=$(echo "$NEW_SPF" | sed 's/?all/~all/')
|
||||||
|
echo " 📝 Neuer SPF: $NEW_SPF"
|
||||||
|
ensure_record "TXT" "$DOMAIN_NAME" "$NEW_SPF" false
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " ℹ️ Kein SPF Record vorhanden. Erstelle neuen."
|
||||||
|
ensure_record "TXT" "$DOMAIN_NAME" "v=spf1 include:amazonses.com ~all" false
|
||||||
fi
|
fi
|
||||||
ensure_record "TXT" "$DOMAIN_NAME" "$FINAL_SPF" false
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# SCHRITT 6: Root Domain MX (Safety First)
|
# SCHRITT 6: Root Domain MX (nur Info, wird nicht geändert)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
echo ""
|
echo ""
|
||||||
echo "--- 6. Root Domain MX ---"
|
echo "--- 6. Root Domain MX (nur Info, wird nicht geändert) ---"
|
||||||
# Hier wollen wir den Inbound SMTP von AWS (falls man AWS WorkMail nutzt oder DMS via AWS ingress)
|
CURRENT_MX=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=MX&name=$DOMAIN_NAME" \
|
||||||
# WARTE: Du nutzt DMS. Dein DMS hat vermutlich eine eigene IP/Hostname (z.B. mail.buddelectric.net).
|
-H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" \
|
||||||
# Wenn du SES NUR ZUM SENDEN nutzt, darfst du den Root MX NICHT auf Amazon ändern!
|
| jq -r '.result[0].content // "keiner"')
|
||||||
#
|
echo " ℹ️ MX vorhanden: $CURRENT_MX (wird nicht geändert)"
|
||||||
# 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
|
# SCHRITT 7: DMARC
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
echo ""
|
echo ""
|
||||||
echo "--- 7. DMARC ---"
|
echo "--- 7. DMARC ---"
|
||||||
|
if [ "$SKIP_DMARC" = "true" ]; then
|
||||||
|
echo " ⏭️ Übersprungen (SKIP_DMARC=true)"
|
||||||
|
echo " ℹ️ Bestehender DMARC-Record bleibt unverändert."
|
||||||
|
else
|
||||||
ensure_record "TXT" "_dmarc.$DOMAIN_NAME" "v=DMARC1; p=none; rua=mailto:postmaster@$DOMAIN_NAME" false
|
ensure_record "TXT" "_dmarc.$DOMAIN_NAME" "v=DMARC1; p=none; rua=mailto:postmaster@$DOMAIN_NAME" false
|
||||||
|
fi
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# SCHRITT 8: Autodiscover / Autoconfig
|
# SCHRITT 8: Mailclient Subdomains (A + CNAME)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
echo ""
|
echo ""
|
||||||
echo "--- 8. Autodiscover / Autoconfig ---"
|
echo "--- 8. Mailclient Subdomains (A + CNAME) ---"
|
||||||
# Ziel ist meist der IMAP/SMTP Server
|
if [ "$SKIP_CLIENT_DNS" = "true" ]; then
|
||||||
echo " ℹ️ Ziel für Clients: $TARGET_MAIL_SERVER"
|
echo " ⏭️ Übersprungen (SKIP_CLIENT_DNS=true)"
|
||||||
|
echo " ℹ️ imap/smtp/pop/webmail bleiben beim alten Provider."
|
||||||
|
echo " ℹ️ Setze SKIP_CLIENT_DNS=false nach MX-Cutover + Client-Umstellung."
|
||||||
|
else
|
||||||
|
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
|
# imap, smtp, pop, webmail → CNAME auf mail.<domain>
|
||||||
ensure_record "CNAME" "autoconfig.$DOMAIN_NAME" "$TARGET_MAIL_SERVER" false
|
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
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
# Füge das zu deinem Skript hinzu (Schritt 9 optional):
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# SCHRITT 9: SRV Records (Service Discovery)
|
# SCHRITT 9: Autodiscover / Autoconfig
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
echo ""
|
echo ""
|
||||||
echo "--- 9. SRV Records (Service Discovery) ---"
|
echo "--- 9. Autodiscover / Autoconfig ---"
|
||||||
# Das hilft Outlook, direkt "email-srvr.com" zu nutzen statt "mail.domain.tld"
|
ensure_record "CNAME" "autodiscover.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false
|
||||||
# Format: _service._proto.name TTL class SRV priority weight port target
|
ensure_record "CNAME" "autoconfig.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false
|
||||||
|
|
||||||
# IMAP SRV
|
# ------------------------------------------------------------------
|
||||||
ensure_record "SRV" "_imap._tcp.$DOMAIN_NAME" "0 5 143 $TARGET_MAIL_SERVER" false
|
# SCHRITT 10: SRV Records
|
||||||
# IMAPS SRV (Port 993)
|
# ------------------------------------------------------------------
|
||||||
ensure_record "SRV" "_imaps._tcp.$DOMAIN_NAME" "0 5 993 $TARGET_MAIL_SERVER" false
|
echo ""
|
||||||
# SUBMISSION SRV (Port 587)
|
echo "--- 10. SRV Records ---"
|
||||||
ensure_record "SRV" "_submission._tcp.$DOMAIN_NAME" "0 5 587 $TARGET_MAIL_SERVER" false
|
if [ "$SKIP_CLIENT_DNS" = "true" ]; then
|
||||||
|
echo " ⏭️ Übersprungen (SKIP_CLIENT_DNS=true)"
|
||||||
echo " ✅ SRV Records gesetzt (Server: $TARGET_MAIL_SERVER)"
|
else
|
||||||
|
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
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "✅ Fertig."
|
echo "============================================================"
|
||||||
|
echo "✅ Fertig für Domain: $DOMAIN_NAME"
|
||||||
|
if [ "$SKIP_CLIENT_DNS" = "true" ]; then
|
||||||
|
echo ""
|
||||||
|
echo " ⚠️ Client-Subdomains wurden NICHT geändert."
|
||||||
|
echo " Nach MX-Cutover + Worker-Validierung erneut ausführen mit:"
|
||||||
|
echo " SKIP_CLIENT_DNS=false SKIP_DMARC=false ./cloudflareMigrationDns.sh"
|
||||||
|
fi
|
||||||
|
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 "============================================================"
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
email {env.CLOUDFLARE_EMAIL}
|
||||||
|
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
|
||||||
|
acme_ca https://acme-v02.api.letsencrypt.org/directory
|
||||||
|
debug
|
||||||
|
}
|
||||||
|
import mail_certs
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
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:
|
||||||
|
- mail_network
|
||||||
|
volumes:
|
||||||
|
- $PWD/Caddyfile:/etc/caddy/Caddyfile
|
||||||
|
- $PWD/mail_certs:/etc/caddy/mail_certs
|
||||||
|
- $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:
|
||||||
|
mail_network:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
caddy_data:
|
||||||
|
external: true
|
||||||
|
caddy_config:
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?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.
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -0,0 +1,122 @@
|
||||||
|
<!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>
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
# email_autodiscover - Dynamisches Autodiscover/Autoconfig Snippet
|
||||||
|
# Importiert im Caddyfile via: import email_autodiscover
|
||||||
|
#
|
||||||
|
# Funktioniert mit JEDER Domain automatisch, solange der Caddy-Block
|
||||||
|
# auf autodiscover.<domain> oder autoconfig.<domain> hört.
|
||||||
|
#
|
||||||
|
# Hostnames werden dynamisch abgeleitet:
|
||||||
|
# autodiscover.cielectrical.com → imap.cielectrical.com / smtp.cielectrical.com
|
||||||
|
# autoconfig.bayarea-cc.com → imap.bayarea-cc.com / smtp.bayarea-cc.com
|
||||||
|
#
|
||||||
|
# {labels.2}.{labels.1} extrahiert die Basisdomain aus dem Host:
|
||||||
|
# autodiscover.cielectrical.com → labels: [com=0, cielectrical=1, autodiscover=2]
|
||||||
|
# → {labels.1}.{labels.0} = cielectrical.com
|
||||||
|
|
||||||
|
(email_settings) {
|
||||||
|
# 1. Outlook Autodiscover (XML)
|
||||||
|
route /autodiscover/autodiscover.xml {
|
||||||
|
header Content-Type "application/xml"
|
||||||
|
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>imap.{labels.1}.{labels.0}</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>pop.{labels.1}.{labels.0}</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>smtp.{labels.1}.{labels.0}</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. Modern Outlook (JSON) - Redirect zum XML Endpoint
|
||||||
|
route /autodiscover/autodiscover.json {
|
||||||
|
header Content-Type "application/json"
|
||||||
|
respond `{
|
||||||
|
"Protocol": "AutodiscoverV1",
|
||||||
|
"Url": "https://autodiscover.{labels.1}.{labels.0}/autodiscover/autodiscover.xml"
|
||||||
|
}` 200
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. Thunderbird Autoconfig
|
||||||
|
route /mail/config-v1.1.xml {
|
||||||
|
header Content-Type "application/xml"
|
||||||
|
respond `<?xml version="1.0"?>
|
||||||
|
<clientConfig version="1.1">
|
||||||
|
<emailProvider id="{labels.1}.{labels.0}">
|
||||||
|
<displayName>{labels.1}.{labels.0} Mail</displayName>
|
||||||
|
<domain>{labels.1}.{labels.0}</domain>
|
||||||
|
<incomingServer type="imap">
|
||||||
|
<hostname>imap.{labels.1}.{labels.0}</hostname>
|
||||||
|
<port>993</port>
|
||||||
|
<socketType>SSL</socketType>
|
||||||
|
<authentication>password-cleartext</authentication>
|
||||||
|
<username>%EMAILADDRESS%</username>
|
||||||
|
</incomingServer>
|
||||||
|
<incomingServer type="pop3">
|
||||||
|
<hostname>pop.{labels.1}.{labels.0}</hostname>
|
||||||
|
<port>995</port>
|
||||||
|
<socketType>SSL</socketType>
|
||||||
|
<authentication>password-cleartext</authentication>
|
||||||
|
<username>%EMAILADDRESS%</username>
|
||||||
|
</incomingServer>
|
||||||
|
<outgoingServer type="smtp">
|
||||||
|
<hostname>smtp.{labels.1}.{labels.0}</hostname>
|
||||||
|
<port>465</port>
|
||||||
|
<socketType>SSL</socketType>
|
||||||
|
<authentication>password-cleartext</authentication>
|
||||||
|
<username>%EMAILADDRESS%</username>
|
||||||
|
</outgoingServer>
|
||||||
|
</emailProvider>
|
||||||
|
</clientConfig>` 200
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4. Apple MobileConfig
|
||||||
|
route /apple {
|
||||||
|
templates {
|
||||||
|
mime "application/x-apple-aspen-config"
|
||||||
|
}
|
||||||
|
header Content-Type "application/x-apple-aspen-config; charset=utf-8"
|
||||||
|
root * /etc/caddy
|
||||||
|
rewrite * /email.mobileconfig.tpl
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,288 @@
|
||||||
|
#!/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)
|
||||||
|
#
|
||||||
|
# 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 einbetten ---
|
||||||
|
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"
|
||||||
|
OUTPUT="${OUTPUT} # Outlook Autodiscover (XML)\n"
|
||||||
|
OUTPUT="${OUTPUT} route /autodiscover/autodiscover.xml {\n"
|
||||||
|
OUTPUT="${OUTPUT} header Content-Type \"application/xml\"\n"
|
||||||
|
OUTPUT="${OUTPUT} respond \`<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
|
||||||
|
OUTPUT="${OUTPUT}<Autodiscover xmlns=\"http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006\">\n"
|
||||||
|
OUTPUT="${OUTPUT} <Response xmlns=\"http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a\">\n"
|
||||||
|
OUTPUT="${OUTPUT} <Account>\n"
|
||||||
|
OUTPUT="${OUTPUT} <AccountType>email</AccountType>\n"
|
||||||
|
OUTPUT="${OUTPUT} <Action>settings</Action>\n"
|
||||||
|
OUTPUT="${OUTPUT} <Protocol>\n"
|
||||||
|
OUTPUT="${OUTPUT} <Type>IMAP</Type>\n"
|
||||||
|
OUTPUT="${OUTPUT} <Server>imap.{labels.1}.{labels.0}</Server>\n"
|
||||||
|
OUTPUT="${OUTPUT} <Port>993</Port>\n"
|
||||||
|
OUTPUT="${OUTPUT} <DomainRequired>on</DomainRequired>\n"
|
||||||
|
OUTPUT="${OUTPUT} <LoginName>{header.X-Anchormailbox}</LoginName>\n"
|
||||||
|
OUTPUT="${OUTPUT} <SPA>off</SPA>\n"
|
||||||
|
OUTPUT="${OUTPUT} <SSL>on</SSL>\n"
|
||||||
|
OUTPUT="${OUTPUT} <AuthRequired>on</AuthRequired>\n"
|
||||||
|
OUTPUT="${OUTPUT} </Protocol>\n"
|
||||||
|
OUTPUT="${OUTPUT} <Protocol>\n"
|
||||||
|
OUTPUT="${OUTPUT} <Type>POP3</Type>\n"
|
||||||
|
OUTPUT="${OUTPUT} <Server>pop.{labels.1}.{labels.0}</Server>\n"
|
||||||
|
OUTPUT="${OUTPUT} <Port>995</Port>\n"
|
||||||
|
OUTPUT="${OUTPUT} <DomainRequired>on</DomainRequired>\n"
|
||||||
|
OUTPUT="${OUTPUT} <LoginName>{header.X-Anchormailbox}</LoginName>\n"
|
||||||
|
OUTPUT="${OUTPUT} <SPA>off</SPA>\n"
|
||||||
|
OUTPUT="${OUTPUT} <SSL>on</SSL>\n"
|
||||||
|
OUTPUT="${OUTPUT} <AuthRequired>on</AuthRequired>\n"
|
||||||
|
OUTPUT="${OUTPUT} </Protocol>\n"
|
||||||
|
OUTPUT="${OUTPUT} <Protocol>\n"
|
||||||
|
OUTPUT="${OUTPUT} <Type>SMTP</Type>\n"
|
||||||
|
OUTPUT="${OUTPUT} <Server>smtp.{labels.1}.{labels.0}</Server>\n"
|
||||||
|
OUTPUT="${OUTPUT} <Port>465</Port>\n"
|
||||||
|
OUTPUT="${OUTPUT} <DomainRequired>on</DomainRequired>\n"
|
||||||
|
OUTPUT="${OUTPUT} <LoginName>{header.X-Anchormailbox}</LoginName>\n"
|
||||||
|
OUTPUT="${OUTPUT} <SPA>off</SPA>\n"
|
||||||
|
OUTPUT="${OUTPUT} <SSL>on</SSL>\n"
|
||||||
|
OUTPUT="${OUTPUT} <AuthRequired>on</AuthRequired>\n"
|
||||||
|
OUTPUT="${OUTPUT} </Protocol>\n"
|
||||||
|
OUTPUT="${OUTPUT} </Account>\n"
|
||||||
|
OUTPUT="${OUTPUT} </Response>\n"
|
||||||
|
OUTPUT="${OUTPUT}</Autodiscover>\` 200\n"
|
||||||
|
OUTPUT="${OUTPUT} }\n"
|
||||||
|
OUTPUT="${OUTPUT}\n"
|
||||||
|
OUTPUT="${OUTPUT} # Modern Outlook (JSON)\n"
|
||||||
|
OUTPUT="${OUTPUT} route /autodiscover/autodiscover.json {\n"
|
||||||
|
OUTPUT="${OUTPUT} header Content-Type \"application/json\"\n"
|
||||||
|
OUTPUT="${OUTPUT} respond \`{\n"
|
||||||
|
OUTPUT="${OUTPUT} \"Protocol\": \"AutodiscoverV1\",\n"
|
||||||
|
OUTPUT="${OUTPUT} \"Url\": \"https://autodiscover.{labels.1}.{labels.0}/autodiscover/autodiscover.xml\"\n"
|
||||||
|
OUTPUT="${OUTPUT} }\` 200\n"
|
||||||
|
OUTPUT="${OUTPUT} }\n"
|
||||||
|
OUTPUT="${OUTPUT}\n"
|
||||||
|
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 \`<?xml version=\"1.0\"?>\n"
|
||||||
|
OUTPUT="${OUTPUT}<clientConfig version=\"1.1\">\n"
|
||||||
|
OUTPUT="${OUTPUT} <emailProvider id=\"{labels.1}.{labels.0}\">\n"
|
||||||
|
OUTPUT="${OUTPUT} <displayName>{labels.1}.{labels.0} Mail</displayName>\n"
|
||||||
|
OUTPUT="${OUTPUT} <domain>{labels.1}.{labels.0}</domain>\n"
|
||||||
|
OUTPUT="${OUTPUT} <incomingServer type=\"imap\">\n"
|
||||||
|
OUTPUT="${OUTPUT} <hostname>imap.{labels.1}.{labels.0}</hostname>\n"
|
||||||
|
OUTPUT="${OUTPUT} <port>993</port>\n"
|
||||||
|
OUTPUT="${OUTPUT} <socketType>SSL</socketType>\n"
|
||||||
|
OUTPUT="${OUTPUT} <authentication>password-cleartext</authentication>\n"
|
||||||
|
OUTPUT="${OUTPUT} <username>%%EMAILADDRESS%%</username>\n"
|
||||||
|
OUTPUT="${OUTPUT} </incomingServer>\n"
|
||||||
|
OUTPUT="${OUTPUT} <incomingServer type=\"pop3\">\n"
|
||||||
|
OUTPUT="${OUTPUT} <hostname>pop.{labels.1}.{labels.0}</hostname>\n"
|
||||||
|
OUTPUT="${OUTPUT} <port>995</port>\n"
|
||||||
|
OUTPUT="${OUTPUT} <socketType>SSL</socketType>\n"
|
||||||
|
OUTPUT="${OUTPUT} <authentication>password-cleartext</authentication>\n"
|
||||||
|
OUTPUT="${OUTPUT} <username>%%EMAILADDRESS%%</username>\n"
|
||||||
|
OUTPUT="${OUTPUT} </incomingServer>\n"
|
||||||
|
OUTPUT="${OUTPUT} <outgoingServer type=\"smtp\">\n"
|
||||||
|
OUTPUT="${OUTPUT} <hostname>smtp.{labels.1}.{labels.0}</hostname>\n"
|
||||||
|
OUTPUT="${OUTPUT} <port>465</port>\n"
|
||||||
|
OUTPUT="${OUTPUT} <socketType>SSL</socketType>\n"
|
||||||
|
OUTPUT="${OUTPUT} <authentication>password-cleartext</authentication>\n"
|
||||||
|
OUTPUT="${OUTPUT} <username>%%EMAILADDRESS%%</username>\n"
|
||||||
|
OUTPUT="${OUTPUT} </outgoingServer>\n"
|
||||||
|
OUTPUT="${OUTPUT} </emailProvider>\n"
|
||||||
|
OUTPUT="${OUTPUT}</clientConfig>\` 200\n"
|
||||||
|
OUTPUT="${OUTPUT} }\n"
|
||||||
|
OUTPUT="${OUTPUT}\n"
|
||||||
|
OUTPUT="${OUTPUT} # Apple MobileConfig\n"
|
||||||
|
OUTPUT="${OUTPUT} route /apple {\n"
|
||||||
|
OUTPUT="${OUTPUT} templates {\n"
|
||||||
|
OUTPUT="${OUTPUT} mime \"application/x-apple-aspen-config\"\n"
|
||||||
|
OUTPUT="${OUTPUT} }\n"
|
||||||
|
OUTPUT="${OUTPUT} header Content-Type \"application/x-apple-aspen-config; charset=utf-8\"\n"
|
||||||
|
OUTPUT="${OUTPUT} root * /etc/caddy\n"
|
||||||
|
OUTPUT="${OUTPUT} rewrite * /email.mobileconfig.tpl\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}"
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
OUTPUT="${OUTPUT}# Roundcube Webmail für $domain\n"
|
||||||
|
OUTPUT="${OUTPUT}webmail.${domain} {\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 hartcodierte Autodiscover-Blöcke existieren ---
|
||||||
|
if [ -f "$CADDYFILE" ]; then
|
||||||
|
if grep -q "autodiscover\.bayarea-cc\.com\|autodiscover\.bizmatch\.net\|autodiscover\.ruehrgedoens\.de" "$CADDYFILE"; then
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ AUFRÄUMEN: Alte hartcodierte Autodiscover-Blöcke im Caddyfile gefunden!"
|
||||||
|
echo " Diese werden jetzt dynamisch über mail_certs generiert."
|
||||||
|
echo " Bitte den alten 'Block A' manuell aus dem Caddyfile entfernen:"
|
||||||
|
echo " → autodiscover.bayarea-cc.com, autodiscover.bizmatch.net, ..."
|
||||||
|
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 ""
|
||||||
|
echo "5. Autodiscover testen:"
|
||||||
|
for domain in $DOMAINS; do
|
||||||
|
echo " curl -s https://autoconfig.${domain}/mail/config-v1.1.xml | head -5"
|
||||||
|
done
|
||||||
|
echo "============================================================"
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -28,13 +28,13 @@ export class BlocklistChecker {
|
||||||
*/
|
*/
|
||||||
async batchCheckBlockedSenders(
|
async batchCheckBlockedSenders(
|
||||||
recipients: string[],
|
recipients: string[],
|
||||||
sender: string,
|
senders: string[], // <-- Geändert zu Array
|
||||||
workerName: string,
|
workerName: string,
|
||||||
): Promise<Record<string, boolean>> {
|
): Promise<Record<string, boolean>> {
|
||||||
const patternsByRecipient =
|
const patternsByRecipient = await this.dynamodb.batchGetBlockedPatterns(recipients);
|
||||||
await this.dynamodb.batchGetBlockedPatterns(recipients);
|
|
||||||
|
|
||||||
const senderClean = extractAddress(sender);
|
// Alle übergebenen Adressen bereinigen
|
||||||
|
const sendersClean = senders.map(s => extractAddress(s)).filter(Boolean);
|
||||||
const result: Record<string, boolean> = {};
|
const result: Record<string, boolean> = {};
|
||||||
|
|
||||||
for (const recipient of recipients) {
|
for (const recipient of recipients) {
|
||||||
|
|
@ -42,10 +42,10 @@ export class BlocklistChecker {
|
||||||
let isBlocked = false;
|
let isBlocked = false;
|
||||||
|
|
||||||
for (const pattern of patterns) {
|
for (const pattern of patterns) {
|
||||||
|
for (const senderClean of sendersClean) {
|
||||||
if (picomatch.isMatch(senderClean, pattern.toLowerCase())) {
|
if (picomatch.isMatch(senderClean, pattern.toLowerCase())) {
|
||||||
log(
|
log(
|
||||||
`⛔ BLOCKED: Sender ${senderClean} matches pattern '${pattern}' ` +
|
`⛔ BLOCKED: Sender ${senderClean} matches pattern '${pattern}' for inbox ${recipient}`,
|
||||||
`for inbox ${recipient}`,
|
|
||||||
'WARNING',
|
'WARNING',
|
||||||
workerName,
|
workerName,
|
||||||
);
|
);
|
||||||
|
|
@ -53,10 +53,10 @@ export class BlocklistChecker {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isBlocked) break;
|
||||||
|
}
|
||||||
result[recipient] = isBlocked;
|
result[recipient] = isBlocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -9,13 +9,13 @@
|
||||||
|
|
||||||
import { createTransport } from 'nodemailer';
|
import { createTransport } from 'nodemailer';
|
||||||
import type { ParsedMail } from 'mailparser';
|
import type { ParsedMail } from 'mailparser';
|
||||||
import type { DynamoDBHandler, EmailRule } from '../aws/dynamodb.js';
|
|
||||||
import type { SESHandler } from '../aws/ses.js';
|
import type { SESHandler } from '../aws/ses.js';
|
||||||
import { extractBodyParts } from './parser.js';
|
import { extractBodyParts } from './parser.js';
|
||||||
import { config, isInternalAddress } from '../config.js';
|
|
||||||
import { log } from '../logger.js';
|
import { log } from '../logger.js';
|
||||||
// Wir nutzen MailComposer direkt für das Erstellen der Raw Bytes
|
// Wir nutzen MailComposer direkt für das Erstellen der Raw Bytes
|
||||||
import MailComposer from 'nodemailer/lib/mail-composer/index.js';
|
import MailComposer from 'nodemailer/lib/mail-composer/index.js';
|
||||||
|
import { DynamoDBHandler, EmailRule } from '../aws/dynamodb.js';
|
||||||
|
import { config, isInternalAddress } from '../config.js';
|
||||||
|
|
||||||
export type MetricsCallback = (action: 'autoreply' | 'forward', domain: string) => void;
|
export type MetricsCallback = (action: 'autoreply' | 'forward', domain: string) => void;
|
||||||
|
|
||||||
|
|
@ -13,7 +13,7 @@ import { config, loadDomains } from './config.js';
|
||||||
import { log } from './logger.js';
|
import { log } from './logger.js';
|
||||||
import { startMetricsServer, type MetricsCollector } from './metrics.js';
|
import { startMetricsServer, type MetricsCollector } from './metrics.js';
|
||||||
import { startHealthServer } from './health.js';
|
import { startHealthServer } from './health.js';
|
||||||
import { UnifiedWorker } from './worker/index.js';
|
import { UnifiedWorker } from './worker/unified-worker.js';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Banner
|
// Banner
|
||||||
|
|
@ -8,8 +8,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createTransport, type Transporter } from 'nodemailer';
|
import { createTransport, type Transporter } from 'nodemailer';
|
||||||
import { config } from '../config.js';
|
|
||||||
import { log } from '../logger.js';
|
import { log } from '../logger.js';
|
||||||
|
import { config } from '../config.js';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Permanent error detection
|
// Permanent error detection
|
||||||
|
|
@ -9,9 +9,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { SQSHandler } from '../aws/sqs.js';
|
import type { SQSHandler } from '../aws/sqs.js';
|
||||||
import type { MessageProcessor } from './message-processor.js';
|
|
||||||
import type { MetricsCollector } from '../metrics.js';
|
import type { MetricsCollector } from '../metrics.js';
|
||||||
import { log } from '../logger.js';
|
import { log } from '../logger.js';
|
||||||
|
import { MessageProcessor } from './message-processor.js';
|
||||||
|
|
||||||
export interface DomainPollerStats {
|
export interface DomainPollerStats {
|
||||||
domain: string;
|
domain: string;
|
||||||
|
|
@ -19,15 +19,12 @@ import type { SESHandler } from '../aws/ses.js';
|
||||||
import type { DynamoDBHandler } from '../aws/dynamodb.js';
|
import type { DynamoDBHandler } from '../aws/dynamodb.js';
|
||||||
import type { EmailDelivery } from '../smtp/delivery.js';
|
import type { EmailDelivery } from '../smtp/delivery.js';
|
||||||
import type { MetricsCollector } from '../metrics.js';
|
import type { MetricsCollector } from '../metrics.js';
|
||||||
import {
|
|
||||||
parseEmail,
|
|
||||||
isProcessedByWorker,
|
|
||||||
BounceHandler,
|
|
||||||
RulesProcessor,
|
|
||||||
BlocklistChecker,
|
|
||||||
} from '../email/index.js';
|
|
||||||
import { domainToBucketName } from '../config.js';
|
|
||||||
import { log } from '../logger.js';
|
import { log } from '../logger.js';
|
||||||
|
import { BlocklistChecker } from '../email/blocklist.js';
|
||||||
|
import { BounceHandler } from '../email/bounce-handler.js';
|
||||||
|
import { parseEmail, isProcessedByWorker } from '../email/parser.js';
|
||||||
|
import { RulesProcessor } from '../email/rules-processor.js';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Processor
|
// Processor
|
||||||
|
|
@ -182,9 +179,17 @@ export class MessageProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. BLOCKLIST CHECK
|
// 6. BLOCKLIST CHECK
|
||||||
|
const sendersToCheck: string[] = [];
|
||||||
|
if (fromAddrFinal) sendersToCheck.push(fromAddrFinal);
|
||||||
|
|
||||||
|
const headerFrom = parsedFinal?.from?.text;
|
||||||
|
if (headerFrom && !sendersToCheck.includes(headerFrom)) {
|
||||||
|
sendersToCheck.push(headerFrom);
|
||||||
|
}
|
||||||
|
|
||||||
const blockedByRecipient = await this.blocklist.batchCheckBlockedSenders(
|
const blockedByRecipient = await this.blocklist.batchCheckBlockedSenders(
|
||||||
recipients,
|
recipients,
|
||||||
fromAddrFinal,
|
sendersToCheck, // <-- Array übergeben
|
||||||
workerName,
|
workerName,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -8,13 +8,17 @@
|
||||||
* - Graceful shutdown
|
* - Graceful shutdown
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { S3Handler, SQSHandler, SESHandler, DynamoDBHandler } from '../aws/index.js';
|
import { DynamoDBHandler } from '../aws/dynamodb';
|
||||||
import { EmailDelivery } from '../smtp/index.js';
|
import { S3Handler} from '../aws/s3.js';
|
||||||
|
import { SQSHandler} from '../aws/sqs.js'
|
||||||
|
import { SESHandler } from '../aws/ses';
|
||||||
|
import { EmailDelivery } from '../smtp/delivery.js';
|
||||||
import { MessageProcessor } from './message-processor.js';
|
import { MessageProcessor } from './message-processor.js';
|
||||||
import { DomainPoller, type DomainPollerStats } from './domain-poller.js';
|
import { DomainPoller, type DomainPollerStats } from './domain-poller.js';
|
||||||
import type { MetricsCollector } from '../metrics.js';
|
import type { MetricsCollector } from '../metrics.js';
|
||||||
import { log } from '../logger.js';
|
import { log } from '../logger.js';
|
||||||
|
|
||||||
|
|
||||||
export class UnifiedWorker {
|
export class UnifiedWorker {
|
||||||
private pollers: DomainPoller[] = [];
|
private pollers: DomainPoller[] = [];
|
||||||
private processor: MessageProcessor;
|
private processor: MessageProcessor;
|
||||||
|
|
@ -10,7 +10,6 @@ ENV/
|
||||||
.venv
|
.venv
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs/
|
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue