@stacksjs/email 0.72.102 → 0.72.103

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.
@@ -1,4 +1,4 @@
1
- import{mkdir,writeFile}from"node:fs/promises";import{join,resolve}from"node:path";import{log}from"@stacksjs/logging";import{template}from"../template";import{BaseEmailDriver}from"./base";const STORE_LIMIT=100,captured=[];export class LogEmailDriver extends BaseEmailDriver{name="log";resolveDir(){const fromEnv=process.env.LOG_MAIL_DIR;if(fromEnv)return resolve(fromEnv);return resolve(join(import.meta.dir,"..","..","..","..","..","logs","mail"))}async send(message,options){try{this.validateMessage(message);let rendered;if(message.template){const t=await template(message.template,options);if(t)rendered={html:t.html,text:t.text}}const html=rendered?.html??message.html,text=rendered?.text??message.text,stamp=new Date,safeSubject=(message.subject||"no-subject").replace(/[^\w.-]+/g,"-").slice(0,60),filename=`${stamp.toISOString().replace(/[:.]/g,"-")}-${safeSubject}.html`,dir=this.resolveDir();try{await mkdir(dir,{recursive:!0});const filePath=join(dir,filename),body=html?html:text?`<pre>${escapeHtml(text)}</pre>`:"<em>(empty body)</em>",headerBlock=renderHeader({stamp,message});await writeFile(filePath,`${headerBlock}
1
+ import{mkdir,writeFile}from"node:fs/promises";import{join,resolve}from"node:path";import{log}from"@stacksjs/logging";import{templateByName}from"../template";import{BaseEmailDriver}from"./base";const STORE_LIMIT=100,captured=[];export class LogEmailDriver extends BaseEmailDriver{name="log";resolveDir(){const fromEnv=process.env.LOG_MAIL_DIR;if(fromEnv)return resolve(fromEnv);return resolve(join(import.meta.dir,"..","..","..","..","..","logs","mail"))}async send(message,options){try{this.validateMessage(message);let rendered;if(message.template){const t=await templateByName(message.template,options);if(t)rendered={html:t.html,text:t.text}}const html=rendered?.html??message.html,text=rendered?.text??message.text,stamp=new Date,safeSubject=(message.subject||"no-subject").replace(/[^\w.-]+/g,"-").slice(0,60),filename=`${stamp.toISOString().replace(/[:.]/g,"-")}-${safeSubject}.html`,dir=this.resolveDir();try{await mkdir(dir,{recursive:!0});const filePath=join(dir,filename),body=html?html:text?`<pre>${escapeHtml(text)}</pre>`:"<em>(empty body)</em>",headerBlock=renderHeader({stamp,message});await writeFile(filePath,`${headerBlock}
2
2
  ${body}
3
3
  `)}catch(err){log.warn(`[email:log] could not write inspection file: ${err.message}`)}const flatTo=Array.isArray(message.to)?message.to.map((t)=>typeof t==="string"?t:t.address).join(", "):typeof message.to==="string"?message.to:message.to.address;log.info(`[email:log] would send \u2192 ${flatTo} :: ${message.subject}`);captured.push({...message,sentAt:stamp,rendered});if(captured.length>STORE_LIMIT)captured.splice(0,captured.length-STORE_LIMIT);return this.handleSuccess(message,`log-${stamp.getTime()}`)}catch(error){return this.handleError(error,message)}}static captured(){return captured}static reset(){captured.length=0}}function renderHeader({stamp,message}){return["<!--",` Captured by @stacksjs/email log driver at ${stamp.toISOString()}`,` From: ${formatAddr(message.from)}`,` To: ${formatList(message.to)}`,message.cc?` Cc: ${formatList(message.cc)}`:null,message.bcc?` Bcc: ${formatList(message.bcc)}`:null,` Subject: ${message.subject}`,"-->"].filter(Boolean).join(`
4
4
  `)}function formatAddr(v){if(!v)return"";if(typeof v==="string")return v;const o=v;return o.name?`${o.name} <${o.address??""}>`:o.address??""}function formatList(v){if(Array.isArray(v))return v.map(formatAddr).join(", ");return formatAddr(v)}function escapeHtml(s){return s.replace(/[&<>"']/g,(c)=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[c])}export default LogEmailDriver;
@@ -1 +1 @@
1
- import{Buffer}from"node:buffer";import{config}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{template}from"../template";import{BaseEmailDriver}from"./base";export class MailgunDriver extends BaseEmailDriver{name="mailgun";apiKey=null;domain=null;endpoint=null;getConfig(){if(!this.apiKey||!this.domain||!this.endpoint){this.apiKey=config.services.mailgun?.apiKey??"";this.domain=config.services.mailgun?.domain??"";this.endpoint=config.services.mailgun?.endpoint??"api.mailgun.net"}return{apiKey:this.apiKey,domain:this.domain,endpoint:this.endpoint}}async send(message,options){const{domain}=this.getConfig(),logContext={provider:this.name,to:message.to,subject:message.subject,domain};log.info("Sending email via Mailgun...",logContext);try{this.validateMessage(message);let htmlContent;if(message.template){const templ=await template(message.template,options);if(templ&&"html"in templ)htmlContent=templ.html}const finalHtml=htmlContent||message.html,formData=new FormData,fromAddress={address:message.from?.address||config.email.from?.address||"",name:message.from?.name||config.email.from?.name};formData.append("from",this.formatMailgunAddress(fromAddress));this.formatMailgunAddresses(message.to).forEach((to)=>formData.append("to",to));if(message.cc)this.formatMailgunAddresses(message.cc).forEach((cc)=>formData.append("cc",cc));if(message.bcc)this.formatMailgunAddresses(message.bcc).forEach((bcc)=>formData.append("bcc",bcc));formData.append("subject",message.subject);if(message.replyTo){const formatted=this.formatMailgunAddresses(Array.isArray(message.replyTo)||typeof message.replyTo==="string"?message.replyTo:[message.replyTo]);if(formatted.length>0)formData.append("h:Reply-To",formatted.join(", "))}if(message.headers){for(const[k,v]of Object.entries(message.headers))if(typeof v==="string")formData.append(`h:${k}`,v)}if(finalHtml)formData.append("html",finalHtml);if(message.text)formData.append("text",message.text);if(message.attachments)message.attachments.forEach((attachment)=>{const content=typeof attachment.content==="string"?attachment.content:this.arrayBufferToBase64(attachment.content);formData.append("attachment",new Blob([content],{type:attachment.contentType}),attachment.filename)});const response=await this.sendWithRetry(formData);return this.handleSuccess(message,response.id)}catch(error){return this.handleError(error,message)}}formatMailgunAddress(address){return address.name?`${address.name} <${address.address}>`:address.address}formatMailgunAddresses(addresses){if(!addresses)return[];if(typeof addresses==="string")return[addresses];return addresses.map((_addr)=>{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(buffer){let binary="";const bytes=new Uint8Array(buffer),len=bytes.byteLength;for(let i=0;i<len;i++)binary+=String.fromCharCode(bytes[i]??0);return typeof btoa==="function"?btoa(binary):Buffer.from(binary).toString("base64")}async sendWithRetry(formData,attempt=1){const{apiKey,domain,endpoint}=this.getConfig(),url=`https://${endpoint}/v3/${domain}/messages`,auth=Buffer.from(`api:${apiKey}`).toString("base64");try{const response=await fetch(url,{method:"POST",headers:{Authorization:`Basic ${auth}`},body:formData});if(!response.ok){const errorData=await response.json();throw Error(`Mailgun API error: ${response.status} - ${JSON.stringify(errorData)}`)}const data=await response.json();log.info(`[${this.name}] Email sent successfully`,{attempt,messageId:data.id});return data}catch(error){if(attempt<(config.services.mailgun?.maxRetries??3)){const retryTimeout=config.services.mailgun?.retryTimeout??1000;log.warn(`[${this.name}] Email send failed, retrying (${attempt}/${config.services.mailgun?.maxRetries??3})`);await new Promise((resolve)=>setTimeout(resolve,retryTimeout));return this.sendWithRetry(formData,attempt+1)}throw error}}}export default MailgunDriver;
1
+ import{Buffer}from"node:buffer";import{config}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{templateByName}from"../template";import{BaseEmailDriver}from"./base";export class MailgunDriver extends BaseEmailDriver{name="mailgun";apiKey=null;domain=null;endpoint=null;getConfig(){if(!this.apiKey||!this.domain||!this.endpoint){this.apiKey=config.services.mailgun?.apiKey??"";this.domain=config.services.mailgun?.domain??"";this.endpoint=config.services.mailgun?.endpoint??"api.mailgun.net"}return{apiKey:this.apiKey,domain:this.domain,endpoint:this.endpoint}}async send(message,options){const{domain}=this.getConfig(),logContext={provider:this.name,to:message.to,subject:message.subject,domain};log.info("Sending email via Mailgun...",logContext);try{this.validateMessage(message);let htmlContent;if(message.template){const templ=await templateByName(message.template,options);if(templ&&"html"in templ)htmlContent=templ.html}const finalHtml=htmlContent||message.html,formData=new FormData,fromAddress={address:message.from?.address||config.email.from?.address||"",name:message.from?.name||config.email.from?.name};formData.append("from",this.formatMailgunAddress(fromAddress));this.formatMailgunAddresses(message.to).forEach((to)=>formData.append("to",to));if(message.cc)this.formatMailgunAddresses(message.cc).forEach((cc)=>formData.append("cc",cc));if(message.bcc)this.formatMailgunAddresses(message.bcc).forEach((bcc)=>formData.append("bcc",bcc));formData.append("subject",message.subject);if(message.replyTo){const formatted=this.formatMailgunAddresses(Array.isArray(message.replyTo)||typeof message.replyTo==="string"?message.replyTo:[message.replyTo]);if(formatted.length>0)formData.append("h:Reply-To",formatted.join(", "))}if(message.headers){for(const[k,v]of Object.entries(message.headers))if(typeof v==="string")formData.append(`h:${k}`,v)}if(finalHtml)formData.append("html",finalHtml);if(message.text)formData.append("text",message.text);if(message.attachments)message.attachments.forEach((attachment)=>{const content=typeof attachment.content==="string"?attachment.content:this.arrayBufferToBase64(attachment.content);formData.append("attachment",new Blob([content],{type:attachment.contentType}),attachment.filename)});const response=await this.sendWithRetry(formData);return this.handleSuccess(message,response.id)}catch(error){return this.handleError(error,message)}}formatMailgunAddress(address){return address.name?`${address.name} <${address.address}>`:address.address}formatMailgunAddresses(addresses){if(!addresses)return[];if(typeof addresses==="string")return[addresses];return addresses.map((_addr)=>{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(buffer){let binary="";const bytes=new Uint8Array(buffer),len=bytes.byteLength;for(let i=0;i<len;i++)binary+=String.fromCharCode(bytes[i]??0);return typeof btoa==="function"?btoa(binary):Buffer.from(binary).toString("base64")}async sendWithRetry(formData,attempt=1){const{apiKey,domain,endpoint}=this.getConfig(),url=`https://${endpoint}/v3/${domain}/messages`,auth=Buffer.from(`api:${apiKey}`).toString("base64");try{const response=await fetch(url,{method:"POST",headers:{Authorization:`Basic ${auth}`},body:formData});if(!response.ok){const errorData=await response.json();throw Error(`Mailgun API error: ${response.status} - ${JSON.stringify(errorData)}`)}const data=await response.json();log.info(`[${this.name}] Email sent successfully`,{attempt,messageId:data.id});return data}catch(error){if(attempt<(config.services.mailgun?.maxRetries??3)){const retryTimeout=config.services.mailgun?.retryTimeout??1000;log.warn(`[${this.name}] Email send failed, retrying (${attempt}/${config.services.mailgun?.maxRetries??3})`);await new Promise((resolve)=>setTimeout(resolve,retryTimeout));return this.sendWithRetry(formData,attempt+1)}throw error}}}export default MailgunDriver;
@@ -1 +1 @@
1
- import{Buffer}from"node:buffer";import{config}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{template}from"../template";import{filterStringHeaders}from"../validation";import{BaseEmailDriver}from"./base";export class MailtrapDriver extends BaseEmailDriver{name="mailtrap";host=null;token=null;inboxId=null;getConfig(){if(this.host===null||this.token===null||this.inboxId===null){this.host=config.services.mailtrap?.host??"https://sandbox.api.mailtrap.io/api/send";this.token=config.services.mailtrap?.token??"";this.inboxId=config.services.mailtrap?.inboxId?Number(config.services.mailtrap.inboxId):void 0}return{host:this.host,token:this.token,inboxId:this.inboxId}}async send(message,options){const{inboxId}=this.getConfig(),logContext={provider:this.name,to:message.to,subject:message.subject,inboxId};log.info("Sending email via Mailtrap...",logContext);try{this.validateMessage(message);let templ;if(message.template)templ=await template(message.template,options);const htmlContent=templ?.html||message.html,replyTo=this.firstMailtrapAddress(message.replyTo),customHeaders=filterStringHeaders(message.headers),mailtrapPayload={from:{email:message.from?.address||config.email.from?.address||"",name:message.from?.name||config.email.from?.name},to:this.formatMailtrapAddresses(message.to),...message.cc&&{cc:this.formatMailtrapAddresses(message.cc)},...message.bcc&&{bcc:this.formatMailtrapAddresses(message.bcc)},...replyTo?{reply_to:replyTo}:{},...customHeaders?{headers:customHeaders}:{},subject:message.subject,...htmlContent&&{html:htmlContent},...message.text&&{text:message.text},...message.attachments&&{attachments:message.attachments.map((attachment)=>({filename:attachment.filename,content:typeof attachment.content==="string"?attachment.content:this.arrayBufferToBase64(attachment.content),type:attachment.contentType||"application/octet-stream"}))}},response=await this.sendWithRetry(mailtrapPayload);return this.handleSuccess(message,response.message_ids?.[0])}catch(error){return this.handleError(error,message)}}formatMailtrapAddresses(addresses){if(!addresses)return[];if(typeof addresses==="string")return[{email:addresses}];return addresses.map((addr)=>{if(typeof addr==="string")return{email:addr};return{email:addr.address,...addr.name&&{name:addr.name}}})}firstMailtrapAddress(value){if(!value)return;if(typeof value==="string")return{email:value};if(Array.isArray(value)){const first=value[0];if(first===void 0)return;if(typeof first==="string")return{email:first};return{email:first.address,...first.name&&{name:first.name}}}return{email:value.address,...value.name&&{name:value.name}}}arrayBufferToBase64(buffer){let binary="";const bytes=new Uint8Array(buffer),len=bytes.byteLength;for(let i=0;i<len;i++)binary+=String.fromCharCode(bytes[i]??0);return typeof btoa==="function"?btoa(binary):Buffer.from(binary).toString("base64")}async sendWithRetry(payload,attempt=1){const{host,token,inboxId}=this.getConfig();if(!inboxId)throw Error("Mailtrap inbox ID is required but not provided. Please set MAILTRAP_INBOX_ID in your environment variables.");const endpoint=`${host}/${inboxId}`;try{const response=await fetch(endpoint,{method:"POST",headers:{Authorization:`Bearer ${token}`,"Content-Type":"application/json"},body:JSON.stringify(payload)});if(!response.ok){const errorData=await response.json();throw Error(`Mailtrap API error: ${response.status} - ${JSON.stringify(errorData)}`)}const data=await response.json();log.info(`[${this.name}] Email sent successfully`,{attempt,messageId:data.message_ids?.[0]});return data}catch(error){if(attempt<(config.services.mailtrap?.maxRetries??3)){const retryTimeout=config.services.mailtrap?.retryTimeout??1000;log.warn(`[${this.name}] Email send failed, retrying (${attempt}/${config.services.mailtrap?.maxRetries??3})`);await new Promise((resolve)=>setTimeout(resolve,retryTimeout));return this.sendWithRetry(payload,attempt+1)}throw error}}}export default MailtrapDriver;
1
+ import{Buffer}from"node:buffer";import{config}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{templateByName}from"../template";import{filterStringHeaders}from"../validation";import{BaseEmailDriver}from"./base";export class MailtrapDriver extends BaseEmailDriver{name="mailtrap";host=null;token=null;inboxId=null;getConfig(){if(this.host===null||this.token===null||this.inboxId===null){this.host=config.services.mailtrap?.host??"https://sandbox.api.mailtrap.io/api/send";this.token=config.services.mailtrap?.token??"";this.inboxId=config.services.mailtrap?.inboxId?Number(config.services.mailtrap.inboxId):void 0}return{host:this.host,token:this.token,inboxId:this.inboxId}}async send(message,options){const{inboxId}=this.getConfig(),logContext={provider:this.name,to:message.to,subject:message.subject,inboxId};log.info("Sending email via Mailtrap...",logContext);try{this.validateMessage(message);let templ;if(message.template)templ=await templateByName(message.template,options);const htmlContent=templ?.html||message.html,replyTo=this.firstMailtrapAddress(message.replyTo),customHeaders=filterStringHeaders(message.headers),mailtrapPayload={from:{email:message.from?.address||config.email.from?.address||"",name:message.from?.name||config.email.from?.name},to:this.formatMailtrapAddresses(message.to),...message.cc&&{cc:this.formatMailtrapAddresses(message.cc)},...message.bcc&&{bcc:this.formatMailtrapAddresses(message.bcc)},...replyTo?{reply_to:replyTo}:{},...customHeaders?{headers:customHeaders}:{},subject:message.subject,...htmlContent&&{html:htmlContent},...message.text&&{text:message.text},...message.attachments&&{attachments:message.attachments.map((attachment)=>({filename:attachment.filename,content:typeof attachment.content==="string"?attachment.content:this.arrayBufferToBase64(attachment.content),type:attachment.contentType||"application/octet-stream"}))}},response=await this.sendWithRetry(mailtrapPayload);return this.handleSuccess(message,response.message_ids?.[0])}catch(error){return this.handleError(error,message)}}formatMailtrapAddresses(addresses){if(!addresses)return[];if(typeof addresses==="string")return[{email:addresses}];return addresses.map((addr)=>{if(typeof addr==="string")return{email:addr};return{email:addr.address,...addr.name&&{name:addr.name}}})}firstMailtrapAddress(value){if(!value)return;if(typeof value==="string")return{email:value};if(Array.isArray(value)){const first=value[0];if(first===void 0)return;if(typeof first==="string")return{email:first};return{email:first.address,...first.name&&{name:first.name}}}return{email:value.address,...value.name&&{name:value.name}}}arrayBufferToBase64(buffer){let binary="";const bytes=new Uint8Array(buffer),len=bytes.byteLength;for(let i=0;i<len;i++)binary+=String.fromCharCode(bytes[i]??0);return typeof btoa==="function"?btoa(binary):Buffer.from(binary).toString("base64")}async sendWithRetry(payload,attempt=1){const{host,token,inboxId}=this.getConfig();if(!inboxId)throw Error("Mailtrap inbox ID is required but not provided. Please set MAILTRAP_INBOX_ID in your environment variables.");const endpoint=`${host}/${inboxId}`;try{const response=await fetch(endpoint,{method:"POST",headers:{Authorization:`Bearer ${token}`,"Content-Type":"application/json"},body:JSON.stringify(payload)});if(!response.ok){const errorData=await response.json();throw Error(`Mailtrap API error: ${response.status} - ${JSON.stringify(errorData)}`)}const data=await response.json();log.info(`[${this.name}] Email sent successfully`,{attempt,messageId:data.message_ids?.[0]});return data}catch(error){if(attempt<(config.services.mailtrap?.maxRetries??3)){const retryTimeout=config.services.mailtrap?.retryTimeout??1000;log.warn(`[${this.name}] Email send failed, retrying (${attempt}/${config.services.mailtrap?.maxRetries??3})`);await new Promise((resolve)=>setTimeout(resolve,retryTimeout));return this.sendWithRetry(payload,attempt+1)}throw error}}}export default MailtrapDriver;
@@ -1 +1 @@
1
- import{Buffer}from"node:buffer";import{config}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{template}from"../template";import{filterStringHeaders}from"../validation";import{BaseEmailDriver}from"./base";export class SendGridDriver extends BaseEmailDriver{name="sendgrid";apiKey=null;getApiKey(){if(!this.apiKey)this.apiKey=config.services.sendgrid?.apiKey??"";return this.apiKey}async send(message,options){const logContext={provider:this.name,to:message.to,subject:message.subject};log.info("Sending email via SendGrid...",logContext);try{this.validateMessage(message);let htmlContent;if(message.template){const templ=await template(message.template,options);if(templ&&"html"in templ)htmlContent=templ.html}const finalHtml=htmlContent||message.html,content=[];if(finalHtml)content.push({type:"text/html",value:finalHtml});if(message.text)content.push({type:"text/plain",value:message.text});if(content.length===0)throw Error("Email must have either HTML or text content");const replyTo=this.firstSendGridAddress(message.replyTo),customHeaders=filterStringHeaders(message.headers),sendgridPayload={personalizations:[{to:this.formatSendGridAddresses(message.to),...message.cc&&{cc:this.formatSendGridAddresses(message.cc)},...message.bcc&&{bcc:this.formatSendGridAddresses(message.bcc)},subject:message.subject}],from:{email:message.from?.address||config.email.from?.address||"",name:message.from?.name||config.email.from?.name},...replyTo?{reply_to:replyTo}:{},...customHeaders?{headers:customHeaders}:{},content,...message.attachments&&{attachments:message.attachments.map((attachment)=>({filename:attachment.filename,content:typeof attachment.content==="string"?attachment.content:this.arrayBufferToBase64(attachment.content),type:attachment.contentType,disposition:"attachment"}))}},response=await this.sendWithRetry(sendgridPayload);return this.handleSuccess(message,response.headers?.get("x-message-id")??void 0)}catch(error){return this.handleError(error,message)}}formatSendGridAddresses(addresses){if(!addresses)return[];if(typeof addresses==="string")return[{email:addresses}];return addresses.map((addr)=>{if(typeof addr==="string")return{email:addr};return{email:addr.address,...addr.name&&{name:addr.name}}})}firstSendGridAddress(value){if(!value)return;if(typeof value==="string")return{email:value};if(Array.isArray(value)){const first=value[0];if(first===void 0)return;if(typeof first==="string")return{email:first};return{email:first.address,...first.name&&{name:first.name}}}return{email:value.address,...value.name&&{name:value.name}}}arrayBufferToBase64(buffer){let binary="";const bytes=new Uint8Array(buffer),len=bytes.byteLength;for(let i=0;i<len;i++)binary+=String.fromCharCode(bytes[i]??0);return typeof btoa==="function"?btoa(binary):Buffer.from(binary).toString("base64")}async sendWithRetry(payload,attempt=1){try{const response=await fetch("https://api.sendgrid.com/v3/mail/send",{method:"POST",headers:{Authorization:`Bearer ${this.getApiKey()}`,"Content-Type":"application/json"},body:JSON.stringify(payload)});if(!response.ok){const errorData=await response.json(),err=Error(`SendGrid API error: ${response.status} - ${JSON.stringify(errorData)}`);err.status=response.status;throw err}log.info(`[${this.name}] Email sent successfully`,{attempt});return response}catch(error){const status=error?.status;if(!(typeof status==="number"&&status>=400&&status<500&&status!==429)&&attempt<(config.services.sendgrid?.maxRetries??3)){const retryTimeout=config.services.sendgrid?.retryTimeout??1000;log.warn(`[${this.name}] Email send failed, retrying (${attempt}/${config.services.sendgrid?.maxRetries??3})`);await new Promise((resolve)=>setTimeout(resolve,retryTimeout));return this.sendWithRetry(payload,attempt+1)}throw error}}}export default SendGridDriver;
1
+ import{Buffer}from"node:buffer";import{config}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{templateByName}from"../template";import{filterStringHeaders}from"../validation";import{BaseEmailDriver}from"./base";export class SendGridDriver extends BaseEmailDriver{name="sendgrid";apiKey=null;getApiKey(){if(!this.apiKey)this.apiKey=config.services.sendgrid?.apiKey??"";return this.apiKey}async send(message,options){const logContext={provider:this.name,to:message.to,subject:message.subject};log.info("Sending email via SendGrid...",logContext);try{this.validateMessage(message);let htmlContent;if(message.template){const templ=await templateByName(message.template,options);if(templ&&"html"in templ)htmlContent=templ.html}const finalHtml=htmlContent||message.html,content=[];if(finalHtml)content.push({type:"text/html",value:finalHtml});if(message.text)content.push({type:"text/plain",value:message.text});if(content.length===0)throw Error("Email must have either HTML or text content");const replyTo=this.firstSendGridAddress(message.replyTo),customHeaders=filterStringHeaders(message.headers),sendgridPayload={personalizations:[{to:this.formatSendGridAddresses(message.to),...message.cc&&{cc:this.formatSendGridAddresses(message.cc)},...message.bcc&&{bcc:this.formatSendGridAddresses(message.bcc)},subject:message.subject}],from:{email:message.from?.address||config.email.from?.address||"",name:message.from?.name||config.email.from?.name},...replyTo?{reply_to:replyTo}:{},...customHeaders?{headers:customHeaders}:{},content,...message.attachments&&{attachments:message.attachments.map((attachment)=>({filename:attachment.filename,content:typeof attachment.content==="string"?attachment.content:this.arrayBufferToBase64(attachment.content),type:attachment.contentType,disposition:"attachment"}))}},response=await this.sendWithRetry(sendgridPayload);return this.handleSuccess(message,response.headers?.get("x-message-id")??void 0)}catch(error){return this.handleError(error,message)}}formatSendGridAddresses(addresses){if(!addresses)return[];if(typeof addresses==="string")return[{email:addresses}];return addresses.map((addr)=>{if(typeof addr==="string")return{email:addr};return{email:addr.address,...addr.name&&{name:addr.name}}})}firstSendGridAddress(value){if(!value)return;if(typeof value==="string")return{email:value};if(Array.isArray(value)){const first=value[0];if(first===void 0)return;if(typeof first==="string")return{email:first};return{email:first.address,...first.name&&{name:first.name}}}return{email:value.address,...value.name&&{name:value.name}}}arrayBufferToBase64(buffer){let binary="";const bytes=new Uint8Array(buffer),len=bytes.byteLength;for(let i=0;i<len;i++)binary+=String.fromCharCode(bytes[i]??0);return typeof btoa==="function"?btoa(binary):Buffer.from(binary).toString("base64")}async sendWithRetry(payload,attempt=1){try{const response=await fetch("https://api.sendgrid.com/v3/mail/send",{method:"POST",headers:{Authorization:`Bearer ${this.getApiKey()}`,"Content-Type":"application/json"},body:JSON.stringify(payload)});if(!response.ok){const errorData=await response.json(),err=Error(`SendGrid API error: ${response.status} - ${JSON.stringify(errorData)}`);err.status=response.status;throw err}log.info(`[${this.name}] Email sent successfully`,{attempt});return response}catch(error){const status=error?.status;if(!(typeof status==="number"&&status>=400&&status<500&&status!==429)&&attempt<(config.services.sendgrid?.maxRetries??3)){const retryTimeout=config.services.sendgrid?.retryTimeout??1000;log.warn(`[${this.name}] Email send failed, retrying (${attempt}/${config.services.sendgrid?.maxRetries??3})`);await new Promise((resolve)=>setTimeout(resolve,retryTimeout));return this.sendWithRetry(payload,attempt+1)}throw error}}}export default SendGridDriver;
@@ -1,4 +1,4 @@
1
- import{config}from"@stacksjs/config";import{SESClient}from"@stacksjs/ts-cloud";import{template}from"../template";import{buildMimeMessage}from"../mime";import{filterStringHeaders}from"../validation";import{BaseEmailDriver}from"./base";export class SESDriver extends BaseEmailDriver{name="ses";client=null;getClient(){if(!this.client){const sesConfig=config?.services?.ses,explicit=sesConfig?.credentials,hasExplicit=!!(explicit?.accessKeyId&&explicit?.secretAccessKey);this.client=new SESClient(sesConfig?.region||"us-east-1",hasExplicit?{accessKeyId:explicit.accessKeyId,secretAccessKey:explicit.secretAccessKey,sessionToken:explicit.sessionToken}:void 0)}return this.client}async send(message,options){try{this.validateMessage(message);let htmlContent;if(message.template){const templ=await template(message.template,options);if(templ&&"html"in templ)htmlContent=templ.html}const finalHtml=htmlContent||message.html;if(!finalHtml&&!message.text)throw Error("Email must have either HTML or text content");const fromAddress=this.formatSourceAddress({address:message.from?.address||config.email.from?.address||"",name:message.from?.name||config.email.from?.name}),toAddresses=this.formatAddresses(message.to),ccAddresses=this.formatAddresses(message.cc),bccAddresses=this.formatAddresses(message.bcc),replyToAddresses=message.replyTo?this.formatAddressList(message.replyTo):[],customHeaders=filterStringHeaders(message.headers);if(!!(message.attachments&&message.attachments.length>0)||!!customHeaders){const raw=buildMimeMessage({from:fromAddress,to:toAddresses.join(", "),cc:ccAddresses.length>0?ccAddresses.join(", "):void 0,replyTo:replyToAddresses.length>0?replyToAddresses.join(", "):void 0,customHeaders,subject:message.subject,text:message.text,html:finalHtml,attachments:message.attachments,messageIdDomain:config.email.domain}),result=await this.getClient().sendRawEmail({source:fromAddress,destinations:[...toAddresses,...ccAddresses,...bccAddresses],rawMessage:raw});return this.handleSuccess(message,result.MessageId)}const body={};if(finalHtml)body.Html={Charset:config.email.charset||"UTF-8",Data:finalHtml};if(message.text)body.Text={Charset:config.email.charset||"UTF-8",Data:message.text};const result=await this.getClient().sendEmail({FromEmailAddress:fromAddress,Destination:{ToAddresses:toAddresses,CcAddresses:ccAddresses,BccAddresses:bccAddresses},...replyToAddresses.length>0?{ReplyToAddresses:replyToAddresses}:{},Content:{Simple:{Subject:{Charset:config.email.charset||"UTF-8",Data:message.subject},Body:body}}});return this.handleSuccess(message,result.MessageId)}catch(error){return this.handleError(this.enrichSesError(error),message)}}formatSourceAddress(from){if(!from.name)return from.address;return`${/[",()<>[\]:;@\\]/.test(from.name)?`"${from.name.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")}"`:from.name} <${from.address}>`}enrichSesError(error){const err=error instanceof Error?error:Error(String(error)),text=`${err.message} ${err.name??""}`.toLowerCase(),region=config?.services?.ses?.region||"us-east-1";if(text.includes("email address is not verified")||text.includes("not authorized to send")||text.includes("messagerejected")&&text.includes("verified")){err.message=`${err.message}
1
+ import{config}from"@stacksjs/config";import{SESClient}from"@stacksjs/ts-cloud";import{templateByName}from"../template";import{buildMimeMessage}from"../mime";import{filterStringHeaders}from"../validation";import{BaseEmailDriver}from"./base";export class SESDriver extends BaseEmailDriver{name="ses";client=null;getClient(){if(!this.client){const sesConfig=config?.services?.ses,explicit=sesConfig?.credentials,hasExplicit=!!(explicit?.accessKeyId&&explicit?.secretAccessKey);this.client=new SESClient(sesConfig?.region||"us-east-1",hasExplicit?{accessKeyId:explicit.accessKeyId,secretAccessKey:explicit.secretAccessKey,sessionToken:explicit.sessionToken}:void 0)}return this.client}async send(message,options){try{this.validateMessage(message);let htmlContent;if(message.template){const templ=await templateByName(message.template,options);if(templ&&"html"in templ)htmlContent=templ.html}const finalHtml=htmlContent||message.html;if(!finalHtml&&!message.text)throw Error("Email must have either HTML or text content");const fromAddress=this.formatSourceAddress({address:message.from?.address||config.email.from?.address||"",name:message.from?.name||config.email.from?.name}),toAddresses=this.formatAddresses(message.to),ccAddresses=this.formatAddresses(message.cc),bccAddresses=this.formatAddresses(message.bcc),replyToAddresses=message.replyTo?this.formatAddressList(message.replyTo):[],customHeaders=filterStringHeaders(message.headers);if(!!(message.attachments&&message.attachments.length>0)||!!customHeaders){const raw=buildMimeMessage({from:fromAddress,to:toAddresses.join(", "),cc:ccAddresses.length>0?ccAddresses.join(", "):void 0,replyTo:replyToAddresses.length>0?replyToAddresses.join(", "):void 0,customHeaders,subject:message.subject,text:message.text,html:finalHtml,attachments:message.attachments,messageIdDomain:config.email.domain}),result=await this.getClient().sendRawEmail({source:fromAddress,destinations:[...toAddresses,...ccAddresses,...bccAddresses],rawMessage:raw});return this.handleSuccess(message,result.MessageId)}const body={};if(finalHtml)body.Html={Charset:config.email.charset||"UTF-8",Data:finalHtml};if(message.text)body.Text={Charset:config.email.charset||"UTF-8",Data:message.text};const result=await this.getClient().sendEmail({FromEmailAddress:fromAddress,Destination:{ToAddresses:toAddresses,CcAddresses:ccAddresses,BccAddresses:bccAddresses},...replyToAddresses.length>0?{ReplyToAddresses:replyToAddresses}:{},Content:{Simple:{Subject:{Charset:config.email.charset||"UTF-8",Data:message.subject},Body:body}}});return this.handleSuccess(message,result.MessageId)}catch(error){return this.handleError(this.enrichSesError(error),message)}}formatSourceAddress(from){if(!from.name)return from.address;return`${/[",()<>[\]:;@\\]/.test(from.name)?`"${from.name.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")}"`:from.name} <${from.address}>`}enrichSesError(error){const err=error instanceof Error?error:Error(String(error)),text=`${err.message} ${err.name??""}`.toLowerCase(),region=config?.services?.ses?.region||"us-east-1";if(text.includes("email address is not verified")||text.includes("not authorized to send")||text.includes("messagerejected")&&text.includes("verified")){err.message=`${err.message}
2
2
 
3
3
  SES sandbox restriction: the From and (in sandbox) every To address must be verified. Verify identities in the SES console under "Verified identities" (region: ${region}), or request production access to lift the recipient restriction.`;return err}if(text.includes("signaturedoesnotmatch")||text.includes("invalidclienttokenid")||text.includes("unable to locate credentials")||text.includes("the security token included in the request is invalid")){err.message=`${err.message}
4
4
 
@@ -1,4 +1,4 @@
1
- import{Buffer}from"node:buffer";import process from"node:process";import*as tls from"node:tls";import*as net from"node:net";import{config}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{template}from"../template";import{buildMimeMessage}from"../mime";import{ENVELOPE_ADDRESS,filterStringHeaders}from"../validation";import{BaseEmailDriver}from"./base";function assertEnvelopeAddress(addr,role){if(typeof addr!=="string"||!ENVELOPE_ADDRESS.test(addr))throw Error(`[smtp] Refusing to send: ${role} envelope address contains forbidden characters or is malformed: ${JSON.stringify(addr)}`)}export class SMTPDriver extends BaseEmailDriver{static SMTP_TIMEOUT=30000;name="smtp";getConfig(){const smtp=config.services?.smtp,env=process.env,host=smtp?.host||env.MAIL_HOST||"127.0.0.1",port=smtp?.port||(env.MAIL_PORT?Number(env.MAIL_PORT):void 0)||587,fromAddress=typeof config.email?.from?.address==="string"?config.email.from.address:"",username=smtp?.username||env.MAIL_USERNAME||fromAddress||"",localPart=(username.includes("@")?username.split("@")[0]:username).toUpperCase().replace(/[^A-Z0-9]/g,"_"),password=smtp?.password||env.MAIL_PASSWORD||(localPart?env[`MAIL_PASSWORD_${localPart}`]:void 0)||"",rawEncryption=smtp?.encryption??env.MAIL_ENCRYPTION??null;return{host,port,username,password,encryption:rawEncryption==="tls"?"starttls":rawEncryption||null}}async send(message,options){const smtpConfig=this.getConfig();if(!smtpConfig.host||smtpConfig.host==="")throw Error("[SMTP] Host is not configured. Set MAIL_HOST in your .env file.");const logContext={provider:this.name,to:message.to,subject:message.subject,host:smtpConfig.host,port:smtpConfig.port};log.info("Sending email via SMTP...",logContext);try{this.validateMessage(message);let htmlContent;if(message.template){const templ=await template(message.template,options);if(templ&&"html"in templ)htmlContent=templ.html}const finalHtml=htmlContent||message.html,fromAddress=message.from?.address||config.email.from?.address||"",fromName=message.from?.name||config.email.from?.name||"",toAddresses=this.formatAddresses(message.to),replyToAddresses=this.formatAddressList(message.replyTo),emailContent=buildMimeMessage({from:fromName?`${fromName} <${fromAddress}>`:fromAddress,to:toAddresses.join(", "),cc:message.cc?this.formatAddresses(message.cc).join(", "):void 0,replyTo:replyToAddresses.length>0?replyToAddresses.join(", "):void 0,customHeaders:filterStringHeaders(message.headers),subject:message.subject,text:message.text,html:finalHtml,attachments:message.attachments,messageIdDomain:config.email.domain}),messageId=await this.sendViaSMTP(smtpConfig,fromAddress,toAddresses,emailContent);return this.handleSuccess(message,messageId)}catch(error){return this.handleError(error,message)}}async sendViaSMTP(smtpConfig,from,to,content){return new Promise((resolve,reject)=>{const timeout=setTimeout(()=>{reject(Error(`SMTP connection timed out after ${SMTPDriver.SMTP_TIMEOUT}ms`))},SMTPDriver.SMTP_TIMEOUT),originalResolve=resolve,originalReject=reject;resolve=(value)=>{clearTimeout(timeout);originalResolve(value)};reject=(reason)=>{clearTimeout(timeout);originalReject(reason)};let socket,buffer="";const _currentCommand="",commandQueue=[],_isProcessing=!1;let completed=!1;const processResponse=(response)=>{log.debug(`[SMTP] Server: ${response.trim()}`);if(parseInt(response.substring(0,3),10)>=400){const error=Error(`SMTP Error: ${response.trim()}`);if(commandQueue.length>0)commandQueue.shift()?.reject(error);return}if(commandQueue.length>0)commandQueue.shift()?.resolve(response)},sendCommand=(cmd)=>{return new Promise((res,rej)=>{commandQueue.push({cmd,resolve:res,reject:rej});log.debug(`[SMTP] Client: ${cmd}`);socket.write(`${cmd}\r
1
+ import{Buffer}from"node:buffer";import process from"node:process";import*as tls from"node:tls";import*as net from"node:net";import{config}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{templateByName}from"../template";import{buildMimeMessage}from"../mime";import{ENVELOPE_ADDRESS,filterStringHeaders}from"../validation";import{BaseEmailDriver}from"./base";function assertEnvelopeAddress(addr,role){if(typeof addr!=="string"||!ENVELOPE_ADDRESS.test(addr))throw Error(`[smtp] Refusing to send: ${role} envelope address contains forbidden characters or is malformed: ${JSON.stringify(addr)}`)}export class SMTPDriver extends BaseEmailDriver{static SMTP_TIMEOUT=30000;name="smtp";getConfig(){const smtp=config.services?.smtp,env=process.env,host=smtp?.host||env.MAIL_HOST||"127.0.0.1",port=smtp?.port||(env.MAIL_PORT?Number(env.MAIL_PORT):void 0)||587,fromAddress=typeof config.email?.from?.address==="string"?config.email.from.address:"",username=smtp?.username||env.MAIL_USERNAME||fromAddress||"",localPart=(username.includes("@")?username.split("@")[0]:username).toUpperCase().replace(/[^A-Z0-9]/g,"_"),password=smtp?.password||env.MAIL_PASSWORD||(localPart?env[`MAIL_PASSWORD_${localPart}`]:void 0)||"",rawEncryption=smtp?.encryption??env.MAIL_ENCRYPTION??null;return{host,port,username,password,encryption:rawEncryption==="tls"?"starttls":rawEncryption||null}}async send(message,options){const smtpConfig=this.getConfig();if(!smtpConfig.host||smtpConfig.host==="")throw Error("[SMTP] Host is not configured. Set MAIL_HOST in your .env file.");const logContext={provider:this.name,to:message.to,subject:message.subject,host:smtpConfig.host,port:smtpConfig.port};log.info("Sending email via SMTP...",logContext);try{this.validateMessage(message);let htmlContent;if(message.template){const templ=await templateByName(message.template,options);if(templ&&"html"in templ)htmlContent=templ.html}const finalHtml=htmlContent||message.html,fromAddress=message.from?.address||config.email.from?.address||"",fromName=message.from?.name||config.email.from?.name||"",toAddresses=this.formatAddresses(message.to),replyToAddresses=this.formatAddressList(message.replyTo),emailContent=buildMimeMessage({from:fromName?`${fromName} <${fromAddress}>`:fromAddress,to:toAddresses.join(", "),cc:message.cc?this.formatAddresses(message.cc).join(", "):void 0,replyTo:replyToAddresses.length>0?replyToAddresses.join(", "):void 0,customHeaders:filterStringHeaders(message.headers),subject:message.subject,text:message.text,html:finalHtml,attachments:message.attachments,messageIdDomain:config.email.domain}),messageId=await this.sendViaSMTP(smtpConfig,fromAddress,toAddresses,emailContent);return this.handleSuccess(message,messageId)}catch(error){return this.handleError(error,message)}}async sendViaSMTP(smtpConfig,from,to,content){return new Promise((resolve,reject)=>{const timeout=setTimeout(()=>{reject(Error(`SMTP connection timed out after ${SMTPDriver.SMTP_TIMEOUT}ms`))},SMTPDriver.SMTP_TIMEOUT),originalResolve=resolve,originalReject=reject;resolve=(value)=>{clearTimeout(timeout);originalResolve(value)};reject=(reason)=>{clearTimeout(timeout);originalReject(reason)};let socket,buffer="";const _currentCommand="",commandQueue=[],_isProcessing=!1;let completed=!1;const processResponse=(response)=>{log.debug(`[SMTP] Server: ${response.trim()}`);if(parseInt(response.substring(0,3),10)>=400){const error=Error(`SMTP Error: ${response.trim()}`);if(commandQueue.length>0)commandQueue.shift()?.reject(error);return}if(commandQueue.length>0)commandQueue.shift()?.resolve(response)},sendCommand=(cmd)=>{return new Promise((res,rej)=>{commandQueue.push({cmd,resolve:res,reject:rej});log.debug(`[SMTP] Client: ${cmd}`);socket.write(`${cmd}\r
2
2
  `)})},handleData=(data)=>{buffer+=data.toString();const lines=buffer.split(`\r
3
3
  `);buffer=lines.pop()||"";for(const line of lines)if(line.length>=3){if(line.length===3||line[3]===" ")processResponse(line)}},runSmtpSession=async()=>{try{await new Promise((res,rej)=>{commandQueue.push({cmd:"GREETING",resolve:res,reject:rej})});const _ehloResponse=await sendCommand(`EHLO ${config.email.domain||"localhost"}`);if(smtpConfig.encryption==="starttls"&&!(socket instanceof tls.TLSSocket)){await sendCommand("STARTTLS");const plainSocket=socket;plainSocket.removeAllListeners("data");socket=await new Promise((res,rej)=>{const tlsSocket=tls.connect({socket:plainSocket,host:smtpConfig.host,servername:smtpConfig.host},()=>{log.debug("[SMTP] TLS connection established");res(tlsSocket)});tlsSocket.on("error",(err)=>{log.error("[SMTP] TLS socket error:",err);rej(err)});tlsSocket.on("data",handleData);tlsSocket.on("close",(hadError)=>{log.debug(`[SMTP] TLS socket closed (hadError: ${hadError})`);while(commandQueue.length>0)commandQueue.shift()?.reject(Error("TLS connection closed unexpectedly"))})});await sendCommand(`EHLO ${config.email.domain||"localhost"}`)}if(smtpConfig.username&&smtpConfig.password){await sendCommand("AUTH LOGIN");await sendCommand(Buffer.from(smtpConfig.username).toString("base64"));await sendCommand(Buffer.from(smtpConfig.password).toString("base64"))}assertEnvelopeAddress(from,"MAIL FROM");for(const recipient of to)assertEnvelopeAddress(recipient,"RCPT TO");await sendCommand(`MAIL FROM:<${from}>`);for(const recipient of to)await sendCommand(`RCPT TO:<${recipient}>`);await sendCommand("DATA");socket.write(`${content}\r
4
4
  .\r
@@ -40,11 +40,11 @@ export declare interface MailableInspection<TProps extends Record<string, unknow
40
40
  subject?: string
41
41
  text?: string
42
42
  html?: string
43
- template?: { name: string, props: TProps }
43
+ template?: { name: EmailTemplateReference, props: TProps }
44
44
  attachments: EmailAttachment[]
45
45
  }
46
46
  declare interface TemplateRef<TProps extends Record<string, unknown> = Record<string, unknown>> {
47
- name: string
47
+ name: EmailTemplateReference
48
48
  props: TProps
49
49
  }
50
50
  /** A template name, as narrow as the application has made it. */
@@ -1,3 +1,4 @@
1
+ import type { EmailTemplateReference } from './mailable';
1
2
  /**
2
3
  * Mark a string as pre-rendered HTML so {@link replaceVariables} splices
3
4
  * it in verbatim instead of escaping. Use ONLY for content that you
@@ -69,7 +70,26 @@ export declare function resetTemplateRegistry(): void;
69
70
  * })
70
71
  * ```
71
72
  */
72
- export declare function template(templateName: string, options?: TemplateOptions): Promise<TemplateResult>;
73
+ export declare function template(templateName: EmailTemplateReference, options?: TemplateOptions): Promise<TemplateResult>;
74
+ /**
75
+ * Render a template whose name is only known at runtime.
76
+ *
77
+ * `template()` above is the authoring entry point: the name is written inline,
78
+ * so it is checked against the templates that exist and a typo is a build
79
+ * error rather than an email that renders empty.
80
+ *
81
+ * This one takes a plain `string`, for the two places where that is the honest
82
+ * type. A driver re-renders from an `EmailMessage` that may have arrived off a
83
+ * queue, so its template name is genuinely unvalidated by the time it gets
84
+ * here. And a caller may deliberately probe for a template the application is
85
+ * not required to provide - `magic-link` is one - falling back when it is
86
+ * absent. Both are real, and neither should have to lie about the name being
87
+ * checked.
88
+ *
89
+ * A missing template resolves to empty `html`/`text` rather than throwing, so
90
+ * a caller can treat emptiness as "not present".
91
+ */
92
+ export declare function templateByName(templateName: string, options?: TemplateOptions): Promise<TemplateResult>;
73
93
  /**
74
94
  * Render a raw HTML string with variables (no file loading)
75
95
  */
package/dist/template.js CHANGED
@@ -2,4 +2,4 @@ var {require}=import.meta;import{config}from"@stacksjs/config";import{log}from"@
2
2
  `).replace(/<\/(p|div|h[1-6]|li|tr)>/gi,`
3
3
  `).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,`
4
4
 
5
- `).trim()}export async function template(templateName,options={}){const{variables={},layout="base",subject="",inline=shouldInlineByDefault()}=options,allVariables={...getDefaultVariables(),subject,...variables},resolved=resolveTemplatePath(templateName);if(!resolved){console.warn(`[Email Template] Template "${templateName}" not found`);return{html:"",text:""}}if(resolved.type==="stx")try{const{renderEmail}=await import("@stacksjs/stx"),result=await renderEmail(resolved.path,allVariables,{componentsDir:defaultsResourcesPath("components/Email")});return{...result,html:inlineCss(result.html,{inline})}}catch(error){log.warn(`[email] STX template rendering failed for ${templateName}: ${error instanceof Error?error.message:String(error)}`);return{html:"",text:""}}let content=fs.readFileSync(resolved.path,"utf-8");content=replaceVariables(content,allVariables);let html;if(layout!==!1){const layoutHtml=loadLayout(layout);if(!layoutHtml){console.warn(`[Email Template] Layout "${layout}" not found, using content only`);html=content}else{allVariables.content=safe(content);html=replaceVariables(layoutHtml,allVariables)}}else html=content;html=inlineCss(html,{inline});const text=htmlToText(html);return{html,text}}export function renderHtml(htmlContent,variables={}){const allVariables={...getDefaultVariables(),...variables},html=replaceVariables(htmlContent,allVariables),text=htmlToText(html);return{html,text}}export function templateExists(templateName){return resolveTemplatePath(templateName)!==null}export function listTemplates(){const emailsPath=resourcesPath("emails");if(!fs.existsSync(emailsPath))return[];const templates=[];function scanDir(dir,prefix=""){const entries=fs.readdirSync(dir,{withFileTypes:!0});for(const entry of entries)if(entry.isDirectory()&&entry.name!=="layouts")scanDir(join(dir,entry.name),`${prefix}${entry.name}/`);else if(entry.isFile()&&(entry.name.endsWith(".html")||entry.name.endsWith(".stx"))){const name=entry.name.replace(/\.(html|stx)$/,"");if(!templates.includes(`${prefix}${name}`))templates.push(`${prefix}${name}`)}}scanDir(emailsPath);return templates}
5
+ `).trim()}export async function template(templateName,options={}){return templateByName(templateName,options)}export async function templateByName(templateName,options={}){const{variables={},layout="base",subject="",inline=shouldInlineByDefault()}=options,allVariables={...getDefaultVariables(),subject,...variables},resolved=resolveTemplatePath(templateName);if(!resolved){console.warn(`[Email Template] Template "${templateName}" not found`);return{html:"",text:""}}if(resolved.type==="stx")try{const{renderEmail}=await import("@stacksjs/stx"),result=await renderEmail(resolved.path,allVariables,{componentsDir:defaultsResourcesPath("components/Email")});return{...result,html:inlineCss(result.html,{inline})}}catch(error){log.warn(`[email] STX template rendering failed for ${templateName}: ${error instanceof Error?error.message:String(error)}`);return{html:"",text:""}}let content=fs.readFileSync(resolved.path,"utf-8");content=replaceVariables(content,allVariables);let html;if(layout!==!1){const layoutHtml=loadLayout(layout);if(!layoutHtml){console.warn(`[Email Template] Layout "${layout}" not found, using content only`);html=content}else{allVariables.content=safe(content);html=replaceVariables(layoutHtml,allVariables)}}else html=content;html=inlineCss(html,{inline});const text=htmlToText(html);return{html,text}}export function renderHtml(htmlContent,variables={}){const allVariables={...getDefaultVariables(),...variables},html=replaceVariables(htmlContent,allVariables),text=htmlToText(html);return{html,text}}export function templateExists(templateName){return resolveTemplatePath(templateName)!==null}export function listTemplates(){const emailsPath=resourcesPath("emails");if(!fs.existsSync(emailsPath))return[];const templates=[];function scanDir(dir,prefix=""){const entries=fs.readdirSync(dir,{withFileTypes:!0});for(const entry of entries)if(entry.isDirectory()&&entry.name!=="layouts")scanDir(join(dir,entry.name),`${prefix}${entry.name}/`);else if(entry.isFile()&&(entry.name.endsWith(".html")||entry.name.endsWith(".stx"))){const name=entry.name.replace(/\.(html|stx)$/,"");if(!templates.includes(`${prefix}${name}`))templates.push(`${prefix}${name}`)}}scanDir(emailsPath);return templates}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/email",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.72.102",
5
+ "version": "0.72.103",
6
6
  "description": "The Stacks Email integration. Painlessly create & manage your inboxes, templates, and send emails.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -57,6 +57,7 @@
57
57
  "build": "bun build.ts",
58
58
  "build:inbound": "bun build-inbound.ts",
59
59
  "typecheck": "bun tsc --noEmit",
60
+ "typecheck:types": "bun tsc --noEmit -p tsconfig.type-tests.json --pretty false",
60
61
  "prepublishOnly": "bun run build"
61
62
  },
62
63
  "dependencies": {
@@ -64,10 +65,10 @@
64
65
  "postal-mime": "^2.7.6"
65
66
  },
66
67
  "devDependencies": {
67
- "@stacksjs/cli": "0.72.102",
68
- "@stacksjs/config": "0.72.102",
68
+ "@stacksjs/cli": "0.72.103",
69
+ "@stacksjs/config": "0.72.103",
69
70
  "better-dx": "^0.2.24",
70
- "@stacksjs/error-handling": "0.72.102",
71
- "@stacksjs/types": "0.72.102"
71
+ "@stacksjs/error-handling": "0.72.103",
72
+ "@stacksjs/types": "0.72.103"
72
73
  }
73
74
  }