@stacksjs/email 0.70.84 → 0.70.86
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 +13 -117
- package/package.json +6 -5
package/dist/index.js
CHANGED
|
@@ -1,132 +1,28 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var
|
|
2
|
+
var MY=Object.defineProperty;var wY=(J)=>J;function EY(J,Y){this[J]=wY.bind(null,Y)}var y=(J,Y)=>{for(var Z in Y)MY(J,Z,{get:Y[Z],enumerable:!0,configurable:!0,set:EY.bind(Y,Z)})};var S=import.meta.require;var xJ={};y(xJ,{CaptureEmailDriver:()=>a});import{config as CY}from"@stacksjs/config";import{log as SY}from"@stacksjs/logging";var GJ=/^[^\s<>"\\\r\n\t]+@[^\s<>"\\\r\n\t]+$/;function AJ(J,Y){if(typeof J!=="string"||!GJ.test(J))throw Error(`Email ${Y} address is malformed or contains forbidden characters: ${JSON.stringify(J)}`)}function TJ(J){if(/[\r\n]/.test(J))throw Error("Email subject contains forbidden line break characters (CR/LF)")}function A(J){if(!J)return;let Y={};for(let[Z,X]of Object.entries(J)){if(typeof X!=="string")continue;if(/[\r\n]/.test(Z)||/[\r\n]/.test(X))continue;Y[Z]=X}return Object.keys(Y).length>0?Y:void 0}class N{config;constructor(J){this.config={maxRetries:J?.maxRetries||3,retryTimeout:J?.retryTimeout||1000,...J}}configure(J){this.config={...this.config,...J}}validateMessage(J){if(!J.from?.address&&!CY.email.from?.address)throw Error("Email sender address is required either in message or config");if(!J.to||Array.isArray(J.to)&&J.to.length===0)throw Error("At least one recipient is required");if(!J.subject)throw Error("Email subject is required");TJ(J.subject);let Y=(X,$)=>{if(!X)return;AJ(X,$)},Z=(X)=>{if(!X)return[];if(typeof X==="string")return[X];if(Array.isArray(X))return X.flatMap((G)=>typeof G==="string"?[G]:G?.address?[G.address]:[]);let $=X;return $.address?[$.address]:[]};if(J.from)Y(J.from.address,"from");for(let X of Z(J.to))Y(X,"to");for(let X of Z(J.cc))Y(X,"cc");for(let X of Z(J.bcc))Y(X,"bcc");return!0}formatAddresses(J){if(!J)return[];if(typeof J==="string")return[J];return J.map((Y)=>{if(typeof Y==="string")return Y;if(!Y.name)return Y.address;return`${/[",()<>[\]:;@\\]/.test(Y.name)?`"${Y.name.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")}"`:Y.name} <${Y.address}>`})}formatAddressList(J){if(!J)return[];if(Array.isArray(J))return this.formatAddresses(J);if(typeof J==="string")return this.formatAddresses(J);return this.formatAddresses([J])}async handleError(J,Y){let Z=J instanceof Error?J:Error(String(J));SY.error(`[${this.name}] Email sending failed`,{error:Z.message,stack:Z.stack,to:Y.to,subject:Y.subject});let X={message:`Email sending failed: ${Z.message}`,success:!1,provider:this.name};if(Y.onError){let $=Y.onError(Z),G=$ instanceof Promise?await $:$;X={...X,...G,success:!1,provider:this.name}}return X}async handleSuccess(J,Y){let Z={message:"Email sent successfully",success:!0,provider:this.name,messageId:Y};try{if(J.handle){let X=J.handle(),$=X instanceof Promise?await X:X;Z={...Z,...$,success:!0,provider:this.name,messageId:Y}}if(J.onSuccess){let X=J.onSuccess(),$=X instanceof Promise?await X:X;Z={...Z,...$,success:!0,provider:this.name,messageId:Y}}}catch(X){return this.handleError(X,J)}return Z}}var v=[],DJ=1;class a extends N{name="capture";async send(J,Y){try{this.validateMessage(J);let Z=new Date,X=`capture-${Z.getTime()}-${DJ++}`;return v.push({...J,sentAt:Z,messageId:X}),this.handleSuccess(J,X)}catch(Z){return this.handleError(Z,J)}}static all(){return v}static last(){return v[v.length-1]}static count(){return v.length}static clear(){v.length=0,DJ=1}}var gJ={};y(gJ,{default:()=>tY,LogEmailDriver:()=>c});import{mkdir as nY,writeFile as oY}from"fs/promises";import{join as fJ,resolve as uJ}from"path";import{log as pJ}from"@stacksjs/logging";import{config as WJ}from"@stacksjs/config";import{log as uY}from"@stacksjs/logging";import{fs as P}from"@stacksjs/storage";import{defaultsResourcesPath as SJ,resourcesPath as kJ}from"@stacksjs/path";import{join as QJ}from"path";function KJ(J,Y={}){let{inline:Z=!0,important:X=!0}=Y;if(!Z)return J;let $=J,G=[];$=$.replace(/<style\b[^>]*\bdata-inline=["']false["'][^>]*>[\s\S]*?<\/style>/gi,(W)=>{return G.push(W),`\x00STX_PASSTHROUGH_${G.length-1}\x00`});let K=[];if($=$.replace(/<style\b[^>]*>([\s\S]*?)<\/style>/gi,(W,z)=>{return K.push(z),""}),K.length===0)return MJ($,G);let Q=[],F=[];for(let W of K)for(let z of kY(W)){let{selector:V,declarations:q}=z;if(!V||!q)continue;if(wJ(V))Q.push({selector:V,decls:q});else F.push(`${V} { ${q.map((_)=>`${_.prop}: ${_.value};`).join(" ")} }`)}for(let{selector:W,decls:z}of Q)$=EJ($,W,z,X);if(F.length>0){let W=`<style>
|
|
3
3
|
${F.join(`
|
|
4
4
|
`)}
|
|
5
5
|
</style>`;$=/<\/head>/i.test($)?$.replace(/<\/head>/i,`${W}
|
|
6
6
|
</head>`):`${W}
|
|
7
|
-
${$}`}return
|
|
7
|
+
${$}`}return MJ($,G)}function MJ(J,Y){if(Y.length===0)return J;return J.replace(/\u0000STX_PASSTHROUGH_(\d+)\u0000/g,(Z,X)=>{return Y[Number.parseInt(X,10)]??""})}function kY(J){let Y=J.replace(/\/\*[\s\S]*?\*\//g,""),Z=[],X=0;while(X<Y.length){let $=Y.indexOf("{",X);if($===-1)break;let G=Y.slice(X,$).trim(),K=1,Q=$+1;while(Q<Y.length&&K>0){let W=Y[Q];if(W==="{")K++;else if(W==="}")K--;Q++}let F=Y.slice($+1,Q-1);if(G.startsWith("@")||F.includes("{"))Z.push({selector:G||"@unknown",declarations:yY(F)});else Z.push({selector:G,declarations:bY(F)});X=Q}return Z}function bY(J){return J.split(";").map((Y)=>Y.trim()).filter(Boolean).map((Y)=>{let Z=Y.indexOf(":");if(Z===-1)return null;return{prop:Y.slice(0,Z).trim(),value:Y.slice(Z+1).trim()}}).filter((Y)=>Y!==null)}function yY(J){return[{prop:"",value:J.trim()}]}function wJ(J){if(!J)return!1;if(J.includes(","))return J.split(",").every((Y)=>wJ(Y.trim()));if(J.startsWith("@"))return!1;if(/[\s>+~:[]/.test(J))return!1;return/^[a-z][a-z0-9-]*?$|^([a-z][a-z0-9-]*)?([.#][a-z][\w-]*)+$/i.test(J)}function EJ(J,Y,Z,X){if(Y.includes(",")){let z=J;for(let V of Y.split(","))z=EJ(z,V.trim(),Z,X);return z}let{tag:$,classes:G,ids:K}=vY(Y),F=new RegExp(`<(${$??"[a-z][a-z0-9-]*"})\\b([^>]*?)(/?)>`,"gi"),W=Z.map((z)=>`${z.prop}:${z.value}${X&&!/!important\b/i.test(z.value)?" !important":""};`).join("");return J.replace(F,(z,V,q)=>{if(!hY(q,G,K))return z;return fY(z,q,W)})}function vY(J){let Y=null,Z=[],X=[],$=0;while($<J.length&&/[a-z0-9-]/i.test(J[$]))Y=(Y??"")+J[$],$++;if(Y==="")Y=null;while($<J.length){let G=J[$];if(G!=="."&&G!=="#")break;let K=$+1;while(K<J.length&&/[\w-]/.test(J[K]))K++;let Q=J.slice($+1,K);if(G===".")Z.push(Q);else X.push(Q);$=K}return{tag:Y,classes:Z,ids:X}}function hY(J,Y,Z){if(Y.length>0){let X=J.match(/\bclass\s*=\s*["']([^"']*)["']/i);if(!X)return!1;let $=X[1].split(/\s+/).filter(Boolean);if(!Y.every((G)=>$.includes(G)))return!1}if(Z.length>0){let X=J.match(/\bid\s*=\s*["']([^"']+)["']/i);if(!X)return!1;if(!Z.every(($)=>X[1]===$))return!1}return!0}function fY(J,Y,Z){let X=Y.match(/\bstyle\s*=\s*["']([^"']*)["']/i);if(X){let G=X[1].trim(),K=`${Z}${G}${G.endsWith(";")||G===""?"":";"}`,Q=Y.replace(X[0],`style="${K}"`);return J.replace(Y,Q)}let $=` style="${Z}"`;return J.replace(/(\s*\/?)>$/,`${$}$1>`)}function CJ(){return(globalThis.process?.env?.APP_ENV??globalThis.process?.env?.NODE_ENV??"").toLowerCase()==="production"}class zJ{value;__safeHtml=!0;constructor(J){this.value=J}}function pY(J){return new zJ(J)}function bJ(){let J=WJ.app.primaryColor||"#3b82f6";return{appName:WJ.app.name||"Stacks",appUrl:WJ.app.url||"https://localhost",primaryColor:J,primaryColorDark:cY(J,15),year:new Date().getFullYear()}}function cY(J,Y){let Z=Number.parseInt(J.replace("#",""),16),X=Math.round(2.55*Y),$=Math.max(0,(Z>>16)-X),G=Math.max(0,(Z>>8&255)-X),K=Math.max(0,(Z&255)-X);return`#${(16777216+$*65536+G*256+K).toString(16).slice(1)}`}function gY(J){return J.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function FJ(J,Y){let Z=J;for(let[X,$]of Object.entries(Y)){let G=X.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),K=new RegExp(`\\{\\{\\s*${G}\\s*\\}\\}`,"g");Z=Z.replace(K,dY($))}return Z}function dY(J){if(J===null||J===void 0)return"";if(J instanceof zJ)return J.value;return gY(String(J))}var yJ=[(J)=>kJ(QJ("emails",J)),(J)=>SJ(QJ("emails",J))];function vJ(J){for(let Y of yJ){if(J.endsWith(".stx")){let $=Y(J);if(P.existsSync($))return{path:$,type:"stx"};continue}if(J.endsWith(".html")){let $=Y(J);if(P.existsSync($))return{path:$,type:"html"};continue}let Z=Y(`${J}.stx`);if(P.existsSync(Z))return{path:Z,type:"stx"};let X=Y(`${J}.html`);if(P.existsSync(X))return{path:X,type:"html"}}return null}function lY(J){let Y=J.endsWith(".html")?J:`${J}.html`;for(let Z of yJ){let X=Z(Y);if(P.existsSync(X))return P.readFileSync(X,"utf-8")}return null}function mY(J){return lY(`layouts/${J}`)}function hJ(J){return J.replace(/<br\s*\/?>/gi,`
|
|
8
8
|
`).replace(/<\/(p|div|h[1-6]|li|tr)>/gi,`
|
|
9
9
|
`).replace(/<\/td>/gi,"\t").replace(/<[^>]*>/g,"").replace(/ /g," ").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'/g,"'").replace(/©/g,"(c)").replace(/\n\s*\n\s*\n/g,`
|
|
10
10
|
|
|
11
|
-
`).trim()}async function I(J,Y={}){let{variables:Z={},layout:X="base",subject:$="",inline:G=
|
|
11
|
+
`).trim()}async function I(J,Y={}){let{variables:Z={},layout:X="base",subject:$="",inline:G=CJ()}=Y,K={...bJ(),subject:$,...Z},Q=vJ(J);if(!Q)return console.warn(`[Email Template] Template "${J}" not found`),{html:"",text:""};if(Q.type==="stx")try{let{renderEmail:V}=await import("@stacksjs/stx"),q=await V(Q.path,K,{componentsDir:SJ("components/Email")});return{...q,html:KJ(q.html,{inline:G})}}catch(V){return uY.warn(`[email] STX template rendering failed for ${J}: ${V instanceof Error?V.message:String(V)}`),{html:"",text:""}}let F=P.readFileSync(Q.path,"utf-8");F=FJ(F,K);let W;if(X!==!1){let V=mY(X);if(!V)console.warn(`[Email Template] Layout "${X}" not found, using content only`),W=F;else K.content=pY(F),W=FJ(V,K)}else W=F;W=KJ(W,{inline:G});let z=hJ(W);return{html:W,text:z}}function oZ(J,Y={}){let Z={...bJ(),...Y},X=FJ(J,Z),$=hJ(X);return{html:X,text:$}}function iZ(J){return vJ(J)!==null}function rZ(){let J=kJ("emails");if(!P.existsSync(J))return[];let Y=[];function Z(X,$=""){let G=P.readdirSync(X,{withFileTypes:!0});for(let K of G)if(K.isDirectory()&&K.name!=="layouts")Z(QJ(X,K.name),`${$}${K.name}/`);else if(K.isFile()&&(K.name.endsWith(".html")||K.name.endsWith(".stx"))){let Q=K.name.replace(/\.(html|stx)$/,"");if(!Y.includes(`${$}${Q}`))Y.push(`${$}${Q}`)}}return Z(J),Y}var cJ=100,h=[];class c extends N{name="log";resolveDir(){let J=process.env.LOG_MAIL_DIR;if(J)return uJ(J);return uJ(fJ(import.meta.dir,"..","..","..","..","..","logs","mail"))}async send(J,Y){try{this.validateMessage(J);let Z;if(J.template){let z=await I(J.template,Y);if(z)Z={html:z.html,text:z.text}}let X=Z?.html??J.html,$=Z?.text??J.text,G=new Date,K=(J.subject||"no-subject").replace(/[^\w.-]+/g,"-").slice(0,60),Q=`${G.toISOString().replace(/[:.]/g,"-")}-${K}.html`,F=this.resolveDir();try{await nY(F,{recursive:!0});let z=fJ(F,Q),V=X?X:$?`<pre>${rY($)}</pre>`:"<em>(empty body)</em>",q=iY({stamp:G,message:J});await oY(z,`${q}
|
|
12
12
|
${V}
|
|
13
|
-
`)}catch(z){
|
|
14
|
-
`)}function NJ(J){if(!J)return"";if(typeof J==="string")return J;let Y=J;return Y.name?`${Y.name} <${Y.address??""}>`:Y.address??""}function UJ(J){if(Array.isArray(J))return J.map(NJ).join(", ");return NJ(J)}function YZ(J){return J.replace(/[&<>"']/g,(Y)=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[Y])}var ZZ=g;var iJ={};v(iJ,{default:()=>XZ,MailgunDriver:()=>d});import{Buffer as nJ}from"buffer";import{config as A}from"@stacksjs/config";import{log as IJ}from"@stacksjs/logging";class d extends H{name="mailgun";apiKey=null;domain=null;endpoint=null;getConfig(){if(!this.apiKey||!this.domain||!this.endpoint)this.apiKey=A.services.mailgun?.apiKey??"",this.domain=A.services.mailgun?.domain??"",this.endpoint=A.services.mailgun?.endpoint??"api.mailgun.net";return{apiKey:this.apiKey,domain:this.domain,endpoint:this.endpoint}}async send(J,Y){let{domain:Z}=this.getConfig(),X={provider:this.name,to:J.to,subject:J.subject,domain:Z};IJ.info("Sending email via Mailgun...",X);try{this.validateMessage(J);let $;if(J.template){let W=await I(J.template,Y);if(W&&"html"in W)$=W.html}let G=$||J.html,K=new FormData,Q={address:J.from?.address||A.email.from?.address||"",name:J.from?.name||A.email.from?.name};if(K.append("from",this.formatMailgunAddress(Q)),this.formatMailgunAddresses(J.to).forEach((W)=>K.append("to",W)),J.cc)this.formatMailgunAddresses(J.cc).forEach((W)=>K.append("cc",W));if(J.bcc)this.formatMailgunAddresses(J.bcc).forEach((W)=>K.append("bcc",W));if(K.append("subject",J.subject),J.replyTo){let W=this.formatMailgunAddresses(Array.isArray(J.replyTo)||typeof J.replyTo==="string"?J.replyTo:[J.replyTo]);if(W.length>0)K.append("h:Reply-To",W.join(", "))}if(J.headers){for(let[W,z]of Object.entries(J.headers))if(typeof z==="string")K.append(`h:${W}`,z)}if(G)K.append("html",G);if(J.text)K.append("text",J.text);if(J.attachments)J.attachments.forEach((W)=>{let z=typeof W.content==="string"?W.content:this.arrayBufferToBase64(W.content);K.append("attachment",new Blob([z],{type:W.contentType}),W.filename)});let F=await this.sendWithRetry(K);return this.handleSuccess(J,F.id)}catch($){return this.handleError($,J)}}formatMailgunAddress(J){return J.name?`${J.name} <${J.address}>`:J.address}formatMailgunAddresses(J){if(!J)return[];if(typeof J==="string")return[J];return J.map((Y)=>{if(typeof Y==="string")return Y;if(!Y.name)return Y.address;return`${/[",()<>[\]:;@\\]/.test(Y.name)?`"${Y.name.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")}"`:Y.name} <${Y.address}>`})}arrayBufferToBase64(J){let Y="",Z=new Uint8Array(J),X=Z.byteLength;for(let $=0;$<X;$++)Y+=String.fromCharCode(Z[$]??0);return typeof btoa==="function"?btoa(Y):nJ.from(Y).toString("base64")}async sendWithRetry(J,Y=1){let{apiKey:Z,domain:X,endpoint:$}=this.getConfig(),G=`https://${$}/v3/${X}/messages`,K=nJ.from(`api:${Z}`).toString("base64");try{let Q=await fetch(G,{method:"POST",headers:{Authorization:`Basic ${K}`},body:J});if(!Q.ok){let W=await Q.json();throw Error(`Mailgun API error: ${Q.status} - ${JSON.stringify(W)}`)}let F=await Q.json();return IJ.info(`[${this.name}] Email sent successfully`,{attempt:Y,messageId:F.id}),F}catch(Q){if(Y<(A.services.mailgun?.maxRetries??3)){let F=A.services.mailgun?.retryTimeout??1000;return IJ.warn(`[${this.name}] Email send failed, retrying (${Y}/${A.services.mailgun?.maxRetries??3})`),await new Promise((W)=>setTimeout(W,F)),this.sendWithRetry(J,Y+1)}throw Q}}}var XZ=d;var rJ={};v(rJ,{default:()=>GZ,MailtrapDriver:()=>l});import{Buffer as $Z}from"buffer";import{config as j}from"@stacksjs/config";import{log as HJ}from"@stacksjs/logging";class l extends H{name="mailtrap";host=null;token=null;inboxId=null;getConfig(){if(this.host===null||this.token===null||this.inboxId===null)this.host=j.services.mailtrap?.host??"https://sandbox.api.mailtrap.io/api/send",this.token=j.services.mailtrap?.token??"",this.inboxId=j.services.mailtrap?.inboxId?Number(j.services.mailtrap.inboxId):void 0;return{host:this.host,token:this.token,inboxId:this.inboxId}}async send(J,Y){let{inboxId:Z}=this.getConfig(),X={provider:this.name,to:J.to,subject:J.subject,inboxId:Z};HJ.info("Sending email via Mailtrap...",X);try{this.validateMessage(J);let $;if(J.template)$=await I(J.template,Y);let G=$?.html||J.html,K=this.firstMailtrapAddress(J.replyTo),Q=T(J.headers),F={from:{email:J.from?.address||j.email.from?.address||"",name:J.from?.name||j.email.from?.name},to:this.formatMailtrapAddresses(J.to),...J.cc&&{cc:this.formatMailtrapAddresses(J.cc)},...J.bcc&&{bcc:this.formatMailtrapAddresses(J.bcc)},...K?{reply_to:K}:{},...Q?{headers:Q}:{},subject:J.subject,...G&&{html:G},...J.text&&{text:J.text},...J.attachments&&{attachments:J.attachments.map((z)=>({filename:z.filename,content:typeof z.content==="string"?z.content:this.arrayBufferToBase64(z.content),type:z.contentType||"application/octet-stream"}))}},W=await this.sendWithRetry(F);return this.handleSuccess(J,W.message_ids?.[0])}catch($){return this.handleError($,J)}}formatMailtrapAddresses(J){if(!J)return[];if(typeof J==="string")return[{email:J}];return J.map((Y)=>{if(typeof Y==="string")return{email:Y};return{email:Y.address,...Y.name&&{name:Y.name}}})}firstMailtrapAddress(J){if(!J)return;if(typeof J==="string")return{email:J};if(Array.isArray(J)){let Y=J[0];if(Y===void 0)return;if(typeof Y==="string")return{email:Y};return{email:Y.address,...Y.name&&{name:Y.name}}}return{email:J.address,...J.name&&{name:J.name}}}arrayBufferToBase64(J){let Y="",Z=new Uint8Array(J),X=Z.byteLength;for(let $=0;$<X;$++)Y+=String.fromCharCode(Z[$]??0);return typeof btoa==="function"?btoa(Y):$Z.from(Y).toString("base64")}async sendWithRetry(J,Y=1){let{host:Z,token:X,inboxId:$}=this.getConfig();if(!$)throw Error("Mailtrap inbox ID is required but not provided. Please set MAILTRAP_INBOX_ID in your environment variables.");let G=`${Z}/${$}`;try{let K=await fetch(G,{method:"POST",headers:{Authorization:`Bearer ${X}`,"Content-Type":"application/json"},body:JSON.stringify(J)});if(!K.ok){let F=await K.json();throw Error(`Mailtrap API error: ${K.status} - ${JSON.stringify(F)}`)}let Q=await K.json();return HJ.info(`[${this.name}] Email sent successfully`,{attempt:Y,messageId:Q.message_ids?.[0]}),Q}catch(K){if(Y<(j.services.mailtrap?.maxRetries??3)){let Q=j.services.mailtrap?.retryTimeout??1000;return HJ.warn(`[${this.name}] Email send failed, retrying (${Y}/${j.services.mailtrap?.maxRetries??3})`),await new Promise((F)=>setTimeout(F,Q)),this.sendWithRetry(J,Y+1)}throw K}}}var GZ=l;var tJ={};v(tJ,{default:()=>WZ,SendGridDriver:()=>m});import{Buffer as KZ}from"buffer";import{config as y}from"@stacksjs/config";import{log as RJ}from"@stacksjs/logging";class m extends H{name="sendgrid";apiKey=null;getApiKey(){if(!this.apiKey)this.apiKey=y.services.sendgrid?.apiKey??"";return this.apiKey}async send(J,Y){let Z={provider:this.name,to:J.to,subject:J.subject};RJ.info("Sending email via SendGrid...",Z);try{this.validateMessage(J);let X;if(J.template){let z=await I(J.template,Y);if(z&&"html"in z)X=z.html}let $=X||J.html,G=[];if($)G.push({type:"text/html",value:$});if(J.text)G.push({type:"text/plain",value:J.text});if(G.length===0)throw Error("Email must have either HTML or text content");let K=this.firstSendGridAddress(J.replyTo),Q=T(J.headers),F={personalizations:[{to:this.formatSendGridAddresses(J.to),...J.cc&&{cc:this.formatSendGridAddresses(J.cc)},...J.bcc&&{bcc:this.formatSendGridAddresses(J.bcc)},subject:J.subject}],from:{email:J.from?.address||y.email.from?.address||"",name:J.from?.name||y.email.from?.name},...K?{reply_to:K}:{},...Q?{headers:Q}:{},content:G,...J.attachments&&{attachments:J.attachments.map((z)=>({filename:z.filename,content:typeof z.content==="string"?z.content:this.arrayBufferToBase64(z.content),type:z.contentType,disposition:"attachment"}))}},W=await this.sendWithRetry(F);return this.handleSuccess(J,W.headers?.get("x-message-id")??void 0)}catch(X){return this.handleError(X,J)}}formatSendGridAddresses(J){if(!J)return[];if(typeof J==="string")return[{email:J}];return J.map((Y)=>{if(typeof Y==="string")return{email:Y};return{email:Y.address,...Y.name&&{name:Y.name}}})}firstSendGridAddress(J){if(!J)return;if(typeof J==="string")return{email:J};if(Array.isArray(J)){let Y=J[0];if(Y===void 0)return;if(typeof Y==="string")return{email:Y};return{email:Y.address,...Y.name&&{name:Y.name}}}return{email:J.address,...J.name&&{name:J.name}}}arrayBufferToBase64(J){let Y="",Z=new Uint8Array(J),X=Z.byteLength;for(let $=0;$<X;$++)Y+=String.fromCharCode(Z[$]??0);return typeof btoa==="function"?btoa(Y):KZ.from(Y).toString("base64")}async sendWithRetry(J,Y=1){try{let Z=await fetch("https://api.sendgrid.com/v3/mail/send",{method:"POST",headers:{Authorization:`Bearer ${this.getApiKey()}`,"Content-Type":"application/json"},body:JSON.stringify(J)});if(!Z.ok){let X=await Z.json(),$=Error(`SendGrid API error: ${Z.status} - ${JSON.stringify(X)}`);throw $.status=Z.status,$}return RJ.info(`[${this.name}] Email sent successfully`,{attempt:Y}),Z}catch(Z){let X=Z?.status;if(!(typeof X==="number"&&X>=400&&X<500&&X!==429)&&Y<(y.services.sendgrid?.maxRetries??3)){let G=y.services.sendgrid?.retryTimeout??1000;return RJ.warn(`[${this.name}] Email send failed, retrying (${Y}/${y.services.sendgrid?.maxRetries??3})`),await new Promise((K)=>setTimeout(K,G)),this.sendWithRetry(J,Y+1)}throw Z}}}var WZ=m;var eJ={};v(eJ,{default:()=>zZ,SESDriver:()=>o});import{config as E}from"@stacksjs/config";import{SESClient as FZ}from"@stacksjs/ts-cloud";import{Buffer as _J}from"buffer";function sJ(J){if(/^[\x00-\x7F]*$/.test(J))return J;return`=?UTF-8?B?${_J.from(J,"utf-8").toString("base64")}?=`}function YJ(J){let{from:Y,to:Z,cc:X,replyTo:$,subject:G,text:K,html:Q,attachments:F,messageIdDomain:W,customHeaders:z}=J,V=[],q=!!(F&&F.length>0),B=`----=_Part_${Date.now()}_${Math.random().toString(36).substring(2)}`;if(V.push(`From: ${Y}`),V.push(`To: ${Z}`),X)V.push(`Cc: ${X}`);if($)V.push(`Reply-To: ${$}`);if(V.push(`Subject: ${sJ(G)}`),V.push("MIME-Version: 1.0"),V.push(`Date: ${new Date().toUTCString()}`),V.push(`Message-ID: <${Date.now()}.${Math.random().toString(36).substring(2)}@${W||"localhost"}>`),z)for(let[N,w]of Object.entries(z))V.push(`${N}: ${w}`);if(q){let N=B,w=`${B}_alt`;V.push(`Content-Type: multipart/mixed; boundary="${N}"`),V.push(""),V.push(`--${N}`),aJ(V,{text:K,html:Q,altBoundary:w});for(let R of F)V.push(`--${N}`),QZ(V,R);V.push(`--${N}--`)}else aJ(V,{text:K,html:Q,altBoundary:B});return V.join(`\r
|
|
15
|
-
`)}function
|
|
16
|
-
`)??$;J.push(G),J.push("")}class
|
|
13
|
+
`)}catch(z){pJ.warn(`[email:log] could not write inspection file: ${z.message}`)}let W=Array.isArray(J.to)?J.to.map((z)=>typeof z==="string"?z:z.address).join(", "):typeof J.to==="string"?J.to:J.to.address;if(pJ.info(`[email:log] would send \u2192 ${W} :: ${J.subject}`),h.push({...J,sentAt:G,rendered:Z}),h.length>cJ)h.splice(0,h.length-cJ);return this.handleSuccess(J,`log-${G.getTime()}`)}catch(Z){return this.handleError(Z,J)}}static captured(){return h}static reset(){h.length=0}}function iY({stamp:J,message:Y}){return["<!--",` Captured by @stacksjs/email log driver at ${J.toISOString()}`,` From: ${qJ(Y.from)}`,` To: ${VJ(Y.to)}`,Y.cc?` Cc: ${VJ(Y.cc)}`:null,Y.bcc?` Bcc: ${VJ(Y.bcc)}`:null,` Subject: ${Y.subject}`,"-->"].filter(Boolean).join(`
|
|
14
|
+
`)}function qJ(J){if(!J)return"";if(typeof J==="string")return J;let Y=J;return Y.name?`${Y.name} <${Y.address??""}>`:Y.address??""}function VJ(J){if(Array.isArray(J))return J.map(qJ).join(", ");return qJ(J)}function rY(J){return J.replace(/[&<>"']/g,(Y)=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[Y])}var tY=c;var lJ={};y(lJ,{default:()=>aY,MailgunDriver:()=>g});import{Buffer as dJ}from"buffer";import{config as T}from"@stacksjs/config";import{log as LJ}from"@stacksjs/logging";class g extends N{name="mailgun";apiKey=null;domain=null;endpoint=null;getConfig(){if(!this.apiKey||!this.domain||!this.endpoint)this.apiKey=T.services.mailgun?.apiKey??"",this.domain=T.services.mailgun?.domain??"",this.endpoint=T.services.mailgun?.endpoint??"api.mailgun.net";return{apiKey:this.apiKey,domain:this.domain,endpoint:this.endpoint}}async send(J,Y){let{domain:Z}=this.getConfig(),X={provider:this.name,to:J.to,subject:J.subject,domain:Z};LJ.info("Sending email via Mailgun...",X);try{this.validateMessage(J);let $;if(J.template){let W=await I(J.template,Y);if(W&&"html"in W)$=W.html}let G=$||J.html,K=new FormData,Q={address:J.from?.address||T.email.from?.address||"",name:J.from?.name||T.email.from?.name};if(K.append("from",this.formatMailgunAddress(Q)),this.formatMailgunAddresses(J.to).forEach((W)=>K.append("to",W)),J.cc)this.formatMailgunAddresses(J.cc).forEach((W)=>K.append("cc",W));if(J.bcc)this.formatMailgunAddresses(J.bcc).forEach((W)=>K.append("bcc",W));if(K.append("subject",J.subject),J.replyTo){let W=this.formatMailgunAddresses(Array.isArray(J.replyTo)||typeof J.replyTo==="string"?J.replyTo:[J.replyTo]);if(W.length>0)K.append("h:Reply-To",W.join(", "))}if(J.headers){for(let[W,z]of Object.entries(J.headers))if(typeof z==="string")K.append(`h:${W}`,z)}if(G)K.append("html",G);if(J.text)K.append("text",J.text);if(J.attachments)J.attachments.forEach((W)=>{let z=typeof W.content==="string"?W.content:this.arrayBufferToBase64(W.content);K.append("attachment",new Blob([z],{type:W.contentType}),W.filename)});let F=await this.sendWithRetry(K);return this.handleSuccess(J,F.id)}catch($){return this.handleError($,J)}}formatMailgunAddress(J){return J.name?`${J.name} <${J.address}>`:J.address}formatMailgunAddresses(J){if(!J)return[];if(typeof J==="string")return[J];return J.map((Y)=>{if(typeof Y==="string")return Y;if(!Y.name)return Y.address;return`${/[",()<>[\]:;@\\]/.test(Y.name)?`"${Y.name.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")}"`:Y.name} <${Y.address}>`})}arrayBufferToBase64(J){let Y="",Z=new Uint8Array(J),X=Z.byteLength;for(let $=0;$<X;$++)Y+=String.fromCharCode(Z[$]??0);return typeof btoa==="function"?btoa(Y):dJ.from(Y).toString("base64")}async sendWithRetry(J,Y=1){let{apiKey:Z,domain:X,endpoint:$}=this.getConfig(),G=`https://${$}/v3/${X}/messages`,K=dJ.from(`api:${Z}`).toString("base64");try{let Q=await fetch(G,{method:"POST",headers:{Authorization:`Basic ${K}`},body:J});if(!Q.ok){let W=await Q.json();throw Error(`Mailgun API error: ${Q.status} - ${JSON.stringify(W)}`)}let F=await Q.json();return LJ.info(`[${this.name}] Email sent successfully`,{attempt:Y,messageId:F.id}),F}catch(Q){if(Y<(T.services.mailgun?.maxRetries??3)){let F=T.services.mailgun?.retryTimeout??1000;return LJ.warn(`[${this.name}] Email send failed, retrying (${Y}/${T.services.mailgun?.maxRetries??3})`),await new Promise((W)=>setTimeout(W,F)),this.sendWithRetry(J,Y+1)}throw Q}}}var aY=g;var mJ={};y(mJ,{default:()=>eY,MailtrapDriver:()=>d});import{Buffer as sY}from"buffer";import{config as B}from"@stacksjs/config";import{log as OJ}from"@stacksjs/logging";class d extends N{name="mailtrap";host=null;token=null;inboxId=null;getConfig(){if(this.host===null||this.token===null||this.inboxId===null)this.host=B.services.mailtrap?.host??"https://sandbox.api.mailtrap.io/api/send",this.token=B.services.mailtrap?.token??"",this.inboxId=B.services.mailtrap?.inboxId?Number(B.services.mailtrap.inboxId):void 0;return{host:this.host,token:this.token,inboxId:this.inboxId}}async send(J,Y){let{inboxId:Z}=this.getConfig(),X={provider:this.name,to:J.to,subject:J.subject,inboxId:Z};OJ.info("Sending email via Mailtrap...",X);try{this.validateMessage(J);let $;if(J.template)$=await I(J.template,Y);let G=$?.html||J.html,K=this.firstMailtrapAddress(J.replyTo),Q=A(J.headers),F={from:{email:J.from?.address||B.email.from?.address||"",name:J.from?.name||B.email.from?.name},to:this.formatMailtrapAddresses(J.to),...J.cc&&{cc:this.formatMailtrapAddresses(J.cc)},...J.bcc&&{bcc:this.formatMailtrapAddresses(J.bcc)},...K?{reply_to:K}:{},...Q?{headers:Q}:{},subject:J.subject,...G&&{html:G},...J.text&&{text:J.text},...J.attachments&&{attachments:J.attachments.map((z)=>({filename:z.filename,content:typeof z.content==="string"?z.content:this.arrayBufferToBase64(z.content),type:z.contentType||"application/octet-stream"}))}},W=await this.sendWithRetry(F);return this.handleSuccess(J,W.message_ids?.[0])}catch($){return this.handleError($,J)}}formatMailtrapAddresses(J){if(!J)return[];if(typeof J==="string")return[{email:J}];return J.map((Y)=>{if(typeof Y==="string")return{email:Y};return{email:Y.address,...Y.name&&{name:Y.name}}})}firstMailtrapAddress(J){if(!J)return;if(typeof J==="string")return{email:J};if(Array.isArray(J)){let Y=J[0];if(Y===void 0)return;if(typeof Y==="string")return{email:Y};return{email:Y.address,...Y.name&&{name:Y.name}}}return{email:J.address,...J.name&&{name:J.name}}}arrayBufferToBase64(J){let Y="",Z=new Uint8Array(J),X=Z.byteLength;for(let $=0;$<X;$++)Y+=String.fromCharCode(Z[$]??0);return typeof btoa==="function"?btoa(Y):sY.from(Y).toString("base64")}async sendWithRetry(J,Y=1){let{host:Z,token:X,inboxId:$}=this.getConfig();if(!$)throw Error("Mailtrap inbox ID is required but not provided. Please set MAILTRAP_INBOX_ID in your environment variables.");let G=`${Z}/${$}`;try{let K=await fetch(G,{method:"POST",headers:{Authorization:`Bearer ${X}`,"Content-Type":"application/json"},body:JSON.stringify(J)});if(!K.ok){let F=await K.json();throw Error(`Mailtrap API error: ${K.status} - ${JSON.stringify(F)}`)}let Q=await K.json();return OJ.info(`[${this.name}] Email sent successfully`,{attempt:Y,messageId:Q.message_ids?.[0]}),Q}catch(K){if(Y<(B.services.mailtrap?.maxRetries??3)){let Q=B.services.mailtrap?.retryTimeout??1000;return OJ.warn(`[${this.name}] Email send failed, retrying (${Y}/${B.services.mailtrap?.maxRetries??3})`),await new Promise((F)=>setTimeout(F,Q)),this.sendWithRetry(J,Y+1)}throw K}}}var eY=d;var nJ={};y(nJ,{default:()=>YZ,SendGridDriver:()=>l});import{Buffer as JZ}from"buffer";import{config as k}from"@stacksjs/config";import{log as UJ}from"@stacksjs/logging";class l extends N{name="sendgrid";apiKey=null;getApiKey(){if(!this.apiKey)this.apiKey=k.services.sendgrid?.apiKey??"";return this.apiKey}async send(J,Y){let Z={provider:this.name,to:J.to,subject:J.subject};UJ.info("Sending email via SendGrid...",Z);try{this.validateMessage(J);let X;if(J.template){let z=await I(J.template,Y);if(z&&"html"in z)X=z.html}let $=X||J.html,G=[];if($)G.push({type:"text/html",value:$});if(J.text)G.push({type:"text/plain",value:J.text});if(G.length===0)throw Error("Email must have either HTML or text content");let K=this.firstSendGridAddress(J.replyTo),Q=A(J.headers),F={personalizations:[{to:this.formatSendGridAddresses(J.to),...J.cc&&{cc:this.formatSendGridAddresses(J.cc)},...J.bcc&&{bcc:this.formatSendGridAddresses(J.bcc)},subject:J.subject}],from:{email:J.from?.address||k.email.from?.address||"",name:J.from?.name||k.email.from?.name},...K?{reply_to:K}:{},...Q?{headers:Q}:{},content:G,...J.attachments&&{attachments:J.attachments.map((z)=>({filename:z.filename,content:typeof z.content==="string"?z.content:this.arrayBufferToBase64(z.content),type:z.contentType,disposition:"attachment"}))}},W=await this.sendWithRetry(F);return this.handleSuccess(J,W.headers?.get("x-message-id")??void 0)}catch(X){return this.handleError(X,J)}}formatSendGridAddresses(J){if(!J)return[];if(typeof J==="string")return[{email:J}];return J.map((Y)=>{if(typeof Y==="string")return{email:Y};return{email:Y.address,...Y.name&&{name:Y.name}}})}firstSendGridAddress(J){if(!J)return;if(typeof J==="string")return{email:J};if(Array.isArray(J)){let Y=J[0];if(Y===void 0)return;if(typeof Y==="string")return{email:Y};return{email:Y.address,...Y.name&&{name:Y.name}}}return{email:J.address,...J.name&&{name:J.name}}}arrayBufferToBase64(J){let Y="",Z=new Uint8Array(J),X=Z.byteLength;for(let $=0;$<X;$++)Y+=String.fromCharCode(Z[$]??0);return typeof btoa==="function"?btoa(Y):JZ.from(Y).toString("base64")}async sendWithRetry(J,Y=1){try{let Z=await fetch("https://api.sendgrid.com/v3/mail/send",{method:"POST",headers:{Authorization:`Bearer ${this.getApiKey()}`,"Content-Type":"application/json"},body:JSON.stringify(J)});if(!Z.ok){let X=await Z.json(),$=Error(`SendGrid API error: ${Z.status} - ${JSON.stringify(X)}`);throw $.status=Z.status,$}return UJ.info(`[${this.name}] Email sent successfully`,{attempt:Y}),Z}catch(Z){let X=Z?.status;if(!(typeof X==="number"&&X>=400&&X<500&&X!==429)&&Y<(k.services.sendgrid?.maxRetries??3)){let G=k.services.sendgrid?.retryTimeout??1000;return UJ.warn(`[${this.name}] Email send failed, retrying (${Y}/${k.services.sendgrid?.maxRetries??3})`),await new Promise((K)=>setTimeout(K,G)),this.sendWithRetry(J,Y+1)}throw Z}}}var YZ=l;var rJ={};y(rJ,{default:()=>$Z,SESDriver:()=>m});import{config as D}from"@stacksjs/config";import{SESClient as XZ}from"@stacksjs/ts-cloud";import{Buffer as NJ}from"buffer";function iJ(J){if(/^[\x00-\x7F]*$/.test(J))return J;return`=?UTF-8?B?${NJ.from(J,"utf-8").toString("base64")}?=`}function s(J){let{from:Y,to:Z,cc:X,replyTo:$,subject:G,text:K,html:Q,attachments:F,messageIdDomain:W,customHeaders:z}=J,V=[],q=!!(F&&F.length>0),_=`----=_Part_${Date.now()}_${Math.random().toString(36).substring(2)}`;if(V.push(`From: ${Y}`),V.push(`To: ${Z}`),X)V.push(`Cc: ${X}`);if($)V.push(`Reply-To: ${$}`);if(V.push(`Subject: ${iJ(G)}`),V.push("MIME-Version: 1.0"),V.push(`Date: ${new Date().toUTCString()}`),V.push(`Message-ID: <${Date.now()}.${Math.random().toString(36).substring(2)}@${W||"localhost"}>`),z)for(let[U,E]of Object.entries(z))V.push(`${U}: ${E}`);if(q){let U=_,E=`${_}_alt`;V.push(`Content-Type: multipart/mixed; boundary="${U}"`),V.push(""),V.push(`--${U}`),oJ(V,{text:K,html:Q,altBoundary:E});for(let H of F)V.push(`--${U}`),ZZ(V,H);V.push(`--${U}--`)}else oJ(V,{text:K,html:Q,altBoundary:_});return V.join(`\r
|
|
15
|
+
`)}function oJ(J,Y){let{text:Z,html:X,altBoundary:$}=Y;if(X&&Z)J.push(`Content-Type: multipart/alternative; boundary="${$}"`),J.push(""),J.push(`--${$}`),J.push("Content-Type: text/plain; charset=UTF-8"),J.push("Content-Transfer-Encoding: 7bit"),J.push(""),J.push(Z),J.push(""),J.push(`--${$}`),J.push("Content-Type: text/html; charset=UTF-8"),J.push("Content-Transfer-Encoding: 7bit"),J.push(""),J.push(X),J.push(""),J.push(`--${$}--`);else if(X)J.push("Content-Type: text/html; charset=UTF-8"),J.push("Content-Transfer-Encoding: 7bit"),J.push(""),J.push(X);else if(Z)J.push("Content-Type: text/plain; charset=UTF-8"),J.push("Content-Transfer-Encoding: 7bit"),J.push(""),J.push(Z)}function ZZ(J,Y){let Z=Y.contentType||"application/octet-stream",X=iJ(Y.filename.replace(/\\/g,"\\\\").replace(/"/g,"\\\""));J.push(`Content-Type: ${Z}; name="${X}"`),J.push(`Content-Disposition: attachment; filename="${X}"`),J.push("Content-Transfer-Encoding: base64"),J.push("");let $=typeof Y.content==="string"?NJ.from(Y.content,"utf-8").toString("base64"):NJ.from(Y.content).toString("base64"),G=$.match(/.{1,76}/g)?.join(`\r
|
|
16
|
+
`)??$;J.push(G),J.push("")}class m extends N{name="ses";client=null;getClient(){if(!this.client){let J=D?.services?.ses,Y=J?.credentials,Z=!!(Y?.accessKeyId&&Y?.secretAccessKey);this.client=new XZ(J?.region||"us-east-1",Z?{accessKeyId:Y.accessKeyId,secretAccessKey:Y.secretAccessKey,sessionToken:Y.sessionToken}:void 0)}return this.client}async send(J,Y){try{this.validateMessage(J);let Z;if(J.template){let U=await I(J.template,Y);if(U&&"html"in U)Z=U.html}let X=Z||J.html;if(!X&&!J.text)throw Error("Email must have either HTML or text content");let $=this.formatSourceAddress({address:J.from?.address||D.email.from?.address||"",name:J.from?.name||D.email.from?.name}),G=this.formatAddresses(J.to),K=this.formatAddresses(J.cc),Q=this.formatAddresses(J.bcc),F=J.replyTo?this.formatAddressList(J.replyTo):[],W=A(J.headers);if(!!(J.attachments&&J.attachments.length>0)||!!W){let U=s({from:$,to:G.join(", "),cc:K.length>0?K.join(", "):void 0,replyTo:F.length>0?F.join(", "):void 0,customHeaders:W,subject:J.subject,text:J.text,html:X,attachments:J.attachments,messageIdDomain:D.email.domain}),E=await this.getClient().sendRawEmail({source:$,destinations:[...G,...K,...Q],rawMessage:U});return this.handleSuccess(J,E.MessageId)}let q={};if(X)q.Html={Charset:D.email.charset||"UTF-8",Data:X};if(J.text)q.Text={Charset:D.email.charset||"UTF-8",Data:J.text};let _=await this.getClient().sendEmail({FromEmailAddress:$,Destination:{ToAddresses:G,CcAddresses:K,BccAddresses:Q},...F.length>0?{ReplyToAddresses:F}:{},Content:{Simple:{Subject:{Charset:D.email.charset||"UTF-8",Data:J.subject},Body:q}}});return this.handleSuccess(J,_.MessageId)}catch(Z){return this.handleError(this.enrichSesError(Z),J)}}formatSourceAddress(J){if(!J.name)return J.address;return`${/[",()<>[\]:;@\\]/.test(J.name)?`"${J.name.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")}"`:J.name} <${J.address}>`}enrichSesError(J){let Y=J instanceof Error?J:Error(String(J)),Z=`${Y.message} ${Y.name??""}`.toLowerCase(),X=D?.services?.ses?.region||"us-east-1";if(Z.includes("email address is not verified")||Z.includes("not authorized to send")||Z.includes("messagerejected")&&Z.includes("verified"))return Y.message=`${Y.message}
|
|
17
17
|
|
|
18
18
|
SES sandbox restriction: the From and (in sandbox) every To address must be verified. Verify identities in the SES console under "Verified identities" (region: ${X}), or request production access to lift the recipient restriction.`,Y;if(Z.includes("signaturedoesnotmatch")||Z.includes("invalidclienttokenid")||Z.includes("unable to locate credentials")||Z.includes("the security token included in the request is invalid"))return Y.message=`${Y.message}
|
|
19
19
|
|
|
20
20
|
SES authentication failed. Check AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY (or services.ses.credentials), and confirm the IAM principal has \`ses:SendEmail\` permission for the From identity.`,Y;if(Z.includes("could not be reached")||Z.includes("econnrefused")||Z.includes("enotfound"))return Y.message=`${Y.message}
|
|
21
21
|
|
|
22
|
-
SES endpoint unreachable. The configured region (\`${X}\`) must match the region where the From identity is verified.`,Y;return Y}}var
|
|
23
|
-
`)})},
|
|
24
|
-
`);z=
|
|
22
|
+
SES endpoint unreachable. The configured region (\`${X}\`) must match the region where the From identity is verified.`,Y;return Y}}var $Z=m;import{config as f}from"@stacksjs/config";import{log as FZ}from"@stacksjs/logging";import{Buffer as tJ}from"buffer";import GZ from"process";import*as n from"tls";import*as sJ from"net";import{config as x}from"@stacksjs/config";import{log as R}from"@stacksjs/logging";function aJ(J,Y){if(typeof J!=="string"||!GJ.test(J))throw Error(`[smtp] Refusing to send: ${Y} envelope address contains forbidden characters or is malformed: ${JSON.stringify(J)}`)}class b extends N{static SMTP_TIMEOUT=30000;name="smtp";getConfig(){let J=x.services?.smtp,Y=GZ.env,Z=J?.host||Y.MAIL_HOST||"127.0.0.1",X=J?.port||(Y.MAIL_PORT?Number(Y.MAIL_PORT):void 0)||587,$=typeof x.email?.from?.address==="string"?x.email.from.address:"",G=J?.username||Y.MAIL_USERNAME||$||"",K=(G.includes("@")?G.split("@")[0]:G).toUpperCase().replace(/[^A-Z0-9]/g,"_"),Q=J?.password||Y.MAIL_PASSWORD||(K?Y[`MAIL_PASSWORD_${K}`]:void 0)||"",F=J?.encryption??Y.MAIL_ENCRYPTION??null;return{host:Z,port:X,username:G,password:Q,encryption:F==="tls"?"starttls":F||null}}async send(J,Y){let Z=this.getConfig();if(!Z.host||Z.host==="")throw Error("[SMTP] Host is not configured. Set MAIL_HOST in your .env file.");let X={provider:this.name,to:J.to,subject:J.subject,host:Z.host,port:Z.port};R.info("Sending email via SMTP...",X);try{this.validateMessage(J);let $;if(J.template){let q=await I(J.template,Y);if(q&&"html"in q)$=q.html}let G=$||J.html,K=J.from?.address||x.email.from?.address||"",Q=J.from?.name||x.email.from?.name||"",F=this.formatAddresses(J.to),W=this.formatAddressList(J.replyTo),z=s({from:Q?`${Q} <${K}>`:K,to:F.join(", "),cc:J.cc?this.formatAddresses(J.cc).join(", "):void 0,replyTo:W.length>0?W.join(", "):void 0,customHeaders:A(J.headers),subject:J.subject,text:J.text,html:G,attachments:J.attachments,messageIdDomain:x.email.domain}),V=await this.sendViaSMTP(Z,K,F,z);return this.handleSuccess(J,V)}catch($){return this.handleError($,J)}}async sendViaSMTP(J,Y,Z,X){return new Promise(($,G)=>{let K=setTimeout(()=>{G(Error(`SMTP connection timed out after ${b.SMTP_TIMEOUT}ms`))},b.SMTP_TIMEOUT),Q=$,F=G;$=(L)=>{clearTimeout(K),Q(L)},G=(L)=>{clearTimeout(K),F(L)};let W,z="",V="",q=[],_=!1,U=!1,E=(L)=>{if(R.debug(`[SMTP] Server: ${L.trim()}`),parseInt(L.substring(0,3),10)>=400){let O=Error(`SMTP Error: ${L.trim()}`);if(q.length>0)q.shift()?.reject(O);return}if(q.length>0)q.shift()?.resolve(L)},H=(L)=>{return new Promise((j,O)=>{q.push({cmd:L,resolve:j,reject:O}),R.debug(`[SMTP] Client: ${L}`),W.write(`${L}\r
|
|
23
|
+
`)})},BJ=(L)=>{z+=L.toString();let j=z.split(`\r
|
|
24
|
+
`);z=j.pop()||"";for(let O of j)if(O.length>=3){if(O.length===3||O[3]===" ")E(O)}},jJ=async()=>{try{await new Promise((O,C)=>{q.push({cmd:"GREETING",resolve:O,reject:C})});let L=await H(`EHLO ${x.email.domain||"localhost"}`);if(J.encryption==="starttls"&&!(W instanceof n.TLSSocket)){await H("STARTTLS");let O=W;O.removeAllListeners("data"),W=await new Promise((C,xY)=>{let r=n.connect({socket:O,host:J.host,servername:J.host},()=>{R.debug("[SMTP] TLS connection established"),C(r)});r.on("error",(t)=>{R.error("[SMTP] TLS socket error:",t),xY(t)}),r.on("data",BJ),r.on("close",(t)=>{R.debug(`[SMTP] TLS socket closed (hadError: ${t})`);while(q.length>0)q.shift()?.reject(Error("TLS connection closed unexpectedly"))})}),await H(`EHLO ${x.email.domain||"localhost"}`)}if(J.username&&J.password)await H("AUTH LOGIN"),await H(tJ.from(J.username).toString("base64")),await H(tJ.from(J.password).toString("base64"));aJ(Y,"MAIL FROM");for(let O of Z)aJ(O,"RCPT TO");await H(`MAIL FROM:<${Y}>`);for(let O of Z)await H(`RCPT TO:<${O}>`);await H("DATA"),W.write(`${X}\r
|
|
25
25
|
.\r
|
|
26
|
-
`),await new Promise((
|
|
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
|
-
`;return Z}function
|
|
29
|
-
<html lang="en">
|
|
30
|
-
<head>
|
|
31
|
-
<meta charset="UTF-8">
|
|
32
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
33
|
-
<title>${U(J)} \u2014 Stacks Mail Preview</title>
|
|
34
|
-
<style>
|
|
35
|
-
:root { color-scheme: light dark; }
|
|
36
|
-
* { box-sizing: border-box; }
|
|
37
|
-
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: #f4f4f5; color: #111827; line-height: 1.5; }
|
|
38
|
-
@media (prefers-color-scheme: dark) {
|
|
39
|
-
body { background: #0a0a0a; color: #ededed; }
|
|
40
|
-
.panel { background: #1a1a1a; border-color: #2a2a2a; }
|
|
41
|
-
a { color: #818cf8; }
|
|
42
|
-
.muted { color: #9ca3af; }
|
|
43
|
-
pre { background: #2a2a2a; color: #ededed; }
|
|
44
|
-
}
|
|
45
|
-
header { padding: 16px 24px; background: #fff; border-bottom: 1px solid #e5e7eb; }
|
|
46
|
-
@media (prefers-color-scheme: dark) { header { background: #1a1a1a; border-color: #2a2a2a; } }
|
|
47
|
-
header h1 { margin: 0; font-size: 16px; font-weight: 600; }
|
|
48
|
-
header h1 a { color: inherit; text-decoration: none; }
|
|
49
|
-
header .crumb { font-size: 13px; color: #6b7280; }
|
|
50
|
-
main { padding: 24px; max-width: 1200px; margin: 0 auto; }
|
|
51
|
-
.panel { background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px; margin-bottom: 16px; }
|
|
52
|
-
.grid { display: grid; grid-template-columns: 220px 1fr; gap: 16px; }
|
|
53
|
-
.pill { display: inline-block; padding: 2px 8px; font-size: 12px; border-radius: 999px; background: #eef2ff; color: #3730a3; }
|
|
54
|
-
.toolbar { display: flex; gap: 8px; padding: 8px 0; flex-wrap: wrap; align-items: center; }
|
|
55
|
-
.toolbar a { padding: 6px 12px; border: 1px solid #d1d5db; border-radius: 6px; text-decoration: none; color: inherit; font-size: 13px; }
|
|
56
|
-
.toolbar a.active { background: #4f46e5; color: white; border-color: #4f46e5; }
|
|
57
|
-
.muted { color: #6b7280; font-size: 13px; }
|
|
58
|
-
iframe { width: 100%; min-height: 800px; border: 0; background: #fff; border-radius: 8px; }
|
|
59
|
-
iframe.mobile { max-width: 375px; margin: 0 auto; display: block; min-height: 700px; }
|
|
60
|
-
pre { background: #f3f4f6; padding: 12px; border-radius: 6px; overflow: auto; font-size: 12px; line-height: 1.5; max-height: 400px; }
|
|
61
|
-
.meta { display: grid; grid-template-columns: 80px 1fr; gap: 6px 12px; font-size: 13px; }
|
|
62
|
-
.meta dt { color: #6b7280; }
|
|
63
|
-
.meta dd { margin: 0; }
|
|
64
|
-
.empty { text-align: center; padding: 48px 24px; color: #6b7280; }
|
|
65
|
-
ul.list { list-style: none; padding: 0; margin: 0; }
|
|
66
|
-
ul.list li { padding: 12px; border-bottom: 1px solid #e5e7eb; }
|
|
67
|
-
@media (prefers-color-scheme: dark) { ul.list li { border-color: #2a2a2a; } }
|
|
68
|
-
ul.list li:last-child { border-bottom: 0; }
|
|
69
|
-
ul.list a { color: inherit; text-decoration: none; font-weight: 500; }
|
|
70
|
-
ul.list a:hover { color: #4f46e5; }
|
|
71
|
-
.error { background: #fef2f2; border: 1px solid #fecaca; color: #991b1b; padding: 12px 16px; border-radius: 6px; }
|
|
72
|
-
@media (prefers-color-scheme: dark) { .error { background: #1f0a0a; border-color: #7f1d1d; color: #fca5a5; } }
|
|
73
|
-
</style>
|
|
74
|
-
</head>
|
|
75
|
-
<body>
|
|
76
|
-
<header>
|
|
77
|
-
<h1><a href="${"/_stacks/mail/preview"}">Mail Preview</a> <span class="crumb">${U(J)}</span></h1>
|
|
78
|
-
</header>
|
|
79
|
-
<main>${Y}</main>
|
|
80
|
-
</body>
|
|
81
|
-
</html>`}function mZ(J){if(J.length===0)return AJ("Index",`
|
|
82
|
-
<div class="empty panel">
|
|
83
|
-
<p>No Mailables found in <code>app/Mail/</code>.</p>
|
|
84
|
-
<p class="muted">Run <code>./buddy make:mail Welcome</code> to scaffold one.</p>
|
|
85
|
-
</div>
|
|
86
|
-
`);let Y=J.map((Z)=>`
|
|
87
|
-
<li>
|
|
88
|
-
<a href="/_stacks/mail/preview/${U(Z.slug)}">${U(Z.name)}</a>
|
|
89
|
-
<div class="muted">app/Mail/${U(Z.name)}.ts \xB7 template: ${U(Z.slug)}.stx</div>
|
|
90
|
-
</li>
|
|
91
|
-
`).join("");return AJ("Index",`
|
|
92
|
-
<div class="panel">
|
|
93
|
-
<p class="muted">${J.length} mailable${J.length===1?"":"s"} discovered. Add sample props at
|
|
94
|
-
<code>resources/emails/_previews/<slug>.ts</code> for richer previews.</p>
|
|
95
|
-
<ul class="list">${Y}</ul>
|
|
96
|
-
</div>
|
|
97
|
-
`)}function oZ(J,Y,Z="desktop"){let{inspection:X,text:$,sampleProps:G,error:K}=Y,Q=`/_stacks/mail/preview/${J.slug}`,F=`${Q}/raw`,W=K?`<div class="panel"><div class="error"><strong>${U(J.name)} failed to render:</strong> ${U(K)}</div></div>`:"",z=K?"":Z==="text"?`<div class="panel"><pre>${U($||"(no plain-text body)")}</pre></div>`:`<div class="panel"><iframe src="${F}" class="${Z==="mobile"?"mobile":""}" title="${U(J.name)} body" sandbox="allow-same-origin"></iframe></div>`,V=G?`<dd><code>resources/emails/_previews/${U(J.slug)}.ts</code></dd>`:`<dd class="muted">No sample file \u2014 edit <code>resources/emails/_previews/${U(J.slug)}.ts</code> to customize.</dd>`;return AJ(J.name,`
|
|
98
|
-
${W}
|
|
99
|
-
|
|
100
|
-
<div class="grid">
|
|
101
|
-
<aside>
|
|
102
|
-
<div class="panel">
|
|
103
|
-
<dl class="meta">
|
|
104
|
-
<dt>Subject</dt><dd>${U(X.subject||"(no subject)")}</dd>
|
|
105
|
-
<dt>To</dt><dd>${t(X.to)}</dd>
|
|
106
|
-
${X.cc?.length?`<dt>Cc</dt><dd>${t(X.cc)}</dd>`:""}
|
|
107
|
-
${X.bcc?.length?`<dt>Bcc</dt><dd>${t(X.bcc)}</dd>`:""}
|
|
108
|
-
${X.from?`<dt>From</dt><dd>${t([X.from])}</dd>`:""}
|
|
109
|
-
${X.replyTo?`<dt>Reply-To</dt><dd>${t([X.replyTo])}</dd>`:""}
|
|
110
|
-
${X.template?`<dt>Template</dt><dd>${U(X.template.name)}.stx</dd>`:""}
|
|
111
|
-
${X.attachments?.length?`<dt>Attachments</dt><dd>${X.attachments.length}</dd>`:""}
|
|
112
|
-
</dl>
|
|
113
|
-
</div>
|
|
114
|
-
|
|
115
|
-
<div class="panel">
|
|
116
|
-
<h3 style="margin: 0 0 8px; font-size: 13px; font-weight: 600;">Sample props</h3>
|
|
117
|
-
${V}
|
|
118
|
-
${G?`<pre>${U(JSON.stringify(G,null,2))}</pre>`:""}
|
|
119
|
-
</div>
|
|
120
|
-
</aside>
|
|
121
|
-
|
|
122
|
-
<section>
|
|
123
|
-
<div class="toolbar">
|
|
124
|
-
<a href="${Q}" class="${Z==="desktop"?"active":""}">Desktop</a>
|
|
125
|
-
<a href="${Q}?view=mobile" class="${Z==="mobile"?"active":""}">Mobile</a>
|
|
126
|
-
<a href="${Q}?view=text" class="${Z==="text"?"active":""}">Text</a>
|
|
127
|
-
<span class="muted" style="margin-left: auto;">${U(J.name)}</span>
|
|
128
|
-
</div>
|
|
129
|
-
${z}
|
|
130
|
-
</section>
|
|
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,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};
|
|
26
|
+
`),await new Promise((O,C)=>{q.push({cmd:"DATA_END",resolve:O,reject:C})}),U=!0;let j=`${Date.now()}.${Math.random().toString(36).substring(2)}@${J.host}`;try{W.write(`QUIT\r
|
|
27
|
+
`)}catch{}W.end(),$(j)}catch(L){if(U){W.end();return}W.end(),G(L)}};if(J.encryption==="ssl")W=n.connect({host:J.host,port:J.port,servername:J.host},()=>{R.debug(`[SMTP] TLS connected to ${J.host}:${J.port}`),jJ()});else W=sJ.connect({host:J.host,port:J.port},()=>{R.debug(`[SMTP] Connected to ${J.host}:${J.port}`),jJ()});W.on("data",BJ),W.setTimeout(b.SMTP_TIMEOUT),W.on("timeout",()=>{W.destroy(Error(`SMTP socket timed out after ${b.SMTP_TIMEOUT}ms`))}),W.on("error",(L)=>{if(U)return;R.error(`[SMTP] Connection error to ${J.host}:${J.port}:`,L),G(L)}),W.on("close",(L)=>{if(R.debug(`[SMTP] Connection closed (hadError: ${L})`),U)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 JY}from"@stacksjs/database";var eJ=!1;function YY(){if(eJ)return;eJ=!0,console.warn("[email/idempotency] email_idempotency table missing \u2014 idempotency keys are accepted but NOT enforced. "+"Run migrations to enable dedup.")}function ZY(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 XY(J){try{let Y=await JY.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(ZY(Y))return YY(),null;throw Y}}async function $Y(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 JY.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(ZY($)){YY();return}let G=$?.message??"";if(G.includes("UNIQUE constraint")||G.includes("Duplicate entry"))return;throw $}}import{db as o}from"@stacksjs/database";var GY=!1;function e(){if(GY)return;GY=!0,console.warn("[email/suppression] email_suppressions table missing \u2014 suppression checks accepted but NOT enforced. "+"Run migrations to enable enforcement.")}function KZ(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 JJ(J){if(KZ(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 YJ(J){return String(J).trim().toLowerCase()}async function cX(J,Y){let Z=YJ(J);try{let X=o.selectFrom("email_suppressions").where("email","=",Z).select(["email"]);if(Y)X=X.where("type","=",Y);let $=await X.executeTakeFirst();return Boolean($)}catch(X){if(JJ(X))return e(),!1;throw X}}async function WZ(J){let Y=YJ(J);try{return await o.selectFrom("email_suppressions").where("email","=",Y).selectAll().execute()??[]}catch(Z){if(JJ(Z))return e(),[];throw Z}}async function KY(J,Y,Z){let X=YJ(J),$=new Date().toISOString().slice(0,19).replace("T"," ");try{await o.insertInto("email_suppressions").values({email:X,type:Y,reason:Z??null,created_at:$}).execute()}catch(G){if(JJ(G)){e();return}let K=G?.message??"";if(K.includes("UNIQUE constraint")||K.includes("Duplicate entry"))return;throw G}}async function gX(J,Y){let Z=YJ(J);try{await o.deleteFrom("email_suppressions").where("email","=",Z).where("type","=",Y).execute()}catch(X){if(JJ(X)){e();return}throw X}}async function QZ(){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 WY(J,Y){let Z=await QZ();if(Z==="off")return null;if(Z==="transactional-allowed"&&Y==="transactional")return null;let X=await WZ(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 zZ{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 ZJ.send({to:Z,from:this.from||{name:f.email.from?.name||"Stacks",address:f.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 HJ{drivers=new Map;defaultDriver;constructor(J={}){this.defaultDriver=J.defaultDriver||f.email.default||"ses",this.registerDefaultDrivers()}registerDefaultDrivers(){this.drivers.set("log",new c),this.drivers.set("ses",new m),this.drivers.set("sendgrid",new l),this.drivers.set("mailgun",new g),this.drivers.set("mailtrap",new d),this.drivers.set("smtp",new b),this.drivers.set("capture",new a)}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 XY(J.idempotencyKey);if(G)return G}let Z=await VZ(J);if(Z)return{success:!1,message:`suppressed:${Z}`,provider:"suppression"};let X={name:f.email.from?.name||"Stacks",address:f.email.from?.address||"no-reply@stacksjs.com"},$=await Y.send({...J,from:J.from||X});if(J.idempotencyKey)await $Y(J.idempotencyKey,J,$);return $}use(J){if(!this.drivers.has(J))throw Error(`Email driver '${J}' is not available`);return new HJ({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);FZ.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 VZ(J){let Y=qZ(J);if(Y.length===0)return null;for(let Z of Y){let X=await WY(Z,J.tag);if(X)return X}return null}function qZ(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 IJ;function QY(){if(!IJ){let J=f?.email?.default||process.env.MAIL_MAILER||"ses";IJ=new HJ({defaultDriver:J})}return IJ}var ZJ=new Proxy({},{get(J,Y){return QY()[Y]},set(J,Y,Z){return QY()[Y]=Z,!0}});import{createHmac as VY,timingSafeEqual as LZ}from"crypto";import XJ from"process";import{Buffer as qY}from"buffer";var OZ=2592000,UZ="/_stacks/email/unsubscribe";function LY(){let J=XJ.env.APP_KEY;if(!J||J.length<16){if(XJ.env.APP_ENV==="production"||XJ.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 FY(J){return J.toString("base64url")}function zY(J){return qY.from(J,"base64url")}function NZ(J,Y=OZ){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"},$=FY(qY.from(JSON.stringify(X))),G=FY(VY("sha256",LY()).update($).digest());return`${$}.${G}`}function G9(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,$=VY("sha256",LY()).update(Z).digest(),G;try{G=zY(X)}catch{return{valid:!1,reason:"malformed"}}if(G.length!==$.length||!LZ(G,$))return{valid:!1,reason:"bad_signature"};let K;try{K=JSON.parse(zY(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 IZ(J,Y,Z={}){let X=NZ(J,Y),$=(Z.baseUrl||XJ.env.APP_URL||"http://localhost").replace(/\/$/,""),G=(Z.routePrefix||UZ).replace(/\/$/,"");return`${$}${G}/${X}`}function K9(J,Y,Z){return{"List-Unsubscribe":`<${IZ(J,Y,Z)}>`,"List-Unsubscribe-Post":"List-Unsubscribe=One-Click"}}import{db as HZ}from"@stacksjs/database";var OY=!1;function RZ(){if(OY)return;OY=!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 _Z(J){let Y=J?.message??"";return Y.includes("no such table")||Y.includes("doesn't exist")}async function i(J,Y){if(!Y)return!0;try{return await HZ.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(_Z(Z))return RZ(),!0;let X=Z?.message??"";if(X.includes("UNIQUE constraint")||X.includes("Duplicate entry"))return!1;throw Z}}async function u(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 UY(J){await u("email:bounce-hard",J),await u("email:bounce",J)}async function NY(J){await u("email:bounce-soft",J),await u("email:bounce",J)}async function IY(J){await u("email:complaint",J)}async function HY(J){await u("email:unsubscribe",J)}function RY(J){switch(J){case"bounce-hard":return"bounce";case"complaint":return"complaint";case"unsubscribe":return"unsubscribe";default:return null}}import{createHmac as PZ,createVerify as PY,timingSafeEqual as RJ}from"crypto";import{Buffer as M}from"buffer";function BY(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 $=PZ("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(!RJ(G,K))return{ok:!1,reason:"bad-signature"};return{ok:!0}}function jY(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 $=_Y(Z,J.expectedUsername),G=_Y(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 _Y(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 RJ($,$),!1}return RJ(Z,X)}var BZ=/^sns\.[a-z0-9-]+\.amazonaws\.com$/;async function jZ(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 AY(J){let Y=J.message;if(!Y||!Y.Signature||!Y.SigningCertURL)return{ok:!1,reason:"missing-signature"};let Z=J.certUrlHostAllowlist??BZ,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??jZ)(Y.SigningCertURL)}catch{return{ok:!1,reason:"cert-fetch-failed"}}let G=AZ(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=PY(`RSA-${Q}`);return F.update(G,"utf8"),F.verify($,K)?{ok:!0}:{ok:!1,reason:"bad-signature"}}function AZ(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
|
+
`;return Z}function TY(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 $=PY("SHA256");return $.update(`${J.timestamp}${J.body}`,"utf8"),$.verify(J.publicKeyPem,X)?{ok:!0}:{ok:!1,reason:"bad-signature"}}var _J={status:200,body:{ok:!0,processed:!1,reason:"duplicate"}};function $J(J){return{status:401,body:{ok:!1,reason:J}}}function w(J){return{status:400,body:{ok:!1,reason:J}}}async function p(J,Y){let Z=RY(J);if(Z)await KY(Y.email,Z,Y.reason);switch(J){case"bounce-hard":await UY(Y);break;case"bounce-soft":await NY(Y);break;case"complaint":await IY(Y);break;case"unsubscribe":await HY(Y);break;case"delivered":break}}async function H9(J,Y){let Z;try{Z=JSON.parse(J)}catch{return w("invalid-json")}let X=Z.signature,$=Z["event-data"];if(!X||!$)return w("missing-fields");let G=BY({timestamp:X.timestamp,token:X.token,signature:X.signature,signingKey:Y.signingKey,toleranceSeconds:Y.toleranceSeconds});if(!G.ok)return $J(G.reason);if(!await i("mailgun",$.id))return _J;let Q=TZ($.event,$.severity);if(!Q)return{status:200,body:{ok:!0,processed:!1,reason:"unhandled-event"}};return await p(Q,{email:$.recipient,provider:"mailgun",reason:$.reason,raw:$}),{status:200,body:{ok:!0,processed:!0,classification:Q}}}function TZ(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 R9(J,Y,Z,X){let $=jY({authorizationHeader:Y,expectedUsername:X.username,expectedPassword:X.password,sourceIp:Z,ipAllowlist:X.ipAllowlist});if(!$.ok)return $J($.reason);let G;try{G=JSON.parse(J)}catch{return w("invalid-json")}let K=String(G.ID??G.MessageID??""),Q=String(G.Email??G.Recipient??"");if(!Q)return w("missing-recipient");if(!await i("postmark",K))return _J;let W=DZ(G);if(!W)return{status:200,body:{ok:!0,processed:!1,reason:"unhandled-event"}};return await p(W,{email:Q,provider:"postmark",reason:G.Description,raw:G}),{status:200,body:{ok:!0,processed:!0,classification:W}}}function DZ(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 _9(J,Y={}){let Z;try{Z=JSON.parse(J)}catch{return w("invalid-json")}let X=await AY({message:Z,certUrlHostAllowlist:Y.certUrlHostAllowlist,fetchCert:Y.fetchCert});if(!X.ok)return $J(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 w("invalid-inner-json")}if(!await i("ses",Z.MessageId))return _J;let K=[];if($.notificationType==="Bounce"&&$.bounce){let Q=$.bounce.bounceType==="Permanent"?"bounce-hard":"bounce-soft";for(let F of $.bounce.bouncedRecipients)await p(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 p("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 p("delivered",{email:Q,provider:"ses",raw:$}),K.push("delivered");return{status:200,body:{ok:!0,processed:K.length>0,classification:K[0]}}}async function P9(J,Y,Z,X){let $=TY({body:J,signature:Y,timestamp:Z,publicKeyPem:X.publicKeyPem,toleranceSeconds:X.toleranceSeconds});if(!$.ok)return $J($.reason);let G;try{G=JSON.parse(J)}catch{return w("invalid-json")}if(!Array.isArray(G))return w("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 i("sendgrid",W))continue;let V=xZ(F.event,F.type);if(!V)continue;await p(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 xZ(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 DY}from"@stacksjs/config";class MZ{_to=[];_cc=[];_bcc=[];_replyTo;_from;_subject;_text;_html;_template;_attachments=[];to(J){return this._to=PJ(J),this}cc(J){return this._cc=PJ(J),this}bcc(J){return this._bcc=PJ(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||EZ(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||wZ(),...this._cc.length?{cc:this._cc}:{},...this._bcc.length?{bcc:this._bcc}:{},...Y?{html:Y}:{},...Z?{text:Z}:{},...this._attachments.length?{attachments:await CZ(this._attachments)}:{}};if(this._replyTo)X.replyTo=this._replyTo;return(J.driver?ZJ.use(J.driver):ZJ).send(X)}}function PJ(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 wZ(){return{name:DY.email.from?.name||"Stacks",address:DY.email.from?.address||"no-reply@stacksjs.com"}}function EZ(J){let Y=Math.max(J.lastIndexOf("/"),J.lastIndexOf("\\"));return Y===-1?J:J.slice(Y+1)}async function CZ(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}))}export{G9 as verifyUnsubscribeToken,AY as verifySesSnsSignature,TY as verifySendgridSignature,jY as verifyPostmarkAuth,BY as verifyMailgunSignature,gX as unsuppress,iZ as templateExists,I as template,RY as suppressionTypeFor,KY as suppress,g9 as shouldInlineByDefault,rJ as ses,nJ as sendgrid,pY as safe,r9 as renderPreviewHtml,n9 as renderMailablePreview,i9 as renderIndexHtml,oZ as renderHtml,i as recordWebhookEventOrSkip,$Y as recordEmailIdempotency,mJ as mailtrap,lJ as mailgun,ZJ as mail,gJ as log,m9 as loadSampleProps,rZ as listTemplates,cX as isSuppressed,KZ as isMissingTableError,c9 as inlineCss,_9 as handleSesWebhook,P9 as handleSendgridWebhook,R9 as handlePostmarkWebhook,H9 as handleMailgunWebhook,WZ as getSuppressions,QZ as getSuppressionPolicy,XY as findEmailByIdempotencyKey,HY as emitEmailUnsubscribe,IY as emitEmailComplaint,NY as emitEmailBounceSoft,UY as emitEmailBounceHard,l9 as discoverMailables,NZ as createUnsubscribeToken,WY as checkSuppressionFor,xJ as capture,IZ as buildUnsubscribeUrl,K9 as buildListUnsubscribeHeaders,zJ as SafeHtml,MZ as Mailable,HJ as Mail,zZ as Email};
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/email",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"
|
|
4
|
+
"sideEffects": false,
|
|
5
|
+
"version": "0.70.86",
|
|
5
6
|
"description": "The Stacks Email integration. Painlessly create & manage your inboxes, templates, and send emails.",
|
|
6
7
|
"author": "Chris Breuer",
|
|
7
8
|
"contributors": [
|
|
@@ -57,10 +58,10 @@
|
|
|
57
58
|
"@stacksjs/ts-cloud": "^0.7.17"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
60
|
-
"@stacksjs/cli": "0.70.
|
|
61
|
-
"@stacksjs/config": "0.70.
|
|
61
|
+
"@stacksjs/cli": "0.70.86",
|
|
62
|
+
"@stacksjs/config": "0.70.86",
|
|
62
63
|
"better-dx": "^0.2.16",
|
|
63
|
-
"@stacksjs/error-handling": "0.70.
|
|
64
|
-
"@stacksjs/types": "0.70.
|
|
64
|
+
"@stacksjs/error-handling": "0.70.86",
|
|
65
|
+
"@stacksjs/types": "0.70.86"
|
|
65
66
|
}
|
|
66
67
|
}
|