Compare commits
397 Commits
3d29e6ffef
...
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 | |||
| 7012f1ffd3 | |||
| 14a212cf14 | |||
| c66f27225c | |||
| 6359adb807 | |||
| cb25a44d69 | |||
| 9a3e279212 | |||
| d84a5a69b0 | |||
| b8152cbc39 | |||
| b8fc26dc46 | |||
| 493743e8aa | |||
| b556ac8283 | |||
| 65866de63b | |||
| 0663a7c6bc | |||
| 9862c77f18 | |||
| 8d1afdeffd | |||
| 3a528b37a1 | |||
| 68f5f0c3f4 | |||
| c645f71225 | |||
| b2a0c6e611 | |||
| d92618f225 | |||
| 05b81d28db | |||
| ac7ebbb7f3 | |||
| 9ef67da70e | |||
| d6f90f444b | |||
| 3deaedc235 | |||
| d98a9086ca | |||
| 34393b0807 | |||
| 4388f6efc2 | |||
| fdbc32bed9 | |||
| 4943bccb3e | |||
| dc57e08030 | |||
| ce87a9e3a5 | |||
| bd6d7a8c92 | |||
| 67b97f514b | |||
| 434f94f882 | |||
| 26adce6ecf | |||
| fc6fa76bc0 | |||
| 0d0391b6ee | |||
| 24b05aa210 | |||
| 9287e9be8b | |||
| f291076a3a | |||
| 726d2607da | |||
| 7413b54af4 | |||
| 5d961fad9d | |||
| 55781bea6f | |||
| 6c65798d56 | |||
| 9ac6357a46 | |||
| 1758b29062 | |||
| b2e4b85205 | |||
| 71869cf458 | |||
| 2ee59c6153 | |||
| 988fd2906c | |||
| dc968e1cac | |||
| 3079593c79 | |||
| 853c02bcb9 | |||
| aff78a12c8 | |||
| 57f969fac6 | |||
| bd0cc5debc | |||
| 04399202a5 | |||
| ae64e98af0 | |||
| ca45cab9bd | |||
| 73df166702 | |||
| 2b6f04afbe | |||
| 7748cdff65 | |||
| be85896404 | |||
| 991047d286 | |||
| 1b05ae48ad | |||
| 3dc5b74a8d | |||
| 67ec6a74ac | |||
| 277b2b2b2f | |||
| f2d633059c | |||
| c9c41685b3 | |||
| dcda223974 | |||
| ae77abacf1 | |||
| aeec0796f8 | |||
| 46799bb63a | |||
| 08a2e13ada | |||
| b6a60f8a20 | |||
| 538abb6e59 | |||
| 82a1baeaf2 | |||
| 2f32bd53df | |||
| 3a12027852 | |||
| d276f9f297 | |||
| 3b63b66472 | |||
| 749abdb29a | |||
| e4f00357da | |||
| b670438ca2 | |||
| 3f669a53a4 | |||
| 7e4b24fc6a | |||
| 1af90e6eb9 | |||
| 7873002167 | |||
| ba36b6753a | |||
| 8233fc4fca | |||
| be8115c3da | |||
| 62d1cc22cf | |||
| 8c2f3a170d | |||
| 311243f44d | |||
| 6b0ea3d0f8 | |||
| 1a4d14b396 | |||
| 09b7a16fd1 | |||
| 92e7e06661 | |||
| 6df151937c | |||
| 768b1b6e53 | |||
| 7cbe56a2f2 | |||
| 0bffc8856f | |||
| 04d07414c9 | |||
| fc1d072404 | |||
| 212341744c | |||
| 9905481e26 | |||
| 67e37f8985 | |||
| 2174fe4869 | |||
| 6ae4da137e | |||
| b1ef230144 | |||
| ec2949bd99 | |||
| 148ab14401 | |||
| 9cf7435380 | |||
| 218e806336 | |||
| ff8d54817a | |||
| 044bb3a960 | |||
| 2179d5110f | |||
| 68dc2ae779 | |||
| 4149655432 | |||
| dae856b601 | |||
| e283546633 | |||
| 7f30b430dd | |||
| 7351684c53 | |||
| 1af82dfd23 | |||
| 5f9613e169 | |||
| df0903958f | |||
| 8f3643e4c1 | |||
| e0a47e5cc9 | |||
| 9bae8a4cbe | |||
| 01c86681f2 | |||
| 76e83d7fd1 | |||
| c87cce7255 | |||
| 634791e9fd | |||
| 3dcbc01284 | |||
| b641acbb78 | |||
| 1a83422d6f | |||
| 8bd0cce4e3 | |||
| c939f7c629 | |||
| 24913f7f5e | |||
| 80c29b40df | |||
| f326cd6a9c | |||
| fefda4846c | |||
| 3f38793658 | |||
| 6ea75209e7 | |||
| f49e4b5d92 | |||
| 70eae79aff | |||
| 810476831e | |||
| 9d4c6d283d | |||
| 795757320d | |||
| eeb2a0e6b2 | |||
| b7cedcae3d | |||
| c128ea96c4 | |||
| 409d6b6ce2 | |||
| aecccbb023 | |||
| a57b73fcc5 | |||
| fe29328c3b | |||
| 872f58e727 | |||
| d653c1b5d1 | |||
| 1386a2ccdc | |||
| 5fd2ddd2a5 | |||
| 4d7a1c54be | |||
| 4fb91e8ba6 | |||
| 4b9807faca | |||
| b4b0b1056a | |||
| 236803d614 | |||
| 6eafdea095 | |||
| 3613401473 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,2 +1,8 @@
|
|||||||
*.jar
|
*.jar
|
||||||
auth
|
auth
|
||||||
|
.env
|
||||||
|
.venv*
|
||||||
|
__pycache__
|
||||||
|
node_modules
|
||||||
|
ses-lambda-python/*
|
||||||
|
!ses-lambda-python/lambda_function.py
|
||||||
40
README
Normal file
40
README
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
Anleitung zur Generierung einer neuen Domain/DNS/Email
|
||||||
|
Grundlegendes
|
||||||
|
- Domain kaufen
|
||||||
|
- Domain bei cloudflare eintragen und den A Record und einen CNAME www eintragen (per Hand)
|
||||||
|
- bei Bedarf die Nameserver beim Domain Halter eintragen (Hetzner, networksolutions) und abwarten, das cloudflare den Status active anzeigt
|
||||||
|
|
||||||
|
AMAZON Einrichtung
|
||||||
|
- Die 3 Scripte zur Amamzon Konfiguration ausführen
|
||||||
|
1. awss3.sh
|
||||||
|
2. awsses.sh
|
||||||
|
3. awsiam.sh (Access Token und SMTP Passwort abspeichern !!!!)
|
||||||
|
- cloudflareDns.sh ausführen und warten bis die Identities alle auf Aktiv gesetzt sind
|
||||||
|
|
||||||
|
Server Arbeiten
|
||||||
|
- .env anpassen und neue Domain eintragen, den Bucket Namen sowie die Usernames
|
||||||
|
- dovecot_passwd_manager.py update ausführen um die passwd anzupassen
|
||||||
|
- Zertifikate erzeugen und kopieren
|
||||||
|
1. DOMAIN=imap.[DOMAIN] EMAIL=andreas.knuth@gmail.com docker compose run --rm certbot
|
||||||
|
2. cp -R letsencrypt/archive/imap.[DOMAIN] ../dovecot/ssl/
|
||||||
|
3. in der dovecor.conf folgenden Eintrag hinzufügen
|
||||||
|
local_name imap.[DOMAIN] {
|
||||||
|
ssl_cert = </etc/dovecot/ssl/imap.[DOMAIN]/fullchain1.pem
|
||||||
|
ssl_key = </etc/dovecot/ssl/imap.[DOMAIN]/privkey1.pem
|
||||||
|
}
|
||||||
|
|
||||||
|
Einrichten des EMail Clients
|
||||||
|
- IMAP
|
||||||
|
1. Server Name: imap.[DOMAIN]
|
||||||
|
2. Port: 993
|
||||||
|
3. UserName: Account Name -> z.B. info@[DOMAIN]
|
||||||
|
4. Security: SSL/TLS
|
||||||
|
5. Auth: Normal Passwd
|
||||||
|
|
||||||
|
-SMTP
|
||||||
|
1. Server Name: email-smtp.us-east-2.amazonaws.com
|
||||||
|
2. Port: 587
|
||||||
|
3. User Name: [Access Token von oben]
|
||||||
|
4. Authentication method: Normal password
|
||||||
|
5. Connection Security: STARTTLS
|
||||||
|
6. Passwort: SMTP Passwort, nicht der Security Token !!!!
|
||||||
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,25 +0,0 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
|
|
||||||
postgres:
|
|
||||||
container_name: postgres_app
|
|
||||||
image: postgres:15.5-alpine3.19
|
|
||||||
volumes:
|
|
||||||
- bizmatch_volume:/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
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
bizmatch_volume:
|
|
||||||
external: true
|
|
||||||
132
caddy/Caddyfile
132
caddy/Caddyfile
@@ -1,76 +1,98 @@
|
|||||||
{
|
{
|
||||||
acme_dns cloudflare q1P7J3uqS96FGj_iiX2mI8y1ulTaIFrTp8tyTXhG
|
email {env.CLOUDFLARE_EMAIL}
|
||||||
|
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
|
||||||
|
acme_ca https://acme-v02.api.letsencrypt.org/directory
|
||||||
|
debug
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prod: Neue Domains
|
||||||
|
www.bizmatch.net {
|
||||||
|
handle /pictures/* {
|
||||||
|
root * /home/aknuth/git/bizmatch-project/bizmatch-server # Prod-Ordner
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
# Statische Dateien (CSS, JS, Bilder) – lange cachen, da sich der Name bei Änderungen ändert
|
||||||
|
header /assets/* Cache-Control "public, max-age=31536000, immutable"
|
||||||
|
header /*.css Cache-Control "public, max-age=31536000, immutable"
|
||||||
|
header /*.js Cache-Control "public, max-age=31536000, immutable"
|
||||||
|
|
||||||
|
# Die index.html und API-Antworten – NIEMALS cachen
|
||||||
|
header /index.html Cache-Control "no-cache, no-store, must-revalidate"
|
||||||
|
|
||||||
|
handle {
|
||||||
|
reverse_proxy host.docker.internal:4200
|
||||||
|
}
|
||||||
|
log {
|
||||||
|
output file /var/log/caddy/access.prod.log # Separate Logs
|
||||||
|
}
|
||||||
|
encode gzip zstd
|
||||||
}
|
}
|
||||||
bizmatch.net {
|
bizmatch.net {
|
||||||
tls {
|
redir https://www.bizmatch.net{uri} permanent
|
||||||
dns cloudflare q1P7J3uqS96FGj_iiX2mI8y1ulTaIFrTp8tyTXhG
|
|
||||||
}
|
}
|
||||||
|
www.qrmaster.net {
|
||||||
|
handle {
|
||||||
|
reverse_proxy host.docker.internal:3050
|
||||||
}
|
}
|
||||||
www.bizmatch.net {
|
log {
|
||||||
tls {
|
output file /var/log/caddy/qrmaster.log
|
||||||
dns cloudflare q1P7J3uqS96FGj_iiX2mI8y1ulTaIFrTp8tyTXhG
|
format console
|
||||||
}
|
}
|
||||||
|
encode gzip
|
||||||
}
|
}
|
||||||
|
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 {
|
auth.bizmatch.net {
|
||||||
reverse_proxy keycloak:8080 {
|
reverse_proxy https://bizmatch-net.firebaseapp.com {
|
||||||
header_up Host {http.request.host}
|
header_up Host bizmatch-net.firebaseapp.com
|
||||||
header_up X-Real-IP {http.request.remote}
|
header_up X-Forwarded-For {remote_host}
|
||||||
header_up X-Forwarded-For {http.request.remote}
|
header_up X-Forwarded-Proto {scheme}
|
||||||
header_up X-Forwarded-Host {http.request.host}
|
header_up X-Real-IP {remote_host}
|
||||||
header_up X-Forwarded-Server {http.request.host}
|
|
||||||
header_up X-Forwarded-Port {http.request.port}
|
|
||||||
header_up X-Forwarded-Proto {http.request.scheme}
|
|
||||||
header_up Upgrade {http.request.header.Upgrade}
|
|
||||||
header_up Connection {http.request.header.Connection}
|
|
||||||
# Entfernen des X-Frame-Options-Headers
|
|
||||||
# header_up -X-Frame-Options
|
|
||||||
}
|
|
||||||
tls {
|
|
||||||
dns cloudflare q1P7J3uqS96FGj_iiX2mI8y1ulTaIFrTp8tyTXhG
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
gitea.bizmatch.net {
|
gitea.bizmatch.net {
|
||||||
reverse_proxy gitea:3500
|
reverse_proxy gitea:3500
|
||||||
tls {
|
|
||||||
dns cloudflare q1P7J3uqS96FGj_iiX2mI8y1ulTaIFrTp8tyTXhG
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
file_server
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
tls {
|
|
||||||
dns cloudflare q1P7J3uqS96FGj_iiX2mI8y1ulTaIFrTp8tyTXhG
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api-dev.bizmatch.net {
|
|
||||||
reverse_proxy host.docker.internal:3000 {
|
|
||||||
header_up X-Real-IP {http.request.header.CF-Connecting-IP}
|
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-For {http.request.header.CF-Connecting-IP}
|
||||||
header_up X-Forwarded-Proto {http.request.header.X-Forwarded-Proto}
|
header_up X-Forwarded-Proto {http.request.header.X-Forwarded-Proto}
|
||||||
header_up CF-IPCountry {http.request.header.CF-IPCountry}
|
header_up CF-IPCountry {http.request.header.CF-IPCountry}
|
||||||
}
|
}
|
||||||
tls {
|
}
|
||||||
dns cloudflare q1P7J3uqS96FGj_iiX2mI8y1ulTaIFrTp8tyTXhG
|
|
||||||
|
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 {
|
||||||
|
reverse_proxy landing:3000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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,11 +1,13 @@
|
|||||||
version: '3.7'
|
|
||||||
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"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- 'host.docker.internal:host-gateway'
|
- 'host.docker.internal:host-gateway'
|
||||||
@@ -13,16 +15,26 @@ services:
|
|||||||
- bizmatch
|
- bizmatch
|
||||||
- keycloak
|
- keycloak
|
||||||
- gitea
|
- gitea
|
||||||
|
- 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/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=${CLOUDFLARE_EMAIL}
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
bizmatch:
|
bizmatch:
|
||||||
@@ -31,6 +43,10 @@ networks:
|
|||||||
external: true
|
external: true
|
||||||
gitea:
|
gitea:
|
||||||
external: true
|
external: true
|
||||||
|
mail_network:
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
10
certbot/docker-compose.yml
Normal file
10
certbot/docker-compose.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
services:
|
||||||
|
certbot:
|
||||||
|
image: certbot/dns-cloudflare
|
||||||
|
volumes:
|
||||||
|
- ./letsencrypt:/etc/letsencrypt
|
||||||
|
- ./cloudflare:/cloudflare
|
||||||
|
environment:
|
||||||
|
- DOMAIN=${DOMAIN:-mail.haiky.app}
|
||||||
|
- EMAIL=${EMAIL:-deine-email@example.com}
|
||||||
|
command: certonly --dns-cloudflare --dns-cloudflare-credentials /cloudflare/cloudflare.ini --email ${EMAIL:-deine-email@example.com} --agree-tos --no-eff-email -d ${DOMAIN:-mail.haiky.app}
|
||||||
11
certbot/get-certificate.sh
Executable file
11
certbot/get-certificate.sh
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
DOMAIN=${1:-mail.haiky.app}
|
||||||
|
EMAIL=${2:-deine-email@example.com}
|
||||||
|
|
||||||
|
echo "Generiere Zertifikat für $DOMAIN mit E-Mail $EMAIL"
|
||||||
|
docker compose run --rm certbot certonly --dns-cloudflare --dns-cloudflare-credentials /cloudflare/cloudflare.ini --email "$EMAIL" --agree-tos --no-eff-email -d "$DOMAIN"
|
||||||
|
|
||||||
|
echo "Ändere Berechtigungen der generierten Zertifikate"
|
||||||
|
sudo chown -R $(id -u):$(id -g) ./letsencrypt
|
||||||
|
|
||||||
|
echo "Fertig! Zertifikate wurden in ./letsencrypt/live/$DOMAIN/ gespeichert"
|
||||||
209
dovecot/awsdomain.sh
Executable file
209
dovecot/awsdomain.sh
Executable file
@@ -0,0 +1,209 @@
|
|||||||
|
#!/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'"
|
||||||
|
exit 1 # Skript mit Fehlercode beenden
|
||||||
|
fi
|
||||||
|
AWS_REGION="us-east-2"
|
||||||
|
EMAIL_PREFIX="emails/"
|
||||||
|
S3_BUCKET_NAME=$(echo "$DOMAIN_NAME" | tr '.' '-' | awk '{print $0 "-emails"}')
|
||||||
|
# Ersetzen Sie alle Punkte durch Bindestriche und erstellen Sie den RULE_NAME
|
||||||
|
RULE_NAME="store-$(echo "$DOMAIN_NAME" | tr '.' '-')-to-s3"
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# S3 Bucket erstellen
|
||||||
|
# ------------------------
|
||||||
|
echo "S3 Bucket erstellen..."
|
||||||
|
aws s3api create-bucket \
|
||||||
|
--bucket ${S3_BUCKET_NAME} \
|
||||||
|
--region ${AWS_REGION} \
|
||||||
|
--create-bucket-configuration LocationConstraint=${AWS_REGION}
|
||||||
|
|
||||||
|
# Öffentlichen Zugriff blockieren
|
||||||
|
aws s3api put-public-access-block \
|
||||||
|
--bucket ${S3_BUCKET_NAME} \
|
||||||
|
--public-access-block-configuration "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"
|
||||||
|
|
||||||
|
# Lebenszyklus-Konfiguration hinzufügen
|
||||||
|
aws s3api put-bucket-lifecycle-configuration \
|
||||||
|
--bucket ${S3_BUCKET_NAME} \
|
||||||
|
--lifecycle-configuration '{
|
||||||
|
"Rules": [
|
||||||
|
{
|
||||||
|
"ID": "DeleteOldEmails",
|
||||||
|
"Status": "Enabled",
|
||||||
|
"Expiration": {
|
||||||
|
"Days": 90
|
||||||
|
},
|
||||||
|
"Filter": {
|
||||||
|
"Prefix": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
|
||||||
|
echo "S3 Bucket Policy hinzufügen..."
|
||||||
|
aws s3api put-bucket-policy \
|
||||||
|
--bucket ${S3_BUCKET_NAME} \
|
||||||
|
--policy '{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Principal": {
|
||||||
|
"Service": "ses.amazonaws.com"
|
||||||
|
},
|
||||||
|
"Action": [
|
||||||
|
"s3:PutObject",
|
||||||
|
"s3:GetBucketLocation",
|
||||||
|
"s3:ListBucket"
|
||||||
|
],
|
||||||
|
"Resource": [
|
||||||
|
"arn:aws:s3:::'${S3_BUCKET_NAME}'",
|
||||||
|
"arn:aws:s3:::'${S3_BUCKET_NAME}'/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# SES Domain-Identität erstellen
|
||||||
|
# ------------------------
|
||||||
|
echo "SES Domain-Identität erstellen..."
|
||||||
|
aws sesv2 create-email-identity \
|
||||||
|
--email-identity ${DOMAIN_NAME} \
|
||||||
|
--region ${AWS_REGION}
|
||||||
|
|
||||||
|
# DKIM-Signierung aktivieren
|
||||||
|
aws sesv2 put-email-identity-dkim-attributes \
|
||||||
|
--email-identity ${DOMAIN_NAME} \
|
||||||
|
--signing-enabled \
|
||||||
|
--region ${AWS_REGION}
|
||||||
|
|
||||||
|
# Mail-From-Domain konfigurieren
|
||||||
|
aws sesv2 put-email-identity-mail-from-attributes \
|
||||||
|
--email-identity ${DOMAIN_NAME} \
|
||||||
|
--mail-from-domain "mail.${DOMAIN_NAME}" \
|
||||||
|
--behavior-on-mx-failure USE_DEFAULT_VALUE \
|
||||||
|
--region ${AWS_REGION}
|
||||||
|
|
||||||
|
# 3. Receipt Rule Set erstellen
|
||||||
|
echo "Receipt Rule for bizmatch ruleset erstellen..."
|
||||||
|
|
||||||
|
aws ses create-receipt-rule --rule-set-name "bizmatch-ruleset" --rule '{
|
||||||
|
"Name": "'"${RULE_NAME}"'",
|
||||||
|
"Enabled": true,
|
||||||
|
"ScanEnabled": true,
|
||||||
|
"Actions": [{
|
||||||
|
"S3Action": {
|
||||||
|
"BucketName": "'"${S3_BUCKET_NAME}"'",
|
||||||
|
"ObjectKeyPrefix": "emails/"
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"TlsPolicy": "Require"
|
||||||
|
}'
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# IAM-User erstellen
|
||||||
|
# -------------------------
|
||||||
|
USER_NAME="${DOMAIN_NAME//./-}-ses-user" # Ersetzt Punkte durch Bindestriche für validen IAM-Username
|
||||||
|
|
||||||
|
NODE_SCRIPT_PATH="./generate_ses_smtp_password.js"
|
||||||
|
# Prüfen, ob das Node.js-Script existiert
|
||||||
|
if [ ! -f "$NODE_SCRIPT_PATH" ]; then
|
||||||
|
echo "Fehler: Das Node.js-Script '$NODE_SCRIPT_PATH' wurde nicht gefunden."
|
||||||
|
echo "Bitte stelle sicher, dass das Script im angegebenen Pfad existiert."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Erstelle IAM-User: $USER_NAME"
|
||||||
|
aws iam create-user --user-name $USER_NAME
|
||||||
|
|
||||||
|
# 2. Policy-Dokument für SES-Vollzugriff erstellen
|
||||||
|
POLICY_NAME="${USER_NAME}-SendRawEmailPolicy"
|
||||||
|
POLICY_DOCUMENT='{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": "ses:SendRawEmail",
|
||||||
|
"Resource": "*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
echo "Erstelle benutzerdefinierte Policy für SES SendRawEmail"
|
||||||
|
POLICY_ARN=$(aws iam create-policy \
|
||||||
|
--policy-name $POLICY_NAME \
|
||||||
|
--policy-document "$POLICY_DOCUMENT" \
|
||||||
|
--query 'Policy.Arn' \
|
||||||
|
--output text)
|
||||||
|
|
||||||
|
echo "Hänge Policy an: $POLICY_ARN"
|
||||||
|
aws iam attach-user-policy \
|
||||||
|
--user-name $USER_NAME \
|
||||||
|
--policy-arn $POLICY_ARN
|
||||||
|
|
||||||
|
# 4. Access Key und Secret Key für den User erstellen
|
||||||
|
echo "Erstelle Access Key für den User: $USER_NAME"
|
||||||
|
KEY_OUTPUT=$(aws iam create-access-key --user-name $USER_NAME)
|
||||||
|
|
||||||
|
# 5. Keys ausgeben (am besten in eine sichere Datei speichern)
|
||||||
|
echo "Zugriffsschlüssel wurden erstellt. Bitte sicher aufbewahren:"
|
||||||
|
echo "$KEY_OUTPUT" | jq .
|
||||||
|
|
||||||
|
# Optional: Keys in separaten Variablen speichern für weitere Verwendung
|
||||||
|
ACCESS_KEY=$(echo "$KEY_OUTPUT" | jq -r .AccessKey.AccessKeyId)
|
||||||
|
SECRET_KEY=$(echo "$KEY_OUTPUT" | jq -r .AccessKey.SecretAccessKey)
|
||||||
|
|
||||||
|
echo "ACCESS_KEY: $ACCESS_KEY"
|
||||||
|
echo "SECRET_KEY: $SECRET_KEY"
|
||||||
|
|
||||||
|
echo "WICHTIG: Speichere den Secret Key jetzt, da er später nicht mehr abgerufen werden kann!"
|
||||||
|
# --------------------------
|
||||||
|
# SMTP Passwort generieren
|
||||||
|
# --------------------------
|
||||||
|
echo -e "\nGeneriere SMTP-Passwort für Region $AWS_REGION..."
|
||||||
|
|
||||||
|
# Führe das Node.js-Script aus, um das SMTP-Passwort zu generieren
|
||||||
|
SMTP_PASSWORD=$(node "$NODE_SCRIPT_PATH" "$SECRET_KEY" "$AWS_REGION")
|
||||||
|
|
||||||
|
# Prüfen, ob die Ausführung erfolgreich war
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Fehler bei der Generierung des SMTP-Passworts. Bitte überprüfe das Node.js-Script."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# SMTP-Benutzername ist der Access Key
|
||||||
|
SMTP_USERNAME="$ACCESS_KEY"
|
||||||
|
|
||||||
|
# Ausgabe der SMTP-Anmeldeinformationen
|
||||||
|
echo -e "\nSMTP-Anmeldeinformationen für Amazon SES in Region $AWS_REGION:"
|
||||||
|
echo "--------------------------------------------------------------"
|
||||||
|
echo "SMTP-Server: email-smtp.$AWS_REGION.amazonaws.com"
|
||||||
|
echo "SMTP-Port: 587 (TLS) oder 465 (SSL)"
|
||||||
|
echo "SMTP-Benutzername: $SMTP_USERNAME"
|
||||||
|
echo "SMTP-Passwort: $SMTP_PASSWORD"
|
||||||
|
|
||||||
|
# Speichere die Anmeldeinformationen in einer Datei
|
||||||
|
echo -e "\nSpeichere SMTP-Anmeldeinformationen in ses_smtp_credentials.txt"
|
||||||
|
cat > "ses_smtp_credentials.txt" << EOF
|
||||||
|
SMTP-Server: email-smtp.$AWS_REGION.amazonaws.com
|
||||||
|
SMTP-Port: 587 (TLS) oder 465 (SSL)
|
||||||
|
SMTP-Benutzername: $SMTP_USERNAME
|
||||||
|
SMTP-Passwort: $SMTP_PASSWORD
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Hinweise für die weitere Konfiguration
|
||||||
|
echo -e "\nHinweise:"
|
||||||
|
echo "1. Stellen Sie sicher, dass Ihre Domains in Amazon SES verifiziert sind."
|
||||||
|
echo "2. Bei Bedarf beantragen Sie die Aufhebung der SES-Sandbox-Einschränkungen."
|
||||||
|
echo "3. Für SMTP-Anwendungen verwenden Sie die SMTP-Anmeldeinformationen (nicht die IAM-Zugangsdaten)."
|
||||||
|
|
||||||
|
# Format für .env-Datei
|
||||||
|
echo -e "\nFür .env-Datei:"
|
||||||
|
echo "AWS_SES_SMTP_USERNAME=$SMTP_USERNAME"
|
||||||
|
echo "AWS_SES_SMTP_PASSWORD=$SMTP_PASSWORD"
|
||||||
|
echo "AWS_SES_SMTP_HOST=email-smtp.$AWS_REGION.amazonaws.com"
|
||||||
|
echo "AWS_SES_SMTP_PORT=587"
|
||||||
127
dovecot/awsiam.sh
Executable file
127
dovecot/awsiam.sh
Executable file
@@ -0,0 +1,127 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# awsiam.sh - Erstellt einen IAM-Benutzer für Amazon SES mit SMTP-Zugangsdaten
|
||||||
|
|
||||||
|
# Überprüfen, ob die Domain-Variable gesetzt ist
|
||||||
|
if [ -z "$DOMAIN_NAME" ]; then
|
||||||
|
echo "Fehler: DOMAIN_NAME ist nicht gesetzt."
|
||||||
|
echo "Bitte setzen Sie die Variable mit: export DOMAIN_NAME='IhreDomain.de'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Konfiguration
|
||||||
|
AWS_REGION=${AWS_REGION:-"us-east-2"}
|
||||||
|
USER_NAME="${DOMAIN_NAME//./-}-ses-user" # Ersetzt Punkte durch Bindestriche für validen IAM-Username
|
||||||
|
NODE_SCRIPT_PATH="./generate_ses_smtp_password.js"
|
||||||
|
OUTPUT_FILE="${DOMAIN_NAME//./_}_ses_credentials.txt" # Sichere Dateibenennung
|
||||||
|
|
||||||
|
# Prüfen, ob das Node.js-Script existiert
|
||||||
|
if [ ! -f "$NODE_SCRIPT_PATH" ]; then
|
||||||
|
echo "Fehler: Das Node.js-Script '$NODE_SCRIPT_PATH' wurde nicht gefunden."
|
||||||
|
echo "Bitte stelle sicher, dass das Script im angegebenen Pfad existiert."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== IAM-Benutzer für SES SMTP-Zugang erstellen ==="
|
||||||
|
echo "Domain: $DOMAIN_NAME"
|
||||||
|
echo "Region: $AWS_REGION"
|
||||||
|
echo "IAM-Benutzername: $USER_NAME"
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# IAM-User erstellen
|
||||||
|
# --------------------------
|
||||||
|
echo "Erstelle IAM-User: $USER_NAME"
|
||||||
|
aws iam create-user --user-name $USER_NAME
|
||||||
|
|
||||||
|
# Benutzerdefinierte Policy für SES-Sendeberechtigungen erstellen
|
||||||
|
POLICY_NAME="${USER_NAME}-SendRawEmailPolicy"
|
||||||
|
POLICY_DOCUMENT='{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": "ses:SendRawEmail",
|
||||||
|
"Resource": "*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
|
||||||
|
echo "Erstelle benutzerdefinierte Policy für SES SendRawEmail"
|
||||||
|
POLICY_ARN=$(aws iam create-policy \
|
||||||
|
--policy-name $POLICY_NAME \
|
||||||
|
--policy-document "$POLICY_DOCUMENT" \
|
||||||
|
--query 'Policy.Arn' \
|
||||||
|
--output text)
|
||||||
|
|
||||||
|
echo "Hänge Policy an: $POLICY_ARN"
|
||||||
|
aws iam attach-user-policy \
|
||||||
|
--user-name $USER_NAME \
|
||||||
|
--policy-arn $POLICY_ARN
|
||||||
|
|
||||||
|
# Access Key und Secret Key für den User erstellen
|
||||||
|
echo "Erstelle Access Key für den User: $USER_NAME"
|
||||||
|
KEY_OUTPUT=$(aws iam create-access-key --user-name $USER_NAME)
|
||||||
|
|
||||||
|
# Keys ausgeben und in Variablen speichern
|
||||||
|
echo "Zugriffsschlüssel wurden erstellt. Bitte sicher aufbewahren:"
|
||||||
|
echo "$KEY_OUTPUT" | jq .
|
||||||
|
|
||||||
|
ACCESS_KEY=$(echo "$KEY_OUTPUT" | jq -r .AccessKey.AccessKeyId)
|
||||||
|
SECRET_KEY=$(echo "$KEY_OUTPUT" | jq -r .AccessKey.SecretAccessKey)
|
||||||
|
|
||||||
|
echo "ACCESS_KEY: $ACCESS_KEY"
|
||||||
|
echo "SECRET_KEY: $SECRET_KEY"
|
||||||
|
|
||||||
|
echo "WICHTIG: Speichere den Secret Key jetzt, da er später nicht mehr abgerufen werden kann!"
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# SMTP Passwort generieren
|
||||||
|
# --------------------------
|
||||||
|
echo -e "\nGeneriere SMTP-Passwort für Region $AWS_REGION..."
|
||||||
|
|
||||||
|
# Führe das Node.js-Script aus, um das SMTP-Passwort zu generieren
|
||||||
|
SMTP_PASSWORD=$(node "$NODE_SCRIPT_PATH" "$SECRET_KEY" "$AWS_REGION")
|
||||||
|
|
||||||
|
# Prüfen, ob die Ausführung erfolgreich war
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Fehler bei der Generierung des SMTP-Passworts. Bitte überprüfe das Node.js-Script."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# SMTP-Benutzername ist der Access Key
|
||||||
|
SMTP_USERNAME="$ACCESS_KEY"
|
||||||
|
|
||||||
|
# Ausgabe der SMTP-Anmeldeinformationen
|
||||||
|
echo -e "\nSMTP-Anmeldeinformationen für Amazon SES in Region $AWS_REGION:"
|
||||||
|
echo "--------------------------------------------------------------"
|
||||||
|
echo "SMTP-Server: email-smtp.$AWS_REGION.amazonaws.com"
|
||||||
|
echo "SMTP-Port: 587 (TLS) oder 465 (SSL)"
|
||||||
|
echo "SMTP-Benutzername: $SMTP_USERNAME"
|
||||||
|
echo "SMTP-Passwort: $SMTP_PASSWORD"
|
||||||
|
|
||||||
|
# Speichere die Anmeldeinformationen in einer Datei
|
||||||
|
echo -e "\nSpeichere SMTP-Anmeldeinformationen in $OUTPUT_FILE"
|
||||||
|
cat > "$OUTPUT_FILE" << EOF
|
||||||
|
DOMAIN_NAME: $DOMAIN_NAME
|
||||||
|
SMTP-Server: email-smtp.$AWS_REGION.amazonaws.com
|
||||||
|
SMTP-Port: 587 (TLS) oder 465 (SSL)
|
||||||
|
SMTP-Benutzername: $SMTP_USERNAME
|
||||||
|
SMTP-Passwort: $SMTP_PASSWORD
|
||||||
|
|
||||||
|
IAM-Benutzer: $USER_NAME
|
||||||
|
Access Key ID: $ACCESS_KEY
|
||||||
|
Secret Access Key: $SECRET_KEY
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 600 "$OUTPUT_FILE" # Nur für den Besitzer lesbar machen
|
||||||
|
|
||||||
|
# Format für .env-Datei
|
||||||
|
echo -e "\nFür .env-Datei:"
|
||||||
|
echo "AWS_SES_SMTP_USERNAME=$SMTP_USERNAME"
|
||||||
|
echo "AWS_SES_SMTP_PASSWORD=$SMTP_PASSWORD"
|
||||||
|
echo "AWS_SES_SMTP_HOST=email-smtp.$AWS_REGION.amazonaws.com"
|
||||||
|
echo "AWS_SES_SMTP_PORT=587"
|
||||||
|
|
||||||
|
echo -e "\nHinweise:"
|
||||||
|
echo "1. Die SMTP-Anmeldeinformationen wurden in $OUTPUT_FILE gespeichert."
|
||||||
|
echo "2. Verwenden Sie diese SMTP-Anmeldeinformationen in Ihrer E-Mail-Anwendung oder Ihrem E-Mail-Server."
|
||||||
|
echo "3. Der IAM-Benutzer hat nur die Berechtigung, E-Mails über SES zu senden."
|
||||||
85
dovecot/awss3.sh
Executable file
85
dovecot/awss3.sh
Executable file
@@ -0,0 +1,85 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# awss3.sh - Erstellt einen S3-Bucket für Amazon SES E-Mail-Speicherung
|
||||||
|
|
||||||
|
# Überprüfen, ob die Domain-Variable gesetzt ist
|
||||||
|
if [ -z "$DOMAIN_NAME" ]; then
|
||||||
|
echo "Fehler: DOMAIN_NAME ist nicht gesetzt."
|
||||||
|
echo "Bitte setzen Sie die Variable mit: export DOMAIN_NAME='IhreDomain.de'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Konfiguration
|
||||||
|
AWS_REGION=${AWS_REGION:-"us-east-2"}
|
||||||
|
EMAIL_PREFIX=${EMAIL_PREFIX:-"emails/"}
|
||||||
|
S3_BUCKET_NAME=$(echo "$DOMAIN_NAME" | tr '.' '-' | awk '{print $0 "-emails"}')
|
||||||
|
|
||||||
|
echo "=== S3 Bucket Configuration für $DOMAIN_NAME ==="
|
||||||
|
echo "Region: $AWS_REGION"
|
||||||
|
echo "Bucket-Name: $S3_BUCKET_NAME"
|
||||||
|
echo "E-Mail-Präfix: $EMAIL_PREFIX"
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# S3 Bucket erstellen
|
||||||
|
# ------------------------
|
||||||
|
echo "S3 Bucket erstellen..."
|
||||||
|
aws s3api create-bucket \
|
||||||
|
--bucket ${S3_BUCKET_NAME} \
|
||||||
|
--region ${AWS_REGION} \
|
||||||
|
--create-bucket-configuration LocationConstraint=${AWS_REGION}
|
||||||
|
|
||||||
|
# Öffentlichen Zugriff blockieren
|
||||||
|
echo "Öffentlichen Zugriff blockieren..."
|
||||||
|
aws s3api put-public-access-block \
|
||||||
|
--bucket ${S3_BUCKET_NAME} \
|
||||||
|
--public-access-block-configuration "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"
|
||||||
|
|
||||||
|
# Lebenszyklus-Konfiguration hinzufügen
|
||||||
|
echo "Lebenszyklus-Konfiguration hinzufügen (E-Mails werden nach 90 Tagen gelöscht)..."
|
||||||
|
aws s3api put-bucket-lifecycle-configuration \
|
||||||
|
--bucket ${S3_BUCKET_NAME} \
|
||||||
|
--lifecycle-configuration '{
|
||||||
|
"Rules": [
|
||||||
|
{
|
||||||
|
"ID": "DeleteOldEmails",
|
||||||
|
"Status": "Enabled",
|
||||||
|
"Expiration": {
|
||||||
|
"Days": 90
|
||||||
|
},
|
||||||
|
"Filter": {
|
||||||
|
"Prefix": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
|
||||||
|
echo "S3 Bucket Policy hinzufügen für SES-Zugriff..."
|
||||||
|
aws s3api put-bucket-policy \
|
||||||
|
--bucket ${S3_BUCKET_NAME} \
|
||||||
|
--policy '{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Principal": {
|
||||||
|
"Service": "ses.amazonaws.com"
|
||||||
|
},
|
||||||
|
"Action": [
|
||||||
|
"s3:PutObject",
|
||||||
|
"s3:GetBucketLocation",
|
||||||
|
"s3:ListBucket"
|
||||||
|
],
|
||||||
|
"Resource": [
|
||||||
|
"arn:aws:s3:::'${S3_BUCKET_NAME}'",
|
||||||
|
"arn:aws:s3:::'${S3_BUCKET_NAME}'/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
|
||||||
|
echo "S3 Bucket $S3_BUCKET_NAME wurde erfolgreich erstellt und konfiguriert."
|
||||||
|
echo "Bucket-ARN: arn:aws:s3:::$S3_BUCKET_NAME"
|
||||||
|
|
||||||
|
# Exportiere Variablen für andere Scripte
|
||||||
|
echo
|
||||||
|
echo "Für die Verwendung in den anderen Scripten setzen Sie:"
|
||||||
|
echo "export S3_BUCKET_NAME=$S3_BUCKET_NAME"
|
||||||
155
dovecot/awsses.sh
Executable file
155
dovecot/awsses.sh
Executable file
@@ -0,0 +1,155 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# awsses.sh - Konfiguriert Amazon SES für eine Domain und erstellt eine Receipt Rule
|
||||||
|
|
||||||
|
# Überprüfen, ob die Domain-Variable gesetzt ist
|
||||||
|
if [ -z "$DOMAIN_NAME" ]; then
|
||||||
|
echo "Fehler: DOMAIN_NAME ist nicht gesetzt."
|
||||||
|
echo "Bitte setzen Sie die Variable mit: export DOMAIN_NAME='IhreDomain.de'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Überprüfen, ob S3_BUCKET_NAME gesetzt ist
|
||||||
|
if [ -z "$S3_BUCKET_NAME" ]; then
|
||||||
|
echo "Warnung: S3_BUCKET_NAME ist nicht gesetzt."
|
||||||
|
echo "Wird automatisch aus DOMAIN_NAME generiert, verwenden Sie idealerweise zuerst awss3.sh."
|
||||||
|
S3_BUCKET_NAME=$(echo "$DOMAIN_NAME" | tr '.' '-' | awk '{print $0 "-emails"}')
|
||||||
|
echo "Generierter Bucket-Name: $S3_BUCKET_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Konfiguration
|
||||||
|
AWS_REGION=${AWS_REGION:-"us-east-2"}
|
||||||
|
EMAIL_PREFIX=${EMAIL_PREFIX:-""}
|
||||||
|
RULE_NAME="store-$(echo "$DOMAIN_NAME" | tr '.' '-')-to-s3"
|
||||||
|
|
||||||
|
echo "=== SES Konfiguration für $DOMAIN_NAME ==="
|
||||||
|
echo "Region: $AWS_REGION"
|
||||||
|
echo "S3 Bucket: $S3_BUCKET_NAME"
|
||||||
|
echo "Receipt Rule Name: $RULE_NAME"
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# SES Domain-Identität erstellen
|
||||||
|
# ------------------------
|
||||||
|
echo "SES Domain-Identität erstellen..."
|
||||||
|
IDENTITY_RESULT=$(aws sesv2 create-email-identity \
|
||||||
|
--email-identity ${DOMAIN_NAME} \
|
||||||
|
--region ${AWS_REGION})
|
||||||
|
|
||||||
|
echo "Identity erstellt. Überprüfen Sie die DNS-Einträge für die Domain-Verifizierung."
|
||||||
|
echo "$IDENTITY_RESULT" | jq .
|
||||||
|
|
||||||
|
# DKIM-Signierung aktivieren
|
||||||
|
echo "DKIM-Signierung aktivieren..."
|
||||||
|
aws sesv2 put-email-identity-dkim-attributes \
|
||||||
|
--email-identity ${DOMAIN_NAME} \
|
||||||
|
--signing-enabled \
|
||||||
|
--region ${AWS_REGION}
|
||||||
|
|
||||||
|
# Mail-From-Domain konfigurieren
|
||||||
|
echo "Mail-From-Domain konfigurieren..."
|
||||||
|
aws sesv2 put-email-identity-mail-from-attributes \
|
||||||
|
--email-identity ${DOMAIN_NAME} \
|
||||||
|
--mail-from-domain "mail.${DOMAIN_NAME}" \
|
||||||
|
--behavior-on-mx-failure USE_DEFAULT_VALUE \
|
||||||
|
--region ${AWS_REGION}
|
||||||
|
|
||||||
|
# Überprüfen, ob der Rule Set existiert, sonst erstellen
|
||||||
|
echo "Überprüfe oder erstelle Receipt Rule Set..."
|
||||||
|
RULESET_EXISTS=$(aws ses describe-receipt-rule-sets --region ${AWS_REGION} | jq -r '.RuleSets[] | select(.Name == "bizmatch-ruleset") | .Name')
|
||||||
|
|
||||||
|
if [ -z "$RULESET_EXISTS" ]; then
|
||||||
|
echo "Receipt Rule Set 'bizmatch-ruleset' existiert nicht, wird erstellt..."
|
||||||
|
aws ses create-receipt-rule-set --rule-set-name "bizmatch-ruleset" --region ${AWS_REGION}
|
||||||
|
else
|
||||||
|
echo "Receipt Rule Set 'bizmatch-ruleset' existiert bereits."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Receipt Rule erstellen
|
||||||
|
echo "Receipt Rule für E-Mail-Empfang erstellen..."
|
||||||
|
aws ses create-receipt-rule --rule-set-name "bizmatch-ruleset" --rule '{
|
||||||
|
"Name": "'"${RULE_NAME}"'",
|
||||||
|
"Enabled": true,
|
||||||
|
"ScanEnabled": true,
|
||||||
|
"Actions": [{
|
||||||
|
"S3Action": {
|
||||||
|
"BucketName": "'"${S3_BUCKET_NAME}"'",
|
||||||
|
"ObjectKeyPrefix": "'"${EMAIL_PREFIX}"'"
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"TlsPolicy": "Require",
|
||||||
|
"Recipients": ["'"${DOMAIN_NAME}"'"]
|
||||||
|
}' --region ${AWS_REGION}
|
||||||
|
|
||||||
|
# Prüfen, ob der Rule Set aktiv ist
|
||||||
|
ACTIVE_RULESET=$(aws ses describe-active-receipt-rule-set --region ${AWS_REGION} | jq -r '.Metadata.Name')
|
||||||
|
|
||||||
|
if [ "$ACTIVE_RULESET" != "bizmatch-ruleset" ]; then
|
||||||
|
echo "Aktiviere Rule Set 'bizmatch-ruleset'..."
|
||||||
|
aws ses set-active-receipt-rule-set --rule-set-name "bizmatch-ruleset" --region ${AWS_REGION}
|
||||||
|
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:"
|
||||||
|
echo "1. DKIM-Einträge (3 CNAME-Einträge)"
|
||||||
|
echo "2. MAIL FROM MX und TXT-Einträge"
|
||||||
|
echo "3. SPF-Eintrag (TXT): v=spf1 include:amazonses.com ~all"
|
||||||
|
echo
|
||||||
|
echo "Nach dem Setzen der DNS-Einträge kann es bis zu 72 Stunden dauern, bis die Verifizierung abgeschlossen ist."
|
||||||
160
dovecot/cloudflareDns.sh
Executable file
160
dovecot/cloudflareDns.sh
Executable file
@@ -0,0 +1,160 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Cloudflare API-Konfiguration
|
||||||
|
# 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
|
||||||
|
AWS_REGION="us-east-2" # AWS-Region
|
||||||
|
if [ -z "$DOMAIN_NAME" ]; then
|
||||||
|
echo "Fehler: DOMAIN_NAME ist nicht gesetzt."
|
||||||
|
echo "Bitte setzen Sie die Variable mit: export DOMAIN_NAME='IhreDomain.de'"
|
||||||
|
exit 1 # Skript mit Fehlercode beenden
|
||||||
|
fi
|
||||||
|
# Überprüfen, ob der erforderliche API-Token gesetzt ist
|
||||||
|
if [ -z "$CF_API_TOKEN" ]; then
|
||||||
|
echo "Fehler: Bitte setze CF_API_TOKEN als Umgebungsvariable oder im Skript."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Zone ID basierend auf Domain-Namen abrufen
|
||||||
|
echo "Zone ID für $DOMAIN_NAME abrufen..."
|
||||||
|
ZONE_RESPONSE=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$DOMAIN_NAME" \
|
||||||
|
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||||
|
-H "Content-Type: application/json")
|
||||||
|
|
||||||
|
# Überprüfen, ob die Antwort erfolgreich war
|
||||||
|
if [ "$(echo $ZONE_RESPONSE | jq -r '.success')" != "true" ]; then
|
||||||
|
echo "Fehler beim Abrufen der Zone ID:"
|
||||||
|
echo $ZONE_RESPONSE | jq .
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Zone ID extrahieren
|
||||||
|
CF_ZONE_ID=$(echo $ZONE_RESPONSE | jq -r '.result[0].id')
|
||||||
|
|
||||||
|
# Überprüfen, ob eine Zone ID gefunden wurde
|
||||||
|
if [ -z "$CF_ZONE_ID" ] || [ "$CF_ZONE_ID" = "null" ]; then
|
||||||
|
echo "Keine Zone ID für $DOMAIN_NAME gefunden. Bitte stelle sicher, dass die Domain bei Cloudflare registriert ist."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Zone ID für $DOMAIN_NAME: $CF_ZONE_ID"
|
||||||
|
|
||||||
|
# Hilfsfunktion für DNS-Einträge anlegen
|
||||||
|
create_dns_record() {
|
||||||
|
local TYPE=$1
|
||||||
|
local NAME=$2
|
||||||
|
local CONTENT=$3
|
||||||
|
local PROXIED=$4
|
||||||
|
local TTL=$5
|
||||||
|
local PRIORITY=$6 # Neu: MX-Priority
|
||||||
|
|
||||||
|
# Standardwerte für Proxied und TTL setzen, falls nicht angegeben
|
||||||
|
if [ -z "$PROXIED" ]; then
|
||||||
|
PROXIED="false"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$TTL" ]; then
|
||||||
|
TTL=3600 # 1 Stunde
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Erstelle $TYPE-Eintrag für $NAME mit Inhalt $CONTENT..."
|
||||||
|
|
||||||
|
# Json Payload vorbereiten abhängig vom Record-Typ
|
||||||
|
local JSON_DATA=""
|
||||||
|
|
||||||
|
if [ "$TYPE" = "MX" ]; then
|
||||||
|
# Bei MX-Einträgen müssen wir die Priority separat angeben
|
||||||
|
if [ -z "$PRIORITY" ]; then
|
||||||
|
PRIORITY=10 # Standard-Priority, falls nicht angegeben
|
||||||
|
fi
|
||||||
|
|
||||||
|
JSON_DATA="{
|
||||||
|
\"type\": \"$TYPE\",
|
||||||
|
\"name\": \"$NAME\",
|
||||||
|
\"content\": \"$CONTENT\",
|
||||||
|
\"ttl\": $TTL,
|
||||||
|
\"priority\": $PRIORITY,
|
||||||
|
\"proxied\": $PROXIED
|
||||||
|
}"
|
||||||
|
elif [ "$TYPE" = "TXT" ]; then
|
||||||
|
# Bei TXT-Einträgen müssen wir sicherstellen, dass der Inhalt in Anführungszeichen steht
|
||||||
|
# Aber Anführungszeichen innerhalb von JSON müssen escaped werden
|
||||||
|
# Wir entfernen zuerst alle vorhandenen Anführungszeichen und fügen sie dann korrekt hinzu
|
||||||
|
CONTENT=$(echo "$CONTENT" | sed 's/"//g')
|
||||||
|
|
||||||
|
JSON_DATA="{
|
||||||
|
\"type\": \"$TYPE\",
|
||||||
|
\"name\": \"$NAME\",
|
||||||
|
\"content\": \"\\\"$CONTENT\\\"\",
|
||||||
|
\"ttl\": $TTL,
|
||||||
|
\"proxied\": $PROXIED
|
||||||
|
}"
|
||||||
|
else
|
||||||
|
# Für alle anderen Record-Typen (z.B. CNAME)
|
||||||
|
JSON_DATA="{
|
||||||
|
\"type\": \"$TYPE\",
|
||||||
|
\"name\": \"$NAME\",
|
||||||
|
\"content\": \"$CONTENT\",
|
||||||
|
\"ttl\": $TTL,
|
||||||
|
\"proxied\": $PROXIED
|
||||||
|
}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# API-Aufruf an Cloudflare
|
||||||
|
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records" \
|
||||||
|
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
--data "$JSON_DATA" | jq .
|
||||||
|
}
|
||||||
|
|
||||||
|
# DKIM-Einträge abrufen und bei Cloudflare eintragen
|
||||||
|
echo "DKIM-Tokens abrufen von AWS SES..."
|
||||||
|
DKIM_TOKENS=$(aws ses get-identity-dkim-attributes \
|
||||||
|
--identities ${DOMAIN_NAME} \
|
||||||
|
--region ${AWS_REGION} \
|
||||||
|
--query "DkimAttributes.\"${DOMAIN_NAME}\".DkimTokens" \
|
||||||
|
--output text)
|
||||||
|
|
||||||
|
# Überprüfen, ob DKIM-Tokens abgerufen wurden
|
||||||
|
if [ -z "$DKIM_TOKENS" ]; then
|
||||||
|
echo "Fehler: Konnte DKIM-Tokens nicht abrufen. Ist die Domain bei AWS SES verifiziert?"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Domain-Verifizierungstoken abrufen
|
||||||
|
VERIFICATION_TOKEN=$(aws ses get-identity-verification-attributes \
|
||||||
|
--identities ${DOMAIN_NAME} \
|
||||||
|
--region ${AWS_REGION} \
|
||||||
|
--query "VerificationAttributes.\"${DOMAIN_NAME}\".VerificationToken" \
|
||||||
|
--output text)
|
||||||
|
|
||||||
|
# DKIM-Einträge anlegen
|
||||||
|
echo "DKIM-Einträge anlegen bei Cloudflare..."
|
||||||
|
for TOKEN in ${DKIM_TOKENS}; do
|
||||||
|
create_dns_record "CNAME" "${TOKEN}._domainkey.${DOMAIN_NAME}" "${TOKEN}.dkim.amazonses.com" "false" 3600
|
||||||
|
done
|
||||||
|
|
||||||
|
# Domain-Verifizierungs-TXT-Eintrag anlegen
|
||||||
|
echo "Domain-Verifizierungs-TXT-Eintrag anlegen bei Cloudflare..."
|
||||||
|
create_dns_record "TXT" "_amazonses.${DOMAIN_NAME}" "${VERIFICATION_TOKEN}" "false" 3600
|
||||||
|
|
||||||
|
# MX-Einträge anlegen
|
||||||
|
echo "MX-Einträge anlegen bei Cloudflare..."
|
||||||
|
create_dns_record "MX" "${DOMAIN_NAME}" "inbound-smtp.${AWS_REGION}.amazonaws.com" "false" 3600 10
|
||||||
|
create_dns_record "MX" "mail.${DOMAIN_NAME}" "feedback-smtp.${AWS_REGION}.amazonses.com" "false" 3600 10
|
||||||
|
|
||||||
|
# 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" 3600
|
||||||
|
|
||||||
|
# SPF-Eintrag anlegen
|
||||||
|
echo "SPF-Eintrag anlegen bei Cloudflare..."
|
||||||
|
create_dns_record "TXT" "mail.${DOMAIN_NAME}" "v=spf1 include:amazonses.com ~all" "false" 3600
|
||||||
|
|
||||||
|
# DMARC-Eintrag anlegen
|
||||||
|
echo "DMARC-Eintrag anlegen bei Cloudflare..."
|
||||||
|
create_dns_record "TXT" "_dmarc.${DOMAIN_NAME}" "v=DMARC1; p=quarantine; pct=100; rua=mailto:postmaster@${DOMAIN_NAME}" "false" 3600
|
||||||
|
|
||||||
|
echo "DNS-Einrichtung abgeschlossen."
|
||||||
|
echo "Es kann bis zu 72 Stunden dauern, bis AWS SES die Domain verifiziert hat."
|
||||||
52
dovecot/config/dovecot.conf
Normal file
52
dovecot/config/dovecot.conf
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Dovecot Konfiguration mit Plain-Text Passwörtern
|
||||||
|
# Für Version 2.3.21.1
|
||||||
|
|
||||||
|
# Protokolle aktivieren
|
||||||
|
protocols = imap pop3
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
log_path = /var/log/dovecot.log
|
||||||
|
info_log_path = /var/log/dovecot-info.log
|
||||||
|
debug_log_path = /var/log/dovecot-debug.log
|
||||||
|
|
||||||
|
# Mail-Location
|
||||||
|
# Für Benutzer mit @ in der E-Mail-Adresse
|
||||||
|
mail_location = maildir:/var/mail/%d/%n
|
||||||
|
|
||||||
|
# Authentifizierung
|
||||||
|
auth_mechanisms = plain login
|
||||||
|
disable_plaintext_auth = no
|
||||||
|
|
||||||
|
# Benutzerdatenbank (passwd-datei)
|
||||||
|
passdb {
|
||||||
|
driver = passwd-file
|
||||||
|
args = scheme=PLAIN username_format=%u /etc/dovecot/passwd
|
||||||
|
}
|
||||||
|
|
||||||
|
userdb {
|
||||||
|
driver = passwd-file
|
||||||
|
args = username_format=%u /etc/dovecot/passwd
|
||||||
|
}
|
||||||
|
|
||||||
|
# Passwort-Schema (plaintext) muss in der passdb definiert werden
|
||||||
|
# Die globale Einstellung default_pass_scheme ist nicht verfügbar
|
||||||
|
|
||||||
|
# Mail-Berechtigungen
|
||||||
|
mail_uid = vmail
|
||||||
|
mail_gid = vmail
|
||||||
|
|
||||||
|
# SSL-Konfiguration
|
||||||
|
ssl = yes
|
||||||
|
ssl_cert = </etc/dovecot/ssl/imap.bizmatch.net/fullchain1.pem
|
||||||
|
ssl_key = </etc/dovecot/ssl/imap.bizmatch.net/privkey1.pem
|
||||||
|
|
||||||
|
# In dovecot.conf hinzufügen
|
||||||
|
local_name imap.haiky.app {
|
||||||
|
ssl_cert = </etc/dovecot/ssl/imap.haiky.app/fullchain1.pem
|
||||||
|
ssl_key = </etc/dovecot/ssl/imap.haiky.app/privkey1.pem
|
||||||
|
}
|
||||||
|
|
||||||
|
local_name imap.andreasknuth.de {
|
||||||
|
ssl_cert = </etc/dovecot/ssl/imap.andreasknuth.de/fullchain1.pem
|
||||||
|
ssl_key = </etc/dovecot/ssl/imap.andreasknuth.de/privkey1.pem
|
||||||
|
}
|
||||||
41
dovecot/config/dovecot/dovecot.conf
Normal file
41
dovecot/config/dovecot/dovecot.conf
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# /etc/dovecot/dovecot.conf (im Container)
|
||||||
|
|
||||||
|
# Grundlegende Einstellungen
|
||||||
|
protocols = imap
|
||||||
|
listen = *, ::
|
||||||
|
|
||||||
|
# Benutzerauthentifizierung
|
||||||
|
auth_mechanisms = plain login
|
||||||
|
|
||||||
|
# Plaintext-Authentifizierung erlauben
|
||||||
|
disable_plaintext_auth = no
|
||||||
|
|
||||||
|
passdb {
|
||||||
|
driver = passwd-file
|
||||||
|
args = /etc/dovecot/users
|
||||||
|
}
|
||||||
|
userdb {
|
||||||
|
driver = passwd-file
|
||||||
|
args = /etc/dovecot/users
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mail-Speicherort
|
||||||
|
mail_location = maildir:/var/mail/%u
|
||||||
|
|
||||||
|
# Da wir hinter Caddy sind, können wir TLS auf Dovecot deaktivieren
|
||||||
|
# oder nur intern auf nicht-exponierten Ports aktivieren
|
||||||
|
ssl = no
|
||||||
|
|
||||||
|
# Wenn Sie dennoch direkten Zugriff auf Dovecot ermöglichen möchten:
|
||||||
|
# ssl = yes
|
||||||
|
# ssl_cert = </etc/dovecot/ssl/fullchain.pem
|
||||||
|
# ssl_key = </etc/dovecot/ssl/privkey.pem
|
||||||
|
|
||||||
|
# IMAP-Konfiguration
|
||||||
|
# protocol imap {
|
||||||
|
# mail_plugins = $mail_plugins
|
||||||
|
# }
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
log_path = /var/log/dovecot.log
|
||||||
|
info_log_path = /var/log/dovecot-info.log
|
||||||
2
dovecot/config/dovecot/users
Normal file
2
dovecot/config/dovecot/users
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
user1:password123
|
||||||
|
user2:secret456
|
||||||
72
dovecot/config/dovecot24.conf
Normal file
72
dovecot/config/dovecot24.conf
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
dovecot_config_version = 2.4.0
|
||||||
|
dovecot_storage_version = 2.4.0
|
||||||
|
|
||||||
|
# Dovecot 2.4.x Konfiguration
|
||||||
|
# Protokolle (korrigiert zurück zu 'imap')
|
||||||
|
protocols = imap pop3
|
||||||
|
|
||||||
|
import_environment {
|
||||||
|
USER_PASSWORD=%{env:USER_PASSWORD|default('password')}
|
||||||
|
DOVEADM_PASSWORD=%{env:DOVEADM_PASSWORD|default('supersecret')}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logging (Block-Syntax)
|
||||||
|
log_path = /var/log/dovecot.log
|
||||||
|
info_log_path = /var/log/dovecot-info.log
|
||||||
|
debug_log_path = /var/log/dovecot-debug.log
|
||||||
|
|
||||||
|
# Mail-Location
|
||||||
|
mail_driver=maildir
|
||||||
|
mailbox_list_layout=index
|
||||||
|
mailbox_list_utf8=yes
|
||||||
|
mail_path=~/mail
|
||||||
|
mail_home=/var/vmail/%{user | domain }/%{user | username }
|
||||||
|
mail_utf8_extensions = yes
|
||||||
|
|
||||||
|
default_internal_user = vmail
|
||||||
|
default_login_user = vmail
|
||||||
|
default_internal_group = vmail
|
||||||
|
|
||||||
|
mail_uid = vmail
|
||||||
|
mail_gid = vmail
|
||||||
|
|
||||||
|
# Authentifizierung
|
||||||
|
# auth_mechanisms = plain login
|
||||||
|
# auth_allow_cleartext = yes
|
||||||
|
|
||||||
|
# Passwd-Datenbank (mit Namen und korrekter Syntax)
|
||||||
|
# passdb passwd-file {
|
||||||
|
# passdb_driver = passwd-file
|
||||||
|
# passdb_args = username_format=%u password_hash=plaintext /etc/dovecot/passwd
|
||||||
|
# }
|
||||||
|
passdb static {
|
||||||
|
password=%{env:USER_PASSWORD}
|
||||||
|
}
|
||||||
|
|
||||||
|
# userdb passwd-file {
|
||||||
|
# userdb_driver = passwd-file
|
||||||
|
# userdb_args = username_format=%u uid=vmail gid=vmail /etc/dovecot/passwd
|
||||||
|
# }
|
||||||
|
ssl = yes
|
||||||
|
ssl_server_cert_file = /etc/dovecot/ssl/imap.bizmatch.net/fullchain1.pem
|
||||||
|
ssl_server_key_file = /etc/dovecot/ssl/imap.bizmatch.net/privkey1.pem
|
||||||
|
# Mail-Berechtigungen (nicht mehr in Service-Blöcken nötig)
|
||||||
|
# uid/gid jetzt direkt in userdb definiert
|
||||||
|
|
||||||
|
# SSL-Einstellungen
|
||||||
|
# ssl = yes
|
||||||
|
# ssl_cert = </etc/dovecot/ssl/imap.bizmatch.net/fullchain1.pem
|
||||||
|
# ssl_key = </etc/dovecot/ssl/imap.bizmatch.net/privkey1.pem
|
||||||
|
|
||||||
|
# SNI-Konfiguration (korrigierte Syntax)
|
||||||
|
# service imap-login {
|
||||||
|
# ssl_server_name = imap.haiky.app {
|
||||||
|
# ssl_cert = </etc/dovecot/ssl/imap.haiky.app/fullchain1.pem
|
||||||
|
# ssl_key = </etc/dovecot/ssl/imap.haiky.app/privkey1.pem
|
||||||
|
# }
|
||||||
|
|
||||||
|
# ssl_server_name = imap.andreasknuth.de {
|
||||||
|
# ssl_cert = </etc/dovecot/ssl/imap.andreasknuth.de/fullchain1.pem
|
||||||
|
# ssl_key = </etc/dovecot/ssl/imap.andreasknuth.de/privkey1.pem
|
||||||
|
# }
|
||||||
|
# }
|
||||||
233
dovecot/config/dovecot241.conf
Normal file
233
dovecot/config/dovecot241.conf
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
dovecot_config_version = 2.4.0
|
||||||
|
dovecot_storage_version = 2.4.0
|
||||||
|
|
||||||
|
base_dir = /run/dovecot
|
||||||
|
state_dir = /run/dovecot
|
||||||
|
|
||||||
|
protocols = imap submission lmtp sieve
|
||||||
|
|
||||||
|
import_environment {
|
||||||
|
USER_PASSWORD=%{env:USER_PASSWORD|default('password')}
|
||||||
|
DOVEADM_PASSWORD=%{env:DOVEADM_PASSWORD|default('supersecret')}
|
||||||
|
}
|
||||||
|
|
||||||
|
mail_driver=maildir
|
||||||
|
mailbox_list_layout=fs
|
||||||
|
mailbox_list_utf8=yes
|
||||||
|
mail_path=./
|
||||||
|
mail_home=/var/mail/%{user | domain }/%{user | username }
|
||||||
|
mail_utf8_extensions = yes
|
||||||
|
|
||||||
|
default_internal_user = vmail
|
||||||
|
default_login_user = vmail
|
||||||
|
default_internal_group = vmail
|
||||||
|
|
||||||
|
mail_uid = vmail
|
||||||
|
mail_gid = vmail
|
||||||
|
|
||||||
|
|
||||||
|
passdb static {
|
||||||
|
password=%{env:USER_PASSWORD}
|
||||||
|
}
|
||||||
|
|
||||||
|
# namespace inbox {
|
||||||
|
# inbox = yes
|
||||||
|
# separator = /
|
||||||
|
# }
|
||||||
|
|
||||||
|
ssl_server {
|
||||||
|
cert_file = /etc/dovecot/ssl/imap.bizmatch.net/fullchain1.pem
|
||||||
|
key_file = /etc/dovecot/ssl/imap.bizmatch.net/privkey1.pem
|
||||||
|
}
|
||||||
|
|
||||||
|
mail_attribute {
|
||||||
|
dict file {
|
||||||
|
path = %{home}/dovecot-attributes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log_path = /dev/stdout
|
||||||
|
|
||||||
|
imap_hibernate_timeout = 5s
|
||||||
|
|
||||||
|
mail_plugins {
|
||||||
|
fts = yes
|
||||||
|
fts_flatcurve = yes
|
||||||
|
mail_log = yes
|
||||||
|
notify = yes
|
||||||
|
}
|
||||||
|
|
||||||
|
mail_log_events = delete undelete expunge save copy mailbox_create mailbox_delete mailbox_rename flag_change
|
||||||
|
|
||||||
|
fts_autoindex = yes
|
||||||
|
fts_autoindex_max_recent_msgs = 999
|
||||||
|
fts_search_add_missing = yes
|
||||||
|
language_filters = normalizer-icu snowball stopwords
|
||||||
|
|
||||||
|
language_tokenizers = generic email-address
|
||||||
|
language_tokenizer_generic_algorithm = simple
|
||||||
|
|
||||||
|
language en {
|
||||||
|
default = yes
|
||||||
|
filters = lowercase snowball english-possessive stopwords
|
||||||
|
}
|
||||||
|
|
||||||
|
fts flatcurve {
|
||||||
|
substring_search = yes
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol imap {
|
||||||
|
mail_plugins {
|
||||||
|
imap_sieve = yes
|
||||||
|
imap_filter_sieve = yes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol lmtp {
|
||||||
|
mail_plugins {
|
||||||
|
sieve = yes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service imap-login {
|
||||||
|
process_min_avail = 1
|
||||||
|
client_limit = 100
|
||||||
|
inet_listener imap {
|
||||||
|
port = 31143
|
||||||
|
}
|
||||||
|
inet_listener imaps {
|
||||||
|
port = 31993
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service pop3-login {
|
||||||
|
process_min_avail = 1
|
||||||
|
client_limit = 100
|
||||||
|
inet_listener pop3 {
|
||||||
|
port = 31110
|
||||||
|
}
|
||||||
|
inet_listener pop3s {
|
||||||
|
port = 31990
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service submission-login {
|
||||||
|
process_min_avail = 1
|
||||||
|
client_limit = 100
|
||||||
|
inet_listener submission {
|
||||||
|
port = 31587
|
||||||
|
}
|
||||||
|
inet_listener submissions {
|
||||||
|
port = 31465
|
||||||
|
ssl = yes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service managesieve-login {
|
||||||
|
process_min_avail = 1
|
||||||
|
client_limit = 100
|
||||||
|
inet_listener sieve {
|
||||||
|
port = 34190
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service doveadm {
|
||||||
|
inet_listener http {
|
||||||
|
port = 8080
|
||||||
|
ssl = yes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service stats {
|
||||||
|
process_min_avail = 1
|
||||||
|
inet_listener http {
|
||||||
|
port = 9090
|
||||||
|
ssl = yes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service lmtp {
|
||||||
|
inet_listener lmtps {
|
||||||
|
port = 31024
|
||||||
|
ssl = yes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doveadm_password = ${env:DOVEADM_PASSWORD}
|
||||||
|
|
||||||
|
event_exporter log {
|
||||||
|
format = json
|
||||||
|
time_format = rfc3339
|
||||||
|
}
|
||||||
|
|
||||||
|
metric auth_success {
|
||||||
|
filter = (event=auth_request_finished AND success=yes)
|
||||||
|
}
|
||||||
|
|
||||||
|
metric auth_failure {
|
||||||
|
filter = (event=auth_request_finished AND NOT success=yes)
|
||||||
|
exporter = log
|
||||||
|
}
|
||||||
|
|
||||||
|
metric imap_command {
|
||||||
|
filter = event=imap_command_finished
|
||||||
|
group_by cmd_name {
|
||||||
|
method discrete {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
group_by tagged_reply_state {
|
||||||
|
method discrete {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
metric smtp_command {
|
||||||
|
filter = event=smtp_server_command_finished and protocol=submission
|
||||||
|
group_by cmd_name {
|
||||||
|
method discrete {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
group_by status_code {
|
||||||
|
method discrete {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
group_by duration {
|
||||||
|
method exponential {
|
||||||
|
base = 10
|
||||||
|
min_magnitude = 1
|
||||||
|
max_magnitude = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
metric lmtp_command {
|
||||||
|
filter = event=smtp_server_command_finished and protocol=lmtp
|
||||||
|
group_by cmd_name {
|
||||||
|
method discrete {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
group_by status_code {
|
||||||
|
method discrete {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
group_by duration {
|
||||||
|
method exponential {
|
||||||
|
base = 10
|
||||||
|
min_magnitude = 1
|
||||||
|
max_magnitude = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
metric mail_delivery {
|
||||||
|
filter = event=mail_delivery_finished
|
||||||
|
group_by duration {
|
||||||
|
method exponential {
|
||||||
|
base = 10
|
||||||
|
min_magnitude = 1
|
||||||
|
max_magnitude = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
!include_try conf.d/*.conf
|
||||||
6
dovecot/config/passwd
Normal file
6
dovecot/config/passwd
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Format: Benutzername:Passwort:UID:GID:Benutzerinfo:Home-Verzeichnis:Shell
|
||||||
|
# Für Plaintext-Passwörter
|
||||||
|
user1:{PLAIN}geheim:1000:1000::/var/mail/user1:/bin/false
|
||||||
|
bizmatch.net:{PLAIN}passwort123:1001:1000::/var/mail/bizmatch.net:/bin/false
|
||||||
|
info@bizmatch.net:{PLAIN}passwort123:1001:1000::/var/mail/bizmatch.net/info:/bin/false
|
||||||
|
aknuth@haiky.app:{PLAIN}passwort123:1001:1000::/var/mail/haiky.app/aknuth:/bin/false
|
||||||
25
dovecot/docker-compose.yml
Normal file
25
dovecot/docker-compose.yml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
services:
|
||||||
|
dovecot:
|
||||||
|
image: dovecot/dovecot:2.3.21.1
|
||||||
|
container_name: dovecot
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "143:143" # IMAP
|
||||||
|
- "993:993" # IMAPS
|
||||||
|
- "110:110" # POP3
|
||||||
|
- "995:995" # POP3S
|
||||||
|
volumes:
|
||||||
|
- ./config:/etc/dovecot
|
||||||
|
- ./ssl:/etc/dovecot/ssl
|
||||||
|
- ./mail:/var/mail
|
||||||
|
- ./log:/var/log
|
||||||
|
- ./entrypoint.sh:/entrypoint.sh # Custom Entrypoint-Script
|
||||||
|
environment:
|
||||||
|
- UMASK=002 # Wird von unserem Entrypoint verwendet
|
||||||
|
# entrypoint: ["/bin/sh", "/entrypoint.sh"]
|
||||||
|
networks:
|
||||||
|
- mail_network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
mail_network:
|
||||||
|
driver: bridge
|
||||||
24
dovecot/docker-compose24.yml
Normal file
24
dovecot/docker-compose24.yml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
services:
|
||||||
|
dovecot:
|
||||||
|
image: dovecot/dovecot:2.4-latest # Oder spezifische 2.4.x Version
|
||||||
|
container_name: dovecot24
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "143:143" # IMAP
|
||||||
|
- "993:31993" # IMAPS (SSL/TLS)
|
||||||
|
- "110:110" # POP3
|
||||||
|
- "995:995" # POP3S (SSL/TLS)
|
||||||
|
volumes:
|
||||||
|
- ./config/dovecot241.conf:/etc/dovecot/dovecot.conf # Pfad zur Konfig
|
||||||
|
- ./ssl:/etc/dovecot/ssl
|
||||||
|
- ./mail:/var/mail
|
||||||
|
- ./log:/var/log
|
||||||
|
environment:
|
||||||
|
- USER_PASSWORD=SUPERSECRET
|
||||||
|
command: ["dovecot", "-F"] # Foreground mit eigener Konfig
|
||||||
|
networks:
|
||||||
|
- mail_network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
mail_network:
|
||||||
|
driver: bridge
|
||||||
354
dovecot/dovecot_passwd_manager.py
Normal file
354
dovecot/dovecot_passwd_manager.py
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Dovecot Passwd Manager
|
||||||
|
|
||||||
|
Verwaltet Benutzerkonten in der Dovecot passwd-Datei basierend auf den konfigurierten
|
||||||
|
E-Mail-Domains und Benutzernamen. Das Script liest die gleichen Umgebungsvariablen wie
|
||||||
|
der s3_email_downloader.py und kann separat ausgeführt werden.
|
||||||
|
|
||||||
|
Nutzung:
|
||||||
|
python3 dovecot_passwd_manager.py # Nur Überprüfung, keine Änderungen
|
||||||
|
python3 dovecot_passwd_manager.py update # Aktualisiert die passwd-Datei
|
||||||
|
python3 dovecot_passwd_manager.py force # Erzwingt Aktualisierung auch ohne Änderungen
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import datetime
|
||||||
|
import subprocess
|
||||||
|
import filecmp
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import NamedTemporaryFile
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# .env-Datei laden
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Logging konfigurieren
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler('dovecot_passwd_manager.log'),
|
||||||
|
logging.StreamHandler()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
logger = logging.getLogger("dovecot-passwd-manager")
|
||||||
|
|
||||||
|
# Konfiguration
|
||||||
|
MAIL_DIR = os.environ.get('MAIL_DIR', './mail')
|
||||||
|
DOVECOT_PASSWD_FILE = os.environ.get('DOVECOT_PASSWD_FILE', './config/passwd')
|
||||||
|
DOVECOT_PASSWD_BACKUP = os.environ.get('DOVECOT_PASSWD_BACKUP', './config/passwd.bak')
|
||||||
|
DOVECOT_CONTAINER = os.environ.get('DOVECOT_CONTAINER', 'dovecot')
|
||||||
|
DEFAULT_PASSWORD_PATTERN = os.environ.get('DEFAULT_PASSWORD_PATTERN', '{domain}{year}!')
|
||||||
|
UID = os.environ.get('MAIL_UID', '1001')
|
||||||
|
GID = os.environ.get('MAIL_GID', '1000')
|
||||||
|
AWS_REGION = os.environ.get('AWS_REGION', 'us-east-2')
|
||||||
|
|
||||||
|
# Domains-Konfiguration
|
||||||
|
DOMAINS_CONFIG_FILE = os.environ.get('DOMAINS_CONFIG_FILE', 'domains_config.json')
|
||||||
|
|
||||||
|
def load_domains_config():
|
||||||
|
"""Lädt die Domain-Konfiguration aus einer JSON-Datei oder aus Umgebungsvariablen"""
|
||||||
|
domains = {}
|
||||||
|
|
||||||
|
# Versuchen, Konfiguration aus JSON-Datei zu laden
|
||||||
|
config_file = Path(DOMAINS_CONFIG_FILE)
|
||||||
|
if config_file.exists():
|
||||||
|
try:
|
||||||
|
with open(config_file, 'r') as f:
|
||||||
|
domains = json.load(f)
|
||||||
|
logger.info(f"Domain-Konfiguration aus {DOMAINS_CONFIG_FILE} geladen")
|
||||||
|
return domains
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Laden der Domain-Konfiguration aus {DOMAINS_CONFIG_FILE}: {str(e)}")
|
||||||
|
|
||||||
|
# Fallback: Konfiguration aus Umgebungsvariablen
|
||||||
|
domain_index = 1
|
||||||
|
while True:
|
||||||
|
domain_key = f"DOMAIN_{domain_index}"
|
||||||
|
domain_name = os.environ.get(domain_key)
|
||||||
|
|
||||||
|
if not domain_name:
|
||||||
|
break # Keine weitere Domain-Definition gefunden
|
||||||
|
|
||||||
|
bucket = os.environ.get(f"{domain_key}_BUCKET", "")
|
||||||
|
prefix = os.environ.get(f"{domain_key}_PREFIX", "emails/")
|
||||||
|
usernames = os.environ.get(f"{domain_key}_USERNAMES", "")
|
||||||
|
region = os.environ.get(f"{domain_key}_REGION", AWS_REGION)
|
||||||
|
|
||||||
|
if domain_name and usernames:
|
||||||
|
domains[domain_name] = {
|
||||||
|
"bucket": bucket,
|
||||||
|
"prefix": prefix,
|
||||||
|
"usernames": usernames.split(','),
|
||||||
|
"region": region
|
||||||
|
}
|
||||||
|
logger.info(f"Domain {domain_name} aus Umgebungsvariablen konfiguriert")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Unvollständige Konfiguration für {domain_name}, wird übersprungen")
|
||||||
|
|
||||||
|
domain_index += 1
|
||||||
|
|
||||||
|
# Fallback für Abwärtskompatibilität
|
||||||
|
if not domains:
|
||||||
|
old_domain = os.environ.get('VALID_DOMAIN', '')
|
||||||
|
old_bucket = os.environ.get('S3_BUCKET', '')
|
||||||
|
old_prefix = os.environ.get('EMAIL_PREFIX', 'emails/')
|
||||||
|
old_usernames = os.environ.get('VALID_USERNAMES', '')
|
||||||
|
|
||||||
|
if old_domain and old_usernames:
|
||||||
|
domains[old_domain] = {
|
||||||
|
"bucket": old_bucket,
|
||||||
|
"prefix": old_prefix,
|
||||||
|
"usernames": old_usernames.split(','),
|
||||||
|
"region": AWS_REGION
|
||||||
|
}
|
||||||
|
logger.info(f"Alte Konfiguration für Domain {old_domain} geladen")
|
||||||
|
|
||||||
|
return domains
|
||||||
|
|
||||||
|
def generate_password(domain):
|
||||||
|
"""Generiert ein Passwort nach dem Muster {domain}{year}!"""
|
||||||
|
current_year = datetime.datetime.now().year
|
||||||
|
domain_prefix = domain.split('.')[0] # Nur den ersten Teil der Domain verwenden
|
||||||
|
return DEFAULT_PASSWORD_PATTERN.replace('{domain}', domain_prefix).replace('{year}', str(current_year))
|
||||||
|
|
||||||
|
def get_required_emails(domains_config):
|
||||||
|
"""
|
||||||
|
Ermittelt eine Liste aller E-Mail-Adressen, die in der passwd-Datei
|
||||||
|
vorhanden sein sollten, basierend auf der Domain-Konfiguration.
|
||||||
|
"""
|
||||||
|
required_emails = []
|
||||||
|
|
||||||
|
for domain, config in domains_config.items():
|
||||||
|
for username in config["usernames"]:
|
||||||
|
email = f"{username}@{domain}"
|
||||||
|
required_emails.append(email)
|
||||||
|
|
||||||
|
return sorted(required_emails)
|
||||||
|
|
||||||
|
def get_existing_emails(passwd_file):
|
||||||
|
"""
|
||||||
|
Liest die bestehende passwd-Datei und gibt eine Liste aller
|
||||||
|
bereits konfigurierten E-Mail-Adressen zurück.
|
||||||
|
"""
|
||||||
|
existing_emails = []
|
||||||
|
|
||||||
|
if os.path.exists(passwd_file):
|
||||||
|
try:
|
||||||
|
with open(passwd_file, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
if line.strip() and ':' in line:
|
||||||
|
email = line.strip().split(':')[0]
|
||||||
|
existing_emails.append(email)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Lesen der passwd-Datei: {str(e)}")
|
||||||
|
|
||||||
|
return sorted(existing_emails)
|
||||||
|
|
||||||
|
def read_existing_entries(passwd_file):
|
||||||
|
"""
|
||||||
|
Liest alle bestehenden Einträge aus der passwd-Datei und
|
||||||
|
gibt ein Dictionary mit E-Mail-Adressen als Schlüssel zurück.
|
||||||
|
"""
|
||||||
|
existing_entries = {}
|
||||||
|
|
||||||
|
if os.path.exists(passwd_file):
|
||||||
|
try:
|
||||||
|
with open(passwd_file, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
if line.strip() and ':' in line:
|
||||||
|
parts = line.strip().split(':')
|
||||||
|
email = parts[0]
|
||||||
|
existing_entries[email] = line.strip()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Lesen der passwd-Datei: {str(e)}")
|
||||||
|
|
||||||
|
return existing_entries
|
||||||
|
|
||||||
|
def update_dovecot_passwd(domains_config, force=False):
|
||||||
|
"""
|
||||||
|
Aktualisiert die Dovecot-Passwortdatei basierend auf der Domain-Konfiguration.
|
||||||
|
Erstellt eine neue temporäre Datei und vergleicht sie mit der vorhandenen,
|
||||||
|
um festzustellen, ob ein Reload von Dovecot erforderlich ist.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True, wenn Änderungen vorgenommen wurden, sonst False
|
||||||
|
"""
|
||||||
|
logger.info("Überprüfe Dovecot-Passwortdatei...")
|
||||||
|
|
||||||
|
# Prüfen, ob Aktualisierung überhaupt notwendig ist
|
||||||
|
required_emails = get_required_emails(domains_config)
|
||||||
|
existing_emails = get_existing_emails(DOVECOT_PASSWD_FILE)
|
||||||
|
|
||||||
|
# Prüfen auf fehlende oder überflüssige E-Mail-Adressen
|
||||||
|
missing_emails = [email for email in required_emails if email not in existing_emails]
|
||||||
|
surplus_emails = [email for email in existing_emails if email not in required_emails]
|
||||||
|
|
||||||
|
if not missing_emails and not surplus_emails and not force:
|
||||||
|
logger.info("Alle erforderlichen E-Mail-Adressen sind bereits konfiguriert, keine Änderung notwendig")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if missing_emails:
|
||||||
|
logger.info(f"Fehlende E-Mail-Adressen: {', '.join(missing_emails)}")
|
||||||
|
|
||||||
|
if surplus_emails:
|
||||||
|
logger.info(f"Überflüssige E-Mail-Adressen: {', '.join(surplus_emails)}")
|
||||||
|
|
||||||
|
logger.info("Aktualisiere Dovecot-Passwortdatei...")
|
||||||
|
|
||||||
|
# Temporäre Datei für die neue Passwd erstellen
|
||||||
|
temp_passwd = NamedTemporaryFile(delete=False, mode='w')
|
||||||
|
temp_passwd_path = temp_passwd.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Bestehende Einträge einlesen
|
||||||
|
existing_entries = read_existing_entries(DOVECOT_PASSWD_FILE)
|
||||||
|
|
||||||
|
# Neue Einträge generieren
|
||||||
|
new_entries = {}
|
||||||
|
for domain, config in domains_config.items():
|
||||||
|
domain_password = generate_password(domain)
|
||||||
|
for username in config["usernames"]:
|
||||||
|
email = f"{username}@{domain}"
|
||||||
|
# Wenn der Eintrag bereits existiert, verwenden wir diesen (damit Passwörter erhalten bleiben)
|
||||||
|
if email in existing_entries:
|
||||||
|
new_entries[email] = existing_entries[email]
|
||||||
|
else:
|
||||||
|
# Format: email:{PLAIN}password:uid:gid::/var/mail/domain/username:/bin/false
|
||||||
|
new_entry = f"{email}:{{PLAIN}}{domain_password}:{UID}:{GID}::/var/mail/{domain}/{username}:/bin/false"
|
||||||
|
new_entries[email] = new_entry
|
||||||
|
logger.info(f"Neuer E-Mail-Account erstellt: {email} mit Standardpasswort")
|
||||||
|
|
||||||
|
# Sortierte Einträge in die temporäre Datei schreiben
|
||||||
|
for email in sorted(new_entries.keys()):
|
||||||
|
temp_passwd.write(f"{new_entries[email]}\n")
|
||||||
|
|
||||||
|
temp_passwd.close()
|
||||||
|
|
||||||
|
# Prüfen, ob Änderungen vorgenommen wurden
|
||||||
|
if os.path.exists(DOVECOT_PASSWD_FILE) and filecmp.cmp(temp_passwd_path, DOVECOT_PASSWD_FILE):
|
||||||
|
logger.info("Keine inhaltlichen Änderungen an der passwd-Datei erforderlich")
|
||||||
|
os.unlink(temp_passwd_path)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Sicherungskopie erstellen
|
||||||
|
if os.path.exists(DOVECOT_PASSWD_FILE):
|
||||||
|
try:
|
||||||
|
shutil.copy2(DOVECOT_PASSWD_FILE, DOVECOT_PASSWD_BACKUP)
|
||||||
|
logger.info(f"Sicherungskopie erstellt: {DOVECOT_PASSWD_BACKUP}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Konnte keine Sicherungskopie erstellen: {str(e)}")
|
||||||
|
|
||||||
|
# Neue Datei aktivieren
|
||||||
|
try:
|
||||||
|
# Stellen Sie sicher, dass das Verzeichnis existiert
|
||||||
|
passwd_dir = os.path.dirname(DOVECOT_PASSWD_FILE)
|
||||||
|
if passwd_dir and not os.path.exists(passwd_dir):
|
||||||
|
os.makedirs(passwd_dir, exist_ok=True)
|
||||||
|
|
||||||
|
shutil.move(temp_passwd_path, DOVECOT_PASSWD_FILE)
|
||||||
|
# Berechtigungen setzen
|
||||||
|
os.chmod(DOVECOT_PASSWD_FILE, 0o600) # Nur Besitzer darf lesen/schreiben
|
||||||
|
logger.info(f"Neue passwd-Datei aktiviert: {DOVECOT_PASSWD_FILE}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Aktivieren der neuen passwd-Datei: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler bei der Aktualisierung der passwd-Datei: {str(e)}")
|
||||||
|
if os.path.exists(temp_passwd_path):
|
||||||
|
os.unlink(temp_passwd_path)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def reload_dovecot():
|
||||||
|
"""
|
||||||
|
Führt einen Reload von Dovecot durch, um die neue Passwortdatei zu aktivieren,
|
||||||
|
ohne den Container neu starten zu müssen.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info(f"Führe Dovecot-Reload durch...")
|
||||||
|
result = subprocess.run(
|
||||||
|
["docker", "exec", DOVECOT_CONTAINER, "doveadm", "reload"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.info("Dovecot-Reload erfolgreich durchgeführt")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"Dovecot-Reload fehlgeschlagen: {result.stderr}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Dovecot-Reload: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Hauptfunktion"""
|
||||||
|
logger.info("Dovecot Passwd Manager gestartet")
|
||||||
|
|
||||||
|
# Kommandozeilenargumente prüfen
|
||||||
|
update_mode = False
|
||||||
|
force_mode = False
|
||||||
|
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
if sys.argv[1].lower() == "update":
|
||||||
|
update_mode = True
|
||||||
|
elif sys.argv[1].lower() == "force":
|
||||||
|
update_mode = True
|
||||||
|
force_mode = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Domain-Konfigurationen laden
|
||||||
|
domains_config = load_domains_config()
|
||||||
|
|
||||||
|
if not domains_config:
|
||||||
|
logger.error("Keine Domain-Konfigurationen gefunden. Bitte konfigurieren Sie mindestens eine Domain.")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f"Folgende Domains werden verarbeitet: {', '.join(domains_config.keys())}")
|
||||||
|
|
||||||
|
# Im Nur-Prüfungs-Modus zeigen wir einfach die erforderlichen Änderungen an
|
||||||
|
if not update_mode:
|
||||||
|
required_emails = get_required_emails(domains_config)
|
||||||
|
existing_emails = get_existing_emails(DOVECOT_PASSWD_FILE)
|
||||||
|
|
||||||
|
missing_emails = [email for email in required_emails if email not in existing_emails]
|
||||||
|
surplus_emails = [email for email in existing_emails if email not in required_emails]
|
||||||
|
|
||||||
|
if not missing_emails and not surplus_emails:
|
||||||
|
logger.info("Alle erforderlichen E-Mail-Adressen sind bereits konfiguriert")
|
||||||
|
else:
|
||||||
|
if missing_emails:
|
||||||
|
logger.info(f"Fehlende E-Mail-Adressen: {', '.join(missing_emails)}")
|
||||||
|
|
||||||
|
if surplus_emails:
|
||||||
|
logger.info(f"Überflüssige E-Mail-Adressen: {', '.join(surplus_emails)}")
|
||||||
|
|
||||||
|
logger.info("Verwenden Sie 'update' als Parameter, um die Änderungen anzuwenden")
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
# Im Update-Modus führen wir die Änderungen durch
|
||||||
|
changes_made = update_dovecot_passwd(domains_config, force=force_mode)
|
||||||
|
|
||||||
|
if changes_made:
|
||||||
|
reload_dovecot()
|
||||||
|
else:
|
||||||
|
logger.info("Keine Änderungen vorgenommen, Dovecot-Reload nicht erforderlich")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
34
dovecot/entrypoint.sh
Executable file
34
dovecot/entrypoint.sh
Executable file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# entrypoint.sh - Custom entrypoint für Dovecot mit UMASK-Einstellung
|
||||||
|
|
||||||
|
# UMASK aus Umgebungsvariable setzen (Standard ist 002, wenn nicht anders angegeben)
|
||||||
|
CUSTOM_UMASK=${UMASK:-002}
|
||||||
|
echo "Setting umask to $CUSTOM_UMASK"
|
||||||
|
umask $CUSTOM_UMASK
|
||||||
|
|
||||||
|
# UMASK auch in /etc/login.defs setzen für zukünftige Logins
|
||||||
|
sed -i "s/^UMASK\s*[0-9]\+/UMASK $CUSTOM_UMASK/" /etc/login.defs
|
||||||
|
# Falls UMASK noch nicht existiert, hinzufügen
|
||||||
|
grep -q "^UMASK" /etc/login.defs || echo "UMASK $CUSTOM_UMASK" >> /etc/login.defs
|
||||||
|
|
||||||
|
# Dovecot-Einstellungen anwenden (falls nötig)
|
||||||
|
if [ -n "$DOVECOT_UID" ] && [ -n "$DOVECOT_GID" ]; then
|
||||||
|
echo "Configuring Dovecot with UID=$DOVECOT_UID, GID=$DOVECOT_GID"
|
||||||
|
sed -i "s/^mail_uid\s*=.*/mail_uid = $DOVECOT_UID/" /etc/dovecot/dovecot.conf
|
||||||
|
sed -i "s/^mail_gid\s*=.*/mail_gid = $DOVECOT_GID/" /etc/dovecot/dovecot.conf
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wenn nicht genügend Berechtigungen für Mail-Verzeichnisse, automatisch korrigieren
|
||||||
|
echo "Checking mail directory permissions..."
|
||||||
|
find /var/mail -type d -exec chmod 775 {} \; 2>/dev/null || true
|
||||||
|
find /var/mail -type f -exec chmod 664 {} \; 2>/dev/null || true
|
||||||
|
|
||||||
|
# Original Docker-Entrypoint ausführen
|
||||||
|
# oder direkter Start von Dovecot, je nach Image
|
||||||
|
if [ -f "/docker-entrypoint.sh" ]; then
|
||||||
|
echo "Executing original docker-entrypoint.sh"
|
||||||
|
exec /docker-entrypoint.sh "$@"
|
||||||
|
else
|
||||||
|
echo "Starting dovecot"
|
||||||
|
exec dovecot -F
|
||||||
|
fi
|
||||||
71
dovecot/generate_ses_smtp_password.js
Normal file
71
dovecot/generate_ses_smtp_password.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtaining Amazon SES SMTP credentials by converting existing AWS credentials
|
||||||
|
*
|
||||||
|
* Script based on:
|
||||||
|
* https://docs.aws.amazon.com/ses/latest/dg/smtp-credentials.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const SMTP_REGIONS = [
|
||||||
|
'us-east-2', // US East (Ohio)
|
||||||
|
'us-east-1', // US East (N. Virginia)
|
||||||
|
'us-west-2', // US West (Oregon)
|
||||||
|
'ap-south-1', // Asia Pacific (Mumbai)
|
||||||
|
'ap-northeast-2', // Asia Pacific (Seoul)
|
||||||
|
'ap-southeast-1', // Asia Pacific (Singapore)
|
||||||
|
'ap-southeast-2', // Asia Pacific (Sydney)
|
||||||
|
'ap-northeast-1', // Asia Pacific (Tokyo)
|
||||||
|
'ca-central-1', // Canada (Central)
|
||||||
|
'eu-central-1', // Europe (Frankfurt)
|
||||||
|
'eu-west-1', // Europe (Ireland)
|
||||||
|
'eu-west-2', // Europe (London)
|
||||||
|
'sa-east-1', // South America (Sao Paulo)
|
||||||
|
'us-gov-west-1', // AWS GovCloud (US)
|
||||||
|
];
|
||||||
|
|
||||||
|
// These values are required to calculate the signature. Do not change them.
|
||||||
|
const DATE = '11111111';
|
||||||
|
const SERVICE = 'ses';
|
||||||
|
const MESSAGE = 'SendRawEmail';
|
||||||
|
const TERMINAL = 'aws4_request';
|
||||||
|
const VERSION = [0x04];
|
||||||
|
|
||||||
|
function sign(key, msg) {
|
||||||
|
return crypto.createHmac('sha256', key).update(msg).digest();
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculate_key(secret_access_key, region) {
|
||||||
|
if (!SMTP_REGIONS.includes(region)) {
|
||||||
|
throw new Error(`The ${region} Region doesn't have an SMTP endpoint`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let signature;
|
||||||
|
|
||||||
|
signature = sign(`AWS4${secret_access_key}`, DATE);
|
||||||
|
signature = sign(signature, region);
|
||||||
|
signature = sign(signature, SERVICE);
|
||||||
|
signature = sign(signature, TERMINAL);
|
||||||
|
signature = sign(signature, MESSAGE);
|
||||||
|
|
||||||
|
const signature_and_version = Buffer.concat([
|
||||||
|
Buffer.from(VERSION),
|
||||||
|
signature,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const smtp_password = Buffer.from(signature_and_version).toString('base64');
|
||||||
|
|
||||||
|
return smtp_password;
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const [secret, region] = process.argv.slice(2);
|
||||||
|
|
||||||
|
console.log(calculate_key(secret, region));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
717
dovecot/s3_email_processor_api.py
Executable file
717
dovecot/s3_email_processor_api.py
Executable file
@@ -0,0 +1,717 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Optimierte S3 E-Mail Processor REST API
|
||||||
|
Mit Kompression, verbesserter Fehlerbehandlung und Request-Tracking
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import hashlib
|
||||||
|
import base64
|
||||||
|
import gzip
|
||||||
|
import boto3
|
||||||
|
from pathlib import Path
|
||||||
|
from email.parser import BytesParser
|
||||||
|
from email import policy
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from flask import Flask, request, jsonify, abort
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
# .env-Datei laden
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Flask-App initialisieren
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# Logging konfigurieren mit Request-ID-Support
|
||||||
|
class RequestIDFilter(logging.Filter):
|
||||||
|
def filter(self, record):
|
||||||
|
from flask import g
|
||||||
|
record.request_id = getattr(g, 'request_id', 'no-request')
|
||||||
|
return True
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - [%(request_id)s] - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler('s3_email_processor_api.log'),
|
||||||
|
logging.StreamHandler()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger("s3-email-processor-api")
|
||||||
|
logger.addFilter(RequestIDFilter())
|
||||||
|
|
||||||
|
# Konfiguration
|
||||||
|
MAIL_DIR = os.environ.get('MAIL_DIR', './mail')
|
||||||
|
API_TOKEN = os.environ.get('API_TOKEN')
|
||||||
|
|
||||||
|
# Request-Tracking für Duplikat-Erkennung
|
||||||
|
processed_requests = {}
|
||||||
|
REQUEST_CACHE_SIZE = 1000
|
||||||
|
REQUEST_CACHE_TTL = 3600 # 1 Stunde
|
||||||
|
|
||||||
|
def require_token(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
auth_header = request.headers.get('Authorization')
|
||||||
|
|
||||||
|
if not auth_header or not auth_header.startswith('Bearer '):
|
||||||
|
logger.warning("Fehlender Authorization-Header")
|
||||||
|
abort(401, description="Fehlender Authorization-Header")
|
||||||
|
|
||||||
|
token = auth_header[7:]
|
||||||
|
|
||||||
|
if not API_TOKEN or token != API_TOKEN:
|
||||||
|
logger.warning("Ungültiger API-Token")
|
||||||
|
abort(403, description="Ungültiger API-Token")
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
def setup_request_context():
|
||||||
|
"""Setzt Request-Kontext für Logging auf"""
|
||||||
|
from flask import g
|
||||||
|
g.request_id = request.headers.get('X-Request-ID', f'req-{int(time.time())}-{id(request)}')
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def before_request():
|
||||||
|
setup_request_context()
|
||||||
|
|
||||||
|
def is_duplicate_request(request_id, etag):
|
||||||
|
"""Prüft, ob Request bereits verarbeitet wurde"""
|
||||||
|
if not request_id or not etag:
|
||||||
|
return False
|
||||||
|
|
||||||
|
key = f"{request_id}:{etag}"
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
# Cache cleanup
|
||||||
|
if len(processed_requests) > REQUEST_CACHE_SIZE:
|
||||||
|
cutoff_time = current_time - REQUEST_CACHE_TTL
|
||||||
|
processed_requests.clear() # Einfache Lösung: kompletten Cache leeren
|
||||||
|
|
||||||
|
# Duplikat-Check
|
||||||
|
if key in processed_requests:
|
||||||
|
last_processed = processed_requests[key]
|
||||||
|
if current_time - last_processed < REQUEST_CACHE_TTL:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Request als verarbeitet markieren
|
||||||
|
processed_requests[key] = current_time
|
||||||
|
return False
|
||||||
|
|
||||||
|
def load_domains_config():
|
||||||
|
"""Lädt die Domain-Konfiguration aus Umgebungsvariablen"""
|
||||||
|
domains = {}
|
||||||
|
|
||||||
|
domain_index = 1
|
||||||
|
while True:
|
||||||
|
domain_key = f"DOMAIN_{domain_index}"
|
||||||
|
domain_name = os.environ.get(domain_key)
|
||||||
|
|
||||||
|
if not domain_name:
|
||||||
|
break
|
||||||
|
|
||||||
|
bucket = os.environ.get(f"{domain_key}_BUCKET", "")
|
||||||
|
prefix = os.environ.get(f"{domain_key}_PREFIX", "emails/")
|
||||||
|
usernames = os.environ.get(f"{domain_key}_USERNAMES", "")
|
||||||
|
region = os.environ.get(f"{domain_key}_REGION", "us-east-2")
|
||||||
|
|
||||||
|
if bucket and usernames:
|
||||||
|
domains[domain_name.lower()] = {
|
||||||
|
"bucket": bucket,
|
||||||
|
"prefix": prefix,
|
||||||
|
"usernames": usernames.split(','),
|
||||||
|
"region": region
|
||||||
|
}
|
||||||
|
logger.info(f"Domain {domain_name} konfiguriert")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Unvollständige Konfiguration für {domain_name}")
|
||||||
|
|
||||||
|
domain_index += 1
|
||||||
|
|
||||||
|
return domains
|
||||||
|
|
||||||
|
def extract_email_address(address):
|
||||||
|
"""Extrahiert die E-Mail-Adresse aus einem komplexen Adressformat"""
|
||||||
|
if not address:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if '@' in address and '<' not in address:
|
||||||
|
return address.strip()
|
||||||
|
|
||||||
|
match = re.search(r'<([^>]+)>', address)
|
||||||
|
if match:
|
||||||
|
return match.group(1)
|
||||||
|
|
||||||
|
return address.strip()
|
||||||
|
|
||||||
|
def is_valid_recipient(to_address, domains_config):
|
||||||
|
"""Prüft, ob die Empfängeradresse gültig ist"""
|
||||||
|
email = extract_email_address(to_address)
|
||||||
|
|
||||||
|
if not email or '@' not in email:
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
username, domain = email.split('@', 1)
|
||||||
|
|
||||||
|
if domain.lower() in domains_config:
|
||||||
|
domain_config = domains_config[domain.lower()]
|
||||||
|
if username.lower() in [u.lower() for u in domain_config["usernames"]]:
|
||||||
|
return True, domain.lower()
|
||||||
|
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
def get_maildir_path(to_address, mail_dir):
|
||||||
|
"""Ermittelt den Pfad im Maildir-Format"""
|
||||||
|
email = extract_email_address(to_address)
|
||||||
|
|
||||||
|
if '@' in email:
|
||||||
|
user, domain = email.split('@', 1)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
mail_dir_path = Path(mail_dir)
|
||||||
|
domain_dir = mail_dir_path / domain
|
||||||
|
user_dir = domain_dir / user
|
||||||
|
|
||||||
|
# Maildir-Struktur erstellen
|
||||||
|
for directory in [mail_dir_path, domain_dir, user_dir]:
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
os.chmod(directory, 0o775)
|
||||||
|
|
||||||
|
# Maildir-Unterverzeichnisse
|
||||||
|
for subdir in ['cur', 'new', 'tmp']:
|
||||||
|
subdir_path = user_dir / subdir
|
||||||
|
subdir_path.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
return user_dir
|
||||||
|
|
||||||
|
def store_email(email_content, to_address, message_id, s3_key, mail_dir):
|
||||||
|
"""Speichert eine E-Mail im Maildir-Format"""
|
||||||
|
try:
|
||||||
|
maildir = get_maildir_path(to_address, mail_dir)
|
||||||
|
if not maildir:
|
||||||
|
logger.error(f"Konnte Maildir für {to_address} nicht ermitteln")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Eindeutigen Dateinamen generieren
|
||||||
|
timestamp = int(time.time())
|
||||||
|
hostname = 'mail'
|
||||||
|
unique_id = hashlib.md5(f"{s3_key}:{timestamp}".encode()).hexdigest()
|
||||||
|
|
||||||
|
filename = f"{timestamp}.{unique_id}.{hostname}:2,"
|
||||||
|
email_path = maildir / 'new' / filename
|
||||||
|
|
||||||
|
with open(email_path, 'wb') as f:
|
||||||
|
f.write(email_content)
|
||||||
|
|
||||||
|
os.chmod(email_path, 0o664)
|
||||||
|
|
||||||
|
logger.info(f"E-Mail gespeichert: {email_path} ({len(email_content)} Bytes)")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Speichern der E-Mail {s3_key}: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def process_single_email(email_content, bucket, key, domain_name):
|
||||||
|
"""Verarbeitet eine einzelne E-Mail mit verbesserter Fehlerbehandlung"""
|
||||||
|
try:
|
||||||
|
all_domains_config = load_domains_config()
|
||||||
|
|
||||||
|
domain_name = domain_name.lower()
|
||||||
|
if domain_name not in all_domains_config:
|
||||||
|
logger.error(f"Domain {domain_name} nicht konfiguriert")
|
||||||
|
return {
|
||||||
|
"action": "error",
|
||||||
|
"error": f"Domain {domain_name} nicht konfiguriert",
|
||||||
|
"status": "error"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Header parsen mit besserer Fehlerbehandlung
|
||||||
|
try:
|
||||||
|
headers = BytesParser(policy=policy.default).parsebytes(email_content, headersonly=True)
|
||||||
|
to_address = headers.get('To', '')
|
||||||
|
from_address = headers.get('From', '')
|
||||||
|
date = headers.get('Date', '')
|
||||||
|
message_id = headers.get('Message-ID', '')
|
||||||
|
subject = headers.get('Subject', '')
|
||||||
|
|
||||||
|
# Header-Validierung
|
||||||
|
if not to_address:
|
||||||
|
logger.warning(f"E-Mail ohne To-Header: {key}")
|
||||||
|
return {
|
||||||
|
"action": "invalid",
|
||||||
|
"error": "Fehlender To-Header",
|
||||||
|
"status": "rejected"
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"Verarbeite: '{subject}' von {from_address} an {to_address}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Parsen der E-Mail-Header {key}: {str(e)}")
|
||||||
|
return {
|
||||||
|
"action": "invalid",
|
||||||
|
"error": f"Header-Parsing-Fehler: {str(e)}",
|
||||||
|
"status": "error"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Empfänger validieren
|
||||||
|
is_valid, recipient_domain = is_valid_recipient(to_address, all_domains_config)
|
||||||
|
|
||||||
|
if not is_valid:
|
||||||
|
logger.info(f"Ungültige Empfängeradresse: {to_address}")
|
||||||
|
return {
|
||||||
|
"action": "invalid",
|
||||||
|
"message": f"Ungültige Empfängeradresse: {to_address}",
|
||||||
|
"status": "rejected",
|
||||||
|
"recipient": to_address,
|
||||||
|
"reason": "invalid_recipient"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Domain-Zugehörigkeit prüfen
|
||||||
|
if recipient_domain != domain_name:
|
||||||
|
logger.info(f"E-Mail gehört zu Domain {recipient_domain}, nicht zu {domain_name}")
|
||||||
|
return {
|
||||||
|
"action": "wrong_domain",
|
||||||
|
"message": f"E-Mail gehört zu Domain {recipient_domain}",
|
||||||
|
"status": "skipped",
|
||||||
|
"expected_domain": domain_name,
|
||||||
|
"actual_domain": recipient_domain
|
||||||
|
}
|
||||||
|
|
||||||
|
# E-Mail speichern
|
||||||
|
if store_email(email_content, to_address, message_id, key, MAIL_DIR):
|
||||||
|
logger.info(f"E-Mail erfolgreich gespeichert für {to_address}")
|
||||||
|
return {
|
||||||
|
"action": "stored",
|
||||||
|
"message": f"E-Mail erfolgreich gespeichert",
|
||||||
|
"status": "success",
|
||||||
|
"recipient": to_address,
|
||||||
|
"sender": from_address,
|
||||||
|
"subject": subject,
|
||||||
|
"size": len(email_content)
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"action": "error",
|
||||||
|
"error": "Fehler beim Speichern der E-Mail",
|
||||||
|
"status": "error"
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unerwarteter Fehler bei E-Mail {key}: {str(e)}", exc_info=True)
|
||||||
|
return {
|
||||||
|
"action": "error",
|
||||||
|
"error": str(e),
|
||||||
|
"status": "error"
|
||||||
|
}
|
||||||
|
|
||||||
|
# API-Endpunkte
|
||||||
|
|
||||||
|
@app.route('/health', methods=['GET'])
|
||||||
|
def health_check():
|
||||||
|
"""Erweiterte Gesundheitsprüfung"""
|
||||||
|
from flask import g
|
||||||
|
|
||||||
|
# Basis-Gesundheitscheck
|
||||||
|
health_status = {
|
||||||
|
"status": "OK",
|
||||||
|
"message": "S3 E-Mail Processor API ist aktiv",
|
||||||
|
"timestamp": int(time.time()),
|
||||||
|
"version": "2.0",
|
||||||
|
"request_id": getattr(g, 'request_id', 'health-check')
|
||||||
|
}
|
||||||
|
|
||||||
|
# Erweiterte Checks
|
||||||
|
try:
|
||||||
|
# Maildir-Zugriff prüfen
|
||||||
|
mail_path = Path(MAIL_DIR)
|
||||||
|
if mail_path.exists() and mail_path.is_dir():
|
||||||
|
health_status["mail_dir"] = "accessible"
|
||||||
|
else:
|
||||||
|
health_status["mail_dir"] = "not_accessible"
|
||||||
|
health_status["status"] = "WARNING"
|
||||||
|
|
||||||
|
# Domain-Konfiguration prüfen
|
||||||
|
domains = load_domains_config()
|
||||||
|
health_status["configured_domains"] = len(domains)
|
||||||
|
health_status["domains"] = list(domains.keys())
|
||||||
|
|
||||||
|
# Cache-Status
|
||||||
|
health_status["request_cache_size"] = len(processed_requests)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
health_status["status"] = "ERROR"
|
||||||
|
health_status["error"] = str(e)
|
||||||
|
|
||||||
|
return jsonify(health_status)
|
||||||
|
|
||||||
|
@app.route('/process/<domain>', methods=['POST'])
|
||||||
|
@require_token
|
||||||
|
def process_email(domain):
|
||||||
|
"""Verarbeitet eine einzelne E-Mail mit verbesserter Fehlerbehandlung"""
|
||||||
|
from flask import g
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# JSON-Payload validieren
|
||||||
|
if not request.is_json:
|
||||||
|
return jsonify({"error": "Content-Type muss application/json sein"}), 400
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
# Erforderliche Felder prüfen
|
||||||
|
required_fields = ['bucket', 'key', 'email_content', 'domain']
|
||||||
|
missing_fields = [field for field in required_fields if field not in data]
|
||||||
|
if missing_fields:
|
||||||
|
return jsonify({
|
||||||
|
"error": f"Fehlende Felder: {', '.join(missing_fields)}"
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# Duplikat-Check
|
||||||
|
request_id = data.get('request_id', g.request_id)
|
||||||
|
etag = data.get('etag')
|
||||||
|
|
||||||
|
if is_duplicate_request(request_id, etag):
|
||||||
|
logger.info(f"Duplikat-Request erkannt: {request_id}:{etag}")
|
||||||
|
return jsonify({
|
||||||
|
"action": "duplicate",
|
||||||
|
"message": "Request bereits verarbeitet",
|
||||||
|
"status": "skipped",
|
||||||
|
"request_id": request_id
|
||||||
|
})
|
||||||
|
|
||||||
|
# E-Mail-Content dekodieren
|
||||||
|
try:
|
||||||
|
email_base64 = data['email_content']
|
||||||
|
|
||||||
|
# Kompression-Support
|
||||||
|
if data.get('compressed', False):
|
||||||
|
compressed_data = base64.b64decode(email_base64)
|
||||||
|
email_content = gzip.decompress(compressed_data)
|
||||||
|
logger.info(f"E-Mail dekomprimiert: {data.get('compressed_size', 0)} -> {len(email_content)} Bytes")
|
||||||
|
else:
|
||||||
|
email_content = base64.b64decode(email_base64)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Dekodieren des E-Mail-Contents: {str(e)}")
|
||||||
|
return jsonify({
|
||||||
|
"error": f"Content-Dekodierung fehlgeschlagen: {str(e)}"
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# Größen-Validierung
|
||||||
|
email_size = len(email_content)
|
||||||
|
max_size = 25 * 1024 * 1024 # 25MB
|
||||||
|
if email_size > max_size:
|
||||||
|
logger.warning(f"E-Mail zu groß: {email_size} Bytes")
|
||||||
|
return jsonify({
|
||||||
|
"action": "too_large",
|
||||||
|
"error": f"E-Mail zu groß: {email_size} Bytes (max: {max_size})",
|
||||||
|
"status": "rejected"
|
||||||
|
}), 413
|
||||||
|
|
||||||
|
# E-Mail verarbeiten
|
||||||
|
logger.info(f"Verarbeite E-Mail: {data['key']} ({email_size} Bytes)")
|
||||||
|
result = process_single_email(
|
||||||
|
email_content=email_content,
|
||||||
|
bucket=data['bucket'],
|
||||||
|
key=data['key'],
|
||||||
|
domain_name=domain
|
||||||
|
)
|
||||||
|
|
||||||
|
# Performance-Metriken hinzufügen
|
||||||
|
processing_time = time.time() - start_time
|
||||||
|
result.update({
|
||||||
|
"processing_time_ms": round(processing_time * 1000, 2),
|
||||||
|
"request_id": request_id,
|
||||||
|
"email_size": email_size
|
||||||
|
})
|
||||||
|
|
||||||
|
# Log-Level basierend auf Ergebnis
|
||||||
|
if result.get("status") == "success":
|
||||||
|
logger.info(f"E-Mail erfolgreich verarbeitet in {processing_time:.2f}s")
|
||||||
|
elif result.get("status") in ["rejected", "skipped"]:
|
||||||
|
logger.info(f"E-Mail {result.get('action')}: {result.get('message')}")
|
||||||
|
else:
|
||||||
|
logger.error(f"E-Mail-Verarbeitung fehlgeschlagen: {result.get('error')}")
|
||||||
|
|
||||||
|
# HTTP-Status basierend auf Ergebnis
|
||||||
|
if result.get("status") == "error":
|
||||||
|
return jsonify(result), 500
|
||||||
|
elif result.get("action") == "too_large":
|
||||||
|
return jsonify(result), 413
|
||||||
|
else:
|
||||||
|
return jsonify(result), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
processing_time = time.time() - start_time
|
||||||
|
logger.error(f"Unerwarteter Fehler nach {processing_time:.2f}s: {str(e)}", exc_info=True)
|
||||||
|
return jsonify({
|
||||||
|
"error": "Interner Server-Fehler",
|
||||||
|
"details": str(e),
|
||||||
|
"processing_time_ms": round(processing_time * 1000, 2),
|
||||||
|
"request_id": getattr(g, 'request_id', 'unknown')
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/retry/<email_id>/<domain>', methods=['GET'])
|
||||||
|
@require_token
|
||||||
|
def retry_single_email(email_id, domain):
|
||||||
|
"""
|
||||||
|
Retry-Endpunkt für einzelne E-Mail basierend auf ID
|
||||||
|
Sucht die E-Mail in S3 anhand der ID und verarbeitet sie erneut
|
||||||
|
"""
|
||||||
|
from flask import g
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
logger.info(f"Retry-Request für E-Mail-ID: {email_id} in Domain: {domain}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Domain-Konfiguration laden
|
||||||
|
all_domains_config = load_domains_config()
|
||||||
|
domain_name = domain.lower()
|
||||||
|
|
||||||
|
if domain_name not in all_domains_config:
|
||||||
|
logger.error(f"Domain {domain_name} nicht konfiguriert")
|
||||||
|
return jsonify({
|
||||||
|
"error": f"Domain {domain_name} nicht konfiguriert",
|
||||||
|
"status": "error",
|
||||||
|
"email_id": email_id
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
domain_config = all_domains_config[domain_name]
|
||||||
|
bucket = domain_config["bucket"]
|
||||||
|
prefix = domain_config["prefix"]
|
||||||
|
region = domain_config["region"]
|
||||||
|
|
||||||
|
# S3-Client initialisieren
|
||||||
|
s3_client = boto3.client('s3', region_name=region)
|
||||||
|
|
||||||
|
# E-Mail anhand der ID suchen
|
||||||
|
logger.info(f"Suche E-Mail mit ID {email_id} in Bucket {bucket}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Paginator für alle Objekte im Bucket
|
||||||
|
paginator = s3_client.get_paginator('list_objects_v2')
|
||||||
|
pages = paginator.paginate(Bucket=bucket, Prefix=prefix)
|
||||||
|
|
||||||
|
found_key = None
|
||||||
|
|
||||||
|
for page in pages:
|
||||||
|
if 'Contents' not in page:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for obj in page['Contents']:
|
||||||
|
key = obj['Key']
|
||||||
|
|
||||||
|
# Verzeichnisse überspringen
|
||||||
|
if key.endswith('/'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# E-Mail-ID aus dem Key extrahieren (oft Teil des Dateinamens)
|
||||||
|
# Verschiedene Möglichkeiten prüfen:
|
||||||
|
# 1. ID ist Teil des Dateinamens
|
||||||
|
if email_id in key:
|
||||||
|
found_key = key
|
||||||
|
break
|
||||||
|
|
||||||
|
# 2. ID könnte in Message-ID der E-Mail sein - Header laden und prüfen
|
||||||
|
try:
|
||||||
|
# Nur Header laden für Performance
|
||||||
|
response = s3_client.get_object(
|
||||||
|
Bucket=bucket,
|
||||||
|
Key=key,
|
||||||
|
Range='bytes=0-2048' # Nur erste 2KB für Header
|
||||||
|
)
|
||||||
|
header_content = response['Body'].read()
|
||||||
|
|
||||||
|
# Nach Message-ID suchen
|
||||||
|
if email_id.encode() in header_content:
|
||||||
|
found_key = key
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as header_e:
|
||||||
|
# Header-Check fehlgeschlagen, weiter mit nächster E-Mail
|
||||||
|
logger.debug(f"Header-Check für {key} fehlgeschlagen: {str(header_e)}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if found_key:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found_key:
|
||||||
|
logger.warning(f"E-Mail mit ID {email_id} nicht gefunden")
|
||||||
|
return jsonify({
|
||||||
|
"error": f"E-Mail mit ID {email_id} nicht gefunden",
|
||||||
|
"status": "not_found",
|
||||||
|
"email_id": email_id,
|
||||||
|
"searched_bucket": bucket,
|
||||||
|
"searched_prefix": prefix
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
logger.info(f"E-Mail gefunden: {found_key}")
|
||||||
|
|
||||||
|
except Exception as search_e:
|
||||||
|
logger.error(f"Fehler beim Suchen der E-Mail: {str(search_e)}")
|
||||||
|
return jsonify({
|
||||||
|
"error": f"Fehler beim Suchen: {str(search_e)}",
|
||||||
|
"status": "error",
|
||||||
|
"email_id": email_id
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
# E-Mail laden und verarbeiten
|
||||||
|
try:
|
||||||
|
response = s3_client.get_object(Bucket=bucket, Key=found_key)
|
||||||
|
email_content = response['Body'].read()
|
||||||
|
|
||||||
|
logger.info(f"E-Mail geladen: {len(email_content)} Bytes")
|
||||||
|
|
||||||
|
# E-Mail verarbeiten (gleiche Logik wie bei process_email)
|
||||||
|
result = process_single_email(
|
||||||
|
email_content=email_content,
|
||||||
|
bucket=bucket,
|
||||||
|
key=found_key,
|
||||||
|
domain_name=domain_name
|
||||||
|
)
|
||||||
|
|
||||||
|
# Performance-Metriken hinzufügen
|
||||||
|
processing_time = time.time() - start_time
|
||||||
|
result.update({
|
||||||
|
"processing_time_ms": round(processing_time * 1000, 2),
|
||||||
|
"request_id": getattr(g, 'request_id', 'retry-request'),
|
||||||
|
"email_size": len(email_content),
|
||||||
|
"email_id": email_id,
|
||||||
|
"s3_key": found_key,
|
||||||
|
"retry": True
|
||||||
|
})
|
||||||
|
|
||||||
|
# Bei erfolgreichem Processing E-Mail aus S3 löschen
|
||||||
|
if result.get('action') == 'stored':
|
||||||
|
try:
|
||||||
|
s3_client.delete_object(Bucket=bucket, Key=found_key)
|
||||||
|
logger.info(f"E-Mail nach erfolgreichem Retry aus S3 gelöscht: {found_key}")
|
||||||
|
result["s3_deleted"] = True
|
||||||
|
except Exception as delete_e:
|
||||||
|
logger.warning(f"Konnte E-Mail nach Retry nicht löschen: {str(delete_e)}")
|
||||||
|
result["s3_deleted"] = False
|
||||||
|
|
||||||
|
# Logging basierend auf Ergebnis
|
||||||
|
if result.get("status") == "success":
|
||||||
|
logger.info(f"Retry erfolgreich für E-Mail-ID {email_id} in {processing_time:.2f}s")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Retry für E-Mail-ID {email_id} nicht erfolgreich: {result.get('message')}")
|
||||||
|
|
||||||
|
# HTTP-Status basierend auf Ergebnis
|
||||||
|
if result.get("status") == "error":
|
||||||
|
return jsonify(result), 500
|
||||||
|
else:
|
||||||
|
return jsonify(result), 200
|
||||||
|
|
||||||
|
except Exception as process_e:
|
||||||
|
logger.error(f"Fehler beim Laden/Verarbeiten der E-Mail {found_key}: {str(process_e)}")
|
||||||
|
return jsonify({
|
||||||
|
"error": f"Verarbeitungsfehler: {str(process_e)}",
|
||||||
|
"status": "error",
|
||||||
|
"email_id": email_id,
|
||||||
|
"s3_key": found_key
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
processing_time = time.time() - start_time
|
||||||
|
logger.error(f"Unerwarteter Fehler bei Retry nach {processing_time:.2f}s: {str(e)}", exc_info=True)
|
||||||
|
return jsonify({
|
||||||
|
"error": "Interner Server-Fehler beim Retry",
|
||||||
|
"details": str(e),
|
||||||
|
"processing_time_ms": round(processing_time * 1000, 2),
|
||||||
|
"request_id": getattr(g, 'request_id', 'unknown'),
|
||||||
|
"email_id": email_id
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/stats', methods=['GET'])
|
||||||
|
@require_token
|
||||||
|
def get_stats():
|
||||||
|
"""API-Statistiken und Metriken"""
|
||||||
|
try:
|
||||||
|
stats = {
|
||||||
|
"api_version": "2.0",
|
||||||
|
"uptime_seconds": int(time.time() - app.start_time) if hasattr(app, 'start_time') else 0,
|
||||||
|
"request_cache": {
|
||||||
|
"size": len(processed_requests),
|
||||||
|
"max_size": REQUEST_CACHE_SIZE,
|
||||||
|
"ttl_seconds": REQUEST_CACHE_TTL
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"mail_dir": MAIL_DIR,
|
||||||
|
"domains": len(load_domains_config())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Maildir-Statistiken
|
||||||
|
try:
|
||||||
|
mail_path = Path(MAIL_DIR)
|
||||||
|
if mail_path.exists():
|
||||||
|
domain_stats = {}
|
||||||
|
for domain_dir in mail_path.iterdir():
|
||||||
|
if domain_dir.is_dir():
|
||||||
|
user_count = len([d for d in domain_dir.iterdir() if d.is_dir()])
|
||||||
|
domain_stats[domain_dir.name] = {"users": user_count}
|
||||||
|
|
||||||
|
stats["maildir_stats"] = domain_stats
|
||||||
|
except Exception as e:
|
||||||
|
stats["maildir_error"] = str(e)
|
||||||
|
|
||||||
|
return jsonify(stats)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
# Cache-Cleanup-Task (läuft alle 10 Minuten)
|
||||||
|
import threading
|
||||||
|
|
||||||
|
def cleanup_cache():
|
||||||
|
"""Bereinigt den Request-Cache periodisch"""
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
current_time = time.time()
|
||||||
|
cutoff_time = current_time - REQUEST_CACHE_TTL
|
||||||
|
|
||||||
|
# Alte Einträge entfernen
|
||||||
|
keys_to_remove = [
|
||||||
|
key for key, timestamp in processed_requests.items()
|
||||||
|
if timestamp < cutoff_time
|
||||||
|
]
|
||||||
|
|
||||||
|
for key in keys_to_remove:
|
||||||
|
processed_requests.pop(key, None)
|
||||||
|
|
||||||
|
if keys_to_remove:
|
||||||
|
logger.info(f"Cache bereinigt: {len(keys_to_remove)} alte Einträge entfernt")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler bei Cache-Bereinigung: {str(e)}")
|
||||||
|
|
||||||
|
# 10 Minuten warten
|
||||||
|
time.sleep(600)
|
||||||
|
|
||||||
|
# Hintergrund-Thread für Cache-Bereinigung starten
|
||||||
|
cleanup_thread = threading.Thread(target=cleanup_cache, daemon=True)
|
||||||
|
cleanup_thread.start()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Start-Zeit für Uptime-Tracking
|
||||||
|
app.start_time = time.time()
|
||||||
|
|
||||||
|
# Überprüfungen beim Start
|
||||||
|
if not API_TOKEN:
|
||||||
|
logger.warning("WARNUNG: Kein API_TOKEN definiert!")
|
||||||
|
|
||||||
|
domains = load_domains_config()
|
||||||
|
logger.info(f"API startet mit {len(domains)} konfigurierten Domains")
|
||||||
|
|
||||||
|
# Server starten
|
||||||
|
app.run(host='0.0.0.0', port=5000, debug=False)
|
||||||
38
dovecot/setup_email_domain.sh
Executable file
38
dovecot/setup_email_domain.sh
Executable file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# setup_email_domain.sh - Ein Wrapper-Script, das alle drei Skripte in der richtigen Reihenfolge ausführt
|
||||||
|
|
||||||
|
# Überprüfen, ob die Domain-Variable gesetzt ist
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "Fehler: Keine Domain angegeben."
|
||||||
|
echo "Verwendung: ./setup_email_domain.sh domain.de [region]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DOMAIN_NAME=$1
|
||||||
|
AWS_REGION=${2:-"us-east-2"}
|
||||||
|
|
||||||
|
# Variablen exportieren
|
||||||
|
export DOMAIN_NAME
|
||||||
|
export AWS_REGION
|
||||||
|
|
||||||
|
echo "=== AWS E-Mail-Infrastruktur für $DOMAIN_NAME einrichten ==="
|
||||||
|
echo "AWS-Region: $AWS_REGION"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Skripte nacheinander ausführen
|
||||||
|
echo "1. S3-Bucket erstellen..."
|
||||||
|
./awss3.sh
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "2. SES-Konfiguration einrichten..."
|
||||||
|
export S3_BUCKET_NAME=$(echo "$DOMAIN_NAME" | tr '.' '-' | awk '{print $0 "-emails"}')
|
||||||
|
./awsses.sh
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "3. IAM-Benutzer und SMTP-Zugangsdaten erstellen..."
|
||||||
|
./awsiam.sh
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "=== Setup abgeschlossen ==="
|
||||||
|
echo "Alle Schritte wurden abgeschlossen. Bitte überprüfen Sie die Ausgaben der einzelnen Skripte."
|
||||||
|
echo "Vergessen Sie nicht, die benötigten DNS-Einträge für Ihre Domain zu setzen, um die SES-Verifizierung abzuschließen."
|
||||||
75
dovecot/start_email_api.sh
Executable file
75
dovecot/start_email_api.sh
Executable file
@@ -0,0 +1,75 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# start_email_api.sh
|
||||||
|
# Dieses Script startet die S3 Email Downloader REST API in einer virtuellen Python-Umgebung
|
||||||
|
|
||||||
|
# Verzeichnis, in dem sich das Script befindet
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
# Name der virtuellen Umgebung
|
||||||
|
VENV_NAME="venv"
|
||||||
|
|
||||||
|
# Python-Executable (falls spezifische Version benötigt wird)
|
||||||
|
PYTHON_EXEC="python3"
|
||||||
|
|
||||||
|
# API Port
|
||||||
|
PORT=${EMAIL_API_PORT:-5000}
|
||||||
|
|
||||||
|
# Umgebungsvariablen Datei
|
||||||
|
ENV_FILE=".env"
|
||||||
|
|
||||||
|
# Prüfen, ob virtuelle Umgebung existiert
|
||||||
|
if [ ! -d "$VENV_NAME" ]; then
|
||||||
|
echo "Virtuelle Umgebung wird erstellt..."
|
||||||
|
$PYTHON_EXEC -m venv "$VENV_NAME"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Fehler beim Erstellen der virtuellen Umgebung!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Virtuelle Umgebung aktivieren
|
||||||
|
source "$VENV_NAME/bin/activate"
|
||||||
|
|
||||||
|
# Abhängigkeiten installieren, falls erforderlich
|
||||||
|
echo "Abhängigkeiten werden installiert..."
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install boto3 flask python-dotenv gunicorn
|
||||||
|
|
||||||
|
# Prüfen, ob .env-Datei existiert
|
||||||
|
if [ ! -f "$ENV_FILE" ]; then
|
||||||
|
echo "WARNUNG: $ENV_FILE nicht gefunden!"
|
||||||
|
echo "Bitte erstellen Sie eine .env-Datei mit den erforderlichen Konfigurationen."
|
||||||
|
echo "Beispiel:"
|
||||||
|
echo "API_TOKEN=ihr_geheimer_token"
|
||||||
|
echo "MAIL_DIR=./mail"
|
||||||
|
echo "AWS_REGION=us-east-2"
|
||||||
|
echo "AWS_ACCESS_KEY_ID=your_access_key"
|
||||||
|
echo "AWS_SECRET_ACCESS_KEY=your_secret_key"
|
||||||
|
echo "DOMAIN_1=example.com"
|
||||||
|
echo "DOMAIN_1_BUCKET=example-bucket"
|
||||||
|
echo "DOMAIN_1_USERNAMES=user1,user2,user3"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Überprüfen, ob API_TOKEN in der .env-Datei gesetzt ist
|
||||||
|
if ! grep -q "API_TOKEN" "$ENV_FILE"; then
|
||||||
|
echo "WARNUNG: API_TOKEN nicht in $ENV_FILE gefunden!"
|
||||||
|
echo "Die API wird ohne Token-Schutz gestartet. Dies ist unsicher für Produktionsumgebungen."
|
||||||
|
read -p "Möchten Sie fortfahren? (j/n): " choice
|
||||||
|
if [[ ! "$choice" =~ ^[jJyY]$ ]]; then
|
||||||
|
echo "Abbruch."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfen, ob das Python-Script existiert
|
||||||
|
API_SCRIPT="s3_email_processor_api.py"
|
||||||
|
if [ ! -f "$API_SCRIPT" ]; then
|
||||||
|
echo "Fehler: $API_SCRIPT nicht gefunden!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# API im Produktionsmodus mit Gunicorn starten
|
||||||
|
echo "Starte S3 Email Downloader API auf Port $PORT..."
|
||||||
|
exec gunicorn --bind "0.0.0.0:$PORT" --workers 2 "s3_email_processor_api:app"
|
||||||
21
email_api/docker-compose-python.yml
Normal file
21
email_api/docker-compose-python.yml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
services:
|
||||||
|
email-api:
|
||||||
|
container_name: email-api
|
||||||
|
image: python:3.12-slim
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host
|
||||||
|
volumes:
|
||||||
|
- ./email_api:/app
|
||||||
|
- /var/mail:/var/mail # Maildir-Zugriff für Health-Check
|
||||||
|
working_dir: /app
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- API_TOKEN=${API_TOKEN}
|
||||||
|
- AWS_REGION=${AWS_REGION}
|
||||||
|
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
|
||||||
|
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
|
||||||
|
command: >
|
||||||
|
bash -c "pip install --upgrade pip &&
|
||||||
|
pip install flask python-dotenv boto3 requests &&
|
||||||
|
python app.py"
|
||||||
50
email_api/docker-compose.yml
Normal file
50
email_api/docker-compose.yml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
email-api:
|
||||||
|
container_name: email-api
|
||||||
|
image: node:22-slim
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host
|
||||||
|
volumes:
|
||||||
|
- ./email_api:/app
|
||||||
|
- /var/mail:/var/mail # Maildir access for health check
|
||||||
|
working_dir: /app
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- API_TOKEN=${API_TOKEN}
|
||||||
|
- AWS_REGION=${AWS_REGION}
|
||||||
|
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
|
||||||
|
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
|
||||||
|
- SMTP_HOST=${SMTP_HOST:-localhost}
|
||||||
|
- SMTP_PORT=${SMTP_PORT:-25}
|
||||||
|
- MAILCOW_API_KEY=${MAILCOW_API_KEY}
|
||||||
|
- MAILCOW_API=${MAILCOW_API}
|
||||||
|
- PGHOST=postgres
|
||||||
|
- PGUSER=${PGUSER:-email_user}
|
||||||
|
- PGPASSWORD=${PGPASSWORD:-email_password}
|
||||||
|
- PGDATABASE=${PGDATABASE:-email_db}
|
||||||
|
- PGPORT=${PGPORT:-5433}
|
||||||
|
command: >
|
||||||
|
bash -c "npm install && node app.js"
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
container_name: email-api-postgres
|
||||||
|
image: postgres:16
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=${PGUSER:-email_user}
|
||||||
|
- POSTGRES_PASSWORD=${PGPASSWORD:-email_password}
|
||||||
|
- POSTGRES_DB=${PGDATABASE:-email_db}
|
||||||
|
volumes:
|
||||||
|
- email_postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
command: >
|
||||||
|
postgres -c port=${PGPORT:-5433}
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
email_postgres_data:
|
||||||
300
email_api/email_api/app.js
Normal file
300
email_api/email_api/app.js
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { Pool } from 'pg';
|
||||||
|
import AWS from 'aws-sdk';
|
||||||
|
import nodemailer from 'nodemailer';
|
||||||
|
import { simpleParser } from 'mailparser';
|
||||||
|
import { Base64 } from 'js-base64';
|
||||||
|
import { createGzip, gunzipSync } from 'zlib';
|
||||||
|
import { createLogger, format, transports } from 'winston';
|
||||||
|
import { config } from 'dotenv';
|
||||||
|
|
||||||
|
// Load environment variables
|
||||||
|
config();
|
||||||
|
|
||||||
|
// Check Node.js version
|
||||||
|
const [major] = process.versions.node.split('.').map(Number);
|
||||||
|
if (major < 22) {
|
||||||
|
throw new Error('Node.js 22 or higher required');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger setup
|
||||||
|
const logger = createLogger({
|
||||||
|
level: 'info',
|
||||||
|
format: format.combine(
|
||||||
|
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||||
|
format.printf(({ timestamp, level, message }) => `${timestamp} ${level.toUpperCase()} ${message}`)
|
||||||
|
),
|
||||||
|
transports: [new transports.Console()]
|
||||||
|
});
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json({ limit: '20mb' }));
|
||||||
|
app.use(express.urlencoded({ limit: '20mb', extended: true }));
|
||||||
|
|
||||||
|
const SMTP_HOST = process.env.SMTP_HOST || 'localhost';
|
||||||
|
const SMTP_PORT = parseInt(process.env.SMTP_PORT || '25', 10);
|
||||||
|
const API_TOKEN = process.env.API_TOKEN;
|
||||||
|
const AWS_REGION = process.env.AWS_REGION || 'us-east-1';
|
||||||
|
const API_KEY = process.env.MAILCOW_API_KEY;
|
||||||
|
const MAILCOW_API = process.env.MAILCOW_API;
|
||||||
|
|
||||||
|
// PostgreSQL client
|
||||||
|
const pool = new Pool({
|
||||||
|
user: process.env.PGUSER || 'email_user',
|
||||||
|
password: process.env.PGPASSWORD || 'email_password',
|
||||||
|
host: process.env.PGHOST || 'postgres',
|
||||||
|
database: process.env.PGDATABASE || 'email_db',
|
||||||
|
port: parseInt(process.env.PGPORT || '5433', 10)
|
||||||
|
});
|
||||||
|
|
||||||
|
// AWS S3 client
|
||||||
|
const s3Client = new AWS.S3({ region: AWS_REGION });
|
||||||
|
|
||||||
|
// Nodemailer transporter
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: SMTP_HOST,
|
||||||
|
port: SMTP_PORT,
|
||||||
|
secure: false, // Adjust if SMTP requires TLS
|
||||||
|
tls: {
|
||||||
|
rejectUnauthorized: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Utility to check if domain exists
|
||||||
|
async function domainExists(domain) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${MAILCOW_API}/get/domain/all`, {
|
||||||
|
headers: { 'X-API-Key': API_KEY },
|
||||||
|
signal: AbortSignal.timeout(5000)
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
const domains = await response.json();
|
||||||
|
return domains.some(d => d.domain_name?.toLowerCase() === domain.toLowerCase());
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error checking domain '${domain}': ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility to check if inbox exists
|
||||||
|
async function inboxExists(domain, localPart) {
|
||||||
|
if (!(await domainExists(domain))) {
|
||||||
|
logger.info(`Domain '${domain}' unknown – skip mailbox lookup`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${MAILCOW_API}/get/mailbox/all/${domain}`, {
|
||||||
|
headers: { 'X-API-Key': API_KEY },
|
||||||
|
signal: AbortSignal.timeout(5000)
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
const mailboxes = await response.json();
|
||||||
|
return mailboxes.some(m => m.local_part?.toLowerCase() === localPart.toLowerCase());
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error checking inbox '${localPart}@${domain}': ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility to mark email as processed in PostgreSQL
|
||||||
|
async function markEmailAsProcessed(domain, key, status, processor = 'rest-api', fromAddr = null, toAddrs = []) {
|
||||||
|
try {
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO email_statuses (domain, s3_key, status, timestamp, processor, from_addr, to_addrs)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
ON CONFLICT (domain, s3_key) DO UPDATE SET
|
||||||
|
status = EXCLUDED.status,
|
||||||
|
timestamp = EXCLUDED.timestamp,
|
||||||
|
processor = EXCLUDED.processor,
|
||||||
|
from_addr = EXCLUDED.from_addr,
|
||||||
|
to_addrs = EXCLUDED.to_addrs`,
|
||||||
|
[domain, key, status, Math.floor(Date.now() / 1000), processor, fromAddr, toAddrs]
|
||||||
|
);
|
||||||
|
logger.info(`Marked ${domain}/${key} as ${status} in database`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error marking ${domain}/${key} in database: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process endpoint
|
||||||
|
app.post('/process/:domain', async (req, res) => {
|
||||||
|
const { domain } = req.params;
|
||||||
|
const auth = req.headers['authorization']; // Fixed: Use req.headers['authorization'] instead of req.headers.get
|
||||||
|
if (auth !== `Bearer ${API_TOKEN}`) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = req.body;
|
||||||
|
if (!data) {
|
||||||
|
return res.status(400).json({ error: 'Invalid payload' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = data.request_id || 'no-request-id';
|
||||||
|
const payloadSummary = Object.fromEntries(
|
||||||
|
Object.entries(data)
|
||||||
|
.filter(([k, v]) => k !== 'email_content' || typeof v === 'string')
|
||||||
|
.map(([k, v]) => [k, k === 'email_content' ? v.length : v])
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`[${requestId}] INCOMING POST /process/${domain}: payload_summary=${JSON.stringify(payloadSummary)}`);
|
||||||
|
|
||||||
|
let recipients = [];
|
||||||
|
let parser;
|
||||||
|
let fromAddr = `lambda@${req.params.domain}`;
|
||||||
|
try {
|
||||||
|
// Decode and parse email
|
||||||
|
const content = data.email_content;
|
||||||
|
const compressed = data.compressed || false;
|
||||||
|
const raw = Base64.decode(content);
|
||||||
|
const emailBytes = compressed ? gunzipSync(Buffer.from(raw, 'binary')).toString('binary') : raw;
|
||||||
|
|
||||||
|
const emailBuffer = Buffer.from(emailBytes, 'binary');
|
||||||
|
parser = await simpleParser(emailBuffer);
|
||||||
|
fromAddr = parser.from?.value[0]?.address || `lambda@${domain}`;
|
||||||
|
recipients = [
|
||||||
|
...(parser.to?.value || []),
|
||||||
|
...(parser.cc?.value || []),
|
||||||
|
...(parser.bcc?.value || [])
|
||||||
|
].map(addr => addr.address).filter(Boolean);
|
||||||
|
|
||||||
|
if (!recipients.length) {
|
||||||
|
await markEmailAsProcessed(domain, data.s3_key, 'noRecipients', 'rest-api', fromAddr, []);
|
||||||
|
return res.status(400).json({ error: 'No recipients' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter valid recipients
|
||||||
|
const validRecipients = [];
|
||||||
|
for (const addr of recipients) {
|
||||||
|
const [local, dom] = addr.split('@');
|
||||||
|
if (!dom || dom.toLowerCase() !== domain.toLowerCase()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (await inboxExists(domain, local)) {
|
||||||
|
validRecipients.push(addr);
|
||||||
|
} else {
|
||||||
|
logger.info(`Skipping non-existent inbox: ${addr}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validRecipients.length) {
|
||||||
|
logger.info(`[${requestId}] No valid inboxes for ${domain} – skip.`);
|
||||||
|
await markEmailAsProcessed(domain, data.s3_key, 'unknownUser', 'rest-api', fromAddr, recipients);
|
||||||
|
return res.status(404).json({ message: 'No valid inboxes – skipped' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send email
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: fromAddr,
|
||||||
|
to: validRecipients,
|
||||||
|
raw: emailBytes
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark as processed
|
||||||
|
await markEmailAsProcessed(domain, data.s3_key, 'true', 'rest-api', fromAddr, validRecipients);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
message: 'Email forwarded',
|
||||||
|
forwarded_to: validRecipients
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[${requestId}] Error in /process/${domain}: ${error.message}`);
|
||||||
|
await markEmailAsProcessed(domain, data.s3_key, 'error', 'rest-api', parser?.from?.value[0]?.address || `lambda@${domain}`, recipients);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stats endpoint
|
||||||
|
app.get('/stats/:domain', async (req, res) => {
|
||||||
|
const { domain } = req.params;
|
||||||
|
const auth = req.headers['authorization']; // Fixed: Use req.headers['authorization'] instead of req.headers.get
|
||||||
|
if (auth !== `Bearer ${API_TOKEN}`) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const bucket = domain.replace(/\./g, '-') + '-emails';
|
||||||
|
let total = 0;
|
||||||
|
const counts = { true: 0, unknownDomain: 0, unknownUser: 0, noRecipients: 0, error: 0 };
|
||||||
|
const details = { unknownDomain: [], unknownUser: [], noRecipients: [], error: [] };
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch statuses from database
|
||||||
|
const { rows: domainStatuses } = await pool.query(
|
||||||
|
'SELECT s3_key, status, from_addr, to_addrs FROM email_statuses WHERE domain = $1',
|
||||||
|
[domain]
|
||||||
|
);
|
||||||
|
|
||||||
|
const statusMap = domainStatuses.reduce((acc, row) => {
|
||||||
|
acc[row.s3_key] = { status: row.status, from: row.from_addr, to: row.to_addrs || [] };
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
// List S3 objects
|
||||||
|
let continuationToken;
|
||||||
|
do {
|
||||||
|
const params = { Bucket: bucket, ContinuationToken: continuationToken };
|
||||||
|
const data = await s3Client.listObjectsV2(params).promise();
|
||||||
|
continuationToken = data.NextContinuationToken;
|
||||||
|
|
||||||
|
for (const obj of data.Contents || []) {
|
||||||
|
const key = obj.Key;
|
||||||
|
total += 1;
|
||||||
|
|
||||||
|
const statusInfo = statusMap[key] || { status: 'none' };
|
||||||
|
const status = statusInfo.status;
|
||||||
|
if (status in counts) {
|
||||||
|
counts[status] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status in details) {
|
||||||
|
try {
|
||||||
|
const objData = await s3Client.getObject({ Bucket: bucket, Key: key }).promise();
|
||||||
|
const parser = new MailParser();
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
parser.on('error', reject);
|
||||||
|
parser.on('end', resolve);
|
||||||
|
parser.write(objData.Body);
|
||||||
|
parser.end();
|
||||||
|
});
|
||||||
|
const fromAddr = parser.from?.value[0]?.address || null;
|
||||||
|
const toAddrs = [
|
||||||
|
...(parser.to?.value || []),
|
||||||
|
...(parser.cc?.value || []),
|
||||||
|
...(parser.bcc?.value || [])
|
||||||
|
].map(addr => addr.address).filter(Boolean);
|
||||||
|
details[status].push({
|
||||||
|
key,
|
||||||
|
from: statusInfo.from || fromAddr,
|
||||||
|
to: statusInfo.to || toAddrs
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error parsing ${bucket}/${key}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (continuationToken);
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
domain,
|
||||||
|
total_messages: total,
|
||||||
|
successful: counts.true,
|
||||||
|
wrong_domain: counts.unknownDomain,
|
||||||
|
unknown_user: counts.unknownUser,
|
||||||
|
no_recipients: counts.noRecipients,
|
||||||
|
errors: counts.error,
|
||||||
|
details
|
||||||
|
};
|
||||||
|
logger.info(`Stats for ${domain}: ${JSON.stringify(result)}`);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error in /stats/${domain}: ${error.message}`);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
app.listen(5000, '0.0.0.0', () => {
|
||||||
|
logger.info('Server running on http://0.0.0.0:5000');
|
||||||
|
});
|
||||||
359
email_api/email_api/app.py
Normal file
359
email_api/email_api/app.py
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
import sys
|
||||||
|
from flask import Flask, request, jsonify
|
||||||
|
import smtplib
|
||||||
|
import base64
|
||||||
|
import gzip
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import boto3
|
||||||
|
from email.parser import BytesParser
|
||||||
|
from email.policy import default
|
||||||
|
from email.utils import getaddresses
|
||||||
|
import requests
|
||||||
|
|
||||||
|
if sys.version_info < (3, 12):
|
||||||
|
raise RuntimeError("Python 3.12 oder höher erforderlich")
|
||||||
|
|
||||||
|
# --- Logging mit Timestamp ---
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s %(levelname)s %(message)s",
|
||||||
|
datefmt="%Y-%m-%d %H:%M:%S"
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
load_dotenv = None
|
||||||
|
try:
|
||||||
|
from dotenv import load_dotenv as _ld
|
||||||
|
load_dotenv = _ld
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if load_dotenv:
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
SMTP_HOST = "localhost"
|
||||||
|
SMTP_PORT = 25
|
||||||
|
API_TOKEN = os.environ.get('API_TOKEN')
|
||||||
|
AWS_REGION = os.environ.get('AWS_REGION', 'us-east-1')
|
||||||
|
API_KEY = os.environ['MAILCOW_API_KEY']
|
||||||
|
MAILCOW_API = os.environ['MAILCOW_API']
|
||||||
|
|
||||||
|
s3_client = boto3.client('s3', region_name=AWS_REGION)
|
||||||
|
def domain_exists(domain):
|
||||||
|
"""
|
||||||
|
Prüft per /get/domain/all, ob `domain` im System ist.
|
||||||
|
"""
|
||||||
|
url = f"{MAILCOW_API}/get/domain/all"
|
||||||
|
headers = {'X-API-Key': API_KEY}
|
||||||
|
resp = requests.get(url, headers=headers, timeout=5)
|
||||||
|
resp.raise_for_status()
|
||||||
|
domains = resp.json()
|
||||||
|
return any(d.get('domain_name', '').lower() == domain.lower() for d in domains)
|
||||||
|
|
||||||
|
def inbox_exists(domain, local_part):
|
||||||
|
"""
|
||||||
|
Liefert True, wenn domain im System ist UND local_part@domain ein Postfach hat.
|
||||||
|
"""
|
||||||
|
# 1) Domain-Check
|
||||||
|
if not domain_exists(domain):
|
||||||
|
logger.info(f"Domain '{domain}' unknown – skip mailbox lookup")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 2) Nur dann Mailbox-Listing holen
|
||||||
|
url = f"{MAILCOW_API}/get/mailbox/all/{domain}"
|
||||||
|
headers = {'X-API-Key': API_KEY}
|
||||||
|
resp = requests.get(url, headers=headers, timeout=5)
|
||||||
|
resp.raise_for_status()
|
||||||
|
mailboxes = resp.json()
|
||||||
|
return any(m.get('local_part', '').lower() == local_part.lower() for m in mailboxes)
|
||||||
|
|
||||||
|
def mark_email_as_processed(bucket, key, status, processor='rest-api'):
|
||||||
|
"""Setzt processed-Metadaten auf einen beliebigen Status."""
|
||||||
|
try:
|
||||||
|
s3_client.copy_object(
|
||||||
|
Bucket=bucket,
|
||||||
|
Key=key,
|
||||||
|
CopySource={'Bucket': bucket, 'Key': key},
|
||||||
|
Metadata={
|
||||||
|
'processed': status,
|
||||||
|
'processed_timestamp': str(int(time.time())),
|
||||||
|
'processor': processor
|
||||||
|
},
|
||||||
|
MetadataDirective='REPLACE'
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Markieren {bucket}/{key}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@app.route('/stats/<domain>', methods=['GET'])
|
||||||
|
def stats_domain(domain):
|
||||||
|
# Auth
|
||||||
|
auth = request.headers.get('Authorization')
|
||||||
|
if auth != f'Bearer {API_TOKEN}':
|
||||||
|
return jsonify({'error': 'Unauthorized'}), 401
|
||||||
|
|
||||||
|
bucket = domain.replace('.', '-') + '-emails'
|
||||||
|
paginator = s3_client.get_paginator('list_objects_v2')
|
||||||
|
|
||||||
|
total = 0
|
||||||
|
counts = {
|
||||||
|
'true': 0,
|
||||||
|
'unknownDomain': 0,
|
||||||
|
'unknownUser': 0
|
||||||
|
}
|
||||||
|
details = {
|
||||||
|
'unknownDomain': [],
|
||||||
|
'unknownUser': []
|
||||||
|
}
|
||||||
|
|
||||||
|
for page in paginator.paginate(Bucket=bucket):
|
||||||
|
for obj in page.get('Contents', []):
|
||||||
|
key = obj['Key']
|
||||||
|
total += 1
|
||||||
|
|
||||||
|
head = s3_client.head_object(Bucket=bucket, Key=key)
|
||||||
|
meta = head.get('Metadata', {})
|
||||||
|
status = meta.get('processed', 'none')
|
||||||
|
if status in counts:
|
||||||
|
counts[status] += 1
|
||||||
|
else:
|
||||||
|
# wir ignorieren andere Status
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Für unknownDomain und unknownUser zusätzlich E-Mail parsen
|
||||||
|
if status in ('unknownDomain', 'unknownUser'):
|
||||||
|
body = s3_client.get_object(Bucket=bucket, Key=key)['Body'].read()
|
||||||
|
try:
|
||||||
|
msg = BytesParser(policy=default).parsebytes(body)
|
||||||
|
from_addr = getaddresses(msg.get_all('from', []))[0][1] if msg.get_all('from') else None
|
||||||
|
to_addrs = [addr for _n, addr in getaddresses(msg.get_all('to', []))]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Parsen {bucket}/{key}: {e}")
|
||||||
|
from_addr = None
|
||||||
|
to_addrs = []
|
||||||
|
details[status].append({
|
||||||
|
'key': key,
|
||||||
|
'from': from_addr,
|
||||||
|
'to': to_addrs
|
||||||
|
})
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'domain': domain,
|
||||||
|
'total_messages': total,
|
||||||
|
'successful': counts['true'],
|
||||||
|
'wrong_domain': counts['unknownDomain'],
|
||||||
|
'unknown_user': counts['unknownUser'],
|
||||||
|
'details': details
|
||||||
|
}
|
||||||
|
logger.info(f"Stats for {domain}: {result}")
|
||||||
|
return jsonify(result), 200
|
||||||
|
|
||||||
|
@app.route('/process/<domain>', methods=['POST'])
|
||||||
|
def process_email(domain):
|
||||||
|
auth = request.headers.get('Authorization')
|
||||||
|
if auth != f'Bearer {API_TOKEN}':
|
||||||
|
return jsonify({'error': 'Unauthorized'}), 401
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
return jsonify({'error': 'Invalid payload'}), 400
|
||||||
|
|
||||||
|
request_id = data.get('request_id', 'no-request-id')
|
||||||
|
|
||||||
|
payload_summary = {
|
||||||
|
k: (len(v) if k == 'email_content' else v)
|
||||||
|
for k, v in data.items()
|
||||||
|
if k != 'email_content' or isinstance(v, (str, bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"[{request_id}] INCOMING POST /process/{domain}: "
|
||||||
|
f"payload_summary={payload_summary}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 1) E-Mail decodieren und parsen wie gehabt
|
||||||
|
content = data.get('email_content')
|
||||||
|
compressed = data.get('compressed', False)
|
||||||
|
raw = base64.b64decode(content)
|
||||||
|
email_bytes = gzip.decompress(raw) if compressed else raw
|
||||||
|
|
||||||
|
msg = BytesParser(policy=default).parsebytes(email_bytes)
|
||||||
|
from_addr = getaddresses(msg.get_all('from', []))[0][1] if msg.get_all('from') else f'lambda@{domain}'
|
||||||
|
recipients = []
|
||||||
|
for hdr in ('to','cc','bcc'):
|
||||||
|
recipients += [addr for _n, addr in getaddresses(msg.get_all(hdr, []))]
|
||||||
|
|
||||||
|
if not recipients:
|
||||||
|
return jsonify({'error': 'No recipients'}), 400
|
||||||
|
|
||||||
|
# 2) Filter: nur Postfächer der angefragten Domain, die auch existieren
|
||||||
|
valid_recipients = []
|
||||||
|
for addr in recipients:
|
||||||
|
try:
|
||||||
|
local, dom = addr.split('@', 1)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if dom.lower() != domain.lower():
|
||||||
|
# andere Domain: überspringen
|
||||||
|
continue
|
||||||
|
if inbox_exists(domain, local):
|
||||||
|
valid_recipients.append(addr)
|
||||||
|
else:
|
||||||
|
logger.info(f"Skipping non-existent inbox: {addr}")
|
||||||
|
|
||||||
|
if not valid_recipients:
|
||||||
|
logger.info(f"[{request_id}] Keine gültigen Inboxes für {domain} – skip.")
|
||||||
|
return jsonify({'message': 'No valid inboxes – skipped'}), 404
|
||||||
|
|
||||||
|
# 3) Senden an die gefilterten Adressen
|
||||||
|
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as smtp:
|
||||||
|
smtp.sendmail(from_addr, valid_recipients, email_bytes)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'message': 'Email forwarded',
|
||||||
|
'forwarded_to': valid_recipients
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/retry/<domain>', methods=['GET'])
|
||||||
|
def retry_domain_emails(domain):
|
||||||
|
auth = request.headers.get('Authorization')
|
||||||
|
if auth != f'Bearer {API_TOKEN}':
|
||||||
|
return jsonify({'error': 'Unauthorized'}), 401
|
||||||
|
|
||||||
|
# 1) Domain-Check ganz am Anfang
|
||||||
|
if not domain_exists(domain):
|
||||||
|
logger.info(f"Retry aborted: unknown domain '{domain}'")
|
||||||
|
return jsonify({'error': f"Unknown domain '{domain}'"}), 404
|
||||||
|
|
||||||
|
bucket = domain.replace('.', '-') + '-emails'
|
||||||
|
paginator = s3_client.get_paginator('list_objects_v2')
|
||||||
|
|
||||||
|
# 2) alle unprocessed Keys sammeln
|
||||||
|
unprocessed = []
|
||||||
|
for page in paginator.paginate(Bucket=bucket):
|
||||||
|
for obj in page.get('Contents', []):
|
||||||
|
head = s3_client.head_object(Bucket=bucket, Key=obj['Key'])
|
||||||
|
if head.get('Metadata', {}).get('processed') != 'true':
|
||||||
|
unprocessed.append(obj['Key'])
|
||||||
|
|
||||||
|
request_id = f"retry-{domain}-{int(time.time())}"
|
||||||
|
logger.info(f"[{request_id}] RETRY for domain={domain}, keys={unprocessed}")
|
||||||
|
|
||||||
|
results = {'processed': [], 'failed': []}
|
||||||
|
|
||||||
|
for key in unprocessed:
|
||||||
|
try:
|
||||||
|
body = s3_client.get_object(Bucket=bucket, Key=key)['Body'].read()
|
||||||
|
msg = BytesParser(policy=default).parsebytes(body)
|
||||||
|
from_addr = (
|
||||||
|
getaddresses(msg.get_all('from', []))[0][1]
|
||||||
|
if msg.get_all('from') else f'retry@{domain}'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sammle alle To/Cc/Bcc
|
||||||
|
recipients = []
|
||||||
|
for hdr in ('to', 'cc', 'bcc'):
|
||||||
|
recipients += [addr for _n, addr in getaddresses(msg.get_all(hdr, []))]
|
||||||
|
|
||||||
|
if not recipients:
|
||||||
|
# gar keine Adressen → überspringen
|
||||||
|
mark_email_as_processed(bucket, key, 'unknownDomain')
|
||||||
|
results['processed'].append(key)
|
||||||
|
results['failed'].append({
|
||||||
|
'key': key,
|
||||||
|
'status': 'unknownDomain',
|
||||||
|
'reason': 'no recipients'
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 3) Domain-Match: nur Mails, die an die angefragte Domain adressiert sind
|
||||||
|
domains_in_mail = {addr.split('@')[-1].lower() for addr in recipients if '@' in addr}
|
||||||
|
if domain.lower() not in domains_in_mail:
|
||||||
|
mark_email_as_processed(bucket, key, 'unknownDomain')
|
||||||
|
results['processed'].append(key)
|
||||||
|
results['failed'].append({
|
||||||
|
'key': key,
|
||||||
|
'status': 'unknownDomain',
|
||||||
|
'from': from_addr,
|
||||||
|
'to': recipients
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 4) Inbox-Check: nur existierende Postfächer zulassen
|
||||||
|
valid_recipients = []
|
||||||
|
for addr in recipients:
|
||||||
|
try:
|
||||||
|
local, dom = addr.split('@', 1)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if dom.lower() == domain.lower() and inbox_exists(domain, local):
|
||||||
|
valid_recipients.append(addr)
|
||||||
|
else:
|
||||||
|
logger.info(f"Skipping non-existent inbox: {addr}")
|
||||||
|
|
||||||
|
if not valid_recipients:
|
||||||
|
mark_email_as_processed(bucket, key, 'unknownUser')
|
||||||
|
results['processed'].append(key)
|
||||||
|
results['failed'].append({
|
||||||
|
'key': key,
|
||||||
|
'status': 'unknownUser',
|
||||||
|
'from': from_addr,
|
||||||
|
'to': recipients
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 5) Versand an die validierten Adressen
|
||||||
|
try:
|
||||||
|
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as smtp:
|
||||||
|
smtp.sendmail(from_addr, valid_recipients, body)
|
||||||
|
mark_email_as_processed(bucket, key, 'true')
|
||||||
|
results['processed'].append(key)
|
||||||
|
|
||||||
|
except smtplib.SMTPRecipientsRefused as e:
|
||||||
|
# falls Mailcow einzelne Adressen ablehnt
|
||||||
|
mark_email_as_processed(bucket, key, 'unknownUser')
|
||||||
|
refused = {
|
||||||
|
addr: {'code': code, 'message': msg.decode('utf-8','ignore') if isinstance(msg, bytes) else str(msg)}
|
||||||
|
for addr, (code, msg) in e.recipients.items()
|
||||||
|
}
|
||||||
|
results['processed'].append(key)
|
||||||
|
results['failed'].append({
|
||||||
|
'key': key,
|
||||||
|
'status': 'unknownUser',
|
||||||
|
'from': from_addr,
|
||||||
|
'to': valid_recipients,
|
||||||
|
'refused': refused
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# alle anderen SMTP-Fehler behandeln wir als unknownDomain
|
||||||
|
mark_email_as_processed(bucket, key, 'unknownDomain')
|
||||||
|
results['processed'].append(key)
|
||||||
|
results['failed'].append({
|
||||||
|
'key': key,
|
||||||
|
'status': 'unknownDomain',
|
||||||
|
'from': from_addr,
|
||||||
|
'to': valid_recipients,
|
||||||
|
'error': str(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Parsing- oder S3-Fehler
|
||||||
|
results['failed'].append({'key': key, 'error': str(e)})
|
||||||
|
|
||||||
|
return jsonify(results), 200
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/health', methods=['GET'])
|
||||||
|
def health_check():
|
||||||
|
return jsonify({'status': 'OK'}), 200
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='0.0.0.0', port=5000)
|
||||||
15
email_api/email_api/package.json
Normal file
15
email_api/email_api/package.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "email-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.19.2",
|
||||||
|
"aws-sdk": "^2.1650.0",
|
||||||
|
"nodemailer": "^6.9.14",
|
||||||
|
"mailparser": "^3.7.1",
|
||||||
|
"js-base64": "^3.7.7",
|
||||||
|
"winston": "^3.13.1",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"pg": "^8.12.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
email_api/init.sql
Normal file
11
email_api/init.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
CREATE TABLE email_statuses (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
domain VARCHAR(255) NOT NULL,
|
||||||
|
s3_key VARCHAR(1024) NOT NULL,
|
||||||
|
status VARCHAR(50) NOT NULL,
|
||||||
|
timestamp BIGINT NOT NULL,
|
||||||
|
processor VARCHAR(50) NOT NULL,
|
||||||
|
from_addr TEXT,
|
||||||
|
to_addrs TEXT[],
|
||||||
|
UNIQUE (domain, s3_key)
|
||||||
|
);
|
||||||
@@ -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"
|
||||||
|
|||||||
1535
haikydb_backup.sql
Normal file
1535
haikydb_backup.sql
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,62 +0,0 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
|
|
||||||
postgres:
|
|
||||||
container_name: postgres_keycloak
|
|
||||||
image: postgres:15.7-alpine3.19
|
|
||||||
volumes:
|
|
||||||
- postgres_volume:/var/lib/postgresql/data
|
|
||||||
# - ./pg_hba.conf:/var/lib/postgresql/data/pg_hba.conf
|
|
||||||
environment:
|
|
||||||
POSTGRES_DB: ${POSTGRES_DB}
|
|
||||||
POSTGRES_USER: ${POSTGRES_USER}
|
|
||||||
POSTGRES_PASSWORD: "test1234"
|
|
||||||
# ports:
|
|
||||||
#- "2345:5432"
|
|
||||||
networks:
|
|
||||||
- keycloak
|
|
||||||
|
|
||||||
auth:
|
|
||||||
container_name: keycloak
|
|
||||||
image: quay.io/keycloak/keycloak:23.0.7
|
|
||||||
# restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
environment:
|
|
||||||
- KC_DB=postgres
|
|
||||||
- KC_DB_URL_HOST=${DB_HOST}
|
|
||||||
- KC_DB_URL_DATABASE=${POSTGRES_DB}
|
|
||||||
- KC_DB_USERNAME=${POSTGRES_USER}
|
|
||||||
- KC_DB_PASSWORD=test1234
|
|
||||||
- KC_PROXY=edge
|
|
||||||
- KC_HOSTNAME=${HOSTNAME}
|
|
||||||
- KC_HOSTNAME_ADMIN=${HOSTNAME}
|
|
||||||
# - KC_TRANSACTION_XA_ENABLED=false
|
|
||||||
- KC_METRICS_ENABLED=true
|
|
||||||
- KC_HEALTH_ENABLED=true
|
|
||||||
- KC_HOSTNAME_STRICT=false
|
|
||||||
- KC_HTTP_ENABLED=true
|
|
||||||
- KC_HOSTNAME_STRICT_HTTPS=false
|
|
||||||
# - PROXY_ADDRESS_FORWARDING=true
|
|
||||||
- KC_LOG_LEVEL=INFO
|
|
||||||
depends_on:
|
|
||||||
- postgres
|
|
||||||
# entrypoint: ["/opt/keycloak/wait-for-postgres.sh", "postgres_keycloak", "/opt/keycloak/bin/kc.sh", "start"]
|
|
||||||
# entrypoint: ["/opt/keycloak/bin/kc.sh", "start", "--db-password='test1234'"]
|
|
||||||
entrypoint: ["/opt/keycloak/bin/kc.sh", "start"]
|
|
||||||
volumes:
|
|
||||||
- ./auth/import:/opt/keycloak/data/import
|
|
||||||
- ./keywind.jar:/opt/keycloak/providers/keywind.jar
|
|
||||||
- ./redirect-uri-authenticator-1.0.0.jar:/opt/keycloak/providers/redirect-uri-authenticator-1.0.0.jar
|
|
||||||
- ./wait-for-postgres.sh:/opt/keycloak/wait-for-postgres.sh
|
|
||||||
networks:
|
|
||||||
- keycloak
|
|
||||||
|
|
||||||
networks:
|
|
||||||
keycloak:
|
|
||||||
external: true
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres_volume:
|
|
||||||
external: true
|
|
||||||
689
mailcow-configs/docker-compose.yml
Normal file
689
mailcow-configs/docker-compose.yml
Normal file
@@ -0,0 +1,689 @@
|
|||||||
|
services:
|
||||||
|
|
||||||
|
unbound-mailcow:
|
||||||
|
image: ghcr.io/mailcow/unbound:1.24
|
||||||
|
environment:
|
||||||
|
- TZ=${TZ}
|
||||||
|
- SKIP_UNBOUND_HEALTHCHECK=${SKIP_UNBOUND_HEALTHCHECK:-n}
|
||||||
|
volumes:
|
||||||
|
- ./data/hooks/unbound:/hooks:Z
|
||||||
|
- ./data/conf/unbound/unbound.conf:/etc/unbound/unbound.conf:ro,Z
|
||||||
|
restart: always
|
||||||
|
tty: true
|
||||||
|
networks:
|
||||||
|
mailcow-network:
|
||||||
|
ipv4_address: ${IPV4_NETWORK:-172.22.1}.254
|
||||||
|
aliases:
|
||||||
|
- unbound
|
||||||
|
|
||||||
|
mysql-mailcow:
|
||||||
|
image: mariadb:10.11
|
||||||
|
depends_on:
|
||||||
|
- unbound-mailcow
|
||||||
|
- netfilter-mailcow
|
||||||
|
stop_grace_period: 45s
|
||||||
|
volumes:
|
||||||
|
- mysql-vol-1:/var/lib/mysql/
|
||||||
|
- mysql-socket-vol-1:/var/run/mysqld/
|
||||||
|
- ./data/conf/mysql/:/etc/mysql/conf.d/:ro,Z
|
||||||
|
environment:
|
||||||
|
- TZ=${TZ}
|
||||||
|
- MYSQL_ROOT_PASSWORD=${DBROOT}
|
||||||
|
- MYSQL_DATABASE=${DBNAME}
|
||||||
|
- MYSQL_USER=${DBUSER}
|
||||||
|
- MYSQL_PASSWORD=${DBPASS}
|
||||||
|
- MYSQL_INITDB_SKIP_TZINFO=1
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "${SQL_PORT:-127.0.0.1:13306}:3306"
|
||||||
|
networks:
|
||||||
|
mailcow-network:
|
||||||
|
aliases:
|
||||||
|
- mysql
|
||||||
|
|
||||||
|
redis-mailcow:
|
||||||
|
image: redis:7.4.2-alpine
|
||||||
|
entrypoint: ["/bin/sh","/redis-conf.sh"]
|
||||||
|
volumes:
|
||||||
|
- redis-vol-1:/data/
|
||||||
|
- ./data/conf/redis/redis-conf.sh:/redis-conf.sh:z
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- netfilter-mailcow
|
||||||
|
ports:
|
||||||
|
- "${REDIS_PORT:-127.0.0.1:7654}:6379"
|
||||||
|
environment:
|
||||||
|
- TZ=${TZ}
|
||||||
|
- REDISPASS=${REDISPASS}
|
||||||
|
- REDISMASTERPASS=${REDISMASTERPASS:-}
|
||||||
|
sysctls:
|
||||||
|
- net.core.somaxconn=4096
|
||||||
|
networks:
|
||||||
|
mailcow-network:
|
||||||
|
ipv4_address: ${IPV4_NETWORK:-172.22.1}.249
|
||||||
|
aliases:
|
||||||
|
- redis
|
||||||
|
|
||||||
|
clamd-mailcow:
|
||||||
|
image: ghcr.io/mailcow/clamd:1.70
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
unbound-mailcow:
|
||||||
|
condition: service_healthy
|
||||||
|
dns:
|
||||||
|
- ${IPV4_NETWORK:-172.22.1}.254
|
||||||
|
environment:
|
||||||
|
- TZ=${TZ}
|
||||||
|
- SKIP_CLAMD=${SKIP_CLAMD:-n}
|
||||||
|
volumes:
|
||||||
|
- ./data/conf/clamav/:/etc/clamav/:Z
|
||||||
|
- clamd-db-vol-1:/var/lib/clamav
|
||||||
|
networks:
|
||||||
|
mailcow-network:
|
||||||
|
aliases:
|
||||||
|
- clamd
|
||||||
|
|
||||||
|
rspamd-mailcow:
|
||||||
|
image: ghcr.io/mailcow/rspamd:2.2
|
||||||
|
stop_grace_period: 30s
|
||||||
|
depends_on:
|
||||||
|
- dovecot-mailcow
|
||||||
|
- clamd-mailcow
|
||||||
|
environment:
|
||||||
|
- TZ=${TZ}
|
||||||
|
- IPV4_NETWORK=${IPV4_NETWORK:-172.22.1}
|
||||||
|
- IPV6_NETWORK=${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64}
|
||||||
|
- REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-}
|
||||||
|
- REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-}
|
||||||
|
- REDISPASS=${REDISPASS}
|
||||||
|
- SPAMHAUS_DQS_KEY=${SPAMHAUS_DQS_KEY:-}
|
||||||
|
volumes:
|
||||||
|
- ./data/hooks/rspamd:/hooks:Z
|
||||||
|
- ./data/conf/rspamd/custom/:/etc/rspamd/custom:z
|
||||||
|
- ./data/conf/rspamd/override.d/:/etc/rspamd/override.d:Z
|
||||||
|
- ./data/conf/rspamd/local.d/:/etc/rspamd/local.d:Z
|
||||||
|
- ./data/conf/rspamd/plugins.d/:/etc/rspamd/plugins.d:Z
|
||||||
|
- ./data/conf/rspamd/lua/:/etc/rspamd/lua/:ro,Z
|
||||||
|
- ./data/conf/rspamd/rspamd.conf.local:/etc/rspamd/rspamd.conf.local:Z
|
||||||
|
- ./data/conf/rspamd/rspamd.conf.override:/etc/rspamd/rspamd.conf.override:Z
|
||||||
|
- rspamd-vol-1:/var/lib/rspamd
|
||||||
|
restart: always
|
||||||
|
hostname: rspamd
|
||||||
|
dns:
|
||||||
|
- ${IPV4_NETWORK:-172.22.1}.254
|
||||||
|
networks:
|
||||||
|
mailcow-network:
|
||||||
|
aliases:
|
||||||
|
- rspamd
|
||||||
|
|
||||||
|
php-fpm-mailcow:
|
||||||
|
image: ghcr.io/mailcow/phpfpm:1.93
|
||||||
|
command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
|
||||||
|
depends_on:
|
||||||
|
- redis-mailcow
|
||||||
|
volumes:
|
||||||
|
- ./data/hooks/phpfpm:/hooks:Z
|
||||||
|
- ./data/web:/web:z
|
||||||
|
- ./data/conf/rspamd/dynmaps:/dynmaps:ro,z
|
||||||
|
- ./data/conf/rspamd/custom/:/rspamd_custom_maps:z
|
||||||
|
- ./data/conf/dovecot/auth/mailcowauth.php:/mailcowauth/mailcowauth.php:z
|
||||||
|
- ./data/web/inc/functions.inc.php:/mailcowauth/functions.inc.php:z
|
||||||
|
- ./data/web/inc/functions.auth.inc.php:/mailcowauth/functions.auth.inc.php:z
|
||||||
|
- ./data/web/inc/sessions.inc.php:/mailcowauth/sessions.inc.php:z
|
||||||
|
- ./data/web/inc/functions.mailbox.inc.php:/mailcowauth/functions.mailbox.inc.php:z
|
||||||
|
- ./data/web/inc/functions.ratelimit.inc.php:/mailcowauth/functions.ratelimit.inc.php:z
|
||||||
|
- ./data/web/inc/functions.acl.inc.php:/mailcowauth/functions.acl.inc.php:z
|
||||||
|
- rspamd-vol-1:/var/lib/rspamd
|
||||||
|
- mysql-socket-vol-1:/var/run/mysqld/
|
||||||
|
- ./data/conf/sogo/:/etc/sogo/:z
|
||||||
|
- ./data/conf/rspamd/meta_exporter:/meta_exporter:ro,z
|
||||||
|
- ./data/conf/phpfpm/crons:/crons:z
|
||||||
|
- ./data/conf/phpfpm/sogo-sso/:/etc/sogo-sso/:z
|
||||||
|
- ./data/conf/phpfpm/php-fpm.d/pools.conf:/usr/local/etc/php-fpm.d/z-pools.conf:Z
|
||||||
|
- ./data/conf/phpfpm/php-conf.d/opcache-recommended.ini:/usr/local/etc/php/conf.d/opcache-recommended.ini:Z
|
||||||
|
- ./data/conf/phpfpm/php-conf.d/upload.ini:/usr/local/etc/php/conf.d/upload.ini:Z
|
||||||
|
- ./data/conf/phpfpm/php-conf.d/other.ini:/usr/local/etc/php/conf.d/zzz-other.ini:Z
|
||||||
|
- ./data/conf/dovecot/global_sieve_before:/global_sieve/before:z
|
||||||
|
- ./data/conf/dovecot/global_sieve_after:/global_sieve/after:z
|
||||||
|
- ./data/assets/templates:/tpls:z
|
||||||
|
- ./data/conf/nginx/:/etc/nginx/conf.d/:z
|
||||||
|
dns:
|
||||||
|
- ${IPV4_NETWORK:-172.22.1}.254
|
||||||
|
environment:
|
||||||
|
- REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-}
|
||||||
|
- REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-}
|
||||||
|
- REDISPASS=${REDISPASS}
|
||||||
|
- LOG_LINES=${LOG_LINES:-9999}
|
||||||
|
- TZ=${TZ}
|
||||||
|
- DBNAME=${DBNAME}
|
||||||
|
- DBUSER=${DBUSER}
|
||||||
|
- DBPASS=${DBPASS}
|
||||||
|
- MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
|
||||||
|
- MAILCOW_PASS_SCHEME=${MAILCOW_PASS_SCHEME:-BLF-CRYPT}
|
||||||
|
- IMAP_PORT=${IMAP_PORT:-143}
|
||||||
|
- IMAPS_PORT=${IMAPS_PORT:-993}
|
||||||
|
- POP_PORT=${POP_PORT:-110}
|
||||||
|
- POPS_PORT=${POPS_PORT:-995}
|
||||||
|
- SIEVE_PORT=${SIEVE_PORT:-4190}
|
||||||
|
- IPV4_NETWORK=${IPV4_NETWORK:-172.22.1}
|
||||||
|
- IPV6_NETWORK=${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64}
|
||||||
|
- SUBMISSION_PORT=${SUBMISSION_PORT:-587}
|
||||||
|
- SMTPS_PORT=${SMTPS_PORT:-465}
|
||||||
|
- SMTP_PORT=${SMTP_PORT:-25}
|
||||||
|
- API_KEY=${API_KEY:-invalid}
|
||||||
|
- API_KEY_READ_ONLY=${API_KEY_READ_ONLY:-invalid}
|
||||||
|
- API_ALLOW_FROM=${API_ALLOW_FROM:-invalid}
|
||||||
|
- COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME:-mailcow-dockerized}
|
||||||
|
- SKIP_FTS=${SKIP_FTS:-y}
|
||||||
|
- SKIP_CLAMD=${SKIP_CLAMD:-n}
|
||||||
|
- SKIP_OLEFY=${SKIP_OLEFY:-n}
|
||||||
|
- SKIP_SOGO=${SKIP_SOGO:-n}
|
||||||
|
- ALLOW_ADMIN_EMAIL_LOGIN=${ALLOW_ADMIN_EMAIL_LOGIN:-n}
|
||||||
|
- MASTER=${MASTER:-y}
|
||||||
|
- DEV_MODE=${DEV_MODE:-n}
|
||||||
|
- DEMO_MODE=${DEMO_MODE:-n}
|
||||||
|
- WEBAUTHN_ONLY_TRUSTED_VENDORS=${WEBAUTHN_ONLY_TRUSTED_VENDORS:-n}
|
||||||
|
- CLUSTERMODE=${CLUSTERMODE:-}
|
||||||
|
- ADDITIONAL_SERVER_NAMES=${ADDITIONAL_SERVER_NAMES:-}
|
||||||
|
restart: always
|
||||||
|
labels:
|
||||||
|
ofelia.enabled: "true"
|
||||||
|
ofelia.job-exec.phpfpm_keycloak_sync.schedule: "@every 1m"
|
||||||
|
ofelia.job-exec.phpfpm_keycloak_sync.no-overlap: "true"
|
||||||
|
ofelia.job-exec.phpfpm_keycloak_sync.command: "/bin/bash -c \"php /crons/keycloak-sync.php || exit 0\""
|
||||||
|
ofelia.job-exec.phpfpm_ldap_sync.schedule: "@every 1m"
|
||||||
|
ofelia.job-exec.phpfpm_ldap_sync.no-overlap: "true"
|
||||||
|
ofelia.job-exec.phpfpm_ldap_sync.command: "/bin/bash -c \"php /crons/ldap-sync.php || exit 0\""
|
||||||
|
networks:
|
||||||
|
mailcow-network:
|
||||||
|
aliases:
|
||||||
|
- phpfpm
|
||||||
|
|
||||||
|
sogo-mailcow:
|
||||||
|
image: ghcr.io/mailcow/sogo:1.133
|
||||||
|
environment:
|
||||||
|
- DBNAME=${DBNAME}
|
||||||
|
- DBUSER=${DBUSER}
|
||||||
|
- DBPASS=${DBPASS}
|
||||||
|
- TZ=${TZ}
|
||||||
|
- LOG_LINES=${LOG_LINES:-9999}
|
||||||
|
- MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
|
||||||
|
- MAILCOW_PASS_SCHEME=${MAILCOW_PASS_SCHEME:-BLF-CRYPT}
|
||||||
|
- ACL_ANYONE=${ACL_ANYONE:-disallow}
|
||||||
|
- ALLOW_ADMIN_EMAIL_LOGIN=${ALLOW_ADMIN_EMAIL_LOGIN:-n}
|
||||||
|
- IPV4_NETWORK=${IPV4_NETWORK:-172.22.1}
|
||||||
|
- SOGO_EXPIRE_SESSION=${SOGO_EXPIRE_SESSION:-480}
|
||||||
|
- SKIP_SOGO=${SKIP_SOGO:-n}
|
||||||
|
- MASTER=${MASTER:-y}
|
||||||
|
- REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-}
|
||||||
|
- REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-}
|
||||||
|
- REDISPASS=${REDISPASS}
|
||||||
|
dns:
|
||||||
|
- ${IPV4_NETWORK:-172.22.1}.254
|
||||||
|
volumes:
|
||||||
|
- ./data/hooks/sogo:/hooks:Z
|
||||||
|
- ./data/conf/sogo/:/etc/sogo/:z
|
||||||
|
- ./data/web/inc/init_db.inc.php:/init_db.inc.php:z
|
||||||
|
- ./data/conf/sogo/custom-favicon.ico:/usr/lib/GNUstep/SOGo/WebServerResources/img/sogo.ico:z
|
||||||
|
- ./data/conf/sogo/custom-shortlogo.svg:/usr/lib/GNUstep/SOGo/WebServerResources/img/sogo-compact.svg:z
|
||||||
|
- ./data/conf/sogo/custom-fulllogo.svg:/usr/lib/GNUstep/SOGo/WebServerResources/img/sogo-full.svg:z
|
||||||
|
- ./data/conf/sogo/custom-fulllogo.png:/usr/lib/GNUstep/SOGo/WebServerResources/img/sogo-logo.png:z
|
||||||
|
- ./data/conf/sogo/custom-theme.js:/usr/lib/GNUstep/SOGo/WebServerResources/js/theme.js:z
|
||||||
|
- ./data/conf/sogo/custom-sogo.js:/usr/lib/GNUstep/SOGo/WebServerResources/js/custom-sogo.js:z
|
||||||
|
- mysql-socket-vol-1:/var/run/mysqld/
|
||||||
|
- sogo-web-vol-1:/sogo_web
|
||||||
|
- sogo-userdata-backup-vol-1:/sogo_backup
|
||||||
|
labels:
|
||||||
|
ofelia.enabled: "true"
|
||||||
|
ofelia.job-exec.sogo_sessions.schedule: "@every 1m"
|
||||||
|
ofelia.job-exec.sogo_sessions.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-tool -v expire-sessions $${SOGO_EXPIRE_SESSION} || exit 0\""
|
||||||
|
ofelia.job-exec.sogo_ealarms.schedule: "@every 1m"
|
||||||
|
ofelia.job-exec.sogo_ealarms.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-ealarms-notify -p /etc/sogo/cron.creds || exit 0\""
|
||||||
|
ofelia.job-exec.sogo_eautoreply.schedule: "@every 5m"
|
||||||
|
ofelia.job-exec.sogo_eautoreply.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-tool update-autoreply -p /etc/sogo/cron.creds || exit 0\""
|
||||||
|
ofelia.job-exec.sogo_backup.schedule: "@every 24h"
|
||||||
|
ofelia.job-exec.sogo_backup.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-tool backup /sogo_backup ALL || exit 0\""
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
mailcow-network:
|
||||||
|
ipv4_address: ${IPV4_NETWORK:-172.22.1}.248
|
||||||
|
aliases:
|
||||||
|
- sogo
|
||||||
|
|
||||||
|
dovecot-mailcow:
|
||||||
|
image: ghcr.io/mailcow/dovecot:2.33
|
||||||
|
depends_on:
|
||||||
|
- mysql-mailcow
|
||||||
|
- netfilter-mailcow
|
||||||
|
- redis-mailcow
|
||||||
|
dns:
|
||||||
|
- ${IPV4_NETWORK:-172.22.1}.254
|
||||||
|
cap_add:
|
||||||
|
- NET_BIND_SERVICE
|
||||||
|
volumes:
|
||||||
|
- ./data/hooks/dovecot:/hooks:Z
|
||||||
|
- ./data/conf/dovecot:/etc/dovecot:z
|
||||||
|
- ./data/assets/ssl:/etc/ssl/mail/:ro,z
|
||||||
|
- ./data/conf/sogo/:/etc/sogo/:z
|
||||||
|
- ./data/conf/phpfpm/sogo-sso/:/etc/phpfpm/:z
|
||||||
|
- vmail-vol-1:/var/vmail
|
||||||
|
- vmail-index-vol-1:/var/vmail_index
|
||||||
|
- crypt-vol-1:/mail_crypt/
|
||||||
|
- ./data/conf/rspamd/custom/:/etc/rspamd/custom:z
|
||||||
|
- ./data/assets/templates:/templates:z
|
||||||
|
- rspamd-vol-1:/var/lib/rspamd
|
||||||
|
- mysql-socket-vol-1:/var/run/mysqld/
|
||||||
|
environment:
|
||||||
|
- DOVECOT_MASTER_USER=${DOVECOT_MASTER_USER:-}
|
||||||
|
- DOVECOT_MASTER_PASS=${DOVECOT_MASTER_PASS:-}
|
||||||
|
- MAILCOW_REPLICA_IP=${MAILCOW_REPLICA_IP:-}
|
||||||
|
- DOVEADM_REPLICA_PORT=${DOVEADM_REPLICA_PORT:-}
|
||||||
|
- LOG_LINES=${LOG_LINES:-9999}
|
||||||
|
- DBNAME=${DBNAME}
|
||||||
|
- DBUSER=${DBUSER}
|
||||||
|
- DBPASS=${DBPASS}
|
||||||
|
- TZ=${TZ}
|
||||||
|
- MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
|
||||||
|
- MAILCOW_PASS_SCHEME=${MAILCOW_PASS_SCHEME:-BLF-CRYPT}
|
||||||
|
- IPV4_NETWORK=${IPV4_NETWORK:-172.22.1}
|
||||||
|
- ALLOW_ADMIN_EMAIL_LOGIN=${ALLOW_ADMIN_EMAIL_LOGIN:-n}
|
||||||
|
- MAILDIR_GC_TIME=${MAILDIR_GC_TIME:-7200}
|
||||||
|
- ACL_ANYONE=${ACL_ANYONE:-disallow}
|
||||||
|
- SKIP_FTS=${SKIP_FTS:-y}
|
||||||
|
- FTS_HEAP=${FTS_HEAP:-512}
|
||||||
|
- FTS_PROCS=${FTS_PROCS:-3}
|
||||||
|
- MAILDIR_SUB=${MAILDIR_SUB:-}
|
||||||
|
- MASTER=${MASTER:-y}
|
||||||
|
- REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-}
|
||||||
|
- REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-}
|
||||||
|
- REDISPASS=${REDISPASS}
|
||||||
|
- COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME:-mailcow-dockerized}
|
||||||
|
ports:
|
||||||
|
- "${DOVEADM_PORT:-127.0.0.1:19991}:12345"
|
||||||
|
- "${IMAP_PORT:-143}:143"
|
||||||
|
- "${IMAPS_PORT:-993}:993"
|
||||||
|
- "${POP_PORT:-110}:110"
|
||||||
|
- "${POPS_PORT:-995}:995"
|
||||||
|
- "${SIEVE_PORT:-4190}:4190"
|
||||||
|
restart: always
|
||||||
|
tty: true
|
||||||
|
labels:
|
||||||
|
ofelia.enabled: "true"
|
||||||
|
ofelia.job-exec.dovecot_imapsync_runner.schedule: "@every 1m"
|
||||||
|
ofelia.job-exec.dovecot_imapsync_runner.no-overlap: "true"
|
||||||
|
ofelia.job-exec.dovecot_imapsync_runner.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu nobody /usr/local/bin/imapsync_runner.pl || exit 0\""
|
||||||
|
ofelia.job-exec.dovecot_trim_logs.schedule: "@every 1m"
|
||||||
|
ofelia.job-exec.dovecot_trim_logs.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu vmail /usr/local/bin/trim_logs.sh || exit 0\""
|
||||||
|
ofelia.job-exec.dovecot_quarantine.schedule: "@every 20m"
|
||||||
|
ofelia.job-exec.dovecot_quarantine.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu vmail /usr/local/bin/quarantine_notify.py || exit 0\""
|
||||||
|
ofelia.job-exec.dovecot_clean_q_aged.schedule: "@every 24h"
|
||||||
|
ofelia.job-exec.dovecot_clean_q_aged.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu vmail /usr/local/bin/clean_q_aged.sh || exit 0\""
|
||||||
|
ofelia.job-exec.dovecot_maildir_gc.schedule: "@every 30m"
|
||||||
|
ofelia.job-exec.dovecot_maildir_gc.command: "/bin/bash -c \"source /source_env.sh ; /usr/local/bin/gosu vmail /usr/local/bin/maildir_gc.sh\""
|
||||||
|
ofelia.job-exec.dovecot_sarules.schedule: "@every 24h"
|
||||||
|
ofelia.job-exec.dovecot_sarules.command: "/bin/bash -c \"/usr/local/bin/sa-rules.sh\""
|
||||||
|
ofelia.job-exec.dovecot_fts.schedule: "@every 24h"
|
||||||
|
ofelia.job-exec.dovecot_fts.command: "/bin/bash -c \"/usr/local/bin/gosu vmail /usr/local/bin/optimize-fts.sh\""
|
||||||
|
ofelia.job-exec.dovecot_repl_health.schedule: "@every 5m"
|
||||||
|
ofelia.job-exec.dovecot_repl_health.command: "/bin/bash -c \"/usr/local/bin/gosu vmail /usr/local/bin/repl_health.sh\""
|
||||||
|
ulimits:
|
||||||
|
nproc: 65535
|
||||||
|
nofile:
|
||||||
|
soft: 20000
|
||||||
|
hard: 40000
|
||||||
|
networks:
|
||||||
|
mailcow-network:
|
||||||
|
ipv4_address: ${IPV4_NETWORK:-172.22.1}.250
|
||||||
|
aliases:
|
||||||
|
- dovecot
|
||||||
|
|
||||||
|
postfix-mailcow:
|
||||||
|
image: ghcr.io/mailcow/postfix:1.80
|
||||||
|
depends_on:
|
||||||
|
mysql-mailcow:
|
||||||
|
condition: service_started
|
||||||
|
unbound-mailcow:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- ./data/hooks/postfix:/hooks:Z
|
||||||
|
- ./data/conf/postfix:/opt/postfix/conf:z
|
||||||
|
- ./data/assets/ssl:/etc/ssl/mail/:ro,z
|
||||||
|
- postfix-vol-1:/var/spool/postfix
|
||||||
|
- crypt-vol-1:/var/lib/zeyple
|
||||||
|
- rspamd-vol-1:/var/lib/rspamd
|
||||||
|
- mysql-socket-vol-1:/var/run/mysqld/
|
||||||
|
environment:
|
||||||
|
- LOG_LINES=${LOG_LINES:-9999}
|
||||||
|
- TZ=${TZ}
|
||||||
|
- DBNAME=${DBNAME}
|
||||||
|
- DBUSER=${DBUSER}
|
||||||
|
- DBPASS=${DBPASS}
|
||||||
|
- REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-}
|
||||||
|
- REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-}
|
||||||
|
- REDISPASS=${REDISPASS}
|
||||||
|
- MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
|
||||||
|
- SPAMHAUS_DQS_KEY=${SPAMHAUS_DQS_KEY:-}
|
||||||
|
cap_add:
|
||||||
|
- NET_BIND_SERVICE
|
||||||
|
ports:
|
||||||
|
- "${SMTP_PORT:-25}:25"
|
||||||
|
- "${SMTPS_PORT:-465}:465"
|
||||||
|
- "${SUBMISSION_PORT:-587}:587"
|
||||||
|
restart: always
|
||||||
|
dns:
|
||||||
|
- ${IPV4_NETWORK:-172.22.1}.254
|
||||||
|
networks:
|
||||||
|
mailcow-network:
|
||||||
|
ipv4_address: ${IPV4_NETWORK:-172.22.1}.253
|
||||||
|
aliases:
|
||||||
|
- postfix
|
||||||
|
|
||||||
|
memcached-mailcow:
|
||||||
|
image: memcached:alpine
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- TZ=${TZ}
|
||||||
|
networks:
|
||||||
|
mailcow-network:
|
||||||
|
aliases:
|
||||||
|
- memcached
|
||||||
|
|
||||||
|
nginx-mailcow:
|
||||||
|
depends_on:
|
||||||
|
- redis-mailcow
|
||||||
|
- php-fpm-mailcow
|
||||||
|
- sogo-mailcow
|
||||||
|
- rspamd-mailcow
|
||||||
|
image: ghcr.io/mailcow/nginx:1.03
|
||||||
|
dns:
|
||||||
|
- ${IPV4_NETWORK:-172.22.1}.254
|
||||||
|
environment:
|
||||||
|
- HTTPS_PORT=${HTTPS_PORT:-8443}
|
||||||
|
- HTTP_PORT=${HTTP_PORT:-8080}
|
||||||
|
- MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
|
||||||
|
- ADDITIONAL_SERVER_NAMES=${ADDITIONAL_SERVER_NAMES:-}
|
||||||
|
- TZ=${TZ}
|
||||||
|
- SKIP_SOGO=${SKIP_SOGO:-n}
|
||||||
|
- SKIP_RSPAMD=${SKIP_RSPAMD:-n}
|
||||||
|
- DISABLE_IPv6=${DISABLE_IPv6:-n}
|
||||||
|
- HTTP_REDIRECT=${HTTP_REDIRECT:-n}
|
||||||
|
- PHPFPMHOST=${PHPFPMHOST:-}
|
||||||
|
- SOGOHOST=${SOGOHOST:-}
|
||||||
|
- RSPAMDHOST=${RSPAMDHOST:-}
|
||||||
|
- REDISHOST=${REDISHOST:-}
|
||||||
|
- IPV4_NETWORK=${IPV4_NETWORK:-172.22.1}
|
||||||
|
- NGINX_USE_PROXY_PROTOCOL=${NGINX_USE_PROXY_PROTOCOL:-n}
|
||||||
|
- TRUSTED_PROXIES=${TRUSTED_PROXIES:-}
|
||||||
|
volumes:
|
||||||
|
- ./data/web:/web:ro,z
|
||||||
|
- ./data/conf/rspamd/dynmaps:/dynmaps:ro,z
|
||||||
|
- ./data/assets/ssl/:/etc/ssl/mail/:ro,z
|
||||||
|
- ./data/conf/nginx/:/etc/nginx/conf.d/:z
|
||||||
|
- ./data/conf/rspamd/meta_exporter:/meta_exporter:ro,z
|
||||||
|
- ./data/conf/dovecot/auth/mailcowauth.php:/mailcowauth/mailcowauth.php:z
|
||||||
|
- ./data/web/inc/functions.inc.php:/mailcowauth/functions.inc.php:z
|
||||||
|
- ./data/web/inc/functions.auth.inc.php:/mailcowauth/functions.auth.inc.php:z
|
||||||
|
- ./data/web/inc/sessions.inc.php:/mailcowauth/sessions.inc.php:z
|
||||||
|
- sogo-web-vol-1:/usr/lib/GNUstep/SOGo/
|
||||||
|
ports:
|
||||||
|
- "${HTTPS_BIND:-}:${HTTPS_PORT:-8443}:${HTTPS_PORT:-8443}"
|
||||||
|
- "${HTTP_BIND:-}:${HTTP_PORT:-8080}:${HTTP_PORT:-8080}"
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
mailcow-network:
|
||||||
|
aliases:
|
||||||
|
- nginx
|
||||||
|
mail_network: {}
|
||||||
|
|
||||||
|
|
||||||
|
acme-mailcow:
|
||||||
|
depends_on:
|
||||||
|
nginx-mailcow:
|
||||||
|
condition: service_started
|
||||||
|
unbound-mailcow:
|
||||||
|
condition: service_healthy
|
||||||
|
image: ghcr.io/mailcow/acme:1.92
|
||||||
|
dns:
|
||||||
|
- ${IPV4_NETWORK:-172.22.1}.254
|
||||||
|
environment:
|
||||||
|
- LOG_LINES=${LOG_LINES:-9999}
|
||||||
|
- ACME_CONTACT=${ACME_CONTACT:-}
|
||||||
|
- ADDITIONAL_SAN=${ADDITIONAL_SAN}
|
||||||
|
- AUTODISCOVER_SAN=${AUTODISCOVER_SAN:-y}
|
||||||
|
- MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
|
||||||
|
- DBNAME=${DBNAME}
|
||||||
|
- DBUSER=${DBUSER}
|
||||||
|
- DBPASS=${DBPASS}
|
||||||
|
- SKIP_LETS_ENCRYPT=${SKIP_LETS_ENCRYPT:-n}
|
||||||
|
- COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME:-mailcow-dockerized}
|
||||||
|
- DIRECTORY_URL=${DIRECTORY_URL:-}
|
||||||
|
- ENABLE_SSL_SNI=${ENABLE_SSL_SNI:-n}
|
||||||
|
- SKIP_IP_CHECK=${SKIP_IP_CHECK:-n}
|
||||||
|
- SKIP_HTTP_VERIFICATION=${SKIP_HTTP_VERIFICATION:-n}
|
||||||
|
- ONLY_MAILCOW_HOSTNAME=${ONLY_MAILCOW_HOSTNAME:-n}
|
||||||
|
- LE_STAGING=${LE_STAGING:-n}
|
||||||
|
- TZ=${TZ}
|
||||||
|
- REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-}
|
||||||
|
- REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-}
|
||||||
|
- REDISPASS=${REDISPASS}
|
||||||
|
- SNAT_TO_SOURCE=${SNAT_TO_SOURCE:-n}
|
||||||
|
- SNAT6_TO_SOURCE=${SNAT6_TO_SOURCE:-n}
|
||||||
|
volumes:
|
||||||
|
- ./data/web/.well-known/acme-challenge:/var/www/acme:z
|
||||||
|
- ./data/assets/ssl:/var/lib/acme/:z
|
||||||
|
- ./data/assets/ssl-example:/var/lib/ssl-example/:ro,Z
|
||||||
|
- mysql-socket-vol-1:/var/run/mysqld/
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
mailcow-network:
|
||||||
|
aliases:
|
||||||
|
- acme
|
||||||
|
|
||||||
|
netfilter-mailcow:
|
||||||
|
image: ghcr.io/mailcow/netfilter:1.61
|
||||||
|
stop_grace_period: 30s
|
||||||
|
restart: always
|
||||||
|
privileged: true
|
||||||
|
environment:
|
||||||
|
- TZ=${TZ}
|
||||||
|
- IPV4_NETWORK=${IPV4_NETWORK:-172.22.1}
|
||||||
|
- IPV6_NETWORK=${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64}
|
||||||
|
- SNAT_TO_SOURCE=${SNAT_TO_SOURCE:-n}
|
||||||
|
- SNAT6_TO_SOURCE=${SNAT6_TO_SOURCE:-n}
|
||||||
|
- REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-}
|
||||||
|
- REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-}
|
||||||
|
- REDISPASS=${REDISPASS}
|
||||||
|
- MAILCOW_REPLICA_IP=${MAILCOW_REPLICA_IP:-}
|
||||||
|
- DISABLE_NETFILTER_ISOLATION_RULE=${DISABLE_NETFILTER_ISOLATION_RULE:-n}
|
||||||
|
network_mode: "host"
|
||||||
|
volumes:
|
||||||
|
- /lib/modules:/lib/modules:ro
|
||||||
|
|
||||||
|
watchdog-mailcow:
|
||||||
|
image: ghcr.io/mailcow/watchdog:2.08
|
||||||
|
dns:
|
||||||
|
- ${IPV4_NETWORK:-172.22.1}.254
|
||||||
|
tmpfs:
|
||||||
|
- /tmp
|
||||||
|
volumes:
|
||||||
|
- rspamd-vol-1:/var/lib/rspamd
|
||||||
|
- mysql-socket-vol-1:/var/run/mysqld/
|
||||||
|
- postfix-vol-1:/var/spool/postfix
|
||||||
|
- ./data/assets/ssl:/etc/ssl/mail/:ro,z
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- postfix-mailcow
|
||||||
|
- dovecot-mailcow
|
||||||
|
- mysql-mailcow
|
||||||
|
- acme-mailcow
|
||||||
|
- redis-mailcow
|
||||||
|
environment:
|
||||||
|
- IPV6_NETWORK=${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64}
|
||||||
|
- LOG_LINES=${LOG_LINES:-9999}
|
||||||
|
- TZ=${TZ}
|
||||||
|
- DBNAME=${DBNAME}
|
||||||
|
- DBUSER=${DBUSER}
|
||||||
|
- DBPASS=${DBPASS}
|
||||||
|
- DBROOT=${DBROOT}
|
||||||
|
- USE_WATCHDOG=${USE_WATCHDOG:-n}
|
||||||
|
- WATCHDOG_NOTIFY_EMAIL=${WATCHDOG_NOTIFY_EMAIL:-}
|
||||||
|
- WATCHDOG_NOTIFY_BAN=${WATCHDOG_NOTIFY_BAN:-y}
|
||||||
|
- WATCHDOG_NOTIFY_START=${WATCHDOG_NOTIFY_START:-y}
|
||||||
|
- WATCHDOG_SUBJECT=${WATCHDOG_SUBJECT:-Watchdog ALERT}
|
||||||
|
- WATCHDOG_NOTIFY_WEBHOOK=${WATCHDOG_NOTIFY_WEBHOOK:-}
|
||||||
|
- WATCHDOG_NOTIFY_WEBHOOK_BODY=${WATCHDOG_NOTIFY_WEBHOOK_BODY:-}
|
||||||
|
- WATCHDOG_EXTERNAL_CHECKS=${WATCHDOG_EXTERNAL_CHECKS:-n}
|
||||||
|
- WATCHDOG_MYSQL_REPLICATION_CHECKS=${WATCHDOG_MYSQL_REPLICATION_CHECKS:-n}
|
||||||
|
- WATCHDOG_VERBOSE=${WATCHDOG_VERBOSE:-n}
|
||||||
|
- MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
|
||||||
|
- COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME:-mailcow-dockerized}
|
||||||
|
- IPV4_NETWORK=${IPV4_NETWORK:-172.22.1}
|
||||||
|
- IP_BY_DOCKER_API=${IP_BY_DOCKER_API:-0}
|
||||||
|
- CHECK_UNBOUND=${CHECK_UNBOUND:-1}
|
||||||
|
- SKIP_CLAMD=${SKIP_CLAMD:-n}
|
||||||
|
- SKIP_OLEFY=${SKIP_OLEFY:-n}
|
||||||
|
- SKIP_LETS_ENCRYPT=${SKIP_LETS_ENCRYPT:-n}
|
||||||
|
- SKIP_SOGO=${SKIP_SOGO:-n}
|
||||||
|
- HTTPS_PORT=${HTTPS_PORT:-8443}
|
||||||
|
- REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-}
|
||||||
|
- REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-}
|
||||||
|
- REDISPASS=${REDISPASS}
|
||||||
|
- EXTERNAL_CHECKS_THRESHOLD=${EXTERNAL_CHECKS_THRESHOLD:-1}
|
||||||
|
- NGINX_THRESHOLD=${NGINX_THRESHOLD:-5}
|
||||||
|
- UNBOUND_THRESHOLD=${UNBOUND_THRESHOLD:-5}
|
||||||
|
- REDIS_THRESHOLD=${REDIS_THRESHOLD:-5}
|
||||||
|
- MYSQL_THRESHOLD=${MYSQL_THRESHOLD:-5}
|
||||||
|
- MYSQL_REPLICATION_THRESHOLD=${MYSQL_REPLICATION_THRESHOLD:-1}
|
||||||
|
- SOGO_THRESHOLD=${SOGO_THRESHOLD:-3}
|
||||||
|
- POSTFIX_THRESHOLD=${POSTFIX_THRESHOLD:-8}
|
||||||
|
- CLAMD_THRESHOLD=${CLAMD_THRESHOLD:-15}
|
||||||
|
- DOVECOT_THRESHOLD=${DOVECOT_THRESHOLD:-12}
|
||||||
|
- DOVECOT_REPL_THRESHOLD=${DOVECOT_REPL_THRESHOLD:-20}
|
||||||
|
- PHPFPM_THRESHOLD=${PHPFPM_THRESHOLD:-5}
|
||||||
|
- RATELIMIT_THRESHOLD=${RATELIMIT_THRESHOLD:-1}
|
||||||
|
- FAIL2BAN_THRESHOLD=${FAIL2BAN_THRESHOLD:-1}
|
||||||
|
- ACME_THRESHOLD=${ACME_THRESHOLD:-1}
|
||||||
|
- RSPAMD_THRESHOLD=${RSPAMD_THRESHOLD:-5}
|
||||||
|
- OLEFY_THRESHOLD=${OLEFY_THRESHOLD:-5}
|
||||||
|
- MAILQ_THRESHOLD=${MAILQ_THRESHOLD:-20}
|
||||||
|
- MAILQ_CRIT=${MAILQ_CRIT:-30}
|
||||||
|
networks:
|
||||||
|
mailcow-network:
|
||||||
|
aliases:
|
||||||
|
- watchdog
|
||||||
|
|
||||||
|
dockerapi-mailcow:
|
||||||
|
image: ghcr.io/mailcow/dockerapi:2.11
|
||||||
|
security_opt:
|
||||||
|
- label=disable
|
||||||
|
restart: always
|
||||||
|
dns:
|
||||||
|
- ${IPV4_NETWORK:-172.22.1}.254
|
||||||
|
environment:
|
||||||
|
- DBROOT=${DBROOT}
|
||||||
|
- TZ=${TZ}
|
||||||
|
- REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-}
|
||||||
|
- REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-}
|
||||||
|
- REDISPASS=${REDISPASS}
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
networks:
|
||||||
|
mailcow-network:
|
||||||
|
aliases:
|
||||||
|
- dockerapi
|
||||||
|
|
||||||
|
olefy-mailcow:
|
||||||
|
image: ghcr.io/mailcow/olefy:1.15
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- TZ=${TZ}
|
||||||
|
- OLEFY_BINDADDRESS=0.0.0.0
|
||||||
|
- OLEFY_BINDPORT=10055
|
||||||
|
- OLEFY_TMPDIR=/tmp
|
||||||
|
- OLEFY_PYTHON_PATH=/usr/bin/python3
|
||||||
|
- OLEFY_OLEVBA_PATH=/usr/bin/olevba
|
||||||
|
- OLEFY_LOGLVL=20
|
||||||
|
- OLEFY_MINLENGTH=500
|
||||||
|
- OLEFY_DEL_TMP=1
|
||||||
|
- SKIP_OLEFY=${SKIP_OLEFY:-n}
|
||||||
|
networks:
|
||||||
|
mailcow-network:
|
||||||
|
aliases:
|
||||||
|
- olefy
|
||||||
|
|
||||||
|
ofelia-mailcow:
|
||||||
|
image: mcuadros/ofelia:latest
|
||||||
|
restart: always
|
||||||
|
command: daemon --docker -f label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}
|
||||||
|
environment:
|
||||||
|
- TZ=${TZ}
|
||||||
|
- COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME}
|
||||||
|
depends_on:
|
||||||
|
- sogo-mailcow
|
||||||
|
- dovecot-mailcow
|
||||||
|
labels:
|
||||||
|
ofelia.enabled: "true"
|
||||||
|
security_opt:
|
||||||
|
- label=disable
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
networks:
|
||||||
|
mailcow-network:
|
||||||
|
aliases:
|
||||||
|
- ofelia
|
||||||
|
|
||||||
|
ipv6nat-mailcow:
|
||||||
|
depends_on:
|
||||||
|
- unbound-mailcow
|
||||||
|
- mysql-mailcow
|
||||||
|
- redis-mailcow
|
||||||
|
- clamd-mailcow
|
||||||
|
- rspamd-mailcow
|
||||||
|
- php-fpm-mailcow
|
||||||
|
- sogo-mailcow
|
||||||
|
- dovecot-mailcow
|
||||||
|
- postfix-mailcow
|
||||||
|
- memcached-mailcow
|
||||||
|
- nginx-mailcow
|
||||||
|
- acme-mailcow
|
||||||
|
- netfilter-mailcow
|
||||||
|
- watchdog-mailcow
|
||||||
|
- dockerapi-mailcow
|
||||||
|
environment:
|
||||||
|
- TZ=${TZ}
|
||||||
|
image: robbertkl/ipv6nat
|
||||||
|
security_opt:
|
||||||
|
- label=disable
|
||||||
|
restart: always
|
||||||
|
privileged: true
|
||||||
|
network_mode: "host"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- /lib/modules:/lib/modules:ro
|
||||||
|
|
||||||
|
networks:
|
||||||
|
mailcow-network:
|
||||||
|
driver: bridge
|
||||||
|
driver_opts:
|
||||||
|
com.docker.network.bridge.name: br-mailcow
|
||||||
|
enable_ipv6: true
|
||||||
|
ipam:
|
||||||
|
driver: default
|
||||||
|
config:
|
||||||
|
- subnet: ${IPV4_NETWORK:-172.22.1}.0/24
|
||||||
|
- subnet: ${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64}
|
||||||
|
mail_network:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
vmail-vol-1:
|
||||||
|
vmail-index-vol-1:
|
||||||
|
mysql-vol-1:
|
||||||
|
mysql-socket-vol-1:
|
||||||
|
redis-vol-1:
|
||||||
|
rspamd-vol-1:
|
||||||
|
postfix-vol-1:
|
||||||
|
crypt-vol-1:
|
||||||
|
sogo-web-vol-1:
|
||||||
|
sogo-userdata-backup-vol-1:
|
||||||
|
clamd-db-vol-1:
|
||||||
300
mailcow-configs/mailcow.conf
Normal file
300
mailcow-configs/mailcow.conf
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
# ------------------------------
|
||||||
|
# mailcow web ui configuration
|
||||||
|
# ------------------------------
|
||||||
|
# example.org is _not_ a valid hostname, use a fqdn here.
|
||||||
|
# Default admin user is "admin"
|
||||||
|
# Default password is "moohoo"
|
||||||
|
|
||||||
|
MAILCOW_HOSTNAME=mail.andreasknuth.de
|
||||||
|
|
||||||
|
# Password hash algorithm
|
||||||
|
# Only certain password hash algorithm are supported. For a fully list of supported schemes,
|
||||||
|
# see https://docs.mailcow.email/models/model-passwd/
|
||||||
|
MAILCOW_PASS_SCHEME=BLF-CRYPT
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# SQL database configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
DBNAME=mailcow
|
||||||
|
DBUSER=mailcow
|
||||||
|
|
||||||
|
# Please use long, random alphanumeric strings (A-Za-z0-9)
|
||||||
|
|
||||||
|
DBPASS=KGekNNga7WLZvNwr2eAYiMhU7aUG
|
||||||
|
DBROOT=gSeRDgCUmndjb38kpEf919naoklx
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# REDIS configuration
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
REDISPASS=LsamNIsi3taCxMgOva0iVfcXOV5O
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# HTTP/S Bindings
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
# You should use HTTPS, but in case of SSL offloaded reverse proxies:
|
||||||
|
# Might be important: This will also change the binding within the container.
|
||||||
|
# If you use a proxy within Docker, point it to the ports you set below.
|
||||||
|
# Do _not_ use IP:PORT in HTTP(S)_BIND or HTTP(S)_PORT
|
||||||
|
# IMPORTANT: Do not use port 8081, 9081, 9082 or 65510!
|
||||||
|
# Example: HTTP_BIND=1.2.3.4
|
||||||
|
# For IPv4 leave it as it is: HTTP_BIND= & HTTPS_PORT=
|
||||||
|
# For IPv6 see https://docs.mailcow.email/post_installation/firststeps-ip_bindings/
|
||||||
|
|
||||||
|
HTTP_PORT=80
|
||||||
|
HTTP_BIND=127.0.0.1
|
||||||
|
|
||||||
|
HTTPS_PORT=8443
|
||||||
|
HTTPS_BIND=127.0.0.1
|
||||||
|
|
||||||
|
# Redirect HTTP connections to HTTPS - y/n
|
||||||
|
HTTP_REDIRECT=n
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# Other bindings
|
||||||
|
# ------------------------------
|
||||||
|
# You should leave that alone
|
||||||
|
# Format: 11.22.33.44:25 or 12.34.56.78:465 etc.
|
||||||
|
|
||||||
|
SMTP_PORT=25
|
||||||
|
SMTPS_PORT=465
|
||||||
|
SUBMISSION_PORT=587
|
||||||
|
IMAP_PORT=143
|
||||||
|
IMAPS_PORT=993
|
||||||
|
POP_PORT=110
|
||||||
|
POPS_PORT=995
|
||||||
|
SIEVE_PORT=4190
|
||||||
|
DOVEADM_PORT=127.0.0.1:19991
|
||||||
|
SQL_PORT=127.0.0.1:13306
|
||||||
|
REDIS_PORT=127.0.0.1:7654
|
||||||
|
|
||||||
|
# Your timezone
|
||||||
|
# See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones for a list of timezones
|
||||||
|
# Use the column named 'TZ identifier' + pay attention for the column named 'Notes'
|
||||||
|
|
||||||
|
TZ=America/Chicago
|
||||||
|
|
||||||
|
# Fixed project name
|
||||||
|
# Please use lowercase letters only
|
||||||
|
|
||||||
|
COMPOSE_PROJECT_NAME=mailcowdockerized
|
||||||
|
|
||||||
|
# Used Docker Compose version
|
||||||
|
# Switch here between native (compose plugin) and standalone
|
||||||
|
# For more informations take a look at the mailcow docs regarding the configuration options.
|
||||||
|
# Normally this should be untouched but if you decided to use either of those you can switch it manually here.
|
||||||
|
# Please be aware that at least one of those variants should be installed on your machine or mailcow will fail.
|
||||||
|
|
||||||
|
DOCKER_COMPOSE_VERSION=native
|
||||||
|
|
||||||
|
# Set this to "allow" to enable the anyone pseudo user. Disabled by default.
|
||||||
|
# When enabled, ACL can be created, that apply to "All authenticated users"
|
||||||
|
# This should probably only be activated on mail hosts, that are used exclusivly by one organisation.
|
||||||
|
# Otherwise a user might share data with too many other users.
|
||||||
|
ACL_ANYONE=disallow
|
||||||
|
|
||||||
|
# Garbage collector cleanup
|
||||||
|
# Deleted domains and mailboxes are moved to /var/vmail/_garbage/timestamp_sanitizedstring
|
||||||
|
# How long should objects remain in the garbage until they are being deleted? (value in minutes)
|
||||||
|
# Check interval is hourly
|
||||||
|
|
||||||
|
MAILDIR_GC_TIME=7200
|
||||||
|
|
||||||
|
# Additional SAN for the certificate
|
||||||
|
#
|
||||||
|
# You can use wildcard records to create specific names for every domain you add to mailcow.
|
||||||
|
# Example: Add domains "example.com" and "example.net" to mailcow, change ADDITIONAL_SAN to a value like:
|
||||||
|
#ADDITIONAL_SAN=imap.*,smtp.*
|
||||||
|
# This will expand the certificate to "imap.example.com", "smtp.example.com", "imap.example.net", "smtp.example.net"
|
||||||
|
# plus every domain you add in the future.
|
||||||
|
#
|
||||||
|
# You can also just add static names...
|
||||||
|
#ADDITIONAL_SAN=srv1.example.net
|
||||||
|
# ...or combine wildcard and static names:
|
||||||
|
#ADDITIONAL_SAN=imap.*,srv1.example.com
|
||||||
|
#
|
||||||
|
|
||||||
|
ADDITIONAL_SAN=
|
||||||
|
|
||||||
|
# Obtain certificates for autodiscover.* and autoconfig.* domains.
|
||||||
|
# This can be useful to switch off in case you are in a scenario where a reverse proxy already handles those.
|
||||||
|
# There are mixed scenarios where ports 80,443 are occupied and you do not want to share certs
|
||||||
|
# between services. So acme-mailcow obtains for maildomains and all web-things get handled
|
||||||
|
# in the reverse proxy.
|
||||||
|
AUTODISCOVER_SAN=y
|
||||||
|
|
||||||
|
# Additional server names for mailcow UI
|
||||||
|
#
|
||||||
|
# Specify alternative addresses for the mailcow UI to respond to
|
||||||
|
# This is useful when you set mail.* as ADDITIONAL_SAN and want to make sure mail.maildomain.com will always point to the mailcow UI.
|
||||||
|
# If the server name does not match a known site, Nginx decides by best-guess and may redirect users to the wrong web root.
|
||||||
|
# You can understand this as server_name directive in Nginx.
|
||||||
|
# Comma separated list without spaces! Example: ADDITIONAL_SERVER_NAMES=a.b.c,d.e.f
|
||||||
|
|
||||||
|
ADDITIONAL_SERVER_NAMES=autoconfig.mail.andreasknuth.de autodiscover.mail.andreasknuth.de
|
||||||
|
|
||||||
|
# Skip running ACME (acme-mailcow, Let's Encrypt certs) - y/n
|
||||||
|
|
||||||
|
SKIP_LETS_ENCRYPT=y
|
||||||
|
|
||||||
|
# Create seperate certificates for all domains - y/n
|
||||||
|
# this will allow adding more than 100 domains, but some email clients will not be able to connect with alternative hostnames
|
||||||
|
# see https://doc.dovecot.org/admin_manual/ssl/sni_support
|
||||||
|
ENABLE_SSL_SNI=n
|
||||||
|
|
||||||
|
# Skip IPv4 check in ACME container - y/n
|
||||||
|
|
||||||
|
SKIP_IP_CHECK=n
|
||||||
|
|
||||||
|
# Skip HTTP verification in ACME container - y/n
|
||||||
|
|
||||||
|
SKIP_HTTP_VERIFICATION=n
|
||||||
|
|
||||||
|
# Skip Unbound (DNS Resolver) Healthchecks (NOT Recommended!) - y/n
|
||||||
|
|
||||||
|
SKIP_UNBOUND_HEALTHCHECK=n
|
||||||
|
|
||||||
|
# Skip ClamAV (clamd-mailcow) anti-virus (Rspamd will auto-detect a missing ClamAV container) - y/n
|
||||||
|
|
||||||
|
SKIP_CLAMD=n
|
||||||
|
|
||||||
|
# Skip Olefy (olefy-mailcow) anti-virus for Office documents (Rspamd will auto-detect a missing Olefy container) - y/n
|
||||||
|
|
||||||
|
SKIP_OLEFY=n
|
||||||
|
|
||||||
|
# Skip SOGo: Will disable SOGo integration and therefore webmail, DAV protocols and ActiveSync support (experimental, unsupported, not fully implemented) - y/n
|
||||||
|
|
||||||
|
SKIP_SOGO=n
|
||||||
|
|
||||||
|
# Skip FTS (Fulltext Search) for Dovecot on low-memory, low-threaded systems or if you simply want to disable it.
|
||||||
|
# Dovecot inside mailcow use Flatcurve as FTS Backend.
|
||||||
|
|
||||||
|
SKIP_FTS=n
|
||||||
|
|
||||||
|
# Dovecot Indexing (FTS) Process maximum heap size in MB, there is no recommendation, please see Dovecot docs.
|
||||||
|
# Flatcurve (Xapian backend) is used as the FTS Indexer. It is supposed to be efficient in CPU and RAM consumption.
|
||||||
|
# However: Please always monitor your Resource consumption!
|
||||||
|
|
||||||
|
FTS_HEAP=128
|
||||||
|
|
||||||
|
# Controls how many processes the Dovecot indexing process can spawn at max.
|
||||||
|
# Too many indexing processes can use a lot of CPU and Disk I/O.
|
||||||
|
# Please visit: https://doc.dovecot.org/configuration_manual/service_configuration/#indexer-worker for more informations
|
||||||
|
|
||||||
|
FTS_PROCS=1
|
||||||
|
|
||||||
|
# Allow admins to log into SOGo as email user (without any password)
|
||||||
|
|
||||||
|
ALLOW_ADMIN_EMAIL_LOGIN=n
|
||||||
|
|
||||||
|
# Enable watchdog (watchdog-mailcow) to restart unhealthy containers
|
||||||
|
|
||||||
|
USE_WATCHDOG=y
|
||||||
|
|
||||||
|
# Send watchdog notifications by mail (sent from watchdog@MAILCOW_HOSTNAME)
|
||||||
|
# CAUTION:
|
||||||
|
# 1. You should use external recipients
|
||||||
|
# 2. Mails are sent unsigned (no DKIM)
|
||||||
|
# 3. If you use DMARC, create a separate DMARC policy ("v=DMARC1; p=none;" in _dmarc.MAILCOW_HOSTNAME)
|
||||||
|
# Multiple rcpts allowed, NO quotation marks, NO spaces
|
||||||
|
|
||||||
|
#WATCHDOG_NOTIFY_EMAIL=a@example.com,b@example.com,c@example.com
|
||||||
|
#WATCHDOG_NOTIFY_EMAIL=
|
||||||
|
|
||||||
|
# Send notifications to a webhook URL that receives a POST request with the content type "application/json".
|
||||||
|
# You can use this to send notifications to services like Discord, Slack and others.
|
||||||
|
#WATCHDOG_NOTIFY_WEBHOOK=https://discord.com/api/webhooks/XXXXXXXXXXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||||
|
# JSON body included in the webhook POST request. Needs to be in single quotes.
|
||||||
|
# Following variables are available: SUBJECT, BODY
|
||||||
|
#WATCHDOG_NOTIFY_WEBHOOK_BODY='{"username": "mailcow Watchdog", "content": "****\n"}'
|
||||||
|
|
||||||
|
# Notify about banned IP (includes whois lookup)
|
||||||
|
WATCHDOG_NOTIFY_BAN=n
|
||||||
|
|
||||||
|
# Send a notification when the watchdog is started.
|
||||||
|
WATCHDOG_NOTIFY_START=y
|
||||||
|
|
||||||
|
# Subject for watchdog mails. Defaults to "Watchdog ALERT" followed by the error message.
|
||||||
|
#WATCHDOG_SUBJECT=
|
||||||
|
|
||||||
|
# Checks if mailcow is an open relay. Requires a SAL. More checks will follow.
|
||||||
|
# https://www.servercow.de/mailcow?lang=en
|
||||||
|
# https://www.servercow.de/mailcow?lang=de
|
||||||
|
# No data is collected. Opt-in and anonymous.
|
||||||
|
# Will only work with unmodified mailcow setups.
|
||||||
|
WATCHDOG_EXTERNAL_CHECKS=n
|
||||||
|
|
||||||
|
# Enable watchdog verbose logging
|
||||||
|
WATCHDOG_VERBOSE=n
|
||||||
|
|
||||||
|
# Max log lines per service to keep in Redis logs
|
||||||
|
|
||||||
|
LOG_LINES=9999
|
||||||
|
|
||||||
|
# Internal IPv4 /24 subnet, format n.n.n (expands to n.n.n.0/24)
|
||||||
|
# Use private IPv4 addresses only, see https://en.wikipedia.org/wiki/Private_network#Private_IPv4_addresses
|
||||||
|
|
||||||
|
IPV4_NETWORK=172.22.1
|
||||||
|
|
||||||
|
# Internal IPv6 subnet in fc00::/7
|
||||||
|
# Use private IPv6 addresses only, see https://en.wikipedia.org/wiki/Private_network#Private_IPv6_addresses
|
||||||
|
|
||||||
|
IPV6_NETWORK=fd4d:6169:6c63:6f77::/64
|
||||||
|
|
||||||
|
# Use this IPv4 for outgoing connections (SNAT)
|
||||||
|
|
||||||
|
#SNAT_TO_SOURCE=
|
||||||
|
|
||||||
|
# Use this IPv6 for outgoing connections (SNAT)
|
||||||
|
|
||||||
|
#SNAT6_TO_SOURCE=
|
||||||
|
|
||||||
|
# Create or override an API key for the web UI
|
||||||
|
# You _must_ define API_ALLOW_FROM, which is a comma separated list of IPs
|
||||||
|
# An API key defined as API_KEY has read-write access
|
||||||
|
# An API key defined as API_KEY_READ_ONLY has read-only access
|
||||||
|
# Allowed chars for API_KEY and API_KEY_READ_ONLY: a-z, A-Z, 0-9, -
|
||||||
|
# You can define API_KEY and/or API_KEY_READ_ONLY
|
||||||
|
|
||||||
|
#API_KEY=
|
||||||
|
#API_KEY_READ_ONLY=
|
||||||
|
#API_ALLOW_FROM=172.22.1.1,127.0.0.1
|
||||||
|
|
||||||
|
# mail_home is ~/Maildir
|
||||||
|
MAILDIR_SUB=Maildir
|
||||||
|
|
||||||
|
# SOGo session timeout in minutes
|
||||||
|
SOGO_EXPIRE_SESSION=480
|
||||||
|
|
||||||
|
# DOVECOT_MASTER_USER and DOVECOT_MASTER_PASS must both be provided. No special chars.
|
||||||
|
# Empty by default to auto-generate master user and password on start.
|
||||||
|
# User expands to DOVECOT_MASTER_USER@mailcow.local
|
||||||
|
# LEAVE EMPTY IF UNSURE
|
||||||
|
DOVECOT_MASTER_USER=
|
||||||
|
# LEAVE EMPTY IF UNSURE
|
||||||
|
DOVECOT_MASTER_PASS=
|
||||||
|
|
||||||
|
# Let's Encrypt registration contact information
|
||||||
|
# Optional: Leave empty for none
|
||||||
|
# This value is only used on first order!
|
||||||
|
# Setting it at a later point will require the following steps:
|
||||||
|
# https://docs.mailcow.email/troubleshooting/debug-reset_tls/
|
||||||
|
ACME_CONTACT=
|
||||||
|
|
||||||
|
# WebAuthn device manufacturer verification
|
||||||
|
# After setting WEBAUTHN_ONLY_TRUSTED_VENDORS=y only devices from trusted manufacturers are allowed
|
||||||
|
# root certificates can be placed for validation under mailcow-dockerized/data/web/inc/lib/WebAuthn/rootCertificates
|
||||||
|
WEBAUTHN_ONLY_TRUSTED_VENDORS=n
|
||||||
|
|
||||||
|
# Spamhaus Data Query Service Key
|
||||||
|
# Optional: Leave empty for none
|
||||||
|
# Enter your key here if you are using a blocked ASN (OVH, AWS, Cloudflare e.g) for the unregistered Spamhaus Blocklist.
|
||||||
|
# If empty, it will completely disable Spamhaus blocklists if it detects that you are running on a server using a blocked AS.
|
||||||
|
# Otherwise it will work normally.
|
||||||
|
SPAMHAUS_DQS_KEY=
|
||||||
|
|
||||||
|
# Prevent netfilter from setting an iptables/nftables rule to isolate the mailcow docker network - y/n
|
||||||
|
# CAUTION: Disabling this may expose container ports to other neighbors on the same subnet, even if the ports are bound to localhost
|
||||||
|
DISABLE_NETFILTER_ISOLATION_RULE=n
|
||||||
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
|
||||||
|
})
|
||||||
|
}
|
||||||
44
ses-lambda-nodejs/email2json.js
Normal file
44
ses-lambda-nodejs/email2json.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
if (process.argv.length < 3) {
|
||||||
|
process.stdout.write('USAGE: nodejs emailtojson.js filename');
|
||||||
|
process.stdout.write('Example: nodejs emailtojson.js emailfile.eml');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const { MailParser } = require("mailparser");
|
||||||
|
const mailpath = process.argv[2];
|
||||||
|
|
||||||
|
let parser = new MailParser();
|
||||||
|
let input = fs.createReadStream(mailpath);
|
||||||
|
let mailobj = {
|
||||||
|
attachments: [],
|
||||||
|
text: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
parser.on('headers', headers => {
|
||||||
|
let headerObj = {};
|
||||||
|
for (let [k, v] of headers) {
|
||||||
|
// We don’t escape the key '__proto__'
|
||||||
|
// which can cause problems on older engines
|
||||||
|
headerObj[k] = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
mailobj.headers = headerObj;
|
||||||
|
});
|
||||||
|
|
||||||
|
parser.on('data', data => {
|
||||||
|
if (data.type === 'attachment') {
|
||||||
|
mailobj.attachments.push(data);
|
||||||
|
data.content.on('readable', () => data.content.read());
|
||||||
|
data.content.on('end', () => data.release());
|
||||||
|
} else {
|
||||||
|
mailobj.text = data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
parser.on('end', () => {
|
||||||
|
process.stdout.write(JSON.stringify(mailobj, (k, v) => (k === 'content' || k === 'release' ? undefined : v), 3));
|
||||||
|
});
|
||||||
|
input.pipe(parser);
|
||||||
30
ses-lambda-nodejs/email2json1.js
Normal file
30
ses-lambda-nodejs/email2json1.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// simple-emailtojson.mjs
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import { simpleParser } from 'mailparser';
|
||||||
|
|
||||||
|
if (process.argv.length < 3) {
|
||||||
|
console.error('USAGE: node simple-emailtojson.mjs <emailfile.eml>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mailpath = process.argv[2];
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const emlBuffer = await fs.readFile(mailpath);
|
||||||
|
const mail = await simpleParser(emlBuffer);
|
||||||
|
|
||||||
|
// Optional: entferne Buffers, die du nicht serialisieren willst
|
||||||
|
if (mail.attachments) {
|
||||||
|
mail.attachments = mail.attachments.map(att => ({
|
||||||
|
filename: att.filename,
|
||||||
|
contentType: att.contentType,
|
||||||
|
size: att.size,
|
||||||
|
// evtl. att.content.toString('base64') oder weglassen
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(mail, null, 2));
|
||||||
|
})().catch(err => {
|
||||||
|
console.error('Fehler beim Parsen:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
68
ses-lambda-nodejs/eml/1.eml
Normal file
68
ses-lambda-nodejs/eml/1.eml
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
Return-Path: <andreas.knuth@gmail.com>
|
||||||
|
Received: from mail-lf1-f54.google.com (mail-lf1-f54.google.com [209.85.167.54])
|
||||||
|
by inbound-smtp.us-east-2.amazonaws.com with SMTP id tl8bodt75rl99agvurj9pt06aaphgs5pj3l7ci01
|
||||||
|
for test@bizmatch.net;
|
||||||
|
Mon, 07 Jul 2025 22:29:30 +0000 (UTC)
|
||||||
|
X-SES-Spam-Verdict: PASS
|
||||||
|
X-SES-Virus-Verdict: PASS
|
||||||
|
Received-SPF: pass (spfCheck: domain of _spf.google.com designates 209.85.167.54 as permitted sender) client-ip=209.85.167.54; envelope-from=andreas.knuth@gmail.com; helo=mail-lf1-f54.google.com;
|
||||||
|
Authentication-Results: amazonses.com;
|
||||||
|
spf=pass (spfCheck: domain of _spf.google.com designates 209.85.167.54 as permitted sender) client-ip=209.85.167.54; envelope-from=andreas.knuth@gmail.com; helo=mail-lf1-f54.google.com;
|
||||||
|
dkim=pass header.i=@gmail.com;
|
||||||
|
dmarc=pass header.from=gmail.com;
|
||||||
|
X-SES-RECEIPT: AEFBQUFBQUFBQUFHZ2VxMTdrTDl5UCtYZjRQUHNhL3YwRWo4YXNNbEVYdGdqUTducmt1L25UY0pMNFNqMitXQWZCbnVsYW1seVdseFQzT1lZT2VUVEtCUWl0b2VDVk94SU5xN3p1K1R3d2lOT0hkb2ZIclEvS0JqNVdtRzAvNnJtejlsOE42dTU3ZTV5K2NIQ0lvOEJtQ0hBSkhrZ2JURHJjWXpVYU5EOEZnMnc0SU8xeS9TUVR6OXZxdmt4WVdCMzNuaUJ2TE9xRzN1WHdZM3VFdUcwYzBrZm9OV3BFMEwrZURnb25PY2h2dVExRXV1Q0ZCSzhIeGRsSTZFdXZwUUVzQ2JQUFVzUjFvZnI0U2g4aXBFZDQxQVNFanJLYXdNS2crKzZPanJySHJWckdXQ21hZ2NOQWc9PQ==
|
||||||
|
X-SES-DKIM-SIGNATURE: a=rsa-sha256; q=dns/txt; b=A7ZG4osvHSz8Grirn5FNbtnZtZoxA4SwzM4NX2SD3xlmdGZ9gEs7o5QAaexpqFo+tVHGze6kCXShR/m5e+Ccoelv+pYGuQsM0UQukPH567mOTd6DBsUnwgGoWyzkR4LyBMSGKX50m3plpMr7OsfydgTtSgmNqx6TaW2uTqAmHG4=; c=relaxed/simple; s=ndjes4mrtuzus6qxu3frw3ubo3gpjndv; d=amazonses.com; t=1751927370; v=1; bh=kl0ZVgKAgL2tPEaQmtmEdFkMF0Wkh08RlXtja41/naQ=; h=From:To:Cc:Bcc:Subject:Date:Message-ID:MIME-Version:Content-Type:X-SES-RECEIPT;
|
||||||
|
Received: by mail-lf1-f54.google.com with SMTP id 2adb3069b0e04-553aba2f99eso547669e87.3
|
||||||
|
for <test@bizmatch.net>; Mon, 07 Jul 2025 15:29:29 -0700 (PDT)
|
||||||
|
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
|
||||||
|
d=gmail.com; s=20230601; t=1751927368; x=1752532168; darn=bizmatch.net;
|
||||||
|
h=to:subject:message-id:date:from:mime-version:from:to:cc:subject
|
||||||
|
:date:message-id:reply-to;
|
||||||
|
bh=kl0ZVgKAgL2tPEaQmtmEdFkMF0Wkh08RlXtja41/naQ=;
|
||||||
|
b=Dv7XQW93T4nV5kY0HB5qVq0H1iB0cYfdQMzSGyu+chsPKK5N+8INipWr1bulAYA4OM
|
||||||
|
UKP7EiY4j3zzrxVLFMjboztDfI4PG2oAYSdxIah+jTdgpliVhIeGqvM87SH4pfSVPnOB
|
||||||
|
JygDwwhB25s9wfwM7XDQ+uaAg/Fdwc6kgXf1d2k28gdnV9cuhToWMBAdCZG+0pic969P
|
||||||
|
HEJlLY+KJBVIvzl8JcVZ6ReT8FeQWGwKfzdrpG8PXyYO8MH4FtAmfji4Av4PO/Q2Ky/u
|
||||||
|
3Razz1QTf8R7dHCndAdXCa5INrMaCQOvXRWMMc22sIfMTtM0RKieL7jfp+T4kzcWd8bp
|
||||||
|
F3BA==
|
||||||
|
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
|
||||||
|
d=1e100.net; s=20230601; t=1751927368; x=1752532168;
|
||||||
|
h=to:subject:message-id:date:from:mime-version:x-gm-message-state
|
||||||
|
:from:to:cc:subject:date:message-id:reply-to;
|
||||||
|
bh=kl0ZVgKAgL2tPEaQmtmEdFkMF0Wkh08RlXtja41/naQ=;
|
||||||
|
b=wDGrMTBQxC0PHTqXvyy2DVWa4au/7y1hd7NkSgRoVX/vVKp1ArewmkY1xWPEG4qp6S
|
||||||
|
X6B9q/qimOqNHs/me0gke2XOeVfgT0Pw+NMSJMf7mCGLZ2+y6sxRttgrh4u2FTxeY0K1
|
||||||
|
RKYdwG7rUcqBYoyU/1h6nJYrotuCs7VYBmWbglChhTJoysmFdnR7eAsD2GnxVM1CDZbI
|
||||||
|
XdVsK/+vOhUHw8uyVB8sILrEtpM4+ETz0BnIveqyldnfXTKj1v1gnXUNi2XgaK+K126b
|
||||||
|
DsXGAP4SwLXUeCHnwGvEfpqTvdVhhOalwR0uCNFWMSOIOuxJbm6hPdU82oz1G6yEUip1
|
||||||
|
pSyw==
|
||||||
|
X-Gm-Message-State: AOJu0YwHBQTUiVzyF4Z+W9Nn+X1DjRnb+ExbYEHAl2nHyJxuSHCcO+92
|
||||||
|
BQdv1ZRanXsQ1Lb4d3pzXr5AoeyNsoAyT3H9Xnu0bZO+zSNpvJ44dQY0WwJc1RKk3WFm8C2xxjl
|
||||||
|
FNPLCFUIKOYoBKSue/IhK5RuJEorabq6yCy11zJUvVQ==
|
||||||
|
X-Gm-Gg: ASbGnctmha0Sl+6s3+7aqdJp4XfRfVYWw1ijYcCHalIyyYoLNA/scbpX0Eqz6/xkLKz
|
||||||
|
Zk8kZ1s2cvvs0Li8JDtKWndBEfOlH2vObiTf1nOjfUXArElHNcXTLauyTSsQhhnX98yufY/FlMM
|
||||||
|
gBVMpCLdinwI7W73wct+qp6JNzoPTJjMqxxr460ujtFDG0M5f6/edKdGc=
|
||||||
|
X-Google-Smtp-Source: AGHT+IGKQO5agz3saT3mvRcQjADlp5mR3Ss7bUoX6CzSwr9FNqw5AekIbPUiMQx0QQJz5SZAtSywG7pqy3jzwJU7gFI=
|
||||||
|
X-Received: by 2002:a05:6512:e90:b0:553:29cc:c47a with SMTP id
|
||||||
|
2adb3069b0e04-556e76ea8b6mr1397840e87.6.1751927367907; Mon, 07 Jul 2025
|
||||||
|
15:29:27 -0700 (PDT)
|
||||||
|
MIME-Version: 1.0
|
||||||
|
From: Andreas Knuth <andreas.knuth@gmail.com>
|
||||||
|
Date: Mon, 7 Jul 2025 17:29:22 -0500
|
||||||
|
X-Gm-Features: Ac12FXylATeuoXeS0LgUwAAC4rygTYy_KTtNVnLhQ8Pv-KiTkX5e5F1AlsvpAY8
|
||||||
|
Message-ID: <CADfCGtb_G+9W11EgfeQhp+V5vb1_gkeq9ZsfqgvsxC9hMNEfJQ@mail.gmail.com>
|
||||||
|
Subject: dsfsd
|
||||||
|
To: test@bizmatch.net
|
||||||
|
Content-Type: multipart/alternative; boundary="0000000000006fc0ff06395e6090"
|
||||||
|
|
||||||
|
--0000000000006fc0ff06395e6090
|
||||||
|
Content-Type: text/plain; charset="UTF-8"
|
||||||
|
|
||||||
|
sdfsdf
|
||||||
|
|
||||||
|
--0000000000006fc0ff06395e6090
|
||||||
|
Content-Type: text/html; charset="UTF-8"
|
||||||
|
|
||||||
|
<div dir="ltr">sdfsdf</div>
|
||||||
|
|
||||||
|
--0000000000006fc0ff06395e6090--
|
||||||
68
ses-lambda-nodejs/eml/2.eml
Normal file
68
ses-lambda-nodejs/eml/2.eml
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
Return-Path: <andreas.knuth@gmail.com>
|
||||||
|
Received: from mail-lj1-f181.google.com (mail-lj1-f181.google.com [209.85.208.181])
|
||||||
|
by inbound-smtp.us-east-2.amazonaws.com with SMTP id uiead4igi9ee1cijaffb2dsd0bhkg8ksjr35kp01;
|
||||||
|
Mon, 07 Jul 2025 15:15:44 +0000 (UTC)
|
||||||
|
X-SES-Spam-Verdict: PASS
|
||||||
|
X-SES-Virus-Verdict: PASS
|
||||||
|
Received-SPF: pass (spfCheck: domain of _spf.google.com designates 209.85.208.181 as permitted sender) client-ip=209.85.208.181; envelope-from=andreas.knuth@gmail.com; helo=mail-lj1-f181.google.com;
|
||||||
|
Authentication-Results: amazonses.com;
|
||||||
|
spf=pass (spfCheck: domain of _spf.google.com designates 209.85.208.181 as permitted sender) client-ip=209.85.208.181; envelope-from=andreas.knuth@gmail.com; helo=mail-lj1-f181.google.com;
|
||||||
|
dkim=pass header.i=@gmail.com;
|
||||||
|
dmarc=pass header.from=gmail.com;
|
||||||
|
X-SES-RECEIPT: AEFBQUFBQUFBQUFFVnVnY0k5TFVXcDdiK0FZbmNLbnRMc3d5c2hrWitkN1l4bEhZdnRvQ0NJUGVPcm5qV1hIblA0WDh0ZERsT2xFS01HUFZFOEtPZ29tdW8yQWtkYzlMckxESzR0d1VjMVF0dWw0bXlBVzkrM3BrVCtJZ2xTdm5nU3VMcEgzcVNqRXp0aEZxdks0NzBpN2s3d25jcnJEUi9PL2ZzbGQ3Z1JrRFZTNGp6bDRnS2JZb3EwQXB2bThlMTE2SFBWZitNSlVUSzloRkZyREJRQVdoQmVWMGtXSWZFbndrT0g5cUtnajhQUTA4ZTlQaGdKWXNJbnFRSXVrNkMzenB4Mm1xWXVvVFRHUDdlU2N0cm5XaGtzV1ZaTDVnZG1aVVIyWjB2MkZ6M3dNNlNDWmNDeXc9PQ==
|
||||||
|
X-SES-DKIM-SIGNATURE: a=rsa-sha256; q=dns/txt; b=DqDTEo6krNCKrTt6VBWj6WkxPHgAqraQunr2h6nI/95ooZ2H4qZ/4Ts5uMU13PJV449VQYWMUL1qX5qbjq0UqKHGjry7RlMmOWxxJY0SRl8Eye6HN0UMAwJibEb0K6piljG9oYAbIigE5e8D63ESnPkuiEeIqztkUg7ngVvDFiE=; c=relaxed/simple; s=ndjes4mrtuzus6qxu3frw3ubo3gpjndv; d=amazonses.com; t=1751901344; v=1; bh=+4eu3AqIZQTs7Asy6nycJyqaDVkIYEQ7Wt4fZW12bas=; h=From:To:Cc:Bcc:Subject:Date:Message-ID:MIME-Version:Content-Type:X-SES-RECEIPT;
|
||||||
|
Received: by mail-lj1-f181.google.com with SMTP id 38308e7fff4ca-32b4483cd3cso1203521fa.3;
|
||||||
|
Mon, 07 Jul 2025 08:15:43 -0700 (PDT)
|
||||||
|
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
|
||||||
|
d=gmail.com; s=20230601; t=1751901342; x=1752506142; darn=bizmatch.net;
|
||||||
|
h=to:subject:message-id:date:from:mime-version:from:to:cc:subject
|
||||||
|
:date:message-id:reply-to;
|
||||||
|
bh=+4eu3AqIZQTs7Asy6nycJyqaDVkIYEQ7Wt4fZW12bas=;
|
||||||
|
b=LgkJUkx/JjaJyQr6qBsVw28Vwcr8g27WAFJXWlIPJ5CEewRfkIT337505lkC3BD85J
|
||||||
|
OlgjVzXj7MjD3bF64ltxJOKRWoXWzk9F9eMQYHYpfkAk5iAoVHQutXw4u1wYZBQH2iCc
|
||||||
|
BH2YapjsD4vO0exwYlJBbMP4Tq6N1Wu1XTdTtJTuPpynNnwB0hjnnJdlNj5jisLRtJe/
|
||||||
|
RSUPTpAQU9Hqxgt7R/1yNUjG5I807hzceJuSs0LW7BQPJwZIK6ZI+EvX88FKo4wjKrDf
|
||||||
|
pWoVTu3iwszFCDFBHZ2LGF4cXggnyiRS/5bZot9WhU59zOmgYJHfhjfZokfihsPqdWdl
|
||||||
|
EHzQ==
|
||||||
|
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
|
||||||
|
d=1e100.net; s=20230601; t=1751901342; x=1752506142;
|
||||||
|
h=to:subject:message-id:date:from:mime-version:x-gm-message-state
|
||||||
|
:from:to:cc:subject:date:message-id:reply-to;
|
||||||
|
bh=+4eu3AqIZQTs7Asy6nycJyqaDVkIYEQ7Wt4fZW12bas=;
|
||||||
|
b=m2xoHX0bVq6CGVKxnGFuWZy9tEjQ/ONvZhhl7uOVZQ7vTaS67YXsLydSk2F8c0c8vY
|
||||||
|
P/s6wy4sDmTOpWISBM04zRWqRvsaHQonyualsQd7U02Zsp/tI0mqdIJ/ni3MsC8G03GL
|
||||||
|
wWWN2e6V6QBG+rAhau2BXGBao+baQSTjSvgefvYDzuWg5ugfdexXhJ7efRcWHQqPb5KU
|
||||||
|
mEjmkLDt5DTBhR091i01YxrrC7Ny3RlEPMqbyq+nwvmDFm5ACNdq9aMaEujVRB1+a3ei
|
||||||
|
BhLrtMRybBlGJPgpCV7IOdA9WRyJyNMR7qbeREz3joz1Errsab/Oa9VYhWqFl5dPLEt8
|
||||||
|
hZzw==
|
||||||
|
X-Forwarded-Encrypted: i=1; AJvYcCXI0RBO9KQUyKzy0MqDnS9enil+Kp0jSrcSTlRViDgsyZLMSUi6TJ5BX9yrcJmHLdEB069pDCoD@bizmatch.net
|
||||||
|
X-Gm-Message-State: AOJu0YysUzHz3l+r4XYKaoIqIKrS6dUxH8itmcz0lOP+MF+FcieWd0V4
|
||||||
|
D3/aLVsWV48ZOfaA0uoEw/lJ1AQC81NVUTAAu29+H/WmFIAD5qOqhAQ2JkiffPJe26VJ4eDTz9e
|
||||||
|
LZoi5HSO+CmLcSML1iUvE0Rd03QjVG85uwNJ8cPQAhpZK
|
||||||
|
X-Gm-Gg: ASbGncurPvR6Brs7OGpPmZ3/vpAbCMXuujgejGh/xAmeSHObnMZSBQnW6wbOzHhmjUn
|
||||||
|
mv90GEruVA4Ru81mpcqOCAjUD7wut8PtZwtavp4RGbRpTEMtetsFEuHxSULHc7fvCdqMHxbDhtK
|
||||||
|
U+JDIDpBXBf9nLjX/9Ia1MHszsdoxH6r2MmQpZpiWk++VE
|
||||||
|
X-Google-Smtp-Source: AGHT+IE+PQnE0dCTFWNExgYOZKVZ7/J425p7hNVhw1g9pL6JkbRpiUfxAw0iO0Y0Bf90ZvQ1b9I9rJjrkK0b/NkUYs8=
|
||||||
|
X-Received: by 2002:a05:6512:6cd:b0:553:2bf7:77bf with SMTP id
|
||||||
|
2adb3069b0e04-556e7bc6d1dmr1353763e87.8.1751901341859; Mon, 07 Jul 2025
|
||||||
|
08:15:41 -0700 (PDT)
|
||||||
|
MIME-Version: 1.0
|
||||||
|
From: Andreas Knuth <andreas.knuth@gmail.com>
|
||||||
|
Date: Mon, 7 Jul 2025 10:15:33 -0500
|
||||||
|
X-Gm-Features: Ac12FXwNFaB1vrcboxVGo5_EQvpg1Kc37eKOdlZdqKdeMb2z20e0CP6o69Pq8k4
|
||||||
|
Message-ID: <CADfCGtYpG78F7DKCWj8DQXsqTteOyg3=Jqw9rgwE2AXZ4YEJ3Q@mail.gmail.com>
|
||||||
|
Subject: info,support - knuth
|
||||||
|
To: info@bizmatch.net, support@bizmatch.net, knuth@andreasknuth.de
|
||||||
|
Content-Type: multipart/alternative; boundary="00000000000029c44406395851f8"
|
||||||
|
|
||||||
|
--00000000000029c44406395851f8
|
||||||
|
Content-Type: text/plain; charset="UTF-8"
|
||||||
|
|
||||||
|
info,support - knuth
|
||||||
|
|
||||||
|
--00000000000029c44406395851f8
|
||||||
|
Content-Type: text/html; charset="UTF-8"
|
||||||
|
|
||||||
|
<div dir="ltr">info,support - knuth</div>
|
||||||
|
|
||||||
|
--00000000000029c44406395851f8--
|
||||||
182
ses-lambda-nodejs/eml/3.eml
Normal file
182
ses-lambda-nodejs/eml/3.eml
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
Return-Path: <pradeepkumar200w@outlook.com>
|
||||||
|
Received: from SEYPR02CU001.outbound.protection.outlook.com (mail-koreacentralazolkn19013080.outbound.protection.outlook.com [52.103.74.80])
|
||||||
|
by inbound-smtp.us-east-2.amazonaws.com with SMTP id qchs4km8p1vevgfk9l2j0hh04706mhi4jd2tjpo1
|
||||||
|
for support@bizmatch.net;
|
||||||
|
Tue, 24 Jun 2025 16:33:44 +0000 (UTC)
|
||||||
|
X-SES-Spam-Verdict: PASS
|
||||||
|
X-SES-Virus-Verdict: PASS
|
||||||
|
Received-SPF: pass (spfCheck: domain of outlook.com designates 52.103.74.80 as permitted sender) client-ip=52.103.74.80; envelope-from=pradeepkumar200w@outlook.com; helo=SEYPR02CU001.outbound.protection.outlook.com;
|
||||||
|
Authentication-Results: amazonses.com;
|
||||||
|
spf=pass (spfCheck: domain of outlook.com designates 52.103.74.80 as permitted sender) client-ip=52.103.74.80; envelope-from=pradeepkumar200w@outlook.com; helo=SEYPR02CU001.outbound.protection.outlook.com;
|
||||||
|
dkim=pass header.i=@outlook.com;
|
||||||
|
dmarc=pass header.from=outlook.com;
|
||||||
|
X-SES-RECEIPT: AEFBQUFBQUFBQUFFMlhQMUNyaFlTZkpkTFZLRVhscVZwVjlXOWZ2L3hWOHU0UXJDbE1YRWlkbWZPWXR2b2dxWmpjL1VPUTVEc05qVHpxbFpFZmlDcU1iNC9mU0dwNVl0dDFuSVlFTGk2UmdTUUt3bGNvdmI1QUFoTndzd1pSdVJ1ZjAzbithRm9mcDM3eWRadUJidjdGaVVmeEZ2RVZiajRhUlk1MUhua3MvL2plKytCUjdUY0hEMEd1QjFJd2hDMzRZcmRRY1pEZHVtMzFaNUIzVUM0MGVUSk80dmJxZWhSbUdaYWI5bnRIYktLMndSUzdaVjh4Z3U0Z1ZybldXWGxYeEx2YnBXd21NVGNFTGI3REFEbTFyTzA0YW9DS215SXJJL0Z4Y0VVVGw5MURCNEs0QUJDTWc9PQ==
|
||||||
|
X-SES-DKIM-SIGNATURE: a=rsa-sha256; q=dns/txt; b=3vPX1a0N+3QUAXXTHFMuvn/FNmc7EPbUvsTd/22aVOpM1vTUq5iHJsB/xlkw3ZArE5CZv4BMEX8vo02wDt9BUdtoLX0+7Yg6KFHTod5sZtota+retIgCAJsL4ZsbVsrsuJe//T6crx7y6e8GR0yCOnJI7W6skPgBubEqoAYxUL4=; c=relaxed/simple; s=ndjes4mrtuzus6qxu3frw3ubo3gpjndv; d=amazonses.com; t=1750782825; v=1; bh=g+9PG3WYMx3Sc8zRsKmZM6AKdWg1fMBV5IpRVhFdquA=; h=From:To:Cc:Bcc:Subject:Date:Message-ID:MIME-Version:Content-Type:X-SES-RECEIPT;
|
||||||
|
ARC-Seal: i=1; a=rsa-sha256; s=arcselector10001; d=microsoft.com; cv=none;
|
||||||
|
b=BTWCUxEqn+to6x1qM6mq3CVd3ujLguc64BtxCZlNFSfq0/WGv2BWX+LJ9JJ1edebSdewRyDEEdIZ+jTfdk74K1ArW4y29EkzVkihr5B5/tPTqt+Aa4ledzchK/M8DI937Bs68r1UKY7RdgzhfIIiCC8X0r1a+deRIfKCUMqkvJygpKG1qh3OPbAQksZnaI9yBcZF51ddkPoHyErNqeKBpufRE8O1EF5JUiaWfX/TRkmSAxG9hfUOQXzpBnrq1zCIPlUxME5DNWRaJgiJMjMzcfgdGg5SY/gzqTgA63x0DlK7L1LF1EOTBDjjCMAIEYPnLRuIcAb2f7m1RWeQWEa0xg==
|
||||||
|
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com;
|
||||||
|
s=arcselector10001;
|
||||||
|
h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-AntiSpam-MessageData-ChunkCount:X-MS-Exchange-AntiSpam-MessageData-0:X-MS-Exchange-AntiSpam-MessageData-1;
|
||||||
|
bh=g+9PG3WYMx3Sc8zRsKmZM6AKdWg1fMBV5IpRVhFdquA=;
|
||||||
|
b=J6RpWVzs/O2uHbi6iYdAtURortYRRdAQE0FgtVPBgDs1TEwgllTnDHnZ3Ee9gqkCl2N5zrNBxPp1Tr0giZe09QhZy77fDATXPV/VWOBikGX5Y7Udyixdr2H4xnVTs24qVz0t1EueJwK8mPxc0XN2PJqgEFzaJNmOXhXe27b0lJ9OYE0RIVX4Sov3mQeUziTFJnmGfmMm/IdUNhimeZZN1xkx2y3wNQYvvJkOmHE2QVltH92gWWmyokW0ETvArswPlrkGE32lOYhZyfzBHMAEmOheiU78MhDbQVKtavt3MedkkgtQiaf5TjUtEuEVkb53YvAZ3WMk5PY6XrWSyTu5vw==
|
||||||
|
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none;
|
||||||
|
dkim=none; arc=none
|
||||||
|
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=outlook.com;
|
||||||
|
s=selector1;
|
||||||
|
h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck;
|
||||||
|
bh=g+9PG3WYMx3Sc8zRsKmZM6AKdWg1fMBV5IpRVhFdquA=;
|
||||||
|
b=d1/pYYSuAkgUYS0TLWHcVe3omzNmJjGQCkpBZfAmvA8MVfnE7aFlk748X6sZtRhTbpBT0Dcr/dvSkhJurZIxaqJlhYyQwrBsJENi2FyfeZBsrdNAuRyrAIFr75aOIVE5ij512zr9Gr6CBqx6F1AzNiYDirgqR8vCxg3f+PGtpqim3V0kIXPy3dvqtm0cEdPoC15Pojkot1eW+xZ0pdHXfQcQMyw7KGJxioGrl4U9gYO8auOw8elO0zdOmlefWWXUz42tJZ/OpyfcOaVfgz+QLW/nutZ0ldwjH/Jwzf4RauWEtMVwrWjJK4vcr/ckg0MHGsA9UmLMvVgSasEDB9ZvzA==
|
||||||
|
Received: from TYZPR03MB6645.apcprd03.prod.outlook.com (2603:1096:400:1ff::12)
|
||||||
|
by SI6PR03MB8610.apcprd03.prod.outlook.com (2603:1096:4:244::5) with
|
||||||
|
Microsoft SMTP Server (version=TLS1_2,
|
||||||
|
cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.8857.21; Tue, 24 Jun
|
||||||
|
2025 16:32:54 +0000
|
||||||
|
Received: from TYZPR03MB6645.apcprd03.prod.outlook.com
|
||||||
|
([fe80::2ede:ba06:80a0:91e8]) by TYZPR03MB6645.apcprd03.prod.outlook.com
|
||||||
|
([fe80::2ede:ba06:80a0:91e8%4]) with mapi id 15.20.8857.025; Tue, 24 Jun 2025
|
||||||
|
16:32:54 +0000
|
||||||
|
From: pradeep kumar <pradeepkumar200w@outlook.com>
|
||||||
|
Subject: Re: looking for SEO
|
||||||
|
Thread-Topic: looking for SEO
|
||||||
|
Thread-Index: AQHbzu/TjqB39zoRVEylLcucXwKa6LQSrIR3
|
||||||
|
Date: Tue, 24 Jun 2025 16:32:53 +0000
|
||||||
|
Message-ID:
|
||||||
|
<TYZPR03MB664557FE3274D64FE1ECD7E68E78A@TYZPR03MB6645.apcprd03.prod.outlook.com>
|
||||||
|
References:
|
||||||
|
<TYZPR03MB664536B9DBBAFC2DA2CE219D8E64A@TYZPR03MB6645.apcprd03.prod.outlook.com>
|
||||||
|
In-Reply-To:
|
||||||
|
<TYZPR03MB664536B9DBBAFC2DA2CE219D8E64A@TYZPR03MB6645.apcprd03.prod.outlook.com>
|
||||||
|
Accept-Language: en-US
|
||||||
|
Content-Language: en-US
|
||||||
|
X-MS-Has-Attach:
|
||||||
|
X-MS-TNEF-Correlator:
|
||||||
|
msip_labels:
|
||||||
|
x-ms-publictraffictype: Email
|
||||||
|
x-ms-traffictypediagnostic: TYZPR03MB6645:EE_|SI6PR03MB8610:EE_
|
||||||
|
x-ms-office365-filtering-correlation-id: 5e83163c-354d-4afc-94d8-08ddb33cc499
|
||||||
|
x-ms-exchange-slblob-mailprops:
|
||||||
|
quCBMN2EvO+3eCjVWNsab2y9cp2aqTLRnXY+dn1RYtLfTylnz7C2Qkw6tbRplokq3/dPa42yatAHZ9P7kIhqgd/w/mEuMzD0r1gJWelOszPqMWzVG8xJMrFua0N6edu9IX//6js6WX+P6WGC1rN926Tnqcmu3Fdq3UobezgXrE8O+jcUWw7f+kGwg7pldDihFU17job4OTxnOUZ+rc5K3hLhfREcgIqGmIsSp2OKcpFRwTQRtwehiye7s3ltvPz2/sk+qcZ9zVtdzBDG3exz8q3yENEoFRBD96woBm338Lo3tsYNxBW8iC49fqrzGGFoU2SbRJ6o/WXvBl4BFCdmdMdb4on5q/7pW+lFjwe3qBisFGtQus+Iir4T/wIXtXwqPjVDjBPcPRNh0PkUesuvF+rpmylqmwOg9uyRRRH8lZt42w3dT/ex1c+SIEZgc1MkYrR/Xkb38lObSg48puyTiFPq71Y7QgDevTyinebLAT0zxH5f9G42gSlTVjHW67HZp0vFxJRfbRNMYdFrSRf9XWCLgP76ZKAB9h1rfRNNbAAWbSCBZ1DfTgqWh7L2Hx4pEooyqVUWr2sS/r+ME0p+YBqpVGPozRTWQ488yZB7nLSAfwJh4U9zshPL4ZzKX++H5wzrCpy1HxeBks2JdrG66ZyGqeaCMSNJs29RGGQlH0nJI63ERVYDM2EZN5a9gioUk93HpJvcELXh2ttXhxeeug==
|
||||||
|
x-microsoft-antispam:
|
||||||
|
BCL:0;ARA:14566002|19110799006|15030799003|461199028|8060799009|8062599006|15080799009|36102599003|56899033|102099032|51005399003|39105399003|39145399003|40105399003|440099028|41105399003|3412199025|12091999003;
|
||||||
|
x-microsoft-antispam-message-info:
|
||||||
|
=?Windows-1252?Q?6J4jUzURB6tFqlRVNH94rwJMw2iJ1lkKRN7XCPMOHnTDiUwW7gR2PKoS?=
|
||||||
|
=?Windows-1252?Q?t+OSqOP7fg7W8pKb0xgzBKMnj+OU0WmGw+ckciCo7XvoRkjTI/zFdWrB?=
|
||||||
|
=?Windows-1252?Q?i7svyWUJQX7d8gfW1eJn4TvGwS6XpIx+ByvDoXXU5EkDS0ziAtDVyQk6?=
|
||||||
|
=?Windows-1252?Q?Om9gS+ygZAIgj6XUEl82JXJB1M94Lr83KTfnCZtJOuOpFedx0BYpkRK+?=
|
||||||
|
=?Windows-1252?Q?k/g12DCoQPKI9tTiDGY1a4yzmUt5u8UREH8nnXkMnGnztPV5Dm4aOhL9?=
|
||||||
|
=?Windows-1252?Q?SSH5GuzhISqUpubAAg97sBMRHYn/mvdgall7S3te6NmAo2qnfxGLHmdi?=
|
||||||
|
=?Windows-1252?Q?mOc9g61j67VJjEr19oqg0eFLyymsLHVZD2EodZ21fPvjyX3G0YfAMAZs?=
|
||||||
|
=?Windows-1252?Q?oLfZLbnZxnI2ZSrS8VhNwaUIn5X+Xh8Ij0qHG7mogT4kW69/trteRK2I?=
|
||||||
|
=?Windows-1252?Q?U14mJi4rSXGQSg4Mywt0XTj8NGDDCBqVy8OKhmt3BTW5L0//cxi8T5gk?=
|
||||||
|
=?Windows-1252?Q?wkkdakJThVu71Ome6CceNGcvVEvKpHXNv6LVpdpjhSliLgyFvJ+Ds1bB?=
|
||||||
|
=?Windows-1252?Q?PstQ+nrQ+dhNcLU2bQFQ2WlrC1uZpkzmRwZM6d3PYuiE8ULmQwROLwDc?=
|
||||||
|
=?Windows-1252?Q?retFeJPpbsnM4wF0bUWHUxnPwc2ZuCbJK0RsW5UpiHljWBrZC53Z6I7v?=
|
||||||
|
=?Windows-1252?Q?f/MU8ksCYqhtM87PLgI93sh9RqYiy1mlTnLZrVeqrTcoch/lJDIn7clE?=
|
||||||
|
=?Windows-1252?Q?DBsq85olQsdTXWydHoRpMT2yXfEmusPnsT095UjTac4sPrmmOvkWMHtH?=
|
||||||
|
=?Windows-1252?Q?3iZc7rGkEPwS5xZzc1SEjhhq1bcDr3PumMj4iwMGwi7yDySg43+P9KAo?=
|
||||||
|
=?Windows-1252?Q?kxl4q/hJzKFzYKjd+Dj9BTCYgSXKFPVs8lBQCqhGc2G9i/KJ2fz23FzV?=
|
||||||
|
=?Windows-1252?Q?5xRQfvqY8559BtNsw9nNEYXf/UifdiXk9JCHinJBnP4N7SYUD1CnSsIV?=
|
||||||
|
=?Windows-1252?Q?xNi0UnC708APiOcnZtBRmQlSYA2swpRmEwwifP50p5TOSNlyY9RsPWkd?=
|
||||||
|
=?Windows-1252?Q?CddIMxRNkRrr1qaxjGSAmq5D4JXg+vMK1Pul3yMak3nKIa/do9KFTJUT?=
|
||||||
|
=?Windows-1252?Q?m4yZyhP0OFQaoAr+Fqjt6pstOCnI3PUCKJjjeWYonAOOpYs3b3KuVHMu?=
|
||||||
|
=?Windows-1252?Q?UcOSYfbs9j1RM2em9dQeBTrxwJT2FLgCGSk/UF8sRx0ci0Xp1tFwNf13?=
|
||||||
|
=?Windows-1252?Q?gA9pSGd13kW131UgDOxVskJe6s2G+hyYP1oWllkcXsW96r97nBzITeXd?=
|
||||||
|
=?Windows-1252?Q?10M6LX2/ry6EJLzdOFgDyukxIB7CiwOeALMUWLSmXrQLG79DAEWASGQS?=
|
||||||
|
=?Windows-1252?Q?22fQCAiLKSxr/U75SPJgTtmyuLWruNhHkcztfmC55ufVT74e1dkPv4+6?=
|
||||||
|
=?Windows-1252?Q?54+uJ8OM3VTw2Ap64gpvrg=3D=3D?=
|
||||||
|
x-ms-exchange-antispam-messagedata-chunkcount: 1
|
||||||
|
x-ms-exchange-antispam-messagedata-0:
|
||||||
|
=?Windows-1252?Q?HvICyUpSWzy/QQbj/+YiOsKmtmSeA6C1F2LfQHbLpAkHXo1Fi3V0s/FD?=
|
||||||
|
=?Windows-1252?Q?jixCBxRrR4Gh1GTFIXOG44mplT+iFXvsWxQkZSOcQqFRTuWaA8WHK56q?=
|
||||||
|
=?Windows-1252?Q?aKupnpXB5oUCQ2F6h4Sy0sHj+RrLqO8vP3AUV1QIWM+qHEYp7oNbR6ss?=
|
||||||
|
=?Windows-1252?Q?ZrBV+O6lgiFw/3TLHsJbcwg/uP7Fkyb/37YpYAQPyO3H8RA39KL2468L?=
|
||||||
|
=?Windows-1252?Q?mRL+MZuzOKNINqdeGycHVRuZcQJ2mdwtNB0gvkMiDFm/1F4Dgpwi2y0H?=
|
||||||
|
=?Windows-1252?Q?f5v/mqpRkrQMdJQfQxDBigbT59Mkl+tbiGgALNXgr0uTpzKxf1juxbqD?=
|
||||||
|
=?Windows-1252?Q?7oKa0/uyDqu+ukCd5r78iYOi0KcDpYQvYuqtgU65+Vzk2vzEaKRXVbRK?=
|
||||||
|
=?Windows-1252?Q?aSDs+VGHfhVXi5bpT4BtedYpW9Hr17EooUqbIVhz1t/jLJmE6CJIY5/r?=
|
||||||
|
=?Windows-1252?Q?rzarSaeCppNqyCs23W/ujPAFTWIxkSvs3EfDGfY6rJIg58cVjfQbvDJP?=
|
||||||
|
=?Windows-1252?Q?I43PgcmkkKZ6vMcFvrXyHPMI2g7QN74GaavTRXmWb6JB3zUeSgLcMjZC?=
|
||||||
|
=?Windows-1252?Q?SB60vJ2/RvMb0PXPtF8pFGE4kqkkzN1krIpabjEYzEp+lxd+dSzpiww+?=
|
||||||
|
=?Windows-1252?Q?UW64TNQX/tPHUehtPFg1UqMzJhY+eQHftbkuwS6GNioYXIRU7cvMzTxB?=
|
||||||
|
=?Windows-1252?Q?VAeLVN2YlJZUyT7pHAZ/5DnlWVv9AD4aqAFAjyuUi9rpHDb1kL6VkTcU?=
|
||||||
|
=?Windows-1252?Q?XgftUf798WAtZ9WlEuu+dBMY7P7fPpc9ysZ5jqfIq20Sb85pOA576qz5?=
|
||||||
|
=?Windows-1252?Q?rZXlfw9Oh6a7pfblNtY+aZWZJuVBTcIJUk8T7QenkPpLkyoNVXX9+5Bk?=
|
||||||
|
=?Windows-1252?Q?Ypsle/vbfGvMRwy9l+kP22eF0B+Q8qfp4hdKzEeflQ0j/8ceKZwwMcbp?=
|
||||||
|
=?Windows-1252?Q?YKzkn+JD2EiH6gDVWS31fj8D7qAo6/W6tx7X3P8RtQPRLcnyqtBz5bT3?=
|
||||||
|
=?Windows-1252?Q?EK1KPhICBx8X9fXeXnk3/SCpjYWorfsEN3Q2JM6mEwdI1mLdRSKCjBXs?=
|
||||||
|
=?Windows-1252?Q?EZn5k6tEQFa8iswi13FVVq3HJzZ0BfugfPyYeTLHcs6gLY3D8dXYGSdm?=
|
||||||
|
=?Windows-1252?Q?Ba5XjxVPILungkIbzkay46gJwmdPn1cBbODEX2PgupopjBtJM6pOFR6J?=
|
||||||
|
=?Windows-1252?Q?nItgP849IbQwRBYFlmnHT3X+xsj3Y6kC68GSlsQZ6DcpQNTxRTR22I6w?=
|
||||||
|
=?Windows-1252?Q?bQI+g3OJy4+9deM+KmDEAenkjnxV9dDqSjnqldF0hjsxJtBu9fcdUmqZ?=
|
||||||
|
=?Windows-1252?Q?mHkUh86zS+A5kscX6tVEwF8s3xsCyPp5cT0LVId339F/dazlH0nxsaxx?=
|
||||||
|
=?Windows-1252?Q?hEpnXiVCjHFS6LUePkgeZHc4KFDQUeZEXeKqZaPZRhc/U7lKrtLC1B5a?=
|
||||||
|
Content-Type: multipart/alternative;
|
||||||
|
boundary="_000_TYZPR03MB664557FE3274D64FE1ECD7E68E78ATYZPR03MB6645apcp_"
|
||||||
|
MIME-Version: 1.0
|
||||||
|
X-OriginatorOrg: outlook.com
|
||||||
|
X-MS-Exchange-CrossTenant-AuthAs: Internal
|
||||||
|
X-MS-Exchange-CrossTenant-AuthSource: TYZPR03MB6645.apcprd03.prod.outlook.com
|
||||||
|
X-MS-Exchange-CrossTenant-RMS-PersistedConsumerOrg: 00000000-0000-0000-0000-000000000000
|
||||||
|
X-MS-Exchange-CrossTenant-Network-Message-Id: 5e83163c-354d-4afc-94d8-08ddb33cc499
|
||||||
|
X-MS-Exchange-CrossTenant-originalarrivaltime: 24 Jun 2025 16:32:53.3821
|
||||||
|
(UTC)
|
||||||
|
X-MS-Exchange-CrossTenant-fromentityheader: Hosted
|
||||||
|
X-MS-Exchange-CrossTenant-id: 84df9e7f-e9f6-40af-b435-aaaaaaaaaaaa
|
||||||
|
X-MS-Exchange-CrossTenant-rms-persistedconsumerorg: 00000000-0000-0000-0000-000000000000
|
||||||
|
X-MS-Exchange-Transport-CrossTenantHeadersStamped: SI6PR03MB8610
|
||||||
|
|
||||||
|
--_000_TYZPR03MB664557FE3274D64FE1ECD7E68E78ATYZPR03MB6645apcp_
|
||||||
|
Content-Type: text/plain; charset="Windows-1252"
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
Hi,
|
||||||
|
Want to rank higher on Google and get more traffic? I offer budget-friendly=
|
||||||
|
SEO services including keyword optimization and content strategy.
|
||||||
|
Let=92s grow your online presence. May I send you a quote& price? If intere=
|
||||||
|
sted.
|
||||||
|
Reply if you'd like to see recent results.
|
||||||
|
Best,
|
||||||
|
Pradeep Kumar,
|
||||||
|
|
||||||
|
|
||||||
|
--_000_TYZPR03MB664557FE3274D64FE1ECD7E68E78ATYZPR03MB6645apcp_
|
||||||
|
Content-Type: text/html; charset="Windows-1252"
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3DWindows-1=
|
||||||
|
252">
|
||||||
|
<style type=3D"text/css" style=3D"display:none;"> P {margin-top:0;margin-bo=
|
||||||
|
ttom:0;} </style>
|
||||||
|
</head>
|
||||||
|
<body dir=3D"ltr">
|
||||||
|
<div class=3D"elementToProof" style=3D"font-family: Aptos, Aptos_EmbeddedFo=
|
||||||
|
nt, Aptos_MSFontService, Calibri, Helvetica, sans-serif; font-size: 12pt; c=
|
||||||
|
olor: rgb(0, 0, 0);">
|
||||||
|
Hi,<br>
|
||||||
|
Want to rank higher on Google and get more traffic? I offer budget-friendly=
|
||||||
|
SEO services including keyword optimization and content strategy.</div>
|
||||||
|
<div id=3D"divRplyFwdMsg"></div>
|
||||||
|
<div style=3D"direction: ltr; text-align: left; text-indent: 0px; line-heig=
|
||||||
|
ht: 1.8; margin: 0cm 0cm 0.0001pt; font-family: Aptos, Aptos_EmbeddedFont, =
|
||||||
|
Aptos_MSFontService, Calibri, Helvetica, sans-serif; font-size: 12pt; color=
|
||||||
|
: rgb(0, 0, 0);">
|
||||||
|
Let=92s grow your online presence. May I send you a quote& price? =
|
||||||
|
If interested.<br>
|
||||||
|
Reply if you'd like to see recent results.<br>
|
||||||
|
Best,</div>
|
||||||
|
<div style=3D"direction: ltr; text-align: left; text-indent: 0px; line-heig=
|
||||||
|
ht: normal; margin: 0cm 0cm 5pt; font-family: Aptos, Aptos_EmbeddedFont, Ap=
|
||||||
|
tos_MSFontService, Calibri, Helvetica, sans-serif; font-size: 12pt; color: =
|
||||||
|
rgb(0, 0, 0);">
|
||||||
|
Pradeep Kumar,</div>
|
||||||
|
<div style=3D"direction: ltr; font-family: Aptos, Aptos_EmbeddedFont, Aptos=
|
||||||
|
_MSFontService, Calibri, Helvetica, sans-serif; font-size: 12pt; color: rgb=
|
||||||
|
(0, 0, 0);">
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
--_000_TYZPR03MB664557FE3274D64FE1ECD7E68E78ATYZPR03MB6645apcp_--
|
||||||
68
ses-lambda-nodejs/eml/4.eml
Normal file
68
ses-lambda-nodejs/eml/4.eml
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
Return-Path: <andreas.knuth@gmail.com>
|
||||||
|
Received: from mail-lj1-f178.google.com (mail-lj1-f178.google.com [209.85.208.178])
|
||||||
|
by inbound-smtp.us-east-2.amazonaws.com with SMTP id 36n4254ephirfcpq0addqo7tfme3fgond15tfq81
|
||||||
|
for info1@bizmatch.net;
|
||||||
|
Mon, 16 Jun 2025 23:42:27 +0000 (UTC)
|
||||||
|
X-SES-Spam-Verdict: PASS
|
||||||
|
X-SES-Virus-Verdict: PASS
|
||||||
|
Received-SPF: pass (spfCheck: domain of _spf.google.com designates 209.85.208.178 as permitted sender) client-ip=209.85.208.178; envelope-from=andreas.knuth@gmail.com; helo=mail-lj1-f178.google.com;
|
||||||
|
Authentication-Results: amazonses.com;
|
||||||
|
spf=pass (spfCheck: domain of _spf.google.com designates 209.85.208.178 as permitted sender) client-ip=209.85.208.178; envelope-from=andreas.knuth@gmail.com; helo=mail-lj1-f178.google.com;
|
||||||
|
dkim=pass header.i=@gmail.com;
|
||||||
|
dmarc=pass header.from=gmail.com;
|
||||||
|
X-SES-RECEIPT: AEFBQUFBQUFBQUFIK25KdWc1UzBCdy9QZGZFOVhYSjZMcGF4THdYenQzSjdWV0V4ODRPTGpkRjVmMXhPeS9YSjQvNmY3UjJ2T1A5blEyVURLQy82aGFycXBXdVRtOXJLdzFRajZPNzloRHFUdzZGVWtocXB6aHBXZG0vQVQza0lLbVhZZjJ6c01tdWg5cTZyRWgwQ0tJSS9hV2lBOHhvUWZONDM4emVkcldnampNSStya3pIS2VUK2g5QW4vK1p2ZXFmSnAxK0M4SU55bkMwajg4Q2RHVDk5a0hnUjFKRFNoQkQ3WGNLMVMrMjdBTTcyZW9mN1RSbEx5cmQ4Zjgzd2ZtWVc2UVNwSEt5UzlNbGtKTUhiYUlUWlNaQ2d0MlFhWGtkOTlLQnUvTGJ4QzhpUGFXeFI0cUE9PQ==
|
||||||
|
X-SES-DKIM-SIGNATURE: a=rsa-sha256; q=dns/txt; b=2C2LtDXnd2Rgahacofi6r/dkp+j6+wUNdDaHVQkdrDl8fLJdyOWE7ouFdpinT5Yj4Zqn1C7CXc4x6CfVd1iGzGA4crjxp/Saqdnl1yAmqRR7CYhnLqN5JYRU+s2PLeq2aGHyhqMKsExzKEwP6TKGZ8z+8j3o17zKDzP2frrjExo=; c=relaxed/simple; s=ndjes4mrtuzus6qxu3frw3ubo3gpjndv; d=amazonses.com; t=1750117347; v=1; bh=GhseSXuGN9tgXhkzyME5Vy3B+ORZlqe0/mkrwVjb1gs=; h=From:To:Cc:Bcc:Subject:Date:Message-ID:MIME-Version:Content-Type:X-SES-RECEIPT;
|
||||||
|
Received: by mail-lj1-f178.google.com with SMTP id 38308e7fff4ca-32b3c889cddso4475251fa.1
|
||||||
|
for <info1@bizmatch.net>; Mon, 16 Jun 2025 16:42:26 -0700 (PDT)
|
||||||
|
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
|
||||||
|
d=gmail.com; s=20230601; t=1750117345; x=1750722145; darn=bizmatch.net;
|
||||||
|
h=to:subject:message-id:date:from:mime-version:from:to:cc:subject
|
||||||
|
:date:message-id:reply-to;
|
||||||
|
bh=GhseSXuGN9tgXhkzyME5Vy3B+ORZlqe0/mkrwVjb1gs=;
|
||||||
|
b=RKJNRI5vZpIscEL5+UwfmGKNRxnvkZtv6IJzX0Y0wTSCm8DFbyGoJ+S5HbaVf8ll6Z
|
||||||
|
f2Oe1EfC1EJe/Sdw6JqtNh1L7xPtCEWhjf8eKvrYfp9OssXa7sejFW8ya4GT7SD1V/xV
|
||||||
|
bTaeZ0r1yU6ST43JuaH+cua3Peyf0AWTkB22bsllbgmlLcRCNTfx5lcMDIPTRaCvYLqK
|
||||||
|
bQxdMlCIydODUeteHgGNcj/oUXgCvbcQgFT59eX8Su7IILe2NxqlhCaKo0GTG2RNWHNY
|
||||||
|
fuEE9j0VBSvRXlKYqOY9f+IcMHvo9W7byT6voqF5EwVY6gXbAHIjc1mRu88goAJVgspv
|
||||||
|
FB5g==
|
||||||
|
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
|
||||||
|
d=1e100.net; s=20230601; t=1750117345; x=1750722145;
|
||||||
|
h=to:subject:message-id:date:from:mime-version:x-gm-message-state
|
||||||
|
:from:to:cc:subject:date:message-id:reply-to;
|
||||||
|
bh=GhseSXuGN9tgXhkzyME5Vy3B+ORZlqe0/mkrwVjb1gs=;
|
||||||
|
b=Ym5RD768CA5BxwJTdC1ML2gfY7n9j8QzCt6B/N6Aln9/RWhvldkXoiaxswQM9MyMd6
|
||||||
|
cOluMW41iLzgnB/taM1IQy+VLy4aQw8k2vfDCEo3NPS5/jFZNM9NiqXPnA0umVoHy6x/
|
||||||
|
T1HK/+f4y6hpzjS782Zl1NnNXD/EjRHvHdhe0ThisJXFsA/P4JsQNbOSSc+inou7jTOs
|
||||||
|
24pKfvdVFXByJB+YJwvt05J16W96ADjYf641C5Zxbw0jASxXZ7wS09bGBKNAwn6/NGR4
|
||||||
|
ekWyOVQHPd7fd9S4RzuFS4Xi0HSS74pAqESgMbHdfMVr5vFBOw1IRRZsaIUy1l+r0OSR
|
||||||
|
LAKw==
|
||||||
|
X-Gm-Message-State: AOJu0Yw5wp5E4tGz30/zaiSpSJ3YuK0o/gb8o/pewmIm/qmBB2rF1G0J
|
||||||
|
9VQQ5HBTezSfU1WZluorBrtWKMtP+JPti+UzsRyEvFh5eFS/zU568gNp1dtY1M0TCDlbYysk+wC
|
||||||
|
kvMu3zTkk7WNUVeLp4NTdmQF/USFdvgDt1FHWhAs=
|
||||||
|
X-Gm-Gg: ASbGnctCvyrglWpXoxufwhI/5JUawGElxa4V19xcjZZ6iMb+bkbvYeFtm4jFpa5wahx
|
||||||
|
RRIGY/LgO5e5DUn7E9TbJI0zFaIO1WEq03SFWKettycYPg4XUt/v0QQOHQBYG0r+JMQAJRK49wV
|
||||||
|
FrEpwioG5krLs5B3q2eozzGS9eu+nZ2owEcsrT3ozeYw5+
|
||||||
|
X-Google-Smtp-Source: AGHT+IE7UBkVfVljOKTDQeI6kUuT8CjiU45xCwI19ARKkhrlSiOJIb5u8llGPnqA29t1O6znR2vjpcOQjb8amyn/YNI=
|
||||||
|
X-Received: by 2002:a05:6512:23a1:b0:54f:c5e7:8f7c with SMTP id
|
||||||
|
2adb3069b0e04-553c95f649emr84676e87.16.1750117345191; Mon, 16 Jun 2025
|
||||||
|
16:42:25 -0700 (PDT)
|
||||||
|
MIME-Version: 1.0
|
||||||
|
From: Andreas Knuth <andreas.knuth@gmail.com>
|
||||||
|
Date: Mon, 16 Jun 2025 18:42:14 -0500
|
||||||
|
X-Gm-Features: AX0GCFtidkvZnLhFoACx19SzatSelvT3f9BT79l90NZuttwS1SlmZtkY52q7Pcw
|
||||||
|
Message-ID: <CADfCGta+hvpvnmUXKibbMstBOM1WJWh1WBKtM4Gf6-no5Hcn2Q@mail.gmail.com>
|
||||||
|
Subject: asd
|
||||||
|
To: info1@bizmatch.net
|
||||||
|
Content-Type: multipart/alternative; boundary="000000000000aceb630637b8f21a"
|
||||||
|
|
||||||
|
--000000000000aceb630637b8f21a
|
||||||
|
Content-Type: text/plain; charset="UTF-8"
|
||||||
|
|
||||||
|
asda
|
||||||
|
|
||||||
|
--000000000000aceb630637b8f21a
|
||||||
|
Content-Type: text/html; charset="UTF-8"
|
||||||
|
|
||||||
|
<div dir="ltr">asda</div>
|
||||||
|
|
||||||
|
--000000000000aceb630637b8f21a--
|
||||||
4573
ses-lambda-nodejs/eml/5.eml
Normal file
4573
ses-lambda-nodejs/eml/5.eml
Normal file
File diff suppressed because it is too large
Load Diff
4573
ses-lambda-nodejs/eml/6.eml
Normal file
4573
ses-lambda-nodejs/eml/6.eml
Normal file
File diff suppressed because it is too large
Load Diff
69
ses-lambda-nodejs/eml/7.eml
Normal file
69
ses-lambda-nodejs/eml/7.eml
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
Return-Path: <andreas.knuth@gmail.com>
|
||||||
|
Received: from mail-lj1-f174.google.com (mail-lj1-f174.google.com [209.85.208.174])
|
||||||
|
by inbound-smtp.us-east-2.amazonaws.com with SMTP id m3eg6h7v3pepnvk6g3mnamc5534m41rd9smb13g1;
|
||||||
|
Tue, 08 Jul 2025 15:22:02 +0000 (UTC)
|
||||||
|
X-SES-Spam-Verdict: PASS
|
||||||
|
X-SES-Virus-Verdict: PASS
|
||||||
|
Received-SPF: pass (spfCheck: domain of _spf.google.com designates 209.85.208.174 as permitted sender) client-ip=209.85.208.174; envelope-from=andreas.knuth@gmail.com; helo=mail-lj1-f174.google.com;
|
||||||
|
Authentication-Results: amazonses.com;
|
||||||
|
spf=pass (spfCheck: domain of _spf.google.com designates 209.85.208.174 as permitted sender) client-ip=209.85.208.174; envelope-from=andreas.knuth@gmail.com; helo=mail-lj1-f174.google.com;
|
||||||
|
dkim=pass header.i=@gmail.com;
|
||||||
|
dmarc=pass header.from=gmail.com;
|
||||||
|
X-SES-RECEIPT: AEFBQUFBQUFBQUFFZmNMTjFMdDNIZmluRHB4RlBVb2RrQmo1bGp4aDFRR1FlMHZ2TmVuMDRuZVJlUE9zb243MVpYU1NUSlNyeC9INld5TGRzbzcwWUI2TXFyYWZzNXlqUHRPVXdCZHowUE01cXg2ZEk0b2c4NzEvbWt5ajFjSHpzR0VCeXFWYm45ZGJLdkVkelZJUnBXc0lWeWpGYzZQcWhic3c5UE9nRmd4V0RwRlEvdVQ4M3lyMW94QkJJRS8yWVFQYUt2aG1sUFFqWmRacyt5YUJnZlcrbTBvb0tYZ21vamRka2YvVzNwVTgxU3dJVDI2VzByUWQydENHRzY2cUtlTXRsOC9maWxqRFdPUHAyQWJNbWtaWC9kRjhET0Z1d2QrVVU4NWFsbUNVb3I1RkVZVjlORnc9PQ==
|
||||||
|
X-SES-DKIM-SIGNATURE: a=rsa-sha256; q=dns/txt; b=Ns7PD33ZcMOuMImq8RydmnQQiPQOcMqRNA6+WBVEqOzrnT8Fr/6ovI6LcNjuLMaNlTyzNW6redDOvY1Fnw9d8PS+R7nDf6/0TVG2sMVeAD0BAfFRbIvFxa1ptoIC/A5DwuS1LrezTIBf2eqYvUaT8ezhh0RFhHPlwNfhMBT28ng=; c=relaxed/simple; s=ndjes4mrtuzus6qxu3frw3ubo3gpjndv; d=amazonses.com; t=1751988122; v=1; bh=eKB/IyUxfA241zhlmu1r99BcuvAQxKeIb9t6YTUDyU4=; h=From:To:Cc:Bcc:Subject:Date:Message-ID:MIME-Version:Content-Type:X-SES-RECEIPT;
|
||||||
|
Received: by mail-lj1-f174.google.com with SMTP id 38308e7fff4ca-32eaaa0e501so896071fa.1;
|
||||||
|
Tue, 08 Jul 2025 08:22:01 -0700 (PDT)
|
||||||
|
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
|
||||||
|
d=gmail.com; s=20230601; t=1751988120; x=1752592920; darn=bizmatch.net;
|
||||||
|
h=cc:to:subject:message-id:date:from:mime-version:from:to:cc:subject
|
||||||
|
:date:message-id:reply-to;
|
||||||
|
bh=eKB/IyUxfA241zhlmu1r99BcuvAQxKeIb9t6YTUDyU4=;
|
||||||
|
b=hvsD5eoGlDNXtRwmmBnxsoBSom1j9nCrIed0UHM+KzN0PhnpithS65y5TmC9Uhqq6z
|
||||||
|
e18tVyrn7pSNkDhg5an3t0lRYtqH/9THaIr/a6iKUsU1cBLY0YnnMx2pNphCEDlJD/2N
|
||||||
|
IpGp5B+/ufie5DVpBZM6cKaH9yhnsM90jzos9/epxfG3uewYbqmWxpl9WSae9PHIkrkq
|
||||||
|
PCsQLYmrU6VlkkvZMxAAg1Czls20bknmjYmB4xgwFhYaYtYW1++TFsCe8F+OXjwa57eY
|
||||||
|
RxhHoYNWl1FVGrN8+eIvCFpdDqDJU/LBoOAk3DJVUj6yEOFQ75fivYd+Ed2HB/Kyl1W7
|
||||||
|
EIRA==
|
||||||
|
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
|
||||||
|
d=1e100.net; s=20230601; t=1751988120; x=1752592920;
|
||||||
|
h=cc:to:subject:message-id:date:from:mime-version:x-gm-message-state
|
||||||
|
:from:to:cc:subject:date:message-id:reply-to;
|
||||||
|
bh=eKB/IyUxfA241zhlmu1r99BcuvAQxKeIb9t6YTUDyU4=;
|
||||||
|
b=qpc7/ecpUw57eIdLO2aVmUr5zxZ5RGQb3pqwoiNjUYJQrlDSiYYuhl7o3KjwKcQHTw
|
||||||
|
TBJ2AjAx/K5hpaRAcXdcHSSHwva/oM1OZtFKjPkwEkselRnumSV1x+tqrDcwP4x+Fkwi
|
||||||
|
Wi4Fg5fIXpuqDTvzFBCpaypQUszQFbKIdiNaLUpR2i5uMzeaPWvHXbzQ9Cd5dW+A0gaX
|
||||||
|
Ep2U+DqipArnieyA2osXjYqO6Sm9T7/qsI7SzqjmEM68FgVarKbACFMBRckR2fcH4otl
|
||||||
|
+4EO2NxDmOJFVYsdAlGZn6RUYrNBjGsg+fFsInwJSnYmAbcZpsxFQid8iW219vvAqOwj
|
||||||
|
yA6w==
|
||||||
|
X-Forwarded-Encrypted: i=1; AJvYcCUgL0vwGzGasuMiUeV58XUd9D4p/+SA8gUHmcJBGicjs4HKE0oXxha/ZpAaHekZMhGJ9D18@bizmatch.net
|
||||||
|
X-Gm-Message-State: AOJu0YzQdxzi0FXHJZ5NKpf/4xZmJQ67KmS9/HXwRrA7GaTn4Z6mPlmd
|
||||||
|
YcqUn88N0jED8g0HP9et9mGxd6JVpyl3VhHO9900xd/cqdGrAHoRr+Tz2CwiTXRV7PqY8DInS8A
|
||||||
|
rVnHukdGlNItxRA2zX9HqRpusEfjceDnlaBev53Uj+w==
|
||||||
|
X-Gm-Gg: ASbGncuDfEE0HlBTh/2RaT7usIau+7Mp8M5Kbyn2LH1jUA9C1q95IxyMto5pvkcB64c
|
||||||
|
CjTP8ehNu2dFwnO3fO9m4q5T6i2mP57aW9+MHLZ/AS+UzjUfo/2BMDBn6+rkKLycfFBgxMagGJ0
|
||||||
|
Kv/XyhtuVMPEPYuLIh2PIB+J3rHuDCWDsnRutU1RD6pYLo
|
||||||
|
X-Google-Smtp-Source: AGHT+IEwHFLwKEd/mVb0yu/gvz3kxVGxEfuH91RsHqyFGR23Pb/RqmQNz0/9QGS4A9tGFPXpXUiJ3k99rBUg8n3wjz8=
|
||||||
|
X-Received: by 2002:a2e:bb81:0:b0:32f:3e83:4389 with SMTP id
|
||||||
|
38308e7fff4ca-32f3e834936mr1827791fa.7.1751988119805; Tue, 08 Jul 2025
|
||||||
|
08:21:59 -0700 (PDT)
|
||||||
|
MIME-Version: 1.0
|
||||||
|
From: Andreas Knuth <andreas.knuth@gmail.com>
|
||||||
|
Date: Tue, 8 Jul 2025 10:21:51 -0500
|
||||||
|
X-Gm-Features: Ac12FXy8RY9akza8W7SQgEoROiDhMjmnSP8GeIafKiGU1XrBvx8uVbFKIO6dq08
|
||||||
|
Message-ID: <CADfCGtbc-MqHYF=_1BihROBqd07wjnWKBRaR=9v0AODyCUPnwg@mail.gmail.com>
|
||||||
|
Subject: test
|
||||||
|
To: info1@bizmatch.net
|
||||||
|
Cc: support@bizmatch.net, info@bizmatch.net
|
||||||
|
Content-Type: multipart/alternative; boundary="00000000000088252706396c85b3"
|
||||||
|
|
||||||
|
--00000000000088252706396c85b3
|
||||||
|
Content-Type: text/plain; charset="UTF-8"
|
||||||
|
|
||||||
|
werewr
|
||||||
|
|
||||||
|
--00000000000088252706396c85b3
|
||||||
|
Content-Type: text/html; charset="UTF-8"
|
||||||
|
|
||||||
|
<div dir="ltr">werewr</div>
|
||||||
|
|
||||||
|
--00000000000088252706396c85b3--
|
||||||
616
ses-lambda-nodejs/eml/nodemailer.eml
Normal file
616
ses-lambda-nodejs/eml/nodemailer.eml
Normal file
@@ -0,0 +1,616 @@
|
|||||||
|
Delivered-To: andris.reinman@gmail.com
|
||||||
|
Received: by 10.28.50.2 with SMTP id y2csp233403wmy;
|
||||||
|
Thu, 13 Oct 2016 04:39:49 -0700 (PDT)
|
||||||
|
X-Received: by 10.25.37.18 with SMTP id l18mr9511740lfl.88.1476358789184;
|
||||||
|
Thu, 13 Oct 2016 04:39:49 -0700 (PDT)
|
||||||
|
Return-Path: <SRS0=63fc=V7=kreata.ee=andris@srs1.zonevs.eu>
|
||||||
|
Received: from srs1.zonevs.eu (srs1.zonevs.eu. [217.146.68.191])
|
||||||
|
by mx.google.com with ESMTPS id l202si1012799lfg.293.2016.10.13.04.39.49
|
||||||
|
for <andris.reinman@gmail.com>
|
||||||
|
(version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128);
|
||||||
|
Thu, 13 Oct 2016 04:39:49 -0700 (PDT)
|
||||||
|
Received-SPF: pass (google.com: best guess record for domain of srs0=63fc=v7=kreata.ee=andris@srs1.zonevs.eu designates 217.146.68.191 as permitted sender) client-ip=217.146.68.191;
|
||||||
|
Authentication-Results: mx.google.com;
|
||||||
|
dkim=pass header.i=@srs1.zonevs.eu;
|
||||||
|
spf=pass (google.com: best guess record for domain of srs0=63fc=v7=kreata.ee=andris@srs1.zonevs.eu designates 217.146.68.191 as permitted sender) smtp.mailfrom=SRS0=63fc=V7=kreata.ee=andris@srs1.zonevs.eu;
|
||||||
|
dmarc=fail (p=NONE dis=NONE) header.from=kreata.ee
|
||||||
|
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=srs1.zonevs.eu;
|
||||||
|
q=dns/txt; s=oct2016; bh=xKHKChGY0vTH8NsecmXwA0OqbinKOXeQbaC2UYp2BAM=;
|
||||||
|
h=from:subject:date:message-id:to:mime-version:content-type;
|
||||||
|
b=Ve18ogdCAG+7WZYkJPOewe1hKjhN4k9unz7bVHMXd6+1CQDRUkLCArQZJzSKxkM481nzXfjFn
|
||||||
|
bI8qOuQL8mRk/8fAjYhxLgnr/3SyVIOhCnXxjdQkRzgouZyl42hqD0gIaCxu9uodtQrp2pbKvyl
|
||||||
|
e+3sG+LhcdJmsPguOfILn14j+irinPSWrospC8PBIDTsUwO8DCyPqSlOADbW0B6TRUHWMf4XUX4
|
||||||
|
W8TH61H1ZI3Xu3k0bvX7rsGHZjsy8dcshcnfYENLCLep8fsQMaB15EErc3RXycBX7CBd0iU1l50
|
||||||
|
pYpUFd6bZehCF0ipTOgA7IJ7ZPafaH0YTU8wRntXOwbg==
|
||||||
|
Received: from host29.guest.zone.eu [217.146.66.6]
|
||||||
|
by srs1.zonevs.eu (ZoneMTA Forwarder) with ESMTP id 157bdd754f70005750.002
|
||||||
|
for <andris.reinman@gmail.com>;
|
||||||
|
Thu, 13 Oct 2016 11:39:48 +0000
|
||||||
|
Content-Type: multipart/mixed;
|
||||||
|
boundary="----sinikael-?=_1-14763587882000.8241290969717285"
|
||||||
|
X-Laziness-Level: 1000
|
||||||
|
From: Andris Kreata <andris@kreata.ee>
|
||||||
|
To: Andris Reinman <andris+123@kreata.ee>, andris.reinman@gmail.com
|
||||||
|
Subject: Nodemailer is unicode friendly =?UTF-8?Q?=E2=9C=94?=
|
||||||
|
(1476358788189)
|
||||||
|
Message-ID: <012d606e-3550-2d94-b566-6cd996de88e3@kreata.ee>
|
||||||
|
X-Mailer: nodemailer (2.6.0; +http://nodemailer.com/;
|
||||||
|
SMTP/2.7.2[client:2.12.0])
|
||||||
|
Date: Thu, 13 Oct 2016 11:39:48 +0000
|
||||||
|
MIME-Version: 1.0
|
||||||
|
X-Zone-Spam-Resolution: no action
|
||||||
|
X-Zone-Spam-Status: No, score=0.408099, required=15, tests=[MIME_GOOD=-0.1,
|
||||||
|
R_MISSING_CHARSET=2.5, DMARC_POLICY_SOFTFAIL=0.1, MIME_UNKNOWN=0.1,
|
||||||
|
R_DKIM_NA=0, BAYES_HAM=-2.1919]
|
||||||
|
X-Original-Sender: andris@kreata.ee
|
||||||
|
X-Zone-Forwarded-For: andris@kreata.ee
|
||||||
|
X-Zone-Forwarded-To: andris.reinman@gmail.com
|
||||||
|
|
||||||
|
------sinikael-?=_1-14763587882000.8241290969717285
|
||||||
|
Content-Type: multipart/alternative;
|
||||||
|
boundary="----sinikael-?=_2-14763587882000.8241290969717285"
|
||||||
|
|
||||||
|
------sinikael-?=_2-14763587882000.8241290969717285
|
||||||
|
Content-Type: text/plain
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
Hello to myself! http://www.nodemailer.com/
|
||||||
|
------sinikael-?=_2-14763587882000.8241290969717285
|
||||||
|
Content-Type: text/watch-html
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
<b>Hello</b> to myself
|
||||||
|
------sinikael-?=_2-14763587882000.8241290969717285
|
||||||
|
Content-Type: multipart/related; type="text/html";
|
||||||
|
boundary="----sinikael-?=_5-14763587882000.8241290969717285"
|
||||||
|
|
||||||
|
------sinikael-?=_5-14763587882000.8241290969717285
|
||||||
|
Content-Type: text/html
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
<p><b>Hello</b> to myself <img src=3D"cid:note@example.com"/></p><p>Here's =
|
||||||
|
a nyan cat for you as an embedded attachment:<br/><img =
|
||||||
|
src=3D"cid:nyan@example.com"/></p>
|
||||||
|
------sinikael-?=_5-14763587882000.8241290969717285
|
||||||
|
Content-Type: image/png; name=image.png
|
||||||
|
Content-ID: <note@example.com>
|
||||||
|
Content-Disposition: attachment; filename=image.png
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lE
|
||||||
|
QVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQ
|
||||||
|
AAAAAElFTkSuQmCC
|
||||||
|
------sinikael-?=_5-14763587882000.8241290969717285
|
||||||
|
Content-Type: image/gif; name="nyan cat =?UTF-8?Q?=E2=9C=94=2Egif?="
|
||||||
|
Content-ID: <nyan@example.com>
|
||||||
|
Content-Disposition: attachment;
|
||||||
|
filename*0*=utf-8''nyan%20cat%20%E2%9C%94.gif
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
R0lGODlh9AFeAaIHAAAAAP+Z/5mZmf/Mmf8zmf+Zmf///wAAACH/C05FVFNDQVBFMi4wAwEAAAAh
|
||||||
|
+QQJBwAHACwAAAAA9AFeAUAD/3i63P4wykmrvTjrzbv/YCiOZGmeaKqubOu+cCzPdG3fQK7vfO//
|
||||||
|
wKBwSCwaj8ikcsks3p7QqFTSrFqv2Kx2yxVOv+AwiTgom8/otHrNbrvf8Lh8Tq/b7/j4UMzv04x5
|
||||||
|
eAGDhIWGh4iJiouMjY6PkJGSk4+BdkZ+mZocgJZ1lKChoqOkpaaGnnSYm6ytVEWpZaeztAEEt7i5
|
||||||
|
urW6vQS1wI2xq67Fxp2pwcqgvs28zbjL0oXDTsbXrchtz9C509/g4eKLb8TY533abNzdt+Pv8PG1
|
||||||
|
5dbo9mHqa/L7/IhZs+280aJH5IKWewhfkXnTr6EyIwIiSpxIsWJFIw4TEcxisf+jx4nmErLKpyaj
|
||||||
|
yVMQP6qkiPEkNTdcVsqUGHJKlywjSKZxqSzgL1opZwpVSUTeRiFDk36sKeUmlpywGPIE5rNWUKVY
|
||||||
|
IxaNdzRI1q9a64EhArYjTgs60UxdK+lq2bcgnVaBS9ci0yhk64a9YjCqG7aAHbnVC1buXMJ670LR
|
||||||
|
gtiwDzjyAgYWXASx5cuYMy8V+4UxYcc9IMeTPJnRYM2oU6tWqlhTTLpcRJee5vPW6dW4c+u23DrT
|
||||||
|
a7hpz0Cq/XM2qNuZDShfzrx53t11m0tn/rxubz+/3wY3M7y28VDIMU8fXx362/HTywPnLJIB6CWy
|
||||||
|
v/MLf9nz7iFC7Vtn317BeyX/8cknD328cQQdfjPpBxt//UGlRYACEjcLgRIVYOGFGGaooYbmdZja
|
||||||
|
hiCGqOF1DXYQm1QCEiIhSpXNJOKLHHoo42Uw1pghiSWqsB0d3VWVYgAURmRjjfkhmFiL66ln0ZBD
|
||||||
|
4pgjCjvO0SNpKQYpAJMvFonUZ0hq16VHWNbo5JMmRBkLdz9a9WVq/7XJxJEFkTmDmzmcaeedeFbj
|
||||||
|
1Yx83segnHjRmeeghBYK05Z9JqramICWYCRudEYq6aSUVupUo9ghCqmlnHbq6aecYpqOpgs+hdaa
|
||||||
|
XzHaGaqp/umAlSupKqpNpCbJ16lKZiVrU6zq6moDsBL166yjglqmm8Qq1Oax/7cm6+yz0EYr7bTU
|
||||||
|
Vmvttdhmq+223Hbr7bfghivuuOSWa+656Kar7rrstuvuu/DGK68roNZr771nzesuvvz2668X+oa7
|
||||||
|
kKEEF2zwwQh31UPA1Q6c8MMQRyxxHnsw/KSZdqapMVcI72oxDn4dvPHI4iTs8cc1YAwhyYasCFA7
|
||||||
|
I+tZMcojhbwOLS6zrPM+Cv9AczY264OzdzsXzXEbJ/8cQ5RGy/fPKTmb0rMPfZmqdAZMN23c06ZE
|
||||||
|
XcrUC+Pa7NVVD7Gy1u8Eu9lWU4G9BJwzX5012vuo7VFLbR+qYFlJ64ivg2ajSLctVLKYa2Zsv+P2
|
||||||
|
Dn7Gic/fIsw9uI8T9opZ4v/jLK5D43GvWquXY08g+eBpW24evoqyNGzKBsL93tnfFP6j3anXrlvf
|
||||||
|
KewN+n+w0wbzxrTbLjybq/ORHd9b9E56y7UFP/zz0O/VeaC6+/qg4MsvQpzz0XdfO+6sV4/V6NoT
|
||||||
|
TTf3WKEn3eHeq+/co6VOD7T4rAVd0iNeG42+Uu5TB7/3E+nfcthnPfn1B1/Ky15bTFcgLJzuc2Zp
|
||||||
|
3X4c960T/UVj+QMPAysUJhABEIAdfBH4GmXBbWDQfKWwUgg39EHvrTBEI7SWyrBXPsoJSIUvxJCW
|
||||||
|
9uQ6Hu4OghTJ4YZi2DD7CQJ/KHTaBoUkRAvtEAiNWaJMKNTEGxVvXDNMhgL/TSPFCf7LMT0EAtn8
|
||||||
|
Y8RCbZGLBGzgFw0TRp/BK1ITi6McB/C/FtoxLhRUGhznyMeH1fGOdyTitf6omTUa8pCIpNMY3QPE
|
||||||
|
VlmNAvuTHsCuEUlJBqFsjUyKIOVFSEeGTlmZHMomX1BJATipkqN8Y72Y9R8ZKtJRj1wk4DzFyve4
|
||||||
|
Elmw/KQsd8nLXvryl8AMpjCHScxiGvOYyEymMpfJzGY685nQjKY0p0nNalqTmonMpjZxec0cbfOb
|
||||||
|
4LxJNy8WznKaswnj9OY518nOPKZzEw7rozznSU+ZufGdtBJCPffJz37KwYD4DF8Q/EnQghYUoAEd
|
||||||
|
QxkJdcaGvuRgqZRmFj3h/9CKmuyKCX3VQgdVUYde1J0ZFZs+E9bRhn4UoSGNwERvRrcMikJ2KbLn
|
||||||
|
JVOKtY3yaHJJLAVMBSRTMdIUAysVWktzSoqdyqen9/xpBYJ6v6HasGu/0xhSqaZUkQ6UhkUlakm3
|
||||||
|
OgjN5aCqVgVCAifhUq521KsAAOtSbToAdkTVrHBFq1ohyVa3dgOueA2AXOcKyqteMK+lu8LLnvo1
|
||||||
|
vaGUkbEcJvkA+w2u6VSrotjrWhMrzMUyVhqOzSphSSFZulI2mJa97CRQWQS2oLWUEfVWaEUbCdJi
|
||||||
|
7iSn7WKsMKrKeAqVtRpM4xODYFrDOtCLkxwL5EKw2p1tdhSlTNAQjOJbH/+uJrUe4Ncs/WpC0h1X
|
||||||
|
FMmd4nKPxobsahek1LPXdMWK1aZdN7edrM924YFWzgXXc+KNXF2Xd97jyNZWQGAu0u6L35k+LpT1
|
||||||
|
0yUEiovbRnj3h58aHnRNlN7xffYBBC7wIg6MPHspmLYw0K0mH6xR2zZVwqPlL2pQJ7wFc0KC8Xtd
|
||||||
|
eWP31tmJGJAwFhZ4F4Pi/jpmrMEw6ncoHOMWmngD9Cugiv/6Dh1v7cU9TjIeDytQK0SxTTimSour
|
||||||
|
hGQlK/nHJzheYZK34oZur8pWDnOK/ZvPGnsSC1G2bvPALOY2n5nMvAqyKLlM5K1+WcNuzjNw4Rze
|
||||||
|
39q4CmnGqU94rOdC0wT/w2X2M4LJW+caGpllhLaIAJWDZ+FN2gCVVu6MXbOFPTO6uo0oK6TZnJRL
|
||||||
|
Z7p2pm5wgJlsvE6P+dMsDTVkdRbpiqQawM+7tXP/TFVKuprXoeky8+pLslpTRNdQjDGyf/BkVqMD
|
||||||
|
gcK+rLGX7OQD4dqUZq6ws88B7UZLeNqHVnRuVG3JJjT7vSXqNqhBDCRSz1ncm9r13bK95U1jq4Sx
|
||||||
|
TpGoF4jnKhbA0Obx94WwbA9831bfsw5xv6sIcOgI3EIEfzad1x2hhPOb3FdieMN18/ACRBxQTN0J
|
||||||
|
EonNFhw2cbc+OLe8hbzyID7843IKuVpG/mjAmFyIKO+BypMNbB7IpOMw/yeTzIVD8ykfeeEn13TL
|
||||||
|
38zzRTN7JUBHtGrZKocpGX02N89hzn3OpVPPW7dRt7e5hv4JuIK73O10N7bFri+y39SsZ1972pHQ
|
||||||
|
xl7LjeoZM7valT531Eq9W3s0qODngPGN3+7v6wr84BffXJ0b/nuIV5fiGU/5NBT+8c+NfLomX/nO
|
||||||
|
0/HamM882891+Vf3/fSolxQvS9/z1Lv+9V1YPeg9Dfva257DKGO902/P+94fQfZLZzk6w/r01uuA
|
||||||
|
22o/pbuDjsXZf1fAEF6+5m3g99EfwLXbXqTu1wb9Dm//ItNvcvA3bH3so1uxzp8t7hH7fdVZ/7/t
|
||||||
|
d7+zzc9n9Bsrl60sIv83x7uEbsZXocuifwEIgFbgf6uEf7YkgPlHgFXAVw74gBAYgRI4gRRYgRZ4
|
||||||
|
gRiYgRq4gRzYgR74gSAYgiI4giRYgiZ4giiYgiq4gizYgi74gjAYgzI4gzRYgzZ4g8bkezp4LzgY
|
||||||
|
XTv4g7TUgycGhERIKUI4hEWYhPt3hJikhE5oGEyoAU84hY4RhTVFhVjIBVa4AB7meV74hRRzfi7Y
|
||||||
|
hWBYhma4ODVIhme4hmxYBtlHgmrYhnIIhm84gnE4h3hYeXUogneYh34oeHsYgn34h4TYT4EYgW4n
|
||||||
|
JewWMx0TfuOUiFW3iBtzUmL4gJAYB5I4iY34fj91iYGWiS5BifUHgZ7/GG2g2FsQ5YgGOIh5d4o8
|
||||||
|
tYmHGFCl6G2uOBmi6FMVmIhqRnKSUHO2mArMV0y6KGi+2ItX9x1TFTa5iHefWHG8GAnFCBjJyAMX
|
||||||
|
OIxOFY1W5wuM6AnBSEzWiDb7RlbHaBzTuAPVyIymuDHhaIx3pYncqIrX9I1as47QOI6zUY7HZ4Hy
|
||||||
|
aF4WJ47tKFXACI/WFGHsiI21eFSNl1QTSJD1+IwHiZD7xYk0xZDZCA0P2TSdtZDzNVgGeZGlkZES
|
||||||
|
SJFF948eyTIgiYgbCTX9WJLkmJB2p5F9aFcWyZIm6ZLKSIEiSZOEkFmjQI+QcJIDli/CmJI6CQk8
|
||||||
|
+VIrGQlAGX3rB3yB/0OLRTlhWMCR9kgJS+l9BXhMOVmURxkKPlkJNkmNxNd/WkmUUWlgU6mSDgmW
|
||||||
|
ETl/QumNZnmWUilYatmRwhCW5jiWSoBMW8lu9JdfaxFbXvd1sbh5cVmSf/kDqNhdv2Z8XzVGfQli
|
||||||
|
iekDi7kGWlZvhYkukflt0vdaJiGY8Udto/gxm1lgk9kDlakGl8l0CvkzpYlbp8kDqWl5jbl7N6lH
|
||||||
|
h+mRsbkDs4kGqyl8rUkzr8lau6kDvXkGv+lgEklKwwUCw1kaa0kZg4mZgMleeLk5h7ecLiBd8hWT
|
||||||
|
9GWXaBSa5Mdb3GWZe8d3o/kE3ElcuVk00YmW08maslmeqnmez1eJ6v/ZnB/wnJPxnuGZftpWnYpz
|
||||||
|
nTngXukJMjzYnU9JcfMInnMpnu9GntbZlgAaoAdKffrpg965iyRpCnHnmeDQXtmZmVCSoQy2ocQ4
|
||||||
|
k5UTn8A5nxPKmCzaoreZaMVHe3uplzzQjH5pn7ZZKRemnS0Qo+DXfcDSng8Zd/eZYCUmkPzHdXWX
|
||||||
|
BE0IawdHk0iqfvXyoySaZRAablk5WQuab1TKoxbqKViKnwg6fqvWgDi6AzoqmWJKnUpqO92oUluK
|
||||||
|
djfqpSMFlRdZpTJ2pUsKpCwgpPKnpnhKXWDKknzKfaBSphcqfubWdbyTjlLWoVQmqKGXOnMalPDW
|
||||||
|
o13QppqlommSqJf/ejlMqqDV9qQ34ak9WZWTIaqjqkZmiqGbOqaGoapISak39KavaqC4KFyzCqc3
|
||||||
|
JqkyuQvAo6u7Om6lyp6/Kp9OYateyaqB4arHaqMzGmdXsHO1KqxDg6tKZKnT6iGZioTLOp5opq3X
|
||||||
|
2A7S+q2YmqwaSm9pegXOyjJ3VqfqmmThyn7jGqHlqqdnNK8VWq95dq9cWJvACmjmCo5r5q0A22MC
|
||||||
|
S0ZytnVMEK8k469ourAb17DXR7DMCh8H26CDZqwW2z0Ym5zkCq8dy48fq7AhG0jseqLuWrJWILEj
|
||||||
|
Q7FNt7KjirFRehiOCQBR9pWVSq8rsWwpp2yTprI4u6ZJQK05Kqk+/5urKhu0RQu0uSG0joeqeelr
|
||||||
|
D4ueUvphjNC03Sq1H0G1TgpIYss4kBqrxfKy+mqoU6oIXrtjIHtsUfuvfFK22Gm1+Yi1aguxS8uv
|
||||||
|
hfC2Rwe2HmG3BUq0AmS0LbsCJLu2WytyjgC4WBe3ATS3FTs8hAsA2PqS9KKxMtq3DJoIkFsacXe5
|
||||||
|
PUa6Z9uoBXcvMps9H7q36lW5Wcu4wXlAqnuyHdW6+UqqsOu6squ5DaJuh2qaksulpzqiNdqnxWt6
|
||||||
|
vku79rK6y4O712ptu5u7MDu77QG8bSu8T4u8Omu8Q5ukj4q3jyl0teu3jAW9yRtvNauo3au81bot
|
||||||
|
Bse1zuig/iBF/v9ms2URdlkKchMXvMYRuoWQdS+Ev2Chv2irgCZrvoABwDtpvxpHwFhhwKhLLfHr
|
||||||
|
uD/CwIMgwCsEwVkhwb06ddejwGyBwe2GdDjHwUrhwdabLRU8c2lCwhocQiicwi+XuCLRwkT3wkkp
|
||||||
|
nRh3vzM8FCq8vNqCw2iiw/6ZCDHcQT8MxDUMqPCLjiJMwoHVww9spevLqXeLxYWrEkH8vobJij27
|
||||||
|
wy6RxGHCt1ustGa7sz/XxPuLwI17BxXpDC5mwlqntVWLxllMq9/7EV0slu+yj24rxidBxlhixph7
|
||||||
|
ulfcuWnMx2x8wM0HxkwryCZByExiyJl7x3qMyR3Rx1e7L1D8uYj/IMXjQMlDYsmIfLwFu8dg0sgT
|
||||||
|
LC6ADLqSnBGkbCOmLL6HrMZQx8ofzC6zCMoNhb46aMva98mWgFfA7HvCDJnEHAjGPLx2envJTDa9
|
||||||
|
7L+/7Mxyt4PRfHeQrEVwZ81FmM24uc0UpXfby77IjMfjq81fWjDNXM6Eic3onFa1BWWFWE+Cy8FH
|
||||||
|
y79uUs/2TLdLbBc2/MT7zM/zdM8QnM8xJygELU8GTcAITb4DvdB81ND4+9DkFNESLUcUbbMWrU4Y
|
||||||
|
ndETs9Er29F6i8rEk4UoPXdOadIjltIuzU4rrcqL8tI0XU4xrcknXdM6nU03Pbait9NAbUg9vcg/
|
||||||
|
HdRG3S9Dnccz/33UTG2i4ZzIhdTUUn2AskSvB4G0Z5zJfnwMyXdFxZnOQ1m5hqx8lkrSTLmlZA2h
|
||||||
|
Zg0tVv2WfQXV77rL84PWXt2ZbTwvbd2UDqvWAV2i3prW/ky8rTzMYm3HZFmocF29Qsxpf13XZd3X
|
||||||
|
X5zYykmk+FrY5rzCvtHV5WfXjlzVgT2ohOpZfO3EZ/rZz7zVosPZg63M98eAoHFLA9ikUBqPVC3b
|
||||||
|
4uTGVYiAwzeQtW2qr43bUKjbTLCKn1JLv03Br+Taw03brW3bsQfccmHcy83bze3buY3cS6isXbqF
|
||||||
|
3N3d3v3d4B3e4j3e5F3e5n3e6J3e6r3e7N3e7v3e8B3f8j3f9Ctd3/Z93/id3/q93/zd3/793wAe
|
||||||
|
4AI+4ARe4AZ+4Aie4Aq+4Aze4A7u4AkAACH5BAkHAAcALAAAAAD0AV4BQAP/eLrc/jDKSau9OOvN
|
||||||
|
u/9gKI5kaZ5oqq5s675wLM90bd9Aru987//AoHBILBqPyKRyySzentCoVNKsWq/YrHbLFU6/4DCJ
|
||||||
|
OCibz+i0es1uu9/wuHxOr9vv+PhQzO/TjHl4AYOEhYaHiImKi4yNjo+QkZKTj4F2Rn6ZmhyAlnWU
|
||||||
|
oKGio6SlpoaedJibrK1URallp7O0AQS3uLm6tbq9BLXAjbGrrsXGnanByqC+zbzNuMvShcNOxtet
|
||||||
|
yG3P0LnT3+Dh4otvxNjnfdps3N234++0XPCQ5dZjWejo6mvz/f6i8hq180arHhETWvKd26fmn8OH
|
||||||
|
AYYImEixosWLGCcagWdw/48FIxlDihRgTmEUhmkgqpwncaTLixvfdfTyscjLmyTtmTwJ681KaQN/
|
||||||
|
sRSCs+hNIrVmXjHKNGPJnTh6uvm5LGi/lk2zVkRa0A0XrWCfSkkYAyUaqminYQXLVmQXLG3jhhTL
|
||||||
|
E4sMs2fS6g22Vq7fnG+r/B0M2CMMkH7pbsBrpt/AvbX6Ep5MubJlpzpdIJarWANjWfMeQ54l+bLp
|
||||||
|
06i1dkaxOW5gK3BGiwvq7lHr1Lhz67Zs+G7p3Rg/D4BEW6jsb7dPG1jOvLlzIsD/Op/eHPrg3mV/
|
||||||
|
R98qdduj4sfBJTdNvbz17W3LUz+fmKaYr5y7rwtPf9D4y1p03w+ehTD2L//wuSYfP/XRt19l+eV2
|
||||||
|
oEUJ/vUfVA689kNsBRoC3jsLClDAhhx26OGHIG6I3oiVhWjiiRw+CCEDEvpAYYWEXDhOhijWCCJO
|
||||||
|
7MUXhH82uWTjjym6t6IKwslBHG0wSkIjkD/iqJ1qRF3X40hM/qjikPeQEcuRViXJ5UBP4tfimHC1
|
||||||
|
dxCWNhQZC2hewhMmb2TG2YSDmaGZHRZr5qnnnnyqkSGJgFK2mp1QaNHnoYgmesmUgTZ62qCEssZo
|
||||||
|
W3JWaumlmGaqaZ2RTvGnk5uGKuqopJZ6RKd8fHoUpxC8ydSZneYoIKwRqPoSpKimY6lvleZaQaYj
|
||||||
|
XOorNsLeKeewEwArQrH/yDbr7LPQRivttNRWa+212Gar7bbcduvtt+CGK+645JZr7rnopqvuuuy2
|
||||||
|
6+678Ma7k6n01msvPvLCe+++/PYrZL7faqnowAQXbPDBaVwJcLMCI+zwwxBHrMq/Cw+p5pptZiyT
|
||||||
|
wbhWDMbFW2os8jcHd+yxpwPmMfIiMp4iWpvV0HryJsKx087KOP+jFBAzu1IzLS3nLHQ4O//QczYp
|
||||||
|
pzT0ygExEnQpRfuAEL5HL/Dz0iI3zTKSXbVhck12VW110mdhfZyrRsU0TtQ9XGCrS1+De7XZkKE9
|
||||||
|
GVezsL3EbnEHTHZedA/SpZtR8j1EUl5tYbjM7M5t9uAYFu6oRvZOzh3j/2NR/YLjgaNl9+L0Wk5R
|
||||||
|
3x+8PRLpD3AezsuBfy7668Ch3oHpbrG62N+NhXZz5xFJDvvvjcrOyaRs3fsi71tDzgjtwDcPvMLD
|
||||||
|
yxqWvccjn8jTijDv/PaWQ0+k67ipfj3X1mdPPHnqPQf+5Omr7/usPP+xPmriI4J9+YRoD2WZ4Z+P
|
||||||
|
Gf9mit97FKejIVQPf8vQX1YalBpVMbCARksVAeEnhAMW6H7SWFKVUMQ97m2wRt6DiqUsWB8MJtB/
|
||||||
|
FfkgBzvoPBWeKITYAtlUvkM+rGnQhR8C1Y6ktEM6Se8iOAwRDK8lQ+84woQju2EQOaRDIPCohwF8
|
||||||
|
H0aW+KEhWquIlkDgKP/mR0F/vcaHVgwXFgOhRYBIkX5eHBMYKbYuQ0nsjXBkgwJZODnhpcuNccxj
|
||||||
|
HudIx+DZDl18HF0aB0nIQtpLbGP74QL/2AAuziWMC3Hk/7wXSMphDpGvUGRTOiZJ/rARS5pc5CVZ
|
||||||
|
hMJNMhKTjdyVseJ0RUwFS5Wo/BUsNzdLaSkrBMyKpS53ycte+vKXwAymMIdJzGIa85jITKYyl8nM
|
||||||
|
ZjrzmdCMpjSnSc1q6tKQ2Mxmr6xpJ21685tv4WY3wUnOcjZBnGgypzrXeUp0hqFheoynPOcZCEi6
|
||||||
|
kwXwpKc+98nPhH3ynjXIZz8HSlA92hOgnsEdn8qIv5K1E6GtUuieGFr/PoeOEqLJkqieKGo9ix4U
|
||||||
|
omNUGUd559F/YjSTBgzZ42poCtYlKWYfRWhIlbZS5ZXCpTCCqUlPGlGBCoJuSKQETiuk0yDwNAMz
|
||||||
|
LVtNhzoKptanqAI8qix9qlSXsXSkHNXbDqTqNonarBtYDWsAtKoDroItpTOcRVDFStLEXdSsiUSr
|
||||||
|
Ea1qU7YWQmtUIWsOphY2RNbPrlfZwhGvSgq9AoCvV0DlXwFLOC0Mtq6jMCxiraBYrzJWJXgdH2RF
|
||||||
|
IdkSkMWvlr0scs44PbyJo7NT7eTlYrqtxYr2FKplUBE44tYhVrIwOzWXa19bitiuVgi09dpDD3Bb
|
||||||
|
O2ZrtyLbrFpIaxrT/5rCsLdtolH1FVqgOnW5UFTQ4bomxwn27626re5SoRHY7H43CIgT7gPRCN5y
|
||||||
|
IVdjys0gc8UEXO6uIUDnzW1ANacZ8fIWGL69W+VeZ1xJ9ZWWVAXcf5UR4CeGjsDD5VViVxkEEi7Y
|
||||||
|
FA3m4YNFV+ATfBbBcp0PPK6bswz38cQwifCySvkqFaO0gj7RHVh5Z2IU2xi30z0Mi9Pm4lr5d3W7
|
||||||
|
61yNb3ziDnc1lDxu71lhnNZ3kBhnQyYyHY28ZPOWll4Wxt9a7bNjKXu5gfpFapelS6osl2/LvUPy
|
||||||
|
l9dM36iuWM1kHpWZrYfm6LL5zqvK8SvhnGcsx3ikdR6zdNrHHD6/jv/Qhc4waz1gaO0mOHePfTLy
|
||||||
|
7OwSRC+n0ZaztAEwDbcwezjKxfuxZiVNY0H7RdOcdhSqFe3pyf4Awo9mk0AIq0VKj2TV820erq0c
|
||||||
|
aj1T2AewDjGBIh1kjtq6dgBkr93WS6lWSzjZV2byXC9sG1NDUDCOXnZ/NBzBATJblMJuCLUjcexH
|
||||||
|
Qru5gv52tKUmQXW3ONbDGTckyj1JK+gn3dteI7tRNcI/JwnNpNkxFT2E5xENvEOLRpqc5jwagMNW
|
||||||
|
4AcXUcG3E/ENJZwV/W4yjByOYYhHfOIUr/jFh5VUBc86vlnz+MHjzAMHv5rbvLZIxQswcl+VHNIn
|
||||||
|
J3WblEhFlu/A5cD/hrkTfSRyZ1Pr5rJ2Gq2hrPKB+1wHQO9B1F9OpaL7Wm4ajcOXir00ni/x6TmY
|
||||||
|
+s+FTnWRzLzmuUL6HOQNamSzMwn6dnPjso6xcbfd3G9HQty77S61G8nuuUZQ3uEexau3MQsFTfya
|
||||||
|
6A3yLsq9Z3hUvOTxwPjG9xrt44r85Dc/h8pbfn+YF5fmOU/62gb+87HrsbeKO/jWu56QlU11vV9P
|
||||||
|
+9qXKvYBtr3udx8q3J++z7wP/r7gCgLWK5m4vwf7YfnNavAaP/TUlb0nbZt84POdUNKf/eNTmX3Z
|
||||||
|
Ht/3MTfl8ZtveFCSP7fPN7rYctnfbR7dlW8+ljHZ3wL6Q+uWxa+l/zDtvwL+Owv/paN/weR/KUCA
|
||||||
|
yAKAjCaAxLeADNiADviAEBiBEjiBFFiBFniBGJiBGriBHNiBHviBIBiCIjiCJFiCJniCKJiCKriC
|
||||||
|
LNiCLviCMBiDMjiDNNhMwneDw1eDCYiDPHh7Ojg7PRiEovKDQCiERgh/RJhQR7iE8peEYsaEUNgi
|
||||||
|
TqiEUViFXTCFpCRtpbeFXJgH0DeB8NaFYjiGZ/CFEhiGZJiGXGiGEYiGaviGm8eGEOiGcFiHBSWH
|
||||||
|
D0iHdriH+4SHDqiHfBiI8eSH9+R3WidvL8UxqgdSdKdSiFggJVV+DWiIDPeIaBGJ2zeJjZgMlgiJ
|
||||||
|
ivh9R0WJ/taJx/+Bidc3h5voCaT4VJ9IiMskihq3ipBhivt2hqkoYljDcTnnDBkDVaeYh7c4bLm4
|
||||||
|
dKGgc3vhi7XYhsEobuM1Y2rFdTmVClRWTWoXOLqodM7oJcjYNhRYjdaFcpNgjHqxjTxQgd7YjOQF
|
||||||
|
NNBIVNK4iDK1jDQ1jOAoCeKYFuS4Vd0Ij1W1NNeYPOkIM+0Iijz1XluXjbIoMqhli/D2Vf94kAhp
|
||||||
|
epLIgARJQ/PokKUIkZkokaImCv1okXmFkb/4hxtZjMTokfSRkMq4kOpYkSZ5jCCZjKioks/IkhSV
|
||||||
|
WSuBkrjEX0czkS3ZCDZ5CB2ZCDiZfwdWNTzZk4vwkxZSkpQwlAH/WJQ7OZJIqSSCtYsGCTUvyY1Z
|
||||||
|
MmGgJZNTuUVViY31KAxZWY6epZOQJ5VfWW2OZZUN+VxliY9bSVmYdJRraQhKWQhBiQhOuYNcuX5q
|
||||||
|
uZbnh14bo17U132/FZHnYpcWOZhAEFzdNX7Wdjru2FqB+ZWO+QOQeV/DlX6KGV5eeZeIkJk+sJl+
|
||||||
|
0pmT6Xbqt3qXOZWk2QOm6U+SiZiCJJB+E5qiiZfVdyuzVZiReZi5V5nawpgO+Zo8EJtowEmpiXer
|
||||||
|
2S3EeZB3p32waV+n6W6CZ5usiZv8OJYdF35t9pjUKZv5ll+f6V6tmTE0GXDeCSf1lTdxSXjZ1pzc
|
||||||
|
8pz0kZ4Pt57X/0mY7mmY54Zu2Omc59km9tmdQwc64Lmfv9mf31me5EKf4TGgvbWbhVea4Zmc3gVm
|
||||||
|
/zmfAeolEEoK0Tl9B3oK0LWclyeflqmdQ9OhYImf0XEvsDONe/aX7Rdu8ZibusmiqVcvLyqcBYiW
|
||||||
|
/behJvmh4rdh3cOjBiaj9QekHimk70akdWSknwaV+KSkjSmhGOqkjgKj8UeXOoaiNsqkSYalfpSh
|
||||||
|
PyqlZUqj+2ij+WOlymYqO0qm32OmcYqmJqema4qjBloqb+qKT4ikU+ql08CdMAKmqJejfIp8tGlJ
|
||||||
|
I+egzLCOXcemhco9WvpieNppcBpXWiiM4yCoFUKokRqfDDqXlf9KmZeqAIwqVI46NJ76qVdqoreT
|
||||||
|
qDiWkX2aqczoZKkqNKvKqo8CpUcWnKWKqHSKc5t6qyUGqbr6O5PqY7DaYacajsTKdKN6rM6TrD3l
|
||||||
|
q34Wi2UUaLAqrWyGeZ5ZZqPIUNqaq9yKrK4aISQKoqVSiZ0zrsZargXnremaYvXCrtbIlHdKrvDK
|
||||||
|
YefKfdZqKvb6jTrnefsqqf2ahf+6ruGarfjKZdtasDcmr8tKPQurRe4arWyxawV6YhpbdtemlTH6
|
||||||
|
rokJrthqsQ2bZmDasUGHYiordWQnl8/msWMarEnnj886afPaFC3bcja2s2O3dzD7ay5bpIC6lCoK
|
||||||
|
rSlraQ+rGz7/C3UvW1byI7KC0pp7yTQ5yxRNG3Y9q7TGuXwzsLQfOyEVaz8n2zpXaxRZCwA3lrZi
|
||||||
|
B7Vfq6/MWWFjC5RlSzcESxFsu7WI9rCHiqkyGyjvVbVJdLZFkbcsy7VSO7Jm+baJ27Y8wK6Cm3Jg
|
||||||
|
q5rYRp4bS6pL8bR71W7jWaJyW7KyeLeKam+germUOyea67XvdKHrBgQBS22iG6tMcG98Zp1DGpJ1
|
||||||
|
0bmtK7agu4qxa7uO+7OWmrlA67arC7zW57pzS4q/q7v+qW0KCnq4WyisK72fO21VOrlxi7qW+7fS
|
||||||
|
uTepqyvIy5tF24nNG71TW7vO67kg+zHVC260WqMVErnk1nQ9/wexfnF2Bxul63u714uLG1e3BJpr
|
||||||
|
M4e/+Wt1sgohGYe9FyTAEYpkBWzAcaG/oaoQCwzA8+vAHmq/XyfBbUHBCSxClfK6VEG/88bBQeTB
|
||||||
|
H4zA07siF6ypGXy0lOB1KazCYAHCLazAI7y8smHCbPkmEWzDWYHDMDlOC8fDDafBKwrEFad8wRu8
|
||||||
|
Tlt1Ede3+XCObsmLZkPDOOTEqQvFWivFB0fF+qCPdWqzV4mrKLzFyTu0E8qzxevGZsfCRZydNLt2
|
||||||
|
FMmp4aHFLsTFb6y2qRvFcTzF+5t2ZCysZvyWqprGe7zGcBy2wuvIbBwSRNy+WAeIGKwIPuw5iqxC
|
||||||
|
fNzGj+x43v8rc3JMybdZx39HbGdcrBDcxIz8yc2Gsacbyik0yosreoVcs5isxPVJG6tqhH1svO0C
|
||||||
|
iwz8Wr0shL+8ue8izJdMzI0bpjx4zKobzLdsx7DbzEXhy55MyuqizDDMW8UchNAcL9xcqwv2zT0Y
|
||||||
|
ztFnynnCdtasfLWHzsk8zad8Yeb8zNlcy9KMJ4I4ebGLv9QKoPq8z4rXzxD7zxoa0AJ9h4QrxNub
|
||||||
|
wwAzegnNTwRdsAZ9olcQ0Yk30fta0cOJeBhNUBoNrxx9XB790f0U0uU60oQ8sVbY0kEIfqa7uy49
|
||||||
|
07YH07LcpDSd0zVdlwut0z69012ZsD891Otk0ysLykSd1N//ZNSRzL5KrdQh+K1PndRR3dOoCcsN
|
||||||
|
jc9GjNXfq80I27gqDZpC7dCmas2/mgl8e0pSPce/tNZeja5cra5s7cJdC8ypY9VnnS9urdXKGtNO
|
||||||
|
zdfmZ9ZqjddibMssDZw37b9zrcOC7XyEPcjystdBS6lN/coVPMaNTUmPfdlBzUpCKyGtZIAYINq7
|
||||||
|
RNpn6X7TgoBF2ITEZNqiOiahrYCvytrD5NohSyaxjdpPSdv7J9s9qtvRotrRw9sD6NtH6tnvZ9sZ
|
||||||
|
BdzFzdxnitypjYRESdxYWN3Wfd3Ynd3avd3c3d3e/d3gHd7iPd7kXd7mfd7ond7qvd7s3d7u/d7w
|
||||||
|
Hd/yPd/0Il3f9n3f+J3f+r3f/N3f/v3fAB7gAj7gBF7gBn7gCM4BCQAAIfkECQcABwAsAAAAAPQB
|
||||||
|
XgFAA/94utz+MMpJq7046827/2AojmRpnmiqrmzrvnAsz3Rt33iu73zv/8CgcEgsGo9IHGDJbDqf
|
||||||
|
0Kh0Sq1ar9isdsvteq/JsJhmHZjP6LR6zW673/C4fE6v2+/4vLw67vs/ZXqCg4SFhoeIiW58f41/
|
||||||
|
WIpwAZOUlZaXmJmam5ydnp+goaKjn5GLYI6pPJCmbaSvsLGys7S1lq1sWKq7OayFtsDBAQTExcbH
|
||||||
|
wsfKBMLNnYa6vNJkV4fO16/L2snaxdjfldCo0+Qvvm7c3cbg7O3u75tw0eX0K+euwerI8PzAX1H9
|
||||||
|
QMkbF+IflHpD7rEJyLBhLINPPOlbF2ygFRIQnSAUonD/jcOPIANUEUCypMmTKFOStMLQIqMKVlTK
|
||||||
|
nFly3kZz1SSF3NlvJM2fKFkGdEnlQkygSFcSvNmioxqe3yYyC+gzqdWZWIQRhXi1q0qbTFk4TQMV
|
||||||
|
m1SGVb2qFZC14puMTdbKBRtWxVg0ZfOCSyu3r0y4gJf4Hfx3ad0Ud8/oXeyML+HHgQE/nlzT8OET
|
||||||
|
ic0wnMhYmGPKoEOLHv2T7mUNcON0hieVGCgspGPLnk1b7cvTEFLrXN2u9VRPsGsLH0589m0fkZNT
|
||||||
|
Uf3JN293wYUbmE69OvXoxQlb3z4dO+HjPZSLh8JcYuvn7bzP5r5dfXa57K277wsehOT3NDMPQM+f
|
||||||
|
0nzS//cJ919QcIFWHyAF4odVTm/019+AogVYG4QmSfhdUSVQGNp4UZTnICXO8UNhASSWaOKJKKZI
|
||||||
|
ooIsjqbiizCWeKAHGhrI4RMefjjMeSJe8VOMQKbY4pCUBWmkjBjOcOMXFOg3Bygh6viajzQdeSRQ
|
||||||
|
NVr1GX1bpmSlkTPiBhODuJAlJVpUFLfkmlxRZpqYIjiJyJloTqEmm3h2YeNFcGKmW5mABipoGlkS
|
||||||
|
aehgb/bpAmCDNupoK4UeKqltlim6KJX55anpppx26umnT1iaEKYLgmrqqaimquoUonJEql+JOtDl
|
||||||
|
WnxudNRksSoQaaa1toqcpjJs2mqnIwjr6w/GwpCsov/ExgnssdBGK+201FZr7bXYZqvtttx26+23
|
||||||
|
4IYr7rjklmvuueimq+667Lbr7rvwxrvBqvTWa+8/8n5777789ntQvtAG8ujABBdssB1hAlyOwAc3
|
||||||
|
7PDDAyesMBJyWkPnxfA4muvESpA5CMadRGkLZxeL0yvHvzL8McibiFwLyXSaLDHKNeiXDsws59zQ
|
||||||
|
VlLQDITN+fCo89D98ByFz8h6vBDROkdm3llunXIygoBxDDTTLDsdstC2GP2vs1VPfDXWvM1KKRUt
|
||||||
|
vVXpA7dCtra+SntE9nNmeyVU0WpPHUHbF+od7thzQ91TmsS1BYzXXtzpN7iAz71Y3fjZO2lli8Ot
|
||||||
|
8tL/jncG+XuST87W29427g7Ojm/u+enZbcyt6L3pk7l/hKMuu6GqH/snOq8/Tfomu87uO+ozH3Y7
|
||||||
|
Prlz4jLvr/6u/PKU9xyEv1Pk2DLXxWfSe1/xVXe9odlflzysSRab4KSsY3J89ZZsT+v4E37/Fft9
|
||||||
|
O48R/LTH/RT60LkPIP2yZWkhouEDm+lwlSfpOeh8e9FfSb4UJOb5joFACh4G1Lc+PBmwPwj8xogg
|
||||||
|
CCMHzo6DMJKgCe7VJPvhAUrUY9oGQSik0ijwbFLYk52QwkIViTBD9irh5fSAQsGRbYU1NBGWXmi3
|
||||||
|
2LnNiFUK4oluaKmKGQJ/sxjgEaE3HhkGcFxO/AUU/x+CRNpQ8UZWZNW5GAWxMj6Mgh6sX+XapaEv
|
||||||
|
uvGNcDQV0ibQxjja8Y54VM4cJYDGCoGOAVIc4hUXFkgXrvEAfWzeIPeoKyJq6Y8LKCSvxGgrSZZK
|
||||||
|
YolUyiF9tqxL5WlYnBLfJxnJtmcpy5TMCqUA2UTKVrrylbCMpSxnScta2vKWuMylLnfJy1768pfA
|
||||||
|
DKYwh0nMYhrzmMhMpjKXycxm2ieP0Iwmk5zpB2la85qQpObzsMnNbjZBm33wpji5Cc6kLceM6Eyn
|
||||||
|
OvXAxHI+85zrjKc850moRboTMVWgpz73mc523lNWJmzFFqGosWxqM4uEGCj+CrpJcCJ0ZQotHkP9
|
||||||
|
qf/MhwoiotWbqD3/aVG5BS6Fs9jdh2S20Xt29H4f9eHLXFeyQtTuoAG1Q+YyCAuROoiklPwnQHeY
|
||||||
|
h5mCVBY27Q9O5afTBpSvpj/FqEIRp5Gi7jSfuxlZUpW6RaZ+06lGjalmgqZSqkbUqkzAalZ5aiav
|
||||||
|
5kVrxpvqK8C6hPmFTWFHNWtD0Dq9rs6CrQBwK1ysptX9yLUsdNUETdeaN4qW8q0Ai+tfm2HJwqBt
|
||||||
|
KIUtKSAb+76Gakuxiw0GZVNyN37g1SibJZBls4XZzNoitCfpbMYim1M6otaPo8VWaU1Li9cqEiCQ
|
||||||
|
lZoE+QbA2F5rtheza3q6aJwraIW1BlGcYaUFXDr/CZcdtk2K4bqG3H8oV7Kr62vunpvAGQ5nurXA
|
||||||
|
ayYf6VtrNfdM3NUgcYnUucm9lLTapS101zuk9pLPoPAlK17k290Yes6+knqvbOPLX2xE1031Op2A
|
||||||
|
f0tgfgSVaAdOo4Rhu9xonVeq6ihehCc84QWbt8GsYenrNszhNHq4WhdeaYZzR+ISO/DEcBoe5jA6
|
||||||
|
2EqM18U45lJrm5iRC0KxxrDjbY6H/F3sCq/HUY0okCdxYyI7GSkVnoaMPUpjtdrYkU/OcvyOtk07
|
||||||
|
+vgSSx4xlifTve6M2XNlNkCTo4xPLye5rg9GX5N/kuY5D6fOZ4aykanh5gY1x8oazvNg8Czk5RH6
|
||||||
|
/8BsnmxG/gviAk+p0PtbdJEhLdo2EXDH77R0gBvtaOAIGnySFtCY/wdqoq7Suu7ldKc5YedLJnfS
|
||||||
|
piO1jk1dkE+Xek1ffk6Y/UFEJZ5Iy/jxtYkSHUlbzxrXbz4goD3Ta2GvCNjZcTaJiN1ISo9GU7nm
|
||||||
|
za5P22xnQzva0qY2Io09lwImG4PL1my3hf3t4ki7AOIGbb10CFUt6k7EP1y3rwWJaPpW0Ls/Cvee
|
||||||
|
R5hD1+pXpn9OL8iAuG9D9hvgW45CUt4dbwuQ0OD1TmjC47xwfSuR3/6GocQRHHKTUHzgOJw3xuGp
|
||||||
|
8XuvOKUTabFjx2ntIpYXxX1NxKqZXPIN0Zzclf+teIxzPqedyzzoP4d4b4UuppOe0Og9D2PSoSB1
|
||||||
|
LpuLjPzM+iBa3e5bYxpeWNe62O/A9a6X++ZYnPLY115dqpt902gXV9jZTnfdRv3too773xw59b77
|
||||||
|
HVSzrOPfB0/4JQWe74VPvOIDc/iaf27xkI88Fxpv25c+/OtSvrxkxwvjeHHeoJq3OkIcf5WNfV7v
|
||||||
|
7zr9bu8uXaaHJ/S0firrHY5yuKLSk3gCZSdpdHtX7l4svW+6Kms9ylj+3h7Bx02ziJ97sTr/+dCP
|
||||||
|
vvSnT/3qW//62M++9rfP/e57//vgD7/4x0/+8pv//OhPv/rXz/72u//98I+//OdP//rb//74z7//
|
||||||
|
/vfP//77v5aSF4DD93+yJ4AGyCEEmBsHuIAImIAFyIAQmBEO+IARWIFeMIFjZYEaeIEYWGwb+IFb
|
||||||
|
0IEeCIIkiHrjd3B1l4IqaFXvh4Ir+IIwaAauh1UuGIM2WHcz6FQ1eIM8KHY5WFQ72INCqE8/qFNB
|
||||||
|
OIRIuE5FaExOdwc7FzON0nnI1IQI94Q6olGYx1FEZzFWOFJRiF/ORIV10IVX+IUmWExiSAdk6IWD
|
||||||
|
IoXHlIZPsoZCZYZLaEtwKAdyOIdtCIbNdIfZloc8gYWxB4RbeG46s20upwwYM1SiJ1Zp6FMKNwoc
|
||||||
|
xxuM+DXO94iOg4hb83JnUomhAn2YCHP4pmLd/7CILsWHzBSKZKOJacWJUuKJTfV8qog1rAhn29BS
|
||||||
|
hOCGTFiIfiaKrkgLk7gasHhVsqhqsVCLgLgYnwWKxohUkZiM/LGMxeiCNzOK0AiFdpeFJtWM2ZBu
|
||||||
|
1yiMbfeJ05hxxINhwfiN4JiNg2iE3EgKyIiOOyGNl9iOkuiNERVYUCGPmSaBYkOPovCO1YOPgRiO
|
||||||
|
sch8/Gh71AiPvAYYiXiLh0OQxGiQEMFXCamQtSCQYGaPnqCPVLNX/ViRFhlFgdGQy3Bc6tiIHXmQ
|
||||||
|
ieWPIdkJGGkJAHkJHMl7iJUvKdaS6TOSm3iOAgGRYSVKHomQ5DhjOPkKL1kJMXkLPtlWQKmSNv/J
|
||||||
|
khYJexGRW21geVJpieRyk9B4lU2QNieJlXzElQWZlVCpkGK5BF5ZlaB3lj95dWUJj2yZlrmwlrMH
|
||||||
|
ctrYLVqZjHFJlXNpWaRHXnV4E3kJiHuJN18pjvTmX5cWmJUEkkWJCYXpWUuZVxbHlkzplo65ijyp
|
||||||
|
bkpXXKpFC+IFdCJ3l9mVmbS4mQupmHlXBSaplrIWG7poO28pJc/IWHW5dI/1kIepJ4WDijhnmkxT
|
||||||
|
m8zWmf1jXFHjmvxTnGfoK4OJHsLJmarZPp95V5NZdkhHmtvSnLqGmtxGnLBpnLqJnKG2mrX3YcBJ
|
||||||
|
NM+ZmiMHa7lJXbvJBdeFnZc1mzqSnt0ZnYf/AmD56ZvUop1reHRntyoKxp/T4p9kCKD/pioDupy6
|
||||||
|
N5RU9piXgKCjiSoLypijR5/XKKE2J6CMxqCiYqBdqKFdoZ9qZKH1AKJWKKKll2AdaqL0gKI9VIos
|
||||||
|
dpt4pzyxyZwYGlLWmG/eWaMldqMNynLlGGK/yKP46aM4BqQfmqNAtaNYo6JI6kUEylxMeoxOqkI0
|
||||||
|
GqXAM6UWVqXOKKNi1qNa6kFKymPnCQ7cKSVQOqaR5qKEdKZRcaUQlqVsel8eKphI1otVVpvWWafM
|
||||||
|
46Y/k6e4o1Rh1qd+aqPleaEQ8YfblW6Geqi+A6jmZBCM+jqFKpqQmqSJeqKCOqQKdal/manA/yap
|
||||||
|
KUOphog/oLqmorqf8hkWaldWe3qOj7qqLYqSq3BHlZqUHReq2lFmsyoah0an14mYpQpHuaqRRiqs
|
||||||
|
XhGsYjo7zHqkxwaWt9png7qTcipnmHoVz7qeabStbreYtroDeHSs9vmk2WoV3voEHJauTlB1EVms
|
||||||
|
b0SuaYqlvDpovnqu78GucUFy69gLuHqqMIms5lqvfqGvTLCu90qwkxSuNKlprOqgKGWtRbpFv/p4
|
||||||
|
DuuZsZacAcqwHfCaLDJbuooxFeuxETJqGpug0pqSqGanEAur3ziyJ1uyf0myG8qxHECznOOlmQWz
|
||||||
|
4ymlMxuzNZuyDftqLCukRJmh+KpnPYuxd/+HsyvaqpW5tOyls4vFsxernBkrtdFKrBL5BbUaPQD7
|
||||||
|
n0lrl17Lns16W2ULrkLbsUCbs3Aqtgq7sSsrnVl7tVs7ll3LrXSLbHpanwIrCwz3cbQqFycHtWMS
|
||||||
|
t3J7I5W6GCH7aHXzboNLuAJnuIl5tu4qHourF437CYEbRJG7FoXbrzc7tmTLIZmbF5vraTUHuZ/r
|
||||||
|
FaFrs/NCurS3JKdbFqnrkh7nua3bFa+7tqOLuCiruGG7GrfLarlbQ7vLu5MrurELvBNqusPbGcWL
|
||||||
|
PKsrbcl7Fb3LtTihcmF5hH1rixPbNMfLQqX7rVOkt7iJvjKRvXi7vfRSuVLAhRILpsnarKz/u7CW
|
||||||
|
C5jqe7ftSkPLC7tt9r4rB7b2Nr8OWb/QanLWO7sJHLz9q7bmm0TOZqIX170t64Qbd627+rgLjL8N
|
||||||
|
/Lz7CsHq6r8TvKn7qCrw2yFPlMHhmzOdi7wMvL+J+8DnK8Mowb7vekrcuze8WK2tOK/88cLkG8MR
|
||||||
|
nL4jLMI0LMHCRsEFZ8FGy0MsTL8Dy8HeRsRHXMNXHHFFvL7/67t20cQ87L0+DL5SHJytAaWSd7nt
|
||||||
|
u3diDFGdhsaRp8Y5LHc97MaOBseQJ8dtWS5+GL1mhceLp8eXycd1fFFQl79I/HeCTJmYecGm8ISA
|
||||||
|
rHiLjC59/L38FcmJN8lj9KpJOE8VC6ll/5pfi9rJPii7yRvKA9appEyEpry7qMxgqrzKntzKrfvK
|
||||||
|
5jnKsrxPn3yotvybuJzL9LTLftrL/cnJwGxGwlynxFygxnzMEJPMbLrMVOp4JVjN3kR5PWfN2nxN
|
||||||
|
2Hy22/zN0NTNH6xJ4FzObyTONoy25rzO+4LOW1xp7BzPFQxLgifP9izAxoevVonISku5Y+C88HyX
|
||||||
|
qmfCYKfPdDnOI0qqTWGZjBzGlcel7jLQ8smWTAHQqfVHEs28nGTQfqmsAe3FqmDRFLZ5HK3QzCzS
|
||||||
|
FrtnFI2nHn3Rh5TRAEwzMA3S1YbQT+vPYoDS5EzSKC3NjJN8X1x8qXR8zdt8+SzUuMdKS4hK1KgB
|
||||||
|
1HvE1CgA1XWxfCdseLIk1QSH1EOH1VFr1PTs1FEN1lM9gCpr1Uft1Um9JkGK1kOr1lct1lnN1sLH
|
||||||
|
1Ycr16RE103p1mYK112t1CL414Ad2II92IRd2IZ92Iid2Iq92Izd2I792JAd2ZI92ZRd2ZZ92Zid
|
||||||
|
2Zq92Zzd2Z792aAd2qJNAgkAACH5BAkHAAcALAAAAAD0AV4BQAP/eLrc/jDKSau9OOvNu/9gKI5k
|
||||||
|
aZ5oqq5s675wLM90bd94ru987//AoHBILBqPSBxgyWw6n9CodEqtWq/YrHbL7XqvybCYZh2Yz+i0
|
||||||
|
es1uu9/wuHxOr9vv+Ly8Ou77P2V6goOEhYaHiIlufH+Nf1iKcAGTlJWWl5iZmpucnZ6foKGio5+R
|
||||||
|
i2COqTyQpm2kr7CxsrO0tZatbFiquzmshbbAwQEExMXGx8LHygTCzZ2GurzSZFeHztevy9rJ2sXY
|
||||||
|
35XQqNPkL75u3N3G4Ozt7u+bcNHl9CvnrsHqyPD8wF9R/UDJGzfin5N6Q+6xCciwYSyDTzzpWxds
|
||||||
|
oBUTEJkgFKJw/43DjyADVBFAsqTJkyhTksTSzyKjClhUypwpYN5Gc9UkhdzZbyTNnyhZ8nNJ5UJM
|
||||||
|
oEht3mzRUQ3PbxOZBfSJtGrSKsKIQrTKdabSpfZyvnmKLSpDql3TnrSS9U3GJmrjriQINmygsWTz
|
||||||
|
fkMrt6/Mt4CX+B38l27dFE3T6F3sjC/hx4EBP55c8uvhE4nRMJzIWJhjyqBDix790/JlBZGrxOkM
|
||||||
|
LyoxUEdJy55Nu3bXlxtTU1nN2p1rqZ5i2x5OvLhs3D4iQxaL7tPv3u6E1zZAvbr161aMU77O3Xp2
|
||||||
|
ysh7KCec+Qyo59DZSafdvf137YTbd3+/fEoN+vBVljeTvj+l9f+zSUYcgPq9BVp4OFGRH037DeCf
|
||||||
|
fwSSJuBwEQZlIHhFlVDhbboZxNuDlaAHz4YCFGDiiSimqOKKJi7o4mgsxijjiQgCcsVkHXqoE4iU
|
||||||
|
iPgOiTMGuWJV+A32WV8klijkkibW6EGSRBrWQYNznOcaj6IAyeSSUSqI4RQH3vjTlks6yQJgMlCJ
|
||||||
|
CJYNHUlbjnCmhqOUOqAZg5rWsDmVl8bF6eeF5NF5WkFv4WLooYi6JeaLjBZn2qB3FpropJQaCmWj
|
||||||
|
mAZ6EaQoXArUn6CGKuqopJZqJ6f3LWqkqay26uqrUtRnJqpIeFqaoA7Y6hWuqui666Yw8SmrFLQS
|
||||||
|
QWqqoRaL2qj/hIqqLEfMUpOssseKUO2z2Gar7bbcduvtt+CGK+645JZr7rnopqvuuuy26+678MYr
|
||||||
|
77z01mvvvfjmqy2s/PbrL6j6rvvvwAQX/E/A3d5V6cIMN+ywHrMiTI/CD1ds8cUPRyxxEngaoufH
|
||||||
|
Q1H66MarMDcIyJ34WAtnH4sDLMlBdIwXypqoTAvLerqsMcw2NJgOzjQH3ZBWxPIcs8kL5XOl0EwH
|
||||||
|
RHQURh9NcdJNBx2ZREsD8zQUGL3Fs89VWx0Y1mZVpOjL1noNM9hhQ+dmXELBs/UTRqnq18jost02
|
||||||
|
a28fh5XZp0xIIa/n6r132T0JO+DfWp8N6OBot2v43ov1vWDB/5muRbi5k1Oel+X5YZ55ZZuX23k7
|
||||||
|
QFMO+uisv4h34Uh7FFDqe6/e+u3avQ7WqB96nnLWnfiK+/DE15ThTbzv6DsnNmsivFzycVdk8SZF
|
||||||
|
j53tXRYNxHirTi072bQvj8nzcVnvHfasm1/d9Egenyb6kKumPPPAi58J+WoJXtul+nev/fuKw9Tp
|
||||||
|
7HcN/KWlfwGy26+28qX/pY19+SNY73jUPHBoiUwzop4GlYRBGe0MAwY0ScEmCKIK7kWBKOlgBjdY
|
||||||
|
PBV60H0PhN8CYfik2OXBSohr2wVdqKLsxaqBUQgTBE3CQxZ9sG5D5FDkpmRDPOAwfEzbYRFP5MMg
|
||||||
|
AhEKQgTdFP9VdEQNqQ0GMiMEAWkhQ00ZDFRzWmKdvpgg+VFqjLMoo//O6Kc0djFcgMGYHvc4gBCy
|
||||||
|
UIClI5cf50LHQhrykIjUyNpQCLdEOvKRkHTV1xgZQTU+YJC6a8QgjWfJXAXwbnfc2CYzeQBMBtIP
|
||||||
|
owykHAtDw6hdMlozgCWtrgUCWroSArYEo7OoJUsb7fKWwAymMIdJzGIa85jITKYyl8nMZjrzmdCM
|
||||||
|
pjSnSc1qWvOa2MymNrfJzW5685vg1GYkx0lOOIWzV+VMpzohcs5UrPOd8NRCOx0Rz3raE2rzhNZu
|
||||||
|
+MjPfvoTYq3MZxul8M+CGvSgawilQGNIUIQ69KH8VOhCIxD/xpPBEY6VIqVAKyqIi2JUZKfMJ0f1
|
||||||
|
4NExZjSk8xzpDUtqv5N2cqILUKliKGdCWEDRPzoLKEwZIFPN0LR+N9MHyHJqn51KoKfm+WkOVybU
|
||||||
|
lhVCoyJt4h08V9NsNDVnT0VpOwdoU6Cy9KsBmNtBjEpRqSYVGFUFq0kdp1OyctWqS1UrS8XaBLKW
|
||||||
|
1XtOkaterva7uM6CrookwalEaVb+6PVzY+vrTWEB2CV0LSOTxOtMD/sUvtLPr7JoLAAey85FStan
|
||||||
|
lP3RJ9vHlpAF7oOpfCnsPnvW0LZjlQUqrdzYWtRgJZErUMVjYR3k2uiMVi5xe4dmkQhbzamWc7vt
|
||||||
|
rW/BdEUp/7SEtg48KiUPqNVvvTVomFXPbxNIhbacNiN9qq63rkuz7Fpwu37rLuDawL34tTVvyfWd
|
||||||
|
eU/I3PCqt3HfZeDijmu6+FJ1sfT9IetEN2DxJsy/yg2wFQs8sNvlFlzkTXAc0Xs5gjnYwNyKsIQf
|
||||||
|
QuHQWbh1D7YugltzVc8V948ovgp/BTnidwBYaCdOsYxZKdEDs9aw/Xix2Oo74x4Pq7bv0vDPtrG8
|
||||||
|
GPv4yCEmR/Jm9tW0/me6R47yfqM7MVGR0KNOnsQmpcxl6gIZIUtuDlizLBIod/nMoqlxGMKMjzF7
|
||||||
|
9X5mBo36qHPb4c3ZAHXG7Xtv0F7SulHMii0xS7cMlDvn+f92hjYy6b4snsD8uKFMvqyOixzn7cz5
|
||||||
|
0K1LdIf1zOiSIbCSf25zoNUhV0L/RNM8RjGqBfzoscZS0c2NwpU3HJxK29EgjrL1p4G7Z8TA+tZT
|
||||||
|
mDWtOWFqBj1uf7o+9hy5Jq1UA/LGvB12KIo9w3/k+tC7bmSnfanoEc6vhG9WsLNTuEUUodlF5UaR
|
||||||
|
ml+J6VsNTNjpIbM/zJxuKp47P/VuUq8zkNp/wRs68raFFLd4b3zne92ebLex3/3tBwW8FgOfYsHh
|
||||||
|
k+8CILwB/d63bUPdUeeEe8eWq3gVsRhruJT8CUCp+MV5autPYZjl0K6Sx+eLsogXceQoP7lgdG5y
|
||||||
|
mqhc48T/3bSKV45UHI+6Gyamd75x7oQsLrjVJPf5wYFugYxvewNFj/bRiXw41/ya6fcct7ZXzlmD
|
||||||
|
AJDjh5K2loXu9LArvNpUXiNkIxXzNan960N3+9tpTHVdzt3vaDeU2sss9jfp3epxV1ceIcp4QlB7
|
||||||
|
4mbse5Al1fjK4+HxkAfliuW1eMt7ng6YzzyvNx+vzn/+9NB9uuifTXZe7j22h4+97P8V2W7P/va4
|
||||||
|
b1Xt2e7u3Pv+93HafeG9DPzi+wvqdLMm4vG5cd7DnflVfn1KSPlr0pNs+cxu/vA53frttbz3Gq9+
|
||||||
|
9zn//YWjtvx8v/ousJ987ecc2Nk/Zi4H6qdnzZ8D9xdm//7P9MtZ9rKG/WdM+2cXAON60xICA2hX
|
||||||
|
CriADNiADviAEBiBEjiBFFiBFniBGJiBGriBHNiBHviBIBiCIjiCJFiCJniCKJiCKriCLNiCLviC
|
||||||
|
MBiDMjiDNFiDNniDOJiDOriDkmN8PsgvPEgBPziEkhSE0kWESPh/RghzSdiE9beEuOSEUmhOUMhu
|
||||||
|
U3iFgVGFVoiFXGh2WohxXRiGB6ODdYd6ZniGuSB5HFiGaNiGaDh+EMiGbjiHnweHDyiHdJiHjGeH
|
||||||
|
DoiHeviHB8WHDeiHgFiI/SSI4ZR1Mjd4POJSiBhNiigHjIgljqiGibhbiTCJjQhS1rdVmGh3mohT
|
||||||
|
nPiI0P8Uif8WinlRiepnVKbYcKjIGKqYeG71iXnyitARi9A3iLToMbZ4i6NoieCUdf9Fc6MwaZ1B
|
||||||
|
VLLIirsYaVXzcFtHEVjleC/XTcKoVMb4RFzHJsiYiwxYjV1HjKJwjYuxjfGni4T4PW3jjJKWjZSY
|
||||||
|
VZ14Tt6Yjh8HV0jnVNL4jpd4jnn1jeIIPuy4ifdIis8kZDPXj714jKlXjt3YYrOgjgepF8MVhwwp
|
||||||
|
Cw75kGQRkXc4kbFQkRbJExjZhxrZVeDYkQiZX8m4UwTpj6RGktHIXtPITSn5jMowbJaVignZfs3S
|
||||||
|
WdcXkvQoaMpVkxd5k66Wk164k9DGkvMGGCpZj/jlkvj/eFc6SVhHiZQQl1jruJJNmYZPGYV/J5WB
|
||||||
|
N1lUKQtAmQkciQkfyVBjaJRfCVphyWFKKZP7kJUJ9ZJMWJReuU/M2JajMJaYUJaXcJYIyEYSE5N6
|
||||||
|
WQl8eQl+eQtCWVde1JWDyZN6KX6yJVyLGVhCiH7pd5LINZWFCWfbR3z3RZkmyY1Q6XzPp5DsQpgP
|
||||||
|
KZmMI5pOeX7SZyFbuS2qeZCsGZruAJil+ZlWkWQ2tpat1ZmWcJtT8FyjiZpcaXuzuS+Q2ZbE6Vym
|
||||||
|
9Zrhh5mwB4wZ1pxhiXcuh5u0oFmhN310aX/YCTIj6RmmGXkAsV5aqWyGt5zZUpv+UZ7BoJ3gl55y
|
||||||
|
qQZ9/4Zs7okt8Nkf8pmUrDZl0HmfaZCf7SmQuzOeH/OfAneeywYF3iWd+uVeq7hawGl0/MiU/ECf
|
||||||
|
5jegtuCd1Dl6CLoU/RlvBlmVvJlmrVkLIBqboGadtKmgesKgKBqgFAqh6jmX2TYavnmdnGmNGjoi
|
||||||
|
Dnqgxwdi4WmAeAlowmmYQ8pd/3Jh+ymeP7qkT5aiufNhDDaiyCOjJMmhflakWQqjzDmlVEp4Nsp6
|
||||||
|
YDo6PRqjZEqlXiqiaZo5azqmF6p1ZWqmqpcpBKamR1osJdqTyyA+b7p6LjKn78mltnCiPDKohOph
|
||||||
|
UYqkweaK7KCoIMKojXqlj+qniMpUWJl0Vnqpf2So/P+5qUHVqarTpKAKpVqaG6TakD6pQ6iaqmFa
|
||||||
|
oZtZp5vxqmFjqbJqG6IqpbY6O7haNbq6q0RKqyRqZZI6RmT2ncQaqmJ6BGxGNU02j0zqos16ZquK
|
||||||
|
LKByiuKzrCF6rWiWrc32J9y6PN5qreAqZeL6aqFSrvJFrVU6rOmqqqRZDtGKjix1rvI6r7OKk2CG
|
||||||
|
rHl5Ufoaq1yxannKQgYbdcjnWFLDni8aqQHbl/BKaehqFQn7fil2sU3Hc/rksKAJsUq6CYlZc9+a
|
||||||
|
Fhrbcxl7ac85lMnhaOgJaSFbMxPrO8xaPSpLsPlxskzQdv7aaDvam+M5siBTsyWhszsnY0YLADzL
|
||||||
|
sj7/67FK9KtL+Y8eRbQkkbQ9ZrUc27Dg9bKylqyWILQfQ7UCgLVIe7OfmnfIyRT7up1QC5fQ+FVi
|
||||||
|
+7NLq7Adam1Zy65ne6NdG7GIObOeOqxyy3M7i7ZfMLeWSXd5y6utmmBx67Qqim2O+7E9S390i6Yg
|
||||||
|
K2oW2bhbq7eVm5leYLgMi7dnyih/GlqaO6H6aTuBC7TGihlrW58wi7mrWbJPi2sCaqWrC3ahe3aj
|
||||||
|
6zqL+5O0y322y7kYW7eFe7e8e7Ck+7u9dbrDq7iQu7kLu1mNqZz+5rW9AbafYHM8xK+E8XOtG3SJ
|
||||||
|
67mv4q6Lob21dlsi571+Ab6ayW/BS0jXy7cA57cN/6q+S8e+feG+9QqA1usv5qsX6Bs8Sldv+ru/
|
||||||
|
Uxe+VRe/nDS/MRuf9lujFLa+B6wW/Ju2TFSxDQzA2MsaA0xsBZxuFRwXFzy53IazG9wvAZwXH7wJ
|
||||||
|
3OtCunu00wu6KCsTJcy0tcTAohqPVxmkwhrC5RbDSstzNDy4Uldv3cd+OIx/y/jAEkujEALEBEe4
|
||||||
|
xfugGwt/ynsSN8yYgqXDdMnDIhvBlSrFEkfFVzzDgivDNpzA7wtCXryfYCyzUNwfL6xCQkzDRazG
|
||||||
|
KrHFhxuYGrzDTSy7ckyp6VHHHXTHRJzGQ3zE6ZbEb0x0gSytPSy1UUTGN2fGNWzFmax5WsTG/ZvB
|
||||||
|
//+rwEdYp4vott5wqvhrwJhsxGiMxZ1Lbkj8rHU5vtUpyrsZux0XtYEKpOqwtkToyibcC4Kptvro
|
||||||
|
RHeHwsJbfMC8xMLsmMRMyqBIk8jMuj64zFzcss7Mf8U8VcdMy5xsfNbcxzswWC7QivRrutMsxLIX
|
||||||
|
zrvbtHZZzpFMUt3cu04KztMrznIXlfC8zXYweL48hOxMvfBieobYeGI7r73aXxlR0J530Oma0Cy2
|
||||||
|
0AxdeQ4NrhA9LgQ90Q5V0dd60eKS0RodiAw8wuCZqRhNeSG90SNN0sa1rqgC0in9TxzdrB6tWxId
|
||||||
|
0wg108Ra05CixGL40/UkfPRcu0Bd1Pck1FksuUb/vdTrhNSv/LBMHdXl5NRVDKdSfdWRRNVn/M1Y
|
||||||
|
3dWJpNWbPHZevdT33M7S5NP4vIXe3NKy3LEoTH3TbNKl98jTWbE8Pc503bory8zBhNZmfctJncxt
|
||||||
|
jEp53cZ7fc0CWNifDIZ23ac/4NcCfZlrXdK2/Jh/rErxe9f5/NaYPdmyWdkIA9niO9TU3NZa69mL
|
||||||
|
BpukTdQYDEwJ6GsH6H8B6L8FiEyv3Smz3dNKyMS5PUy37bqx/dK7jXXD3dfFTYB/4qu1ncO9rX/H
|
||||||
|
rQLPnaDNrQG/Ddapoa3JDalP6MfBXUzVXb3ZranTDb/jfUvf3cXdrdvlPdrB94Xu/d7wHd/yPd/0
|
||||||
|
L13f9n3f+J3f+r3f/N3f/v3fAB7gAj7gBF7gBn7gCJ7gCr7gDN7gDv7gEB7hyJQAACH5BAkHAAcA
|
||||||
|
LAAAAAD0AV4BQAP/eLrc/jDKSau9OOvNu/9gKI5kaZ5oqq5s675wLM90bd94ru987//AoHBILBqP
|
||||||
|
SBxgyWw6n9CodEqtWq/YrHbL7XqvybCYZh2Yz+i0es1uu9/wuHxOr9vv+Ly8Ou77P2V6goOEhYaH
|
||||||
|
iIlufH+Nf1iKcAGTlJWWl5iZmpucnZ6foKGio5+Ri2COqTyQpm2kr7CxsrO0tZatbFiquzmshbbA
|
||||||
|
wQEExMXGx8LHygTCzZ2GurzSZFeHztevy9rJ2sXY35XQqNPkL75u3N3G4Ozt7u+bcNHl9CvnrsHq
|
||||||
|
yPD8nl9R/YTJG0fhn5N6qe6xCciwISaDT4DpWxdsoJULEJkgdKRw/43Dj9+qCBhJsqTJkyhHYgGp
|
||||||
|
yWJGACljyhQwb6OPjmpY6gwmcqbPkyt33nrzksnPozQJ2lxVTZLQaxOZ8aSCtOpRKwxdUrXKFWXN
|
||||||
|
pTtwpnkKdaKwnl3TlsQaUOsUtXC/Mi1adIRYNGTzkkILt69MukX9Co4pNyzgl3abvtHLGBTfwZCT
|
||||||
|
HjYYubJkRjCwWP45GWIchmYb+9u6ubTp06iBKm0ROHVawJ9Fu4tKTLPr27hz615LxUTr3ZzpxpbN
|
||||||
|
jjYB28CTK1/+uneQzpSvKkb3yThxWchxG9jOvbt3K8wFex/fHXxlzD+g/0N69wwo69dhZb9Nvr75
|
||||||
|
8HDrk78PGT3YCf/qQTFcfB/N59pvuBlI2EuW+fdfBAE+MSCBDSmIGoK3WegVg+c596AL7ZlB4SXw
|
||||||
|
1aIhSQWkqOKKLLboYor4xVjZizTWqKKDH6YQ4gAjWlIiLSeOZOOQLrL3mGBH+hWkkEQ2mSKOOZ6w
|
||||||
|
4xzv0dbjJEs66aSRpPXX5WBLCqBlk1BGaQFsuHh05Vlf5hbhmxBFVpiZIqCZ5lhrTvVWcnD26YWc
|
||||||
|
q9F5pp+B3GnooYWEKeOiXl4kqI6EVoHopJTeoSijmMYV6KMAXrFbpKCGKuqopPbJKQqXNlrqqqy2
|
||||||
|
6uqpNqU605wNyBrcFGAl2ZejFdi6IK+wpufnDITCGqlvwwYLRLH/MjDL6bElOKvstNRWa+212Gar
|
||||||
|
7bbcduvtt+CGK+645JZr7rnopqvuuuy26+678MYr77z0EuHqvfjmi1i94err778Ae8hvsIVWavDB
|
||||||
|
CCdcR5kD01OwwhBHLHGlDDecxJSI5KkxP5TSarEOGE+4MSY/1hJanuIA+/FN0w0yMicl03LymilX
|
||||||
|
vLINO6Yz88s8Z0XUpjcr0fJC+VjZ89Ft/axy0CAPrSbSsgEmkdHAuCUFRvsyDaHTOUEdNV1TRyWQ
|
||||||
|
0jYvUJfWWz9MtNcO6aopW3lZvZ6qAvObM9ttt5lgFT6fwt/eS9N7N96TiK2nFMrBzTHZeqfm8Q3+
|
||||||
|
Jqb204T34zZ+//pmStLjOOsruaROVW554zFmrvlldbPm6af5ijybPhtffvrs4XGO7N8Zth46Pzv3
|
||||||
|
KDvtwANne7QcZmondaJ3YpyvwTc/e9k1mH4r6IslrzxtzOen33e/A78996SrBT01+HJJPfLXG453
|
||||||
|
9mp9X173tLvPHe5Kpv4ooa5nEjPS7DeXEev0SwmGkGQ/QeFvd9bDzurcVDzAwQ91cwMUrto1uCvt
|
||||||
|
Tz4LjMmYiOS8DoppgzYa37cq2KMLviJLIKyRB52XwhqJ0FshQ+AmTPi1AJakhTQyH+I6tCcJdg+H
|
||||||
|
L3pht2JYPU/QUDQoBCKLdBiFBoXPfz9UIouEeD/hUCqBnHigE/8DFiEfFnBcxzsUFjehRR5yUT1e
|
||||||
|
nOAQ7zWxNt6pfyvE1PAIlsHcnfGOeMzjqyhYxwPp8Y+ADOSb3AXHzQGNAYU0CRX7UMbpXW1QNuTK
|
||||||
|
HFeWSAg+slORhGIUcvXErgQOApWc5MekZY5kPYuUIUAl2hBpysy00oCqBMQrV0nLWtrylrjMpS53
|
||||||
|
ycte+vKXwAymMIdJzGIa85jITKYyl8nMZjrzmdCMpjSnSU1YCvKa2MxaNRmZzW56kwvbfMQ3x0lO
|
||||||
|
NYZTDOVMpzqXcM5lnc+N8IynPC31xXaqYHLzzKc+97lIe3oAn/sMqEAl1k9/1opruBijQifRsUMa
|
||||||
|
lIiEWKhCG/r/SYMqAKIukygWKVpQaGJUEBrd6KREWc2PUs5rRyRF70ZUs3o+FKF3qFxKR7FSCrXU
|
||||||
|
nBY9KEBjSriZiqKmBLrpJXOq03eCtKdUswVQ4yPUTRLVASSUWVJDSlVKyA0KT4UqTN1TNPVV9atX
|
||||||
|
fUJWi0qF/MXCp19NXlgPMlazbVVEaYWH1JQ6VVqstQlYy8guoxrXkICNrl6txV01AkmI7PWtPOqr
|
||||||
|
O+ZqsrrOYrDsLKxBDrtTrioWSJ2U5BUYA9kspHGodkPsZU2UWasEJW6MixPdcFovvo52FI30yWnJ
|
||||||
|
0tlKWtKpDXPta0MR21ltVi+17ePbOgpG0YousJjtIQCn0Lc2/9iWiaBtrXFlutQTlnYzioNHZ4Xn
|
||||||
|
0Hfp9mjInUVvCUiF5uZCuI7rbvQ8V6fpIhV2h2vi6aRnPPWSD1+fK6sMdyuK8fqxfPO1L7HYm0r3
|
||||||
|
8he21+UugDVH0lLmK79TMOuBO+Hf9C64vhWFVCZLoy8Jf6O6xKlwHEcMXdw6WMSe1F0ReQffPKGY
|
||||||
|
xDD+FXE18Fzp4MvD2ABxDXcY4x5/1sQgaiCjwrg2Fqsjdgn2sZL/ImAIfyHARcGx9ZaH3iVbWTcz
|
||||||
|
JmsE5WjFFYeUyhu+sphNk2VW/o/BXUbfl7FX5TG7mcMuhVy+Siyg/S4UzC+uivy2E+b47bnPVSmz
|
||||||
|
lOZsY6OedP+Gjj1ajX2yZwMA+nSNfnShgdwD+vq2snA1YqJ7tuiZRDrPyvl0kh2J1ecQmtRSkHLh
|
||||||
|
Ns2zTstE1ModMax5vFqxKuuAXp6wq2UcHQYCeoD1Y20V+6TquO5agEL+7wOBvas4fwjXatZ1m+F8
|
||||||
|
Zl8vO9nBji66vksctI5mw1Jc4puXE+4VCfrWBh6Rtyk87Q+WuwDjJve7C3BuOmI6sWtadxbbPW94
|
||||||
|
xzs5/a63sdJNIX2Tkd/z/jfA5y3wU5m0a5oO73WSWG46N2GLtAZTu91d7oaf8t5Uqg6rkYjwd1vc
|
||||||
|
KGaU74+h8JOAO7tfiA15xHWsF4qH++RLwLjKa71zDTL85fb/fok1RC5xTo+a5+vMeLMzDPMoX/Gr
|
||||||
|
oDZt0m3bYGwR+U5pjXqgp75xnBM2XVdPU9aPrnGug7rqdLrxQNe+XYVD2ePDbhXb5+7crrtdwXC3
|
||||||
|
ptzpznc0HPvu4msyDNnY98L/HfApZjq1Dq9Iszv+8aQipN2RDvnKW/4wkpc0eS/P+c6fjV2M543n
|
||||||
|
R3/GcIZSvTXOe6XJzmSgn17xTHt92VIPdI6wnteUTlue0S5dzeNe21r1/aWBTw6tw/6ikx9+7Uc5
|
||||||
|
S9WZyuGx/Gfzaxn9e04/R9AiQfXRtn1UXf/Z3afx99tK/vKb//zoT7/618/+9rv//fCPv/znT//6
|
||||||
|
2//++M+///73z//++///ABiAAjiABFiABniACJiACriADNiADviAEBiBEjiBFEhJpHeBo1KBoISB
|
||||||
|
HJh9GmhmHRiCEfKBwSeCJtgZJKhlJ7iCepWCbsWCMNiCLngAMViDkzWDNGiDOghODwhyhfeDQDgH
|
||||||
|
qvdUPhiERniEaTCERFWESNiEQaiEOcWETjiFfAeFFiWFVJiFA2WFL6VfWviFR8iFzPRweDBhL8NR
|
||||||
|
y1dSMZcxZrgxaChsS7iGQ9eGKDNSgqeGWJhRdEgzdnh850SGPLWHV/KGxHeFcmgIgsiHiMJ7HnWI
|
||||||
|
v5CIPUKIuReHeXhUkGhTfSiGvASIlsU2BgczLaaIg8CIz//EiZnmiSOnUqE4iIlyh9RkiviGikUH
|
||||||
|
CzTXGE1VauQHi9Q1i9mwipHYin5oeo4YbUjziYh2ZBpzi7aWi8OID3hjjJpQi5wFjJpIWYaWB7so
|
||||||
|
jUTXDW5IjWn4igQnC9B4iaLRWebHbarIi+QoiucVjNuEjjSViuv4i35TjboEjz8lj/OIifX4jdOE
|
||||||
|
j6EwjvtIW6lViF14jXgSNto4kOVYkJMYheF4Vvq4Pn9Vh/0Ihw/webgEkFWijpXDWOy4BnOkkbfE
|
||||||
|
kQyJJRUpVR4pCuYoWf9gjV5IjCd5cEWhkL74WA6JiwWhTSUZkTP5ECk5CwJ5CS3ZKzxpSyZ5kiAp
|
||||||
|
lBMJCkX/uZMyuJE++ZOWsJTi2JSlkJPLCJWGdY9TSZWUYJUSuZKh8JSY1JW5lJRtaHy/hVoX2Wub
|
||||||
|
54/mopZmyJbZJRTBJXzI5o5g95WXaJd8A1xa2QUrt5WhdW9gGQCAWV6C+ZZblm1yWS50KW2xFpfM
|
||||||
|
1Zh1x2yahJHxMpkHtpiX6ZaZiW2Bx5fb5peQqHVeZ14imXxlZ49xF5PO+F7ICAyqOWkAkTSOyXLL
|
||||||
|
FZnk4pkEQpaOcXtLF5qLs5tPgHe+WVyIeVwLyW6V6UBSwJpqEHqiB5t6J5tF9ozPuW/RaUfGqV2D
|
||||||
|
eXG9yZnwApzxIZyfcJuoFhG6OZrEuZkG6V2ouTHq+W1K/yedudkPbYdlrngu6Hkd9wmd+Qme0/me
|
||||||
|
7cierYedEhA57dWc2ciN8cWbaHZhXGaaQfZgD4qQeAGWCgqZrfI8/+l8+LWh2nloDPmhxekqIoqh
|
||||||
|
JHovThYFxSaIKjpcLPp2y6lhJVpgEPqTNVqa99KiDKp7MGqiEWZn+/ij8rkqQpqj3qehPMqhnTiT
|
||||||
|
Spp4QYqj5jloemmje5drr1Oba1KliMcnI9oB1mlIauel7dCdQiGmY1qe83k7bnpbpTKjXSWhLhaf
|
||||||
|
b9qkWSqneqpZaSqTxXGTFDKne2qgfUo8WwqkrmKnNrkMSPadh9pjpCh+i7qkdYqk4MCmO2Gok3oh
|
||||||
|
ZcoBmv9ZO2k2m/DAqTrhqZ9KZqG6AaPKHGGXkEaGp2H6p6sqI5WaAa+6HLHaoVWFZ7Z6q1ZWb7ua
|
||||||
|
OKW6nRoFrJIqrIdKrKSJOceKonfGZpfKrFfmrNV2oUKnqVikrAVqrXuKraqFYduqpgrlrT0HrpMq
|
||||||
|
rnA5ZNEKcVSFrhSqrp86pEQaoriZatyaQPKanLoxa+m6QgA7r6/5kL1warJVn0NJctVqFQPrrzD2
|
||||||
|
sE6gczrJMha6oCcKr6A4oIXqmn4hseQZsX8Gmoa5ehf7ezK6r5SwsI1xpgIAsignsvLTsNdpsEJz
|
||||||
|
snsppae4sagaYh7bFzCbczEWtDCRchVrslfannVmriT/gpW+87PaM7OqOhhES7ElOxdJm7A9yrOE
|
||||||
|
anQ0ixRVO7QjG6wq4aS8AG2meow9u2Nkq7RcAKcBm7PtaplXm50RUmwsyxguW6xG+62qka0Fe7Sx
|
||||||
|
ebcq21d7+6ys+muIi6lfN3DEVrjGBrWM+pjKdl18u3WJ+h9oi6yfKbmMuwVwS7AYS7kgWrdp5yeO
|
||||||
|
SlWHC7iVu6yNx7qly1ZB9yapG1KrO6766bevi7uBa7pmsrnSulu3O7cWdm2wu6KCC6D1qRd5a3NS
|
||||||
|
RK+C4XKZO0LLmxfNW3IVB719Ib1x2nQ6G4sl5LQ0+Tv9pr3b+3PTO3hbq27iqwnOq0TmCxfca7PM
|
||||||
|
+b0a/3O94JZw8ZsW85u8c1m9ZIG/5Ku/+8sV/eu7vwnATyHAnVS++QqxlCe0hTmxPnHAsrsuuriN
|
||||||
|
Xcu2rstxN/fAFDzBRSvCMSsTFoxXfFSJwdu0HMsS7wtEXme1IRzBIWvC6Nu94JLBM7fBDDvAJgfC
|
||||||
|
Ndy7MyzEoosSJ9y46qLD6bO2DPHCOBTDfTvEdBu3JnHEkQV6zci5+tO+Loy9H+y2JUzEYTzFRXwS
|
||||||
|
VgwAmfe9MrfEPNyyXvy8QDzGsRvEZAzBPvdu9lp8WbzCPsLFIOHELQTFIizDZVzFN0y/09KrdtCR
|
||||||
|
TOzGHZy4o0fCV3yaTjcpY/fI1BbJNIzE/1vJiHLJuv97GhgoyWicxO+aUFDXtl5ndqSMwafcCqBM
|
||||||
|
xcXrea1syp4sRqmMydh1gbXcl7dsKLFcyLPceb3MLYEKhm3ksuCaq4tHeMjsRspsrcycyM78zBMT
|
||||||
|
zcw6zejWqNaczJ5bwMqXvmvEzd1MUN8Mzijrv5JZzeWsMNgsrNo8u6vSzub8tehcs+JszOxMzwfz
|
||||||
|
zrcazxsxvDs40OSUxqoczgSd0Nlk0Lq8yQr90KWXwm4K0RR9TQwdyoRc0Rr9Lxcty5m80SANMB0t
|
||||||
|
zB8d0iYtjDRrO7SXz9zU0HE8yWdpqwDtyimNeue8u4isCiSLwCAo060qL7LnbCuNw7bn0mAM0w16
|
||||||
|
03TQytOxp9QqrdRoStSNsNMXHNNGnc5MHTRB3adDndMJ0bZ+uNVSbTFijchdrc7SQNUobJQ17aK9
|
||||||
|
BycD9nwfJ9cxCh2bOH5PCtfQh9eWqtdeSdcnNkh7DdhRKth/7deuRNi/G366ytda7diKith2O4KR
|
||||||
|
bdhpCdnah9kI4YFGGiB3rdgswNixItp5Jdk9Cdr2oNn1wNmFTdk4+NqwHduyPdu0Xdu2fdu4ndu6
|
||||||
|
vdu83du+/dvAHdzCPdzEXdzGfdzIndzKvdzM3dzO/dzQHd3SrQIJAAAh+QQJBwAHACwAAAAA9AFe
|
||||||
|
AUAD/3i63P4wykmrvTjrzbv/YCiOZGmeaKqubOu+cCzPdG3feK7vfO//wKBwSCwaj0gcYMlsOp/Q
|
||||||
|
qHRKrVqv2Kx2y+16r8mwmGYdmM/otHrNbrvf8Lh8Tq/b7/i8vDru+z9leoKDhIWGh4iJbnx/jX9Y
|
||||||
|
inABk5SVlpeYmZqbnJ2en6ChoqOfkYtgjqk8kKZtpK+wsbKztLWWrWxYqrs5rIW2wMEBBMTFxsfC
|
||||||
|
x8oEws2dhrq80mRXh87Xr8vatl9R2LPQqBXdTtPSvm7J2srf7bXkT+7tcNHj8EvmvOiuwevs8gAx
|
||||||
|
VRFAsKDBgwgTEsQScBS9e0wUSpwooF4+IfvYNNzIsf/SQIogETLs5A9ZsIcQAYRcWVHcxSAZ13Sc
|
||||||
|
2fAjy5sgrcxEaROnz4MWX/6IqYamUXc9fypdWGXnGyxLowYVk1IG0TRHAZYkJixp1K8KU4oFS1bi
|
||||||
|
1DBVY1xFk1XeVmbBvJYtKzbl3LsFz+qAOlfvhrVn2gqOSwWv4cOIE/v0q+TKXcYZAJsZTJmWXMWY
|
||||||
|
M2sGC9kGX7p178WZubWyR8ebU6tevZoRxsusE0oeYJr0WwKfUxvYzbu3byux8foe3hs4XtcwYQc3
|
||||||
|
OLt2x9u4UasmTt34crLUiVvvS8VRXe6B3oCC7hxY7s12W0vPCdEw8j7fQYdP94l8+Xfr0bdXv51i
|
||||||
|
+sf/3QmlQWhUjHafaeclVMCCDDbo4IMQLnjdhJhFaOGFDL4noD0ERmHggZQliBCGJELIUn9kKaeU
|
||||||
|
iCOW6OKCGm7oQnMgbmIffii2+GKJJ6r4k4+L5afQji7GKCMLNNaYyY0BAYlYh1B2CKAVR/ow2xzj
|
||||||
|
3aZkLE4eFuWXY4FnZJUm1IWLRlsaxSKFbOpHJZk7mHlmUWnStGabeCbWGZwjdJnSnIAGKugZYBZq
|
||||||
|
qFh89uAnRIM26qgih0YqaReJtnDnk5NmqummnBLI3puVGnGpl52WauqpqELx6ZihWinkUnuOGuQU
|
||||||
|
R3bZY4Ac2uqfS62+digJmva6gKQzECvsEMaKEOyx/8nC0Oyx0EYr7bTUVmvttdhmq+223Hbr7bfg
|
||||||
|
hivuuOSWa+656Kar7rrstuvuu/DGK2+ZqdZr7733zEsuvvz26y+u+k4736MEF2zwwU8BHHCVAyPs
|
||||||
|
8MMQA8rqwlRV02idGA/W6J4UN9awHhmLwmQtpYVMSTigdpzcx3mYnOVb6vjj8iQoT6yyx1V8aMvI
|
||||||
|
M79SV888KSxBWjfnWqAk/WjZsyw/zxw0rRYQXfQESS4dYmHHXeEJz+Ak/F+KvAZctdWC6TrlFFzL
|
||||||
|
8rQUsXHM7thkZ2W2mFI4dUqOmbm9Ltxxq4l1nszdCzhQYaMFkVUWi9f3kiWb9/fgLdkLeV6F7/Uq
|
||||||
|
rP+V/5U4fYvL/fjkoE+ut1qXr5j5gJvz0/lRc4fuumajO1v6j/fq3FDjCM7++u68yyb0C7LeWq/t
|
||||||
|
AeFeWfC9J++6zTizLXrqaK4u8m3Iz5XdcHgnf/1vrU/E/A3ZU8i39JxAV7188LCG/NfoR+FdmGCz
|
||||||
|
jFV9SpN/fvzp87foPe79bjj7UoGeTOgHM/IFYFRE4pHyeJdAEn2PYZEingEtozsBNBBDC2TgBS30
|
||||||
|
wG+Nb0tp81kFN2gh4TnvbFHoX/gKQsIIddBbH1RSCEmBwBY+yIQpzNoUVAgkGz7ohd26khxeZrzO
|
||||||
|
dY8z/yoUCqHWMSFKkHEFtN/nVJNEJdKNieiS06D/JsiNCmYwebHLolgcxUUcHfGLgAvjtvYHj4i5
|
||||||
|
0WFVjGMW3HU/JMrxjnjk16r8NzUH1PEreQykIE+1Ryz2MQJ/9F3KEOlFnADRD2dUpCGH1siQqHFh
|
||||||
|
iRTJ6YZVSRw+oVZTtOMkGbnCm1xSbJECVqag9awZpfKQVHulslbJLFnK7lewzKUud8nLXvryl8AM
|
||||||
|
pjCHScxiGvOYyEymMpfJzGY685nQjKY0p0nNalrzmtjMZjMHyc1uIkqb7/OmOMe5BXCGk5zoTKf7
|
||||||
|
zPkIdbrznQBgp6+m8MZ62vOed3ikPGd5NHz6858AHYA+9wkC+QX0oAh91EAJ+gAnGqKMEN3ExjbJ
|
||||||
|
/9ADOPQXEc3oLQZ1ymtelBAaDSnNOEpRhn50ECIN6UQXWVE/CrBlBpxhNkoCtEJ01JonjZ70ZEqK
|
||||||
|
ImKsZnxsqUVfiocJ8nQUPq0TUEcpVAXkdIDkO+onmlabpUrhAlLzZQxnGsWUUtVka1un0bohzK32
|
||||||
|
tH5eFUtN7/bArPbSrEhFq0i/GrKwQgGrhwsmXFPKtFACUmskkSst7OqFJV5VZXvlKywiqUmdBLar
|
||||||
|
tSBsFwwrVoolVrE09GsAHVs+wXaNrfy74mGbSFS2YNYZjDUdFezWhkxijqX6uuxpQ5Fa2q22I5Jt
|
||||||
|
QttK2i7Zznaqms1bU3DrtdqakrdvK21gfkuYHf8Gh7MbyW1E1IfclcEDcQZdrlGTyqXgik9woLtp
|
||||||
|
88hKuuxOhouQpaBznye58FYXCG5FknLPu12aNveEkMNX6MQLvrzeMmdIq6/M7ptDNBoYU7C1XCmP
|
||||||
|
m2DNmZc2zHXceg9MYdFWVlGdLORoCzpfCEfYjAWusIj/2uDx4leUQY1Mhz/cxQmP+MWzWugKXEs4
|
||||||
|
ez3RHdz13IJhzGMEb7i8xsXXjduRY9ZluMdIRvFdqbHjlQg5wBspst+anOQqb/bH//WuI2sH5dvZ
|
||||||
|
tzY09sn2imPc4IyZN1R2cop7UWbKQmHIGTXfkcFy5t2kGXJ1NsCdNVwOZLXZwm/uMl/lvOev5Ln/
|
||||||
|
0Hk6dJtlbKk/t4+egk4poR09EUVr+XWWdjH+LjwUSitZCnCO6KQvrcP8UbGSAPx0n9uZatUCWHFb
|
||||||
|
82zfwrzl0J66ya2+MqcrlusY95Nzj5Vyz2jNYFOnZn37cTMTzmlrVQca1sH+svSI7cnC6o/UkTP2
|
||||||
|
oz8pLUmF+sM19CGDrLwccTeI0dPwdqRZfJpSmnvc5I7Nu2G0ZjipG9rsvkS4zR1vec8b3dXybWWk
|
||||||
|
Clx3z7vaTuBhiLe98CH9u97hEjhlCO6JfYsb4botdcOd/YSQzLsAAKeWxAdD8U5Y3IcYn66yl6Bw
|
||||||
|
TSvo4Uw118gFU3JOnNyGKWe5xlW18xM7/N0h/xdYh7FEQGHPzNO7gie1+Quup9Ip1uldHNK9p/Sl
|
||||||
|
vzddTp8f1I3usqmbpepz/nqJIz7GhxZd2qvbCrX73fNdz0uLgsq3vsPOduGO/V1wD5TcLbH2ujN8
|
||||||
|
yaT908X2Tom++53jq94btgWf0MbrAez46iMbyeH4yuMB8veS/OIZZfnOzwHz9tK8y8PCec+b/vSD
|
||||||
|
CLrovV5j0Lt+nHwGPDANn/TX256bsee2Xun+2tv7Po+5T/wvaU/13xtfjsFvwjCJHSvekz7m6cZ2
|
||||||
|
zuMZNecH7u6HZH5J+656P49e125vqPUph/3VS7/29eY+xFOx6LaOnynln1orPbAsYc1fvrjcff/+
|
||||||
|
Q1D/Xt1/xrY0fAEICLRkfwPYaPvXVAq4gAzYgA74gBAYgRI4gRRYgRZ4gRiYgRq4gRzYgR74gSAY
|
||||||
|
giI4giRYgiZ4giiYgiq4gizYgi74gjAYgzI4gzRYgzZ4gziYgzq4g2V1fD5YKjxIAT84hJsShLFE
|
||||||
|
hEiYgEbYAEnYhIWyhKTkhFIYGlAIAVN4hXVRheKHhVxIDlroUl0YhpSigw+GemZ4hmjQfRFYhmjY
|
||||||
|
hqenhhDIhm44h5UHhw8oh3SYhwdlhw6Ih3r4h/jEhw3oh4BYiG4kiNmUdXdAeGSzUoiYTIpoB4xo
|
||||||
|
NY64fvIUiXUwiUtTidDXUphIB5q4VoHCdNL/9IlEF4pgRVLxd4lDhwio6DKciGULaIpD9Ip1pYqP
|
||||||
|
iEy0+G22WFW4aIns9IkChna0wHXlYVXhJ1TCGFOyFgvG6BzIKHuD2Iq86DI1F23LIIqCQIrRtIxR
|
||||||
|
1Yyw8Iy+SAjcCE3euFPgyFXroI2Pd3XgdI5pl45nNWCwaFPuqE3w2DnXmAl0RRnRqHtC6F8C+GAx
|
||||||
|
I45Sp1ZOU1zrF1+7NHNQZJCzhpD1CFqdyAAMqUsOiQn7GDf9+FMKWZGclC/692rARjLyOEEdqVQf
|
||||||
|
KYtWKJBatWJJE3UZlZJpIl34UH0iOXswCQwbSTY0uSU2SX1j9QU9SJAxCZFp0n7QVRlBaXWr/6gu
|
||||||
|
GXkJPakkSjlc49ha8LNpINlbO9mLm1CVt9VZMhkLTfl+8JeLIteVXsmP52dJgCWWSKkJZYlokgSM
|
||||||
|
WKeWazl339d7YWkjJykKcxlk9yhzeJmX7eZziNcEW0eMn4WVveZrW5lcRmmYAtGWyccEi0mPthCY
|
||||||
|
lil2aCl0k0mZfNeZWgkFU4kJQblbT3mXoSmahUeaiQkArJULZllsdilGremaBwSbJDYFs7kGxFd8
|
||||||
|
n9lthSmarFeX3kBcFImYdjec0RKV7HacjeWbyumY0kl+zslKxUmZ13l91MkRqUldq4mbJKk6ujkJ
|
||||||
|
3YmddVOdtEmXfBmZipebrpme05cp+zWYcf/ikghYnjr1jYzZV3v5Xe3FXtlZAheZAtAJImO5WLyJ
|
||||||
|
GfrlXuOJYTkJPNvZNwsqQgE6IQ9KoLcpodcFZL9mnv6pmSDGcxyKKvcZoaugn/jHn1DFjP/ZXRl6
|
||||||
|
HRuaX/ipYB+aZZCGb+gYoww6o8tRo4NTjtiVoxQqn/oYl2wJpM8FXifKkkhwoCiQoAdyoZnFpIf3
|
||||||
|
RUTKT4KpomDook93nruJpVm6QFvKf7XJdFQaYfRZprBzoyrglCG3pszVpm7qoHCKoGmap05VoXlp
|
||||||
|
p3eqJ3x6AnLaoVsIplonpoAaqKTipQDonq1nqF8aov2pm4vKqG0njSsKqd6ZKtVYkNvgHMH/ialo
|
||||||
|
VKBDxanqiSqfepShCma1SaoiZqqFqqrrhmM+amSoCqsjJqt7amO1SmS3OmWXqqt4wqu5mm2e+qvf
|
||||||
|
oKTfMKrECkaSSoDH+mQ8qhXBaiev+qylGq3016vDo6zYwKzY4KwIkWnMqT11dqxnmYyP2qXfWq1u
|
||||||
|
ca0zQa4HYa4bpzz2aqKAtmzeR6a3hqimdVqj5q90lq7DGhX52nGZyq/zdK4CCrDahVkD67CIkbAJ
|
||||||
|
d2AWm3ErJ5TWRbE06qeiRj3ZChIZq3JoVLI6t7FEoK4bywSrOqIlQa8GgbIqgbEG26DTya5shrM+
|
||||||
|
RqkvqlgTe6+KQbMURrQLe5MNK7QP67Nh/wq0IsuyJHuzBAs4Rquy/eqxQQqyEBW0+uom2vamuJZs
|
||||||
|
+4q0kJSVvcm0iQqX8tp1I4t+3SCek/e2R8ux8GG24Adq4EoJpwkiMpuqXwC3m9dsf8ewZfuYtrmj
|
||||||
|
JemXVjpsbSuc5AC4TGq4kAmQdSu5aoakGvmXbAu1bvu31xa5Yju4ZFu5oXu3HpK3k7C3B9K36yq3
|
||||||
|
/xq4X5uYzBa7roa2AYuN62hArIusrntsqFa6stsI8VGapwuvmbu4RzdCB6etYPFxpqpKluuWmCuV
|
||||||
|
mmsyN9dCzNu8MAellXJviUt410tC2fsVzsut0XcoL4tZ4btB4xsV5QufMuK9IsqI63tB7f+7FO/L
|
||||||
|
vYkiv5UKvsr7bverFPmrs/YWQahbp//LbwHsEwOsqf5nwMabb/XbQNPXcgrbsik7EQ1MuawJsfQV
|
||||||
|
MqqLngl8cZc7tY7btcSLwi8HdOYbcFp7HyE8pj7ycRU8txastCy0vQRMdtNbIzE8wQlUwxt7wyqs
|
||||||
|
I+b2vEJBp1FWvSY3wihXwlhba1F8uFi7wcLXwbYbsRnzw06Mc1CMw7Y1xfVZsxRhxcrHlT2soExs
|
||||||
|
c12MvV9cxKarsWMrxxJhxoQLlS9cHjEMCm06hBhMt6hEiPOrCXvsqiYcm5D3xzeTj4S8xhjTxz+o
|
||||||
|
yIhFjQdcyLlzyGf7e5IceB4MimdHokb/xLNhbHybbFmUHMGWYMnHI8qTe3uljEmn/L0PubbWy8pS
|
||||||
|
TMpzfMeB3MmniLutOm22TMWunMujS5iMB1KfnLswuwy7S6xnmi15Byia2My6+szYEs1zMs2Nu8DW
|
||||||
|
fC3YfCbazLkLzLv6i3dlt0WTSM2w2s3W8s24EM4HW6bs7MLHrHfpvM0BPM98ErdfYIh1KIbfVDT8
|
||||||
|
7AX+7HgAHdCLDLuUV9AJddBSKi8D3QUM3dAOPaGTDLqiMdF7WNFGetFTXM8aHYgc7YXyp9DdENIo
|
||||||
|
XU9I7NFg7LcEndIw/TArjcWYPMojfdMVpM/jsrs43dOjotPiwtM+PdQZCtQ83J1EndRw/+zSDsxL
|
||||||
|
Qq3UUE3GntnC8PLUUZ3Ul1nMb4XPyHnVPZ3VgLzVudp8LDvTRyrGb8zBUSjKRt10ZknWYNnU5hDX
|
||||||
|
cj2pNc3Uau3Ub7192WrWrnTXkbrDp1pbbe1Be4196ge/rIbW0huZ2ueob3fYzJPY5bwLdJ3Xh8rW
|
||||||
|
g2rOY83XZU3VRXDZV7zWgE3Ogi3QB9gB/dcq/xenqd2Qr+1grU0msz2lsZ1LtY0Bqx0quU0vSijW
|
||||||
|
hgK9vb0hwy3cT1iUwd0nBfjAv92ixz2Sz42mk6Kdzd2u0T2Qyc2ltw1B1e3a3Y3b263by83a4e3b
|
||||||
|
2f2F6J3e6r3e7N3e7v3e8B3f8j3f9CZd3/Z93/id3/q93/zd3/793wAe4AI+4ARe4AZ+4Aie4Aq+
|
||||||
|
4NqUAAA7
|
||||||
|
------sinikael-?=_5-14763587882000.8241290969717285--
|
||||||
|
|
||||||
|
------sinikael-?=_2-14763587882000.8241290969717285--
|
||||||
|
|
||||||
|
------sinikael-?=_1-14763587882000.8241290969717285
|
||||||
|
Content-Type: text/plain; name=notes.txt
|
||||||
|
Content-Disposition: attachment; filename=notes.txt
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
Some notes about this e-mail
|
||||||
|
------sinikael-?=_1-14763587882000.8241290969717285--
|
||||||
167
ses-lambda-nodejs/index.js
Normal file
167
ses-lambda-nodejs/index.js
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import { S3Client, GetObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3';
|
||||||
|
import { simpleParser } from 'mailparser';
|
||||||
|
import { gzipSync } from 'zlib';
|
||||||
|
import { Base64 } from 'js-base64';
|
||||||
|
import { createLogger, format, transports } from 'winston';
|
||||||
|
import { config } from 'dotenv';
|
||||||
|
|
||||||
|
// Load environment variables
|
||||||
|
config();
|
||||||
|
|
||||||
|
// Logger setup
|
||||||
|
const logger = createLogger({
|
||||||
|
level: 'info',
|
||||||
|
format: format.combine(
|
||||||
|
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||||
|
format.printf(({ timestamp, level, message }) => `${timestamp} ${level.toUpperCase()} ${message}`)
|
||||||
|
),
|
||||||
|
transports: [new transports.Console()]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Environment variables
|
||||||
|
const API_BASE_URL = process.env.API_BASE_URL;
|
||||||
|
const API_TOKEN = process.env.API_TOKEN;
|
||||||
|
const MAX_EMAIL_SIZE = parseInt(process.env.MAX_EMAIL_SIZE || '10485760', 10);
|
||||||
|
const AWS_REGION = process.env.AWS_REGION || 'us-east-1';
|
||||||
|
|
||||||
|
// Log environment variables (omit sensitive values like API_TOKEN)
|
||||||
|
logger.info(`Environment: API_BASE_URL=${API_BASE_URL}, AWS_REGION=${AWS_REGION}, MAX_EMAIL_SIZE=${MAX_EMAIL_SIZE}`);
|
||||||
|
|
||||||
|
// Validate environment variables
|
||||||
|
if (!API_BASE_URL || !API_TOKEN) {
|
||||||
|
logger.error('Missing required environment variables: API_BASE_URL or API_TOKEN');
|
||||||
|
throw new Error('Missing required environment variables');
|
||||||
|
}
|
||||||
|
|
||||||
|
// S3 client
|
||||||
|
const s3Client = new S3Client({ region: AWS_REGION });
|
||||||
|
|
||||||
|
// Utility to convert stream to buffer
|
||||||
|
async function streamToBuffer(stream) {
|
||||||
|
try {
|
||||||
|
const chunks = [];
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
chunks.push(chunk);
|
||||||
|
}
|
||||||
|
return Buffer.concat(chunks);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to convert stream to buffer: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility to call the REST API
|
||||||
|
async function callApiOnce(payload, domain, requestId) {
|
||||||
|
const url = `${API_BASE_URL}/process/${domain}`;
|
||||||
|
logger.info(
|
||||||
|
`[${requestId}] Preparing POST to ${url}: ` +
|
||||||
|
`domain=${domain}, key=${payload.s3_key}, bucket=${payload.s3_bucket}, ` +
|
||||||
|
`orig_size=${payload.original_size}, comp_size=${payload.compressed_size}`
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${API_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Request-ID': requestId
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
signal: AbortSignal.timeout(25000)
|
||||||
|
});
|
||||||
|
const responseBody = await response.text();
|
||||||
|
logger.info(`[${requestId}] API response: status=${response.status}, body=${responseBody}`);
|
||||||
|
if (response.ok) {
|
||||||
|
logger.info(`[${requestId}] API call successful`);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
logger.error(`[${requestId}] API returned ${response.status}: ${responseBody}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[${requestId}] API call failed: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lambda handler
|
||||||
|
export const handler = async (event, context) => {
|
||||||
|
const reqId = context.awsRequestId;
|
||||||
|
logger.info(`[${reqId}] Starting Lambda execution`);
|
||||||
|
logger.info(`[${reqId}] Event: ${JSON.stringify(event)}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rec = event.Records[0].s3;
|
||||||
|
const bucket = rec.bucket.name;
|
||||||
|
const key = decodeURIComponent(rec.object.key.replace(/\+/g, ' '));
|
||||||
|
logger.info(`[${reqId}] Processing ${bucket}/${key}`);
|
||||||
|
|
||||||
|
// Check email size
|
||||||
|
logger.info(`[${reqId}] Fetching object metadata for ${bucket}/${key}`);
|
||||||
|
const headCommand = new HeadObjectCommand({ Bucket: bucket, Key: key });
|
||||||
|
const head = await s3Client.send(headCommand);
|
||||||
|
const size = head.ContentLength;
|
||||||
|
logger.info(`[${reqId}] Object size: ${size} bytes`);
|
||||||
|
if (size > MAX_EMAIL_SIZE) {
|
||||||
|
logger.warning(`[${reqId}] Email too large: ${size} bytes (max: ${MAX_EMAIL_SIZE})`);
|
||||||
|
return { statusCode: 413, body: JSON.stringify({ error: 'Email too large' }) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load email content
|
||||||
|
logger.info(`[${reqId}] Fetching object content from ${bucket}/${key}`);
|
||||||
|
const getObjectCommand = new GetObjectCommand({ Bucket: bucket, Key: key });
|
||||||
|
const { Body } = await s3Client.send(getObjectCommand);
|
||||||
|
logger.info(`[${reqId}] Object content retrieved, converting to buffer`);
|
||||||
|
const body = await streamToBuffer(Body);
|
||||||
|
logger.info(`[${reqId}] Buffer size: ${body.length} bytes`);
|
||||||
|
|
||||||
|
// Parse and log from/to
|
||||||
|
let fromAddr = '';
|
||||||
|
let toAddrs = [];
|
||||||
|
try {
|
||||||
|
logger.info(`[${reqId}] Parsing email content`);
|
||||||
|
const parser = await simpleParser(body);
|
||||||
|
fromAddr = parser.from?.value[0]?.address || '';
|
||||||
|
toAddrs = [
|
||||||
|
...(parser.to?.value || []),
|
||||||
|
...(parser.cc?.value || []),
|
||||||
|
...(parser.bcc?.value || [])
|
||||||
|
].map(addr => addr.address).filter(Boolean);
|
||||||
|
logger.info(`[${reqId}] Parsed email: from=${fromAddr}, to=${toAddrs}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[${reqId}] Error parsing email: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compress and build payload
|
||||||
|
logger.info(`[${reqId}] Compressing email content`);
|
||||||
|
const compressed = gzipSync(body);
|
||||||
|
const payload = {
|
||||||
|
s3_bucket: bucket,
|
||||||
|
s3_key: key,
|
||||||
|
domain: bucket.replace(/-/g, '.').replace('.emails', ''),
|
||||||
|
email_content: Base64.encode(compressed.toString('binary')),
|
||||||
|
compressed: true,
|
||||||
|
etag: head.ETag.replace(/"/g, ''),
|
||||||
|
request_id: reqId,
|
||||||
|
original_size: body.length,
|
||||||
|
compressed_size: compressed.length
|
||||||
|
};
|
||||||
|
logger.info(`[${reqId}] Payload prepared: domain=${payload.domain}, compressed_size=${payload.compressed_size}`);
|
||||||
|
|
||||||
|
// Send to REST API
|
||||||
|
logger.info(`[${reqId}] Sending payload to REST API`);
|
||||||
|
const success = await callApiOnce(payload, payload.domain, reqId);
|
||||||
|
|
||||||
|
// Log result
|
||||||
|
if (success) {
|
||||||
|
logger.info(`[${reqId}] Email processed successfully`);
|
||||||
|
} else {
|
||||||
|
logger.info(`[${reqId}] Email processing failed, status handled by REST API`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`[${reqId}] Lambda execution completed`);
|
||||||
|
return { statusCode: 200, body: JSON.stringify({ message: 'Done' }) };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[${reqId}] Error processing event: ${error.message}, stack: ${error.stack}`);
|
||||||
|
return { statusCode: 500, body: JSON.stringify({ error: 'Internal server error' }) };
|
||||||
|
}
|
||||||
|
};
|
||||||
2270
ses-lambda-nodejs/package-lock.json
generated
Normal file
2270
ses-lambda-nodejs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
ses-lambda-nodejs/package.json
Normal file
13
ses-lambda-nodejs/package.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "email-lambda",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.658.1",
|
||||||
|
"nodemailer": "^6.9.14",
|
||||||
|
"mailparser": "^3.7.1",
|
||||||
|
"js-base64": "^3.7.7",
|
||||||
|
"winston": "^3.13.1",
|
||||||
|
"dotenv": "^16.4.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
ses-lambda-nodejs/ses-lambda.zip
Normal file
BIN
ses-lambda-nodejs/ses-lambda.zip
Normal file
Binary file not shown.
101
ses-lambda-nodejs/test-parser.js
Normal file
101
ses-lambda-nodejs/test-parser.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
/* import { MailParser } from 'mailparser';
|
||||||
|
import { createLogger, format, transports } from 'winston';
|
||||||
|
|
||||||
|
const logger = createLogger({
|
||||||
|
level: 'info',
|
||||||
|
format: format.combine(
|
||||||
|
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||||
|
format.printf(({ timestamp, level, message }) => `${timestamp} ${level.toUpperCase()} ${message}`)
|
||||||
|
),
|
||||||
|
transports: [new transports.Console()]
|
||||||
|
}); */
|
||||||
|
|
||||||
|
const emailContent = `
|
||||||
|
Return-Path: <andreas.knuth@gmail.com>
|
||||||
|
Received: from mail-lf1-f54.google.com (mail-lf1-f54.google.com [209.85.167.54])
|
||||||
|
by inbound-smtp.us-east-2.amazonaws.com with SMTP id tl8bodt75rl99agvurj9pt06aaphgs5pj3l7ci01
|
||||||
|
for test@bizmatch.net;
|
||||||
|
Mon, 07 Jul 2025 22:29:30 +0000 (UTC)
|
||||||
|
X-SES-Spam-Verdict: PASS
|
||||||
|
X-SES-Virus-Verdict: PASS
|
||||||
|
Received-SPF: pass (spfCheck: domain of _spf.google.com designates 209.85.167.54 as permitted sender) client-ip=209.85.167.54; envelope-from=andreas.knuth@gmail.com; helo=mail-lf1-f54.google.com;
|
||||||
|
Authentication-Results: amazonses.com;
|
||||||
|
spf=pass (spfCheck: domain of _spf.google.com designates 209.85.167.54 as permitted sender) client-ip=209.85.167.54; envelope-from=andreas.knuth@gmail.com; helo=mail-lf1-f54.google.com;
|
||||||
|
dkim=pass header.i=@gmail.com;
|
||||||
|
dmarc=pass header.from=gmail.com;
|
||||||
|
X-SES-RECEIPT: AEFBQUFBQUFBQUFHZ2VxMTdrTDl5UCtYZjRQUHNhL3YwRWo4YXNNbEVYdGdqUTducmt1L25UY0pMNFNqMitXQWZCbnVsYW1seVdseFQzT1lZT2VUVEtCUWl0b2VDVk94SU5xN3p1K1R3d2lOT0hkb2ZIclEvS0JqNVdtRzAvNnJtejlsOE42dTU3ZTV5K2NIQ0lvOEJtQ0hBSkhrZ2JURHJjWXpVYU5EOEZnMnc0SU8xeS9TUVR6OXZxdmt4WVdCMzNuaUJ2TE9xRzN1WHdZM3VFdUcwYzBrZm9OV3BFMEwrZURnb25PY2h2dVExRXV1Q0ZCSzhIeGRsSTZFdXZwUUVzQ2JQUFVzUjFvZnI0U2g4aXBFZDQxQVNFanJLYXdNS2crKzZPanJySHJWckdXQ21hZ2NOQWc9PQ==
|
||||||
|
X-SES-DKIM-SIGNATURE: a=rsa-sha256; q=dns/txt; b=A7ZG4osvHSz8Grirn5FNbtnZtZoxA4SwzM4NX2SD3xlmdGZ9gEs7o5QAaexpqFo+tVHGze6kCXShR/m5e+Ccoelv+pYGuQsM0UQukPH567mOTd6DBsUnwgGoWyzkR4LyBMSGKX50m3plpMr7OsfydgTtSgmNqx6TaW2uTqAmHG4=; c=relaxed/simple; s=ndjes4mrtuzus6qxu3frw3ubo3gpjndv; d=amazonses.com; t=1751927370; v=1; bh=kl0ZVgKAgL2tPEaQmtmEdFkMF0Wkh08RlXtja41/naQ=; h=From:To:Cc:Bcc:Subject:Date:Message-ID:MIME-Version:Content-Type:X-SES-RECEIPT;
|
||||||
|
Received: by mail-lf1-f54.google.com with SMTP id 2adb3069b0e04-553aba2f99eso547669e87.3
|
||||||
|
for <test@bizmatch.net>; Mon, 07 Jul 2025 15:29:29 -0700 (PDT)
|
||||||
|
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
|
||||||
|
d=gmail.com; s=20230601; t=1751927368; x=1752532168; darn=bizmatch.net;
|
||||||
|
h=to:subject:message-id:date:from:mime-version:from:to:cc:subject
|
||||||
|
:date:message-id:reply-to;
|
||||||
|
bh=kl0ZVgKAgL2tPEaQmtmEdFkMF0Wkh08RlXtja41/naQ=;
|
||||||
|
b=Dv7XQW93T4nV5kY0HB5qVq0H1iB0cYfdQMzSGyu+chsPKK5N+8INipWr1bulAYA4OM
|
||||||
|
UKP7EiY4j3zzrxVLFMjboztDfI4PG2oAYSdxIah+jTdgpliVhIeGqvM87SH4pfSVPnOB
|
||||||
|
JygDwwhB25s9wfwM7XDQ+uaAg/Fdwc6kgXf1d2k28gdnV9cuhToWMBAdCZG+0pic969P
|
||||||
|
HEJlLY+KJBVIvzl8JcVZ6ReT8FeQWGwKfzdrpG8PXyYO8MH4FtAmfji4Av4PO/Q2Ky/u
|
||||||
|
3Razz1QTf8R7dHCndAdXCa5INrMaCQOvXRWMMc22sIfMTtM0RKieL7jfp+T4kzcWd8bp
|
||||||
|
F3BA==
|
||||||
|
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
|
||||||
|
d=1e100.net; s=20230601; t=1751927368; x=1752532168;
|
||||||
|
h=to:subject:message-id:date:from:mime-version:x-gm-message-state
|
||||||
|
:from:to:cc:subject:date:message-id:reply-to;
|
||||||
|
bh=kl0ZVgKAgL2tPEaQmtmEdFkMF0Wkh08RlXtja41/naQ=;
|
||||||
|
b=wDGrMTBQxC0PHTqXvyy2DVWa4au/7y1hd7NkSgRoVX/vVKp1ArewmkY1xWPEG4qp6S
|
||||||
|
X6B9q/qimOqNHs/me0gke2XOeVfgT0Pw+NMSJMf7mCGLZ2+y6sxRttgrh4u2FTxeY0K1
|
||||||
|
RKYdwG7rUcqBYoyU/1h6nJYrotuCs7VYBmWbglChhTJoysmFdnR7eAsD2GnxVM1CDZbI
|
||||||
|
XdVsK/+vOhUHw8uyVB8sILrEtpM4+ETz0BnIveqyldnfXTKj1v1gnXUNi2XgaK+K126b
|
||||||
|
DsXGAP4SwLXUeCHnwGvEfpqTvdVhhOalwR0uCNFWMSOIOuxJbm6hPdU82oz1G6yEUip1
|
||||||
|
pSyw==
|
||||||
|
X-Gm-Message-State: AOJu0YwHBQTUiVzyF4Z+W9Nn+X1DjRnb+ExbYEHAl2nHyJxuSHCcO+92
|
||||||
|
BQdv1ZRanXsQ1Lb4d3pzXr5AoeyNsoAyT3H9Xnu0bZO+zSNpvJ44dQY0WwJc1RKk3WFm8C2xxjl
|
||||||
|
FNPLCFUIKOYoBKSue/IhK5RuJEorabq6yCy11zJUvVQ==
|
||||||
|
X-Gm-Gg: ASbGnctmha0Sl+6s3+7aqdJp4XfRfVYWw1ijYcCHalIyyYoLNA/scbpX0Eqz6/xkLKz
|
||||||
|
Zk8kZ1s2cvvs0Li8JDtKWndBEfOlH2vObiTf1nOjfUXArElHNcXTLauyTSsQhhnX98yufY/FlMM
|
||||||
|
gBVMpCLdinwI7W73wct+qp6JNzoPTJjMqxxr460ujtFDG0M5f6/edKdGc=
|
||||||
|
X-Google-Smtp-Source: AGHT+IGKQO5agz3saT3mvRcQjADlp5mR3Ss7bUoX6CzSwr9FNqw5AekIbPUiMQx0QQJz5SZAtSywG7pqy3jzwJU7gFI=
|
||||||
|
X-Received: by 2002:a05:6512:e90:b0:553:29cc:c47a with SMTP id
|
||||||
|
2adb3069b0e04-556e76ea8b6mr1397840e87.6.1751927367907; Mon, 07 Jul 2025
|
||||||
|
15:29:27 -0700 (PDT)
|
||||||
|
MIME-Version: 1.0
|
||||||
|
From: Andreas Knuth <andreas.knuth@gmail.com>
|
||||||
|
Date: Mon, 7 Jul 2025 17:29:22 -0500
|
||||||
|
X-Gm-Features: Ac12FXylATeuoXeS0LgUwAAC4rygTYy_KTtNVnLhQ8Pv-KiTkX5e5F1AlsvpAY8
|
||||||
|
Message-ID: <CADfCGtb_G+9W11EgfeQhp+V5vb1_gkeq9ZsfqgvsxC9hMNEfJQ@mail.gmail.com>
|
||||||
|
Subject: dsfsd
|
||||||
|
To: test@bizmatch.net
|
||||||
|
Content-Type: multipart/alternative; boundary="0000000000006fc0ff06395e6090"
|
||||||
|
|
||||||
|
--0000000000006fc0ff06395e6090
|
||||||
|
Content-Type: text/plain; charset="UTF-8"
|
||||||
|
|
||||||
|
sdfsdf
|
||||||
|
|
||||||
|
--0000000000006fc0ff06395e6090
|
||||||
|
Content-Type: text/html; charset="UTF-8"
|
||||||
|
|
||||||
|
<div dir="ltr">sdfsdf</div>
|
||||||
|
|
||||||
|
--0000000000006fc0ff06395e6090--
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { simpleParser } = require("mailparser");
|
||||||
|
|
||||||
|
// Callback style
|
||||||
|
simpleParser(source, options, (err, mail) => {
|
||||||
|
if (err) throw err;
|
||||||
|
console.log(mail.subject);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Promise style
|
||||||
|
simpleParser(source, options)
|
||||||
|
.then((mail) => console.log(mail.subject))
|
||||||
|
.catch(console.error);
|
||||||
|
|
||||||
|
// async/await
|
||||||
|
//const mail = await simpleParser(source, options);
|
||||||
|
(async () => {
|
||||||
|
await simpleParser(source, options);
|
||||||
|
})
|
||||||
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)}"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user