@stacksjs/email 0.70.37 → 0.70.42

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.
@@ -0,0 +1,13 @@
1
+ import type { EmailAddress, EmailDriver, EmailDriverConfig, EmailMessage, EmailResult } from '@stacksjs/types';
2
+ import type { TemplateOptions } from '../template';
3
+ export declare abstract class BaseEmailDriver implements EmailDriver {
4
+ abstract name: string;
5
+ protected config: Required<EmailDriverConfig>;
6
+ constructor(config?: EmailDriverConfig);
7
+ configure(config: EmailDriverConfig): void;
8
+ abstract send(message: EmailMessage, options?: TemplateOptions): Promise<EmailResult>;
9
+ protected validateMessage(message: EmailMessage): boolean;
10
+ protected formatAddresses(addresses: string | string[] | EmailAddress[] | undefined): string[];
11
+ protected handleError(error: unknown, message: EmailMessage): Promise<EmailResult>;
12
+ protected handleSuccess(message: EmailMessage, messageId?: string): Promise<EmailResult>;
13
+ }
@@ -5,3 +5,4 @@ export declare class MailgunDriver extends BaseEmailDriver {
5
5
  name: string;
6
6
  send(message: EmailMessage, options?: TemplateOptions): Promise<EmailResult>;
7
7
  }
8
+ export default MailgunDriver;
@@ -0,0 +1,14 @@
1
+ import { BaseEmailDriver } from './base';
2
+ import type { EmailAddress, EmailMessage, EmailResult } from '@stacksjs/types';
3
+ import type { TemplateOptions } from '../template';
4
+ /**
5
+ * SMTP Driver for email sending
6
+ * Works with any SMTP server: Mailtrap, Mailgun, SendGrid, SES, etc.
7
+ * Supports STARTTLS (port 587) and direct TLS (port 465)
8
+ */
9
+ export declare class SMTPDriver extends BaseEmailDriver {
10
+ name: string;
11
+ send(message: EmailMessage, options?: TemplateOptions): Promise<EmailResult>;
12
+ protected formatAddresses(addresses: string | string[] | EmailAddress[] | undefined): string[];
13
+ }
14
+ export default SMTPDriver;
@@ -1,4 +1,4 @@
1
- export * from './drivers';
1
+ export * from './drivers/index';
2
2
  export * from './email';
3
3
  export * from './mailable';
4
4
  export * from './template';
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  // @bun
2
- var MJ=Object.defineProperty;var NJ=(J)=>J;function xJ(J,U){this[J]=NJ.bind(null,U)}var E=(J,U)=>{for(var X in U)MJ(J,X,{get:U[X],enumerable:!0,configurable:!0,set:xJ.bind(U,X)})};var y=import.meta.require;var GJ={};E(GJ,{default:()=>vJ,LogEmailDriver:()=>C});import{mkdir as jJ,writeFile as yJ}from"fs/promises";import{join as $J,resolve as KJ}from"path";import{log as WJ}from"@stacksjs/logging";import{config as i}from"@stacksjs/config";import{log as EJ}from"@stacksjs/logging";import{fs as w}from"@stacksjs/storage";import{resourcesPath as T}from"@stacksjs/path";import{join as S}from"path";function XJ(){let J=i.app.primaryColor||"#3b82f6";return{appName:i.app.name||"Stacks",appUrl:i.app.url||"https://localhost",primaryColor:J,primaryColorDark:TJ(J,15),year:new Date().getFullYear()}}function TJ(J,U){let X=Number.parseInt(J.replace("#",""),16),Y=Math.round(2.55*U),Z=Math.max(0,(X>>16)-Y),W=Math.max(0,(X>>8&255)-Y),K=Math.max(0,(X&255)-Y);return`#${(16777216+Z*65536+W*256+K).toString(16).slice(1)}`}function d(J,U){let X=J;for(let[Y,Z]of Object.entries(U)){let W=Y.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),K=new RegExp(`\\{\\{\\s*${W}\\s*\\}\\}`,"g");X=X.replace(K,String(Z??""))}return X}function YJ(J){if(J.endsWith(".stx")){let Y=T(S("emails",J));if(w.existsSync(Y))return{path:Y,type:"stx"};return null}if(J.endsWith(".html")){let Y=T(S("emails",J));if(w.existsSync(Y))return{path:Y,type:"html"};return null}let U=T(S("emails",`${J}.stx`));if(w.existsSync(U))return{path:U,type:"stx"};let X=T(S("emails",`${J}.html`));if(w.existsSync(X))return{path:X,type:"html"};return null}function SJ(J){let U=J.endsWith(".html")?J:`${J}.html`,X=T(S("emails",U));if(w.existsSync(X))return w.readFileSync(X,"utf-8");return null}function PJ(J){return SJ(`layouts/${J}`)}function ZJ(J){return J.replace(/<br\s*\/?>/gi,`
2
+ var NJ=Object.defineProperty;var xJ=(J)=>J;function EJ(J,U){this[J]=xJ.bind(null,U)}var T=(J,U)=>{for(var X in U)NJ(J,X,{get:U[X],enumerable:!0,configurable:!0,set:EJ.bind(U,X)})};var y=import.meta.require;var GJ={};T(GJ,{default:()=>vJ,LogEmailDriver:()=>C});import{mkdir as jJ,writeFile as yJ}from"fs/promises";import{join as $J,resolve as KJ}from"path";import{log as WJ}from"@stacksjs/logging";import{config as i}from"@stacksjs/config";import{log as TJ}from"@stacksjs/logging";import{fs as I}from"@stacksjs/storage";import{resourcesPath as _}from"@stacksjs/path";import{join as S}from"path";function XJ(){let J=i.app.primaryColor||"#3b82f6";return{appName:i.app.name||"Stacks",appUrl:i.app.url||"https://localhost",primaryColor:J,primaryColorDark:_J(J,15),year:new Date().getFullYear()}}function _J(J,U){let X=Number.parseInt(J.replace("#",""),16),Y=Math.round(2.55*U),Z=Math.max(0,(X>>16)-Y),W=Math.max(0,(X>>8&255)-Y),K=Math.max(0,(X&255)-Y);return`#${(16777216+Z*65536+W*256+K).toString(16).slice(1)}`}function g(J,U){let X=J;for(let[Y,Z]of Object.entries(U)){let W=Y.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),K=new RegExp(`\\{\\{\\s*${W}\\s*\\}\\}`,"g");X=X.replace(K,String(Z??""))}return X}function YJ(J){if(J.endsWith(".stx")){let Y=_(S("emails",J));if(I.existsSync(Y))return{path:Y,type:"stx"};return null}if(J.endsWith(".html")){let Y=_(S("emails",J));if(I.existsSync(Y))return{path:Y,type:"html"};return null}let U=_(S("emails",`${J}.stx`));if(I.existsSync(U))return{path:U,type:"stx"};let X=_(S("emails",`${J}.html`));if(I.existsSync(X))return{path:X,type:"html"};return null}function SJ(J){let U=J.endsWith(".html")?J:`${J}.html`,X=_(S("emails",U));if(I.existsSync(X))return I.readFileSync(X,"utf-8");return null}function PJ(J){return SJ(`layouts/${J}`)}function ZJ(J){return J.replace(/<br\s*\/?>/gi,`
3
3
  `).replace(/<\/(p|div|h[1-6]|li|tr)>/gi,`
4
4
  `).replace(/<\/td>/gi,"\t").replace(/<[^>]*>/g,"").replace(/&nbsp;/g," ").replace(/&amp;/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&quot;/g,'"').replace(/&#39;/g,"'").replace(/&copy;/g,"(c)").replace(/\n\s*\n\s*\n/g,`
5
5
 
6
- `).trim()}async function Q(J,U={}){let{variables:X={},layout:Y="base",subject:Z=""}=U,W={...XJ(),subject:Z,...X},K=YJ(J);if(!K)return console.warn(`[Email Template] Template "${J}" not found`),{html:"",text:""};if(K.type==="stx")try{let{renderEmail:z}=await import("@stacksjs/stx");return await z(K.path,W)}catch(z){return EJ.warn(`[email] STX template rendering failed for ${J}: ${z instanceof Error?z.message:String(z)}`),{html:"",text:""}}let G=w.readFileSync(K.path,"utf-8");G=d(G,W);let $;if(Y!==!1){let z=PJ(Y);if(!z)console.warn(`[Email Template] Layout "${Y}" not found, using content only`),$=G;else W.content=G,$=d(z,W)}else $=G;let F=ZJ($);return{html:$,text:F}}function $U(J,U={}){let X={...XJ(),...U},Y=d(J,X),Z=ZJ(Y);return{html:Y,text:Z}}function KU(J){return YJ(J)!==null}function WU(){let J=T("emails");if(!w.existsSync(J))return[];let U=[];function X(Y,Z=""){let W=w.readdirSync(Y,{withFileTypes:!0});for(let K of W)if(K.isDirectory()&&K.name!=="layouts")X(S(Y,K.name),`${Z}${K.name}/`);else if(K.isFile()&&(K.name.endsWith(".html")||K.name.endsWith(".stx"))){let G=K.name.replace(/\.(html|stx)$/,"");if(!U.includes(`${Z}${G}`))U.push(`${Z}${G}`)}}return X(J),U}import{config as DJ}from"@stacksjs/config";import{log as kJ}from"@stacksjs/logging";class V{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&&!DJ.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");let U=/^[^\s<>"\\\r\n\t]+@[^\s<>"\\\r\n\t]+$/,X=(Z,W)=>{if(!Z)return;if(!U.test(Z))throw Error(`Email ${W} address is malformed or contains forbidden characters: ${JSON.stringify(Z)}`)},Y=(Z)=>{if(!Z)return[];if(typeof Z==="string")return[Z];if(Array.isArray(Z))return Z.flatMap((K)=>typeof K==="string"?[K]:K?.address?[K.address]:[]);let W=Z;return W.address?[W.address]:[]};if(J.from)X(J.from.address,"from");for(let Z of Y(J.to))X(Z,"to");for(let Z of Y(J.cc))X(Z,"cc");for(let Z of Y(J.bcc))X(Z,"bcc");return!0}formatAddresses(J){if(!J)return[];if(typeof J==="string")return[J];return J.map((U)=>{if(typeof addr==="string")return addr;if(!addr.name)return addr.address;return`${/[",()<>[\]:;@\\]/.test(addr.name)?`"${addr.name.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")}"`:addr.name} <${addr.address}>`})}async handleError(J,U){let X=J instanceof Error?J:Error(String(J));kJ.error(`[${this.name}] Email sending failed`,{error:X.message,stack:X.stack,to:U.to,subject:U.subject});let Y={message:`Email sending failed: ${X.message}`,success:!1,provider:this.name};if(U.onError){let Z=U.onError(X),W=Z instanceof Promise?await Z:Z;Y={...Y,...W,success:!1,provider:this.name}}return Y}async handleSuccess(J,U){let X={message:"Email sent successfully",success:!0,provider:this.name,messageId:U};try{if(J.handle){let Y=J.handle(),Z=Y instanceof Promise?await Y:Y;X={...X,...Z,success:!0,provider:this.name,messageId:U}}if(J.onSuccess){let Y=J.onSuccess(),Z=Y instanceof Promise?await Y:Y;X={...X,...Z,success:!0,provider:this.name,messageId:U}}}catch(Y){return this.handleError(Y,J)}return X}}var FJ=100,P=[];class C extends V{name="log";resolveDir(){let J=process.env.LOG_MAIL_DIR;if(J)return KJ(J);return KJ($J(import.meta.dir,"..","..","..","..","..","logs","mail"))}async send(J,U){try{this.validateMessage(J);let X;if(J.template){let z=await Q(J.template,U);if(z)X={html:z.html,text:z.text}}let Y=X?.html??J.html,Z=X?.text??J.text,W=new Date,K=(J.subject||"no-subject").replace(/[^\w.-]+/g,"-").slice(0,60),G=`${W.toISOString().replace(/[:.]/g,"-")}-${K}.html`,$=this.resolveDir();try{await jJ($,{recursive:!0});let z=$J($,G),j=Y?Y:Z?`<pre>${bJ(Z)}</pre>`:"<em>(empty body)</em>",_=CJ({stamp:W,message:J});await yJ(z,`${_}
6
+ `).trim()}async function V(J,U={}){let{variables:X={},layout:Y="base",subject:Z=""}=U,W={...XJ(),subject:Z,...X},K=YJ(J);if(!K)return console.warn(`[Email Template] Template "${J}" not found`),{html:"",text:""};if(K.type==="stx")try{let{renderEmail:z}=await import("@stacksjs/stx");return await z(K.path,W)}catch(z){return TJ.warn(`[email] STX template rendering failed for ${J}: ${z instanceof Error?z.message:String(z)}`),{html:"",text:""}}let G=I.readFileSync(K.path,"utf-8");G=g(G,W);let $;if(Y!==!1){let z=PJ(Y);if(!z)console.warn(`[Email Template] Layout "${Y}" not found, using content only`),$=G;else W.content=G,$=g(z,W)}else $=G;let F=ZJ($);return{html:$,text:F}}function $U(J,U={}){let X={...XJ(),...U},Y=g(J,X),Z=ZJ(Y);return{html:Y,text:Z}}function KU(J){return YJ(J)!==null}function WU(){let J=_("emails");if(!I.existsSync(J))return[];let U=[];function X(Y,Z=""){let W=I.readdirSync(Y,{withFileTypes:!0});for(let K of W)if(K.isDirectory()&&K.name!=="layouts")X(S(Y,K.name),`${Z}${K.name}/`);else if(K.isFile()&&(K.name.endsWith(".html")||K.name.endsWith(".stx"))){let G=K.name.replace(/\.(html|stx)$/,"");if(!U.includes(`${Z}${G}`))U.push(`${Z}${G}`)}}return X(J),U}import{config as DJ}from"@stacksjs/config";import{log as kJ}from"@stacksjs/logging";class H{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&&!DJ.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");let U=/^[^\s<>"\\\r\n\t]+@[^\s<>"\\\r\n\t]+$/,X=(Z,W)=>{if(!Z)return;if(!U.test(Z))throw Error(`Email ${W} address is malformed or contains forbidden characters: ${JSON.stringify(Z)}`)},Y=(Z)=>{if(!Z)return[];if(typeof Z==="string")return[Z];if(Array.isArray(Z))return Z.flatMap((K)=>typeof K==="string"?[K]:K?.address?[K.address]:[]);let W=Z;return W.address?[W.address]:[]};if(J.from)X(J.from.address,"from");for(let Z of Y(J.to))X(Z,"to");for(let Z of Y(J.cc))X(Z,"cc");for(let Z of Y(J.bcc))X(Z,"bcc");return!0}formatAddresses(J){if(!J)return[];if(typeof J==="string")return[J];return J.map((U)=>{if(typeof U==="string")return U;if(!U.name)return U.address;return`${/[",()<>[\]:;@\\]/.test(U.name)?`"${U.name.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")}"`:U.name} <${U.address}>`})}async handleError(J,U){let X=J instanceof Error?J:Error(String(J));kJ.error(`[${this.name}] Email sending failed`,{error:X.message,stack:X.stack,to:U.to,subject:U.subject});let Y={message:`Email sending failed: ${X.message}`,success:!1,provider:this.name};if(U.onError){let Z=U.onError(X),W=Z instanceof Promise?await Z:Z;Y={...Y,...W,success:!1,provider:this.name}}return Y}async handleSuccess(J,U){let X={message:"Email sent successfully",success:!0,provider:this.name,messageId:U};try{if(J.handle){let Y=J.handle(),Z=Y instanceof Promise?await Y:Y;X={...X,...Z,success:!0,provider:this.name,messageId:U}}if(J.onSuccess){let Y=J.onSuccess(),Z=Y instanceof Promise?await Y:Y;X={...X,...Z,success:!0,provider:this.name,messageId:U}}}catch(Y){return this.handleError(Y,J)}return X}}var FJ=100,P=[];class C extends H{name="log";resolveDir(){let J=process.env.LOG_MAIL_DIR;if(J)return KJ(J);return KJ($J(import.meta.dir,"..","..","..","..","..","logs","mail"))}async send(J,U){try{this.validateMessage(J);let X;if(J.template){let z=await V(J.template,U);if(z)X={html:z.html,text:z.text}}let Y=X?.html??J.html,Z=X?.text??J.text,W=new Date,K=(J.subject||"no-subject").replace(/[^\w.-]+/g,"-").slice(0,60),G=`${W.toISOString().replace(/[:.]/g,"-")}-${K}.html`,$=this.resolveDir();try{await jJ($,{recursive:!0});let z=$J($,G),j=Y?Y:Z?`<pre>${bJ(Z)}</pre>`:"<em>(empty body)</em>",Q=CJ({stamp:W,message:J});await yJ(z,`${Q}
7
7
  ${j}
8
- `)}catch(z){WJ.warn(`[email:log] could not write inspection file: ${z.message}`)}let F=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(WJ.info(`[email:log] would send \u2192 ${F} :: ${J.subject}`),P.push({...J,sentAt:W,rendered:X}),P.length>FJ)P.splice(0,P.length-FJ);return this.handleSuccess(J,`log-${W.getTime()}`)}catch(X){return this.handleError(X,J)}}static captured(){return P}static reset(){P.length=0}}function CJ({stamp:J,message:U}){return["<!--",` Captured by @stacksjs/email log driver at ${J.toISOString()}`,` From: ${n(U.from)}`,` To: ${g(U.to)}`,U.cc?` Cc: ${g(U.cc)}`:null,U.bcc?` Bcc: ${g(U.bcc)}`:null,` Subject: ${U.subject}`,"-->"].filter(Boolean).join(`
9
- `)}function n(J){if(!J)return"";if(typeof J==="string")return J;let U=J;return U.name?`${U.name} <${U.address??""}>`:U.address??""}function g(J){if(Array.isArray(J))return J.map(n).join(", ");return n(J)}function bJ(J){return J.replace(/[&<>"']/g,(U)=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[U])}var vJ=C;var OJ={};E(OJ,{default:()=>hJ,MailgunDriver:()=>b});import{Buffer as zJ}from"buffer";import{config as R}from"@stacksjs/config";import{log as o}from"@stacksjs/logging";class b extends V{name="mailgun";apiKey=null;domain=null;endpoint=null;getConfig(){if(!this.apiKey||!this.domain||!this.endpoint)this.apiKey=R.services.mailgun?.apiKey??"",this.domain=R.services.mailgun?.domain??"",this.endpoint=R.services.mailgun?.endpoint??"api.mailgun.net";return{apiKey:this.apiKey,domain:this.domain,endpoint:this.endpoint}}async send(J,U){let{domain:X}=this.getConfig(),Y={provider:this.name,to:J.to,subject:J.subject,domain:X};o.info("Sending email via Mailgun...",Y);try{this.validateMessage(J);let Z;if(J.template){let F=await Q(J.template,U);if(F&&"html"in F)Z=F.html}let W=Z||J.html,K=new FormData,G={address:J.from?.address||R.email.from?.address||"",name:J.from?.name||R.email.from?.name};if(K.append("from",this.formatMailgunAddress(G)),this.formatMailgunAddresses(J.to).forEach((F)=>K.append("to",F)),J.cc)this.formatMailgunAddresses(J.cc).forEach((F)=>K.append("cc",F));if(J.bcc)this.formatMailgunAddresses(J.bcc).forEach((F)=>K.append("bcc",F));if(K.append("subject",J.subject),W)K.append("html",W);if(J.text)K.append("text",J.text);if(J.attachments)J.attachments.forEach((F)=>{let z=typeof F.content==="string"?F.content:this.arrayBufferToBase64(F.content);K.append("attachment",new Blob([z],{type:F.contentType}),F.filename)});let $=await this.sendWithRetry(K);return this.handleSuccess(J,$.id)}catch(Z){return this.handleError(Z,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((U)=>{if(typeof addr==="string")return addr;if(!addr.name)return addr.address;return`${/[",()<>[\]:;@\\]/.test(addr.name)?`"${addr.name.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")}"`:addr.name} <${addr.address}>`})}arrayBufferToBase64(J){let U="",X=new Uint8Array(J),Y=X.byteLength;for(let Z=0;Z<Y;Z++)U+=String.fromCharCode(X[Z]??0);return typeof btoa==="function"?btoa(U):zJ.from(U).toString("base64")}async sendWithRetry(J,U=1){let{apiKey:X,domain:Y,endpoint:Z}=this.getConfig(),W=`https://${Z}/v3/${Y}/messages`,K=zJ.from(`api:${X}`).toString("base64");try{let G=await fetch(W,{method:"POST",headers:{Authorization:`Basic ${K}`},body:J});if(!G.ok){let F=await G.json();throw Error(`Mailgun API error: ${G.status} - ${JSON.stringify(F)}`)}let $=await G.json();return o.info(`[${this.name}] Email sent successfully`,{attempt:U,messageId:$.id}),$}catch(G){if(U<(R.services.mailgun?.maxRetries??3)){let $=R.services.mailgun?.retryTimeout??1000;return o.warn(`[${this.name}] Email send failed, retrying (${U}/${R.services.mailgun?.maxRetries??3})`),await new Promise((F)=>setTimeout(F,$)),this.sendWithRetry(J,U+1)}throw G}}}var hJ=b;var qJ={};E(qJ,{default:()=>fJ,MailtrapDriver:()=>v});import{Buffer as uJ}from"buffer";import{config as I}from"@stacksjs/config";import{log as r}from"@stacksjs/logging";class v extends V{name="mailtrap";host=null;token=null;inboxId=null;getConfig(){if(this.host===null||this.token===null||this.inboxId===null)this.host=I.services.mailtrap?.host??"https://sandbox.api.mailtrap.io/api/send",this.token=I.services.mailtrap?.token??"",this.inboxId=I.services.mailtrap?.inboxId?Number(I.services.mailtrap.inboxId):void 0;return{host:this.host,token:this.token,inboxId:this.inboxId}}async send(J,U){let{inboxId:X}=this.getConfig(),Y={provider:this.name,to:J.to,subject:J.subject,inboxId:X};r.info("Sending email via Mailtrap...",Y);try{this.validateMessage(J);let Z;if(J.template)Z=await Q(J.template,U);let W=Z?.html||J.html,K={from:{email:J.from?.address||I.email.from?.address||"",name:J.from?.name||I.email.from?.name},to:this.formatMailtrapAddresses(J.to),...J.cc&&{cc:this.formatMailtrapAddresses(J.cc)},...J.bcc&&{bcc:this.formatMailtrapAddresses(J.bcc)},subject:J.subject,...W&&{html:W},...J.text&&{text:J.text},...J.attachments&&{attachments:J.attachments.map(($)=>({filename:$.filename,content:typeof $.content==="string"?$.content:this.arrayBufferToBase64($.content),type:$.contentType||"application/octet-stream"}))}},G=await this.sendWithRetry(K);return this.handleSuccess(J,G.message_ids?.[0])}catch(Z){return this.handleError(Z,J)}}formatMailtrapAddresses(J){if(!J)return[];if(typeof J==="string")return[{email:J}];return J.map((U)=>{if(typeof U==="string")return{email:U};return{email:U.address,...U.name&&{name:U.name}}})}arrayBufferToBase64(J){let U="",X=new Uint8Array(J),Y=X.byteLength;for(let Z=0;Z<Y;Z++)U+=String.fromCharCode(X[Z]??0);return typeof btoa==="function"?btoa(U):uJ.from(U).toString("base64")}async sendWithRetry(J,U=1){let{host:X,token:Y,inboxId:Z}=this.getConfig();if(!Z)throw Error("Mailtrap inbox ID is required but not provided. Please set MAILTRAP_INBOX_ID in your environment variables.");let W=`${X}/${Z}`;try{let K=await fetch(W,{method:"POST",headers:{Authorization:`Bearer ${Y}`,"Content-Type":"application/json"},body:JSON.stringify(J)});if(!K.ok){let $=await K.json();throw Error(`Mailtrap API error: ${K.status} - ${JSON.stringify($)}`)}let G=await K.json();return r.info(`[${this.name}] Email sent successfully`,{attempt:U,messageId:G.message_ids?.[0]}),G}catch(K){if(U<(I.services.mailtrap?.maxRetries??3)){let G=I.services.mailtrap?.retryTimeout??1000;return r.warn(`[${this.name}] Email send failed, retrying (${U}/${I.services.mailtrap?.maxRetries??3})`),await new Promise(($)=>setTimeout($,G)),this.sendWithRetry(J,U+1)}throw K}}}var fJ=v;var QJ={};E(QJ,{NodemailerDriver:()=>_J});class _J{async send(){throw Error("Nodemailer driver is not yet implemented. Use smtp, ses, sendgrid, mailgun, or mailtrap instead.")}}var VJ={};E(VJ,{default:()=>pJ,SendGridDriver:()=>h});import{Buffer as cJ}from"buffer";import{config as N}from"@stacksjs/config";import{log as m}from"@stacksjs/logging";class h extends V{name="sendgrid";apiKey=null;getApiKey(){if(!this.apiKey)this.apiKey=N.services.sendgrid?.apiKey??"";return this.apiKey}async send(J,U){let X={provider:this.name,to:J.to,subject:J.subject};m.info("Sending email via SendGrid...",X);try{this.validateMessage(J);let Y;if(J.template){let $=await Q(J.template,U);if($&&"html"in $)Y=$.html}let Z=Y||J.html,W=[];if(Z)W.push({type:"text/html",value:Z});if(J.text)W.push({type:"text/plain",value:J.text});if(W.length===0)throw Error("Email must have either HTML or text content");let K={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||N.email.from?.address||"",name:J.from?.name||N.email.from?.name},content:W,...J.attachments&&{attachments:J.attachments.map(($)=>({filename:$.filename,content:typeof $.content==="string"?$.content:this.arrayBufferToBase64($.content),type:$.contentType,disposition:"attachment"}))}},G=await this.sendWithRetry(K);return this.handleSuccess(J,G.headers?.get("x-message-id")??void 0)}catch(Y){return this.handleError(Y,J)}}formatSendGridAddresses(J){if(!J)return[];if(typeof J==="string")return[{email:J}];return J.map((U)=>{if(typeof U==="string")return{email:U};return{email:U.address,...U.name&&{name:U.name}}})}arrayBufferToBase64(J){let U="",X=new Uint8Array(J),Y=X.byteLength;for(let Z=0;Z<Y;Z++)U+=String.fromCharCode(X[Z]??0);return typeof btoa==="function"?btoa(U):cJ.from(U).toString("base64")}async sendWithRetry(J,U=1){try{let X=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(!X.ok){let Y=await X.json(),Z=Error(`SendGrid API error: ${X.status} - ${JSON.stringify(Y)}`);throw Z.status=X.status,Z}return m.info(`[${this.name}] Email sent successfully`,{attempt:U}),X}catch(X){let Y=X?.status;if(!(typeof Y==="number"&&Y>=400&&Y<500&&Y!==429)&&U<(N.services.sendgrid?.maxRetries??3)){let W=N.services.sendgrid?.retryTimeout??1000;return m.warn(`[${this.name}] Email send failed, retrying (${U}/${N.services.sendgrid?.maxRetries??3})`),await new Promise((K)=>setTimeout(K,W)),this.sendWithRetry(J,U+1)}throw X}}}var pJ=h;var HJ={};E(HJ,{default:()=>iJ,SESDriver:()=>u});import{SESClient as lJ}from"@stacksjs/ts-cloud";import{config as D}from"@stacksjs/config";class u extends V{name="ses";client=null;getClient(){if(!this.client)this.client=new lJ(D?.services?.ses?.region||"us-east-1");return this.client}async send(J,U){try{this.validateMessage(J);let X;if(J.template){let K=await Q(J.template,U);if(K&&"html"in K)X=K.html}let Y=X||J.html,Z={};if(Y)Z.Html={Charset:D.email.charset||"UTF-8",Data:Y};if(J.text)Z.Text={Charset:D.email.charset||"UTF-8",Data:J.text};if(Object.keys(Z).length===0)throw Error("Email must have either HTML or text content");let W=await this.getClient().sendEmail({FromEmailAddress:this.formatSourceAddress({address:J.from?.address||D.email.from?.address||"",name:J.from?.name||D.email.from?.name}),Destination:{ToAddresses:this.formatAddresses(J.to),CcAddresses:this.formatAddresses(J.cc),BccAddresses:this.formatAddresses(J.bcc)},Content:{Simple:{Subject:{Charset:D.email.charset||"UTF-8",Data:J.subject},Body:Z}}});return this.handleSuccess(J,W.MessageId)}catch(X){return this.handleError(X,J)}}formatSourceAddress(J){return J.name?`${J.name} <${J.address}>`:J.address}formatAddresses(J){if(!J)return[];if(typeof J==="string")return[J];return J.map((U)=>typeof U==="string"?U:U.address)}}var iJ=u;import{config as k}from"@stacksjs/config";import{Buffer as t}from"buffer";import*as f from"tls";import*as LJ from"net";import{config as B}from"@stacksjs/config";import{log as L}from"@stacksjs/logging";function dJ(J){if(/^[\x00-\x7F]*$/.test(J))return J;return`=?UTF-8?B?${t.from(J,"utf-8").toString("base64")}?=`}var gJ=/^[^\s<>"\\\r\n\t]+@[^\s<>"\\\r\n\t]+$/;function BJ(J,U){if(typeof J!=="string"||!gJ.test(J))throw Error(`[smtp] Refusing to send: ${U} envelope address contains forbidden characters or is malformed: ${JSON.stringify(J)}`)}class x extends V{static SMTP_TIMEOUT=30000;name="smtp";smtpConfig=null;getConfig(){if(!this.smtpConfig){let J=B.services.smtp?.encryption;this.smtpConfig={host:B.services.smtp?.host||"127.0.0.1",port:B.services.smtp?.port||587,username:B.services.smtp?.username||"",password:B.services.smtp?.password||"",encryption:J==="tls"?"starttls":J||null}}return this.smtpConfig}async send(J,U){let X=this.getConfig();if(!X.host||X.host==="")throw Error("[SMTP] Host is not configured. Set MAIL_HOST in your .env file.");let Y={provider:this.name,to:J.to,subject:J.subject,host:X.host,port:X.port};L.info("Sending email via SMTP...",Y);try{this.validateMessage(J);let Z;if(J.template){let _=await Q(J.template,U);if(_&&"html"in _)Z=_.html}let W=Z||J.html,K=J.from?.address||B.email.from?.address||"",G=J.from?.name||B.email.from?.name||"",$=this.formatAddresses(J.to),F=`----=_Part_${Date.now()}_${Math.random().toString(36).substring(2)}`,z=this.buildEmailContent({from:G?`${G} <${K}>`:K,to:$.join(", "),cc:J.cc?this.formatAddresses(J.cc).join(", "):void 0,subject:J.subject,text:J.text,html:W,boundary:F}),j=await this.sendViaSMTP(X,K,$,z);return this.handleSuccess(J,j)}catch(Z){return this.handleError(Z,J)}}buildEmailContent(J){let{from:U,to:X,cc:Y,subject:Z,text:W,html:K,boundary:G}=J,$=[];if($.push(`From: ${U}`),$.push(`To: ${X}`),Y)$.push(`Cc: ${Y}`);if($.push(`Subject: ${dJ(Z)}`),$.push("MIME-Version: 1.0"),$.push(`Date: ${new Date().toUTCString()}`),$.push(`Message-ID: <${Date.now()}.${Math.random().toString(36).substring(2)}@${B.email.domain||"localhost"}>`),K&&W)$.push(`Content-Type: multipart/alternative; boundary="${G}"`),$.push(""),$.push(`--${G}`),$.push("Content-Type: text/plain; charset=UTF-8"),$.push("Content-Transfer-Encoding: 7bit"),$.push(""),$.push(W),$.push(""),$.push(`--${G}`),$.push("Content-Type: text/html; charset=UTF-8"),$.push("Content-Transfer-Encoding: 7bit"),$.push(""),$.push(K),$.push(""),$.push(`--${G}--`);else if(K)$.push("Content-Type: text/html; charset=UTF-8"),$.push("Content-Transfer-Encoding: 7bit"),$.push(""),$.push(K);else if(W)$.push("Content-Type: text/plain; charset=UTF-8"),$.push("Content-Transfer-Encoding: 7bit"),$.push(""),$.push(W);return $.join(`\r
10
- `)}async sendViaSMTP(J,U,X,Y){return new Promise((Z,W)=>{let K=setTimeout(()=>{W(Error(`SMTP connection timed out after ${x.SMTP_TIMEOUT}ms`))},x.SMTP_TIMEOUT),G=Z,$=W;Z=(O)=>{clearTimeout(K),G(O)},W=(O)=>{clearTimeout(K),$(O)};let F,z="",j="",_=[],aJ=!1,AJ=(O)=>{if(L.debug(`[SMTP] Server: ${O.trim()}`),parseInt(O.substring(0,3),10)>=400){let q=Error(`SMTP Error: ${O.trim()}`);if(_.length>0)_.shift()?.reject(q);return}if(_.length>0)_.shift()?.resolve(O)},H=(O)=>{return new Promise((A,q)=>{_.push({cmd:O,resolve:A,reject:q}),L.debug(`[SMTP] Client: ${O}`),F.write(`${O}\r
11
- `)})},JJ=(O)=>{z+=O.toString();let A=z.split(`\r
12
- `);z=A.pop()||"";for(let q of A)if(q.length>=3){if(q.length===3||q[3]===" ")AJ(q)}},UJ=async()=>{try{await new Promise((q,M)=>{_.push({cmd:"GREETING",resolve:q,reject:M})});let O=await H(`EHLO ${B.email.domain||"localhost"}`);if(J.encryption==="starttls"&&!(F instanceof f.TLSSocket)){await H("STARTTLS");let q=F;q.removeAllListeners("data"),F=await new Promise((M,RJ)=>{let c=f.connect({socket:q,host:J.host,servername:J.host},()=>{L.debug("[SMTP] TLS connection established"),M(c)});c.on("error",(p)=>{L.error("[SMTP] TLS socket error:",p),RJ(p)}),c.on("data",JJ),c.on("close",(p)=>{L.debug(`[SMTP] TLS socket closed (hadError: ${p})`);while(_.length>0)_.shift()?.reject(Error("TLS connection closed unexpectedly"))})}),await H(`EHLO ${B.email.domain||"localhost"}`)}if(J.username&&J.password)await H("AUTH LOGIN"),await H(t.from(J.username).toString("base64")),await H(t.from(J.password).toString("base64"));BJ(U,"MAIL FROM");for(let q of X)BJ(q,"RCPT TO");await H(`MAIL FROM:<${U}>`);for(let q of X)await H(`RCPT TO:<${q}>`);await H("DATA"),F.write(`${Y}\r
8
+ `)}catch(z){WJ.warn(`[email:log] could not write inspection file: ${z.message}`)}let F=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(WJ.info(`[email:log] would send \u2192 ${F} :: ${J.subject}`),P.push({...J,sentAt:W,rendered:X}),P.length>FJ)P.splice(0,P.length-FJ);return this.handleSuccess(J,`log-${W.getTime()}`)}catch(X){return this.handleError(X,J)}}static captured(){return P}static reset(){P.length=0}}function CJ({stamp:J,message:U}){return["<!--",` Captured by @stacksjs/email log driver at ${J.toISOString()}`,` From: ${d(U.from)}`,` To: ${n(U.to)}`,U.cc?` Cc: ${n(U.cc)}`:null,U.bcc?` Bcc: ${n(U.bcc)}`:null,` Subject: ${U.subject}`,"-->"].filter(Boolean).join(`
9
+ `)}function d(J){if(!J)return"";if(typeof J==="string")return J;let U=J;return U.name?`${U.name} <${U.address??""}>`:U.address??""}function n(J){if(Array.isArray(J))return J.map(d).join(", ");return d(J)}function bJ(J){return J.replace(/[&<>"']/g,(U)=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[U])}var vJ=C;var OJ={};T(OJ,{default:()=>hJ,MailgunDriver:()=>b});import{Buffer as zJ}from"buffer";import{config as M}from"@stacksjs/config";import{log as o}from"@stacksjs/logging";class b extends H{name="mailgun";apiKey=null;domain=null;endpoint=null;getConfig(){if(!this.apiKey||!this.domain||!this.endpoint)this.apiKey=M.services.mailgun?.apiKey??"",this.domain=M.services.mailgun?.domain??"",this.endpoint=M.services.mailgun?.endpoint??"api.mailgun.net";return{apiKey:this.apiKey,domain:this.domain,endpoint:this.endpoint}}async send(J,U){let{domain:X}=this.getConfig(),Y={provider:this.name,to:J.to,subject:J.subject,domain:X};o.info("Sending email via Mailgun...",Y);try{this.validateMessage(J);let Z;if(J.template){let F=await V(J.template,U);if(F&&"html"in F)Z=F.html}let W=Z||J.html,K=new FormData,G={address:J.from?.address||M.email.from?.address||"",name:J.from?.name||M.email.from?.name};if(K.append("from",this.formatMailgunAddress(G)),this.formatMailgunAddresses(J.to).forEach((F)=>K.append("to",F)),J.cc)this.formatMailgunAddresses(J.cc).forEach((F)=>K.append("cc",F));if(J.bcc)this.formatMailgunAddresses(J.bcc).forEach((F)=>K.append("bcc",F));if(K.append("subject",J.subject),W)K.append("html",W);if(J.text)K.append("text",J.text);if(J.attachments)J.attachments.forEach((F)=>{let z=typeof F.content==="string"?F.content:this.arrayBufferToBase64(F.content);K.append("attachment",new Blob([z],{type:F.contentType}),F.filename)});let $=await this.sendWithRetry(K);return this.handleSuccess(J,$.id)}catch(Z){return this.handleError(Z,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((U)=>{if(typeof U==="string")return U;if(!U.name)return U.address;return`${/[",()<>[\]:;@\\]/.test(U.name)?`"${U.name.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")}"`:U.name} <${U.address}>`})}arrayBufferToBase64(J){let U="",X=new Uint8Array(J),Y=X.byteLength;for(let Z=0;Z<Y;Z++)U+=String.fromCharCode(X[Z]??0);return typeof btoa==="function"?btoa(U):zJ.from(U).toString("base64")}async sendWithRetry(J,U=1){let{apiKey:X,domain:Y,endpoint:Z}=this.getConfig(),W=`https://${Z}/v3/${Y}/messages`,K=zJ.from(`api:${X}`).toString("base64");try{let G=await fetch(W,{method:"POST",headers:{Authorization:`Basic ${K}`},body:J});if(!G.ok){let F=await G.json();throw Error(`Mailgun API error: ${G.status} - ${JSON.stringify(F)}`)}let $=await G.json();return o.info(`[${this.name}] Email sent successfully`,{attempt:U,messageId:$.id}),$}catch(G){if(U<(M.services.mailgun?.maxRetries??3)){let $=M.services.mailgun?.retryTimeout??1000;return o.warn(`[${this.name}] Email send failed, retrying (${U}/${M.services.mailgun?.maxRetries??3})`),await new Promise((F)=>setTimeout(F,$)),this.sendWithRetry(J,U+1)}throw G}}}var hJ=b;var qJ={};T(qJ,{default:()=>fJ,MailtrapDriver:()=>v});import{Buffer as uJ}from"buffer";import{config as A}from"@stacksjs/config";import{log as m}from"@stacksjs/logging";class v extends H{name="mailtrap";host=null;token=null;inboxId=null;getConfig(){if(this.host===null||this.token===null||this.inboxId===null)this.host=A.services.mailtrap?.host??"https://sandbox.api.mailtrap.io/api/send",this.token=A.services.mailtrap?.token??"",this.inboxId=A.services.mailtrap?.inboxId?Number(A.services.mailtrap.inboxId):void 0;return{host:this.host,token:this.token,inboxId:this.inboxId}}async send(J,U){let{inboxId:X}=this.getConfig(),Y={provider:this.name,to:J.to,subject:J.subject,inboxId:X};m.info("Sending email via Mailtrap...",Y);try{this.validateMessage(J);let Z;if(J.template)Z=await V(J.template,U);let W=Z?.html||J.html,K={from:{email:J.from?.address||A.email.from?.address||"",name:J.from?.name||A.email.from?.name},to:this.formatMailtrapAddresses(J.to),...J.cc&&{cc:this.formatMailtrapAddresses(J.cc)},...J.bcc&&{bcc:this.formatMailtrapAddresses(J.bcc)},subject:J.subject,...W&&{html:W},...J.text&&{text:J.text},...J.attachments&&{attachments:J.attachments.map(($)=>({filename:$.filename,content:typeof $.content==="string"?$.content:this.arrayBufferToBase64($.content),type:$.contentType||"application/octet-stream"}))}},G=await this.sendWithRetry(K);return this.handleSuccess(J,G.message_ids?.[0])}catch(Z){return this.handleError(Z,J)}}formatMailtrapAddresses(J){if(!J)return[];if(typeof J==="string")return[{email:J}];return J.map((U)=>{if(typeof U==="string")return{email:U};return{email:U.address,...U.name&&{name:U.name}}})}arrayBufferToBase64(J){let U="",X=new Uint8Array(J),Y=X.byteLength;for(let Z=0;Z<Y;Z++)U+=String.fromCharCode(X[Z]??0);return typeof btoa==="function"?btoa(U):uJ.from(U).toString("base64")}async sendWithRetry(J,U=1){let{host:X,token:Y,inboxId:Z}=this.getConfig();if(!Z)throw Error("Mailtrap inbox ID is required but not provided. Please set MAILTRAP_INBOX_ID in your environment variables.");let W=`${X}/${Z}`;try{let K=await fetch(W,{method:"POST",headers:{Authorization:`Bearer ${Y}`,"Content-Type":"application/json"},body:JSON.stringify(J)});if(!K.ok){let $=await K.json();throw Error(`Mailtrap API error: ${K.status} - ${JSON.stringify($)}`)}let G=await K.json();return m.info(`[${this.name}] Email sent successfully`,{attempt:U,messageId:G.message_ids?.[0]}),G}catch(K){if(U<(A.services.mailtrap?.maxRetries??3)){let G=A.services.mailtrap?.retryTimeout??1000;return m.warn(`[${this.name}] Email send failed, retrying (${U}/${A.services.mailtrap?.maxRetries??3})`),await new Promise(($)=>setTimeout($,G)),this.sendWithRetry(J,U+1)}throw K}}}var fJ=v;var VJ={};T(VJ,{NodemailerDriver:()=>QJ});class QJ{async send(){throw Error("Nodemailer driver is not yet implemented. Use smtp, ses, sendgrid, mailgun, or mailtrap instead.")}}var HJ={};T(HJ,{default:()=>pJ,SendGridDriver:()=>h});import{Buffer as cJ}from"buffer";import{config as x}from"@stacksjs/config";import{log as r}from"@stacksjs/logging";class h extends H{name="sendgrid";apiKey=null;getApiKey(){if(!this.apiKey)this.apiKey=x.services.sendgrid?.apiKey??"";return this.apiKey}async send(J,U){let X={provider:this.name,to:J.to,subject:J.subject};r.info("Sending email via SendGrid...",X);try{this.validateMessage(J);let Y;if(J.template){let $=await V(J.template,U);if($&&"html"in $)Y=$.html}let Z=Y||J.html,W=[];if(Z)W.push({type:"text/html",value:Z});if(J.text)W.push({type:"text/plain",value:J.text});if(W.length===0)throw Error("Email must have either HTML or text content");let K={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||x.email.from?.address||"",name:J.from?.name||x.email.from?.name},content:W,...J.attachments&&{attachments:J.attachments.map(($)=>({filename:$.filename,content:typeof $.content==="string"?$.content:this.arrayBufferToBase64($.content),type:$.contentType,disposition:"attachment"}))}},G=await this.sendWithRetry(K);return this.handleSuccess(J,G.headers?.get("x-message-id")??void 0)}catch(Y){return this.handleError(Y,J)}}formatSendGridAddresses(J){if(!J)return[];if(typeof J==="string")return[{email:J}];return J.map((U)=>{if(typeof U==="string")return{email:U};return{email:U.address,...U.name&&{name:U.name}}})}arrayBufferToBase64(J){let U="",X=new Uint8Array(J),Y=X.byteLength;for(let Z=0;Z<Y;Z++)U+=String.fromCharCode(X[Z]??0);return typeof btoa==="function"?btoa(U):cJ.from(U).toString("base64")}async sendWithRetry(J,U=1){try{let X=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(!X.ok){let Y=await X.json(),Z=Error(`SendGrid API error: ${X.status} - ${JSON.stringify(Y)}`);throw Z.status=X.status,Z}return r.info(`[${this.name}] Email sent successfully`,{attempt:U}),X}catch(X){let Y=X?.status;if(!(typeof Y==="number"&&Y>=400&&Y<500&&Y!==429)&&U<(x.services.sendgrid?.maxRetries??3)){let W=x.services.sendgrid?.retryTimeout??1000;return r.warn(`[${this.name}] Email send failed, retrying (${U}/${x.services.sendgrid?.maxRetries??3})`),await new Promise((K)=>setTimeout(K,W)),this.sendWithRetry(J,U+1)}throw X}}}var pJ=h;var BJ={};T(BJ,{default:()=>iJ,SESDriver:()=>u});import{SESClient as lJ}from"@stacksjs/ts-cloud";import{config as D}from"@stacksjs/config";class u extends H{name="ses";client=null;getClient(){if(!this.client)this.client=new lJ(D?.services?.ses?.region||"us-east-1");return this.client}async send(J,U){try{this.validateMessage(J);let X;if(J.template){let K=await V(J.template,U);if(K&&"html"in K)X=K.html}let Y=X||J.html,Z={};if(Y)Z.Html={Charset:D.email.charset||"UTF-8",Data:Y};if(J.text)Z.Text={Charset:D.email.charset||"UTF-8",Data:J.text};if(Object.keys(Z).length===0)throw Error("Email must have either HTML or text content");let W=await this.getClient().sendEmail({FromEmailAddress:this.formatSourceAddress({address:J.from?.address||D.email.from?.address||"",name:J.from?.name||D.email.from?.name}),Destination:{ToAddresses:this.formatAddresses(J.to),CcAddresses:this.formatAddresses(J.cc),BccAddresses:this.formatAddresses(J.bcc)},Content:{Simple:{Subject:{Charset:D.email.charset||"UTF-8",Data:J.subject},Body:Z}}});return this.handleSuccess(J,W.MessageId)}catch(X){return this.handleError(X,J)}}formatSourceAddress(J){return J.name?`${J.name} <${J.address}>`:J.address}formatAddresses(J){if(!J)return[];if(typeof J==="string")return[J];return J.map((U)=>typeof U==="string"?U:U.address)}}var iJ=u;import{config as k}from"@stacksjs/config";import{Buffer as t}from"buffer";import*as f from"tls";import*as wJ from"net";import{config as L}from"@stacksjs/config";import{log as w}from"@stacksjs/logging";function gJ(J){if(/^[\x00-\x7F]*$/.test(J))return J;return`=?UTF-8?B?${t.from(J,"utf-8").toString("base64")}?=`}var nJ=/^[^\s<>"\\\r\n\t]+@[^\s<>"\\\r\n\t]+$/;function LJ(J,U){if(typeof J!=="string"||!nJ.test(J))throw Error(`[smtp] Refusing to send: ${U} envelope address contains forbidden characters or is malformed: ${JSON.stringify(J)}`)}class E extends H{static SMTP_TIMEOUT=30000;name="smtp";smtpConfig=null;getConfig(){if(!this.smtpConfig){let J=L.services.smtp?.encryption;this.smtpConfig={host:L.services.smtp?.host||"127.0.0.1",port:L.services.smtp?.port||587,username:L.services.smtp?.username||"",password:L.services.smtp?.password||"",encryption:J==="tls"?"starttls":J||null}}return this.smtpConfig}async send(J,U){let X=this.getConfig();if(!X.host||X.host==="")throw Error("[SMTP] Host is not configured. Set MAIL_HOST in your .env file.");let Y={provider:this.name,to:J.to,subject:J.subject,host:X.host,port:X.port};w.info("Sending email via SMTP...",Y);try{this.validateMessage(J);let Z;if(J.template){let Q=await V(J.template,U);if(Q&&"html"in Q)Z=Q.html}let W=Z||J.html,K=J.from?.address||L.email.from?.address||"",G=J.from?.name||L.email.from?.name||"",$=this.formatAddresses(J.to),F=`----=_Part_${Date.now()}_${Math.random().toString(36).substring(2)}`,z=this.buildEmailContent({from:G?`${G} <${K}>`:K,to:$.join(", "),cc:J.cc?this.formatAddresses(J.cc).join(", "):void 0,subject:J.subject,text:J.text,html:W,boundary:F}),j=await this.sendViaSMTP(X,K,$,z);return this.handleSuccess(J,j)}catch(Z){return this.handleError(Z,J)}}buildEmailContent(J){let{from:U,to:X,cc:Y,subject:Z,text:W,html:K,boundary:G}=J,$=[];if($.push(`From: ${U}`),$.push(`To: ${X}`),Y)$.push(`Cc: ${Y}`);if($.push(`Subject: ${gJ(Z)}`),$.push("MIME-Version: 1.0"),$.push(`Date: ${new Date().toUTCString()}`),$.push(`Message-ID: <${Date.now()}.${Math.random().toString(36).substring(2)}@${L.email.domain||"localhost"}>`),K&&W)$.push(`Content-Type: multipart/alternative; boundary="${G}"`),$.push(""),$.push(`--${G}`),$.push("Content-Type: text/plain; charset=UTF-8"),$.push("Content-Transfer-Encoding: 7bit"),$.push(""),$.push(W),$.push(""),$.push(`--${G}`),$.push("Content-Type: text/html; charset=UTF-8"),$.push("Content-Transfer-Encoding: 7bit"),$.push(""),$.push(K),$.push(""),$.push(`--${G}--`);else if(K)$.push("Content-Type: text/html; charset=UTF-8"),$.push("Content-Transfer-Encoding: 7bit"),$.push(""),$.push(K);else if(W)$.push("Content-Type: text/plain; charset=UTF-8"),$.push("Content-Transfer-Encoding: 7bit"),$.push(""),$.push(W);return $.join(`\r
10
+ `)}async sendViaSMTP(J,U,X,Y){return new Promise((Z,W)=>{let K=setTimeout(()=>{W(Error(`SMTP connection timed out after ${E.SMTP_TIMEOUT}ms`))},E.SMTP_TIMEOUT),G=Z,$=W;Z=(O)=>{clearTimeout(K),G(O)},W=(O)=>{clearTimeout(K),$(O)};let F,z="",j="",Q=[],aJ=!1,RJ=(O)=>{if(w.debug(`[SMTP] Server: ${O.trim()}`),parseInt(O.substring(0,3),10)>=400){let q=Error(`SMTP Error: ${O.trim()}`);if(Q.length>0)Q.shift()?.reject(q);return}if(Q.length>0)Q.shift()?.resolve(O)},B=(O)=>{return new Promise((R,q)=>{Q.push({cmd:O,resolve:R,reject:q}),w.debug(`[SMTP] Client: ${O}`),F.write(`${O}\r
11
+ `)})},JJ=(O)=>{z+=O.toString();let R=z.split(`\r
12
+ `);z=R.pop()||"";for(let q of R)if(q.length>=3){if(q.length===3||q[3]===" ")RJ(q)}},UJ=async()=>{try{await new Promise((q,N)=>{Q.push({cmd:"GREETING",resolve:q,reject:N})});let O=await B(`EHLO ${L.email.domain||"localhost"}`);if(J.encryption==="starttls"&&!(F instanceof f.TLSSocket)){await B("STARTTLS");let q=F;q.removeAllListeners("data"),F=await new Promise((N,MJ)=>{let c=f.connect({socket:q,host:J.host,servername:J.host},()=>{w.debug("[SMTP] TLS connection established"),N(c)});c.on("error",(p)=>{w.error("[SMTP] TLS socket error:",p),MJ(p)}),c.on("data",JJ),c.on("close",(p)=>{w.debug(`[SMTP] TLS socket closed (hadError: ${p})`);while(Q.length>0)Q.shift()?.reject(Error("TLS connection closed unexpectedly"))})}),await B(`EHLO ${L.email.domain||"localhost"}`)}if(J.username&&J.password)await B("AUTH LOGIN"),await B(t.from(J.username).toString("base64")),await B(t.from(J.password).toString("base64"));LJ(U,"MAIL FROM");for(let q of X)LJ(q,"RCPT TO");await B(`MAIL FROM:<${U}>`);for(let q of X)await B(`RCPT TO:<${q}>`);await B("DATA"),F.write(`${Y}\r
13
13
  .\r
14
- `),await new Promise((q,M)=>{_.push({cmd:"DATA_END",resolve:q,reject:M})}),await H("QUIT"),F.end();let A=`${Date.now()}.${Math.random().toString(36).substring(2)}@${J.host}`;Z(A)}catch(O){F.end(),W(O)}};if(J.encryption==="ssl")F=f.connect({host:J.host,port:J.port,servername:J.host},()=>{L.debug(`[SMTP] TLS connected to ${J.host}:${J.port}`),UJ()});else F=LJ.connect({host:J.host,port:J.port},()=>{L.debug(`[SMTP] Connected to ${J.host}:${J.port}`),UJ()});F.on("data",JJ),F.setTimeout(x.SMTP_TIMEOUT),F.on("timeout",()=>{F.destroy(Error(`SMTP socket timed out after ${x.SMTP_TIMEOUT}ms`))}),F.on("error",(O)=>{L.error(`[SMTP] Connection error to ${J.host}:${J.port}:`,O),W(O)}),F.on("close",(O)=>{L.debug(`[SMTP] Connection closed (hadError: ${O})`);while(_.length>0)_.shift()?.reject(Error("Connection closed unexpectedly"))})})}formatAddresses(J){if(!J)return[];if(typeof J==="string")return[J];return J.map((U)=>{if(typeof U==="string")return U;return U.address})}}class nJ{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"),U=J.resourcesPath(`views/emails/${this.template}.html`),X=Bun.file(U);if(await X.exists())return await X.text()}catch{}if(this.template.includes("<"))return this.template;return`<p>${this.template}</p>`}async send(J){let U=J??this.to,X=Array.isArray(U)?U:U?[U]:[];if(X.length===0)throw Error("No recipient specified for email");try{if(await l.send({to:X,from:this.from||{name:k.email.from?.name||"Stacks",address:k.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(Y){if(this.onError)return this.onError(Y instanceof Error?Y:Error(String(Y)));throw Y}}}class s{drivers=new Map;defaultDriver;constructor(J={}){this.defaultDriver=J.defaultDriver||k.email.default||"ses",this.registerDefaultDrivers()}registerDefaultDrivers(){this.drivers.set("log",new C),this.drivers.set("ses",new u),this.drivers.set("sendgrid",new h),this.drivers.set("mailgun",new b),this.drivers.set("mailtrap",new v),this.drivers.set("smtp",new x)}async send(J){let U=this.drivers.get(this.defaultDriver);if(!U){let Y=[...this.drivers.keys()].sort().join(", ");throw Error(`Email driver '${this.defaultDriver}' is not registered. Available drivers: [${Y}]. Check config.email.default or the MAIL_MAILER environment variable.`)}let X={name:k.email.from?.name||"Stacks",address:k.email.from?.address||"no-reply@stacksjs.com"};return U.send({...J,from:J.from||X})}use(J){if(!this.drivers.has(J))throw Error(`Email driver '${J}' is not available`);return new s({defaultDriver:J})}async queue(J){try{let{job:U}=await import("@stacksjs/queue");await U("SendEmail",{message:J,driver:this.defaultDriver}).onQueue("emails").dispatch()}catch{await this.send(J)}}async later(J,U){try{let{job:X}=await import("@stacksjs/queue");await X("SendEmail",{message:U,driver:this.defaultDriver}).onQueue("emails").delay(J).dispatch()}catch{await this.send(U)}}async queueOn(J,U){try{let{job:X}=await import("@stacksjs/queue");await X("SendEmail",{message:U,driver:this.defaultDriver}).onQueue(J).dispatch()}catch{await this.send(U)}}}var a;function wJ(){if(!a){let J=k?.email?.default||process.env.MAIL_MAILER||"ses";a=new s({defaultDriver:J})}return a}var l=new Proxy({},{get(J,U){return wJ()[U]},set(J,U,X){return wJ()[U]=X,!0}});import{config as IJ}from"@stacksjs/config";class oJ{_to=[];_cc=[];_bcc=[];_replyTo;_from;_subject;_text;_html;_template;_attachments=[];to(J){return this._to=e(J),this}cc(J){return this._cc=e(J),this}bcc(J){return this._bcc=e(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,U={}){return this._template={name:J,props:U},this}attach(J,U){return this._attachments.push({filename:U||mJ(J),content:`__file__:${J}`,encoding:"binary"}),this}attachData(J,U,X){return this._attachments.push({filename:U,content:J,contentType:X,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 U=this._html,X=this._text;if(this._template){let W=await Q(this._template.name,{variables:this._template.props,subject:this._subject});if(!U)U=W.html;if(!X)X=W.text}let Y={to:this._to,subject:this._subject,from:this._from||rJ(),...this._cc.length?{cc:this._cc}:{},...this._bcc.length?{bcc:this._bcc}:{},...U?{html:U}:{},...X?{text:X}:{},...this._attachments.length?{attachments:await tJ(this._attachments)}:{}};if(this._replyTo)Y.replyTo=this._replyTo;return(J.driver?l.use(J.driver):l).send(Y)}}function e(J){let U=Array.isArray(J)?J:[J];if(U.some((Y)=>typeof Y==="object"&&Y!==null))return U.map((Y)=>typeof Y==="string"?{address:Y}:Y);return U}function rJ(){return{name:IJ.email.from?.name||"Stacks",address:IJ.email.from?.address||"no-reply@stacksjs.com"}}function mJ(J){let U=Math.max(J.lastIndexOf("/"),J.lastIndexOf("\\"));return U===-1?J:J.slice(U+1)}async function tJ(J){return Promise.all(J.map(async(U)=>{if(typeof U.content==="string"&&U.content.startsWith("__file__:")){let X=U.content.slice(9),Y=Bun.file(X),Z=new Uint8Array(await Y.arrayBuffer());return{...U,content:Z,contentType:U.contentType||Y.type||void 0,encoding:"binary"}}return U}))}export{KU as templateExists,Q as template,HJ as ses,VJ as sendgrid,$U as renderHtml,QJ as nodemailer,qJ as mailtrap,OJ as mailgun,l as mail,GJ as log,WU as listTemplates,oJ as Mailable,nJ as Email};
14
+ `),await new Promise((q,N)=>{Q.push({cmd:"DATA_END",resolve:q,reject:N})}),await B("QUIT"),F.end();let R=`${Date.now()}.${Math.random().toString(36).substring(2)}@${J.host}`;Z(R)}catch(O){F.end(),W(O)}};if(J.encryption==="ssl")F=f.connect({host:J.host,port:J.port,servername:J.host},()=>{w.debug(`[SMTP] TLS connected to ${J.host}:${J.port}`),UJ()});else F=wJ.connect({host:J.host,port:J.port},()=>{w.debug(`[SMTP] Connected to ${J.host}:${J.port}`),UJ()});F.on("data",JJ),F.setTimeout(E.SMTP_TIMEOUT),F.on("timeout",()=>{F.destroy(Error(`SMTP socket timed out after ${E.SMTP_TIMEOUT}ms`))}),F.on("error",(O)=>{w.error(`[SMTP] Connection error to ${J.host}:${J.port}:`,O),W(O)}),F.on("close",(O)=>{w.debug(`[SMTP] Connection closed (hadError: ${O})`);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((U)=>{if(typeof U==="string")return U;return U.address})}}class dJ{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"),U=J.resourcesPath(`views/emails/${this.template}.html`),X=Bun.file(U);if(await X.exists())return await X.text()}catch{}if(this.template.includes("<"))return this.template;return`<p>${this.template}</p>`}async send(J){let U=J??this.to,X=Array.isArray(U)?U:U?[U]:[];if(X.length===0)throw Error("No recipient specified for email");try{if(await l.send({to:X,from:this.from||{name:k.email.from?.name||"Stacks",address:k.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(Y){if(this.onError)return this.onError(Y instanceof Error?Y:Error(String(Y)));throw Y}}}class s{drivers=new Map;defaultDriver;constructor(J={}){this.defaultDriver=J.defaultDriver||k.email.default||"ses",this.registerDefaultDrivers()}registerDefaultDrivers(){this.drivers.set("log",new C),this.drivers.set("ses",new u),this.drivers.set("sendgrid",new h),this.drivers.set("mailgun",new b),this.drivers.set("mailtrap",new v),this.drivers.set("smtp",new E)}async send(J){let U=this.drivers.get(this.defaultDriver);if(!U){let Y=[...this.drivers.keys()].sort().join(", ");throw Error(`Email driver '${this.defaultDriver}' is not registered. Available drivers: [${Y}]. Check config.email.default or the MAIL_MAILER environment variable.`)}let X={name:k.email.from?.name||"Stacks",address:k.email.from?.address||"no-reply@stacksjs.com"};return U.send({...J,from:J.from||X})}use(J){if(!this.drivers.has(J))throw Error(`Email driver '${J}' is not available`);return new s({defaultDriver:J})}async queue(J){try{let{job:U}=await import("@stacksjs/queue");await U("SendEmail",{message:J,driver:this.defaultDriver}).onQueue("emails").dispatch()}catch{await this.send(J)}}async later(J,U){try{let{job:X}=await import("@stacksjs/queue");await X("SendEmail",{message:U,driver:this.defaultDriver}).onQueue("emails").delay(J).dispatch()}catch{await this.send(U)}}async queueOn(J,U){try{let{job:X}=await import("@stacksjs/queue");await X("SendEmail",{message:U,driver:this.defaultDriver}).onQueue(J).dispatch()}catch{await this.send(U)}}}var a;function IJ(){if(!a){let J=k?.email?.default||process.env.MAIL_MAILER||"ses";a=new s({defaultDriver:J})}return a}var l=new Proxy({},{get(J,U){return IJ()[U]},set(J,U,X){return IJ()[U]=X,!0}});import{config as AJ}from"@stacksjs/config";class oJ{_to=[];_cc=[];_bcc=[];_replyTo;_from;_subject;_text;_html;_template;_attachments=[];to(J){return this._to=e(J),this}cc(J){return this._cc=e(J),this}bcc(J){return this._bcc=e(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,U={}){return this._template={name:J,props:U},this}attach(J,U){return this._attachments.push({filename:U||rJ(J),content:`__file__:${J}`,encoding:"binary"}),this}attachData(J,U,X){return this._attachments.push({filename:U,content:J,contentType:X,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 U=this._html,X=this._text;if(this._template){let W=await V(this._template.name,{variables:this._template.props,subject:this._subject});if(!U)U=W.html;if(!X)X=W.text}let Y={to:this._to,subject:this._subject,from:this._from||mJ(),...this._cc.length?{cc:this._cc}:{},...this._bcc.length?{bcc:this._bcc}:{},...U?{html:U}:{},...X?{text:X}:{},...this._attachments.length?{attachments:await tJ(this._attachments)}:{}};if(this._replyTo)Y.replyTo=this._replyTo;return(J.driver?l.use(J.driver):l).send(Y)}}function e(J){let U=Array.isArray(J)?J:[J];if(U.some((Y)=>typeof Y==="object"&&Y!==null))return U.map((Y)=>typeof Y==="string"?{address:Y}:Y);return U}function mJ(){return{name:AJ.email.from?.name||"Stacks",address:AJ.email.from?.address||"no-reply@stacksjs.com"}}function rJ(J){let U=Math.max(J.lastIndexOf("/"),J.lastIndexOf("\\"));return U===-1?J:J.slice(U+1)}async function tJ(J){return Promise.all(J.map(async(U)=>{if(typeof U.content==="string"&&U.content.startsWith("__file__:")){let X=U.content.slice(9),Y=Bun.file(X),Z=new Uint8Array(await Y.arrayBuffer());return{...U,content:Z,contentType:U.contentType||Y.type||void 0,encoding:"binary"}}return U}))}export{KU as templateExists,V as template,BJ as ses,HJ as sendgrid,$U as renderHtml,VJ as nodemailer,qJ as mailtrap,OJ as mailgun,l as mail,GJ as log,WU as listTemplates,oJ as Mailable,dJ as Email};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/email",
3
3
  "type": "module",
4
- "version": "0.70.37",
4
+ "version": "0.70.42",
5
5
  "description": "The Stacks Email integration. Painlessly create & manage your inboxes, templates, and send emails.",
6
6
  "author": "Chris Breuer",
7
7
  "contributors": [
@@ -53,10 +53,10 @@
53
53
  "@stacksjs/ts-cloud": "^0.2.15"
54
54
  },
55
55
  "devDependencies": {
56
- "@stacksjs/cli": "0.70.30",
57
- "@stacksjs/config": "0.70.30",
56
+ "@stacksjs/cli": "^0.70.42",
57
+ "@stacksjs/config": "^0.70.42",
58
58
  "better-dx": "^0.2.12",
59
- "@stacksjs/error-handling": "0.70.30",
60
- "@stacksjs/types": "0.70.30"
59
+ "@stacksjs/error-handling": "^0.70.42",
60
+ "@stacksjs/types": "^0.70.42"
61
61
  }
62
62
  }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes