void 0.10.12 → 0.20.0
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/AGENTS_TEMPLATE.md +6 -0
- package/{skills/void/docs/node_modules/void/node_modules/pglite-server/LICENSE.md → LICENSE} +1 -1
- package/README.md +34 -8
- package/dist/{auth-qgMlYp7Z.d.mts → auth-DkcFflXV.d.mts} +10 -11
- package/dist/auth-W9WII-mN.mjs +630 -0
- package/dist/{auth-cmd-BqsdZJp5.mjs → auth-cmd-CYVhSNFy.mjs} +4 -3
- package/dist/{auth-migrations-BTZ-ATvQ.mjs → auth-migrations-9vif1uj8.mjs} +53 -12
- package/dist/{better-auth-shared-BQooDxbw.mjs → better-auth-shared-CYw1T3k4.mjs} +7 -10
- package/dist/{better-auth-shared-DealXecJ.d.mts → better-auth-shared-DSCeohOK.d.mts} +10 -5
- package/dist/{build-cmd-Bujrv5q-.mjs → build-cmd-CkVD1uOh.mjs} +6 -4
- package/dist/{cache-C11V8Fxq.mjs → cache-QcUfR-ff.mjs} +9 -5
- package/dist/{cancel-deploy-fwFYF04b.mjs → cancel-deploy-DsvWKFTe.mjs} +3 -2
- package/dist/cf-access-AJ1ehiFR.mjs +42 -0
- package/dist/cf-access-DsSsZUPr.mjs +67 -0
- package/dist/cli/cli.d.mts +1 -1
- package/dist/cli/cli.mjs +1592 -153
- package/dist/cli/env-schema-probe.d.mts +48 -61
- package/dist/cli/env-schema-probe.mjs +10 -58
- package/dist/{client-Gb71-XkG.mjs → client-CyCHWSO_.mjs} +61 -463
- package/dist/cloudflare-auth-DRkGe7-s.mjs +170 -0
- package/dist/cloudflare-cmd-D3ME2GAe.mjs +62 -0
- package/dist/cloudflare-connect-BivMefMA.mjs +56 -0
- package/dist/cloudflare-operations-CPTpRW6d.mjs +566 -0
- package/dist/{config-BkTvs43g.mjs → config-BQFq7QvD.mjs} +85 -41
- package/dist/{config-CutEMNGJ.mjs → config-NOG_U1aK.mjs} +10 -15
- package/dist/connect-D9Yr-T5f.mjs +79 -0
- package/dist/{create-project-DsYvl3TB.mjs → create-project-DMV-csEm.mjs} +26 -16
- package/dist/database-provider.d.mts +21 -0
- package/dist/database-provider.mjs +6 -0
- package/dist/{db-DOiJMRt2.mjs → db-BxoUWL0F.mjs} +600 -115
- package/dist/{delete-mh6p-zkQ.mjs → delete-iJOqxBz1.mjs} +9 -6
- package/dist/{deploy-u7Rv9q_q.mjs → deploy-CcDoeBIf.mjs} +3451 -1986
- package/dist/dev-inbox-DkgRWLkW.mjs +307 -0
- package/dist/{discover-CJHyvYfR.mjs → discover-xvfrgJeo.mjs} +3 -3
- package/dist/{magic-string.es-ZQjdJFFn.mjs → dist-BR1quN_w.mjs} +570 -216
- package/dist/{dist-DaKKDf8D.mjs → dist-BrsS7cai.mjs} +17 -2
- package/dist/{dist-BuiRJkTd.mjs → dist-m40_XgNh.mjs} +48 -33
- package/dist/{domain-DiaNQbrl.mjs → domain-gu_iaHmn.mjs} +41 -4
- package/dist/dotenv-VQxupEUv.mjs +181 -0
- package/dist/edge.d.mts +2 -0
- package/dist/edge.mjs +2 -0
- package/dist/email-Ce6SQq-i.mjs +182 -0
- package/dist/email-r6DHJyAB.mjs +262 -0
- package/dist/{entry-D7yy4xVH.mjs → entry-DU3oDoQ3.mjs} +2 -2
- package/dist/env-D4Emu-M_.mjs +95 -0
- package/dist/env-DmU2To0C.mjs +73 -0
- package/dist/env-helpers-CyKtOBpj.d.mts +22 -0
- package/dist/env-public-D_6u46fX.d.mts +135 -0
- package/dist/{env-types-QBj-ndax.mjs → env-types-BNPhro-M.mjs} +2 -2
- package/dist/{env-validation-Dea3v3ej.mjs → env-validation-ENpMy6Ez.mjs} +68 -122
- package/dist/{gen-o_w-8yI8.mjs → gen-BKw6qHIg.mjs} +70 -15
- package/dist/{github-cmd-DKcGUNsj.mjs → github-cmd-DJS5Ab-H.mjs} +39 -31
- package/dist/{handler-Cjh8uM3Y.d.mts → handler-D1hLsObx.d.mts} +124 -122
- package/dist/{head-nmvOgFjd.d.mts → head-Do8P4puT.d.mts} +9 -8
- package/dist/{headers-nsHIFixA.mjs → headers-D8QfRX9Y.mjs} +12 -10
- package/dist/inbound-BJ70in1n.d.mts +354 -0
- package/dist/inbound-CNKxb3FY.mjs +694 -0
- package/dist/index.d.mts +61 -34
- package/dist/index.mjs +1048 -321
- package/dist/{init-3rgHKBVi.mjs → init-Dx5cgKqK.mjs} +329 -271
- package/dist/link-D_rm5sRH.mjs +52 -0
- package/dist/{list-CPwFDZ_c.mjs → list-M8XShti7.mjs} +37 -5
- package/dist/{runner-kapo9aPs.mjs → local-d1-BE8KBbMy.mjs} +116 -6
- package/dist/{login-BT3H8PN3.mjs → login-C8-UuLnp.mjs} +17 -15
- package/dist/{logs-Bt313ax7.mjs → logs-DwFU7dMW.mjs} +16 -2
- package/dist/mime-BJD7d_qL.mjs +1216 -0
- package/dist/neon-DHwd2zvC.mjs +54 -0
- package/dist/{node-Da0UcsGA.mjs → node-BkaXcpAc.mjs} +29 -8
- package/dist/operator-cmd-DYWRbWUA.mjs +348 -0
- package/dist/{agents-CtgBYqld.mjs → output-tFQLLj26.mjs} +676 -211
- package/dist/{package-json-Cx1osYo6.mjs → package-json-CPoWX79C.mjs} +1 -1
- package/dist/pages/client.d.mts +43 -41
- package/dist/pages/client.mjs +40 -33
- package/dist/pages/head-client.d.mts +10 -12
- package/dist/pages/head.d.mts +1 -1
- package/dist/pages/index.d.mts +37 -14
- package/dist/pages/index.mjs +5 -5
- package/dist/pages/islands-plugin.d.mts +25 -27
- package/dist/pages/islands-plugin.mjs +6 -4
- package/dist/pages/prefetch.d.mts +6 -7
- package/dist/pages/protocol.d.mts +2 -2
- package/dist/pages/protocol.mjs +2 -1
- package/dist/pages/serialize.d.mts +7 -8
- package/dist/platform-cmd-BEOVv1PN.mjs +123 -0
- package/dist/platform-domain-BszUfiS8.mjs +228 -0
- package/dist/platform-lifecycle-CjSr6xf0.mjs +4450 -0
- package/dist/platform-management-W30iCaTL.mjs +698 -0
- package/dist/platform-recovery-pHHv4ZSg.mjs +99 -0
- package/dist/{plugin-inference-DMeavIJ6.mjs → plugin-inference-BDRfZngg.mjs} +35 -19
- package/dist/prepare-BfJvFUtJ.mjs +14 -0
- package/dist/{prepare-BoKHgMNx.mjs → prepare-CBetXvsN.mjs} +15 -24
- package/dist/{preset-BjyR3lzz.mjs → preset-lAy0B0BQ.mjs} +25 -212
- package/dist/{project-cmd-D_w-4w5B.mjs → project-cmd-DCpk1cDt.mjs} +25 -12
- package/dist/{project-paths-BQd7OmIo.mjs → project-paths-SK8nMHPp.mjs} +3 -1
- package/dist/{project-tsconfig-B-QtXjLQ.mjs → project-tsconfig-Ql2XsSQp.mjs} +2 -2
- package/dist/{protocol-Bnb0LFp3.d.mts → protocol-C-pqYJjE.d.mts} +3 -4
- package/dist/{provision-rShh6MKY.mjs → provision-Blnstcm2.mjs} +78 -45
- package/dist/r2-conditions-D1Wk8i7b.mjs +30 -0
- package/dist/{requests-B8sZxaFM.mjs → requests-Dn8Vheh1.mjs} +5 -3
- package/dist/{resolve-project-BBMtLLV9.mjs → resolve-project--Vxawf7z.mjs} +2 -2
- package/dist/rollback-CtlPXEBi.mjs +166 -0
- package/dist/{rolldown-runtime-DJK8HYOj.mjs → rolldown-runtime-rQ84J-ij.mjs} +1 -1
- package/dist/{route-types-COI2DsZv.mjs → route-types-Da-DpyUp.mjs} +85 -26
- package/dist/routes-stub.d.mts +22 -23
- package/dist/runner-mysql-CgRFl3s6.mjs +61 -0
- package/dist/{runner-pg-CHM76xuC.mjs → runner-pg-DCkWPsWS.mjs} +18 -6
- package/dist/runtime/ai.d.mts +21 -14
- package/dist/runtime/ai.mjs +5 -4
- package/dist/runtime/auth-client-react.d.mts +3 -5
- package/dist/runtime/auth-client-solid.d.mts +3 -5
- package/dist/runtime/auth-client-svelte.d.mts +3 -5
- package/dist/runtime/auth-client-vue.d.mts +3 -5
- package/dist/runtime/auth-client.d.mts +3 -5
- package/dist/runtime/auth.d.mts +1 -1
- package/dist/runtime/better-auth-mysql.d.mts +10 -0
- package/dist/runtime/better-auth-mysql.mjs +49 -0
- package/dist/runtime/better-auth-pg.d.mts +8 -9
- package/dist/runtime/better-auth-pg.mjs +2 -2
- package/dist/runtime/better-auth.d.mts +8 -9
- package/dist/runtime/better-auth.mjs +2 -2
- package/dist/runtime/client-react.d.mts +1 -1
- package/dist/runtime/client-solid.d.mts +1 -1
- package/dist/runtime/client-svelte.d.mts +1 -1
- package/dist/runtime/client-vue.d.mts +1 -1
- package/dist/runtime/client.d.mts +1 -1
- package/dist/runtime/db-mysql.d.mts +2 -0
- package/dist/runtime/db-mysql.mjs +1 -0
- package/dist/runtime/db.d.mts +10 -11
- package/dist/runtime/durable.d.mts +47 -0
- package/dist/runtime/durable.mjs +146 -0
- package/dist/runtime/email/testing.d.mts +112 -0
- package/dist/runtime/email/testing.mjs +283 -0
- package/dist/runtime/email.d.mts +30 -0
- package/dist/runtime/email.mjs +573 -0
- package/dist/runtime/env-helpers.d.mts +2 -2
- package/dist/runtime/env-helpers.mjs +5 -42
- package/dist/runtime/env-public-client.d.mts +10 -11
- package/dist/runtime/env-public-client.mjs +1 -1
- package/dist/runtime/env-public.d.mts +2 -2
- package/dist/runtime/env-public.mjs +104 -49
- package/dist/runtime/env.d.mts +19 -18
- package/dist/runtime/env.mjs +15 -2
- package/dist/runtime/fetch-stream.d.mts +20 -21
- package/dist/runtime/fetch.d.mts +15 -16
- package/dist/runtime/handler.d.mts +1 -1
- package/dist/runtime/isr.d.mts +21 -22
- package/dist/runtime/isr.mjs +26 -8
- package/dist/runtime/kv.d.mts +9 -10
- package/dist/runtime/live-client.d.mts +5 -7
- package/dist/runtime/live-client.mjs +9 -7
- package/dist/runtime/live-server.d.mts +4 -5
- package/dist/runtime/live.d.mts +22 -24
- package/dist/runtime/live.mjs +1 -1
- package/dist/runtime/log.d.mts +16 -17
- package/dist/runtime/migration-handler-mysql.d.mts +4 -0
- package/dist/runtime/migration-handler-mysql.mjs +81 -0
- package/dist/runtime/migration-handler-pg.d.mts +2 -4
- package/dist/runtime/migration-handler.d.mts +5 -6
- package/dist/runtime/migration-handler.mjs +4 -3
- package/dist/runtime/queues.d.mts +3 -4
- package/dist/runtime/queues.mjs +2 -1
- package/dist/runtime/remote/binding-handler.d.mts +10 -12
- package/dist/runtime/remote/binding-handler.mjs +24 -3
- package/dist/runtime/remote/index.d.mts +5 -6
- package/dist/runtime/remote/index.mjs +21 -18
- package/dist/runtime/response.d.mts +10 -11
- package/dist/runtime/sandbox.d.mts +56 -55
- package/dist/runtime/sandbox.mjs +57 -49
- package/dist/runtime/schema-mysql.d.mts +1 -0
- package/dist/runtime/schema-mysql.mjs +2 -0
- package/dist/runtime/seed.d.mts +14 -9
- package/dist/runtime/sse-client.d.mts +6 -7
- package/dist/runtime/sse.d.mts +11 -12
- package/dist/runtime/storage.d.mts +3 -4
- package/dist/runtime/validator.d.mts +1 -1
- package/dist/runtime/ws-server.d.mts +12 -12
- package/dist/runtime/ws-server.mjs +32 -4
- package/dist/runtime/ws.d.mts +19 -21
- package/dist/{scan-DYXkrasO.mjs → scan-BMH4rzlv.mjs} +53 -31
- package/dist/{scan-DEwlM_Xy.mjs → scan-CpK-57ug.mjs} +9 -5
- package/dist/{secret-Dt32J6RI.mjs → secret-BqTxGqki.mjs} +62 -5
- package/dist/{skills-CLjN0uUO.mjs → skills-Q46GZMO-.mjs} +6 -4
- package/dist/{standard-schema-DJ0HW7QP.d.mts → standard-schema-Fo_vCAZh.d.mts} +6 -6
- package/dist/{subcommand-prompt-BzV8iQZo.mjs → subcommand-prompt-WfySCQ7S.mjs} +67 -48
- package/dist/sveltekit.d.mts +12 -11
- package/dist/sveltekit.mjs +1 -1
- package/dist/types-BAp5AEBU.d.mts +79 -0
- package/dist/types-CKWnYgfy.d.mts +1 -0
- package/dist/{validate-Cw_RLeTj.mjs → validate-Bihr8WBi.mjs} +3 -3
- package/dist/wrangler--imS8n0d.mjs +1796 -0
- package/dist/{yarn-pnp-DJn3SAHF.mjs → yarn-pnp-DxSInkzL.mjs} +1 -1
- package/package.json +79 -65
- package/schema.json +35 -3
- package/skills/migrate-vite-cloudflare-to-void/SKILL.md +1 -1
- package/skills/void/SKILL.md +59 -2
- package/skills/void/docs/guide/ai.md +32 -14
- package/skills/void/docs/guide/app-types.md +6 -6
- package/skills/void/docs/guide/auth.md +14 -16
- package/skills/void/docs/guide/database/d1.md +6 -0
- package/skills/void/docs/guide/database/mysql.md +60 -0
- package/skills/void/docs/guide/database/postgresql.md +14 -9
- package/skills/void/docs/guide/database.md +39 -26
- package/skills/void/docs/guide/deployment.md +163 -22
- package/skills/void/docs/guide/durable-state.md +140 -0
- package/skills/void/docs/guide/edge/headers.md +5 -5
- package/skills/void/docs/guide/edge/prerendering.md +2 -0
- package/skills/void/docs/guide/edge/revalidation.md +21 -6
- package/skills/void/docs/guide/edge/rewrites.md +18 -14
- package/skills/void/docs/guide/edge/static-assets.md +23 -8
- package/skills/void/docs/guide/email.md +609 -0
- package/skills/void/docs/guide/env-migration.md +109 -0
- package/skills/void/docs/guide/env-vars.md +70 -248
- package/skills/void/docs/guide/index.md +15 -17
- package/skills/void/docs/guide/jobs.md +8 -5
- package/skills/void/docs/guide/live.md +7 -15
- package/skills/void/docs/guide/pages-routing/actions-and-forms.md +8 -4
- package/skills/void/docs/guide/pages-routing/islands.md +3 -3
- package/skills/void/docs/guide/pages-routing/loaders.md +6 -4
- package/skills/void/docs/guide/pages-routing/overview.md +7 -7
- package/skills/void/docs/guide/platform-administration.md +212 -0
- package/skills/void/docs/guide/platform-development.md +235 -0
- package/skills/void/docs/guide/queues.md +10 -10
- package/skills/void/docs/guide/quickstart.md +46 -67
- package/skills/void/docs/guide/remote-dev.md +8 -6
- package/skills/void/docs/guide/sandboxes.md +33 -16
- package/skills/void/docs/guide/self-hosted-platform.md +553 -0
- package/skills/void/docs/guide/server-routing.md +25 -5
- package/skills/void/docs/guide/sse.md +4 -4
- package/skills/void/docs/guide/ssg.md +5 -3
- package/skills/void/docs/guide/storage.md +2 -2
- package/skills/void/docs/guide/websockets.md +18 -7
- package/skills/void/docs/index.md +3 -3
- package/skills/void/docs/integrations/agents.md +6 -64
- package/skills/void/docs/integrations/cloudflare.md +165 -146
- package/skills/void/docs/integrations/frameworks/analog.md +4 -4
- package/skills/void/docs/integrations/frameworks/astro.md +5 -5
- package/skills/void/docs/integrations/frameworks/nuxt.md +5 -5
- package/skills/void/docs/integrations/frameworks/overview.md +3 -3
- package/skills/void/docs/integrations/frameworks/react-router.md +3 -3
- package/skills/void/docs/integrations/frameworks/sveltekit.md +3 -3
- package/skills/void/docs/integrations/frameworks/tanstack-start.md +2 -2
- package/skills/void/docs/integrations/nodejs-bun-deno.md +11 -4
- package/skills/void/docs/reference/api.md +42 -6
- package/skills/void/docs/reference/cli.md +637 -159
- package/skills/void/docs/reference/config.md +80 -25
- package/skills/void/docs/reference/resource-inference.md +13 -9
- package/skills/void/docs/reference/structure.md +10 -11
- package/AGENT_PROMPT.md +0 -19
- package/dist/cf-access-Bqw81xAf.mjs +0 -22
- package/dist/env-CZy5MorI.mjs +0 -299
- package/dist/env-helpers-z4stu8uc.d.mts +0 -52
- package/dist/env-mask-Dd47NbR6.mjs +0 -90
- package/dist/env-public-BfiLcMBk.d.mts +0 -140
- package/dist/link-CdGHSIy-.mjs +0 -45
- package/dist/mcp-DoM3_nhd.mjs +0 -377
- package/dist/project-paths-GpziKeQQ.d.mts +0 -25
- package/dist/providers-BNKRacMr.d.mts +0 -7
- package/dist/proxy-D-3_D-Gl.mjs +0 -5
- package/dist/rollback-CkvTFXx5.mjs +0 -90
- package/dist/runtime/isr-cache.d.mts +0 -207
- package/dist/runtime/isr-cache.mjs +0 -523
- package/dist/types-lLjNE9Qp.d.mts +0 -51
- package/getting-started-prompt.txt +0 -28
- package/skills/void/command/void.md +0 -7
- package/skills/void/docs/integrations/auth-providers.md +0 -0
- package/skills/void/docs/integrations/payment-processors.md +0 -0
- package/skills/void/docs/node_modules/@iconify/vue/README.md +0 -408
- package/skills/void/docs/node_modules/@iconify/vue/offline/readme.md +0 -5
- package/skills/void/docs/node_modules/@voidzero-dev/vitepress-theme/README.md +0 -103
- package/skills/void/docs/node_modules/oxc-minify/README.md +0 -78
- package/skills/void/docs/node_modules/reka-ui/README.md +0 -80
- package/skills/void/docs/node_modules/vitepress/README.md +0 -28
- package/skills/void/docs/node_modules/vitepress/template/api-examples.md +0 -49
- package/skills/void/docs/node_modules/vitepress/template/index.md +0 -28
- package/skills/void/docs/node_modules/vitepress/template/markdown-examples.md +0 -85
- package/skills/void/docs/node_modules/vitepress-plugin-group-icons/README.md +0 -101
- package/skills/void/docs/node_modules/void/AGENT_PROMPT.md +0 -19
- package/skills/void/docs/node_modules/void/CLAUDE.md +0 -219
- package/skills/void/docs/node_modules/void/README.md +0 -90
- package/skills/void/docs/node_modules/void/node_modules/@clack/prompts/CHANGELOG.md +0 -685
- package/skills/void/docs/node_modules/void/node_modules/@clack/prompts/README.md +0 -396
- package/skills/void/docs/node_modules/void/node_modules/@cloudflare/sandbox/README.md +0 -219
- package/skills/void/docs/node_modules/void/node_modules/@cloudflare/vite-plugin/README.md +0 -37
- package/skills/void/docs/node_modules/void/node_modules/@cloudflare/workers-types/README.md +0 -135
- package/skills/void/docs/node_modules/void/node_modules/@electric-sql/pglite/README.md +0 -189
- package/skills/void/docs/node_modules/void/node_modules/@hono/oauth-providers/CHANGELOG.md +0 -143
- package/skills/void/docs/node_modules/void/node_modules/@hono/oauth-providers/README.md +0 -1272
- package/skills/void/docs/node_modules/void/node_modules/@napi-rs/keyring/README.md +0 -19
- package/skills/void/docs/node_modules/void/node_modules/@types/better-sqlite3/README.md +0 -15
- package/skills/void/docs/node_modules/void/node_modules/@types/node/README.md +0 -15
- package/skills/void/docs/node_modules/void/node_modules/@types/pg/README.md +0 -15
- package/skills/void/docs/node_modules/void/node_modules/@types/proper-lockfile/README.md +0 -51
- package/skills/void/docs/node_modules/void/node_modules/@typescript/native-preview/README.md +0 -22
- package/skills/void/docs/node_modules/void/node_modules/@typescript/native-preview/vendor/vscode-jsonrpc/README.md +0 -69
- package/skills/void/docs/node_modules/void/node_modules/@void/md/README.md +0 -153
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/@shikijs/engine-javascript/README.md +0 -9
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/@shikijs/transformers/README.md +0 -9
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/@types/node/README.md +0 -15
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/gray-matter/CHANGELOG.md +0 -24
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/gray-matter/README.md +0 -565
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/markdown-exit/README.md +0 -127
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/markdown-it-anchor/README.md +0 -600
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/markdown-it-attrs/README.md +0 -386
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/markdown-it-container/README.md +0 -95
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/markdown-it-emoji/README.md +0 -101
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/markdown-it-footnote/README.md +0 -135
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/pathslash/README.md +0 -64
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/shiki/README.md +0 -15
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/tinyglobby/README.md +0 -25
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/AGENTS.md +0 -16
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/README.md +0 -220
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/config/build.md +0 -21
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/config/check.md +0 -35
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/config/create.md +0 -70
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/config/fmt.md +0 -20
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/config/index.md +0 -35
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/config/lint.md +0 -26
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/config/pack.md +0 -17
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/config/run.md +0 -364
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/config/staged.md +0 -15
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/config/test.md +0 -18
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/automatic-data-tracking.md +0 -145
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/build.md +0 -40
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/cache.md +0 -107
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/check.md +0 -60
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/ci.md +0 -62
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/commit-hooks.md +0 -60
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/create.md +0 -341
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/dev.md +0 -24
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/docker.md +0 -175
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/env.md +0 -167
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/fmt.md +0 -41
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/github-actions-cache.md +0 -165
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/ide-integration.md +0 -101
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/implode.md +0 -23
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/index.md +0 -134
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/install.md +0 -199
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/lint.md +0 -50
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/migrate-rules.md +0 -347
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/migrate.md +0 -197
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/monorepo.md +0 -176
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/pack.md +0 -69
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/run.md +0 -356
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/test.md +0 -35
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/troubleshooting.md +0 -108
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/upgrade.md +0 -101
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/vpx.md +0 -66
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/why.md +0 -39
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/index.md +0 -12
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/team.md +0 -35
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/templates/generator/README.md +0 -35
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/templates/monorepo/README.md +0 -29
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vue/README.md +0 -58
- package/skills/void/docs/node_modules/void/node_modules/arktype/README.md +0 -165
- package/skills/void/docs/node_modules/void/node_modules/better-auth/LICENSE.md +0 -20
- package/skills/void/docs/node_modules/void/node_modules/better-auth/README.md +0 -32
- package/skills/void/docs/node_modules/void/node_modules/better-sqlite3/README.md +0 -99
- package/skills/void/docs/node_modules/void/node_modules/blake3-jit/README.md +0 -108
- package/skills/void/docs/node_modules/void/node_modules/drizzle-arktype/README.md +0 -51
- package/skills/void/docs/node_modules/void/node_modules/drizzle-kit/README.md +0 -79
- package/skills/void/docs/node_modules/void/node_modules/drizzle-orm/README.md +0 -44
- package/skills/void/docs/node_modules/void/node_modules/drizzle-valibot/README.md +0 -51
- package/skills/void/docs/node_modules/void/node_modules/drizzle-zod/README.md +0 -65
- package/skills/void/docs/node_modules/void/node_modules/es-module-lexer/README.md +0 -403
- package/skills/void/docs/node_modules/void/node_modules/estree-walker/README.md +0 -48
- package/skills/void/docs/node_modules/void/node_modules/hono/README.md +0 -85
- package/skills/void/docs/node_modules/void/node_modules/ignore/README.md +0 -452
- package/skills/void/docs/node_modules/void/node_modules/jsonc-parser/CHANGELOG.md +0 -76
- package/skills/void/docs/node_modules/void/node_modules/jsonc-parser/LICENSE.md +0 -21
- package/skills/void/docs/node_modules/void/node_modules/jsonc-parser/README.md +0 -364
- package/skills/void/docs/node_modules/void/node_modules/jsonc-parser/SECURITY.md +0 -41
- package/skills/void/docs/node_modules/void/node_modules/magic-string/README.md +0 -325
- package/skills/void/docs/node_modules/void/node_modules/ofetch/README.md +0 -398
- package/skills/void/docs/node_modules/void/node_modules/pathslash/README.md +0 -64
- package/skills/void/docs/node_modules/void/node_modules/pg/README.md +0 -96
- package/skills/void/docs/node_modules/void/node_modules/pglite-server/README.md +0 -135
- package/skills/void/docs/node_modules/void/node_modules/picocolors/README.md +0 -21
- package/skills/void/docs/node_modules/void/node_modules/proper-lockfile/CHANGELOG.md +0 -108
- package/skills/void/docs/node_modules/void/node_modules/proper-lockfile/README.md +0 -183
- package/skills/void/docs/node_modules/void/node_modules/tinyglobby/README.md +0 -25
- package/skills/void/docs/node_modules/void/node_modules/valibot/LICENSE.md +0 -9
- package/skills/void/docs/node_modules/void/node_modules/valibot/README.md +0 -94
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/AGENTS.md +0 -16
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/README.md +0 -220
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/config/build.md +0 -21
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/config/check.md +0 -35
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/config/create.md +0 -70
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/config/fmt.md +0 -20
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/config/index.md +0 -35
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/config/lint.md +0 -26
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/config/pack.md +0 -17
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/config/run.md +0 -364
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/config/staged.md +0 -15
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/config/test.md +0 -18
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/automatic-data-tracking.md +0 -145
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/build.md +0 -40
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/cache.md +0 -107
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/check.md +0 -60
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/ci.md +0 -62
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/commit-hooks.md +0 -60
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/create.md +0 -341
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/dev.md +0 -24
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/docker.md +0 -175
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/env.md +0 -167
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/fmt.md +0 -41
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/github-actions-cache.md +0 -165
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/ide-integration.md +0 -101
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/implode.md +0 -23
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/index.md +0 -134
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/install.md +0 -199
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/lint.md +0 -50
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/migrate-rules.md +0 -347
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/migrate.md +0 -197
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/monorepo.md +0 -176
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/pack.md +0 -69
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/run.md +0 -356
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/test.md +0 -35
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/troubleshooting.md +0 -108
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/upgrade.md +0 -101
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/vpx.md +0 -66
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/why.md +0 -39
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/index.md +0 -12
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/team.md +0 -35
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/templates/generator/README.md +0 -35
- package/skills/void/docs/node_modules/void/node_modules/vite-plus/templates/monorepo/README.md +0 -29
- package/skills/void/docs/node_modules/void/node_modules/wrangler/README.md +0 -63
- package/skills/void/docs/node_modules/void/node_modules/zod/README.md +0 -191
- package/skills/void/docs/node_modules/void/skills/migrate-vite-cloudflare-to-void/SKILL.md +0 -175
- package/skills/void/docs/node_modules/void/skills/void/SKILL.md +0 -76
- package/skills/void/docs/node_modules/void/skills/void/command/void.md +0 -7
- package/skills/void/docs/node_modules/void/test/e2e/README.md +0 -85
|
@@ -0,0 +1,1216 @@
|
|
|
1
|
+
import { validate } from "email-validator";
|
|
2
|
+
//#region src/runtime/email/mime.ts
|
|
3
|
+
/**
|
|
4
|
+
* Minimal RFC 5322 / 2047 / 2045 MIME builder for the void/email runtime.
|
|
5
|
+
*
|
|
6
|
+
* Scope is intentionally narrow — text/plain, text/html, multipart/alternative,
|
|
7
|
+
* and multipart/mixed for attachments — covering >95% of transactional sends.
|
|
8
|
+
* Inline images use multipart/related when both attachments and html are present.
|
|
9
|
+
*/
|
|
10
|
+
/** Cloudflare Email Workers reject messages larger than ~10 MB after base64 expansion. */
|
|
11
|
+
const MAX_MESSAGE_BYTES = 10485760;
|
|
12
|
+
/**
|
|
13
|
+
* Upper bound on the TOTAL attachment bytes `normalizeEmail` will materialize.
|
|
14
|
+
*
|
|
15
|
+
* Base64 expands by `ceil(n / 3) * 4`, so an attachment set larger than three
|
|
16
|
+
* quarters of `MAX_MESSAGE_BYTES` cannot fit in a message that passes the
|
|
17
|
+
* check at the end of `buildMime` no matter how small everything else is.
|
|
18
|
+
* `buildMime` remains the authority on the final size — this bound only stops
|
|
19
|
+
* `normalizeEmail` from materializing, and then expanding, bytes already known
|
|
20
|
+
* not to fit.
|
|
21
|
+
*
|
|
22
|
+
* It has to be charged here because that final check comes far too late to
|
|
23
|
+
* help. Measured peak live memory is about 10x the attachment bytes (8 MiB in
|
|
24
|
+
* gives 83 MiB live) once the normalized copy, the base64 binary string, the
|
|
25
|
+
* 76-column slices, the joins, `rawText` and its `TextEncoder.encode` are all
|
|
26
|
+
* resident at once. Against a 128 MB isolate ceiling, a set over roughly
|
|
27
|
+
* 12 MiB terminates the isolate rather than returning the `MIME_ERROR` the
|
|
28
|
+
* docs promise for an oversize payload.
|
|
29
|
+
*
|
|
30
|
+
* This is a developer-experience guardrail, NOT the security boundary.
|
|
31
|
+
* Attachments come from the app's own code, never from a remote sender, and
|
|
32
|
+
* the platform proxy already caps the base64 length before decoding and the
|
|
33
|
+
* raw length after. What this buys is a precise, catchable failure at the call
|
|
34
|
+
* that went wrong, instead of a dead isolate with nothing a caller could act
|
|
35
|
+
* on.
|
|
36
|
+
*/
|
|
37
|
+
const MAX_ATTACHMENT_BYTES_TOTAL = Math.floor(MAX_MESSAGE_BYTES * 3 / 4);
|
|
38
|
+
const CRLF = "\r\n";
|
|
39
|
+
const ASCII_PRINTABLE = /^[\x20-\x7e]*$/;
|
|
40
|
+
const HEADER_NAME = /^[!-9;-~]+$/;
|
|
41
|
+
function rejectCRLF(field, value) {
|
|
42
|
+
for (let i = 0; i < value.length; i++) {
|
|
43
|
+
const c = value.charCodeAt(i);
|
|
44
|
+
if (c === 13 || c === 10 || c === 0) throw new MimeError("MIME_ERROR", `${field}: must not contain CR, LF, or NUL.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Refuse any other C0 control or DEL in a value that is rendered VERBATIM
|
|
49
|
+
* into a header — an attachment's filename, media type and Content-ID.
|
|
50
|
+
* `rejectCRLF` is enough for a value `encodeWord` renders (the subject, a
|
|
51
|
+
* custom header value, a display name): anything outside printable ASCII
|
|
52
|
+
* sends the whole value into base64, so a control never reaches the wire.
|
|
53
|
+
* These three go through `quoteParam` or straight into the line, so
|
|
54
|
+
* `invoice\x01.pdf` was written raw into `name="…"` and `filename="…"`,
|
|
55
|
+
* captured as a success under `void dev` and by the test harness, and left
|
|
56
|
+
* for the receiving MTA to refuse.
|
|
57
|
+
*
|
|
58
|
+
* RFC 5322 §3.2.4 `qtext` is printable US-ASCII less `"` and `\`; a
|
|
59
|
+
* control inside a quoted-string is `obs-qtext`, which §4 says MUST NOT be
|
|
60
|
+
* generated. HTAB is deliberately NOT refused: `quoted-string` is
|
|
61
|
+
* `DQUOTE *([FWS] qcontent) [FWS] DQUOTE` and `FWS` is built from `WSP`
|
|
62
|
+
* (SP / HTAB), so a tab between characters is folding white space exactly
|
|
63
|
+
* as a space is — and `obs-NO-WS-CTL`, the RFC's own list of the controls
|
|
64
|
+
* that used to be `qtext`, leaves %d9 out. The same set is illegal in an
|
|
65
|
+
* RFC 2045 §5.1 `token` (no CTLs) and an RFC 5322 §3.6.4 `msg-id`
|
|
66
|
+
* (`dot-atom-text`), so one rule serves the media type and the Content-ID.
|
|
67
|
+
*
|
|
68
|
+
* The message names the code point and never echoes the value, as
|
|
69
|
+
* `validateAddressParts` does. Runs AFTER the filename's length bound, so
|
|
70
|
+
* the scan is over a value already known to fit a line.
|
|
71
|
+
*/
|
|
72
|
+
function rejectControls(field, value) {
|
|
73
|
+
for (let i = 0; i < value.length; i++) {
|
|
74
|
+
const c = value.charCodeAt(i);
|
|
75
|
+
if (c < 32 && c !== 9 || c === 127) throw new MimeError("MIME_ERROR", `${field} contains a control character (U+${c.toString(16).toUpperCase().padStart(4, "0")}).`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** RFC 2045 §5.1 `tspecials`: what a `token` may not contain besides SPACE and CTLs. */
|
|
79
|
+
const TSPECIALS = "()<>@,;:\\\"/[]?=";
|
|
80
|
+
/** RFC 2045 §5.1 `token` character: printable US-ASCII less SPACE and `tspecials`. */
|
|
81
|
+
function isTokenChar(c) {
|
|
82
|
+
return c > 32 && c < 127 && !TSPECIALS.includes(String.fromCharCode(c));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Refuse an attachment media type that does not parse as RFC 2045 §5.1
|
|
86
|
+
* `type "/" subtype *(";" parameter)`.
|
|
87
|
+
*
|
|
88
|
+
* `rejectControls` answers only for CTLs, and `buildAttachmentPart` writes
|
|
89
|
+
* the value verbatim ahead of its own `; name="…"`, so `text/plain; x="`
|
|
90
|
+
* rendered `Content-Type: text/plain; x="; name="a.txt"` — captured as a
|
|
91
|
+
* success under `void dev` and by the harness, while Python's `email`
|
|
92
|
+
* reports `end of header inside quoted string` on that line and reads the
|
|
93
|
+
* builder's `name` parameter as the VALUE of `x`. `text` (no subtype),
|
|
94
|
+
* `text/plain; charset` (no value) and `text/plain;;` went the same way.
|
|
95
|
+
*
|
|
96
|
+
* The grammar is the RFC's, read as a generator: a `token` is printable
|
|
97
|
+
* US-ASCII less SPACE and `tspecials`; a `value` is a `token` or a
|
|
98
|
+
* quoted-string, whose content is anything but a bare `"` or `\` (the same
|
|
99
|
+
* rule `quoteParam` renders `name="…"` by, so a caller's quoted value may
|
|
100
|
+
* carry what the builder's own already does). Optional WSP is admitted on
|
|
101
|
+
* either side of `;` — the RFC 822 implicit LWSP every parser accepts and
|
|
102
|
+
* the form HTTP's `Content-Type` writes — and nowhere else: `text/ plain`,
|
|
103
|
+
* a trailing `;`, and an RFC 822 comment are refused rather than repaired.
|
|
104
|
+
* Two parameters are also refused on their meaning: a `name`, because the
|
|
105
|
+
* builder appends `name="<filename>"` and RFC 6838 §4.3 makes it "an error
|
|
106
|
+
* for a specific parameter to be specified more than once"; and by that
|
|
107
|
+
* same sentence any attribute given twice, compared case-insensitively as
|
|
108
|
+
* §5.1 says attributes match. Type and subtype are kept as given — matching
|
|
109
|
+
* is case-insensitive, so `TEXT/PLAIN` is legal and renders unchanged.
|
|
110
|
+
*
|
|
111
|
+
* One pass, no backtracking, and the only allocation is one lower-cased
|
|
112
|
+
* attribute per parameter for the duplicate check, which is why this runs
|
|
113
|
+
* AFTER the value's length bound. The message names the rule and never
|
|
114
|
+
* echoes the value.
|
|
115
|
+
*/
|
|
116
|
+
function validateMediaType(field, value) {
|
|
117
|
+
const syntax = () => new MimeError("MIME_ERROR", `${field} is not a valid media type (RFC 2045 §5.1: type/subtype, then ; attribute=value parameters).`);
|
|
118
|
+
const n = value.length;
|
|
119
|
+
let i = 0;
|
|
120
|
+
/** Advance over a `token`; the number of characters consumed. */
|
|
121
|
+
const token = () => {
|
|
122
|
+
const start = i;
|
|
123
|
+
while (i < n && isTokenChar(value.charCodeAt(i))) i++;
|
|
124
|
+
return i - start;
|
|
125
|
+
};
|
|
126
|
+
const wsp = () => {
|
|
127
|
+
while (i < n && (value[i] === " " || value[i] === " ")) i++;
|
|
128
|
+
};
|
|
129
|
+
if (token() === 0 || value[i] !== "/") throw syntax();
|
|
130
|
+
i++;
|
|
131
|
+
if (token() === 0) throw syntax();
|
|
132
|
+
const seen = /* @__PURE__ */ new Set();
|
|
133
|
+
while (i < n) {
|
|
134
|
+
wsp();
|
|
135
|
+
if (value[i] !== ";") throw syntax();
|
|
136
|
+
i++;
|
|
137
|
+
wsp();
|
|
138
|
+
const start = i;
|
|
139
|
+
if (token() === 0 || value[i] !== "=") throw syntax();
|
|
140
|
+
const attribute = value.slice(start, i).toLowerCase();
|
|
141
|
+
i++;
|
|
142
|
+
if (value[i] === "\"") {
|
|
143
|
+
i++;
|
|
144
|
+
for (;;) {
|
|
145
|
+
if (i >= n) throw syntax();
|
|
146
|
+
const ch = value[i++];
|
|
147
|
+
if (ch === "\"") break;
|
|
148
|
+
if (ch === "\\") {
|
|
149
|
+
if (i >= n) throw syntax();
|
|
150
|
+
i++;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} else if (token() === 0) throw syntax();
|
|
154
|
+
if (attribute === "name") throw new MimeError("MIME_ERROR", `${field} must not carry a name= parameter; the builder adds name="<filename>" itself.`);
|
|
155
|
+
if (seen.has(attribute)) throw new MimeError("MIME_ERROR", `${field} gives the same parameter twice; attribute names are case-insensitive and each may appear once (RFC 6838 §4.3).`);
|
|
156
|
+
seen.add(attribute);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** RFC 5322 §3.2.3 `atext` besides ALPHA and DIGIT. */
|
|
160
|
+
const ATEXT_SYMBOLS = "!#$%&'*+-/=?^_`{|}~";
|
|
161
|
+
/** RFC 5322 §3.2.3 `atext` character. */
|
|
162
|
+
function isAtext(c) {
|
|
163
|
+
return c >= 48 && c <= 57 || c >= 65 && c <= 90 || c >= 97 && c <= 122 || ATEXT_SYMBOLS.includes(String.fromCharCode(c));
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Refuse an attachment Content-ID that would not render as one RFC 5322
|
|
167
|
+
* §3.6.4 `msg-id`.
|
|
168
|
+
*
|
|
169
|
+
* `rejectControls` answers only for CTLs, and `buildAttachmentPart` writes
|
|
170
|
+
* the value verbatim — adding `<` and `>` unless the value already starts
|
|
171
|
+
* with `<` — so `logo other` rendered `Content-ID: <logo other>`, `<logo`
|
|
172
|
+
* rendered `Content-ID: <logo` and `logo>` rendered `Content-ID: <logo>>`:
|
|
173
|
+
* captured as a success under `void dev` and by the harness, and left for
|
|
174
|
+
* the receiver to refuse or to fail the `cid:` reference against.
|
|
175
|
+
*
|
|
176
|
+
* The grammar is the RFC's, read as a generator, with two deliberate
|
|
177
|
+
* differences. An `msg-id` is `"<" id-left "@" id-right ">"`, each side a
|
|
178
|
+
* `dot-atom-text` (§3.2.3: `1*atext *("." 1*atext)`, so no leading,
|
|
179
|
+
* trailing or doubled dot — the strict form; `obs-id-left`/`obs-id-right`
|
|
180
|
+
* are §4 obsolete syntax, which MUST NOT be generated); `no-fold-literal`
|
|
181
|
+
* (`[…]` for `id-right`) is left out. First difference: the `@` and its
|
|
182
|
+
* right side are OPTIONAL. RFC 2392 §2 resolves a `cid:` URL by wrapping
|
|
183
|
+
* its body in `<` `>` and matching that against `Content-ID`, so an id
|
|
184
|
+
* without `@` — the documented `contentId: 'logo'` + `cid:logo` — is what
|
|
185
|
+
* every client already resolves, and the domain that would make it a
|
|
186
|
+
* proper `msg-id` adds nothing a reference could use. Second: the angle
|
|
187
|
+
* brackets are optional too, because the builder has always added them
|
|
188
|
+
* around a bare id; given, they must be one pair around the whole value.
|
|
189
|
+
*
|
|
190
|
+
* Refused, then: whitespace of any kind, a second `@`, `<` or `>` anywhere
|
|
191
|
+
* but as that one pair, `"`, `(`, `)`, `,`, `;`, `:`, `\`, `[`, `]`,
|
|
192
|
+
* anything non-ASCII, and the empty string (which used to be validated
|
|
193
|
+
* and then silently NOT rendered). Everything accepted renders exactly as
|
|
194
|
+
* it did.
|
|
195
|
+
*
|
|
196
|
+
* One pass, no allocation, which is why this runs AFTER the value's length
|
|
197
|
+
* bound. The message names the rule and never echoes the value.
|
|
198
|
+
*/
|
|
199
|
+
function validateContentId(field, value) {
|
|
200
|
+
const syntax = () => new MimeError("MIME_ERROR", `${field} is not a valid Content-ID (RFC 5322 §3.6.4 msg-id: an identifier of atext, "." and "@", optionally in one pair of angle brackets).`);
|
|
201
|
+
let i = 0;
|
|
202
|
+
let end = value.length;
|
|
203
|
+
if (value[0] === "<") {
|
|
204
|
+
if (end < 2 || value[end - 1] !== ">") throw syntax();
|
|
205
|
+
i = 1;
|
|
206
|
+
end--;
|
|
207
|
+
}
|
|
208
|
+
/** Advance over a `dot-atom-text`; false when there is none, or a dot is misplaced. */
|
|
209
|
+
const dotAtomText = () => {
|
|
210
|
+
let run = 0;
|
|
211
|
+
for (; i < end; i++) {
|
|
212
|
+
const c = value.charCodeAt(i);
|
|
213
|
+
if (isAtext(c)) run++;
|
|
214
|
+
else if (c === 46 && run > 0) run = 0;
|
|
215
|
+
else break;
|
|
216
|
+
}
|
|
217
|
+
return run > 0;
|
|
218
|
+
};
|
|
219
|
+
if (!dotAtomText()) throw syntax();
|
|
220
|
+
if (i < end) {
|
|
221
|
+
if (value[i] !== "@") throw syntax();
|
|
222
|
+
i++;
|
|
223
|
+
if (!dotAtomText()) throw syntax();
|
|
224
|
+
}
|
|
225
|
+
if (i !== end) throw syntax();
|
|
226
|
+
}
|
|
227
|
+
const COMMON_TYPES = {
|
|
228
|
+
txt: "text/plain",
|
|
229
|
+
html: "text/html",
|
|
230
|
+
htm: "text/html",
|
|
231
|
+
css: "text/css",
|
|
232
|
+
csv: "text/csv",
|
|
233
|
+
pdf: "application/pdf",
|
|
234
|
+
json: "application/json",
|
|
235
|
+
xml: "application/xml",
|
|
236
|
+
zip: "application/zip",
|
|
237
|
+
png: "image/png",
|
|
238
|
+
jpg: "image/jpeg",
|
|
239
|
+
jpeg: "image/jpeg",
|
|
240
|
+
gif: "image/gif",
|
|
241
|
+
webp: "image/webp",
|
|
242
|
+
svg: "image/svg+xml",
|
|
243
|
+
ico: "image/x-icon",
|
|
244
|
+
ics: "text/calendar",
|
|
245
|
+
eml: "message/rfc822"
|
|
246
|
+
};
|
|
247
|
+
function inferContentType(filename) {
|
|
248
|
+
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
|
|
249
|
+
return COMMON_TYPES[ext] ?? "application/octet-stream";
|
|
250
|
+
}
|
|
251
|
+
var MimeError = class extends Error {
|
|
252
|
+
code;
|
|
253
|
+
constructor(code, message) {
|
|
254
|
+
super(message);
|
|
255
|
+
this.name = "MimeError";
|
|
256
|
+
this.code = code;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
function fieldErrorCode(field) {
|
|
260
|
+
if (field === "from") return "INVALID_FROM";
|
|
261
|
+
if (field === "to") return "INVALID_TO";
|
|
262
|
+
return "MIME_ERROR";
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Validate one parsed address (and its display name, when there is one),
|
|
266
|
+
* throwing a `MimeError` for `field` on anything undeliverable. Exported so
|
|
267
|
+
* the inbound proxy's `forward(to)` and hand-built `reply()` refuse a bare
|
|
268
|
+
* address by the same rule `sendEmail` applies to its `to`.
|
|
269
|
+
*
|
|
270
|
+
* The address rule is `email-validator`, and nothing else. It is also the
|
|
271
|
+
* platform router's rule (`isValidEmailAddress` in the proxy's
|
|
272
|
+
* `email-router.ts`), so there is exactly ONE address syntax in the tree:
|
|
273
|
+
* an action the framework records is never dropped by the router for its
|
|
274
|
+
* shape, and a test harness never captures as a success what the managed
|
|
275
|
+
* deployment would lose. The hand-written predecessor grew one character
|
|
276
|
+
* per review round — the exactly-one-`@` rule, then every C0 control and
|
|
277
|
+
* DEL, then `:`, then a parity test against the router's own regex, which
|
|
278
|
+
* had drifted on the dot in the domain — and still admitted `a..b@x.com`,
|
|
279
|
+
* `.a@x.com` and `a@x..com`, which no dot-atom allows and every MTA
|
|
280
|
+
* refuses. One maintained rule on both sides ends that series.
|
|
281
|
+
*
|
|
282
|
+
* What it enforces: at most 254 characters in total; an ASCII dot-atom
|
|
283
|
+
* local part of at most 64 characters (RFC 5322 §3.2.3 `atext`, dots only
|
|
284
|
+
* between atoms — no quoted local part, no whitespace, no control, no
|
|
285
|
+
* special); a dotted domain of ASCII labels of at most 63 characters each,
|
|
286
|
+
* with a TLD that starts with a letter and has at least two characters; so
|
|
287
|
+
* `user@localhost`, `x@y` and a raw IDN or SMTPUTF8 address are refused
|
|
288
|
+
* (an IDN domain is accepted as punycode, `xn--…`). ASCII-only makes
|
|
289
|
+
* octets and characters the same thing, so the RFC 5321 §4.5.3.1 octet
|
|
290
|
+
* limits are the package's length checks; the 64 is also what caps the
|
|
291
|
+
* default `<slug>+noreply@` sender (`VALID_SLUG` in `runtime/email.ts`,
|
|
292
|
+
* `MAX_SLUG_LENGTH` = `64 - 8` in `cli/project-slug.ts` and the API). One
|
|
293
|
+
* known quirk, pinned in the tests rather than worked around: the FIRST
|
|
294
|
+
* character of the local part may not be a backtick, while any later one
|
|
295
|
+
* may.
|
|
296
|
+
*
|
|
297
|
+
* `MAX_ADDRESS_LENGTH` still runs first, on the raw string, so the scan is
|
|
298
|
+
* bounded before anything reads the address; the package's own 254 check
|
|
299
|
+
* runs before its regex for the same reason. The message never echoes the
|
|
300
|
+
* address: it may hold a control character, and naming the rule is enough.
|
|
301
|
+
*/
|
|
302
|
+
function validateAddressParts(field, email, name) {
|
|
303
|
+
if (email.length > 320) throw new MimeError(fieldErrorCode(field), `${field}: address is ${email.length} characters, over the 320-character limit.`);
|
|
304
|
+
if (!validate(email)) throw new MimeError(fieldErrorCode(field), `${field}: address is not a valid email address (ASCII local part and dotted domain, RFC 5322 dot-atom; at most 64 characters before the '@' and 254 in total).`);
|
|
305
|
+
if (name !== void 0) {
|
|
306
|
+
rejectCRLF(`${field} display name`, name);
|
|
307
|
+
if (ASCII_PRINTABLE.test(name) && RFC5322_SPECIALS.test(name) && name.length + QUOTED_NAME_LINE_OVERHEAD > MAX_LINE_OCTETS) throw new MimeError(fieldErrorCode(field), `${field}: display name is ${name.length} characters and must be quoted, which cannot be folded, so its line would exceed the ${MAX_LINE_OCTETS}-octet limit in RFC 5322 §2.1.1. Shorten it.`);
|
|
308
|
+
if (name.includes("@")) throw new MimeError(fieldErrorCode(field), `${field}: display name '${name}' must not contain '@' (likely a malformed address).`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Undo the quoted-pairs in the content of a quoted display name: `\x` →
|
|
313
|
+
* `x` for any `x` (RFC 5322 §3.2.4 — `qtext` excludes `"` and `\`, so a
|
|
314
|
+
* quoted-string carries either only as a quoted-pair). The parsed name is
|
|
315
|
+
* the RAW text `formatAddress` will quote and escape again, so leaving the
|
|
316
|
+
* pairs in place rendered `"Smith \"Bob\"" <a@x.dev>` as
|
|
317
|
+
* `"Smith \\\"Bob\\\""` and the recipient saw `Smith \"Bob\"`, while the
|
|
318
|
+
* object form of the same name rendered correctly. Only the quoted branch
|
|
319
|
+
* of `parseAddress` calls this; a backslash in an unquoted name is a
|
|
320
|
+
* character like any other, as it is in the object form.
|
|
321
|
+
*
|
|
322
|
+
* NOT `text.replace(/\\(.)/g, '$1')`: a global-regex replacement holds one
|
|
323
|
+
* part per match before it builds the result, about 60 bytes each, and
|
|
324
|
+
* nothing bounds this name before it runs — the quoted-string line guard
|
|
325
|
+
* in `validateAddressParts` needs the DECODED name, and a non-ASCII name is
|
|
326
|
+
* never bounded at all. Measured, 10 MiB of `\"` pairs peaked at 307 MB of
|
|
327
|
+
* heap that way and terminated a 128 MB isolate; this shape peaks at 49 MB.
|
|
328
|
+
* Same discipline as `to7bitBody`: one counting pass, a buffer of exactly
|
|
329
|
+
* the output size, one fill, and the 32K-chunk conversion `base64` uses.
|
|
330
|
+
* The common case — no backslash at all — is one scan and no allocation.
|
|
331
|
+
* The regex that admits the text guarantees every backslash is followed by
|
|
332
|
+
* a character, so the fill never reads past the end.
|
|
333
|
+
*/
|
|
334
|
+
function unquotePairs(text) {
|
|
335
|
+
const first = text.indexOf("\\");
|
|
336
|
+
if (first === -1) return text;
|
|
337
|
+
let pairs = 0;
|
|
338
|
+
for (let i = first; i < text.length; i++) if (text.charCodeAt(i) === 92) {
|
|
339
|
+
pairs++;
|
|
340
|
+
i++;
|
|
341
|
+
}
|
|
342
|
+
const out = new Uint16Array(text.length - pairs);
|
|
343
|
+
let n = 0;
|
|
344
|
+
for (let i = 0; i < text.length; i++) {
|
|
345
|
+
let c = text.charCodeAt(i);
|
|
346
|
+
if (c === 92) c = text.charCodeAt(++i);
|
|
347
|
+
out[n++] = c;
|
|
348
|
+
}
|
|
349
|
+
let s = "";
|
|
350
|
+
const chunk = 32768;
|
|
351
|
+
for (let i = 0; i < n; i += chunk) s += String.fromCharCode(...out.subarray(i, i + chunk));
|
|
352
|
+
return s;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Parse one address — a bare `email@domain` or `Display Name <email@domain>`
|
|
356
|
+
* — into its parts, throwing a `MimeError` for `field` on anything else.
|
|
357
|
+
* Exported so `void.json` `email.from` is validated by the same rules the
|
|
358
|
+
* runtime applies to `from` at send time.
|
|
359
|
+
*/
|
|
360
|
+
function parseAddress(value, field) {
|
|
361
|
+
if (typeof value === "string") {
|
|
362
|
+
rejectCRLF(`${field} address`, value);
|
|
363
|
+
const quoted = value.match(/^\s*"((?:[^"\\]|\\.)*)"\s*<\s*([^>\s]+)\s*>\s*$/);
|
|
364
|
+
const angle = quoted ?? value.match(/^\s*(?:([^"<\s](?:[^"<]*[^"<\s])?)\s*)?<\s*([^>\s]+)\s*>\s*$/);
|
|
365
|
+
if (angle) {
|
|
366
|
+
const name = (quoted ? unquotePairs(angle[1] ?? "") : angle[1])?.trim();
|
|
367
|
+
const email = angle[2];
|
|
368
|
+
validateAddressParts(field, email, name);
|
|
369
|
+
return name ? {
|
|
370
|
+
email,
|
|
371
|
+
name
|
|
372
|
+
} : { email };
|
|
373
|
+
}
|
|
374
|
+
const trimmed = value.trim();
|
|
375
|
+
validateAddressParts(field, trimmed);
|
|
376
|
+
return { email: trimmed };
|
|
377
|
+
}
|
|
378
|
+
validateAddressParts(field, value.email ?? "", value.name);
|
|
379
|
+
return value.name ? {
|
|
380
|
+
email: value.email,
|
|
381
|
+
name: value.name
|
|
382
|
+
} : { email: value.email };
|
|
383
|
+
}
|
|
384
|
+
function parseAddresses(value, field) {
|
|
385
|
+
return (Array.isArray(value) ? value : [value]).map((entry) => parseAddress(entry, field));
|
|
386
|
+
}
|
|
387
|
+
/** Entries in one recipient field, counted the way `parseAddresses` will see them. */
|
|
388
|
+
function countRecipients(value) {
|
|
389
|
+
if (!value) return 0;
|
|
390
|
+
return Array.isArray(value) ? value.length : 1;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Keeps the `exceeds Cloudflare limit of <N> bytes` tail that `buildMime`'s
|
|
394
|
+
* own size error already uses, so the documented shape — and any caller
|
|
395
|
+
* matching on it — holds for both paths.
|
|
396
|
+
*/
|
|
397
|
+
function attachmentsTooLargeError(bytes) {
|
|
398
|
+
return new MimeError("MIME_ERROR", `email: attachment size ${bytes} bytes exceeds Cloudflare limit of ${MAX_ATTACHMENT_BYTES_TOTAL} bytes.`);
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Size of `content` WITHOUT materializing it.
|
|
402
|
+
*
|
|
403
|
+
* A string reports its UTF-16 `length`, which is a sound LOWER bound on its
|
|
404
|
+
* UTF-8 byte length — every code unit encodes to at least one byte — so this
|
|
405
|
+
* can never reject a string that would have fit; the exact length is charged
|
|
406
|
+
* after the encode. Anything else reports 0 and is left to `toBytes` to reject
|
|
407
|
+
* by type.
|
|
408
|
+
*/
|
|
409
|
+
function declaredSize(content) {
|
|
410
|
+
if (content instanceof Uint8Array) return content.byteLength;
|
|
411
|
+
if (content instanceof ArrayBuffer) return content.byteLength;
|
|
412
|
+
if (typeof Blob !== "undefined" && content instanceof Blob) return content.size;
|
|
413
|
+
if (typeof content === "string") return content.length;
|
|
414
|
+
return 0;
|
|
415
|
+
}
|
|
416
|
+
async function toBytes(content) {
|
|
417
|
+
if (content instanceof Uint8Array) return content;
|
|
418
|
+
if (content instanceof ArrayBuffer) return new Uint8Array(content);
|
|
419
|
+
if (typeof Blob !== "undefined" && content instanceof Blob) return new Uint8Array(await content.arrayBuffer());
|
|
420
|
+
if (typeof content === "string") return new TextEncoder().encode(content);
|
|
421
|
+
throw new MimeError("MIME_ERROR", "attachment content must be string, Uint8Array, ArrayBuffer, or Blob.");
|
|
422
|
+
}
|
|
423
|
+
async function normalizeAttachment(att) {
|
|
424
|
+
if (!att.filename) throw new MimeError("MIME_ERROR", "attachment: missing filename.");
|
|
425
|
+
rejectCRLF("attachment filename", att.filename);
|
|
426
|
+
if (att.filename.length + QUOTED_FILENAME_LINE_OVERHEAD > MAX_LINE_OCTETS) throw new MimeError("MIME_ERROR", `attachment filename is ${att.filename.length} characters; a quoted \`filename=\` parameter cannot be folded, so its line would exceed the ${MAX_LINE_OCTETS}-octet limit in RFC 5322 §2.1.1. Shorten the filename.`);
|
|
427
|
+
rejectControls("attachment filename", att.filename);
|
|
428
|
+
if (att.contentType !== void 0) {
|
|
429
|
+
rejectCRLF("attachment contentType", att.contentType);
|
|
430
|
+
if (att.contentType.length + CONTENT_TYPE_LINE_OVERHEAD > MAX_LINE_OCTETS) throw new MimeError("MIME_ERROR", `attachment contentType is ${att.contentType.length} characters; at most ${MAX_LINE_OCTETS - CONTENT_TYPE_LINE_OVERHEAD} fit on the Content-Type line under the ${MAX_LINE_OCTETS}-octet limit in RFC 5322 §2.1.1. Shorten it.`);
|
|
431
|
+
rejectControls("attachment contentType", att.contentType);
|
|
432
|
+
validateMediaType("attachment contentType", att.contentType);
|
|
433
|
+
}
|
|
434
|
+
if (att.contentId !== void 0) {
|
|
435
|
+
rejectCRLF("attachment contentId", att.contentId);
|
|
436
|
+
const brackets = att.contentId.startsWith("<") ? 0 : 2;
|
|
437
|
+
if (att.contentId.length + brackets + CONTENT_ID_LINE_OVERHEAD > MAX_LINE_OCTETS) throw new MimeError("MIME_ERROR", `attachment contentId is ${att.contentId.length} characters; at most ${MAX_LINE_OCTETS - CONTENT_ID_LINE_OVERHEAD - brackets} fit on the Content-ID line under the ${MAX_LINE_OCTETS}-octet limit in RFC 5322 §2.1.1. Shorten it.`);
|
|
438
|
+
rejectControls("attachment contentId", att.contentId);
|
|
439
|
+
validateContentId("attachment contentId", att.contentId);
|
|
440
|
+
}
|
|
441
|
+
if (att.disposition !== void 0 && att.disposition !== "attachment" && att.disposition !== "inline") throw new MimeError("MIME_ERROR", "attachment disposition must be 'attachment' or 'inline'.");
|
|
442
|
+
const bytes = await toBytes(att.content);
|
|
443
|
+
return {
|
|
444
|
+
filename: att.filename,
|
|
445
|
+
contentType: att.contentType ?? inferContentType(att.filename),
|
|
446
|
+
bytes,
|
|
447
|
+
contentId: att.contentId,
|
|
448
|
+
disposition: att.disposition ?? (att.contentId ? "inline" : "attachment")
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Validate inputs and produce a normalized email shape that can be handed to
|
|
453
|
+
* the MIME builder, the dev inbox, or the test harness without re-parsing.
|
|
454
|
+
*/
|
|
455
|
+
async function normalizeEmail(opts) {
|
|
456
|
+
if (!opts.text && !opts.html) throw new MimeError("MIME_ERROR", "email: at least one of 'text' or 'html' is required.");
|
|
457
|
+
if (opts.subject !== void 0) rejectCRLF("subject", opts.subject);
|
|
458
|
+
if (opts.headers) for (const [name, value] of Object.entries(opts.headers)) {
|
|
459
|
+
if (!HEADER_NAME.test(name)) throw new MimeError("MIME_ERROR", `headers: '${name}' is not a valid RFC 5322 header name.`);
|
|
460
|
+
if (name.length + HEADER_NAME_LINE_OVERHEAD > MAX_LINE_OCTETS) throw new MimeError("MIME_ERROR", `headers: a header name is ${name.length} characters; a field name cannot be folded, so its line would exceed the ${MAX_LINE_OCTETS}-octet limit in RFC 5322 §2.1.1. Shorten the name.`);
|
|
461
|
+
rejectCRLF(`header '${name}'`, value);
|
|
462
|
+
}
|
|
463
|
+
const recipientCount = countRecipients(opts.to) + countRecipients(opts.cc) + countRecipients(opts.bcc);
|
|
464
|
+
if (recipientCount > 100) throw new MimeError("INVALID_TO", `email: too many recipients (max 100, got ${recipientCount}) across 'to', 'cc' and 'bcc'.`);
|
|
465
|
+
const from = parseAddress(opts.from, "from");
|
|
466
|
+
const to = parseAddresses(opts.to, "to");
|
|
467
|
+
if (to.length === 0) throw new MimeError("INVALID_TO", "email: 'to' must include at least one recipient.");
|
|
468
|
+
const cc = opts.cc ? parseAddresses(opts.cc, "cc") : [];
|
|
469
|
+
const bcc = opts.bcc ? parseAddresses(opts.bcc, "bcc") : [];
|
|
470
|
+
const replyTo = opts.replyTo ? parseAddress(opts.replyTo, "replyTo") : void 0;
|
|
471
|
+
const attachmentList = opts.attachments ?? [];
|
|
472
|
+
if (!Array.isArray(attachmentList)) throw new MimeError("MIME_ERROR", "email: attachments must be an array.");
|
|
473
|
+
if (attachmentList.length > MAX_ATTACHMENTS) throw new MimeError("MIME_ERROR", `email: too many attachments (max ${MAX_ATTACHMENTS}, got ${attachmentList.length}).`);
|
|
474
|
+
const attachments = [];
|
|
475
|
+
let attachmentBytes = 0;
|
|
476
|
+
for (const att of attachmentList) {
|
|
477
|
+
const declared = declaredSize(att.content);
|
|
478
|
+
if (attachmentBytes + declared > MAX_ATTACHMENT_BYTES_TOTAL) throw attachmentsTooLargeError(attachmentBytes + declared);
|
|
479
|
+
const normalized = await normalizeAttachment(att);
|
|
480
|
+
attachmentBytes += normalized.bytes.byteLength;
|
|
481
|
+
if (attachmentBytes > MAX_ATTACHMENT_BYTES_TOTAL) throw attachmentsTooLargeError(attachmentBytes);
|
|
482
|
+
attachments.push(normalized);
|
|
483
|
+
}
|
|
484
|
+
if (opts.threading?.inReplyTo) rejectCRLF("threading.inReplyTo", opts.threading.inReplyTo);
|
|
485
|
+
if (opts.threading?.references) rejectCRLF("threading.references", opts.threading.references);
|
|
486
|
+
return {
|
|
487
|
+
from,
|
|
488
|
+
to,
|
|
489
|
+
cc,
|
|
490
|
+
bcc,
|
|
491
|
+
replyTo,
|
|
492
|
+
subject: opts.subject ?? "",
|
|
493
|
+
text: opts.text,
|
|
494
|
+
html: opts.html,
|
|
495
|
+
headers: opts.headers ? { ...opts.headers } : {},
|
|
496
|
+
attachments,
|
|
497
|
+
threading: opts.threading
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* RFC 2047 §2 caps an encoded-word at 75 characters *including* the
|
|
502
|
+
* `=?UTF-8?B?` prefix (10) and the `?=` suffix (2). That leaves 63
|
|
503
|
+
* characters of base64, and base64 spends 4 characters per 3 input
|
|
504
|
+
* bytes, so 45 source bytes is the largest payload that fits — 48 would
|
|
505
|
+
* need 64.
|
|
506
|
+
*/
|
|
507
|
+
const ENCODED_WORD_PAYLOAD_BYTES = 45;
|
|
508
|
+
/**
|
|
509
|
+
* RFC 2047 encoded-word for non-ASCII text in headers (UTF-8, base64).
|
|
510
|
+
*
|
|
511
|
+
* Text longer than one word's payload becomes several encoded-words
|
|
512
|
+
* separated by a space, which `foldHeader` turns into CRLF + SPACE
|
|
513
|
+
* wherever a line would otherwise overrun (RFC 2047 §6.2: whitespace
|
|
514
|
+
* between adjacent encoded-words is dropped on decode either way). A
|
|
515
|
+
* single unbounded word breached both §2's 75-character ceiling and —
|
|
516
|
+
* for a long subject or display name — RFC 5322 §2.1.1's 998-octet hard
|
|
517
|
+
* line limit, which nothing downstream repairs.
|
|
518
|
+
*/
|
|
519
|
+
function encodeWord(text) {
|
|
520
|
+
if (ASCII_PRINTABLE.test(text)) return text;
|
|
521
|
+
const encoder = new TextEncoder();
|
|
522
|
+
const pending = /* @__PURE__ */ new Uint8Array(49);
|
|
523
|
+
let length = 0;
|
|
524
|
+
const words = [];
|
|
525
|
+
const flush = () => {
|
|
526
|
+
if (length > 0) {
|
|
527
|
+
words.push(`=?UTF-8?B?${base64(pending.subarray(0, length))}?=`);
|
|
528
|
+
length = 0;
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
for (const char of text) {
|
|
532
|
+
const bytes = encoder.encode(char);
|
|
533
|
+
if (length > 0 && length + bytes.length > ENCODED_WORD_PAYLOAD_BYTES) flush();
|
|
534
|
+
pending.set(bytes, length);
|
|
535
|
+
length += bytes.length;
|
|
536
|
+
}
|
|
537
|
+
flush();
|
|
538
|
+
return words.join(" ");
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* RFC 5322 §3.2.3, verbatim:
|
|
542
|
+
*
|
|
543
|
+
* specials = "(" / ")" / "<" / ">" / "[" / "]" / ":" / ";" / "@" /
|
|
544
|
+
* "\" / "," / "." / DQUOTE
|
|
545
|
+
*
|
|
546
|
+
* A display name is a §3.2.5 `phrase = 1*word`, `word = atom /
|
|
547
|
+
* quoted-string`, and `atext` is everything printable EXCEPT these, so an
|
|
548
|
+
* ASCII name containing any of them must be rendered as one quoted-string.
|
|
549
|
+
* The class below lists the thirteen in the RFC's order so it can be checked
|
|
550
|
+
* against the production by eye. This check has been wrong twice: it once
|
|
551
|
+
* sat inside a branch that had already excluded every special, so it was
|
|
552
|
+
* unreachable and a comma in a name split the address list; then the list
|
|
553
|
+
* itself lacked `.`, so `Dr. Smith <a@x.dev>` went out unquoted. That is
|
|
554
|
+
* `obs-phrase` (§4.1, `word *(word / "." / CFWS)`), which §4 says MUST NOT
|
|
555
|
+
* be generated, and a strict receiver flags it as obsolete syntax (Python's
|
|
556
|
+
* `email` parser: "period in 'phrase'"). Every conforming generator —
|
|
557
|
+
* Python's `email.headerregistry.Address`, for one — emits `"Dr. Smith"`.
|
|
558
|
+
*/
|
|
559
|
+
const RFC5322_SPECIALS = /[()<>[\]:;@\\,."]/;
|
|
560
|
+
function formatAddress(addr) {
|
|
561
|
+
if (!addr.name) return addr.email;
|
|
562
|
+
if (!ASCII_PRINTABLE.test(addr.name)) return `${encodeWord(addr.name)} <${addr.email}>`;
|
|
563
|
+
if (RFC5322_SPECIALS.test(addr.name)) return `"${addr.name.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}" <${addr.email}>`;
|
|
564
|
+
return `${addr.name} <${addr.email}>`;
|
|
565
|
+
}
|
|
566
|
+
function formatAddressList(addrs) {
|
|
567
|
+
return addrs.map(formatAddress).join(", ");
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Soft-fold a long header line. Fold points, strongest first: address-list
|
|
571
|
+
* separators (a `, ` outside any double-quoted string), then any remaining
|
|
572
|
+
* unquoted whitespace inside a part. A naive `split(/, /)` would split
|
|
573
|
+
* inside `"Smith, John"`, corrupting quoted display names, and folding only
|
|
574
|
+
* between whole parts is not enough — one recipient carrying a long encoded
|
|
575
|
+
* display name, or a `References:` run of message IDs, is a single part
|
|
576
|
+
* that can blow past RFC 5322 §2.1.1's 998-octet hard limit on its own.
|
|
577
|
+
*
|
|
578
|
+
* RFC 2047 §2 limits any header line carrying an encoded-word to 76
|
|
579
|
+
* characters; RFC 5322 §2.1.1 makes 78 a SHOULD and 998 a MUST. Folding at
|
|
580
|
+
* 76 satisfies all three. A comma always stays on the line it terminates.
|
|
581
|
+
* A token longer than the budget still gets its own line, which is the
|
|
582
|
+
* best a WSP-only fold rule allows (§3.2.2 permits folding only at WSP).
|
|
583
|
+
*
|
|
584
|
+
* One pass, and the only thing kept per input character is the folded
|
|
585
|
+
* output itself: each finished line is one flat string in `lines`. The
|
|
586
|
+
* earlier shape — tokenize the whole value into an array of slices, wrap
|
|
587
|
+
* each in a `{ text, comma }` object, then grow the result with `out +=`
|
|
588
|
+
* per token — held about 130 bytes of heap per token for the length of the
|
|
589
|
+
* fold (a slice, an object and a cons-string node each), so a legal 3 MB
|
|
590
|
+
* subject of one-letter words terminated a 128 MB isolate. That was a
|
|
591
|
+
* per-TOKEN cost, invisible to any size projection: the message fit.
|
|
592
|
+
*/
|
|
593
|
+
function foldHeader(name, value) {
|
|
594
|
+
const line = `${name}: ${value}`;
|
|
595
|
+
if (line.length <= 78) return line;
|
|
596
|
+
const MAX_LINE = 76;
|
|
597
|
+
const lines = [];
|
|
598
|
+
let current = [`${name}:`];
|
|
599
|
+
let lineLen = name.length + 1;
|
|
600
|
+
let chunks = 0;
|
|
601
|
+
const emit = (text, comma) => {
|
|
602
|
+
chunks++;
|
|
603
|
+
const piece = comma ? `${text},` : text;
|
|
604
|
+
if (piece.length > 0 && lineLen + 1 + piece.length > MAX_LINE) {
|
|
605
|
+
lines.push(current.join(" "));
|
|
606
|
+
current = ["", piece];
|
|
607
|
+
lineLen = 1 + piece.length;
|
|
608
|
+
} else {
|
|
609
|
+
current.push(piece);
|
|
610
|
+
lineLen += 1 + piece.length;
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
let tokenStart = -1;
|
|
614
|
+
let heldStart = -1;
|
|
615
|
+
let heldEnd = -1;
|
|
616
|
+
const endToken = (i) => {
|
|
617
|
+
if (tokenStart < 0) return;
|
|
618
|
+
if (heldStart >= 0) emit(value.slice(heldStart, heldEnd), false);
|
|
619
|
+
heldStart = tokenStart;
|
|
620
|
+
heldEnd = i;
|
|
621
|
+
tokenStart = -1;
|
|
622
|
+
};
|
|
623
|
+
const endPart = (isLast) => {
|
|
624
|
+
if (heldStart >= 0) {
|
|
625
|
+
emit(value.slice(heldStart, heldEnd), !isLast);
|
|
626
|
+
heldStart = -1;
|
|
627
|
+
} else emit("", !isLast);
|
|
628
|
+
};
|
|
629
|
+
let inQuotes = false;
|
|
630
|
+
let escape = false;
|
|
631
|
+
for (let i = 0; i < value.length; i++) {
|
|
632
|
+
const ch = value[i];
|
|
633
|
+
if (escape) {
|
|
634
|
+
escape = false;
|
|
635
|
+
if (tokenStart < 0) tokenStart = i;
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
if (ch === "\\" && inQuotes) {
|
|
639
|
+
escape = true;
|
|
640
|
+
if (tokenStart < 0) tokenStart = i;
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
if (ch === "\"") {
|
|
644
|
+
inQuotes = !inQuotes;
|
|
645
|
+
if (tokenStart < 0) tokenStart = i;
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if (!inQuotes && ch === "," && value[i + 1] === " ") {
|
|
649
|
+
endToken(i);
|
|
650
|
+
endPart(false);
|
|
651
|
+
i++;
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
if (!inQuotes && (ch === " " || ch === " ")) {
|
|
655
|
+
endToken(i);
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (tokenStart < 0) tokenStart = i;
|
|
659
|
+
}
|
|
660
|
+
endToken(value.length);
|
|
661
|
+
endPart(true);
|
|
662
|
+
if (chunks <= 1) return line;
|
|
663
|
+
lines.push(current.join(" "));
|
|
664
|
+
return lines.join(CRLF);
|
|
665
|
+
}
|
|
666
|
+
/** RFC 5322 §2.1.1: each line MUST be at most 998 octets, excluding CRLF. */
|
|
667
|
+
const MAX_LINE_OCTETS = 998;
|
|
668
|
+
/** ` filename="` + `"`: the shortest line a quoted filename parameter can fold onto. */
|
|
669
|
+
const QUOTED_FILENAME_LINE_OVERHEAD = 12;
|
|
670
|
+
/**
|
|
671
|
+
* ` ` + `;`: the shortest line an attachment's media type can fold onto,
|
|
672
|
+
* the whole value taken as one token. `foldHeader` would split a value
|
|
673
|
+
* of several parameters at their spaces, so this bound is exact for the
|
|
674
|
+
* one-token media type every attachment carries in practice and
|
|
675
|
+
* conservative for a run of parameters that together pass the line
|
|
676
|
+
* limit; that is accepted, because the alternative — find the fold points
|
|
677
|
+
* first — is the scan this bound exists to run ahead of.
|
|
678
|
+
*/
|
|
679
|
+
const CONTENT_TYPE_LINE_OVERHEAD = 2;
|
|
680
|
+
/**
|
|
681
|
+
* `Content-ID: `: the only line an attachment's Content-ID can sit on. The
|
|
682
|
+
* value is one token — `<id>` — so `foldHeader` cannot fold it onto a
|
|
683
|
+
* continuation line the way it can a media type, and the field name is
|
|
684
|
+
* part of what the id must fit beside. Exact, not conservative: the `<`
|
|
685
|
+
* and `>` the builder adds around a bare id are charged at the call.
|
|
686
|
+
*/
|
|
687
|
+
const CONTENT_ID_LINE_OVERHEAD = 12;
|
|
688
|
+
/** ` "` + `"`: the shortest line a quoted display name can fold onto. */
|
|
689
|
+
const QUOTED_NAME_LINE_OVERHEAD = 3;
|
|
690
|
+
/** `:` alone: the shortest line a header name can sit on, its value folded onto the next. */
|
|
691
|
+
const HEADER_NAME_LINE_OVERHEAD = 1;
|
|
692
|
+
/**
|
|
693
|
+
* First line of `text` that exceeds the octet limit, if any.
|
|
694
|
+
*
|
|
695
|
+
* Every line is scanned, not just the top header block: a multipart part's
|
|
696
|
+
* own headers (`Content-Disposition: …; filename="…"`) live inside the body
|
|
697
|
+
* and are just as unfoldable. Body content cannot trip this — text is
|
|
698
|
+
* quoted-printable re-encoded above 990 octets and base64 is emitted at a
|
|
699
|
+
* fixed width — so a hit is always a header.
|
|
700
|
+
*
|
|
701
|
+
* One pass over the string, counting UTF-8 octets per CRLF-delimited line,
|
|
702
|
+
* with NO allocation. The earlier `text.split(CRLF)` held one array slot and
|
|
703
|
+
* one substring per line for the whole scan, and a legal body of millions of
|
|
704
|
+
* short lines — pasted logs, say — is millions of lines. Only a CRLF pair
|
|
705
|
+
* ends a line, exactly as the split did; a bare CR or LF is content.
|
|
706
|
+
*/
|
|
707
|
+
function findOverlongLine(text) {
|
|
708
|
+
let index = 0;
|
|
709
|
+
let octets = 0;
|
|
710
|
+
for (let i = 0; i < text.length; i++) {
|
|
711
|
+
const c = text.charCodeAt(i);
|
|
712
|
+
if (c === 13 && text.charCodeAt(i + 1) === 10) {
|
|
713
|
+
if (octets > MAX_LINE_OCTETS) return {
|
|
714
|
+
index,
|
|
715
|
+
octets
|
|
716
|
+
};
|
|
717
|
+
index++;
|
|
718
|
+
octets = 0;
|
|
719
|
+
i++;
|
|
720
|
+
} else if (c < 128) octets += 1;
|
|
721
|
+
else if (c < 2048) octets += 2;
|
|
722
|
+
else if (c >= 55296 && c <= 56319 && i + 1 < text.length) {
|
|
723
|
+
const d = text.charCodeAt(i + 1);
|
|
724
|
+
if (d >= 56320 && d <= 57343) {
|
|
725
|
+
octets += 4;
|
|
726
|
+
i++;
|
|
727
|
+
} else octets += 3;
|
|
728
|
+
} else octets += 3;
|
|
729
|
+
}
|
|
730
|
+
return octets > MAX_LINE_OCTETS ? {
|
|
731
|
+
index,
|
|
732
|
+
octets
|
|
733
|
+
} : void 0;
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Escape a value for a quoted-string parameter (RFC 2045 §5.1 value, whose
|
|
737
|
+
* quoted-string comes from RFC 5322 §3.2.4).
|
|
738
|
+
*
|
|
739
|
+
* `qtext` excludes both `\\` and `"`, so each may appear only as a
|
|
740
|
+
* quoted-pair. Deleting the quotes and leaving backslashes raw — what this
|
|
741
|
+
* did before — is wrong in two ways: a TRAILING backslash escapes the
|
|
742
|
+
* closing quote and leaves the parameter unterminated, and every embedded
|
|
743
|
+
* backslash is silently eaten by a conformant parser, so a Windows path
|
|
744
|
+
* `C:\\Users\\bob\\report.pdf` arrived as `C:Usersbobreport.pdf`. Measured,
|
|
745
|
+
* Python's `email.policy.default` and postal-mime both report
|
|
746
|
+
* `InvalidHeaderDefect: end of header inside quoted string` on the first
|
|
747
|
+
* case and disagree with a naive parser on the second.
|
|
748
|
+
*
|
|
749
|
+
* Backslash first, then quote — the other order would double-escape.
|
|
750
|
+
*
|
|
751
|
+
* Note this file's own `foldHeader` already tracks quoted-pairs when
|
|
752
|
+
* folding, so the emitter was contradicting the folder.
|
|
753
|
+
*/
|
|
754
|
+
function quoteParam(value) {
|
|
755
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
756
|
+
}
|
|
757
|
+
function base64(bytes) {
|
|
758
|
+
let binary = "";
|
|
759
|
+
const chunk = 32768;
|
|
760
|
+
for (let i = 0; i < bytes.length; i += chunk) binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
761
|
+
return btoa(binary);
|
|
762
|
+
}
|
|
763
|
+
function base64Wrapped(bytes) {
|
|
764
|
+
const encoded = base64(bytes);
|
|
765
|
+
const lines = [];
|
|
766
|
+
for (let i = 0; i < encoded.length; i += 76) lines.push(encoded.slice(i, i + 76));
|
|
767
|
+
return lines.join(CRLF);
|
|
768
|
+
}
|
|
769
|
+
function quotedPrintable(text) {
|
|
770
|
+
const bytes = new TextEncoder().encode(text);
|
|
771
|
+
const out = new Uint8Array(Math.ceil(bytes.length * 3 * (76 / 73)) + 8);
|
|
772
|
+
let n = 0;
|
|
773
|
+
let lineLen = 0;
|
|
774
|
+
const HEX = "0123456789ABCDEF";
|
|
775
|
+
const put = (c) => {
|
|
776
|
+
out[n++] = c;
|
|
777
|
+
};
|
|
778
|
+
const flush = (c0, c1, c2) => {
|
|
779
|
+
const width = c2 !== void 0 ? 3 : c1 !== void 0 ? 2 : 1;
|
|
780
|
+
if (lineLen + width > 75) {
|
|
781
|
+
put(61);
|
|
782
|
+
put(13);
|
|
783
|
+
put(10);
|
|
784
|
+
lineLen = 0;
|
|
785
|
+
}
|
|
786
|
+
put(c0);
|
|
787
|
+
if (c1 !== void 0) put(c1);
|
|
788
|
+
if (c2 !== void 0) put(c2);
|
|
789
|
+
lineLen += width;
|
|
790
|
+
};
|
|
791
|
+
const quote = (b) => flush(61, HEX.charCodeAt(b >> 4), HEX.charCodeAt(b & 15));
|
|
792
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
793
|
+
const b = bytes[i];
|
|
794
|
+
if (b === 10) {
|
|
795
|
+
put(13);
|
|
796
|
+
put(10);
|
|
797
|
+
lineLen = 0;
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (b === 13) {
|
|
801
|
+
if (bytes[i + 1] === 10) continue;
|
|
802
|
+
put(13);
|
|
803
|
+
put(10);
|
|
804
|
+
lineLen = 0;
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
if (b === 32 || b === 9) {
|
|
808
|
+
if (i === bytes.length - 1 || bytes[i + 1] === 10 || bytes[i + 1] === 13) quote(b);
|
|
809
|
+
else flush(b);
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
if (b === 61 || b < 32 || b > 126) {
|
|
813
|
+
quote(b);
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
flush(b);
|
|
817
|
+
}
|
|
818
|
+
return bytesToAsciiString(out.subarray(0, n));
|
|
819
|
+
}
|
|
820
|
+
/** Pure-ASCII bytes to a string, in the same 32K chunks `base64` uses. */
|
|
821
|
+
function bytesToAsciiString(bytes) {
|
|
822
|
+
let s = "";
|
|
823
|
+
const chunk = 32768;
|
|
824
|
+
for (let i = 0; i < bytes.length; i += chunk) s += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
825
|
+
return s;
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Whether a body must be quoted-printable encoded: anything outside
|
|
829
|
+
* printable ASCII, or any line longer than 990 characters (RFC 5322 §2.1.1
|
|
830
|
+
* makes 998 octets the hard limit). Only LF and CRLF end a line; a bare CR
|
|
831
|
+
* is a control character like any other and forces the encoding.
|
|
832
|
+
*
|
|
833
|
+
* One pass over the string with NO allocation. The earlier shape — strip
|
|
834
|
+
* every line break with a global `replace`, test the copy, then `split` the
|
|
835
|
+
* text into an array of lines — was linear in the message but held a copy
|
|
836
|
+
* of the body plus one array slot and one substring per line, and a legal
|
|
837
|
+
* 6 MB body of millions of two-character lines needed 64 MB for the strip
|
|
838
|
+
* and another 24-40 MB for the split. Those are per-LINE costs, invisible
|
|
839
|
+
* to any size projection: the message fit.
|
|
840
|
+
*/
|
|
841
|
+
function needsQuotedPrintable(text) {
|
|
842
|
+
let lineLen = 0;
|
|
843
|
+
for (let i = 0; i < text.length; i++) {
|
|
844
|
+
const c = text.charCodeAt(i);
|
|
845
|
+
if (c === 10) {
|
|
846
|
+
lineLen = 0;
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
if (c === 13) {
|
|
850
|
+
if (text.charCodeAt(i + 1) === 10) continue;
|
|
851
|
+
return true;
|
|
852
|
+
}
|
|
853
|
+
if (c < 32 || c > 126) return true;
|
|
854
|
+
lineLen++;
|
|
855
|
+
if (lineLen > 990) return true;
|
|
856
|
+
}
|
|
857
|
+
return false;
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* The 7bit body: `text` with every bare LF widened to CRLF, and every CRLF
|
|
861
|
+
* kept. Only called once `needsQuotedPrintable` has returned false, so every
|
|
862
|
+
* character is printable ASCII, LF, or the CR of a CRLF — one byte each —
|
|
863
|
+
* and a byte buffer is exact.
|
|
864
|
+
*
|
|
865
|
+
* NOT `text.replace(/\r?\n/g, CRLF)`: V8 builds a global-regex replacement
|
|
866
|
+
* from a parts array that holds two or three slots per match, grown by
|
|
867
|
+
* doubling, so that one line cost 144 MB of heap on a legal 6 MB body of
|
|
868
|
+
* 3M lines and terminated a 128 MB isolate — the largest single allocation
|
|
869
|
+
* in the whole build, for a body that renders to 9 MB. The buffer here is
|
|
870
|
+
* the output size, known up front from one counting pass. A body that is
|
|
871
|
+
* already CRLF-only (or has no line breaks) is returned as-is.
|
|
872
|
+
*/
|
|
873
|
+
function to7bitBody(text) {
|
|
874
|
+
let bare = 0;
|
|
875
|
+
for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10 && (i === 0 || text.charCodeAt(i - 1) !== 13)) bare++;
|
|
876
|
+
if (bare === 0) return text;
|
|
877
|
+
const out = new Uint8Array(text.length + bare);
|
|
878
|
+
let n = 0;
|
|
879
|
+
for (let i = 0; i < text.length; i++) {
|
|
880
|
+
const c = text.charCodeAt(i);
|
|
881
|
+
if (c === 10 && (i === 0 || text.charCodeAt(i - 1) !== 13)) out[n++] = 13;
|
|
882
|
+
out[n++] = c;
|
|
883
|
+
}
|
|
884
|
+
return bytesToAsciiString(out);
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Lower bound on the octets `text` will occupy in the rendered message, in
|
|
888
|
+
* one pass over the string and with NO allocation. Every UTF-8 byte yields at
|
|
889
|
+
* least one octet on either body path — the 7bit path copies bytes and only
|
|
890
|
+
* ever widens a bare LF to CRLF; the quoted-printable path emits each
|
|
891
|
+
* non-ASCII byte as `=XX` (3 octets) and every other byte as itself, and its
|
|
892
|
+
* soft breaks only add. So `ascii + 3 * nonAscii` can never overshoot the
|
|
893
|
+
* real size, which keeps a false rejection impossible; `buildMime`'s final
|
|
894
|
+
* check remains the authority. `TextEncoder.encode` would give the same count
|
|
895
|
+
* but at the price of a full copy, which is exactly the allocation this
|
|
896
|
+
* bound exists to avoid.
|
|
897
|
+
*/
|
|
898
|
+
function minBodyOctets(text) {
|
|
899
|
+
let octets = 0;
|
|
900
|
+
for (let i = 0; i < text.length; i++) {
|
|
901
|
+
const c = text.charCodeAt(i);
|
|
902
|
+
if (c < 128) octets += 1;
|
|
903
|
+
else if (c < 2048) octets += 6;
|
|
904
|
+
else if (c >= 55296 && c <= 56319 && i + 1 < text.length) {
|
|
905
|
+
const d = text.charCodeAt(i + 1);
|
|
906
|
+
if (d >= 56320 && d <= 57343) {
|
|
907
|
+
octets += 12;
|
|
908
|
+
i++;
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
octets += 9;
|
|
912
|
+
} else octets += 9;
|
|
913
|
+
}
|
|
914
|
+
return octets;
|
|
915
|
+
}
|
|
916
|
+
/** Octets a base64 body of `n` bytes occupies at 76 columns, excluding CRLF. */
|
|
917
|
+
function base64Octets(n) {
|
|
918
|
+
return Math.ceil(n / 3) * 4;
|
|
919
|
+
}
|
|
920
|
+
function renderPart(part) {
|
|
921
|
+
return part.headers.map(([k, v]) => foldHeader(k, v)).join(CRLF) + "\r\n\r\n" + part.body;
|
|
922
|
+
}
|
|
923
|
+
function buildBodyPart(contentType, body) {
|
|
924
|
+
const useQp = needsQuotedPrintable(body);
|
|
925
|
+
return {
|
|
926
|
+
headers: [["Content-Type", `${contentType}; charset=UTF-8`], ["Content-Transfer-Encoding", useQp ? "quoted-printable" : "7bit"]],
|
|
927
|
+
body: useQp ? quotedPrintable(body) : to7bitBody(body)
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
function buildAttachmentPart(att) {
|
|
931
|
+
const headers = [
|
|
932
|
+
["Content-Type", `${att.contentType}; name="${quoteParam(att.filename)}"`],
|
|
933
|
+
["Content-Transfer-Encoding", "base64"],
|
|
934
|
+
["Content-Disposition", `${att.disposition}; filename="${quoteParam(att.filename)}"`]
|
|
935
|
+
];
|
|
936
|
+
if (att.contentId) {
|
|
937
|
+
const cid = att.contentId.startsWith("<") ? att.contentId : `<${att.contentId}>`;
|
|
938
|
+
headers.push(["Content-ID", cid]);
|
|
939
|
+
}
|
|
940
|
+
return {
|
|
941
|
+
headers,
|
|
942
|
+
body: base64Wrapped(att.bytes)
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
function makeBoundary(prefix) {
|
|
946
|
+
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
947
|
+
crypto.getRandomValues(bytes);
|
|
948
|
+
return `=_void_${prefix}_${base64(bytes).replace(/[+/=]/g, "")}`;
|
|
949
|
+
}
|
|
950
|
+
function wrapMultipart(type, parts, extra = {}) {
|
|
951
|
+
const boundary = makeBoundary(type);
|
|
952
|
+
const headerEntries = [["Content-Type", `multipart/${type}; boundary="${boundary}"`]];
|
|
953
|
+
for (const [k, v] of Object.entries(extra)) headerEntries.push([k, v]);
|
|
954
|
+
return {
|
|
955
|
+
headers: headerEntries,
|
|
956
|
+
body: parts.map((p) => `--${boundary}${CRLF}${renderPart(p)}`).join(CRLF) + `${CRLF}--${boundary}--${CRLF}`
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
const RESERVED_HEADERS = new Set([
|
|
960
|
+
"from",
|
|
961
|
+
"to",
|
|
962
|
+
"cc",
|
|
963
|
+
"bcc",
|
|
964
|
+
"subject",
|
|
965
|
+
"reply-to",
|
|
966
|
+
"date",
|
|
967
|
+
"message-id",
|
|
968
|
+
"in-reply-to",
|
|
969
|
+
"references",
|
|
970
|
+
"mime-version",
|
|
971
|
+
"content-type",
|
|
972
|
+
"content-transfer-encoding",
|
|
973
|
+
"content-disposition"
|
|
974
|
+
].map((h) => h.toLowerCase()));
|
|
975
|
+
/**
|
|
976
|
+
* Lower bound on the octets one header VALUE occupies once `encodeWord` (or
|
|
977
|
+
* `formatAddress`, for a display name) and `foldHeader` have rendered it, in
|
|
978
|
+
* one pass over the string and with NO allocation.
|
|
979
|
+
*
|
|
980
|
+
* `encodeWord` leaves a value alone when every character is printable ASCII
|
|
981
|
+
* and otherwise B-encodes ALL of it, so the two paths need different terms:
|
|
982
|
+
*
|
|
983
|
+
* - Plain: every character is at least one octet, EXCEPT whitespace.
|
|
984
|
+
* `foldHeader` re-joins a long value from its non-WSP tokens, so a run of
|
|
985
|
+
* spaces or tabs collapses to one, and a bound that charged them could
|
|
986
|
+
* reject a message that fits. Only the non-WSP characters count.
|
|
987
|
+
* - Encoded: each RFC 2047 word carries at most `ENCODED_WORD_PAYLOAD_BYTES`
|
|
988
|
+
* (45) UTF-8 bytes as base64 — `ceil(n / 3) * 4 >= 4n / 3` octets — inside
|
|
989
|
+
* 12 octets of `=?UTF-8?B?` and `?=`, so there are at least `floor(n / 45)`
|
|
990
|
+
* words' worth of framing. Whitespace inside the value is base64 like any
|
|
991
|
+
* other byte; only the single separators between words fold.
|
|
992
|
+
*
|
|
993
|
+
* Folding (`foldHeader`) can only ADD octets beyond that — a `CRLF SP` per
|
|
994
|
+
* line — and quoting a display name (`formatAddress`) adds the quotes and
|
|
995
|
+
* escapes, so neither path can render fewer octets than this returns.
|
|
996
|
+
* Measured, the encoded bound is 96% of the rendered size for CJK, where the
|
|
997
|
+
* real cost is 5 octets per character; a UTF-16 `length` would have counted
|
|
998
|
+
* 1 and let the isolate die on a header five times over the limit.
|
|
999
|
+
*/
|
|
1000
|
+
function minHeaderValueOctets(value) {
|
|
1001
|
+
let utf8 = 0;
|
|
1002
|
+
let visible = 0;
|
|
1003
|
+
let encoded = false;
|
|
1004
|
+
for (let i = 0; i < value.length; i++) {
|
|
1005
|
+
const c = value.charCodeAt(i);
|
|
1006
|
+
if (c < 128) {
|
|
1007
|
+
utf8 += 1;
|
|
1008
|
+
if (c === 32 || c === 9) continue;
|
|
1009
|
+
if (c < 32 || c === 127) encoded = true;
|
|
1010
|
+
visible += 1;
|
|
1011
|
+
} else {
|
|
1012
|
+
encoded = true;
|
|
1013
|
+
if (c < 2048) utf8 += 2;
|
|
1014
|
+
else if (c >= 55296 && c <= 56319 && i + 1 < value.length) {
|
|
1015
|
+
const d = value.charCodeAt(i + 1);
|
|
1016
|
+
if (d >= 56320 && d <= 57343) {
|
|
1017
|
+
utf8 += 4;
|
|
1018
|
+
i++;
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
utf8 += 3;
|
|
1022
|
+
} else utf8 += 3;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
if (!encoded) return visible;
|
|
1026
|
+
return Math.floor(utf8 * 4 / 3) + 12 * Math.floor(utf8 / ENCODED_WORD_PAYLOAD_BYTES);
|
|
1027
|
+
}
|
|
1028
|
+
/** Lower bound on one rendered address: its display name, plus the address itself verbatim. */
|
|
1029
|
+
function minAddressOctets(addr) {
|
|
1030
|
+
return addr.email.length + (addr.name ? minHeaderValueOctets(addr.name) : 0);
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Lower bound on the octets of a value rendered VERBATIM — never through
|
|
1034
|
+
* `encodeWord` — into a header `foldHeader` may still fold: `In-Reply-To`,
|
|
1035
|
+
* `References`, and an attachment part's `Content-Type` media type and
|
|
1036
|
+
* `Content-ID`. Every non-WSP UTF-16 code unit is at least one octet of
|
|
1037
|
+
* UTF-8; a WSP run may collapse to a single space when the line folds, so
|
|
1038
|
+
* none is charged. The `<>` around a Content-ID and the fold's `CRLF SP`
|
|
1039
|
+
* only add.
|
|
1040
|
+
*
|
|
1041
|
+
* `minHeaderValueOctets` cannot stand in here: a non-ASCII value sends it
|
|
1042
|
+
* down the encoded-word formula (4.8 octets per CJK character) while these
|
|
1043
|
+
* values render as raw UTF-8 (3), so it would overshoot and could reject a
|
|
1044
|
+
* message that fits.
|
|
1045
|
+
*/
|
|
1046
|
+
function minVerbatimOctets(value) {
|
|
1047
|
+
let octets = 0;
|
|
1048
|
+
for (let i = 0; i < value.length; i++) {
|
|
1049
|
+
const c = value.charCodeAt(i);
|
|
1050
|
+
if (c !== 32 && c !== 9) octets += 1;
|
|
1051
|
+
}
|
|
1052
|
+
return octets;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Octets every attachment part costs before any of its values: what
|
|
1056
|
+
* `wrapMultipart`, `renderPart` and `buildAttachmentPart` emit for a part
|
|
1057
|
+
* whose filename, media type, disposition and content are all empty. Each
|
|
1058
|
+
* term is emitted verbatim on every path — `foldHeader` returns a short
|
|
1059
|
+
* line as-is and re-joins a long one from its tokens with at least one
|
|
1060
|
+
* space between them, so the space after `Name:` and the one after `;` are
|
|
1061
|
+
* floors, not counts — and a fold's `CRLF SP` only adds. The boundary's
|
|
1062
|
+
* random suffix is left out: `makeBoundary` strips `+`, `/` and `=`, so it
|
|
1063
|
+
* has no provable minimum, and `mixed` is the shortest prefix an attachment
|
|
1064
|
+
* part can sit under.
|
|
1065
|
+
*/
|
|
1066
|
+
const ATTACHMENT_PART_MIN_OCTETS = 117;
|
|
1067
|
+
/**
|
|
1068
|
+
* Cap on the attachment COUNT, checked against the raw list in
|
|
1069
|
+
* `normalizeEmail` before a single entry is normalized.
|
|
1070
|
+
*
|
|
1071
|
+
* Every part costs at least `ATTACHMENT_PART_MIN_OCTETS` of framing plus
|
|
1072
|
+
* its filename twice, and `normalizeAttachment` refuses an empty filename,
|
|
1073
|
+
* so the size projection in `buildMime` charges a part at least 119 octets
|
|
1074
|
+
* and refuses one more part than this whatever else the message holds.
|
|
1075
|
+
* That projection runs only after `normalizeEmail` has normalized EVERY
|
|
1076
|
+
* entry, and `MAX_ATTACHMENT_BYTES_TOTAL` cannot bound that loop for a
|
|
1077
|
+
* zero-byte attachment — its running total never advances — while the
|
|
1078
|
+
* normalized copy (the object, a fresh `Uint8Array`, the `TextEncoder`
|
|
1079
|
+
* that made it) costs about 275 bytes of live heap per entry. Measured, 2M
|
|
1080
|
+
* empty attachments terminated a 128 MB isolate in 0.3 s instead of
|
|
1081
|
+
* returning `MIME_ERROR`; this many normalize in 24 MB and are then refused
|
|
1082
|
+
* by the post-render size check under the same ceiling.
|
|
1083
|
+
*
|
|
1084
|
+
* Derived, not chosen: it is exactly the largest count the projection can
|
|
1085
|
+
* admit, so this cannot reject a message that would have been accepted, and
|
|
1086
|
+
* `buildMime` remains the authority on the final size. Same class as
|
|
1087
|
+
* `MAX_ATTACHMENT_BYTES_TOTAL` — a developer-experience guardrail on the
|
|
1088
|
+
* app's own input, not the security boundary.
|
|
1089
|
+
*/
|
|
1090
|
+
const MAX_ATTACHMENTS = Math.floor(MAX_MESSAGE_BYTES / 119);
|
|
1091
|
+
/**
|
|
1092
|
+
* Octets every custom header costs around its value: the `: ` after the
|
|
1093
|
+
* name and the CRLF that ends its line. Exact, not a floor, once the name
|
|
1094
|
+
* is counted: `buildMime` renders each non-reserved custom header through
|
|
1095
|
+
* `foldHeader`, which emits the name verbatim, then `: ` — or, when the
|
|
1096
|
+
* value's first token does not fit beside the name, `:` CRLF SP, which is
|
|
1097
|
+
* more — and `join(CRLF)` ends every header line, a custom header never
|
|
1098
|
+
* being the last of the block (the body part's own headers follow it).
|
|
1099
|
+
*/
|
|
1100
|
+
const CUSTOM_HEADER_FRAMING_OCTETS = 4;
|
|
1101
|
+
/**
|
|
1102
|
+
* Lower bound on the octets of every header `buildMime` renders from
|
|
1103
|
+
* caller-supplied values: `Subject`, the custom headers, the addresses in
|
|
1104
|
+
* `From`, `To`, `Cc` and `Reply-To`, the `In-Reply-To` / `References` pair
|
|
1105
|
+
* `replyEmail` threads through (remote-sender controlled, up to the whole
|
|
1106
|
+
* inbound message), and each attachment part's `Content-Type`, `Content-ID`
|
|
1107
|
+
* and — twice, once per header it is rendered into — filename. `Bcc` is
|
|
1108
|
+
* never rendered and a reserved custom header is dropped, so neither is
|
|
1109
|
+
* counted — charging either could reject a message that fits. The fixed
|
|
1110
|
+
* `Name: ` and `CRLF` of the headers `buildMime` itself names — `Subject`,
|
|
1111
|
+
* the address lists, the threading pair — are omitted for the same reason
|
|
1112
|
+
* a lower bound may omit any term. They are NOT omitted where the caller
|
|
1113
|
+
* controls the name or the count, because there they can be the whole
|
|
1114
|
+
* message:
|
|
1115
|
+
*
|
|
1116
|
+
* - A custom header's name is rendered verbatim, and `normalizeEmail`
|
|
1117
|
+
* bounds ONE name at under a kilobyte, not the sum, nor the count. With
|
|
1118
|
+
* empty values the names and the `: ` CRLF around each are the entire
|
|
1119
|
+
* size, and the framing is exact once the name is counted (see
|
|
1120
|
+
* `CUSTOM_HEADER_FRAMING_OCTETS`), so charging both cannot reject a
|
|
1121
|
+
* message that fits. Measured before the name was charged, 60k empty
|
|
1122
|
+
* headers with 500-character names — a 30 MB message — projected to
|
|
1123
|
+
* zero, rendered in full and peaked at 121 MB of a 128 MB isolate; 100k
|
|
1124
|
+
* of them (50 MB) went to 169 MB.
|
|
1125
|
+
* - An attachment part: `normalizeAttachment` bounds ONE filename at under
|
|
1126
|
+
* a kilobyte, not the sum, and a message is free to carry any number of
|
|
1127
|
+
* parts, so the filename and the framing every part is wrapped in are
|
|
1128
|
+
* the whole size of a message of empty attachments. Measured, 50k
|
|
1129
|
+
* zero-byte attachments with legal 986-character filenames rendered to
|
|
1130
|
+
* 106 MB — held three to four times over between the parts, their join,
|
|
1131
|
+
* `rawText` and `raw` — and terminated a 128 MB isolate before the final
|
|
1132
|
+
* size check could return `MIME_ERROR`.
|
|
1133
|
+
*
|
|
1134
|
+
* The header COUNT is deliberately not bounded here. Each header now costs
|
|
1135
|
+
* at least five octets (a one-character name, `: `, CRLF), so the count
|
|
1136
|
+
* the projection can admit is finite, and past a few hundred thousand keys
|
|
1137
|
+
* the caller's own object — measured at about 74 bytes per key, before
|
|
1138
|
+
* `sendEmail` is called — is the dominant cost, which no check inside this
|
|
1139
|
+
* module can pre-empt. Rendering does add live heap per header (about
|
|
1140
|
+
* 150 bytes: two `Object.entries`, the `topHeaders` pair, the folded line),
|
|
1141
|
+
* so a legal message of 500k four-character headers still does not fit a
|
|
1142
|
+
* 128 MB isolate while one of 400k does; a bound on that would be the
|
|
1143
|
+
* framework's own number — the platform proxy bounds bytes, not headers —
|
|
1144
|
+
* and is left as a product decision rather than invented here.
|
|
1145
|
+
*/
|
|
1146
|
+
function minHeaderOctets(email) {
|
|
1147
|
+
let octets = minHeaderValueOctets(email.subject);
|
|
1148
|
+
for (const [name, value] of Object.entries(email.headers)) if (!RESERVED_HEADERS.has(name.toLowerCase())) octets += name.length + CUSTOM_HEADER_FRAMING_OCTETS + minHeaderValueOctets(value);
|
|
1149
|
+
octets += minAddressOctets(email.from);
|
|
1150
|
+
for (const addr of email.to) octets += minAddressOctets(addr);
|
|
1151
|
+
for (const addr of email.cc) octets += minAddressOctets(addr);
|
|
1152
|
+
if (email.replyTo) octets += minAddressOctets(email.replyTo);
|
|
1153
|
+
if (email.threading?.inReplyTo) octets += minVerbatimOctets(email.threading.inReplyTo);
|
|
1154
|
+
if (email.threading?.references) octets += minVerbatimOctets(email.threading.references);
|
|
1155
|
+
for (const att of email.attachments) {
|
|
1156
|
+
octets += ATTACHMENT_PART_MIN_OCTETS + 2 * att.filename.length;
|
|
1157
|
+
octets += minVerbatimOctets(att.contentType);
|
|
1158
|
+
if (att.contentId) octets += minVerbatimOctets(att.contentId);
|
|
1159
|
+
}
|
|
1160
|
+
return octets;
|
|
1161
|
+
}
|
|
1162
|
+
function generateMessageId(fromEmail) {
|
|
1163
|
+
const domain = fromEmail.split("@")[1] ?? "localhost";
|
|
1164
|
+
const bytes = /* @__PURE__ */ new Uint8Array(12);
|
|
1165
|
+
crypto.getRandomValues(bytes);
|
|
1166
|
+
return `<${base64(bytes).replace(/[+/=]/g, "")}@${domain}>`;
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Build the raw RFC 5322 message body. Returns the message bytes plus the
|
|
1170
|
+
* generated Message-ID header so callers can surface it in the result.
|
|
1171
|
+
*/
|
|
1172
|
+
function buildMime(email) {
|
|
1173
|
+
const projected = minHeaderOctets(email) + (email.text ? minBodyOctets(email.text) : 0) + (email.html ? minBodyOctets(email.html) : 0) + email.attachments.reduce((sum, a) => sum + base64Octets(a.bytes.byteLength), 0);
|
|
1174
|
+
if (projected > 10485760) throw new MimeError("MIME_ERROR", `email: message is at least ${projected} bytes once encoded, which exceeds Cloudflare limit of ${MAX_MESSAGE_BYTES} bytes.`);
|
|
1175
|
+
const messageId = generateMessageId(email.from.email);
|
|
1176
|
+
const topHeaders = [
|
|
1177
|
+
["Date", (/* @__PURE__ */ new Date()).toUTCString().replace("GMT", "+0000")],
|
|
1178
|
+
["From", formatAddress(email.from)],
|
|
1179
|
+
["To", formatAddressList(email.to)]
|
|
1180
|
+
];
|
|
1181
|
+
if (email.cc.length > 0) topHeaders.push(["Cc", formatAddressList(email.cc)]);
|
|
1182
|
+
if (email.replyTo) topHeaders.push(["Reply-To", formatAddress(email.replyTo)]);
|
|
1183
|
+
topHeaders.push(["Subject", encodeWord(email.subject)]);
|
|
1184
|
+
topHeaders.push(["Message-ID", messageId]);
|
|
1185
|
+
if (email.threading?.inReplyTo) topHeaders.push(["In-Reply-To", email.threading.inReplyTo]);
|
|
1186
|
+
if (email.threading?.references) topHeaders.push(["References", email.threading.references]);
|
|
1187
|
+
topHeaders.push(["MIME-Version", "1.0"]);
|
|
1188
|
+
for (const [name, value] of Object.entries(email.headers)) {
|
|
1189
|
+
if (RESERVED_HEADERS.has(name.toLowerCase())) continue;
|
|
1190
|
+
topHeaders.push([name, encodeWord(value)]);
|
|
1191
|
+
}
|
|
1192
|
+
const textPart = email.text ? buildBodyPart("text/plain", email.text) : void 0;
|
|
1193
|
+
const htmlPart = email.html ? buildBodyPart("text/html", email.html) : void 0;
|
|
1194
|
+
let bodyPart;
|
|
1195
|
+
if (textPart && htmlPart) bodyPart = wrapMultipart("alternative", [textPart, htmlPart]);
|
|
1196
|
+
else bodyPart = textPart ?? htmlPart;
|
|
1197
|
+
if (email.attachments.length > 0) {
|
|
1198
|
+
const inlineAttachments = email.attachments.filter((a) => a.disposition === "inline");
|
|
1199
|
+
const fileAttachments = email.attachments.filter((a) => a.disposition !== "inline");
|
|
1200
|
+
if (inlineAttachments.length > 0 && htmlPart) bodyPart = wrapMultipart("related", [bodyPart, ...inlineAttachments.map(buildAttachmentPart)]);
|
|
1201
|
+
else fileAttachments.push(...inlineAttachments);
|
|
1202
|
+
if (fileAttachments.length > 0) bodyPart = wrapMultipart("mixed", [bodyPart, ...fileAttachments.map(buildAttachmentPart)]);
|
|
1203
|
+
}
|
|
1204
|
+
for (const [k, v] of bodyPart.headers) topHeaders.push([k, v]);
|
|
1205
|
+
const rawText = topHeaders.map(([k, v]) => foldHeader(k, v)).join(CRLF) + "\r\n\r\n" + bodyPart.body;
|
|
1206
|
+
const raw = new TextEncoder().encode(rawText);
|
|
1207
|
+
const overlong = findOverlongLine(rawText);
|
|
1208
|
+
if (overlong !== void 0) throw new MimeError("MIME_ERROR", `email: header line ${overlong.index + 1} is ${overlong.octets} octets, over the ${MAX_LINE_OCTETS}-octet limit in RFC 5322 §2.1.1. A value with no whitespace cannot be folded — shorten it, or move it into the body.`);
|
|
1209
|
+
if (raw.byteLength > 10485760) throw new MimeError("MIME_ERROR", `email: message size ${raw.byteLength} bytes exceeds Cloudflare limit of ${MAX_MESSAGE_BYTES} bytes.`);
|
|
1210
|
+
return {
|
|
1211
|
+
raw,
|
|
1212
|
+
messageId
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
//#endregion
|
|
1216
|
+
export { parseAddress as a, normalizeEmail as i, MimeError as n, validateAddressParts as o, buildMime as r, MAX_MESSAGE_BYTES as t };
|