@stacksjs/email 0.70.79 → 0.70.80

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/dist/index.js CHANGED
@@ -24,7 +24,7 @@ SES endpoint unreachable. The configured region (\`${X}\`) must match the region
24
24
  `);z=D.pop()||"";for(let L of D)if(L.length>=3){if(L.length===3||L[3]===" ")w(L)}},xJ=async()=>{try{await new Promise((L,S)=>{q.push({cmd:"GREETING",resolve:L,reject:S})});let O=await R(`EHLO ${x.email.domain||"localhost"}`);if(J.encryption==="starttls"&&!(W instanceof n.TLSSocket)){await R("STARTTLS");let L=W;L.removeAllListeners("data"),W=await new Promise((S,kY)=>{let a=n.connect({socket:L,host:J.host,servername:J.host},()=>{_.debug("[SMTP] TLS connection established"),S(a)});a.on("error",(s)=>{_.error("[SMTP] TLS socket error:",s),kY(s)}),a.on("data",EJ),a.on("close",(s)=>{_.debug(`[SMTP] TLS socket closed (hadError: ${s})`);while(q.length>0)q.shift()?.reject(Error("TLS connection closed unexpectedly"))})}),await R(`EHLO ${x.email.domain||"localhost"}`)}if(J.username&&J.password)await R("AUTH LOGIN"),await R(JY.from(J.username).toString("base64")),await R(JY.from(J.password).toString("base64"));YY(Y,"MAIL FROM");for(let L of Z)YY(L,"RCPT TO");await R(`MAIL FROM:<${Y}>`);for(let L of Z)await R(`RCPT TO:<${L}>`);await R("DATA"),W.write(`${X}\r
25
25
  .\r
26
26
  `),await new Promise((L,S)=>{q.push({cmd:"DATA_END",resolve:L,reject:S})}),N=!0;let D=`${Date.now()}.${Math.random().toString(36).substring(2)}@${J.host}`;try{W.write(`QUIT\r
27
- `)}catch{}W.end(),$(D)}catch(O){if(N){W.end();return}W.end(),G(O)}};if(J.encryption==="ssl")W=n.connect({host:J.host,port:J.port,servername:J.host},()=>{_.debug(`[SMTP] TLS connected to ${J.host}:${J.port}`),xJ()});else W=ZY.connect({host:J.host,port:J.port},()=>{_.debug(`[SMTP] Connected to ${J.host}:${J.port}`),xJ()});W.on("data",EJ),W.setTimeout(b.SMTP_TIMEOUT),W.on("timeout",()=>{W.destroy(Error(`SMTP socket timed out after ${b.SMTP_TIMEOUT}ms`))}),W.on("error",(O)=>{if(N)return;_.error(`[SMTP] Connection error to ${J.host}:${J.port}:`,O),G(O)}),W.on("close",(O)=>{if(_.debug(`[SMTP] Connection closed (hadError: ${O})`),N)return;while(q.length>0)q.shift()?.reject(Error("Connection closed unexpectedly"))})})}formatAddresses(J){if(!J)return[];if(typeof J==="string")return[J];return J.map((Y)=>{if(typeof Y==="string")return Y;return Y.address})}}import{db as $Y}from"@stacksjs/database";var XY=!1;function GY(){if(XY)return;XY=!0,console.warn("[email/idempotency] email_idempotency table missing \u2014 idempotency keys are accepted but NOT enforced. "+"Run migrations to enable dedup.")}function KY(J){let Y=(J?.message??"").toLowerCase();return Y.includes("no such table")||Y.includes("doesn't exist")||Y.includes("connection closed")||Y.includes("unable to open database")||Y.includes("econnrefused")||Y.includes("connection terminated")||Y.includes("no database")||Y.includes("database connection")}async function WY(J){try{let Y=await $Y.selectFrom("email_idempotency").where("idempotency_key","=",J).selectAll().executeTakeFirst();if(!Y)return null;return{success:Boolean(Y.success),message:`Idempotent replay \u2014 original send recorded ${Y.created_at}`,provider:String(Y.provider??"cache"),messageId:Y.message_id??void 0}}catch(Y){if(KY(Y))return GY(),null;throw Y}}async function QY(J,Y,Z){if(!Z.success)return;let X=Array.isArray(Y.to)?Y.to.map(($)=>typeof $==="string"?$:$.address).join(", "):typeof Y.to==="string"?Y.to:Y.to.address;try{await $Y.insertInto("email_idempotency").values({idempotency_key:J,message_id:Z.messageId??null,recipient:X,subject:Y.subject,provider:Z.provider,success:1,created_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute()}catch($){if(KY($)){GY();return}let G=$?.message??"";if(G.includes("UNIQUE constraint")||G.includes("Duplicate entry"))return;throw $}}import{db as i}from"@stacksjs/database";var FY=!1;function ZJ(){if(FY)return;FY=!0,console.warn("[email/suppression] email_suppressions table missing \u2014 suppression checks accepted but NOT enforced. "+"Run migrations to enable enforcement.")}function qZ(J){let Y=J?.message??"";return Y.includes("no such table")||Y.includes("doesn't exist")}function XJ(J){if(qZ(J))return!0;let Y=(J?.message??"").toLowerCase();return Y.includes("connection closed")||Y.includes("unable to open database")||Y.includes("econnrefused")||Y.includes("connection terminated")||Y.includes("no database")||Y.includes("database connection")}function $J(J){return String(J).trim().toLowerCase()}async function Z9(J,Y){let Z=$J(J);try{let X=i.selectFrom("email_suppressions").where("email","=",Z).select(["email"]);if(Y)X=X.where("type","=",Y);let $=await X.executeTakeFirst();return Boolean($)}catch(X){if(XJ(X))return ZJ(),!1;throw X}}async function OZ(J){let Y=$J(J);try{return await i.selectFrom("email_suppressions").where("email","=",Y).selectAll().execute()??[]}catch(Z){if(XJ(Z))return ZJ(),[];throw Z}}async function zY(J,Y,Z){let X=$J(J),$=new Date().toISOString().slice(0,19).replace("T"," ");try{await i.insertInto("email_suppressions").values({email:X,type:Y,reason:Z??null,created_at:$}).execute()}catch(G){if(XJ(G)){ZJ();return}let K=G?.message??"";if(K.includes("UNIQUE constraint")||K.includes("Duplicate entry"))return;throw G}}async function X9(J,Y){let Z=$J(J);try{await i.deleteFrom("email_suppressions").where("email","=",Z).where("type","=",Y).execute()}catch(X){if(XJ(X)){ZJ();return}throw X}}async function LZ(){try{let{config:J}=await import("@stacksjs/config"),Y=J?.email?.suppressionPolicy;if(Y==="strict"||Y==="transactional-allowed"||Y==="off")return Y}catch{}return"strict"}async function VY(J,Y){let Z=await LZ();if(Z==="off")return null;if(Z==="transactional-allowed"&&Y==="transactional")return null;let X=await OZ(J);if(X.length===0)return null;let $=["unsubscribe","complaint","bounce","manual"];for(let G of $)if(X.some((K)=>K.type===G))return G;return X[0].type}class NZ{name;subject;to;from;template;handle;onError;onSuccess;constructor(J){this.name=J.name,this.subject=J.subject,this.to=J.to,this.from=J.from,this.template=J.template,this.handle=J.handle,this.onError=J.onError,this.onSuccess=J.onSuccess}async renderTemplate(){if(!this.template)return"";try{let{path:J}=await import("@stacksjs/path"),Y=J.resourcesPath(`views/emails/${this.template}.html`),Z=Bun.file(Y);if(await Z.exists())return await Z.text()}catch{}if(this.template.includes("<"))return this.template;return`<p>${this.template}</p>`}async send(J){let Y=J??this.to,Z=Array.isArray(Y)?Y:Y?[Y]:[];if(Z.length===0)throw Error("No recipient specified for email");try{if(await GJ.send({to:Z,from:this.from||{name:u.email.from?.name||"Stacks",address:u.email.from?.address||"no-reply@stacksjs.com"},subject:this.subject,html:await this.renderTemplate()}),this.onSuccess)this.onSuccess();if(this.handle)return this.handle();return{message:"Email sent"}}catch(X){if(this.onError)return this.onError(X instanceof Error?X:Error(String(X)));throw X}}}class PJ{drivers=new Map;defaultDriver;constructor(J={}){this.defaultDriver=J.defaultDriver||u.email.default||"ses",this.registerDefaultDrivers()}registerDefaultDrivers(){this.drivers.set("log",new g),this.drivers.set("ses",new o),this.drivers.set("sendgrid",new m),this.drivers.set("mailgun",new d),this.drivers.set("mailtrap",new l),this.drivers.set("smtp",new b),this.drivers.set("capture",new e)}async send(J){let Y=this.drivers.get(this.defaultDriver);if(!Y){let G=[...this.drivers.keys()].sort().join(", ");throw Error(`Email driver '${this.defaultDriver}' is not registered. Available drivers: [${G}]. Check config.email.default or the MAIL_MAILER environment variable.`)}if(J.idempotencyKey){let G=await WY(J.idempotencyKey);if(G)return G}let Z=await IZ(J);if(Z)return{success:!1,message:`suppressed:${Z}`,provider:"suppression"};let X={name:u.email.from?.name||"Stacks",address:u.email.from?.address||"no-reply@stacksjs.com"},$=await Y.send({...J,from:J.from||X});if(J.idempotencyKey)await QY(J.idempotencyKey,J,$);return $}use(J){if(!this.drivers.has(J))throw Error(`Email driver '${J}' is not available`);return new PJ({defaultDriver:J})}async queue(J){await this.dispatchOrFallback(J,async()=>{let{job:Y}=await import("@stacksjs/queue");await Y("SendEmail",{message:J,driver:this.defaultDriver}).onQueue("emails").dispatch()},{context:"queue"})}async later(J,Y){await this.dispatchOrFallback(Y,async()=>{let{job:Z}=await import("@stacksjs/queue");await Z("SendEmail",{message:Y,driver:this.defaultDriver}).onQueue("emails").delay(J).dispatch()},{context:"later",delaySeconds:J})}async queueOn(J,Y){await this.dispatchOrFallback(Y,async()=>{let{job:Z}=await import("@stacksjs/queue");await Z("SendEmail",{message:Y,driver:this.defaultDriver}).onQueue(J).dispatch()},{context:"queueOn",queueName:J})}async dispatchOrFallback(J,Y,Z){try{await Y();return}catch(X){let $=X instanceof Error?X.message:String(X);UZ.warn("[email] Queue dispatch failed; falling back to synchronous send. "+"Background email pipeline is degraded \u2014 check the queue worker / broker.",{...Z,reason:$})}await this.send(J)}}async function IZ(J){let Y=HZ(J);if(Y.length===0)return null;for(let Z of Y){let X=await VY(Z,J.tag);if(X)return X}return null}function HZ(J){let Y=[],Z=(X)=>{if(typeof X==="string"){Y.push(X);return}if(X&&typeof X==="object"&&"address"in X&&typeof X.address==="string")Y.push(X.address)};for(let X of["to","cc","bcc"]){let $=J[X];if(!$)continue;if(Array.isArray($))for(let G of $)Z(G);else Z($)}return Y}var BJ;function qY(){if(!BJ){let J=u?.email?.default||process.env.MAIL_MAILER||"ses";BJ=new PJ({defaultDriver:J})}return BJ}var GJ=new Proxy({},{get(J,Y){return qY()[Y]},set(J,Y,Z){return qY()[Y]=Z,!0}});import{createHmac as UY,timingSafeEqual as RZ}from"crypto";import KJ from"process";import{Buffer as NY}from"buffer";var _Z=2592000,BZ="/_stacks/email/unsubscribe";function IY(){let J=KJ.env.APP_KEY;if(!J||J.length<16){if(KJ.env.APP_ENV==="production"||KJ.env.NODE_ENV==="production")throw Error("[email/unsubscribe] APP_KEY is missing or too short (need \u226516 chars). Cannot sign unsubscribe URL.")}return J||"stacks-default-key-dev-only-do-not-use-prod"}function OY(J){return J.toString("base64url")}function LY(J){return NY.from(J,"base64url")}function PZ(J,Y=_Z){if(!J)throw Error("[email/unsubscribe] email is required");let Z=Math.floor(Date.now()/1000)+Math.floor(Y),X={email:String(J).trim().toLowerCase(),exp:Z,iss:"stacks"},$=OY(NY.from(JSON.stringify(X))),G=OY(UY("sha256",IY()).update($).digest());return`${$}.${G}`}function _9(J){if(typeof J!=="string")return{valid:!1,reason:"malformed"};let Y=J.split(".");if(Y.length!==2)return{valid:!1,reason:"malformed"};let[Z,X]=Y,$=UY("sha256",IY()).update(Z).digest(),G;try{G=LY(X)}catch{return{valid:!1,reason:"malformed"}}if(G.length!==$.length||!RZ(G,$))return{valid:!1,reason:"bad_signature"};let K;try{K=JSON.parse(LY(Z).toString("utf8"))}catch{return{valid:!1,reason:"malformed"}}if(typeof K.exp!=="number"||Math.floor(Date.now()/1000)>=K.exp)return{valid:!1,reason:"expired"};if(!K.email||typeof K.email!=="string")return{valid:!1,reason:"malformed"};return{valid:!0,email:K.email}}function jZ(J,Y,Z={}){let X=PZ(J,Y),$=(Z.baseUrl||KJ.env.APP_URL||"http://localhost").replace(/\/$/,""),G=(Z.routePrefix||BZ).replace(/\/$/,"");return`${$}${G}/${X}`}function B9(J,Y,Z){return{"List-Unsubscribe":`<${jZ(J,Y,Z)}>`,"List-Unsubscribe-Post":"List-Unsubscribe=One-Click"}}import{db as DZ}from"@stacksjs/database";var HY=!1;function TZ(){if(HY)return;HY=!0,console.warn("[email/webhook-dedup] email_webhook_events table missing \u2014 webhook idempotency NOT enforced. "+"Providers may double-deliver retries; run migrations to enable dedup.")}function AZ(J){let Y=J?.message??"";return Y.includes("no such table")||Y.includes("doesn't exist")}async function r(J,Y){if(!Y)return!0;try{return await DZ.insertInto("email_webhook_events").values({provider:J,event_id:Y,processed_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute(),!0}catch(Z){if(AZ(Z))return TZ(),!0;let X=Z?.message??"";if(X.includes("UNIQUE constraint")||X.includes("Duplicate entry"))return!1;throw Z}}async function p(J,Y){try{let Z=await import("@stacksjs/events").catch(()=>null);if(!Z)return;let X=Z.dispatch;if(typeof X!=="function")return;X(J,Y)}catch{}}async function RY(J){await p("email:bounce-hard",J),await p("email:bounce",J)}async function _Y(J){await p("email:bounce-soft",J),await p("email:bounce",J)}async function BY(J){await p("email:complaint",J)}async function PY(J){await p("email:unsubscribe",J)}function jY(J){switch(J){case"bounce-hard":return"bounce";case"complaint":return"complaint";case"unsubscribe":return"unsubscribe";default:return null}}import{createHmac as EZ,createVerify as TY,timingSafeEqual as jJ}from"crypto";import{Buffer as M}from"buffer";function AY(J){if(!J.signingKey)return{ok:!1,reason:"missing-config"};if(!J.timestamp||!J.token||!J.signature)return{ok:!1,reason:"missing-signature"};let Y=J.toleranceSeconds??300,Z=Number(J.timestamp);if(!Number.isFinite(Z))return{ok:!1,reason:"bad-signature"};let X=Math.floor(Date.now()/1000);if(Math.abs(X-Z)>Y)return{ok:!1,reason:"expired"};let $=EZ("sha256",J.signingKey).update(`${J.timestamp}${J.token}`).digest("hex"),G;try{G=M.from(J.signature,"hex")}catch{return{ok:!1,reason:"bad-signature"}}let K=M.from($,"hex");if(G.length!==K.length)return{ok:!1,reason:"bad-signature"};if(!jJ(G,K))return{ok:!1,reason:"bad-signature"};return{ok:!0}}function EY(J){if(!J.expectedUsername||!J.expectedPassword)return{ok:!1,reason:"missing-config"};if(!J.authorizationHeader||!J.authorizationHeader.startsWith("Basic "))return{ok:!1,reason:"missing-signature"};let Y;try{Y=M.from(J.authorizationHeader.slice(6),"base64").toString("utf8")}catch{return{ok:!1,reason:"bad-signature"}}let[Z,X]=Y.split(":");if(!Z||X===void 0)return{ok:!1,reason:"bad-signature"};let $=DY(Z,J.expectedUsername),G=DY(X,J.expectedPassword);if(!$||!G)return{ok:!1,reason:"bad-signature"};if(J.ipAllowlist&&J.ipAllowlist.length>0){if(!J.sourceIp||!J.ipAllowlist.includes(J.sourceIp))return{ok:!1,reason:"bad-signature"}}return{ok:!0}}function DY(J,Y){let Z=M.from(J,"utf8"),X=M.from(Y,"utf8");if(Z.length!==X.length){let $=M.alloc(Math.max(Z.length,X.length));return jJ($,$),!1}return jJ(Z,X)}var xZ=/^sns\.[a-z0-9-]+\.amazonaws\.com$/;async function MZ(J){let Y=await fetch(J,{redirect:"error"});if(!Y.ok)throw Error(`SNS cert fetch returned ${Y.status}`);return await Y.text()}async function xY(J){let Y=J.message;if(!Y||!Y.Signature||!Y.SigningCertURL)return{ok:!1,reason:"missing-signature"};let Z=J.certUrlHostAllowlist??xZ,X;try{X=new URL(Y.SigningCertURL)}catch{return{ok:!1,reason:"untrusted-cert-url"}}if(X.protocol!=="https:")return{ok:!1,reason:"untrusted-cert-url"};if(!Z.test(X.host))return{ok:!1,reason:"untrusted-cert-url"};let $;try{$=await(J.fetchCert??MZ)(Y.SigningCertURL)}catch{return{ok:!1,reason:"cert-fetch-failed"}}let G=CZ(Y);if(!G)return{ok:!1,reason:"bad-signature"};let K;try{K=M.from(Y.Signature,"base64")}catch{return{ok:!1,reason:"bad-signature"}}let Q=Y.SignatureVersion==="2"?"SHA256":"SHA1",F=TY(`RSA-${Q}`);return F.update(G,"utf8"),F.verify($,K)?{ok:!0}:{ok:!1,reason:"bad-signature"}}function CZ(J){let Y=[];if(J.Type==="Notification"){if(Y.push("Message",J.Message),Y.push("MessageId",J.MessageId),J.Subject!==void 0)Y.push("Subject",J.Subject);Y.push("Timestamp",J.Timestamp),Y.push("TopicArn",J.TopicArn),Y.push("Type",J.Type)}else if(J.Type==="SubscriptionConfirmation"||J.Type==="UnsubscribeConfirmation"){if(Y.push("Message",J.Message),Y.push("MessageId",J.MessageId),!J.SubscribeURL||!J.Token)return null;Y.push("SubscribeURL",J.SubscribeURL),Y.push("Timestamp",J.Timestamp),Y.push("Token",J.Token),Y.push("TopicArn",J.TopicArn),Y.push("Type",J.Type)}else return null;let Z="";for(let X of Y)Z+=`${X}
27
+ `)}catch{}W.end(),$(D)}catch(O){if(N){W.end();return}W.end(),G(O)}};if(J.encryption==="ssl")W=n.connect({host:J.host,port:J.port,servername:J.host},()=>{_.debug(`[SMTP] TLS connected to ${J.host}:${J.port}`),xJ()});else W=ZY.connect({host:J.host,port:J.port},()=>{_.debug(`[SMTP] Connected to ${J.host}:${J.port}`),xJ()});W.on("data",EJ),W.setTimeout(b.SMTP_TIMEOUT),W.on("timeout",()=>{W.destroy(Error(`SMTP socket timed out after ${b.SMTP_TIMEOUT}ms`))}),W.on("error",(O)=>{if(N)return;_.error(`[SMTP] Connection error to ${J.host}:${J.port}:`,O),G(O)}),W.on("close",(O)=>{if(_.debug(`[SMTP] Connection closed (hadError: ${O})`),N)return;while(q.length>0)q.shift()?.reject(Error("Connection closed unexpectedly"))})})}formatAddresses(J){if(!J)return[];if(typeof J==="string")return[J];return J.map((Y)=>{if(typeof Y==="string")return Y;return Y.address})}}import{db as $Y}from"@stacksjs/database";var XY=!1;function GY(){if(XY)return;XY=!0,console.warn("[email/idempotency] email_idempotency table missing \u2014 idempotency keys are accepted but NOT enforced. "+"Run migrations to enable dedup.")}function KY(J){let Y=(J?.message??"").toLowerCase();return Y.includes("no such table")||Y.includes("doesn't exist")||Y.includes("connection closed")||Y.includes("unable to open database")||Y.includes("econnrefused")||Y.includes("connection terminated")||Y.includes("no database")||Y.includes("database connection")}async function WY(J){try{let Y=await $Y.selectFrom("email_idempotency").where("idempotency_key","=",J).selectAll().executeTakeFirst();if(!Y)return null;return{success:Boolean(Y.success),message:`Idempotent replay \u2014 original send recorded ${Y.created_at}`,provider:String(Y.provider??"cache"),messageId:Y.message_id??void 0}}catch(Y){if(KY(Y))return GY(),null;throw Y}}async function QY(J,Y,Z){if(!Z.success)return;let X=Array.isArray(Y.to)?Y.to.map(($)=>typeof $==="string"?$:$.address).join(", "):typeof Y.to==="string"?Y.to:Y.to.address;try{await $Y.insertInto("email_idempotency").values({idempotency_key:J,message_id:Z.messageId??null,recipient:X,subject:Y.subject,provider:Z.provider,success:1,created_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute()}catch($){if(KY($)){GY();return}let G=$?.message??"";if(G.includes("UNIQUE constraint")||G.includes("Duplicate entry"))return;throw $}}import{db as i}from"@stacksjs/database";var FY=!1;function ZJ(){if(FY)return;FY=!0,console.warn("[email/suppression] email_suppressions table missing \u2014 suppression checks accepted but NOT enforced. "+"Run migrations to enable enforcement.")}function qZ(J){let Y=J,Z=Y?.message??"";return Y?.code==="42P01"||Z.includes("no such table")||Z.includes("doesn't exist")||/relation "[^"]*" does not exist/i.test(Z)}function XJ(J){if(qZ(J))return!0;let Y=(J?.message??"").toLowerCase();return Y.includes("connection closed")||Y.includes("unable to open database")||Y.includes("econnrefused")||Y.includes("connection terminated")||Y.includes("no database")||Y.includes("database connection")}function $J(J){return String(J).trim().toLowerCase()}async function Z9(J,Y){let Z=$J(J);try{let X=i.selectFrom("email_suppressions").where("email","=",Z).select(["email"]);if(Y)X=X.where("type","=",Y);let $=await X.executeTakeFirst();return Boolean($)}catch(X){if(XJ(X))return ZJ(),!1;throw X}}async function OZ(J){let Y=$J(J);try{return await i.selectFrom("email_suppressions").where("email","=",Y).selectAll().execute()??[]}catch(Z){if(XJ(Z))return ZJ(),[];throw Z}}async function zY(J,Y,Z){let X=$J(J),$=new Date().toISOString().slice(0,19).replace("T"," ");try{await i.insertInto("email_suppressions").values({email:X,type:Y,reason:Z??null,created_at:$}).execute()}catch(G){if(XJ(G)){ZJ();return}let K=G?.message??"";if(K.includes("UNIQUE constraint")||K.includes("Duplicate entry"))return;throw G}}async function X9(J,Y){let Z=$J(J);try{await i.deleteFrom("email_suppressions").where("email","=",Z).where("type","=",Y).execute()}catch(X){if(XJ(X)){ZJ();return}throw X}}async function LZ(){try{let{config:J}=await import("@stacksjs/config"),Y=J?.email?.suppressionPolicy;if(Y==="strict"||Y==="transactional-allowed"||Y==="off")return Y}catch{}return"strict"}async function VY(J,Y){let Z=await LZ();if(Z==="off")return null;if(Z==="transactional-allowed"&&Y==="transactional")return null;let X=await OZ(J);if(X.length===0)return null;let $=["unsubscribe","complaint","bounce","manual"];for(let G of $)if(X.some((K)=>K.type===G))return G;return X[0].type}class NZ{name;subject;to;from;template;handle;onError;onSuccess;constructor(J){this.name=J.name,this.subject=J.subject,this.to=J.to,this.from=J.from,this.template=J.template,this.handle=J.handle,this.onError=J.onError,this.onSuccess=J.onSuccess}async renderTemplate(){if(!this.template)return"";try{let{path:J}=await import("@stacksjs/path"),Y=J.resourcesPath(`views/emails/${this.template}.html`),Z=Bun.file(Y);if(await Z.exists())return await Z.text()}catch{}if(this.template.includes("<"))return this.template;return`<p>${this.template}</p>`}async send(J){let Y=J??this.to,Z=Array.isArray(Y)?Y:Y?[Y]:[];if(Z.length===0)throw Error("No recipient specified for email");try{if(await GJ.send({to:Z,from:this.from||{name:u.email.from?.name||"Stacks",address:u.email.from?.address||"no-reply@stacksjs.com"},subject:this.subject,html:await this.renderTemplate()}),this.onSuccess)this.onSuccess();if(this.handle)return this.handle();return{message:"Email sent"}}catch(X){if(this.onError)return this.onError(X instanceof Error?X:Error(String(X)));throw X}}}class PJ{drivers=new Map;defaultDriver;constructor(J={}){this.defaultDriver=J.defaultDriver||u.email.default||"ses",this.registerDefaultDrivers()}registerDefaultDrivers(){this.drivers.set("log",new g),this.drivers.set("ses",new o),this.drivers.set("sendgrid",new m),this.drivers.set("mailgun",new d),this.drivers.set("mailtrap",new l),this.drivers.set("smtp",new b),this.drivers.set("capture",new e)}async send(J){let Y=this.drivers.get(this.defaultDriver);if(!Y){let G=[...this.drivers.keys()].sort().join(", ");throw Error(`Email driver '${this.defaultDriver}' is not registered. Available drivers: [${G}]. Check config.email.default or the MAIL_MAILER environment variable.`)}if(J.idempotencyKey){let G=await WY(J.idempotencyKey);if(G)return G}let Z=await IZ(J);if(Z)return{success:!1,message:`suppressed:${Z}`,provider:"suppression"};let X={name:u.email.from?.name||"Stacks",address:u.email.from?.address||"no-reply@stacksjs.com"},$=await Y.send({...J,from:J.from||X});if(J.idempotencyKey)await QY(J.idempotencyKey,J,$);return $}use(J){if(!this.drivers.has(J))throw Error(`Email driver '${J}' is not available`);return new PJ({defaultDriver:J})}async queue(J){await this.dispatchOrFallback(J,async()=>{let{job:Y}=await import("@stacksjs/queue");await Y("SendEmail",{message:J,driver:this.defaultDriver}).onQueue("emails").dispatch()},{context:"queue"})}async later(J,Y){await this.dispatchOrFallback(Y,async()=>{let{job:Z}=await import("@stacksjs/queue");await Z("SendEmail",{message:Y,driver:this.defaultDriver}).onQueue("emails").delay(J).dispatch()},{context:"later",delaySeconds:J})}async queueOn(J,Y){await this.dispatchOrFallback(Y,async()=>{let{job:Z}=await import("@stacksjs/queue");await Z("SendEmail",{message:Y,driver:this.defaultDriver}).onQueue(J).dispatch()},{context:"queueOn",queueName:J})}async dispatchOrFallback(J,Y,Z){try{await Y();return}catch(X){let $=X instanceof Error?X.message:String(X);UZ.warn("[email] Queue dispatch failed; falling back to synchronous send. "+"Background email pipeline is degraded \u2014 check the queue worker / broker.",{...Z,reason:$})}await this.send(J)}}async function IZ(J){let Y=HZ(J);if(Y.length===0)return null;for(let Z of Y){let X=await VY(Z,J.tag);if(X)return X}return null}function HZ(J){let Y=[],Z=(X)=>{if(typeof X==="string"){Y.push(X);return}if(X&&typeof X==="object"&&"address"in X&&typeof X.address==="string")Y.push(X.address)};for(let X of["to","cc","bcc"]){let $=J[X];if(!$)continue;if(Array.isArray($))for(let G of $)Z(G);else Z($)}return Y}var BJ;function qY(){if(!BJ){let J=u?.email?.default||process.env.MAIL_MAILER||"ses";BJ=new PJ({defaultDriver:J})}return BJ}var GJ=new Proxy({},{get(J,Y){return qY()[Y]},set(J,Y,Z){return qY()[Y]=Z,!0}});import{createHmac as UY,timingSafeEqual as RZ}from"crypto";import KJ from"process";import{Buffer as NY}from"buffer";var _Z=2592000,BZ="/_stacks/email/unsubscribe";function IY(){let J=KJ.env.APP_KEY;if(!J||J.length<16){if(KJ.env.APP_ENV==="production"||KJ.env.NODE_ENV==="production")throw Error("[email/unsubscribe] APP_KEY is missing or too short (need \u226516 chars). Cannot sign unsubscribe URL.")}return J||"stacks-default-key-dev-only-do-not-use-prod"}function OY(J){return J.toString("base64url")}function LY(J){return NY.from(J,"base64url")}function PZ(J,Y=_Z){if(!J)throw Error("[email/unsubscribe] email is required");let Z=Math.floor(Date.now()/1000)+Math.floor(Y),X={email:String(J).trim().toLowerCase(),exp:Z,iss:"stacks"},$=OY(NY.from(JSON.stringify(X))),G=OY(UY("sha256",IY()).update($).digest());return`${$}.${G}`}function _9(J){if(typeof J!=="string")return{valid:!1,reason:"malformed"};let Y=J.split(".");if(Y.length!==2)return{valid:!1,reason:"malformed"};let[Z,X]=Y,$=UY("sha256",IY()).update(Z).digest(),G;try{G=LY(X)}catch{return{valid:!1,reason:"malformed"}}if(G.length!==$.length||!RZ(G,$))return{valid:!1,reason:"bad_signature"};let K;try{K=JSON.parse(LY(Z).toString("utf8"))}catch{return{valid:!1,reason:"malformed"}}if(typeof K.exp!=="number"||Math.floor(Date.now()/1000)>=K.exp)return{valid:!1,reason:"expired"};if(!K.email||typeof K.email!=="string")return{valid:!1,reason:"malformed"};return{valid:!0,email:K.email}}function jZ(J,Y,Z={}){let X=PZ(J,Y),$=(Z.baseUrl||KJ.env.APP_URL||"http://localhost").replace(/\/$/,""),G=(Z.routePrefix||BZ).replace(/\/$/,"");return`${$}${G}/${X}`}function B9(J,Y,Z){return{"List-Unsubscribe":`<${jZ(J,Y,Z)}>`,"List-Unsubscribe-Post":"List-Unsubscribe=One-Click"}}import{db as DZ}from"@stacksjs/database";var HY=!1;function TZ(){if(HY)return;HY=!0,console.warn("[email/webhook-dedup] email_webhook_events table missing \u2014 webhook idempotency NOT enforced. "+"Providers may double-deliver retries; run migrations to enable dedup.")}function AZ(J){let Y=J?.message??"";return Y.includes("no such table")||Y.includes("doesn't exist")}async function r(J,Y){if(!Y)return!0;try{return await DZ.insertInto("email_webhook_events").values({provider:J,event_id:Y,processed_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute(),!0}catch(Z){if(AZ(Z))return TZ(),!0;let X=Z?.message??"";if(X.includes("UNIQUE constraint")||X.includes("Duplicate entry"))return!1;throw Z}}async function p(J,Y){try{let Z=await import("@stacksjs/events").catch(()=>null);if(!Z)return;let X=Z.dispatch;if(typeof X!=="function")return;X(J,Y)}catch{}}async function RY(J){await p("email:bounce-hard",J),await p("email:bounce",J)}async function _Y(J){await p("email:bounce-soft",J),await p("email:bounce",J)}async function BY(J){await p("email:complaint",J)}async function PY(J){await p("email:unsubscribe",J)}function jY(J){switch(J){case"bounce-hard":return"bounce";case"complaint":return"complaint";case"unsubscribe":return"unsubscribe";default:return null}}import{createHmac as EZ,createVerify as TY,timingSafeEqual as jJ}from"crypto";import{Buffer as M}from"buffer";function AY(J){if(!J.signingKey)return{ok:!1,reason:"missing-config"};if(!J.timestamp||!J.token||!J.signature)return{ok:!1,reason:"missing-signature"};let Y=J.toleranceSeconds??300,Z=Number(J.timestamp);if(!Number.isFinite(Z))return{ok:!1,reason:"bad-signature"};let X=Math.floor(Date.now()/1000);if(Math.abs(X-Z)>Y)return{ok:!1,reason:"expired"};let $=EZ("sha256",J.signingKey).update(`${J.timestamp}${J.token}`).digest("hex"),G;try{G=M.from(J.signature,"hex")}catch{return{ok:!1,reason:"bad-signature"}}let K=M.from($,"hex");if(G.length!==K.length)return{ok:!1,reason:"bad-signature"};if(!jJ(G,K))return{ok:!1,reason:"bad-signature"};return{ok:!0}}function EY(J){if(!J.expectedUsername||!J.expectedPassword)return{ok:!1,reason:"missing-config"};if(!J.authorizationHeader||!J.authorizationHeader.startsWith("Basic "))return{ok:!1,reason:"missing-signature"};let Y;try{Y=M.from(J.authorizationHeader.slice(6),"base64").toString("utf8")}catch{return{ok:!1,reason:"bad-signature"}}let[Z,X]=Y.split(":");if(!Z||X===void 0)return{ok:!1,reason:"bad-signature"};let $=DY(Z,J.expectedUsername),G=DY(X,J.expectedPassword);if(!$||!G)return{ok:!1,reason:"bad-signature"};if(J.ipAllowlist&&J.ipAllowlist.length>0){if(!J.sourceIp||!J.ipAllowlist.includes(J.sourceIp))return{ok:!1,reason:"bad-signature"}}return{ok:!0}}function DY(J,Y){let Z=M.from(J,"utf8"),X=M.from(Y,"utf8");if(Z.length!==X.length){let $=M.alloc(Math.max(Z.length,X.length));return jJ($,$),!1}return jJ(Z,X)}var xZ=/^sns\.[a-z0-9-]+\.amazonaws\.com$/;async function MZ(J){let Y=await fetch(J,{redirect:"error"});if(!Y.ok)throw Error(`SNS cert fetch returned ${Y.status}`);return await Y.text()}async function xY(J){let Y=J.message;if(!Y||!Y.Signature||!Y.SigningCertURL)return{ok:!1,reason:"missing-signature"};let Z=J.certUrlHostAllowlist??xZ,X;try{X=new URL(Y.SigningCertURL)}catch{return{ok:!1,reason:"untrusted-cert-url"}}if(X.protocol!=="https:")return{ok:!1,reason:"untrusted-cert-url"};if(!Z.test(X.host))return{ok:!1,reason:"untrusted-cert-url"};let $;try{$=await(J.fetchCert??MZ)(Y.SigningCertURL)}catch{return{ok:!1,reason:"cert-fetch-failed"}}let G=CZ(Y);if(!G)return{ok:!1,reason:"bad-signature"};let K;try{K=M.from(Y.Signature,"base64")}catch{return{ok:!1,reason:"bad-signature"}}let Q=Y.SignatureVersion==="2"?"SHA256":"SHA1",F=TY(`RSA-${Q}`);return F.update(G,"utf8"),F.verify($,K)?{ok:!0}:{ok:!1,reason:"bad-signature"}}function CZ(J){let Y=[];if(J.Type==="Notification"){if(Y.push("Message",J.Message),Y.push("MessageId",J.MessageId),J.Subject!==void 0)Y.push("Subject",J.Subject);Y.push("Timestamp",J.Timestamp),Y.push("TopicArn",J.TopicArn),Y.push("Type",J.Type)}else if(J.Type==="SubscriptionConfirmation"||J.Type==="UnsubscribeConfirmation"){if(Y.push("Message",J.Message),Y.push("MessageId",J.MessageId),!J.SubscribeURL||!J.Token)return null;Y.push("SubscribeURL",J.SubscribeURL),Y.push("Timestamp",J.Timestamp),Y.push("Token",J.Token),Y.push("TopicArn",J.TopicArn),Y.push("Type",J.Type)}else return null;let Z="";for(let X of Y)Z+=`${X}
28
28
  `;return Z}function MY(J){if(!J.publicKeyPem)return{ok:!1,reason:"missing-config"};if(!J.signature||!J.timestamp)return{ok:!1,reason:"missing-signature"};let Y=J.toleranceSeconds??300,Z=Number(J.timestamp);if(!Number.isFinite(Z))return{ok:!1,reason:"bad-signature"};if(Math.abs(Math.floor(Date.now()/1000)-Z)>Y)return{ok:!1,reason:"expired"};let X;try{X=M.from(J.signature,"base64")}catch{return{ok:!1,reason:"bad-signature"}}let $=TY("SHA256");return $.update(`${J.timestamp}${J.body}`,"utf8"),$.verify(J.publicKeyPem,X)?{ok:!0}:{ok:!1,reason:"bad-signature"}}var DJ={status:200,body:{ok:!0,processed:!1,reason:"duplicate"}};function WJ(J){return{status:401,body:{ok:!1,reason:J}}}function C(J){return{status:400,body:{ok:!1,reason:J}}}async function c(J,Y){let Z=jY(J);if(Z)await zY(Y.email,Z,Y.reason);switch(J){case"bounce-hard":await RY(Y);break;case"bounce-soft":await _Y(Y);break;case"complaint":await BY(Y);break;case"unsubscribe":await PY(Y);break;case"delivered":break}}async function k9(J,Y){let Z;try{Z=JSON.parse(J)}catch{return C("invalid-json")}let X=Z.signature,$=Z["event-data"];if(!X||!$)return C("missing-fields");let G=AY({timestamp:X.timestamp,token:X.token,signature:X.signature,signingKey:Y.signingKey,toleranceSeconds:Y.toleranceSeconds});if(!G.ok)return WJ(G.reason);if(!await r("mailgun",$.id))return DJ;let Q=wZ($.event,$.severity);if(!Q)return{status:200,body:{ok:!0,processed:!1,reason:"unhandled-event"}};return await c(Q,{email:$.recipient,provider:"mailgun",reason:$.reason,raw:$}),{status:200,body:{ok:!0,processed:!0,classification:Q}}}function wZ(J,Y){switch(J){case"failed":return Y==="temporary"?"bounce-soft":"bounce-hard";case"complained":return"complaint";case"unsubscribed":return"unsubscribe";case"delivered":return"delivered";default:return null}}async function y9(J,Y,Z,X){let $=EY({authorizationHeader:Y,expectedUsername:X.username,expectedPassword:X.password,sourceIp:Z,ipAllowlist:X.ipAllowlist});if(!$.ok)return WJ($.reason);let G;try{G=JSON.parse(J)}catch{return C("invalid-json")}let K=String(G.ID??G.MessageID??""),Q=String(G.Email??G.Recipient??"");if(!Q)return C("missing-recipient");if(!await r("postmark",K))return DJ;let W=SZ(G);if(!W)return{status:200,body:{ok:!0,processed:!1,reason:"unhandled-event"}};return await c(W,{email:Q,provider:"postmark",reason:G.Description,raw:G}),{status:200,body:{ok:!0,processed:!0,classification:W}}}function SZ(J){switch(J.RecordType){case"Bounce":return J.TypeCode===1?"bounce-hard":"bounce-soft";case"SpamComplaint":return"complaint";case"SubscriptionChange":return J.SuppressSending?"unsubscribe":null;case"Delivery":return"delivered";default:return null}}async function b9(J,Y={}){let Z;try{Z=JSON.parse(J)}catch{return C("invalid-json")}let X=await xY({message:Z,certUrlHostAllowlist:Y.certUrlHostAllowlist,fetchCert:Y.fetchCert});if(!X.ok)return WJ(X.reason);if(Z.Type==="SubscriptionConfirmation"){if(Y.autoConfirmSubscriptions!==!1&&Z.SubscribeURL)try{await fetch(Z.SubscribeURL)}catch{}return{status:200,body:{ok:!0,processed:!0,reason:"subscription-confirmed"}}}if(Z.Type==="UnsubscribeConfirmation")return{status:200,body:{ok:!0,processed:!0,reason:"subscription-removed"}};let $;try{$=JSON.parse(Z.Message)}catch{return C("invalid-inner-json")}if(!await r("ses",Z.MessageId))return DJ;let K=[];if($.notificationType==="Bounce"&&$.bounce){let Q=$.bounce.bounceType==="Permanent"?"bounce-hard":"bounce-soft";for(let F of $.bounce.bouncedRecipients)await c(Q,{email:F.emailAddress,provider:"ses",reason:F.diagnosticCode,raw:$}),K.push(Q)}else if($.notificationType==="Complaint"&&$.complaint)for(let Q of $.complaint.complainedRecipients)await c("complaint",{email:Q.emailAddress,provider:"ses",reason:$.complaint.complaintFeedbackType,raw:$}),K.push("complaint");else if($.notificationType==="Delivery"&&$.delivery)for(let Q of $.delivery.recipients)await c("delivered",{email:Q,provider:"ses",raw:$}),K.push("delivered");return{status:200,body:{ok:!0,processed:K.length>0,classification:K[0]}}}async function v9(J,Y,Z,X){let $=MY({body:J,signature:Y,timestamp:Z,publicKeyPem:X.publicKeyPem,toleranceSeconds:X.toleranceSeconds});if(!$.ok)return WJ($.reason);let G;try{G=JSON.parse(J)}catch{return C("invalid-json")}if(!Array.isArray(G))return C("expected-array");let K=0,Q=[];for(let F of G){if(!F.email)continue;let W=F.sg_event_id??`${F.event}:${F.email}:${Date.now()}`;if(!await r("sendgrid",W))continue;let V=kZ(F.event,F.type);if(!V)continue;await c(V,{email:F.email,provider:"sendgrid",reason:F.reason,raw:F}),K++,Q.push(V)}return{status:200,body:{ok:!0,processed:K>0,classification:Q[0]}}}function kZ(J,Y){switch(J){case"bounce":return Y==="blocked"?"bounce-soft":"bounce-hard";case"dropped":return"bounce-hard";case"spamreport":return"complaint";case"unsubscribe":return"unsubscribe";case"group_unsubscribe":return"unsubscribe";case"delivered":return"delivered";default:return null}}import{config as CY}from"@stacksjs/config";class QJ{_to=[];_cc=[];_bcc=[];_replyTo;_from;_subject;_text;_html;_template;_attachments=[];to(J){return this._to=TJ(J),this}cc(J){return this._cc=TJ(J),this}bcc(J){return this._bcc=TJ(J),this}replyTo(J){return this._replyTo=typeof J==="string"?{address:J}:J,this}from(J){return this._from=J,this}subject(J){return this._subject=J,this}text(J){return this._text=J,this}html(J){return this._html=J,this}template(J,...Y){let Z=Y[0]??{};return this._template={name:J,props:Z},this}inspect(){return{to:this._to,cc:this._cc,bcc:this._bcc,from:this._from,replyTo:this._replyTo,subject:this._subject,text:this._text,html:this._html,template:this._template,attachments:this._attachments}}attach(J,Y){return this._attachments.push({filename:Y||bZ(J),content:`__file__:${J}`,encoding:"binary"}),this}attachData(J,Y,Z){return this._attachments.push({filename:Y,content:J,contentType:Z,encoding:typeof J==="string"?"utf8":"binary"}),this}async send(J={}){if(await Promise.resolve(this.build()),this._to.length===0)throw Error("[Mailable] no recipients \u2014 call this.to(...) inside build()");if(!this._subject)throw Error("[Mailable] no subject \u2014 call this.subject(...) inside build()");let Y=this._html,Z=this._text;if(this._template){let G=await I(this._template.name,{variables:this._template.props,subject:this._subject});if(!Y)Y=G.html;if(!Z)Z=G.text}let X={to:this._to,subject:this._subject,from:this._from||yZ(),...this._cc.length?{cc:this._cc}:{},...this._bcc.length?{bcc:this._bcc}:{},...Y?{html:Y}:{},...Z?{text:Z}:{},...this._attachments.length?{attachments:await vZ(this._attachments)}:{}};if(this._replyTo)X.replyTo=this._replyTo;return(J.driver?GJ.use(J.driver):GJ).send(X)}}function TJ(J){let Y=Array.isArray(J)?J:[J];if(Y.some((X)=>typeof X==="object"&&X!==null))return Y.map((X)=>typeof X==="string"?{address:X}:X);return Y}function yZ(){return{name:CY.email.from?.name||"Stacks",address:CY.email.from?.address||"no-reply@stacksjs.com"}}function bZ(J){let Y=Math.max(J.lastIndexOf("/"),J.lastIndexOf("\\"));return Y===-1?J:J.slice(Y+1)}async function vZ(J){return Promise.all(J.map(async(Y)=>{if(typeof Y.content==="string"&&Y.content.startsWith("__file__:")){let Z=Y.content.slice(9),X=Bun.file(Z),$=new Uint8Array(await X.arrayBuffer());return{...Y,content:$,contentType:Y.contentType||X.type||void 0,encoding:"binary"}}return Y}))}import{readdirSync as hZ}from"fs";import{join as fZ}from"path";import{existsSync as wY}from"@stacksjs/storage";import{userEmailsPath as uZ,userMailPath as pZ}from"@stacksjs/path";import{kebabCase as cZ}from"@stacksjs/strings";function gZ(){let J=pZ();if(!wY(J))return[];let Y=hZ(J,{withFileTypes:!0}),Z=[];for(let X of Y){if(!X.isFile())continue;if(!X.name.endsWith(".ts"))continue;if(X.name.endsWith(".d.ts"))continue;if(X.name.endsWith(".test.ts"))continue;if(X.name.endsWith(".spec.ts"))continue;let $=X.name.replace(/\.ts$/,"");Z.push({name:$,path:fZ(J,X.name),slug:cZ($)})}return Z.sort((X,$)=>X.name.localeCompare($.name)),Z}async function SY(J){let Y=uZ(`_previews/${J}.ts`);if(!wY(Y))return null;try{let Z=await import(Y),X=Z.default??Z.props??null;return X&&typeof X==="object"?X:null}catch(Z){return{__preview_error__:`Failed to load sample props: ${Z instanceof Error?Z.message:String(Z)}`}}}async function dZ(J){let Y={inspection:{to:[],cc:[],bcc:[],attachments:[]},html:"",text:"",sampleProps:null};try{let Z=await import(J.path),X=lZ(Z);if(!X)return{...Y,error:`No subclass of \`Mailable\` exported from ${J.path}.`};let $=await SY(J.slug),G=new X($??{});if(!(G instanceof QJ))return{...Y,sampleProps:$,error:"Constructor did not produce a Mailable instance."};await G.build();let K=G.inspect();if(!K.template)return{inspection:K,html:K.html??"",text:K.text??"",sampleProps:$};let Q=await I(K.template.name,{variables:K.template.props,subject:K.subject});return{inspection:K,html:Q.html,text:Q.text,sampleProps:$}}catch(Z){return{...Y,error:Z instanceof Error?`${Z.name}: ${Z.message}`:String(Z)}}}function lZ(J){let Y=[];if(J.default)Y.push(J.default);for(let Z of Object.keys(J))if(Z!=="default")Y.push(J[Z]);for(let Z of Y){if(typeof Z!=="function")continue;if(!(Z.prototype instanceof QJ))continue;return Z}return null}function U(J){return String(J??"").replace(/[&<>"']/g,(Y)=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[Y])}function t(J){if(!J?.length)return"\u2014";return J.map((Y)=>{if(typeof Y==="string")return U(Y);if(Y.name)return`${U(Y.name)} &lt;${U(Y.address)}&gt;`;return U(Y.address)}).join(", ")}function AJ(J,Y){return`<!DOCTYPE html>
29
29
  <html lang="en">
30
30
  <head>
@@ -129,4 +129,4 @@ SES endpoint unreachable. The configured region (\`${X}\`) must match the region
129
129
  ${z}
130
130
  </section>
131
131
  </div>
132
- `)}export{_9 as verifyUnsubscribeToken,xY as verifySesSnsSignature,MY as verifySendgridSignature,EY as verifyPostmarkAuth,AY as verifyMailgunSignature,X9 as unsuppress,FX as templateExists,I as template,jY as suppressionTypeFor,zY as suppress,zJ as shouldInlineByDefault,eJ as ses,tJ as sendgrid,oY as safe,oZ as renderPreviewHtml,dZ as renderMailablePreview,mZ as renderIndexHtml,QX as renderHtml,r as recordWebhookEventOrSkip,QY as recordEmailIdempotency,rJ as mailtrap,iJ as mailgun,GJ as mail,oJ as log,SY as loadSampleProps,zX as listTemplates,Z9 as isSuppressed,JJ as inlineCss,b9 as handleSesWebhook,v9 as handleSendgridWebhook,y9 as handlePostmarkWebhook,k9 as handleMailgunWebhook,OZ as getSuppressions,LZ as getSuppressionPolicy,WY as findEmailByIdempotencyKey,PY as emitEmailUnsubscribe,BY as emitEmailComplaint,_Y as emitEmailBounceSoft,RY as emitEmailBounceHard,gZ as discoverMailables,PZ as createUnsubscribeToken,VY as checkSuppressionFor,SJ as capture,jZ as buildUnsubscribeUrl,B9 as buildListUnsubscribeHeaders,LJ as SafeHtml,QJ as Mailable,PJ as Mail,NZ as Email};
132
+ `)}export{_9 as verifyUnsubscribeToken,xY as verifySesSnsSignature,MY as verifySendgridSignature,EY as verifyPostmarkAuth,AY as verifyMailgunSignature,X9 as unsuppress,FX as templateExists,I as template,jY as suppressionTypeFor,zY as suppress,zJ as shouldInlineByDefault,eJ as ses,tJ as sendgrid,oY as safe,oZ as renderPreviewHtml,dZ as renderMailablePreview,mZ as renderIndexHtml,QX as renderHtml,r as recordWebhookEventOrSkip,QY as recordEmailIdempotency,rJ as mailtrap,iJ as mailgun,GJ as mail,oJ as log,SY as loadSampleProps,zX as listTemplates,Z9 as isSuppressed,qZ as isMissingTableError,JJ as inlineCss,b9 as handleSesWebhook,v9 as handleSendgridWebhook,y9 as handlePostmarkWebhook,k9 as handleMailgunWebhook,OZ as getSuppressions,LZ as getSuppressionPolicy,WY as findEmailByIdempotencyKey,PY as emitEmailUnsubscribe,BY as emitEmailComplaint,_Y as emitEmailBounceSoft,RY as emitEmailBounceHard,gZ as discoverMailables,PZ as createUnsubscribeToken,VY as checkSuppressionFor,SJ as capture,jZ as buildUnsubscribeUrl,B9 as buildListUnsubscribeHeaders,LJ as SafeHtml,QJ as Mailable,PJ as Mail,NZ as Email};
@@ -1,3 +1,16 @@
1
+ /**
2
+ * Does this error mean the backing table simply hasn't been migrated yet?
3
+ * Exported for direct unit coverage across dialects.
4
+ *
5
+ * Each supported database phrases "missing table" differently, and the
6
+ * suppression layer is fail-open — so a matcher that only knows sqlite/mysql
7
+ * would let Postgres's wording slip through and hard-fail every send on an
8
+ * un-migrated Postgres DB (stacksjs/stacks#1976). We scope the Postgres check
9
+ * to `undefined_table` specifically (SQLSTATE 42P01 / `relation "..." does not
10
+ * exist`) rather than a bare `does not exist`, so a genuine `column ... does
11
+ * not exist` schema bug still surfaces instead of being silently swallowed.
12
+ */
13
+ export declare function isMissingTableError(err: unknown): boolean;
1
14
  /**
2
15
  * Is the address suppressed? Pass a specific `type` to check only
3
16
  * one kind (e.g. unsubscribe-only — bounces from the same address
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/email",
3
3
  "type": "module",
4
- "version": "0.70.79",
4
+ "version": "0.70.80",
5
5
  "description": "The Stacks Email integration. Painlessly create & manage your inboxes, templates, and send emails.",
6
6
  "author": "Chris Breuer",
7
7
  "contributors": [
@@ -57,10 +57,10 @@
57
57
  "@stacksjs/ts-cloud": "^0.7.17"
58
58
  },
59
59
  "devDependencies": {
60
- "@stacksjs/cli": "0.70.79",
61
- "@stacksjs/config": "0.70.79",
60
+ "@stacksjs/cli": "0.70.80",
61
+ "@stacksjs/config": "0.70.80",
62
62
  "better-dx": "^0.2.16",
63
- "@stacksjs/error-handling": "0.70.79",
64
- "@stacksjs/types": "0.70.79"
63
+ "@stacksjs/error-handling": "0.70.80",
64
+ "@stacksjs/types": "0.70.80"
65
65
  }
66
66
  }