Compare commits
225 Commits
675c00209c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 56b7d065b8 | |||
| 96348c17ce | |||
| 02b721ff51 | |||
| 3a628fe676 | |||
| 93535750a2 | |||
| c949457f4c | |||
| 5292e2728f | |||
| 8c1770882b | |||
| 6837cf4f17 | |||
| 5c61a74e3d | |||
| ffe6bdd0f4 | |||
| f391e35221 | |||
| 9f88b58f96 | |||
| 9d32e0962e | |||
| 4d3ca7bb14 | |||
| 27f929dfbc | |||
| be0642c389 | |||
| 69af529410 | |||
| c4d8e980da | |||
| 676e0a91b6 | |||
| d626b19e4a | |||
| 8efc6bfcd2 | |||
| 7fcc380b0f | |||
| 38e327c847 | |||
| 5e8559ec97 | |||
| 2ee5fc8842 | |||
| 9e107cb96c | |||
| 0ef8eb0938 | |||
| 05aac691b3 | |||
| de57180976 | |||
| 45ae435223 | |||
| 6cd4371829 | |||
| eec4458604 | |||
| 4e2369f35c | |||
| c73b400f52 | |||
| c6ee22ef12 | |||
| 4842bd7f03 | |||
| 6f289424e3 | |||
| 900ae6c257 | |||
| 8ef9420396 | |||
| 2dd1dc21b6 | |||
| 0794242198 | |||
| d926064493 | |||
| 0d6bf386d0 | |||
| 3ad3ab38c8 | |||
| 200567f23c | |||
| ab4958dfa2 | |||
| aa75224d03 | |||
| 4530a2f80e | |||
| 83ae97e627 | |||
| fa2ef2b743 | |||
| 7f9042e612 | |||
| 2cfa226361 | |||
| d37109a696 | |||
| 4f7dc6f8b4 | |||
| afdd3d903a | |||
| 6c6b4d345f | |||
| 28741de633 | |||
| 7d1c0b9a6d | |||
| 38b425e1d8 | |||
| b3d184259e | |||
| dfddc38c89 | |||
| 29e35bfad6 | |||
| fe9651409e | |||
| 9438eeaa75 | |||
| 286619fc62 | |||
| 969aec9278 | |||
| 1bb297f0cf | |||
| 5dc09a5651 | |||
| d0af616b8d | |||
| 7cbbaedd5e | |||
| 9acc06646a | |||
| f541ea9248 | |||
| 96fa643095 | |||
| c70f031dff | |||
| 61820fe772 | |||
| 7e3fac6907 | |||
| 013f1c8994 | |||
| c24049d3fb | |||
| dde671fe3d | |||
| f340cc0a43 | |||
| aded85eb66 | |||
| 9dd22589de | |||
| 0ddfa51265 | |||
| 4a5222a781 | |||
| 490721c8b6 | |||
| ce6a115684 | |||
| f79dde2d1d | |||
| 08d1c0f265 | |||
| 3b3d20f89a | |||
| 286de26c87 | |||
| 1b899985a1 | |||
| cd731c502b | |||
| f0096bc27f | |||
| ac008aff8e | |||
| 432259d459 | |||
| b9066a8f59 | |||
| 39d50b7d3b | |||
| 5533fbff14 | |||
| 824fbbe3eb | |||
| 08657e7282 | |||
| 1968faab99 | |||
| 2617f049f3 | |||
| 3d961d6536 | |||
| 7f071435c7 | |||
| 5469a01893 | |||
| d04bb2f5cb | |||
| 1bf893f683 | |||
| fecfc59988 | |||
| a2c4ac8685 | |||
| 29181ce13b | |||
| 08489162bf | |||
| d5b7986761 | |||
| 92afa46d5d | |||
| 3da5e3c814 | |||
| 22eadee4cd | |||
| a709172a99 | |||
| 0f29d06653 | |||
| ce296ecdab | |||
| a22a30ac3b | |||
| 798842ba9b | |||
| 9b490a9233 | |||
| 5ed6c15ba2 | |||
| bf5569522a | |||
| b8915cb692 | |||
| 3c84604de8 | |||
| b210e49ad4 | |||
| 38a1a08c2a | |||
| b8dd30987e | |||
| ee26c3dc0a | |||
| bc038b0a70 | |||
| 6331391f1c | |||
| 22af3a5273 | |||
| ee3e5952ac | |||
| 7bd4c73306 | |||
| 541059c0c4 | |||
| cd545ee056 | |||
| ac68074178 | |||
| c62b72dac0 | |||
| ce0b44ac9c | |||
| 0d2d5d9e38 | |||
| 3553cdcf59 | |||
| 06f6ee43cc | |||
| 834aa48d09 | |||
| 14f6b30444 | |||
| e9a266534a | |||
| 96f3ccbc1a | |||
| 3b3cb3aec1 | |||
| 669ef1b220 | |||
| ffee5c0568 | |||
| ee235d5863 | |||
| 06e070f9b7 | |||
| dde2134d87 | |||
| 0f7e8c1dd5 | |||
| bdcd57aba8 | |||
| 57ec03cebe | |||
| 7282cbdd59 | |||
| 082c465985 | |||
| 101a128c9f | |||
| 8b5b984d22 | |||
| 2562fc49b0 | |||
| 212fa09534 | |||
| a3eef3055e | |||
| f66016633e | |||
| 57fbce27f6 | |||
| b10f49a283 | |||
| 47b5b7e8fd | |||
| 379cc87257 | |||
| d80df95f43 | |||
| ceaf82d5da | |||
| fb5b0cc48e | |||
| dfadc74b2d | |||
| a3873f8649 | |||
| cbe58d4cb2 | |||
| 7692aef4fc | |||
| 31aea63c61 | |||
| 973be97c70 | |||
| bd38b9a5f2 | |||
| 09f6bf1a27 | |||
| b72cfdc67e | |||
| e96631bafd | |||
| 76b8f17ed3 | |||
| 77ec9800aa | |||
| 3efc9ab1f1 | |||
| 4c34709526 | |||
| b8e3cb6e1f | |||
| de7a541857 | |||
| e8327f6824 | |||
| 4ad79c3c29 | |||
| 7e2b2ca310 | |||
| 4be8ff61c5 | |||
| f51e4ab44b | |||
| 338c630f57 | |||
| 08501f863a | |||
| d5aaa64555 | |||
| 215d7a3978 | |||
| 217ad84815 | |||
| e349f0142f | |||
| 8f1fdbfb96 | |||
| 766f0d4a18 | |||
| 1369944996 | |||
| 96950e43b0 | |||
| b9cc17b997 | |||
| e0db2595a9 | |||
| a6db7b130b | |||
| 8bb05f499f | |||
| 0ce09ef969 | |||
| e73641f258 | |||
| 2e3ca446f5 | |||
| 1d88681f28 | |||
| 24f6890357 | |||
| fc9102c5d3 | |||
| 210148c305 | |||
| b126861406 | |||
| ee7b6fd1fb | |||
| 04e9f4ccec | |||
| e7519cc0b5 | |||
| 3a585ea604 | |||
| 1525cda50f | |||
| 80acd37da7 | |||
| 4efb69a356 | |||
| 53827a8277 | |||
| 55959ce22d | |||
| d550342492 | |||
| ade3a5780f |
6
app/.env
6
app/.env
@@ -1,6 +0,0 @@
|
||||
DB_HOST=postgres
|
||||
DB_PORT=5432
|
||||
DB_SCHEMA=public
|
||||
POSTGRES_DB=bizmatch
|
||||
POSTGRES_USER=bizmatch
|
||||
POSTGRES_PASSWORD=xieng7Seih
|
||||
44
app/docker-compose.dev.yml
Normal file
44
app/docker-compose.dev.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
services:
|
||||
app:
|
||||
image: node:22-alpine
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ~/git/bizmatch-project/bizmatch-server:/app
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- DB_HOST=postgres
|
||||
- DB_PORT=5432
|
||||
- DB_NAME=${POSTGRES_DB}
|
||||
- DB_USER=${POSTGRES_USER}
|
||||
- DB_PASSWORD=${POSTGRES_PASSWORD}
|
||||
env_file:
|
||||
- ~/git/docker/app/.env # Pfad zur .env-Datei
|
||||
command: sh -c "npm install && npm run build --omit=dev && node dist/src/main.js"
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- postgres
|
||||
networks:
|
||||
- bizmatch
|
||||
|
||||
postgres:
|
||||
container_name: bizmatchdb
|
||||
image: postgres:latest
|
||||
restart: always
|
||||
volumes:
|
||||
- ${PWD}/bizmatchdb-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
env_file:
|
||||
- ~/git/docker/app/.env # Neu: Separate Env-File für Prod
|
||||
ports:
|
||||
- "5432:5432"
|
||||
networks:
|
||||
- bizmatch
|
||||
|
||||
networks:
|
||||
bizmatch:
|
||||
external: true
|
||||
47
app/docker-compose.prod.yml
Normal file
47
app/docker-compose.prod.yml
Normal file
@@ -0,0 +1,47 @@
|
||||
services:
|
||||
app:
|
||||
image: node:22-alpine
|
||||
container_name: bizmatch-app-prod # Neu: Unterscheide Namen
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- /home/aknuth/git/bizmatch-project-prod/bizmatch-server:/app # Verwende Prod-Checkout
|
||||
ports:
|
||||
- "3001:3000" # Neu: Host-Port 3001, Container-Port bleibt 3000
|
||||
env_file:
|
||||
- path: ./env.prod # Neu: Separate Env-File für Prod
|
||||
required: true
|
||||
environment:
|
||||
- NODE_ENV=development # Neu: Production-Modus (für Nest.js-Config)
|
||||
- DB_HOST=postgres-prod # Neu: Passe an neuen Service-Namen
|
||||
- DB_PORT=5432
|
||||
- DB_NAME=${POSTGRES_DB} # Neu: Separate DB-Name aus Env-File
|
||||
- DB_USER=${POSTGRES_USER}
|
||||
- DB_PASSWORD=${POSTGRES_PASSWORD}
|
||||
command: sh -c "npm install && npm run build && node dist/src/main.js" # Entferne --omit=dev für Prod
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- postgres-prod
|
||||
networks:
|
||||
- bizmatch-prod # Neu: Separates Network für Isolation
|
||||
|
||||
postgres-prod: # Neu: Umbenannt für Unterscheidung
|
||||
container_name: bizmatchdb-prod
|
||||
image: postgres:latest
|
||||
restart: always
|
||||
volumes:
|
||||
- ${PWD}/bizmatchdb-data-prod:/var/lib/postgresql/data # Neu: Separates Daten-Volume
|
||||
env_file:
|
||||
- path: ./env.prod # Neu: Separate Env-File für Prod
|
||||
required: true
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
ports:
|
||||
- "5433:5432" # Neu: Host-Port 5433, Container-Port bleibt 5432
|
||||
networks:
|
||||
- bizmatch-prod
|
||||
|
||||
networks:
|
||||
bizmatch-prod:
|
||||
external: true # Neu: Erstelle es mit `docker network create bizmatch-prod`
|
||||
@@ -1,20 +0,0 @@
|
||||
services:
|
||||
|
||||
postgres:
|
||||
container_name: bizmatchdb
|
||||
image: postgres:latest
|
||||
restart: always
|
||||
volumes:
|
||||
- ${PWD}/bizmatchdb-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
ports:
|
||||
- "5432:5432"
|
||||
networks:
|
||||
- bizmatch
|
||||
|
||||
networks:
|
||||
bizmatch:
|
||||
external: true
|
||||
File diff suppressed because it is too large
Load Diff
130
caddy/Caddyfile
130
caddy/Caddyfile
@@ -4,35 +4,59 @@
|
||||
acme_ca https://acme-v02.api.letsencrypt.org/directory
|
||||
debug
|
||||
}
|
||||
bizmatch.net {
|
||||
|
||||
}
|
||||
# Prod: Neue Domains
|
||||
www.bizmatch.net {
|
||||
|
||||
}
|
||||
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
|
||||
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 {
|
||||
reverse_proxy host.docker.internal:4200
|
||||
}
|
||||
log {
|
||||
output stderr
|
||||
output file /var/log/caddy/access.prod.log # Separate Logs
|
||||
}
|
||||
encode gzip zstd
|
||||
}
|
||||
bizmatch.net {
|
||||
redir https://www.bizmatch.net{uri} permanent
|
||||
}
|
||||
www.qrmaster.net {
|
||||
handle {
|
||||
reverse_proxy host.docker.internal:3050
|
||||
}
|
||||
log {
|
||||
output file /var/log/caddy/qrmaster.log
|
||||
format console
|
||||
}
|
||||
encode gzip
|
||||
}
|
||||
www.bayarea-cc.com {
|
||||
redir https://bayarea-cc.com{uri} permanent
|
||||
qrmaster.net {
|
||||
redir https://www.qrmaster.net{uri} permanent
|
||||
}
|
||||
www.innungsapp.com {
|
||||
handle {
|
||||
reverse_proxy host.docker.internal:3010
|
||||
}
|
||||
log {
|
||||
output file /var/log/caddy/innungsapp.log
|
||||
format console
|
||||
}
|
||||
encode gzip
|
||||
}
|
||||
innungsapp.com {
|
||||
redir https://www.innungsapp.com{uri} permanent
|
||||
}
|
||||
|
||||
auth.bizmatch.net {
|
||||
reverse_proxy https://bizmatch-net.firebaseapp.com {
|
||||
header_up Host bizmatch-net.firebaseapp.com
|
||||
@@ -45,54 +69,30 @@ gitea.bizmatch.net {
|
||||
reverse_proxy gitea:3500
|
||||
}
|
||||
|
||||
dev.bizmatch.net {
|
||||
handle /pictures/* {
|
||||
root * /home/aknuth/git/bizmatch-project/bizmatch-server
|
||||
file_server
|
||||
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}
|
||||
}
|
||||
}
|
||||
|
||||
greenlenspro.com {
|
||||
encode zstd gzip
|
||||
|
||||
@storage path /storage /storage/*
|
||||
handle @storage {
|
||||
uri strip_prefix /storage
|
||||
reverse_proxy minio:9000
|
||||
}
|
||||
|
||||
@api path /api /api/* /auth /auth/* /v1 /v1/* /health /plants /plants/*
|
||||
handle @api {
|
||||
reverse_proxy api:3000
|
||||
}
|
||||
|
||||
handle {
|
||||
root * /srv
|
||||
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-dev.bizmatch.net {
|
||||
reverse_proxy host.docker.internal:3000 {
|
||||
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}
|
||||
reverse_proxy landing:3000
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
}
|
||||
|
||||
mail.andreasknuth.de {
|
||||
reverse_proxy nginx-mailcow:8080
|
||||
}
|
||||
web.email-bayarea.com {
|
||||
reverse_proxy nginx-mailcow:8080
|
||||
}
|
||||
mail.email-srvr.com autodiscover.mail.email-srvr.com autoconfig.mail.email-srvr.com {
|
||||
reverse_proxy nginx-mailcow:8080
|
||||
}
|
||||
13
caddy/Dockerfile.caddy
Normal file
13
caddy/Dockerfile.caddy
Normal file
@@ -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
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
services:
|
||||
caddy:
|
||||
image: custom-caddy:2.9.1-rr1
|
||||
container_name: caddy
|
||||
image: iarekylew00t/caddy-cloudflare:latest
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
@@ -13,18 +16,25 @@ services:
|
||||
- keycloak
|
||||
- gitea
|
||||
- mail_network
|
||||
- greenlens_net
|
||||
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/dist/bizmatch/browser:/srv
|
||||
- /home/aknuth/git/bizmatch-project/bizmatch/dist/bizmatch/browser:/srv
|
||||
- /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=q1P7J3uqS96FGj_iiX2mI8y1ulTaIFrTp8tyTXhG
|
||||
- CLOUDFLARE_EMAIL=andreas.knuth@gmail.com
|
||||
- CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}
|
||||
- CLOUDFLARE_EMAIL=${CLOUDFLARE_EMAIL}
|
||||
|
||||
networks:
|
||||
bizmatch:
|
||||
@@ -35,6 +45,8 @@ networks:
|
||||
external: true
|
||||
mail_network:
|
||||
external: true
|
||||
greenlens_net:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
|
||||
29
caddy/email-setup/autodiscover.xml
Normal file
29
caddy/email-setup/autodiscover.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
|
||||
<Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
|
||||
<Account>
|
||||
<AccountType>email</AccountType>
|
||||
<Action>settings</Action>
|
||||
<Protocol>
|
||||
<Type>IMAP</Type>
|
||||
<Server>mail.email-srvr.com</Server>
|
||||
<Port>993</Port>
|
||||
<DomainRequired>off</DomainRequired>
|
||||
<LoginName></LoginName>
|
||||
<SPA>off</SPA>
|
||||
<SSL>on</SSL>
|
||||
<AuthRequired>on</AuthRequired>
|
||||
</Protocol>
|
||||
<Protocol>
|
||||
<Type>SMTP</Type>
|
||||
<Server>mail.email-srvr.com</Server>
|
||||
<Port>465</Port>
|
||||
<DomainRequired>off</DomainRequired>
|
||||
<LoginName></LoginName>
|
||||
<SPA>off</SPA>
|
||||
<SSL>on</SSL>
|
||||
<AuthRequired>on</AuthRequired>
|
||||
</Protocol>
|
||||
</Account>
|
||||
</Response>
|
||||
</Autodiscover>
|
||||
BIN
caddy/email-setup/logo.png
Normal file
BIN
caddy/email-setup/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
122
caddy/email-setup/setup.html
Normal file
122
caddy/email-setup/setup.html
Normal file
@@ -0,0 +1,122 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Email Setup</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: #f2f2f7; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; padding: 20px; box-sizing: border-box; }
|
||||
.card { background: white; padding: 2.5rem; border-radius: 24px; box-shadow: 0 12px 30px rgba(0,0,0,0.1); width: 100%; max-width: 420px; text-align: center; transition: all 0.3s ease; }
|
||||
.logo { width: 80px; height: 80px; margin-bottom: 1.5rem; }
|
||||
h1 { margin: 0 0 1rem 0; color: #1a1a1a; font-size: 1.8rem; }
|
||||
p { color: #666; line-height: 1.5; margin-bottom: 2rem; }
|
||||
|
||||
/* Input Section */
|
||||
#input-section { transition: opacity 0.3s ease; }
|
||||
input { width: 100%; padding: 16px; margin-bottom: 16px; border: 2px solid #eee; border-radius: 14px; font-size: 16px; box-sizing: border-box; transition: border-color 0.2s; outline: none; }
|
||||
input:focus { border-color: #007AFF; }
|
||||
button { width: 100%; padding: 16px; background: #007AFF; color: white; border: none; border-radius: 14px; font-size: 18px; font-weight: 600; cursor: pointer; transition: background 0.2s, transform 0.1s; }
|
||||
button:hover { background: #0062cc; }
|
||||
button:active { transform: scale(0.98); }
|
||||
|
||||
/* QR Section (initially hidden) */
|
||||
#qr-section { display: none; opacity: 0; transition: opacity 0.5s ease; }
|
||||
#qrcode { margin: 2rem auto; padding: 15px; background: white; border-radius: 16px; box-shadow: 0 4px 12px rgba(0,0,0,0.08); display: inline-block; }
|
||||
#qrcode img { margin: auto; } /* Centers the generated QR code */
|
||||
|
||||
.hint { font-size: 0.9rem; color: #888; margin-top: 1.5rem; }
|
||||
.hint strong { color: #333; }
|
||||
.error { color: #d32f2f; background: #fde8e8; padding: 10px; border-radius: 8px; font-size: 0.9rem; display: none; margin-bottom: 16px; }
|
||||
.back-btn { background: transparent; color: #007AFF; margin-top: 1rem; font-size: 16px; }
|
||||
.back-btn:hover { background: #f0f8ff; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="card">
|
||||
<img src="/logo.png" alt="Logo" class="logo">
|
||||
|
||||
<div id="input-section">
|
||||
<h1>Email Setup</h1>
|
||||
<p>Enter your email address to automatically configure your iPhone or iPad.</p>
|
||||
|
||||
<div id="error-msg" class="error">Please enter a valid email address.</div>
|
||||
|
||||
<input type="email" id="email" placeholder="name@company.com" required autocomplete="email">
|
||||
<button onclick="generateQR()">Generate QR Code</button>
|
||||
</div>
|
||||
|
||||
<div id="qr-section">
|
||||
<h1>Scan me!</h1>
|
||||
<p>Open the <strong>Camera app</strong> on your iPhone and point it at this code.</p>
|
||||
|
||||
<div id="qrcode"></div>
|
||||
|
||||
<p class="hint">
|
||||
Tap the banner that appears at the top.<br>
|
||||
Click <strong>"Allow"</strong> and then go to <strong>Settings</strong> to install the profile.
|
||||
</p>
|
||||
<button class="back-btn" onclick="resetForm()">Back</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const inputSection = document.getElementById('input-section');
|
||||
const qrSection = document.getElementById('qr-section');
|
||||
const emailInput = document.getElementById('email');
|
||||
const errorMsg = document.getElementById('error-msg');
|
||||
let qrcode = null;
|
||||
|
||||
function generateQR() {
|
||||
const email = emailInput.value.trim();
|
||||
|
||||
if (!email || !email.includes('@') || email.split('@')[1].length < 3) {
|
||||
errorMsg.style.display = 'block';
|
||||
emailInput.focus();
|
||||
return;
|
||||
}
|
||||
errorMsg.style.display = 'none';
|
||||
|
||||
const domain = email.split('@')[1];
|
||||
// The magic link
|
||||
const targetUrl = `https://autodiscover.${domain}/apple?email=${email}`;
|
||||
|
||||
// Hide input, show QR
|
||||
inputSection.style.display = 'none';
|
||||
qrSection.style.display = 'block';
|
||||
setTimeout(() => qrSection.style.opacity = '1', 50);
|
||||
|
||||
// Generate (or update) QR Code
|
||||
if (qrcode === null) {
|
||||
qrcode = new QRCode(document.getElementById("qrcode"), {
|
||||
text: targetUrl,
|
||||
width: 200,
|
||||
height: 200,
|
||||
colorDark : "#000000",
|
||||
colorLight : "#ffffff",
|
||||
correctLevel : QRCode.CorrectLevel.H
|
||||
});
|
||||
} else {
|
||||
qrcode.clear();
|
||||
qrcode.makeCode(targetUrl);
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
qrSection.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
qrSection.style.display = 'none';
|
||||
inputSection.style.display = 'block';
|
||||
emailInput.value = '';
|
||||
emailInput.focus();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
emailInput.addEventListener("keypress", function(event) {
|
||||
if (event.key === "Enter") generateQR();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
67
caddy/email.mobileconfig.tpl
Normal file
67
caddy/email.mobileconfig.tpl
Normal file
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PayloadContent</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>EmailAccountDescription</key>
|
||||
<string>{{.Req.URL.Query.Get "email"}}</string>
|
||||
<key>EmailAccountName</key>
|
||||
<string>{{.Req.URL.Query.Get "email"}}</string>
|
||||
<key>EmailAccountType</key>
|
||||
<string>EmailTypeIMAP</string>
|
||||
<key>EmailAddress</key>
|
||||
<string>{{.Req.URL.Query.Get "email"}}</string>
|
||||
<key>IncomingMailServerAuthentication</key>
|
||||
<string>EmailAuthPassword</string>
|
||||
<key>IncomingMailServerHostName</key>
|
||||
<string>mail.email-srvr.com</string>
|
||||
<key>IncomingMailServerPortNumber</key>
|
||||
<integer>993</integer>
|
||||
<key>IncomingMailServerUseSSL</key>
|
||||
<true/>
|
||||
<key>IncomingMailServerUsername</key>
|
||||
<string>{{.Req.URL.Query.Get "email"}}</string>
|
||||
<key>OutgoingMailServerAuthentication</key>
|
||||
<string>EmailAuthPassword</string>
|
||||
<key>OutgoingMailServerHostName</key>
|
||||
<string>mail.email-srvr.com</string>
|
||||
<key>OutgoingMailServerPortNumber</key>
|
||||
<integer>465</integer>
|
||||
<key>OutgoingMailServerUseSSL</key>
|
||||
<true/>
|
||||
<key>OutgoingMailServerUsername</key>
|
||||
<string>{{.Req.URL.Query.Get "email"}}</string>
|
||||
<key>PayloadDescription</key>
|
||||
<string>E-Mail Konfiguration für {{.Req.URL.Query.Get "email"}}</string>
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>{{.Req.URL.Query.Get "email"}}</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>com.email-srvr.profile.{{.Req.URL.Query.Get "email"}}</string>
|
||||
<key>PayloadType</key>
|
||||
<string>com.apple.mail.managed</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>{{uuidv4}}</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</array>
|
||||
<key>PayloadDescription</key>
|
||||
<string>Automatische E-Mail Einrichtung für {{.Req.URL.Query.Get "email"}}</string>
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>E-Mail Einstellungen</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>com.email-srvr.profile.root</string>
|
||||
<key>PayloadOrganization</key>
|
||||
<string>IT Support</string>
|
||||
<key>PayloadRemovalDisallowed</key>
|
||||
<false/>
|
||||
<key>PayloadType</key>
|
||||
<string>Configuration</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>{{uuidv4}}</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</plist>
|
||||
97
caddy/email_autodiscover
Normal file
97
caddy/email_autodiscover
Normal file
@@ -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 `<?xml version="1.0" encoding="utf-8"?>
|
||||
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
|
||||
<Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
|
||||
<Account>
|
||||
<AccountType>email</AccountType>
|
||||
<Action>settings</Action>
|
||||
<Protocol>
|
||||
<Type>IMAP</Type>
|
||||
<Server>mail.email-srvr.com</Server>
|
||||
<Port>993</Port>
|
||||
<DomainRequired>on</DomainRequired>
|
||||
<LoginName>{header.X-Anchormailbox}</LoginName>
|
||||
<SPA>off</SPA>
|
||||
<SSL>on</SSL>
|
||||
<AuthRequired>on</AuthRequired>
|
||||
</Protocol>
|
||||
<Protocol>
|
||||
<Type>POP3</Type>
|
||||
<Server>mail.email-srvr.com</Server>
|
||||
<Port>995</Port>
|
||||
<DomainRequired>on</DomainRequired>
|
||||
<LoginName>{header.X-Anchormailbox}</LoginName>
|
||||
<SPA>off</SPA>
|
||||
<SSL>on</SSL>
|
||||
<AuthRequired>on</AuthRequired>
|
||||
</Protocol>
|
||||
<Protocol>
|
||||
<Type>SMTP</Type>
|
||||
<Server>mail.email-srvr.com</Server>
|
||||
<Port>465</Port>
|
||||
<DomainRequired>on</DomainRequired>
|
||||
<LoginName>{header.X-Anchormailbox}</LoginName>
|
||||
<SPA>off</SPA>
|
||||
<SSL>on</SSL>
|
||||
<AuthRequired>on</AuthRequired>
|
||||
</Protocol>
|
||||
</Account>
|
||||
</Response>
|
||||
</Autodiscover>` 200
|
||||
}
|
||||
|
||||
# 2. JSON Autodiscover (Modern Outlook) - bleibt gleich
|
||||
route /autodiscover/autodiscover.json {
|
||||
header Content-Type "application/json"
|
||||
respond `{
|
||||
"Protocol": "AutodiscoverV1",
|
||||
"Url": "https://autodiscover.bayarea-cc.com/autodiscover/autodiscover.xml"
|
||||
}` 200
|
||||
}
|
||||
|
||||
# 3. Thunderbird Autoconfig - bleibt gleich (dort funktioniert %EMAILADDRESS% ja nativ)
|
||||
route /mail/config-v1.1.xml {
|
||||
header Content-Type "application/xml"
|
||||
respond `<?xml version="1.0"?>
|
||||
<clientConfig version="1.1">
|
||||
<emailProvider id="email-srvr.com">
|
||||
<displayName>Rackspace Email</displayName>
|
||||
<incomingServer type="imap">
|
||||
<hostname>mail.email-srvr.com</hostname>
|
||||
<port>993</port>
|
||||
<socketType>SSL</socketType>
|
||||
<authentication>password-cleartext</authentication>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
</incomingServer>
|
||||
<outgoingServer type="smtp">
|
||||
<hostname>mail.email-srvr.com</hostname>
|
||||
<port>465</port>
|
||||
<socketType>SSL</socketType>
|
||||
<authentication>password-cleartext</authentication>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
</outgoingServer>
|
||||
</emailProvider>
|
||||
</clientConfig>` 200
|
||||
}
|
||||
|
||||
# NEU: Apple MobileConfig Route
|
||||
# Aufrufbar über: /apple?email=kunde@domain.de
|
||||
route /apple {
|
||||
# KORREKTUR: Wir müssen Caddy sagen, dass er diesen MIME-Type bearbeiten soll!
|
||||
templates {
|
||||
mime "application/x-apple-aspen-config"
|
||||
}
|
||||
|
||||
# Den richtigen MIME-Type setzen
|
||||
header Content-Type "application/x-apple-aspen-config; charset=utf-8"
|
||||
|
||||
# Pfad zur Datei im Container
|
||||
root * /etc/caddy
|
||||
rewrite * /email.mobileconfig.tpl
|
||||
file_server
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
# awsdomain.sh - Konfiguriert Cloudflare mit den Amazon SES Angaben
|
||||
if [ -z "$DOMAIN_NAME" ]; then
|
||||
echo "Fehler: DOMAIN_NAME ist nicht gesetzt."
|
||||
echo "Bitte setzen Sie die Variable mit: export DOMAIN_NAME='IhreDomain.de'"
|
||||
|
||||
@@ -18,7 +18,7 @@ fi
|
||||
|
||||
# Konfiguration
|
||||
AWS_REGION=${AWS_REGION:-"us-east-2"}
|
||||
EMAIL_PREFIX=${EMAIL_PREFIX:-"emails/"}
|
||||
EMAIL_PREFIX=${EMAIL_PREFIX:-""}
|
||||
RULE_NAME="store-$(echo "$DOMAIN_NAME" | tr '.' '-')-to-s3"
|
||||
|
||||
echo "=== SES Konfiguration für $DOMAIN_NAME ==="
|
||||
@@ -89,6 +89,62 @@ else
|
||||
echo "Rule Set 'bizmatch-ruleset' ist bereits aktiv."
|
||||
fi
|
||||
|
||||
# ------------------------
|
||||
# Lambda-Funktion mit SES verknüpfen
|
||||
# ------------------------
|
||||
echo "Verknüpfe Lambda-Funktion 'ses-to-sqs' mit SES..."
|
||||
|
||||
# Lambda ARN ermitteln
|
||||
LAMBDA_ARN=$(aws lambda get-function \
|
||||
--function-name ses-to-sqs \
|
||||
--region ${AWS_REGION} \
|
||||
--query 'Configuration.FunctionArn' \
|
||||
--output text)
|
||||
|
||||
if [ -z "$LAMBDA_ARN" ]; then
|
||||
echo "FEHLER: Lambda-Funktion 'ses-to-sqs' nicht gefunden!"
|
||||
echo "Bitte zuerst Lambda-Funktion deployen."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Lambda ARN: $LAMBDA_ARN"
|
||||
|
||||
# SES Permission für Lambda hinzufügen (falls noch nicht vorhanden)
|
||||
echo "Füge SES-Berechtigung zur Lambda-Funktion hinzu..."
|
||||
aws lambda add-permission \
|
||||
--function-name ses-to-sqs \
|
||||
--statement-id "AllowSESInvoke-${DOMAIN_NAME//./}" \
|
||||
--action "lambda:InvokeFunction" \
|
||||
--principal ses.amazonaws.com \
|
||||
--source-account $(aws sts get-caller-identity --query Account --output text) \
|
||||
--region ${AWS_REGION} 2>/dev/null || echo "Permission bereits vorhanden"
|
||||
|
||||
# Receipt Rule UPDATE: Lambda Action hinzufügen
|
||||
echo "Aktualisiere Receipt Rule mit Lambda Action..."
|
||||
aws ses update-receipt-rule --rule-set-name "bizmatch-ruleset" --rule '{
|
||||
"Name": "'"${RULE_NAME}"'",
|
||||
"Enabled": true,
|
||||
"ScanEnabled": true,
|
||||
"Actions": [
|
||||
{
|
||||
"S3Action": {
|
||||
"BucketName": "'"${S3_BUCKET_NAME}"'",
|
||||
"ObjectKeyPrefix": "'"${EMAIL_PREFIX}"'"
|
||||
}
|
||||
},
|
||||
{
|
||||
"LambdaAction": {
|
||||
"FunctionArn": "'"${LAMBDA_ARN}"'",
|
||||
"InvocationType": "Event"
|
||||
}
|
||||
}
|
||||
],
|
||||
"TlsPolicy": "Require",
|
||||
"Recipients": ["'"${DOMAIN_NAME}"'"]
|
||||
}' --region ${AWS_REGION}
|
||||
|
||||
echo "✅ Lambda-Funktion erfolgreich mit SES verknüpft!"
|
||||
|
||||
echo "SES-Konfiguration für $DOMAIN_NAME abgeschlossen."
|
||||
echo
|
||||
echo "WICHTIG: Überprüfen Sie die Ausgabe oben für DNS-Einträge, die Sie bei Ihrem DNS-Provider setzen müssen:"
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
# Setze deine API-Schlüssel und Zone-ID als Umgebungsvariablen oder ersetze sie direkt
|
||||
|
||||
# CF_ZONE_ID="1b7756cee93ed8ba8c05bdc3cb0a5da8" # Die Zone-ID deiner Domain bei Cloudflare
|
||||
# DOMAIN_NAME="andreasknuth.de" # Deine Domain
|
||||
AWS_REGION="us-east-2" # AWS-Region
|
||||
if [ -z "$DOMAIN_NAME" ]; then
|
||||
echo "Fehler: DOMAIN_NAME ist nicht gesetzt."
|
||||
@@ -147,7 +146,7 @@ create_dns_record "MX" "mail.${DOMAIN_NAME}" "feedback-smtp.${AWS_REGION}.amazon
|
||||
|
||||
# CNAME für mail.{Domain} anlegen
|
||||
echo "CNAME für mail.${DOMAIN_NAME} anlegen bei Cloudflare..."
|
||||
create_dns_record "CNAME" "imap.${DOMAIN_NAME}" "${DOMAIN_NAME}" "false" 1
|
||||
create_dns_record "CNAME" "imap.${DOMAIN_NAME}" "${DOMAIN_NAME}" "false" 3600
|
||||
|
||||
# SPF-Eintrag anlegen
|
||||
echo "SPF-Eintrag anlegen bei Cloudflare..."
|
||||
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
volumes:
|
||||
- gitea-data:/data
|
||||
#- ./gitea/gitea-ssh:/data/git/.ssh
|
||||
- /home/git/.ssh/:/data/git/.ssh
|
||||
#- /home/git/.ssh/:/data/git/.ssh
|
||||
ports:
|
||||
- "3500:3500"
|
||||
- "2222:22"
|
||||
|
||||
387
ses-lambda-new-python/lambda_function.py
Normal file
387
ses-lambda-new-python/lambda_function.py
Normal file
@@ -0,0 +1,387 @@
|
||||
import os
|
||||
import boto3
|
||||
import json
|
||||
import time
|
||||
from email.parser import BytesParser
|
||||
from email.policy import SMTP as SMTPPolicy
|
||||
|
||||
s3 = boto3.client('s3')
|
||||
sqs = boto3.client('sqs', region_name='us-east-2')
|
||||
|
||||
# AWS Region
|
||||
AWS_REGION = 'us-east-2'
|
||||
|
||||
# Metadata Keys
|
||||
PROCESSED_KEY = 'processed'
|
||||
PROCESSED_VALUE = 'true'
|
||||
|
||||
|
||||
def domain_to_bucket(domain: str) -> str:
|
||||
"""Konvertiert Domain zu S3 Bucket Namen"""
|
||||
domain = domain.lower()
|
||||
return domain.replace('.', '-') + '-emails'
|
||||
|
||||
|
||||
def domain_to_queue_name(domain: str) -> str:
|
||||
"""Konvertiert Domain zu SQS Queue Namen"""
|
||||
domain = domain.lower()
|
||||
return domain.replace('.', '-') + '-queue'
|
||||
|
||||
|
||||
def get_queue_url_for_domain(domain: str) -> str:
|
||||
"""Ermittelt SQS Queue URL für Domain"""
|
||||
queue_name = domain_to_queue_name(domain)
|
||||
|
||||
try:
|
||||
response = sqs.get_queue_url(QueueName=queue_name)
|
||||
queue_url = response['QueueUrl']
|
||||
print(f"✓ Found queue: {queue_name}")
|
||||
return queue_url
|
||||
|
||||
except sqs.exceptions.QueueDoesNotExist:
|
||||
raise Exception(
|
||||
f"Queue does not exist: {queue_name} "
|
||||
f"(for domain: {domain.lower()})"
|
||||
)
|
||||
except Exception as e:
|
||||
raise Exception(f"Error getting queue URL for {domain.lower()}: {e}")
|
||||
|
||||
|
||||
def is_already_processed(bucket: str, key: str) -> bool:
|
||||
"""Prüft ob E-Mail bereits verarbeitet wurde"""
|
||||
try:
|
||||
head = s3.head_object(Bucket=bucket, Key=key)
|
||||
metadata = head.get('Metadata', {}) or {}
|
||||
|
||||
if metadata.get(PROCESSED_KEY) == PROCESSED_VALUE:
|
||||
processed_at = metadata.get('processed_at', 'unknown')
|
||||
print(f"✓ Already processed at {processed_at}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except s3.exceptions.NoSuchKey:
|
||||
print(f"⚠ Object {key} not found in {bucket}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠ Error checking processed status: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def set_processing_lock(bucket: str, key: str) -> bool:
|
||||
"""
|
||||
Setzt Processing Lock um Duplicate Processing zu verhindern
|
||||
Returns: True wenn Lock erfolgreich gesetzt, False wenn bereits locked
|
||||
"""
|
||||
try:
|
||||
head = s3.head_object(Bucket=bucket, Key=key)
|
||||
metadata = head.get('Metadata', {}) or {}
|
||||
|
||||
# Prüfe auf existierenden Lock
|
||||
processing_started = metadata.get('processing_started')
|
||||
if processing_started:
|
||||
lock_age = time.time() - float(processing_started)
|
||||
|
||||
if lock_age < 300: # 5 Minuten Lock
|
||||
print(f"⚠ Processing lock active (age: {lock_age:.0f}s)")
|
||||
return False
|
||||
else:
|
||||
print(f"⚠ Stale lock detected ({lock_age:.0f}s old), overriding")
|
||||
|
||||
# Setze neuen Lock
|
||||
new_meta = metadata.copy()
|
||||
new_meta['processing_started'] = str(int(time.time()))
|
||||
|
||||
s3.copy_object(
|
||||
Bucket=bucket,
|
||||
Key=key,
|
||||
CopySource={'Bucket': bucket, 'Key': key},
|
||||
Metadata=new_meta,
|
||||
MetadataDirective='REPLACE'
|
||||
)
|
||||
|
||||
print(f"✓ Processing lock set")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠ Error setting processing lock: {e}")
|
||||
return True
|
||||
|
||||
|
||||
def mark_as_queued(bucket: str, key: str, queue_name: str):
|
||||
"""Markiert E-Mail als in Queue eingereiht"""
|
||||
try:
|
||||
head = s3.head_object(Bucket=bucket, Key=key)
|
||||
metadata = head.get('Metadata', {}) or {}
|
||||
|
||||
metadata['queued_at'] = str(int(time.time()))
|
||||
metadata['queued_to'] = queue_name
|
||||
metadata['status'] = 'queued'
|
||||
metadata.pop('processing_started', None)
|
||||
|
||||
s3.copy_object(
|
||||
Bucket=bucket,
|
||||
Key=key,
|
||||
CopySource={'Bucket': bucket, 'Key': key},
|
||||
Metadata=metadata,
|
||||
MetadataDirective='REPLACE'
|
||||
)
|
||||
|
||||
print(f"✓ Marked as queued to {queue_name}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠ Failed to mark as queued: {e}")
|
||||
|
||||
|
||||
def send_to_queue(queue_url: str, bucket: str, key: str,
|
||||
from_addr: str, recipients: list, domain: str,
|
||||
subject: str, message_id: str):
|
||||
"""
|
||||
Sendet E-Mail-Job in domain-spezifische SQS Queue
|
||||
EINE Message mit ALLEN Recipients für diese Domain
|
||||
"""
|
||||
|
||||
queue_name = queue_url.split('/')[-1]
|
||||
|
||||
message = {
|
||||
'bucket': bucket,
|
||||
'key': key,
|
||||
'from': from_addr,
|
||||
'recipients': recipients, # Liste aller Empfänger
|
||||
'domain': domain,
|
||||
'subject': subject,
|
||||
'message_id': message_id,
|
||||
'timestamp': int(time.time())
|
||||
}
|
||||
|
||||
try:
|
||||
response = sqs.send_message(
|
||||
QueueUrl=queue_url,
|
||||
MessageBody=json.dumps(message, ensure_ascii=False),
|
||||
MessageAttributes={
|
||||
'domain': {
|
||||
'StringValue': domain,
|
||||
'DataType': 'String'
|
||||
},
|
||||
'bucket': {
|
||||
'StringValue': bucket,
|
||||
'DataType': 'String'
|
||||
},
|
||||
'recipient_count': {
|
||||
'StringValue': str(len(recipients)),
|
||||
'DataType': 'Number'
|
||||
},
|
||||
'message_id': {
|
||||
'StringValue': message_id,
|
||||
'DataType': 'String'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
sqs_message_id = response['MessageId']
|
||||
print(f"✓ Queued to {queue_name}: SQS MessageId={sqs_message_id}")
|
||||
print(f" Recipients: {len(recipients)} - {', '.join(recipients)}")
|
||||
|
||||
# Als queued markieren
|
||||
mark_as_queued(bucket, key, queue_name)
|
||||
|
||||
return sqs_message_id
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to queue message: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def lambda_handler(event, context):
|
||||
"""
|
||||
Lambda Handler für SES Events
|
||||
Eine Domain pro Event = eine Queue Message mit allen Recipients
|
||||
"""
|
||||
|
||||
print(f"{'='*70}")
|
||||
print(f"Lambda invoked: {context.aws_request_id}")
|
||||
print(f"Region: {AWS_REGION}")
|
||||
print(f"{'='*70}")
|
||||
|
||||
# SES Event parsen
|
||||
try:
|
||||
record = event['Records'][0]
|
||||
ses = record['ses']
|
||||
except (KeyError, IndexError) as e:
|
||||
print(f"✗ Invalid event structure: {e}")
|
||||
return {
|
||||
'statusCode': 400,
|
||||
'body': json.dumps({'error': 'Invalid SES event'})
|
||||
}
|
||||
|
||||
mail = ses['mail']
|
||||
receipt = ses['receipt']
|
||||
|
||||
message_id = mail['messageId']
|
||||
source = mail['source']
|
||||
timestamp = mail.get('timestamp', '')
|
||||
recipients = receipt.get('recipients', [])
|
||||
|
||||
# FRÜHES LOGGING: S3 Key und Recipients
|
||||
print(f"\n🔑 S3 Key: {message_id}")
|
||||
print(f"👥 Recipients ({len(recipients)}): {', '.join(recipients)}")
|
||||
|
||||
if not recipients:
|
||||
print(f"✗ No recipients found in event")
|
||||
return {
|
||||
'statusCode': 400,
|
||||
'body': json.dumps({
|
||||
'error': 'No recipients in event',
|
||||
'message_id': message_id
|
||||
})
|
||||
}
|
||||
|
||||
# Domain extrahieren (alle Recipients haben gleiche Domain!)
|
||||
domain = recipients[0].split('@')[1].lower()
|
||||
bucket = domain_to_bucket(domain)
|
||||
|
||||
print(f"\n📧 Email Event:")
|
||||
print(f" MessageId: {message_id}")
|
||||
print(f" From: {source}")
|
||||
print(f" Domain: {domain}")
|
||||
print(f" Bucket: {bucket}")
|
||||
print(f" Timestamp: {timestamp}")
|
||||
print(f" Recipients: {len(recipients)}")
|
||||
|
||||
# Queue für Domain ermitteln
|
||||
try:
|
||||
queue_url = get_queue_url_for_domain(domain)
|
||||
queue_name = queue_url.split('/')[-1]
|
||||
print(f" Queue: {queue_name}")
|
||||
except Exception as e:
|
||||
print(f"\n✗ ERROR: {e}")
|
||||
return {
|
||||
'statusCode': 500,
|
||||
'body': json.dumps({
|
||||
'error': 'queue_not_configured',
|
||||
'domain': domain,
|
||||
'recipients': recipients,
|
||||
'message': str(e)
|
||||
})
|
||||
}
|
||||
|
||||
# S3 Object finden
|
||||
try:
|
||||
print(f"\n📦 Searching S3...")
|
||||
response = s3.list_objects_v2(
|
||||
Bucket=bucket,
|
||||
Prefix=message_id,
|
||||
MaxKeys=1
|
||||
)
|
||||
|
||||
if 'Contents' not in response or not response['Contents']:
|
||||
raise Exception(f"No S3 object found for message {message_id}")
|
||||
|
||||
key = response['Contents'][0]['Key']
|
||||
size = response['Contents'][0]['Size']
|
||||
print(f" Found: s3://{bucket}/{key}")
|
||||
print(f" Size: {size:,} bytes ({size/1024:.1f} KB)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n✗ S3 ERROR: {e}")
|
||||
return {
|
||||
'statusCode': 404,
|
||||
'body': json.dumps({
|
||||
'error': 's3_object_not_found',
|
||||
'message_id': message_id,
|
||||
'bucket': bucket,
|
||||
'details': str(e)
|
||||
})
|
||||
}
|
||||
|
||||
# Duplicate Check
|
||||
print(f"\n🔍 Checking for duplicates...")
|
||||
if is_already_processed(bucket, key):
|
||||
print(f" Already processed, skipping")
|
||||
return {
|
||||
'statusCode': 200,
|
||||
'body': json.dumps({
|
||||
'status': 'already_processed',
|
||||
'message_id': message_id,
|
||||
'recipients': recipients
|
||||
})
|
||||
}
|
||||
|
||||
# Processing Lock setzen
|
||||
print(f"\n🔒 Setting processing lock...")
|
||||
if not set_processing_lock(bucket, key):
|
||||
print(f" Already being processed by another instance")
|
||||
return {
|
||||
'statusCode': 200,
|
||||
'body': json.dumps({
|
||||
'status': 'already_processing',
|
||||
'message_id': message_id,
|
||||
'recipients': recipients
|
||||
})
|
||||
}
|
||||
|
||||
# E-Mail laden um Subject zu extrahieren
|
||||
subject = '(unknown)'
|
||||
try:
|
||||
print(f"\n📖 Reading email for metadata...")
|
||||
obj = s3.get_object(Bucket=bucket, Key=key)
|
||||
raw_bytes = obj['Body'].read()
|
||||
|
||||
# Nur Headers parsen (schneller)
|
||||
parsed = BytesParser(policy=SMTPPolicy).parsebytes(raw_bytes)
|
||||
subject = parsed.get('subject', '(no subject)')
|
||||
|
||||
print(f" Subject: {subject}")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ⚠ Could not parse email (continuing): {e}")
|
||||
|
||||
# In Queue einreihen (EINE Message mit ALLEN Recipients)
|
||||
try:
|
||||
print(f"\n📤 Queuing to {queue_name}...")
|
||||
|
||||
sqs_message_id = send_to_queue(
|
||||
queue_url=queue_url,
|
||||
bucket=bucket,
|
||||
key=key,
|
||||
from_addr=source,
|
||||
recipients=recipients, # ALLE Recipients
|
||||
domain=domain,
|
||||
subject=subject,
|
||||
message_id=message_id
|
||||
)
|
||||
|
||||
print(f"\n{'='*70}")
|
||||
print(f"✅ SUCCESS - Email queued for delivery")
|
||||
print(f"{'='*70}\n")
|
||||
|
||||
return {
|
||||
'statusCode': 200,
|
||||
'body': json.dumps({
|
||||
'status': 'queued',
|
||||
'message_id': message_id,
|
||||
'sqs_message_id': sqs_message_id,
|
||||
'queue': queue_name,
|
||||
'domain': domain,
|
||||
'recipients': recipients,
|
||||
'recipient_count': len(recipients),
|
||||
'subject': subject
|
||||
})
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n{'='*70}")
|
||||
print(f"✗ FAILED TO QUEUE")
|
||||
print(f"{'='*70}")
|
||||
print(f"Error: {e}")
|
||||
|
||||
return {
|
||||
'statusCode': 500,
|
||||
'body': json.dumps({
|
||||
'error': 'failed_to_queue',
|
||||
'message': str(e),
|
||||
'message_id': message_id,
|
||||
'recipients': recipients
|
||||
})
|
||||
}
|
||||
@@ -98,16 +98,14 @@ def lambda_handler(event, context):
|
||||
print(f"Empfänger nach Domain-Filter ({expected_domain}): {recipients}")
|
||||
|
||||
if not recipients:
|
||||
raise Exception("Keine Empfänger gefunden, Abbruch")
|
||||
|
||||
print("Keine Empfänger gefunden, setze Metadatum und überspringe SMTP")
|
||||
else:
|
||||
# Gültige Postfächer abrufen und Empfänger filtern
|
||||
valid_inboxes = get_valid_inboxes()
|
||||
valid_recipients = [rcpt for rcpt in recipients if rcpt.lower() in valid_inboxes]
|
||||
print(f"Gültige Empfänger: {valid_recipients}")
|
||||
|
||||
if not valid_recipients:
|
||||
raise Exception("Keine gültigen Postfächer für die Empfänger gefunden, Abbruch")
|
||||
|
||||
if valid_recipients:
|
||||
# SMTP-Verbindung und Envelope
|
||||
start = time.time()
|
||||
print("=== SMTP: Verbinde zu", MAILCOW_HOST, "Port", MAILCOW_PORT)
|
||||
@@ -127,6 +125,8 @@ def lambda_handler(event, context):
|
||||
|
||||
smtp.data(raw_bytes)
|
||||
print(f"SMTP-Transfer in {time.time()-start:.2f}s abgeschlossen ...")
|
||||
else:
|
||||
print("Keine gültigen Postfächer für die Empfänger gefunden, setze Metadatum und überspringe SMTP")
|
||||
|
||||
# Metadatum "processed": "true" hinzufügen
|
||||
try:
|
||||
@@ -148,5 +148,5 @@ def lambda_handler(event, context):
|
||||
|
||||
return {
|
||||
'statusCode': 200,
|
||||
'body': f"E-Mail erfolgreich an {MAILCOW_HOST}:{MAILCOW_PORT} weitergeleitet ..."
|
||||
'body': f"E-Mail verarbeitet für {bucket}, SMTP-Weiterleitung: {bool(valid_recipients)}"
|
||||
}
|
||||
Reference in New Issue
Block a user