Compare commits

...

267 Commits

Author SHA1 Message Date
56b7d065b8 get rid of old stuff 2026-04-10 18:31:16 -05:00
96348c17ce remove bayarea 2026-04-09 17:58:08 -05:00
02b721ff51 remove annaville 2026-04-09 10:16:09 -05:00
3a628fe676 api on port 3000 2026-04-03 15:13:24 -05:00
93535750a2 internal name 2026-04-03 14:42:07 -05:00
c949457f4c greenlens_net 2026-04-03 14:37:12 -05:00
5292e2728f caddy_net 2026-04-03 14:35:20 -05:00
8c1770882b host.docker.internal 2026-04-03 13:28:24 -05:00
6837cf4f17 greenlenspro.com 2026-04-03 13:04:38 -05:00
5c61a74e3d Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-04-03 12:49:52 -05:00
ffe6bdd0f4 greenlenspro.com 2026-04-03 12:49:42 -05:00
f391e35221 innungsapp.com 2026-03-08 09:13:24 -05:00
9f88b58f96 innungsapp 2026-03-02 14:38:15 -06:00
9d32e0962e buddelectric.bayarea-cc.com 2026-02-10 09:15:40 -06:00
4d3ca7bb14 switch back 2026-02-03 11:59:17 -06:00
27f929dfbc update for samsung 2026-02-03 11:49:58 -06:00
be0642c389 reduce caching 2026-02-02 17:52:41 -06:00
69af529410 add volume 2026-02-02 13:13:07 -06:00
c4d8e980da host.docker.internal 2026-02-02 13:11:19 -06:00
676e0a91b6 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-02-02 12:18:10 -06:00
d626b19e4a www.bizmatch.net change 2026-02-02 12:17:51 -06:00
8efc6bfcd2 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-28 16:48:21 -06:00
7fcc380b0f english 2026-01-28 16:48:15 -06:00
38e327c847 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-28 16:40:46 -06:00
5e8559ec97 QRCode 2026-01-28 16:40:40 -06:00
2ee5fc8842 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-28 16:06:05 -06:00
9e107cb96c email setup page 2026-01-28 16:05:59 -06:00
0ef8eb0938 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-28 14:59:05 -06:00
05aac691b3 dfgfdg 2026-01-28 14:59:02 -06:00
de57180976 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-28 14:53:29 -06:00
45ae435223 fix 2026-01-28 14:53:22 -06:00
6cd4371829 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-28 14:44:06 -06:00
eec4458604 apple 2026-01-28 14:43:34 -06:00
4e2369f35c Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-28 14:14:30 -06:00
c73b400f52 ruehrgedoens 2026-01-28 14:14:09 -06:00
c6ee22ef12 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-27 16:47:48 -06:00
4842bd7f03 imap 2026-01-27 16:47:40 -06:00
6f289424e3 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-27 16:43:30 -06:00
900ae6c257 only pop 2026-01-27 16:43:19 -06:00
8ef9420396 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-27 16:38:54 -06:00
2dd1dc21b6 fix for wrong closing .. 2026-01-27 16:38:47 -06:00
0794242198 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-27 16:27:59 -06:00
d926064493 pop 2026-01-27 16:27:54 -06:00
0d6bf386d0 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-27 16:14:58 -06:00
3ad3ab38c8 <LoginName>{header.X-Anchormailbox}</LoginName> 2026-01-27 16:14:53 -06:00
200567f23c Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-27 15:48:03 -06:00
ab4958dfa2 dsfdsf 2026-01-27 15:47:42 -06:00
aa75224d03 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-27 15:20:33 -06:00
4530a2f80e fixed loginname 2026-01-27 14:09:36 -06:00
83ae97e627 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-27 13:58:05 -06:00
fa2ef2b743 mail.email-srvr.com cert 2026-01-27 13:57:16 -06:00
7f9042e612 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-27 13:28:32 -06:00
2cfa226361 debug 2026-01-27 13:28:17 -06:00
d37109a696 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-27 13:18:04 -06:00
4f7dc6f8b4 autodiscover 2026-01-27 13:17:55 -06:00
afdd3d903a Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-27 13:14:47 -06:00
6c6b4d345f autodiscover 2026-01-27 13:14:39 -06:00
28741de633 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-27 09:54:31 -06:00
7d1c0b9a6d remove debug 2026-01-27 09:54:29 -06:00
38b425e1d8 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-27 09:53:58 -06:00
b3d184259e info log 2026-01-27 09:53:54 -06:00
dfddc38c89 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-27 09:52:08 -06:00
29e35bfad6 email_autodiscover 2026-01-27 09:52:05 -06:00
fe9651409e Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-27 09:49:29 -06:00
9438eeaa75 container_name: caddy 2026-01-27 09:49:24 -06:00
286619fc62 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-27 09:48:08 -06:00
969aec9278 import 2026-01-27 09:48:04 -06:00
1bb297f0cf Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-27 09:43:11 -06:00
5dc09a5651 remove autodiscover 2026-01-27 09:43:07 -06:00
d0af616b8d Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-27 09:38:22 -06:00
7cbbaedd5e autodiscover 2026-01-27 09:38:06 -06:00
9acc06646a Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-25 14:34:56 -06:00
f541ea9248 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-25 14:34:03 -06:00
96fa643095 roundcube 2026-01-25 14:33:44 -06:00
c70f031dff Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-01 09:58:47 -06:00
61820fe772 qrmaster 2026-01-01 09:58:22 -06:00
7e3fac6907 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-01 06:24:03 -06:00
013f1c8994 path to container ... 2026-01-01 06:23:54 -06:00
c24049d3fb Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-01 06:18:58 -06:00
dde671fe3d neues root 2026-01-01 06:18:51 -06:00
f340cc0a43 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2026-01-01 06:11:35 -06:00
aded85eb66 use config-email in prod mode 2026-01-01 06:11:08 -06:00
9dd22589de Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-12-31 11:37:18 -06:00
0ddfa51265 api.email-bayarea.com 2025-12-31 11:36:59 -06:00
4a5222a781 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-12-31 10:56:21 -06:00
490721c8b6 pictures in bizmatch-projects folder 2025-12-31 10:55:53 -06:00
ce6a115684 config.email-bayarea.com 2025-12-31 10:50:35 -06:00
f79dde2d1d Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-11-17 15:52:26 -06:00
08d1c0f265 new certificate generation 2025-11-17 15:37:03 -06:00
3b3d20f89a actual function 2025-10-18 16:45:56 -05:00
286de26c87 actual 2025-10-16 21:34:23 -05:00
1b899985a1 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-10-13 15:47:09 -05:00
cd731c502b fix 2025-10-13 15:47:06 -05:00
f0096bc27f Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-10-13 15:22:20 -05:00
ac008aff8e SES - Lambda Invokation added 2025-10-13 15:21:54 -05:00
432259d459 remove emails prefix 2025-10-10 17:42:30 -05:00
b9066a8f59 ac 2025-10-09 17:58:27 -05:00
39d50b7d3b Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-10-02 18:46:23 -05:00
5533fbff14 fghfgh 2025-10-02 18:46:18 -05:00
824fbbe3eb Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-10-02 18:41:24 -05:00
08657e7282 xcvxcv 2025-10-02 18:41:19 -05:00
1968faab99 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-10-02 18:35:00 -05:00
2617f049f3 host.docker.internal 2025-10-02 18:34:54 -05:00
3d961d6536 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-10-02 18:29:02 -05:00
7f071435c7 reverse_prox 2025-10-02 18:28:41 -05:00
5469a01893 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-10-01 21:20:53 -05:00
d04bb2f5cb mount 2025-10-01 21:20:49 -05:00
1bf893f683 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-10-01 17:37:27 -05:00
fecfc59988 fix 2025-10-01 17:37:20 -05:00
a2c4ac8685 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-10-01 17:26:09 -05:00
29181ce13b new port 2025-10-01 17:25:57 -05:00
08489162bf Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-10-01 17:20:17 -05:00
d5b7986761 annavillesda 2025-10-01 17:19:53 -05:00
92afa46d5d Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-19 17:32:05 -05:00
3da5e3c814 s3mail 2025-09-19 17:31:51 -05:00
22eadee4cd retry mechanism 2025-09-17 17:38:49 -05:00
a709172a99 Fixes 2025-09-17 17:19:34 -05:00
0f29d06653 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-15 16:43:07 -05:00
ce296ecdab iitwelders 2025-09-15 16:39:41 -05:00
a22a30ac3b Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-14 16:38:02 -05:00
798842ba9b email prefix removed 2025-09-14 16:37:55 -05:00
9b490a9233 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-11 15:24:42 -05:00
5ed6c15ba2 fghfgh 2025-09-11 15:24:40 -05:00
bf5569522a Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-11 15:19:01 -05:00
b8915cb692 dfgdfg 2025-09-11 15:18:59 -05:00
3c84604de8 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-11 15:16:06 -05:00
b210e49ad4 dfgdfg 2025-09-11 15:15:59 -05:00
38a1a08c2a Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-11 15:12:46 -05:00
b8dd30987e sdfsdf 2025-09-11 15:12:43 -05:00
ee26c3dc0a Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-11 15:10:17 -05:00
bc038b0a70 dfgfdg 2025-09-11 15:10:11 -05:00
6331391f1c Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-11 15:06:37 -05:00
22af3a5273 sdfsd 2025-09-11 15:06:31 -05:00
ee3e5952ac Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-11 15:02:56 -05:00
7bd4c73306 fghfgh 2025-09-11 15:02:52 -05:00
541059c0c4 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-11 14:58:11 -05:00
cd545ee056 sdfsdf 2025-09-11 14:58:04 -05:00
ac68074178 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-11 14:53:17 -05:00
c62b72dac0 sdfsdf 2025-09-11 14:53:10 -05:00
ce0b44ac9c Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-11 14:45:50 -05:00
0d2d5d9e38 sdfsdf 2025-09-11 14:45:45 -05:00
3553cdcf59 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-11 14:43:00 -05:00
06f6ee43cc sdfdsf 2025-09-11 14:42:54 -05:00
834aa48d09 dfgdfg 2025-09-11 14:40:42 -05:00
14f6b30444 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-11 14:37:19 -05:00
e9a266534a asdsa 2025-09-11 14:37:16 -05:00
96f3ccbc1a Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-11 14:36:03 -05:00
3b3cb3aec1 sdgfsdf 2025-09-11 14:35:43 -05:00
669ef1b220 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-11 14:04:28 -05:00
ffee5c0568 replace 2025-09-11 14:04:22 -05:00
ee235d5863 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-11 14:00:19 -05:00
06e070f9b7 replace_response 2025-09-11 14:00:15 -05:00
dde2134d87 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-11 13:58:49 -05:00
0f7e8c1dd5 remove 2025-09-11 13:58:34 -05:00
bdcd57aba8 replace-response 2025-09-11 13:58:00 -05:00
57ec03cebe Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-11 13:35:29 -05:00
7282cbdd59 new name 2025-09-11 13:34:59 -05:00
082c465985 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-11 13:14:24 -05:00
101a128c9f order 2025-09-11 13:14:20 -05:00
8b5b984d22 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-11 13:12:03 -05:00
2562fc49b0 korrekturen 2025-09-11 13:10:53 -05:00
212fa09534 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-11 13:02:54 -05:00
a3eef3055e CADDY_VERSION=v2.9.1 2025-09-11 13:02:50 -05:00
f66016633e Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-11 12:56:39 -05:00
57fbce27f6 caddy with replace/response 2025-09-11 12:56:27 -05:00
b10f49a283 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-11 12:24:18 -05:00
47b5b7e8fd iitwelders 2. try 2025-09-11 12:24:15 -05:00
379cc87257 Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-11 12:18:13 -05:00
d80df95f43 iitwelders as proxy 2025-09-11 12:17:57 -05:00
ceaf82d5da Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-11 11:59:04 -05:00
fb5b0cc48e nqsltd & gregknoppcpa 2025-09-11 11:58:21 -05:00
dfadc74b2d :Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-09-01 13:55:13 -05:00
a3873f8649 bizmatch.net redir to www.bizmatch.net 2025-09-01 13:55:10 -05:00
cbe58d4cb2 ses2dms 2025-08-31 16:11:14 -05:00
7692aef4fc Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-08-28 17:34:11 -05:00
31aea63c61 wewer 2025-08-28 17:34:02 -05:00
973be97c70 deleted 2025-08-28 17:11:07 -05:00
bd38b9a5f2 home dir 2025-08-28 17:07:36 -05:00
09f6bf1a27 dms removed 2025-08-28 17:03:07 -05:00
b72cfdc67e sdfsdf 2025-08-28 16:52:29 -05:00
e96631bafd Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-08-28 16:47:25 -05:00
76b8f17ed3 changes 2025-08-28 16:47:18 -05:00
77ec9800aa Merge branch 'master' of git.bizmatch.net:aknuth/docker 2025-08-28 16:43:02 -05:00
3efc9ab1f1 changed port & 2025-08-28 16:43:00 -05:00
4c34709526 merge 2025-08-27 17:56:00 -05:00
b8e3cb6e1f hash 2025-08-27 17:53:54 -05:00
de7a541857 hamptonbrown 2025-08-27 17:50:16 -05:00
e8327f6824 neuer PW Hash 2025-08-25 18:10:27 -05:00
4ad79c3c29 tls 2025-08-25 17:52:36 -05:00
7e2b2ca310 sqllite 2025-08-22 18:05:34 -05:00
4be8ff61c5 change ports 2025-08-22 17:58:50 -05:00
f51e4ab44b changed formatting 2025-08-22 17:56:56 -05:00
338c630f57 docker-mailserver 2025-08-22 17:56:01 -05:00
08501f863a host.docker.internal instead of localhost 2025-08-22 15:19:54 -05:00
d5aaa64555 different comments signs 2025-08-22 15:05:03 -05:00
215d7a3978 cielectrical changed 2025-08-22 14:56:19 -05:00
217ad84815 localhost 2025-08-22 14:36:34 -05:00
e349f0142f cielectrical 2025-08-22 14:20:16 -05:00
8f1fdbfb96 change hostname 2025-08-18 17:20:20 -05:00
766f0d4a18 fancytextstuff 2025-08-18 17:17:41 -05:00
1369944996 dfgdfg 2025-08-01 15:14:23 -05:00
96950e43b0 dfgdfg 2025-08-01 15:13:12 -05:00
b9cc17b997 sdfsdf 2025-08-01 14:58:19 -05:00
e0db2595a9 dfgdfg 2025-08-01 14:55:08 -05:00
a6db7b130b sdfsdf 2025-08-01 12:43:16 -05:00
8bb05f499f sdfsd 2025-08-01 12:41:35 -05:00
0ce09ef969 updated env variables 2025-08-01 10:55:11 -05:00
e73641f258 sdfsdf 2025-07-31 17:51:52 -05:00
2e3ca446f5 asdas 2025-07-31 17:45:35 -05:00
1d88681f28 sdfdsf 2025-07-31 17:38:05 -05:00
24f6890357 sdfsdf 2025-07-31 17:37:22 -05:00
fc9102c5d3 sdfdsf 2025-07-31 17:28:10 -05:00
210148c305 sdfsdf 2025-07-31 17:20:37 -05:00
b126861406 sdfsdf 2025-07-31 16:53:20 -05:00
ee7b6fd1fb prod preparation 2025-07-30 17:30:39 -05:00
04e9f4ccec asdsad 2025-07-27 17:16:17 -05:00
e7519cc0b5 asdsad 2025-07-27 16:59:13 -05:00
3a585ea604 fsdf 2025-07-27 16:54:47 -05:00
1525cda50f sdfgds 2025-07-27 16:52:30 -05:00
80acd37da7 sdf 2025-07-27 15:59:45 -05:00
4efb69a356 asdfas 2025-07-27 15:59:17 -05:00
53827a8277 fix 2025-07-27 15:55:20 -05:00
55959ce22d test 2025-07-27 15:48:30 -05:00
d550342492 test 2025-07-27 15:20:04 -05:00
ade3a5780f no exception even if recipient list is empty 2025-07-21 15:33:04 -05:00
675c00209c check for valid Domain 2025-07-21 11:51:01 -05:00
5fadef1aac changes 2025-07-21 11:40:40 -05:00
7012f1ffd3 rejectUnauthorized: false 2025-07-15 10:26:53 -05:00
14a212cf14 increase payload 2025-07-15 10:19:39 -05:00
c66f27225c syntax error corrections 2025-07-15 10:13:54 -05:00
6359adb807 fixed 2025-07-08 16:36:36 -05:00
cb25a44d69 mailparser updated 2025-07-08 16:31:29 -05:00
9a3e279212 db 2025-07-07 15:18:53 -05:00
d84a5a69b0 db 2025-07-07 15:14:34 -05:00
b8152cbc39 db 2025-07-07 15:06:07 -05:00
b8fc26dc46 Port change 2025-07-07 15:05:22 -05:00
493743e8aa updated 2025-07-07 14:53:41 -05:00
b556ac8283 container_name changed 2025-07-07 14:13:59 -05:00
65866de63b upodate to node.js 2025-07-07 14:12:09 -05:00
0663a7c6bc autodiscover 2025-06-27 17:52:52 -05:00
9862c77f18 mail.email-srvr.com added 2025-06-23 20:06:47 -05:00
8d1afdeffd web.email-bayarea.com added 2025-06-23 19:32:45 -05:00
3a528b37a1 change retry method 2025-06-17 09:35:58 -05:00
68f5f0c3f4 check inboxes & domains 2025-06-16 19:53:50 -05:00
c645f71225 check for valid recipients 2025-06-16 19:38:45 -05:00
b2a0c6e611 requests 2025-06-15 22:59:05 -05:00
d92618f225 import requests 2025-06-15 22:56:56 -05:00
05b81d28db MAILCOW_API 2025-06-15 22:53:50 -05:00
ac7ebbb7f3 bugfix 2025-06-15 22:51:10 -05:00
9ef67da70e domain_exists 2025-06-15 22:35:12 -05:00
d6f90f444b fixed retry 2025-06-15 13:27:44 -05:00
3deaedc235 bugfix retry 2025-06-14 21:22:31 -05:00
d98a9086ca bugfix for retry function 2025-06-14 21:20:13 -05:00
34393b0807 /stats/<domain> 2025-06-14 20:50:01 -05:00
4388f6efc2 parse body, new metadata 2025-06-14 20:27:56 -05:00
fdbc32bed9 verbessertes Fehlerhandling 2025-06-14 20:22:34 -05:00
4943bccb3e Korrekturen 2025-06-13 17:27:05 -05:00
dc57e08030 bugfix 2025-06-13 16:44:03 -05:00
ce87a9e3a5 BugFixes 2025-06-13 16:40:54 -05:00
bd6d7a8c92 move of logging 2025-06-13 16:17:30 -05:00
67b97f514b another logging 2025-06-13 16:15:49 -05:00
434f94f882 BugFixing 2025-06-13 16:08:11 -05:00
26adce6ecf logging 2025-06-13 15:59:28 -05:00
fc6fa76bc0 updated Lambda & REST API 2025-06-13 11:00:52 -05:00
0d0391b6ee sendmail 2025-06-12 20:09:41 -05:00
24b05aa210 changes 2025-06-12 19:12:21 -05:00
9287e9be8b removed 2025-06-12 17:38:22 -05:00
45 changed files with 14652 additions and 2119 deletions

5
.gitignore vendored
View File

@@ -2,4 +2,7 @@
auth
.env
.venv*
__pycache__
__pycache__
node_modules
ses-lambda-python/*
!ses-lambda-python/lambda_function.py

View File

@@ -1,6 +0,0 @@
DB_HOST=postgres
DB_PORT=5432
DB_SCHEMA=public
POSTGRES_DB=bizmatch
POSTGRES_USER=bizmatch
POSTGRES_PASSWORD=xieng7Seih

View 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

View 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`

View File

@@ -1,20 +0,0 @@
services:
postgres:
container_name: bizmatchdb
image: postgres:latest
restart: always
volumes:
- ${PWD}/bizmatchdb-data:/var/lib/postgresql/data
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
ports:
- "5432:5432"
networks:
- bizmatch
networks:
bizmatch:
external: true

File diff suppressed because it is too large Load Diff

View File

@@ -4,35 +4,59 @@
acme_ca https://acme-v02.api.letsencrypt.org/directory
debug
}
bizmatch.net {
}
# 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
}
bayarea-cc.com {
# TLS-Direktive entfernen, falls Cloudflare die Verbindung terminiert
# tls {
# dns cloudflare {env.CLOUDFLARE_API_TOKEN}
# }
handle /api {
reverse_proxy host.docker.internal:3001
}
bizmatch.net {
redir https://www.bizmatch.net{uri} permanent
}
www.qrmaster.net {
handle {
root * /app
try_files {path} /index.html
file_server
reverse_proxy host.docker.internal:3050
}
log {
output stderr
output file /var/log/caddy/qrmaster.log
format console
}
encode gzip
}
www.bayarea-cc.com {
redir https://bayarea-cc.com{uri} permanent
qrmaster.net {
redir https://www.qrmaster.net{uri} permanent
}
www.innungsapp.com {
handle {
reverse_proxy host.docker.internal:3010
}
log {
output file /var/log/caddy/innungsapp.log
format console
}
encode gzip
}
innungsapp.com {
redir https://www.innungsapp.com{uri} permanent
}
auth.bizmatch.net {
reverse_proxy https://bizmatch-net.firebaseapp.com {
header_up Host bizmatch-net.firebaseapp.com
@@ -45,48 +69,30 @@ gitea.bizmatch.net {
reverse_proxy gitea:3500
}
dev.bizmatch.net {
handle /pictures/* {
root * /home/aknuth/git/bizmatch-project/bizmatch-server
file_server
api.bizmatch.net {
reverse_proxy host.docker.internal:3001 { # Neu: Proxy auf Prod-Port 3001
header_up X-Real-IP {http.request.header.CF-Connecting-IP}
header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
header_up X-Forwarded-Proto {http.request.header.X-Forwarded-Proto}
header_up CF-IPCountry {http.request.header.CF-IPCountry}
}
}
greenlenspro.com {
encode zstd gzip
@storage path /storage /storage/*
handle @storage {
uri strip_prefix /storage
reverse_proxy minio:9000
}
@api path /api /api/* /auth /auth/* /v1 /v1/* /health /plants /plants/*
handle @api {
reverse_proxy api:3000
}
handle {
root * /srv
try_files {path} {path}/ /index.html
file_server
reverse_proxy landing:3000
}
log {
output file /var/log/caddy/access.log {
roll_size 10MB
roll_keep 5
roll_keep_for 48h
}
}
encode gzip
}
api-dev.bizmatch.net {
reverse_proxy host.docker.internal:3000 {
header_up X-Real-IP {http.request.header.CF-Connecting-IP}
header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
header_up X-Forwarded-Proto {http.request.header.X-Forwarded-Proto}
header_up CF-IPCountry {http.request.header.CF-IPCountry}
}
}
mailsync.bizmatch.net {
reverse_proxy host.docker.internal:5000 {
header_up X-Real-IP {http.request.header.CF-Connecting-IP}
header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
header_up X-Forwarded-Proto {http.request.header.X-Forwarded-Proto}
header_up CF-IPCountry {http.request.header.CF-IPCountry}
}
}
mail.andreasknuth.de autodiscover.mail.andreasknuth.de autoconfig.mail.andreasknuth.de {
reverse_proxy nginx-mailcow:8080
}

13
caddy/Dockerfile.caddy Normal file
View 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

View File

@@ -1,7 +1,10 @@
services:
caddy:
image: custom-caddy:2.9.1-rr1
container_name: caddy
image: iarekylew00t/caddy-cloudflare:latest
build:
context: .
dockerfile: Dockerfile.caddy
restart: unless-stopped
ports:
- "80:80"
@@ -13,18 +16,25 @@ services:
- keycloak
- gitea
- mail_network
- greenlens_net
volumes:
- $PWD/Caddyfile:/etc/caddy/Caddyfile
- $PWD/email_autodiscover:/etc/caddy/email_autodiscover
- $PWD/email.mobileconfig.tpl:/etc/caddy/email.mobileconfig.tpl
- $PWD/email-setup:/var/www/email-setup
- caddy_data:/data
- caddy_config:/config
#- /home/aknuth/git/bizmatch/dist/bizmatch/browser:/srv
- /home/aknuth/git/bizmatch-project/bizmatch/dist/bizmatch/browser:/srv
- /home/aknuth/git/bizmatch-project/bizmatch/dist/bizmatch/browser:/home/aknuth/git/bizmatch-project/bizmatch/dist/bizmatch/browser
- /home/aknuth/git/bizmatch-project-prod/bizmatch/dist/bizmatch/browser:/home/aknuth/git/bizmatch-project-prod/bizmatch/dist/bizmatch/browser
- /home/aknuth/git/bizmatch-project/bizmatch-server/pictures:/home/aknuth/git/bizmatch-project/bizmatch-server/pictures
- /home/aknuth/git/bizmatch-project-prod/bizmatch-server/pictures:/home/aknuth/git/bizmatch-project-prod/bizmatch-server/pictures
- /home/aknuth/git/annaville-sda-site/dist:/home/aknuth/git/annaville-sda-site/dist:ro # ← DAS FEHLT!
- /home/aknuth/git/bay-area-affiliates/dist/bay-area-affiliates/browser:/app
- /home/aknuth/log/caddy:/var/log/caddy
- /home/aknuth/git/config-email/frontend/dist:/home/aknuth/git/config-email/frontend/dist:ro
environment:
- CLOUDFLARE_API_TOKEN=q1P7J3uqS96FGj_iiX2mI8y1ulTaIFrTp8tyTXhG
- CLOUDFLARE_EMAIL=andreas.knuth@gmail.com
- CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}
- CLOUDFLARE_EMAIL=${CLOUDFLARE_EMAIL}
networks:
bizmatch:
@@ -35,6 +45,8 @@ networks:
external: true
mail_network:
external: true
greenlens_net:
external: true
volumes:
caddy_data:

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View 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>

View 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
View 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
}
}

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# awsdomain.sh - Konfiguriert Cloudflare mit den Amazon SES Angaben
if [ -z "$DOMAIN_NAME" ]; then
echo "Fehler: DOMAIN_NAME ist nicht gesetzt."
echo "Bitte setzen Sie die Variable mit: export DOMAIN_NAME='IhreDomain.de'"

View File

@@ -18,7 +18,7 @@ fi
# Konfiguration
AWS_REGION=${AWS_REGION:-"us-east-2"}
EMAIL_PREFIX=${EMAIL_PREFIX:-"emails/"}
EMAIL_PREFIX=${EMAIL_PREFIX:-""}
RULE_NAME="store-$(echo "$DOMAIN_NAME" | tr '.' '-')-to-s3"
echo "=== SES Konfiguration für $DOMAIN_NAME ==="
@@ -89,6 +89,62 @@ else
echo "Rule Set 'bizmatch-ruleset' ist bereits aktiv."
fi
# ------------------------
# Lambda-Funktion mit SES verknüpfen
# ------------------------
echo "Verknüpfe Lambda-Funktion 'ses-to-sqs' mit SES..."
# Lambda ARN ermitteln
LAMBDA_ARN=$(aws lambda get-function \
--function-name ses-to-sqs \
--region ${AWS_REGION} \
--query 'Configuration.FunctionArn' \
--output text)
if [ -z "$LAMBDA_ARN" ]; then
echo "FEHLER: Lambda-Funktion 'ses-to-sqs' nicht gefunden!"
echo "Bitte zuerst Lambda-Funktion deployen."
exit 1
fi
echo "Lambda ARN: $LAMBDA_ARN"
# SES Permission für Lambda hinzufügen (falls noch nicht vorhanden)
echo "Füge SES-Berechtigung zur Lambda-Funktion hinzu..."
aws lambda add-permission \
--function-name ses-to-sqs \
--statement-id "AllowSESInvoke-${DOMAIN_NAME//./}" \
--action "lambda:InvokeFunction" \
--principal ses.amazonaws.com \
--source-account $(aws sts get-caller-identity --query Account --output text) \
--region ${AWS_REGION} 2>/dev/null || echo "Permission bereits vorhanden"
# Receipt Rule UPDATE: Lambda Action hinzufügen
echo "Aktualisiere Receipt Rule mit Lambda Action..."
aws ses update-receipt-rule --rule-set-name "bizmatch-ruleset" --rule '{
"Name": "'"${RULE_NAME}"'",
"Enabled": true,
"ScanEnabled": true,
"Actions": [
{
"S3Action": {
"BucketName": "'"${S3_BUCKET_NAME}"'",
"ObjectKeyPrefix": "'"${EMAIL_PREFIX}"'"
}
},
{
"LambdaAction": {
"FunctionArn": "'"${LAMBDA_ARN}"'",
"InvocationType": "Event"
}
}
],
"TlsPolicy": "Require",
"Recipients": ["'"${DOMAIN_NAME}"'"]
}' --region ${AWS_REGION}
echo "✅ Lambda-Funktion erfolgreich mit SES verknüpft!"
echo "SES-Konfiguration für $DOMAIN_NAME abgeschlossen."
echo
echo "WICHTIG: Überprüfen Sie die Ausgabe oben für DNS-Einträge, die Sie bei Ihrem DNS-Provider setzen müssen:"

View File

@@ -4,7 +4,6 @@
# Setze deine API-Schlüssel und Zone-ID als Umgebungsvariablen oder ersetze sie direkt
# CF_ZONE_ID="1b7756cee93ed8ba8c05bdc3cb0a5da8" # Die Zone-ID deiner Domain bei Cloudflare
# DOMAIN_NAME="andreasknuth.de" # Deine Domain
AWS_REGION="us-east-2" # AWS-Region
if [ -z "$DOMAIN_NAME" ]; then
echo "Fehler: DOMAIN_NAME ist nicht gesetzt."
@@ -147,7 +146,7 @@ create_dns_record "MX" "mail.${DOMAIN_NAME}" "feedback-smtp.${AWS_REGION}.amazon
# CNAME für mail.{Domain} anlegen
echo "CNAME für mail.${DOMAIN_NAME} anlegen bei Cloudflare..."
create_dns_record "CNAME" "imap.${DOMAIN_NAME}" "${DOMAIN_NAME}" "false" 1
create_dns_record "CNAME" "imap.${DOMAIN_NAME}" "${DOMAIN_NAME}" "false" 3600
# SPF-Eintrag anlegen
echo "SPF-Eintrag anlegen bei Cloudflare..."

View 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"

View File

@@ -1,15 +1,50 @@
version: '3.8'
services:
email-api:
container_name: email-api
image: python:3.12-slim
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 "pip install --upgrade pip && pip install flask python-dotenv && python app.py"
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
View 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');
});

View File

@@ -5,105 +5,355 @@ import base64
import gzip
import logging
import os
from dotenv import load_dotenv
import time
import boto3
from email.parser import BytesParser
from email.policy import default
from email.utils import getaddresses
import requests
# Python-Version prüfen
if sys.version_info < (3, 12):
raise RuntimeError("Python 3.12 oder höher erforderlich")
# .env-Datei laden
load_dotenv()
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
# --- 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__)
# Konfiguration
SMTP_HOST = "localhost" # Host-Netzwerkmodus
SMTP_PORT = 25 # Fest auf Port 25 ohne TLS
API_TOKEN = os.environ.get('API_TOKEN')
if not API_TOKEN:
raise ValueError("API_TOKEN Umgebungsvariable nicht gesetzt")
load_dotenv = None
try:
from dotenv import load_dotenv as _ld
load_dotenv = _ld
except ImportError:
pass
logger.info(f"API_TOKEN loaded: {API_TOKEN}")
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_header = request.headers.get('Authorization')
if not auth_header or auth_header != f'Bearer {API_TOKEN}':
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 JSON payload'}), 400
return jsonify({'error': 'Invalid payload'}), 400
request_id = data.get('request_id')
email_content = data.get('email_content')
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
logger.info(f"[{request_id}] Processing email for domain: {domain}")
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, []))]
try:
# Entkomprimieren, falls komprimiert
if compressed:
email_bytes = base64.b64decode(email_content)
email_content = gzip.decompress(email_bytes)
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:
email_content = base64.b64decode(email_content)
logger.info(f"Skipping non-existent inbox: {addr}")
# E-Mail-Header parsen
email_msg = BytesParser(policy=default).parsebytes(email_content)
# From-Adresse extrahieren (konsistent mit Lambda)
from_headers = email_msg.get_all('from', [])
if from_headers:
from_addr = getaddresses(from_headers)[0][1]
else:
from_addr = 'lambda@andreasknuth.de' # Fallback
# Empfänger aus allen relevanten Headern extrahieren (konsistent mit Lambda)
to_addrs = [addr for _name, addr in getaddresses(email_msg.get_all('to', []))]
cc_addrs = [addr for _name, addr in getaddresses(email_msg.get_all('cc', []))]
bcc_addrs = [addr for _name, addr in getaddresses(email_msg.get_all('bcc', []))]
recipients = to_addrs + cc_addrs + bcc_addrs
# Debug-Ausgabe für Empfänger
logger.info(f"[{request_id}] To-Adressen: {to_addrs}")
logger.info(f"[{request_id}] CC-Adressen: {cc_addrs}")
logger.info(f"[{request_id}] BCC-Adressen: {bcc_addrs}")
logger.info(f"[{request_id}] Alle Empfänger: {recipients}")
if not recipients:
# Zusätzliche Debug-Info
logger.error(f"[{request_id}] Verfügbare Header: {list(email_msg.keys())}")
logger.error(f"[{request_id}] To-Header roh: {email_msg.get('To')}")
logger.error(f"[{request_id}] CC-Header roh: {email_msg.get('Cc')}")
raise ValueError("Keine Empfänger (To/CC/BCC) in der E-Mail gefunden")
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
logger.info(f"[{request_id}] From: {from_addr}, Recipients: {recipients}")
# 3) Senden an die gefilterten Adressen
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as smtp:
smtp.sendmail(from_addr, valid_recipients, email_bytes)
# An Postfix weiterleiten - für jeden Empfänger einzeln
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as smtp:
smtp.mail(from_addr)
for recipient in recipients:
smtp.rcpt(recipient)
smtp.data(email_content)
logger.info(f"[{request_id}] Email forwarded to Postfix for {domain} - {len(recipients)} recipients")
return jsonify({
'message': 'Email forwarded',
'forwarded_to': valid_recipients
}), 200
return jsonify({
'message': 'Email processed',
'request_id': request_id,
'recipients_count': len(recipients),
'recipients': recipients
}), 200
except Exception as e:
logger.error(f"[{request_id}] Error processing email: {str(e)}")
return jsonify({'error': str(e), 'request_id': request_id}), 500
@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)
app.run(host='0.0.0.0', port=5000)

View 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
View 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)
);

View File

@@ -20,7 +20,7 @@ services:
volumes:
- gitea-data:/data
#- ./gitea/gitea-ssh:/data/git/.ssh
- /home/git/.ssh/:/data/git/.ssh
#- /home/git/.ssh/:/data/git/.ssh
ports:
- "3500:3500"
- "2222:22"

View File

@@ -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

View 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
})
}

View 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 dont 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);

View 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);
});

View 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--

View 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
View 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&nbsp;grow your online presence. May I send you a quote&amp; 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_--

View 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

File diff suppressed because it is too large Load Diff

4573
ses-lambda-nodejs/eml/6.eml Normal file

File diff suppressed because it is too large Load Diff

View 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--

View 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
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
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
View 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

File diff suppressed because it is too large Load Diff

View 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"
}
}

Binary file not shown.

View 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);
})

View 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)}"
}

View File

@@ -1,20 +0,0 @@
name = "WildDuck Webmail"
[www]
port = 3000
host = "0.0.0.0"
secure = false
[api]
url = "http://wildduck:8080"
[dbs]
redis = "redis://redis:6379/1"
mongodb = "mongodb://mongo:27017/wildduck"
[attachments]
type = "gridstore"
bucket = "attachments"
[log]
level = "info"

View File

@@ -1,81 +0,0 @@
name = "WildDuck Mail Server"
[api]
port = 8080
host = "0.0.0.0"
secure = false
[dbs]
mongodb = "mongodb://mongo:27017/wildduck"
redis = "redis://redis:6379/2"
gridfs = "mongodb://mongo:27017/wildduck"
[imap]
port = 143
host = "0.0.0.0"
secure = false
starttls = true
[imaps]
port = 993
host = "0.0.0.0"
secure = true
[pop3]
port = 110
host = "0.0.0.0"
secure = false
starttls = true
[pop3s]
port = 995
host = "0.0.0.0"
secure = true
[smtp]
port = 25
host = "0.0.0.0"
secure = false
starttls = true
[submission]
port = 587
host = "0.0.0.0"
secure = false
starttls = true
authRequired = true
[attachments]
type = "gridstore"
bucket = "attachments"
[log]
level = "info"
[sender]
name = "mail.andreasknuth.de"
address = "mailer-daemon@andreasknuth.de"
[emailDomain]
default = "andreasknuth.de"
[acme]
enabled = false
[elasticsearch]
enabled = false
[lmtp]
enabled = false
[tasks]
enabled = true
deleteAfter = 30
# Access Control Fix
[accesscontrol]
enabled = false
# Roles Fix
[roles]
enabled = false

View File

@@ -1,68 +0,0 @@
services:
wildduck:
image: ghcr.io/zone-eu/wildduck:1.45.5
container_name: wildduck-server
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080" # API
- "143:143" # IMAP
- "993:993" # IMAPS
- "110:110" # POP3
- "995:995" # POP3S
- "587:587" # SMTP Submission
- "25:25" # SMTP
depends_on:
- mongo
- redis
volumes:
- ./config/wildduck:/wildduck/config
networks:
- mail_network
wildduck-webmail:
image: nodemailer/wildduck-webmail:latest
container_name: wildduck-webmail
restart: unless-stopped
ports:
- "127.0.0.1:4000:3000"
depends_on:
- mongo
- redis
- wildduck
environment:
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_DB=1
- API_HOST=wildduck
- API_PORT=8080
volumes:
- ./config/wildduck-webmail:/app/config
networks:
- mail_network
mongo:
image: mongo:7.0
container_name: wildduck-mongo
restart: unless-stopped
volumes:
- mongo_data:/data/db
networks:
- mail_network
redis:
image: redis:7.2-alpine
container_name: wildduck-redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks:
- mail_network
volumes:
mongo_data:
redis_data:
networks:
mail_network:
external: true