stratal 0.0.21 → 0.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/bin/cloudflare-workers-loader.mjs +80 -7
- package/dist/bin/cloudflare-workers-loader.mjs.map +1 -1
- package/dist/bin/quarry.mjs +84 -160
- package/dist/bin/quarry.mjs.map +1 -1
- package/dist/cache/index.d.mts +8 -46
- package/dist/cache/index.d.mts.map +1 -1
- package/dist/cache/index.mjs +134 -97
- package/dist/cache/index.mjs.map +1 -1
- package/dist/{cache.service-DsnKuNyO.d.mts → cache.service-uElmBtdS.d.mts} +29 -39
- package/dist/cache.service-uElmBtdS.d.mts.map +1 -0
- package/dist/{command-BgSlsS4M.mjs → command-BvmUAPPQ.mjs} +15 -4
- package/dist/command-BvmUAPPQ.mjs.map +1 -0
- package/dist/{command-Cmmf0oHX.d.mts → command-CPhFHjG3.d.mts} +3 -2
- package/dist/command-CPhFHjG3.d.mts.map +1 -0
- package/dist/command-not-found.error-ONAZ2Bpk.mjs +14 -0
- package/dist/command-not-found.error-ONAZ2Bpk.mjs.map +1 -0
- package/dist/config/index.d.mts +24 -11
- package/dist/config/index.d.mts.map +1 -1
- package/dist/config/index.mjs +32 -57
- package/dist/config/index.mjs.map +1 -1
- package/dist/{consumer-registry-B7yUNh0q.d.mts → consumer-registry-D3iMTSdy.d.mts} +54 -22
- package/dist/consumer-registry-D3iMTSdy.d.mts.map +1 -0
- package/dist/container-storage-BmOJ4_Na.mjs +52 -0
- package/dist/container-storage-BmOJ4_Na.mjs.map +1 -0
- package/dist/{controller.decorator-B9vwn0zK.mjs → controller.decorator-C5UVeJS3.mjs} +8 -8
- package/dist/controller.decorator-C5UVeJS3.mjs.map +1 -0
- package/dist/cron/index.d.mts +103 -7
- package/dist/cron/index.d.mts.map +1 -1
- package/dist/cron/index.mjs +2 -2
- package/dist/cron-job-NesZRk8F.d.mts +58 -0
- package/dist/cron-job-NesZRk8F.d.mts.map +1 -0
- package/dist/{cron-manager-DQSK8uoV.mjs → cron.module-Bgzq5hiT.mjs} +47 -17
- package/dist/cron.module-Bgzq5hiT.mjs.map +1 -0
- package/dist/decorate-CuAoSZvs.mjs +16 -0
- package/dist/deep-merge-ByiAOZ3r.mjs +18 -0
- package/dist/deep-merge-ByiAOZ3r.mjs.map +1 -0
- package/dist/di/index.d.mts +2 -2
- package/dist/di/index.mjs +4 -3
- package/dist/di-DseMn-z9.mjs +524 -0
- package/dist/di-DseMn-z9.mjs.map +1 -0
- package/dist/email/index.d.mts +40 -122
- package/dist/email/index.d.mts.map +1 -1
- package/dist/email/index.mjs +446 -131
- package/dist/email/index.mjs.map +1 -1
- package/dist/en-CDZBMcc1.mjs +202 -0
- package/dist/en-CDZBMcc1.mjs.map +1 -0
- package/dist/{env-D1rcZ8_r.d.mts → env-ug22bJj7.d.mts} +1 -1
- package/dist/env-ug22bJj7.d.mts.map +1 -0
- package/dist/errors/index.d.mts +2 -2
- package/dist/errors/index.mjs +4 -2
- package/dist/errors-mXYxG0XB.mjs +333 -0
- package/dist/errors-mXYxG0XB.mjs.map +1 -0
- package/dist/events/index.d.mts +14 -3
- package/dist/events/index.d.mts.map +1 -1
- package/dist/events/index.mjs +2 -2
- package/dist/{events-CzCV8jI8.mjs → events-BXJGZjpG.mjs} +23 -13
- package/dist/events-BXJGZjpG.mjs.map +1 -0
- package/dist/exception-context-kEoMFwze.mjs +429 -0
- package/dist/exception-context-kEoMFwze.mjs.map +1 -0
- package/dist/{gateway-context-CXmXtaUP.mjs → gateway-context-TMu_AlJt.mjs} +38 -31
- package/dist/gateway-context-TMu_AlJt.mjs.map +1 -0
- package/dist/guards/index.d.mts +3 -3
- package/dist/guards/index.d.mts.map +1 -1
- package/dist/guards/index.mjs +1 -1
- package/dist/{guards-DU1_J9YA.mjs → guards-DALPXy3_.mjs} +6 -5
- package/dist/guards-DALPXy3_.mjs.map +1 -0
- package/dist/hono-app-CvV3hOfT.mjs +161 -0
- package/dist/hono-app-CvV3hOfT.mjs.map +1 -0
- package/dist/{http-method.decorator-BrgHMdLQ.mjs → http-method.decorator-ByWZb9DO.mjs} +7 -6
- package/dist/http-method.decorator-ByWZb9DO.mjs.map +1 -0
- package/dist/i18n/index.d.mts +238 -3
- package/dist/i18n/index.d.mts.map +1 -0
- package/dist/i18n/index.mjs +39 -3
- package/dist/i18n/index.mjs.map +1 -0
- package/dist/i18n/messages/en/index.d.mts +2 -2
- package/dist/i18n/messages/en/index.mjs +2 -2
- package/dist/i18n/utils/index.d.mts +4 -26
- package/dist/i18n/utils/index.d.mts.map +1 -1
- package/dist/i18n/utils/index.mjs +2 -2
- package/dist/i18n/validation/index.d.mts +3 -2
- package/dist/i18n/validation/index.mjs +4 -2
- package/dist/i18n.module-DRQAZoSZ.mjs +222 -0
- package/dist/i18n.module-DRQAZoSZ.mjs.map +1 -0
- package/dist/i18n.tokens-CZ_v8oyS.mjs +19 -0
- package/dist/i18n.tokens-CZ_v8oyS.mjs.map +1 -0
- package/dist/{index-7-hU3GTV.d.mts → index-0ItCjaqw.d.mts} +1 -1
- package/dist/index-0ItCjaqw.d.mts.map +1 -0
- package/dist/index-B5JBRcWD.d.mts +544 -0
- package/dist/index-B5JBRcWD.d.mts.map +1 -0
- package/dist/index-BUt92sAE.d.mts +124 -0
- package/dist/index-BUt92sAE.d.mts.map +1 -0
- package/dist/{index-ByOyTmqf.d.mts → index-B_JoEl3V.d.mts} +751 -2229
- package/dist/index-B_JoEl3V.d.mts.map +1 -0
- package/dist/index-DtBNIFuP.d.mts +42 -0
- package/dist/index-DtBNIFuP.d.mts.map +1 -0
- package/dist/{index-C1KvMncZ.d.mts → index-HgOLNruQ.d.mts} +3 -108
- package/dist/index-HgOLNruQ.d.mts.map +1 -0
- package/dist/index.d.mts +6 -43
- package/dist/index.mjs +3 -2
- package/dist/{is-command-C6a7WTPw.mjs → is-command-CEPO9n8c.mjs} +2 -2
- package/dist/{is-command-C6a7WTPw.mjs.map → is-command-CEPO9n8c.mjs.map} +1 -1
- package/dist/{is-seeder-CebjZCDn.mjs → is-seeder-Gvh_AM71.mjs} +1 -1
- package/dist/{is-seeder-CebjZCDn.mjs.map → is-seeder-Gvh_AM71.mjs.map} +1 -1
- package/dist/lazy-module-loader-Ib383jH_.d.mts +60 -0
- package/dist/lazy-module-loader-Ib383jH_.d.mts.map +1 -0
- package/dist/locale-path.service-D-dHiIPc.mjs +165 -0
- package/dist/locale-path.service-D-dHiIPc.mjs.map +1 -0
- package/dist/locale-url-nZrZxqJP.mjs +44 -0
- package/dist/locale-url-nZrZxqJP.mjs.map +1 -0
- package/dist/locale-url.service-C2EWmGdq.mjs +41 -0
- package/dist/locale-url.service-C2EWmGdq.mjs.map +1 -0
- package/dist/logger/index.d.mts +2 -2
- package/dist/logger/index.mjs +170 -2
- package/dist/logger/index.mjs.map +1 -0
- package/dist/macroable/index.d.mts +2 -2
- package/dist/macroable/index.mjs +1 -1
- package/dist/{macroable-BmufBshB.mjs → macroable-cvDTFZ_A.mjs} +1 -1
- package/dist/{macroable-BmufBshB.mjs.map → macroable-cvDTFZ_A.mjs.map} +1 -1
- package/dist/metadata-DzzprcID.mjs +39 -0
- package/dist/metadata-DzzprcID.mjs.map +1 -0
- package/dist/module/index.d.mts +7 -24
- package/dist/module/index.d.mts.map +1 -1
- package/dist/module/index.mjs +10 -2
- package/dist/module/index.mjs.map +1 -0
- package/dist/module-registry-Dm-pqHd3.mjs +554 -0
- package/dist/module-registry-Dm-pqHd3.mjs.map +1 -0
- package/dist/module.decorator-CYHY6pG5.mjs +19 -0
- package/dist/module.decorator-CYHY6pG5.mjs.map +1 -0
- package/dist/openapi/index.d.mts +44 -8
- package/dist/openapi/index.d.mts.map +1 -1
- package/dist/openapi/index.mjs +3 -3
- package/dist/openapi-CstuTM8S.mjs +309 -0
- package/dist/openapi-CstuTM8S.mjs.map +1 -0
- package/dist/{openapi-tools.service-Zs-Ewv7F.mjs → openapi-tools.service-BC5EC3R3.mjs} +8 -2
- package/dist/openapi-tools.service-BC5EC3R3.mjs.map +1 -0
- package/dist/{openapi.service-Bt9bCIrd.d.mts → openapi.service-YhTiJ1bO.d.mts} +3 -3
- package/dist/openapi.service-YhTiJ1bO.d.mts.map +1 -0
- package/dist/quarry/index.d.mts +14 -163
- package/dist/quarry/index.d.mts.map +1 -1
- package/dist/quarry/index.mjs +6 -5
- package/dist/quarry/runner.d.mts +184 -0
- package/dist/quarry/runner.d.mts.map +1 -0
- package/dist/quarry/runner.mjs +945 -0
- package/dist/quarry/runner.mjs.map +1 -0
- package/dist/quarry-registry-CXg0RFXq.d.mts +69 -0
- package/dist/quarry-registry-CXg0RFXq.d.mts.map +1 -0
- package/dist/quarry.module-BuRPGMDm.mjs +312 -0
- package/dist/quarry.module-BuRPGMDm.mjs.map +1 -0
- package/dist/queue/index.d.mts +3 -3
- package/dist/queue/index.mjs +57 -48
- package/dist/queue/index.mjs.map +1 -1
- package/dist/queue.module-nddvxzCB.mjs +613 -0
- package/dist/queue.module-nddvxzCB.mjs.map +1 -0
- package/dist/queue.tokens-DjHnFmre.mjs +11 -0
- package/dist/queue.tokens-DjHnFmre.mjs.map +1 -0
- package/dist/{r2-storage.provider-DuonKeYm.mjs → r2-storage.provider-DCxQt9dD.mjs} +6 -6
- package/dist/r2-storage.provider-DCxQt9dD.mjs.map +1 -0
- package/dist/{rate-limit.decorator-6qzNcSOt.mjs → rate-limit.decorator-BPAie_p3.mjs} +6 -11
- package/dist/rate-limit.decorator-BPAie_p3.mjs.map +1 -0
- package/dist/rate-limiter/index.d.mts +11 -50
- package/dist/rate-limiter/index.d.mts.map +1 -1
- package/dist/rate-limiter/index.mjs +33 -42
- package/dist/rate-limiter/index.mjs.map +1 -1
- package/dist/route-name-DGoBOfPg.mjs +171 -0
- package/dist/route-name-DGoBOfPg.mjs.map +1 -0
- package/dist/route-registration.service-D6vSwiKP.mjs +918 -0
- package/dist/route-registration.service-D6vSwiKP.mjs.map +1 -0
- package/dist/route-registry-CYqLp2Nj.mjs +123 -0
- package/dist/route-registry-CYqLp2Nj.mjs.map +1 -0
- package/dist/router/index.d.mts +2 -2
- package/dist/router/index.mjs +18 -7
- package/dist/router-CWGBD-Bg.mjs +78 -0
- package/dist/router-CWGBD-Bg.mjs.map +1 -0
- package/dist/router-resolver-D4YlPNlm.mjs +88 -0
- package/dist/router-resolver-D4YlPNlm.mjs.map +1 -0
- package/dist/seeder/index.d.mts +16 -11
- package/dist/seeder/index.d.mts.map +1 -1
- package/dist/seeder/index.mjs +5 -3
- package/dist/seeder-7ubkms-Y.mjs +81 -0
- package/dist/seeder-7ubkms-Y.mjs.map +1 -0
- package/dist/seeder-registry-CyUmKsJq.mjs +57 -0
- package/dist/seeder-registry-CyUmKsJq.mjs.map +1 -0
- package/dist/seeder.module-CYYwk3Qk.mjs +15 -0
- package/dist/seeder.module-CYYwk3Qk.mjs.map +1 -0
- package/dist/{signed-url-BQPbv2In.mjs → signed-url-DIU0sK_6.mjs} +1 -1
- package/dist/{signed-url-BQPbv2In.mjs.map → signed-url-DIU0sK_6.mjs.map} +1 -1
- package/dist/storage/index.d.mts +15 -39
- package/dist/storage/index.d.mts.map +1 -1
- package/dist/storage/index.mjs +3 -3
- package/dist/storage/providers/index.d.mts +2 -2
- package/dist/storage/providers/index.d.mts.map +1 -1
- package/dist/storage/providers/index.mjs +1 -1
- package/dist/{storage-D8CBP72Z.mjs → storage-MDZypIE9.mjs} +66 -59
- package/dist/storage-MDZypIE9.mjs.map +1 -0
- package/dist/{storage-provider.interface-Bd6vA4ak.d.mts → storage-provider.interface-ClUwxz4S.d.mts} +2 -3
- package/dist/storage-provider.interface-ClUwxz4S.d.mts.map +1 -0
- package/dist/storage.error-Dnib4VHc.mjs +8 -0
- package/dist/storage.error-Dnib4VHc.mjs.map +1 -0
- package/dist/stratal-DL9M38_s.mjs +383 -0
- package/dist/stratal-DL9M38_s.mjs.map +1 -0
- package/dist/stratal-DwDJPY9N.d.mts +43 -0
- package/dist/stratal-DwDJPY9N.d.mts.map +1 -0
- package/dist/tiered-cache.service-Dv3BhxxE.d.mts +79 -0
- package/dist/tiered-cache.service-Dv3BhxxE.d.mts.map +1 -0
- package/dist/trailing-slash-CFyw8nYu.mjs +34 -0
- package/dist/trailing-slash-CFyw8nYu.mjs.map +1 -0
- package/dist/{types-cySNS_lp.d.mts → types-CmV_9xBD.d.mts} +1 -1
- package/dist/types-CmV_9xBD.d.mts.map +1 -0
- package/dist/uri-h7Q8Jug9.mjs +251 -0
- package/dist/uri-h7Q8Jug9.mjs.map +1 -0
- package/dist/{usage-generator-BUdlhnCK.mjs → usage-generator-DAWYasuP.mjs} +7 -4
- package/dist/usage-generator-DAWYasuP.mjs.map +1 -0
- package/dist/validation-CpOjviyT.mjs +49 -0
- package/dist/validation-CpOjviyT.mjs.map +1 -0
- package/dist/validation.context-CRvmrhq7.mjs +117 -0
- package/dist/validation.context-CRvmrhq7.mjs.map +1 -0
- package/dist/versioning.service-C6aHky8-.mjs +36 -0
- package/dist/versioning.service-C6aHky8-.mjs.map +1 -0
- package/dist/websocket/index.d.mts +16 -14
- package/dist/websocket/index.d.mts.map +1 -1
- package/dist/websocket/index.mjs +2 -2
- package/dist/workers/index.d.mts +2 -2
- package/dist/workers/index.d.mts.map +1 -1
- package/dist/workers/index.mjs +3 -2
- package/dist/workers/index.mjs.map +1 -1
- package/dist/zod-eKqqhZ5_.mjs +72 -0
- package/dist/zod-eKqqhZ5_.mjs.map +1 -0
- package/dist/{index-Bnpfq6uk.d.mts → zod-wecrEVAs.d.mts} +63 -133
- package/dist/zod-wecrEVAs.d.mts.map +1 -0
- package/package.json +28 -39
- package/dist/base-email.provider-CfQCA08m.mjs +0 -42
- package/dist/base-email.provider-CfQCA08m.mjs.map +0 -1
- package/dist/cache.service-DsnKuNyO.d.mts.map +0 -1
- package/dist/cache.tokens-B7Rw1C9Q.mjs +0 -6
- package/dist/cache.tokens-B7Rw1C9Q.mjs.map +0 -1
- package/dist/colors-DJaRDXoS.mjs +0 -16
- package/dist/colors-DJaRDXoS.mjs.map +0 -1
- package/dist/command-BgSlsS4M.mjs.map +0 -1
- package/dist/command-Cmmf0oHX.d.mts.map +0 -1
- package/dist/consumer-registry-B7yUNh0q.d.mts.map +0 -1
- package/dist/controller.decorator-B9vwn0zK.mjs.map +0 -1
- package/dist/cron-manager-CmTimEjf.d.mts +0 -131
- package/dist/cron-manager-CmTimEjf.d.mts.map +0 -1
- package/dist/cron-manager-DQSK8uoV.mjs.map +0 -1
- package/dist/en-DSH_bhh6.mjs +0 -308
- package/dist/en-DSH_bhh6.mjs.map +0 -1
- package/dist/env-D1rcZ8_r.d.mts.map +0 -1
- package/dist/errors-COW9-Mar.mjs +0 -1739
- package/dist/errors-COW9-Mar.mjs.map +0 -1
- package/dist/errors-ORxu1-Bb.mjs +0 -74
- package/dist/errors-ORxu1-Bb.mjs.map +0 -1
- package/dist/events-CzCV8jI8.mjs.map +0 -1
- package/dist/gateway-context-CXmXtaUP.mjs.map +0 -1
- package/dist/guards-DU1_J9YA.mjs.map +0 -1
- package/dist/http-method.decorator-BrgHMdLQ.mjs.map +0 -1
- package/dist/i18n.module-CzXLW9Hy.mjs +0 -2532
- package/dist/i18n.module-CzXLW9Hy.mjs.map +0 -1
- package/dist/index-7-hU3GTV.d.mts.map +0 -1
- package/dist/index-Bnpfq6uk.d.mts.map +0 -1
- package/dist/index-ByOyTmqf.d.mts.map +0 -1
- package/dist/index-C1KvMncZ.d.mts.map +0 -1
- package/dist/index-DBd_2wv8.d.mts +0 -263
- package/dist/index-DBd_2wv8.d.mts.map +0 -1
- package/dist/index-DUzWs0z7.d.mts +0 -494
- package/dist/index-DUzWs0z7.d.mts.map +0 -1
- package/dist/index.d.mts.map +0 -1
- package/dist/logger-DlV7NtvD.mjs +0 -440
- package/dist/logger-DlV7NtvD.mjs.map +0 -1
- package/dist/module-BzLg57FK.mjs +0 -866
- package/dist/module-BzLg57FK.mjs.map +0 -1
- package/dist/openapi-tools.service-Zs-Ewv7F.mjs.map +0 -1
- package/dist/openapi.service-Bt9bCIrd.d.mts.map +0 -1
- package/dist/quarry-registry-BwY2hOxm.mjs +0 -699
- package/dist/quarry-registry-BwY2hOxm.mjs.map +0 -1
- package/dist/queue.module-BhCjZp6H.mjs +0 -409
- package/dist/queue.module-BhCjZp6H.mjs.map +0 -1
- package/dist/r2-storage.provider-DuonKeYm.mjs.map +0 -1
- package/dist/rate-limit.decorator-6qzNcSOt.mjs.map +0 -1
- package/dist/resend.provider-DB4IlFjG.mjs +0 -68
- package/dist/resend.provider-DB4IlFjG.mjs.map +0 -1
- package/dist/seeder-zoEfEw9i.mjs +0 -138
- package/dist/seeder-zoEfEw9i.mjs.map +0 -1
- package/dist/setup-CefZKV_e.mjs +0 -37
- package/dist/setup-CefZKV_e.mjs.map +0 -1
- package/dist/smtp.provider-B6D7zuWX.mjs +0 -76
- package/dist/smtp.provider-B6D7zuWX.mjs.map +0 -1
- package/dist/storage-D8CBP72Z.mjs.map +0 -1
- package/dist/storage-provider.interface-Bd6vA4ak.d.mts.map +0 -1
- package/dist/stratal-CNwpbSZl.mjs +0 -535
- package/dist/stratal-CNwpbSZl.mjs.map +0 -1
- package/dist/types-cySNS_lp.d.mts.map +0 -1
- package/dist/usage-generator-BUdlhnCK.mjs.map +0 -1
- package/dist/validation-DtJwAv7O.mjs +0 -248
- package/dist/validation-DtJwAv7O.mjs.map +0 -1
- /package/dist/{chunk-D1SwGrFN.mjs → chunk-BBjsoOtd.mjs} +0 -0
package/dist/email/index.mjs
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
1
|
+
import { c as Transient, d as inject } from "../di-DseMn-z9.mjs";
|
|
2
|
+
import { a as ApplicationError } from "../container-storage-BmOJ4_Na.mjs";
|
|
3
|
+
import { n as __decorateParam, t as __decorate } from "../decorate-CuAoSZvs.mjs";
|
|
4
|
+
import { LOGGER_TOKENS } from "../logger/index.mjs";
|
|
5
|
+
import "../errors-mXYxG0XB.mjs";
|
|
6
|
+
import { n as Module } from "../module.decorator-CYHY6pG5.mjs";
|
|
7
|
+
import "../module/index.mjs";
|
|
8
|
+
import { t as QUEUE_TOKENS } from "../queue.tokens-DjHnFmre.mjs";
|
|
6
9
|
import "../queue/index.mjs";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
+
import { p as STORAGE_TOKENS } from "../storage-MDZypIE9.mjs";
|
|
11
|
+
import { r as z } from "../zod-eKqqhZ5_.mjs";
|
|
12
|
+
import { t as withZodI18n } from "../validation-CpOjviyT.mjs";
|
|
13
|
+
//#region src/email/email.error.ts
|
|
14
|
+
var EmailError = class extends ApplicationError {};
|
|
15
|
+
//#endregion
|
|
10
16
|
//#region src/email/email.tokens.ts
|
|
11
17
|
/**
|
|
12
18
|
* Dependency Injection Tokens for Email Module
|
|
@@ -36,12 +42,24 @@ const EMAIL_TOKENS = {
|
|
|
36
42
|
EmailProvider: Symbol.for("stratal:email:provider"),
|
|
37
43
|
/**
|
|
38
44
|
* Queue sender for email dispatch.
|
|
39
|
-
* Bound via EmailModule.forRoot({ queue: '
|
|
45
|
+
* Bound via EmailModule.forRoot({ queue: 'QUEUE_BINDING_NAME' })
|
|
40
46
|
*/
|
|
41
47
|
EmailQueue: Symbol.for("stratal:email:queue")
|
|
42
48
|
};
|
|
43
49
|
//#endregion
|
|
44
50
|
//#region src/email/consumers/email.consumer.ts
|
|
51
|
+
/**
|
|
52
|
+
* Strictly decode a base64 attachment payload. `Buffer.from(_, 'base64')` is
|
|
53
|
+
* lenient and silently drops invalid characters, which would ship a corrupt
|
|
54
|
+
* attachment. We re-encode and compare (modulo canonical padding) to reject
|
|
55
|
+
* malformed input loudly instead.
|
|
56
|
+
*/
|
|
57
|
+
function decodeBase64Attachment(content, filename) {
|
|
58
|
+
const buffer = Buffer.from(content, "base64");
|
|
59
|
+
const normalize = (value) => value.replace(/=+$/, "");
|
|
60
|
+
if (normalize(buffer.toString("base64")) !== normalize(content)) throw new EmailError(`Invalid base64 content for attachment "${filename}"`);
|
|
61
|
+
return buffer;
|
|
62
|
+
}
|
|
45
63
|
let EmailConsumer = class EmailConsumer {
|
|
46
64
|
logger;
|
|
47
65
|
providerFactory;
|
|
@@ -64,7 +82,7 @@ let EmailConsumer = class EmailConsumer {
|
|
|
64
82
|
});
|
|
65
83
|
try {
|
|
66
84
|
const resolvedAttachments = await this.resolveAttachments(payload.attachments);
|
|
67
|
-
const result = await
|
|
85
|
+
const result = await this.providerFactory.create().send({
|
|
68
86
|
...payload,
|
|
69
87
|
attachments: resolvedAttachments
|
|
70
88
|
});
|
|
@@ -103,7 +121,7 @@ let EmailConsumer = class EmailConsumer {
|
|
|
103
121
|
return Promise.all(attachments.map(async (attachment) => {
|
|
104
122
|
if ("content" in attachment) return {
|
|
105
123
|
filename: attachment.filename,
|
|
106
|
-
content:
|
|
124
|
+
content: decodeBase64Attachment(attachment.content, attachment.filename),
|
|
107
125
|
contentType: attachment.contentType
|
|
108
126
|
};
|
|
109
127
|
const result = await this.storage.download(attachment.storageKey, attachment.disk);
|
|
@@ -119,12 +137,7 @@ EmailConsumer = __decorate([
|
|
|
119
137
|
Transient(),
|
|
120
138
|
__decorateParam(0, inject(LOGGER_TOKENS.LoggerService)),
|
|
121
139
|
__decorateParam(1, inject(EMAIL_TOKENS.EmailProviderFactory)),
|
|
122
|
-
__decorateParam(2, inject(STORAGE_TOKENS.StorageService))
|
|
123
|
-
__decorateMetadata("design:paramtypes", [
|
|
124
|
-
Object,
|
|
125
|
-
Object,
|
|
126
|
-
Object
|
|
127
|
-
])
|
|
140
|
+
__decorateParam(2, inject(STORAGE_TOKENS.StorageService))
|
|
128
141
|
], EmailConsumer);
|
|
129
142
|
//#endregion
|
|
130
143
|
//#region src/email/services/email.service.ts
|
|
@@ -142,11 +155,16 @@ let EmailService = class EmailService {
|
|
|
142
155
|
* @param input - Email message details
|
|
143
156
|
*/
|
|
144
157
|
async send({ template, ...input }) {
|
|
158
|
+
let html;
|
|
159
|
+
if (template) {
|
|
160
|
+
const { render } = await import("@react-email/render");
|
|
161
|
+
html = await render(template);
|
|
162
|
+
}
|
|
145
163
|
await this.queue.dispatch({
|
|
146
164
|
type: "email.send",
|
|
147
165
|
payload: {
|
|
148
166
|
...input,
|
|
149
|
-
html
|
|
167
|
+
html
|
|
150
168
|
}
|
|
151
169
|
});
|
|
152
170
|
}
|
|
@@ -162,102 +180,424 @@ let EmailService = class EmailService {
|
|
|
162
180
|
for (const message of input.messages) await this.send(message);
|
|
163
181
|
}
|
|
164
182
|
};
|
|
165
|
-
EmailService = __decorate([
|
|
166
|
-
Transient(EMAIL_TOKENS.EmailService),
|
|
167
|
-
__decorateParam(0, inject(EMAIL_TOKENS.EmailQueue)),
|
|
168
|
-
__decorateMetadata("design:paramtypes", [Object])
|
|
169
|
-
], EmailService);
|
|
183
|
+
EmailService = __decorate([Transient(EMAIL_TOKENS.EmailService), __decorateParam(0, inject(EMAIL_TOKENS.EmailQueue))], EmailService);
|
|
170
184
|
//#endregion
|
|
171
|
-
//#region src/email/
|
|
172
|
-
/**
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
185
|
+
//#region src/email/smtp/smtp-client.ts
|
|
186
|
+
/** Abort a server read if it stalls, so a hung endpoint can't wedge the Worker. */
|
|
187
|
+
const RESPONSE_TIMEOUT_MS = 3e4;
|
|
188
|
+
function parseSmtpUrl(config) {
|
|
189
|
+
try {
|
|
190
|
+
const parsed = new URL(config.url);
|
|
191
|
+
const secure = parsed.protocol === "smtps:";
|
|
192
|
+
return {
|
|
193
|
+
host: parsed.hostname,
|
|
194
|
+
port: parsed.port ? parseInt(parsed.port, 10) : secure ? 465 : 587,
|
|
195
|
+
secure,
|
|
196
|
+
username: parsed.username ? decodeURIComponent(parsed.username) : void 0,
|
|
197
|
+
password: parsed.password ? decodeURIComponent(parsed.password) : void 0
|
|
198
|
+
};
|
|
199
|
+
} catch {
|
|
200
|
+
throw new EmailError(`Invalid SMTP URL: ${config.url}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** Parse an EHLO reply into the capabilities/mechanisms it advertises. */
|
|
204
|
+
function parseCapabilities(ehloText) {
|
|
205
|
+
const auth = /* @__PURE__ */ new Set();
|
|
206
|
+
let startTls = false;
|
|
207
|
+
for (const rawLine of ehloText.split("\r\n")) {
|
|
208
|
+
const line = rawLine.slice(4).trim().toUpperCase();
|
|
209
|
+
if (!line) continue;
|
|
210
|
+
const [keyword, ...rest] = line.split(/\s+/);
|
|
211
|
+
if (keyword === "STARTTLS") startTls = true;
|
|
212
|
+
if (keyword === "AUTH") rest.forEach((m) => auth.add(m));
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
startTls,
|
|
216
|
+
auth
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
var SmtpClient = class {
|
|
220
|
+
parsed;
|
|
221
|
+
constructor(config) {
|
|
222
|
+
this.parsed = parseSmtpUrl(config);
|
|
223
|
+
}
|
|
224
|
+
async send(mimeRaw, options) {
|
|
225
|
+
const { connect } = await import("cloudflare:sockets");
|
|
226
|
+
const { host, port, secure, username, password } = this.parsed;
|
|
227
|
+
const implicitTls = secure || port === 465;
|
|
228
|
+
const socket = connect({
|
|
229
|
+
hostname: host,
|
|
230
|
+
port
|
|
231
|
+
}, {
|
|
232
|
+
secureTransport: implicitTls ? "on" : "starttls",
|
|
233
|
+
allowHalfOpen: false
|
|
234
|
+
});
|
|
235
|
+
let activeSocket = socket;
|
|
236
|
+
let reader = socket.readable.getReader();
|
|
237
|
+
let writer = socket.writable.getWriter();
|
|
238
|
+
const decoder = new TextDecoder();
|
|
239
|
+
const encoder = new TextEncoder();
|
|
240
|
+
const readChunk = async () => {
|
|
241
|
+
const read = reader.read();
|
|
242
|
+
read.catch(() => {});
|
|
243
|
+
let timer;
|
|
244
|
+
const timeout = new Promise((_, reject) => {
|
|
245
|
+
timer = setTimeout(() => reject(new EmailError(`SMTP timeout: no response within ${RESPONSE_TIMEOUT_MS}ms`)), RESPONSE_TIMEOUT_MS);
|
|
246
|
+
});
|
|
247
|
+
try {
|
|
248
|
+
return await Promise.race([read, timeout]);
|
|
249
|
+
} finally {
|
|
250
|
+
clearTimeout(timer);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
let leftover = "";
|
|
254
|
+
const readResponse = async () => {
|
|
255
|
+
let buffer = leftover;
|
|
256
|
+
leftover = "";
|
|
257
|
+
while (true) {
|
|
258
|
+
if (buffer.indexOf("\r\n") !== -1) {
|
|
259
|
+
const lines = buffer.split("\r\n");
|
|
260
|
+
let consumed = 0;
|
|
261
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
262
|
+
const line = lines[i];
|
|
263
|
+
consumed += line.length + 2;
|
|
264
|
+
if (line[3] === " ") {
|
|
265
|
+
const code = parseInt(line.slice(0, 3), 10);
|
|
266
|
+
leftover = buffer.slice(consumed);
|
|
267
|
+
return {
|
|
268
|
+
code,
|
|
269
|
+
text: buffer.slice(0, consumed)
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const result = await readChunk();
|
|
275
|
+
if (result.done) throw new EmailError("SMTP connection closed unexpectedly");
|
|
276
|
+
buffer += decoder.decode(result.value, { stream: true });
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
const sendCommand = async (command) => {
|
|
280
|
+
await writer.write(encoder.encode(command + "\r\n"));
|
|
281
|
+
return readResponse();
|
|
282
|
+
};
|
|
283
|
+
const expectCode = async (command, expected) => {
|
|
284
|
+
const response = await sendCommand(command);
|
|
285
|
+
if (response.code !== expected) throw new EmailError(`SMTP error: expected ${expected}, got ${response.code}: ${response.text.trim()}`);
|
|
286
|
+
return response.text;
|
|
287
|
+
};
|
|
288
|
+
try {
|
|
289
|
+
const greeting = await readResponse();
|
|
290
|
+
if (greeting.code !== 220) throw new EmailError(`SMTP server rejected connection: ${greeting.text.trim()}`);
|
|
291
|
+
let capabilities = parseCapabilities(await expectCode(`EHLO ${host}`, 250));
|
|
292
|
+
let encrypted = implicitTls;
|
|
293
|
+
if (!implicitTls && capabilities.startTls) {
|
|
294
|
+
await expectCode("STARTTLS", 220);
|
|
295
|
+
reader.releaseLock();
|
|
296
|
+
writer.releaseLock();
|
|
297
|
+
activeSocket = socket.startTls();
|
|
298
|
+
reader = activeSocket.readable.getReader();
|
|
299
|
+
writer = activeSocket.writable.getWriter();
|
|
300
|
+
leftover = "";
|
|
301
|
+
capabilities = parseCapabilities(await expectCode(`EHLO ${host}`, 250));
|
|
302
|
+
encrypted = true;
|
|
303
|
+
}
|
|
304
|
+
if (username && password) {
|
|
305
|
+
if (!encrypted) throw new EmailError("Refusing to send SMTP credentials over an unencrypted connection: the server did not offer STARTTLS. Use an smtps:// URL or a server that supports STARTTLS.");
|
|
306
|
+
await this.authenticate(expectCode, capabilities, username, password);
|
|
307
|
+
}
|
|
308
|
+
await expectCode(`MAIL FROM:<${options.from}>`, 250);
|
|
309
|
+
for (const recipient of options.to) await expectCode(`RCPT TO:<${recipient}>`, 250);
|
|
310
|
+
await expectCode("DATA", 354);
|
|
311
|
+
const dotStuffed = (mimeRaw.startsWith(".") ? "." + mimeRaw : mimeRaw).replace(/\r\n\./g, "\r\n..");
|
|
312
|
+
await writer.write(encoder.encode(dotStuffed + "\r\n.\r\n"));
|
|
313
|
+
const dataResponse = await readResponse();
|
|
314
|
+
if (dataResponse.code !== 250) throw new EmailError(`SMTP DATA rejected: ${dataResponse.text.trim()}`);
|
|
315
|
+
const messageId = dataResponse.text.match(/<([^>]+)>/)?.[1] ?? `${Date.now()}@${host}`;
|
|
316
|
+
await sendCommand("QUIT").catch(() => {});
|
|
317
|
+
return { messageId };
|
|
318
|
+
} finally {
|
|
319
|
+
reader.releaseLock();
|
|
320
|
+
writer.releaseLock();
|
|
321
|
+
await activeSocket.close().catch(() => {});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/** Authenticate using a server-advertised SASL mechanism (PLAIN or LOGIN). */
|
|
325
|
+
async authenticate(expectCode, capabilities, username, password) {
|
|
326
|
+
if (capabilities.auth.has("PLAIN")) {
|
|
327
|
+
await expectCode(`AUTH PLAIN ${Buffer.from(`\0${username}\0${password}`).toString("base64")}`, 235);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (capabilities.auth.has("LOGIN")) {
|
|
331
|
+
await expectCode("AUTH LOGIN", 334);
|
|
332
|
+
await expectCode(Buffer.from(username).toString("base64"), 334);
|
|
333
|
+
await expectCode(Buffer.from(password).toString("base64"), 235);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
throw new EmailError(capabilities.auth.size === 0 ? "SMTP credentials were provided but the server does not advertise AUTH." : `SMTP server does not support a known AUTH mechanism (offered: ${[...capabilities.auth].join(", ")}). Supported: PLAIN, LOGIN.`);
|
|
183
337
|
}
|
|
184
338
|
};
|
|
185
339
|
//#endregion
|
|
186
|
-
//#region src/email/
|
|
340
|
+
//#region src/email/smtp/mime.ts
|
|
187
341
|
/**
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* This prevents the SMTP email provider from initializing.
|
|
192
|
-
*
|
|
193
|
-
* Resolution: Set the SMTP_URL environment variable with format: smtp://user:pass@host:port
|
|
342
|
+
* Hard cap on a single attachment's resolved size (20 MB). Attachments are fully
|
|
343
|
+
* buffered into memory before base64 encoding, so an unbounded attachment would
|
|
344
|
+
* let a single message exhaust the Worker's memory. Exceeding this throws.
|
|
194
345
|
*/
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
346
|
+
const MAX_ATTACHMENT_SIZE_BYTES = 20 * 1024 * 1024;
|
|
347
|
+
function generateBoundary() {
|
|
348
|
+
return `----=_Part_${crypto.randomUUID().replace(/-/g, "")}`;
|
|
349
|
+
}
|
|
350
|
+
function generateMessageId(fromEmail) {
|
|
351
|
+
const domain = fromEmail.split("@")[1] || "localhost";
|
|
352
|
+
return `<${crypto.randomUUID()}@${domain}>`;
|
|
353
|
+
}
|
|
354
|
+
function isAscii(str) {
|
|
355
|
+
return /^[ -~]*$/.test(str);
|
|
356
|
+
}
|
|
357
|
+
/** Remove CR/LF so a value can't inject extra headers (header smuggling). */
|
|
358
|
+
function stripCrlf(value) {
|
|
359
|
+
return value.replace(/[\r\n]/g, "");
|
|
360
|
+
}
|
|
202
361
|
/**
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
* Resolution: Ensure SMTP_URL is correctly formatted: smtp://user:pass@host:port
|
|
362
|
+
* Encode a header value as one or more RFC 2047 base64 encoded-words, folding so
|
|
363
|
+
* no produced line exceeds the 76-char limit strict MTAs enforce. The UTF-8 byte
|
|
364
|
+
* stream is chunked at <=45 source bytes per encoded-word (<=60 base64 chars,
|
|
365
|
+
* keeping `=?UTF-8?B?...?=` <=75 chars) WITHOUT splitting a multibyte sequence.
|
|
366
|
+
* Multiple encoded-words are folded with CRLF + a single space.
|
|
209
367
|
*/
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
368
|
+
function encodeEncodedWords(clean) {
|
|
369
|
+
const bytes = Buffer.from(clean, "utf-8");
|
|
370
|
+
const MAX_SOURCE_BYTES = 45;
|
|
371
|
+
const words = [];
|
|
372
|
+
let i = 0;
|
|
373
|
+
while (i < bytes.length) {
|
|
374
|
+
let end = Math.min(i + MAX_SOURCE_BYTES, bytes.length);
|
|
375
|
+
while (end > i && end < bytes.length && (bytes[end] & 192) === 128) end--;
|
|
376
|
+
const slice = bytes.subarray(i, end);
|
|
377
|
+
words.push(`=?UTF-8?B?${slice.toString("base64")}?=`);
|
|
378
|
+
i = end;
|
|
213
379
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
380
|
+
return words.join("\r\n ");
|
|
381
|
+
}
|
|
382
|
+
function encodeHeaderValue(value) {
|
|
383
|
+
const clean = stripCrlf(value);
|
|
384
|
+
if (isAscii(clean)) return clean;
|
|
385
|
+
return encodeEncodedWords(clean);
|
|
386
|
+
}
|
|
217
387
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
* This is a runtime error that may be temporary.
|
|
222
|
-
*
|
|
223
|
-
* Resolution: Check SMTP server availability, network connectivity, or wait and retry.
|
|
388
|
+
* Validate an envelope address (used verbatim in `MAIL FROM`/`RCPT TO` SMTP
|
|
389
|
+
* commands). Raw CR/LF would allow SMTP command injection, so we throw rather
|
|
390
|
+
* than silently strip — a corrupted envelope must never reach the wire.
|
|
224
391
|
*/
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
392
|
+
function assertNoCrlf(address) {
|
|
393
|
+
if (/[\r\n]/.test(address)) throw new EmailError("Email envelope address contains CR/LF, which would allow SMTP command injection");
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Escape a value for an RFC 5322 quoted-string: backslash MUST be escaped first
|
|
397
|
+
* (so the escapes added for `"` aren't themselves re-escaped), then `"`.
|
|
398
|
+
*/
|
|
399
|
+
function escapeQuotedString(value) {
|
|
400
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Encode a MIME parameter (e.g. `name`/`filename`) safely. ASCII values are
|
|
404
|
+
* emitted as a quoted-string with `"`/`\` escaped; non-ASCII values use the
|
|
405
|
+
* RFC 2231 extended syntax (`param*=UTF-8''…`). CR/LF are always stripped so a
|
|
406
|
+
* crafted filename can't inject headers or break the MIME structure.
|
|
407
|
+
*/
|
|
408
|
+
function encodeMimeParam(param, value) {
|
|
409
|
+
const clean = stripCrlf(value);
|
|
410
|
+
if (isAscii(clean)) return `${param}="${escapeQuotedString(clean)}"`;
|
|
411
|
+
return `${param}*=UTF-8''${Array.from(Buffer.from(clean, "utf-8")).map((b) => /[A-Za-z0-9!#$&+\-.^_`|~]/.test(String.fromCharCode(b)) ? String.fromCharCode(b) : `%${b.toString(16).toUpperCase().padStart(2, "0")}`).join("")}`;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Extract the bare address for the SMTP envelope and validate it has no raw
|
|
415
|
+
* CR/LF before it is written into a `MAIL FROM`/`RCPT TO` command.
|
|
416
|
+
*/
|
|
417
|
+
function extractEmail(address) {
|
|
418
|
+
assertNoCrlf(address);
|
|
419
|
+
const match = address.match(/<([^>]+)>/);
|
|
420
|
+
const email = (match ? match[1] : address).trim();
|
|
421
|
+
if (/[<>\s]/.test(email)) throw new EmailError(`Invalid email address for SMTP envelope: ${JSON.stringify(address)}`);
|
|
422
|
+
return email;
|
|
423
|
+
}
|
|
424
|
+
function formatAddress(email, name) {
|
|
425
|
+
const cleanEmail = stripCrlf(email);
|
|
426
|
+
if (!name) return cleanEmail;
|
|
427
|
+
const cleanName = stripCrlf(name);
|
|
428
|
+
return `${isAscii(cleanName) ? `"${escapeQuotedString(cleanName)}"` : encodeHeaderValue(cleanName)} <${cleanEmail}>`;
|
|
429
|
+
}
|
|
430
|
+
function formatDate(date) {
|
|
431
|
+
return date.toUTCString().replace("GMT", "+0000");
|
|
432
|
+
}
|
|
433
|
+
function wrapBase64(base64) {
|
|
434
|
+
const lines = [];
|
|
435
|
+
for (let i = 0; i < base64.length; i += 76) lines.push(base64.slice(i, i + 76));
|
|
436
|
+
return lines.join("\r\n");
|
|
437
|
+
}
|
|
438
|
+
async function resolveContent(content) {
|
|
439
|
+
const buffer = Buffer.isBuffer(content) ? content : Buffer.from(await new Response(content).arrayBuffer());
|
|
440
|
+
if (buffer.byteLength > 20971520) throw new EmailError(`Email attachment exceeds the maximum size of ${MAX_ATTACHMENT_SIZE_BYTES} bytes (got ${buffer.byteLength} bytes)`);
|
|
441
|
+
return buffer;
|
|
442
|
+
}
|
|
443
|
+
/** Base64-encode a text body so arbitrary content (long lines, leading dots,
|
|
444
|
+
* 8-bit data) is transported safely regardless of line length or SMTP escaping. */
|
|
445
|
+
function encodeTextBody(content) {
|
|
446
|
+
return wrapBase64(Buffer.from(content, "utf-8").toString("base64"));
|
|
447
|
+
}
|
|
448
|
+
function buildBodyPart(text, html) {
|
|
449
|
+
if (text && html) {
|
|
450
|
+
const boundary = generateBoundary();
|
|
451
|
+
return {
|
|
452
|
+
content: [
|
|
453
|
+
`--${boundary}`,
|
|
454
|
+
"Content-Type: text/plain; charset=utf-8",
|
|
455
|
+
"Content-Transfer-Encoding: base64",
|
|
456
|
+
"",
|
|
457
|
+
encodeTextBody(text),
|
|
458
|
+
`--${boundary}`,
|
|
459
|
+
"Content-Type: text/html; charset=utf-8",
|
|
460
|
+
"Content-Transfer-Encoding: base64",
|
|
461
|
+
"",
|
|
462
|
+
encodeTextBody(html),
|
|
463
|
+
`--${boundary}--`
|
|
464
|
+
].join("\r\n"),
|
|
465
|
+
contentType: `multipart/alternative; boundary="${boundary}"`
|
|
466
|
+
};
|
|
231
467
|
}
|
|
232
|
-
|
|
468
|
+
if (html) return {
|
|
469
|
+
content: encodeTextBody(html),
|
|
470
|
+
contentType: "text/html; charset=utf-8",
|
|
471
|
+
cte: "base64"
|
|
472
|
+
};
|
|
473
|
+
return {
|
|
474
|
+
content: encodeTextBody(text ?? ""),
|
|
475
|
+
contentType: "text/plain; charset=utf-8",
|
|
476
|
+
cte: "base64"
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
async function buildAttachmentPart(attachment) {
|
|
480
|
+
const base64 = wrapBase64((await resolveContent(attachment.content)).toString("base64"));
|
|
481
|
+
return [
|
|
482
|
+
`Content-Type: ${attachment.contentType || "application/octet-stream"}; ${encodeMimeParam("name", attachment.filename)}`,
|
|
483
|
+
"Content-Transfer-Encoding: base64",
|
|
484
|
+
`Content-Disposition: attachment; ${encodeMimeParam("filename", attachment.filename)}`,
|
|
485
|
+
"",
|
|
486
|
+
base64
|
|
487
|
+
].join("\r\n");
|
|
488
|
+
}
|
|
489
|
+
async function buildMimeMessage(message, defaultFrom) {
|
|
490
|
+
const fromAddr = message.from ? formatAddress(message.from.email, message.from.name) : formatAddress(defaultFrom.email, defaultFrom.name);
|
|
491
|
+
const fromEmail = extractEmail(message.from?.email ?? defaultFrom.email);
|
|
492
|
+
const toList = Array.isArray(message.to) ? message.to : [message.to];
|
|
493
|
+
const headers = [
|
|
494
|
+
`From: ${fromAddr}`,
|
|
495
|
+
`To: ${toList.map(stripCrlf).join(", ")}`,
|
|
496
|
+
`Subject: ${encodeHeaderValue(message.subject)}`,
|
|
497
|
+
`Date: ${formatDate(/* @__PURE__ */ new Date())}`,
|
|
498
|
+
`Message-ID: ${generateMessageId(fromEmail)}`,
|
|
499
|
+
"MIME-Version: 1.0"
|
|
500
|
+
];
|
|
501
|
+
if (message.replyTo) headers.push(`Reply-To: ${stripCrlf(message.replyTo)}`);
|
|
502
|
+
if (message.cc?.length) headers.push(`Cc: ${message.cc.map(stripCrlf).join(", ")}`);
|
|
503
|
+
const allRecipients = [
|
|
504
|
+
...toList,
|
|
505
|
+
...message.cc ?? [],
|
|
506
|
+
...message.bcc ?? []
|
|
507
|
+
].map(extractEmail);
|
|
508
|
+
const body = buildBodyPart(message.text, message.html);
|
|
509
|
+
if (!(message.attachments && message.attachments.length > 0)) {
|
|
510
|
+
headers.push(`Content-Type: ${body.contentType}`);
|
|
511
|
+
if (body.cte) headers.push(`Content-Transfer-Encoding: ${body.cte}`);
|
|
512
|
+
return {
|
|
513
|
+
raw: headers.join("\r\n") + "\r\n\r\n" + body.content,
|
|
514
|
+
envelope: {
|
|
515
|
+
from: fromEmail,
|
|
516
|
+
to: allRecipients
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
const mixedBoundary = generateBoundary();
|
|
521
|
+
headers.push(`Content-Type: multipart/mixed; boundary="${mixedBoundary}"`);
|
|
522
|
+
const parts = [`--${mixedBoundary}`, `Content-Type: ${body.contentType}`];
|
|
523
|
+
if (body.cte) parts.push(`Content-Transfer-Encoding: ${body.cte}`);
|
|
524
|
+
parts.push("", body.content);
|
|
525
|
+
for (const attachment of message.attachments) {
|
|
526
|
+
parts.push(`--${mixedBoundary}`);
|
|
527
|
+
parts.push(await buildAttachmentPart(attachment));
|
|
528
|
+
}
|
|
529
|
+
parts.push(`--${mixedBoundary}--`);
|
|
530
|
+
return {
|
|
531
|
+
raw: headers.join("\r\n") + "\r\n\r\n" + parts.join("\r\n"),
|
|
532
|
+
envelope: {
|
|
533
|
+
from: fromEmail,
|
|
534
|
+
to: allRecipients
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
}
|
|
233
538
|
//#endregion
|
|
234
|
-
//#region src/email/
|
|
539
|
+
//#region src/email/providers/base-email.provider.ts
|
|
235
540
|
/**
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
* Thrown when Resend API returns an error during email sending.
|
|
239
|
-
* This is a runtime error from the Resend service.
|
|
541
|
+
* Base Email Provider
|
|
240
542
|
*
|
|
241
|
-
*
|
|
543
|
+
* Abstract base class for email providers.
|
|
544
|
+
* Provides shared implementation of sendBatch() to reduce code duplication.
|
|
242
545
|
*/
|
|
243
|
-
var
|
|
244
|
-
|
|
245
|
-
|
|
546
|
+
var BaseEmailProvider = class {
|
|
547
|
+
/**
|
|
548
|
+
* Send multiple emails in a batch
|
|
549
|
+
*
|
|
550
|
+
* Default implementation sends emails sequentially.
|
|
551
|
+
* Concrete providers can override for optimized batch sending.
|
|
552
|
+
*/
|
|
553
|
+
async sendBatch(messages) {
|
|
554
|
+
const results = [];
|
|
555
|
+
let successful = 0;
|
|
556
|
+
let failed = 0;
|
|
557
|
+
for (const message of messages) try {
|
|
558
|
+
const result = await this.send(message);
|
|
559
|
+
results.push(result);
|
|
560
|
+
successful++;
|
|
561
|
+
} catch (error) {
|
|
562
|
+
results.push({
|
|
563
|
+
messageId: "",
|
|
564
|
+
accepted: false,
|
|
565
|
+
metadata: { error: error instanceof Error ? error.message : "Unknown error" }
|
|
566
|
+
});
|
|
567
|
+
failed++;
|
|
568
|
+
}
|
|
569
|
+
return {
|
|
570
|
+
total: messages.length,
|
|
571
|
+
successful,
|
|
572
|
+
failed,
|
|
573
|
+
results
|
|
574
|
+
};
|
|
246
575
|
}
|
|
247
576
|
};
|
|
248
577
|
//#endregion
|
|
249
|
-
//#region src/email/
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
578
|
+
//#region src/email/providers/smtp.provider.ts
|
|
579
|
+
var SmtpProvider = class extends BaseEmailProvider {
|
|
580
|
+
options;
|
|
581
|
+
constructor(options) {
|
|
582
|
+
super();
|
|
583
|
+
this.options = options;
|
|
584
|
+
if (!this.options.smtp?.url) throw new EmailError("SMTP URL is required");
|
|
585
|
+
}
|
|
586
|
+
async send(message) {
|
|
587
|
+
const mime = await buildMimeMessage(message, this.options.from);
|
|
588
|
+
try {
|
|
589
|
+
return {
|
|
590
|
+
messageId: (await new SmtpClient(this.options.smtp).send(mime.raw, {
|
|
591
|
+
from: mime.envelope.from,
|
|
592
|
+
to: mime.envelope.to
|
|
593
|
+
})).messageId,
|
|
594
|
+
accepted: true,
|
|
595
|
+
metadata: { provider: "smtp" }
|
|
596
|
+
};
|
|
597
|
+
} catch (error) {
|
|
598
|
+
if (error instanceof EmailError) throw error;
|
|
599
|
+
throw new EmailError(`SMTP send failed: ${error.message}`);
|
|
600
|
+
}
|
|
261
601
|
}
|
|
262
602
|
};
|
|
263
603
|
//#endregion
|
|
@@ -267,57 +607,35 @@ let EmailProviderFactory = class EmailProviderFactory {
|
|
|
267
607
|
constructor(options) {
|
|
268
608
|
this.options = options;
|
|
269
609
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
*
|
|
273
|
-
* @returns Email provider implementation
|
|
274
|
-
* @throws EmailProviderNotSupportedError if provider is not supported
|
|
275
|
-
*/
|
|
276
|
-
async create() {
|
|
277
|
-
switch (this.options.provider) {
|
|
278
|
-
case "resend": {
|
|
279
|
-
const { ResendProvider } = await import("../resend.provider-DB4IlFjG.mjs");
|
|
280
|
-
return new ResendProvider(this.options);
|
|
281
|
-
}
|
|
282
|
-
case "smtp": {
|
|
283
|
-
const { SmtpProvider } = await import("../smtp.provider-B6D7zuWX.mjs");
|
|
284
|
-
return new SmtpProvider(this.options);
|
|
285
|
-
}
|
|
286
|
-
default: throw new EmailProviderNotSupportedError(this.options.provider);
|
|
287
|
-
}
|
|
610
|
+
create() {
|
|
611
|
+
return new SmtpProvider(this.options);
|
|
288
612
|
}
|
|
289
613
|
};
|
|
290
|
-
EmailProviderFactory = __decorate([
|
|
291
|
-
Transient(EMAIL_TOKENS.EmailProviderFactory),
|
|
292
|
-
__decorateParam(0, inject(EMAIL_TOKENS.Options)),
|
|
293
|
-
__decorateMetadata("design:paramtypes", [Object])
|
|
294
|
-
], EmailProviderFactory);
|
|
614
|
+
EmailProviderFactory = __decorate([Transient(EMAIL_TOKENS.EmailProviderFactory), __decorateParam(0, inject(EMAIL_TOKENS.Options))], EmailProviderFactory);
|
|
295
615
|
//#endregion
|
|
296
616
|
//#region src/email/email.module.ts
|
|
297
617
|
/**
|
|
298
618
|
* Email Module
|
|
299
619
|
*
|
|
300
620
|
* Provides email sending capabilities with provider abstraction.
|
|
301
|
-
*
|
|
621
|
+
* Sends email via SMTP. Emails are dispatched asynchronously via Cloudflare Queues.
|
|
302
622
|
* Emails are sent asynchronously via Cloudflare Queues.
|
|
303
623
|
*
|
|
304
624
|
* **Usage:**
|
|
305
625
|
* ```typescript
|
|
306
626
|
* // In AppModule imports with static options
|
|
307
627
|
* EmailModule.forRoot({
|
|
308
|
-
* provider: 'resend',
|
|
309
|
-
* apiKey: 'your-api-key',
|
|
310
628
|
* from: { name: 'App', email: 'noreply@example.com' },
|
|
311
|
-
*
|
|
629
|
+
* smtp: { url: 'smtp://user:pass@smtp.example.com:587' },
|
|
630
|
+
* queue: 'NOTIFICATIONS_QUEUE',
|
|
312
631
|
* })
|
|
313
632
|
*
|
|
314
633
|
* // Or with async options from config namespaces
|
|
315
634
|
* EmailModule.forRootAsync({
|
|
316
635
|
* inject: [emailConfig.KEY],
|
|
317
636
|
* useFactory: (email) => ({
|
|
318
|
-
* provider: email.provider,
|
|
319
|
-
* apiKey: email.apiKey,
|
|
320
637
|
* from: email.from,
|
|
638
|
+
* smtp: email.smtp,
|
|
321
639
|
* queue: email.queue,
|
|
322
640
|
* }),
|
|
323
641
|
* })
|
|
@@ -344,10 +662,9 @@ let EmailModule = _EmailModule = class EmailModule {
|
|
|
344
662
|
* @example
|
|
345
663
|
* ```typescript
|
|
346
664
|
* EmailModule.forRoot({
|
|
347
|
-
* provider: 'resend',
|
|
348
|
-
* apiKey: env.RESEND_API_KEY,
|
|
349
665
|
* from: { name: 'App', email: 'noreply@example.com' },
|
|
350
|
-
*
|
|
666
|
+
* smtp: { url: 'smtp://user:pass@smtp.example.com:587' },
|
|
667
|
+
* queue: 'NOTIFICATIONS_QUEUE',
|
|
351
668
|
* })
|
|
352
669
|
* ```
|
|
353
670
|
*/
|
|
@@ -376,8 +693,6 @@ let EmailModule = _EmailModule = class EmailModule {
|
|
|
376
693
|
* EmailModule.forRootAsync({
|
|
377
694
|
* inject: [emailConfig.KEY],
|
|
378
695
|
* useFactory: (email) => ({
|
|
379
|
-
* provider: email.provider,
|
|
380
|
-
* apiKey: email.apiKey,
|
|
381
696
|
* from: email.from,
|
|
382
697
|
* smtp: email.smtp,
|
|
383
698
|
* queue: email.queue,
|
|
@@ -523,7 +838,7 @@ const emailMessageSchema = z.object({
|
|
|
523
838
|
* Email attachments
|
|
524
839
|
*/
|
|
525
840
|
attachments: z.array(emailAttachmentSchema).optional()
|
|
526
|
-
}).refine((data) => data.html ?? data.text,
|
|
841
|
+
}).refine((data) => data.html ?? data.text, withZodI18n("zodI18n.errors.custom.emailOrTextRequired"));
|
|
527
842
|
//#endregion
|
|
528
843
|
//#region src/email/contracts/send-email.input.ts
|
|
529
844
|
/**
|
|
@@ -550,6 +865,6 @@ const sendBatchEmailInputSchema = z.object({
|
|
|
550
865
|
*/
|
|
551
866
|
messages: z.array(sendEmailInputSchema).min(1).max(100) });
|
|
552
867
|
//#endregion
|
|
553
|
-
export {
|
|
868
|
+
export { BaseEmailProvider, EMAIL_TOKENS, EmailError, EmailModule, EmailProviderFactory, EmailService, emailAddressSchema, emailAttachmentSchema, emailMessageSchema, inlineEmailAttachmentSchema, sendBatchEmailInputSchema, sendEmailInputSchema, storageEmailAttachmentSchema };
|
|
554
869
|
|
|
555
870
|
//# sourceMappingURL=index.mjs.map
|