@stacksjs/email 0.70.23 → 0.70.26

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,6 @@
1
+ export * as log from './log';
2
+ export * as mailgun from './mailgun';
3
+ export * as mailtrap from './mailtrap';
4
+ export * as nodemailer from './nodemailer';
5
+ export * as sendgrid from './sendgrid';
6
+ export * as ses from './ses';
@@ -0,0 +1,30 @@
1
+ import { BaseEmailDriver } from './base';
2
+ import type { EmailMessage, EmailResult } from '@stacksjs/types';
3
+ import type { TemplateOptions } from '../template';
4
+ declare const captured: CapturedEmail[];
5
+ /**
6
+ * Local-only email driver that never opens a network socket. Renders
7
+ * the message to disk so devs can inspect it (and tests can read it),
8
+ * and remembers the last N sends in-memory so tests can assert against
9
+ * them without scraping log output.
10
+ *
11
+ * Pick this driver when:
12
+ * - running tests (no SMTP credentials, deterministic output)
13
+ * - local development (no AWS/SendGrid setup, no mailbox spam)
14
+ * - CI smoke tests where we want to assert "an email was sent"
15
+ *
16
+ * In production, use `ses` / `sendgrid` / `mailgun` / `smtp` instead.
17
+ */
18
+ declare interface CapturedEmail extends EmailMessage {
19
+ sentAt: Date
20
+ rendered?: { html?: string, text?: string }
21
+ }
22
+ export declare class LogEmailDriver extends BaseEmailDriver {
23
+ name: string;
24
+ send(message: EmailMessage, options?: TemplateOptions): Promise<EmailResult>;
25
+ static captured(): readonly CapturedEmail[];
26
+ static reset(): void;
27
+ }
28
+ // Convenience export to mirror the other drivers' module shape — the
29
+ // drivers/index.ts re-exports each driver namespace (`export * as log`).
30
+ export default LogEmailDriver;
@@ -0,0 +1,7 @@
1
+ import { BaseEmailDriver } from './base';
2
+ import type { EmailMessage, EmailResult } from '@stacksjs/types';
3
+ import type { TemplateOptions } from '../template';
4
+ export declare class MailgunDriver extends BaseEmailDriver {
5
+ name: string;
6
+ send(message: EmailMessage, options?: TemplateOptions): Promise<EmailResult>;
7
+ }
@@ -0,0 +1,8 @@
1
+ import { BaseEmailDriver } from './base';
2
+ import type { EmailMessage, EmailResult } from '@stacksjs/types';
3
+ import type { TemplateOptions } from '../template';
4
+ export declare class MailtrapDriver extends BaseEmailDriver {
5
+ name: string;
6
+ send(message: EmailMessage, options?: TemplateOptions): Promise<EmailResult>;
7
+ }
8
+ export default MailtrapDriver;
@@ -0,0 +1,3 @@
1
+ export declare class NodemailerDriver {
2
+ send(): Promise<void>;
3
+ }
@@ -0,0 +1,8 @@
1
+ import { BaseEmailDriver } from './base';
2
+ import type { EmailMessage, EmailResult } from '@stacksjs/types';
3
+ import type { TemplateOptions } from '../template';
4
+ export declare class SendGridDriver extends BaseEmailDriver {
5
+ name: string;
6
+ send(message: EmailMessage, options?: TemplateOptions): Promise<EmailResult>;
7
+ }
8
+ export default SendGridDriver;
@@ -0,0 +1,9 @@
1
+ import { BaseEmailDriver } from './base';
2
+ import type { EmailMessage, EmailResult } from '@stacksjs/types';
3
+ import type { TemplateOptions } from '../template';
4
+ export declare class SESDriver extends BaseEmailDriver {
5
+ name: string;
6
+ send(message: EmailMessage, options?: TemplateOptions): Promise<EmailResult>;
7
+ protected formatAddresses(addresses: string | string[] | { address: string, name?: string }[] | undefined): string[];
8
+ }
9
+ export default SESDriver;
@@ -0,0 +1,39 @@
1
+ import type { EmailMessage, EmailResult } from '@stacksjs/types';
2
+ import type { Message } from './types';
3
+ export declare const mail: Mail;
4
+ /** Result returned by email handler callbacks */
5
+ declare interface EmailHandlerResult {
6
+ message: string
7
+ }
8
+ /** Configuration for the sender address */
9
+ declare interface EmailFromAddress {
10
+ name: string
11
+ address: string
12
+ }
13
+ /** Configuration for the Mail singleton */
14
+ declare interface MailConfig {
15
+ defaultDriver?: string
16
+ }
17
+ /**
18
+ * Email notification class for defining email notifications
19
+ */
20
+ export declare class Email {
21
+ name: string;
22
+ subject: string;
23
+ to: string | string[];
24
+ from?: EmailFromAddress;
25
+ template: string;
26
+ handle?: () => Promise<EmailHandlerResult>;
27
+ onError?: (error: Error) => Promise<EmailHandlerResult>;
28
+ onSuccess?: () => void;
29
+ constructor(options: Message);
30
+ send(to?: string | string[]): Promise<EmailHandlerResult>;
31
+ }
32
+ declare class Mail {
33
+ constructor(options?: MailConfig);
34
+ send(message: EmailMessage): Promise<EmailResult>;
35
+ use(driver: string): Mail;
36
+ queue(message: EmailMessage): Promise<void>;
37
+ later(delaySeconds: number, message: EmailMessage): Promise<void>;
38
+ queueOn(queueName: string, message: EmailMessage): Promise<void>;
39
+ }
@@ -0,0 +1,5 @@
1
+ export * from './drivers';
2
+ export * from './email';
3
+ export * from './mailable';
4
+ export * from './template';
5
+ export * from './types';
@@ -0,0 +1,81 @@
1
+ import type { EmailAddress, EmailAttachment, EmailResult } from '@stacksjs/types';
2
+ /**
3
+ * Options accepted by {@link Mailable.send} — currently allows scoping
4
+ * the send to a specific driver registered on the Mail singleton (e.g.
5
+ * `'log'` to swallow the email in a test, `'ses'` to force production
6
+ * delivery during a one-off backfill).
7
+ */
8
+ export declare interface MailableSendOptions {
9
+ driver?: string
10
+ }
11
+ /**
12
+ * Internal stash for template rendering — populated by {@link Mailable.template}
13
+ * and consumed in {@link Mailable.send} after `build()` resolves.
14
+ */
15
+ declare interface TemplateRef {
16
+ name: string
17
+ props: Record<string, unknown>
18
+ }
19
+ /**
20
+ * Allowed recipient input — accepts a single address, an array of addresses,
21
+ * or already-shaped {@link EmailAddress} objects. Strings are treated as
22
+ * `address` only (no display name).
23
+ */
24
+ export type MailableAddressInput = string | string[] | EmailAddress | EmailAddress[];
25
+ /**
26
+ * Laravel-style class-based email definition. Subclass `Mailable`,
27
+ * implement `build()`, and call `.send()` to dispatch.
28
+ *
29
+ * Compared to the existing function-form `Email` / direct `mail.send()`
30
+ * APIs this gives you:
31
+ * - encapsulation of recipient/subject/body building per email type
32
+ * - chainable, immutable-feeling fluent setters
33
+ * - a single hook (`build`) where view-model -> message translation happens
34
+ * - automatic STX template rendering via the existing `template()` helper
35
+ *
36
+ * The class still ultimately routes through the same `mail` singleton, so
37
+ * configured drivers, queueing, and `from` defaults all behave identically.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * import { Mailable } from '@stacksjs/email'
42
+ *
43
+ * export default class WelcomeMail extends Mailable {
44
+ * constructor(private user: { name: string, email: string }) { super() }
45
+ *
46
+ * build() {
47
+ * return this
48
+ * .to(this.user.email)
49
+ * .subject('Welcome!')
50
+ * .template('welcome', { name: this.user.name })
51
+ * }
52
+ * }
53
+ *
54
+ * await new WelcomeMail(user).send()
55
+ * ```
56
+ */
57
+ export declare abstract class Mailable {
58
+ protected _to: string[] | EmailAddress[];
59
+ protected _cc: string[] | EmailAddress[];
60
+ protected _bcc: string[] | EmailAddress[];
61
+ protected _replyTo?: EmailAddress;
62
+ protected _from?: EmailAddress;
63
+ protected _subject?: string;
64
+ protected _text?: string;
65
+ protected _html?: string;
66
+ protected _template?: TemplateRef;
67
+ protected _attachments: EmailAttachment[];
68
+ abstract build(): this | Promise<this>;
69
+ to(address: MailableAddressInput): this;
70
+ cc(address: MailableAddressInput): this;
71
+ bcc(address: MailableAddressInput): this;
72
+ replyTo(address: string | EmailAddress): this;
73
+ from(addr: EmailAddress): this;
74
+ subject(s: string): this;
75
+ text(body: string): this;
76
+ html(body: string): this;
77
+ template(name: string, props?: Record<string, unknown>): this;
78
+ attach(path: string, name?: string): this;
79
+ attachData(buffer: Uint8Array | string, name: string, mime?: string): this;
80
+ send(options?: MailableSendOptions): Promise<EmailResult>;
81
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Render an email template with optional layout
3
+ *
4
+ * Supports both .stx and .html templates. When a .stx template
5
+ * is found, it uses the STX engine for rendering (with directives,
6
+ * server scripts, etc.). When an .html template is found, it uses
7
+ * simple {{ variable }} replacement with layout wrapping.
8
+ *
9
+ * .stx templates are preferred over .html when both exist.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * // STX template (resources/emails/welcome.stx)
14
+ * const { html, text } = await template('welcome', {
15
+ * variables: { userName: 'John' }
16
+ * })
17
+ *
18
+ * // HTML template with layout
19
+ * const { html, text } = await template('notification', {
20
+ * layout: 'base',
21
+ * variables: { message: 'Hello' }
22
+ * })
23
+ *
24
+ * // Without layout (HTML templates only)
25
+ * const { html, text } = await template('simple', {
26
+ * layout: false
27
+ * })
28
+ * ```
29
+ */
30
+ export declare function template(templateName: string, options?: TemplateOptions): Promise<TemplateResult>;
31
+ /**
32
+ * Render a raw HTML string with variables (no file loading)
33
+ */
34
+ export declare function renderHtml(htmlContent: string, variables?: TemplateVariables): TemplateResult;
35
+ /**
36
+ * Check if a template exists (.stx or .html)
37
+ */
38
+ export declare function templateExists(templateName: string): boolean;
39
+ /**
40
+ * List available templates (.stx and .html)
41
+ */
42
+ export declare function listTemplates(): string[];
43
+ export declare interface TemplateResult {
44
+ html: string
45
+ text: string
46
+ }
47
+ export declare interface TemplateOptions {
48
+ variables?: TemplateVariables
49
+ layout?: string | false
50
+ subject?: string
51
+ }
52
+ /** Allowed types for email template variable values */
53
+ export type TemplateVariableValue = string | number | boolean | undefined | null;
54
+ /** Map of variable names to their values for template replacement */
55
+ export type TemplateVariables = Record<string, TemplateVariableValue>;
@@ -1,13 +1,13 @@
1
1
  export declare interface Message {
2
2
  name: string
3
3
  subject: string
4
- to: string
4
+ to: string | string[]
5
5
  from?: {
6
6
  name: string
7
7
  address: string
8
8
  }
9
9
  template: string
10
- handle?: () => Promise<{ message: string }>
10
+ handle?: () => Promise<{ message: string }>
11
11
  onError?: (error: Error) => Promise<{ message: string }>
12
12
  onSuccess?: () => void
13
13
  }
@@ -34,4 +34,4 @@ export declare interface EmailParams {
34
34
  from: string
35
35
  subject: string
36
36
  html: string
37
- }
37
+ }
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "@stacksjs/email",
3
3
  "type": "module",
4
- "version": "0.70.23",
4
+ "version": "0.70.26",
5
5
  "description": "The Stacks Email integration. Painlessly create & manage your inboxes, templates, and send emails.",
6
6
  "author": "Chris Breuer",
7
- "contributors": ["Chris Breuer <chris@stacksjs.org>"],
7
+ "contributors": [
8
+ "Chris Breuer <chris@stacksjs.com>"
9
+ ],
8
10
  "license": "MIT",
9
11
  "funding": "https://github.com/sponsors/chrisbbreuer",
10
12
  "homepage": "https://github.com/stacksjs/stacks/tree/main/storage/framework/core/email#readme",
@@ -16,31 +18,45 @@
16
18
  "bugs": {
17
19
  "url": "https://github.com/stacksjs/stacks/issues"
18
20
  },
19
- "keywords": ["email", "ses", "aws", "stacks", "emailjs", "mailgun"],
21
+ "keywords": [
22
+ "email",
23
+ "ses",
24
+ "aws",
25
+ "stacks",
26
+ "emailjs",
27
+ "mailgun"
28
+ ],
20
29
  "exports": {
21
30
  ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "bun": "./src/index.ts",
22
33
  "import": "./dist/index.js"
23
34
  },
24
35
  "./*": {
36
+ "bun": "./src/*",
25
37
  "import": "./dist/*"
26
38
  }
27
39
  },
28
40
  "module": "dist/index.js",
29
41
  "types": "dist/index.d.ts",
30
- "files": ["README.md", "dist"],
42
+ "files": [
43
+ "README.md",
44
+ "dist"
45
+ ],
31
46
  "scripts": {
32
47
  "build": "bun build.ts",
33
48
  "build:inbound": "bun build-inbound.ts",
34
49
  "typecheck": "bun tsc --noEmit",
35
50
  "prepublishOnly": "bun run build"
36
51
  },
52
+ "dependencies": {
53
+ "@stacksjs/ts-cloud": "^0.2.15"
54
+ },
37
55
  "devDependencies": {
38
- "@stacksjs/cli": "0.70.22",
39
- "@stacksjs/config": "0.70.22",
40
- "@stacksjs/development": "0.70.22",
41
- "@stacksjs/error-handling": "0.70.22",
42
- "@stacksjs/types": "0.70.22",
43
- "aws-sdk": "^2.1692.0",
44
- "vue-email": "^0.8.11"
56
+ "@stacksjs/cli": "0.70.23",
57
+ "@stacksjs/config": "0.70.23",
58
+ "better-dx": "^0.2.12",
59
+ "@stacksjs/error-handling": "0.70.23",
60
+ "@stacksjs/types": "0.70.23"
45
61
  }
46
62
  }
package/dist/base.d.ts DELETED
@@ -1,130 +0,0 @@
1
- import type { EmailDriver, EmailDriverConfig, EmailMessage, EmailResult, RenderOptions } from '@stacksjs/types';
2
-
3
- export declare abstract class BaseEmailDriver implements EmailDriver {
4
- public abstract name: string
5
- protected config: Required<EmailDriverConfig>
6
-
7
- constructor(config?: EmailDriverConfig) {
8
- this.config = {
9
- maxRetries: config?.maxRetries || 3,
10
- retryTimeout: config?.retryTimeout || 1000,
11
- ...config,
12
- }
13
- }
14
-
15
- public configure(config: EmailDriverConfig): void {
16
- this.config = { ...this.config, ...config }
17
- }
18
-
19
- public abstract send(message: EmailMessage, options?: RenderOptions): Promise<EmailResult>
20
-
21
- protected validateMessage(message: EmailMessage): boolean {
22
- if (!message.from?.address) {
23
- throw new Error('Email sender address is required')
24
- }
25
-
26
- if (!message.to || (Array.isArray(message.to) && message.to.length === 0)) {
27
- throw new Error('At least one recipient is required')
28
- }
29
-
30
- if (!message.subject) {
31
- throw new Error('Email subject is required')
32
- }
33
-
34
- return true
35
- }
36
-
37
- protected formatAddresses(addresses: string | string[] | EmailAddress[] | undefined): string[] {
38
- if (!addresses)
39
- return []
40
-
41
- if (typeof addresses === 'string') {
42
- return [addresses]
43
- }
44
-
45
- return addresses.map((addr) => {
46
- if (typeof addr === 'string')
47
- return addr
48
- return addr.name ? `${addr.name} <${addr.address}>` : addr.address
49
- })
50
- }
51
-
52
- protected async handleError(error: unknown, message: EmailMessage): Promise<EmailResult> {
53
- const err = error instanceof Error ? error : new Error(String(error))
54
-
55
- log.error(`[${this.name}] Email sending failed`, {
56
- error: err.message,
57
- stack: err.stack,
58
- to: message.to,
59
- subject: message.subject,
60
- })
61
-
62
- let result: EmailResult = {
63
- message: `Email sending failed: ${err.message}`,
64
- success: false,
65
- provider: this.name,
66
- }
67
-
68
- if (message.onError) {
69
- const customResult = message.onError(err)
70
- const handlerResult = customResult instanceof Promise
71
- ? await customResult
72
- : customResult
73
-
74
- result = {
75
- ...result,
76
- ...handlerResult,
77
- success: false,
78
- provider: this.name,
79
- }
80
- }
81
-
82
- return result
83
- }
84
-
85
- protected async handleSuccess(message: EmailMessage, messageId?: string): Promise<EmailResult> {
86
- let result: EmailResult = {
87
- message: 'Email sent successfully',
88
- success: true,
89
- provider: this.name,
90
- messageId,
91
- }
92
-
93
- try {
94
- if (message.handle) {
95
- const customResult = message.handle()
96
- const handlerResult = customResult instanceof Promise
97
- ? await customResult
98
- : customResult
99
-
100
- result = {
101
- ...result,
102
- ...handlerResult,
103
- success: true,
104
- provider: this.name,
105
- messageId,
106
- }
107
- }
108
-
109
- if (message.onSuccess) {
110
- const successResult = message.onSuccess()
111
- const handlerResult = successResult instanceof Promise
112
- ? await successResult
113
- : successResult
114
-
115
- result = {
116
- ...result,
117
- ...handlerResult,
118
- success: true,
119
- provider: this.name,
120
- messageId,
121
- }
122
- }
123
- }
124
- catch (error) {
125
- return this.handleError(error, message)
126
- }
127
-
128
- return result
129
- }
130
- }
package/dist/config.d.ts DELETED
@@ -1,3 +0,0 @@
1
- export declare const email: typeof notification.email;
2
-
3
- export default email;
package/dist/email.d.ts DELETED
@@ -1,2 +0,0 @@
1
- declare const driver: unknown;
2
- export declare const mail: Mail;
package/dist/index.d.ts DELETED
@@ -1,3 +0,0 @@
1
- export * from './drivers'
2
- export * from './email'
3
- export * from './types'
package/dist/mailgun.d.ts DELETED
@@ -1,141 +0,0 @@
1
- import type { EmailAddress, EmailMessage, EmailResult, RenderOptions } from '@stacksjs/types';
2
-
3
- export declare class MailgunDriver extends BaseEmailDriver {
4
- public name = 'mailgun'
5
- private apiKey: string
6
- private domain: string
7
- private endpoint: string
8
-
9
- constructor() {
10
- super()
11
- this.apiKey = config.services.mailgun?.apiKey ?? ''
12
- this.domain = config.services.mailgun?.domain ?? ''
13
- this.endpoint = config.services.mailgun?.endpoint ?? 'api.mailgun.net'
14
- }
15
-
16
- public async send(message: EmailMessage, options?: RenderOptions): Promise<EmailResult> {
17
- const logContext = {
18
- provider: this.name,
19
- to: message.to,
20
- subject: message.subject,
21
- domain: this.domain,
22
- }
23
-
24
- log.info('Sending email via Mailgun...', logContext)
25
-
26
- try {
27
- this.validateMessage(message)
28
-
29
- let htmlContent: string | undefined
30
- if (message.template) {
31
- const templ = await template(message.template, options)
32
- if (templ && 'html' in templ) {
33
- htmlContent = templ.html
34
- }
35
- }
36
-
37
- const formData = new FormData()
38
- formData.append('from', this.formatMailgunAddress(message.from))
39
-
40
- this.formatMailgunAddresses(message.to).forEach(to => formData.append('to', to))
41
-
42
- if (message.cc)
43
- this.formatMailgunAddresses(message.cc).forEach(cc => formData.append('cc', cc))
44
-
45
- if (message.bcc)
46
- this.formatMailgunAddresses(message.bcc).forEach(bcc => formData.append('bcc', bcc))
47
-
48
- formData.append('subject', message.subject)
49
-
50
- if (htmlContent) {
51
- formData.append('html', htmlContent)
52
- }
53
-
54
- if (message.text)
55
- formData.append('text', message.text)
56
-
57
- if (message.attachments) {
58
- message.attachments.forEach((attachment) => {
59
- const content = typeof attachment.content === 'string'
60
- ? attachment.content
61
- : this.arrayBufferToBase64(attachment.content)
62
-
63
- formData.append('attachment', new Blob([content], { type: attachment.contentType }), attachment.filename)
64
- })
65
- }
66
-
67
- const response = await this.sendWithRetry(formData)
68
- return this.handleSuccess(message, response.id)
69
- }
70
- catch (error) {
71
- return this.handleError(error, message)
72
- }
73
- }
74
-
75
- private formatMailgunAddress(address: EmailAddress): string {
76
- return address.name ? `${address.name} <${address.address}>` : address.address
77
- }
78
-
79
- private formatMailgunAddresses(addresses: string | string[] | EmailAddress[] | undefined): string[] {
80
- if (!addresses)
81
- return []
82
-
83
- if (typeof addresses === 'string')
84
- return [addresses]
85
-
86
- return addresses.map((addr) => {
87
- if (typeof addr === 'string')
88
- return addr
89
- return addr.name ? `${addr.name} <${addr.address}>` : addr.address
90
- })
91
- }
92
-
93
- private arrayBufferToBase64(buffer: Uint8Array): string {
94
- let binary = ''
95
- const bytes = new Uint8Array(buffer)
96
- const len = bytes.byteLength
97
-
98
- for (let i = 0; i < len; i++) {
99
- binary += String.fromCharCode(bytes[i])
100
- }
101
-
102
- return typeof btoa === 'function'
103
- ? btoa(binary)
104
- : Buffer.from(binary).toString('base64')
105
- }
106
-
107
- private async sendWithRetry(formData: FormData, attempt = 1): Promise<any> {
108
- const url = `https:
109
- const auth = Buffer.from(`api:${this.apiKey}`).toString('base64')
110
-
111
- try {
112
- const response = await fetch(url, {
113
- method: 'POST',
114
- headers: {
115
- Authorization: `Basic ${auth}`,
116
- },
117
- body: formData,
118
- })
119
-
120
- if (!response.ok) {
121
- const errorData = await response.json()
122
- throw new Error(`Mailgun API error: ${response.status} - ${JSON.stringify(errorData)}`)
123
- }
124
-
125
- const data = await response.json()
126
- log.info(`[${this.name}] Email sent successfully`, { attempt, messageId: data.id })
127
- return data
128
- }
129
- catch (error) {
130
- if (attempt < (config.services.mailgun?.maxRetries ?? 3)) {
131
- const retryTimeout = config.services.mailgun?.retryTimeout ?? 1000
132
- log.warn(`[${this.name}] Email send failed, retrying (${attempt}/${config.services.mailgun?.maxRetries ?? 3})`)
133
- await new Promise(resolve => setTimeout(resolve, retryTimeout))
134
- return this.sendWithRetry(formData, attempt + 1)
135
- }
136
- throw error
137
- }
138
- }
139
- }
140
-
141
- export default MailgunDriver;