Compare commits
227 Commits
7012f1ffd3
...
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 | |||
| 675c00209c | |||
| 5fadef1aac |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -3,4 +3,6 @@ auth
|
|||||||
.env
|
.env
|
||||||
.venv*
|
.venv*
|
||||||
__pycache__
|
__pycache__
|
||||||
node_modules
|
node_modules
|
||||||
|
ses-lambda-python/*
|
||||||
|
!ses-lambda-python/lambda_function.py
|
||||||
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
126
caddy/Caddyfile
126
caddy/Caddyfile
@@ -4,35 +4,59 @@
|
|||||||
acme_ca https://acme-v02.api.letsencrypt.org/directory
|
acme_ca https://acme-v02.api.letsencrypt.org/directory
|
||||||
debug
|
debug
|
||||||
}
|
}
|
||||||
bizmatch.net {
|
|
||||||
|
|
||||||
}
|
# Prod: Neue Domains
|
||||||
www.bizmatch.net {
|
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 {
|
||||||
|
reverse_proxy host.docker.internal:4200
|
||||||
|
}
|
||||||
|
log {
|
||||||
|
output file /var/log/caddy/access.prod.log # Separate Logs
|
||||||
|
}
|
||||||
|
encode gzip zstd
|
||||||
}
|
}
|
||||||
bayarea-cc.com {
|
bizmatch.net {
|
||||||
# TLS-Direktive entfernen, falls Cloudflare die Verbindung terminiert
|
redir https://www.bizmatch.net{uri} permanent
|
||||||
# tls {
|
}
|
||||||
# dns cloudflare {env.CLOUDFLARE_API_TOKEN}
|
www.qrmaster.net {
|
||||||
# }
|
|
||||||
|
|
||||||
handle /api {
|
|
||||||
reverse_proxy host.docker.internal:3001
|
|
||||||
}
|
|
||||||
handle {
|
handle {
|
||||||
root * /app
|
reverse_proxy host.docker.internal:3050
|
||||||
try_files {path} /index.html
|
|
||||||
file_server
|
|
||||||
}
|
}
|
||||||
log {
|
log {
|
||||||
output stderr
|
output file /var/log/caddy/qrmaster.log
|
||||||
format console
|
format console
|
||||||
}
|
}
|
||||||
encode gzip
|
encode gzip
|
||||||
}
|
}
|
||||||
www.bayarea-cc.com {
|
qrmaster.net {
|
||||||
redir https://bayarea-cc.com{uri} permanent
|
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 {
|
auth.bizmatch.net {
|
||||||
reverse_proxy https://bizmatch-net.firebaseapp.com {
|
reverse_proxy https://bizmatch-net.firebaseapp.com {
|
||||||
header_up Host bizmatch-net.firebaseapp.com
|
header_up Host bizmatch-net.firebaseapp.com
|
||||||
@@ -45,54 +69,30 @@ gitea.bizmatch.net {
|
|||||||
reverse_proxy gitea:3500
|
reverse_proxy gitea:3500
|
||||||
}
|
}
|
||||||
|
|
||||||
dev.bizmatch.net {
|
api.bizmatch.net {
|
||||||
handle /pictures/* {
|
reverse_proxy host.docker.internal:3001 { # Neu: Proxy auf Prod-Port 3001
|
||||||
root * /home/aknuth/git/bizmatch-project/bizmatch-server
|
header_up X-Real-IP {http.request.header.CF-Connecting-IP}
|
||||||
file_server
|
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 {
|
handle {
|
||||||
root * /srv
|
reverse_proxy landing:3000
|
||||||
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}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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:
|
services:
|
||||||
caddy:
|
caddy:
|
||||||
|
image: custom-caddy:2.9.1-rr1
|
||||||
container_name: caddy
|
container_name: caddy
|
||||||
image: iarekylew00t/caddy-cloudflare:latest
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.caddy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
@@ -13,18 +16,25 @@ services:
|
|||||||
- keycloak
|
- keycloak
|
||||||
- gitea
|
- gitea
|
||||||
- mail_network
|
- mail_network
|
||||||
|
- greenlens_net
|
||||||
volumes:
|
volumes:
|
||||||
- $PWD/Caddyfile:/etc/caddy/Caddyfile
|
- $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_data:/data
|
||||||
- caddy_config:/config
|
- caddy_config:/config
|
||||||
#- /home/aknuth/git/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/bizmatch/dist/bizmatch/browser:/srv
|
- /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/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/git/bay-area-affiliates/dist/bay-area-affiliates/browser:/app
|
||||||
- /home/aknuth/log/caddy:/var/log/caddy
|
- /home/aknuth/log/caddy:/var/log/caddy
|
||||||
|
- /home/aknuth/git/config-email/frontend/dist:/home/aknuth/git/config-email/frontend/dist:ro
|
||||||
environment:
|
environment:
|
||||||
- CLOUDFLARE_API_TOKEN=q1P7J3uqS96FGj_iiX2mI8y1ulTaIFrTp8tyTXhG
|
- CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}
|
||||||
- CLOUDFLARE_EMAIL=andreas.knuth@gmail.com
|
- CLOUDFLARE_EMAIL=${CLOUDFLARE_EMAIL}
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
bizmatch:
|
bizmatch:
|
||||||
@@ -35,6 +45,8 @@ networks:
|
|||||||
external: true
|
external: true
|
||||||
mail_network:
|
mail_network:
|
||||||
external: true
|
external: true
|
||||||
|
greenlens_net:
|
||||||
|
external: true
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
caddy_data:
|
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
|
#!/bin/bash
|
||||||
|
# awsdomain.sh - Konfiguriert Cloudflare mit den Amazon SES Angaben
|
||||||
if [ -z "$DOMAIN_NAME" ]; then
|
if [ -z "$DOMAIN_NAME" ]; then
|
||||||
echo "Fehler: DOMAIN_NAME ist nicht gesetzt."
|
echo "Fehler: DOMAIN_NAME ist nicht gesetzt."
|
||||||
echo "Bitte setzen Sie die Variable mit: export DOMAIN_NAME='IhreDomain.de'"
|
echo "Bitte setzen Sie die Variable mit: export DOMAIN_NAME='IhreDomain.de'"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ fi
|
|||||||
|
|
||||||
# Konfiguration
|
# Konfiguration
|
||||||
AWS_REGION=${AWS_REGION:-"us-east-2"}
|
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"
|
RULE_NAME="store-$(echo "$DOMAIN_NAME" | tr '.' '-')-to-s3"
|
||||||
|
|
||||||
echo "=== SES Konfiguration für $DOMAIN_NAME ==="
|
echo "=== SES Konfiguration für $DOMAIN_NAME ==="
|
||||||
@@ -89,6 +89,62 @@ else
|
|||||||
echo "Rule Set 'bizmatch-ruleset' ist bereits aktiv."
|
echo "Rule Set 'bizmatch-ruleset' ist bereits aktiv."
|
||||||
fi
|
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 "SES-Konfiguration für $DOMAIN_NAME abgeschlossen."
|
||||||
echo
|
echo
|
||||||
echo "WICHTIG: Überprüfen Sie die Ausgabe oben für DNS-Einträge, die Sie bei Ihrem DNS-Provider setzen müssen:"
|
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
|
# 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
|
# CF_ZONE_ID="1b7756cee93ed8ba8c05bdc3cb0a5da8" # Die Zone-ID deiner Domain bei Cloudflare
|
||||||
# DOMAIN_NAME="andreasknuth.de" # Deine Domain
|
|
||||||
AWS_REGION="us-east-2" # AWS-Region
|
AWS_REGION="us-east-2" # AWS-Region
|
||||||
if [ -z "$DOMAIN_NAME" ]; then
|
if [ -z "$DOMAIN_NAME" ]; then
|
||||||
echo "Fehler: DOMAIN_NAME ist nicht gesetzt."
|
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
|
# CNAME für mail.{Domain} anlegen
|
||||||
echo "CNAME für mail.${DOMAIN_NAME} anlegen bei Cloudflare..."
|
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
|
# SPF-Eintrag anlegen
|
||||||
echo "SPF-Eintrag anlegen bei Cloudflare..."
|
echo "SPF-Eintrag anlegen bei Cloudflare..."
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- gitea-data:/data
|
- gitea-data:/data
|
||||||
#- ./gitea/gitea-ssh:/data/git/.ssh
|
#- ./gitea/gitea-ssh:/data/git/.ssh
|
||||||
- /home/git/.ssh/:/data/git/.ssh
|
#- /home/git/.ssh/:/data/git/.ssh
|
||||||
ports:
|
ports:
|
||||||
- "3500:3500"
|
- "3500:3500"
|
||||||
- "2222:22"
|
- "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
|
||||||
|
})
|
||||||
|
}
|
||||||
152
ses-lambda-python/lambda_function.py
Normal file
152
ses-lambda-python/lambda_function.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import os
|
||||||
|
import boto3
|
||||||
|
import smtplib
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
from email.parser import BytesParser
|
||||||
|
from email.policy import default
|
||||||
|
from email.utils import getaddresses
|
||||||
|
|
||||||
|
s3 = boto3.client('s3')
|
||||||
|
|
||||||
|
MAILCOW_HOST = os.environ['MAILCOW_SMTP_HOST']
|
||||||
|
MAILCOW_PORT = int(os.environ.get('MAILCOW_SMTP_PORT', 587))
|
||||||
|
SMTP_USER = os.environ.get('MAILCOW_SMTP_USER')
|
||||||
|
SMTP_PASS = os.environ.get('MAILCOW_SMTP_PASS')
|
||||||
|
MAILCOW_API_KEY = os.environ.get('MAILCOW_API_KEY')
|
||||||
|
|
||||||
|
def domain_to_bucket(domain):
|
||||||
|
return domain.replace('.', '-') + '-emails'
|
||||||
|
|
||||||
|
def bucket_to_domain(bucket):
|
||||||
|
return bucket.replace('-emails', '').replace('-', '.')
|
||||||
|
|
||||||
|
def get_valid_inboxes():
|
||||||
|
url = 'https://mail.email-srvr.com/api/v1/get/mailbox/all'
|
||||||
|
headers = {'X-API-Key': MAILCOW_API_KEY}
|
||||||
|
try:
|
||||||
|
response = requests.get(url, headers=headers, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
mailboxes = response.json()
|
||||||
|
return {mb['username'].lower() for mb in mailboxes if mb['active_int'] == 1}
|
||||||
|
except requests.RequestException as e:
|
||||||
|
print(f"Fehler beim Abrufen der Postfächer: {e}")
|
||||||
|
raise Exception("Konnte gültige Postfächer nicht abrufen")
|
||||||
|
|
||||||
|
def lambda_handler(event, context):
|
||||||
|
rec = event['Records'][0]
|
||||||
|
|
||||||
|
if 'ses' in rec:
|
||||||
|
ses = rec['ses']
|
||||||
|
msg_id = ses['mail']['messageId']
|
||||||
|
recipients = ses['receipt']['recipients']
|
||||||
|
first_recipient = recipients[0]
|
||||||
|
domain = first_recipient.split('@')[1]
|
||||||
|
bucket = domain_to_bucket(domain)
|
||||||
|
prefix = f"emails/{msg_id}"
|
||||||
|
print(f"SES-Receipt erkannt, domain={domain}, bucket={bucket}, prefix={prefix}")
|
||||||
|
|
||||||
|
resp_list = s3.list_objects_v2(Bucket=bucket, Prefix=prefix)
|
||||||
|
if 'Contents' not in resp_list or not resp_list['Contents']:
|
||||||
|
raise Exception(f"Kein Objekt unter Prefix {prefix} in Bucket {bucket} gefunden")
|
||||||
|
key = resp_list['Contents'][0]['Key']
|
||||||
|
|
||||||
|
elif 's3' in rec:
|
||||||
|
s3info = rec['s3']
|
||||||
|
bucket = s3info['bucket']['name']
|
||||||
|
key = s3info['object']['key']
|
||||||
|
print("S3-Put erkannt, bucket =", bucket, "key =", key)
|
||||||
|
recipients = []
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception("Unbekannter Event-Typ")
|
||||||
|
|
||||||
|
# Prüfen, ob das Objekt bereits verarbeitet wurde
|
||||||
|
try:
|
||||||
|
resp = s3.head_object(Bucket=bucket, Key=key)
|
||||||
|
if resp.get('Metadata', {}).get('processed') == 'true':
|
||||||
|
print(f"Objekt {key} bereits verarbeitet (processed=true), überspringe Verarbeitung")
|
||||||
|
return {
|
||||||
|
'statusCode': 200,
|
||||||
|
'body': f"Objekt {key} bereits verarbeitet, keine erneute Weiterleitung"
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fehler beim Prüfen der Metadaten: {e}")
|
||||||
|
|
||||||
|
# Raw-Mail aus S3 holen
|
||||||
|
resp = s3.get_object(Bucket=bucket, Key=key)
|
||||||
|
raw_bytes = resp['Body'].read()
|
||||||
|
print(f"E-Mail geladen: {len(raw_bytes)} Bytes")
|
||||||
|
|
||||||
|
# Parsen für Logging
|
||||||
|
parsed = BytesParser(policy=default).parsebytes(raw_bytes)
|
||||||
|
subj = parsed.get('subject', '(kein Subject)')
|
||||||
|
frm_addr = getaddresses(parsed.get_all('from', []))[0][1]
|
||||||
|
print(f"Parsed: From={frm_addr} Subject={subj}")
|
||||||
|
|
||||||
|
# Empfänger aus Headern ziehen, falls nicht aus SES
|
||||||
|
if not recipients:
|
||||||
|
to_addrs = [addr for _name, addr in getaddresses(parsed.get_all('to', []))]
|
||||||
|
cc_addrs = [addr for _name, addr in getaddresses(parsed.get_all('cc', []))]
|
||||||
|
bcc_addrs = [addr for _name, addr in getaddresses(parsed.get_all('bcc', []))]
|
||||||
|
recipients = to_addrs + cc_addrs + bcc_addrs
|
||||||
|
print("Empfänger aus Headern:", recipients)
|
||||||
|
|
||||||
|
# Im S3-Flow nur Empfänger mit passender Domain behalten
|
||||||
|
expected_domain = bucket_to_domain(bucket)
|
||||||
|
recipients = [rcpt for rcpt in recipients if rcpt.lower().split('@')[1] == expected_domain]
|
||||||
|
print(f"Empfänger nach Domain-Filter ({expected_domain}): {recipients}")
|
||||||
|
|
||||||
|
if not recipients:
|
||||||
|
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 valid_recipients:
|
||||||
|
# SMTP-Verbindung und Envelope
|
||||||
|
start = time.time()
|
||||||
|
print("=== SMTP: Verbinde zu", MAILCOW_HOST, "Port", MAILCOW_PORT)
|
||||||
|
with smtplib.SMTP(MAILCOW_HOST, MAILCOW_PORT, timeout=30) as smtp:
|
||||||
|
smtp.ehlo()
|
||||||
|
smtp.starttls()
|
||||||
|
smtp.ehlo()
|
||||||
|
if SMTP_USER and SMTP_PASS:
|
||||||
|
smtp.login(SMTP_USER, SMTP_PASS)
|
||||||
|
|
||||||
|
print("=== SMTP: MAIL FROM", frm_addr)
|
||||||
|
smtp.mail(frm_addr)
|
||||||
|
|
||||||
|
for rcpt in valid_recipients:
|
||||||
|
print("=== SMTP: RCPT TO", rcpt)
|
||||||
|
smtp.rcpt(rcpt)
|
||||||
|
|
||||||
|
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:
|
||||||
|
resp = s3.head_object(Bucket=bucket, Key=key)
|
||||||
|
current_metadata = resp.get('Metadata', {})
|
||||||
|
new_metadata = current_metadata.copy()
|
||||||
|
new_metadata['processed'] = 'true'
|
||||||
|
s3.copy_object(
|
||||||
|
Bucket=bucket,
|
||||||
|
Key=key,
|
||||||
|
CopySource={'Bucket': bucket, 'Key': key},
|
||||||
|
Metadata=new_metadata,
|
||||||
|
MetadataDirective='REPLACE'
|
||||||
|
)
|
||||||
|
print("Metadatum 'processed:true' hinzugefügt.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fehler beim Schreiben des Metadatums: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
return {
|
||||||
|
'statusCode': 200,
|
||||||
|
'body': f"E-Mail verarbeitet für {bucket}, SMTP-Weiterleitung: {bool(valid_recipients)}"
|
||||||
|
}
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
import time
|
|
||||||
import gzip
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import urllib.request
|
|
||||||
import urllib.error
|
|
||||||
import urllib.parse
|
|
||||||
import logging
|
|
||||||
import boto3
|
|
||||||
import base64
|
|
||||||
from email.parser import BytesParser
|
|
||||||
from email.policy import default
|
|
||||||
from email.utils import getaddresses
|
|
||||||
|
|
||||||
logger = logging.getLogger()
|
|
||||||
logger.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
API_BASE_URL = os.environ['API_BASE_URL']
|
|
||||||
API_TOKEN = os.environ['API_TOKEN']
|
|
||||||
MAX_EMAIL_SIZE = int(os.environ.get('MAX_EMAIL_SIZE', '10485760'))
|
|
||||||
|
|
||||||
s3_client = boto3.client('s3')
|
|
||||||
|
|
||||||
def mark_email_processed(bucket, key, metadata, s3_client, processor='lambda'):
|
|
||||||
"""Setzt in S3 das processed-Flag per Metadata."""
|
|
||||||
try:
|
|
||||||
s3_client.copy_object(
|
|
||||||
Bucket=bucket,
|
|
||||||
Key=key,
|
|
||||||
CopySource={'Bucket': bucket, 'Key': key},
|
|
||||||
Metadata={
|
|
||||||
'processed': metadata,
|
|
||||||
'processed_timestamp': str(int(time.time())),
|
|
||||||
'processor': processor
|
|
||||||
},
|
|
||||||
MetadataDirective='REPLACE'
|
|
||||||
)
|
|
||||||
logger.info(f"Marked S3 object {bucket}/{key} as {metadata}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Markieren {bucket}/{key}: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def call_api_once(payload, domain, request_id):
|
|
||||||
"""Single-shot POST, kein Retry."""
|
|
||||||
url = f"{API_BASE_URL}/process/{domain}"
|
|
||||||
data = json.dumps(payload).encode('utf-8')
|
|
||||||
req = urllib.request.Request(url, data=data, method='POST')
|
|
||||||
req.add_header('Authorization', f'Bearer {API_TOKEN}')
|
|
||||||
req.add_header('Content-Type', 'application/json')
|
|
||||||
req.add_header('X-Request-ID', request_id)
|
|
||||||
logger.info(f"[{request_id}] OUTGOING POST {url}: "
|
|
||||||
f"domain={domain}, key={payload['s3_key']}, bucket={payload['s3_bucket']}, "
|
|
||||||
f"orig_size={payload['original_size']}, comp_size={payload['compressed_size']}")
|
|
||||||
|
|
||||||
with urllib.request.urlopen(req, timeout=25) as resp:
|
|
||||||
code = resp.getcode()
|
|
||||||
if code == 200:
|
|
||||||
logger.info(f"[{request_id}] API returned 200 OK")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
body = resp.read().decode('utf-8', errors='ignore')
|
|
||||||
logger.error(f"[{request_id}] API returned {code}: {body}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def lambda_handler(event, context):
|
|
||||||
req_id = context.aws_request_id
|
|
||||||
rec = event['Records'][0]['s3']
|
|
||||||
bucket = rec['bucket']['name']
|
|
||||||
key = urllib.parse.unquote_plus(rec['object']['key'])
|
|
||||||
logger.info(f"[{req_id}] Processing {bucket}/{key}")
|
|
||||||
|
|
||||||
# Kopf-Check
|
|
||||||
head = s3_client.head_object(Bucket=bucket, Key=key)
|
|
||||||
metadata = head.get('Metadata', {})
|
|
||||||
if metadata.get('processed') == 'true':
|
|
||||||
logger.info(f"[{req_id}] Skipping already processed object")
|
|
||||||
return {'statusCode': 200, 'body': 'Already processed'}
|
|
||||||
|
|
||||||
size = head['ContentLength']
|
|
||||||
if size > MAX_EMAIL_SIZE:
|
|
||||||
logger.warning(f"[{req_id}] Email too large: {size} bytes")
|
|
||||||
return {'statusCode': 413}
|
|
||||||
|
|
||||||
# E-Mail Inhalt laden
|
|
||||||
body = s3_client.get_object(Bucket=bucket, Key=key)['Body'].read()
|
|
||||||
|
|
||||||
# 1) Parsen und Loggen von from/to
|
|
||||||
try:
|
|
||||||
msg = BytesParser(policy=default).parsebytes(body)
|
|
||||||
from_addr = getaddresses(msg.get_all('from', []))[0][1] if msg.get_all('from') else ''
|
|
||||||
to_addrs = [addr for _n, addr in getaddresses(msg.get_all('to', []))]
|
|
||||||
logger.info(f"[{req_id}] Parsed email: from={from_addr}, to={to_addrs}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[{req_id}] Fehler beim Parsen der Email: {e}")
|
|
||||||
from_addr = ''
|
|
||||||
to_addrs = []
|
|
||||||
|
|
||||||
# 2) Komprimieren und Payload bauen
|
|
||||||
compressed = gzip.compress(body)
|
|
||||||
payload = {
|
|
||||||
's3_bucket': bucket,
|
|
||||||
's3_key': key,
|
|
||||||
'domain': bucket.replace('-', '.').rsplit('.emails',1)[0],
|
|
||||||
'email_content': base64.b64encode(compressed).decode(),
|
|
||||||
'compressed': True,
|
|
||||||
'etag': head['ETag'].strip('"'),
|
|
||||||
'request_id': req_id,
|
|
||||||
'original_size': len(body),
|
|
||||||
'compressed_size': len(compressed)
|
|
||||||
}
|
|
||||||
|
|
||||||
# 3) Single API call
|
|
||||||
try:
|
|
||||||
success = call_api_once(payload, payload['domain'], req_id)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[{req_id}] API-Call-Exception: {e}")
|
|
||||||
success = False
|
|
||||||
|
|
||||||
# 4) Handling nach API-Call
|
|
||||||
if success:
|
|
||||||
# normal processed
|
|
||||||
mark_email_processed(bucket, key, 'true', s3_client)
|
|
||||||
else:
|
|
||||||
# nur wenn es to_addrs gibt
|
|
||||||
if to_addrs:
|
|
||||||
bucket_domain = payload['domain']
|
|
||||||
domains = [addr.split('@')[-1] for addr in to_addrs if '@' in addr]
|
|
||||||
status = 'unknownUser' if bucket_domain in domains else 'unknownDomain'
|
|
||||||
mark_email_processed(bucket, key, status, s3_client)
|
|
||||||
else:
|
|
||||||
logger.info(f"[{req_id}] Keine Empfänger, kein Markieren")
|
|
||||||
|
|
||||||
return {'statusCode': 200, 'body': 'Done'}
|
|
||||||
Reference in New Issue
Block a user