sently 0.7.0 → 0.8.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/CHANGELOG.md +31 -0
- package/README.md +223 -18
- package/dist/adapters/bun.js +1 -1
- package/dist/adapters/cf.js +2 -2
- package/dist/adapters/cf.js.map +1 -1
- package/dist/adapters/deno.js +1 -1
- package/dist/adapters/node.js +1 -1
- package/dist/auth/oauth2.js +2 -2
- package/dist/auth/oauth2.js.map +1 -1
- package/dist/{chunk-d5rwm3br.js → chunk-5scdgffb.js} +2 -2
- package/dist/{chunk-d5rwm3br.js.map → chunk-5scdgffb.js.map} +1 -1
- package/dist/{chunk-a1c87hn1.js → chunk-6bkj4y4r.js} +2 -2
- package/dist/{chunk-a1c87hn1.js.map → chunk-6bkj4y4r.js.map} +1 -1
- package/dist/{chunk-cvrbfke9.js → chunk-8phpsns1.js} +3 -3
- package/dist/{chunk-cvrbfke9.js.map → chunk-8phpsns1.js.map} +1 -1
- package/dist/chunk-9zkv5azm.js +4 -0
- package/dist/chunk-9zkv5azm.js.map +10 -0
- package/dist/{chunk-qp4y0kp2.js → chunk-f3dz43cy.js} +2 -2
- package/dist/{chunk-qp4y0kp2.js.map → chunk-f3dz43cy.js.map} +1 -1
- package/dist/chunk-m07smapm.js +3 -0
- package/dist/chunk-m07smapm.js.map +10 -0
- package/dist/chunk-n5dan5bz.js +4 -0
- package/dist/chunk-n5dan5bz.js.map +10 -0
- package/dist/{chunk-keksa6fx.js → chunk-nbmq8ejv.js} +2 -2
- package/dist/{chunk-keksa6fx.js.map → chunk-nbmq8ejv.js.map} +1 -1
- package/dist/chunk-qh90yr6d.js +4 -0
- package/dist/{chunk-gh5n759p.js.map → chunk-qh90yr6d.js.map} +2 -2
- package/dist/{chunk-r7ee9tk7.js → chunk-vhxzhx8b.js} +3 -3
- package/dist/{chunk-r7ee9tk7.js.map → chunk-vhxzhx8b.js.map} +1 -1
- package/dist/{chunk-rm78s1ek.js → chunk-wbvhaqcp.js} +3 -3
- package/dist/{chunk-rm78s1ek.js.map → chunk-wbvhaqcp.js.map} +1 -1
- package/dist/{chunk-74rha3ac.js → chunk-wsymfbkn.js} +3 -3
- package/dist/{chunk-74rha3ac.js.map → chunk-wsymfbkn.js.map} +1 -1
- package/dist/chunk-yr56b0ts.js +4 -0
- package/dist/chunk-yr56b0ts.js.map +11 -0
- package/dist/{chunk-kebhy657.js → chunk-z62q7kqr.js} +2 -2
- package/dist/{chunk-kebhy657.js.map → chunk-z62q7kqr.js.map} +1 -1
- package/dist/{chunk-gkkv7yqm.js → chunk-zst8an2p.js} +3 -3
- package/dist/{chunk-gkkv7yqm.js.map → chunk-zst8an2p.js.map} +1 -1
- package/dist/core/errors.js +2 -2
- package/dist/core/errors.js.map +1 -1
- package/dist/core/provider-label.d.ts +6 -0
- package/dist/core/smtp.js +2 -2
- package/dist/core/smtp.js.map +1 -1
- package/dist/core/types.d.ts +27 -4
- package/dist/detect.js +2 -2
- package/dist/detect.js.map +1 -1
- package/dist/dkim.js +2 -2
- package/dist/dkim.js.map +1 -1
- package/dist/idempotency.d.ts +1 -0
- package/dist/idempotency.js +2 -2
- package/dist/idempotency.js.map +3 -3
- package/dist/index.d.ts +49 -0
- package/dist/index.js +58 -0
- package/dist/mailer.js +2 -2
- package/dist/mailer.js.map +1 -1
- package/dist/observability/console.d.ts +12 -0
- package/dist/observability/console.js +3 -0
- package/dist/observability/console.js.map +10 -0
- package/dist/plugins/react.js +2 -2
- package/dist/plugins/react.js.map +1 -1
- package/dist/plugins/template.js +1 -1
- package/dist/pool/pool.js +2 -2
- package/dist/pool/pool.js.map +1 -1
- package/dist/smtp-mailer.js +2 -2
- package/dist/smtp-mailer.js.map +2 -2
- package/dist/transports/brevo.d.ts +1 -0
- package/dist/transports/brevo.js +2 -2
- package/dist/transports/brevo.js.map +3 -3
- package/dist/transports/cloudflare-email.d.ts +58 -0
- package/dist/transports/cloudflare-email.js +3 -0
- package/dist/transports/cloudflare-email.js.map +10 -0
- package/dist/transports/fallback.d.ts +100 -0
- package/dist/transports/fallback.js +3 -0
- package/dist/transports/fallback.js.map +9 -0
- package/dist/transports/loops.d.ts +32 -0
- package/dist/transports/loops.js +3 -0
- package/dist/transports/loops.js.map +10 -0
- package/dist/transports/mailersend.d.ts +27 -0
- package/dist/transports/mailersend.js +3 -0
- package/dist/transports/mailersend.js.map +10 -0
- package/dist/transports/mailgun.d.ts +1 -0
- package/dist/transports/mailgun.js +2 -2
- package/dist/transports/mailgun.js.map +3 -3
- package/dist/transports/mailtrap.d.ts +36 -0
- package/dist/transports/mailtrap.js +3 -0
- package/dist/transports/mailtrap.js.map +10 -0
- package/dist/transports/plunk.d.ts +29 -0
- package/dist/transports/plunk.js +3 -0
- package/dist/transports/plunk.js.map +10 -0
- package/dist/transports/postmark.d.ts +1 -0
- package/dist/transports/postmark.js +2 -2
- package/dist/transports/postmark.js.map +3 -3
- package/dist/transports/preview.d.ts +1 -0
- package/dist/transports/preview.js +2 -2
- package/dist/transports/preview.js.map +3 -3
- package/dist/transports/resend.d.ts +1 -0
- package/dist/transports/resend.js +2 -2
- package/dist/transports/resend.js.map +3 -3
- package/dist/transports/retry.d.ts +1 -0
- package/dist/transports/retry.js +2 -2
- package/dist/transports/retry.js.map +3 -3
- package/dist/transports/sendgrid.d.ts +1 -0
- package/dist/transports/sendgrid.js +2 -2
- package/dist/transports/sendgrid.js.map +3 -3
- package/dist/transports/ses.d.ts +1 -0
- package/dist/transports/ses.js +3 -3
- package/dist/transports/ses.js.map +3 -3
- package/dist/transports/smtp.d.ts +1 -0
- package/dist/transports/smtp.js +2 -2
- package/dist/transports/smtp.js.map +1 -1
- package/dist/transports/sparkpost.d.ts +30 -0
- package/dist/transports/sparkpost.js +3 -0
- package/dist/transports/sparkpost.js.map +10 -0
- package/dist/transports/weighted-fallback.d.ts +65 -0
- package/dist/transports/weighted-fallback.js +3 -0
- package/dist/transports/weighted-fallback.js.map +10 -0
- package/dist/webhooks/brevo.js +1 -1
- package/dist/webhooks/mailgun.js +1 -1
- package/dist/webhooks/postmark.js +1 -1
- package/dist/webhooks/resend.js +1 -1
- package/dist/webhooks/sendgrid.js +1 -1
- package/dist/webhooks/ses.js +1 -1
- package/dist/webhooks/timing-safe-equal.js +1 -1
- package/package.json +37 -1
- package/dist/chunk-4mvp3b85.js +0 -3
- package/dist/chunk-4mvp3b85.js.map +0 -10
- package/dist/chunk-gh5n759p.js +0 -4
- package/dist/chunk-ka6t8g77.js +0 -4
- package/dist/chunk-ka6t8g77.js.map +0 -11
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
function x(j){if(j.provider!==void 0&&j.provider.length>0)return j.provider;let q=j.constructor.name;if(q==="Object")return"custom";if(q.endsWith("Transport"))return q.slice(0,-9).toLowerCase();return q.toLowerCase()||"unknown"}
|
|
2
|
+
export{x as A};
|
|
3
|
+
|
|
4
|
+
//# debugId=F01F6E80D9AFEE8E64756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/core/provider-label.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import type { Transport } from \"./types.js\";\n\n/**\n * Resolve a stable provider label for observability and diagnostics.\n * Prefers {@link Transport.provider} when set; falls back to constructor name.\n */\nexport function getProviderLabel(transport: Transport): string {\n if (transport.provider !== undefined && transport.provider.length > 0) {\n return transport.provider;\n }\n\n const name = transport.constructor.name;\n if (name === \"Object\") {\n return \"custom\";\n }\n if (name.endsWith(\"Transport\")) {\n return name.slice(0, -\"Transport\".length).toLowerCase();\n }\n return name.toLowerCase() || \"unknown\";\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "AAMO,SAAS,CAAgB,CAAC,EAA8B,CAC7D,GAAI,EAAU,WAAa,QAAa,EAAU,SAAS,OAAS,EAClE,OAAO,EAAU,SAGnB,IAAM,EAAO,EAAU,YAAY,KACnC,GAAI,IAAS,SACX,MAAO,SAET,GAAI,EAAK,SAAS,WAAW,EAC3B,OAAO,EAAK,MAAM,EAAG,EAAmB,EAAE,YAAY,EAExD,OAAO,EAAK,YAAY,GAAK",
|
|
8
|
+
"debugId": "F01F6E80D9AFEE8E64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
"import type { MailOptions } from \"./types.js\";\n\n/**\n * Resolve the idempotency key for a message.\n * Uses explicit `idempotencyKey` or derives from `messageId` when present.\n */\nexport function resolveIdempotencyKey(options: MailOptions): string | undefined {\n if (options.idempotencyKey) {\n return options.idempotencyKey;\n }\n if (options.messageId) {\n return `msg:${options.messageId}`;\n }\n return undefined;\n}\n"
|
|
6
6
|
],
|
|
7
7
|
"mappings": "AAMO,SAAS,CAAqB,CAAC,EAA0C,CAC9E,GAAI,EAAQ,eACV,OAAO,EAAQ,eAEjB,GAAI,EAAQ,UACV,MAAO,OAAO,EAAQ,YAExB",
|
|
8
|
-
"debugId": "
|
|
8
|
+
"debugId": "8B3534058EABEDA664756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{s as U}from"./chunk-wbvhaqcp.js";import{u as X}from"./chunk-zst8an2p.js";import{v as b}from"./chunk-vhxzhx8b.js";import{H as k,I as $,J as Z,K as O,L as q,M as L,N as v,O as N,P as A,Q as j,S as E}from"./chunk-8phpsns1.js";import{_ as C}from"./chunk-nbmq8ejv.js";class T{provider="smtp";config;adapter=null;constructor(z){this.config=P(z)}async send(z){let B={...z,attachments:await b(z.attachments)},G=await X(B,this.config.dkim),F=await this.getAdapter(),W=this.config.direct?await M(G.envelope.from.split("@")[1]??this.config.host):this.config.host;await F.connect(W,this.config.port),this.adapter=F;try{return await D(F,this.config),await S(F,G)}finally{await H(F),this.adapter=null}}async verify(){try{let z=await this.getAdapter();await z.connect(this.config.host,this.config.port);try{return await D(z,this.config),{ok:!0,provider:"smtp"}}finally{await H(z)}}catch(z){return{ok:!1,provider:"smtp",message:z instanceof Error?z.message:String(z)}}}async close(){if(this.adapter)await this.adapter.close(),this.adapter=null}async getAdapter(){if(!this.config.adapter)throw new $("No socket adapter configured",0,"CONNECT","");return this.config.adapter}}function P(z){let B=z.secure??!1;return{host:z.host,port:z.port??(B?465:587),secure:B,...z.auth!==void 0?{auth:z.auth}:{},...z.requireTLS!==void 0?{requireTLS:z.requireTLS}:{},...z.dkim!==void 0?{dkim:z.dkim}:{},...z.tls!==void 0?{tls:z.tls}:{},...z.connectionTimeout!==void 0?{connectionTimeout:z.connectionTimeout}:{},...z.greetingTimeout!==void 0?{greetingTimeout:z.greetingTimeout}:{},...z.socketTimeout!==void 0?{socketTimeout:z.socketTimeout}:{},...z.direct!==void 0?{direct:z.direct}:{},...z.adapter!==void 0?{adapter:z.adapter}:{}}}async function D(z,B){let G=await J(z);N(G,[220],"greeting");let F=await I(z,B.host),W=F.some((Q)=>Q.toUpperCase()==="STARTTLS");if(!B.secure&&!z.secure&&W){await Y(z,Z({type:"STARTTLS"}));let Q=await J(z);N(Q,[220],"STARTTLS"),await z.startTLS(B.tls),F=await I(z,B.host)}if(B.auth){let Q=B.requireTLS??!0,V=z.secure||B.secure;if(Q&&!V)throw new $("Refusing to authenticate over unencrypted connection. Set requireTLS: false to disable this check (not recommended).",0,"AUTH","");await l(z,B.auth,F)}}async function S(z,B){await _(z,{type:"MAIL_FROM",address:B.envelope.from});let G=await J(z);N(G,[250],"MAIL FROM");let F=[],W=[];for(let K of B.envelope.to)if(await Y(z,Z({type:"RCPT_TO",address:K})),(await J(z)).isSuccess)F.push(K);else W.push(K);await _(z,{type:"DATA"});let Q=await J(z);N(Q,[354],"DATA");let V;try{await Y(z,Z({type:"DATA_BODY",content:B.raw})),V=await J(z)}catch(K){if(await Y(z,Z({type:"DATA_BODY",content:B.raw})),V=await J(z),V.isError)throw K}return N(V,[250],"DATA end"),{messageId:B.messageId,accepted:F,rejected:W,response:V.message,envelope:B.envelope}}async function H(z){try{await _(z,{type:"QUIT"}),await J(z)}catch{}finally{await z.close()}}async function I(z,B){await _(z,{type:"EHLO",domain:B});let G=await J(z);return N(G,[250],"EHLO"),v(G)}async function l(z,B,G){if(B.type==="OAUTH2"&&B.oauth2){let K=await new U(B.oauth2).buildXOAUTH2();await _(z,{type:"AUTH_XOAUTH2",xoauth2String:K});let x=await J(z);if(x.code===334)await Y(z,E("")),x=await J(z);N(x,[235],"AUTH XOAUTH2");return}let F=B.type??L(G);if(F==="CRAM-MD5"){let V=y(B,"CRAM-MD5");await _(z,{type:"AUTH_CRAM_MD5_INIT"});let K=await J(z);N(K,[334],"AUTH CRAM-MD5");let x=K.message.trim(),w=await k(x,B.user,V);await _(z,{type:"AUTH_CRAM_MD5_RESPONSE",response:w}),K=await J(z),N(K,[235],"AUTH CRAM-MD5 response");return}if(F==="PLAIN"){let V=y(B,"PLAIN");await Y(z,Z({type:"AUTH_PLAIN",user:B.user,pass:V}));let K=await J(z);N(K,[235],"AUTH PLAIN");return}let W=y(B,"LOGIN");await Y(z,Z({type:"AUTH_LOGIN",user:B.user,pass:W}));let Q=await J(z);N(Q,[334],"AUTH LOGIN"),await Y(z,j(B.user)),Q=await J(z),N(Q,[334],"AUTH LOGIN user"),await Y(z,A(W)),Q=await J(z),N(Q,[235],"AUTH LOGIN pass")}function y(z,B){if(!z.pass)throw new $(`Password required for ${B} authentication`,0,`AUTH ${B}`,"");return z.pass}async function _(z,B){await Y(z,Z(B))}async function Y(z,B){await z.write(B)}async function J(z){let B=[];for await(let G of z.read()){B.push(G);let F=q(B);if(F)return O(F)}throw new $("Connection closed while reading SMTP response",0,"READ","")}async function M(z){let G=await(await import("node:dns/promises")).resolveMx(z);if(G.length===0)throw new $(`No MX records for ${z}`,0,"MX","");return G.sort((F,W)=>F.priority-W.priority),G[0]?.exchange??z}export{T as k,P as l,D as m,S as n,H as o,J as p};
|
|
2
|
+
|
|
3
|
+
//# debugId=27963FE5C2BAA8F464756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/transports/smtp.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @module\n * SMTP transport — orchestrates socket adapter, MIME builder, and protocol logic.\n *\n * @example\n * ```ts\n * import { SMTPTransport } from \"sently/transports/smtp\";\n * import { NodeAdapter } from \"sently/adapters/node\";\n * import { createMailer } from \"sently/mailer\";\n *\n * const mailer = await createMailer({\n * transport: new SMTPTransport({\n * host: \"smtp.example.com\",\n * auth: { user: \"you@example.com\", pass: \"secret\" },\n * adapter: new NodeAdapter(),\n * }),\n * });\n * ```\n */\nimport { OAuth2Client } from \"../auth/oauth2.js\";\nimport { buildMIME, type MIMEBuildResult } from \"../core/mime.js\";\nimport type { SMTPResponse } from \"../core/smtp.js\";\nimport {\n accumulateResponse,\n assertResponse,\n computeCRAMMD5,\n encodeAuthLoginPass,\n encodeAuthLoginUser,\n encodeCommand,\n encodeLine,\n parseEHLO,\n parseResponse,\n SMTPError,\n selectAuthMethod,\n} from \"../core/smtp.js\";\nimport type {\n MailOptions,\n SendResult,\n SMTPConfig,\n SocketAdapter,\n Transport,\n VerifyResult,\n} from \"../core/types.js\";\nimport { resolveAttachments } from \"./resolve-attachments.js\";\n\n/**\n * SMTP transport orchestrating adapter, MIME builder, and protocol logic.\n */\nexport class SMTPTransport implements Transport {\n readonly provider = \"smtp\";\n\n /** Resolved SMTP configuration with defaults applied. */\n private readonly config: ResolvedSMTPConfig;\n /** Active socket adapter for the current session, if any. */\n private adapter: SocketAdapter | null = null;\n\n /** Creates an SMTP transport with the given configuration. */\n constructor(config: SMTPConfig) {\n this.config = resolveSMTPConfig(config);\n }\n\n /** Sends an email via SMTP using the configured adapter. */\n async send(options: MailOptions): Promise<SendResult> {\n const resolvedOptions = {\n ...options,\n attachments: await resolveAttachments(options.attachments),\n };\n const mime = await buildMIME(resolvedOptions, this.config.dkim);\n const adapter = await this.getAdapter();\n\n const host = this.config.direct\n ? await resolveMX(mime.envelope.from.split(\"@\")[1] ?? this.config.host)\n : this.config.host;\n\n await adapter.connect(host, this.config.port);\n this.adapter = adapter;\n\n try {\n await openSMTPSession(adapter, this.config);\n return await deliverSMTPMessage(adapter, mime);\n } finally {\n await closeSMTPSession(adapter);\n this.adapter = null;\n }\n }\n\n /** Verifies SMTP connectivity and authentication without sending mail. */\n async verify(): Promise<VerifyResult> {\n try {\n const adapter = await this.getAdapter();\n await adapter.connect(this.config.host, this.config.port);\n\n try {\n await openSMTPSession(adapter, this.config);\n return { ok: true, provider: \"smtp\" };\n } finally {\n await closeSMTPSession(adapter);\n }\n } catch (err) {\n return {\n ok: false,\n provider: \"smtp\",\n message: err instanceof Error ? err.message : String(err),\n };\n }\n }\n\n /** Closes the underlying socket adapter if connected. */\n async close(): Promise<void> {\n if (this.adapter) {\n await this.adapter.close();\n this.adapter = null;\n }\n }\n\n /** Returns the configured socket adapter or throws if none is set. */\n private async getAdapter(): Promise<SocketAdapter> {\n if (!this.config.adapter) {\n throw new SMTPError(\"No socket adapter configured\", 0, \"CONNECT\", \"\");\n }\n return this.config.adapter;\n }\n}\n\n/** Resolved SMTP transport configuration with defaults applied. */\nexport interface ResolvedSMTPConfig {\n /** SMTP server hostname. */\n host: string;\n /** SMTP port with secure/default applied. */\n port: number;\n /** Whether implicit TLS is used. */\n secure: boolean;\n /** Resolved authentication credentials, if any. */\n auth?: SMTPConfig[\"auth\"];\n /** Whether AUTH requires an encrypted connection. */\n requireTLS?: boolean;\n /** TLS options for STARTTLS and direct TLS. */\n tls?: SMTPConfig[\"tls\"];\n /** DKIM signing configuration. */\n dkim?: SMTPConfig[\"dkim\"];\n /** Socket connect timeout in milliseconds. */\n connectionTimeout?: number;\n /** Timeout waiting for the SMTP greeting in milliseconds. */\n greetingTimeout?: number;\n /** Idle socket timeout in milliseconds. */\n socketTimeout?: number;\n /** Direct-to-MX delivery mode. */\n direct?: boolean;\n /** Runtime socket adapter. */\n adapter?: SocketAdapter;\n}\n\n/** Apply defaults to SMTP configuration. */\nexport function resolveSMTPConfig(config: SMTPConfig): ResolvedSMTPConfig {\n const secure = config.secure ?? false;\n return {\n host: config.host,\n port: config.port ?? (secure ? 465 : 587),\n secure,\n ...(config.auth !== undefined ? { auth: config.auth } : {}),\n ...(config.requireTLS !== undefined ? { requireTLS: config.requireTLS } : {}),\n ...(config.dkim !== undefined ? { dkim: config.dkim } : {}),\n ...(config.tls !== undefined ? { tls: config.tls } : {}),\n ...(config.connectionTimeout !== undefined\n ? { connectionTimeout: config.connectionTimeout }\n : {}),\n ...(config.greetingTimeout !== undefined ? { greetingTimeout: config.greetingTimeout } : {}),\n ...(config.socketTimeout !== undefined ? { socketTimeout: config.socketTimeout } : {}),\n ...(config.direct !== undefined ? { direct: config.direct } : {}),\n ...(config.adapter !== undefined ? { adapter: config.adapter } : {}),\n };\n}\n\n/**\n * Connect greeting, EHLO, optional STARTTLS, and AUTH on an open adapter.\n */\nexport async function openSMTPSession(\n adapter: SocketAdapter,\n config: ResolvedSMTPConfig,\n): Promise<void> {\n const greeting = await readSMTPResponse(adapter);\n assertResponse(greeting, [220], \"greeting\");\n\n let capabilities = await ehlo(adapter, config.host);\n const supportsStartTls = capabilities.some((cap) => cap.toUpperCase() === \"STARTTLS\");\n if (!config.secure && !adapter.secure && supportsStartTls) {\n await sendRaw(adapter, encodeCommand({ type: \"STARTTLS\" }));\n const starttlsResp = await readSMTPResponse(adapter);\n assertResponse(starttlsResp, [220], \"STARTTLS\");\n await adapter.startTLS(config.tls);\n capabilities = await ehlo(adapter, config.host);\n }\n\n if (config.auth) {\n const tlsRequired = config.requireTLS ?? true;\n const encrypted = adapter.secure || config.secure;\n if (tlsRequired && !encrypted) {\n throw new SMTPError(\n \"Refusing to authenticate over unencrypted connection. \" +\n \"Set requireTLS: false to disable this check (not recommended).\",\n 0,\n \"AUTH\",\n \"\",\n );\n }\n await authenticate(adapter, config.auth, capabilities);\n }\n}\n\n/**\n * MAIL FROM, RCPT TO, and DATA for a built MIME message on an authenticated session.\n */\nexport async function deliverSMTPMessage(\n adapter: SocketAdapter,\n mime: MIMEBuildResult,\n): Promise<SendResult> {\n await sendCommand(adapter, { type: \"MAIL_FROM\", address: mime.envelope.from });\n const mailResp = await readSMTPResponse(adapter);\n assertResponse(mailResp, [250], \"MAIL FROM\");\n\n const accepted: string[] = [];\n const rejected: string[] = [];\n\n for (const recipient of mime.envelope.to) {\n await sendRaw(adapter, encodeCommand({ type: \"RCPT_TO\", address: recipient }));\n const rcptResp = await readSMTPResponse(adapter);\n if (rcptResp.isSuccess) {\n accepted.push(recipient);\n } else {\n rejected.push(recipient);\n }\n }\n\n await sendCommand(adapter, { type: \"DATA\" });\n const dataResp = await readSMTPResponse(adapter);\n assertResponse(dataResp, [354], \"DATA\");\n\n let finalResp: SMTPResponse;\n try {\n await sendRaw(adapter, encodeCommand({ type: \"DATA_BODY\", content: mime.raw }));\n finalResp = await readSMTPResponse(adapter);\n } catch (err) {\n await sendRaw(adapter, encodeCommand({ type: \"DATA_BODY\", content: mime.raw }));\n finalResp = await readSMTPResponse(adapter);\n if (finalResp.isError) {\n throw err;\n }\n }\n assertResponse(finalResp, [250], \"DATA end\");\n\n return {\n messageId: mime.messageId,\n accepted,\n rejected,\n response: finalResp.message,\n envelope: mime.envelope,\n };\n}\n\n/**\n * QUIT and close an SMTP session adapter.\n */\nexport async function closeSMTPSession(adapter: SocketAdapter): Promise<void> {\n try {\n await sendCommand(adapter, { type: \"QUIT\" });\n await readSMTPResponse(adapter);\n } catch {\n // ignore errors during shutdown\n } finally {\n await adapter.close();\n }\n}\n\nasync function ehlo(adapter: SocketAdapter, host: string): Promise<string[]> {\n await sendCommand(adapter, { type: \"EHLO\", domain: host });\n const response = await readSMTPResponse(adapter);\n assertResponse(response, [250], \"EHLO\");\n return parseEHLO(response);\n}\n\nasync function authenticate(\n adapter: SocketAdapter,\n auth: NonNullable<SMTPConfig[\"auth\"]>,\n capabilities: string[],\n): Promise<void> {\n if (auth.type === \"OAUTH2\" && auth.oauth2) {\n const client = new OAuth2Client(auth.oauth2);\n const xoauth2 = await client.buildXOAUTH2();\n await sendCommand(adapter, { type: \"AUTH_XOAUTH2\", xoauth2String: xoauth2 });\n let resp = await readSMTPResponse(adapter);\n if (resp.code === 334) {\n await sendRaw(adapter, encodeLine(\"\"));\n resp = await readSMTPResponse(adapter);\n }\n assertResponse(resp, [235], \"AUTH XOAUTH2\");\n return;\n }\n\n const method = auth.type ?? selectAuthMethod(capabilities);\n\n if (method === \"CRAM-MD5\") {\n const pass = requirePassword(auth, \"CRAM-MD5\");\n await sendCommand(adapter, { type: \"AUTH_CRAM_MD5_INIT\" });\n let resp = await readSMTPResponse(adapter);\n assertResponse(resp, [334], \"AUTH CRAM-MD5\");\n const challenge = resp.message.trim();\n const response = await computeCRAMMD5(challenge, auth.user, pass);\n await sendCommand(adapter, { type: \"AUTH_CRAM_MD5_RESPONSE\", response });\n resp = await readSMTPResponse(adapter);\n assertResponse(resp, [235], \"AUTH CRAM-MD5 response\");\n return;\n }\n\n if (method === \"PLAIN\") {\n const pass = requirePassword(auth, \"PLAIN\");\n await sendRaw(adapter, encodeCommand({ type: \"AUTH_PLAIN\", user: auth.user, pass }));\n const resp = await readSMTPResponse(adapter);\n assertResponse(resp, [235], \"AUTH PLAIN\");\n return;\n }\n\n const pass = requirePassword(auth, \"LOGIN\");\n await sendRaw(adapter, encodeCommand({ type: \"AUTH_LOGIN\", user: auth.user, pass }));\n let resp = await readSMTPResponse(adapter);\n assertResponse(resp, [334], \"AUTH LOGIN\");\n\n await sendRaw(adapter, encodeAuthLoginUser(auth.user));\n resp = await readSMTPResponse(adapter);\n assertResponse(resp, [334], \"AUTH LOGIN user\");\n\n await sendRaw(adapter, encodeAuthLoginPass(pass));\n resp = await readSMTPResponse(adapter);\n assertResponse(resp, [235], \"AUTH LOGIN pass\");\n}\n\nfunction requirePassword(auth: NonNullable<SMTPConfig[\"auth\"]>, method: string): string {\n if (!auth.pass) {\n throw new SMTPError(`Password required for ${method} authentication`, 0, `AUTH ${method}`, \"\");\n }\n return auth.pass;\n}\n\nasync function sendCommand(\n adapter: SocketAdapter,\n command: Parameters<typeof encodeCommand>[0],\n): Promise<void> {\n await sendRaw(adapter, encodeCommand(command));\n}\n\nasync function sendRaw(adapter: SocketAdapter, data: Uint8Array): Promise<void> {\n await adapter.write(data);\n}\n\n/** Reads and parses a complete SMTP response from the adapter. */\nasync function readSMTPResponse(adapter: SocketAdapter): Promise<SMTPResponse> {\n const chunks: Uint8Array[] = [];\n for await (const chunk of adapter.read()) {\n chunks.push(chunk);\n const complete = accumulateResponse(chunks);\n if (complete) {\n return parseResponse(complete);\n }\n }\n throw new SMTPError(\"Connection closed while reading SMTP response\", 0, \"READ\", \"\");\n}\n\nasync function resolveMX(domain: string): Promise<string> {\n const dns = await import(\"node:dns/promises\");\n const records = await dns.resolveMx(domain);\n if (records.length === 0) {\n throw new SMTPError(`No MX records for ${domain}`, 0, \"MX\", \"\");\n }\n records.sort((a: { priority: number }, b: { priority: number }) => a.priority - b.priority);\n return records[0]?.exchange ?? domain;\n}\n\nexport { SMTPError } from \"../core/smtp.js\";\n/** @internal Test helper for raw line writes. */\nexport { encodeLine, readSMTPResponse };\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "sSAgDO,AAAM,LAAmC,LACrC,GAAW,OAGH,OAET,QAAgC,KAGxC,WAAW,CAAC,EAAoB,CAC9B,KAAK,OAAS,EAAkB,CAAM,OAIlC,KAAI,CAAC,EAA2C,CACpD,IAAM,EAAkB,IACnB,EACH,YAAa,MAAM,EAAmB,EAAQ,WAAW,CAC3D,EACM,EAAO,MAAM,EAAU,EAAiB,KAAK,OAAO,IAAI,EACxD,EAAU,MAAM,KAAK,WAAW,EAEhC,EAAO,KAAK,OAAO,OACrB,MAAM,EAAU,EAAK,SAAS,KAAK,MAAM,GAAG,EAAE,IAAM,KAAK,OAAO,IAAI,EACpE,KAAK,OAAO,KAEhB,MAAM,EAAQ,QAAQ,EAAM,KAAK,OAAO,IAAI,EAC5C,KAAK,QAAU,EAEf,GAAI,CAEF,OADA,MAAM,EAAgB,EAAS,KAAK,MAAM,EACnC,MAAM,EAAmB,EAAS,CAAI,SAC7C,CACA,MAAM,EAAiB,CAAO,EAC9B,KAAK,QAAU,WAKb,OAAM,EAA0B,CACpC,GAAI,CACF,IAAM,EAAU,MAAM,KAAK,WAAW,EACtC,MAAM,EAAQ,QAAQ,KAAK,OAAO,KAAM,KAAK,OAAO,IAAI,EAExD,GAAI,CAEF,OADA,MAAM,EAAgB,EAAS,KAAK,MAAM,EACnC,CAAE,GAAI,GAAM,SAAU,MAAO,SACpC,CACA,MAAM,EAAiB,CAAO,GAEhC,MAAO,EAAK,CACZ,MAAO,CACL,GAAI,GACJ,SAAU,OACV,QAAS,aAAe,MAAQ,EAAI,QAAU,OAAO,CAAG,CAC1D,QAKE,MAAK,EAAkB,CAC3B,GAAI,KAAK,QACP,MAAM,KAAK,QAAQ,MAAM,EACzB,KAAK,QAAU,UAKL,WAAU,EAA2B,CACjD,GAAI,CAAC,KAAK,OAAO,QACf,MAAM,IAAI,EAAU,+BAAgC,EAAG,UAAW,EAAE,EAEtE,OAAO,KAAK,OAAO,QAEvB,CA+BO,SAAS,CAAiB,CAAC,EAAwC,CACxE,IAAM,EAAS,EAAO,QAAU,GAChC,MAAO,CACL,KAAM,EAAO,KACb,KAAM,EAAO,OAAS,EAAS,IAAM,KACrC,YACI,EAAO,OAAS,OAAY,CAAE,KAAM,EAAO,IAAK,EAAI,CAAC,KACrD,EAAO,aAAe,OAAY,CAAE,WAAY,EAAO,UAAW,EAAI,CAAC,KACvE,EAAO,OAAS,OAAY,CAAE,KAAM,EAAO,IAAK,EAAI,CAAC,KACrD,EAAO,MAAQ,OAAY,CAAE,IAAK,EAAO,GAAI,EAAI,CAAC,KAClD,EAAO,oBAAsB,OAC7B,CAAE,kBAAmB,EAAO,iBAAkB,EAC9C,CAAC,KACD,EAAO,kBAAoB,OAAY,CAAE,gBAAiB,EAAO,eAAgB,EAAI,CAAC,KACtF,EAAO,gBAAkB,OAAY,CAAE,cAAe,EAAO,aAAc,EAAI,CAAC,KAChF,EAAO,SAAW,OAAY,CAAE,OAAQ,EAAO,MAAO,EAAI,CAAC,KAC3D,EAAO,UAAY,OAAY,CAAE,QAAS,EAAO,OAAQ,EAAI,CAAC,CACpE,EAMF,eAAsB,CAAe,CACnC,EACA,EACe,CACf,IAAM,EAAW,MAAM,EAAiB,CAAO,EAC/C,EAAe,EAAU,CAAC,GAAG,EAAG,UAAU,EAE1C,IAAI,EAAe,MAAM,EAAK,EAAS,EAAO,IAAI,EAC5C,EAAmB,EAAa,KAAK,CAAC,IAAQ,EAAI,YAAY,IAAM,UAAU,EACpF,GAAI,CAAC,EAAO,QAAU,CAAC,EAAQ,QAAU,EAAkB,CACzD,MAAM,EAAQ,EAAS,EAAc,CAAE,KAAM,UAAW,CAAC,CAAC,EAC1D,IAAM,EAAe,MAAM,EAAiB,CAAO,EACnD,EAAe,EAAc,CAAC,GAAG,EAAG,UAAU,EAC9C,MAAM,EAAQ,SAAS,EAAO,GAAG,EACjC,EAAe,MAAM,EAAK,EAAS,EAAO,IAAI,EAGhD,GAAI,EAAO,KAAM,CACf,IAAM,EAAc,EAAO,YAAc,GACnC,EAAY,EAAQ,QAAU,EAAO,OAC3C,GAAI,GAAe,CAAC,EAClB,MAAM,IAAI,EACR,uHAEA,EACA,OACA,EACF,EAEF,MAAM,EAAa,EAAS,EAAO,KAAM,CAAY,GAOzD,eAAsB,CAAkB,CACtC,EACA,EACqB,CACrB,MAAM,EAAY,EAAS,CAAE,KAAM,YAAa,QAAS,EAAK,SAAS,IAAK,CAAC,EAC7E,IAAM,EAAW,MAAM,EAAiB,CAAO,EAC/C,EAAe,EAAU,CAAC,GAAG,EAAG,WAAW,EAE3C,IAAM,EAAqB,CAAC,EACtB,EAAqB,CAAC,EAE5B,QAAW,KAAa,EAAK,SAAS,GAGpC,GAFA,MAAM,EAAQ,EAAS,EAAc,CAAE,KAAM,UAAW,QAAS,CAAU,CAAC,CAAC,GAC5D,MAAM,EAAiB,CAAO,GAClC,UACX,EAAS,KAAK,CAAS,EAEvB,OAAS,KAAK,CAAS,EAI3B,MAAM,EAAY,EAAS,CAAE,KAAM,MAAO,CAAC,EAC3C,IAAM,EAAW,MAAM,EAAiB,CAAO,EAC/C,EAAe,EAAU,CAAC,GAAG,EAAG,MAAM,EAEtC,IAAI,EACJ,GAAI,CACF,MAAM,EAAQ,EAAS,EAAc,CAAE,KAAM,YAAa,QAAS,EAAK,GAAI,CAAC,CAAC,EAC9E,EAAY,MAAM,EAAiB,CAAO,EAC1C,MAAO,EAAK,CAGZ,GAFA,MAAM,EAAQ,EAAS,EAAc,CAAE,KAAM,YAAa,QAAS,EAAK,GAAI,CAAC,CAAC,EAC9E,EAAY,MAAM,EAAiB,CAAO,EACtC,EAAU,QACZ,MAAM,EAKV,OAFA,EAAe,EAAW,CAAC,GAAG,EAAG,UAAU,EAEpC,CACL,UAAW,EAAK,UAChB,WACA,WACA,SAAU,EAAU,QACpB,SAAU,EAAK,QACjB,EAMF,eAAsB,CAAgB,CAAC,EAAuC,CAC5E,GAAI,CACF,MAAM,EAAY,EAAS,CAAE,KAAM,MAAO,CAAC,EAC3C,MAAM,EAAiB,CAAO,EAC9B,KAAM,SAEN,CACA,MAAM,EAAQ,MAAM,GAIxB,eAAe,CAAI,CAAC,EAAwB,EAAiC,CAC3E,MAAM,EAAY,EAAS,CAAE,KAAM,OAAQ,OAAQ,CAAK,CAAC,EACzD,IAAM,EAAW,MAAM,EAAiB,CAAO,EAE/C,OADA,EAAe,EAAU,CAAC,GAAG,EAAG,MAAM,EAC/B,EAAU,CAAQ,EAG3B,eAAe,CAAY,CACzB,EACA,EACA,EACe,CACf,GAAI,EAAK,OAAS,UAAY,EAAK,OAAQ,CAEzC,IAAM,EAAU,MADD,IAAI,EAAa,EAAK,MAAM,EACd,aAAa,EAC1C,MAAM,EAAY,EAAS,CAAE,KAAM,eAAgB,cAAe,CAAQ,CAAC,EAC3E,IAAI,EAAO,MAAM,EAAiB,CAAO,EACzC,GAAI,EAAK,OAAS,IAChB,MAAM,EAAQ,EAAS,EAAW,EAAE,CAAC,EACrC,EAAO,MAAM,EAAiB,CAAO,EAEvC,EAAe,EAAM,CAAC,GAAG,EAAG,cAAc,EAC1C,OAGF,IAAM,EAAS,EAAK,MAAQ,EAAiB,CAAY,EAEzD,GAAI,IAAW,WAAY,CACzB,IAAM,EAAO,EAAgB,EAAM,UAAU,EAC7C,MAAM,EAAY,EAAS,CAAE,KAAM,oBAAqB,CAAC,EACzD,IAAI,EAAO,MAAM,EAAiB,CAAO,EACzC,EAAe,EAAM,CAAC,GAAG,EAAG,eAAe,EAC3C,IAAM,EAAY,EAAK,QAAQ,KAAK,EAC9B,EAAW,MAAM,EAAe,EAAW,EAAK,KAAM,CAAI,EAChE,MAAM,EAAY,EAAS,CAAE,KAAM,yBAA0B,UAAS,CAAC,EACvE,EAAO,MAAM,EAAiB,CAAO,EACrC,EAAe,EAAM,CAAC,GAAG,EAAG,wBAAwB,EACpD,OAGF,GAAI,IAAW,QAAS,CACtB,IAAM,EAAO,EAAgB,EAAM,OAAO,EAC1C,MAAM,EAAQ,EAAS,EAAc,CAAE,KAAM,aAAc,KAAM,EAAK,KAAM,MAAK,CAAC,CAAC,EACnF,IAAM,EAAO,MAAM,EAAiB,CAAO,EAC3C,EAAe,EAAM,CAAC,GAAG,EAAG,YAAY,EACxC,OAGF,IAAM,EAAO,EAAgB,EAAM,OAAO,EAC1C,MAAM,EAAQ,EAAS,EAAc,CAAE,KAAM,aAAc,KAAM,EAAK,KAAM,MAAK,CAAC,CAAC,EACnF,IAAI,EAAO,MAAM,EAAiB,CAAO,EACzC,EAAe,EAAM,CAAC,GAAG,EAAG,YAAY,EAExC,MAAM,EAAQ,EAAS,EAAoB,EAAK,IAAI,CAAC,EACrD,EAAO,MAAM,EAAiB,CAAO,EACrC,EAAe,EAAM,CAAC,GAAG,EAAG,iBAAiB,EAE7C,MAAM,EAAQ,EAAS,EAAoB,CAAI,CAAC,EAChD,EAAO,MAAM,EAAiB,CAAO,EACrC,EAAe,EAAM,CAAC,GAAG,EAAG,iBAAiB,EAG/C,SAAS,CAAe,CAAC,EAAuC,EAAwB,CACtF,GAAI,CAAC,EAAK,KACR,MAAM,IAAI,EAAU,yBAAyB,mBAAyB,EAAG,QAAQ,IAAU,EAAE,EAE/F,OAAO,EAAK,KAGd,eAAe,CAAW,CACxB,EACA,EACe,CACf,MAAM,EAAQ,EAAS,EAAc,CAAO,CAAC,EAG/C,eAAe,CAAO,CAAC,EAAwB,EAAiC,CAC9E,MAAM,EAAQ,MAAM,CAAI,EAI1B,eAAe,CAAgB,CAAC,EAA+C,CAC7E,IAAM,EAAuB,CAAC,EAC9B,cAAiB,KAAS,EAAQ,KAAK,EAAG,CACxC,EAAO,KAAK,CAAK,EACjB,IAAM,EAAW,EAAmB,CAAM,EAC1C,GAAI,EACF,OAAO,EAAc,CAAQ,EAGjC,MAAM,IAAI,EAAU,gDAAiD,EAAG,OAAQ,EAAE,EAGpF,eAAe,CAAS,CAAC,EAAiC,CAExD,IAAM,EAAU,MADJ,KAAa,8BACC,UAAU,CAAM,EAC1C,GAAI,EAAQ,SAAW,EACrB,MAAM,IAAI,EAAU,qBAAqB,IAAU,EAAG,KAAM,EAAE,EAGhE,OADA,EAAQ,KAAK,CAAC,EAAyB,IAA4B,EAAE,SAAW,EAAE,QAAQ,EACnF,EAAQ,IAAI,UAAY",
|
|
8
|
+
"debugId": "27963FE5C2BAA8F464756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{A as J}from"./chunk-9zkv5azm.js";import{X as Q}from"./chunk-6bkj4y4r.js";class K extends Error{attempts;constructor(w){super(`All ${w.length} transports failed`);this.name="FallbackError",this.attempts=w}}var X=J;function T(w){if(w instanceof Q&&w.sentlyCode==="SMTP_AUTH_FAILED")return!1;let q=w?.statusCode;if(typeof q==="number"){if(q===400||q===401||q===403)return!1;return!0}if(w?.code===535)return!1;return!0}class U{transports;provider="fallback";shouldFallback;onFallback;cooldownMs;now;unhealthyUntil;mailerOnFallback;constructor(w,q){this.transports=w;if(w.length===0)throw Error("FallbackTransport requires at least one transport");this.shouldFallback=q?.shouldFallback??T,this.onFallback=q?.onFallback,this.cooldownMs=q?.cooldownMs??0,this.now=q?.now??(()=>Date.now()),this.unhealthyUntil=q?.cooldownState??new Map}setMailerOnFallback(w){this.mailerOnFallback=w}isCoolingDown(w){let q=this.unhealthyUntil.get(w);if(q===void 0)return!1;if(this.now()>=q)return this.unhealthyUntil.delete(w),!1;return!0}markUnhealthy(w){if(this.cooldownMs>0)this.unhealthyUntil.set(w,this.now()+this.cooldownMs)}async send(w){let q=[],z=[];for(let G=0;G<this.transports.length;G++){let M=this.transports[G],D=J(M);if(this.isCoolingDown(D)){z.push({provider:D,error:Error(`Provider ${D} is in cooldown`)});continue}try{return{...await M.send(w),provider:D,providerIndex:G}}catch(H){q.push({provider:D,error:H}),this.markUnhealthy(D);let N=this.findNextTryIndex(G);if(N===-1)break;if(this.shouldFallback(H)===!1)throw H;let R=J(this.transports[N]);if(this.onFallback?.(G,H),this.mailerOnFallback!==void 0)await this.mailerOnFallback(D,R,H)}}let B=[...q,...z];if(B.length===0)throw new K([{provider:"fallback",error:Error("All providers are in cooldown")}]);throw new K(B)}findNextTryIndex(w){for(let q=w+1;q<this.transports.length;q++){let z=J(this.transports[q]);if(!this.isCoolingDown(z))return q}return-1}async verify(){let q=(await this.verifyAll()).providers.find((z)=>z.ok);if(q!==void 0)return{ok:!0,provider:q.provider,...q.message!==void 0?{message:q.message}:{}};return{ok:!1,provider:"fallback",message:"no transport verified"}}async verifyAll(){let w=[];for(let q of this.transports){let z=J(q);if(q.verify===void 0){w.push({provider:z,ok:!1,message:"no verify() method"});continue}try{let B=await q.verify();w.push({provider:z,ok:B.ok,...B.message!==void 0?{message:B.message}:{}})}catch(B){w.push({provider:z,ok:!1,message:B instanceof Error?B.message:String(B)})}}return{ok:w.some((q)=>q.ok),providers:w}}async close(){await Promise.allSettled(this.transports.map((w)=>w.close?.()))}}
|
|
2
|
+
export{K as h,X as i,U as j};
|
|
3
|
+
|
|
4
|
+
//# debugId=3C85F02B08FD5F1E64756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/transports/fallback.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @module\n * FallbackTransport — routes through an ordered list of transports,\n * advancing to the next when the current one fails. Use for provider\n * failover: if your primary provider has an outage, the next takes over.\n * Composes with RetryTransport — wrap each entry in RetryTransport to\n * retry within a provider before failing over to the next.\n *\n * @example\n * ```ts\n * import { FallbackTransport } from \"sently/transports/fallback\";\n * import { RetryTransport } from \"sently/transports/retry\";\n * import { ResendTransport } from \"sently/transports/resend\";\n * import { SESTransport } from \"sently/transports/ses\";\n *\n * const transport = new FallbackTransport(\n * [\n * new RetryTransport(new ResendTransport({ apiKey })),\n * new RetryTransport(new SESTransport({ accessKeyId, secretAccessKey })),\n * ],\n * { cooldownMs: 300_000 },\n * );\n * ```\n */\nimport { SentlyError } from \"../core/errors.js\";\nimport { getProviderLabel } from \"../core/provider-label.js\";\nimport type { MailOptions, SendResult, Transport, VerifyResult } from \"../core/types.js\";\n\n/** One failed provider attempt collected during failover. */\nexport interface FallbackAttempt {\n /** Provider label (e.g. `\"resend\"`, `\"ses\"`). */\n provider: string;\n /** Error thrown by that transport. */\n error: unknown;\n}\n\n/** Per-provider result from {@link FallbackTransport.verifyAll}. */\nexport interface FallbackProviderVerifyResult {\n /** Provider label. */\n provider: string;\n /** Whether this provider verified successfully. */\n ok: boolean;\n /** Human-readable status from the provider verify call. */\n message?: string;\n}\n\n/** Result of verifying every transport in a fallback chain. */\nexport interface FallbackVerifyAllResult {\n /** True when at least one provider in the chain verified successfully. */\n ok: boolean;\n /** Per-provider verify results in chain order. */\n providers: FallbackProviderVerifyResult[];\n}\n\n/** Options for {@link FallbackTransport} failover behavior. */\nexport interface FallbackOptions {\n /**\n * Decide whether a given error should trigger failover to the next\n * transport. Default: fail over on everything EXCEPT permanent client\n * errors (HTTP 400/401/403, SMTP permanent 5xx auth failures), since\n * those will fail identically on every provider.\n */\n shouldFallback?: (error: unknown) => boolean;\n /** Optional callback fired before advancing to the next transport. */\n onFallback?: (failedIndex: number, error: unknown) => void;\n /**\n * After a failed send, skip this provider until the cooldown expires (ms).\n * Prevents hammering a failing provider during an outage.\n */\n cooldownMs?: number;\n /** Injectable clock for cooldown and testing. Default: `Date.now`. */\n now?: () => number;\n /**\n * Shared cooldown state across fallback instances (e.g. {@link WeightedFallbackTransport}).\n * @internal\n */\n cooldownState?: Map<string, number>;\n}\n\n/**\n * Thrown when every transport in the chain fails.\n * {@link FallbackAttempt} entries record which provider failed and why.\n */\nexport class FallbackError extends Error {\n /** All failed attempts in order — which provider failed and why. */\n readonly attempts: FallbackAttempt[];\n\n /** Creates an error summarizing every failed transport attempt. */\n constructor(attempts: FallbackAttempt[]) {\n super(`All ${attempts.length} transports failed`);\n this.name = \"FallbackError\";\n this.attempts = attempts;\n }\n}\n\n/** @deprecated Use {@link getProviderLabel} from `sently/core` patterns. */\nexport const inferProviderLabel = getProviderLabel;\n\nfunction defaultShouldFallback(error: unknown): boolean {\n if (error instanceof SentlyError && error.sentlyCode === \"SMTP_AUTH_FAILED\") {\n return false;\n }\n\n const status = (error as { statusCode?: number })?.statusCode;\n if (typeof status === \"number\") {\n if (status === 400 || status === 401 || status === 403) {\n return false;\n }\n return true;\n }\n\n const code = (error as { code?: number })?.code;\n if (code === 535) {\n return false;\n }\n\n return true;\n}\n\ntype MailerOnFallback = (\n failedProvider: string,\n nextProvider: string,\n error: unknown,\n) => void | Promise<void>;\n\n/**\n * Decorator transport that fails over through an ordered list of transports.\n */\nexport class FallbackTransport implements Transport {\n readonly provider = \"fallback\";\n\n private readonly shouldFallback: (error: unknown) => boolean;\n private readonly onFallback?: FallbackOptions[\"onFallback\"];\n private readonly cooldownMs: number;\n private readonly now: () => number;\n private readonly unhealthyUntil: Map<string, number>;\n private mailerOnFallback: MailerOnFallback | undefined;\n\n /**\n * Creates a failover transport over an ordered list of backends.\n * @throws {Error} When the transports array is empty.\n */\n constructor(\n private readonly transports: Transport[],\n options?: FallbackOptions,\n ) {\n if (transports.length === 0) {\n throw new Error(\"FallbackTransport requires at least one transport\");\n }\n this.shouldFallback = options?.shouldFallback ?? defaultShouldFallback;\n this.onFallback = options?.onFallback;\n this.cooldownMs = options?.cooldownMs ?? 0;\n this.now = options?.now ?? (() => Date.now());\n this.unhealthyUntil = options?.cooldownState ?? new Map<string, number>();\n }\n\n /**\n * Register a per-send onFallback callback from mailer lifecycle hooks.\n * @internal Used by {@link MailerImpl}; cleared after each send.\n */\n setMailerOnFallback(callback: MailerOnFallback | undefined): void {\n this.mailerOnFallback = callback;\n }\n\n private isCoolingDown(provider: string): boolean {\n const until = this.unhealthyUntil.get(provider);\n if (until === undefined) {\n return false;\n }\n if (this.now() >= until) {\n this.unhealthyUntil.delete(provider);\n return false;\n }\n return true;\n }\n\n private markUnhealthy(provider: string): void {\n if (this.cooldownMs > 0) {\n this.unhealthyUntil.set(provider, this.now() + this.cooldownMs);\n }\n }\n\n /** Sends through transports in order until one succeeds. */\n async send(message: MailOptions): Promise<SendResult> {\n const attempts: FallbackAttempt[] = [];\n const skipped: FallbackAttempt[] = [];\n\n for (let i = 0; i < this.transports.length; i++) {\n const transport = this.transports[i] as Transport;\n const provider = getProviderLabel(transport);\n\n if (this.isCoolingDown(provider)) {\n skipped.push({\n provider,\n error: new Error(`Provider ${provider} is in cooldown`),\n });\n continue;\n }\n\n try {\n const result = await transport.send(message);\n return {\n ...result,\n provider,\n providerIndex: i,\n };\n } catch (err) {\n attempts.push({ provider, error: err });\n this.markUnhealthy(provider);\n\n const nextIndex = this.findNextTryIndex(i);\n if (nextIndex === -1) {\n break;\n }\n if (this.shouldFallback(err) === false) {\n throw err;\n }\n\n const nextProvider = getProviderLabel(this.transports[nextIndex] as Transport);\n this.onFallback?.(i, err);\n if (this.mailerOnFallback !== undefined) {\n await this.mailerOnFallback(provider, nextProvider, err);\n }\n }\n }\n\n const allAttempts = [...attempts, ...skipped];\n if (allAttempts.length === 0) {\n throw new FallbackError([\n { provider: \"fallback\", error: new Error(\"All providers are in cooldown\") },\n ]);\n }\n\n throw new FallbackError(allAttempts);\n }\n\n private findNextTryIndex(currentIndex: number): number {\n for (let j = currentIndex + 1; j < this.transports.length; j++) {\n const provider = getProviderLabel(this.transports[j] as Transport);\n if (!this.isCoolingDown(provider)) {\n return j;\n }\n }\n return -1;\n }\n\n /**\n * Returns the result of the first transport whose verify() returns `ok: true`.\n * Use {@link verifyAll} for full-chain visibility.\n */\n async verify(): Promise<VerifyResult> {\n const all = await this.verifyAll();\n const firstOk = all.providers.find((entry) => entry.ok);\n if (firstOk !== undefined) {\n return {\n ok: true,\n provider: firstOk.provider,\n ...(firstOk.message !== undefined ? { message: firstOk.message } : {}),\n };\n }\n\n return {\n ok: false,\n provider: \"fallback\",\n message: \"no transport verified\",\n };\n }\n\n /** Verifies every transport in the chain and returns per-provider results. */\n async verifyAll(): Promise<FallbackVerifyAllResult> {\n const providers: FallbackProviderVerifyResult[] = [];\n\n for (const transport of this.transports) {\n const provider = getProviderLabel(transport);\n if (transport.verify === undefined) {\n providers.push({ provider, ok: false, message: \"no verify() method\" });\n continue;\n }\n\n try {\n const result = await transport.verify();\n providers.push({\n provider,\n ok: result.ok,\n ...(result.message !== undefined ? { message: result.message } : {}),\n });\n } catch (err) {\n providers.push({\n provider,\n ok: false,\n message: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n return {\n ok: providers.some((entry) => entry.ok),\n providers,\n };\n }\n\n /** Calls close() on every transport; one failure does not block the others. */\n async close(): Promise<void> {\n await Promise.allSettled(this.transports.map((transport) => transport.close?.()));\n }\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "3C85F02B08FD5F1E64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{V}from"./chunk-5scdgffb.js";function W(q){for(let E=0;E<q.length;E++){let z=q.charCodeAt(E);if(z<=31||z===127||z===8232||z===8233)return z}return-1}function K(q,E="address"){let z=W(q);if(z!==-1){let G=z.toString(16).padStart(2,"0");throw Error(`Email ${E} contains a forbidden control character (0x${G}). CR, LF, NUL, and other control characters are not allowed.`)}}function X(q){if(Array.isArray(q))return q.flatMap((z)=>X(z));if(typeof q==="object"){if(K(q.address,"address"),q.name!==void 0)K(q.name,"display name");return[{...q}]}K(q,"address");let E=q.trim();if(!E)return[];return Y(E).map(Z)}function $(q){if(K(q.address,"address"),q.name)return K(q.name,"display name"),`${V(q.name)} <${q.address}>`;return q.address}function w(q){return X(q).map((E)=>E.address)}function x(q){if(W(q)!==-1)return!1;return/^[^\s@<>]+@[^\s@<>]+\.[^\s@<>]+$/.test(q)}function Y(q){let E=[],z="",G=!1,J=!1;for(let O=0;O<q.length;O++){let I=q[O]??"";if(I==='"'&&q[O-1]!=="\\"){G=!G,z+=I;continue}if(I==="<"&&!G){J=!0,z+=I;continue}if(I===">"&&!G){J=!1,z+=I;continue}if(I===","&&!G&&!J){if(z.trim())E.push(z.trim());z="";continue}z+=I}if(z.trim())E.push(z.trim());return E}function Z(q){let E=q.trim(),z=E.match(/^(?:"([^"]*)"|([^<]*?))\s*<([^>]+)>$/);if(z){let G=(z[1]??z[2]??"").trim(),J=(z[3]??"").trim();if(G)return{name:G,address:J};return{address:J}}if(E.startsWith('"')&&E.endsWith('"'))return{address:E.slice(1,-1)};return{address:E}}
|
|
2
|
+
export{K as C,X as D,$ as E,w as F,x as G};
|
|
3
|
+
|
|
4
|
+
//# debugId=80DC74FA55E2FA5964756E2164756E21
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"// src/core/address.ts\nimport { encodeHeader } from \"./base64.js\";\nimport type { Address, AddressInput } from \"./types.js\";\n\n/**\n * Find the first forbidden control character in a string.\n *\n * Forbidden characters must never appear in an email address or display name,\n * because each enables a distinct attack:\n * - CR (0x0D) / LF (0x0A): email header injection and SMTP command injection\n * - NUL (0x00): C-string truncation / parser confusion in downstream agents\n * - other C0 controls (0x01–0x1F), DEL (0x7F): header/parser confusion\n * - U+2028 / U+2029: line separators that some parsers treat as newlines\n *\n * @returns The char code of the first forbidden character, or -1 if none.\n */\nfunction findForbiddenChar(value: string): number {\n for (let i = 0; i < value.length; i++) {\n const code = value.charCodeAt(i);\n if (code <= 0x1f || code === 0x7f || code === 0x2028 || code === 0x2029) {\n return code;\n }\n }\n return -1;\n}\n\n/**\n * Assert that a raw address or display-name value contains no forbidden\n * control characters. Throws immediately (fail closed) — the library never\n * attempts to strip or rewrite hostile input into an accepted value.\n *\n * The check is intentionally performed on the RAW input, before any trimming\n * or normalization, so hostile values are rejected rather than repaired.\n *\n * @param value - The raw, untransformed string to validate.\n * @param label - Field label used in the error message (e.g. \"address\").\n * @throws {Error} If the value contains any forbidden control character.\n */\nexport function assertSafeAddress(value: string, label = \"address\"): void {\n const code = findForbiddenChar(value);\n if (code !== -1) {\n const hex = code.toString(16).padStart(2, \"0\");\n throw new Error(\n `Email ${label} contains a forbidden control character (0x${hex}). ` +\n \"CR, LF, NUL, and other control characters are not allowed.\",\n );\n }\n}\n\n/**\n * Normalize any AddressInput form into Address[].\n *\n * Every address and display name is validated against control-character\n * injection before any transformation. This is the single chokepoint shared\n * by all transports and address fields (From, To, Cc, Bcc, Reply-To), so the\n * protection is uniform and secure by default.\n *\n * @throws {Error} If any address or name contains a forbidden control character.\n */\nexport function parseAddresses(input: AddressInput): Address[] {\n if (Array.isArray(input)) {\n return input.flatMap((item) => parseAddresses(item));\n }\n\n if (typeof input === \"object\") {\n assertSafeAddress(input.address, \"address\");\n if (input.name !== undefined) {\n assertSafeAddress(input.name, \"display name\");\n }\n return [{ ...input }];\n }\n\n // Validate the raw input string before splitting or trimming so injected\n // newlines cannot hide inside a multi-address list.\n assertSafeAddress(input, \"address\");\n\n const trimmed = input.trim();\n if (!trimmed) {\n return [];\n }\n\n return splitAddressList(trimmed).map(parseSingleAddress);\n}\n\n/**\n * Format an Address for SMTP envelope commands (MAIL FROM / RCPT TO).\n */\nexport function toEnvelope(address: Address): string {\n return address.address;\n}\n\n/**\n * Format an Address for use in a MIME header (From, To, CC, etc.).\n *\n * Re-validates the address and name at render time so a header can never be\n * emitted with an embedded control character, even if the {@link Address} was\n * constructed without going through {@link parseAddresses}.\n *\n * @throws {Error} If the address or name contains a forbidden control character.\n */\nexport function toMIMEHeader(address: Address): string {\n assertSafeAddress(address.address, \"address\");\n if (address.name) {\n assertSafeAddress(address.name, \"display name\");\n const name = encodeHeader(address.name);\n return `${name} <${address.address}>`;\n }\n return address.address;\n}\n\n/**\n * Extract plain email strings from any AddressInput.\n */\nexport function extractEmails(input: AddressInput): string[] {\n return parseAddresses(input).map((addr) => addr.address);\n}\n\n/**\n * Basic email format validation (format only, no DNS lookup).\n *\n * Rejects any control character (including CR, LF, tab, and NUL) before\n * applying the structural check, so a \"valid\" result is always safe to place\n * into a header or SMTP command.\n */\nexport function isValidEmail(email: string): boolean {\n if (findForbiddenChar(email) !== -1) return false;\n return /^[^\\s@<>]+@[^\\s@<>]+\\.[^\\s@<>]+$/.test(email);\n}\n\nfunction splitAddressList(input: string): string[] {\n const parts: string[] = [];\n let current = \"\";\n let inQuotes = false;\n let inAngle = false;\n\n for (let i = 0; i < input.length; i++) {\n const char = input[i] ?? \"\";\n if (char === '\"' && input[i - 1] !== \"\\\\\") {\n inQuotes = !inQuotes;\n current += char;\n continue;\n }\n if (char === \"<\" && !inQuotes) {\n inAngle = true;\n current += char;\n continue;\n }\n if (char === \">\" && !inQuotes) {\n inAngle = false;\n current += char;\n continue;\n }\n if (char === \",\" && !inQuotes && !inAngle) {\n if (current.trim()) {\n parts.push(current.trim());\n }\n current = \"\";\n continue;\n }\n current += char;\n }\n\n if (current.trim()) {\n parts.push(current.trim());\n }\n\n return parts;\n}\n\nfunction parseSingleAddress(input: string): Address {\n const trimmed = input.trim();\n\n const angleMatch = trimmed.match(/^(?:\"([^\"]*)\"|([^<]*?))\\s*<([^>]+)>$/);\n if (angleMatch) {\n const name = (angleMatch[1] ?? angleMatch[2] ?? \"\").trim();\n const address = (angleMatch[3] ?? \"\").trim();\n if (name) {\n return { name, address };\n }\n return { address };\n }\n\n if (trimmed.startsWith('\"') && trimmed.endsWith('\"')) {\n return { address: trimmed.slice(1, -1) };\n }\n\n return { address: trimmed };\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "mCAgBA,SAAS,CAAiB,CAAC,EAAuB,CAChD,QAAS,EAAI,EAAG,EAAI,EAAM,OAAQ,IAAK,CACrC,IAAM,EAAO,EAAM,WAAW,CAAC,EAC/B,GAAI,GAAQ,IAAQ,IAAS,KAAQ,IAAS,MAAU,IAAS,KAC/D,OAAO,EAGX,MAAO,GAeF,SAAS,CAAiB,CAAC,EAAe,EAAQ,UAAiB,CACxE,IAAM,EAAO,EAAkB,CAAK,EACpC,GAAI,IAAS,GAAI,CACf,IAAM,EAAM,EAAK,SAAS,EAAE,EAAE,SAAS,EAAG,GAAG,EAC7C,MAAU,MACR,SAAS,+CAAmD,gEAE9D,GAcG,SAAS,CAAc,CAAC,EAAgC,CAC7D,GAAI,MAAM,QAAQ,CAAK,EACrB,OAAO,EAAM,QAAQ,CAAC,IAAS,EAAe,CAAI,CAAC,EAGrD,GAAI,OAAO,IAAU,SAAU,CAE7B,GADA,EAAkB,EAAM,QAAS,SAAS,EACtC,EAAM,OAAS,OACjB,EAAkB,EAAM,KAAM,cAAc,EAE9C,MAAO,CAAC,IAAK,CAAM,CAAC,EAKtB,EAAkB,EAAO,SAAS,EAElC,IAAM,EAAU,EAAM,KAAK,EAC3B,GAAI,CAAC,EACH,MAAO,CAAC,EAGV,OAAO,EAAiB,CAAO,EAAE,IAAI,CAAkB,EAmBlD,SAAS,CAAY,CAAC,EAA0B,CAErD,GADA,EAAkB,EAAQ,QAAS,SAAS,EACxC,EAAQ,KAGV,OAFA,EAAkB,EAAQ,KAAM,cAAc,EAEvC,GADM,EAAa,EAAQ,IAAI,MACnB,EAAQ,WAE7B,OAAO,EAAQ,QAMV,SAAS,CAAa,CAAC,EAA+B,CAC3D,OAAO,EAAe,CAAK,EAAE,IAAI,CAAC,IAAS,EAAK,OAAO,EAUlD,SAAS,CAAY,CAAC,EAAwB,CACnD,GAAI,EAAkB,CAAK,IAAM,GAAI,MAAO,GAC5C,MAAO,mCAAmC,KAAK,CAAK,EAGtD,SAAS,CAAgB,CAAC,EAAyB,CACjD,IAAM,EAAkB,CAAC,EACrB,EAAU,GACV,EAAW,GACX,EAAU,GAEd,QAAS,EAAI,EAAG,EAAI,EAAM,OAAQ,IAAK,CACrC,IAAM,EAAO,EAAM,IAAM,GACzB,GAAI,IAAS,KAAO,EAAM,EAAI,KAAO,KAAM,CACzC,EAAW,CAAC,EACZ,GAAW,EACX,SAEF,GAAI,IAAS,KAAO,CAAC,EAAU,CAC7B,EAAU,GACV,GAAW,EACX,SAEF,GAAI,IAAS,KAAO,CAAC,EAAU,CAC7B,EAAU,GACV,GAAW,EACX,SAEF,GAAI,IAAS,KAAO,CAAC,GAAY,CAAC,EAAS,CACzC,GAAI,EAAQ,KAAK,EACf,EAAM,KAAK,EAAQ,KAAK,CAAC,EAE3B,EAAU,GACV,SAEF,GAAW,EAGb,GAAI,EAAQ,KAAK,EACf,EAAM,KAAK,EAAQ,KAAK,CAAC,EAG3B,OAAO,EAGT,SAAS,CAAkB,CAAC,EAAwB,CAClD,IAAM,EAAU,EAAM,KAAK,EAErB,EAAa,EAAQ,MAAM,sCAAsC,EACvE,GAAI,EAAY,CACd,IAAM,GAAQ,EAAW,IAAM,EAAW,IAAM,IAAI,KAAK,EACnD,GAAW,EAAW,IAAM,IAAI,KAAK,EAC3C,GAAI,EACF,MAAO,CAAE,OAAM,SAAQ,EAEzB,MAAO,CAAE,SAAQ,EAGnB,GAAI,EAAQ,WAAW,GAAG,GAAK,EAAQ,SAAS,GAAG,EACjD,MAAO,CAAE,QAAS,EAAQ,MAAM,EAAG,EAAE,CAAE,EAGzC,MAAO,CAAE,QAAS,CAAQ",
|
|
8
|
+
"debugId": "80DC74FA55E2FA5964756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{
|
|
2
|
-
export{K as
|
|
1
|
+
import{_ as C}from"./chunk-nbmq8ejv.js";async function K(D,y){let E=D??[],j=[];for(let b of E){if(b.content instanceof Uint8Array){j.push(b);continue}if(b.path){let z;try{z=await import("node:fs/promises")}catch{throw Error("attachment.path is not supported on this runtime — use attachment.content (Uint8Array) instead")}if(y?.basePath){let{resolve:A,sep:H}=await import("node:path"),k=A(b.path),w=A(y.basePath);if(!(k===w||k.startsWith(w+H)))throw Error(`[sently] Attachment path "${k}" escapes basePath "${w}". Use absolute paths within the allowed directory.`)}let F=await z.readFile(b.path),{path:I,...G}=b;j.push({...G,content:new Uint8Array(F)});continue}if(typeof b.content==="string"){j.push(b);continue}j.push(b)}return j}
|
|
2
|
+
export{K as v};
|
|
3
3
|
|
|
4
|
-
//# debugId=
|
|
4
|
+
//# debugId=3442FCDDB36F8C9264756E2164756E21
|
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
"import type { Attachment } from \"../core/types.js\";\n\n/** Options for {@link resolveAttachments}. */\nexport interface ResolveAttachmentsOptions {\n /**\n * If set, attachment paths must resolve within this directory.\n * Prevents path traversal via `..` segments and sibling-directory\n * prefix matches. Opt-in only.\n *\n * Note: this check uses `node:path` `resolve()`, which does NOT\n * dereference symlinks. A symlink located inside `basePath` that\n * points outside of it will pass this check. If symlink traversal is\n * a concern, resolve paths with `fs.realpath()` before passing them in.\n */\n basePath?: string;\n}\n\n/**\n * Resolve attachment.path to in-memory Uint8Array content.\n * @throws When attachment.path is used on runtimes without node:fs/promises\n */\nexport async function resolveAttachments(\n attachments: Attachment[] | undefined,\n options?: ResolveAttachmentsOptions,\n): Promise<Attachment[]> {\n const list = attachments ?? [];\n const resolved: Attachment[] = [];\n\n for (const attachment of list) {\n if (attachment.content instanceof Uint8Array) {\n resolved.push(attachment);\n continue;\n }\n\n if (attachment.path) {\n let fs: typeof import(\"node:fs/promises\");\n try {\n fs = await import(\"node:fs/promises\");\n } catch {\n throw new Error(\n \"attachment.path is not supported on this runtime — use attachment.content (Uint8Array) instead\",\n );\n }\n\n if (options?.basePath) {\n const { resolve, sep } = await import(\"node:path\");\n const resolvedPath = resolve(attachment.path);\n const resolvedBase = resolve(options.basePath);\n // startsWith alone is vulnerable: \"/var/data-secret\" passes \"/var/data\".\n // Require an exact match or a trailing path separator.\n const isWithin =\n resolvedPath === resolvedBase || resolvedPath.startsWith(resolvedBase + sep);\n if (!isWithin) {\n throw new Error(\n `[sently] Attachment path \"${resolvedPath}\" escapes basePath \"${resolvedBase}\". ` +\n \"Use absolute paths within the allowed directory.\",\n );\n }\n }\n\n const data = await fs.readFile(attachment.path);\n const { path: _path, ...rest } = attachment;\n resolved.push({ ...rest, content: new Uint8Array(data) });\n continue;\n }\n\n if (typeof attachment.content === \"string\") {\n resolved.push(attachment);\n continue;\n }\n\n resolved.push(attachment);\n }\n\n return resolved;\n}\n"
|
|
6
6
|
],
|
|
7
7
|
"mappings": "wCAqBA,eAAsB,CAAkB,CACtC,EACA,EACuB,CACvB,IAAM,EAAO,GAAe,CAAC,EACvB,EAAyB,CAAC,EAEhC,QAAW,KAAc,EAAM,CAC7B,GAAI,EAAW,mBAAmB,WAAY,CAC5C,EAAS,KAAK,CAAU,EACxB,SAGF,GAAI,EAAW,KAAM,CACnB,IAAI,EACJ,GAAI,CACF,EAAK,KAAa,4BAClB,KAAM,CACN,MAAU,MACR,gGACF,EAGF,GAAI,GAAS,SAAU,CACrB,IAAQ,UAAS,OAAQ,KAAa,qBAChC,EAAe,EAAQ,EAAW,IAAI,EACtC,EAAe,EAAQ,EAAQ,QAAQ,EAK7C,GAAI,EADF,IAAiB,GAAgB,EAAa,WAAW,EAAe,CAAG,GAE3E,MAAU,MACR,6BAA6B,wBAAmC,sDAElE,EAIJ,IAAM,EAAO,MAAM,EAAG,SAAS,EAAW,IAAI,GACtC,KAAM,KAAU,GAAS,EACjC,EAAS,KAAK,IAAK,EAAM,QAAS,IAAI,WAAW,CAAI,CAAE,CAAC,EACxD,SAGF,GAAI,OAAO,EAAW,UAAY,SAAU,CAC1C,EAAS,KAAK,CAAU,EACxB,SAGF,EAAS,KAAK,CAAU,EAG1B,OAAO",
|
|
8
|
-
"debugId": "
|
|
8
|
+
"debugId": "3442FCDDB36F8C9264756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{
|
|
2
|
-
export{Q as
|
|
1
|
+
import{T as D,W as J}from"./chunk-5scdgffb.js";var Q="https://oauth2.googleapis.com/token",$="https://login.microsoftonline.com/common/oauth2/v2.0/token",V=30000;class W{config;cachedToken=null;expiresAt=0;refreshPromise=null;constructor(m){if(this.config=m,m.accessToken)this.cachedToken=m.accessToken,this.expiresAt=Date.now()+3600000}async getAccessToken(){if(this.config.getToken)return this.config.getToken();if(this.cachedToken&&Date.now()<this.expiresAt-V)return this.cachedToken;if(!this.refreshPromise)this.refreshPromise=this.refreshAccessToken().finally(()=>{this.refreshPromise=null});return this.refreshPromise}async refreshAccessToken(){if(this.config.getToken){let v=await this.config.getToken();return this.cachedToken=v,this.expiresAt=Date.now()+3600000,v}let m=this.config.tokenUrl??Q,z=new URLSearchParams({client_id:this.config.clientId,client_secret:this.config.clientSecret,refresh_token:this.config.refreshToken,grant_type:"refresh_token"}),q=await fetch(m,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:z.toString()});if(!q.ok){let v=await q.text();throw Error(`OAuth2 token refresh failed (${q.status}): ${v}`)}let C=await q.json();return this.cachedToken=C.access_token,this.expiresAt=Date.now()+C.expires_in*1000,C.access_token}async buildXOAUTH2(){let m=await this.getAccessToken(),z=`user=${this.config.user}\x01auth=Bearer ${m}\x01\x01`;return D(J(z)).replace(/\r\n/g,"")}}
|
|
2
|
+
export{Q as q,$ as r,W as s};
|
|
3
3
|
|
|
4
|
-
//# debugId=
|
|
4
|
+
//# debugId=81D6302F3F754CF964756E2164756E21
|
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
"/**\n * @module\n * OAuth2 / XOAUTH2 authentication for SMTP.\n * Supports Gmail, Microsoft 365, and custom OAuth2 providers.\n *\n * @example\n * ```ts\n * import { OAuth2Client } from \"sently/auth/oauth2\";\n * const client = new OAuth2Client({\n * user: \"me@gmail.com\",\n * clientId: \"...\",\n * clientSecret: \"...\",\n * refreshToken: \"...\",\n * });\n * const token = await client.getAccessToken();\n * ```\n */\nimport { encodeBase64, encodeUtf8 } from \"../core/base64.js\";\nimport type { OAuth2Config } from \"../core/types.js\";\n\n/** Default Google OAuth2 token endpoint. */\nexport const GOOGLE_TOKEN_URL = \"https://oauth2.googleapis.com/token\";\n\n/** Microsoft OAuth2 token endpoint (common tenant). */\nexport const MICROSOFT_TOKEN_URL = \"https://login.microsoftonline.com/common/oauth2/v2.0/token\";\n\n/** OAuth2 token endpoint response shape. */\nexport interface TokenResponse {\n /** Bearer access token for API or SMTP XOAUTH2. */\n access_token: string;\n /** Token lifetime in seconds. */\n expires_in: number;\n /** Token type (typically `\"Bearer\"`). */\n token_type: string;\n}\n\nconst EXPIRY_BUFFER_MS = 30_000;\n\n/**\n * OAuth2 client with in-memory token cache and automatic refresh.\n */\nexport class OAuth2Client {\n /** OAuth2 configuration supplied at construction. */\n private readonly config: OAuth2Config;\n /** Cached access token, or null before the first fetch. */\n private cachedToken: string | null = null;\n /** Expiry timestamp (ms) for the cached access token. */\n private expiresAt = 0;\n /** In-flight refresh promise used to deduplicate concurrent refreshes. */\n private refreshPromise: Promise<string> | null = null;\n\n /** Creates an OAuth2 client from configuration. */\n constructor(config: OAuth2Config) {\n this.config = config;\n if (config.accessToken) {\n this.cachedToken = config.accessToken;\n this.expiresAt = Date.now() + 3_600_000;\n }\n }\n\n /**\n * Get a valid access token (cached when still valid, refreshed otherwise).\n */\n async getAccessToken(): Promise<string> {\n if (this.config.getToken) {\n return this.config.getToken();\n }\n\n if (this.cachedToken && Date.now() < this.expiresAt - EXPIRY_BUFFER_MS) {\n return this.cachedToken;\n }\n\n if (!this.refreshPromise) {\n this.refreshPromise = this.refreshAccessToken().finally(() => {\n this.refreshPromise = null;\n });\n }\n return this.refreshPromise;\n }\n\n /**\n * Force-refresh the access token regardless of expiry.\n */\n async refreshAccessToken(): Promise<string> {\n if (this.config.getToken) {\n const token = await this.config.getToken();\n this.cachedToken = token;\n this.expiresAt = Date.now() + 3_600_000;\n return token;\n }\n\n const tokenUrl = this.config.tokenUrl ?? GOOGLE_TOKEN_URL;\n const body = new URLSearchParams({\n client_id: this.config.clientId,\n client_secret: this.config.clientSecret,\n refresh_token: this.config.refreshToken,\n grant_type: \"refresh_token\",\n });\n\n const response = await fetch(tokenUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: body.toString(),\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`OAuth2 token refresh failed (${response.status}): ${text}`);\n }\n\n const data = (await response.json()) as TokenResponse;\n this.cachedToken = data.access_token;\n this.expiresAt = Date.now() + data.expires_in * 1000;\n return data.access_token;\n }\n\n /**\n * Build the XOAUTH2 SASL string for SMTP AUTH (base64-encoded).\n */\n async buildXOAUTH2(): Promise<string> {\n const token = await this.getAccessToken();\n const raw = `user=${this.config.user}\\x01auth=Bearer ${token}\\x01\\x01`;\n return encodeBase64(encodeUtf8(raw)).replace(/\\r\\n/g, \"\");\n }\n}\n"
|
|
6
6
|
],
|
|
7
7
|
"mappings": "+CAqBO,IAAM,EAAmB,sCAGnB,EAAsB,6DAY7B,EAAmB,MAKlB,MAAM,CAAa,CAEP,OAET,YAA6B,KAE7B,UAAY,EAEZ,eAAyC,KAGjD,WAAW,CAAC,EAAsB,CAEhC,GADA,KAAK,OAAS,EACV,EAAO,YACT,KAAK,YAAc,EAAO,YAC1B,KAAK,UAAY,KAAK,IAAI,EAAI,aAO5B,eAAc,EAAoB,CACtC,GAAI,KAAK,OAAO,SACd,OAAO,KAAK,OAAO,SAAS,EAG9B,GAAI,KAAK,aAAe,KAAK,IAAI,EAAI,KAAK,UAAY,EACpD,OAAO,KAAK,YAGd,GAAI,CAAC,KAAK,eACR,KAAK,eAAiB,KAAK,mBAAmB,EAAE,QAAQ,IAAM,CAC5D,KAAK,eAAiB,KACvB,EAEH,OAAO,KAAK,oBAMR,mBAAkB,EAAoB,CAC1C,GAAI,KAAK,OAAO,SAAU,CACxB,IAAM,EAAQ,MAAM,KAAK,OAAO,SAAS,EAGzC,OAFA,KAAK,YAAc,EACnB,KAAK,UAAY,KAAK,IAAI,EAAI,QACvB,EAGT,IAAM,EAAW,KAAK,OAAO,UAAY,EACnC,EAAO,IAAI,gBAAgB,CAC/B,UAAW,KAAK,OAAO,SACvB,cAAe,KAAK,OAAO,aAC3B,cAAe,KAAK,OAAO,aAC3B,WAAY,eACd,CAAC,EAEK,EAAW,MAAM,MAAM,EAAU,CACrC,OAAQ,OACR,QAAS,CAAE,eAAgB,mCAAoC,EAC/D,KAAM,EAAK,SAAS,CACtB,CAAC,EAED,GAAI,CAAC,EAAS,GAAI,CAChB,IAAM,EAAO,MAAM,EAAS,KAAK,EACjC,MAAU,MAAM,gCAAgC,EAAS,YAAY,GAAM,EAG7E,IAAM,EAAQ,MAAM,EAAS,KAAK,EAGlC,OAFA,KAAK,YAAc,EAAK,aACxB,KAAK,UAAY,KAAK,IAAI,EAAI,EAAK,WAAa,KACzC,EAAK,kBAMR,aAAY,EAAoB,CACpC,IAAM,EAAQ,MAAM,KAAK,eAAe,EAClC,EAAM,QAAQ,KAAK,OAAO,uBAAuB,YACvD,OAAO,EAAa,EAAW,CAAG,CAAC,EAAE,QAAQ,QAAS,EAAE,EAE5D",
|
|
8
|
-
"debugId": "
|
|
8
|
+
"debugId": "81D6302F3F754CF964756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{
|
|
2
|
-
export{w
|
|
1
|
+
import{_ as q}from"./chunk-nbmq8ejv.js";function w(){if(typeof Bun<"u")return"bun";if(typeof Deno<"u")return"deno";if(typeof caches<"u"&&globalThis.navigator?.userAgent==="Cloudflare-Workers")return"cf-workers";if(typeof window<"u")return"browser";if(typeof process<"u"&&process.versions?.node)return"node";return"unknown"}async function x(k){let v=w();switch(v){case"node":{let{NodeAdapter:g}=await import("./adapters/node.js");return new g(k)}case"bun":{let{BunAdapter:g}=await import("./adapters/bun.js");return new g(k)}case"deno":{let{DenoAdapter:g}=await import("./adapters/deno.js");return new g(k)}case"cf-workers":{let{CloudflareAdapter:g}=await import("./adapters/cf.js");return new g(k)}default:throw Error(`No socket adapter available for runtime: ${v}`)}}
|
|
2
|
+
export{w,x};
|
|
3
3
|
|
|
4
|
-
//# debugId=
|
|
4
|
+
//# debugId=5FF26FD321F29A4864756E2164756E21
|
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
"// src/detect.ts\nimport type { Runtime, SocketAdapter, TLSOptions } from \"./core/types.js\";\n\n/** Detect the current JavaScript runtime. */\nexport function detectRuntime(): Runtime {\n if (typeof Bun !== \"undefined\") {\n return \"bun\";\n }\n if (typeof Deno !== \"undefined\") {\n return \"deno\";\n }\n if (typeof caches !== \"undefined\" && globalThis.navigator?.userAgent === \"Cloudflare-Workers\") {\n return \"cf-workers\";\n }\n if (typeof window !== \"undefined\") {\n return \"browser\";\n }\n if (typeof process !== \"undefined\" && process.versions?.node) {\n return \"node\";\n }\n return \"unknown\";\n}\n\n/**\n * Dynamically import and instantiate the correct adapter for the current runtime.\n */\nexport async function createDefaultAdapter(options?: {\n secure?: boolean;\n connectionTimeout?: number;\n tls?: TLSOptions;\n}): Promise<SocketAdapter> {\n const runtime = detectRuntime();\n\n switch (runtime) {\n case \"node\": {\n const { NodeAdapter } = await import(\"./adapters/node.js\");\n return new NodeAdapter(options);\n }\n case \"bun\": {\n const { BunAdapter } = await import(\"./adapters/bun.js\");\n return new BunAdapter(options);\n }\n case \"deno\": {\n const { DenoAdapter } = await import(\"./adapters/deno.js\");\n return new DenoAdapter(options);\n }\n case \"cf-workers\": {\n const { CloudflareAdapter } = await import(\"./adapters/cf.js\");\n return new CloudflareAdapter(options);\n }\n default:\n throw new Error(`No socket adapter available for runtime: ${runtime}`);\n }\n}\n\ndeclare const Bun: unknown;\ndeclare const Deno: unknown;\n"
|
|
6
6
|
],
|
|
7
7
|
"mappings": "wCAIO,SAAS,CAAa,EAAY,CACvC,GAAI,OAAO,IAAQ,IACjB,MAAO,MAET,GAAI,OAAO,KAAS,IAClB,MAAO,OAET,GAAI,OAAO,OAAW,KAAe,WAAW,WAAW,YAAc,qBACvE,MAAO,aAET,GAAI,OAAO,OAAW,IACpB,MAAO,UAET,GAAI,OAAO,QAAY,KAAe,QAAQ,UAAU,KACtD,MAAO,OAET,MAAO,UAMT,eAAsB,CAAoB,CAAC,EAIhB,CACzB,IAAM,EAAU,EAAc,EAE9B,OAAQ,OACD,OAAQ,CACX,IAAQ,eAAgB,KAAa,8BACrC,OAAO,IAAI,EAAY,CAAO,CAChC,KACK,MAAO,CACV,IAAQ,cAAe,KAAa,6BACpC,OAAO,IAAI,EAAW,CAAO,CAC/B,KACK,OAAQ,CACX,IAAQ,eAAgB,KAAa,8BACrC,OAAO,IAAI,EAAY,CAAO,CAChC,KACK,aAAc,CACjB,IAAQ,qBAAsB,KAAa,4BAC3C,OAAO,IAAI,EAAkB,CAAO,CACtC,SAEE,MAAU,MAAM,4CAA4C,GAAS",
|
|
8
|
-
"debugId": "
|
|
8
|
+
"debugId": "5FF26FD321F29A4864756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{A as E}from"./chunk-9zkv5azm.js";import{B as M}from"./chunk-z62q7kqr.js";import{F as D}from"./chunk-qh90yr6d.js";import{X as S}from"./chunk-6bkj4y4r.js";async function _(j,w){if(!w||w.length===0)return j;let H=j;for(let z of w)H=await z(H);return H}function P(j){return typeof j.setMailerOnRetry==="function"}function f(j){return typeof j.setMailerOnFallback==="function"}var b="SMTP config passed to transport-only createMailer. Use: import { createSMTPMailer } from 'sently' or import { createSMTPMailer } from 'sently/smtp'.";async function l(j){if(j.transport===void 0||"host"in j&&j.transport===void 0)throw new S(b,"INVALID_CONFIG");return new k(j.transport,j.plugins??[],j.hooks)}function v(j){return j.attachments!==void 0&&j.attachments.length>0}function x(j){return E(j)}function q(j,w){return{...j.messageId!==void 0?{messageId:j.messageId}:{},to:D(j.to),subject:j.subject,provider:x(w)}}async function C(j,...w){if(j===void 0)return;try{await j(...w)}catch(H){if(!(typeof process<"u"&&process.env?.NODE_ENV==="production"))console.warn("[sently] Mailer hook threw; send continues:",H)}}class k{transport;plugins;hooks;constructor(j,w=[],H){this.transport=j;this.plugins=w;this.hooks=H}async send(j){let w=await _(j,this.plugins),H=q(w,this.transport);if(await C(this.hooks?.onSend,H),this.hooks?.onRetry!==void 0&&P(this.transport))this.transport.setMailerOnRetry((B,G)=>{C(this.hooks?.onRetry,H,B,G)});if(this.hooks?.onFallback!==void 0&&f(this.transport))this.transport.setMailerOnFallback((B,G,I)=>{C(this.hooks?.onFallback,H,B,G,I)});let z=performance.now();try{let B=await this.transport.send(w),G={...H,messageId:B.messageId};return await C(this.hooks?.onSuccess,G,B,performance.now()-z),B}catch(B){throw await C(this.hooks?.onError,H,B,performance.now()-z),B}finally{if(P(this.transport))this.transport.setMailerOnRetry(void 0);if(f(this.transport))this.transport.setMailerOnFallback(void 0)}}async processMessage(j){return _(j,this.plugins)}async sendBulk(j,w){if(j.length===0)return{total:0,sent:0,failed:0,results:[]};let H=Array(j.length),z=w?.stopOnError??!1,B=!1,G=async(X,$,J,U,Z)=>{if(H[$]={status:"sent",result:J},U!==void 0&&this.hooks!==void 0){let W=q(U,this.transport);await C(this.hooks.onSend,W),await C(this.hooks.onSuccess,{...W,messageId:J.messageId},J,Z)}w?.onSuccess?.(X,$,J)},I=async(X,$,J,U,Z)=>{if(H[$]={status:"failed",error:J},U!==void 0&&this.hooks!==void 0){let W=q(U,this.transport);await C(this.hooks.onSend,W),await C(this.hooks.onError,W,J,Z)}if(w?.onError?.(X,$,J),z)B=!0};if(this.transport.sendBatch){let X=[],$=[];for(let[J,U]of j.entries())if(v(U))$.push({index:J,message:U});else X.push({index:J,message:U});if(X.length>0&&!B){let J=this.transport.batchMax??X.length,U=w?.rateDelta??2,Z=U>0?new M(U,w?.rateLimit??1000,w?.now):null;for(let W=0;W<X.length&&!B;W+=J){let V=X.slice(W,W+J);if(Z)await Z.acquire();try{let Y=await Promise.all(V.map(({message:A})=>this.processMessage(A))),Q=performance.now(),K=await this.transport.sendBatch(Y),T=performance.now()-Q;for(let A=0;A<V.length;A++){let F=V[A],R=Y[A],L=K[A];if(L===void 0)await I(F.message,F.index,Error("Batch response missing result for message"),R,T);else if(L.batchError!==void 0)await I(F.message,F.index,L.batchError,R,T);else await G(F.message,F.index,L,R,T)}}catch(Y){let Q=await Promise.all(V.map(({message:K})=>this.processMessage(K)));for(let K=0;K<V.length;K++){if(B)break;let T=V[K];await I(T.message,T.index,Y,Q[K])}}}}if($.length>0&&!B){let J=w?.concurrency??1,U=[...$],Z=0;await new Promise((W)=>{let V=()=>{if(U.length===0&&Z===0||B)W()},Y=()=>{if(U.length===0||B){V();return}let Q=U.shift();if(Q===void 0){V();return}Z++,this.send(Q.message).then((K)=>{H[Q.index]={status:"sent",result:K},w?.onSuccess?.(Q.message,Q.index,K)}).catch((K)=>{if(H[Q.index]={status:"failed",error:K},w?.onError?.(Q.message,Q.index,K),z)B=!0}).finally(()=>{Z--,Y(),V()})};for(let Q=0;Q<J;Q++)Y()})}}else{let X=w?.concurrency??1,$=[...j.entries()],J=0;await new Promise((U)=>{let Z=()=>{if($.length===0&&J===0||B)U()},W=()=>{if($.length===0||B){Z();return}let V=$.shift();if(V===void 0){Z();return}let[Y,Q]=V;J++,this.send(Q).then((K)=>{H[Y]={status:"sent",result:K},w?.onSuccess?.(Q,Y,K)}).catch((K)=>{if(H[Y]={status:"failed",error:K},w?.onError?.(Q,Y,K),z)B=!0}).finally(()=>{J--,W(),Z()})};for(let V=0;V<X;V++)W()})}let N=0,O=0;for(let X of H){if(X===void 0)continue;if(X.status==="sent")N++;else O++}return{total:j.length,sent:N,failed:O,results:H}}verify(){if(this.transport.verify)return this.transport.verify();return Promise.resolve({ok:!0,provider:"mailer"})}close(){if(this.transport.close)return this.transport.close();return Promise.resolve()}}
|
|
2
|
+
export{l as y,k as z};
|
|
3
|
+
|
|
4
|
+
//# debugId=A6BD4A8094E8BBE064756E2164756E21
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/core/plugin.ts", "../src/mailer.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @module\n * Plugin pipeline for sently.\n * Plugins transform MailOptions before message construction.\n * They run sequentially — each receives the previous plugin's output.\n *\n * @example\n * ```ts\n * import { runPlugins } from \"sently/core/plugin\";\n * const result = await runPlugins(options, [pluginA, pluginB]);\n * ```\n */\nimport type { MailOptions, MailPlugin } from \"./types.js\";\n\n/**\n * Run a list of plugins sequentially over MailOptions.\n * If plugins is empty or undefined, returns options unchanged.\n * Each plugin may be sync or async.\n *\n * @param options - the original mail options\n * @param plugins - ordered list of plugins to apply\n * @returns transformed mail options after all plugins have run\n */\nexport async function runPlugins(\n options: MailOptions,\n plugins: MailPlugin[] | undefined,\n): Promise<MailOptions> {\n if (!plugins || plugins.length === 0) {\n return options;\n }\n\n let current = options;\n for (const plugin of plugins) {\n current = await plugin(current);\n }\n return current;\n}\n",
|
|
6
|
+
"/**\n * @module\n * Lightweight mailer factory for custom transports — no SMTP code in the bundle.\n *\n * Use this entry instead of `sently` when you pass a transport explicitly\n * (Resend, SendGrid, etc.) and want the smallest bundle size.\n *\n * @example\n * ```ts\n * import { createMailer } from \"sently/mailer\";\n * import { ResendTransport } from \"sently/transports/resend\";\n *\n * const mailer = await createMailer({\n * transport: new ResendTransport({ apiKey: process.env.RESEND_API_KEY! }),\n * });\n *\n * await mailer.send({\n * from: \"onboarding@yourdomain.com\",\n * to: \"recipient@example.com\",\n * subject: \"Hello\",\n * html: \"<p>Sent via Resend</p>\",\n * });\n * ```\n */\nimport { extractEmails } from \"./core/address.js\";\nimport { SentlyError } from \"./core/errors.js\";\nimport { runPlugins } from \"./core/plugin.js\";\nimport { getProviderLabel } from \"./core/provider-label.js\";\nimport { RateLimiter } from \"./core/rate-limiter.js\";\nimport type {\n BulkSendOptions,\n BulkSendResult,\n Mailer,\n MailerHookContext,\n MailerHooks,\n MailOptions,\n MailPlugin,\n SendResult,\n Transport,\n TransportMailerOptions,\n VerifyResult,\n} from \"./core/types.js\";\n\nexport type { MailerHookContext, MailerHooks, TransportMailerOptions };\n\n/** Retry decorator wired by mailer hooks (duck-typed to avoid pulling retry into all bundles). */\ninterface RetryHookTransport extends Transport {\n setMailerOnRetry(callback: ((attempt: number, error: unknown) => void) | undefined): void;\n}\n\nfunction isRetryHookTransport(transport: Transport): transport is RetryHookTransport {\n return typeof (transport as RetryHookTransport).setMailerOnRetry === \"function\";\n}\n\n/** Fallback decorator wired by mailer hooks (duck-typed). */\ninterface FallbackHookTransport extends Transport {\n setMailerOnFallback(\n callback: ((failedProvider: string, nextProvider: string, error: unknown) => void) | undefined,\n ): void;\n}\n\nfunction isFallbackHookTransport(transport: Transport): transport is FallbackHookTransport {\n return typeof (transport as FallbackHookTransport).setMailerOnFallback === \"function\";\n}\n\nconst TRANSPORT_ONLY_SMTP_CONFIG_MESSAGE =\n \"SMTP config passed to transport-only createMailer. Use: import { createSMTPMailer } from 'sently' or import { createSMTPMailer } from 'sently/smtp'.\";\n\n/**\n * Create a mailer that wraps a custom {@link Transport} (HTTP API, preview, retry, etc.).\n */\nexport async function createMailer(options: TransportMailerOptions): Promise<Mailer> {\n if (options.transport === undefined || (\"host\" in options && options.transport === undefined)) {\n throw new SentlyError(TRANSPORT_ONLY_SMTP_CONFIG_MESSAGE, \"INVALID_CONFIG\");\n }\n\n return new MailerImpl(options.transport, options.plugins ?? [], options.hooks);\n}\n\nfunction hasAttachments(message: MailOptions): boolean {\n return message.attachments !== undefined && message.attachments.length > 0;\n}\n\n/** Infer a provider label from a transport instance for hook context. */\nfunction inferProvider(transport: Transport): string {\n return getProviderLabel(transport);\n}\n\n/** Build hook context from mail options (no body fields). */\nfunction buildHookContext(options: MailOptions, transport: Transport): MailerHookContext {\n return {\n ...(options.messageId !== undefined ? { messageId: options.messageId } : {}),\n to: extractEmails(options.to),\n subject: options.subject,\n provider: inferProvider(transport),\n };\n}\n\n/**\n * Invoke a mailer hook without letting hook failures break the send.\n * In non-production environments, hook errors are logged with `console.warn`.\n */\nasync function invokeHook<T extends unknown[]>(\n hook: ((...args: T) => void | Promise<void>) | undefined,\n ...args: T\n): Promise<void> {\n if (hook === undefined) {\n return;\n }\n try {\n await hook(...args);\n } catch (hookError) {\n const isProduction = typeof process !== \"undefined\" && process.env?.NODE_ENV === \"production\";\n if (!isProduction) {\n console.warn(\"[sently] Mailer hook threw; send continues:\", hookError);\n }\n }\n}\n\n/** Internal mailer implementation shared with the full `sently` entry. */\nexport class MailerImpl implements Mailer {\n constructor(\n private readonly transport: Transport,\n private readonly plugins: MailPlugin[] = [],\n private readonly hooks?: MailerHooks,\n ) {}\n\n async send(options: MailOptions): Promise<SendResult> {\n const processed = await runPlugins(options, this.plugins);\n const ctx = buildHookContext(processed, this.transport);\n\n await invokeHook(this.hooks?.onSend, ctx);\n\n if (this.hooks?.onRetry !== undefined && isRetryHookTransport(this.transport)) {\n this.transport.setMailerOnRetry((attempt, error) => {\n void invokeHook(this.hooks?.onRetry, ctx, attempt, error);\n });\n }\n\n if (this.hooks?.onFallback !== undefined && isFallbackHookTransport(this.transport)) {\n this.transport.setMailerOnFallback((failedProvider, nextProvider, error) => {\n void invokeHook(this.hooks?.onFallback, ctx, failedProvider, nextProvider, error);\n });\n }\n\n const start = performance.now();\n\n try {\n const result = await this.transport.send(processed);\n const successCtx: MailerHookContext = {\n ...ctx,\n messageId: result.messageId,\n };\n await invokeHook(this.hooks?.onSuccess, successCtx, result, performance.now() - start);\n return result;\n } catch (error) {\n await invokeHook(this.hooks?.onError, ctx, error, performance.now() - start);\n throw error;\n } finally {\n if (isRetryHookTransport(this.transport)) {\n this.transport.setMailerOnRetry(undefined);\n }\n if (isFallbackHookTransport(this.transport)) {\n this.transport.setMailerOnFallback(undefined);\n }\n }\n }\n\n private async processMessage(message: MailOptions): Promise<MailOptions> {\n return runPlugins(message, this.plugins);\n }\n\n async sendBulk(messages: MailOptions[], options?: BulkSendOptions): Promise<BulkSendResult> {\n if (messages.length === 0) {\n return { total: 0, sent: 0, failed: 0, results: [] };\n }\n\n const results: BulkSendResult[\"results\"] = new Array(messages.length);\n const stopOnError = options?.stopOnError ?? false;\n let halted = false;\n\n const recordSuccess = async (\n message: MailOptions,\n index: number,\n result: SendResult,\n processed?: MailOptions,\n durationMs?: number,\n ): Promise<void> => {\n results[index] = { status: \"sent\", result };\n if (processed !== undefined && this.hooks !== undefined) {\n const ctx = buildHookContext(processed, this.transport);\n await invokeHook(this.hooks.onSend, ctx);\n await invokeHook(\n this.hooks.onSuccess,\n { ...ctx, messageId: result.messageId },\n result,\n durationMs,\n );\n }\n options?.onSuccess?.(message, index, result);\n };\n\n const recordFailure = async (\n message: MailOptions,\n index: number,\n error: unknown,\n processed?: MailOptions,\n durationMs?: number,\n ): Promise<void> => {\n results[index] = { status: \"failed\", error };\n if (processed !== undefined && this.hooks !== undefined) {\n const ctx = buildHookContext(processed, this.transport);\n await invokeHook(this.hooks.onSend, ctx);\n await invokeHook(this.hooks.onError, ctx, error, durationMs);\n }\n options?.onError?.(message, index, error);\n if (stopOnError) {\n halted = true;\n }\n };\n\n if (this.transport.sendBatch) {\n const batchEntries: Array<{ index: number; message: MailOptions }> = [];\n const singleEntries: Array<{ index: number; message: MailOptions }> = [];\n\n for (const [index, message] of messages.entries()) {\n if (hasAttachments(message)) {\n singleEntries.push({ index, message });\n } else {\n batchEntries.push({ index, message });\n }\n }\n\n if (batchEntries.length > 0 && !halted) {\n const chunkSize = this.transport.batchMax ?? batchEntries.length;\n const rateDelta = options?.rateDelta ?? 2;\n const rateLimiter =\n rateDelta > 0\n ? new RateLimiter(rateDelta, options?.rateLimit ?? 1000, options?.now)\n : null;\n\n for (\n let chunkStart = 0;\n chunkStart < batchEntries.length && !halted;\n chunkStart += chunkSize\n ) {\n const chunk = batchEntries.slice(chunkStart, chunkStart + chunkSize);\n\n if (rateLimiter) {\n await rateLimiter.acquire();\n }\n\n try {\n const processed = await Promise.all(\n chunk.map(({ message }) => this.processMessage(message)),\n );\n const batchStart = performance.now();\n const batchResults = await this.transport.sendBatch(processed);\n const batchDurationMs = performance.now() - batchStart;\n\n for (let i = 0; i < chunk.length; i++) {\n const entry = chunk[i] as { index: number; message: MailOptions };\n const processedMessage = processed[i] as MailOptions;\n const result = batchResults[i];\n if (result === undefined) {\n await recordFailure(\n entry.message,\n entry.index,\n new Error(\"Batch response missing result for message\"),\n processedMessage,\n batchDurationMs,\n );\n } else if (result.batchError !== undefined) {\n await recordFailure(\n entry.message,\n entry.index,\n result.batchError,\n processedMessage,\n batchDurationMs,\n );\n } else {\n await recordSuccess(\n entry.message,\n entry.index,\n result,\n processedMessage,\n batchDurationMs,\n );\n }\n }\n } catch (error) {\n const processed = await Promise.all(\n chunk.map(({ message }) => this.processMessage(message)),\n );\n for (let i = 0; i < chunk.length; i++) {\n if (halted) {\n break;\n }\n const entry = chunk[i] as { index: number; message: MailOptions };\n await recordFailure(entry.message, entry.index, error, processed[i]);\n }\n }\n }\n }\n\n if (singleEntries.length > 0 && !halted) {\n const concurrency = options?.concurrency ?? 1;\n const queue = [...singleEntries];\n let active = 0;\n\n await new Promise<void>((resolve) => {\n const maybeDone = (): void => {\n if ((queue.length === 0 && active === 0) || halted) {\n resolve();\n }\n };\n\n const processNext = (): void => {\n if (queue.length === 0 || halted) {\n maybeDone();\n return;\n }\n\n const entry = queue.shift();\n if (entry === undefined) {\n maybeDone();\n return;\n }\n\n active++;\n void this.send(entry.message)\n .then((result) => {\n results[entry.index] = { status: \"sent\", result };\n options?.onSuccess?.(entry.message, entry.index, result);\n })\n .catch((error: unknown) => {\n results[entry.index] = { status: \"failed\", error };\n options?.onError?.(entry.message, entry.index, error);\n if (stopOnError) {\n halted = true;\n }\n })\n .finally(() => {\n active--;\n processNext();\n maybeDone();\n });\n };\n\n for (let i = 0; i < concurrency; i++) {\n processNext();\n }\n });\n }\n } else {\n const concurrency = options?.concurrency ?? 1;\n const queue = [...messages.entries()];\n let active = 0;\n\n await new Promise<void>((resolve) => {\n const maybeDone = (): void => {\n if ((queue.length === 0 && active === 0) || halted) {\n resolve();\n }\n };\n\n const processNext = (): void => {\n if (queue.length === 0 || halted) {\n maybeDone();\n return;\n }\n\n const entry = queue.shift();\n if (entry === undefined) {\n maybeDone();\n return;\n }\n\n const [index, message] = entry;\n active++;\n\n void this.send(message)\n .then((result) => {\n results[index] = { status: \"sent\", result };\n options?.onSuccess?.(message, index, result);\n })\n .catch((error: unknown) => {\n results[index] = { status: \"failed\", error };\n options?.onError?.(message, index, error);\n if (stopOnError) {\n halted = true;\n }\n })\n .finally(() => {\n active--;\n processNext();\n maybeDone();\n });\n };\n\n for (let i = 0; i < concurrency; i++) {\n processNext();\n }\n });\n }\n\n let sent = 0;\n let failed = 0;\n for (const result of results) {\n if (result === undefined) {\n continue;\n }\n if (result.status === \"sent\") {\n sent++;\n } else {\n failed++;\n }\n }\n\n return {\n total: messages.length,\n sent,\n failed,\n results,\n };\n }\n\n verify(): Promise<VerifyResult> {\n if (this.transport.verify) {\n return this.transport.verify();\n }\n return Promise.resolve({ ok: true, provider: \"mailer\" });\n }\n\n close(): Promise<void> {\n if (this.transport.close) {\n return this.transport.close();\n }\n return Promise.resolve();\n }\n}\n"
|
|
7
|
+
],
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "A6BD4A8094E8BBE064756E2164756E21",
|
|
10
|
+
"names": []
|
|
11
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
class y{rateDelta;rateLimit;now;static defaultNow=Date.now;tokens;lastRefill;waiters=[];constructor(h,x,j=y.defaultNow){this.rateDelta=h;this.rateLimit=x;this.now=j;this.tokens=h,this.lastRefill=j()}async acquire(){for(;;){if(this.refill(),this.tokens>0){this.tokens-=1;return}if(this.now===y.defaultNow){let h=this.now()-this.lastRefill,x=Math.max(1,this.rateLimit-h);await new Promise((j)=>setTimeout(j,x));continue}await new Promise((h)=>{this.waiters.push(h)})}}notify(){this.refill()}refill(){let x=this.now()-this.lastRefill;if(x>=this.rateLimit){let j=Math.floor(x/this.rateLimit);this.tokens=Math.min(this.rateDelta,this.tokens+j*this.rateDelta),this.lastRefill+=j*this.rateLimit;while(this.tokens>0&&this.waiters.length>0)this.tokens-=1,this.waiters.shift()?.()}}}
|
|
2
|
-
export{y as
|
|
2
|
+
export{y as B};
|
|
3
3
|
|
|
4
|
-
//# debugId=
|
|
4
|
+
//# debugId=9226F5E0447083C564756E2164756E21
|
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
"/**\n * Token bucket rate limiter with lazy refill on acquire.\n */\nexport class RateLimiter {\n private static readonly defaultNow = Date.now;\n\n /** Remaining tokens in the current rate-limit window. */\n private tokens: number;\n /** Timestamp (ms) of the last token refill. */\n private lastRefill: number;\n /** Resolvers waiting for a token when the bucket is empty. */\n private waiters: Array<() => void> = [];\n\n /** Creates a rate limiter with the given burst size and window duration. */\n constructor(\n private readonly rateDelta: number,\n private readonly rateLimit: number,\n private readonly now: () => number = RateLimiter.defaultNow,\n ) {\n this.tokens = rateDelta;\n this.lastRefill = now();\n }\n\n /** Wait until a token is available, then consume one. */\n async acquire(): Promise<void> {\n for (;;) {\n this.refill();\n if (this.tokens > 0) {\n this.tokens -= 1;\n return;\n }\n\n if (this.now === RateLimiter.defaultNow) {\n const elapsed = this.now() - this.lastRefill;\n const waitMs = Math.max(1, this.rateLimit - elapsed);\n await new Promise<void>((resolve) => setTimeout(resolve, waitMs));\n continue;\n }\n\n await new Promise<void>((resolve) => {\n this.waiters.push(resolve);\n });\n }\n }\n\n /** Wake waiters after the clock advances (for testing). */\n notify(): void {\n this.refill();\n }\n\n /** Refills tokens based on elapsed time and wakes waiting acquirers. */\n private refill(): void {\n const t = this.now();\n const elapsed = t - this.lastRefill;\n if (elapsed >= this.rateLimit) {\n const periods = Math.floor(elapsed / this.rateLimit);\n this.tokens = Math.min(this.rateDelta, this.tokens + periods * this.rateDelta);\n this.lastRefill += periods * this.rateLimit;\n while (this.tokens > 0 && this.waiters.length > 0) {\n this.tokens -= 1;\n const next = this.waiters.shift();\n next?.();\n }\n }\n }\n}\n"
|
|
6
6
|
],
|
|
7
7
|
"mappings": "AAGO,MAAM,CAAY,CAYJ,UACA,UACA,UAbK,YAAa,KAAK,IAGlC,OAEA,WAEA,QAA6B,CAAC,EAGtC,WAAW,CACQ,EACA,EACA,EAAoB,EAAY,WACjD,CAHiB,iBACA,iBACA,WAEjB,KAAK,OAAS,EACd,KAAK,WAAa,EAAI,OAIlB,QAAO,EAAkB,CAC7B,OAAS,CAEP,GADA,KAAK,OAAO,EACR,KAAK,OAAS,EAAG,CACnB,KAAK,QAAU,EACf,OAGF,GAAI,KAAK,MAAQ,EAAY,WAAY,CACvC,IAAM,EAAU,KAAK,IAAI,EAAI,KAAK,WAC5B,EAAS,KAAK,IAAI,EAAG,KAAK,UAAY,CAAO,EACnD,MAAM,IAAI,QAAc,CAAC,IAAY,WAAW,EAAS,CAAM,CAAC,EAChE,SAGF,MAAM,IAAI,QAAc,CAAC,IAAY,CACnC,KAAK,QAAQ,KAAK,CAAO,EAC1B,GAKL,MAAM,EAAS,CACb,KAAK,OAAO,EAIN,MAAM,EAAS,CAErB,IAAM,EADI,KAAK,IAAI,EACC,KAAK,WACzB,GAAI,GAAW,KAAK,UAAW,CAC7B,IAAM,EAAU,KAAK,MAAM,EAAU,KAAK,SAAS,EACnD,KAAK,OAAS,KAAK,IAAI,KAAK,UAAW,KAAK,OAAS,EAAU,KAAK,SAAS,EAC7E,KAAK,YAAc,EAAU,KAAK,UAClC,MAAO,KAAK,OAAS,GAAK,KAAK,QAAQ,OAAS,EAC9C,KAAK,QAAU,EACF,KAAK,QAAQ,MAAM,IACzB,GAIf",
|
|
8
|
-
"debugId": "
|
|
8
|
+
"debugId": "9226F5E0447083C564756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{C as v,D as j,E as D,F as T,G as y}from"./chunk-qh90yr6d.js";import{T as F,V as M,W as P}from"./chunk-5scdgffb.js";import{_ as b}from"./chunk-nbmq8ejv.js";function N(q){return q.replace(/\r\n?|\n/g," ").trim()}function w(q){if(v(q.address,"address"),q.name!==void 0)v(q.name,"display name");let O=q.address.trim();if(!y(O))throw Error(`Invalid email address: ${JSON.stringify(q.address)}`);let G={address:O};if(q.name)G.name=N(q.name);return G}var Q=`\r
|
|
2
2
|
`;async function c(q,O){let G=q.messageId??f(),X=(q.date??new Date).toUTCString(),Y=j(q.from),Z=j(q.to),K=q.cc?j(q.cc):[];if(Y.length===0)throw Error("Missing from address");if(Z.length===0)throw Error("Missing to address");let H={from:Y[0]?.address??"",to:[...T(q.to),...q.cc?T(q.cc):[],...q.bcc?T(q.bcc):[]]},E=q.attachments??[],V=E.filter((J)=>J.inline||J.contentId),I=E.filter((J)=>!J.inline&&!J.contentId),W=k(q);if(V.length>0){let J=S();W={contentType:`multipart/related; boundary="${J}"`,content:x(J,[L(k(q)),...V.map(R)])}}if(I.length>0){let J=S();W={contentType:`multipart/mixed; boundary="${J}"`,content:x(J,[L(W),...I.map(R)])}}let _=[$("From",Y.map((J)=>D(w(J))).join(", ")),$("To",Z.map((J)=>D(w(J))).join(", "))];if(K.length>0)_.push($("Cc",K.map((J)=>D(w(J))).join(", ")));if(q.replyTo)_.push($("Reply-To",j(q.replyTo).map((J)=>D(w(J))).join(", ")));if(_.push($("Subject",M(N(q.subject))),$("Date",X),$("Message-ID",G),"MIME-Version: 1.0"),q.priority==="high")_.push("X-Priority: 1","Importance: high");else if(q.priority==="low")_.push("X-Priority: 5","Importance: low");if(q.headers)for(let[J,B]of Object.entries(q.headers))_.push($(N(J),N(B)));if(_.push(`Content-Type: ${W.contentType}`),W.contentTransferEncoding)_.push(`Content-Transfer-Encoding: ${W.contentTransferEncoding}`);let z=`${_.join(Q)}${Q}${Q}${W.content}`,U=P(z);if(O){let{signDKIM:J}=await import("./dkim.js"),{header:B}=await J(U,O),g=`${B}${Q}${z}`;U=P(g)}return{raw:U,envelope:H,messageId:G,size:U.length}}function k(q){let O=Boolean(q.text),G=Boolean(q.html);if(O&&G){let X=S();return{contentType:`multipart/alternative; boundary="${X}"`,content:x(X,[C({contentType:"text/plain; charset=utf-8",contentTransferEncoding:"8bit",content:q.text??""}),C({contentType:"text/html; charset=utf-8",contentTransferEncoding:"8bit",content:q.html??""})])}}if(G)return{contentType:"text/html; charset=utf-8",contentTransferEncoding:"8bit",content:q.html??""};return{contentType:"text/plain; charset=utf-8",contentTransferEncoding:"8bit",content:q.text??""}}function C(q){let O=[`Content-Type: ${q.contentType}`];if(q.contentTransferEncoding)O.push(`Content-Transfer-Encoding: ${q.contentTransferEncoding}`);return`${O.join(Q)}${Q}${Q}${q.content}`}function L(q){let O=[`Content-Type: ${q.contentType}`];if(q.contentTransferEncoding)O.push(`Content-Transfer-Encoding: ${q.contentTransferEncoding}`);return`${O.join(Q)}${Q}${Q}${q.content}`}function R(q){if(!q.content||typeof q.content==="string")throw Error(`Attachment "${q.filename}" requires Uint8Array content`);let O=N(q.filename??""),G=[`Content-Type: ${q.contentType??"application/octet-stream"}`,"Content-Transfer-Encoding: base64",`Content-Disposition: ${q.inline?"inline":"attachment"}; filename="${O}"`];if(q.contentId)G.push(`Content-ID: <${q.contentId}>`);if(q.headers)for(let[X,Y]of Object.entries(q.headers)){let Z=N(X),K=N(Y);G.push(`${Z}: ${K}`)}return`${G.join(Q)}${Q}${Q}${F(q.content)}`}function x(q,O){let G=O.map((X)=>`--${q}${Q}${X}`);return G.push(`--${q}--`),G.join(Q)}function f(){let q=crypto.getRandomValues(new Uint8Array(8)),O=Array.from(q,(G)=>G.toString(16).padStart(2,"0")).join("");return`<${Date.now()}.${O}@sently>`}function S(){let q=crypto.getRandomValues(new Uint8Array(12));return`----sently_${Array.from(q,(G)=>G.toString(16).padStart(2,"0")).join("")}`}function $(q,O){let G=`${q}: ${O}`;if(G.length<=76)return G;let X=[],Y=G;while(Y.length>76){let Z=Y.lastIndexOf(" ",76);if(Z<=q.length+1)Z=76;X.push(Y.slice(0,Z)),Y=` ${Y.slice(Z).trimStart()}`}return X.push(Y),X.join(`${Q} `)}
|
|
3
|
-
export{c as
|
|
3
|
+
export{c as u};
|
|
4
4
|
|
|
5
|
-
//# debugId=
|
|
5
|
+
//# debugId=C00C3770E0C105B564756E2164756E21
|
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
"// src/core/mime.ts\nimport {\n assertSafeAddress,\n extractEmails,\n isValidEmail,\n parseAddresses,\n toMIMEHeader,\n} from \"./address.js\";\nimport { encodeBase64, encodeHeader, encodeUtf8 } from \"./base64.js\";\nimport type { Address, Attachment, DKIMConfig, Envelope, MailOptions } from \"./types.js\";\n\nfunction sanitizeHeaderValue(value: string): string {\n return value.replace(/\\r\\n?|\\n/g, \" \").trim();\n}\n\nfunction sanitizeAddress(addr: Address): Address {\n // Security check runs on the raw value FIRST and fails closed: any control\n // character (CR/LF/NUL/…) is rejected outright rather than stripped and\n // accepted. parseAddresses() already enforces this upstream; re-asserting\n // here keeps the function safe if it is ever called directly.\n assertSafeAddress(addr.address, \"address\");\n if (addr.name !== undefined) {\n assertSafeAddress(addr.name, \"display name\");\n }\n\n // Only ordinary surrounding whitespace is trimmed — the address identity is\n // never altered (no CR/LF-to-space repair).\n const address = addr.address.trim();\n if (!isValidEmail(address)) {\n throw new Error(`Invalid email address: ${JSON.stringify(addr.address)}`);\n }\n\n const sanitized: Address = { address };\n if (addr.name) {\n sanitized.name = sanitizeHeaderValue(addr.name);\n }\n return sanitized;\n}\n\n/** Result of building a complete MIME message. */\nexport interface MIMEBuildResult {\n /** Complete raw MIME message bytes ready to send. */\n raw: Uint8Array;\n /** SMTP envelope derived from From/To/Cc/Bcc. */\n envelope: Envelope;\n /** Message-ID assigned or supplied for the message. */\n messageId: string;\n /** Size of the raw MIME message in bytes. */\n size: number;\n}\n\nconst CRLF = \"\\r\\n\";\n\n/**\n * Build a complete MIME message as a Uint8Array ready for SMTP DATA.\n * When `dkim` is provided, signs the message and prepends the DKIM-Signature header.\n */\nexport async function buildMIME(options: MailOptions, dkim?: DKIMConfig): Promise<MIMEBuildResult> {\n const messageId = options.messageId ?? generateMessageId();\n const date = (options.date ?? new Date()).toUTCString();\n const fromAddrs = parseAddresses(options.from);\n const toAddrs = parseAddresses(options.to);\n const ccAddrs = options.cc ? parseAddresses(options.cc) : [];\n\n if (fromAddrs.length === 0) {\n throw new Error(\"Missing from address\");\n }\n if (toAddrs.length === 0) {\n throw new Error(\"Missing to address\");\n }\n\n const envelope: Envelope = {\n from: fromAddrs[0]?.address ?? \"\",\n to: [\n ...extractEmails(options.to),\n ...(options.cc ? extractEmails(options.cc) : []),\n ...(options.bcc ? extractEmails(options.bcc) : []),\n ],\n };\n\n const attachments = options.attachments ?? [];\n const inlineAttachments = attachments.filter((a) => a.inline || a.contentId);\n const regularAttachments = attachments.filter((a) => !a.inline && !a.contentId);\n\n let root = buildSimpleBody(options);\n\n if (inlineAttachments.length > 0) {\n const boundary = generateBoundary();\n root = {\n contentType: `multipart/related; boundary=\"${boundary}\"`,\n content: assembleMultipart(boundary, [\n formatNestedPart(buildSimpleBody(options)),\n ...inlineAttachments.map(formatAttachmentPart),\n ]),\n };\n }\n\n if (regularAttachments.length > 0) {\n const boundary = generateBoundary();\n root = {\n contentType: `multipart/mixed; boundary=\"${boundary}\"`,\n content: assembleMultipart(boundary, [\n formatNestedPart(root),\n ...regularAttachments.map(formatAttachmentPart),\n ]),\n };\n }\n\n const headers: string[] = [\n foldHeader(\"From\", fromAddrs.map((a) => toMIMEHeader(sanitizeAddress(a))).join(\", \")),\n foldHeader(\"To\", toAddrs.map((a) => toMIMEHeader(sanitizeAddress(a))).join(\", \")),\n ];\n\n if (ccAddrs.length > 0) {\n headers.push(foldHeader(\"Cc\", ccAddrs.map((a) => toMIMEHeader(sanitizeAddress(a))).join(\", \")));\n }\n\n if (options.replyTo) {\n headers.push(\n foldHeader(\n \"Reply-To\",\n parseAddresses(options.replyTo)\n .map((a) => toMIMEHeader(sanitizeAddress(a)))\n .join(\", \"),\n ),\n );\n }\n\n headers.push(\n foldHeader(\"Subject\", encodeHeader(sanitizeHeaderValue(options.subject))),\n foldHeader(\"Date\", date),\n foldHeader(\"Message-ID\", messageId),\n \"MIME-Version: 1.0\",\n );\n\n if (options.priority === \"high\") {\n headers.push(\"X-Priority: 1\", \"Importance: high\");\n } else if (options.priority === \"low\") {\n headers.push(\"X-Priority: 5\", \"Importance: low\");\n }\n\n if (options.headers) {\n for (const [key, value] of Object.entries(options.headers)) {\n headers.push(foldHeader(sanitizeHeaderValue(key), sanitizeHeaderValue(value)));\n }\n }\n\n headers.push(`Content-Type: ${root.contentType}`);\n if (root.contentTransferEncoding) {\n headers.push(`Content-Transfer-Encoding: ${root.contentTransferEncoding}`);\n }\n\n const rawText = `${headers.join(CRLF)}${CRLF}${CRLF}${root.content}`;\n let raw = encodeUtf8(rawText);\n\n if (dkim) {\n const { signDKIM } = await import(\"../dkim.js\");\n const { header } = await signDKIM(raw, dkim);\n const signedText = `${header}${CRLF}${rawText}`;\n raw = encodeUtf8(signedText);\n }\n\n return { raw, envelope, messageId, size: raw.length };\n}\n\ninterface SimpleBody {\n contentType: string;\n contentTransferEncoding?: string;\n content: string;\n}\n\nfunction buildSimpleBody(options: MailOptions): SimpleBody {\n const hasText = Boolean(options.text);\n const hasHtml = Boolean(options.html);\n\n if (hasText && hasHtml) {\n const boundary = generateBoundary();\n return {\n contentType: `multipart/alternative; boundary=\"${boundary}\"`,\n content: assembleMultipart(boundary, [\n formatSimplePart({\n contentType: \"text/plain; charset=utf-8\",\n contentTransferEncoding: \"8bit\",\n content: options.text ?? \"\",\n }),\n formatSimplePart({\n contentType: \"text/html; charset=utf-8\",\n contentTransferEncoding: \"8bit\",\n content: options.html ?? \"\",\n }),\n ]),\n };\n }\n\n if (hasHtml) {\n return {\n contentType: \"text/html; charset=utf-8\",\n contentTransferEncoding: \"8bit\",\n content: options.html ?? \"\",\n };\n }\n\n return {\n contentType: \"text/plain; charset=utf-8\",\n contentTransferEncoding: \"8bit\",\n content: options.text ?? \"\",\n };\n}\n\nfunction formatSimplePart(part: SimpleBody): string {\n const headers = [`Content-Type: ${part.contentType}`];\n if (part.contentTransferEncoding) {\n headers.push(`Content-Transfer-Encoding: ${part.contentTransferEncoding}`);\n }\n return `${headers.join(CRLF)}${CRLF}${CRLF}${part.content}`;\n}\n\nfunction formatNestedPart(part: SimpleBody): string {\n const headers = [`Content-Type: ${part.contentType}`];\n if (part.contentTransferEncoding) {\n headers.push(`Content-Transfer-Encoding: ${part.contentTransferEncoding}`);\n }\n return `${headers.join(CRLF)}${CRLF}${CRLF}${part.content}`;\n}\n\nfunction formatAttachmentPart(attachment: Attachment): string {\n if (!attachment.content || typeof attachment.content === \"string\") {\n throw new Error(`Attachment \"${attachment.filename}\" requires Uint8Array content`);\n }\n\n const safeFilename = sanitizeHeaderValue(attachment.filename ?? \"\");\n const headers = [\n `Content-Type: ${attachment.contentType ?? \"application/octet-stream\"}`,\n \"Content-Transfer-Encoding: base64\",\n `Content-Disposition: ${attachment.inline ? \"inline\" : \"attachment\"}; filename=\"${safeFilename}\"`,\n ];\n\n if (attachment.contentId) {\n headers.push(`Content-ID: <${attachment.contentId}>`);\n }\n\n if (attachment.headers) {\n for (const [key, value] of Object.entries(attachment.headers)) {\n const safeKey = sanitizeHeaderValue(key);\n const safeValue = sanitizeHeaderValue(value);\n headers.push(`${safeKey}: ${safeValue}`);\n }\n }\n\n return `${headers.join(CRLF)}${CRLF}${CRLF}${encodeBase64(attachment.content)}`;\n}\n\nfunction assembleMultipart(boundary: string, parts: string[]): string {\n const segments = parts.map((part) => `--${boundary}${CRLF}${part}`);\n segments.push(`--${boundary}--`);\n return segments.join(CRLF);\n}\n\nfunction generateMessageId(): string {\n const random = crypto.getRandomValues(new Uint8Array(8));\n const hex = Array.from(random, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n return `<${Date.now()}.${hex}@sently>`;\n}\n\nfunction generateBoundary(): string {\n const random = crypto.getRandomValues(new Uint8Array(12));\n const hex = Array.from(random, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n return `----sently_${hex}`;\n}\n\nfunction foldHeader(name: string, value: string): string {\n const line = `${name}: ${value}`;\n if (line.length <= 76) {\n return line;\n }\n\n const chunks: string[] = [];\n let remaining = line;\n\n while (remaining.length > 76) {\n let breakAt = remaining.lastIndexOf(\" \", 76);\n if (breakAt <= name.length + 1) {\n breakAt = 76;\n }\n chunks.push(remaining.slice(0, breakAt));\n remaining = ` ${remaining.slice(breakAt).trimStart()}`;\n }\n chunks.push(remaining);\n\n return chunks.join(`${CRLF} `);\n}\n"
|
|
6
6
|
],
|
|
7
7
|
"mappings": "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",
|
|
8
|
-
"debugId": "
|
|
8
|
+
"debugId": "C00C3770E0C105B564756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/core/errors.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{X as a,Y as b,Z as c}from"../chunk-6bkj4y4r.js";import"../chunk-nbmq8ejv.js";export{c as smtpCodeToSentlyCode,b as httpStatusToSentlyCode,a as SentlyError};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=EA669FFC2ACB394064756E2164756E21
|
package/dist/core/errors.js.map
CHANGED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Transport } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a stable provider label for observability and diagnostics.
|
|
4
|
+
* Prefers {@link Transport.provider} when set; falls back to constructor name.
|
|
5
|
+
*/
|
|
6
|
+
export declare function getProviderLabel(transport: Transport): string;
|
package/dist/core/smtp.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{H as a,I as b,J as c,K as d,L as e,M as f,N as g,O as h,P as i,Q as j,R as k,S as l}from"../chunk-8phpsns1.js";import"../chunk-5scdgffb.js";import"../chunk-6bkj4y4r.js";import"../chunk-nbmq8ejv.js";export{f as selectAuthMethod,d as parseResponse,g as parseEHLO,l as encodeLine,c as encodeCommand,j as encodeAuthLoginUser,i as encodeAuthLoginPass,k as encodeAuthCramResponse,a as computeCRAMMD5,h as assertResponse,e as accumulateResponse,b as SMTPError};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=48767988D5CC2FF564756E2164756E21
|
package/dist/core/smtp.js.map
CHANGED
package/dist/core/types.d.ts
CHANGED
|
@@ -87,6 +87,13 @@ export interface SendResult {
|
|
|
87
87
|
deduped?: boolean;
|
|
88
88
|
/** Set when this item failed inside an otherwise successful batch HTTP response. */
|
|
89
89
|
batchError?: unknown;
|
|
90
|
+
/** Identifier of the transport that handled the send; set by FallbackTransport. */
|
|
91
|
+
provider?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Zero-based index of the transport that handled the send in a fallback chain.
|
|
94
|
+
* Set by FallbackTransport (e.g. `1` means provider #0 failed and #1 succeeded).
|
|
95
|
+
*/
|
|
96
|
+
providerIndex?: number;
|
|
90
97
|
}
|
|
91
98
|
/** SMTP envelope addresses (MAIL FROM / RCPT TO). */
|
|
92
99
|
export interface Envelope {
|
|
@@ -134,6 +141,11 @@ export interface VerifyResult {
|
|
|
134
141
|
}
|
|
135
142
|
/** Pluggable mail delivery backend (SMTP, HTTP API, etc.). */
|
|
136
143
|
export interface Transport {
|
|
144
|
+
/**
|
|
145
|
+
* Stable provider identifier for observability (e.g. `"resend"`, `"ses"`).
|
|
146
|
+
* Prefer this over constructor-name inference in hooks and fallback diagnostics.
|
|
147
|
+
*/
|
|
148
|
+
readonly provider?: string;
|
|
137
149
|
/** Send a message through this transport. */
|
|
138
150
|
send(options: MailOptions): Promise<SendResult>;
|
|
139
151
|
/**
|
|
@@ -362,12 +374,23 @@ export interface MailerHookContext {
|
|
|
362
374
|
export interface MailerHooks {
|
|
363
375
|
/** Fired before the transport sends the message. */
|
|
364
376
|
onSend?: (ctx: MailerHookContext) => void | Promise<void>;
|
|
365
|
-
/**
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
377
|
+
/**
|
|
378
|
+
* Fired after a successful send.
|
|
379
|
+
* @param durationMs — elapsed milliseconds from send start to success (optional third argument).
|
|
380
|
+
*/
|
|
381
|
+
onSuccess?: (ctx: MailerHookContext, result: SendResult, durationMs?: number) => void | Promise<void>;
|
|
382
|
+
/**
|
|
383
|
+
* Fired when a send throws (error is re-thrown after the hook runs).
|
|
384
|
+
* @param durationMs — elapsed milliseconds from send start to failure (optional third argument).
|
|
385
|
+
*/
|
|
386
|
+
onError?: (ctx: MailerHookContext, error: unknown, durationMs?: number) => void | Promise<void>;
|
|
369
387
|
/** Fired before each retry attempt (requires {@link RetryTransport}). */
|
|
370
388
|
onRetry?: (ctx: MailerHookContext, attempt: number, error: unknown) => void | Promise<void>;
|
|
389
|
+
/**
|
|
390
|
+
* Fired when {@link FallbackTransport} fails over to the next provider.
|
|
391
|
+
* Requires a {@link FallbackTransport} (or {@link WeightedFallbackTransport}) in the mailer stack.
|
|
392
|
+
*/
|
|
393
|
+
onFallback?: (ctx: MailerHookContext, failedProvider: string, nextProvider: string, error: unknown) => void | Promise<void>;
|
|
371
394
|
}
|
|
372
395
|
/** Options for {@link createMailer} from `sently/mailer` — transport-only, smallest bundle. */
|
|
373
396
|
export interface TransportMailerOptions {
|
package/dist/detect.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{w as a,x as b}from"./chunk-wsymfbkn.js";import"./chunk-nbmq8ejv.js";export{a as detectRuntime,b as createDefaultAdapter};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=F3D60768456D4ED364756E2164756E21
|