From b072083318829f261074ab212a83b8bb02cab256 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 12:19:34 -0600 Subject: [PATCH 01/74] caddy --- caddy/Caddyfile | 303 +++++++++++++++++++++++++++++ caddy/Dockerfile.caddy | 13 ++ caddy/docker-compose.yml | 51 +++++ caddy/email-setup/autodiscover.xml | 29 +++ caddy/email-setup/logo.png | Bin 0 -> 1923 bytes caddy/email-setup/setup.html | 122 ++++++++++++ caddy/email.mobileconfig.tpl | 67 +++++++ caddy/email_autodiscover | 97 +++++++++ 8 files changed, 682 insertions(+) create mode 100644 caddy/Caddyfile create mode 100644 caddy/Dockerfile.caddy create mode 100644 caddy/docker-compose.yml create mode 100644 caddy/email-setup/autodiscover.xml create mode 100644 caddy/email-setup/logo.png create mode 100644 caddy/email-setup/setup.html create mode 100644 caddy/email.mobileconfig.tpl create mode 100644 caddy/email_autodiscover diff --git a/caddy/Caddyfile b/caddy/Caddyfile new file mode 100644 index 0000000..eb167b5 --- /dev/null +++ b/caddy/Caddyfile @@ -0,0 +1,303 @@ +{ + email {env.CLOUDFLARE_EMAIL} + acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN} + acme_ca https://acme-v02.api.letsencrypt.org/directory + debug +} +import email_autodiscover +# --------------------------------------------------------- +# Block A: Die dedizierten Autodiscover Domains +# --------------------------------------------------------- +autodiscover.bayarea-cc.com, autodiscover.bizmatch.net, +autodiscover.ruehrgedoens.de, autoconfig.ruehrgedoens.de, +autoconfig.bayarea-cc.com, autoconfig.bizmatch.net { + + # Hier rufen wir das Snippet auf + import email_settings + + # Fallback für Aufrufe auf Root dieser Subdomains + respond "Autodiscover Service Online" 200 +} + +# Prod: Neue Domains +www.bizmatch.net { + handle /pictures/* { + root * /home/aknuth/git/bizmatch-project/bizmatch-server # Prod-Ordner + file_server + } + # Statische Dateien (CSS, JS, Bilder) – lange cachen, da sich der Name bei Änderungen ändert + header /assets/* Cache-Control "public, max-age=31536000, immutable" + header /*.css Cache-Control "public, max-age=31536000, immutable" + header /*.js Cache-Control "public, max-age=31536000, immutable" + + # Die index.html und API-Antworten – NIEMALS cachen + header /index.html Cache-Control "no-cache, no-store, must-revalidate" + + #handle { + # root * /home/aknuth/git/bizmatch-project-prod/bizmatch/dist/bizmatch/browser # Neuer Prod-Dist-Ordner + # try_files {path} {path}/ /index.html + # file_server + #} + handle { + reverse_proxy host.docker.internal:4200 + } + log { + output file /var/log/caddy/access.prod.log # Separate Logs + } + encode gzip zstd +} +bizmatch.net { + redir https://www.bizmatch.net{uri} permanent + import email_settings +} +www.qrmaster.net { + handle { + reverse_proxy host.docker.internal:3050 + } + log { + output file /var/log/caddy/qrmaster.log + format console + } + encode gzip +} +qrmaster.net { + redir https://www.qrmaster.net{uri} permanent +} +bayarea-cc.com { + # TLS-Direktive entfernen, falls Cloudflare die Verbindung terminiert + # tls { + # dns cloudflare {env.CLOUDFLARE_API_TOKEN} + # } + + handle /api { + reverse_proxy host.docker.internal:3001 + } + handle { + root * /app + try_files {path} /index.html + file_server + } + log { + output stderr + format console + } + encode gzip + import email_settings +} +www.bayarea-cc.com { + redir https://bayarea-cc.com{uri} permanent +} +setup.bayarea-cc.com { + # Wir setzen das Root-Verzeichnis auf den neuen Pfad im Container + root * /var/www/email-setup + + # Webserver-Standardverhalten + file_server + + # Wenn jemand nur die Domain aufruft, zeige setup.html + try_files {path} /setup.html +} +cielectrical.bayarea-cc.com { + # wenn du API innerhalb von Next bedienst, weiterleiten an den Next Prozess + handle { + reverse_proxy host.docker.internal:3000 + } + log { + output file /var/log/caddy/cielectrical.log + format console + } + encode gzip +} +hamptonbrown.bayarea-cc.com { + # wenn du API innerhalb von Next bedienst, weiterleiten an den Next Prozess + handle { + reverse_proxy host.docker.internal:3010 + } + log { + output file /var/log/caddy/hamptonbrown.log + format console + } + encode gzip +} +nqsltd.bayarea-cc.com { + # wenn du API innerhalb von Next bedienst, weiterleiten an den Next Prozess + handle { + reverse_proxy host.docker.internal:3020 + } + log { + output file /var/log/caddy/nqsltd.log + format console + } + encode gzip +} +gregknoppcpa.bayarea-cc.com { + # wenn du API innerhalb von Next bedienst, weiterleiten an den Next Prozess + handle { + reverse_proxy host.docker.internal:3030 + } + log { + output file /var/log/caddy/gregknoppcpa.log + format console + } + encode gzip +} +buddelectric.bayarea-cc.com { + # wenn du API innerhalb von Next bedienst, weiterleiten an den Next Prozess + handle { + reverse_proxy host.docker.internal:3040 + } + log { + output file /var/log/caddy/buddelectric.log + format console + } + encode gzip zstd +} +iitwelders.bayarea-cc.com { + # wenn du API innerhalb von Next bedienst, weiterleiten an den Next Prozess + handle { + reverse_proxy host.docker.internal:8080 + } + log { + output file /var/log/caddy/iitwelders.log + format console + } + encode gzip +} +fancytextstuff.com { + # wenn du API innerhalb von Next bedienst, weiterleiten an den Next Prozess + handle { + reverse_proxy host.docker.internal:3010 + } + log { + output file /var/log/caddy/fancytext.log + format console + } + encode gzip +} +www.fancytextstuff.com { + redir https://fancytextstuff.com{uri} permanent +} +auth.bizmatch.net { + reverse_proxy https://bizmatch-net.firebaseapp.com { + header_up Host bizmatch-net.firebaseapp.com + header_up X-Forwarded-For {remote_host} + header_up X-Forwarded-Proto {scheme} + header_up X-Real-IP {remote_host} + } +} +gitea.bizmatch.net { + reverse_proxy gitea:3500 +} + +dev.bizmatch.net { + handle /pictures/* { + root * /home/aknuth/git/bizmatch-project/bizmatch-server + file_server + } + + handle { + root * /home/aknuth/git/bizmatch-project/bizmatch/dist/bizmatch/browser + try_files {path} {path}/ /index.html + file_server + } + + log { + output file /var/log/caddy/access.log { + roll_size 10MB + roll_keep 5 + roll_keep_for 48h + } + } + + encode gzip + +} + + +api.bizmatch.net { + reverse_proxy host.docker.internal:3001 { # Neu: Proxy auf Prod-Port 3001 + header_up X-Real-IP {http.request.header.CF-Connecting-IP} + header_up X-Forwarded-For {http.request.header.CF-Connecting-IP} + header_up X-Forwarded-Proto {http.request.header.X-Forwarded-Proto} + header_up CF-IPCountry {http.request.header.CF-IPCountry} + } +} +mailsync.bizmatch.net { + reverse_proxy host.docker.internal:5000 { + header_up X-Real-IP {http.request.header.CF-Connecting-IP} + header_up X-Forwarded-For {http.request.header.CF-Connecting-IP} + header_up X-Forwarded-Proto {http.request.header.X-Forwarded-Proto} + header_up CF-IPCountry {http.request.header.CF-IPCountry} + } +} + +# Roundcube für docker-mailserver +app.email-bayarea.com { + reverse_proxy roundcube:80 + + log { + output stderr + format console + } + + encode gzip +} +# Roundcube für docker-mailserver +config.email-bayarea.com { + + root * /home/aknuth/git/config-email/frontend/dist + try_files {path} {path}/ /index.html + file_server + + log { + output file /var/log/caddy/config-email.log + } + + encode gzip +} +# Roundcube für docker-mailserver +api.email-bayarea.com { + reverse_proxy host.docker.internal:3002 + + log { + output stderr + format console + } + + encode gzip +} +annavillesda.org { + # API requests to backend + handle /api/* { + reverse_proxy host.docker.internal:3070 + } + + # Frontend static files + handle { + root * /home/aknuth/git/annaville-sda-site/dist + try_files {path} {path}/ /index.html + file_server + } + + log { + output file /var/log/caddy/access.prod.log + } + + encode gzip +} +www.annavillesda.org { + redir https://annavillesda.org{uri} permanent +} +# ----------------- +# just for certificate generation +# ----------------- +mail.andreasknuth.de { + reverse_proxy nginx-mailcow:8080 +} +web.email-bayarea.com { + reverse_proxy nginx-mailcow:8080 +} +# Dieser Block dient nur dazu, das Zertifikat für den Mailserver zu beschaffen/erneuern. +mail.email-srvr.com { + respond "Mailserver Certificate Authority is running." 200 +} diff --git a/caddy/Dockerfile.caddy b/caddy/Dockerfile.caddy new file mode 100644 index 0000000..66d36f1 --- /dev/null +++ b/caddy/Dockerfile.caddy @@ -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 + diff --git a/caddy/docker-compose.yml b/caddy/docker-compose.yml new file mode 100644 index 0000000..bbb49e9 --- /dev/null +++ b/caddy/docker-compose.yml @@ -0,0 +1,51 @@ +services: + caddy: + image: custom-caddy:2.9.1-rr1 + container_name: caddy + build: + context: . + dockerfile: Dockerfile.caddy + restart: unless-stopped + ports: + - "80:80" + - "443:443" + extra_hosts: + - 'host.docker.internal:host-gateway' + networks: + - bizmatch + - keycloak + - gitea + - mail_network + volumes: + - $PWD/Caddyfile:/etc/caddy/Caddyfile + - $PWD/email_autodiscover:/etc/caddy/email_autodiscover + - $PWD/email.mobileconfig.tpl:/etc/caddy/email.mobileconfig.tpl + - $PWD/email-setup:/var/www/email-setup + - caddy_data:/data + - caddy_config:/config + - /home/aknuth/git/bizmatch-project/bizmatch/dist/bizmatch/browser:/home/aknuth/git/bizmatch-project/bizmatch/dist/bizmatch/browser + - /home/aknuth/git/bizmatch-project-prod/bizmatch/dist/bizmatch/browser:/home/aknuth/git/bizmatch-project-prod/bizmatch/dist/bizmatch/browser + - /home/aknuth/git/bizmatch-project/bizmatch-server/pictures:/home/aknuth/git/bizmatch-project/bizmatch-server/pictures + - /home/aknuth/git/bizmatch-project-prod/bizmatch-server/pictures:/home/aknuth/git/bizmatch-project-prod/bizmatch-server/pictures + - /home/aknuth/git/annaville-sda-site/dist:/home/aknuth/git/annaville-sda-site/dist:ro # ← DAS FEHLT! + - /home/aknuth/git/bay-area-affiliates/dist/bay-area-affiliates/browser:/app + - /home/aknuth/log/caddy:/var/log/caddy + - /home/aknuth/git/config-email/frontend/dist:/home/aknuth/git/config-email/frontend/dist:ro + environment: + - CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN} + - CLOUDFLARE_EMAIL=${CLOUDFLARE_EMAIL} + +networks: + bizmatch: + external: true + keycloak: + external: true + gitea: + external: true + mail_network: + external: true + +volumes: + caddy_data: + external: true + caddy_config: diff --git a/caddy/email-setup/autodiscover.xml b/caddy/email-setup/autodiscover.xml new file mode 100644 index 0000000..b855f09 --- /dev/null +++ b/caddy/email-setup/autodiscover.xml @@ -0,0 +1,29 @@ + + + + + email + settings + + IMAP + mail.email-srvr.com + 993 + off + + off + on + on + + + SMTP + mail.email-srvr.com + 465 + off + + off + on + on + + + + \ No newline at end of file diff --git a/caddy/email-setup/logo.png b/caddy/email-setup/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..564e44346f9dda40a713a2401ac3d0adacb7bce2 GIT binary patch literal 1923 zcmbVNYgAL$65fXchXRT5Vl)9MK@m`B^Z_0RaIKkldz96OaHm_QzfK{^*~s*)y}&%r~>w-fL!k zxs-hYXhTy&0025D&@WW$Y3PCaEH8Oig;=24g9!N}#jGFYt*lptToZq0)?K*B z3rUfM$3k#Dzq5myU{pE8vJ#06#_-7`C4PPbCzT2_E+^IH%ZwgWsH`C`I3no>Vi)EO z3sEu+($pXYiWPAU=KKST$D~)#6V!=^9)U{=v+TZ6NXn976Cw?V4eJ7^YIYSS4&+n+ zfaNv`_E%R;;N75(nfZd5_?C>2=|;%`3gWzz0fR6+@1kj_9nfvsua=196AO9eo#&e- zJC%Lcf>9Vv4E>$cnD_Te@rzGAmTy;a^w&@moRuwuk<$g=h!&K!^W$?Vc_=e{QB6;5H3nSgdEbUV^G;F@|iI)Qv9NWOiP!Lc0_b;o*{T(5= zt$F`7Q{Hul<(dK*M#w!(GCJ~b*tZ9T;Jfp_r}rv|E;)CrC+|`rGah?#X~~l~O8Jo7 zC*VuiNMYUz0WqRRH>eQPaNaDbkZ5@Tv}5k1964jT?m9=$h`jR(wABTs(2(-(vyw~G z9L*;sQAg7AdR(RJ8QMSC9v}p1C**b?W(Mm+2Y<<1?fi(-wqP$1IhHWYLrjnFc zA*OfcY~HE)YQHP4Jbgc;TuhRrfHy2_6#GhMlFc$*|h8v#;* zpTKXe`pjc?>@GOnf=PILs!%<1jsv$j=M(g$`i$7KQ$IrDUd+pme2KxrO_~Es<2}Ze zHx_$x4mX8Tjg-1y(Gh75N2g5;|D+>09w*)2D+jb7{oNdPO zija^6Z{7er^yo=AWXCGa7u=8p4q;Ii0v&S@djGO54PiXPwY;{&lFM|>k*Y_J!+iTe zae{r1_4OxUBYUG!XK0`YxHFGgFTWXepo z0Frzpl)KdzpJ&Qj!SaoH3QWP3=-hL6WaJA1xSX%% zyq~@2?y+`v47Z)e2xo_a(e6O>oHSNF6z$t%M|$l9M12PMC6B~ZX?rG~HAq|2;wJBJ zp*I}`3B|9eF&`&p#NqYd+2#+9qbjqPpL`<3jPh*(hl%Abl8 z!f9Q72Y&%BsCYJxd@MJElP{#IrecV0{*wWs+dF)7C>8dHqZah+b=ZRII2-h8B zDhp9<3mkCskp1xhRsA>C>o4|h?NGNg>;7$&#l3YR9KRF96yKDPlaRGT6@x}k<3KHX znS)J=1+|LXe})IX8B>e6THN@cUVBHm2uE=x-I9n+-Z}N&t&vkQPF`SZ_3ai0i z!1&@nDo3^BNJLo_J(OpNxU_X}POSNYS*4QB^x#6j3PIu->`KFG7K%Dwib!>5Y(%(Y zmcL<1gHtR|3q_42FftXvN6GX?CNi1dltFvCLa3h09i{hGPD%0L+JuD=UVthW2Rlw? zK_>@x2?@{OT?RAHA1$2>t(LGhT;u|A$-B&p^Q6`#on(BkPoe1liRw?%>ieBA+o-#~ U$TjbrmQ_KJ|31GOAL^O^0NONNhX4Qo literal 0 HcmV?d00001 diff --git a/caddy/email-setup/setup.html b/caddy/email-setup/setup.html new file mode 100644 index 0000000..63c2b5d --- /dev/null +++ b/caddy/email-setup/setup.html @@ -0,0 +1,122 @@ + + + + + + Email Setup + + + + + +
+ + +
+

Email Setup

+

Enter your email address to automatically configure your iPhone or iPad.

+ +
Please enter a valid email address.
+ + + +
+ +
+

Scan me!

+

Open the Camera app on your iPhone and point it at this code.

+ +
+ +

+ Tap the banner that appears at the top.
+ Click "Allow" and then go to Settings to install the profile. +

+ +
+
+ + + + + \ No newline at end of file diff --git a/caddy/email.mobileconfig.tpl b/caddy/email.mobileconfig.tpl new file mode 100644 index 0000000..55c3d8e --- /dev/null +++ b/caddy/email.mobileconfig.tpl @@ -0,0 +1,67 @@ + + + + + PayloadContent + + + EmailAccountDescription + {{.Req.URL.Query.Get "email"}} + EmailAccountName + {{.Req.URL.Query.Get "email"}} + EmailAccountType + EmailTypeIMAP + EmailAddress + {{.Req.URL.Query.Get "email"}} + IncomingMailServerAuthentication + EmailAuthPassword + IncomingMailServerHostName + mail.email-srvr.com + IncomingMailServerPortNumber + 993 + IncomingMailServerUseSSL + + IncomingMailServerUsername + {{.Req.URL.Query.Get "email"}} + OutgoingMailServerAuthentication + EmailAuthPassword + OutgoingMailServerHostName + mail.email-srvr.com + OutgoingMailServerPortNumber + 465 + OutgoingMailServerUseSSL + + OutgoingMailServerUsername + {{.Req.URL.Query.Get "email"}} + PayloadDescription + E-Mail Konfiguration für {{.Req.URL.Query.Get "email"}} + PayloadDisplayName + {{.Req.URL.Query.Get "email"}} + PayloadIdentifier + com.email-srvr.profile.{{.Req.URL.Query.Get "email"}} + PayloadType + com.apple.mail.managed + PayloadUUID + {{uuidv4}} + PayloadVersion + 1 + + + PayloadDescription + Automatische E-Mail Einrichtung für {{.Req.URL.Query.Get "email"}} + PayloadDisplayName + E-Mail Einstellungen + PayloadIdentifier + com.email-srvr.profile.root + PayloadOrganization + IT Support + PayloadRemovalDisallowed + + PayloadType + Configuration + PayloadUUID + {{uuidv4}} + PayloadVersion + 1 + + \ No newline at end of file diff --git a/caddy/email_autodiscover b/caddy/email_autodiscover new file mode 100644 index 0000000..5f9b24f --- /dev/null +++ b/caddy/email_autodiscover @@ -0,0 +1,97 @@ +(email_settings) { + # 1. Autodiscover für Outlook + route /autodiscover/autodiscover.xml { + header Content-Type "application/xml" + # Wir nutzen {header.X-Anchormailbox} um die Email dynamisch einzufügen + respond ` + + + + email + settings + + IMAP + mail.email-srvr.com + 993 + on + {header.X-Anchormailbox} + off + on + on + + + POP3 + mail.email-srvr.com + 995 + on + {header.X-Anchormailbox} + off + on + on + + + SMTP + mail.email-srvr.com + 465 + on + {header.X-Anchormailbox} + off + on + on + + + +` 200 + } + + # 2. JSON Autodiscover (Modern Outlook) - bleibt gleich + route /autodiscover/autodiscover.json { + header Content-Type "application/json" + respond `{ + "Protocol": "AutodiscoverV1", + "Url": "https://autodiscover.bayarea-cc.com/autodiscover/autodiscover.xml" + }` 200 + } + + # 3. Thunderbird Autoconfig - bleibt gleich (dort funktioniert %EMAILADDRESS% ja nativ) + route /mail/config-v1.1.xml { + header Content-Type "application/xml" + respond ` + + + Rackspace Email + + mail.email-srvr.com + 993 + SSL + password-cleartext + %EMAILADDRESS% + + + mail.email-srvr.com + 465 + SSL + password-cleartext + %EMAILADDRESS% + + +` 200 + } + + # NEU: Apple MobileConfig Route + # Aufrufbar über: /apple?email=kunde@domain.de + route /apple { + # KORREKTUR: Wir müssen Caddy sagen, dass er diesen MIME-Type bearbeiten soll! + templates { + mime "application/x-apple-aspen-config" + } + + # Den richtigen MIME-Type setzen + header Content-Type "application/x-apple-aspen-config; charset=utf-8" + + # Pfad zur Datei im Container + root * /etc/caddy + rewrite * /email.mobileconfig.tpl + file_server + } +} \ No newline at end of file From ee19b5b659e527bda6683f75c20ae77d2ca46e69 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 12:58:24 -0600 Subject: [PATCH 02/74] changes --- DMS/setup-dms-tls.sh | 202 ++++++++++++++++++ basic_setup/cloudflareMigrationDns.sh | 154 +++++++------- caddy/Caddyfile | 282 -------------------------- caddy/docker-compose.yml | 7 - caddy/update-caddy-certs.sh | 137 +++++++++++++ 5 files changed, 413 insertions(+), 369 deletions(-) create mode 100755 DMS/setup-dms-tls.sh create mode 100755 caddy/update-caddy-certs.sh diff --git a/DMS/setup-dms-tls.sh b/DMS/setup-dms-tls.sh new file mode 100755 index 0000000..a3c166e --- /dev/null +++ b/DMS/setup-dms-tls.sh @@ -0,0 +1,202 @@ +#!/bin/bash +# setup-dms-tls.sh +# Generiert Dovecot und Postfix SNI-Konfigurationen für Multi-Domain TLS. +# Liest die vorhandenen Domains aus den DMS Accounts und erstellt: +# - docker-data/dms/config/dovecot-sni.cf (Dovecot SNI pro Domain) +# - docker-data/dms/config/postfix-main.cf (Postfix SNI Map + TLS Chain) +# +# Voraussetzung: +# - Caddy hat Wildcard-Certs gezogen (z.B. *.andreasknuth.de) +# - Cert-Verzeichnis ist gemountet unter /etc/mail/certs im Container +# - Konvention Cert-Pfad: /etc/mail/certs/DOMAIN_NAME/*.DOMAIN_NAME.crt|.key +# +# Usage: +# DMS_CONTAINER=mailserver ./setup-dms-tls.sh +# DMS_CONTAINER=mailserver DEFAULT_DOMAIN=email-srvr.com ./setup-dms-tls.sh + +set -e + +DMS_CONTAINER=${DMS_CONTAINER:-"mailserver"} +CONFIG_DIR=${CONFIG_DIR:-"./docker-data/dms/config"} +CERTS_BASE_PATH=${CERTS_BASE_PATH:-"/etc/mail/certs"} + +# Die Default-Domain für DMS hostname/domainname (bleibt email-srvr.com) +DEFAULT_DOMAIN=${DEFAULT_DOMAIN:-"email-srvr.com"} + +echo "============================================================" +echo " 🔐 DMS TLS SNI Setup (Multi-Domain)" +echo " Container: $DMS_CONTAINER" +echo " Config Dir: $CONFIG_DIR" +echo " Certs Base: $CERTS_BASE_PATH" +echo " Default Domain: $DEFAULT_DOMAIN" +echo "============================================================" + +# --- Alle Domains aus DMS Accounts 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 Accounts anlegen: ./manage_mail_user.sh add user@domain.com PW" + exit 1 +fi + +echo " Gefundene Domains:" +for d in $DOMAINS; do echo " - $d"; done + +# --- Cert-Verfügbarkeit prüfen --- +echo "" +echo "🔍 Prüfe Zertifikat-Verfügbarkeit (im Container)..." +DOMAINS_WITH_CERTS="" +DOMAINS_WITHOUT_CERTS="" + +for domain in $DOMAINS; do + # Caddy speichert Wildcard-Certs als: *.domain.tld/ + # Pfad im Container (über den Volume-Mount): /etc/mail/certs/*.domain.tld/ + CERT_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.crt" + KEY_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.key" + + # Prüfe ob die Datei im Container existiert + if docker exec "$DMS_CONTAINER" test -f "$CERT_PATH" 2>/dev/null; then + echo " ✅ $domain → Cert gefunden" + DOMAINS_WITH_CERTS="$DOMAINS_WITH_CERTS $domain" + else + echo " ⚠️ $domain → KEIN Cert unter $CERT_PATH" + echo " Caddy-Block '*.${domain}' eintragen und Caddy neu starten!" + DOMAINS_WITHOUT_CERTS="$DOMAINS_WITHOUT_CERTS $domain" + fi +done + +if [ -n "$DOMAINS_WITHOUT_CERTS" ]; then + echo "" + echo "⚠️ WARNUNG: Fehlende Certs für:$DOMAINS_WITHOUT_CERTS" + echo " Diese Domains werden NICHT in die SNI-Configs eingetragen." + echo " Bitte Certs erzeugen und Script erneut ausführen." + echo "" +fi + +if [ -z "$DOMAINS_WITH_CERTS" ]; then + echo "❌ Kein einziges Zertifikat gefunden! Abbruch." + exit 1 +fi + +# ================================================================ +# DOVECOT SNI Konfiguration generieren +# ================================================================ +DOVECOT_CFG="$CONFIG_DIR/dovecot-sni.cf" +echo "" +echo "📝 Generiere Dovecot SNI Konfiguration: $DOVECOT_CFG" + +cat > "$DOVECOT_CFG" << 'HEADER' +# dovecot-sni.cf - Automatisch generiert von setup-dms-tls.sh +# SNI-basierte TLS-Konfiguration für mehrere Domains. +# Dovecot wählt das Zertifikat anhand des SNI-Hostnamens des Clients. +# Dieses File wird via Volume-Mount in den Container eingebunden. +# +# Gemounteter Pfad: /tmp/docker-mailserver/dovecot-sni.cf +# In DMS docker-compose.yml volumes Sektion: +# - ./docker-data/dms/config/dovecot-sni.cf:/tmp/docker-mailserver/dovecot-sni.cf:ro + +HEADER + +for domain in $DOMAINS_WITH_CERTS; do + CERT_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.crt" + KEY_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.key" + + cat >> "$DOVECOT_CFG" << EOF +# Domain: $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_CFG erstellt ($(echo $DOMAINS_WITH_CERTS | wc -w) Domains)" + +# ================================================================ +# POSTFIX SNI Konfiguration generieren +# ================================================================ +POSTFIX_CFG="$CONFIG_DIR/postfix-main.cf" +echo "" +echo "📝 Generiere Postfix SNI Konfiguration: $POSTFIX_CFG" + +# Prüfe ob postfix-main.cf schon existiert und sichere sie +if [ -f "$POSTFIX_CFG" ]; then + cp "$POSTFIX_CFG" "${POSTFIX_CFG}.bak.$(date +%Y%m%d%H%M%S)" + echo " ℹ️ Backup erstellt: ${POSTFIX_CFG}.bak.*" +fi + +# TLS Chain Files für Postfix aufbauen +# Postfix unterstützt smtpd_tls_chain_files mit mehreren Key/Cert Paaren +CHAIN_FILES="" +for domain in $DOMAINS_WITH_CERTS; do + KEY_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.key" + CERT_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.crt" + if [ -z "$CHAIN_FILES" ]; then + CHAIN_FILES=" $KEY_PATH, $CERT_PATH" + else + CHAIN_FILES="$CHAIN_FILES,\n $KEY_PATH, $CERT_PATH" + fi +done + +cat > "$POSTFIX_CFG" << POSTFIX_CONF +# postfix-main.cf - Automatisch generiert von setup-dms-tls.sh +# Postfix SNI-Konfiguration für mehrere Domains. +# DMS lädt dieses File automatisch beim Start via /tmp/docker-mailserver/ + +# ------------------------------------------------------------------ +# TLS Chain Files (Key + Cert pro Domain) +# Postfix wählt das passende Paar automatisch per SNI +# ------------------------------------------------------------------ +smtpd_tls_chain_files = +$(printf '%b' "$CHAIN_FILES") + +POSTFIX_CONF + +echo " ✅ $POSTFIX_CFG erstellt" + +# ================================================================ +# Hinweise für docker-compose.yml +# ================================================================ +echo "" +echo "============================================================" +echo "📋 Nächste Schritte:" +echo "" +echo "1. Volume-Mounts in DMS docker-compose.yml hinzufügen:" +echo "" +echo " volumes:" +echo " # Bestehend (Caddy Certs - gesamtes Verzeichnis):" +echo " - /var/lib/docker/volumes/caddy_data/_data/caddy/certificates/" +echo " acme-v02.api.letsencrypt.org-directory:/etc/mail/certs:ro" +echo "" +echo " # NEU - Dovecot SNI:" +echo " - ./docker-data/dms/config/dovecot-sni.cf:/tmp/docker-mailserver/dovecot-sni.cf:ro" +echo "" +echo " # Postfix-main.cf wird von DMS automatisch geladen wenn sie liegt unter:" +echo " - ./docker-data/dms/config/postfix-main.cf:/tmp/docker-mailserver/postfix-main.cf:ro" +echo "" +echo "2. DMS neu starten:" +echo " docker compose restart mailserver" +echo "" +echo "3. TLS testen:" +for domain in $DOMAINS_WITH_CERTS; do + echo " openssl s_client -connect mail.$domain:993 -servername mail.$domain" +done +echo "============================================================" \ No newline at end of file diff --git a/basic_setup/cloudflareMigrationDns.sh b/basic_setup/cloudflareMigrationDns.sh index 9d3b0a9..754e1a0 100755 --- a/basic_setup/cloudflareMigrationDns.sh +++ b/basic_setup/cloudflareMigrationDns.sh @@ -1,7 +1,8 @@ #!/bin/bash # cloudflareMigrationDns.sh # 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 +# NEU: Setzt mail/imap/smtp/pop Subdomains für domain-spezifischen Mailserver-Zugang set -e @@ -9,8 +10,13 @@ set -e AWS_REGION=${AWS_REGION:-"us-east-2"} DRY_RUN=${DRY_RUN:-"false"} -# Ziel für Autodiscover/IMAP (wohin sollen Mail-Clients verbinden?) -# Standard: mail.deinedomain.tld. Kann überschrieben werden. +# IP des Mailservers - PFLICHT wenn keine CNAME-Kette gewünscht +# export MAIL_SERVER_IP="1.2.3.4" +MAIL_SERVER_IP=${MAIL_SERVER_IP:-""} + +# Ziel-Server für Mailclients. Standard: mail. +# Wenn MAIL_SERVER_IP gesetzt ist, bekommt mail. einen A-Record +# und imap/smtp/pop/webmail zeigen per CNAME auf mail. TARGET_MAIL_SERVER=${TARGET_MAIL_SERVER:-"mail.${DOMAIN_NAME}"} # --- CHECKS --- @@ -19,9 +25,18 @@ if [ -z "$CF_API_TOKEN" ]; then echo "❌ Fehler: CF_API_TOKEN fehlt."; exit 1; if ! command -v jq &> /dev/null; then echo "❌ Fehler: 'jq' fehlt."; exit 1; fi if ! command -v aws &> /dev/null; then echo "❌ Fehler: 'aws' CLI fehlt."; exit 1; fi +if [ -z "$MAIL_SERVER_IP" ] && [ "$TARGET_MAIL_SERVER" == "mail.$DOMAIN_NAME" ]; then + echo "⚠️ WARNUNG: MAIL_SERVER_IP ist nicht gesetzt!" + echo " mail.$DOMAIN_NAME braucht einen A-Record." + echo " Bitte setzen: export MAIL_SERVER_IP=" + exit 1 +fi + echo "============================================================" echo " 🛡️ DNS Migration Setup für: $DOMAIN_NAME" echo " 🌍 Region: $AWS_REGION" +echo " 📬 Mail-Server Target: $TARGET_MAIL_SERVER" +[ -n "$MAIL_SERVER_IP" ] && echo " 🖥️ Server IP: $MAIL_SERVER_IP" [ "$DRY_RUN" = "true" ] && echo " ⚠️ DRY RUN MODE - Keine Änderungen!" echo "============================================================" @@ -38,41 +53,31 @@ echo " ✅ Zone ID: $ZONE_ID" # ------------------------------------------------------------------ # FUNKTION: ensure_record -# Prüft Existenz -> Create oder Update (je nach Typ) # ------------------------------------------------------------------ ensure_record() { local type=$1 local name=$2 local content=$3 local proxied=${4:-false} - local priority=$5 # Optional für MX + local priority=$5 echo " ⚙️ Prüfe $type $name..." - # Bestehenden Record suchen - # Hinweis: Wir suchen exakt nach Name und Typ local search_res=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=$type&name=$name" \ -H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json") local rec_id=$(echo "$search_res" | jq -r '.result[0].id') local rec_content=$(echo "$search_res" | jq -r '.result[0].content') - # JSON Body bauen if [ "$type" == "MX" ]; then json_data=$(jq -n --arg t "$type" --arg n "$name" --arg c "$content" --argjson p "$proxied" --argjson prio "$priority" \ '{type: $t, name: $n, content: $c, ttl: 3600, proxied: $p, priority: $prio}') - elif [ "$type" == "TXT" ]; then - # Bei TXT Quotes escapen falls nötig, aber jq macht das meist gut - json_data=$(jq -n --arg t "$type" --arg n "$name" --arg c "$content" --argjson p "$proxied" \ - '{type: $t, name: $n, content: $c, ttl: 3600, proxied: $p}') else json_data=$(jq -n --arg t "$type" --arg n "$name" --arg c "$content" --argjson p "$proxied" \ '{type: $t, name: $n, content: $c, ttl: 3600, proxied: $p}') fi - # LOGIK if [ "$rec_id" == "null" ]; then - # --- CREATE --- if [ "$DRY_RUN" = "true" ]; then echo " [DRY] Würde ERSTELLEN: $content" else @@ -81,34 +86,27 @@ ensure_record() { if [ "$(echo $res | jq -r .success)" == "true" ]; then echo " ✅ Erstellt." else - echo " ❌ Fehler beim Erstellen: $(echo $res | jq -r .errors[0].message)" + echo " ❌ Fehler: $(echo $res | jq -r .errors[0].message)" fi fi else - # --- EXISTS --- if [ "$rec_content" == "$content" ]; then - echo " 🆗 Identisch vorhanden. Überspringe." + echo " 🆗 Identisch. Überspringe." else - # Inhalt anders -> Update oder Error? if [ "$type" == "MX" ] && [ "$name" == "$DOMAIN_NAME" ]; then - echo " ⛔ MX Record existiert aber ist anders!" - echo " Gefunden: $rec_content" - echo " Erwartet: $content" - echo " ABBRUCH: Bitte alten MX Record ID $rec_id manuell löschen." - # Wir brechen hier nicht das ganze Script ab, aber setzen den neuen nicht. + echo " ⛔ MX existiert aber anders! Gefunden: $rec_content / Erwartet: $content" + echo " Bitte Record ID $rec_id manuell löschen." return fi - - # Für TXT (SPF/DMARC) oder CNAME machen wir ein UPDATE (Overwrite) if [ "$DRY_RUN" = "true" ]; then - echo " [DRY] Würde UPDATEN von '$rec_content' auf '$content'" + echo " [DRY] Würde UPDATEN: '$rec_content' → '$content'" else res=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$rec_id" \ -H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" --data "$json_data") if [ "$(echo $res | jq -r .success)" == "true" ]; then echo " 🔄 Aktualisiert." else - echo " ❌ Fehler beim Update: $(echo $res | jq -r .errors[0].message)" + echo " ❌ Fehler: $(echo $res | jq -r .errors[0].message)" fi fi fi @@ -120,37 +118,36 @@ ensure_record() { # ------------------------------------------------------------------ echo "" echo "--- 1. MAIL FROM Domain ---" -# Wenn von außen nicht gesetzt, versuche via AWS if [ -z "$MAIL_FROM_DOMAIN" ]; then - echo " Variable MAIL_FROM_DOMAIN leer, frage AWS SES..." SES_JSON=$(aws sesv2 get-email-identity --email-identity $DOMAIN_NAME --region $AWS_REGION 2>/dev/null) MAIL_FROM_DOMAIN=$(echo "$SES_JSON" | jq -r '.MailFromAttributes.MailFromDomain') - if [ "$MAIL_FROM_DOMAIN" == "null" ] || [ -z "$MAIL_FROM_DOMAIN" ]; then MAIL_FROM_DOMAIN="mail.$DOMAIN_NAME" - echo " ⚠️ Keine MAIL FROM in SES gefunden. Fallback auf: $MAIL_FROM_DOMAIN" + echo " ⚠️ Kein MAIL FROM in SES. Fallback: $MAIL_FROM_DOMAIN" fi else - echo " Nutze vorgegebene MAIL FROM: $MAIL_FROM_DOMAIN" + echo " Nutze: $MAIL_FROM_DOMAIN" fi # ------------------------------------------------------------------ -# SCHRITT 2: DKIM Records (CNAME) +# SCHRITT 2: DKIM Records # ------------------------------------------------------------------ echo "" echo "--- 2. DKIM Records ---" -TOKENS=$(aws ses get-identity-dkim-attributes --identities $DOMAIN_NAME --region $AWS_REGION --query "DkimAttributes.\"$DOMAIN_NAME\".DkimTokens" --output text) +TOKENS=$(aws ses get-identity-dkim-attributes --identities $DOMAIN_NAME --region $AWS_REGION \ + --query "DkimAttributes.\"$DOMAIN_NAME\".DkimTokens" --output text) for token in $TOKENS; do ensure_record "CNAME" "${token}._domainkey.$DOMAIN_NAME" "${token}.dkim.amazonses.com" false done # ------------------------------------------------------------------ -# SCHRITT 3: SES Verification (_amazonses) +# SCHRITT 3: SES Verification # ------------------------------------------------------------------ echo "" echo "--- 3. SES Verification TXT ---" -VERIF_TOKEN=$(aws ses get-identity-verification-attributes --identities $DOMAIN_NAME --region $AWS_REGION --query "VerificationAttributes.\"$DOMAIN_NAME\".VerificationToken" --output text) -if [ "$VERIF_TOKEN" != "None" ]; then +VERIF_TOKEN=$(aws ses get-identity-verification-attributes --identities $DOMAIN_NAME \ + --region $AWS_REGION --query "VerificationAttributes.\"$DOMAIN_NAME\".VerificationToken" --output text) +if [ "$VERIF_TOKEN" != "None" ] && [ -n "$VERIF_TOKEN" ]; then ensure_record "TXT" "_amazonses.$DOMAIN_NAME" "$VERIF_TOKEN" false fi @@ -159,47 +156,27 @@ fi # ------------------------------------------------------------------ echo "" echo "--- 4. MAIL FROM Subdomain ($MAIL_FROM_DOMAIN) ---" -# MX für die Subdomain (feedback loop) ensure_record "MX" "$MAIL_FROM_DOMAIN" "feedback-smtp.$AWS_REGION.amazonses.com" false 10 -# SPF für die Subdomain (strikte SES Regel) ensure_record "TXT" "$MAIL_FROM_DOMAIN" "v=spf1 include:amazonses.com ~all" false # ------------------------------------------------------------------ -# SCHRITT 5: Root Domain SPF (Merge Logic) +# SCHRITT 5: Root Domain SPF # ------------------------------------------------------------------ echo "" echo "--- 5. Root Domain SPF ---" if [ -n "$OLD_PROVIDER_SPF" ]; then - # Merge: SES + Alter Provider FINAL_SPF="v=spf1 include:amazonses.com $OLD_PROVIDER_SPF ~all" - echo " ℹ️ Modus: Migration (SES + Alt)" else - # Nur SES FINAL_SPF="v=spf1 include:amazonses.com ~all" - echo " ℹ️ Modus: SES only" fi ensure_record "TXT" "$DOMAIN_NAME" "$FINAL_SPF" false # ------------------------------------------------------------------ -# SCHRITT 6: Root Domain MX (Safety First) +# SCHRITT 6: Root Domain MX # ------------------------------------------------------------------ echo "" echo "--- 6. Root Domain MX ---" -# Hier wollen wir den Inbound SMTP von AWS (falls man AWS WorkMail nutzt oder DMS via AWS ingress) -# WARTE: Du nutzt DMS. Dein DMS hat vermutlich eine eigene IP/Hostname (z.B. mail.buddelectric.net). -# Wenn du SES NUR ZUM SENDEN nutzt, darfst du den Root MX NICHT auf Amazon ändern! -# -# Annahme: Du willst den MX für den Empfang setzen. -# Da du oben "feedback-smtp" erwähnt hast, geht es wohl um den SES Return-Path. -# Aber der "echte MX" für die Domain ($DOMAIN_NAME) zeigt auf DEINEN Mailserver (DMS). -# -# Falls du den MX auf deinen DMS Server zeigen lassen willst: -TARGET_MX=${TARGET_MX:-"mail.$DOMAIN_NAME"} -echo " ℹ️ Ziel-MX ist: $TARGET_MX" - -# HINWEIS: MX Records brauchen oft einen Hostnamen, keine IP. -# Wir prüfen, ob ein MX existiert. -ensure_record "MX" "$DOMAIN_NAME" "$TARGET_MX" false 10 +ensure_record "MX" "$DOMAIN_NAME" "mail.$DOMAIN_NAME" false 10 # ------------------------------------------------------------------ # SCHRITT 7: DMARC @@ -209,34 +186,51 @@ echo "--- 7. DMARC ---" ensure_record "TXT" "_dmarc.$DOMAIN_NAME" "v=DMARC1; p=none; rua=mailto:postmaster@$DOMAIN_NAME" false # ------------------------------------------------------------------ -# SCHRITT 8: Autodiscover / Autoconfig +# SCHRITT 8 (NEU): Mailclient Subdomains # ------------------------------------------------------------------ echo "" -echo "--- 8. Autodiscover / Autoconfig ---" -# Ziel ist meist der IMAP/SMTP Server -echo " ℹ️ Ziel für Clients: $TARGET_MAIL_SERVER" +echo "--- 8. Mailclient Subdomains (A + CNAME) ---" -ensure_record "CNAME" "autodiscover.$DOMAIN_NAME" "$TARGET_MAIL_SERVER" false -ensure_record "CNAME" "autoconfig.$DOMAIN_NAME" "$TARGET_MAIL_SERVER" false +if [ -n "$MAIL_SERVER_IP" ]; then + # A-Record für mail. 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 +# imap, smtp, pop, webmail → CNAME auf mail. +ensure_record "CNAME" "imap.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false +ensure_record "CNAME" "smtp.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false +ensure_record "CNAME" "pop.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false +ensure_record "CNAME" "webmail.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false -# Füge das zu deinem Skript hinzu (Schritt 9 optional): # ------------------------------------------------------------------ -# SCHRITT 9: SRV Records (Service Discovery) +# SCHRITT 9: Autodiscover / Autoconfig # ------------------------------------------------------------------ echo "" -echo "--- 9. SRV Records (Service Discovery) ---" -# Das hilft Outlook, direkt "email-srvr.com" zu nutzen statt "mail.domain.tld" -# Format: _service._proto.name TTL class SRV priority weight port target +echo "--- 9. Autodiscover / Autoconfig ---" +ensure_record "CNAME" "autodiscover.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false +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 -# IMAPS SRV (Port 993) -ensure_record "SRV" "_imaps._tcp.$DOMAIN_NAME" "0 5 993 $TARGET_MAIL_SERVER" false -# SUBMISSION SRV (Port 587) -ensure_record "SRV" "_submission._tcp.$DOMAIN_NAME" "0 5 587 $TARGET_MAIL_SERVER" false - -echo " ✅ SRV Records gesetzt (Server: $TARGET_MAIL_SERVER)" +# ------------------------------------------------------------------ +# SCHRITT 10: SRV Records +# ------------------------------------------------------------------ +echo "" +echo "--- 10. SRV Records ---" +ensure_record "SRV" "_imap._tcp.$DOMAIN_NAME" "0 5 143 mail.$DOMAIN_NAME" false +ensure_record "SRV" "_imaps._tcp.$DOMAIN_NAME" "0 5 993 mail.$DOMAIN_NAME" false +ensure_record "SRV" "_submission._tcp.$DOMAIN_NAME" "0 5 587 mail.$DOMAIN_NAME" false echo "" -echo "✅ Fertig." \ No newline at end of file +echo "============================================================" +echo "✅ Fertig für Domain: $DOMAIN_NAME" +echo "" +echo " Mailclient-Konfiguration für Kunden:" +echo " IMAP: imap.$DOMAIN_NAME Port 993 (SSL)" +echo " SMTP: smtp.$DOMAIN_NAME Port 587 (STARTTLS) oder 465 (SSL)" +echo " POP3: pop.$DOMAIN_NAME Port 995 (SSL)" +echo " Webmail: webmail.$DOMAIN_NAME" +echo "============================================================" \ No newline at end of file diff --git a/caddy/Caddyfile b/caddy/Caddyfile index eb167b5..5f16483 100644 --- a/caddy/Caddyfile +++ b/caddy/Caddyfile @@ -19,285 +19,3 @@ autoconfig.bayarea-cc.com, autoconfig.bizmatch.net { respond "Autodiscover Service Online" 200 } -# Prod: Neue Domains -www.bizmatch.net { - handle /pictures/* { - root * /home/aknuth/git/bizmatch-project/bizmatch-server # Prod-Ordner - file_server - } - # Statische Dateien (CSS, JS, Bilder) – lange cachen, da sich der Name bei Änderungen ändert - header /assets/* Cache-Control "public, max-age=31536000, immutable" - header /*.css Cache-Control "public, max-age=31536000, immutable" - header /*.js Cache-Control "public, max-age=31536000, immutable" - - # Die index.html und API-Antworten – NIEMALS cachen - header /index.html Cache-Control "no-cache, no-store, must-revalidate" - - #handle { - # root * /home/aknuth/git/bizmatch-project-prod/bizmatch/dist/bizmatch/browser # Neuer Prod-Dist-Ordner - # try_files {path} {path}/ /index.html - # file_server - #} - handle { - reverse_proxy host.docker.internal:4200 - } - log { - output file /var/log/caddy/access.prod.log # Separate Logs - } - encode gzip zstd -} -bizmatch.net { - redir https://www.bizmatch.net{uri} permanent - import email_settings -} -www.qrmaster.net { - handle { - reverse_proxy host.docker.internal:3050 - } - log { - output file /var/log/caddy/qrmaster.log - format console - } - encode gzip -} -qrmaster.net { - redir https://www.qrmaster.net{uri} permanent -} -bayarea-cc.com { - # TLS-Direktive entfernen, falls Cloudflare die Verbindung terminiert - # tls { - # dns cloudflare {env.CLOUDFLARE_API_TOKEN} - # } - - handle /api { - reverse_proxy host.docker.internal:3001 - } - handle { - root * /app - try_files {path} /index.html - file_server - } - log { - output stderr - format console - } - encode gzip - import email_settings -} -www.bayarea-cc.com { - redir https://bayarea-cc.com{uri} permanent -} -setup.bayarea-cc.com { - # Wir setzen das Root-Verzeichnis auf den neuen Pfad im Container - root * /var/www/email-setup - - # Webserver-Standardverhalten - file_server - - # Wenn jemand nur die Domain aufruft, zeige setup.html - try_files {path} /setup.html -} -cielectrical.bayarea-cc.com { - # wenn du API innerhalb von Next bedienst, weiterleiten an den Next Prozess - handle { - reverse_proxy host.docker.internal:3000 - } - log { - output file /var/log/caddy/cielectrical.log - format console - } - encode gzip -} -hamptonbrown.bayarea-cc.com { - # wenn du API innerhalb von Next bedienst, weiterleiten an den Next Prozess - handle { - reverse_proxy host.docker.internal:3010 - } - log { - output file /var/log/caddy/hamptonbrown.log - format console - } - encode gzip -} -nqsltd.bayarea-cc.com { - # wenn du API innerhalb von Next bedienst, weiterleiten an den Next Prozess - handle { - reverse_proxy host.docker.internal:3020 - } - log { - output file /var/log/caddy/nqsltd.log - format console - } - encode gzip -} -gregknoppcpa.bayarea-cc.com { - # wenn du API innerhalb von Next bedienst, weiterleiten an den Next Prozess - handle { - reverse_proxy host.docker.internal:3030 - } - log { - output file /var/log/caddy/gregknoppcpa.log - format console - } - encode gzip -} -buddelectric.bayarea-cc.com { - # wenn du API innerhalb von Next bedienst, weiterleiten an den Next Prozess - handle { - reverse_proxy host.docker.internal:3040 - } - log { - output file /var/log/caddy/buddelectric.log - format console - } - encode gzip zstd -} -iitwelders.bayarea-cc.com { - # wenn du API innerhalb von Next bedienst, weiterleiten an den Next Prozess - handle { - reverse_proxy host.docker.internal:8080 - } - log { - output file /var/log/caddy/iitwelders.log - format console - } - encode gzip -} -fancytextstuff.com { - # wenn du API innerhalb von Next bedienst, weiterleiten an den Next Prozess - handle { - reverse_proxy host.docker.internal:3010 - } - log { - output file /var/log/caddy/fancytext.log - format console - } - encode gzip -} -www.fancytextstuff.com { - redir https://fancytextstuff.com{uri} permanent -} -auth.bizmatch.net { - reverse_proxy https://bizmatch-net.firebaseapp.com { - header_up Host bizmatch-net.firebaseapp.com - header_up X-Forwarded-For {remote_host} - header_up X-Forwarded-Proto {scheme} - header_up X-Real-IP {remote_host} - } -} -gitea.bizmatch.net { - reverse_proxy gitea:3500 -} - -dev.bizmatch.net { - handle /pictures/* { - root * /home/aknuth/git/bizmatch-project/bizmatch-server - file_server - } - - handle { - root * /home/aknuth/git/bizmatch-project/bizmatch/dist/bizmatch/browser - try_files {path} {path}/ /index.html - file_server - } - - log { - output file /var/log/caddy/access.log { - roll_size 10MB - roll_keep 5 - roll_keep_for 48h - } - } - - encode gzip - -} - - -api.bizmatch.net { - reverse_proxy host.docker.internal:3001 { # Neu: Proxy auf Prod-Port 3001 - header_up X-Real-IP {http.request.header.CF-Connecting-IP} - header_up X-Forwarded-For {http.request.header.CF-Connecting-IP} - header_up X-Forwarded-Proto {http.request.header.X-Forwarded-Proto} - header_up CF-IPCountry {http.request.header.CF-IPCountry} - } -} -mailsync.bizmatch.net { - reverse_proxy host.docker.internal:5000 { - header_up X-Real-IP {http.request.header.CF-Connecting-IP} - header_up X-Forwarded-For {http.request.header.CF-Connecting-IP} - header_up X-Forwarded-Proto {http.request.header.X-Forwarded-Proto} - header_up CF-IPCountry {http.request.header.CF-IPCountry} - } -} - -# Roundcube für docker-mailserver -app.email-bayarea.com { - reverse_proxy roundcube:80 - - log { - output stderr - format console - } - - encode gzip -} -# Roundcube für docker-mailserver -config.email-bayarea.com { - - root * /home/aknuth/git/config-email/frontend/dist - try_files {path} {path}/ /index.html - file_server - - log { - output file /var/log/caddy/config-email.log - } - - encode gzip -} -# Roundcube für docker-mailserver -api.email-bayarea.com { - reverse_proxy host.docker.internal:3002 - - log { - output stderr - format console - } - - encode gzip -} -annavillesda.org { - # API requests to backend - handle /api/* { - reverse_proxy host.docker.internal:3070 - } - - # Frontend static files - handle { - root * /home/aknuth/git/annaville-sda-site/dist - try_files {path} {path}/ /index.html - file_server - } - - log { - output file /var/log/caddy/access.prod.log - } - - encode gzip -} -www.annavillesda.org { - redir https://annavillesda.org{uri} permanent -} -# ----------------- -# just for certificate generation -# ----------------- -mail.andreasknuth.de { - reverse_proxy nginx-mailcow:8080 -} -web.email-bayarea.com { - reverse_proxy nginx-mailcow:8080 -} -# Dieser Block dient nur dazu, das Zertifikat für den Mailserver zu beschaffen/erneuern. -mail.email-srvr.com { - respond "Mailserver Certificate Authority is running." 200 -} diff --git a/caddy/docker-compose.yml b/caddy/docker-compose.yml index bbb49e9..41c6677 100644 --- a/caddy/docker-compose.yml +++ b/caddy/docker-compose.yml @@ -23,14 +23,7 @@ services: - $PWD/email-setup:/var/www/email-setup - caddy_data:/data - caddy_config:/config - - /home/aknuth/git/bizmatch-project/bizmatch/dist/bizmatch/browser:/home/aknuth/git/bizmatch-project/bizmatch/dist/bizmatch/browser - - /home/aknuth/git/bizmatch-project-prod/bizmatch/dist/bizmatch/browser:/home/aknuth/git/bizmatch-project-prod/bizmatch/dist/bizmatch/browser - - /home/aknuth/git/bizmatch-project/bizmatch-server/pictures:/home/aknuth/git/bizmatch-project/bizmatch-server/pictures - - /home/aknuth/git/bizmatch-project-prod/bizmatch-server/pictures:/home/aknuth/git/bizmatch-project-prod/bizmatch-server/pictures - - /home/aknuth/git/annaville-sda-site/dist:/home/aknuth/git/annaville-sda-site/dist:ro # ← DAS FEHLT! - - /home/aknuth/git/bay-area-affiliates/dist/bay-area-affiliates/browser:/app - /home/aknuth/log/caddy:/var/log/caddy - - /home/aknuth/git/config-email/frontend/dist:/home/aknuth/git/config-email/frontend/dist:ro environment: - CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN} - CLOUDFLARE_EMAIL=${CLOUDFLARE_EMAIL} diff --git a/caddy/update-caddy-certs.sh b/caddy/update-caddy-certs.sh new file mode 100755 index 0000000..70c3952 --- /dev/null +++ b/caddy/update-caddy-certs.sh @@ -0,0 +1,137 @@ +#!/bin/bash +# update-caddy-certs.sh +# Liest alle Domains aus dem DMS und generiert die notwendigen +# Caddyfile-Blöcke für Wildcard-Zertifikate. +# +# Die generierten Blöcke werden NICHT automatisch in das Caddyfile geschrieben, +# sondern in eine separate Datei (caddy_mail_certs.conf) ausgegeben, +# die per "import" in das Hauptcaddyfile eingebunden werden kann. +# +# Usage: +# DMS_CONTAINER=mailserver ./update-caddy-certs.sh +# DMS_CONTAINER=mailserver CADDY_DIR=/pfad/zu/caddy ./update-caddy-certs.sh +# DMS_CONTAINER=mailserver DRY_RUN=true ./update-caddy-certs.sh + +set -e + +DMS_CONTAINER=${DMS_CONTAINER:-"mailserver"} +CADDY_DIR=${CADDY_DIR:-"."} # Verzeichnis wo das Caddyfile liegt +OUTPUT_FILE=${OUTPUT_FILE:-"$CADDY_DIR/mail_certs"} # Ohne Extension - Caddy importiert ohne .conf +DRY_RUN=${DRY_RUN:-"false"} + +echo "============================================================" +echo " 📜 Caddy Wildcard-Cert Konfig Generator" +echo " DMS Container: $DMS_CONTAINER" +echo " Output: $OUTPUT_FILE" +[ "$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 Accounts gefunden!" + exit 1 +fi + +echo " Gefundene Domains:" +for d in $DOMAINS; do echo " - $d"; done + +# --- email-srvr.com immer einschließen (Default-Domain des DMS) --- +EXTRA_DOMAINS="email-srvr.com" +for extra in $EXTRA_DOMAINS; do + if ! echo "$DOMAINS" | grep -q "^${extra}$"; then + DOMAINS="$DOMAINS $extra" + echo " + $extra (Default DMS Domain - immer dabei)" + fi +done + +# --- Konfig generieren --- +echo "" +echo "📝 Generiere Caddy-Konfiguration..." + +CONTENT="" +CONTENT="${CONTENT}# mail_certs - Automatisch generiert von update-caddy-certs.sh\n" +CONTENT="${CONTENT}# Wildcard-Zertifikate für alle DMS-Domains.\n" +CONTENT="${CONTENT}# Einbinden im Hauptcaddyfile: import mail_certs\n" +CONTENT="${CONTENT}# Generiert: $(date)\n" +CONTENT="${CONTENT}\n" + +for domain in $DOMAINS; do + echo " → Block für: $domain" + CONTENT="${CONTENT}# Wildcard-Cert für $domain\n" + CONTENT="${CONTENT}*.${domain}, ${domain} {\n" + CONTENT="${CONTENT} tls {\n" + CONTENT="${CONTENT} dns cloudflare {env.CLOUDFLARE_API_TOKEN}\n" + CONTENT="${CONTENT} }\n" + CONTENT="${CONTENT} respond \"OK\" 200\n" + CONTENT="${CONTENT}}\n" + CONTENT="${CONTENT}\n" +done + +# --- Ausgabe --- +if [ "$DRY_RUN" = "true" ]; then + echo "" + echo "--- VORSCHAU (DRY RUN) ---" + printf '%b' "$CONTENT" + echo "--- ENDE VORSCHAU ---" +else + printf '%b' "$CONTENT" > "$OUTPUT_FILE" + echo "" + echo " ✅ Geschrieben: $OUTPUT_FILE" +fi + +# --- Prüfen ob Import im Caddyfile vorhanden --- +CADDYFILE="$CADDY_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 ERFORDERLICH:" + echo " 'import mail_certs' fehlt noch im Caddyfile!" + echo " Bitte folgende Zeile am Anfang (nach dem globalen Block) eintragen:" + echo "" + echo " import mail_certs" + echo "" + echo " Oder automatisch einfügen? (y/N)" + read -r answer + if [ "$answer" = "y" ] || [ "$answer" = "Y" ]; then + # Import nach der ersten Zeile mit "import " einfügen (falls schon welche da sind) + # oder nach dem globalen {} Block + if grep -q "^import " "$CADDYFILE"; then + # Schreibe nach der letzten import-Zeile + sed -i "/^import /a import mail_certs" "$CADDYFILE" + else + # Schreibe nach dem schließenden } des globalen Blocks + sed -i "/^}/a \\\nimport mail_certs" "$CADDYFILE" + fi + echo " ✅ Import eingefügt." + fi + fi +fi + +# --- Caddy reload --- +echo "" +echo "============================================================" +echo "🔄 Nächste Schritte:" +echo "" +echo "1. Caddyfile prüfen - 'import mail_certs' muss vorhanden sein" +echo "" +echo "2. Caddy Konfiguration validieren:" +echo " docker exec caddy caddy validate --config /etc/caddy/Caddyfile" +echo "" +echo "3. Caddy neu laden (kein Downtime):" +echo " docker exec caddy caddy reload --config /etc/caddy/Caddyfile" +echo "" +echo "4. Cert-Generierung verfolgen (dauert ~30s pro Domain):" +echo " docker logs -f caddy 2>&1 | grep -i 'certificate\|acme\|tls'" +echo "" +echo "5. Cert-Pfade prüfen:" +echo " ls /var/lib/docker/volumes/caddy_data/_data/caddy/certificates/" +echo " acme-v02.api.letsencrypt.org-directory/" +echo "============================================================" \ No newline at end of file From 8808d811135b5e8812f489dc6af72fbc5dcd4686 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 13:00:24 -0600 Subject: [PATCH 03/74] update --- DMS/docker-compose.yml | 121 ++++++++++++++---------- DMS/setup-dms-tls.sh | 177 ++++++++++++++++++------------------ caddy/update-caddy-certs.sh | 147 +++++++++++++++--------------- 3 files changed, 232 insertions(+), 213 deletions(-) diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index 978f24a..d7efbad 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -1,23 +1,26 @@ services: mailserver: - # image: docker.io/mailserver/docker-mailserver:latest # AUSKOMMENTIERT build: context: . dockerfile: Dockerfile image: dms-custom:latest 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: - - "25:25" # SMTP (parallel zu MailCow auf Port 25) - - "587:587" # SMTP Submission - - "465:465" # SMTP SSL - - "143:143" # IMAP - - "993:993" # IMAP SSL - - "110:110" # POP3 - - "995:995" # POP3 SSL - - "127.0.0.1:11334:11334" # Bindet nur an Localhost! + - "25:25" + - "587:587" + - "465:465" + - "143:143" + - "993:993" + - "110:110" + - "995:995" + - "127.0.0.1:11334:11334" + volumes: - ./docker-data/dms/mail-data/:/var/mail/ - ./docker-data/dms/mail-state/:/var/mail-state/ @@ -27,67 +30,88 @@ services: - /etc/localtime:/etc/localtime:ro - ./sync_dynamodb_to_sieve.py:/scripts/sync.py: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:/tmp/docker-mailserver/dovecot-sni.cf:ro + 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 - # Diese Pfade beziehen sich auf das INNERE des Containers (wo wir hin mounten) - - SSL_CERT_PATH=/etc/mail/certs/mail.email-srvr.com.crt - - SSL_KEY_PATH=/etc/mail/certs/mail.email-srvr.com.key + - SSL_CERT_PATH=/etc/mail/certs/node1.email-srvr.com/node1.email-srvr.com.crt + - SSL_KEY_PATH=/etc/mail/certs/node1.email-srvr.com/node1.email-srvr.com.key + + # SPAM / Rspamd - ENABLE_OPENDKIM=1 - ENABLE_OPENDMARC=0 - ENABLE_POLICYD_SPF=0 - # #### SPAM SECTION ##### - # SPAM Rspamd aktivieren - ENABLE_RSPAMD=1 - # Greylisting AUS (vermeidet Verzögerungen) - RSPAMD_GREYLISTING=0 - # Eigene Mails NICHT scannen (vermeidet Probleme beim Senden) - RSPAMD_CHECK_AUTHENTICATED=0 - # Hostname Check AN (filtert Botnets, sehr sicher) - RSPAMD_HFILTER=1 - # Spam sortieren statt löschen (Sieve Magic) - MOVE_SPAM_TO_JUNK=1 - # Alte Dienste aus - ENABLE_AMAVIS=0 - ENABLE_SPAMASSASSIN=0 - ENABLE_POSTGREY=0 - # 2. ClamAV deaktivieren (Anti-Virus) - ENABLE_CLAMAV=0 - # HACKERSCHUTZ (Pflicht!) + + # Sicherheit - ENABLE_FAIL2BAN=1 - # DNS Resolver (verhindert Spamhaus-Probleme) - - ENABLE_UNBOUND=1 - # #### END SPAM SECTION ##### - # END SPAM SECTION + - ENABLE_UNBOUND=1 + + # Sonstige - ENABLE_MANAGESIEVE=0 - ENABLE_POP3=1 - RSPAMD_LEARN=1 - ONE_DIR=1 - ENABLE_UPDATE_CHECK=0 - PERMIT_DOCKER=network - # - PERMIT_DOCKER=empty - - SSL_TYPE=manual - - SSL_CERT_PATH=/tmp/docker-mailserver/ssl/cert.pem - - SSL_KEY_PATH=/tmp/docker-mailserver/ssl/key.pem - # Amazon SES SMTP Relay + - SPOOF_PROTECTION=0 + - ENABLE_SRS=0 + - LOG_LEVEL=info + + # Amazon SES Relay - RELAY_HOST=email-smtp.us-east-2.amazonaws.com - RELAY_PORT=587 - RELAY_USER=${SES_SMTP_USER} - RELAY_PASSWORD=${SES_SMTP_PASSWORD} - # Content Filter AWS Credentials + + # AWS Credentials - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} - - AWS_REGION=us-east-2 - # Weitere Einstellungen - - POSTFIX_OVERRIDE_HOSTNAME=email-srvr.com + - AWS_REGION=us-east-2 + + # Postfix + # POSTFIX_OVERRIDE_HOSTNAME: Was Postfix im EHLO/HELO Banner sendet. + # node1.email-srvr.com passt zum TLS-Cert und ist der echte Hostname. + - POSTFIX_OVERRIDE_HOSTNAME=node1.email-srvr.com - POSTFIX_MYNETWORKS=172.16.0.0/12 172.17.0.0/12 172.18.0.0/12 [::1]/128 [fe80::]/64 - POSTFIX_MAILBOX_SIZE_LIMIT=0 - POSTFIX_MESSAGE_SIZE_LIMIT=0 - - SPOOF_PROTECTION=0 - - ENABLE_SRS=0 - # Debug-Einstellungen - - LOG_LEVEL=info + cap_add: - NET_ADMIN - SYS_PTRACE @@ -95,7 +119,6 @@ services: networks: mail_network: aliases: - - mail.email-srvr.com - mailserver roundcube: @@ -111,16 +134,14 @@ services: - ROUNDCUBEMAIL_DB_NAME=roundcube - ROUNDCUBEMAIL_DB_USER=roundcube - ROUNDCUBEMAIL_DB_PASSWORD=${ROUNDCUBE_DB_PASSWORD} - # Einfache Konfiguration ohne SSL-Probleme (für ersten Test) - - ROUNDCUBEMAIL_DEFAULT_HOST=ssl://mail.email-srvr.com + # Roundcube verbindet intern über den Docker-Alias + - ROUNDCUBEMAIL_DEFAULT_HOST=ssl://mailserver - ROUNDCUBEMAIL_DEFAULT_PORT=993 - - ROUNDCUBEMAIL_SMTP_SERVER=tls://mail.email-srvr.com + - ROUNDCUBEMAIL_SMTP_SERVER=tls://mailserver - ROUNDCUBEMAIL_SMTP_PORT=587 - #- ROUNDCUBEMAIL_PLUGINS=password,email_config,managesieve - ROUNDCUBEMAIL_PLUGINS=password,email_config - # In docker-compose.yml bei roundcube hinzufügen: ports: - - "8888:80" # Host:Container + - "8888:80" volumes: - ./docker-data/roundcube/config:/var/roundcube/config - ./docker-data/roundcube/plugins/email_config:/var/www/html/plugins/email_config:ro @@ -145,4 +166,4 @@ services: networks: mail_network: - external: true \ No newline at end of file + external: true \ No newline at end of file diff --git a/DMS/setup-dms-tls.sh b/DMS/setup-dms-tls.sh index a3c166e..ab0ec85 100755 --- a/DMS/setup-dms-tls.sh +++ b/DMS/setup-dms-tls.sh @@ -1,37 +1,40 @@ #!/bin/bash # setup-dms-tls.sh -# Generiert Dovecot und Postfix SNI-Konfigurationen für Multi-Domain TLS. -# Liest die vorhandenen Domains aus den DMS Accounts und erstellt: -# - docker-data/dms/config/dovecot-sni.cf (Dovecot SNI pro Domain) -# - docker-data/dms/config/postfix-main.cf (Postfix SNI Map + TLS Chain) +# Gehört ins Root-Verzeichnis des DMS (neben docker-compose.yml). # -# Voraussetzung: -# - Caddy hat Wildcard-Certs gezogen (z.B. *.andreasknuth.de) -# - Cert-Verzeichnis ist gemountet unter /etc/mail/certs im Container -# - Konvention Cert-Pfad: /etc/mail/certs/DOMAIN_NAME/*.DOMAIN_NAME.crt|.key +# Generiert Dovecot- und Postfix-SNI-Konfigurationen für Multi-Domain TLS. +# Liest Domains aus dem laufenden DMS und erstellt: +# - docker-data/dms/config/dovecot-sni.cf +# - docker-data/dms/config/postfix-main.cf +# +# Cert-Konvention (Caddy Wildcard): +# /etc/mail/certs/*.domain.tld/*.domain.tld.crt +# /etc/mail/certs/*.domain.tld/*.domain.tld.key # # Usage: -# DMS_CONTAINER=mailserver ./setup-dms-tls.sh -# DMS_CONTAINER=mailserver DEFAULT_DOMAIN=email-srvr.com ./setup-dms-tls.sh +# ./setup-dms-tls.sh +# DMS_CONTAINER=mailserver NODE_HOSTNAME=node1.email-srvr.com ./setup-dms-tls.sh set -e DMS_CONTAINER=${DMS_CONTAINER:-"mailserver"} -CONFIG_DIR=${CONFIG_DIR:-"./docker-data/dms/config"} +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CONFIG_DIR="$SCRIPT_DIR/docker-data/dms/config" CERTS_BASE_PATH=${CERTS_BASE_PATH:-"/etc/mail/certs"} -# Die Default-Domain für DMS hostname/domainname (bleibt email-srvr.com) -DEFAULT_DOMAIN=${DEFAULT_DOMAIN:-"email-srvr.com"} +# 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 " Container: $DMS_CONTAINER" -echo " Config Dir: $CONFIG_DIR" -echo " Certs Base: $CERTS_BASE_PATH" -echo " Default Domain: $DEFAULT_DOMAIN" +echo " DMS Container: $DMS_CONTAINER" +echo " Config Dir: $CONFIG_DIR" +echo " Certs Base: $CERTS_BASE_PATH" +echo " Node Hostname: $NODE_HOSTNAME" echo "============================================================" -# --- Alle Domains aus DMS Accounts lesen --- +# --- Domains aus DMS lesen --- echo "" echo "📋 Lese Domains aus DMS..." DOMAINS=$(docker exec "$DMS_CONTAINER" setup email list 2>/dev/null \ @@ -40,74 +43,80 @@ DOMAINS=$(docker exec "$DMS_CONTAINER" setup email list 2>/dev/null \ if [ -z "$DOMAINS" ]; then echo "❌ Keine Accounts im DMS gefunden!" - echo " Bitte zuerst Accounts anlegen: ./manage_mail_user.sh add user@domain.com PW" + echo " Bitte zuerst anlegen: ./manage_mail_user.sh add user@domain.com PW" exit 1 fi echo " Gefundene Domains:" for d in $DOMAINS; do echo " - $d"; done -# --- Cert-Verfügbarkeit prüfen --- +# --- Cert-Verfügbarkeit im Container prüfen --- echo "" -echo "🔍 Prüfe Zertifikat-Verfügbarkeit (im Container)..." -DOMAINS_WITH_CERTS="" -DOMAINS_WITHOUT_CERTS="" +echo "🔍 Prüfe Zertifikat-Verfügbarkeit..." +DOMAINS_OK="" +DOMAINS_MISSING="" for domain in $DOMAINS; do - # Caddy speichert Wildcard-Certs als: *.domain.tld/ - # Pfad im Container (über den Volume-Mount): /etc/mail/certs/*.domain.tld/ CERT_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.crt" KEY_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.key" - # Prüfe ob die Datei im Container existiert if docker exec "$DMS_CONTAINER" test -f "$CERT_PATH" 2>/dev/null; then - echo " ✅ $domain → Cert gefunden" - DOMAINS_WITH_CERTS="$DOMAINS_WITH_CERTS $domain" + echo " ✅ $domain → Cert vorhanden" + DOMAINS_OK="$DOMAINS_OK $domain" else echo " ⚠️ $domain → KEIN Cert unter $CERT_PATH" - echo " Caddy-Block '*.${domain}' eintragen und Caddy neu starten!" - DOMAINS_WITHOUT_CERTS="$DOMAINS_WITHOUT_CERTS $domain" + echo " → update-caddy-certs.sh ausführen + caddy reload!" + DOMAINS_MISSING="$DOMAINS_MISSING $domain" fi done -if [ -n "$DOMAINS_WITHOUT_CERTS" ]; then - echo "" - echo "⚠️ WARNUNG: Fehlende Certs für:$DOMAINS_WITHOUT_CERTS" - echo " Diese Domains werden NICHT in die SNI-Configs eingetragen." - echo " Bitte Certs erzeugen und Script erneut ausführen." - echo "" +# Node-Hostname Cert prüfen (direktes Cert, kein Wildcard) +NODE_CERT_PATH="$CERTS_BASE_PATH/$NODE_HOSTNAME/$NODE_HOSTNAME.crt" +NODE_KEY_PATH="$CERTS_BASE_PATH/$NODE_HOSTNAME/$NODE_HOSTNAME.key" +if docker exec "$DMS_CONTAINER" test -f "$NODE_CERT_PATH" 2>/dev/null; then + echo " ✅ $NODE_HOSTNAME → Cert vorhanden (Node Default)" + NODE_CERT_OK=true +else + echo " ⚠️ $NODE_HOSTNAME → KEIN Cert! Caddy-Block im Caddyfile prüfen." + NODE_CERT_OK=false fi -if [ -z "$DOMAINS_WITH_CERTS" ]; then - echo "❌ Kein einziges Zertifikat gefunden! Abbruch." +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 generieren +# DOVECOT SNI Konfiguration # ================================================================ DOVECOT_CFG="$CONFIG_DIR/dovecot-sni.cf" echo "" -echo "📝 Generiere Dovecot SNI Konfiguration: $DOVECOT_CFG" +echo "📝 Generiere: $DOVECOT_CFG" cat > "$DOVECOT_CFG" << 'HEADER' # dovecot-sni.cf - Automatisch generiert von setup-dms-tls.sh -# SNI-basierte TLS-Konfiguration für mehrere Domains. -# Dovecot wählt das Zertifikat anhand des SNI-Hostnamens des Clients. -# Dieses File wird via Volume-Mount in den Container eingebunden. +# 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. # -# Gemounteter Pfad: /tmp/docker-mailserver/dovecot-sni.cf -# In DMS docker-compose.yml volumes Sektion: +# Volume-Mount in docker-compose.yml: # - ./docker-data/dms/config/dovecot-sni.cf:/tmp/docker-mailserver/dovecot-sni.cf:ro HEADER -for domain in $DOMAINS_WITH_CERTS; do +for domain in $DOMAINS_OK; do CERT_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.crt" KEY_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.key" cat >> "$DOVECOT_CFG" << EOF -# Domain: $domain +# $domain local_name mail.$domain { ssl_cert = <$CERT_PATH ssl_key = <$KEY_PATH @@ -128,75 +137,69 @@ local_name pop.$domain { EOF done -echo " ✅ $DOVECOT_CFG erstellt ($(echo $DOMAINS_WITH_CERTS | wc -w) Domains)" +echo " ✅ Dovecot SNI: $(echo $DOMAINS_OK | wc -w) Domain(s)" # ================================================================ -# POSTFIX SNI Konfiguration generieren +# POSTFIX SNI Konfiguration # ================================================================ POSTFIX_CFG="$CONFIG_DIR/postfix-main.cf" echo "" -echo "📝 Generiere Postfix SNI Konfiguration: $POSTFIX_CFG" +echo "📝 Generiere: $POSTFIX_CFG" -# Prüfe ob postfix-main.cf schon existiert und sichere sie +# Backup falls vorhanden if [ -f "$POSTFIX_CFG" ]; then cp "$POSTFIX_CFG" "${POSTFIX_CFG}.bak.$(date +%Y%m%d%H%M%S)" - echo " ℹ️ Backup erstellt: ${POSTFIX_CFG}.bak.*" + echo " ℹ️ Backup: ${POSTFIX_CFG}.bak.*" fi -# TLS Chain Files für Postfix aufbauen -# Postfix unterstützt smtpd_tls_chain_files mit mehreren Key/Cert Paaren -CHAIN_FILES="" -for domain in $DOMAINS_WITH_CERTS; do +# smtpd_tls_chain_files aufbauen: Key + Cert Paar pro Domain +# Postfix wählt automatisch per SNI das passende Paar +CHAIN_LINES="" +for domain in $DOMAINS_OK; do KEY_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.key" CERT_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.crt" - if [ -z "$CHAIN_FILES" ]; then - CHAIN_FILES=" $KEY_PATH, $CERT_PATH" + if [ -z "$CHAIN_LINES" ]; then + CHAIN_LINES=" $KEY_PATH, $CERT_PATH" else - CHAIN_FILES="$CHAIN_FILES,\n $KEY_PATH, $CERT_PATH" + CHAIN_LINES="$CHAIN_LINES,\n $KEY_PATH, $CERT_PATH" fi done -cat > "$POSTFIX_CFG" << POSTFIX_CONF +cat > "$POSTFIX_CFG" << POSTFIX_EOF # postfix-main.cf - Automatisch generiert von setup-dms-tls.sh -# Postfix SNI-Konfiguration für mehrere Domains. -# DMS lädt dieses File automatisch beim Start via /tmp/docker-mailserver/ +# Postfix SNI-Konfiguration: pro Kundendomain ein Key/Cert-Paar. +# Postfix wählt beim TLS-Handshake das passende Paar per SNI. +# DMS lädt dieses File automatisch beim Start. -# ------------------------------------------------------------------ -# TLS Chain Files (Key + Cert pro Domain) -# Postfix wählt das passende Paar automatisch per SNI -# ------------------------------------------------------------------ +# TLS Chain: Key + Cert Paare (Postfix >= 3.4) smtpd_tls_chain_files = -$(printf '%b' "$CHAIN_FILES") +$(printf '%b' "$CHAIN_LINES") -POSTFIX_CONF +POSTFIX_EOF -echo " ✅ $POSTFIX_CFG erstellt" +echo " ✅ Postfix SNI: $(echo $DOMAINS_OK | wc -w) Domain(s)" # ================================================================ -# Hinweise für docker-compose.yml +# Zusammenfassung # ================================================================ echo "" echo "============================================================" +echo "✅ Konfigurationen generiert." +echo "" echo "📋 Nächste Schritte:" echo "" -echo "1. Volume-Mounts in DMS docker-compose.yml hinzufügen:" -echo "" -echo " volumes:" -echo " # Bestehend (Caddy Certs - gesamtes Verzeichnis):" -echo " - /var/lib/docker/volumes/caddy_data/_data/caddy/certificates/" -echo " acme-v02.api.letsencrypt.org-directory:/etc/mail/certs:ro" -echo "" -echo " # NEU - Dovecot SNI:" -echo " - ./docker-data/dms/config/dovecot-sni.cf:/tmp/docker-mailserver/dovecot-sni.cf:ro" -echo "" -echo " # Postfix-main.cf wird von DMS automatisch geladen wenn sie liegt unter:" -echo " - ./docker-data/dms/config/postfix-main.cf:/tmp/docker-mailserver/postfix-main.cf:ro" -echo "" -echo "2. DMS neu starten:" +echo "1. DMS neu starten:" echo " docker compose restart mailserver" echo "" -echo "3. TLS testen:" -for domain in $DOMAINS_WITH_CERTS; do - echo " openssl s_client -connect mail.$domain:993 -servername mail.$domain" +echo "2. TLS testen (SNI):" +for domain in $DOMAINS_OK; do + echo " openssl s_client -connect mail.$domain:993 -servername mail.$domain 2>/dev/null | grep 'subject\|issuer'" done +echo "" +echo "3. Bei neuen Domains:" +echo " a) Accounts anlegen: ./manage_mail_user.sh add user@newdomain.com PW" +echo " b) Im Caddy-Dir: ./update-caddy-certs.sh && docker exec caddy caddy reload ..." +echo " c) Warten bis Cert generiert (~30s)" +echo " d) Dieses Script erneut ausführen" +echo " e) docker compose restart mailserver" echo "============================================================" \ No newline at end of file diff --git a/caddy/update-caddy-certs.sh b/caddy/update-caddy-certs.sh index 70c3952..ed4ca2a 100755 --- a/caddy/update-caddy-certs.sh +++ b/caddy/update-caddy-certs.sh @@ -1,28 +1,35 @@ #!/bin/bash # update-caddy-certs.sh -# Liest alle Domains aus dem DMS und generiert die notwendigen -# Caddyfile-Blöcke für Wildcard-Zertifikate. +# Gehört ins Caddy-Verzeichnis (neben dem Caddyfile). # -# Die generierten Blöcke werden NICHT automatisch in das Caddyfile geschrieben, -# sondern in eine separate Datei (caddy_mail_certs.conf) ausgegeben, -# die per "import" in das Hauptcaddyfile eingebunden werden kann. +# Liest alle Domains aus dem DMS und generiert die Wildcard-Cert-Blöcke +# für Caddy in die Datei "mail_certs" (per "import mail_certs" im Caddyfile). +# +# Bei neuen Domains: Script erneut laufen lassen + caddy reload. # # Usage: -# DMS_CONTAINER=mailserver ./update-caddy-certs.sh -# DMS_CONTAINER=mailserver CADDY_DIR=/pfad/zu/caddy ./update-caddy-certs.sh -# DMS_CONTAINER=mailserver DRY_RUN=true ./update-caddy-certs.sh +# ./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_DIR=${CADDY_DIR:-"."} # Verzeichnis wo das Caddyfile liegt -OUTPUT_FILE=${OUTPUT_FILE:-"$CADDY_DIR/mail_certs"} # Ohne Extension - Caddy importiert ohne .conf +CADDY_CONTAINER=${CADDY_CONTAINER:-"caddy"} +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +OUTPUT_FILE="$SCRIPT_DIR/mail_certs" DRY_RUN=${DRY_RUN:-"false"} +# Node-Hostname des Mailservers (für Default-Cert Block) +# Wird immer mit eingetragen, auch wenn keine DMS-Accounts existieren. +NODE_HOSTNAME=${NODE_HOSTNAME:-"node1.email-srvr.com"} + echo "============================================================" echo " 📜 Caddy Wildcard-Cert Konfig Generator" -echo " DMS Container: $DMS_CONTAINER" -echo " Output: $OUTPUT_FILE" +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 "============================================================" @@ -34,104 +41,92 @@ DOMAINS=$(docker exec "$DMS_CONTAINER" setup email list 2>/dev/null \ | sort -u) if [ -z "$DOMAINS" ]; then - echo "❌ Keine Accounts gefunden!" - exit 1 + echo "⚠️ Keine DMS-Accounts gefunden. Nur Node-Hostname wird eingetragen." fi -echo " Gefundene Domains:" -for d in $DOMAINS; do echo " - $d"; done - -# --- email-srvr.com immer einschließen (Default-Domain des DMS) --- -EXTRA_DOMAINS="email-srvr.com" -for extra in $EXTRA_DOMAINS; do - if ! echo "$DOMAINS" | grep -q "^${extra}$"; then - DOMAINS="$DOMAINS $extra" - echo " + $extra (Default DMS Domain - immer dabei)" - fi -done +if [ -n "$DOMAINS" ]; then + echo " Gefundene Domains:" + for d in $DOMAINS; do echo " - $d"; done +fi # --- Konfig generieren --- echo "" echo "📝 Generiere Caddy-Konfiguration..." -CONTENT="" -CONTENT="${CONTENT}# mail_certs - Automatisch generiert von update-caddy-certs.sh\n" -CONTENT="${CONTENT}# Wildcard-Zertifikate für alle DMS-Domains.\n" -CONTENT="${CONTENT}# Einbinden im Hauptcaddyfile: import mail_certs\n" -CONTENT="${CONTENT}# Generiert: $(date)\n" -CONTENT="${CONTENT}\n" +OUTPUT="" +OUTPUT="${OUTPUT}# mail_certs - Automatisch generiert von update-caddy-certs.sh\n" +OUTPUT="${OUTPUT}# Wildcard-Zertifikate für DMS-Domains + Node-Hostname.\n" +OUTPUT="${OUTPUT}# Einbinden im Caddyfile: import mail_certs\n" +OUTPUT="${OUTPUT}# Generiert: $(date)\n" +OUTPUT="${OUTPUT}\n" +# Node-Hostname immer als erstes (Default-Cert des DMS) +echo " → Node-Hostname Block: $NODE_HOSTNAME" +OUTPUT="${OUTPUT}# Node-Hostname (Default-Cert für DMS Fallback)\n" +OUTPUT="${OUTPUT}${NODE_HOSTNAME} {\n" +OUTPUT="${OUTPUT} tls {\n" +OUTPUT="${OUTPUT} dns cloudflare {env.CLOUDFLARE_API_TOKEN}\n" +OUTPUT="${OUTPUT} }\n" +OUTPUT="${OUTPUT} respond \"OK\" 200\n" +OUTPUT="${OUTPUT}}\n\n" + +# Wildcard-Blocks pro Kundendomain for domain in $DOMAINS; do - echo " → Block für: $domain" - CONTENT="${CONTENT}# Wildcard-Cert für $domain\n" - CONTENT="${CONTENT}*.${domain}, ${domain} {\n" - CONTENT="${CONTENT} tls {\n" - CONTENT="${CONTENT} dns cloudflare {env.CLOUDFLARE_API_TOKEN}\n" - CONTENT="${CONTENT} }\n" - CONTENT="${CONTENT} respond \"OK\" 200\n" - CONTENT="${CONTENT}}\n" - CONTENT="${CONTENT}\n" + echo " → Wildcard Block: *.${domain}" + OUTPUT="${OUTPUT}# Wildcard-Cert für $domain\n" + OUTPUT="${OUTPUT}*.${domain}, ${domain} {\n" + OUTPUT="${OUTPUT} tls {\n" + OUTPUT="${OUTPUT} dns cloudflare {env.CLOUDFLARE_API_TOKEN}\n" + OUTPUT="${OUTPUT} }\n" + OUTPUT="${OUTPUT} respond \"OK\" 200\n" + OUTPUT="${OUTPUT}}\n\n" done # --- Ausgabe --- if [ "$DRY_RUN" = "true" ]; then echo "" - echo "--- VORSCHAU (DRY RUN) ---" - printf '%b' "$CONTENT" - echo "--- ENDE VORSCHAU ---" + echo "--- VORSCHAU ---" + printf '%b' "$OUTPUT" + echo "--- ENDE ---" else - printf '%b' "$CONTENT" > "$OUTPUT_FILE" - echo "" + printf '%b' "$OUTPUT" > "$OUTPUT_FILE" echo " ✅ Geschrieben: $OUTPUT_FILE" fi -# --- Prüfen ob Import im Caddyfile vorhanden --- -CADDYFILE="$CADDY_DIR/Caddyfile" +# --- 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 ERFORDERLICH:" - echo " 'import mail_certs' fehlt noch im Caddyfile!" - echo " Bitte folgende Zeile am Anfang (nach dem globalen Block) eintragen:" + echo "⚠️ AKTION: 'import mail_certs' fehlt noch im Caddyfile!" + echo " Bitte nach dem globalen {} Block eintragen:" echo "" - echo " import mail_certs" - echo "" - echo " Oder automatisch einfügen? (y/N)" - read -r answer - if [ "$answer" = "y" ] || [ "$answer" = "Y" ]; then - # Import nach der ersten Zeile mit "import " einfügen (falls schon welche da sind) - # oder nach dem globalen {} Block - if grep -q "^import " "$CADDYFILE"; then - # Schreibe nach der letzten import-Zeile - sed -i "/^import /a import mail_certs" "$CADDYFILE" - else - # Schreibe nach dem schließenden } des globalen Blocks - sed -i "/^}/a \\\nimport mail_certs" "$CADDYFILE" - fi - echo " ✅ Import eingefügt." - fi + echo " { ← globaler Block" + echo " email {env.CLOUDFLARE_EMAIL}" + echo " ..." + echo " }" + echo " import mail_certs ← hier einfügen" + echo " import email_autodiscover" + echo " ..." fi fi -# --- Caddy reload --- echo "" echo "============================================================" echo "🔄 Nächste Schritte:" echo "" -echo "1. Caddyfile prüfen - 'import mail_certs' muss vorhanden sein" +echo "1. Caddy Konfiguration validieren:" +echo " docker exec $CADDY_CONTAINER caddy validate --config /etc/caddy/Caddyfile" echo "" -echo "2. Caddy Konfiguration validieren:" -echo " docker exec caddy caddy validate --config /etc/caddy/Caddyfile" +echo "2. Caddy neu laden (kein Downtime):" +echo " docker exec $CADDY_CONTAINER caddy reload --config /etc/caddy/Caddyfile" echo "" -echo "3. Caddy neu laden (kein Downtime):" -echo " docker exec caddy caddy reload --config /etc/caddy/Caddyfile" +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-Generierung verfolgen (dauert ~30s pro Domain):" -echo " docker logs -f caddy 2>&1 | grep -i 'certificate\|acme\|tls'" -echo "" -echo "5. Cert-Pfade prüfen:" +echo "4. Cert-Pfade kontrollieren:" echo " ls /var/lib/docker/volumes/caddy_data/_data/caddy/certificates/" echo " acme-v02.api.letsencrypt.org-directory/" echo "============================================================" \ No newline at end of file From aee2335c48f155dc1844194c930f788f839a02b5 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 13:19:58 -0600 Subject: [PATCH 04/74] import mail_certs --- caddy/Caddyfile | 1 + 1 file changed, 1 insertion(+) diff --git a/caddy/Caddyfile b/caddy/Caddyfile index 5f16483..19dc860 100644 --- a/caddy/Caddyfile +++ b/caddy/Caddyfile @@ -4,6 +4,7 @@ acme_ca https://acme-v02.api.letsencrypt.org/directory debug } +import mail_certs import email_autodiscover # --------------------------------------------------------- # Block A: Die dedizierten Autodiscover Domains From 956214f8c9136ccb0d2adda3d60b44f95309d354 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 13:30:38 -0600 Subject: [PATCH 05/74] mail_network --- caddy/docker-compose.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/caddy/docker-compose.yml b/caddy/docker-compose.yml index 41c6677..4f3e6fa 100644 --- a/caddy/docker-compose.yml +++ b/caddy/docker-compose.yml @@ -12,9 +12,6 @@ services: extra_hosts: - 'host.docker.internal:host-gateway' networks: - - bizmatch - - keycloak - - gitea - mail_network volumes: - $PWD/Caddyfile:/etc/caddy/Caddyfile @@ -29,12 +26,6 @@ services: - CLOUDFLARE_EMAIL=${CLOUDFLARE_EMAIL} networks: - bizmatch: - external: true - keycloak: - external: true - gitea: - external: true mail_network: external: true From f9723b2b68e16065d47e0cc0b4d8802bbfe8e29e Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 13:36:01 -0600 Subject: [PATCH 06/74] mail-certs --- caddy/docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/caddy/docker-compose.yml b/caddy/docker-compose.yml index 4f3e6fa..e1f250b 100644 --- a/caddy/docker-compose.yml +++ b/caddy/docker-compose.yml @@ -15,6 +15,7 @@ services: - 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 From ce26d864b5c6f330243378f19c4f355a6add14de Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 14:28:52 -0600 Subject: [PATCH 07/74] wildcard instead of * --- DMS/setup-dms-tls.sh | 49 +++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/DMS/setup-dms-tls.sh b/DMS/setup-dms-tls.sh index ab0ec85..d6be340 100755 --- a/DMS/setup-dms-tls.sh +++ b/DMS/setup-dms-tls.sh @@ -8,8 +8,10 @@ # - docker-data/dms/config/postfix-main.cf # # Cert-Konvention (Caddy Wildcard): -# /etc/mail/certs/*.domain.tld/*.domain.tld.crt -# /etc/mail/certs/*.domain.tld/*.domain.tld.key +# 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 @@ -50,6 +52,15 @@ fi echo " Gefundene Domains:" for d in $DOMAINS; do echo " - $d"; done +# --- Cert-Pfad Hilfsfunktionen --- +# Caddy speichert Wildcard-Certs unter: wildcard_.domain.tld/wildcard_.domain.tld.crt +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..." @@ -57,11 +68,11 @@ DOMAINS_OK="" DOMAINS_MISSING="" for domain in $DOMAINS; do - CERT_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.crt" - KEY_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.key" + 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 vorhanden" + echo " ✅ $domain → $CERT_PATH" DOMAINS_OK="$DOMAINS_OK $domain" else echo " ⚠️ $domain → KEIN Cert unter $CERT_PATH" @@ -72,13 +83,10 @@ done # Node-Hostname Cert prüfen (direktes Cert, kein Wildcard) NODE_CERT_PATH="$CERTS_BASE_PATH/$NODE_HOSTNAME/$NODE_HOSTNAME.crt" -NODE_KEY_PATH="$CERTS_BASE_PATH/$NODE_HOSTNAME/$NODE_HOSTNAME.key" if docker exec "$DMS_CONTAINER" test -f "$NODE_CERT_PATH" 2>/dev/null; then echo " ✅ $NODE_HOSTNAME → Cert vorhanden (Node Default)" - NODE_CERT_OK=true else echo " ⚠️ $NODE_HOSTNAME → KEIN Cert! Caddy-Block im Caddyfile prüfen." - NODE_CERT_OK=false fi if [ -n "$DOMAINS_MISSING" ]; then @@ -106,14 +114,17 @@ cat > "$DOVECOT_CFG" << 'HEADER' # Dovecot liest dieses File über den Volume-Mount in /tmp/docker-mailserver/ # und wendet es automatisch an. # +# Caddy Wildcard-Cert Pfad-Schema: +# wildcard_.domain.tld/wildcard_.domain.tld.crt|.key +# # Volume-Mount in docker-compose.yml: # - ./docker-data/dms/config/dovecot-sni.cf:/tmp/docker-mailserver/dovecot-sni.cf:ro HEADER for domain in $DOMAINS_OK; do - CERT_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.crt" - KEY_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.key" + CERT_PATH=$(wildcard_cert_path "$domain") + KEY_PATH=$(wildcard_key_path "$domain") cat >> "$DOVECOT_CFG" << EOF # $domain @@ -138,6 +149,10 @@ EOF done echo " ✅ Dovecot SNI: $(echo $DOMAINS_OK | wc -w) Domain(s)" +echo "" +echo " --- dovecot-sni.cf Inhalt ---" +cat "$DOVECOT_CFG" +echo " --- Ende ---" # ================================================================ # POSTFIX SNI Konfiguration @@ -146,18 +161,17 @@ POSTFIX_CFG="$CONFIG_DIR/postfix-main.cf" echo "" echo "📝 Generiere: $POSTFIX_CFG" -# Backup falls vorhanden if [ -f "$POSTFIX_CFG" ]; then cp "$POSTFIX_CFG" "${POSTFIX_CFG}.bak.$(date +%Y%m%d%H%M%S)" echo " ℹ️ Backup: ${POSTFIX_CFG}.bak.*" fi -# smtpd_tls_chain_files aufbauen: Key + Cert Paar pro Domain +# smtpd_tls_chain_files: Key + Cert Paar pro Domain # Postfix wählt automatisch per SNI das passende Paar CHAIN_LINES="" for domain in $DOMAINS_OK; do - KEY_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.key" - CERT_PATH="$CERTS_BASE_PATH/*.$domain/*.$domain.crt" + KEY_PATH=$(wildcard_key_path "$domain") + CERT_PATH=$(wildcard_cert_path "$domain") if [ -z "$CHAIN_LINES" ]; then CHAIN_LINES=" $KEY_PATH, $CERT_PATH" else @@ -170,6 +184,9 @@ cat > "$POSTFIX_CFG" << POSTFIX_EOF # Postfix SNI-Konfiguration: pro Kundendomain ein Key/Cert-Paar. # Postfix wählt beim TLS-Handshake das passende Paar per SNI. # DMS lädt dieses File automatisch beim Start. +# +# Caddy Wildcard-Cert Pfad-Schema: +# wildcard_.domain.tld/wildcard_.domain.tld.crt|.key # TLS Chain: Key + Cert Paare (Postfix >= 3.4) smtpd_tls_chain_files = @@ -178,6 +195,10 @@ $(printf '%b' "$CHAIN_LINES") POSTFIX_EOF echo " ✅ Postfix SNI: $(echo $DOMAINS_OK | wc -w) Domain(s)" +echo "" +echo " --- postfix-main.cf Inhalt ---" +cat "$POSTFIX_CFG" +echo " --- Ende ---" # ================================================================ # Zusammenfassung From 3e656dacfac0f001397d603623a6561e8b8f326a Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 14:55:00 -0600 Subject: [PATCH 08/74] update --- basic_setup/cloudflareMigrationDns.sh | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/basic_setup/cloudflareMigrationDns.sh b/basic_setup/cloudflareMigrationDns.sh index 754e1a0..680a3b8 100755 --- a/basic_setup/cloudflareMigrationDns.sh +++ b/basic_setup/cloudflareMigrationDns.sh @@ -174,9 +174,20 @@ ensure_record "TXT" "$DOMAIN_NAME" "$FINAL_SPF" false # ------------------------------------------------------------------ # SCHRITT 6: Root Domain MX # ------------------------------------------------------------------ +# WICHTIG: Der MX Record zeigt auf Amazon SES (inbound-smtp.*.amazonaws.com), +# da eingehende Mails über SES → S3 → SQS → Worker → DMS laufen. +# Der DMS ist NICHT direkt aus dem Internet erreichbar. +# Dieser Record wird daher NICHT angefasst. echo "" -echo "--- 6. Root Domain MX ---" -ensure_record "MX" "$DOMAIN_NAME" "mail.$DOMAIN_NAME" false 10 +echo "--- 6. Root Domain MX (nur Info, wird nicht geändert) ---" +EXISTING_MX=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=MX&name=$DOMAIN_NAME" \ + -H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" | jq -r '.result[0].content') +if [ "$EXISTING_MX" == "null" ] || [ -z "$EXISTING_MX" ]; then + echo " ⚠️ Kein MX Record gefunden! Bitte manuell in SES/Cloudflare setzen:" + echo " inbound-smtp.$AWS_REGION.amazonaws.com (Prio 10)" +else + echo " ℹ️ MX vorhanden: $EXISTING_MX (wird nicht geändert)" +fi # ------------------------------------------------------------------ # SCHRITT 7: DMARC @@ -215,15 +226,6 @@ echo "--- 9. Autodiscover / Autoconfig ---" ensure_record "CNAME" "autodiscover.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false ensure_record "CNAME" "autoconfig.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false -# ------------------------------------------------------------------ -# SCHRITT 10: SRV Records -# ------------------------------------------------------------------ -echo "" -echo "--- 10. SRV Records ---" -ensure_record "SRV" "_imap._tcp.$DOMAIN_NAME" "0 5 143 mail.$DOMAIN_NAME" false -ensure_record "SRV" "_imaps._tcp.$DOMAIN_NAME" "0 5 993 mail.$DOMAIN_NAME" false -ensure_record "SRV" "_submission._tcp.$DOMAIN_NAME" "0 5 587 mail.$DOMAIN_NAME" false - echo "" echo "============================================================" echo "✅ Fertig für Domain: $DOMAIN_NAME" From a84bb23af0a3455e817995f675fcf8c3a09fe7a0 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 15:10:20 -0600 Subject: [PATCH 09/74] update --- caddy/update-caddy-certs.sh | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/caddy/update-caddy-certs.sh b/caddy/update-caddy-certs.sh index ed4ca2a..ca0d076 100755 --- a/caddy/update-caddy-certs.sh +++ b/caddy/update-caddy-certs.sh @@ -70,9 +70,12 @@ OUTPUT="${OUTPUT} }\n" OUTPUT="${OUTPUT} respond \"OK\" 200\n" OUTPUT="${OUTPUT}}\n\n" -# Wildcard-Blocks pro Kundendomain +# Wildcard-Blocks + webmail Block pro Kundendomain for domain in $DOMAINS; do echo " → Wildcard Block: *.${domain}" + echo " → Webmail Block: webmail.${domain}" + + # Wildcard-Cert Block (für Cert-Generierung + Fallback) OUTPUT="${OUTPUT}# Wildcard-Cert für $domain\n" OUTPUT="${OUTPUT}*.${domain}, ${domain} {\n" OUTPUT="${OUTPUT} tls {\n" @@ -80,6 +83,18 @@ for domain in $DOMAINS; do OUTPUT="${OUTPUT} }\n" OUTPUT="${OUTPUT} respond \"OK\" 200\n" OUTPUT="${OUTPUT}}\n\n" + + # Webmail Block (Roundcube) - muss VOR dem Wildcard-Block matchen + # Caddy wertet Blöcke in Reihenfolge aus, spezifischere Hosts gewinnen + 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" done # --- Ausgabe --- From 173b3f382fcc5288b31225c19c2a801bf07a2402 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 15:24:42 -0600 Subject: [PATCH 10/74] dfgdf --- DMS/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index d7efbad..fe6dedb 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -51,7 +51,7 @@ services: # Dovecot SNI Konfiguration (generiert von setup-dms-tls.sh) # DMS lädt /tmp/docker-mailserver/dovecot-sni.cf automatisch. # ------------------------------------------------------- - - ./docker-data/dms/config/dovecot-sni.cf:/tmp/docker-mailserver/dovecot-sni.cf:ro + - ./docker-data/dms/config/dovecot-sni.cf:/etc/dovecot/conf.d/99-sni.conf:ro environment: # ------------------------------------------------------- From a1c7fecc2780c5e1a8878bd4b27d637df1784130 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 15:31:33 -0600 Subject: [PATCH 11/74] sdf --- DMS/docker-compose.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index fe6dedb..78a417d 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -135,10 +135,10 @@ services: - ROUNDCUBEMAIL_DB_USER=roundcube - ROUNDCUBEMAIL_DB_PASSWORD=${ROUNDCUBE_DB_PASSWORD} # Roundcube verbindet intern über den Docker-Alias - - ROUNDCUBEMAIL_DEFAULT_HOST=ssl://mailserver - - ROUNDCUBEMAIL_DEFAULT_PORT=993 - - ROUNDCUBEMAIL_SMTP_SERVER=tls://mailserver - - ROUNDCUBEMAIL_SMTP_PORT=587 + - ROUNDCUBEMAIL_DEFAULT_HOST=mailserver # kein ssl:// prefix + - ROUNDCUBEMAIL_DEFAULT_PORT=143 # IMAP ohne SSL + - ROUNDCUBEMAIL_SMTP_SERVER=mailserver # kein tls:// prefix + - ROUNDCUBEMAIL_SMTP_PORT=25 # SMTP intern direkt - ROUNDCUBEMAIL_PLUGINS=password,email_config ports: - "8888:80" From 4ac32f43d0c18af7baf336dd20524c525d32bc96 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 15:39:02 -0600 Subject: [PATCH 12/74] xvcxv --- DMS/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index 78a417d..918b84f 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -135,7 +135,7 @@ services: - ROUNDCUBEMAIL_DB_USER=roundcube - ROUNDCUBEMAIL_DB_PASSWORD=${ROUNDCUBE_DB_PASSWORD} # Roundcube verbindet intern über den Docker-Alias - - ROUNDCUBEMAIL_DEFAULT_HOST=mailserver # kein ssl:// prefix + - ROUNDCUBEMAIL_DEFAULT_HOST=tls://mailserver # kein ssl:// prefix - ROUNDCUBEMAIL_DEFAULT_PORT=143 # IMAP ohne SSL - ROUNDCUBEMAIL_SMTP_SERVER=mailserver # kein tls:// prefix - ROUNDCUBEMAIL_SMTP_PORT=25 # SMTP intern direkt From 8f0a899b669d33bad3f839f58545dc017ede2fa7 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 15:41:21 -0600 Subject: [PATCH 13/74] sdsdf --- DMS/docker-data/dms/config/user-patches.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/DMS/docker-data/dms/config/user-patches.sh b/DMS/docker-data/dms/config/user-patches.sh index a84b070..f1ba0c2 100644 --- a/DMS/docker-data/dms/config/user-patches.sh +++ b/DMS/docker-data/dms/config/user-patches.sh @@ -18,4 +18,7 @@ if [ -f "$ACCOUNTS_FILE" ]; then cat "$WHITELIST_FILE" else echo "FEHLER: $ACCOUNTS_FILE wurde nicht gefunden!" -fi \ No newline at end of file +fi + +# Plaintext Auth für interne Docker-Verbindungen erlauben +echo "disable_plaintext_auth = no" >> /etc/dovecot/conf.d/10-auth.conf \ No newline at end of file From dd41497f0b01528de10a74ee271cfd7a8cadeab9 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 15:45:28 -0600 Subject: [PATCH 14/74] asdasd --- .../plugins/email_config/config/config.inc.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php diff --git a/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php b/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php new file mode 100644 index 0000000..7fb1067 --- /dev/null +++ b/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php @@ -0,0 +1,14 @@ + [ + 'verify_peer' => false, + 'verify_peer_name' => false, + ], +]; +$config['smtp_conn_options'] = [ + 'ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + ], +]; \ No newline at end of file From b90c8aec9e796b46c73e3e4348ee37f2c78fdfea Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 15:51:33 -0600 Subject: [PATCH 15/74] dfgdfg --- .../plugins/email_config/config/tls.inc.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 DMS/docker-data/roundcube/plugins/email_config/config/tls.inc.php diff --git a/DMS/docker-data/roundcube/plugins/email_config/config/tls.inc.php b/DMS/docker-data/roundcube/plugins/email_config/config/tls.inc.php new file mode 100644 index 0000000..b88fcb9 --- /dev/null +++ b/DMS/docker-data/roundcube/plugins/email_config/config/tls.inc.php @@ -0,0 +1,13 @@ + [ + 'verify_peer' => false, + 'verify_peer_name' => false, + ], +]; +$config['smtp_conn_options'] = [ + 'ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + ], +]; \ No newline at end of file From 915b0e59be9be3e08a2daf8bdc7dbedc48302418 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 16:00:09 -0600 Subject: [PATCH 16/74] sdfsdf --- DMS/docker-compose.yml | 9 +++++---- .../plugins/email_config/config/config.inc.php | 14 -------------- .../plugins/email_config/config/tls.inc.php | 13 ------------- 3 files changed, 5 insertions(+), 31 deletions(-) delete mode 100644 DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php delete mode 100644 DMS/docker-data/roundcube/plugins/email_config/config/tls.inc.php diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index 918b84f..7d7f363 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -120,6 +120,7 @@ services: mail_network: aliases: - mailserver + - node1.email-srvr.com roundcube: image: roundcube/roundcubemail:latest @@ -135,10 +136,10 @@ services: - ROUNDCUBEMAIL_DB_USER=roundcube - ROUNDCUBEMAIL_DB_PASSWORD=${ROUNDCUBE_DB_PASSWORD} # Roundcube verbindet intern über den Docker-Alias - - ROUNDCUBEMAIL_DEFAULT_HOST=tls://mailserver # kein ssl:// prefix - - ROUNDCUBEMAIL_DEFAULT_PORT=143 # IMAP ohne SSL - - ROUNDCUBEMAIL_SMTP_SERVER=mailserver # kein tls:// prefix - - ROUNDCUBEMAIL_SMTP_PORT=25 # SMTP intern direkt + - ROUNDCUBEMAIL_DEFAULT_HOST=ssl://node1.email-srvr.com + - ROUNDCUBEMAIL_DEFAULT_PORT=993 + - ROUNDCUBEMAIL_SMTP_SERVER=tls://node1.email-srvr.com + - ROUNDCUBEMAIL_SMTP_PORT=587 - ROUNDCUBEMAIL_PLUGINS=password,email_config ports: - "8888:80" diff --git a/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php b/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php deleted file mode 100644 index 7fb1067..0000000 --- a/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php +++ /dev/null @@ -1,14 +0,0 @@ - [ - 'verify_peer' => false, - 'verify_peer_name' => false, - ], -]; -$config['smtp_conn_options'] = [ - 'ssl' => [ - 'verify_peer' => false, - 'verify_peer_name' => false, - ], -]; \ No newline at end of file diff --git a/DMS/docker-data/roundcube/plugins/email_config/config/tls.inc.php b/DMS/docker-data/roundcube/plugins/email_config/config/tls.inc.php deleted file mode 100644 index b88fcb9..0000000 --- a/DMS/docker-data/roundcube/plugins/email_config/config/tls.inc.php +++ /dev/null @@ -1,13 +0,0 @@ - [ - 'verify_peer' => false, - 'verify_peer_name' => false, - ], -]; -$config['smtp_conn_options'] = [ - 'ssl' => [ - 'verify_peer' => false, - 'verify_peer_name' => false, - ], -]; \ No newline at end of file From 7956d2d6f5460ef3c468d123e1fbb92506150f3d Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 16:06:30 -0600 Subject: [PATCH 17/74] dgdfg --- .../roundcube/plugins/email_config/config/config.inc.php | 1 + 1 file changed, 1 insertion(+) create mode 100644 DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php diff --git a/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php b/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php new file mode 100644 index 0000000..90e80ae --- /dev/null +++ b/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php @@ -0,0 +1 @@ +$config['language'] = 'en_US'; \ No newline at end of file From b1a295df85bba9955299630c38df1b2f39e92116 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 16:06:54 -0600 Subject: [PATCH 18/74] sdfsdf --- DMS/docker-data/dms/config/user-patches.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/DMS/docker-data/dms/config/user-patches.sh b/DMS/docker-data/dms/config/user-patches.sh index f1ba0c2..a84b070 100644 --- a/DMS/docker-data/dms/config/user-patches.sh +++ b/DMS/docker-data/dms/config/user-patches.sh @@ -18,7 +18,4 @@ if [ -f "$ACCOUNTS_FILE" ]; then cat "$WHITELIST_FILE" else echo "FEHLER: $ACCOUNTS_FILE wurde nicht gefunden!" -fi - -# Plaintext Auth für interne Docker-Verbindungen erlauben -echo "disable_plaintext_auth = no" >> /etc/dovecot/conf.d/10-auth.conf \ No newline at end of file +fi \ No newline at end of file From 4452dae34c472b0b7855dcf2f700f8664cfa0ca4 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 17:58:08 -0600 Subject: [PATCH 19/74] dfgdfg --- DMS/docker-compose.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index 7d7f363..122a35a 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -147,7 +147,10 @@ services: - ./docker-data/roundcube/config:/var/roundcube/config - ./docker-data/roundcube/plugins/email_config:/var/www/html/plugins/email_config:ro networks: - - mail_network + mail_network: + aliases: + - mailserver + - node1.email-srvr.com restart: unless-stopped roundcube-db: @@ -162,7 +165,10 @@ services: volumes: - ./docker-data/roundcube/db:/var/lib/postgresql/data networks: - - mail_network + mail_network: + aliases: + - mailserver + - node1.email-srvr.com restart: unless-stopped networks: From bf96810d09fc53c9400a45dec0168ea888e42080 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 18:00:42 -0600 Subject: [PATCH 20/74] sdfsdf --- DMS/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index 122a35a..da28cbb 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -139,7 +139,7 @@ services: - ROUNDCUBEMAIL_DEFAULT_HOST=ssl://node1.email-srvr.com - ROUNDCUBEMAIL_DEFAULT_PORT=993 - ROUNDCUBEMAIL_SMTP_SERVER=tls://node1.email-srvr.com - - ROUNDCUBEMAIL_SMTP_PORT=587 + - ROUNDCUBEMAIL_SMTP_PORT=465 - ROUNDCUBEMAIL_PLUGINS=password,email_config ports: - "8888:80" From 42d16063a10c4a34fd9f4aacb759eaaccf1983a7 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 18:03:29 -0600 Subject: [PATCH 21/74] sdfdsf --- DMS/docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index da28cbb..974ec88 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -138,8 +138,8 @@ services: # Roundcube verbindet intern über den Docker-Alias - ROUNDCUBEMAIL_DEFAULT_HOST=ssl://node1.email-srvr.com - ROUNDCUBEMAIL_DEFAULT_PORT=993 - - ROUNDCUBEMAIL_SMTP_SERVER=tls://node1.email-srvr.com - - ROUNDCUBEMAIL_SMTP_PORT=465 + - ROUNDCUBEMAIL_SMTP_SERVER=node1.email-srvr.com + - ROUNDCUBEMAIL_SMTP_PORT=25 - ROUNDCUBEMAIL_PLUGINS=password,email_config ports: - "8888:80" From 0b0b7ddb82cbd216ac8f8187a429d213f036d308 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 18:06:23 -0600 Subject: [PATCH 22/74] dfgdfg --- DMS/docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index 974ec88..122a35a 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -138,8 +138,8 @@ services: # Roundcube verbindet intern über den Docker-Alias - ROUNDCUBEMAIL_DEFAULT_HOST=ssl://node1.email-srvr.com - ROUNDCUBEMAIL_DEFAULT_PORT=993 - - ROUNDCUBEMAIL_SMTP_SERVER=node1.email-srvr.com - - ROUNDCUBEMAIL_SMTP_PORT=25 + - ROUNDCUBEMAIL_SMTP_SERVER=tls://node1.email-srvr.com + - ROUNDCUBEMAIL_SMTP_PORT=587 - ROUNDCUBEMAIL_PLUGINS=password,email_config ports: - "8888:80" From c20d47103663eff523f92eb8838918f2cea7f2c8 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 18:20:35 -0600 Subject: [PATCH 23/74] removed --- DMS/docker-data/dms/config/ssl/cert.pem | 49 ------------------------- DMS/docker-data/dms/config/ssl/key.pem | 5 --- 2 files changed, 54 deletions(-) delete mode 100644 DMS/docker-data/dms/config/ssl/cert.pem delete mode 100644 DMS/docker-data/dms/config/ssl/key.pem diff --git a/DMS/docker-data/dms/config/ssl/cert.pem b/DMS/docker-data/dms/config/ssl/cert.pem deleted file mode 100644 index df1b11f..0000000 --- a/DMS/docker-data/dms/config/ssl/cert.pem +++ /dev/null @@ -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----- \ No newline at end of file diff --git a/DMS/docker-data/dms/config/ssl/key.pem b/DMS/docker-data/dms/config/ssl/key.pem deleted file mode 100644 index 5b4ab2d..0000000 --- a/DMS/docker-data/dms/config/ssl/key.pem +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN EC PRIVATE KEY----- -MHcCAQEEIFvBg5uuw4K36qMR6CYx09cfDcSPJOsCtQi/M/HKSYN1oAoGCCqGSM49 -AwEHoUQDQgAEM3FJe3K26CB2mrrGn/jynEPUNWJ4rRGKbVAp5i11eK6wS/MCbqit -p184hRS/0yKKrriVeTfgbiWqBqaXRSQpqg== ------END EC PRIVATE KEY----- \ No newline at end of file From a5a7096cc72522d313303b2f394d07a876b6bd90 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 18:30:04 -0600 Subject: [PATCH 24/74] sdfsdf --- DMS/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index 122a35a..8448499 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -46,7 +46,7 @@ services: # /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 - + - /var/lib/docker/volumes/caddy_data/_data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/node1.email-srvr.com:/etc/mail/certs:ro # ------------------------------------------------------- # Dovecot SNI Konfiguration (generiert von setup-dms-tls.sh) # DMS lädt /tmp/docker-mailserver/dovecot-sni.cf automatisch. From 06e25b33e095e168e7558ab06fc513794fb1c375 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 18:33:37 -0600 Subject: [PATCH 25/74] asdasd --- DMS/docker-compose.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index 8448499..ed837d2 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -46,7 +46,6 @@ services: # /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 - - /var/lib/docker/volumes/caddy_data/_data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/node1.email-srvr.com:/etc/mail/certs:ro # ------------------------------------------------------- # Dovecot SNI Konfiguration (generiert von setup-dms-tls.sh) # DMS lädt /tmp/docker-mailserver/dovecot-sni.cf automatisch. From bbc24cbb63ff4aa7bd0d83ad08cbcf5b3a155dbf Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 20:57:58 -0600 Subject: [PATCH 26/74] sdfsd --- DMS/docker-compose.yml | 10 ++-------- .../plugins/email_config/config/config.inc.php | 10 +++++++++- email-worker/.gitignore | 1 - 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index ed837d2..fee9590 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -146,10 +146,7 @@ services: - ./docker-data/roundcube/config:/var/roundcube/config - ./docker-data/roundcube/plugins/email_config:/var/www/html/plugins/email_config:ro networks: - mail_network: - aliases: - - mailserver - - node1.email-srvr.com + mail_network restart: unless-stopped roundcube-db: @@ -164,10 +161,7 @@ services: volumes: - ./docker-data/roundcube/db:/var/lib/postgresql/data networks: - mail_network: - aliases: - - mailserver - - node1.email-srvr.com + mail_network restart: unless-stopped networks: diff --git a/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php b/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php index 90e80ae..8f1a760 100644 --- a/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php +++ b/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php @@ -1 +1,9 @@ -$config['language'] = 'en_US'; \ No newline at end of file +$config['language'] = 'en_US'; +// SMTP STARTTLS mit deaktivierter Zert-Prüfung (sicher im internen Docker-Netzwerk) +$config['smtp_conn_options'] = array( + 'ssl' => array( + 'verify_peer' => false, + 'verify_peer_name' => false, + 'allow_self_signed' => true, + ), +); \ No newline at end of file diff --git a/email-worker/.gitignore b/email-worker/.gitignore index 1626c91..6e9d2ba 100644 --- a/email-worker/.gitignore +++ b/email-worker/.gitignore @@ -10,7 +10,6 @@ ENV/ .venv # Logs -logs/ *.log # Environment From bd3b2db23544e8dc39776cd73ec2bf785ea0f7e8 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 20:59:52 -0600 Subject: [PATCH 27/74] sdfsdf --- DMS/docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index fee9590..748edd1 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -146,7 +146,7 @@ services: - ./docker-data/roundcube/config:/var/roundcube/config - ./docker-data/roundcube/plugins/email_config:/var/www/html/plugins/email_config:ro networks: - mail_network + - mail_network restart: unless-stopped roundcube-db: @@ -161,7 +161,7 @@ services: volumes: - ./docker-data/roundcube/db:/var/lib/postgresql/data networks: - mail_network + - mail_network restart: unless-stopped networks: From 51405a3ec5ef3dddee66884e4811a2b1495ddbb9 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 21:18:06 -0600 Subject: [PATCH 28/74] sdfsdf --- DMS/docker-compose.yml | 4 ++-- .../roundcube/plugins/email_config/config/config.inc.php | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index 748edd1..e9feab5 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -137,13 +137,13 @@ services: # Roundcube verbindet intern über den Docker-Alias - ROUNDCUBEMAIL_DEFAULT_HOST=ssl://node1.email-srvr.com - ROUNDCUBEMAIL_DEFAULT_PORT=993 - - ROUNDCUBEMAIL_SMTP_SERVER=tls://node1.email-srvr.com + - ROUNDCUBEMAIL_SMTP_SERVER=tls://mailserver - ROUNDCUBEMAIL_SMTP_PORT=587 - ROUNDCUBEMAIL_PLUGINS=password,email_config ports: - "8888:80" 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 networks: - mail_network diff --git a/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php b/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php index 8f1a760..7b3d877 100644 --- a/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php +++ b/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php @@ -1,5 +1,6 @@ + + array( 'verify_peer' => false, From 552dd73f0a65a6334fda987cfb9df799230a2c8f Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 21:28:07 -0600 Subject: [PATCH 29/74] sdfsd --- DMS/docker-compose.yml | 4 ++-- .../roundcube/plugins/email_config/config/config.inc.php | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index e9feab5..44fb353 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -137,8 +137,8 @@ services: # Roundcube verbindet intern über den Docker-Alias - ROUNDCUBEMAIL_DEFAULT_HOST=ssl://node1.email-srvr.com - ROUNDCUBEMAIL_DEFAULT_PORT=993 - - ROUNDCUBEMAIL_SMTP_SERVER=tls://mailserver - - ROUNDCUBEMAIL_SMTP_PORT=587 + - ROUNDCUBEMAIL_SMTP_SERVER=ssl://mailserver + - ROUNDCUBEMAIL_SMTP_PORT=465 - ROUNDCUBEMAIL_PLUGINS=password,email_config ports: - "8888:80" diff --git a/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php b/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php index 7b3d877..a06c3ef 100644 --- a/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php +++ b/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php @@ -1,6 +1,7 @@ - array( 'verify_peer' => false, From b2d41e2baa4f379a617b72ba01d5001b0eec3857 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 21:44:13 -0600 Subject: [PATCH 30/74] sdfsd --- DMS/docker-compose.yml | 4 ++-- .../roundcube/plugins/email_config/config/config.inc.php | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index 44fb353..79e6843 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -137,8 +137,8 @@ services: # Roundcube verbindet intern über den Docker-Alias - ROUNDCUBEMAIL_DEFAULT_HOST=ssl://node1.email-srvr.com - ROUNDCUBEMAIL_DEFAULT_PORT=993 - - ROUNDCUBEMAIL_SMTP_SERVER=ssl://mailserver - - ROUNDCUBEMAIL_SMTP_PORT=465 + - ROUNDCUBEMAIL_SMTP_SERVER=tls://mailserver # intern, kein externer DNS-SNI-Chaos + - ROUNDCUBEMAIL_SMTP_PORT=587 - ROUNDCUBEMAIL_PLUGINS=password,email_config ports: - "8888:80" diff --git a/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php b/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php index a06c3ef..d163ef0 100644 --- a/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php +++ b/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php @@ -2,6 +2,9 @@ $config['language'] = 'en_US'; +$config['smtp_server'] = 'ssl://mailserver'; +$config['smtp_port'] = 465; + $config['smtp_conn_options'] = array( 'ssl' => array( 'verify_peer' => false, From 39e862cdd5d3ecff5fb6ebfac85db26bc85ee00f Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 21:47:03 -0600 Subject: [PATCH 31/74] dfgdfg --- .../plugins/email_config/config/config.inc.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php b/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php index d163ef0..a3a25e2 100644 --- a/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php +++ b/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php @@ -1,7 +1,11 @@ false, 'allow_self_signed' => true, ), -); \ No newline at end of file +); + +$config['language'] = 'en_US'; \ No newline at end of file From 69fbb670f1aabaae44d5d9d7eaefcd7662d4baef Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 21:51:09 -0600 Subject: [PATCH 32/74] move --- .../roundcube/{plugins/email_config => }/config/config.inc.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename DMS/docker-data/roundcube/{plugins/email_config => }/config/config.inc.php (100%) diff --git a/DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php b/DMS/docker-data/roundcube/config/config.inc.php similarity index 100% rename from DMS/docker-data/roundcube/plugins/email_config/config/config.inc.php rename to DMS/docker-data/roundcube/config/config.inc.php From 7bc8cbb9f763ba082930852d4ec271bc63d66ede Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 21:58:27 -0600 Subject: [PATCH 33/74] sdfsdf --- DMS/docker-data/roundcube/config/config.inc.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/DMS/docker-data/roundcube/config/config.inc.php b/DMS/docker-data/roundcube/config/config.inc.php index a3a25e2..caab2c7 100644 --- a/DMS/docker-data/roundcube/config/config.inc.php +++ b/DMS/docker-data/roundcube/config/config.inc.php @@ -1,10 +1,6 @@ Date: Sun, 22 Feb 2026 22:16:22 -0600 Subject: [PATCH 34/74] sdfsdf --- DMS/docker-compose.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index 79e6843..4ebf0d7 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -140,6 +140,9 @@ services: - ROUNDCUBEMAIL_SMTP_SERVER=tls://mailserver # intern, kein externer DNS-SNI-Chaos - ROUNDCUBEMAIL_SMTP_PORT=587 - ROUNDCUBEMAIL_PLUGINS=password,email_config + # 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: - "8888:80" volumes: From ee02d505c67e87ede3ce69dea9cd841b8ff3e0a9 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 22:22:28 -0600 Subject: [PATCH 35/74] sdfsdf --- DMS/docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index 4ebf0d7..bfe3dda 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -137,8 +137,8 @@ services: # Roundcube verbindet intern über den Docker-Alias - ROUNDCUBEMAIL_DEFAULT_HOST=ssl://node1.email-srvr.com - ROUNDCUBEMAIL_DEFAULT_PORT=993 - - ROUNDCUBEMAIL_SMTP_SERVER=tls://mailserver # intern, kein externer DNS-SNI-Chaos - - ROUNDCUBEMAIL_SMTP_PORT=587 + - ROUNDCUBEMAIL_SMTP_SERVER=ssl://node1.email-srvr.com # intern, kein externer DNS-SNI-Chaos + - ROUNDCUBEMAIL_SMTP_PORT=465 - ROUNDCUBEMAIL_PLUGINS=password,email_config # NEU: Schaltet die strikte PHP-Zertifikatsprüfung für interne Verbindungen ab - ROUNDCUBEMAIL_IMAP_CONN_OPTIONS={"ssl":{"verify_peer":false,"verify_peer_name":false}} From 3f919360981921f312959cf6cb80cb6af8c1a995 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 22:26:49 -0600 Subject: [PATCH 36/74] dfgdfg --- DMS/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index bfe3dda..8f323c0 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -146,7 +146,7 @@ services: ports: - "8888:80" volumes: - - ./docker-data/roundcube/config:/var/www/html/config + # - ./docker-data/roundcube/config:/var/www/html/config - ./docker-data/roundcube/plugins/email_config:/var/www/html/plugins/email_config:ro networks: - mail_network From 3381fd68c24326d0c02355f8f8a3a929f1474389 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 22 Feb 2026 22:30:33 -0600 Subject: [PATCH 37/74] sdfsdf --- DMS/docker-compose.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index 8f323c0..05f8a4c 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -137,8 +137,13 @@ services: # Roundcube verbindet intern über den Docker-Alias - ROUNDCUBEMAIL_DEFAULT_HOST=ssl://node1.email-srvr.com - ROUNDCUBEMAIL_DEFAULT_PORT=993 - - ROUNDCUBEMAIL_SMTP_SERVER=ssl://node1.email-srvr.com # intern, kein externer DNS-SNI-Chaos - - ROUNDCUBEMAIL_SMTP_PORT=465 + # Interner Traffic ohne TLS + - ROUNDCUBEMAIL_SMTP_SERVER=mailserver + - ROUNDCUBEMAIL_SMTP_PORT=25 + + # WICHTIG: Variablen LEER lassen, damit Roundcube keine Authentifizierung versucht! + - ROUNDCUBEMAIL_SMTP_USER= + - ROUNDCUBEMAIL_SMTP_PASSWORD= - ROUNDCUBEMAIL_PLUGINS=password,email_config # NEU: Schaltet die strikte PHP-Zertifikatsprüfung für interne Verbindungen ab - ROUNDCUBEMAIL_IMAP_CONN_OPTIONS={"ssl":{"verify_peer":false,"verify_peer_name":false}} From 98c78d8dcec5dd50d74f6c4f4981e5a82c3a55a6 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Mon, 23 Feb 2026 09:51:46 -0600 Subject: [PATCH 38/74] dfgdfg --- DMS/docker-compose.yml | 4 +- DMS/update_dms_config.sh | 193 +++++++++++++++++++++++---------------- 2 files changed, 114 insertions(+), 83 deletions(-) diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index 05f8a4c..3d10200 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -138,8 +138,8 @@ services: - ROUNDCUBEMAIL_DEFAULT_HOST=ssl://node1.email-srvr.com - ROUNDCUBEMAIL_DEFAULT_PORT=993 # Interner Traffic ohne TLS - - ROUNDCUBEMAIL_SMTP_SERVER=mailserver - - ROUNDCUBEMAIL_SMTP_PORT=25 + - ROUNDCUBEMAIL_SMTP_SERVER=tls://node1.email-srvr.com + - ROUNDCUBEMAIL_SMTP_PORT=587 # WICHTIG: Variablen LEER lassen, damit Roundcube keine Authentifizierung versucht! - ROUNDCUBEMAIL_SMTP_USER= diff --git a/DMS/update_dms_config.sh b/DMS/update_dms_config.sh index 3fb73bf..5be8c50 100644 --- a/DMS/update_dms_config.sh +++ b/DMS/update_dms_config.sh @@ -1,91 +1,122 @@ #!/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 +# setup-dms-tls.sh +# Generiert SNI-Konfigurationen für Dovecot und Postfix anhand der DMS-Accounts. +# +# Vorbereitung: +# Stelle sicher, dass dieses Skript ausführbar ist: chmod +x setup-dms-tls.sh set -e -DOMAIN=$1 -DOCKER_COMPOSE_FILE="./docker-compose.yml" -HEADER_CHECKS_FILE="./docker-data/dms/config/postfix/smtp_header_checks" +DMS_CONTAINER=${DMS_CONTAINER:-"mailserver"} +CONFIG_DIR="./docker-data/dms/config" +NODE_HOSTNAME=${NODE_HOSTNAME:-"node1.email-srvr.com"} -if [ -z "$DOMAIN" ]; then - echo "Usage: $0 " - echo "Example: $0 cielectrical.com" - exit 1 +# Zieldateien im Config-Verzeichnis (werden in den Container gemountet) +DOVECOT_SNI_FILE="$CONFIG_DIR/dovecot-sni.cf" +POSTFIX_MAIN_FILE="$CONFIG_DIR/postfix-main.cf" +POSTFIX_SNI_FILE="$CONFIG_DIR/postfix-sni.map" + +echo "============================================================" +echo " 🔐 DMS SNI-Config Generator (Postfix & Dovecot)" +echo " DMS Container: $DMS_CONTAINER" +echo " Node Hostname: $NODE_HOSTNAME" +echo "============================================================" + +# Sicherstellen, dass das Config-Verzeichnis existiert +mkdir -p "$CONFIG_DIR" + +# --- Domains aus DMS lesen --- +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." + # Wir erstellen trotzdem leere/Basis-Dateien, damit Postfix nicht abstürzt fi -echo "=== Aktualisiere lokale Konfiguration für $DOMAIN ===" +# ========================================== +# 1. Postfix Main Config (postfix-main.cf) +# ========================================== +echo "📝 Erstelle $POSTFIX_MAIN_FILE ..." +cat < "$POSTFIX_MAIN_FILE" +# 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 = /etc/mail/certs/${NODE_HOSTNAME}/${NODE_HOSTNAME}.key, /etc/mail/certs/${NODE_HOSTNAME}/${NODE_HOSTNAME}.crt -# --------------------------------------------- -# 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. SNI-Mapping aktivieren +# Wir nutzen 'texthash', damit Postfix die Klartext-Map direkt lesen kann, +# ohne dass 'postmap' im Container ausgeführt werden muss! +tls_server_sni_maps = texthash:/tmp/docker-mailserver/postfix-sni.map +EOF -# --------------------------------------------- -# 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 +# ========================================== +# 2. Dovecot & Postfix SNI Maps initialisieren +# ========================================== +echo "📝 Initialisiere Map-Dateien..." +echo "# dovecot-sni.cf - Automatisch generiert" > "$DOVECOT_SNI_FILE" +echo "# postfix-sni.map - Automatisch generiert (Format: domain key_pfad cert_pfad)" > "$POSTFIX_SNI_FILE" -echo "========================================================" -echo "Konfiguration aktualisiert." -echo "HINWEIS: Damit die Änderungen wirksam werden, führen Sie bitte aus:" -echo " docker compose up -d --force-recreate" -echo "========================================================" \ No newline at end of file +# ========================================== +# 3. Schleife über alle Kundendomains +# ========================================== +for domain in $DOMAINS; do + echo " -> Füge SNI-Regeln für $domain hinzu..." + + # Pfade, wie Caddy sie für Wildcards anlegt (wildcard_.domain.tld) + CERT_DIR="wildcard_.${domain}" + CERT_KEY="/etc/mail/certs/${CERT_DIR}/${CERT_DIR}.key" + CERT_CRT="/etc/mail/certs/${CERT_DIR}/${CERT_DIR}.crt" + + # --- Dovecot Block --- + cat <> "$DOVECOT_SNI_FILE" + +# --- $domain --- +local_name mail.${domain} { + ssl_cert = <${CERT_CRT} + ssl_key = <${CERT_KEY} +} +local_name imap.${domain} { + ssl_cert = <${CERT_CRT} + ssl_key = <${CERT_KEY} +} +local_name smtp.${domain} { + ssl_cert = <${CERT_CRT} + ssl_key = <${CERT_KEY} +} +local_name pop.${domain} { + ssl_cert = <${CERT_CRT} + ssl_key = <${CERT_KEY} +} +local_name ${domain} { + ssl_cert = <${CERT_CRT} + ssl_key = <${CERT_KEY} +} +EOF + + # --- Postfix Map Einträge --- + # Bei Postfix SNI Maps müssen Key und Cert in einer Zeile hinter dem Hostnamen stehen + cat <> "$POSTFIX_SNI_FILE" +mail.${domain} ${CERT_KEY} ${CERT_CRT} +smtp.${domain} ${CERT_KEY} ${CERT_CRT} +imap.${domain} ${CERT_KEY} ${CERT_CRT} +pop.${domain} ${CERT_KEY} ${CERT_CRT} +${domain} ${CERT_KEY} ${CERT_CRT} +EOF + +done + +echo "" +echo "✅ Alle Konfigurationsdateien erfolgreich generiert!" + +# ========================================== +# 4. Dienste im Container neu laden +# ========================================== +echo "🔄 Lade Postfix und Dovecot neu (ohne Downtime)..." +docker exec "$DMS_CONTAINER" postfix reload || echo "⚠️ Postfix Reload fehlgeschlagen (Container läuft nicht?)" +docker exec "$DMS_CONTAINER" dovecot reload || echo "⚠️ Dovecot Reload fehlgeschlagen" + +echo "============================================================" +echo "🎉 Fertig!" \ No newline at end of file From 7920ab07b8cf99ba5fc49f16f0edaa8b2c6a5f95 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Mon, 23 Feb 2026 10:00:36 -0600 Subject: [PATCH 39/74] gfhgfh --- DMS/setup-dms-tls.sh | 91 ++++++++++++----------------- DMS/update_dms_config.sh | 122 --------------------------------------- 2 files changed, 36 insertions(+), 177 deletions(-) delete mode 100644 DMS/update_dms_config.sh diff --git a/DMS/setup-dms-tls.sh b/DMS/setup-dms-tls.sh index d6be340..f721c83 100755 --- a/DMS/setup-dms-tls.sh +++ b/DMS/setup-dms-tls.sh @@ -6,6 +6,7 @@ # 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 @@ -53,7 +54,6 @@ echo " Gefundene Domains:" for d in $DOMAINS; do echo " - $d"; done # --- Cert-Pfad Hilfsfunktionen --- -# Caddy speichert Wildcard-Certs unter: wildcard_.domain.tld/wildcard_.domain.tld.crt wildcard_cert_path() { echo "$CERTS_BASE_PATH/wildcard_.${1}/wildcard_.${1}.crt" } @@ -76,13 +76,14 @@ for domain in $DOMAINS; do DOMAINS_OK="$DOMAINS_OK $domain" else echo " ⚠️ $domain → KEIN Cert unter $CERT_PATH" - echo " → update-caddy-certs.sh ausführen + caddy reload!" + 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 @@ -113,13 +114,6 @@ cat > "$DOVECOT_CFG" << 'HEADER' # 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. -# -# Caddy Wildcard-Cert Pfad-Schema: -# wildcard_.domain.tld/wildcard_.domain.tld.crt|.key -# -# Volume-Mount in docker-compose.yml: -# - ./docker-data/dms/config/dovecot-sni.cf:/tmp/docker-mailserver/dovecot-sni.cf:ro - HEADER for domain in $DOMAINS_OK; do @@ -149,56 +143,49 @@ EOF done echo " ✅ Dovecot SNI: $(echo $DOMAINS_OK | wc -w) Domain(s)" -echo "" -echo " --- dovecot-sni.cf Inhalt ---" -cat "$DOVECOT_CFG" -echo " --- Ende ---" # ================================================================ -# POSTFIX SNI Konfiguration +# 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" +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)" - echo " ℹ️ Backup: ${POSTFIX_CFG}.bak.*" fi -# smtpd_tls_chain_files: Key + Cert Paar pro Domain -# Postfix wählt automatisch per SNI das passende Paar -CHAIN_LINES="" +# 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") - if [ -z "$CHAIN_LINES" ]; then - CHAIN_LINES=" $KEY_PATH, $CERT_PATH" - else - CHAIN_LINES="$CHAIN_LINES,\n $KEY_PATH, $CERT_PATH" - fi + + 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 -cat > "$POSTFIX_CFG" << POSTFIX_EOF -# postfix-main.cf - Automatisch generiert von setup-dms-tls.sh -# Postfix SNI-Konfiguration: pro Kundendomain ein Key/Cert-Paar. -# Postfix wählt beim TLS-Handshake das passende Paar per SNI. -# DMS lädt dieses File automatisch beim Start. -# -# Caddy Wildcard-Cert Pfad-Schema: -# wildcard_.domain.tld/wildcard_.domain.tld.crt|.key - -# TLS Chain: Key + Cert Paare (Postfix >= 3.4) -smtpd_tls_chain_files = -$(printf '%b' "$CHAIN_LINES") - -POSTFIX_EOF - -echo " ✅ Postfix SNI: $(echo $DOMAINS_OK | wc -w) Domain(s)" -echo "" -echo " --- postfix-main.cf Inhalt ---" -cat "$POSTFIX_CFG" -echo " --- Ende ---" +echo " ✅ Postfix SNI: $(echo $DOMAINS_OK | wc -w) Domain(s) konfiguriert" # ================================================================ # Zusammenfassung @@ -207,20 +194,14 @@ 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. DMS neu starten:" -echo " docker compose restart mailserver" -echo "" -echo "2. TLS testen (SNI):" +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 "" -echo "3. Bei neuen Domains:" -echo " a) Accounts anlegen: ./manage_mail_user.sh add user@newdomain.com PW" -echo " b) Im Caddy-Dir: ./update-caddy-certs.sh && docker exec caddy caddy reload ..." -echo " c) Warten bis Cert generiert (~30s)" -echo " d) Dieses Script erneut ausführen" -echo " e) docker compose restart mailserver" echo "============================================================" \ No newline at end of file diff --git a/DMS/update_dms_config.sh b/DMS/update_dms_config.sh deleted file mode 100644 index 5be8c50..0000000 --- a/DMS/update_dms_config.sh +++ /dev/null @@ -1,122 +0,0 @@ -#!/bin/bash -# setup-dms-tls.sh -# Generiert SNI-Konfigurationen für Dovecot und Postfix anhand der DMS-Accounts. -# -# Vorbereitung: -# Stelle sicher, dass dieses Skript ausführbar ist: chmod +x setup-dms-tls.sh - -set -e - -DMS_CONTAINER=${DMS_CONTAINER:-"mailserver"} -CONFIG_DIR="./docker-data/dms/config" -NODE_HOSTNAME=${NODE_HOSTNAME:-"node1.email-srvr.com"} - -# Zieldateien im Config-Verzeichnis (werden in den Container gemountet) -DOVECOT_SNI_FILE="$CONFIG_DIR/dovecot-sni.cf" -POSTFIX_MAIN_FILE="$CONFIG_DIR/postfix-main.cf" -POSTFIX_SNI_FILE="$CONFIG_DIR/postfix-sni.map" - -echo "============================================================" -echo " 🔐 DMS SNI-Config Generator (Postfix & Dovecot)" -echo " DMS Container: $DMS_CONTAINER" -echo " Node Hostname: $NODE_HOSTNAME" -echo "============================================================" - -# Sicherstellen, dass das Config-Verzeichnis existiert -mkdir -p "$CONFIG_DIR" - -# --- Domains aus DMS lesen --- -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." - # Wir erstellen trotzdem leere/Basis-Dateien, damit Postfix nicht abstürzt -fi - -# ========================================== -# 1. Postfix Main Config (postfix-main.cf) -# ========================================== -echo "📝 Erstelle $POSTFIX_MAIN_FILE ..." -cat < "$POSTFIX_MAIN_FILE" -# 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 = /etc/mail/certs/${NODE_HOSTNAME}/${NODE_HOSTNAME}.key, /etc/mail/certs/${NODE_HOSTNAME}/${NODE_HOSTNAME}.crt - -# 2. SNI-Mapping aktivieren -# Wir nutzen 'texthash', damit Postfix die Klartext-Map direkt lesen kann, -# ohne dass 'postmap' im Container ausgeführt werden muss! -tls_server_sni_maps = texthash:/tmp/docker-mailserver/postfix-sni.map -EOF - -# ========================================== -# 2. Dovecot & Postfix SNI Maps initialisieren -# ========================================== -echo "📝 Initialisiere Map-Dateien..." -echo "# dovecot-sni.cf - Automatisch generiert" > "$DOVECOT_SNI_FILE" -echo "# postfix-sni.map - Automatisch generiert (Format: domain key_pfad cert_pfad)" > "$POSTFIX_SNI_FILE" - -# ========================================== -# 3. Schleife über alle Kundendomains -# ========================================== -for domain in $DOMAINS; do - echo " -> Füge SNI-Regeln für $domain hinzu..." - - # Pfade, wie Caddy sie für Wildcards anlegt (wildcard_.domain.tld) - CERT_DIR="wildcard_.${domain}" - CERT_KEY="/etc/mail/certs/${CERT_DIR}/${CERT_DIR}.key" - CERT_CRT="/etc/mail/certs/${CERT_DIR}/${CERT_DIR}.crt" - - # --- Dovecot Block --- - cat <> "$DOVECOT_SNI_FILE" - -# --- $domain --- -local_name mail.${domain} { - ssl_cert = <${CERT_CRT} - ssl_key = <${CERT_KEY} -} -local_name imap.${domain} { - ssl_cert = <${CERT_CRT} - ssl_key = <${CERT_KEY} -} -local_name smtp.${domain} { - ssl_cert = <${CERT_CRT} - ssl_key = <${CERT_KEY} -} -local_name pop.${domain} { - ssl_cert = <${CERT_CRT} - ssl_key = <${CERT_KEY} -} -local_name ${domain} { - ssl_cert = <${CERT_CRT} - ssl_key = <${CERT_KEY} -} -EOF - - # --- Postfix Map Einträge --- - # Bei Postfix SNI Maps müssen Key und Cert in einer Zeile hinter dem Hostnamen stehen - cat <> "$POSTFIX_SNI_FILE" -mail.${domain} ${CERT_KEY} ${CERT_CRT} -smtp.${domain} ${CERT_KEY} ${CERT_CRT} -imap.${domain} ${CERT_KEY} ${CERT_CRT} -pop.${domain} ${CERT_KEY} ${CERT_CRT} -${domain} ${CERT_KEY} ${CERT_CRT} -EOF - -done - -echo "" -echo "✅ Alle Konfigurationsdateien erfolgreich generiert!" - -# ========================================== -# 4. Dienste im Container neu laden -# ========================================== -echo "🔄 Lade Postfix und Dovecot neu (ohne Downtime)..." -docker exec "$DMS_CONTAINER" postfix reload || echo "⚠️ Postfix Reload fehlgeschlagen (Container läuft nicht?)" -docker exec "$DMS_CONTAINER" dovecot reload || echo "⚠️ Dovecot Reload fehlgeschlagen" - -echo "============================================================" -echo "🎉 Fertig!" \ No newline at end of file From 73dd4425962177f50b005fc44ce9ce7c144d45ca Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Mon, 23 Feb 2026 10:30:16 -0600 Subject: [PATCH 40/74] sdfsdf --- DMS/docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index 3d10200..3c94774 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -138,8 +138,8 @@ services: - ROUNDCUBEMAIL_DEFAULT_HOST=ssl://node1.email-srvr.com - ROUNDCUBEMAIL_DEFAULT_PORT=993 # Interner Traffic ohne TLS - - ROUNDCUBEMAIL_SMTP_SERVER=tls://node1.email-srvr.com - - ROUNDCUBEMAIL_SMTP_PORT=587 + - ROUNDCUBEMAIL_SMTP_SERVER=ssl://node1.email-srvr.com + - ROUNDCUBEMAIL_SMTP_PORT=465 # WICHTIG: Variablen LEER lassen, damit Roundcube keine Authentifizierung versucht! - ROUNDCUBEMAIL_SMTP_USER= From a077b38998dc1a5948706966b3b830774e59b66f Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Wed, 25 Feb 2026 16:43:12 -0600 Subject: [PATCH 41/74] outlook adoptions --- DMS/docker-data/dms/config/dovecot.cf | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 DMS/docker-data/dms/config/dovecot.cf diff --git a/DMS/docker-data/dms/config/dovecot.cf b/DMS/docker-data/dms/config/dovecot.cf new file mode 100644 index 0000000..5270727 --- /dev/null +++ b/DMS/docker-data/dms/config/dovecot.cf @@ -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 \ No newline at end of file From 8995cede7d9f7fec80992aca645ebe38b6c1f39d Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Mon, 2 Mar 2026 15:40:55 -0600 Subject: [PATCH 42/74] flags SKIP_CLIENT_DNS and SKIP_DMARC --- basic_setup/cloudflareMigrationDns.sh | 220 ++++++++++++++++++-------- 1 file changed, 152 insertions(+), 68 deletions(-) diff --git a/basic_setup/cloudflareMigrationDns.sh b/basic_setup/cloudflareMigrationDns.sh index 680a3b8..4ca7949 100755 --- a/basic_setup/cloudflareMigrationDns.sh +++ b/basic_setup/cloudflareMigrationDns.sh @@ -2,7 +2,18 @@ # cloudflareMigrationDns.sh # Setzt DNS Records für Amazon SES Migration + Cloudflare # Unterstützt: DKIM, SPF (Merge), DMARC, MX, Autodiscover -# NEU: Setzt mail/imap/smtp/pop Subdomains für domain-spezifischen Mailserver-Zugang +# 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 @@ -10,13 +21,14 @@ set -e AWS_REGION=${AWS_REGION:-"us-east-2"} DRY_RUN=${DRY_RUN:-"false"} +# Migrations-Flags (NEU) +SKIP_CLIENT_DNS=${SKIP_CLIENT_DNS:-"false"} +SKIP_DMARC=${SKIP_DMARC:-"false"} + # IP des Mailservers - PFLICHT wenn keine CNAME-Kette gewünscht -# export MAIL_SERVER_IP="1.2.3.4" MAIL_SERVER_IP=${MAIL_SERVER_IP:-""} # Ziel-Server für Mailclients. Standard: mail. -# Wenn MAIL_SERVER_IP gesetzt ist, bekommt mail. einen A-Record -# und imap/smtp/pop/webmail zeigen per CNAME auf mail. TARGET_MAIL_SERVER=${TARGET_MAIL_SERVER:-"mail.${DOMAIN_NAME}"} # --- CHECKS --- @@ -28,8 +40,8 @@ if ! command -v aws &> /dev/null; then echo "❌ Fehler: 'aws' CLI fehlt."; exit if [ -z "$MAIL_SERVER_IP" ] && [ "$TARGET_MAIL_SERVER" == "mail.$DOMAIN_NAME" ]; then echo "⚠️ WARNUNG: MAIL_SERVER_IP ist nicht gesetzt!" echo " mail.$DOMAIN_NAME braucht einen A-Record." - echo " Bitte setzen: export MAIL_SERVER_IP=" - exit 1 + echo " Setze: export MAIL_SERVER_IP=" + # Kein exit - Abschnitt 8 wird ggf. übersprungen fi echo "============================================================" @@ -38,6 +50,8 @@ 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!" +[ "$SKIP_CLIENT_DNS" = "true" ] && echo " ⏭️ SKIP: Client-Subdomains (imap/smtp/pop/webmail/SRV)" +[ "$SKIP_DMARC" = "true" ] && echo " ⏭️ SKIP: DMARC Record" echo "============================================================" # 1. ZONE ID HOLEN @@ -53,13 +67,14 @@ echo " ✅ Zone ID: $ZONE_ID" # ------------------------------------------------------------------ # FUNKTION: ensure_record +# Prüft Existenz -> Create oder Update (je nach Typ) # ------------------------------------------------------------------ ensure_record() { local type=$1 local name=$2 local content=$3 local proxied=${4:-false} - local priority=$5 + local priority=$5 # Optional für MX echo " ⚙️ Prüfe $type $name..." @@ -86,7 +101,7 @@ ensure_record() { if [ "$(echo $res | jq -r .success)" == "true" ]; then echo " ✅ Erstellt." else - echo " ❌ Fehler: $(echo $res | jq -r .errors[0].message)" + echo " ❌ Fehler beim Erstellen: $(echo $res | jq -r '.errors[0].message')" fi fi else @@ -94,8 +109,8 @@ ensure_record() { echo " 🆗 Identisch. Überspringe." else if [ "$type" == "MX" ] && [ "$name" == "$DOMAIN_NAME" ]; then - echo " ⛔ MX existiert aber anders! Gefunden: $rec_content / Erwartet: $content" - echo " Bitte Record ID $rec_id manuell löschen." + echo " ⛔ Root-MX existiert aber ist anders: $rec_content" + echo " → Wird NICHT automatisch geändert (Migrations-Schutz)" return fi if [ "$DRY_RUN" = "true" ]; then @@ -104,9 +119,9 @@ ensure_record() { res=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$rec_id" \ -H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" --data "$json_data") if [ "$(echo $res | jq -r .success)" == "true" ]; then - echo " 🔄 Aktualisiert." + echo " ✅ Aktualisiert." else - echo " ❌ Fehler: $(echo $res | jq -r .errors[0].message)" + echo " ❌ Fehler beim Updaten: $(echo $res | jq -r '.errors[0].message')" fi fi fi @@ -114,19 +129,20 @@ ensure_record() { } # ------------------------------------------------------------------ -# SCHRITT 1: MAIL FROM ermitteln +# SCHRITT 1: MAIL FROM Domain (aus SES lesen) # ------------------------------------------------------------------ echo "" echo "--- 1. MAIL FROM Domain ---" -if [ -z "$MAIL_FROM_DOMAIN" ]; then - SES_JSON=$(aws sesv2 get-email-identity --email-identity $DOMAIN_NAME --region $AWS_REGION 2>/dev/null) - MAIL_FROM_DOMAIN=$(echo "$SES_JSON" | jq -r '.MailFromAttributes.MailFromDomain') - if [ "$MAIL_FROM_DOMAIN" == "null" ] || [ -z "$MAIL_FROM_DOMAIN" ]; then - MAIL_FROM_DOMAIN="mail.$DOMAIN_NAME" - echo " ⚠️ Kein MAIL FROM in SES. Fallback: $MAIL_FROM_DOMAIN" - fi -else - echo " Nutze: $MAIL_FROM_DOMAIN" +MAIL_FROM_DOMAIN=$(aws sesv2 get-email-identity \ + --email-identity "$DOMAIN_NAME" \ + --region "$AWS_REGION" \ + --query 'MailFromAttributes.MailFromDomain' \ + --output text 2>/dev/null || echo "NONE") + +if [ "$MAIL_FROM_DOMAIN" == "NONE" ] || [ "$MAIL_FROM_DOMAIN" == "None" ] || [ -z "$MAIL_FROM_DOMAIN" ]; then + echo " ℹ️ Keine MAIL FROM Domain in SES konfiguriert." + echo " → Überspringe MAIL FROM DNS Setup." + MAIL_FROM_DOMAIN="" fi # ------------------------------------------------------------------ @@ -134,89 +150,138 @@ fi # ------------------------------------------------------------------ echo "" echo "--- 2. DKIM Records ---" -TOKENS=$(aws ses get-identity-dkim-attributes --identities $DOMAIN_NAME --region $AWS_REGION \ - --query "DkimAttributes.\"$DOMAIN_NAME\".DkimTokens" --output text) -for token in $TOKENS; do - ensure_record "CNAME" "${token}._domainkey.$DOMAIN_NAME" "${token}.dkim.amazonses.com" false -done +DKIM_TOKENS=$(aws sesv2 get-email-identity \ + --email-identity "$DOMAIN_NAME" \ + --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 +else + echo " ⚠️ Keine DKIM Tokens gefunden. SES Identity angelegt?" +fi # ------------------------------------------------------------------ -# SCHRITT 3: SES Verification +# SCHRITT 3: SES Verification TXT # ------------------------------------------------------------------ echo "" echo "--- 3. SES Verification TXT ---" -VERIF_TOKEN=$(aws ses get-identity-verification-attributes --identities $DOMAIN_NAME \ - --region $AWS_REGION --query "VerificationAttributes.\"$DOMAIN_NAME\".VerificationToken" --output text) -if [ "$VERIF_TOKEN" != "None" ] && [ -n "$VERIF_TOKEN" ]; then - ensure_record "TXT" "_amazonses.$DOMAIN_NAME" "$VERIF_TOKEN" false +VERIFICATION_TOKEN=$(aws ses get-identity-verification-attributes \ + --identities "$DOMAIN_NAME" \ + --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 # ------------------------------------------------------------------ # SCHRITT 4: MAIL FROM Subdomain (MX + SPF) # ------------------------------------------------------------------ echo "" -echo "--- 4. MAIL FROM Subdomain ($MAIL_FROM_DOMAIN) ---" -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 +echo "--- 4. MAIL FROM Subdomain (${MAIL_FROM_DOMAIN:-'nicht konfiguriert'}) ---" + +if [ -n "$MAIL_FROM_DOMAIN" ]; then + # 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 +else + echo " ℹ️ Übersprungen (keine MAIL FROM Domain konfiguriert)." +fi # ------------------------------------------------------------------ -# SCHRITT 5: Root Domain SPF +# SCHRITT 5: Root Domain SPF (Merge mit altem Provider) # ------------------------------------------------------------------ echo "" echo "--- 5. Root Domain SPF ---" -if [ -n "$OLD_PROVIDER_SPF" ]; then - FINAL_SPF="v=spf1 include:amazonses.com $OLD_PROVIDER_SPF ~all" + +# Aktuellen SPF-Record lesen +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 | startswith("v=spf1"))][0].content // ""') + +if [ -n "$CURRENT_SPF" ]; then + # 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 + # amazonses.com einfügen direkt nach v=spf1 + NEW_SPF=$(echo "$CURRENT_SPF" | sed 's/v=spf1 /v=spf1 include:amazonses.com /') + # ?all → ~all upgraden + NEW_SPF=$(echo "$NEW_SPF" | sed 's/?all/~all/') + # Anführungszeichen entfernen falls vorhanden + NEW_SPF=$(echo "$NEW_SPF" | tr -d '"') + ensure_record "TXT" "$DOMAIN_NAME" "$NEW_SPF" false + fi else - FINAL_SPF="v=spf1 include:amazonses.com ~all" + echo " ℹ️ Kein SPF Record vorhanden. Erstelle neuen." + ensure_record "TXT" "$DOMAIN_NAME" "v=spf1 include:amazonses.com ~all" false fi -ensure_record "TXT" "$DOMAIN_NAME" "$FINAL_SPF" false # ------------------------------------------------------------------ -# SCHRITT 6: Root Domain MX +# SCHRITT 6: Root Domain MX (nur Info, wird nicht geändert) # ------------------------------------------------------------------ -# WICHTIG: Der MX Record zeigt auf Amazon SES (inbound-smtp.*.amazonaws.com), -# da eingehende Mails über SES → S3 → SQS → Worker → DMS laufen. -# Der DMS ist NICHT direkt aus dem Internet erreichbar. -# Dieser Record wird daher NICHT angefasst. echo "" echo "--- 6. Root Domain MX (nur Info, wird nicht geändert) ---" -EXISTING_MX=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=MX&name=$DOMAIN_NAME" \ - -H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" | jq -r '.result[0].content') -if [ "$EXISTING_MX" == "null" ] || [ -z "$EXISTING_MX" ]; then - echo " ⚠️ Kein MX Record gefunden! Bitte manuell in SES/Cloudflare setzen:" - echo " inbound-smtp.$AWS_REGION.amazonaws.com (Prio 10)" -else - echo " ℹ️ MX vorhanden: $EXISTING_MX (wird nicht geändert)" -fi +CURRENT_MX=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=MX&name=$DOMAIN_NAME" \ + -H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" \ + | jq -r '.result[0].content // "keiner"') +echo " ℹ️ MX vorhanden: $CURRENT_MX (wird nicht geändert)" # ------------------------------------------------------------------ # SCHRITT 7: DMARC # ------------------------------------------------------------------ echo "" echo "--- 7. DMARC ---" -ensure_record "TXT" "_dmarc.$DOMAIN_NAME" "v=DMARC1; p=none; rua=mailto:postmaster@$DOMAIN_NAME" false +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 +fi # ------------------------------------------------------------------ -# SCHRITT 8 (NEU): Mailclient Subdomains +# SCHRITT 8: Mailclient Subdomains (A + CNAME) # ------------------------------------------------------------------ echo "" echo "--- 8. Mailclient Subdomains (A + CNAME) ---" - -if [ -n "$MAIL_SERVER_IP" ]; then - # A-Record für mail. direkt auf Server-IP - ensure_record "A" "mail.$DOMAIN_NAME" "$MAIL_SERVER_IP" false +if [ "$SKIP_CLIENT_DNS" = "true" ]; then + 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 - # 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 + if [ -n "$MAIL_SERVER_IP" ]; then + # A-Record für mail. 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 -fi -# imap, smtp, pop, webmail → CNAME auf mail. -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 + # imap, smtp, pop, webmail → CNAME auf mail. + 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 # ------------------------------------------------------------------ # SCHRITT 9: Autodiscover / Autoconfig @@ -226,9 +291,28 @@ echo "--- 9. Autodiscover / Autoconfig ---" ensure_record "CNAME" "autodiscover.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false ensure_record "CNAME" "autoconfig.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false +# ------------------------------------------------------------------ +# SCHRITT 10: SRV Records +# ------------------------------------------------------------------ +echo "" +echo "--- 10. SRV Records ---" +if [ "$SKIP_CLIENT_DNS" = "true" ]; then + echo " ⏭️ Übersprungen (SKIP_CLIENT_DNS=true)" +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 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)" From 7173da31d4a2b4eeef395bef37a702e35f31191c Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Mon, 2 Mar 2026 15:43:54 -0600 Subject: [PATCH 43/74] fix --- basic_setup/cloudflareMigrationDns.sh | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/basic_setup/cloudflareMigrationDns.sh b/basic_setup/cloudflareMigrationDns.sh index 4ca7949..6fe0381 100755 --- a/basic_setup/cloudflareMigrationDns.sh +++ b/basic_setup/cloudflareMigrationDns.sh @@ -212,11 +212,17 @@ echo "" echo "--- 5. Root Domain SPF ---" # Aktuellen SPF-Record lesen +# Cloudflare liefert TXT-Content manchmal mit Anführungszeichen, +# 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 | startswith("v=spf1"))][0].content // ""') + | 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" @@ -225,8 +231,7 @@ if [ -n "$CURRENT_SPF" ]; then NEW_SPF=$(echo "$CURRENT_SPF" | sed 's/v=spf1 /v=spf1 include:amazonses.com /') # ?all → ~all upgraden NEW_SPF=$(echo "$NEW_SPF" | sed 's/?all/~all/') - # Anführungszeichen entfernen falls vorhanden - NEW_SPF=$(echo "$NEW_SPF" | tr -d '"') + echo " 📝 Neuer SPF: $NEW_SPF" ensure_record "TXT" "$DOMAIN_NAME" "$NEW_SPF" false fi else From 80596ab347dba05f75d45139313434bed0358742 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Mon, 2 Mar 2026 16:48:55 -0600 Subject: [PATCH 44/74] autodiscover --- caddy/email_autodiscover | 57 +++++++++++++++++++++++-------------- caddy/update-caddy-certs.sh | 54 ++++++++++++++++++++++++++++++----- 2 files changed, 83 insertions(+), 28 deletions(-) diff --git a/caddy/email_autodiscover b/caddy/email_autodiscover index 5f9b24f..78ccedd 100644 --- a/caddy/email_autodiscover +++ b/caddy/email_autodiscover @@ -1,8 +1,21 @@ +# email_autodiscover - Dynamisches Autodiscover/Autoconfig Snippet +# Importiert im Caddyfile via: import email_autodiscover +# +# Funktioniert mit JEDER Domain automatisch, solange der Caddy-Block +# auf autodiscover. oder autoconfig. 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. Autodiscover für Outlook + # 1. Outlook Autodiscover (XML) route /autodiscover/autodiscover.xml { header Content-Type "application/xml" - # Wir nutzen {header.X-Anchormailbox} um die Email dynamisch einzufügen respond ` @@ -11,7 +24,7 @@ settings IMAP - mail.email-srvr.com + imap.{labels.1}.{labels.0} 993 on {header.X-Anchormailbox} @@ -21,17 +34,17 @@ POP3 - mail.email-srvr.com + pop.{labels.1}.{labels.0} 995 on {header.X-Anchormailbox} off on on - + SMTP - mail.email-srvr.com + smtp.{labels.1}.{labels.0} 465 on {header.X-Anchormailbox} @@ -44,31 +57,39 @@ ` 200 } - # 2. JSON Autodiscover (Modern Outlook) - bleibt gleich + # 2. Modern Outlook (JSON) - Redirect zum XML Endpoint route /autodiscover/autodiscover.json { header Content-Type "application/json" respond `{ "Protocol": "AutodiscoverV1", - "Url": "https://autodiscover.bayarea-cc.com/autodiscover/autodiscover.xml" + "Url": "https://autodiscover.{labels.1}.{labels.0}/autodiscover/autodiscover.xml" }` 200 } - # 3. Thunderbird Autoconfig - bleibt gleich (dort funktioniert %EMAILADDRESS% ja nativ) + # 3. Thunderbird Autoconfig route /mail/config-v1.1.xml { header Content-Type "application/xml" respond ` - - Rackspace Email + + {labels.1}.{labels.0} Mail + {labels.1}.{labels.0} - mail.email-srvr.com + imap.{labels.1}.{labels.0} 993 SSL password-cleartext %EMAILADDRESS% + + pop.{labels.1}.{labels.0} + 995 + SSL + password-cleartext + %EMAILADDRESS% + - mail.email-srvr.com + smtp.{labels.1}.{labels.0} 465 SSL password-cleartext @@ -78,20 +99,14 @@ ` 200 } - # NEU: Apple MobileConfig Route - # Aufrufbar über: /apple?email=kunde@domain.de + # 4. Apple MobileConfig route /apple { - # KORREKTUR: Wir müssen Caddy sagen, dass er diesen MIME-Type bearbeiten soll! templates { mime "application/x-apple-aspen-config" } - - # Den richtigen MIME-Type setzen header Content-Type "application/x-apple-aspen-config; charset=utf-8" - - # Pfad zur Datei im Container root * /etc/caddy rewrite * /email.mobileconfig.tpl file_server } -} \ No newline at end of file +} diff --git a/caddy/update-caddy-certs.sh b/caddy/update-caddy-certs.sh index ca0d076..8090ef0 100755 --- a/caddy/update-caddy-certs.sh +++ b/caddy/update-caddy-certs.sh @@ -5,6 +5,11 @@ # 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: @@ -21,7 +26,6 @@ OUTPUT_FILE="$SCRIPT_DIR/mail_certs" DRY_RUN=${DRY_RUN:-"false"} # Node-Hostname des Mailservers (für Default-Cert Block) -# Wird immer mit eingetragen, auch wenn keine DMS-Accounts existieren. NODE_HOSTNAME=${NODE_HOSTNAME:-"node1.email-srvr.com"} echo "============================================================" @@ -55,7 +59,7 @@ echo "📝 Generiere Caddy-Konfiguration..." OUTPUT="" OUTPUT="${OUTPUT}# mail_certs - Automatisch generiert von update-caddy-certs.sh\n" -OUTPUT="${OUTPUT}# Wildcard-Zertifikate für DMS-Domains + Node-Hostname.\n" +OUTPUT="${OUTPUT}# 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" @@ -70,12 +74,17 @@ OUTPUT="${OUTPUT} }\n" OUTPUT="${OUTPUT} respond \"OK\" 200\n" OUTPUT="${OUTPUT}}\n\n" -# Wildcard-Blocks + webmail Block pro Kundendomain +# Wildcard-Blocks + Webmail + Autodiscover pro Kundendomain for domain in $DOMAINS; do - echo " → Wildcard Block: *.${domain}" - echo " → Webmail Block: webmail.${domain}" + 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" @@ -84,8 +93,7 @@ for domain in $DOMAINS; do OUTPUT="${OUTPUT} respond \"OK\" 200\n" OUTPUT="${OUTPUT}}\n\n" - # Webmail Block (Roundcube) - muss VOR dem Wildcard-Block matchen - # Caddy wertet Blöcke in Reihenfolge aus, spezifischere Hosts gewinnen + # Webmail Block (Roundcube) OUTPUT="${OUTPUT}# Roundcube Webmail für $domain\n" OUTPUT="${OUTPUT}webmail.${domain} {\n" OUTPUT="${OUTPUT} reverse_proxy roundcube:80\n" @@ -95,6 +103,13 @@ for domain in $DOMAINS; do 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 --- @@ -126,6 +141,26 @@ if [ -f "$CADDYFILE" ]; then echo " import email_autodiscover" echo " ..." fi + + # Autodiscover-Snippet prüfen + if grep -q "import email_autodiscover" "$CADDYFILE"; then + echo " ✅ 'import email_autodiscover' bereits im Caddyfile vorhanden." + else + echo "" + echo "⚠️ AKTION: 'import email_autodiscover' fehlt noch im Caddyfile!" + echo " Die Datei email_autodiscover enthält das (email_settings) Snippet." + 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 "" @@ -144,4 +179,9 @@ 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 "============================================================" \ No newline at end of file From d91152c035f4a2bb765e23fcc31d2eefc701a177 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Mon, 2 Mar 2026 16:49:31 -0600 Subject: [PATCH 45/74] autodiscover entfernt --- caddy/Caddyfile | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/caddy/Caddyfile b/caddy/Caddyfile index 19dc860..0a47469 100644 --- a/caddy/Caddyfile +++ b/caddy/Caddyfile @@ -6,17 +6,5 @@ } import mail_certs import email_autodiscover -# --------------------------------------------------------- -# Block A: Die dedizierten Autodiscover Domains -# --------------------------------------------------------- -autodiscover.bayarea-cc.com, autodiscover.bizmatch.net, -autodiscover.ruehrgedoens.de, autoconfig.ruehrgedoens.de, -autoconfig.bayarea-cc.com, autoconfig.bizmatch.net { - - # Hier rufen wir das Snippet auf - import email_settings - - # Fallback für Aufrufe auf Root dieser Subdomains - respond "Autodiscover Service Online" 200 -} + From 282298c361baffbf1f581ecfba9cba7d1243c67c Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Mon, 2 Mar 2026 16:55:02 -0600 Subject: [PATCH 46/74] change --- caddy/Caddyfile | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/caddy/Caddyfile b/caddy/Caddyfile index 0a47469..05f8541 100644 --- a/caddy/Caddyfile +++ b/caddy/Caddyfile @@ -4,7 +4,19 @@ acme_ca https://acme-v02.api.letsencrypt.org/directory debug } -import mail_certs import email_autodiscover - +import mail_certs +# --------------------------------------------------------- +# Block A: Die dedizierten Autodiscover Domains +# --------------------------------------------------------- +autodiscover.bayarea-cc.com, autodiscover.bizmatch.net, +autodiscover.ruehrgedoens.de, autoconfig.ruehrgedoens.de, +autoconfig.bayarea-cc.com, autoconfig.bizmatch.net { + + # Hier rufen wir das Snippet auf + import email_settings + + # Fallback für Aufrufe auf Root dieser Subdomains + respond "Autodiscover Service Online" 200 +} From a090e940f13993251eb9a08a2478f19aeffe90d3 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Mon, 2 Mar 2026 16:55:47 -0600 Subject: [PATCH 47/74] sdfsdf --- caddy/Caddyfile | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/caddy/Caddyfile b/caddy/Caddyfile index 05f8541..94cef70 100644 --- a/caddy/Caddyfile +++ b/caddy/Caddyfile @@ -5,18 +5,4 @@ debug } import email_autodiscover -import mail_certs -# --------------------------------------------------------- -# Block A: Die dedizierten Autodiscover Domains -# --------------------------------------------------------- -autodiscover.bayarea-cc.com, autodiscover.bizmatch.net, -autodiscover.ruehrgedoens.de, autoconfig.ruehrgedoens.de, -autoconfig.bayarea-cc.com, autoconfig.bizmatch.net { - - # Hier rufen wir das Snippet auf - import email_settings - - # Fallback für Aufrufe auf Root dieser Subdomains - respond "Autodiscover Service Online" 200 -} - +import mail_certs \ No newline at end of file From c56cae16d6ba2ffa5f633f23357b79587d610026 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Mon, 2 Mar 2026 16:58:26 -0600 Subject: [PATCH 48/74] sdfsdf --- caddy/Caddyfile | 1 - caddy/update-caddy-certs.sh | 115 +++++++++++++++++++++++++++++++++--- 2 files changed, 108 insertions(+), 8 deletions(-) diff --git a/caddy/Caddyfile b/caddy/Caddyfile index 94cef70..6a20a4c 100644 --- a/caddy/Caddyfile +++ b/caddy/Caddyfile @@ -4,5 +4,4 @@ acme_ca https://acme-v02.api.letsencrypt.org/directory debug } -import email_autodiscover import mail_certs \ No newline at end of file diff --git a/caddy/update-caddy-certs.sh b/caddy/update-caddy-certs.sh index 8090ef0..34a6157 100755 --- a/caddy/update-caddy-certs.sh +++ b/caddy/update-caddy-certs.sh @@ -64,6 +64,110 @@ 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 \`\n" +OUTPUT="${OUTPUT}\n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT} email\n" +OUTPUT="${OUTPUT} settings\n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT} IMAP\n" +OUTPUT="${OUTPUT} imap.{labels.1}.{labels.0}\n" +OUTPUT="${OUTPUT} 993\n" +OUTPUT="${OUTPUT} on\n" +OUTPUT="${OUTPUT} {header.X-Anchormailbox}\n" +OUTPUT="${OUTPUT} off\n" +OUTPUT="${OUTPUT} on\n" +OUTPUT="${OUTPUT} on\n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT} POP3\n" +OUTPUT="${OUTPUT} pop.{labels.1}.{labels.0}\n" +OUTPUT="${OUTPUT} 995\n" +OUTPUT="${OUTPUT} on\n" +OUTPUT="${OUTPUT} {header.X-Anchormailbox}\n" +OUTPUT="${OUTPUT} off\n" +OUTPUT="${OUTPUT} on\n" +OUTPUT="${OUTPUT} on\n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT} SMTP\n" +OUTPUT="${OUTPUT} smtp.{labels.1}.{labels.0}\n" +OUTPUT="${OUTPUT} 465\n" +OUTPUT="${OUTPUT} on\n" +OUTPUT="${OUTPUT} {header.X-Anchormailbox}\n" +OUTPUT="${OUTPUT} off\n" +OUTPUT="${OUTPUT} on\n" +OUTPUT="${OUTPUT} on\n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT}\` 200\n" +OUTPUT="${OUTPUT} }\n" +OUTPUT="${OUTPUT}\n" +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 \`\n" +OUTPUT="${OUTPUT}\n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT} {labels.1}.{labels.0} Mail\n" +OUTPUT="${OUTPUT} {labels.1}.{labels.0}\n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT} imap.{labels.1}.{labels.0}\n" +OUTPUT="${OUTPUT} 993\n" +OUTPUT="${OUTPUT} SSL\n" +OUTPUT="${OUTPUT} password-cleartext\n" +OUTPUT="${OUTPUT} %%EMAILADDRESS%%\n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT} pop.{labels.1}.{labels.0}\n" +OUTPUT="${OUTPUT} 995\n" +OUTPUT="${OUTPUT} SSL\n" +OUTPUT="${OUTPUT} password-cleartext\n" +OUTPUT="${OUTPUT} %%EMAILADDRESS%%\n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT} smtp.{labels.1}.{labels.0}\n" +OUTPUT="${OUTPUT} 465\n" +OUTPUT="${OUTPUT} SSL\n" +OUTPUT="${OUTPUT} password-cleartext\n" +OUTPUT="${OUTPUT} %%EMAILADDRESS%%\n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT}\` 200\n" +OUTPUT="${OUTPUT} }\n" +OUTPUT="${OUTPUT}\n" +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" @@ -138,17 +242,14 @@ if [ -f "$CADDYFILE" ]; then echo " ..." echo " }" echo " import mail_certs ← hier einfügen" - echo " import email_autodiscover" - echo " ..." fi - # Autodiscover-Snippet prüfen + # Prüfe ob alte email_autodiscover Referenz entfernt werden kann if grep -q "import email_autodiscover" "$CADDYFILE"; then - echo " ✅ 'import email_autodiscover' bereits im Caddyfile vorhanden." - else echo "" - echo "⚠️ AKTION: 'import email_autodiscover' fehlt noch im Caddyfile!" - echo " Die Datei email_autodiscover enthält das (email_settings) Snippet." + 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 From 22d937ddfd4710c8e85a10668856b5047630a305 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Mon, 2 Mar 2026 18:07:16 -0600 Subject: [PATCH 49/74] imapsync --- DMS/run_sync.sh | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100755 DMS/run_sync.sh diff --git a/DMS/run_sync.sh b/DMS/run_sync.sh new file mode 100755 index 0000000..4c12aee --- /dev/null +++ b/DMS/run_sync.sh @@ -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" \ No newline at end of file From f6601501c0468c9700ed4af0fd72a57f44632c32 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Thu, 5 Mar 2026 17:01:48 +0000 Subject: [PATCH 50/74] disabled fail2ban --- DMS/docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index 3c94774..a1dc234 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -78,7 +78,7 @@ services: - ENABLE_CLAMAV=0 # Sicherheit - - ENABLE_FAIL2BAN=1 + - ENABLE_FAIL2BAN=0 - ENABLE_UNBOUND=1 # Sonstige @@ -174,4 +174,4 @@ services: networks: mail_network: - external: true \ No newline at end of file + external: true From 726df19a76787e1910acac53c024b5656d79eaf2 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Thu, 5 Mar 2026 11:03:32 -0600 Subject: [PATCH 51/74] ignoreip for fail2ban --- DMS/docker-data/dms/config/fail2ban-jail.cf | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 DMS/docker-data/dms/config/fail2ban-jail.cf diff --git a/DMS/docker-data/dms/config/fail2ban-jail.cf b/DMS/docker-data/dms/config/fail2ban-jail.cf new file mode 100644 index 0000000..b1490d4 --- /dev/null +++ b/DMS/docker-data/dms/config/fail2ban-jail.cf @@ -0,0 +1,3 @@ +[DEFAULT] +# Whitelist: Localhost, private Docker-Netze und die Office-IP in Texas +ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 69.223.70.143 \ No newline at end of file From f1b2c33996777ce27b09aecd5b500ecefb64ddc6 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Thu, 5 Mar 2026 14:28:39 -0600 Subject: [PATCH 52/74] ENABLE_FAIL2BAN=1 --- DMS/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index a1dc234..279c94f 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -78,7 +78,7 @@ services: - ENABLE_CLAMAV=0 # Sicherheit - - ENABLE_FAIL2BAN=0 + - ENABLE_FAIL2BAN=1 - ENABLE_UNBOUND=1 # Sonstige From 908bb76c3a7dcbfbbdd62ddc78e1beced009cf81 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sat, 7 Mar 2026 12:05:06 -0600 Subject: [PATCH 53/74] ip address change --- DMS/docker-data/dms/config/fail2ban-jail.cf | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/DMS/docker-data/dms/config/fail2ban-jail.cf b/DMS/docker-data/dms/config/fail2ban-jail.cf index c6d4592..eb20763 100644 --- a/DMS/docker-data/dms/config/fail2ban-jail.cf +++ b/DMS/docker-data/dms/config/fail2ban-jail.cf @@ -1,7 +1,6 @@ [DEFAULT] -# Whitelist: Localhost, private Docker-Netze und die Office-IP in Texas -ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 69.223.70.143 - +# 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 [dovecot] # Erhöht die Anzahl der erlaubten Fehlversuche auf 20 From c826d4c2991318077ea1b6789a7e8162ac15e084 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sat, 7 Mar 2026 14:59:41 -0600 Subject: [PATCH 54/74] move and imports changed --- .gitignore | 3 +- email-worker-nodejs/package-lock.json | 3190 +++++++++++++++++ email-worker-nodejs/{ => src/aws}/dynamodb.ts | 0 email-worker-nodejs/{ => src/aws}/s3.ts | 0 email-worker-nodejs/{ => src/aws}/ses.ts | 0 email-worker-nodejs/{ => src/aws}/sqs.ts | 0 email-worker-nodejs/{ => src}/config.ts | 0 .../{ => src/email}/blocklist.ts | 0 .../{ => src/email}/bounce-handler.ts | 0 email-worker-nodejs/{ => src/email}/parser.ts | 0 .../{ => src/email}/rules-processor.ts | 4 +- email-worker-nodejs/{ => src}/health.ts | 0 email-worker-nodejs/{ => src}/logger.ts | 0 email-worker-nodejs/{ => src}/main.ts | 2 +- email-worker-nodejs/{ => src}/metrics.ts | 0 .../{ => src/smtp}/delivery.ts | 3 +- .../{ => src/worker}/domain-poller.ts | 2 +- .../{ => src/worker}/message-processor.ts | 13 +- .../{ => src/worker}/unified-worker.ts | 8 +- 19 files changed, 3209 insertions(+), 16 deletions(-) create mode 100644 email-worker-nodejs/package-lock.json rename email-worker-nodejs/{ => src/aws}/dynamodb.ts (100%) rename email-worker-nodejs/{ => src/aws}/s3.ts (100%) rename email-worker-nodejs/{ => src/aws}/ses.ts (100%) rename email-worker-nodejs/{ => src/aws}/sqs.ts (100%) rename email-worker-nodejs/{ => src}/config.ts (100%) rename email-worker-nodejs/{ => src/email}/blocklist.ts (100%) rename email-worker-nodejs/{ => src/email}/bounce-handler.ts (100%) rename email-worker-nodejs/{ => src/email}/parser.ts (100%) rename email-worker-nodejs/{ => src/email}/rules-processor.ts (99%) rename email-worker-nodejs/{ => src}/health.ts (100%) rename email-worker-nodejs/{ => src}/logger.ts (100%) rename email-worker-nodejs/{ => src}/main.ts (98%) rename email-worker-nodejs/{ => src}/metrics.ts (100%) rename email-worker-nodejs/{ => src/smtp}/delivery.ts (99%) rename email-worker-nodejs/{ => src/worker}/domain-poller.ts (98%) rename email-worker-nodejs/{ => src/worker}/message-processor.ts (97%) rename email-worker-nodejs/{ => src/worker}/unified-worker.ts (92%) diff --git a/.gitignore b/.gitignore index 2eea525..97aca2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.env \ No newline at end of file +.env +node_modules \ No newline at end of file diff --git a/email-worker-nodejs/package-lock.json b/email-worker-nodejs/package-lock.json new file mode 100644 index 0000000..8af610f --- /dev/null +++ b/email-worker-nodejs/package-lock.json @@ -0,0 +1,3190 @@ +{ + "name": "unified-email-worker", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "unified-email-worker", + "version": "2.0.0", + "dependencies": { + "@aws-sdk/client-dynamodb": "^3.700.0", + "@aws-sdk/client-s3": "^3.700.0", + "@aws-sdk/client-ses": "^3.700.0", + "@aws-sdk/client-sqs": "^3.700.0", + "@aws-sdk/lib-dynamodb": "^3.700.0", + "mailparser": "^3.7.1", + "nodemailer": "^6.9.16", + "picomatch": "^4.0.2", + "pino": "^9.5.0", + "pino-pretty": "^13.0.0", + "prom-client": "^15.1.3" + }, + "devDependencies": { + "@types/mailparser": "^3.4.5", + "@types/node": "^22.10.0", + "@types/nodemailer": "^6.4.17", + "@types/picomatch": "^3.0.1", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb": { + "version": "3.1004.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.1004.0.tgz", + "integrity": "sha512-NYQPxtfc0hWNUlbZMkgjAyudPzgK6WA5BRh9+/zCQYrBvgtk347g64gnxqyyF+W3QWNhFEp7Eo3heHiNN++9Wg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/credential-provider-node": "^3.972.18", + "@aws-sdk/dynamodb-codec": "^3.972.19", + "@aws-sdk/middleware-endpoint-discovery": "^3.972.7", + "@aws-sdk/middleware-host-header": "^3.972.7", + "@aws-sdk/middleware-logger": "^3.972.7", + "@aws-sdk/middleware-recursion-detection": "^3.972.7", + "@aws-sdk/middleware-user-agent": "^3.972.19", + "@aws-sdk/region-config-resolver": "^3.972.7", + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/util-endpoints": "^3.996.4", + "@aws-sdk/util-user-agent-browser": "^3.972.7", + "@aws-sdk/util-user-agent-node": "^3.973.4", + "@smithy/config-resolver": "^4.4.10", + "@smithy/core": "^3.23.8", + "@smithy/fetch-http-handler": "^5.3.13", + "@smithy/hash-node": "^4.2.11", + "@smithy/invalid-dependency": "^4.2.11", + "@smithy/middleware-content-length": "^4.2.11", + "@smithy/middleware-endpoint": "^4.4.22", + "@smithy/middleware-retry": "^4.4.39", + "@smithy/middleware-serde": "^4.2.12", + "@smithy/middleware-stack": "^4.2.11", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/node-http-handler": "^4.4.14", + "@smithy/protocol-http": "^5.3.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.38", + "@smithy/util-defaults-mode-node": "^4.2.41", + "@smithy/util-endpoints": "^3.3.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-retry": "^4.2.11", + "@smithy/util-utf8": "^4.2.2", + "@smithy/util-waiter": "^4.2.11", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.1004.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1004.0.tgz", + "integrity": "sha512-m0zNfpsona9jQdX1cHtHArOiuvSGZPsgp/KRZS2YjJhKah96G2UN3UNGZQ6aVjXIQjCY6UanCJo0uW9Xf2U41w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/credential-provider-node": "^3.972.18", + "@aws-sdk/middleware-bucket-endpoint": "^3.972.7", + "@aws-sdk/middleware-expect-continue": "^3.972.7", + "@aws-sdk/middleware-flexible-checksums": "^3.973.4", + "@aws-sdk/middleware-host-header": "^3.972.7", + "@aws-sdk/middleware-location-constraint": "^3.972.7", + "@aws-sdk/middleware-logger": "^3.972.7", + "@aws-sdk/middleware-recursion-detection": "^3.972.7", + "@aws-sdk/middleware-sdk-s3": "^3.972.18", + "@aws-sdk/middleware-ssec": "^3.972.7", + "@aws-sdk/middleware-user-agent": "^3.972.19", + "@aws-sdk/region-config-resolver": "^3.972.7", + "@aws-sdk/signature-v4-multi-region": "^3.996.6", + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/util-endpoints": "^3.996.4", + "@aws-sdk/util-user-agent-browser": "^3.972.7", + "@aws-sdk/util-user-agent-node": "^3.973.4", + "@smithy/config-resolver": "^4.4.10", + "@smithy/core": "^3.23.8", + "@smithy/eventstream-serde-browser": "^4.2.11", + "@smithy/eventstream-serde-config-resolver": "^4.3.11", + "@smithy/eventstream-serde-node": "^4.2.11", + "@smithy/fetch-http-handler": "^5.3.13", + "@smithy/hash-blob-browser": "^4.2.12", + "@smithy/hash-node": "^4.2.11", + "@smithy/hash-stream-node": "^4.2.11", + "@smithy/invalid-dependency": "^4.2.11", + "@smithy/md5-js": "^4.2.11", + "@smithy/middleware-content-length": "^4.2.11", + "@smithy/middleware-endpoint": "^4.4.22", + "@smithy/middleware-retry": "^4.4.39", + "@smithy/middleware-serde": "^4.2.12", + "@smithy/middleware-stack": "^4.2.11", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/node-http-handler": "^4.4.14", + "@smithy/protocol-http": "^5.3.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.38", + "@smithy/util-defaults-mode-node": "^4.2.41", + "@smithy/util-endpoints": "^3.3.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-retry": "^4.2.11", + "@smithy/util-stream": "^4.5.17", + "@smithy/util-utf8": "^4.2.2", + "@smithy/util-waiter": "^4.2.11", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-ses": { + "version": "3.1004.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.1004.0.tgz", + "integrity": "sha512-uzqNkrc9SgvV7mJufzFV1yFaJcL/lYBiPk/QPO3wEvDwddzpLYq87SaxvQHqqwvW11GueIKLnZkR2zxlaiWncQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/credential-provider-node": "^3.972.18", + "@aws-sdk/middleware-host-header": "^3.972.7", + "@aws-sdk/middleware-logger": "^3.972.7", + "@aws-sdk/middleware-recursion-detection": "^3.972.7", + "@aws-sdk/middleware-user-agent": "^3.972.19", + "@aws-sdk/region-config-resolver": "^3.972.7", + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/util-endpoints": "^3.996.4", + "@aws-sdk/util-user-agent-browser": "^3.972.7", + "@aws-sdk/util-user-agent-node": "^3.973.4", + "@smithy/config-resolver": "^4.4.10", + "@smithy/core": "^3.23.8", + "@smithy/fetch-http-handler": "^5.3.13", + "@smithy/hash-node": "^4.2.11", + "@smithy/invalid-dependency": "^4.2.11", + "@smithy/middleware-content-length": "^4.2.11", + "@smithy/middleware-endpoint": "^4.4.22", + "@smithy/middleware-retry": "^4.4.39", + "@smithy/middleware-serde": "^4.2.12", + "@smithy/middleware-stack": "^4.2.11", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/node-http-handler": "^4.4.14", + "@smithy/protocol-http": "^5.3.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.38", + "@smithy/util-defaults-mode-node": "^4.2.41", + "@smithy/util-endpoints": "^3.3.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-retry": "^4.2.11", + "@smithy/util-utf8": "^4.2.2", + "@smithy/util-waiter": "^4.2.11", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-sqs": { + "version": "3.1004.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.1004.0.tgz", + "integrity": "sha512-aCREPa+SyOE6pD2JuD32E6HJAX9ik+qyXtyXPsTzPL5hDvCk85ccZUwSFpk1ErxubB4v832IDukvxfXOclQtzA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/credential-provider-node": "^3.972.18", + "@aws-sdk/middleware-host-header": "^3.972.7", + "@aws-sdk/middleware-logger": "^3.972.7", + "@aws-sdk/middleware-recursion-detection": "^3.972.7", + "@aws-sdk/middleware-sdk-sqs": "^3.972.13", + "@aws-sdk/middleware-user-agent": "^3.972.19", + "@aws-sdk/region-config-resolver": "^3.972.7", + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/util-endpoints": "^3.996.4", + "@aws-sdk/util-user-agent-browser": "^3.972.7", + "@aws-sdk/util-user-agent-node": "^3.973.4", + "@smithy/config-resolver": "^4.4.10", + "@smithy/core": "^3.23.8", + "@smithy/fetch-http-handler": "^5.3.13", + "@smithy/hash-node": "^4.2.11", + "@smithy/invalid-dependency": "^4.2.11", + "@smithy/md5-js": "^4.2.11", + "@smithy/middleware-content-length": "^4.2.11", + "@smithy/middleware-endpoint": "^4.4.22", + "@smithy/middleware-retry": "^4.4.39", + "@smithy/middleware-serde": "^4.2.12", + "@smithy/middleware-stack": "^4.2.11", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/node-http-handler": "^4.4.14", + "@smithy/protocol-http": "^5.3.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.38", + "@smithy/util-defaults-mode-node": "^4.2.41", + "@smithy/util-endpoints": "^3.3.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-retry": "^4.2.11", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.973.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.18.tgz", + "integrity": "sha512-GUIlegfcK2LO1J2Y98sCJy63rQSiLiDOgVw7HiHPRqfI2vb3XozTVqemwO0VSGXp54ngCnAQz0Lf0YPCBINNxA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/xml-builder": "^3.972.10", + "@smithy/core": "^3.23.8", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/property-provider": "^4.2.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/signature-v4": "^5.3.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/crc64-nvme": { + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.4.tgz", + "integrity": "sha512-HKZIZLbRyvzo/bXZU7Zmk6XqU+1C9DjI56xd02vwuDIxedxBEqP17t9ExhbP9QFeNq/a3l9GOcyirFXxmbDhmw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.16.tgz", + "integrity": "sha512-HrdtnadvTGAQUr18sPzGlE5El3ICphnH6SU7UQOMOWFgRKbTRNN8msTxM4emzguUso9CzaHU2xy5ctSrmK5YNA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/types": "^3.973.5", + "@smithy/property-provider": "^4.2.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.18.tgz", + "integrity": "sha512-NyB6smuZAixND5jZumkpkunQ0voc4Mwgkd+SZ6cvAzIB7gK8HV8Zd4rS8Kn5MmoGgusyNfVGG+RLoYc4yFiw+A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/types": "^3.973.5", + "@smithy/fetch-http-handler": "^5.3.13", + "@smithy/node-http-handler": "^4.4.14", + "@smithy/property-provider": "^4.2.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/util-stream": "^4.5.17", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.17.tgz", + "integrity": "sha512-dFqh7nfX43B8dO1aPQHOcjC0SnCJ83H3F+1LoCh3X1P7E7N09I+0/taID0asU6GCddfDExqnEvQtDdkuMe5tKQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/credential-provider-env": "^3.972.16", + "@aws-sdk/credential-provider-http": "^3.972.18", + "@aws-sdk/credential-provider-login": "^3.972.17", + "@aws-sdk/credential-provider-process": "^3.972.16", + "@aws-sdk/credential-provider-sso": "^3.972.17", + "@aws-sdk/credential-provider-web-identity": "^3.972.17", + "@aws-sdk/nested-clients": "^3.996.7", + "@aws-sdk/types": "^3.973.5", + "@smithy/credential-provider-imds": "^4.2.11", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.17.tgz", + "integrity": "sha512-gf2E5b7LpKb+JX2oQsRIDxdRZjBFZt2olCGlWCdb3vBERbXIPgm2t1R5mEnwd4j0UEO/Tbg5zN2KJbHXttJqwA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/nested-clients": "^3.996.7", + "@aws-sdk/types": "^3.973.5", + "@smithy/property-provider": "^4.2.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.18.tgz", + "integrity": "sha512-ZDJa2gd1xiPg/nBDGhUlat02O8obaDEnICBAVS8qieZ0+nDfaB0Z3ec6gjZj27OqFTjnB/Q5a0GwQwb7rMVViw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.16", + "@aws-sdk/credential-provider-http": "^3.972.18", + "@aws-sdk/credential-provider-ini": "^3.972.17", + "@aws-sdk/credential-provider-process": "^3.972.16", + "@aws-sdk/credential-provider-sso": "^3.972.17", + "@aws-sdk/credential-provider-web-identity": "^3.972.17", + "@aws-sdk/types": "^3.973.5", + "@smithy/credential-provider-imds": "^4.2.11", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.16.tgz", + "integrity": "sha512-n89ibATwnLEg0ZdZmUds5bq8AfBAdoYEDpqP3uzPLaRuGelsKlIvCYSNNvfgGLi8NaHPNNhs1HjJZYbqkW9b+g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/types": "^3.973.5", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.17.tgz", + "integrity": "sha512-wGtte+48xnhnhHMl/MsxzacBPs5A+7JJedjiP452IkHY7vsbYKcvQBqFye8LwdTJVeHtBHv+JFeTscnwepoWGg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/nested-clients": "^3.996.7", + "@aws-sdk/token-providers": "3.1004.0", + "@aws-sdk/types": "^3.973.5", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.17.tgz", + "integrity": "sha512-8aiVJh6fTdl8gcyL+sVNcNwTtWpmoFa1Sh7xlj6Z7L/cZ/tYMEBHq44wTYG8Kt0z/PpGNopD89nbj3FHl9QmTA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/nested-clients": "^3.996.7", + "@aws-sdk/types": "^3.973.5", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/dynamodb-codec": { + "version": "3.972.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.972.19.tgz", + "integrity": "sha512-M/y+eA53imfFxA/QjrpfnyhG/eLIHxsNI6GmevwIVoX7E1vGYyevKt1iJ+aZwOVbL9lLeHtlBuYUEhYgVjwDpQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.18", + "@smithy/core": "^3.23.8", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/endpoint-cache": { + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.972.4.tgz", + "integrity": "sha512-GdASDnWanLnHxKK0hqV97xz23QmfA/C8yGe0PiuEmWiHSe+x+x+mFEj4sXqx9IbfyPncWz8f4EhNwBSG9cgYCg==", + "license": "Apache-2.0", + "dependencies": { + "mnemonist": "0.38.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/lib-dynamodb": { + "version": "3.1004.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.1004.0.tgz", + "integrity": "sha512-vjs33MmH7et6T/dMhkykgKGDLRUVuV8sd9X6qtXKWN5HMRgtQYNvabHVXTpm4OVPG28+wUOWKAHNwvwsc23LaQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/util-dynamodb": "^3.996.2", + "@smithy/core": "^3.23.8", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-dynamodb": "^3.1004.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.7.tgz", + "integrity": "sha512-goX+axlJ6PQlRnzE2bQisZ8wVrlm6dXJfBzMJhd8LhAIBan/w1Kl73fJnalM/S+18VnpzIHumyV6DtgmvqG5IA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-config-provider": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-endpoint-discovery": { + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.972.7.tgz", + "integrity": "sha512-ZeFfgAVOGR+fDq/JAPsVA3P07ba74hIppoGfmQyfzZMfAQAzc9Lbg5pndZU8EanzfKnlXbv6y09OMrSkTsUuOg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/endpoint-cache": "^3.972.4", + "@aws-sdk/types": "^3.973.5", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.7.tgz", + "integrity": "sha512-mvWqvm61bmZUKmmrtl2uWbokqpenY3Mc3Jf4nXB/Hse6gWxLPaCQThmhPBDzsPSV8/Odn8V6ovWt3pZ7vy4BFQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.5", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.973.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.973.4.tgz", + "integrity": "sha512-7CH2jcGmkvkHc5Buz9IGbdjq1729AAlgYJiAvGq7qhCHqYleCsriWdSnmsqWTwdAfXHMT+pkxX3w6v5tJNcSug==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/crc64-nvme": "^3.972.4", + "@aws-sdk/types": "^3.973.5", + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-stream": "^4.5.17", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.7.tgz", + "integrity": "sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.5", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.7.tgz", + "integrity": "sha512-vdK1LJfffBp87Lj0Bw3WdK1rJk9OLDYdQpqoKgmpIZPe+4+HawZ6THTbvjhJt4C4MNnRrHTKHQjkwBiIpDBoig==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.5", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.7.tgz", + "integrity": "sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.5", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.7.tgz", + "integrity": "sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.5", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.18.tgz", + "integrity": "sha512-5E3XxaElrdyk6ZJ0TjH7Qm6ios4b/qQCiLr6oQ8NK7e4Kn6JBTJCaYioQCQ65BpZ1+l1mK5wTAac2+pEz0Smpw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/core": "^3.23.8", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/signature-v4": "^5.3.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-stream": "^4.5.17", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-sqs": { + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.972.13.tgz", + "integrity": "sha512-DiGD+Q636hk1+nTq/zSZe1Ih5GhEwmOzH/3oR3A+0/53hVK8E4OWFqAf9dwPE9pufp4mCmn/bE6qU8mE9Q6sig==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.5", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.7.tgz", + "integrity": "sha512-G9clGVuAml7d8DYzY6DnRi7TIIDRvZ3YpqJPz/8wnWS5fYx/FNWNmkO6iJVlVkQg9BfeMzd+bVPtPJOvC4B+nQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.5", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.19.tgz", + "integrity": "sha512-Km90fcXt3W/iqujHzuM6IaDkYCj73gsYufcuWXApWdzoTy6KGk8fnchAjePMARU0xegIR3K4N3yIo1vy7OVe8A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/util-endpoints": "^3.996.4", + "@smithy/core": "^3.23.8", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-retry": "^4.2.11", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.996.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.7.tgz", + "integrity": "sha512-MlGWA8uPaOs5AiTZ5JLM4uuWDm9EEAnm9cqwvqQIc6kEgel/8s1BaOWm9QgUcfc9K8qd7KkC3n43yDbeXOA2tg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/middleware-host-header": "^3.972.7", + "@aws-sdk/middleware-logger": "^3.972.7", + "@aws-sdk/middleware-recursion-detection": "^3.972.7", + "@aws-sdk/middleware-user-agent": "^3.972.19", + "@aws-sdk/region-config-resolver": "^3.972.7", + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/util-endpoints": "^3.996.4", + "@aws-sdk/util-user-agent-browser": "^3.972.7", + "@aws-sdk/util-user-agent-node": "^3.973.4", + "@smithy/config-resolver": "^4.4.10", + "@smithy/core": "^3.23.8", + "@smithy/fetch-http-handler": "^5.3.13", + "@smithy/hash-node": "^4.2.11", + "@smithy/invalid-dependency": "^4.2.11", + "@smithy/middleware-content-length": "^4.2.11", + "@smithy/middleware-endpoint": "^4.4.22", + "@smithy/middleware-retry": "^4.4.39", + "@smithy/middleware-serde": "^4.2.12", + "@smithy/middleware-stack": "^4.2.11", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/node-http-handler": "^4.4.14", + "@smithy/protocol-http": "^5.3.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.38", + "@smithy/util-defaults-mode-node": "^4.2.41", + "@smithy/util-endpoints": "^3.3.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-retry": "^4.2.11", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.7.tgz", + "integrity": "sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.5", + "@smithy/config-resolver": "^4.4.10", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.6.tgz", + "integrity": "sha512-NnsOQsVmJXy4+IdPFUjRCWPn9qNH1TzS/f7MiWgXeoHs903tJpAWQWQtoFvLccyPoBgomKP9L89RRr2YsT/L0g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "^3.972.18", + "@aws-sdk/types": "^3.973.5", + "@smithy/protocol-http": "^5.3.11", + "@smithy/signature-v4": "^5.3.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1004.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1004.0.tgz", + "integrity": "sha512-j9BwZZId9sFp+4GPhf6KrwO8Tben2sXibZA8D1vv2I1zBdvkUHcBA2g4pkqIpTRalMTLC0NPkBPX0gERxfy/iA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/nested-clients": "^3.996.7", + "@aws-sdk/types": "^3.973.5", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.5.tgz", + "integrity": "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", + "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-dynamodb": { + "version": "3.996.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.996.2.tgz", + "integrity": "sha512-ddpwaZmjBzcApYN7lgtAXjk+u+GO8fiPsxzuc59UqP+zqdxI1gsenPvkyiHiF9LnYnyRGijz6oN2JylnN561qQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-dynamodb": "^3.1003.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.4.tgz", + "integrity": "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.5", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", + "@smithy/util-endpoints": "^3.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.7.tgz", + "integrity": "sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.5", + "@smithy/types": "^4.13.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.973.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.4.tgz", + "integrity": "sha512-uqKeLqZ9D3nQjH7HGIERNXK9qnSpUK08l4MlJ5/NZqSSdeJsVANYp437EM9sEzwU28c2xfj2V6qlkqzsgtKs6Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.19", + "@aws-sdk/types": "^3.973.5", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.10.tgz", + "integrity": "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "fast-xml-parser": "5.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz", + "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", + "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "selderee": "^0.11.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.11.tgz", + "integrity": "sha512-Hj4WoYWMJnSpM6/kchsm4bUNTL9XiSyhvoMb2KIq4VJzyDt7JpGHUZHkVNPZVC7YE1tf8tPeVauxpFBKGW4/KQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.2.tgz", + "integrity": "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.3.tgz", + "integrity": "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.10.tgz", + "integrity": "sha512-IRTkd6ps0ru+lTWnfnsbXzW80A8Od8p3pYiZnW98K2Hb20rqfsX7VTlfUwhrcOeSSy68Gn9WBofwPuw3e5CCsg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.3.2", + "@smithy/util-middleware": "^4.2.11", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.23.9", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.9.tgz", + "integrity": "sha512-1Vcut4LEL9HZsdpI0vFiRYIsaoPwZLjAxnVQDUMQK8beMS+EYPLDQCXtbzfxmM5GzSgjfe2Q9M7WaXwIMQllyQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.2.12", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-stream": "^4.5.17", + "@smithy/util-utf8": "^4.2.2", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.11.tgz", + "integrity": "sha512-lBXrS6ku0kTj3xLmsJW0WwqWbGQ6ueooYyp/1L9lkyT0M02C+DWwYwc5aTyXFbRaK38ojALxNixg+LxKSHZc0g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.11", + "@smithy/property-provider": "^4.2.11", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.11.tgz", + "integrity": "sha512-Sf39Ml0iVX+ba/bgMPxaXWAAFmHqYLTmbjAPfLPLY8CrYkRDEqZdUsKC1OwVMCdJXfAt0v4j49GIJ8DoSYAe6w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.13.0", + "@smithy/util-hex-encoding": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.11.tgz", + "integrity": "sha512-3rEpo3G6f/nRS7fQDsZmxw/ius6rnlIpz4UX6FlALEzz8JoSxFmdBt0SZnthis+km7sQo6q5/3e+UJcuQivoXA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.11", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.11.tgz", + "integrity": "sha512-XeNIA8tcP/GDWnnKkO7qEm/bg0B/bP9lvIXZBXcGZwZ+VYM8h8k9wuDvUODtdQ2Wcp2RcBkPTCSMmaniVHrMlA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.11.tgz", + "integrity": "sha512-fzbCh18rscBDTQSCrsp1fGcclLNF//nJyhjldsEl/5wCYmgpHblv5JSppQAyQI24lClsFT0wV06N1Porn0IsEw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.11.tgz", + "integrity": "sha512-MJ7HcI+jEkqoWT5vp+uoVaAjBrmxBtKhZTeynDRG/seEjJfqyg3SiqMMqyPnAMzmIfLaeJ/uiuSDP/l9AnMy/Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.2.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.13", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.13.tgz", + "integrity": "sha512-U2Hcfl2s3XaYjikN9cT4mPu8ybDbImV3baXR0PkVlC0TTx808bRP3FaPGAzPtB8OByI+JqJ1kyS+7GEgae7+qQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.11", + "@smithy/querystring-builder": "^4.2.11", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.12.tgz", + "integrity": "sha512-1wQE33DsxkM/waftAhCH9VtJbUGyt1PJ9YRDpOu+q9FUi73LLFUZ2fD8A61g2mT1UY9k7b99+V1xZ41Rz4SHRQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.2.2", + "@smithy/chunked-blob-reader-native": "^4.2.3", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.11.tgz", + "integrity": "sha512-T+p1pNynRkydpdL015ruIoyPSRw9e/SQOWmSAMmmprfswMrd5Ow5igOWNVlvyVFZlxXqGmyH3NQwfwy8r5Jx0A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.11.tgz", + "integrity": "sha512-hQsTjwPCRY8w9GK07w1RqJi3e+myh0UaOWBBhZ1UMSDgofH/Q1fEYzU1teaX6HkpX/eWDdm7tAGR0jBPlz9QEQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.11.tgz", + "integrity": "sha512-cGNMrgykRmddrNhYy1yBdrp5GwIgEkniS7k9O1VLB38yxQtlvrxpZtUVvo6T4cKpeZsriukBuuxfJcdZQc/f/g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.11.tgz", + "integrity": "sha512-350X4kGIrty0Snx2OWv7rPM6p6vM7RzryvFs6B/56Cux3w3sChOb3bymo5oidXJlPcP9fIRxGUCk7GqpiSOtng==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.11.tgz", + "integrity": "sha512-UvIfKYAKhCzr4p6jFevPlKhQwyQwlJ6IeKLDhmV1PlYfcW3RL4ROjNEDtSik4NYMi9kDkH7eSwyTP3vNJ/u/Dw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.23", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.23.tgz", + "integrity": "sha512-UEFIejZy54T1EJn2aWJ45voB7RP2T+IRzUqocIdM6GFFa5ClZncakYJfcYnoXt3UsQrZZ9ZRauGm77l9UCbBLw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.9", + "@smithy/middleware-serde": "^4.2.12", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", + "@smithy/util-middleware": "^4.2.11", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.40", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.40.tgz", + "integrity": "sha512-YhEMakG1Ae57FajERdHNZ4ShOPIY7DsgV+ZoAxo/5BT0KIe+f6DDU2rtIymNNFIj22NJfeeI6LWIifrwM0f+rA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/service-error-classification": "^4.2.11", + "@smithy/smithy-client": "^4.12.3", + "@smithy/types": "^4.13.0", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-retry": "^4.2.11", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.12.tgz", + "integrity": "sha512-W9g1bOLui7Xn5FABRVS0o3rXL0gfN37d/8I/W7i0N7oxjx9QecUmXEMSUMADTODwdtka9cN43t5BI2CodLJpng==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.11.tgz", + "integrity": "sha512-s+eenEPW6RgliDk2IhjD2hWOxIx1NKrOHxEwNUaUXxYBxIyCcDfNULZ2Mu15E3kwcJWBedTET/kEASPV1A1Akg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.11", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.11.tgz", + "integrity": "sha512-xD17eE7kaLgBBGf5CZQ58hh2YmwK1Z0O8YhffwB/De2jsL0U3JklmhVYJ9Uf37OtUDLF2gsW40Xwwag9U869Gg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.4.14", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.14.tgz", + "integrity": "sha512-DamSqaU8nuk0xTJDrYnRzZndHwwRnyj/n/+RqGGCcBKB4qrQem0mSDiWdupaNWdwxzyMU91qxDmHOCazfhtO3A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/querystring-builder": "^4.2.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.11.tgz", + "integrity": "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.11.tgz", + "integrity": "sha512-hI+barOVDJBkNt4y0L2mu3Ugc0w7+BpJ2CZuLwXtSltGAAwCb3IvnalGlbDV/UCS6a9ZuT3+exd1WxNdLb5IlQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.11.tgz", + "integrity": "sha512-7spdikrYiljpket6u0up2Ck2mxhy7dZ0+TDd+S53Dg2DHd6wg+YNJrTCHiLdgZmEXZKI7LJZcwL3721ZRDFiqA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "@smithy/util-uri-escape": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.11.tgz", + "integrity": "sha512-nE3IRNjDltvGcoThD2abTozI1dkSy8aX+a2N1Rs55en5UsdyyIXgGEmevUL3okZFoJC77JgRGe99xYohhsjivQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.11.tgz", + "integrity": "sha512-HkMFJZJUhzU3HvND1+Yw/kYWXp4RPDLBWLcK1n+Vqw8xn4y2YiBhdww8IxhkQjP/QlZun5bwm3vcHc8AqIU3zw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.6.tgz", + "integrity": "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.11.tgz", + "integrity": "sha512-V1L6N9aKOBAN4wEHLyqjLBnAz13mtILU0SeDrjOaIZEeN6IFa6DxwRt1NNpOdmSpQUfkBj0qeD3m6P77uzMhgQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.3.tgz", + "integrity": "sha512-7k4UxjSpHmPN2AxVhvIazRSzFQjWnud3sOsXcFStzagww17j1cFQYqTSiQ8xuYK3vKLR1Ni8FzuT3VlKr3xCNw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.9", + "@smithy/middleware-endpoint": "^4.4.23", + "@smithy/middleware-stack": "^4.2.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-stream": "^4.5.17", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.0.tgz", + "integrity": "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.11.tgz", + "integrity": "sha512-oTAGGHo8ZYc5VZsBREzuf5lf2pAurJQsccMusVZ85wDkX66ojEc/XauiGjzCj50A61ObFTPe6d7Pyt6UBYaing==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.39", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.39.tgz", + "integrity": "sha512-ui7/Ho/+VHqS7Km2wBw4/Ab4RktoiSshgcgpJzC4keFPs6tLJS4IQwbeahxQS3E/w98uq6E1mirCH/id9xIXeQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.11", + "@smithy/smithy-client": "^4.12.3", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.42", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.42.tgz", + "integrity": "sha512-QDA84CWNe8Akpj15ofLO+1N3Rfg8qa2K5uX0y6HnOp4AnRYRgWrKx/xzbYNbVF9ZsyJUYOfcoaN3y93wA/QJ2A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.10", + "@smithy/credential-provider-imds": "^4.2.11", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/property-provider": "^4.2.11", + "@smithy/smithy-client": "^4.12.3", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.2.tgz", + "integrity": "sha512-+4HFLpE5u29AbFlTdlKIT7jfOzZ8PDYZKTb3e+AgLz986OYwqTourQ5H+jg79/66DB69Un1+qKecLnkZdAsYcA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.11.tgz", + "integrity": "sha512-r3dtF9F+TpSZUxpOVVtPfk09Rlo4lT6ORBqEvX3IBT6SkQAdDSVKR5GcfmZbtl7WKhKnmb3wbDTQ6ibR2XHClw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.11.tgz", + "integrity": "sha512-XSZULmL5x6aCTTii59wJqKsY1l3eMIAomRAccW7Tzh9r8s7T/7rdo03oektuH5jeYRlJMPcNP92EuRDvk9aXbw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.2.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.17", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.17.tgz", + "integrity": "sha512-793BYZ4h2JAQkNHcEnyFxDTcZbm9bVybD0UV/LEWmZ5bkTms7JqjfrLMi2Qy0E5WFcCzLwCAPgcvcvxoeALbAQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.13", + "@smithy/node-http-handler": "^4.4.14", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.11.tgz", + "integrity": "sha512-x7Rh2azQPs3XxbvCzcttRErKKvLnbZfqRf/gOjw2pb+ZscX88e5UkRPCB67bVnsFHxayvMvmePfKTqsRb+is1A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@types/mailparser": { + "version": "3.4.6", + "resolved": "https://registry.npmjs.org/@types/mailparser/-/mailparser-3.4.6.tgz", + "integrity": "sha512-wVV3cnIKzxTffaPH8iRnddX1zahbYB1ZEoAxyhoBo3TBCBuK6nZ8M8JYO/RhsCuuBVOw/DEN/t/ENbruwlxn6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "iconv-lite": "^0.6.3" + } + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/nodemailer": { + "version": "6.4.23", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.23.tgz", + "integrity": "sha512-aFV3/NsYFLSx9mbb5gtirBSXJnAlrusoKNuPbxsASWc7vrKLmIrTQRpdcxNcSFL3VW2A2XpeLEavwb2qMi6nlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/picomatch": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-3.0.2.tgz", + "integrity": "sha512-n0i8TD3UDB7paoMMxA3Y65vUncFJXjcUf7lQY7YyKGl6031FNjfsLs6pdLFCy2GNFxItPJG8GvvpbZc2skH7WA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@zone-eu/mailsplit": { + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.8.tgz", + "integrity": "sha512-eEyACj4JZ7sjzRvy26QhLgKEMWwQbsw1+QZnlLX+/gihcNH07lVPOcnwf5U6UAL7gkc//J3jVd76o/WS+taUiA==", + "license": "(MIT OR EUPL-1.1+)", + "dependencies": { + "libbase64": "1.3.0", + "libmime": "5.3.7", + "libqp": "2.1.1" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", + "license": "MIT" + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/encoding-japanese": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.2.0.tgz", + "integrity": "sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==", + "license": "MIT", + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/fast-copy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.2.tgz", + "integrity": "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==", + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fast-xml-builder": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz", + "integrity": "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", + "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.0.0", + "strnum": "^2.1.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "license": "MIT" + }, + "node_modules/html-to-text": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", + "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", + "license": "MIT", + "dependencies": { + "@selderee/plugin-htmlparser2": "^0.11.0", + "deepmerge": "^4.3.1", + "dom-serializer": "^2.0.0", + "htmlparser2": "^8.0.2", + "selderee": "^0.11.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/leac": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", + "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/libbase64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.3.0.tgz", + "integrity": "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==", + "license": "MIT" + }, + "node_modules/libmime": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.7.tgz", + "integrity": "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==", + "license": "MIT", + "dependencies": { + "encoding-japanese": "2.2.0", + "iconv-lite": "0.6.3", + "libbase64": "1.3.0", + "libqp": "2.1.1" + } + }, + "node_modules/libqp": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/libqp/-/libqp-2.1.1.tgz", + "integrity": "sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==", + "license": "MIT" + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/mailparser": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.9.3.tgz", + "integrity": "sha512-AnB0a3zROum6fLaa52L+/K2SoRJVyFDk78Ea6q1D0ofcZLxWEWDtsS1+OrVqKbV7r5dulKL/AwYQccFGAPpuYQ==", + "license": "MIT", + "dependencies": { + "@zone-eu/mailsplit": "5.4.8", + "encoding-japanese": "2.2.0", + "he": "1.2.0", + "html-to-text": "9.0.5", + "iconv-lite": "0.7.2", + "libmime": "5.3.7", + "linkify-it": "5.0.0", + "nodemailer": "7.0.13", + "punycode.js": "2.3.1", + "tlds": "1.261.0" + } + }, + "node_modules/mailparser/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mailparser/node_modules/nodemailer": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.13.tgz", + "integrity": "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mnemonist": { + "version": "0.38.3", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.3.tgz", + "integrity": "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==", + "license": "MIT", + "dependencies": { + "obliterator": "^1.6.1" + } + }, + "node_modules/nodemailer": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/obliterator": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-1.6.1.tgz", + "integrity": "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==", + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseley": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", + "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", + "license": "MIT", + "dependencies": { + "leac": "^0.6.0", + "peberminta": "^0.9.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/peberminta": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", + "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", + "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^4.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^4.0.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^5.0.2" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/prom-client": { + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz", + "integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.4.0", + "tdigest": "^0.1.1" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/selderee": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", + "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", + "license": "MIT", + "dependencies": { + "parseley": "^0.12.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", + "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "license": "MIT", + "dependencies": { + "bintrees": "1.0.2" + } + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/tlds": { + "version": "1.261.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz", + "integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==", + "license": "MIT", + "bin": { + "tlds": "bin.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/email-worker-nodejs/dynamodb.ts b/email-worker-nodejs/src/aws/dynamodb.ts similarity index 100% rename from email-worker-nodejs/dynamodb.ts rename to email-worker-nodejs/src/aws/dynamodb.ts diff --git a/email-worker-nodejs/s3.ts b/email-worker-nodejs/src/aws/s3.ts similarity index 100% rename from email-worker-nodejs/s3.ts rename to email-worker-nodejs/src/aws/s3.ts diff --git a/email-worker-nodejs/ses.ts b/email-worker-nodejs/src/aws/ses.ts similarity index 100% rename from email-worker-nodejs/ses.ts rename to email-worker-nodejs/src/aws/ses.ts diff --git a/email-worker-nodejs/sqs.ts b/email-worker-nodejs/src/aws/sqs.ts similarity index 100% rename from email-worker-nodejs/sqs.ts rename to email-worker-nodejs/src/aws/sqs.ts diff --git a/email-worker-nodejs/config.ts b/email-worker-nodejs/src/config.ts similarity index 100% rename from email-worker-nodejs/config.ts rename to email-worker-nodejs/src/config.ts diff --git a/email-worker-nodejs/blocklist.ts b/email-worker-nodejs/src/email/blocklist.ts similarity index 100% rename from email-worker-nodejs/blocklist.ts rename to email-worker-nodejs/src/email/blocklist.ts diff --git a/email-worker-nodejs/bounce-handler.ts b/email-worker-nodejs/src/email/bounce-handler.ts similarity index 100% rename from email-worker-nodejs/bounce-handler.ts rename to email-worker-nodejs/src/email/bounce-handler.ts diff --git a/email-worker-nodejs/parser.ts b/email-worker-nodejs/src/email/parser.ts similarity index 100% rename from email-worker-nodejs/parser.ts rename to email-worker-nodejs/src/email/parser.ts diff --git a/email-worker-nodejs/rules-processor.ts b/email-worker-nodejs/src/email/rules-processor.ts similarity index 99% rename from email-worker-nodejs/rules-processor.ts rename to email-worker-nodejs/src/email/rules-processor.ts index 1b5f893..8d40f49 100644 --- a/email-worker-nodejs/rules-processor.ts +++ b/email-worker-nodejs/src/email/rules-processor.ts @@ -9,13 +9,13 @@ import { createTransport } from 'nodemailer'; import type { ParsedMail } from 'mailparser'; -import type { DynamoDBHandler, EmailRule } from '../aws/dynamodb.js'; import type { SESHandler } from '../aws/ses.js'; import { extractBodyParts } from './parser.js'; -import { config, isInternalAddress } from '../config.js'; import { log } from '../logger.js'; // Wir nutzen MailComposer direkt für das Erstellen der Raw Bytes 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; diff --git a/email-worker-nodejs/health.ts b/email-worker-nodejs/src/health.ts similarity index 100% rename from email-worker-nodejs/health.ts rename to email-worker-nodejs/src/health.ts diff --git a/email-worker-nodejs/logger.ts b/email-worker-nodejs/src/logger.ts similarity index 100% rename from email-worker-nodejs/logger.ts rename to email-worker-nodejs/src/logger.ts diff --git a/email-worker-nodejs/main.ts b/email-worker-nodejs/src/main.ts similarity index 98% rename from email-worker-nodejs/main.ts rename to email-worker-nodejs/src/main.ts index bed36a8..805505c 100644 --- a/email-worker-nodejs/main.ts +++ b/email-worker-nodejs/src/main.ts @@ -13,7 +13,7 @@ import { config, loadDomains } from './config.js'; import { log } from './logger.js'; import { startMetricsServer, type MetricsCollector } from './metrics.js'; import { startHealthServer } from './health.js'; -import { UnifiedWorker } from './worker/index.js'; +import { UnifiedWorker } from './worker/unified-worker.js'; // --------------------------------------------------------------------------- // Banner diff --git a/email-worker-nodejs/metrics.ts b/email-worker-nodejs/src/metrics.ts similarity index 100% rename from email-worker-nodejs/metrics.ts rename to email-worker-nodejs/src/metrics.ts diff --git a/email-worker-nodejs/delivery.ts b/email-worker-nodejs/src/smtp/delivery.ts similarity index 99% rename from email-worker-nodejs/delivery.ts rename to email-worker-nodejs/src/smtp/delivery.ts index 5e3250e..d6fd34a 100644 --- a/email-worker-nodejs/delivery.ts +++ b/email-worker-nodejs/src/smtp/delivery.ts @@ -8,8 +8,9 @@ */ import { createTransport, type Transporter } from 'nodemailer'; -import { config } from '../config.js'; + import { log } from '../logger.js'; +import { config } from '../config.js'; // --------------------------------------------------------------------------- // Permanent error detection diff --git a/email-worker-nodejs/domain-poller.ts b/email-worker-nodejs/src/worker/domain-poller.ts similarity index 98% rename from email-worker-nodejs/domain-poller.ts rename to email-worker-nodejs/src/worker/domain-poller.ts index f979917..91973ae 100644 --- a/email-worker-nodejs/domain-poller.ts +++ b/email-worker-nodejs/src/worker/domain-poller.ts @@ -9,9 +9,9 @@ */ import type { SQSHandler } from '../aws/sqs.js'; -import type { MessageProcessor } from './message-processor.js'; import type { MetricsCollector } from '../metrics.js'; import { log } from '../logger.js'; +import { MessageProcessor } from './message-processor.js'; export interface DomainPollerStats { domain: string; diff --git a/email-worker-nodejs/message-processor.ts b/email-worker-nodejs/src/worker/message-processor.ts similarity index 97% rename from email-worker-nodejs/message-processor.ts rename to email-worker-nodejs/src/worker/message-processor.ts index 9e5f5b6..4114c00 100644 --- a/email-worker-nodejs/message-processor.ts +++ b/email-worker-nodejs/src/worker/message-processor.ts @@ -19,15 +19,12 @@ import type { SESHandler } from '../aws/ses.js'; import type { DynamoDBHandler } from '../aws/dynamodb.js'; import type { EmailDelivery } from '../smtp/delivery.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 { 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 diff --git a/email-worker-nodejs/unified-worker.ts b/email-worker-nodejs/src/worker/unified-worker.ts similarity index 92% rename from email-worker-nodejs/unified-worker.ts rename to email-worker-nodejs/src/worker/unified-worker.ts index 405840a..8f4dad6 100644 --- a/email-worker-nodejs/unified-worker.ts +++ b/email-worker-nodejs/src/worker/unified-worker.ts @@ -8,13 +8,17 @@ * - Graceful shutdown */ -import { S3Handler, SQSHandler, SESHandler, DynamoDBHandler } from '../aws/index.js'; -import { EmailDelivery } from '../smtp/index.js'; +import { DynamoDBHandler } from '../aws/dynamodb'; +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 { DomainPoller, type DomainPollerStats } from './domain-poller.js'; import type { MetricsCollector } from '../metrics.js'; import { log } from '../logger.js'; + export class UnifiedWorker { private pollers: DomainPoller[] = []; private processor: MessageProcessor; From 56c7b51e3551830acae693cb5e3a20ba40fbd8ff Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sat, 7 Mar 2026 15:01:56 -0600 Subject: [PATCH 55/74] changed blocked sender list --- email-worker-nodejs/src/email/blocklist.ts | 34 +++++++++---------- .../src/worker/message-processor.ts | 10 +++++- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/email-worker-nodejs/src/email/blocklist.ts b/email-worker-nodejs/src/email/blocklist.ts index 28f1f0a..6fdcdc1 100644 --- a/email-worker-nodejs/src/email/blocklist.ts +++ b/email-worker-nodejs/src/email/blocklist.ts @@ -26,15 +26,15 @@ export class BlocklistChecker { * Batch-check whether a sender is blocked for each recipient. * Uses a single batch DynamoDB call for efficiency. */ - async batchCheckBlockedSenders( +async batchCheckBlockedSenders( recipients: string[], - sender: string, + senders: string[], // <-- Geändert zu Array workerName: string, ): Promise> { - const patternsByRecipient = - await this.dynamodb.batchGetBlockedPatterns(recipients); - - const senderClean = extractAddress(sender); + const patternsByRecipient = await this.dynamodb.batchGetBlockedPatterns(recipients); + + // Alle übergebenen Adressen bereinigen + const sendersClean = senders.map(s => extractAddress(s)).filter(Boolean); const result: Record = {}; for (const recipient of recipients) { @@ -42,21 +42,21 @@ export class BlocklistChecker { let isBlocked = false; for (const pattern of patterns) { - if (picomatch.isMatch(senderClean, pattern.toLowerCase())) { - log( - `⛔ BLOCKED: Sender ${senderClean} matches pattern '${pattern}' ` + - `for inbox ${recipient}`, - 'WARNING', - workerName, - ); - isBlocked = true; - break; + for (const senderClean of sendersClean) { + if (picomatch.isMatch(senderClean, pattern.toLowerCase())) { + log( + `⛔ BLOCKED: Sender ${senderClean} matches pattern '${pattern}' for inbox ${recipient}`, + 'WARNING', + workerName, + ); + isBlocked = true; + break; + } } + if (isBlocked) break; } - result[recipient] = isBlocked; } - return result; } } diff --git a/email-worker-nodejs/src/worker/message-processor.ts b/email-worker-nodejs/src/worker/message-processor.ts index 4114c00..cdec270 100644 --- a/email-worker-nodejs/src/worker/message-processor.ts +++ b/email-worker-nodejs/src/worker/message-processor.ts @@ -179,9 +179,17 @@ export class MessageProcessor { } // 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( recipients, - fromAddrFinal, + sendersToCheck, // <-- Array übergeben workerName, ); From 3ab46f163a753294d903edd53bb1977840ff7e68 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sat, 7 Mar 2026 15:05:44 -0600 Subject: [PATCH 56/74] ipadresses --- DMS/docker-data/dms/config/fail2ban-jail.cf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DMS/docker-data/dms/config/fail2ban-jail.cf b/DMS/docker-data/dms/config/fail2ban-jail.cf index eb20763..a1ef651 100644 --- a/DMS/docker-data/dms/config/fail2ban-jail.cf +++ b/DMS/docker-data/dms/config/fail2ban-jail.cf @@ -1,6 +1,6 @@ [DEFAULT] # 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] # Erhöht die Anzahl der erlaubten Fehlversuche auf 20 From 12af8577f31efe06674f26080ad57ab581c815a9 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sat, 7 Mar 2026 15:47:14 -0600 Subject: [PATCH 57/74] changes --- email-worker-nodejs/docker-compose.yml | 4 ++-- email-worker-nodejs/src/logger.ts | 15 +++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/email-worker-nodejs/docker-compose.yml b/email-worker-nodejs/docker-compose.yml index 929e0f1..93f29b3 100644 --- a/email-worker-nodejs/docker-compose.yml +++ b/email-worker-nodejs/docker-compose.yml @@ -10,8 +10,8 @@ services: - ./domains.txt:/etc/email-worker/domains.txt:ro - worker-logs:/var/log/email-worker ports: - - "8000:8000" # Prometheus metrics - - "8080:8080" # Health check + - "9000:8000" # Prometheus metrics (Host:Container) + - "9090:8080" # Health check (Host:Container) # Connect to DMS on the host or Docker network extra_hosts: - "host.docker.internal:host-gateway" diff --git a/email-worker-nodejs/src/logger.ts b/email-worker-nodejs/src/logger.ts index 545b695..e37ce14 100644 --- a/email-worker-nodejs/src/logger.ts +++ b/email-worker-nodejs/src/logger.ts @@ -103,12 +103,15 @@ function ensureFileStream(): WriteStream | null { // --------------------------------------------------------------------------- const logger = pino({ level: 'info', - formatters: { - level(label) { - return { level: label }; - }, - }, - timestamp: pino.stdTimeFunctions.isoTime, + transport: { + target: 'pino-pretty', + options: { + colorize: true, + translateTime: 'SYS:yyyy-mm-dd HH:MM:ss', + ignore: 'pid,hostname', + singleLine: true + } + } }); // --------------------------------------------------------------------------- From 757855866c46538098a0af22cb627e56217cf253 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sat, 7 Mar 2026 16:44:53 -0600 Subject: [PATCH 58/74] printstats --- .../src/worker/unified-worker.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/email-worker-nodejs/src/worker/unified-worker.ts b/email-worker-nodejs/src/worker/unified-worker.ts index 8f4dad6..ea50df6 100644 --- a/email-worker-nodejs/src/worker/unified-worker.ts +++ b/email-worker-nodejs/src/worker/unified-worker.ts @@ -23,6 +23,7 @@ export class UnifiedWorker { private pollers: DomainPoller[] = []; private processor: MessageProcessor; private sqs: SQSHandler; + private statusInterval: NodeJS.Timeout | null = null; constructor( private domains: string[], @@ -78,10 +79,16 @@ export class UnifiedWorker { this.pollers.map((p) => p.stats.domain).join(', '), 'SUCCESS', ); + + // Starte den 5-Minuten-Status-Report + this.statusInterval = setInterval(() => { + this.printStatus(); + }, 5 * 60 * 1000); } async stop(): Promise { log('🛑 Stopping all domain pollers...'); + if (this.statusInterval) clearInterval(this.statusInterval); // <-- Neue Zeile await Promise.all(this.pollers.map((p) => p.stop())); log('✅ All pollers stopped.'); } @@ -103,4 +110,25 @@ export class UnifiedWorker { return { totalProcessed, totalErrors, domains }; } + + private printStatus(): void { + const stats = this.getStats(); + // Zähle aktive Poller + const activePollers = this.pollers.filter((p) => p.stats.running).length; + const totalPollers = this.pollers.length; + + // Formatiere die Domain-Statistiken (z.B. hotshpotshga:1) + const domainStats = stats.domains + .map((d) => { + const shortName = d.domain.split('.')[0].substring(0, 12); + return `${shortName}:${d.processed}`; + }) + .join(' | '); + + log( + `📊 Status: ${activePollers}/${totalPollers} active, total:${stats.totalProcessed} | ${domainStats}`, + 'INFO', + 'unified-worker' + ); + } } From cd4444906770191173700181f152446e9fe1d41a Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sat, 7 Mar 2026 17:04:50 -0600 Subject: [PATCH 59/74] send mail even in case of parsing error ... --- email-worker-nodejs/src/worker/message-processor.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/email-worker-nodejs/src/worker/message-processor.ts b/email-worker-nodejs/src/worker/message-processor.ts index cdec270..1e01da3 100644 --- a/email-worker-nodejs/src/worker/message-processor.ts +++ b/email-worker-nodejs/src/worker/message-processor.ts @@ -135,6 +135,7 @@ export class MessageProcessor { let finalRawBytes = rawBytes; let fromAddrFinal = fromAddr; let isBounce = false; + let parsedFinal: ParsedMail | null = null; // <-- Hier deklarieren try { const parsed = await parseEmail(rawBytes); @@ -147,7 +148,6 @@ export class MessageProcessor { subject, workerName, ); - isBounce = bounceResult.isBounce; finalRawBytes = bounceResult.rawBytes; @@ -165,17 +165,17 @@ export class MessageProcessor { } // Re-parse after modifications for rules processing - var parsedFinal = await parseEmail(finalRawBytes); + parsedFinal = await parseEmail(finalRawBytes); } catch (err: any) { log( - `⚠ Parsing/Logic Error: ${err.message ?? err}. Sending original.`, + `⚠ Parsing/Logic Error: ${err.message ?? err}. Sending original RAW mail without rules.`, 'WARNING', workerName, ); log(`Full error: ${err.stack ?? err}`, 'ERROR', workerName); fromAddrFinal = fromAddr; isBounce = false; - var parsedFinal = await parseEmail(rawBytes); + parsedFinal = null; // <-- GANZ WICHTIG: Kein erneuter Parse-Versuch! } // 6. BLOCKLIST CHECK @@ -215,7 +215,7 @@ export class MessageProcessor { } // Process rules (OOO, Forwarding) — not for bounces or already forwarded - if (!isBounce && !skipRules) { + if (!isBounce && !skipRules && parsedFinal !== null) { const metricsCallback = (action: 'autoreply' | 'forward', dom: string) => { if (action === 'autoreply') this.metrics?.incrementAutoreply(dom); else if (action === 'forward') this.metrics?.incrementForward(dom); From 285ffffb3a90ce0a92b0a92e9396ede309b2182b Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sat, 7 Mar 2026 17:08:50 -0600 Subject: [PATCH 60/74] add missing import --- email-worker-nodejs/src/worker/message-processor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/email-worker-nodejs/src/worker/message-processor.ts b/email-worker-nodejs/src/worker/message-processor.ts index 1e01da3..12ce679 100644 --- a/email-worker-nodejs/src/worker/message-processor.ts +++ b/email-worker-nodejs/src/worker/message-processor.ts @@ -19,6 +19,7 @@ import type { SESHandler } from '../aws/ses.js'; import type { DynamoDBHandler } from '../aws/dynamodb.js'; import type { EmailDelivery } from '../smtp/delivery.js'; import type { MetricsCollector } from '../metrics.js'; +import type { ParsedMail } from 'mailparser'; import { log } from '../logger.js'; import { BlocklistChecker } from '../email/blocklist.js'; From 5e4859a5c4a9061a22bd74017264bce31249d483 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 8 Mar 2026 16:32:16 -0500 Subject: [PATCH 61/74] changes from main branch --- email-worker-nodejs/docker-compose.yml | 2 +- email-worker-nodejs/src/logger.ts | 27 ++++++++++++++----- monitoring/docker-compose.yml | 36 ++++++++++++++++++++++++++ monitoring/prometheus.yml | 25 ++++++++++++++++++ 4 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 monitoring/docker-compose.yml create mode 100644 monitoring/prometheus.yml diff --git a/email-worker-nodejs/docker-compose.yml b/email-worker-nodejs/docker-compose.yml index 93f29b3..3fff942 100644 --- a/email-worker-nodejs/docker-compose.yml +++ b/email-worker-nodejs/docker-compose.yml @@ -8,7 +8,7 @@ services: env_file: .env volumes: - ./domains.txt:/etc/email-worker/domains.txt:ro - - worker-logs:/var/log/email-worker + - ./logs:/var/log/email-worker ports: - "9000:8000" # Prometheus metrics (Host:Container) - "9090:8080" # Health check (Host:Container) diff --git a/email-worker-nodejs/src/logger.ts b/email-worker-nodejs/src/logger.ts index e37ce14..f624c65 100644 --- a/email-worker-nodejs/src/logger.ts +++ b/email-worker-nodejs/src/logger.ts @@ -104,13 +104,26 @@ function ensureFileStream(): WriteStream | null { const logger = pino({ level: 'info', transport: { - target: 'pino-pretty', - options: { - colorize: true, - translateTime: 'SYS:yyyy-mm-dd HH:MM:ss', - ignore: 'pid,hostname', - singleLine: true - } + targets: [ + { + // 1. Schicke bunte Logs in die Konsole (für docker compose logs -f) + target: 'pino-pretty', + options: { + colorize: true, + translateTime: 'SYS:yyyy-mm-dd HH:MM:ss', + ignore: 'pid,hostname', + singleLine: true + } + }, + { + // 2. Schreibe gleichzeitig alles unformatiert in die Datei + target: 'pino/file', + options: { + destination: '/var/log/email-worker/worker.log', + mkdir: true + } + } + ] } }); diff --git a/monitoring/docker-compose.yml b/monitoring/docker-compose.yml new file mode 100644 index 0000000..e83553b --- /dev/null +++ b/monitoring/docker-compose.yml @@ -0,0 +1,36 @@ +services: + prometheus: + image: prom/prometheus:latest + container_name: prometheus + restart: unless-stopped + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro + - prometheus_data:/prometheus + ports: + - "9091:9090" + extra_hosts: + - "host.docker.internal:host-gateway" # Damit er deinen Worker findet + + grafana: + image: grafana/grafana:latest + container_name: grafana + restart: unless-stopped + volumes: + - grafana_data:/var/lib/grafana + ports: + - "4000:3000" + depends_on: + - prometheus + + blackbox_exporter: + image: prom/blackbox-exporter:latest + container_name: blackbox_exporter + restart: unless-stopped + ports: + - "9115:9115" + extra_hosts: # <-- Diese Zeile neu + - "host.docker.internal:host-gateway" # <-- Diese Zeile neu + +volumes: + prometheus_data: + grafana_data: \ No newline at end of file diff --git a/monitoring/prometheus.yml b/monitoring/prometheus.yml new file mode 100644 index 0000000..8d77907 --- /dev/null +++ b/monitoring/prometheus.yml @@ -0,0 +1,25 @@ +global: + scrape_interval: 15s + +scrape_configs: + # 1. Scraping deines Node.js Email-Workers + - job_name: 'email-worker' + static_configs: + - targets: ['host.docker.internal:9000'] + + # 2. Port-Überwachung deines Mailservers (IMAP 993 & POP3 995) + - job_name: 'mailserver_ports' + metrics_path: /probe + params: + module: [tcp_connect] # Prüft nur, ob der TCP-Port offen ist + static_configs: + - targets: + - host.docker.internal:993 # IMAPS + - host.docker.internal:995 # POP3S + relabel_configs: + - source_labels: [__address__] + target_label: __param_target + - source_labels: [__param_target] + target_label: instance + - target_label: __address__ + replacement: blackbox_exporter:9115 # Der Exporter führt den Check aus \ No newline at end of file From bd8efc867a07b8fa632236818dec3f03121d1b4d Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Wed, 11 Mar 2026 19:47:37 -0500 Subject: [PATCH 62/74] whitelist feature --- DMS/Dockerfile | 7 ++- DMS/dynamic_whitelist.py | 87 +++++++++++++++++++++++++++++++++++ DMS/whitelist-supervisor.conf | 6 +++ 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 DMS/dynamic_whitelist.py create mode 100644 DMS/whitelist-supervisor.conf diff --git a/DMS/Dockerfile b/DMS/Dockerfile index eaba22a..ef3bcc6 100644 --- a/DMS/Dockerfile +++ b/DMS/Dockerfile @@ -23,4 +23,9 @@ RUN chmod +x /scripts/sync.py COPY sieve-schedule /etc/sieve-schedule # 5. Supervisor Konfiguration kopieren -COPY sieve-supervisor.conf /etc/supervisor/conf.d/sieve-sync.conf \ No newline at end of file +COPY sieve-supervisor.conf /etc/supervisor/conf.d/sieve-sync.conf + +# 6. Dynamic Whitelist Script und Supervisor-Config kopieren +COPY dynamic_whitelist.py /scripts/dynamic_whitelist.py +RUN chmod +x /scripts/dynamic_whitelist.py +COPY whitelist-supervisor.conf /etc/supervisor/conf.d/dynamic-whitelist.conf \ No newline at end of file diff --git a/DMS/dynamic_whitelist.py b/DMS/dynamic_whitelist.py new file mode 100644 index 0000000..f984bf3 --- /dev/null +++ b/DMS/dynamic_whitelist.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +import os +import re +import time +import subprocess +import threading +from datetime import datetime +try: + from croniter import croniter +except ImportError: + print("Bitte 'croniter' via pip installieren!") + exit(1) + +LOG_FILE = '/var/log/mail/mail.log' +WHITELIST_DURATION_SEC = 24 * 60 * 60 # 24 Stunden +CRON_SCHEDULE = "0 * * * *" # Jede Stunde + +active_ips = {} + +# Regex für Dovecot IMAP/POP3 erfolgreiche Logins +LOGIN_REGEX = re.compile(r"dovecot: (?:imap|pop3)-login: Login: user=<[^>]+>.*rip=([0-9]{1,3}(?:\.[0-9]{1,3}){3}),") +# Private Netze (Docker/Local) ignorieren +IGNORE_REGEX = re.compile(r"^(172\.|10\.|192\.168\.|127\.)") + +def run_command(cmd): + try: + subprocess.run(cmd, shell=True, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except Exception as e: + print(f"Fehler bei: {cmd} - {e}") + +def cleanup_job(): + """Cron-Thread für das stündliche Aufräumen abgelaufener IPs.""" + iter = croniter(CRON_SCHEDULE, datetime.now()) + while True: + next_run = iter.get_next(datetime) + sleep_seconds = (next_run - datetime.now()).total_seconds() + + if sleep_seconds > 0: + time.sleep(sleep_seconds) + + print(f"[{datetime.now()}] Starte stündlichen Whitelist-Cleanup...") + now = time.time() + expired_ips = [ip for ip, timestamp in active_ips.items() if now - timestamp > WHITELIST_DURATION_SEC] + + for ip in expired_ips: + print(f"[{datetime.now()}] Whitelist für {ip} abgelaufen. Entferne...") + run_command(f"fail2ban-client set dovecot delignoreip {ip}") + run_command(f"fail2ban-client set postfix delignoreip {ip}") + del active_ips[ip] + +def follow_log(): + """Verwendet System 'tail -F', da dies Log-Rotation automatisch handhabt.""" + print(f"[{datetime.now()}] Dynamic Whitelist Monitor gestartet...") + + while not os.path.exists(LOG_FILE): + time.sleep(2) + + process = subprocess.Popen(['tail', '-F', LOG_FILE], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True) + + for line in process.stdout: + match = LOGIN_REGEX.search(line) + if match: + ip = match.group(1) + + if IGNORE_REGEX.match(ip): + continue + + now = time.time() + + # Neue IP in die Fail2ban Whitelist eintragen + if ip not in active_ips: + print(f"[{datetime.now()}] Neuer erfolgreicher Login von {ip}. Setze auf Whitelist...") + run_command(f"fail2ban-client set dovecot addignoreip {ip}") + run_command(f"fail2ban-client set postfix addignoreip {ip}") + + # Timestamp (Last Seen) aktualisieren + active_ips[ip] = now + +if __name__ == '__main__': + # Warte kurz, bis Fail2ban nach einem Container-Start hochgefahren ist + time.sleep(15) + + # Cron-Cleanup im Hintergrund starten + threading.Thread(target=cleanup_job, daemon=True).start() + + # Log-Überwachung in der Endlosschleife starten + follow_log() \ No newline at end of file diff --git a/DMS/whitelist-supervisor.conf b/DMS/whitelist-supervisor.conf new file mode 100644 index 0000000..60472f5 --- /dev/null +++ b/DMS/whitelist-supervisor.conf @@ -0,0 +1,6 @@ +[program:dynamic-whitelist] +command=/usr/bin/python3 -u /scripts/dynamic_whitelist.py +autostart=true +autorestart=true +stderr_logfile=/var/log/supervisor/dynamic-whitelist.err.log +stdout_logfile=/var/log/supervisor/dynamic-whitelist.out.log \ No newline at end of file From 386be31671095ab39b28f93b9c4c4a391c47b521 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Thu, 12 Mar 2026 18:56:18 -0500 Subject: [PATCH 63/74] update autodiscover --- caddy/email-setup/autodiscover.xml | 29 ------- caddy/email.mobileconfig.tpl | 54 ++++++++----- caddy/email_autodiscover | 112 --------------------------- caddy/update-caddy-certs.sh | 119 ++++++++++++++++++----------- 4 files changed, 109 insertions(+), 205 deletions(-) delete mode 100644 caddy/email-setup/autodiscover.xml delete mode 100644 caddy/email_autodiscover diff --git a/caddy/email-setup/autodiscover.xml b/caddy/email-setup/autodiscover.xml deleted file mode 100644 index b855f09..0000000 --- a/caddy/email-setup/autodiscover.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - email - settings - - IMAP - mail.email-srvr.com - 993 - off - - off - on - on - - - SMTP - mail.email-srvr.com - 465 - off - - off - on - on - - - - \ No newline at end of file diff --git a/caddy/email.mobileconfig.tpl b/caddy/email.mobileconfig.tpl index 55c3d8e..43944eb 100644 --- a/caddy/email.mobileconfig.tpl +++ b/caddy/email.mobileconfig.tpl @@ -1,67 +1,85 @@ +{{/* ─────────────────────────────────────────────────── + Apple MobileConfig Template (Caddy templates) + + Dynamisch: Leitet die Basisdomain aus dem Host-Header ab. + Erwartet Aufruf auf autodiscover./apple?email=user@domain + + Hostname-Logik: + Host = autodiscover.cielectrical.com + splitList "." .Req.Host → [autodiscover, cielectrical, com] + Basisdomain = cielectrical.com (Index 1+) + IMAP = imap.cielectrical.com + SMTP = smtp.cielectrical.com + ─────────────────────────────────────────────────── */}} +{{- $email := .Req.URL.Query.Get "email" -}} +{{- $hostParts := splitList "." .Req.Host -}} +{{- $baseDomain := join "." (slice $hostParts 1) -}} PayloadContent EmailAccountDescription - {{.Req.URL.Query.Get "email"}} + {{ $email }} EmailAccountName - {{.Req.URL.Query.Get "email"}} + {{ $email }} EmailAccountType EmailTypeIMAP EmailAddress - {{.Req.URL.Query.Get "email"}} + {{ $email }} IncomingMailServerAuthentication EmailAuthPassword IncomingMailServerHostName - mail.email-srvr.com + imap.{{ $baseDomain }} IncomingMailServerPortNumber 993 IncomingMailServerUseSSL - + > IncomingMailServerUsername - {{.Req.URL.Query.Get "email"}} + {{ $email }} OutgoingMailServerAuthentication EmailAuthPassword OutgoingMailServerHostName - mail.email-srvr.com + smtp.{{ $baseDomain }} OutgoingMailServerPortNumber 465 OutgoingMailServerUseSSL OutgoingMailServerUsername - {{.Req.URL.Query.Get "email"}} + {{ $email }} + OutgoingPasswordRequired + PayloadDescription - E-Mail Konfiguration für {{.Req.URL.Query.Get "email"}} + E-Mail Konfiguration für {{ $email }} PayloadDisplayName - {{.Req.URL.Query.Get "email"}} + {{ $baseDomain }} E-Mail PayloadIdentifier - com.email-srvr.profile.{{.Req.URL.Query.Get "email"}} + com.{{ $baseDomain }}.email.account PayloadType com.apple.mail.managed PayloadUUID - {{uuidv4}} + {{ uuidv4 }} PayloadVersion 1 PayloadDescription - Automatische E-Mail Einrichtung für {{.Req.URL.Query.Get "email"}} + Automatische E-Mail Einrichtung für {{ $email }} PayloadDisplayName - E-Mail Einstellungen + {{ $baseDomain }} E-Mail Einstellungen PayloadIdentifier - com.email-srvr.profile.root + com.{{ $baseDomain }}.email.profile PayloadOrganization - IT Support + Bay Area Affiliates, Inc. PayloadRemovalDisallowed PayloadType Configuration PayloadUUID - {{uuidv4}} + {{ uuidv4 }} PayloadVersion 1 - \ No newline at end of file + diff --git a/caddy/email_autodiscover b/caddy/email_autodiscover deleted file mode 100644 index 78ccedd..0000000 --- a/caddy/email_autodiscover +++ /dev/null @@ -1,112 +0,0 @@ -# email_autodiscover - Dynamisches Autodiscover/Autoconfig Snippet -# Importiert im Caddyfile via: import email_autodiscover -# -# Funktioniert mit JEDER Domain automatisch, solange der Caddy-Block -# auf autodiscover. oder autoconfig. 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 ` - - - - email - settings - - IMAP - imap.{labels.1}.{labels.0} - 993 - on - {header.X-Anchormailbox} - off - on - on - - - POP3 - pop.{labels.1}.{labels.0} - 995 - on - {header.X-Anchormailbox} - off - on - on - - - SMTP - smtp.{labels.1}.{labels.0} - 465 - on - {header.X-Anchormailbox} - off - on - on - - - -` 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 ` - - - {labels.1}.{labels.0} Mail - {labels.1}.{labels.0} - - imap.{labels.1}.{labels.0} - 993 - SSL - password-cleartext - %EMAILADDRESS% - - - pop.{labels.1}.{labels.0} - 995 - SSL - password-cleartext - %EMAILADDRESS% - - - smtp.{labels.1}.{labels.0} - 465 - SSL - password-cleartext - %EMAILADDRESS% - - -` 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 - } -} diff --git a/caddy/update-caddy-certs.sh b/caddy/update-caddy-certs.sh index 34a6157..50343c7 100755 --- a/caddy/update-caddy-certs.sh +++ b/caddy/update-caddy-certs.sh @@ -9,6 +9,7 @@ # - Wildcard-Cert Block (*.domain + domain) # - Webmail Block (reverse_proxy zu Roundcube) # - Autodiscover/Autoconfig Block (importiert email_settings Snippet) +# - Email-Setup Block (QR-Code Seite für iPhone) # # Bei neuen Domains: Script erneut laufen lassen + caddy reload. # @@ -64,13 +65,18 @@ OUTPUT="${OUTPUT}# Einbinden im Caddyfile: import mail_certs\n" OUTPUT="${OUTPUT}# Generiert: $(date)\n" OUTPUT="${OUTPUT}\n" -# --- Autodiscover/Autoconfig Snippet einbetten --- +# ===================================================================== +# Autodiscover/Autoconfig Snippet (dynamisch) +# {labels.1}.{labels.0} = Basisdomain aus Hostname +# ===================================================================== OUTPUT="${OUTPUT}# ═══════════════════════════════════════════════\n" OUTPUT="${OUTPUT}# Autodiscover/Autoconfig Snippet (dynamisch)\n" OUTPUT="${OUTPUT}# {labels.1}.{labels.0} = Basisdomain aus Hostname\n" OUTPUT="${OUTPUT}# ═══════════════════════════════════════════════\n" OUTPUT="${OUTPUT}(email_settings) {\n" -OUTPUT="${OUTPUT} # Outlook Autodiscover (XML)\n" + +# --- 1. Outlook Classic Autodiscover (POST + GET XML) --- +OUTPUT="${OUTPUT} # Outlook Autodiscover (XML) - POST und GET\n" OUTPUT="${OUTPUT} route /autodiscover/autodiscover.xml {\n" OUTPUT="${OUTPUT} header Content-Type \"application/xml\"\n" OUTPUT="${OUTPUT} respond \`\n" @@ -83,18 +89,8 @@ OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} IMAP\n" OUTPUT="${OUTPUT} imap.{labels.1}.{labels.0}\n" OUTPUT="${OUTPUT} 993\n" -OUTPUT="${OUTPUT} on\n" -OUTPUT="${OUTPUT} {header.X-Anchormailbox}\n" -OUTPUT="${OUTPUT} off\n" -OUTPUT="${OUTPUT} on\n" -OUTPUT="${OUTPUT} on\n" -OUTPUT="${OUTPUT} \n" -OUTPUT="${OUTPUT} \n" -OUTPUT="${OUTPUT} POP3\n" -OUTPUT="${OUTPUT} pop.{labels.1}.{labels.0}\n" -OUTPUT="${OUTPUT} 995\n" -OUTPUT="${OUTPUT} on\n" -OUTPUT="${OUTPUT} {header.X-Anchormailbox}\n" +OUTPUT="${OUTPUT} off\n" +OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} off\n" OUTPUT="${OUTPUT} on\n" OUTPUT="${OUTPUT} on\n" @@ -103,8 +99,8 @@ OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} SMTP\n" OUTPUT="${OUTPUT} smtp.{labels.1}.{labels.0}\n" OUTPUT="${OUTPUT} 465\n" -OUTPUT="${OUTPUT} on\n" -OUTPUT="${OUTPUT} {header.X-Anchormailbox}\n" +OUTPUT="${OUTPUT} off\n" +OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} off\n" OUTPUT="${OUTPUT} on\n" OUTPUT="${OUTPUT} on\n" @@ -114,15 +110,18 @@ OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT}\` 200\n" OUTPUT="${OUTPUT} }\n" OUTPUT="${OUTPUT}\n" -OUTPUT="${OUTPUT} # Modern Outlook (JSON)\n" + +# --- 2. Outlook New / Microsoft 365 (JSON v2) --- +# Outlook New sendet GET auf /autodiscover/autodiscover.json?Protocol=AutodiscoverV1&... +# Antwort muss den XML-Endpoint zurückgeben +OUTPUT="${OUTPUT} # Outlook New/365 (JSON → Redirect zu XML)\n" OUTPUT="${OUTPUT} route /autodiscover/autodiscover.json {\n" OUTPUT="${OUTPUT} header Content-Type \"application/json\"\n" -OUTPUT="${OUTPUT} respond \`{\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} respond \`{\"Protocol\":\"AutodiscoverV1\",\"Url\":\"https://autodiscover.{labels.1}.{labels.0}/autodiscover/autodiscover.xml\"}\` 200\n" OUTPUT="${OUTPUT} }\n" OUTPUT="${OUTPUT}\n" + +# --- 3. Thunderbird Autoconfig --- OUTPUT="${OUTPUT} # Thunderbird Autoconfig\n" OUTPUT="${OUTPUT} route /mail/config-v1.1.xml {\n" OUTPUT="${OUTPUT} header Content-Type \"application/xml\"\n" @@ -138,13 +137,6 @@ OUTPUT="${OUTPUT} SSL\n" OUTPUT="${OUTPUT} password-cleartext\n" OUTPUT="${OUTPUT} %%EMAILADDRESS%%\n" OUTPUT="${OUTPUT} \n" -OUTPUT="${OUTPUT} \n" -OUTPUT="${OUTPUT} pop.{labels.1}.{labels.0}\n" -OUTPUT="${OUTPUT} 995\n" -OUTPUT="${OUTPUT} SSL\n" -OUTPUT="${OUTPUT} password-cleartext\n" -OUTPUT="${OUTPUT} %%EMAILADDRESS%%\n" -OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT} smtp.{labels.1}.{labels.0}\n" OUTPUT="${OUTPUT} 465\n" @@ -156,16 +148,41 @@ OUTPUT="${OUTPUT} \n" OUTPUT="${OUTPUT}\` 200\n" OUTPUT="${OUTPUT} }\n" OUTPUT="${OUTPUT}\n" -OUTPUT="${OUTPUT} # Apple MobileConfig\n" + +# --- 4. Apple MobileConfig (Template) --- +OUTPUT="${OUTPUT} # Apple MobileConfig (dynamisches Template)\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} header Content-Disposition \"attachment; filename=email.mobileconfig\"\n" OUTPUT="${OUTPUT} root * /etc/caddy\n" OUTPUT="${OUTPUT} rewrite * /email.mobileconfig.tpl\n" OUTPUT="${OUTPUT} file_server\n" OUTPUT="${OUTPUT} }\n" + +# --- 5. Samsung Email (nutzt ebenfalls autoconfig, kein extra Block nötig) --- +# Samsung Email-App versucht: +# 1. https://autoconfig./mail/config-v1.1.xml (= Thunderbird-Format, schon abgedeckt) +# 2. Alternativ: Outlook Autodiscover XML +# → Kein separater Block erforderlich. + +OUTPUT="${OUTPUT}}\n\n" + +# ===================================================================== +# Email-Setup Snippet (QR-Code Seite für iPhone) +# ===================================================================== +OUTPUT="${OUTPUT}# ═══════════════════════════════════════════════\n" +OUTPUT="${OUTPUT}# Email-Setup Snippet (QR-Code Seite)\n" +OUTPUT="${OUTPUT}# ═══════════════════════════════════════════════\n" +OUTPUT="${OUTPUT}(email_setup_page) {\n" +OUTPUT="${OUTPUT} route /email-setup* {\n" +OUTPUT="${OUTPUT} uri strip_prefix /email-setup\n" +OUTPUT="${OUTPUT} root * /var/www/email-setup\n" +OUTPUT="${OUTPUT} try_files {path} /setup.html\n" +OUTPUT="${OUTPUT} file_server\n" +OUTPUT="${OUTPUT} }\n" OUTPUT="${OUTPUT}}\n\n" # Node-Hostname immer als erstes (Default-Cert des DMS) @@ -183,6 +200,7 @@ for domain in $DOMAINS; do echo " → Wildcard Block: *.${domain}" echo " → Webmail Block: webmail.${domain}" echo " → Autodiscover Block: autodiscover.${domain}, autoconfig.${domain}" + echo " → Email-Setup Block: webmail.${domain}/email-setup" # Wildcard-Cert Block (für Cert-Generierung + Fallback) OUTPUT="${OUTPUT}# ═══════════════════════════════════════════════\n" @@ -197,9 +215,10 @@ for domain in $DOMAINS; do OUTPUT="${OUTPUT} respond \"OK\" 200\n" OUTPUT="${OUTPUT}}\n\n" - # Webmail Block (Roundcube) - OUTPUT="${OUTPUT}# Roundcube Webmail für $domain\n" + # Webmail Block (Roundcube + Email-Setup) + OUTPUT="${OUTPUT}# Roundcube Webmail + Email-Setup für $domain\n" OUTPUT="${OUTPUT}webmail.${domain} {\n" + OUTPUT="${OUTPUT} import email_setup_page\n" OUTPUT="${OUTPUT} reverse_proxy roundcube:80\n" OUTPUT="${OUTPUT} encode gzip\n" OUTPUT="${OUTPUT} log {\n" @@ -253,15 +272,17 @@ if [ -f "$CADDYFILE" ]; then 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 +# --- Prüfe ob alte Dateien noch existieren --- +if [ -f "$SCRIPT_DIR/email_autodiscover" ]; then + echo "" + echo "⚠️ AUFRÄUMEN: Datei 'email_autodiscover' kann entfernt werden!" + echo " Das Snippet ist jetzt in mail_certs eingebettet." +fi + +if [ -f "$SCRIPT_DIR/email-setup/autodiscover.xml" ]; then + echo "" + echo "⚠️ AUFRÄUMEN: 'email-setup/autodiscover.xml' kann entfernt werden!" + echo " Statische XML wird nicht mehr benötigt (dynamisch über Caddy)." fi echo "" @@ -277,12 +298,18 @@ 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:" +echo "4. Autodiscover testen:" for domain in $DOMAINS; do - echo " curl -s https://autoconfig.${domain}/mail/config-v1.1.xml | head -5" + echo " # Thunderbird:" + echo " curl -s https://autoconfig.${domain}/mail/config-v1.1.xml | head -10" + echo " # Outlook:" + echo " curl -s https://autodiscover.${domain}/autodiscover/autodiscover.xml | head -10" + echo " # Apple (sollte .mobileconfig liefern):" + echo " curl -sI \"https://autodiscover.${domain}/apple?email=test@${domain}\"" + echo "" +done +echo "5. iPhone Email-Setup QR-Code Seite:" +for domain in $DOMAINS; do + echo " https://webmail.${domain}/email-setup" done echo "============================================================" \ No newline at end of file From 4caa51991f5545b05eb58ffc4c9dfb66cebc739b Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Thu, 12 Mar 2026 19:11:36 -0500 Subject: [PATCH 64/74] update for mobile --- caddy/docker-compose.yml | 6 +-- ...bileconfig.tpl => email.mobileconfig.html} | 53 +++++++++---------- caddy/update-caddy-certs.sh | 9 ++-- 3 files changed, 32 insertions(+), 36 deletions(-) rename caddy/{email.mobileconfig.tpl => email.mobileconfig.html} (63%) diff --git a/caddy/docker-compose.yml b/caddy/docker-compose.yml index e1f250b..88df53a 100644 --- a/caddy/docker-compose.yml +++ b/caddy/docker-compose.yml @@ -16,8 +16,8 @@ services: 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 + # email_autodiscover entfernt - Snippet ist jetzt in mail_certs eingebettet + - $PWD/email.mobileconfig.html:/etc/caddy/email.mobileconfig.html - $PWD/email-setup:/var/www/email-setup - caddy_data:/data - caddy_config:/config @@ -33,4 +33,4 @@ networks: volumes: caddy_data: external: true - caddy_config: + caddy_config: \ No newline at end of file diff --git a/caddy/email.mobileconfig.tpl b/caddy/email.mobileconfig.html similarity index 63% rename from caddy/email.mobileconfig.tpl rename to caddy/email.mobileconfig.html index 43944eb..9643e6e 100644 --- a/caddy/email.mobileconfig.tpl +++ b/caddy/email.mobileconfig.html @@ -2,75 +2,72 @@ {{/* ─────────────────────────────────────────────────── - Apple MobileConfig Template (Caddy templates) + Apple MobileConfig Template (Caddy templates + Sprig) - Dynamisch: Leitet die Basisdomain aus dem Host-Header ab. - Erwartet Aufruf auf autodiscover./apple?email=user@domain + Aufruf: https://autodiscover./apple?email=user@domain.com - Hostname-Logik: - Host = autodiscover.cielectrical.com - splitList "." .Req.Host → [autodiscover, cielectrical, com] - Basisdomain = cielectrical.com (Index 1+) - IMAP = imap.cielectrical.com - SMTP = smtp.cielectrical.com + Domain-Extraktion aus der E-Mail-Adresse: + email = sam@cielectrical.com + splitList "@" → ["sam", "cielectrical.com"] + last → cielectrical.com + → imap.cielectrical.com / smtp.cielectrical.com ─────────────────────────────────────────────────── */}} {{- $email := .Req.URL.Query.Get "email" -}} -{{- $hostParts := splitList "." .Req.Host -}} -{{- $baseDomain := join "." (slice $hostParts 1) -}} +{{- $domain := last (splitList "@" $email) -}} PayloadContent EmailAccountDescription - {{ $email }} + {{$email}} EmailAccountName - {{ $email }} + {{$email}} EmailAccountType EmailTypeIMAP EmailAddress - {{ $email }} + {{$email}} IncomingMailServerAuthentication EmailAuthPassword IncomingMailServerHostName - imap.{{ $baseDomain }} + imap.{{$domain}} IncomingMailServerPortNumber 993 IncomingMailServerUseSSL - > + IncomingMailServerUsername - {{ $email }} + {{$email}} OutgoingMailServerAuthentication EmailAuthPassword OutgoingMailServerHostName - smtp.{{ $baseDomain }} + smtp.{{$domain}} OutgoingMailServerPortNumber 465 OutgoingMailServerUseSSL OutgoingMailServerUsername - {{ $email }} + {{$email}} OutgoingPasswordRequired PayloadDescription - E-Mail Konfiguration für {{ $email }} + E-Mail Konfiguration PayloadDisplayName - {{ $baseDomain }} E-Mail + {{$domain}} E-Mail PayloadIdentifier - com.{{ $baseDomain }}.email.account + com.{{$domain}}.email.account PayloadType com.apple.mail.managed PayloadUUID - {{ uuidv4 }} + {{uuidv4}} PayloadVersion 1 PayloadDescription - Automatische E-Mail Einrichtung für {{ $email }} + E-Mail Einrichtung fuer {{$email}} PayloadDisplayName - {{ $baseDomain }} E-Mail Einstellungen + {{$domain}} E-Mail PayloadIdentifier - com.{{ $baseDomain }}.email.profile + com.{{$domain}}.email.profile PayloadOrganization Bay Area Affiliates, Inc. PayloadRemovalDisallowed @@ -78,8 +75,8 @@ PayloadType Configuration PayloadUUID - {{ uuidv4 }} + {{uuidv4}} PayloadVersion 1 - + \ No newline at end of file diff --git a/caddy/update-caddy-certs.sh b/caddy/update-caddy-certs.sh index 50343c7..cebd90b 100755 --- a/caddy/update-caddy-certs.sh +++ b/caddy/update-caddy-certs.sh @@ -151,14 +151,13 @@ OUTPUT="${OUTPUT}\n" # --- 4. Apple MobileConfig (Template) --- OUTPUT="${OUTPUT} # Apple MobileConfig (dynamisches Template)\n" +OUTPUT="${OUTPUT} # .tpl → .html damit file_server text/html liefert und templates rendert\n" OUTPUT="${OUTPUT} route /apple {\n" -OUTPUT="${OUTPUT} templates {\n" -OUTPUT="${OUTPUT} mime \"application/x-apple-aspen-config\"\n" -OUTPUT="${OUTPUT} }\n" +OUTPUT="${OUTPUT} root * /etc/caddy\n" +OUTPUT="${OUTPUT} rewrite * /email.mobileconfig.html\n" +OUTPUT="${OUTPUT} templates\n" OUTPUT="${OUTPUT} header Content-Type \"application/x-apple-aspen-config; charset=utf-8\"\n" OUTPUT="${OUTPUT} header Content-Disposition \"attachment; filename=email.mobileconfig\"\n" -OUTPUT="${OUTPUT} root * /etc/caddy\n" -OUTPUT="${OUTPUT} rewrite * /email.mobileconfig.tpl\n" OUTPUT="${OUTPUT} file_server\n" OUTPUT="${OUTPUT} }\n" From a11ed8c52603ce9d689de69c9d969b54cb787990 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Thu, 12 Mar 2026 19:27:07 -0500 Subject: [PATCH 65/74] fix --- caddy/docker-compose.yml | 2 +- caddy/email.mobileconfig.html | 82 ----------------------------------- caddy/update-caddy-certs.sh | 78 ++++++++++++++++++++++++++++++--- 3 files changed, 72 insertions(+), 90 deletions(-) delete mode 100644 caddy/email.mobileconfig.html diff --git a/caddy/docker-compose.yml b/caddy/docker-compose.yml index 88df53a..374deb9 100644 --- a/caddy/docker-compose.yml +++ b/caddy/docker-compose.yml @@ -17,7 +17,7 @@ services: - $PWD/Caddyfile:/etc/caddy/Caddyfile - $PWD/mail_certs:/etc/caddy/mail_certs # email_autodiscover entfernt - Snippet ist jetzt in mail_certs eingebettet - - $PWD/email.mobileconfig.html:/etc/caddy/email.mobileconfig.html + # email.mobileconfig.html entfernt - Inhalt ist jetzt inline in mail_certs - $PWD/email-setup:/var/www/email-setup - caddy_data:/data - caddy_config:/config diff --git a/caddy/email.mobileconfig.html b/caddy/email.mobileconfig.html deleted file mode 100644 index 9643e6e..0000000 --- a/caddy/email.mobileconfig.html +++ /dev/null @@ -1,82 +0,0 @@ - - - -{{/* ─────────────────────────────────────────────────── - Apple MobileConfig Template (Caddy templates + Sprig) - - Aufruf: https://autodiscover./apple?email=user@domain.com - - Domain-Extraktion aus der E-Mail-Adresse: - email = sam@cielectrical.com - splitList "@" → ["sam", "cielectrical.com"] - last → cielectrical.com - → imap.cielectrical.com / smtp.cielectrical.com - ─────────────────────────────────────────────────── */}} -{{- $email := .Req.URL.Query.Get "email" -}} -{{- $domain := last (splitList "@" $email) -}} - - PayloadContent - - - EmailAccountDescription - {{$email}} - EmailAccountName - {{$email}} - EmailAccountType - EmailTypeIMAP - EmailAddress - {{$email}} - IncomingMailServerAuthentication - EmailAuthPassword - IncomingMailServerHostName - imap.{{$domain}} - IncomingMailServerPortNumber - 993 - IncomingMailServerUseSSL - - IncomingMailServerUsername - {{$email}} - OutgoingMailServerAuthentication - EmailAuthPassword - OutgoingMailServerHostName - smtp.{{$domain}} - OutgoingMailServerPortNumber - 465 - OutgoingMailServerUseSSL - - OutgoingMailServerUsername - {{$email}} - OutgoingPasswordRequired - - PayloadDescription - E-Mail Konfiguration - PayloadDisplayName - {{$domain}} E-Mail - PayloadIdentifier - com.{{$domain}}.email.account - PayloadType - com.apple.mail.managed - PayloadUUID - {{uuidv4}} - PayloadVersion - 1 - - - PayloadDescription - E-Mail Einrichtung fuer {{$email}} - PayloadDisplayName - {{$domain}} E-Mail - PayloadIdentifier - com.{{$domain}}.email.profile - PayloadOrganization - Bay Area Affiliates, Inc. - PayloadRemovalDisallowed - - PayloadType - Configuration - PayloadUUID - {{uuidv4}} - PayloadVersion - 1 - - \ No newline at end of file diff --git a/caddy/update-caddy-certs.sh b/caddy/update-caddy-certs.sh index cebd90b..7196e42 100755 --- a/caddy/update-caddy-certs.sh +++ b/caddy/update-caddy-certs.sh @@ -149,16 +149,80 @@ OUTPUT="${OUTPUT}\` 200\n" OUTPUT="${OUTPUT} }\n" OUTPUT="${OUTPUT}\n" -# --- 4. Apple MobileConfig (Template) --- -OUTPUT="${OUTPUT} # Apple MobileConfig (dynamisches Template)\n" -OUTPUT="${OUTPUT} # .tpl → .html damit file_server text/html liefert und templates rendert\n" +# --- 4. Apple MobileConfig (inline, wie Autodiscover/Autoconfig) --- +OUTPUT="${OUTPUT} # Apple MobileConfig (inline respond)\n" OUTPUT="${OUTPUT} route /apple {\n" -OUTPUT="${OUTPUT} root * /etc/caddy\n" -OUTPUT="${OUTPUT} rewrite * /email.mobileconfig.html\n" -OUTPUT="${OUTPUT} templates\n" OUTPUT="${OUTPUT} header Content-Type \"application/x-apple-aspen-config; charset=utf-8\"\n" OUTPUT="${OUTPUT} header Content-Disposition \"attachment; filename=email.mobileconfig\"\n" -OUTPUT="${OUTPUT} file_server\n" +OUTPUT="${OUTPUT} respond \`\n" +OUTPUT="${OUTPUT}\n" +OUTPUT="${OUTPUT}\n" +OUTPUT="${OUTPUT}\n" +OUTPUT="${OUTPUT} PayloadContent\n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT} EmailAccountDescription\n" +OUTPUT="${OUTPUT} {query.email}\n" +OUTPUT="${OUTPUT} EmailAccountName\n" +OUTPUT="${OUTPUT} {query.email}\n" +OUTPUT="${OUTPUT} EmailAccountType\n" +OUTPUT="${OUTPUT} EmailTypeIMAP\n" +OUTPUT="${OUTPUT} EmailAddress\n" +OUTPUT="${OUTPUT} {query.email}\n" +OUTPUT="${OUTPUT} IncomingMailServerAuthentication\n" +OUTPUT="${OUTPUT} EmailAuthPassword\n" +OUTPUT="${OUTPUT} IncomingMailServerHostName\n" +OUTPUT="${OUTPUT} imap.{labels.1}.{labels.0}\n" +OUTPUT="${OUTPUT} IncomingMailServerPortNumber\n" +OUTPUT="${OUTPUT} 993\n" +OUTPUT="${OUTPUT} IncomingMailServerUseSSL\n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT} IncomingMailServerUsername\n" +OUTPUT="${OUTPUT} {query.email}\n" +OUTPUT="${OUTPUT} OutgoingMailServerAuthentication\n" +OUTPUT="${OUTPUT} EmailAuthPassword\n" +OUTPUT="${OUTPUT} OutgoingMailServerHostName\n" +OUTPUT="${OUTPUT} smtp.{labels.1}.{labels.0}\n" +OUTPUT="${OUTPUT} OutgoingMailServerPortNumber\n" +OUTPUT="${OUTPUT} 465\n" +OUTPUT="${OUTPUT} OutgoingMailServerUseSSL\n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT} OutgoingMailServerUsername\n" +OUTPUT="${OUTPUT} {query.email}\n" +OUTPUT="${OUTPUT} OutgoingPasswordRequired\n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT} PayloadDescription\n" +OUTPUT="${OUTPUT} E-Mail Konfiguration\n" +OUTPUT="${OUTPUT} PayloadDisplayName\n" +OUTPUT="${OUTPUT} {labels.1}.{labels.0} E-Mail\n" +OUTPUT="${OUTPUT} PayloadIdentifier\n" +OUTPUT="${OUTPUT} com.{labels.1}.{labels.0}.email.account\n" +OUTPUT="${OUTPUT} PayloadType\n" +OUTPUT="${OUTPUT} com.apple.mail.managed\n" +OUTPUT="${OUTPUT} PayloadUUID\n" +OUTPUT="${OUTPUT} A1B2C3D4-E5F6-7890-ABCD-EF1234567890\n" +OUTPUT="${OUTPUT} PayloadVersion\n" +OUTPUT="${OUTPUT} 1\n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT} PayloadDescription\n" +OUTPUT="${OUTPUT} E-Mail Einrichtung\n" +OUTPUT="${OUTPUT} PayloadDisplayName\n" +OUTPUT="${OUTPUT} {labels.1}.{labels.0} E-Mail\n" +OUTPUT="${OUTPUT} PayloadIdentifier\n" +OUTPUT="${OUTPUT} com.{labels.1}.{labels.0}.email.profile\n" +OUTPUT="${OUTPUT} PayloadOrganization\n" +OUTPUT="${OUTPUT} Bay Area Affiliates, Inc.\n" +OUTPUT="${OUTPUT} PayloadRemovalDisallowed\n" +OUTPUT="${OUTPUT} \n" +OUTPUT="${OUTPUT} PayloadType\n" +OUTPUT="${OUTPUT} Configuration\n" +OUTPUT="${OUTPUT} PayloadUUID\n" +OUTPUT="${OUTPUT} F0E1D2C3-B4A5-6789-0FED-CBA987654321\n" +OUTPUT="${OUTPUT} PayloadVersion\n" +OUTPUT="${OUTPUT} 1\n" +OUTPUT="${OUTPUT}\n" +OUTPUT="${OUTPUT}\` 200\n" OUTPUT="${OUTPUT} }\n" # --- 5. Samsung Email (nutzt ebenfalls autoconfig, kein extra Block nötig) --- From 2192f146ea575226f65d6d28c630c8fcdb96bb6a Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Fri, 13 Mar 2026 17:03:17 -0500 Subject: [PATCH 66/74] remove SRV Records --- basic_setup/cloudflareMigrationDns.sh | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/basic_setup/cloudflareMigrationDns.sh b/basic_setup/cloudflareMigrationDns.sh index 6fe0381..a5fb926 100755 --- a/basic_setup/cloudflareMigrationDns.sh +++ b/basic_setup/cloudflareMigrationDns.sh @@ -296,19 +296,6 @@ echo "--- 9. Autodiscover / Autoconfig ---" ensure_record "CNAME" "autodiscover.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false ensure_record "CNAME" "autoconfig.$DOMAIN_NAME" "mail.$DOMAIN_NAME" false -# ------------------------------------------------------------------ -# SCHRITT 10: SRV Records -# ------------------------------------------------------------------ -echo "" -echo "--- 10. SRV Records ---" -if [ "$SKIP_CLIENT_DNS" = "true" ]; then - echo " ⏭️ Übersprungen (SKIP_CLIENT_DNS=true)" -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 für Domain: $DOMAIN_NAME" From 369be75066c73c655730367bfad8b5cc1530b889 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Fri, 13 Mar 2026 17:52:54 -0500 Subject: [PATCH 67/74] fix --- basic_setup/cloudflareMigrationDns.sh | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/basic_setup/cloudflareMigrationDns.sh b/basic_setup/cloudflareMigrationDns.sh index a5fb926..2920d7c 100755 --- a/basic_setup/cloudflareMigrationDns.sh +++ b/basic_setup/cloudflareMigrationDns.sh @@ -81,8 +81,23 @@ ensure_record() { local search_res=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=$type&name=$name" \ -H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json") - local rec_id=$(echo "$search_res" | jq -r '.result[0].id') - local rec_content=$(echo "$search_res" | jq -r '.result[0].content') + local rec_id + local rec_content + + if [ "$type" == "TXT" ] && [ "$name" == "$DOMAIN_NAME" ] && [[ "$content" == v=spf1* ]]; then + # Spezialfall Root-Domain SPF: Filtere gezielt den SPF-Eintrag heraus, + # damit z.B. Google Site Verification nicht überschrieben wird. + rec_id=$(echo "$search_res" | jq -r '.result[] | select(.content | contains("v=spf1")) | .id' | head -n 1) + rec_content=$(echo "$search_res" | jq -r '.result[] | select(.content | contains("v=spf1")) | .content' | head -n 1) + else + # Standardverhalten für alle anderen (A, CNAME, MX, etc.) + rec_id=$(echo "$search_res" | jq -r '.result[0].id') + rec_content=$(echo "$search_res" | jq -r '.result[0].content') + fi + + # Fallback für jq, damit das restliche Skript funktioniert + [ -z "$rec_id" ] && rec_id="null" + [ -z "$rec_content" ] && rec_content="null" if [ "$type" == "MX" ]; then json_data=$(jq -n --arg t "$type" --arg n "$name" --arg c "$content" --argjson p "$proxied" --argjson prio "$priority" \ From 6016fbe13da8fbfaf960b72d3b3b93a06350d05f Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Fri, 13 Mar 2026 20:11:35 -0500 Subject: [PATCH 68/74] remove version --- email-worker-nodejs/docker-compose.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/email-worker-nodejs/docker-compose.yml b/email-worker-nodejs/docker-compose.yml index 3fff942..5289f4f 100644 --- a/email-worker-nodejs/docker-compose.yml +++ b/email-worker-nodejs/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.8" - services: email-worker: build: . From 688d49e21868977568fd0186a37d5fbde8f8c287 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Fri, 13 Mar 2026 20:12:52 -0500 Subject: [PATCH 69/74] remove python worker --- email-worker/.dockerignore | 38 -- email-worker/.env.example | 42 -- email-worker/.gitignore | 35 -- email-worker/Dockerfile | 37 -- email-worker/Makefile | 50 --- email-worker/aws/__init__.py | 11 - email-worker/aws/dynamodb_handler.py | 192 --------- email-worker/aws/s3_handler.py | 193 --------- email-worker/aws/ses_handler.py | 53 --- email-worker/aws/sqs_handler.py | 103 ----- email-worker/config.py | 100 ----- email-worker/docker-compose.yml | 85 ---- email-worker/docs/ARCHITECTURE.md | 381 ------------------ email-worker/docs/CHANGELOG.md | 37 -- email-worker/docs/COMPATIBILITY.md | 311 -------------- email-worker/docs/MIGRATION.md | 366 ----------------- email-worker/docs/QUICKSTART.md | 330 --------------- email-worker/docs/README.md | 306 -------------- email-worker/docs/SUMMARY.md | 247 ------------ email-worker/domain_poller.py | 109 ----- email-worker/domains.txt | 6 - email-worker/email_processing/__init__.py | 11 - email-worker/email_processing/blocklist.py | 100 ----- .../email_processing/bounce_handler.py | 99 ----- email-worker/email_processing/parser.py | 80 ---- .../email_processing/rules_processor.py | 365 ----------------- email-worker/health_server.py | 85 ---- email-worker/logger.py | 79 ---- email-worker/main.py | 50 --- email-worker/metrics/__init__.py | 8 - email-worker/metrics/prometheus.py | 142 ------- email-worker/requirements.txt | 2 - email-worker/smtp/__init__.py | 8 - email-worker/smtp/delivery.py | 187 --------- email-worker/smtp/pool.py | 113 ------ email-worker/unified_worker.py | 201 --------- email-worker/worker.py | 352 ---------------- 37 files changed, 4914 deletions(-) delete mode 100644 email-worker/.dockerignore delete mode 100644 email-worker/.env.example delete mode 100644 email-worker/.gitignore delete mode 100644 email-worker/Dockerfile delete mode 100644 email-worker/Makefile delete mode 100644 email-worker/aws/__init__.py delete mode 100644 email-worker/aws/dynamodb_handler.py delete mode 100644 email-worker/aws/s3_handler.py delete mode 100644 email-worker/aws/ses_handler.py delete mode 100644 email-worker/aws/sqs_handler.py delete mode 100644 email-worker/config.py delete mode 100644 email-worker/docker-compose.yml delete mode 100644 email-worker/docs/ARCHITECTURE.md delete mode 100644 email-worker/docs/CHANGELOG.md delete mode 100644 email-worker/docs/COMPATIBILITY.md delete mode 100644 email-worker/docs/MIGRATION.md delete mode 100644 email-worker/docs/QUICKSTART.md delete mode 100644 email-worker/docs/README.md delete mode 100644 email-worker/docs/SUMMARY.md delete mode 100644 email-worker/domain_poller.py delete mode 100644 email-worker/domains.txt delete mode 100644 email-worker/email_processing/__init__.py delete mode 100644 email-worker/email_processing/blocklist.py delete mode 100644 email-worker/email_processing/bounce_handler.py delete mode 100644 email-worker/email_processing/parser.py delete mode 100644 email-worker/email_processing/rules_processor.py delete mode 100644 email-worker/health_server.py delete mode 100644 email-worker/logger.py delete mode 100644 email-worker/main.py delete mode 100644 email-worker/metrics/__init__.py delete mode 100644 email-worker/metrics/prometheus.py delete mode 100644 email-worker/requirements.txt delete mode 100644 email-worker/smtp/__init__.py delete mode 100644 email-worker/smtp/delivery.py delete mode 100644 email-worker/smtp/pool.py delete mode 100644 email-worker/unified_worker.py delete mode 100644 email-worker/worker.py diff --git a/email-worker/.dockerignore b/email-worker/.dockerignore deleted file mode 100644 index 1b401b8..0000000 --- a/email-worker/.dockerignore +++ /dev/null @@ -1,38 +0,0 @@ -# Documentation -*.md -!README.md - -# Git -.git -.gitignore - -# Python -__pycache__ -*.pyc -*.pyo -*.pyd -.Python -*.so - -# Logs -logs/ -*.log - -# Environment -.env -.env.example - -# IDE -.vscode/ -.idea/ -*.swp -*.swo - -# OS -.DS_Store -Thumbs.db - -# Build -*.tar.gz -dist/ -build/ diff --git a/email-worker/.env.example b/email-worker/.env.example deleted file mode 100644 index 5c36780..0000000 --- a/email-worker/.env.example +++ /dev/null @@ -1,42 +0,0 @@ -# AWS Credentials -AWS_REGION=us-east-2 -AWS_ACCESS_KEY_ID=your_access_key_here -AWS_SECRET_ACCESS_KEY=your_secret_key_here - -# Domains Configuration -DOMAINS=example.com,another.com -# Alternative: Use domains file -# DOMAINS_FILE=/etc/email-worker/domains.txt - -# Worker Settings -WORKER_THREADS=10 -POLL_INTERVAL=20 -MAX_MESSAGES=10 -VISIBILITY_TIMEOUT=300 - -# SMTP Configuration -SMTP_HOST=localhost -SMTP_PORT=25 -SMTP_USE_TLS=false -SMTP_USER= -SMTP_PASS= -SMTP_POOL_SIZE=5 -INTERNAL_SMTP_PORT=2525 - -# LMTP Configuration (Optional) -LMTP_ENABLED=false -LMTP_HOST=localhost -LMTP_PORT=24 - -# DynamoDB Tables -DYNAMODB_RULES_TABLE=email-rules -DYNAMODB_MESSAGES_TABLE=ses-outbound-messages -DYNAMODB_BLOCKED_TABLE=email-blocked-senders - -# Bounce Handling -BOUNCE_LOOKUP_RETRIES=3 -BOUNCE_LOOKUP_DELAY=1.0 - -# Monitoring Ports -METRICS_PORT=8000 -HEALTH_PORT=8080 diff --git a/email-worker/.gitignore b/email-worker/.gitignore deleted file mode 100644 index 6e9d2ba..0000000 --- a/email-worker/.gitignore +++ /dev/null @@ -1,35 +0,0 @@ -# Python -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -env/ -venv/ -ENV/ -.venv - -# Logs -*.log - -# Environment -.env - -# IDE -.vscode/ -.idea/ -*.swp -*.swo - -# OS -.DS_Store -Thumbs.db - -# Build -dist/ -build/ -*.egg-info/ - -# Archives -*.tar.gz -*.zip diff --git a/email-worker/Dockerfile b/email-worker/Dockerfile deleted file mode 100644 index 7660a2c..0000000 --- a/email-worker/Dockerfile +++ /dev/null @@ -1,37 +0,0 @@ -FROM python:3.11-slim - -LABEL maintainer="andreas@knuth.dev" -LABEL description="Unified multi-domain email worker (modular version)" - -# System packages -RUN apt-get update && apt-get install -y --no-install-recommends \ - curl \ - && rm -rf /var/lib/apt/lists/* - -# Non-root user -RUN useradd -m -u 1000 worker && \ - mkdir -p /app /var/log/email-worker /etc/email-worker && \ - chown -R worker:worker /app /var/log/email-worker /etc/email-worker - -# Python dependencies -COPY requirements.txt /app/ -RUN pip install --no-cache-dir -r /app/requirements.txt - -# Worker code (all modules) -COPY --chown=worker:worker aws/ /app/aws/ -COPY --chown=worker:worker email_processing/ /app/email_processing/ -COPY --chown=worker:worker smtp/ /app/smtp/ -COPY --chown=worker:worker metrics/ /app/metrics/ -COPY --chown=worker:worker *.py /app/ - -WORKDIR /app -USER worker - -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ - CMD curl -f http://localhost:8080/health || exit 1 - -# Unbuffered output -ENV PYTHONUNBUFFERED=1 - -CMD ["python3", "main.py"] diff --git a/email-worker/Makefile b/email-worker/Makefile deleted file mode 100644 index b088779..0000000 --- a/email-worker/Makefile +++ /dev/null @@ -1,50 +0,0 @@ -.PHONY: help install run test lint clean docker-build docker-run docker-stop docker-logs - -help: - @echo "Available commands:" - @echo " make install - Install dependencies" - @echo " make run - Run worker locally" - @echo " make test - Run tests (TODO)" - @echo " make lint - Run linting" - @echo " make clean - Clean up files" - @echo " make docker-build - Build Docker image" - @echo " make docker-run - Run with docker-compose" - @echo " make docker-stop - Stop docker-compose" - @echo " make docker-logs - Show docker logs" - -install: - pip install -r requirements.txt - -run: - python3 main.py - -test: - @echo "TODO: Add tests" - # python3 -m pytest tests/ - -lint: - @echo "Running pylint..." - -pylint --rcfile=.pylintrc *.py **/*.py 2>/dev/null || echo "pylint not installed" - @echo "Running flake8..." - -flake8 --max-line-length=120 . 2>/dev/null || echo "flake8 not installed" - -clean: - find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true - find . -type f -name "*.pyc" -delete - find . -type f -name "*.pyo" -delete - find . -type f -name "*.log" -delete - -docker-build: - docker build -t unified-email-worker:latest . - -docker-run: - docker-compose up -d - -docker-stop: - docker-compose down - -docker-logs: - docker-compose logs -f email-worker - -docker-restart: docker-stop docker-build docker-run - @echo "Worker restarted" diff --git a/email-worker/aws/__init__.py b/email-worker/aws/__init__.py deleted file mode 100644 index cb80192..0000000 --- a/email-worker/aws/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python3 -""" -AWS service handlers -""" - -from .s3_handler import S3Handler -from .sqs_handler import SQSHandler -from .ses_handler import SESHandler -from .dynamodb_handler import DynamoDBHandler - -__all__ = ['S3Handler', 'SQSHandler', 'SESHandler', 'DynamoDBHandler'] diff --git a/email-worker/aws/dynamodb_handler.py b/email-worker/aws/dynamodb_handler.py deleted file mode 100644 index 436eae1..0000000 --- a/email-worker/aws/dynamodb_handler.py +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/env python3 -""" -DynamoDB operations handler -""" - -import time -from typing import Optional, Dict, Any, List -import boto3 -from botocore.exceptions import ClientError - -from logger import log -from config import config - - -class DynamoDBHandler: - """Handles all DynamoDB operations""" - - def __init__(self): - self.resource = boto3.resource('dynamodb', region_name=config.aws_region) - self.available = False - self.rules_table = None - self.messages_table = None - self.blocked_table = None - - self._initialize_tables() - - def _initialize_tables(self): - """Initialize DynamoDB table connections""" - try: - self.rules_table = self.resource.Table(config.rules_table) - self.messages_table = self.resource.Table(config.messages_table) - self.blocked_table = self.resource.Table(config.blocked_table) - - # Test connection - self.rules_table.table_status - self.messages_table.table_status - self.blocked_table.table_status - - self.available = True - log("✓ DynamoDB tables connected successfully") - - except Exception as e: - log(f"⚠ DynamoDB not fully available: {e}", 'WARNING') - self.available = False - - def get_email_rules(self, email_address: str) -> Optional[Dict[str, Any]]: - """ - Get email rules for recipient (OOO, Forwarding) - - Args: - email_address: Recipient email address - - Returns: - Rule dictionary or None if not found - """ - if not self.available or not self.rules_table: - return None - - try: - response = self.rules_table.get_item(Key={'email_address': email_address}) - return response.get('Item') - - except ClientError as e: - if e.response['Error']['Code'] != 'ResourceNotFoundException': - log(f"⚠ DynamoDB error for {email_address}: {e}", 'ERROR') - return None - - except Exception as e: - log(f"⚠ DynamoDB error for {email_address}: {e}", 'WARNING') - return None - - def get_bounce_info(self, message_id: str, worker_name: str = 'unified') -> Optional[Dict]: - """ - Get bounce information from DynamoDB with retry logic - - Args: - message_id: SES Message ID - worker_name: Worker name for logging - - Returns: - Bounce info dictionary or None - """ - if not self.available or not self.messages_table: - return None - - for attempt in range(config.bounce_lookup_retries): - try: - response = self.messages_table.get_item(Key={'MessageId': message_id}) - item = response.get('Item') - - if item: - return { - 'original_source': item.get('original_source', ''), - 'bounceType': item.get('bounceType', 'Unknown'), - 'bounceSubType': item.get('bounceSubType', 'Unknown'), - 'bouncedRecipients': item.get('bouncedRecipients', []), - 'timestamp': item.get('timestamp', '') - } - - if attempt < config.bounce_lookup_retries - 1: - log( - f" Bounce record not found yet, retrying in {config.bounce_lookup_delay}s " - f"(attempt {attempt + 1}/{config.bounce_lookup_retries})...", - 'INFO', - worker_name - ) - time.sleep(config.bounce_lookup_delay) - else: - log( - f"⚠ No bounce record found after {config.bounce_lookup_retries} attempts " - f"for Message-ID: {message_id}", - 'WARNING', - worker_name - ) - return None - - except Exception as e: - log( - f"⚠ DynamoDB Error (attempt {attempt + 1}/{config.bounce_lookup_retries}): {e}", - 'ERROR', - worker_name - ) - if attempt < config.bounce_lookup_retries - 1: - time.sleep(config.bounce_lookup_delay) - else: - return None - - return None - - def get_blocked_patterns(self, email_address: str) -> List[str]: - """ - Get blocked sender patterns for recipient - - Args: - email_address: Recipient email address - - Returns: - List of blocked patterns (may include wildcards) - """ - if not self.available or not self.blocked_table: - return [] - - try: - response = self.blocked_table.get_item(Key={'email_address': email_address}) - item = response.get('Item', {}) - return item.get('blocked_patterns', []) - - except Exception as e: - log(f"⚠ Error getting block list for {email_address}: {e}", 'ERROR') - return [] - - def batch_get_blocked_patterns(self, email_addresses: List[str]) -> Dict[str, List[str]]: - """ - Batch get blocked patterns for multiple recipients (more efficient) - - Args: - email_addresses: List of recipient email addresses - - Returns: - Dictionary mapping email_address -> list of blocked patterns - """ - if not self.available or not self.blocked_table: - return {addr: [] for addr in email_addresses} - - try: - # DynamoDB BatchGetItem - keys = [{'email_address': addr} for addr in email_addresses] - response = self.resource.batch_get_item( - RequestItems={ - config.blocked_table: {'Keys': keys} - } - ) - - items = response.get('Responses', {}).get(config.blocked_table, []) - - # Build result dictionary - result = {} - for email_address in email_addresses: - matching_item = next( - (item for item in items if item['email_address'] == email_address), - None - ) - if matching_item: - result[email_address] = matching_item.get('blocked_patterns', []) - else: - result[email_address] = [] - - return result - - except Exception as e: - log(f"⚠ Batch blocklist check error: {e}", 'ERROR') - return {addr: [] for addr in email_addresses} diff --git a/email-worker/aws/s3_handler.py b/email-worker/aws/s3_handler.py deleted file mode 100644 index d0ed849..0000000 --- a/email-worker/aws/s3_handler.py +++ /dev/null @@ -1,193 +0,0 @@ -#!/usr/bin/env python3 -""" -S3 operations handler -""" - -import time -from typing import Optional, List -import boto3 -from botocore.exceptions import ClientError - -from logger import log -from config import config, domain_to_bucket_name - - -class S3Handler: - """Handles all S3 operations""" - - def __init__(self): - self.client = boto3.client('s3', region_name=config.aws_region) - - def get_email(self, domain: str, message_id: str, receive_count: int) -> Optional[bytes]: - """ - Download email from S3 - - Args: - domain: Email domain - message_id: SES Message ID - receive_count: Number of times this message was received from queue - - Returns: - Raw email bytes or None if not found/error - """ - bucket = domain_to_bucket_name(domain) - - try: - response = self.client.get_object(Bucket=bucket, Key=message_id) - return response['Body'].read() - - except self.client.exceptions.NoSuchKey: - if receive_count < 5: - log(f"⏳ S3 Object not found yet (Attempt {receive_count}). Retrying...", 'WARNING') - return None - else: - log(f"❌ S3 Object missing permanently after retries.", 'ERROR') - raise - - except ClientError as e: - if e.response['Error']['Code'] == 'NoSuchKey': - if receive_count < 5: - log(f"⏳ S3 Object not found yet (Attempt {receive_count}). Retrying...", 'WARNING') - return None - else: - log(f"❌ S3 Object missing permanently after retries.", 'ERROR') - raise - log(f"❌ S3 Download Error: {e}", 'ERROR') - raise - - except Exception as e: - log(f"❌ S3 Download Error: {e}", 'ERROR') - raise - - def mark_as_processed( - self, - domain: str, - message_id: str, - worker_name: str, - invalid_inboxes: Optional[List[str]] = None - ): - """Mark email as successfully delivered""" - bucket = domain_to_bucket_name(domain) - - try: - head = self.client.head_object(Bucket=bucket, Key=message_id) - metadata = head.get('Metadata', {}) or {} - - metadata['processed'] = 'true' - metadata['processed_at'] = str(int(time.time())) - metadata['processed_by'] = worker_name - metadata['status'] = 'delivered' - metadata.pop('processing_started', None) - metadata.pop('queued_at', None) - - if invalid_inboxes: - metadata['invalid_inboxes'] = ','.join(invalid_inboxes) - log(f"⚠ Invalid inboxes recorded: {', '.join(invalid_inboxes)}", 'WARNING', worker_name) - - self.client.copy_object( - Bucket=bucket, - Key=message_id, - CopySource={'Bucket': bucket, 'Key': message_id}, - Metadata=metadata, - MetadataDirective='REPLACE' - ) - - except Exception as e: - log(f"Failed to mark as processed: {e}", 'WARNING', worker_name) - - def mark_as_all_invalid( - self, - domain: str, - message_id: str, - invalid_inboxes: List[str], - worker_name: str - ): - """Mark email as failed because all recipients are invalid""" - bucket = domain_to_bucket_name(domain) - - try: - head = self.client.head_object(Bucket=bucket, Key=message_id) - metadata = head.get('Metadata', {}) or {} - - metadata['processed'] = 'true' - metadata['processed_at'] = str(int(time.time())) - metadata['processed_by'] = worker_name - metadata['status'] = 'failed' - metadata['error'] = 'All recipients are invalid (mailboxes do not exist)' - metadata['invalid_inboxes'] = ','.join(invalid_inboxes) - metadata.pop('processing_started', None) - metadata.pop('queued_at', None) - - self.client.copy_object( - Bucket=bucket, - Key=message_id, - CopySource={'Bucket': bucket, 'Key': message_id}, - Metadata=metadata, - MetadataDirective='REPLACE' - ) - - except Exception as e: - log(f"Failed to mark as all invalid: {e}", 'WARNING', worker_name) - - def mark_as_blocked( - self, - domain: str, - message_id: str, - blocked_recipients: List[str], - sender: str, - worker_name: str - ): - """ - Mark email as blocked by sender blacklist - - This sets metadata BEFORE deletion for audit trail - """ - bucket = domain_to_bucket_name(domain) - - try: - head = self.client.head_object(Bucket=bucket, Key=message_id) - metadata = head.get('Metadata', {}) or {} - - metadata['processed'] = 'true' - metadata['processed_at'] = str(int(time.time())) - metadata['processed_by'] = worker_name - metadata['status'] = 'blocked' - metadata['blocked_recipients'] = ','.join(blocked_recipients) - metadata['blocked_sender'] = sender - metadata.pop('processing_started', None) - metadata.pop('queued_at', None) - - self.client.copy_object( - Bucket=bucket, - Key=message_id, - CopySource={'Bucket': bucket, 'Key': message_id}, - Metadata=metadata, - MetadataDirective='REPLACE' - ) - - log(f"✓ Marked as blocked in S3 metadata", 'INFO', worker_name) - - except Exception as e: - log(f"⚠ Failed to mark as blocked: {e}", 'ERROR', worker_name) - raise - - def delete_blocked_email( - self, - domain: str, - message_id: str, - worker_name: str - ): - """ - Delete email after marking as blocked - - Only call this after mark_as_blocked() succeeded - """ - bucket = domain_to_bucket_name(domain) - - try: - self.client.delete_object(Bucket=bucket, Key=message_id) - log(f"🗑 Deleted blocked email from S3", 'SUCCESS', worker_name) - - except Exception as e: - log(f"⚠ Failed to delete blocked email: {e}", 'ERROR', worker_name) - raise diff --git a/email-worker/aws/ses_handler.py b/email-worker/aws/ses_handler.py deleted file mode 100644 index 8a249bf..0000000 --- a/email-worker/aws/ses_handler.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python3 -""" -SES operations handler -""" - -import boto3 -from botocore.exceptions import ClientError - -from logger import log -from config import config - - -class SESHandler: - """Handles all SES operations""" - - def __init__(self): - self.client = boto3.client('ses', region_name=config.aws_region) - - def send_raw_email( - self, - source: str, - destination: str, - raw_message: bytes, - worker_name: str - ) -> bool: - """ - Send raw email via SES - - Args: - source: From address - destination: To address - raw_message: Raw MIME message bytes - worker_name: Worker name for logging - - Returns: - True if sent successfully, False otherwise - """ - try: - self.client.send_raw_email( - Source=source, - Destinations=[destination], - RawMessage={'Data': raw_message} - ) - return True - - except ClientError as e: - error_code = e.response['Error']['Code'] - log(f"⚠ SES send failed to {destination} ({error_code}): {e}", 'ERROR', worker_name) - return False - - except Exception as e: - log(f"⚠ SES send failed to {destination}: {e}", 'ERROR', worker_name) - return False diff --git a/email-worker/aws/sqs_handler.py b/email-worker/aws/sqs_handler.py deleted file mode 100644 index 020e268..0000000 --- a/email-worker/aws/sqs_handler.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python3 -""" -SQS operations handler -""" - -from typing import Optional, List, Dict, Any -import boto3 -from botocore.exceptions import ClientError - -from logger import log -from config import config, domain_to_queue_name - - -class SQSHandler: - """Handles all SQS operations""" - - def __init__(self): - self.client = boto3.client('sqs', region_name=config.aws_region) - - def get_queue_url(self, domain: str) -> Optional[str]: - """ - Get SQS queue URL for domain - - Args: - domain: Email domain - - Returns: - Queue URL or None if not found - """ - queue_name = domain_to_queue_name(domain) - - try: - response = self.client.get_queue_url(QueueName=queue_name) - return response['QueueUrl'] - - except ClientError as e: - if e.response['Error']['Code'] == 'AWS.SimpleQueueService.NonExistentQueue': - log(f"Queue not found for domain: {domain}", 'WARNING') - else: - log(f"Error getting queue URL for {domain}: {e}", 'ERROR') - return None - - def receive_messages(self, queue_url: str) -> List[Dict[str, Any]]: - """ - Receive messages from queue - - Args: - queue_url: SQS Queue URL - - Returns: - List of message dictionaries - """ - try: - response = self.client.receive_message( - QueueUrl=queue_url, - MaxNumberOfMessages=config.max_messages, - WaitTimeSeconds=config.poll_interval, - VisibilityTimeout=config.visibility_timeout, - AttributeNames=['ApproximateReceiveCount', 'SentTimestamp'] - ) - - return response.get('Messages', []) - - except Exception as e: - log(f"Error receiving messages: {e}", 'ERROR') - return [] - - def delete_message(self, queue_url: str, receipt_handle: str): - """ - Delete message from queue - - Args: - queue_url: SQS Queue URL - receipt_handle: Message receipt handle - """ - try: - self.client.delete_message( - QueueUrl=queue_url, - ReceiptHandle=receipt_handle - ) - except Exception as e: - log(f"Error deleting message: {e}", 'ERROR') - raise - - def get_queue_size(self, queue_url: str) -> int: - """ - Get approximate number of messages in queue - - Args: - queue_url: SQS Queue URL - - Returns: - Number of messages (0 if error) - """ - try: - attrs = self.client.get_queue_attributes( - QueueUrl=queue_url, - AttributeNames=['ApproximateNumberOfMessages'] - ) - return int(attrs['Attributes'].get('ApproximateNumberOfMessages', 0)) - - except Exception: - return 0 diff --git a/email-worker/config.py b/email-worker/config.py deleted file mode 100644 index bcc78a5..0000000 --- a/email-worker/config.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python3 -""" -Configuration management for unified email worker -""" - -import os -from dataclasses import dataclass -from typing import Set - - -@dataclass -class Config: - """Worker Configuration""" - # AWS - aws_region: str = os.environ.get('AWS_REGION', 'us-east-2') - - # Domains to process - domains_list: str = os.environ.get('DOMAINS', '') - domains_file: str = os.environ.get('DOMAINS_FILE', '/etc/email-worker/domains.txt') - - # Worker Settings - worker_threads: int = int(os.environ.get('WORKER_THREADS', '10')) - poll_interval: int = int(os.environ.get('POLL_INTERVAL', '20')) - max_messages: int = int(os.environ.get('MAX_MESSAGES', '10')) - visibility_timeout: int = int(os.environ.get('VISIBILITY_TIMEOUT', '300')) - - # SMTP for delivery - smtp_host: str = os.environ.get('SMTP_HOST', 'localhost') - smtp_port: int = int(os.environ.get('SMTP_PORT', '25')) - smtp_use_tls: bool = os.environ.get('SMTP_USE_TLS', 'false').lower() == 'true' - smtp_user: str = os.environ.get('SMTP_USER', '') - smtp_pass: str = os.environ.get('SMTP_PASS', '') - smtp_pool_size: int = int(os.environ.get('SMTP_POOL_SIZE', '5')) - - # Internal SMTP (bypasses transport_maps) - internal_smtp_port: int = int(os.environ.get('INTERNAL_SMTP_PORT', '2525')) - - # LMTP for local delivery (bypasses Postfix transport_maps) - lmtp_enabled: bool = os.environ.get('LMTP_ENABLED', 'false').lower() == 'true' - lmtp_host: str = os.environ.get('LMTP_HOST', 'localhost') - lmtp_port: int = int(os.environ.get('LMTP_PORT', '24')) - - # DynamoDB Tables - rules_table: str = os.environ.get('DYNAMODB_RULES_TABLE', 'email-rules') - messages_table: str = os.environ.get('DYNAMODB_MESSAGES_TABLE', 'ses-outbound-messages') - blocked_table: str = os.environ.get('DYNAMODB_BLOCKED_TABLE', 'email-blocked-senders') - - # Bounce Handling - bounce_lookup_retries: int = int(os.environ.get('BOUNCE_LOOKUP_RETRIES', '3')) - bounce_lookup_delay: float = float(os.environ.get('BOUNCE_LOOKUP_DELAY', '1.0')) - - # Monitoring - metrics_port: int = int(os.environ.get('METRICS_PORT', '8000')) - health_port: int = int(os.environ.get('HEALTH_PORT', '8080')) - - -# Global configuration instance -config = Config() - -# Global set of managed domains (populated at startup) -MANAGED_DOMAINS: Set[str] = set() - - -def load_domains() -> list[str]: - """Load domains from config and populate MANAGED_DOMAINS global""" - global MANAGED_DOMAINS - domains = [] - - if config.domains_list: - domains.extend([d.strip() for d in config.domains_list.split(',') if d.strip()]) - - if os.path.exists(config.domains_file): - with open(config.domains_file, 'r') as f: - for line in f: - domain = line.strip() - if domain and not domain.startswith('#'): - domains.append(domain) - - domains = list(set(domains)) - MANAGED_DOMAINS = set(d.lower() for d in domains) - - return domains - - -def is_internal_address(email_address: str) -> bool: - """Check if email address belongs to one of our managed domains""" - if '@' not in email_address: - return False - domain = email_address.split('@')[1].lower() - return domain in MANAGED_DOMAINS - - -def domain_to_queue_name(domain: str) -> str: - """Convert domain to SQS queue name""" - return domain.replace('.', '-') + '-queue' - - -def domain_to_bucket_name(domain: str) -> str: - """Convert domain to S3 bucket name""" - return domain.replace('.', '-') + '-emails' diff --git a/email-worker/docker-compose.yml b/email-worker/docker-compose.yml deleted file mode 100644 index 2fcd892..0000000 --- a/email-worker/docker-compose.yml +++ /dev/null @@ -1,85 +0,0 @@ -services: - unified-worker: - build: - context: . - dockerfile: Dockerfile - container_name: unified-email-worker - restart: unless-stopped - network_mode: host # Für lokalen SMTP-Zugriff - - volumes: - # Domain-Liste (eine Domain pro Zeile) - - ./domains.txt:/etc/email-worker/domains.txt:ro - # Logs - - ./logs:/var/log/email-worker - - environment: - # AWS Credentials - - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} - - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} - - AWS_REGION=us-east-2 - - # Domains via File (domains.txt) - - DOMAINS_FILE=/etc/email-worker/domains.txt - - # Alternative: Domains direkt als Liste - # - DOMAINS=andreasknuth.de,bayarea-cc.com,bizmatch.net - - # Worker Settings - - WORKER_THREADS=${WORKER_THREADS:-10} - - POLL_INTERVAL=${POLL_INTERVAL:-20} - - MAX_MESSAGES=${MAX_MESSAGES:-10} - - VISIBILITY_TIMEOUT=${VISIBILITY_TIMEOUT:-300} - - # SMTP (lokal zum DMS) - - SMTP_HOST=${SMTP_HOST:-localhost} - - SMTP_PORT=${SMTP_PORT:-25} - - SMTP_POOL_SIZE=${SMTP_POOL_SIZE:-5} - - SMTP_USE_TLS=false - - # Internal SMTP Port (bypasses transport_maps) - - INTERNAL_SMTP_PORT=25 - - # LMTP (Optional - für direktes Dovecot Delivery) - - LMTP_ENABLED=${LMTP_ENABLED:-false} - - LMTP_HOST=${LMTP_HOST:-localhost} - - LMTP_PORT=${LMTP_PORT:-24} - - # DynamoDB Tables - - DYNAMODB_RULES_TABLE=${DYNAMODB_RULES_TABLE:-email-rules} - - DYNAMODB_MESSAGES_TABLE=${DYNAMODB_MESSAGES_TABLE:-ses-outbound-messages} - - DYNAMODB_BLOCKED_TABLE=${DYNAMODB_BLOCKED_TABLE:-email-blocked-senders} - - # Bounce Handling - - BOUNCE_LOOKUP_RETRIES=${BOUNCE_LOOKUP_RETRIES:-3} - - BOUNCE_LOOKUP_DELAY=${BOUNCE_LOOKUP_DELAY:-1.0} - - # Monitoring - - METRICS_PORT=8000 - - HEALTH_PORT=8080 - - ports: - # Prometheus Metrics - - "8000:8000" - # Health Check - - "8080:8080" - - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 10s - - logging: - driver: "json-file" - options: - max-size: "50m" - max-file: "10" - - deploy: - resources: - limits: - memory: 512M - reservations: - memory: 256M diff --git a/email-worker/docs/ARCHITECTURE.md b/email-worker/docs/ARCHITECTURE.md deleted file mode 100644 index 1611c0e..0000000 --- a/email-worker/docs/ARCHITECTURE.md +++ /dev/null @@ -1,381 +0,0 @@ -# Architecture Documentation - -## 📐 System Overview - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ AWS Cloud Services │ -├─────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ SQS │────▶│ S3 │ │ SES │ │ -│ │ Queues │ │ Buckets │ │ Sending │ │ -│ └──────────┘ └──────────┘ └──────────┘ │ -│ │ │ │ │ -│ │ │ │ │ -│ ┌────▼─────────────────▼─────────────────▼───────────────┐ │ -│ │ DynamoDB Tables │ │ -│ │ • email-rules (OOO, Forwarding) │ │ -│ │ • ses-outbound-messages (Bounce Tracking) │ │ -│ │ • email-blocked-senders (Blocklist) │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────┘ - │ - │ Polling & Processing - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ Unified Email Worker │ -├─────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Main Thread (unified_worker.py) │ │ -│ │ • Coordination │ │ -│ │ • Status Monitoring │ │ -│ │ • Signal Handling │ │ -│ └────────────┬────────────────────────────────────────────┘ │ -│ │ │ -│ ├──▶ Domain Poller Thread 1 (example.com) │ -│ ├──▶ Domain Poller Thread 2 (another.com) │ -│ ├──▶ Domain Poller Thread 3 (...) │ -│ ├──▶ Health Server Thread (port 8080) │ -│ └──▶ Metrics Server Thread (port 8000) │ -│ │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ SMTP Connection Pool │ │ -│ │ • Connection Reuse │ │ -│ │ • Health Checks │ │ -│ │ • Auto-reconnect │ │ -│ └──────────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────┘ - │ - │ SMTP/LMTP Delivery - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ Mail Server (Docker Mailserver) │ -├─────────────────────────────────────────────────────────────────────┤ -│ │ -│ Port 25 (SMTP - from pool) │ -│ Port 2525 (SMTP - internal delivery, bypasses transport_maps) │ -│ Port 24 (LMTP - direct to Dovecot, bypasses Postfix) │ -│ │ -└─────────────────────────────────────────────────────────────────────┘ -``` - -## 🔄 Message Flow - -### 1. Email Reception -``` -1. SES receives email -2. SES stores in S3 bucket (domain-emails/) -3. SES publishes SNS notification -4. SNS enqueues message to SQS (domain-queue) -``` - -### 2. Worker Processing -``` -┌─────────────────────────────────────────────────────────────┐ -│ Domain Poller (domain_poller.py) │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 1. Poll SQS Queue (20s long poll) │ -│ • Receive up to 10 messages │ -│ • Extract SES notification from SNS wrapper │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 2. Download from S3 (s3_handler.py) │ -│ • Get raw email bytes │ -│ • Handle retry if not found yet │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 3. Parse Email (parser.py) │ -│ • Parse MIME structure │ -│ • Extract headers, body, attachments │ -│ • Check for loop prevention marker │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 4. Bounce Detection (bounce_handler.py) │ -│ • Check if from mailer-daemon@amazonses.com │ -│ • Lookup original sender in DynamoDB │ -│ • Rewrite From/Reply-To headers │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 5. Blocklist Check (blocklist.py) │ -│ • Batch lookup blocked patterns for all recipients │ -│ • Check sender against wildcard patterns │ -│ • Mark blocked recipients │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 6. Process Rules for Each Recipient (rules_processor.py) │ -│ ├─▶ Auto-Reply (OOO) │ -│ │ • Check if ooo_active = true │ -│ │ • Don't reply to auto-submitted messages │ -│ │ • Create reply with original message quoted │ -│ │ • Send via SES (external) or Port 2525 (internal) │ -│ │ │ -│ └─▶ Forwarding │ -│ • Get forward addresses from rule │ -│ • Create forward with FWD: prefix │ -│ • Preserve attachments │ -│ • Send via SES (external) or Port 2525 (internal) │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 7. SMTP Delivery (delivery.py) │ -│ • Get connection from pool │ -│ • Send to each recipient (not blocked) │ -│ • Track success/permanent/temporary failures │ -│ • Return connection to pool │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 8. Update S3 Metadata (s3_handler.py) │ -│ ├─▶ All Blocked: mark_as_blocked() + delete() │ -│ ├─▶ Some Success: mark_as_processed() │ -│ └─▶ All Invalid: mark_as_all_invalid() │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 9. Delete from Queue │ -│ • Success or permanent failure → delete │ -│ • Temporary failure → keep in queue (retry) │ -└─────────────────────────────────────────────────────────────┘ -``` - -## 🧩 Component Details - -### AWS Handlers (`aws/`) - -#### `s3_handler.py` -- **Purpose**: All S3 operations -- **Key Methods**: - - `get_email()`: Download with retry logic - - `mark_as_processed()`: Update metadata on success - - `mark_as_all_invalid()`: Update metadata on permanent failure - - `mark_as_blocked()`: Set metadata before deletion - - `delete_blocked_email()`: Delete after marking - -#### `sqs_handler.py` -- **Purpose**: Queue operations -- **Key Methods**: - - `get_queue_url()`: Resolve domain to queue - - `receive_messages()`: Long poll with attributes - - `delete_message()`: Remove after processing - - `get_queue_size()`: For metrics - -#### `ses_handler.py` -- **Purpose**: Send emails via SES -- **Key Methods**: - - `send_raw_email()`: Send raw MIME message - -#### `dynamodb_handler.py` -- **Purpose**: All DynamoDB operations -- **Key Methods**: - - `get_email_rules()`: OOO and forwarding rules - - `get_bounce_info()`: Bounce lookup with retry - - `get_blocked_patterns()`: Single recipient - - `batch_get_blocked_patterns()`: Multiple recipients (efficient!) - -### Email Processors (`email_processing/`) - -#### `parser.py` -- **Purpose**: Email parsing utilities -- **Key Methods**: - - `parse_bytes()`: Parse raw email - - `extract_body_parts()`: Get text/html bodies - - `is_processed_by_worker()`: Loop detection - -#### `bounce_handler.py` -- **Purpose**: Bounce detection and rewriting -- **Key Methods**: - - `is_ses_bounce_notification()`: Detect MAILER-DAEMON - - `apply_bounce_logic()`: Rewrite headers - -#### `blocklist.py` -- **Purpose**: Sender blocking with wildcards -- **Key Methods**: - - `is_sender_blocked()`: Single check - - `batch_check_blocked_senders()`: Batch check (preferred!) -- **Wildcard Support**: Uses `fnmatch` for patterns like `*@spam.com` - -#### `rules_processor.py` -- **Purpose**: OOO and forwarding logic -- **Key Methods**: - - `process_rules_for_recipient()`: Main entry point - - `_handle_ooo()`: Auto-reply logic - - `_handle_forwards()`: Forwarding logic - - `_create_ooo_reply()`: Build OOO message - - `_create_forward_message()`: Build forward with attachments - -### SMTP Components (`smtp/`) - -#### `pool.py` -- **Purpose**: Connection pooling -- **Features**: - - Lazy initialization - - Health checks (NOOP) - - Auto-reconnect on stale connections - - Thread-safe queue - -#### `delivery.py` -- **Purpose**: Actual email delivery -- **Features**: - - SMTP or LMTP support - - Retry logic for connection errors - - Permanent vs temporary failure detection - - Connection pool integration - -### Monitoring (`metrics/`) - -#### `prometheus.py` -- **Purpose**: Metrics collection -- **Metrics**: - - Counters: processed, bounces, autoreplies, forwards, blocked - - Gauges: in_flight, queue_size - - Histograms: processing_time - -## 🔐 Security Features - -### 1. Domain Validation -Each worker only processes messages for its assigned domains: -```python -if recipient_domain.lower() != domain.lower(): - log("Security: Ignored message for wrong domain") - return True # Delete from queue -``` - -### 2. Loop Prevention -Detects already-processed emails: -```python -if parsed.get('X-SES-Worker-Processed'): - log("Loop prevention: Already processed") - skip_rules = True -``` - -### 3. Blocklist Wildcards -Supports flexible patterns: -```python -blocked_patterns = [ - "*@spam.com", # Any user at spam.com - "noreply@*.com", # noreply at any .com - "newsletter@example.*" # newsletter at any example TLD -] -``` - -### 4. Internal vs External Routing -Prevents SES loops for internal forwards: -```python -if is_internal_address(forward_to): - # Direct SMTP to port 2525 (bypasses transport_maps) - send_internal_email(...) -else: - # Send via SES - ses.send_raw_email(...) -``` - -## 📊 Data Flow Diagrams - -### Bounce Rewriting Flow -``` -SES Bounce → Worker → DynamoDB Lookup → Header Rewrite → Delivery - ↓ - Message-ID - ↓ - ses-outbound-messages - {MessageId: "abc", - original_source: "real@sender.com", - bouncedRecipients: ["failed@domain.com"]} - ↓ - Rewrite From: mailer-daemon@amazonses.com - → failed@domain.com -``` - -### Blocklist Check Flow -``` -Incoming Email → Batch DynamoDB Call → Pattern Matching → Decision - ↓ ↓ ↓ ↓ -sender@spam.com Get patterns for fnmatch() Block/Allow - all recipients "*@spam.com" - matches! -``` - -## ⚡ Performance Optimizations - -### 1. Batch DynamoDB Calls -```python -# ❌ Old way: N calls for N recipients -for recipient in recipients: - patterns = dynamodb.get_blocked_patterns(recipient) - -# ✅ New way: 1 call for N recipients -patterns_by_recipient = dynamodb.batch_get_blocked_patterns(recipients) -``` - -### 2. Connection Pooling -```python -# ❌ Old way: New connection per email -conn = smtplib.SMTP(host, port) -conn.sendmail(...) -conn.quit() - -# ✅ New way: Reuse connections -conn = pool.get_connection() # Reuses existing -conn.sendmail(...) -pool.return_connection(conn) # Returns to pool -``` - -### 3. Parallel Domain Processing -``` -Domain 1 Thread ──▶ Process 10 emails/poll -Domain 2 Thread ──▶ Process 10 emails/poll -Domain 3 Thread ──▶ Process 10 emails/poll - (All in parallel!) -``` - -## 🔄 Error Handling Strategy - -### Retry Logic -- **Temporary Errors**: Keep in queue, retry (visibility timeout) -- **Permanent Errors**: Mark in S3, delete from queue -- **S3 Not Found**: Retry up to 5 times (eventual consistency) - -### Connection Failures -```python -for attempt in range(max_retries): - try: - conn.sendmail(...) - return True - except SMTPServerDisconnected: - log("Connection lost, retrying...") - time.sleep(0.3) - continue # Try again -``` - -### Audit Trail -All actions recorded in S3 metadata: -```json -{ - "processed": "true", - "processed_at": "1706000000", - "processed_by": "worker-example.com", - "status": "delivered", - "invalid_inboxes": "baduser@example.com", - "blocked_sender": "spam@bad.com" -} -``` diff --git a/email-worker/docs/CHANGELOG.md b/email-worker/docs/CHANGELOG.md deleted file mode 100644 index 005c6d1..0000000 --- a/email-worker/docs/CHANGELOG.md +++ /dev/null @@ -1,37 +0,0 @@ -# Changelog - -## v1.0.1 - 2025-01-23 - -### Fixed -- **CRITICAL:** Renamed `email/` directory to `email_processing/` to avoid namespace conflict with Python's built-in `email` module - - This fixes the `ImportError: cannot import name 'BytesParser' from partially initialized module 'email.parser'` error - - All imports updated accordingly - - No functional changes, only namespace fix - -### Changed -- Updated all documentation to reflect new directory name -- Updated Dockerfile to copy `email_processing/` instead of `email/` - -## v1.0.0 - 2025-01-23 - -### Added -- Modular architecture (27 files vs 1 monolith) -- Batch DynamoDB operations (10x performance improvement) -- Sender blocklist with wildcard support -- LMTP direct delivery support -- Enhanced metrics and monitoring -- Comprehensive documentation (6 MD files) - -### Fixed -- `signal.SIGINT` typo (was `signalIGINT`) -- Missing S3 metadata audit trail for blocked emails -- Inefficient DynamoDB calls (N calls → 1 batch call) -- S3 delete error handling (proper retry logic) - -### Documentation -- README.md - Full feature documentation -- QUICKSTART.md - Quick deployment guide for your setup -- ARCHITECTURE.md - Detailed system architecture -- MIGRATION.md - Migration from monolith -- COMPATIBILITY.md - 100% compatibility proof -- SUMMARY.md - All improvements overview diff --git a/email-worker/docs/COMPATIBILITY.md b/email-worker/docs/COMPATIBILITY.md deleted file mode 100644 index c1a5237..0000000 --- a/email-worker/docs/COMPATIBILITY.md +++ /dev/null @@ -1,311 +0,0 @@ -# Kompatibilität mit bestehendem Setup - -## ✅ 100% Kompatibel - -Die modulare Version ist **vollständig kompatibel** mit deinem bestehenden Setup: - -### 1. Dockerfile -- ✅ Gleicher Base Image: `python:3.11-slim` -- ✅ Gleicher User: `worker` (UID 1000) -- ✅ Gleiche Verzeichnisse: `/app`, `/var/log/email-worker`, `/etc/email-worker` -- ✅ Gleicher Health Check: `curl http://localhost:8080/health` -- ✅ Gleiche Labels: `maintainer`, `description` -- **Änderung:** Kopiert nun mehrere Module statt einer Datei - -### 2. docker-compose.yml -- ✅ Gleicher Container Name: `unified-email-worker` -- ✅ Gleicher Network Mode: `host` -- ✅ Gleiche Volumes: `domains.txt`, `logs/` -- ✅ Gleiche Ports: `8000`, `8080` -- ✅ Gleiche Environment Variables -- ✅ Gleiche Resource Limits: 512M / 256M -- ✅ Gleiche Logging Config: 50M / 10 files -- **Neu:** Zusätzliche optionale Env Vars (abwärtskompatibel) - -### 3. requirements.txt -- ✅ Gleiche Dependencies: `boto3`, `prometheus-client` -- ✅ Aktualisierte Versionen (>=1.34.0 statt >=1.26.0) -- **Kompatibel:** Alte Version funktioniert auch, neue ist empfohlen - -### 4. domains.txt -- ✅ Gleiches Format: Eine Domain pro Zeile -- ✅ Kommentare mit `#` funktionieren -- ✅ Gleiche Location: `/etc/email-worker/domains.txt` -- **Keine Änderung nötig** - -## 🔄 Was ist neu/anders? - -### Dateistruktur -**Alt:** -``` -/ -├── Dockerfile -├── docker-compose.yml -├── requirements.txt -├── domains.txt -└── unified_worker.py (800+ Zeilen) -``` - -**Neu:** -``` -/ -├── Dockerfile -├── docker-compose.yml -├── requirements.txt -├── domains.txt -├── main.py # Entry Point -├── config.py # Konfiguration -├── logger.py # Logging -├── worker.py # Message Processing -├── unified_worker.py # Worker Coordinator -├── domain_poller.py # Queue Polling -├── health_server.py # Health Check Server -├── aws/ -│ ├── s3_handler.py -│ ├── sqs_handler.py -│ ├── ses_handler.py -│ └── dynamodb_handler.py -├── email_processing/ -│ ├── parser.py -│ ├── bounce_handler.py -│ ├── blocklist.py -│ └── rules_processor.py -├── smtp/ -│ ├── pool.py -│ └── delivery.py -└── metrics/ - └── prometheus.py -``` - -### Neue optionale Umgebungsvariablen - -Diese sind **optional** und haben sinnvolle Defaults: - -```bash -# Internal SMTP Port (neu) -INTERNAL_SMTP_PORT=2525 # Default: 2525 - -# LMTP Support (neu) -LMTP_ENABLED=false # Default: false -LMTP_HOST=localhost # Default: localhost -LMTP_PORT=24 # Default: 24 - -# Blocklist Table (neu) -DYNAMODB_BLOCKED_TABLE=email-blocked-senders # Default: email-blocked-senders -``` - -**Wichtig:** Wenn du diese nicht setzt, funktioniert alles wie vorher! - -## 🚀 Deployment - -### Option 1: Drop-In Replacement -```bash -# Alte Dateien sichern -cp unified_worker.py unified_worker.py.backup -cp Dockerfile Dockerfile.backup -cp docker-compose.yml docker-compose.yml.backup - -# Neue Dateien entpacken -tar -xzf email-worker-modular.tar.gz -cd email-worker/ - -# domains.txt und .env anpassen (falls nötig) -# Dann normal deployen: -docker-compose build -docker-compose up -d -``` - -### Option 2: Side-by-Side (Empfohlen) -```bash -# Altes Setup bleibt in /opt/email-worker-old -# Neues Setup in /opt/email-worker - -# Neue Version entpacken -cd /opt -tar -xzf email-worker-modular.tar.gz -mv email-worker email-worker-new - -# Container Namen unterscheiden: -# In docker-compose.yml: -container_name: unified-email-worker-new - -# Starten -cd email-worker-new -docker-compose up -d - -# Parallel laufen lassen (24h Test) -# Dann alte Version stoppen, neue umbenennen -``` - -## 🔍 Verifikation der Kompatibilität - -### 1. Environment Variables -Alle deine bestehenden Env Vars funktionieren: - -```bash -# Deine bisherigen Vars (alle kompatibel) -AWS_ACCESS_KEY_ID ✅ -AWS_SECRET_ACCESS_KEY ✅ -AWS_REGION ✅ -WORKER_THREADS ✅ -POLL_INTERVAL ✅ -MAX_MESSAGES ✅ -VISIBILITY_TIMEOUT ✅ -SMTP_HOST ✅ -SMTP_PORT ✅ -SMTP_POOL_SIZE ✅ -METRICS_PORT ✅ -HEALTH_PORT ✅ -``` - -### 2. DynamoDB Tables -Bestehende Tables funktionieren ohne Änderung: - -```bash -# Bounce Tracking (bereits vorhanden) -ses-outbound-messages ✅ - -# Email Rules (bereits vorhanden?) -email-rules ✅ - -# Blocklist (neu, optional) -email-blocked-senders 🆕 Optional -``` - -### 3. API Endpoints -Gleiche Endpoints wie vorher: - -```bash -# Health Check -GET http://localhost:8080/health ✅ Gleiche Response - -# Domains List -GET http://localhost:8080/domains ✅ Gleiche Response - -# Prometheus Metrics -GET http://localhost:8000/metrics ✅ Kompatibel + neue Metrics -``` - -### 4. Logging -Gleiches Format, gleiche Location: - -```bash -# Logs in Container -/var/log/email-worker/ ✅ Gleich - -# Log Format -[timestamp] [LEVEL] [worker-name] [thread] message ✅ Gleich -``` - -### 5. S3 Metadata -Gleiches Schema, volle Kompatibilität: - -```json -{ - "processed": "true", - "processed_at": "1706000000", - "processed_by": "worker-andreasknuth-de", - "status": "delivered", - "invalid_inboxes": "..." -} -``` - -**Neu:** Zusätzliche Metadata bei blockierten Emails: -```json -{ - "status": "blocked", - "blocked_sender": "spam@bad.com", - "blocked_recipients": "user@andreasknuth.de" -} -``` - -## ⚠️ Breaking Changes - -**KEINE!** Die modulare Version ist 100% abwärtskompatibel. - -Die einzigen Unterschiede sind: -1. ✅ **Mehr Dateien** statt einer (aber gleiches Verhalten) -2. ✅ **Neue optionale Features** (müssen nicht genutzt werden) -3. ✅ **Bessere Performance** (durch Batch-Calls) -4. ✅ **Mehr Metrics** (zusätzliche, alte bleiben) - -## 🧪 Testing Checklist - -Nach Deployment prüfen: - -```bash -# 1. Container läuft -docker ps | grep unified-email-worker -✅ Status: Up - -# 2. Health Check -curl http://localhost:8080/health | jq -✅ "status": "healthy" - -# 3. Domains geladen -curl http://localhost:8080/domains -✅ ["andreasknuth.de"] - -# 4. Logs ohne Fehler -docker-compose logs | grep ERROR -✅ Keine kritischen Fehler - -# 5. Test Email senden -# Email via SES senden -✅ Wird zugestellt - -# 6. Metrics verfügbar -curl http://localhost:8000/metrics | grep emails_processed -✅ Metrics werden erfasst -``` - -## 💡 Empfohlener Rollout-Plan - -### Phase 1: Testing (1-2 Tage) -- Neuen Container parallel zum alten starten -- Nur 1 Test-Domain zuweisen -- Logs monitoren -- Performance vergleichen - -### Phase 2: Staged Rollout (3-7 Tage) -- 50% der Domains auf neue Version -- Metrics vergleichen (alte vs neue) -- Bei Problemen: Rollback auf alte Version - -### Phase 3: Full Rollout -- Alle Domains auf neue Version -- Alte Version als Backup behalten (1 Woche) -- Dann alte Version dekommissionieren - -## 🔙 Rollback-Plan - -Falls Probleme auftreten: - -```bash -# 1. Neue Version stoppen -docker-compose -f docker-compose.yml down - -# 2. Backup wiederherstellen -cp unified_worker.py.backup unified_worker.py -cp Dockerfile.backup Dockerfile -cp docker-compose.yml.backup docker-compose.yml - -# 3. Alte Version starten -docker-compose build -docker-compose up -d - -# 4. Verifizieren -curl http://localhost:8080/health -``` - -**Downtime:** < 30 Sekunden (Zeit für Container Restart) - -## ✅ Fazit - -Die modulare Version ist ein **Drop-In Replacement**: -- Gleiche Konfiguration -- Gleiche API -- Gleiche Infrastruktur -- **Bonus:** Bessere Performance, mehr Features, weniger Bugs - -Einziger Unterschied: Mehr Dateien, aber alle in einem tarball verpackt. diff --git a/email-worker/docs/MIGRATION.md b/email-worker/docs/MIGRATION.md deleted file mode 100644 index 4a5bbf1..0000000 --- a/email-worker/docs/MIGRATION.md +++ /dev/null @@ -1,366 +0,0 @@ -# Migration Guide: Monolith → Modular Architecture - -## 🎯 Why Migrate? - -### Problems with Monolith -- ❌ **Single file > 800 lines** - hard to navigate -- ❌ **Mixed responsibilities** - S3, SQS, SMTP, DynamoDB all in one place -- ❌ **Hard to test** - can't test components in isolation -- ❌ **Difficult to debug** - errors could be anywhere -- ❌ **Critical bugs** - `signalIGINT` typo, missing audit trail -- ❌ **Performance issues** - N DynamoDB calls for N recipients - -### Benefits of Modular -- ✅ **Separation of Concerns** - each module has one job -- ✅ **Easy to Test** - mock S3Handler, test in isolation -- ✅ **Better Performance** - batch DynamoDB calls -- ✅ **Maintainable** - changes isolated to specific files -- ✅ **Extensible** - easy to add new features -- ✅ **Bug Fixes** - all critical bugs fixed - -## 🔄 Migration Steps - -### Step 1: Backup Current Setup -```bash -# Backup monolith -cp unified_worker.py unified_worker.py.backup - -# Backup any configuration -cp .env .env.backup -``` - -### Step 2: Clone New Structure -```bash -# Download modular version -git clone email-worker-modular -cd email-worker-modular - -# Copy environment variables -cp .env.example .env -# Edit .env with your settings -``` - -### Step 3: Update Configuration - -The modular version uses the SAME environment variables, so your existing `.env` should work: - -```bash -# No changes needed to these: -AWS_REGION=us-east-2 -DOMAINS=example.com,another.com -SMTP_HOST=localhost -SMTP_PORT=25 -# ... etc -``` - -**New variables** (optional): -```bash -# For internal delivery (bypasses transport_maps) -INTERNAL_SMTP_PORT=2525 - -# For blocklist feature -DYNAMODB_BLOCKED_TABLE=email-blocked-senders -``` - -### Step 4: Install Dependencies -```bash -pip install -r requirements.txt -``` - -### Step 5: Test Locally -```bash -# Run worker -python3 main.py - -# Check health endpoint -curl http://localhost:8080/health - -# Check metrics -curl http://localhost:8000/metrics -``` - -### Step 6: Deploy - -#### Docker Deployment -```bash -# Build image -docker build -t unified-email-worker:latest . - -# Run with docker-compose -docker-compose up -d - -# Check logs -docker-compose logs -f email-worker -``` - -#### Systemd Deployment -```bash -# Create systemd service -sudo nano /etc/systemd/system/email-worker.service -``` - -```ini -[Unit] -Description=Unified Email Worker -After=network.target - -[Service] -Type=simple -User=worker -WorkingDirectory=/opt/email-worker -EnvironmentFile=/opt/email-worker/.env -ExecStart=/usr/bin/python3 /opt/email-worker/main.py -Restart=always -RestartSec=10 - -[Install] -WantedBy=multi-user.target -``` - -```bash -# Enable and start -sudo systemctl enable email-worker -sudo systemctl start email-worker -sudo systemctl status email-worker -``` - -### Step 7: Monitor Migration -```bash -# Watch logs -tail -f /var/log/syslog | grep email-worker - -# Check metrics -watch -n 5 'curl -s http://localhost:8000/metrics | grep emails_processed' - -# Monitor S3 metadata -aws s3api head-object \ - --bucket example-com-emails \ - --key \ - --query Metadata -``` - -## 🔍 Verification Checklist - -After migration, verify all features work: - -- [ ] **Email Delivery** - ```bash - # Send test email via SES - # Check it arrives in mailbox - ``` - -- [ ] **Bounce Rewriting** - ```bash - # Trigger a bounce (send to invalid@example.com) - # Verify bounce comes FROM the failed recipient - ``` - -- [ ] **Auto-Reply (OOO)** - ```bash - # Set OOO in DynamoDB: - aws dynamodb put-item \ - --table-name email-rules \ - --item '{"email_address": {"S": "test@example.com"}, "ooo_active": {"BOOL": true}, "ooo_message": {"S": "I am away"}}' - - # Send email to test@example.com - # Verify auto-reply received - ``` - -- [ ] **Forwarding** - ```bash - # Set forward rule: - aws dynamodb put-item \ - --table-name email-rules \ - --item '{"email_address": {"S": "test@example.com"}, "forwards": {"L": [{"S": "other@example.com"}]}}' - - # Send email to test@example.com - # Verify other@example.com receives forwarded email - ``` - -- [ ] **Blocklist** - ```bash - # Block sender: - aws dynamodb put-item \ - --table-name email-blocked-senders \ - --item '{"email_address": {"S": "test@example.com"}, "blocked_patterns": {"L": [{"S": "spam@*.com"}]}}' - - # Send email from spam@bad.com to test@example.com - # Verify email is blocked (not delivered, S3 deleted) - ``` - -- [ ] **Metrics** - ```bash - curl http://localhost:8000/metrics | grep emails_processed - ``` - -- [ ] **Health Check** - ```bash - curl http://localhost:8080/health | jq - ``` - -## 🐛 Troubleshooting Migration Issues - -### Issue: Worker not starting -```bash -# Check Python version -python3 --version # Should be 3.11+ - -# Check dependencies -pip list | grep boto3 - -# Check logs -python3 main.py # Run in foreground to see errors -``` - -### Issue: No emails processing -```bash -# Check queue URLs -curl http://localhost:8080/domains - -# Verify SQS permissions -aws sqs list-queues - -# Check worker logs for errors -tail -f /var/log/email-worker.log -``` - -### Issue: Bounces not rewriting -```bash -# Verify DynamoDB table exists -aws dynamodb describe-table --table-name ses-outbound-messages - -# Check if Lambda is writing bounce records -aws dynamodb scan --table-name ses-outbound-messages --limit 5 - -# Verify worker can read DynamoDB -# (Check logs for "DynamoDB tables connected successfully") -``` - -### Issue: Performance degradation -```bash -# Check if batch calls are used -grep "batch_get_blocked_patterns" main.py # Should exist in modular version - -# Monitor DynamoDB read capacity -aws cloudwatch get-metric-statistics \ - --namespace AWS/DynamoDB \ - --metric-name ConsumedReadCapacityUnits \ - --dimensions Name=TableName,Value=email-blocked-senders \ - --start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) \ - --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \ - --period 300 \ - --statistics Sum -``` - -## 📊 Comparison: Before vs After - -| Feature | Monolith | Modular | Improvement | -|---------|----------|---------|-------------| -| Lines of Code | 800+ in 1 file | ~150 per file | ✅ Easier to read | -| DynamoDB Calls | N per message | 1 per message | ✅ 10x faster | -| Error Handling | Missing in places | Comprehensive | ✅ More reliable | -| Testability | Hard | Easy | ✅ Can unit test | -| Audit Trail | Incomplete | Complete | ✅ Better compliance | -| Bugs Fixed | - | 4 critical | ✅ More stable | -| Extensibility | Hard | Easy | ✅ Future-proof | - -## 🎓 Code Comparison Examples - -### Example 1: Blocklist Check - -**Monolith (Inefficient):** -```python -for recipient in recipients: - if is_sender_blocked(recipient, sender, worker_name): - # DynamoDB call for EACH recipient! - blocked_recipients.append(recipient) -``` - -**Modular (Efficient):** -```python -# ONE DynamoDB call for ALL recipients -blocked_by_recipient = blocklist.batch_check_blocked_senders( - recipients, sender, worker_name -) -for recipient in recipients: - if blocked_by_recipient[recipient]: - blocked_recipients.append(recipient) -``` - -### Example 2: S3 Blocked Email Handling - -**Monolith (Missing Audit Trail):** -```python -if all_blocked: - s3.delete_object(Bucket=bucket, Key=key) # ❌ No metadata! -``` - -**Modular (Proper Audit):** -```python -if all_blocked: - s3.mark_as_blocked(domain, key, blocked, sender, worker) # ✅ Set metadata - s3.delete_blocked_email(domain, key, worker) # ✅ Then delete -``` - -### Example 3: Signal Handling - -**Monolith (Bug):** -```python -signal.signal(signal.SIGTERM, handler) -signal.signal(signalIGINT, handler) # ❌ Typo! Should be signal.SIGINT -``` - -**Modular (Fixed):** -```python -signal.signal(signal.SIGTERM, handler) -signal.signal(signal.SIGINT, handler) # ✅ Correct -``` - -## 🔄 Rollback Plan - -If you need to rollback: - -```bash -# Stop new worker -docker-compose down -# or -sudo systemctl stop email-worker - -# Restore monolith -cp unified_worker.py.backup unified_worker.py - -# Restart old worker -python3 unified_worker.py -# or restore old systemd service -``` - -## 💡 Best Practices After Migration - -1. **Monitor Metrics**: Set up Prometheus/Grafana dashboards -2. **Set up Alerts**: Alert on queue buildup, high error rates -3. **Regular Updates**: Keep dependencies updated -4. **Backup Rules**: Export DynamoDB rules regularly -5. **Test in Staging**: Always test rule changes in non-prod first - -## 📚 Additional Resources - -- [ARCHITECTURE.md](ARCHITECTURE.md) - Detailed architecture diagrams -- [README.md](README.md) - Complete feature documentation -- [Makefile](Makefile) - Common commands - -## ❓ FAQ - -**Q: Will my existing DynamoDB tables work?** -A: Yes! Same schema, just need to add `email-blocked-senders` table for blocklist feature. - -**Q: Do I need to change my Lambda functions?** -A: No, bounce tracking Lambda stays the same. - -**Q: Can I migrate one domain at a time?** -A: Yes! Run both workers with different `DOMAINS` settings, then migrate gradually. - -**Q: What about my existing S3 metadata?** -A: New worker reads and writes same metadata format, fully compatible. - -**Q: How do I add new features?** -A: Just add a new module in appropriate directory (e.g., new file in `email/`), import in `worker.py`. diff --git a/email-worker/docs/QUICKSTART.md b/email-worker/docs/QUICKSTART.md deleted file mode 100644 index 6b2000d..0000000 --- a/email-worker/docs/QUICKSTART.md +++ /dev/null @@ -1,330 +0,0 @@ -# Quick Start Guide - -## 🚀 Deployment auf deinem System - -### Voraussetzungen -- Docker & Docker Compose installiert -- AWS Credentials mit Zugriff auf SQS, S3, SES, DynamoDB -- Docker Mailserver (DMS) läuft lokal - -### 1. Vorbereitung - -```bash -# Ins Verzeichnis wechseln -cd /pfad/zu/email-worker - -# domains.txt anpassen (falls weitere Domains) -nano domains.txt - -# Logs-Verzeichnis erstellen -mkdir -p logs -``` - -### 2. Umgebungsvariablen - -Erstelle `.env` Datei: - -```bash -# AWS Credentials -AWS_ACCESS_KEY_ID=dein_access_key -AWS_SECRET_ACCESS_KEY=dein_secret_key - -# Optional: Worker Settings überschreiben -WORKER_THREADS=10 -POLL_INTERVAL=20 -MAX_MESSAGES=10 - -# Optional: SMTP Settings -SMTP_HOST=localhost -SMTP_PORT=25 - -# Optional: LMTP für direktes Dovecot Delivery -# LMTP_ENABLED=true -# LMTP_PORT=24 -``` - -### 3. Build & Start - -```bash -# Image bauen -docker-compose build - -# Starten -docker-compose up -d - -# Logs anschauen -docker-compose logs -f -``` - -### 4. Verifizierung - -```bash -# Health Check -curl http://localhost:8080/health | jq - -# Domains prüfen -curl http://localhost:8080/domains - -# Metrics (Prometheus) -curl http://localhost:8000/metrics | grep emails_processed - -# Container Status -docker ps | grep unified-email-worker -``` - -### 5. Test Email senden - -```bash -# Via AWS SES Console oder CLI eine Test-Email senden -aws ses send-email \ - --from sender@andreasknuth.de \ - --destination ToAddresses=test@andreasknuth.de \ - --message Subject={Data="Test"},Body={Text={Data="Test message"}} - -# Worker Logs beobachten -docker-compose logs -f | grep "Processing:" -``` - -## 🔧 Wartung - -### Logs anschauen -```bash -# Live Logs -docker-compose logs -f - -# Nur Worker Logs -docker logs -f unified-email-worker - -# Logs im Volume -tail -f logs/*.log -``` - -### Neustart -```bash -# Neustart nach Code-Änderungen -docker-compose restart - -# Kompletter Rebuild -docker-compose down -docker-compose build -docker-compose up -d -``` - -### Update -```bash -# Neue Version pullen/kopieren -git pull # oder manuell Dateien ersetzen - -# Rebuild & Restart -docker-compose down -docker-compose build -docker-compose up -d -``` - -## 📊 Monitoring - -### Prometheus Metrics (Port 8000) -```bash -# Alle Metrics -curl http://localhost:8000/metrics - -# Verarbeitete Emails -curl -s http://localhost:8000/metrics | grep emails_processed_total - -# Queue Größe -curl -s http://localhost:8000/metrics | grep queue_messages_available - -# Blocked Senders -curl -s http://localhost:8000/metrics | grep blocked_senders_total -``` - -### Health Check (Port 8080) -```bash -# Status -curl http://localhost:8080/health | jq - -# Domains -curl http://localhost:8080/domains | jq -``` - -## 🔐 DynamoDB Tabellen Setup - -### Email Rules (OOO, Forwarding) -```bash -# Tabelle erstellen (falls nicht vorhanden) -aws dynamodb create-table \ - --table-name email-rules \ - --attribute-definitions AttributeName=email_address,AttributeType=S \ - --key-schema AttributeName=email_address,KeyType=HASH \ - --billing-mode PAY_PER_REQUEST \ - --region us-east-2 - -# OOO Regel hinzufügen -aws dynamodb put-item \ - --table-name email-rules \ - --item '{ - "email_address": {"S": "andreas@andreasknuth.de"}, - "ooo_active": {"BOOL": true}, - "ooo_message": {"S": "Ich bin derzeit nicht erreichbar."}, - "ooo_content_type": {"S": "text"} - }' \ - --region us-east-2 - -# Forward Regel hinzufügen -aws dynamodb put-item \ - --table-name email-rules \ - --item '{ - "email_address": {"S": "info@andreasknuth.de"}, - "forwards": {"L": [ - {"S": "andreas@andreasknuth.de"} - ]} - }' \ - --region us-east-2 -``` - -### Blocked Senders -```bash -# Tabelle erstellen (falls nicht vorhanden) -aws dynamodb create-table \ - --table-name email-blocked-senders \ - --attribute-definitions AttributeName=email_address,AttributeType=S \ - --key-schema AttributeName=email_address,KeyType=HASH \ - --billing-mode PAY_PER_REQUEST \ - --region us-east-2 - -# Blocklist hinzufügen -aws dynamodb put-item \ - --table-name email-blocked-senders \ - --item '{ - "email_address": {"S": "andreas@andreasknuth.de"}, - "blocked_patterns": {"L": [ - {"S": "*@spam.com"}, - {"S": "noreply@*.marketing.com"} - ]} - }' \ - --region us-east-2 -``` - -## 🐛 Troubleshooting - -### Worker startet nicht -```bash -# Logs prüfen -docker-compose logs unified-worker - -# Container Status -docker ps -a | grep unified - -# Manuell starten (Debug) -docker-compose run --rm unified-worker python3 main.py -``` - -### Keine Emails werden verarbeitet -```bash -# Queue URLs prüfen -curl http://localhost:8080/domains - -# AWS Permissions prüfen -aws sqs list-queues --region us-east-2 - -# DynamoDB Verbindung prüfen (in Logs) -docker-compose logs | grep "DynamoDB" -``` - -### Bounces werden nicht umgeschrieben -```bash -# DynamoDB Bounce Records prüfen -aws dynamodb scan \ - --table-name ses-outbound-messages \ - --limit 5 \ - --region us-east-2 - -# Worker Logs nach "Bounce detected" durchsuchen -docker-compose logs | grep "Bounce detected" -``` - -### SMTP Delivery Fehler -```bash -# SMTP Verbindung testen -docker-compose exec unified-worker nc -zv localhost 25 - -# Worker Logs -docker-compose logs | grep "SMTP" -``` - -## 📈 Performance Tuning - -### Mehr Worker Threads -```bash -# In .env -WORKER_THREADS=20 # Default: 10 -``` - -### Längeres Polling -```bash -# In .env -POLL_INTERVAL=30 # Default: 20 (Sekunden) -``` - -### Größerer Connection Pool -```bash -# In .env -SMTP_POOL_SIZE=10 # Default: 5 -``` - -### LMTP für bessere Performance -```bash -# In .env -LMTP_ENABLED=true -LMTP_PORT=24 -``` - -## 🔄 Migration vom Monolithen - -### Side-by-Side Deployment -```bash -# Alte Version läuft als "unified-email-worker-old" -# Neue Version als "unified-email-worker" - -# domains.txt aufteilen: -# old: andreasknuth.de -# new: andere-domain.de - -# Nach Verifizierung alle Domains auf new migrieren -``` - -### Zero-Downtime Switch -```bash -# 1. Neue Version starten (andere Domains) -docker-compose up -d - -# 2. Beide parallel laufen lassen (24h) -# 3. Monitoring: Metrics vergleichen -curl http://localhost:8000/metrics - -# 4. Alte Version stoppen -docker stop unified-email-worker-old - -# 5. domains.txt updaten (alle Domains) -# 6. Neue Version neustarten -docker-compose restart -``` - -## ✅ Checkliste nach Deployment - -- [ ] Container läuft: `docker ps | grep unified` -- [ ] Health Check OK: `curl http://localhost:8080/health` -- [ ] Domains geladen: `curl http://localhost:8080/domains` -- [ ] Logs ohne Fehler: `docker-compose logs | grep ERROR` -- [ ] Test-Email erfolgreich: Email an Test-Adresse senden -- [ ] Bounce Rewriting funktioniert: Bounce-Email testen -- [ ] Metrics erreichbar: `curl http://localhost:8000/metrics` -- [ ] DynamoDB Tables vorhanden: AWS Console prüfen - -## 📞 Support - -Bei Problemen: -1. Logs prüfen: `docker-compose logs -f` -2. Health Check: `curl http://localhost:8080/health` -3. AWS Console: Queues, S3 Buckets, DynamoDB prüfen -4. Container neu starten: `docker-compose restart` diff --git a/email-worker/docs/README.md b/email-worker/docs/README.md deleted file mode 100644 index e71f0ed..0000000 --- a/email-worker/docs/README.md +++ /dev/null @@ -1,306 +0,0 @@ -# Unified Email Worker (Modular Version) - -Multi-domain email processing worker for AWS SES/S3/SQS with bounce handling, auto-replies, forwarding, and sender blocking. - -## 🏗️ Architecture - -``` -email-worker/ -├── config.py # Configuration management -├── logger.py # Structured logging -├── aws/ # AWS service handlers -│ ├── s3_handler.py # S3 operations (download, metadata) -│ ├── sqs_handler.py # SQS polling -│ ├── ses_handler.py # SES email sending -│ └── dynamodb_handler.py # DynamoDB (rules, bounces, blocklist) -├── email_processing/ # Email processing -│ ├── parser.py # Email parsing utilities -│ ├── bounce_handler.py # Bounce detection & rewriting -│ ├── rules_processor.py # OOO & forwarding logic -│ └── blocklist.py # Sender blocking with wildcards -├── smtp/ # SMTP delivery -│ ├── pool.py # Connection pooling -│ └── delivery.py # SMTP/LMTP delivery with retry -├── metrics/ # Monitoring -│ └── prometheus.py # Prometheus metrics -├── worker.py # Message processing logic -├── domain_poller.py # Domain queue poller -├── unified_worker.py # Main worker coordinator -├── health_server.py # Health check HTTP server -└── main.py # Entry point -``` - -## ✨ Features - -- ✅ **Multi-Domain Processing**: Parallel processing of multiple domains via thread pool -- ✅ **Bounce Detection**: Automatic SES bounce notification rewriting -- ✅ **Auto-Reply/OOO**: Out-of-office automatic replies -- ✅ **Email Forwarding**: Rule-based forwarding to internal/external addresses -- ✅ **Sender Blocking**: Wildcard-based sender blocklist per recipient -- ✅ **SMTP Connection Pooling**: Efficient reuse of connections -- ✅ **LMTP Support**: Direct delivery to Dovecot (bypasses Postfix transport_maps) -- ✅ **Prometheus Metrics**: Comprehensive monitoring -- ✅ **Health Checks**: HTTP health endpoint for container orchestration -- ✅ **Graceful Shutdown**: Proper cleanup on SIGTERM/SIGINT - -## 🔧 Configuration - -All configuration via environment variables: - -### AWS Settings -```bash -AWS_REGION=us-east-2 -``` - -### Domains -```bash -# Option 1: Comma-separated list -DOMAINS=example.com,another.com - -# Option 2: File with one domain per line -DOMAINS_FILE=/etc/email-worker/domains.txt -``` - -### Worker Settings -```bash -WORKER_THREADS=10 -POLL_INTERVAL=20 # SQS long polling (seconds) -MAX_MESSAGES=10 # Max messages per poll -VISIBILITY_TIMEOUT=300 # Message visibility timeout (seconds) -``` - -### SMTP Delivery -```bash -SMTP_HOST=localhost -SMTP_PORT=25 -SMTP_USE_TLS=false -SMTP_USER= -SMTP_PASS= -SMTP_POOL_SIZE=5 -INTERNAL_SMTP_PORT=2525 # Port for internal delivery (bypasses transport_maps) -``` - -### LMTP (Direct Dovecot Delivery) -```bash -LMTP_ENABLED=false # Set to 'true' to use LMTP -LMTP_HOST=localhost -LMTP_PORT=24 -``` - -### DynamoDB Tables -```bash -DYNAMODB_RULES_TABLE=email-rules -DYNAMODB_MESSAGES_TABLE=ses-outbound-messages -DYNAMODB_BLOCKED_TABLE=email-blocked-senders -``` - -### Bounce Handling -```bash -BOUNCE_LOOKUP_RETRIES=3 -BOUNCE_LOOKUP_DELAY=1.0 -``` - -### Monitoring -```bash -METRICS_PORT=8000 # Prometheus metrics -HEALTH_PORT=8080 # Health check endpoint -``` - -## 📊 DynamoDB Schemas - -### email-rules -```json -{ - "email_address": "user@example.com", // Partition Key - "ooo_active": true, - "ooo_message": "I am currently out of office...", - "ooo_content_type": "text", // "text" or "html" - "forwards": ["other@example.com", "external@gmail.com"] -} -``` - -### ses-outbound-messages -```json -{ - "MessageId": "abc123...", // Partition Key (SES Message-ID) - "original_source": "sender@example.com", - "recipients": ["recipient@other.com"], - "timestamp": "2025-01-01T12:00:00Z", - "bounceType": "Permanent", - "bounceSubType": "General", - "bouncedRecipients": ["recipient@other.com"] -} -``` - -### email-blocked-senders -```json -{ - "email_address": "user@example.com", // Partition Key - "blocked_patterns": [ - "spam@*.com", // Wildcard support - "noreply@badsite.com", - "*@malicious.org" - ] -} -``` - -## 🚀 Usage - -### Installation -```bash -cd email-worker -pip install -r requirements.txt -``` - -### Run -```bash -python3 main.py -``` - -### Docker -```dockerfile -FROM python:3.11-slim - -WORKDIR /app -COPY . /app - -RUN pip install --no-cache-dir -r requirements.txt - -CMD ["python3", "main.py"] -``` - -## 📈 Metrics - -Available at `http://localhost:8000/metrics`: - -- `emails_processed_total{domain, status}` - Total emails processed -- `emails_in_flight` - Currently processing emails -- `email_processing_seconds{domain}` - Processing time histogram -- `queue_messages_available{domain}` - Queue size gauge -- `bounces_processed_total{domain, type}` - Bounce notifications -- `autoreplies_sent_total{domain}` - Auto-replies sent -- `forwards_sent_total{domain}` - Forwards sent -- `blocked_senders_total{domain}` - Blocked emails - -## 🏥 Health Checks - -Available at `http://localhost:8080/health`: - -```json -{ - "status": "healthy", - "domains": 5, - "domain_list": ["example.com", "another.com"], - "dynamodb": true, - "features": { - "bounce_rewriting": true, - "auto_reply": true, - "forwarding": true, - "blocklist": true, - "lmtp": false - }, - "timestamp": "2025-01-22T10:00:00.000000" -} -``` - -## 🔍 Key Improvements in Modular Version - -### 1. **Fixed Critical Bugs** -- ✅ Fixed `signal.SIGINT` typo (was `signalIGINT`) -- ✅ Proper S3 metadata before deletion (audit trail) -- ✅ Batch DynamoDB calls for blocklist (performance) -- ✅ Error handling for S3 delete failures - -### 2. **Better Architecture** -- **Separation of Concerns**: Each component has single responsibility -- **Testability**: Easy to unit test individual components -- **Maintainability**: Changes isolated to specific modules -- **Extensibility**: Easy to add new features - -### 3. **Performance** -- **Batch Blocklist Checks**: One DynamoDB call for all recipients -- **Connection Pooling**: Reusable SMTP connections -- **Efficient Metrics**: Optional Prometheus integration - -### 4. **Reliability** -- **Proper Error Handling**: Each component handles its own errors -- **Graceful Degradation**: Works even if DynamoDB unavailable -- **Audit Trail**: All actions logged to S3 metadata - -## 🔐 Security Features - -1. **Domain Validation**: Workers only process their assigned domains -2. **Loop Prevention**: Detects and skips already-processed emails -3. **Blocklist Support**: Wildcard-based sender blocking -4. **Internal vs External**: Separate handling prevents loops - -## 📝 Example Usage - -### Enable OOO for user -```python -import boto3 - -dynamodb = boto3.resource('dynamodb') -table = dynamodb.Table('email-rules') - -table.put_item(Item={ - 'email_address': 'john@example.com', - 'ooo_active': True, - 'ooo_message': 'I am out of office until Feb 1st.', - 'ooo_content_type': 'html' -}) -``` - -### Block spam senders -```python -table = dynamodb.Table('email-blocked-senders') - -table.put_item(Item={ - 'email_address': 'john@example.com', - 'blocked_patterns': [ - '*@spam.com', - 'noreply@*.marketing.com', - 'newsletter@*' - ] -}) -``` - -### Forward emails -```python -table = dynamodb.Table('email-rules') - -table.put_item(Item={ - 'email_address': 'support@example.com', - 'forwards': [ - 'john@example.com', - 'jane@example.com', - 'external@gmail.com' - ] -}) -``` - -## 🐛 Troubleshooting - -### Worker not processing emails -1. Check queue URLs: `curl http://localhost:8080/domains` -2. Check logs for SQS errors -3. Verify IAM permissions for SQS/S3/SES/DynamoDB - -### Bounces not rewritten -1. Check DynamoDB table name: `DYNAMODB_MESSAGES_TABLE` -2. Verify Lambda function is writing bounce records -3. Check logs for DynamoDB lookup errors - -### Auto-replies not sent -1. Verify DynamoDB rules table accessible -2. Check `ooo_active` is `true` (boolean, not string) -3. Review logs for SES send errors - -### Blocked emails still delivered -1. Verify blocklist table exists and is accessible -2. Check wildcard patterns are lowercase -3. Review logs for blocklist check errors - -## 📄 License - -MIT License - See LICENSE file for details diff --git a/email-worker/docs/SUMMARY.md b/email-worker/docs/SUMMARY.md deleted file mode 100644 index ea306e8..0000000 --- a/email-worker/docs/SUMMARY.md +++ /dev/null @@ -1,247 +0,0 @@ -# 📋 Refactoring Summary - -## ✅ Critical Bugs Fixed - -### 1. **Signal Handler Typo** (CRITICAL) -**Old:** -```python -signal.signal(signalIGINT, signal_handler) # ❌ NameError at startup -``` -**New:** -```python -signal.signal(signal.SIGINT, signal_handler) # ✅ Fixed -``` -**Impact:** Worker couldn't start due to Python syntax error - ---- - -### 2. **Missing Audit Trail for Blocked Emails** (HIGH) -**Old:** -```python -if all_blocked: - s3.delete_object(Bucket=bucket, Key=key) # ❌ No metadata -``` -**New:** -```python -if all_blocked: - s3.mark_as_blocked(domain, key, blocked, sender, worker) # ✅ Metadata first - s3.delete_blocked_email(domain, key, worker) # ✅ Then delete -``` -**Impact:** -- ❌ No compliance trail (who blocked, when, why) -- ❌ Impossible to troubleshoot -- ✅ Now: Full audit trail in S3 metadata before deletion - ---- - -### 3. **Inefficient DynamoDB Calls** (MEDIUM - Performance) -**Old:** -```python -for recipient in recipients: - patterns = dynamodb.get_item(Key={'email_address': recipient}) # N calls! - if is_blocked(patterns, sender): - blocked.append(recipient) -``` -**New:** -```python -# 1 batch call for all recipients -patterns_map = dynamodb.batch_get_blocked_patterns(recipients) -for recipient in recipients: - if is_blocked(patterns_map[recipient], sender): - blocked.append(recipient) -``` -**Impact:** -- Old: 10 recipients = 10 DynamoDB calls = higher latency + costs -- New: 10 recipients = 1 DynamoDB call = **10x faster, 10x cheaper** - ---- - -### 4. **S3 Delete Error Handling** (MEDIUM) -**Old:** -```python -try: - s3.delete_object(...) -except Exception as e: - log(f"Failed: {e}") - # ❌ Queue message still deleted → inconsistent state -return True -``` -**New:** -```python -try: - s3.mark_as_blocked(...) - s3.delete_blocked_email(...) -except Exception as e: - log(f"Failed: {e}") - return False # ✅ Keep in queue for retry -``` -**Impact:** Prevents orphaned S3 objects when delete fails - ---- - -## 🏗️ Architecture Improvements - -### Modular Structure -``` -Before: 1 file, 800+ lines -After: 27 files, ~150 lines each -``` - -| Module | Responsibility | LOC | -|--------|---------------|-----| -| `config.py` | Configuration management | 85 | -| `logger.py` | Structured logging | 20 | -| `aws/s3_handler.py` | S3 operations | 180 | -| `aws/sqs_handler.py` | SQS polling | 95 | -| `aws/ses_handler.py` | SES sending | 45 | -| `aws/dynamodb_handler.py` | DynamoDB access | 175 | -| `email_processing/parser.py` | Email parsing | 75 | -| `email_processing/bounce_handler.py` | Bounce detection | 95 | -| `email_processing/blocklist.py` | Sender blocking | 90 | -| `email_processing/rules_processor.py` | OOO & forwarding | 285 | -| `smtp/pool.py` | Connection pooling | 110 | -| `smtp/delivery.py` | SMTP/LMTP delivery | 165 | -| `metrics/prometheus.py` | Metrics collection | 140 | -| `worker.py` | Message processing | 265 | -| `domain_poller.py` | Queue polling | 105 | -| `unified_worker.py` | Worker coordination | 180 | -| `health_server.py` | Health checks | 85 | -| `main.py` | Entry point | 45 | - -**Total:** ~2,420 lines (well-organized vs 800 spaghetti) - ---- - -## 🎯 Benefits Summary - -### Maintainability -- ✅ **Single Responsibility**: Each class has one job -- ✅ **Easy to Navigate**: Find code by feature -- ✅ **Reduced Coupling**: Changes isolated to modules -- ✅ **Better Documentation**: Each module documented - -### Testability -- ✅ **Unit Testing**: Mock `S3Handler`, test `BounceHandler` independently -- ✅ **Integration Testing**: Test components in isolation -- ✅ **Faster CI/CD**: Test only changed modules - -### Performance -- ✅ **Batch Operations**: 10x fewer DynamoDB calls -- ✅ **Connection Pooling**: Reuse SMTP connections -- ✅ **Parallel Processing**: One thread per domain - -### Reliability -- ✅ **Error Isolation**: Errors in one module don't crash others -- ✅ **Comprehensive Logging**: Structured, searchable logs -- ✅ **Audit Trail**: All actions recorded in S3 metadata -- ✅ **Graceful Degradation**: Works even if DynamoDB down - -### Extensibility -Adding new features is now easy: - -**Example: Add DKIM Signing** -1. Create `email_processing/dkim_signer.py` -2. Add to `worker.py`: `signed_bytes = dkim.sign(raw_bytes)` -3. Done! No touching 800-line monolith - ---- - -## 📊 Performance Comparison - -| Metric | Monolith | Modular | Improvement | -|--------|----------|---------|-------------| -| DynamoDB Calls/Email | N (per recipient) | 1 (batch) | **10x reduction** | -| SMTP Connections/Email | 1 (new each time) | Pooled (reused) | **5x fewer** | -| Startup Time | ~2s | ~1s | **2x faster** | -| Memory Usage | ~150MB | ~120MB | **20% less** | -| Lines per Feature | Mixed in 800 | ~100-150 | **Clearer** | - ---- - -## 🔒 Security Improvements - -1. **Audit Trail**: Every action logged with timestamp, worker ID -2. **Domain Validation**: Workers only process assigned domains -3. **Loop Prevention**: Detects recursive processing -4. **Blocklist**: Per-recipient wildcard blocking -5. **Separate Internal Routing**: Prevents SES loops - ---- - -## 📝 Migration Path - -### Zero Downtime Migration -1. Deploy modular version alongside monolith -2. Route half domains to new worker -3. Monitor metrics, logs for issues -4. Gradually shift all traffic -5. Decommission monolith - -### Rollback Strategy -- Same environment variables -- Same DynamoDB schema -- Easy to switch back if needed - ---- - -## 🎓 Code Quality Metrics - -### Complexity Reduction -- **Cyclomatic Complexity**: Reduced from 45 → 8 per function -- **Function Length**: Max 50 lines (was 200+) -- **File Length**: Max 285 lines (was 800+) - -### Code Smells Removed -- ❌ God Object (1 class doing everything) -- ❌ Long Methods (200+ line functions) -- ❌ Duplicate Code (3 copies of S3 metadata update) -- ❌ Magic Numbers (hardcoded retry counts) - -### Best Practices Added -- ✅ Type Hints (where appropriate) -- ✅ Docstrings (all public methods) -- ✅ Logging (structured, consistent) -- ✅ Error Handling (specific exceptions) - ---- - -## 🚀 Next Steps - -### Recommended Follow-ups -1. **Add Unit Tests**: Use `pytest` with mocked AWS services -2. **CI/CD Pipeline**: Automated testing and deployment -3. **Monitoring Dashboard**: Grafana + Prometheus -4. **Alert Rules**: Notify on high error rates -5. **Load Testing**: Verify performance at scale - -### Future Enhancements (Easy to Add Now!) -- **DKIM Signing**: New module in `email/` -- **Spam Filtering**: New module in `email/` -- **Rate Limiting**: New module in `smtp/` -- **Queue Prioritization**: Modify `domain_poller.py` -- **Multi-Region**: Add region config - ---- - -## 📚 Documentation - -All documentation included: - -- **README.md**: Features, configuration, usage -- **ARCHITECTURE.md**: System design, data flows -- **MIGRATION.md**: Step-by-step migration guide -- **SUMMARY.md**: This file - key improvements -- **Code Comments**: Inline documentation -- **Docstrings**: All public methods documented - ---- - -## ✨ Key Takeaway - -The refactoring transforms a **fragile 800-line monolith** into a **robust, modular system** that is: -- **Faster** (batch operations) -- **Safer** (better error handling, audit trail) -- **Easier to maintain** (clear structure) -- **Ready to scale** (extensible architecture) - -All while **fixing 4 critical bugs** and maintaining **100% backwards compatibility**. diff --git a/email-worker/domain_poller.py b/email-worker/domain_poller.py deleted file mode 100644 index 35ca9e4..0000000 --- a/email-worker/domain_poller.py +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env python3 -""" -Domain queue poller -""" - -import json -import time -import threading -import traceback - -from logger import log -from aws import SQSHandler -from worker import MessageProcessor -from metrics.prometheus import MetricsCollector - - -class DomainPoller: - """Polls SQS queue for a single domain""" - - def __init__( - self, - domain: str, - queue_url: str, - message_processor: MessageProcessor, - sqs: SQSHandler, - metrics: MetricsCollector, - stop_event: threading.Event, - stats_dict: dict, - stats_lock: threading.Lock - ): - self.domain = domain - self.queue_url = queue_url - self.processor = message_processor - self.sqs = sqs - self.metrics = metrics - self.stop_event = stop_event - self.stats_dict = stats_dict - self.stats_lock = stats_lock - self.worker_name = f"worker-{domain}" - self.messages_processed = 0 - - def poll(self): - """Main polling loop""" - log(f"🚀 Starting poller for {self.domain}", 'INFO', self.worker_name) - - while not self.stop_event.is_set(): - try: - # Receive messages from queue - messages = self.sqs.receive_messages(self.queue_url) - - # Update queue size metric - if self.metrics: - queue_size = self.sqs.get_queue_size(self.queue_url) - self.metrics.set_queue_size(self.domain, queue_size) - - if not messages: - continue - - log(f"✉ Received {len(messages)} message(s)", 'INFO', self.worker_name) - - for message in messages: - if self.stop_event.is_set(): - break - - receipt_handle = message['ReceiptHandle'] - receive_count = int(message.get('Attributes', {}).get('ApproximateReceiveCount', 1)) - - if self.metrics: - self.metrics.increment_in_flight() - start_time = time.time() - - try: - success = self.processor.process_message(self.domain, message, receive_count) - - if success: - self.sqs.delete_message(self.queue_url, receipt_handle) - self.messages_processed += 1 - - # Update shared stats - with self.stats_lock: - self.stats_dict[self.domain] = self.messages_processed - else: - log( - f"⚠ Retry queued (attempt {receive_count}/3)", - 'WARNING', - self.worker_name - ) - - except json.JSONDecodeError as e: - log(f"✗ Invalid message format: {e}", 'ERROR', self.worker_name) - self.sqs.delete_message(self.queue_url, receipt_handle) - - except Exception as e: - log(f"✗ Error processing message: {e}", 'ERROR', self.worker_name) - traceback.print_exc() - - finally: - if self.metrics: - self.metrics.decrement_in_flight() - self.metrics.observe_processing_time( - self.domain, - time.time() - start_time - ) - - except Exception as e: - log(f"✗ Error polling: {e}", 'ERROR', self.worker_name) - time.sleep(5) - - log(f"👋 Stopped (processed: {self.messages_processed})", 'INFO', self.worker_name) diff --git a/email-worker/domains.txt b/email-worker/domains.txt deleted file mode 100644 index ec8aa40..0000000 --- a/email-worker/domains.txt +++ /dev/null @@ -1,6 +0,0 @@ -# domains.txt - Liste aller zu verarbeitenden Domains -# Eine Domain pro Zeile -# Zeilen mit # werden ignoriert - -# Production Domains -andreasknuth.de diff --git a/email-worker/email_processing/__init__.py b/email-worker/email_processing/__init__.py deleted file mode 100644 index 2775518..0000000 --- a/email-worker/email_processing/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python3 -""" -Email processing components -""" - -from .parser import EmailParser -from .bounce_handler import BounceHandler -from .rules_processor import RulesProcessor -from .blocklist import BlocklistChecker - -__all__ = ['EmailParser', 'BounceHandler', 'RulesProcessor', 'BlocklistChecker'] diff --git a/email-worker/email_processing/blocklist.py b/email-worker/email_processing/blocklist.py deleted file mode 100644 index a5dea0c..0000000 --- a/email-worker/email_processing/blocklist.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python3 -""" -Sender blocklist checking with wildcard support -""" - -import fnmatch -from typing import List, Dict -from email.utils import parseaddr - -from logger import log -from aws.dynamodb_handler import DynamoDBHandler - - -class BlocklistChecker: - """Checks if senders are blocked""" - - def __init__(self, dynamodb: DynamoDBHandler): - self.dynamodb = dynamodb - - def is_sender_blocked( - self, - recipient: str, - sender: str, - worker_name: str - ) -> bool: - """ - Check if sender is blocked for this recipient - - Args: - recipient: Recipient email address - sender: Sender email address (may include name) - worker_name: Worker name for logging - - Returns: - True if sender is blocked - """ - patterns = self.dynamodb.get_blocked_patterns(recipient) - - if not patterns: - return False - - sender_clean = parseaddr(sender)[1].lower() - - for pattern in patterns: - if fnmatch.fnmatch(sender_clean, pattern.lower()): - log( - f"⛔ BLOCKED: Sender {sender_clean} matches pattern '{pattern}' " - f"for inbox {recipient}", - 'WARNING', - worker_name - ) - return True - - return False - - def batch_check_blocked_senders( - self, - recipients: List[str], - senders: List[str], # <-- Geändert: Erwartet nun eine Liste - worker_name: str - ) -> Dict[str, bool]: - """ - Batch check if ANY of the senders are blocked for multiple recipients (more efficient) - - Args: - recipients: List of recipient email addresses - senders: List of sender email addresses (Envelope & Header) - worker_name: Worker name for logging - - Returns: - Dictionary mapping recipient -> is_blocked (bool) - """ - # Get all blocked patterns in one batch call - patterns_by_recipient = self.dynamodb.batch_get_blocked_patterns(recipients) - - # Alle übergebenen Adressen bereinigen - senders_clean = [parseaddr(s)[1].lower() for s in senders if s] - result = {} - - for recipient in recipients: - patterns = patterns_by_recipient.get(recipient, []) - - is_blocked = False - for pattern in patterns: - for sender_clean in senders_clean: - if fnmatch.fnmatch(sender_clean, pattern.lower()): - log( - f"⛔ BLOCKED: Sender {sender_clean} matches pattern '{pattern}' " - f"for inbox {recipient}", - 'WARNING', - worker_name - ) - is_blocked = True - break # Bricht die Senders-Schleife ab - if is_blocked: - break # Bricht die Pattern-Schleife ab - - result[recipient] = is_blocked - - return result diff --git a/email-worker/email_processing/bounce_handler.py b/email-worker/email_processing/bounce_handler.py deleted file mode 100644 index 625612d..0000000 --- a/email-worker/email_processing/bounce_handler.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env python3 -""" -Bounce detection and header rewriting -""" - -from typing import Tuple, Any - -from logger import log -from aws.dynamodb_handler import DynamoDBHandler - - -class BounceHandler: - """Handles bounce detection and header rewriting""" - - def __init__(self, dynamodb: DynamoDBHandler): - self.dynamodb = dynamodb - - @staticmethod - def is_ses_bounce_notification(parsed_email) -> bool: - """Check if email is from SES MAILER-DAEMON""" - try: - from_header = (parsed_email.get('From') or '').lower() - except (AttributeError, TypeError, KeyError): - # Malformed From header - safely extract raw value - try: - from_header = str(parsed_email.get_all('From', [''])[0]).lower() - except: - from_header = '' - - return 'mailer-daemon@' in from_header and 'amazonses.com' in from_header - - def apply_bounce_logic( - self, - parsed, - subject: str, - worker_name: str = 'unified' - ) -> Tuple[Any, bool]: - """ - Check for SES Bounce, lookup in DynamoDB and rewrite headers - - Args: - parsed: Parsed email message object - subject: Email subject - worker_name: Worker name for logging - - Returns: - Tuple of (parsed_email_object, was_modified_bool) - """ - if not self.is_ses_bounce_notification(parsed): - return parsed, False - - log("🔍 Detected SES MAILER-DAEMON bounce notification", 'INFO', worker_name) - - # Extract Message-ID from header - message_id = (parsed.get('Message-ID') or '').strip('<>').split('@')[0] - - if not message_id: - log("⚠ Could not extract Message-ID from bounce notification", 'WARNING', worker_name) - return parsed, False - - log(f" Looking up Message-ID: {message_id}", 'INFO', worker_name) - - # Lookup in DynamoDB - bounce_info = self.dynamodb.get_bounce_info(message_id, worker_name) - - if not bounce_info: - return parsed, False - - # Bounce Info ausgeben - original_source = bounce_info['original_source'] - bounced_recipients = bounce_info['bouncedRecipients'] - bounce_type = bounce_info['bounceType'] - bounce_subtype = bounce_info['bounceSubType'] - - log(f"✓ Found bounce info:", 'INFO', worker_name) - log(f" Original sender: {original_source}", 'INFO', worker_name) - log(f" Bounce type: {bounce_type}/{bounce_subtype}", 'INFO', worker_name) - log(f" Bounced recipients: {bounced_recipients}", 'INFO', worker_name) - - if bounced_recipients: - new_from = bounced_recipients[0] - - # Rewrite Headers - parsed['X-Original-SES-From'] = parsed.get('From', '') - parsed['X-Bounce-Type'] = f"{bounce_type}/{bounce_subtype}" - parsed.replace_header('From', new_from) - - if not parsed.get('Reply-To'): - parsed['Reply-To'] = new_from - - # Subject anpassen - if 'delivery status notification' in subject.lower() or 'thanks for your submission' in subject.lower(): - parsed.replace_header('Subject', f"Delivery Status: {new_from}") - - log(f"✓ Rewritten FROM: {new_from}", 'SUCCESS', worker_name) - return parsed, True - - log("⚠ No bounced recipients found in bounce info", 'WARNING', worker_name) - return parsed, False \ No newline at end of file diff --git a/email-worker/email_processing/parser.py b/email-worker/email_processing/parser.py deleted file mode 100644 index 0c554f8..0000000 --- a/email-worker/email_processing/parser.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 -""" -Email parsing utilities -""" - -from typing import Tuple, Optional -from email.parser import BytesParser -from email.policy import SMTP as SMTPPolicy - - -class EmailParser: - """Email parsing utilities""" - - @staticmethod - def parse_bytes(raw_bytes: bytes): - """Parse raw email bytes into email.message object""" - return BytesParser(policy=SMTPPolicy).parsebytes(raw_bytes) - - @staticmethod - def extract_body_parts(parsed) -> Tuple[str, Optional[str]]: - """ - Extract both text/plain and text/html body parts - - Args: - parsed: Parsed email message object - - Returns: - Tuple of (text_body, html_body or None) - """ - text_body = '' - html_body = None - - if parsed.is_multipart(): - for part in parsed.walk(): - content_type = part.get_content_type() - - if content_type == 'text/plain': - try: - text_body += part.get_payload(decode=True).decode('utf-8', errors='ignore') - except Exception: - pass - - elif content_type == 'text/html': - try: - html_body = part.get_payload(decode=True).decode('utf-8', errors='ignore') - except Exception: - pass - else: - try: - payload = parsed.get_payload(decode=True) - if payload: - decoded = payload.decode('utf-8', errors='ignore') - if parsed.get_content_type() == 'text/html': - html_body = decoded - else: - text_body = decoded - except Exception: - text_body = str(parsed.get_payload()) - - return text_body.strip() if text_body else '(No body content)', html_body - - @staticmethod - def is_processed_by_worker(parsed) -> bool: - """ - Check if email was already processed by our worker (loop detection) - - Args: - parsed: Parsed email message object - - Returns: - True if already processed - """ - x_worker_processed = parsed.get('X-SES-Worker-Processed', '') - auto_submitted = parsed.get('Auto-Submitted', '') - - # Only skip if OUR header is present - is_processed_by_us = bool(x_worker_processed) - is_our_auto_reply = auto_submitted == 'auto-replied' and x_worker_processed - - return is_processed_by_us or is_our_auto_reply diff --git a/email-worker/email_processing/rules_processor.py b/email-worker/email_processing/rules_processor.py deleted file mode 100644 index 743ee34..0000000 --- a/email-worker/email_processing/rules_processor.py +++ /dev/null @@ -1,365 +0,0 @@ -#!/usr/bin/env python3 -""" -Email rules processing (Auto-Reply/OOO and Forwarding) -""" - -import smtplib -from email.mime.text import MIMEText -from email.mime.multipart import MIMEMultipart -from email.utils import parseaddr, formatdate, make_msgid -from botocore.exceptions import ClientError - -from logger import log -from config import config, is_internal_address -from aws.dynamodb_handler import DynamoDBHandler -from aws.ses_handler import SESHandler -from email_processing.parser import EmailParser - - -class RulesProcessor: - """Processes email rules (OOO, Forwarding)""" - - def __init__(self, dynamodb: DynamoDBHandler, ses: SESHandler): - self.dynamodb = dynamodb - self.ses = ses - - def process_rules_for_recipient( - self, - recipient: str, - parsed, - domain: str, - worker_name: str, - metrics_callback=None - ): - """ - Process OOO and Forward rules for a recipient - - Args: - recipient: Recipient email address - parsed: Parsed email message object - domain: Email domain - worker_name: Worker name for logging - metrics_callback: Optional callback to increment metrics - """ - rule = self.dynamodb.get_email_rules(recipient.lower()) - - if not rule: - return False # NEU: Return-Wert - - original_from = parsed.get('From', '') - sender_name, sender_addr = parseaddr(original_from) - if not sender_addr: - sender_addr = original_from - - # ============================================ - # OOO / Auto-Reply handling - # ============================================ - if rule.get('ooo_active', False): - self._handle_ooo( - recipient, - parsed, - sender_addr, - rule, - domain, - worker_name, - metrics_callback - ) - - # ============================================ - # Forward handling - # ============================================ - forwards = rule.get('forwards', []) - has_legacy_forward = False # NEU - - if forwards: - if rule.get('forward_smtp_override'): - has_legacy_forward = True # NEU - self._handle_forwards( - recipient, parsed, original_from, forwards, - domain, worker_name, metrics_callback, rule=rule - ) - return has_legacy_forward # NEU: statt kein Return - - def _handle_ooo( - self, - recipient: str, - parsed, - sender_addr: str, - rule: dict, - domain: str, - worker_name: str, - metrics_callback=None - ): - """Handle Out-of-Office auto-reply""" - # Don't reply to automatic messages - auto_submitted = parsed.get('Auto-Submitted', '') - precedence = (parsed.get('Precedence') or '').lower() - - if auto_submitted and auto_submitted != 'no': - log(f" ⏭ Skipping OOO for auto-submitted message", 'INFO', worker_name) - return - - if precedence in ['bulk', 'junk', 'list']: - log(f" ⏭ Skipping OOO for {precedence} message", 'INFO', worker_name) - return - - if any(x in sender_addr.lower() for x in ['noreply', 'no-reply', 'mailer-daemon']): - log(f" ⏭ Skipping OOO for noreply address", 'INFO', worker_name) - return - - try: - ooo_msg = rule.get('ooo_message', 'I am out of office.') - content_type = rule.get('ooo_content_type', 'text') - ooo_reply = self._create_ooo_reply(parsed, recipient, ooo_msg, content_type) - ooo_bytes = ooo_reply.as_bytes() - - # Distinguish: Internal (Port 2525) vs External (SES) - if is_internal_address(sender_addr): - # Internal address → direct via Port 2525 - success = self._send_internal_email(recipient, sender_addr, ooo_bytes, worker_name) - if success: - log(f"✓ Sent OOO reply internally to {sender_addr}", 'SUCCESS', worker_name) - else: - log(f"⚠ Internal OOO reply failed to {sender_addr}", 'WARNING', worker_name) - else: - # External address → via SES - success = self.ses.send_raw_email(recipient, sender_addr, ooo_bytes, worker_name) - if success: - log(f"✓ Sent OOO reply externally to {sender_addr} via SES", 'SUCCESS', worker_name) - - if metrics_callback: - metrics_callback('autoreply', domain) - - except Exception as e: - log(f"⚠ OOO reply failed to {sender_addr}: {e}", 'ERROR', worker_name) - - - def _handle_forwards( - self, - recipient: str, - parsed, - original_from: str, - forwards: list, - domain: str, - worker_name: str, - metrics_callback=None, - rule: dict = None - ): - """Handle email forwarding""" - smtp_override = None - if rule: - smtp_override = rule.get('forward_smtp_override') - - for forward_to in forwards: - try: - if smtp_override: - # Migration: Original-Mail unverändert weiterleiten - raw_bytes = parsed.as_bytes() - success = self._send_via_legacy_smtp( - recipient, forward_to, raw_bytes, - smtp_override, worker_name - ) - if success: - log(f"✓ Forwarded via legacy SMTP to {forward_to} " - f"({smtp_override.get('host', '?')})", - 'SUCCESS', worker_name) - else: - log(f"⚠ Legacy SMTP forward failed to {forward_to}", - 'WARNING', worker_name) - else: - # Normaler Forward (neue FWD-Message) - fwd_msg = self._create_forward_message( - parsed, recipient, forward_to, original_from - ) - fwd_bytes = fwd_msg.as_bytes() - - if is_internal_address(forward_to): - success = self._send_internal_email( - recipient, forward_to, fwd_bytes, worker_name - ) - if success: - log(f"✓ Forwarded internally to {forward_to}", - 'SUCCESS', worker_name) - else: - log(f"⚠ Internal forward failed to {forward_to}", - 'WARNING', worker_name) - else: - success = self.ses.send_raw_email( - recipient, forward_to, fwd_bytes, worker_name - ) - if success: - log(f"✓ Forwarded externally to {forward_to} via SES", - 'SUCCESS', worker_name) - - if metrics_callback: - metrics_callback('forward', domain) - - except Exception as e: - log(f"⚠ Forward failed to {forward_to}: {e}", - 'ERROR', worker_name) - - @staticmethod - def _send_via_legacy_smtp( - from_addr: str, - to_addr: str, - raw_message: bytes, - smtp_config: dict, - worker_name: str - ) -> bool: - """ - Send email directly to a legacy SMTP server (for migration). - Bypasses SES completely to avoid mail loops. - """ - try: - host = smtp_config.get('host', '') - - # DynamoDB speichert Zahlen als Decimal, daher int() - port = int(smtp_config.get('port', 25)) - use_tls = smtp_config.get('tls', False) - username = smtp_config.get('username') - password = smtp_config.get('password') - - if not host: - log(f" ✗ Legacy SMTP: no host configured", 'ERROR', worker_name) - return False - - with smtplib.SMTP(host, port, timeout=30) as conn: - conn.ehlo() - if use_tls: - conn.starttls() - conn.ehlo() - if username and password: - conn.login(username, password) - conn.sendmail(from_addr, [to_addr], raw_message) - return True - - except Exception as e: - log( - f" ✗ Legacy SMTP failed ({smtp_config.get('host', '?')}:" - f"{smtp_config.get('port', '?')}): {e}", - 'ERROR', worker_name - ) - return False - - @staticmethod - def _send_internal_email(from_addr: str, to_addr: str, raw_message: bytes, worker_name: str) -> bool: - """ - Send email via internal SMTP port (bypasses transport_maps) - - Args: - from_addr: From address - to_addr: To address - raw_message: Raw MIME message bytes - worker_name: Worker name for logging - - Returns: - True on success, False on failure - """ - try: - with smtplib.SMTP(config.smtp_host, config.internal_smtp_port, timeout=30) as conn: - conn.ehlo() - conn.sendmail(from_addr, [to_addr], raw_message) - return True - except Exception as e: - log(f" ✗ Internal delivery failed to {to_addr}: {e}", 'ERROR', worker_name) - return False - - @staticmethod - def _create_ooo_reply(original_parsed, recipient: str, ooo_msg: str, content_type: str = 'text'): - """Create Out-of-Office reply as complete MIME message""" - text_body, html_body = EmailParser.extract_body_parts(original_parsed) - original_subject = original_parsed.get('Subject', '(no subject)') - original_from = original_parsed.get('From', 'unknown') - - msg = MIMEMultipart('mixed') - msg['From'] = recipient - msg['To'] = original_from - msg['Subject'] = f"Out of Office: {original_subject}" - msg['Date'] = formatdate(localtime=True) - msg['Message-ID'] = make_msgid(domain=recipient.split('@')[1]) - msg['In-Reply-To'] = original_parsed.get('Message-ID', '') - msg['References'] = original_parsed.get('Message-ID', '') - msg['Auto-Submitted'] = 'auto-replied' - msg['X-SES-Worker-Processed'] = 'ooo-reply' - - body_part = MIMEMultipart('alternative') - - # Text version - text_content = f"{ooo_msg}\n\n--- Original Message ---\n" - text_content += f"From: {original_from}\n" - text_content += f"Subject: {original_subject}\n\n" - text_content += text_body - body_part.attach(MIMEText(text_content, 'plain', 'utf-8')) - - # HTML version (if desired and original available) - if content_type == 'html' or html_body: - html_content = f"
{ooo_msg}



" - html_content += "Original Message
" - html_content += f"From: {original_from}
" - html_content += f"Subject: {original_subject}

" - html_content += (html_body if html_body else text_body.replace('\n', '
')) - body_part.attach(MIMEText(html_content, 'html', 'utf-8')) - - msg.attach(body_part) - return msg - - @staticmethod - def _create_forward_message(original_parsed, recipient: str, forward_to: str, original_from: str): - """Create Forward message as complete MIME message""" - original_subject = original_parsed.get('Subject', '(no subject)') - original_date = original_parsed.get('Date', 'unknown') - - msg = MIMEMultipart('mixed') - msg['From'] = recipient - msg['To'] = forward_to - msg['Subject'] = f"FWD: {original_subject}" - msg['Date'] = formatdate(localtime=True) - msg['Message-ID'] = make_msgid(domain=recipient.split('@')[1]) - msg['Reply-To'] = original_from - msg['X-SES-Worker-Processed'] = 'forwarded' - - text_body, html_body = EmailParser.extract_body_parts(original_parsed) - body_part = MIMEMultipart('alternative') - - # Text version - fwd_text = "---------- Forwarded message ---------\n" - fwd_text += f"From: {original_from}\n" - fwd_text += f"Date: {original_date}\n" - fwd_text += f"Subject: {original_subject}\n" - fwd_text += f"To: {recipient}\n\n" - fwd_text += text_body - body_part.attach(MIMEText(fwd_text, 'plain', 'utf-8')) - - # HTML version - if html_body: - fwd_html = "
" - fwd_html += "---------- Forwarded message ---------
" - fwd_html += f"From: {original_from}
" - fwd_html += f"Date: {original_date}
" - fwd_html += f"Subject: {original_subject}
" - fwd_html += f"To: {recipient}

" - fwd_html += html_body - fwd_html += "
" - body_part.attach(MIMEText(fwd_html, 'html', 'utf-8')) - - msg.attach(body_part) - - # Copy attachments - FIX FILENAMES - if original_parsed.is_multipart(): - for part in original_parsed.walk(): - if part.get_content_maintype() == 'multipart': - continue - if part.get_content_type() in ['text/plain', 'text/html']: - continue - - # Fix malformed filename in Content-Disposition - content_disp = part.get('Content-Disposition', '') - if 'filename=' in content_disp and '"' not in content_disp: - # Add quotes around filename with spaces - import re - fixed_disp = re.sub(r'filename=([^;"\s]+(?:\s+[^;"\s]+)*)', r'filename="\1"', content_disp) - part.replace_header('Content-Disposition', fixed_disp) - - msg.attach(part) - - return msg diff --git a/email-worker/health_server.py b/email-worker/health_server.py deleted file mode 100644 index 62eadb7..0000000 --- a/email-worker/health_server.py +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env python3 -""" -HTTP health check server -""" - -import sys -import json -import threading -from http.server import HTTPServer, BaseHTTPRequestHandler -from datetime import datetime - -from logger import log -from config import config - - -class SilentHTTPServer(HTTPServer): - """HTTP Server that ignores connection reset errors from scanners""" - - def handle_error(self, request, client_address): - exc_type = sys.exc_info()[0] - if exc_type in (ConnectionResetError, BrokenPipeError, ConnectionAbortedError): - pass # Silently ignore - these are just scanners/health checks disconnecting - else: - log(f"Health server error from {client_address[0]}: {sys.exc_info()[1]}", 'WARNING') - - -class HealthHandler(BaseHTTPRequestHandler): - """Health check request handler""" - - worker = None # Will be set by start_health_server() - dynamodb_available = False - - def do_GET(self): - if self.path == '/health' or self.path == '/': - self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.end_headers() - - status = { - 'status': 'healthy', - 'domains': len(self.worker.queue_urls) if self.worker else 0, - 'domain_list': list(self.worker.queue_urls.keys()) if self.worker else [], - 'dynamodb': self.dynamodb_available, - 'features': { - 'bounce_rewriting': True, - 'auto_reply': self.dynamodb_available, - 'forwarding': self.dynamodb_available, - 'blocklist': self.dynamodb_available, - 'lmtp': config.lmtp_enabled - }, - 'timestamp': datetime.utcnow().isoformat() - } - self.wfile.write(json.dumps(status, indent=2).encode()) - - elif self.path == '/domains': - self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.end_headers() - domain_list = list(self.worker.queue_urls.keys()) if self.worker else [] - self.wfile.write(json.dumps(domain_list).encode()) - - else: - self.send_response(404) - self.end_headers() - - def log_message(self, format, *args): - pass # Suppress HTTP access logs - - -def start_health_server(worker, dynamodb_available: bool): - """ - Start HTTP health check server - - Args: - worker: UnifiedWorker instance - dynamodb_available: Whether DynamoDB is available - """ - # Set class attributes for handler - HealthHandler.worker = worker - HealthHandler.dynamodb_available = dynamodb_available - - server = SilentHTTPServer(('0.0.0.0', config.health_port), HealthHandler) - thread = threading.Thread(target=server.serve_forever, daemon=True, name='health-server') - thread.start() - log(f"Health server on port {config.health_port}") diff --git a/email-worker/logger.py b/email-worker/logger.py deleted file mode 100644 index 5c83534..0000000 --- a/email-worker/logger.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env python3 -""" -Structured logging for email worker with Daily Rotation (Robust Version) -""" - -import os -import sys -import logging -import threading -from logging.handlers import TimedRotatingFileHandler - -# Konfiguration -LOG_DIR = "/var/log/email-worker" -LOG_FILE = os.path.join(LOG_DIR, "worker.log") - -# Logger initialisieren -logger = logging.getLogger("unified-worker") -logger.setLevel(logging.INFO) -logger.propagate = False - -# Formatierung -formatter = logging.Formatter( - '[%(asctime)s] [%(levelname)s] [%(threadName)s] %(message)s', - datefmt='%Y-%m-%d %H:%M:%S' -) - -# 1. Console Handler (Immer aktiv!) -console_handler = logging.StreamHandler(sys.stdout) -console_handler.setFormatter(formatter) -logger.addHandler(console_handler) - -# 2. File Handler (Robustes Setup) -try: - # Versuchen, das Verzeichnis zu erstellen, falls es fehlt - os.makedirs(LOG_DIR, exist_ok=True) - - file_handler = TimedRotatingFileHandler( - LOG_FILE, - when="midnight", - interval=1, - backupCount=30, - encoding='utf-8' - ) - file_handler.setFormatter(formatter) - file_handler.suffix = "%Y-%m-%d" - logger.addHandler(file_handler) - - # Erfolgsmeldung auf Konsole (damit wir sehen, dass es geklappt hat) - print(f"✓ Logging to file enabled: {LOG_FILE}") - -except Exception as e: - # Fallback: Ausführliche Fehlerdiagnose auf stdout - error_msg = f"⚠ LOGGING ERROR: Could not write to {LOG_FILE}\n" - error_msg += f" Error: {e}\n" - try: - error_msg += f" Current User (UID): {os.getuid()}\n" - error_msg += f" Current Group (GID): {os.getgid()}\n" - except: - pass - print(error_msg) - -def log(message: str, level: str = 'INFO', worker_name: str = 'unified-worker'): - """ - Structured logging function - """ - lvl_map = { - 'DEBUG': logging.DEBUG, - 'INFO': logging.INFO, - 'WARNING': logging.WARNING, - 'ERROR': logging.ERROR, - 'CRITICAL': logging.CRITICAL, - 'SUCCESS': logging.INFO - } - - log_level = lvl_map.get(level.upper(), logging.INFO) - prefix = "[SUCCESS] " if level.upper() == 'SUCCESS' else "" - final_message = f"[{worker_name}] {prefix}{message}" - - logger.log(log_level, final_message) \ No newline at end of file diff --git a/email-worker/main.py b/email-worker/main.py deleted file mode 100644 index 8b749dd..0000000 --- a/email-worker/main.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python3 -""" -Main entry point for unified email worker -""" - -import sys -import signal - -from logger import log -from config import config -from unified_worker import UnifiedWorker -from health_server import start_health_server -from metrics.prometheus import start_metrics_server - - -def main(): - """Main entry point""" - - # Create worker instance - worker = UnifiedWorker() - - # Signal handlers for graceful shutdown - def signal_handler(signum, frame): - log(f"Received signal {signum}") - worker.stop() - sys.exit(0) - - signal.signal(signal.SIGTERM, signal_handler) - signal.signal(signal.SIGINT, signal_handler) # Fixed: was signalIGINT in old version - - # Setup worker - worker.setup() - - # Start metrics server (if available) - metrics = start_metrics_server(config.metrics_port) - if metrics: - worker.set_metrics(metrics) - - # Start health check server - start_health_server(worker, worker.dynamodb.available) - - # Print startup banner - worker.print_startup_banner() - - # Start worker - worker.start() - - -if __name__ == '__main__': - main() diff --git a/email-worker/metrics/__init__.py b/email-worker/metrics/__init__.py deleted file mode 100644 index 72317f5..0000000 --- a/email-worker/metrics/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python3 -""" -Metrics collection -""" - -from .prometheus import MetricsCollector, start_metrics_server - -__all__ = ['MetricsCollector', 'start_metrics_server'] diff --git a/email-worker/metrics/prometheus.py b/email-worker/metrics/prometheus.py deleted file mode 100644 index 1afe569..0000000 --- a/email-worker/metrics/prometheus.py +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env python3 -""" -Prometheus metrics collection -""" - -from typing import Optional - -from logger import log - -# Try to import Prometheus client -try: - from prometheus_client import start_http_server, Counter, Gauge, Histogram - PROMETHEUS_ENABLED = True -except ImportError: - PROMETHEUS_ENABLED = False - - -class MetricsCollector: - """Collects and exposes Prometheus metrics""" - - def __init__(self): - self.enabled = PROMETHEUS_ENABLED - - if self.enabled: - # Email processing metrics - self.emails_processed = Counter( - 'emails_processed_total', - 'Total emails processed', - ['domain', 'status'] - ) - - self.emails_in_flight = Gauge( - 'emails_in_flight', - 'Emails currently being processed' - ) - - self.processing_time = Histogram( - 'email_processing_seconds', - 'Time to process email', - ['domain'] - ) - - self.queue_size = Gauge( - 'queue_messages_available', - 'Messages in queue', - ['domain'] - ) - - # Bounce metrics - self.bounces_processed = Counter( - 'bounces_processed_total', - 'Bounce notifications processed', - ['domain', 'type'] - ) - - # Rules metrics - self.autoreplies_sent = Counter( - 'autoreplies_sent_total', - 'Auto-replies sent', - ['domain'] - ) - - self.forwards_sent = Counter( - 'forwards_sent_total', - 'Forwards sent', - ['domain'] - ) - - # Blocklist metrics - self.blocked_senders = Counter( - 'blocked_senders_total', - 'Emails blocked by blacklist', - ['domain'] - ) - - def increment_processed(self, domain: str, status: str): - """Increment processed email counter""" - if self.enabled: - self.emails_processed.labels(domain=domain, status=status).inc() - - def increment_in_flight(self): - """Increment in-flight email gauge""" - if self.enabled: - self.emails_in_flight.inc() - - def decrement_in_flight(self): - """Decrement in-flight email gauge""" - if self.enabled: - self.emails_in_flight.dec() - - def observe_processing_time(self, domain: str, seconds: float): - """Record processing time""" - if self.enabled: - self.processing_time.labels(domain=domain).observe(seconds) - - def set_queue_size(self, domain: str, size: int): - """Set queue size""" - if self.enabled: - self.queue_size.labels(domain=domain).set(size) - - def increment_bounce(self, domain: str, bounce_type: str): - """Increment bounce counter""" - if self.enabled: - self.bounces_processed.labels(domain=domain, type=bounce_type).inc() - - def increment_autoreply(self, domain: str): - """Increment autoreply counter""" - if self.enabled: - self.autoreplies_sent.labels(domain=domain).inc() - - def increment_forward(self, domain: str): - """Increment forward counter""" - if self.enabled: - self.forwards_sent.labels(domain=domain).inc() - - def increment_blocked(self, domain: str): - """Increment blocked sender counter""" - if self.enabled: - self.blocked_senders.labels(domain=domain).inc() - - -def start_metrics_server(port: int) -> Optional[MetricsCollector]: - """ - Start Prometheus metrics HTTP server - - Args: - port: Port to listen on - - Returns: - MetricsCollector instance or None if Prometheus not available - """ - if not PROMETHEUS_ENABLED: - log("⚠ Prometheus client not installed, metrics disabled", 'WARNING') - return None - - try: - start_http_server(port) - log(f"Prometheus metrics on port {port}") - return MetricsCollector() - except Exception as e: - log(f"Failed to start metrics server: {e}", 'ERROR') - return None diff --git a/email-worker/requirements.txt b/email-worker/requirements.txt deleted file mode 100644 index b366aaa..0000000 --- a/email-worker/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -boto3>=1.34.0 -prometheus-client>=0.19.0 diff --git a/email-worker/smtp/__init__.py b/email-worker/smtp/__init__.py deleted file mode 100644 index 2eca07c..0000000 --- a/email-worker/smtp/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python3 -""" -SMTP connection handling -""" - -from .pool import SMTPPool - -__all__ = ['SMTPPool'] diff --git a/email-worker/smtp/delivery.py b/email-worker/smtp/delivery.py deleted file mode 100644 index ce02a88..0000000 --- a/email-worker/smtp/delivery.py +++ /dev/null @@ -1,187 +0,0 @@ -#!/usr/bin/env python3 -""" -SMTP/LMTP email delivery with retry logic -""" - -import time -import smtplib -from typing import Tuple, Optional - -from logger import log -from config import config -from smtp.pool import SMTPPool - - -class EmailDelivery: - """Handles email delivery via SMTP or LMTP""" - - def __init__(self, smtp_pool: SMTPPool): - self.smtp_pool = smtp_pool - - @staticmethod - def is_permanent_recipient_error(error_msg: str) -> bool: - """Check if error is permanent for this recipient (inbox doesn't exist)""" - permanent_indicators = [ - '550', # Mailbox unavailable / not found - '551', # User not local - '553', # Mailbox name not allowed / invalid - 'mailbox not found', - 'user unknown', - 'no such user', - 'recipient rejected', - 'does not exist', - 'invalid recipient', - 'unknown user' - ] - - error_lower = error_msg.lower() - return any(indicator in error_lower for indicator in permanent_indicators) - - def send_to_recipient( - self, - from_addr: str, - recipient: str, - raw_message: bytes, - worker_name: str, - max_retries: int = 2 - ) -> Tuple[bool, Optional[str], bool]: - """ - Send email via SMTP/LMTP to ONE recipient - - If LMTP is enabled, delivers directly to Dovecot (bypasses transport_maps). - With retry logic for connection errors. - - Args: - from_addr: From address - recipient: Recipient address - raw_message: Raw MIME message bytes - worker_name: Worker name for logging - max_retries: Maximum retry attempts - - Returns: - Tuple of (success: bool, error: str or None, is_permanent: bool) - """ - last_error = None - use_lmtp = config.lmtp_enabled - - for attempt in range(max_retries + 1): - conn = None - - try: - if use_lmtp: - # LMTP connection directly to Dovecot (bypasses Postfix/transport_maps) - conn = smtplib.LMTP(config.lmtp_host, config.lmtp_port, timeout=30) - conn.ehlo() - else: - # Normal SMTP connection from pool - conn = self.smtp_pool.get_connection() - if not conn: - last_error = "Could not get SMTP connection" - log( - f" ⚠ {recipient}: No SMTP connection " - f"(attempt {attempt + 1}/{max_retries + 1})", - 'WARNING', - worker_name - ) - time.sleep(0.5) - continue - - result = conn.sendmail(from_addr, [recipient], raw_message) - - # Success - if use_lmtp: - conn.quit() - else: - self.smtp_pool.return_connection(conn) - - if isinstance(result, dict) and result: - error = str(result.get(recipient, 'Unknown refusal')) - is_permanent = self.is_permanent_recipient_error(error) - log( - f" ✗ {recipient}: {error} ({'permanent' if is_permanent else 'temporary'})", - 'ERROR', - worker_name - ) - return False, error, is_permanent - else: - delivery_method = "LMTP" if use_lmtp else "SMTP" - log(f" ✓ {recipient}: Delivered ({delivery_method})", 'SUCCESS', worker_name) - return True, None, False - - except smtplib.SMTPServerDisconnected as e: - # Connection was closed - Retry with new connection - log( - f" ⚠ {recipient}: Connection lost, retrying... " - f"(attempt {attempt + 1}/{max_retries + 1})", - 'WARNING', - worker_name - ) - last_error = str(e) - if conn: - try: - conn.quit() - except: - pass - time.sleep(0.3) - continue - - except smtplib.SMTPRecipientsRefused as e: - if conn and not use_lmtp: - self.smtp_pool.return_connection(conn) - elif conn: - try: - conn.quit() - except: - pass - error_msg = str(e) - is_permanent = self.is_permanent_recipient_error(error_msg) - log(f" ✗ {recipient}: Recipients refused - {error_msg}", 'ERROR', worker_name) - return False, error_msg, is_permanent - - except smtplib.SMTPException as e: - error_msg = str(e) - # On connection errors: Retry - if 'disconnect' in error_msg.lower() or 'closed' in error_msg.lower() or 'connection' in error_msg.lower(): - log( - f" ⚠ {recipient}: Connection error, retrying... " - f"(attempt {attempt + 1}/{max_retries + 1})", - 'WARNING', - worker_name - ) - last_error = error_msg - if conn: - try: - conn.quit() - except: - pass - time.sleep(0.3) - continue - - if conn and not use_lmtp: - self.smtp_pool.return_connection(conn) - elif conn: - try: - conn.quit() - except: - pass - is_permanent = self.is_permanent_recipient_error(error_msg) - log(f" ✗ {recipient}: Error - {error_msg}", 'ERROR', worker_name) - return False, error_msg, is_permanent - - except Exception as e: - # Unknown error - if conn: - try: - conn.quit() - except: - pass - log(f" ✗ {recipient}: Unexpected error - {e}", 'ERROR', worker_name) - return False, str(e), False - - # All retries failed - log( - f" ✗ {recipient}: All retries failed - {last_error}", - 'ERROR', - worker_name - ) - return False, last_error or "Connection failed after retries", False diff --git a/email-worker/smtp/pool.py b/email-worker/smtp/pool.py deleted file mode 100644 index 63f6e89..0000000 --- a/email-worker/smtp/pool.py +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env python3 -""" -SMTP Connection Pool with robust connection handling -""" - -import smtplib -from queue import Queue, Empty -from typing import Optional - -from logger import log -from config import config - - -class SMTPPool: - """Thread-safe SMTP Connection Pool""" - - def __init__(self, host: str, port: int, pool_size: int = 5): - self.host = host - self.port = port - self.pool_size = pool_size - self._pool: Queue = Queue(maxsize=pool_size) - self._initialized = False - - def _create_connection(self) -> Optional[smtplib.SMTP]: - """Create new SMTP connection""" - try: - conn = smtplib.SMTP(self.host, self.port, timeout=30) - conn.ehlo() - if config.smtp_use_tls: - conn.starttls() - conn.ehlo() - if config.smtp_user and config.smtp_pass: - conn.login(config.smtp_user, config.smtp_pass) - log(f" 📡 New SMTP connection created to {self.host}:{self.port}") - return conn - except Exception as e: - log(f"Failed to create SMTP connection: {e}", 'ERROR') - return None - - def _test_connection(self, conn: smtplib.SMTP) -> bool: - """Test if connection is still alive""" - try: - status = conn.noop()[0] - return status == 250 - except Exception: - return False - - def initialize(self): - """Pre-create connections""" - if self._initialized: - return - - # Only 1-2 connections initially, rest on-demand - for _ in range(min(2, self.pool_size)): - conn = self._create_connection() - if conn: - self._pool.put(conn) - - self._initialized = True - log(f"SMTP pool initialized with {self._pool.qsize()} connections (max: {self.pool_size})") - - def get_connection(self, timeout: float = 5.0) -> Optional[smtplib.SMTP]: - """Get a valid connection from pool or create new one""" - # Try to get from pool - try: - conn = self._pool.get(block=False) - # Test if connection is still alive - if self._test_connection(conn): - return conn - else: - # Connection is dead, close and create new one - try: - conn.quit() - except: - pass - log(f" ♻ Recycled stale SMTP connection") - return self._create_connection() - except Empty: - # Pool empty, create new connection - return self._create_connection() - - def return_connection(self, conn: smtplib.SMTP): - """Return connection to pool if still valid""" - if conn is None: - return - - # Check if connection is still good - if not self._test_connection(conn): - try: - conn.quit() - except: - pass - log(f" 🗑 Discarded broken SMTP connection") - return - - # Try to return to pool - try: - self._pool.put_nowait(conn) - except: - # Pool full, close connection - try: - conn.quit() - except: - pass - - def close_all(self): - """Close all connections""" - while not self._pool.empty(): - try: - conn = self._pool.get_nowait() - conn.quit() - except: - pass diff --git a/email-worker/unified_worker.py b/email-worker/unified_worker.py deleted file mode 100644 index 8bbc9da..0000000 --- a/email-worker/unified_worker.py +++ /dev/null @@ -1,201 +0,0 @@ -#!/usr/bin/env python3 -""" -Unified Worker - coordinates all domain pollers -""" - -import sys -import time -import threading -from typing import List, Dict - -from logger import log -from config import config, load_domains -from aws import S3Handler, SQSHandler, SESHandler, DynamoDBHandler -from smtp import SMTPPool -from smtp.delivery import EmailDelivery -from worker import MessageProcessor -from domain_poller import DomainPoller -from metrics.prometheus import MetricsCollector - - -class UnifiedWorker: - """Main worker coordinating all domain pollers""" - - def __init__(self): - self.stop_event = threading.Event() - self.domains: List[str] = [] - self.queue_urls: Dict[str, str] = {} - self.poller_threads: List[threading.Thread] = [] - - # Shared stats across all pollers - self.domain_stats: Dict[str, int] = {} # domain -> processed count - self.stats_lock = threading.Lock() - - # AWS handlers - self.s3 = S3Handler() - self.sqs = SQSHandler() - self.ses = SESHandler() - self.dynamodb = DynamoDBHandler() - - # SMTP pool - self.smtp_pool = SMTPPool(config.smtp_host, config.smtp_port, config.smtp_pool_size) - - # Email delivery - self.delivery = EmailDelivery(self.smtp_pool) - - # Metrics - self.metrics: MetricsCollector = None - - # Message processor - self.processor = MessageProcessor( - self.s3, - self.sqs, - self.ses, - self.dynamodb, - self.delivery, - None # Metrics will be set later - ) - - def setup(self): - """Initialize worker""" - self.domains = load_domains() - - if not self.domains: - log("❌ No domains configured!", 'ERROR') - sys.exit(1) - - # Get queue URLs - for domain in self.domains: - url = self.sqs.get_queue_url(domain) - if url: - self.queue_urls[domain] = url - log(f" ✓ {domain} -> queue found") - else: - log(f" ✗ {domain} -> Queue not found!", 'WARNING') - - if not self.queue_urls: - log("❌ No valid queues found!", 'ERROR') - sys.exit(1) - - # Initialize SMTP pool - self.smtp_pool.initialize() - - log(f"Initialized with {len(self.queue_urls)} domains") - - def start(self): - """Start all domain pollers""" - # Initialize stats for all domains - for domain in self.queue_urls.keys(): - self.domain_stats[domain] = 0 - - # Create poller for each domain - for domain, queue_url in self.queue_urls.items(): - poller = DomainPoller( - domain=domain, - queue_url=queue_url, - message_processor=self.processor, - sqs=self.sqs, - metrics=self.metrics, - stop_event=self.stop_event, - stats_dict=self.domain_stats, - stats_lock=self.stats_lock - ) - - thread = threading.Thread( - target=poller.poll, - name=f"poller-{domain}", - daemon=True - ) - thread.start() - self.poller_threads.append(thread) - - log(f"Started {len(self.poller_threads)} domain pollers") - - # Periodic status log (every 5 minutes) - last_status_log = time.time() - status_interval = 300 # 5 minutes - - try: - while not self.stop_event.is_set(): - self.stop_event.wait(timeout=10) - - # Log status summary every 5 minutes - if time.time() - last_status_log > status_interval: - self._log_status_table() - last_status_log = time.time() - except KeyboardInterrupt: - pass - - def _log_status_table(self): - """Log a compact status table""" - active_threads = sum(1 for t in self.poller_threads if t.is_alive()) - - with self.stats_lock: - total_processed = sum(self.domain_stats.values()) - - # Build compact stats: only show domains with activity or top domains - stats_parts = [] - for domain in sorted(self.queue_urls.keys()): - count = self.domain_stats.get(domain, 0) - if count > 0: # Only show active domains - # Shorten domain for display - short_domain = domain.split('.')[0][:12] - stats_parts.append(f"{short_domain}:{count}") - - if stats_parts: - stats_line = " | ".join(stats_parts) - else: - stats_line = "no activity" - - log( - f"📊 Status: {active_threads}/{len(self.poller_threads)} active, " - f"total:{total_processed} | {stats_line}" - ) - - def stop(self): - """Stop gracefully""" - log("⚠ Stopping worker...") - self.stop_event.set() - - # Wait for poller threads (max 10 seconds each) - for thread in self.poller_threads: - thread.join(timeout=10) - if thread.is_alive(): - log(f"Warning: {thread.name} did not stop gracefully", 'WARNING') - - self.smtp_pool.close_all() - log("👋 Worker stopped") - - def set_metrics(self, metrics: MetricsCollector): - """Set metrics collector""" - self.metrics = metrics - self.processor.metrics = metrics - - def print_startup_banner(self): - """Print startup information""" - log(f"\n{'='*70}") - log(f"🚀 UNIFIED EMAIL WORKER") - log(f"{'='*70}") - log(f" Domains: {len(self.queue_urls)}") - log(f" DynamoDB: {'Connected' if self.dynamodb.available else 'Not Available'}") - - if config.lmtp_enabled: - log(f" Delivery: LMTP -> {config.lmtp_host}:{config.lmtp_port} (bypasses transport_maps)") - else: - log(f" Delivery: SMTP -> {config.smtp_host}:{config.smtp_port}") - - log(f" Poll Interval: {config.poll_interval}s") - log(f" Visibility: {config.visibility_timeout}s") - log(f"") - log(f" Features:") - log(f" ✓ Bounce Detection & Header Rewriting") - log(f" {'✓' if self.dynamodb.available else '✗'} Auto-Reply / Out-of-Office") - log(f" {'✓' if self.dynamodb.available else '✗'} Email Forwarding") - log(f" {'✓' if self.dynamodb.available else '✗'} Blocked Senders (Wildcard)") - log(f" {'✓' if self.metrics else '✗'} Prometheus Metrics") - log(f" {'✓' if config.lmtp_enabled else '✗'} LMTP Direct Delivery") - log(f"") - log(f" Active Domains:") - for domain in sorted(self.queue_urls.keys()): - log(f" • {domain}") - log(f"{'='*70}\n") diff --git a/email-worker/worker.py b/email-worker/worker.py deleted file mode 100644 index 29e27a5..0000000 --- a/email-worker/worker.py +++ /dev/null @@ -1,352 +0,0 @@ -#!/usr/bin/env python3 -""" -Email message processing worker -""" - -import json -import traceback - -from logger import log -from aws import S3Handler, SQSHandler, SESHandler, DynamoDBHandler -from email_processing import EmailParser, BounceHandler, RulesProcessor, BlocklistChecker -from smtp.delivery import EmailDelivery -from metrics.prometheus import MetricsCollector -from email.parser import BytesParser # War wahrscheinlich schon da, prüfen -from email.policy import compat32 # <--- NEU: Hinzufügen - - -class MessageProcessor: - """Processes individual email messages""" - - def __init__( - self, - s3: S3Handler, - sqs: SQSHandler, - ses: SESHandler, - dynamodb: DynamoDBHandler, - delivery: EmailDelivery, - metrics: MetricsCollector - ): - self.s3 = s3 - self.sqs = sqs - self.ses = ses - self.dynamodb = dynamodb - self.delivery = delivery - self.metrics = metrics - - # Initialize sub-processors - self.parser = EmailParser() - self.bounce_handler = BounceHandler(dynamodb) - self.rules_processor = RulesProcessor(dynamodb, ses) - self.blocklist = BlocklistChecker(dynamodb) - - def process_message(self, domain: str, message: dict, receive_count: int) -> bool: - """ - Process one email message from queue - - Args: - domain: Email domain - message: SQS message dict - receive_count: Number of times received - - Returns: - True to delete from queue, False to retry - """ - worker_name = f"worker-{domain}" - - try: - # 1. UNPACKING (SNS -> SES) - message_body = json.loads(message['Body']) - - if 'Message' in message_body and 'Type' in message_body: - # It's an SNS Notification - sns_content = message_body['Message'] - if isinstance(sns_content, str): - ses_msg = json.loads(sns_content) - else: - ses_msg = sns_content - else: - ses_msg = message_body - - # 2. EXTRACT DATA - mail = ses_msg.get('mail', {}) - receipt = ses_msg.get('receipt', {}) - - message_id = mail.get('messageId') - - # FIX: Ignore Amazon SES Setup Notification - if message_id == "AMAZON_SES_SETUP_NOTIFICATION": - log("ℹ️ Received Amazon SES Setup Notification. Ignoring.", 'INFO', worker_name) - return True - - from_addr = mail.get('source') - recipients = receipt.get('recipients', []) - - if not message_id: - log("❌ Error: No messageId in event payload", 'ERROR', worker_name) - return True - - # Domain Validation - if recipients: - first_recipient = recipients[0] - recipient_domain = first_recipient.split('@')[1] - - if recipient_domain.lower() != domain.lower(): - log( - f"⚠ Security: Ignored message for {recipient_domain} " - f"(I am worker for {domain})", - 'WARNING', - worker_name - ) - return True - else: - log("⚠ Warning: No recipients in event", 'WARNING', worker_name) - return True - - key = message_id - - # Compact single-line log for email processing - recipients_str = recipients[0] if len(recipients) == 1 else f"{len(recipients)} recipients" - log(f"📧 Processing: {key[:20]}... -> {recipients_str}", 'INFO', worker_name) - - # 3. DOWNLOAD FROM S3 - raw_bytes = self.s3.get_email(domain, message_id, receive_count) - if raw_bytes is None: - # S3 object not found yet, retry - return False - - # 4. LOOP DETECTION - temp_parsed = self.parser.parse_bytes(raw_bytes) - skip_rules = self.parser.is_processed_by_worker(temp_parsed) - - if skip_rules: - log("🔄 Loop prevention: Already processed by worker", 'INFO', worker_name) - - # 5. PARSING & BOUNCE LOGIC - try: - # --- FIX 2.0: Pre-Sanitize via Legacy Mode --- - # Der strikte Parser crasht SOFORT beim Zugriff auf kaputte Header. - # Wir müssen erst "nachsichtig" parsen, reparieren und Bytes neu generieren. - try: - # 1. Parsen im Compat32-Modus (ignoriert Syntaxfehler) - lenient_parser = BytesParser(policy=compat32) - temp_msg = lenient_parser.parsebytes(raw_bytes) - - # 2. Prüfen und Reparieren - bad_msg_id = temp_msg.get('Message-ID', '') - if bad_msg_id and ('[' in bad_msg_id or ']' in bad_msg_id): - clean_id = bad_msg_id.replace('[', '').replace(']', '') - temp_msg.replace_header('Message-ID', clean_id) - - # 3. Bytes mit repariertem Header neu schreiben - raw_bytes = temp_msg.as_bytes() - log(f" 🔧 Sanitized malformed Message-ID via Legacy Mode: {clean_id}", 'INFO', worker_name) - - if self.metrics: - self.metrics.increment_bounce(domain, 'sanitized_header') - - except Exception as e_sanitize: - # Sollte nicht passieren, aber wir wollen hier nicht abbrechen - log(f" ⚠ Sanitization warning: {e_sanitize}", 'WARNING', worker_name) - # --------------------------------------------- - - - parsed = self.parser.parse_bytes(raw_bytes) - - # --- FIX START: Sanitize Malformed Headers --- - # Fix für Microsofts <[uuid]@domain> Message-IDs, die Python crashen lassen - current_msg_id = parsed.get('Message-ID', '') - if current_msg_id and ('[' in current_msg_id or ']' in current_msg_id): - # Klammern entfernen, aber spitze Klammern behalten - clean_id = current_msg_id.replace('[', '').replace(']', '') - parsed.replace_header('Message-ID', clean_id) - log(" 🔧 Sanitized malformed Message-ID", 'INFO', worker_name) - # --- FIX END --- - - subject = parsed.get('Subject', '(no subject)') - - # Bounce header rewriting - is_bounce = self.bounce_handler.is_ses_bounce_notification(parsed) - parsed, modified = self.bounce_handler.apply_bounce_logic(parsed, subject, worker_name) - - if modified: - log(" ✨ Bounce detected & headers rewritten via DynamoDB", 'INFO', worker_name) - raw_bytes = parsed.as_bytes() - from_addr_final = parsed.get('From') - - if self.metrics: - self.metrics.increment_bounce(domain, 'rewritten') - else: - from_addr_final = from_addr - - # Marker für alle Emails von extern setzen - if not skip_rules: # Nur wenn nicht bereits processed - parsed['X-SES-Worker-Processed'] = 'delivered' - raw_bytes = parsed.as_bytes() # <--- Hier knallte es vorher - - except Exception as e: - # --- VERBESSERTES ERROR LOGGING --- - error_msg = f"⚠ Parsing/Logic Error: {e}. Sending original." - log(error_msg, 'WARNING', worker_name) - - # Den vollen Traceback ins Log schreiben (als ERROR markiert) - tb_str = traceback.format_exc() - log(f"Full Traceback:\n{tb_str}", 'ERROR', worker_name) - # ---------------------------------- - - # Fallback: Wir versuchen trotzdem, die Original-Mail zuzustellen - from_addr_final = from_addr - is_bounce = False - skip_rules = False - - # 6. BLOCKLIST CHECK (Batch for efficiency) - senders_to_check = [] - - # 1. Die Envelope-Adresse (aus dem SES Event / Return-Path) - if from_addr: - senders_to_check.append(from_addr) - - # 2. Die echte Header-Adresse (aus der MIME-E-Mail geparst) - header_from = parsed.get('From') - if header_from and header_from not in senders_to_check: - senders_to_check.append(header_from) - - # 3. Falls die Bounce-Logik die Adresse umgeschrieben hat - if from_addr_final and from_addr_final not in senders_to_check: - senders_to_check.append(from_addr_final) - - # Prüfe nun alle extrahierten Adressen gegen die Datenbank - blocked_by_recipient = self.blocklist.batch_check_blocked_senders( - recipients, - senders_to_check, # <-- Übergabe der Liste - worker_name - ) - - # 7. PROCESS RECIPIENTS - log(f"📤 Sending to {len(recipients)} recipient(s)...", 'INFO', worker_name) - - successful = [] - failed_permanent = [] - failed_temporary = [] - blocked_recipients = [] - - for recipient in recipients: - # Check if blocked - if blocked_by_recipient.get(recipient, False): - log( - f"🗑 Silently dropping message for {recipient} (Sender blocked)", - 'INFO', - worker_name - ) - blocked_recipients.append(recipient) - if self.metrics: - self.metrics.increment_blocked(domain) - continue - - # Process rules (OOO, Forwarding) - not for bounces or already forwarded - skip_local_delivery = False # NEU - if not is_bounce and not skip_rules: - def metrics_callback(action_type: str, dom: str): - """Callback for metrics from rules processor""" - if self.metrics: - if action_type == 'autoreply': - self.metrics.increment_autoreply(dom) - elif action_type == 'forward': - self.metrics.increment_forward(dom) - - skip_local_delivery = self.rules_processor.process_rules_for_recipient( - recipient, - parsed, - domain, - worker_name, - metrics_callback - ) - - # SMTP Delivery - if skip_local_delivery: # NEU - log(f" ⏭ Skipping local delivery for {recipient} (legacy forward active)", - 'INFO', worker_name) - successful.append(recipient) # Zählt als "handled" - else: - success, error, is_perm = self.delivery.send_to_recipient( - from_addr_final, recipient, raw_bytes, worker_name - ) - - if success: - successful.append(recipient) - if self.metrics: - self.metrics.increment_processed(domain, 'success') - elif is_perm: - failed_permanent.append(recipient) - if self.metrics: - self.metrics.increment_processed(domain, 'permanent_failure') - else: - failed_temporary.append(recipient) - if self.metrics: - self.metrics.increment_processed(domain, 'temporary_failure') - - # 8. RESULT & CLEANUP - total_handled = len(successful) + len(failed_permanent) + len(blocked_recipients) - - if total_handled == len(recipients): - # All recipients handled (success, permanent fail, or blocked) - - if len(blocked_recipients) == len(recipients): - # All recipients blocked - mark and delete S3 object - try: - self.s3.mark_as_blocked( - domain, - message_id, - blocked_recipients, - from_addr_final, - worker_name - ) - self.s3.delete_blocked_email(domain, message_id, worker_name) - except Exception as e: - log(f"⚠ Failed to handle blocked email: {e}", 'ERROR', worker_name) - # Don't delete from queue if S3 operations failed - return False - - elif len(successful) > 0: - # At least one success - self.s3.mark_as_processed( - domain, - message_id, - worker_name, - failed_permanent if failed_permanent else None - ) - - elif len(failed_permanent) > 0: - # All failed permanently - self.s3.mark_as_all_invalid( - domain, - message_id, - failed_permanent, - worker_name - ) - - # Build result summary - result_parts = [] - if successful: - result_parts.append(f"{len(successful)} OK") - if failed_permanent: - result_parts.append(f"{len(failed_permanent)} invalid") - if blocked_recipients: - result_parts.append(f"{len(blocked_recipients)} blocked") - - log(f"✅ Completed ({', '.join(result_parts)})", 'SUCCESS', worker_name) - return True - - else: - # Some recipients had temporary failures - log( - f"🔄 Temp failure ({len(failed_temporary)} failed), will retry", - 'WARNING', - worker_name - ) - return False - - except Exception as e: - log(f"❌ CRITICAL WORKER ERROR: {e}", 'ERROR', worker_name) - traceback.print_exc() - return False From 6e2a061cf308fd34dc2acbd74569113d063fab11 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 15 Mar 2026 15:07:01 -0500 Subject: [PATCH 70/74] add ip --- DMS/docker-data/dms/config/fail2ban-jail.cf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DMS/docker-data/dms/config/fail2ban-jail.cf b/DMS/docker-data/dms/config/fail2ban-jail.cf index a1ef651..4f30e0f 100644 --- a/DMS/docker-data/dms/config/fail2ban-jail.cf +++ b/DMS/docker-data/dms/config/fail2ban-jail.cf @@ -1,6 +1,6 @@ [DEFAULT] # 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 69.223.70.143 +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 24.155.193.233 [dovecot] # Erhöht die Anzahl der erlaubten Fehlversuche auf 20 From 36c122bf53dff074595996931ee22233eec09bec Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Thu, 19 Mar 2026 18:18:42 -0500 Subject: [PATCH 71/74] new spam config --- .../rspamd/override.d/docker_whitelist.map | 9 ---- .../config/rspamd/override.d/multimap.conf | 15 +++--- .../dms/config/rspamd/override.d/scores.conf | 11 ++++ DMS/docker-data/dms/config/user-patches.sh | 50 +++++++++++++------ 4 files changed, 54 insertions(+), 31 deletions(-) create mode 100644 DMS/docker-data/dms/config/rspamd/override.d/scores.conf diff --git a/DMS/docker-data/dms/config/rspamd/override.d/docker_whitelist.map b/DMS/docker-data/dms/config/rspamd/override.d/docker_whitelist.map index 7a5d779..e69de29 100644 --- a/DMS/docker-data/dms/config/rspamd/override.d/docker_whitelist.map +++ b/DMS/docker-data/dms/config/rspamd/override.d/docker_whitelist.map @@ -1,9 +0,0 @@ -bayarea-cc.com -ruehrgedoens.de -annavillesda.org -bizmatch.net -biz-match.com -qrmaster.net -nqsltd.com -iitwelders.com -# Weitere Domains hier eintragen \ No newline at end of file diff --git a/DMS/docker-data/dms/config/rspamd/override.d/multimap.conf b/DMS/docker-data/dms/config/rspamd/override.d/multimap.conf index 8055a78..5664217 100644 --- a/DMS/docker-data/dms/config/rspamd/override.d/multimap.conf +++ b/DMS/docker-data/dms/config/rspamd/override.d/multimap.conf @@ -1,14 +1,15 @@ DOCKER_WL { - # ÄNDERUNG: Wir prüfen jetzt den Absender (Envelope From) + # Pruefe den Absender (Envelope From) gegen die Domain-Whitelist type = "from"; filter = "email:domain"; - - # Pfad bleibt gleich + + # Pfad zur Whitelist-Datei mit eigenen Domains map = "/etc/rspamd/override.d/docker_whitelist.map"; - + symbol = "DOCKER_WHITELIST"; - score = -50.0; description = "Whitelist fuer eigene Domains"; - prefilter = true; - action = "accept"; + + # WICHTIG: KEIN prefilter und KEIN action! + # prefilter = true + action = "accept" ueberspringt die Score-Vergabe komplett. + # Der Score wird ueber scores.conf separat definiert. } \ No newline at end of file diff --git a/DMS/docker-data/dms/config/rspamd/override.d/scores.conf b/DMS/docker-data/dms/config/rspamd/override.d/scores.conf new file mode 100644 index 0000000..ae3a33e --- /dev/null +++ b/DMS/docker-data/dms/config/rspamd/override.d/scores.conf @@ -0,0 +1,11 @@ +# scores.conf - Score-Definitionen fuer custom Symbole +# Rspamd ignoriert den "score" Parameter in multimap.conf in neueren Versionen. +# Der Score MUSS hier separat definiert werden. + +symbols { + "DOCKER_WHITELIST" { + weight = -50.0; + description = "Whitelist fuer eigene Domains - reduziert Score um 50 Punkte"; + group = "local_whitelist"; + } +} \ No newline at end of file diff --git a/DMS/docker-data/dms/config/user-patches.sh b/DMS/docker-data/dms/config/user-patches.sh index a84b070..52f40bc 100755 --- a/DMS/docker-data/dms/config/user-patches.sh +++ b/DMS/docker-data/dms/config/user-patches.sh @@ -1,21 +1,41 @@ #!/bin/bash -# user-patches.sh läuft bei jedem Start von DMS automatisch +# user-patches.sh laeuft bei jedem Start von DMS automatisch ACCOUNTS_FILE="/tmp/docker-mailserver/postfix-accounts.cf" WHITELIST_FILE="/etc/rspamd/override.d/docker_whitelist.map" -echo "Patching: Generiere Rspamd Whitelist aus Accounts..." +# Statische Domains, die IMMER in der Whitelist sein sollen +# (auch wenn sie keinen Account im DMS haben) +STATIC_DOMAINS=( + "bayarea-cc.com" + "ruehrgedoens.de" + "annavillesda.org" + "bizmatch.net" + "biz-match.com" + "qrmaster.net" + "nqsltd.com" + "iitwelders.com" +) -if [ -f "$ACCOUNTS_FILE" ]; then - # Whitelist generieren - awk -F'|' '{print $1}' "$ACCOUNTS_FILE" | cut -d'@' -f2 | sort | uniq > "$WHITELIST_FILE" - - # Berechtigungen korrigieren - chmod 644 "$WHITELIST_FILE" - chown _rspamd:_rspamd "$WHITELIST_FILE" 2>/dev/null || true - - echo "Whitelist erfolgreich erstellt:" - cat "$WHITELIST_FILE" -else - echo "FEHLER: $ACCOUNTS_FILE wurde nicht gefunden!" -fi \ No newline at end of file +echo "Patching: Generiere Rspamd Whitelist aus Accounts + statischen Domains..." + +{ + # 1. Statische Domains ausgeben + for domain in "${STATIC_DOMAINS[@]}"; do + echo "$domain" + done + + # 2. Dynamische Domains aus Accounts hinzufuegen (falls vorhanden) + if [ -f "$ACCOUNTS_FILE" ]; then + awk -F'|' '{print $1}' "$ACCOUNTS_FILE" | cut -d'@' -f2 + else + echo "WARNUNG: $ACCOUNTS_FILE nicht gefunden!" >&2 + fi +} | sort | uniq > "$WHITELIST_FILE" + +# Berechtigungen korrigieren +chmod 644 "$WHITELIST_FILE" +chown _rspamd:_rspamd "$WHITELIST_FILE" 2>/dev/null || true + +echo "Whitelist erfolgreich erstellt:" +cat "$WHITELIST_FILE" \ No newline at end of file From b732cebd9430f6e5918a3213ebc7205ec13e047b Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Fri, 20 Mar 2026 10:33:33 -0500 Subject: [PATCH 72/74] updated spam corrections --- .../dms/config/rspamd/local.d/multimap.conf | 8 ++++++ .../rspamd/override.d/force_actions.conf | 6 +++++ .../config/rspamd/override.d/multimap.conf | 15 ----------- .../dms/config/rspamd/override.d/scores.conf | 11 -------- DMS/docker-data/dms/config/user-patches.sh | 26 ++++++++++++------- 5 files changed, 30 insertions(+), 36 deletions(-) create mode 100644 DMS/docker-data/dms/config/rspamd/local.d/multimap.conf create mode 100644 DMS/docker-data/dms/config/rspamd/override.d/force_actions.conf delete mode 100644 DMS/docker-data/dms/config/rspamd/override.d/multimap.conf delete mode 100644 DMS/docker-data/dms/config/rspamd/override.d/scores.conf diff --git a/DMS/docker-data/dms/config/rspamd/local.d/multimap.conf b/DMS/docker-data/dms/config/rspamd/local.d/multimap.conf new file mode 100644 index 0000000..c7ec77c --- /dev/null +++ b/DMS/docker-data/dms/config/rspamd/local.d/multimap.conf @@ -0,0 +1,8 @@ +DOCKER_WL { + type = "from"; + filter = "email:domain"; + map = "/etc/rspamd/override.d/docker_whitelist.map"; + symbol = "DOCKER_WHITELIST"; + description = "Whitelist fuer eigene Domains"; + score = -50.0; +} \ No newline at end of file diff --git a/DMS/docker-data/dms/config/rspamd/override.d/force_actions.conf b/DMS/docker-data/dms/config/rspamd/override.d/force_actions.conf new file mode 100644 index 0000000..49c76bc --- /dev/null +++ b/DMS/docker-data/dms/config/rspamd/override.d/force_actions.conf @@ -0,0 +1,6 @@ +rules { + DOCKER_WHITELIST_FORCE { + action = "no action"; + expression = "DOCKER_WHITELIST"; + } +} \ No newline at end of file diff --git a/DMS/docker-data/dms/config/rspamd/override.d/multimap.conf b/DMS/docker-data/dms/config/rspamd/override.d/multimap.conf deleted file mode 100644 index 5664217..0000000 --- a/DMS/docker-data/dms/config/rspamd/override.d/multimap.conf +++ /dev/null @@ -1,15 +0,0 @@ -DOCKER_WL { - # Pruefe den Absender (Envelope From) gegen die Domain-Whitelist - type = "from"; - filter = "email:domain"; - - # Pfad zur Whitelist-Datei mit eigenen Domains - map = "/etc/rspamd/override.d/docker_whitelist.map"; - - symbol = "DOCKER_WHITELIST"; - description = "Whitelist fuer eigene Domains"; - - # WICHTIG: KEIN prefilter und KEIN action! - # prefilter = true + action = "accept" ueberspringt die Score-Vergabe komplett. - # Der Score wird ueber scores.conf separat definiert. -} \ No newline at end of file diff --git a/DMS/docker-data/dms/config/rspamd/override.d/scores.conf b/DMS/docker-data/dms/config/rspamd/override.d/scores.conf deleted file mode 100644 index ae3a33e..0000000 --- a/DMS/docker-data/dms/config/rspamd/override.d/scores.conf +++ /dev/null @@ -1,11 +0,0 @@ -# scores.conf - Score-Definitionen fuer custom Symbole -# Rspamd ignoriert den "score" Parameter in multimap.conf in neueren Versionen. -# Der Score MUSS hier separat definiert werden. - -symbols { - "DOCKER_WHITELIST" { - weight = -50.0; - description = "Whitelist fuer eigene Domains - reduziert Score um 50 Punkte"; - group = "local_whitelist"; - } -} \ No newline at end of file diff --git a/DMS/docker-data/dms/config/user-patches.sh b/DMS/docker-data/dms/config/user-patches.sh index 52f40bc..d1ea6e2 100755 --- a/DMS/docker-data/dms/config/user-patches.sh +++ b/DMS/docker-data/dms/config/user-patches.sh @@ -4,8 +4,7 @@ ACCOUNTS_FILE="/tmp/docker-mailserver/postfix-accounts.cf" WHITELIST_FILE="/etc/rspamd/override.d/docker_whitelist.map" -# Statische Domains, die IMMER in der Whitelist sein sollen -# (auch wenn sie keinen Account im DMS haben) +# --- Rspamd Whitelist generieren --- STATIC_DOMAINS=( "bayarea-cc.com" "ruehrgedoens.de" @@ -20,22 +19,29 @@ STATIC_DOMAINS=( echo "Patching: Generiere Rspamd Whitelist aus Accounts + statischen Domains..." { - # 1. Statische Domains ausgeben for domain in "${STATIC_DOMAINS[@]}"; do echo "$domain" done - - # 2. Dynamische Domains aus Accounts hinzufuegen (falls vorhanden) if [ -f "$ACCOUNTS_FILE" ]; then awk -F'|' '{print $1}' "$ACCOUNTS_FILE" | cut -d'@' -f2 - else - echo "WARNUNG: $ACCOUNTS_FILE nicht gefunden!" >&2 fi } | sort | uniq > "$WHITELIST_FILE" -# Berechtigungen korrigieren chmod 644 "$WHITELIST_FILE" chown _rspamd:_rspamd "$WHITELIST_FILE" 2>/dev/null || true +echo "Whitelist erstellt:" +cat "$WHITELIST_FILE" -echo "Whitelist erfolgreich erstellt:" -cat "$WHITELIST_FILE" \ No newline at end of file +# --- local.d configs manuell kopieren (DMS kopiert local.d nicht automatisch) --- +echo "Patching: Kopiere custom rspamd local.d configs..." +SRC="/tmp/docker-mailserver/rspamd/local.d" +DST="/etc/rspamd/local.d" +if [ -d "$SRC" ]; then + for f in "$SRC"/*; do + [ -f "$f" ] || continue + cp "$f" "$DST/$(basename "$f")" + chown root:root "$DST/$(basename "$f")" + chmod 644 "$DST/$(basename "$f")" + echo " Kopiert: $(basename "$f") -> $DST/" + done +fi \ No newline at end of file From 61fce745afbaf7647a1039409f3029b396851623 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Tue, 24 Mar 2026 20:23:34 -0500 Subject: [PATCH 73/74] moving certs --- DMS/docker-compose.yml | 3 ++- caddy/.gitignore | 2 ++ caddy/docker-compose.yml | 9 ++------- 3 files changed, 6 insertions(+), 8 deletions(-) create mode 100644 caddy/.gitignore diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml index 279c94f..79d2502 100644 --- a/DMS/docker-compose.yml +++ b/DMS/docker-compose.yml @@ -45,7 +45,8 @@ services: # 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 + # - /var/lib/docker/volumes/caddy_data/_data/caddy/certificates/acme-v02.api.letsencrypt.org-directory:/etc/mail/certs:ro + - /home/aknuth/git/email-amazon/caddy/caddy-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. diff --git a/caddy/.gitignore b/caddy/.gitignore new file mode 100644 index 0000000..8983a46 --- /dev/null +++ b/caddy/.gitignore @@ -0,0 +1,2 @@ +caddy-data/ +caddy-config/ \ No newline at end of file diff --git a/caddy/docker-compose.yml b/caddy/docker-compose.yml index 374deb9..8df14f0 100644 --- a/caddy/docker-compose.yml +++ b/caddy/docker-compose.yml @@ -19,8 +19,8 @@ services: # email_autodiscover entfernt - Snippet ist jetzt in mail_certs eingebettet # email.mobileconfig.html entfernt - Inhalt ist jetzt inline in mail_certs - $PWD/email-setup:/var/www/email-setup - - caddy_data:/data - - caddy_config:/config + - ./caddy-data:/data + - ./caddy-config:/config - /home/aknuth/log/caddy:/var/log/caddy environment: - CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN} @@ -29,8 +29,3 @@ services: networks: mail_network: external: true - -volumes: - caddy_data: - external: true - caddy_config: \ No newline at end of file From 2ebe0484a456051ab9f3ae297e79a0f03435ac3b Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Fri, 3 Apr 2026 16:15:20 -0500 Subject: [PATCH 74/74] create topic, subscription and queues per Domain --- basic_setup/create-queue.sh | 205 +++++++++++++++++++++++++++++------- 1 file changed, 168 insertions(+), 37 deletions(-) diff --git a/basic_setup/create-queue.sh b/basic_setup/create-queue.sh index 643a521..aa00828 100755 --- a/basic_setup/create-queue.sh +++ b/basic_setup/create-queue.sh @@ -1,55 +1,58 @@ #!/bin/bash -# create-queue.sh +# create-queue.sh (v2 — mit SNS Fan-Out + Standby Queue) # Usage: DOMAIN=andreasknuth.de ./create-queue.sh +# +# Erstellt pro Domain: +# - Primary Queue + DLQ (wie bisher, für Contabo) +# - Standby Queue + DLQ (NEU, für Office-VM) +# - SNS Topic (NEU, Fan-Out) +# - 2 SNS Subscriptions (NEU, Topic → Primary + Standby) set -e AWS_REGION="us-east-2" -# Domain aus Environment Variable if [ -z "$DOMAIN" ]; then echo "Error: DOMAIN environment variable not set" echo "Usage: DOMAIN=andreasknuth.de $0" exit 1 fi -QUEUE_NAME="${DOMAIN//./-}-queue" +DOMAIN_SLUG="${DOMAIN//./-}" +QUEUE_NAME="${DOMAIN_SLUG}-queue" DLQ_NAME="${QUEUE_NAME}-dlq" +STANDBY_QUEUE_NAME="${DOMAIN_SLUG}-standby-queue" +STANDBY_DLQ_NAME="${STANDBY_QUEUE_NAME}-dlq" +TOPIC_NAME="${DOMAIN_SLUG}-topic" + +ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text) echo "========================================" -echo "Creating SQS Queue for Email Delivery" +echo "Creating SQS + SNS for Email Delivery" echo "========================================" echo "" -echo "📧 Domain: $DOMAIN" -echo " Region: $AWS_REGION" +echo "📧 Domain: $DOMAIN" +echo " Region: $AWS_REGION" +echo " Account: $ACCOUNT_ID" echo "" -# Dead Letter Queue erstellen +# ============================================================ +# 1. Primary DLQ + Queue (wie bisher) +# ============================================================ +echo "━━━ Primary Queue (Contabo) ━━━" + echo "Creating DLQ: $DLQ_NAME" DLQ_URL=$(aws sqs create-queue \ --queue-name "${DLQ_NAME}" \ --region "${AWS_REGION}" \ - --attributes '{ - "MessageRetentionPeriod": "1209600" - }' \ - --query 'QueueUrl' \ - --output text 2>/dev/null || aws sqs get-queue-url --queue-name "${DLQ_NAME}" --region "${AWS_REGION}" --query 'QueueUrl' --output text) + --attributes '{"MessageRetentionPeriod": "1209600"}' \ + --query 'QueueUrl' --output text 2>/dev/null \ + || aws sqs get-queue-url --queue-name "${DLQ_NAME}" --region "${AWS_REGION}" --query 'QueueUrl' --output text) +DLQ_ARN=$(aws sqs get-queue-attributes --queue-url "${DLQ_URL}" --region "${AWS_REGION}" \ + --attribute-names QueueArn --query 'Attributes.QueueArn' --output text) +echo " ✓ DLQ: ${DLQ_ARN}" -echo " ✓ DLQ URL: ${DLQ_URL}" - -# DLQ ARN ermitteln -DLQ_ARN=$(aws sqs get-queue-attributes \ - --queue-url "${DLQ_URL}" \ - --region "${AWS_REGION}" \ - --attribute-names QueueArn \ - --query 'Attributes.QueueArn' \ - --output text) - -echo " ✓ DLQ ARN: ${DLQ_ARN}" -echo "" - -# Haupt-Queue erstellen mit Redrive Policy -echo "Creating Main Queue: $QUEUE_NAME" +echo "Creating Queue: $QUEUE_NAME" QUEUE_URL=$(aws sqs create-queue \ --queue-name "${QUEUE_NAME}" \ --region "${AWS_REGION}" \ @@ -59,18 +62,146 @@ QUEUE_URL=$(aws sqs create-queue \ \"ReceiveMessageWaitTimeSeconds\": \"20\", \"RedrivePolicy\": \"{\\\"deadLetterTargetArn\\\":\\\"${DLQ_ARN}\\\",\\\"maxReceiveCount\\\":\\\"3\\\"}\" }" \ - --query 'QueueUrl' \ - --output text 2>/dev/null || aws sqs get-queue-url --queue-name "${QUEUE_NAME}" --region "${AWS_REGION}" --query 'QueueUrl' --output text) + --query 'QueueUrl' --output text 2>/dev/null \ + || aws sqs get-queue-url --queue-name "${QUEUE_NAME}" --region "${AWS_REGION}" --query 'QueueUrl' --output text) +QUEUE_ARN=$(aws sqs get-queue-attributes --queue-url "${QUEUE_URL}" --region "${AWS_REGION}" \ + --attribute-names QueueArn --query 'Attributes.QueueArn' --output text) +echo " ✓ Queue: ${QUEUE_ARN}" +echo "" -echo " ✓ Queue URL: ${QUEUE_URL}" +# ============================================================ +# 2. Standby DLQ + Queue (NEU) +# ============================================================ +echo "━━━ Standby Queue (Office-VM) ━━━" + +echo "Creating Standby DLQ: $STANDBY_DLQ_NAME" +STANDBY_DLQ_URL=$(aws sqs create-queue \ + --queue-name "${STANDBY_DLQ_NAME}" \ + --region "${AWS_REGION}" \ + --attributes '{"MessageRetentionPeriod": "1209600"}' \ + --query 'QueueUrl' --output text 2>/dev/null \ + || aws sqs get-queue-url --queue-name "${STANDBY_DLQ_NAME}" --region "${AWS_REGION}" --query 'QueueUrl' --output text) +STANDBY_DLQ_ARN=$(aws sqs get-queue-attributes --queue-url "${STANDBY_DLQ_URL}" --region "${AWS_REGION}" \ + --attribute-names QueueArn --query 'Attributes.QueueArn' --output text) +echo " ✓ Standby DLQ: ${STANDBY_DLQ_ARN}" + +echo "Creating Standby Queue: $STANDBY_QUEUE_NAME" +STANDBY_QUEUE_URL=$(aws sqs create-queue \ + --queue-name "${STANDBY_QUEUE_NAME}" \ + --region "${AWS_REGION}" \ + --attributes "{ + \"VisibilityTimeout\": \"300\", + \"MessageRetentionPeriod\": \"86400\", + \"ReceiveMessageWaitTimeSeconds\": \"20\", + \"RedrivePolicy\": \"{\\\"deadLetterTargetArn\\\":\\\"${STANDBY_DLQ_ARN}\\\",\\\"maxReceiveCount\\\":\\\"3\\\"}\" + }" \ + --query 'QueueUrl' --output text 2>/dev/null \ + || aws sqs get-queue-url --queue-name "${STANDBY_QUEUE_NAME}" --region "${AWS_REGION}" --query 'QueueUrl' --output text) +STANDBY_QUEUE_ARN=$(aws sqs get-queue-attributes --queue-url "${STANDBY_QUEUE_URL}" --region "${AWS_REGION}" \ + --attribute-names QueueArn --query 'Attributes.QueueArn' --output text) +echo " ✓ Standby Queue: ${STANDBY_QUEUE_ARN}" echo "" + +# ============================================================ +# 3. SNS Topic (NEU) +# ============================================================ +echo "━━━ SNS Topic (Fan-Out) ━━━" + +echo "Creating Topic: $TOPIC_NAME" +TOPIC_ARN=$(aws sns create-topic \ + --name "${TOPIC_NAME}" \ + --region "${AWS_REGION}" \ + --query 'TopicArn' --output text) +echo " ✓ Topic: ${TOPIC_ARN}" +echo "" + +# ============================================================ +# 4. SNS → SQS Subscriptions (NEU) +# ============================================================ +echo "━━━ Subscriptions ━━━" + +# SNS braucht Berechtigung, in die SQS Queues zu schreiben +# Policy für Primary Queue +POLICY_PRIMARY="{ + \"Version\": \"2012-10-17\", + \"Statement\": [{ + \"Effect\": \"Allow\", + \"Principal\": {\"Service\": \"sns.amazonaws.com\"}, + \"Action\": \"sqs:SendMessage\", + \"Resource\": \"${QUEUE_ARN}\", + \"Condition\": {\"ArnEquals\": {\"aws:SourceArn\": \"${TOPIC_ARN}\"}} + }] +}" + +aws sqs set-queue-attributes \ + --queue-url "${QUEUE_URL}" \ + --region "${AWS_REGION}" \ + --attributes "{\"Policy\": $(echo "$POLICY_PRIMARY" | jq -c '.' | jq -Rs '.')}" \ + > /dev/null +echo " ✓ Primary Queue Policy gesetzt" + +# Policy für Standby Queue +POLICY_STANDBY="{ + \"Version\": \"2012-10-17\", + \"Statement\": [{ + \"Effect\": \"Allow\", + \"Principal\": {\"Service\": \"sns.amazonaws.com\"}, + \"Action\": \"sqs:SendMessage\", + \"Resource\": \"${STANDBY_QUEUE_ARN}\", + \"Condition\": {\"ArnEquals\": {\"aws:SourceArn\": \"${TOPIC_ARN}\"}} + }] +}" + +aws sqs set-queue-attributes \ + --queue-url "${STANDBY_QUEUE_URL}" \ + --region "${AWS_REGION}" \ + --attributes "{\"Policy\": $(echo "$POLICY_STANDBY" | jq -c '.' | jq -Rs '.')}" \ + > /dev/null +echo " ✓ Standby Queue Policy gesetzt" + +# Subscription: Topic → Primary Queue +SUB_PRIMARY=$(aws sns subscribe \ + --topic-arn "${TOPIC_ARN}" \ + --protocol sqs \ + --notification-endpoint "${QUEUE_ARN}" \ + --region "${AWS_REGION}" \ + --attributes '{"RawMessageDelivery": "true"}' \ + --query 'SubscriptionArn' --output text) +echo " ✓ Subscription Primary: ${SUB_PRIMARY}" + +# Subscription: Topic → Standby Queue +SUB_STANDBY=$(aws sns subscribe \ + --topic-arn "${TOPIC_ARN}" \ + --protocol sqs \ + --notification-endpoint "${STANDBY_QUEUE_ARN}" \ + --region "${AWS_REGION}" \ + --attributes '{"RawMessageDelivery": "true"}' \ + --query 'SubscriptionArn' --output text) +echo " ✓ Subscription Standby: ${SUB_STANDBY}" +echo "" + +# ============================================================ +# Zusammenfassung +# ============================================================ echo "========================================" -echo "✅ Queue created successfully!" +echo "✅ Setup complete for $DOMAIN" echo "========================================" echo "" -echo "Configuration:" -echo " Domain: $DOMAIN" -echo " Queue: $QUEUE_NAME" -echo " Queue URL: $QUEUE_URL" -echo " DLQ: $DLQ_NAME" -echo " Region: $AWS_REGION" \ No newline at end of file +echo "Primary (Contabo):" +echo " Queue: $QUEUE_URL" +echo " DLQ: $DLQ_URL" +echo "" +echo "Standby (Office-VM):" +echo " Queue: $STANDBY_QUEUE_URL" +echo " DLQ: $STANDBY_DLQ_URL" +echo "" +echo "SNS Fan-Out:" +echo " Topic: $TOPIC_ARN" +echo " → Primary: $SUB_PRIMARY" +echo " → Standby: $SUB_STANDBY" +echo "" +echo "⚠️ Nächste Schritte:" +echo " 1. Lambda-Funktion updaten: sns.publish() statt sqs.send_message()" +echo " 2. Lambda IAM Role: sns:Publish Berechtigung hinzufügen" +echo " 3. Worker auf Office-VM: QUEUE_SUFFIX=-standby-queue konfigurieren" +echo " 4. Worker auf Office-VM: STANDBY_MODE=true setzen" \ No newline at end of file