@stacksjs/email 0.72.98 → 0.72.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/email.js +1 -1
- package/dist/mailable.d.ts +25 -1
- package/dist/template.d.ts +2 -0
- package/dist/template.js +1 -1
- package/package.json +5 -5
package/dist/email.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{config}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{CaptureEmailDriver}from"./drivers/capture";import{LogEmailDriver}from"./drivers/log";import{MailgunDriver}from"./drivers/mailgun";import{MailtrapDriver}from"./drivers/mailtrap";import{SendGridDriver}from"./drivers/sendgrid";import{SESDriver}from"./drivers/ses";import{SMTPDriver}from"./drivers/smtp";import{findEmailByIdempotencyKey,recordEmailIdempotency}from"./idempotency";import{checkSuppressionFor}from"./suppression";export class EmailDeliveryError extends Error{result;constructor(result){super(result.message||`Email delivery failed through ${result.provider||"the configured provider"}.`);this.name="EmailDeliveryError";this.result=result}}export class Email{name;subject;to;from;template;handle;onError;onSuccess;constructor(options){this.name=options.name;this.subject=options.subject;this.to=options.to;this.from=options.from;this.template=options.template;this.handle=options.handle;this.onError=options.onError;this.onSuccess=options.onSuccess}async renderTemplate(){if(!this.template)return"";try{const{path:p}=await import("@stacksjs/path"),templatePath=p.resourcesPath(`views/emails/${this.template}.html`),file=Bun.file(templatePath);if(await file.exists())return await file.text()}catch{}if(this.template.includes("<"))return this.template;return`<p>${this.template}</p>`}async send(to){const target=to??this.to,recipients=Array.isArray(target)?target:target?[target]:[];if(recipients.length===0)throw Error("No recipient specified for email");try{await mail.sendOrFail({to:recipients,from:this.from||{name:config.email.from?.name||"Stacks",address:config.email.from?.address||"no-reply@stacksjs.com"},subject:this.subject,html:await this.renderTemplate()});if(this.onSuccess)this.onSuccess();if(this.handle)return this.handle();return{message:"Email sent"}}catch(error){if(this.onError)return this.onError(error instanceof Error?error:Error(String(error)));throw error}}}export class Mail{drivers=new Map;defaultDriver;constructor(options={}){this.defaultDriver=options.defaultDriver||config.email.default||"ses";this.registerDefaultDrivers()}registerDefaultDrivers(){this.drivers.set("log",new LogEmailDriver);this.drivers.set("ses",new SESDriver);this.drivers.set("sendgrid",new SendGridDriver);this.drivers.set("mailgun",new MailgunDriver);this.drivers.set("mailtrap",new MailtrapDriver);this.drivers.set("smtp",new SMTPDriver);this.drivers.set("capture",new CaptureEmailDriver)}async send(message){const driver=this.drivers.get(this.defaultDriver);if(!driver){const available=[...this.drivers.keys()].sort().join(", ");throw Error(`Email driver '${this.defaultDriver}' is not registered. Available drivers: [${available}]. Check config.email.default or the MAIL_MAILER environment variable.`)}if(message.idempotencyKey){const cached=await findEmailByIdempotencyKey(message.idempotencyKey);if(cached)return cached}const suppressionType=await checkSuppressionForFirstRecipient(message);if(suppressionType)return{success:!1,message:`suppressed:${suppressionType}`,provider:"suppression"};const defaultFrom={name:config.email.from?.name||"Stacks",address:config.email.from?.address||"no-reply@stacksjs.com"},result=await driver.send({...message,from:message.from||defaultFrom});if(message.idempotencyKey)await recordEmailIdempotency(message.idempotencyKey,message,result);return result}async sendOrFail(message){const result=await this.send(message);if(!result.success)throw new EmailDeliveryError(result);return result}use(driver){if(!this.drivers.has(driver))throw Error(`Email driver '${driver}' is not available`);return new Mail({defaultDriver:driver})}async queue(message){await this.dispatchOrFallback(message,async()=>{const{job}=await import("@stacksjs/queue");await job("
|
|
1
|
+
import{config}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{CaptureEmailDriver}from"./drivers/capture";import{LogEmailDriver}from"./drivers/log";import{MailgunDriver}from"./drivers/mailgun";import{MailtrapDriver}from"./drivers/mailtrap";import{SendGridDriver}from"./drivers/sendgrid";import{SESDriver}from"./drivers/ses";import{SMTPDriver}from"./drivers/smtp";import{findEmailByIdempotencyKey,recordEmailIdempotency}from"./idempotency";import{checkSuppressionFor}from"./suppression";export class EmailDeliveryError extends Error{result;constructor(result){super(result.message||`Email delivery failed through ${result.provider||"the configured provider"}.`);this.name="EmailDeliveryError";this.result=result}}export class Email{name;subject;to;from;template;handle;onError;onSuccess;constructor(options){this.name=options.name;this.subject=options.subject;this.to=options.to;this.from=options.from;this.template=options.template;this.handle=options.handle;this.onError=options.onError;this.onSuccess=options.onSuccess}async renderTemplate(){if(!this.template)return"";try{const{path:p}=await import("@stacksjs/path"),templatePath=p.resourcesPath(`views/emails/${this.template}.html`),file=Bun.file(templatePath);if(await file.exists())return await file.text()}catch{}if(this.template.includes("<"))return this.template;return`<p>${this.template}</p>`}async send(to){const target=to??this.to,recipients=Array.isArray(target)?target:target?[target]:[];if(recipients.length===0)throw Error("No recipient specified for email");try{await mail.sendOrFail({to:recipients,from:this.from||{name:config.email.from?.name||"Stacks",address:config.email.from?.address||"no-reply@stacksjs.com"},subject:this.subject,html:await this.renderTemplate()});if(this.onSuccess)this.onSuccess();if(this.handle)return this.handle();return{message:"Email sent"}}catch(error){if(this.onError)return this.onError(error instanceof Error?error:Error(String(error)));throw error}}}export class Mail{drivers=new Map;defaultDriver;constructor(options={}){this.defaultDriver=options.defaultDriver||config.email.default||"ses";this.registerDefaultDrivers()}registerDefaultDrivers(){this.drivers.set("log",new LogEmailDriver);this.drivers.set("ses",new SESDriver);this.drivers.set("sendgrid",new SendGridDriver);this.drivers.set("mailgun",new MailgunDriver);this.drivers.set("mailtrap",new MailtrapDriver);this.drivers.set("smtp",new SMTPDriver);this.drivers.set("capture",new CaptureEmailDriver)}async send(message){const driver=this.drivers.get(this.defaultDriver);if(!driver){const available=[...this.drivers.keys()].sort().join(", ");throw Error(`Email driver '${this.defaultDriver}' is not registered. Available drivers: [${available}]. Check config.email.default or the MAIL_MAILER environment variable.`)}if(message.idempotencyKey){const cached=await findEmailByIdempotencyKey(message.idempotencyKey);if(cached)return cached}const suppressionType=await checkSuppressionForFirstRecipient(message);if(suppressionType)return{success:!1,message:`suppressed:${suppressionType}`,provider:"suppression"};const defaultFrom={name:config.email.from?.name||"Stacks",address:config.email.from?.address||"no-reply@stacksjs.com"},result=await driver.send({...message,from:message.from||defaultFrom});if(message.idempotencyKey)await recordEmailIdempotency(message.idempotencyKey,message,result);return result}async sendOrFail(message){const result=await this.send(message);if(!result.success)throw new EmailDeliveryError(result);return result}use(driver){if(!this.drivers.has(driver))throw Error(`Email driver '${driver}' is not available`);return new Mail({defaultDriver:driver})}async queue(message){await this.dispatchOrFallback(message,async()=>{const{job}=await import("@stacksjs/queue");await job("SendEmailJob",{message,driver:this.defaultDriver}).onQueue("emails").dispatch()},{context:"queue"})}async later(delaySeconds,message){await this.dispatchOrFallback(message,async()=>{const{job}=await import("@stacksjs/queue");await job("SendEmailJob",{message,driver:this.defaultDriver}).onQueue("emails").delay(delaySeconds).dispatch()},{context:"later",delaySeconds})}async queueOn(queueName,message){await this.dispatchOrFallback(message,async()=>{const{job}=await import("@stacksjs/queue");await job("SendEmailJob",{message,driver:this.defaultDriver}).onQueue(queueName).dispatch()},{context:"queueOn",queueName})}async dispatchOrFallback(message,dispatch,logExtra){try{await dispatch();return}catch(error){const reason=error instanceof Error?error.message:String(error);log.warn("[email] Queue dispatch failed; falling back to synchronous send. Background email pipeline is degraded - check the queue worker / broker.",{...logExtra,reason})}await this.send(message)}}async function checkSuppressionForFirstRecipient(message){const recipients=collectRecipientAddresses(message);if(recipients.length===0)return null;for(const addr of recipients){const matched=await checkSuppressionFor(addr,message.tag);if(matched)return matched}return null}function collectRecipientAddresses(message){const out=[],pushOne=(v)=>{if(typeof v==="string"){out.push(v);return}if(v&&typeof v==="object"&&"address"in v&&typeof v.address==="string")out.push(v.address)};for(const field of["to","cc","bcc"]){const v=message[field];if(!v)continue;if(Array.isArray(v))for(const item of v)pushOne(item);else pushOne(v)}return out}let _mail;function getMail(){if(!_mail){const driver=config?.email?.default||process.env.MAIL_MAILER||"ses";_mail=new Mail({defaultDriver:driver})}return _mail}export const mail=new Proxy({},{get(_t,prop){return getMail()[prop]},set(_t,prop,value){getMail()[prop]=value;return!0}});
|
package/dist/mailable.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import type { EmailAddress, EmailAttachment, EmailResult } from '@stacksjs/types';
|
|
2
|
+
/**
|
|
3
|
+
* Augmentation target: every email template this application can render.
|
|
4
|
+
*
|
|
5
|
+
* The keys are template names as `template(...)` takes them - `'welcome'`,
|
|
6
|
+
* `'layouts/base'` - without the extension. `buddy generate` writes the
|
|
7
|
+
* augmentation from `resources/emails/` and the framework defaults behind it,
|
|
8
|
+
* the same two roots `resolveTemplatePath` probes.
|
|
9
|
+
*/
|
|
10
|
+
// eslint-disable-next-line ts/no-empty-object-type -- augmentation target; empty by design
|
|
11
|
+
export declare interface EmailTemplates {}
|
|
2
12
|
/**
|
|
3
13
|
* Options accepted by {@link Mailable.send} — currently allows scoping
|
|
4
14
|
* the send to a specific driver registered on the Mail singleton (e.g.
|
|
@@ -37,6 +47,20 @@ declare interface TemplateRef<TProps extends Record<string, unknown> = Record<st
|
|
|
37
47
|
name: string
|
|
38
48
|
props: TProps
|
|
39
49
|
}
|
|
50
|
+
/** A template name, as narrow as the application has made it. */
|
|
51
|
+
export type EmailTemplateName = keyof EmailTemplates extends never
|
|
52
|
+
? string
|
|
53
|
+
: keyof EmailTemplates & string;
|
|
54
|
+
/**
|
|
55
|
+
* A template name, with or without the extension.
|
|
56
|
+
*
|
|
57
|
+
* `resolveTemplatePath` accepts `'welcome'`, `'welcome.stx'` and
|
|
58
|
+
* `'welcome.html'` - a bare name preferring `.stx` - so the type has to as
|
|
59
|
+
* well, or it would reject calls that work.
|
|
60
|
+
*/
|
|
61
|
+
export type EmailTemplateReference = | EmailTemplateName
|
|
62
|
+
| `${EmailTemplateName}.stx`
|
|
63
|
+
| `${EmailTemplateName}.html`;
|
|
40
64
|
/**
|
|
41
65
|
* Allowed recipient input — accepts a single address, an array of addresses,
|
|
42
66
|
* or already-shaped {@link EmailAddress} objects. Strings are treated as
|
|
@@ -108,7 +132,7 @@ export declare abstract class Mailable<TProps extends Record<string, unknown> =
|
|
|
108
132
|
subject(s: string): this;
|
|
109
133
|
text(body: string): this;
|
|
110
134
|
html(body: string): this;
|
|
111
|
-
template(name:
|
|
135
|
+
template(name: EmailTemplateReference, ...rest: TemplateArgs<TProps>): this;
|
|
112
136
|
inspect(): MailableInspection<TProps>;
|
|
113
137
|
attach(path: string, name?: string): this;
|
|
114
138
|
attachData(buffer: Uint8Array | string, name: string, mime?: string): this;
|
package/dist/template.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ export declare function safe(html: string): SafeHtml;
|
|
|
30
30
|
* single regex for short strings.
|
|
31
31
|
*/
|
|
32
32
|
export declare function escapeHtml(input: string): string;
|
|
33
|
+
/** For tests, and for a dev server that regenerated the registry. */
|
|
34
|
+
export declare function resetTemplateRegistry(): void;
|
|
33
35
|
/**
|
|
34
36
|
* Render an email template with optional layout
|
|
35
37
|
*
|
package/dist/template.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{config}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{fs}from"@stacksjs/storage";import{defaultsResourcesPath,resourcesPath}from"@stacksjs/path";import{join}from"node:path";import{inlineCss,shouldInlineByDefault}from"./css-inliner";export class SafeHtml{value;__safeHtml=!0;constructor(value){this.value=value}}export function safe(html){return new SafeHtml(html)}function getDefaultVariables(){const primaryColor=config.app.primaryColor||"#3b82f6";return{appName:config.app.name||"Stacks",appUrl:config.app.url||"https://localhost",primaryColor,primaryColorDark:darkenColor(primaryColor,15),year:new Date().getFullYear()}}function darkenColor(hex,percent){const num=Number.parseInt(hex.replace("#",""),16),amt=Math.round(2.55*percent),R=Math.max(0,(num>>16)-amt),G=Math.max(0,(num>>8&255)-amt),B=Math.max(0,(num&255)-amt);return`#${(16777216+R*65536+G*256+B).toString(16).slice(1)}`}export function escapeHtml(input){return input.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function replaceVariables(html,variables){let result=html;for(const[key,value]of Object.entries(variables)){const escapedKey=key.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),regex=new RegExp(`\\{\\{\\s*${escapedKey}\\s*\\}\\}`,"g");result=result.replace(regex,renderTemplateValue(value))}return result}function renderTemplateValue(value){if(value===null||value===void 0)return"";if(value instanceof SafeHtml)return value.value;return escapeHtml(String(value))}const templateRoots=[(relativePath)=>resourcesPath(join("emails",relativePath)),(relativePath)=>defaultsResourcesPath(join("emails",relativePath))];function resolveTemplatePath(templateName){for(const root of templateRoots){if(templateName.endsWith(".stx")){const fullPath=root(templateName);if(fs.existsSync(fullPath))return{path:fullPath,type:"stx"};continue}if(templateName.endsWith(".html")){const fullPath=root(templateName);if(fs.existsSync(fullPath))return{path:fullPath,type:"html"};continue}const stxPath=root(`${templateName}.stx`);if(fs.existsSync(stxPath))return{path:stxPath,type:"stx"};const htmlPath=root(`${templateName}.html`);if(fs.existsSync(htmlPath))return{path:htmlPath,type:"html"}}return null}function loadHtmlTemplate(templatePath){const path=templatePath.endsWith(".html")?templatePath:`${templatePath}.html`;for(const root of templateRoots){const fullPath=root(path);if(fs.existsSync(fullPath))return fs.readFileSync(fullPath,"utf-8")}return null}function loadLayout(layoutName){return loadHtmlTemplate(`layouts/${layoutName}`)}function htmlToText(html){return html.replace(/<br\s*\/?>/gi,`
|
|
1
|
+
var {require}=import.meta;import{config}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{fs}from"@stacksjs/storage";import{defaultsResourcesPath,resourcesPath,storagePath}from"@stacksjs/path";import{join,resolve as resolvePath}from"node:path";import{inlineCss,shouldInlineByDefault}from"./css-inliner";export class SafeHtml{value;__safeHtml=!0;constructor(value){this.value=value}}export function safe(html){return new SafeHtml(html)}function getDefaultVariables(){const primaryColor=config.app.primaryColor||"#3b82f6";return{appName:config.app.name||"Stacks",appUrl:config.app.url||"https://localhost",primaryColor,primaryColorDark:darkenColor(primaryColor,15),year:new Date().getFullYear()}}function darkenColor(hex,percent){const num=Number.parseInt(hex.replace("#",""),16),amt=Math.round(2.55*percent),R=Math.max(0,(num>>16)-amt),G=Math.max(0,(num>>8&255)-amt),B=Math.max(0,(num&255)-amt);return`#${(16777216+R*65536+G*256+B).toString(16).slice(1)}`}export function escapeHtml(input){return input.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function replaceVariables(html,variables){let result=html;for(const[key,value]of Object.entries(variables)){const escapedKey=key.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),regex=new RegExp(`\\{\\{\\s*${escapedKey}\\s*\\}\\}`,"g");result=result.replace(regex,renderTemplateValue(value))}return result}function renderTemplateValue(value){if(value===null||value===void 0)return"";if(value instanceof SafeHtml)return value.value;return escapeHtml(String(value))}const templateRoots=[(relativePath)=>resourcesPath(join("emails",relativePath)),(relativePath)=>defaultsResourcesPath(join("emails",relativePath))];let templateRegistry=null,templateRegistryLoaded=!1;function loadTemplateRegistry(){if(templateRegistryLoaded)return templateRegistry;templateRegistryLoaded=!0;try{const dir=storagePath("framework/auto-imports"),module=require(`${dir}/emails.ts`);if(!module.emails)return null;templateRegistry=Object.fromEntries(Object.entries(module.emails).map(([name,file])=>[name,{path:resolvePath(dir,file),type:file.endsWith(".stx")?"stx":"html"}]))}catch{templateRegistry=null}return templateRegistry}export function resetTemplateRegistry(){templateRegistry=null;templateRegistryLoaded=!1}function resolveTemplatePath(templateName){const registry=loadTemplateRegistry();if(registry){const bare=templateName.replace(/\.(?:stx|html)$/,""),hit=registry[bare];if(hit){if(templateName.endsWith(".stx")&&hit.type!=="stx");else if(templateName.endsWith(".html")&&hit.type!=="html");else if(fs.existsSync(hit.path))return hit}}for(const root of templateRoots){if(templateName.endsWith(".stx")){const fullPath=root(templateName);if(fs.existsSync(fullPath))return{path:fullPath,type:"stx"};continue}if(templateName.endsWith(".html")){const fullPath=root(templateName);if(fs.existsSync(fullPath))return{path:fullPath,type:"html"};continue}const stxPath=root(`${templateName}.stx`);if(fs.existsSync(stxPath))return{path:stxPath,type:"stx"};const htmlPath=root(`${templateName}.html`);if(fs.existsSync(htmlPath))return{path:htmlPath,type:"html"}}return null}function loadHtmlTemplate(templatePath){const path=templatePath.endsWith(".html")?templatePath:`${templatePath}.html`;for(const root of templateRoots){const fullPath=root(path);if(fs.existsSync(fullPath))return fs.readFileSync(fullPath,"utf-8")}return null}function loadLayout(layoutName){return loadHtmlTemplate(`layouts/${layoutName}`)}function htmlToText(html){return html.replace(/<br\s*\/?>/gi,`
|
|
2
2
|
`).replace(/<\/(p|div|h[1-6]|li|tr)>/gi,`
|
|
3
3
|
`).replace(/<\/td>/gi,"\t").replace(/<[^>]*>/g,"").replace(/ /g," ").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'/g,"'").replace(/©/g,"(c)").replace(/\n\s*\n\s*\n/g,`
|
|
4
4
|
|
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.
|
|
5
|
+
"version": "0.72.99",
|
|
6
6
|
"description": "The Stacks Email integration. Painlessly create & manage your inboxes, templates, and send emails.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -64,10 +64,10 @@
|
|
|
64
64
|
"postal-mime": "^2.7.6"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
|
-
"@stacksjs/cli": "0.72.
|
|
68
|
-
"@stacksjs/config": "0.72.
|
|
67
|
+
"@stacksjs/cli": "0.72.99",
|
|
68
|
+
"@stacksjs/config": "0.72.99",
|
|
69
69
|
"better-dx": "^0.2.24",
|
|
70
|
-
"@stacksjs/error-handling": "0.72.
|
|
71
|
-
"@stacksjs/types": "0.72.
|
|
70
|
+
"@stacksjs/error-handling": "0.72.99",
|
|
71
|
+
"@stacksjs/types": "0.72.99"
|
|
72
72
|
}
|
|
73
73
|
}
|