@stacksjs/email 0.74.36 → 0.74.38

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.
@@ -10,6 +10,12 @@ import type { TemplateOptions } from '../template';
10
10
  * does template rendering) — chosen by tests that just need to
11
11
  * assert "this flow sent that email" without any I/O side effect.
12
12
  *
13
+ * Import it from `@stacksjs/email`, not from
14
+ * `@stacksjs/email/drivers/capture`: in a workspace checkout the barrel
15
+ * resolves to `src` and the subpath to `dist`, which are two module graphs
16
+ * with two separate `captured` arrays. `@stacksjs/testing`'s `mailFake()`
17
+ * wraps all of this.
18
+ *
13
19
  * Pick this driver when:
14
20
  * - running fast unit tests that shouldn't touch the filesystem
15
21
  * - asserting on the exact message shape (subject/body/headers)
@@ -28,7 +34,7 @@ import type { TemplateOptions } from '../template';
28
34
  * config.email.default = 'capture'
29
35
  *
30
36
  * // in tests
31
- * import { CaptureEmailDriver } from '@stacksjs/email/drivers/capture'
37
+ * import { CaptureEmailDriver } from '@stacksjs/email'
32
38
  *
33
39
  * beforeEach(() => CaptureEmailDriver.clear())
34
40
  *
@@ -1,4 +1,11 @@
1
+ export type { CapturedMessage } from './capture';
1
2
  export * as capture from './capture';
3
+ // Also by name. The namespace form alone meant the only way to reach the class
4
+ // was `@stacksjs/email/drivers/capture`, and in a workspace checkout that
5
+ // subpath resolves to `dist` while the barrel resolves to `src` - two module
6
+ // graphs, two capture stores, so a test asserting on one saw nothing the app
7
+ // wrote to the other (stacksjs/stacks#2581).
8
+ export { CaptureEmailDriver } from './capture';
2
9
  export * as log from './log';
3
10
  export * as mailgun from './mailgun';
4
11
  export * as mailtrap from './mailtrap';
@@ -1 +1 @@
1
- export* as capture from"./capture";export* as log from"./log";export* as mailgun from"./mailgun";export* as mailtrap from"./mailtrap";export* as sendgrid from"./sendgrid";export* as ses from"./ses";
1
+ export* as capture from"./capture";export{CaptureEmailDriver}from"./capture";export* as log from"./log";export* as mailgun from"./mailgun";export* as mailtrap from"./mailtrap";export* as sendgrid from"./sendgrid";export* as ses from"./ses";
package/dist/email.d.ts CHANGED
@@ -38,6 +38,8 @@ export declare class Mail {
38
38
  send(message: EmailMessage): Promise<EmailResult>;
39
39
  sendOrFail(message: EmailMessage): Promise<EmailResult>;
40
40
  use(driver: string): Mail;
41
+ fake(): string;
42
+ restoreDriver(driver: string): void;
41
43
  queue(message: EmailMessage): Promise<void>;
42
44
  later(delaySeconds: number, message: EmailMessage): Promise<void>;
43
45
  queueOn(queueName: string, message: EmailMessage): Promise<void>;
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("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}});
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})}fake(){const previous=this.defaultDriver;this.defaultDriver="capture";return previous}restoreDriver(driver){this.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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/email",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.74.36",
5
+ "version": "0.74.38",
6
6
  "description": "The Stacks Email integration. Painlessly create & manage your inboxes, templates, and send emails.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -61,26 +61,26 @@
61
61
  "prepublishOnly": "bun run build"
62
62
  },
63
63
  "dependencies": {
64
- "@stacksjs/config": "0.74.36",
65
- "@stacksjs/database": "0.74.36",
66
- "@stacksjs/events": "0.74.36",
67
- "@stacksjs/logging": "0.74.36",
64
+ "@stacksjs/config": "0.74.38",
65
+ "@stacksjs/database": "0.74.38",
66
+ "@stacksjs/events": "0.74.38",
67
+ "@stacksjs/logging": "0.74.38",
68
68
  "@stacksjs/mail": "^0.3.6",
69
- "@stacksjs/path": "0.74.36",
70
- "@stacksjs/storage": "0.74.36",
71
- "@stacksjs/strings": "0.74.36",
69
+ "@stacksjs/path": "0.74.38",
70
+ "@stacksjs/storage": "0.74.38",
71
+ "@stacksjs/strings": "0.74.38",
72
72
  "@stacksjs/stx": "^0.2.274",
73
- "@stacksjs/ts-cloud": "^0.14.0",
74
- "@stacksjs/utils": "0.74.36"
73
+ "@stacksjs/ts-cloud": "^0.15.1",
74
+ "@stacksjs/utils": "0.74.38"
75
75
  },
76
76
  "devDependencies": {
77
- "@stacksjs/cli": "0.74.36",
78
- "@stacksjs/error-handling": "0.74.36",
79
- "@stacksjs/types": "0.74.36",
77
+ "@stacksjs/cli": "0.74.38",
78
+ "@stacksjs/error-handling": "0.74.38",
79
+ "@stacksjs/types": "0.74.38",
80
80
  "better-dx": "^0.2.24"
81
81
  },
82
82
  "peerDependencies": {
83
- "@stacksjs/queue": "0.74.36"
83
+ "@stacksjs/queue": "0.74.38"
84
84
  },
85
85
  "peerDependenciesMeta": {
86
86
  "@stacksjs/queue": {