@stacksjs/email 0.70.44 โ 0.70.53
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/LICENSE.md +21 -0
- package/README.md +65 -1
- package/dist/css-inliner.d.ts +26 -0
- package/dist/drivers/base.d.ts +1 -0
- package/dist/drivers/capture.d.ts +55 -0
- package/dist/drivers/index.d.ts +5 -1
- package/dist/drivers/ses.d.ts +0 -1
- package/dist/email.d.ts +1 -1
- package/dist/idempotency.d.ts +20 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +127 -9
- package/dist/mailable.d.ts +40 -5
- package/dist/mime.d.ts +37 -0
- package/dist/preview-ui.d.ts +11 -0
- package/dist/preview.d.ts +50 -0
- package/dist/suppression.d.ts +69 -0
- package/dist/template.d.ts +46 -2
- package/dist/unsubscribe.d.ts +39 -0
- package/dist/validation.d.ts +48 -0
- package/dist/webhook-dedup.d.ts +10 -0
- package/dist/webhook-events.d.ts +34 -0
- package/dist/webhook-handlers.d.ts +27 -0
- package/dist/webhook-signatures.d.ts +91 -0
- package/package.json +6 -6
- package/dist/drivers/nodemailer.d.ts +0 -3
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Open Web Foundation
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -111,11 +111,75 @@ EMAILJS*HOST=example.com
|
|
|
111
111
|
EMAILJS*USERNAME=username
|
|
112
112
|
EMAILJS*PASSWORD=password
|
|
113
113
|
EMAILJS*PORT=25
|
|
114
|
-
|
|
114
|
+
EMAILJS*SECURE=true
|
|
115
115
|
```
|
|
116
116
|
|
|
117
117
|
Learn more in the docs.
|
|
118
118
|
|
|
119
|
+
## ๐งฑ Email components (`<EmailLayout>` & co.)
|
|
120
|
+
|
|
121
|
+
Bundled stx components for composing Outlook / Gmail-safe HTML in
|
|
122
|
+
`resources/components/Email/`. Drop them into any `.stx` template;
|
|
123
|
+
the framework auto-discovers them when rendering through
|
|
124
|
+
`@stacksjs/email`'s `template()` helper.
|
|
125
|
+
|
|
126
|
+
| Component | Slot/content | Notable props |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
| `<EmailLayout>` | default + `head`/`header`/`footer` slots | `title`, `width` (px), `bodyBg`, `contentBg` |
|
|
129
|
+
| `<EmailSection>` | default | `padding`, `background` |
|
|
130
|
+
| `<EmailText>` | default | `size` (`sm`/`md`/`lg`/`heading`), `align`, `color`, `spacing` |
|
|
131
|
+
| `<EmailButton>` | default = button label | `href` (required), `color`, `bg`, `padX`, `padY` |
|
|
132
|
+
| `<EmailDivider>` | none | `color`, `spacing` |
|
|
133
|
+
| `<EmailImage>` | none | `src` (required), `alt` (required), `width`, `height`, `display` |
|
|
134
|
+
| `<EmailLink>` | default | `href` (required), `color`, `underline` |
|
|
135
|
+
|
|
136
|
+
Each one renders bulletproof table-based markup with inline styles
|
|
137
|
+
so it works in Outlook 2007-2019, Gmail (web + Android), and Apple
|
|
138
|
+
Mail without an inliner pass. Example:
|
|
139
|
+
|
|
140
|
+
```stx
|
|
141
|
+
<script server>
|
|
142
|
+
const userName = props.userName || 'there'
|
|
143
|
+
const appUrl = props.appUrl || 'https://stacksjs.com'
|
|
144
|
+
</script>
|
|
145
|
+
|
|
146
|
+
<EmailLayout title="Welcome">
|
|
147
|
+
<EmailText size="heading">Welcome aboard, {{ userName }}!</EmailText>
|
|
148
|
+
<EmailText>Thanks for signing up โ here's a button to get started:</EmailText>
|
|
149
|
+
<EmailButton href="{{ appUrl }}">Open the app</EmailButton>
|
|
150
|
+
<EmailDivider />
|
|
151
|
+
<EmailText size="sm" color="#6b7280" spacing="0">Reply to this email if you have questions.</EmailText>
|
|
152
|
+
</EmailLayout>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The bundled `welcome.stx`, `password-reset.stx`, `password-changed.stx`,
|
|
156
|
+
and `email-verification.stx` are written using these components โ they
|
|
157
|
+
double as worked examples. Copy + edit for app-specific designs.
|
|
158
|
+
|
|
159
|
+
## ๐จ CSS inlining
|
|
160
|
+
|
|
161
|
+
Gmail (especially the Android app) and older Outlook clients strip
|
|
162
|
+
or ignore `<style>` blocks, so styles have to ride on each element via
|
|
163
|
+
`style="โฆ"`. Stacks ships a pass-through inliner that runs after
|
|
164
|
+
`renderEmail()` and before the result hands back:
|
|
165
|
+
|
|
166
|
+
- **On by default in production** (`APP*ENV` or `NODE_ENV` is
|
|
167
|
+
`production`). Off in dev so previews show the un-mutated stx
|
|
168
|
+
output.
|
|
169
|
+
- **Per-call opt-out** via `template(name, { inline: false })`.
|
|
170
|
+
- **Per-block opt-out** via `<style data-inline="false">โฆ</style>` โ
|
|
171
|
+
those blocks pass through untouched. Useful for `@media`-queried
|
|
172
|
+
responsive rules that aren't inline-friendly anyway.
|
|
173
|
+
|
|
174
|
+
The inliner handles class / id / tag / chained selectors (`.btn`,
|
|
175
|
+
`#cta`, `p`, `a.btn`, `.btn.primary`). Selectors with descendant
|
|
176
|
+
combinators, pseudo-classes, or `@media` queries stay in a slimmed
|
|
177
|
+
`<style>` block so clients that DO support styles still pick them up.
|
|
178
|
+
|
|
179
|
+
The bundled `<EmailLayout>` & co. components are already inline-styled
|
|
180
|
+
by construction, so the inliner is mainly a safety net for userland
|
|
181
|
+
CSS โ drop a `<style>` block in your template and let it rip.
|
|
182
|
+
|
|
119
183
|
## ๐งช Testing
|
|
120
184
|
|
|
121
185
|
```bash
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline `<style>` blocks into per-element `style=""` attributes.
|
|
3
|
+
*
|
|
4
|
+
* Simple selectors (class `.x`, id `#x`, tag `p`, and chains like
|
|
5
|
+
* `p.foo` or `.a.b`) are inlined and removed from the source block.
|
|
6
|
+
* Rules that can't be safely inlined (`@media`, pseudo-classes,
|
|
7
|
+
* descendant / sibling combinators) stay in a slimmed-down `<style>`
|
|
8
|
+
* block so clients that DO honour styles can still apply them.
|
|
9
|
+
*
|
|
10
|
+
* Returns the HTML unchanged when `inline: false`. `<style
|
|
11
|
+
* data-inline="false">` blocks are passed through verbatim regardless
|
|
12
|
+
* of `inline`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function inlineCss(html: string, options?: InlineCssOptions): string;
|
|
15
|
+
/**
|
|
16
|
+
* Honour the global production-default. Apps that want explicit
|
|
17
|
+
* control should pass `inline` through {@link inlineCss} directly.
|
|
18
|
+
*/
|
|
19
|
+
export declare function shouldInlineByDefault(): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Options accepted by {@link inlineCss}.
|
|
22
|
+
*/
|
|
23
|
+
export declare interface InlineCssOptions {
|
|
24
|
+
inline?: boolean
|
|
25
|
+
important?: boolean
|
|
26
|
+
}
|
package/dist/drivers/base.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare abstract class BaseEmailDriver implements EmailDriver {
|
|
|
8
8
|
abstract send(message: EmailMessage, options?: TemplateOptions): Promise<EmailResult>;
|
|
9
9
|
protected validateMessage(message: EmailMessage): boolean;
|
|
10
10
|
protected formatAddresses(addresses: string | string[] | EmailAddress[] | undefined): string[];
|
|
11
|
+
protected formatAddressList(value: string | string[] | EmailAddress | EmailAddress[] | undefined): string[];
|
|
11
12
|
protected handleError(error: unknown, message: EmailMessage): Promise<EmailResult>;
|
|
12
13
|
protected handleSuccess(message: EmailMessage, messageId?: string): Promise<EmailResult>;
|
|
13
14
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { BaseEmailDriver } from './base';
|
|
2
|
+
import type { EmailMessage, EmailResult } from '@stacksjs/types';
|
|
3
|
+
import type { TemplateOptions } from '../template';
|
|
4
|
+
/**
|
|
5
|
+
* Test-only capture driver (stacksjs/stacks#1871 M-12).
|
|
6
|
+
*
|
|
7
|
+
* Records every `mail.send(...)` payload in memory without opening
|
|
8
|
+
* a network socket or writing to disk. Lighter-weight than the `log`
|
|
9
|
+
* driver (which writes inspection files to `storage/logs/mail/` and
|
|
10
|
+
* does template rendering) โ chosen by tests that just need to
|
|
11
|
+
* assert "this flow sent that email" without any I/O side effect.
|
|
12
|
+
*
|
|
13
|
+
* Pick this driver when:
|
|
14
|
+
* - running fast unit tests that shouldn't touch the filesystem
|
|
15
|
+
* - asserting on the exact message shape (subject/body/headers)
|
|
16
|
+
* before any driver-specific transformation
|
|
17
|
+
* - writing tests that mutate captured state and need
|
|
18
|
+
* `CaptureEmailDriver.clear()` between cases
|
|
19
|
+
*
|
|
20
|
+
* The `log` driver remains the better fit for local dev (because
|
|
21
|
+
* the disk dump makes inspection easy) and for CI smoke tests that
|
|
22
|
+
* want to surface a render failure visibly. Capture is for unit
|
|
23
|
+
* tests.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* // bunfig.toml or test setup
|
|
28
|
+
* config.email.default = 'capture'
|
|
29
|
+
*
|
|
30
|
+
* // in tests
|
|
31
|
+
* import { CaptureEmailDriver } from '@stacksjs/email/drivers/capture'
|
|
32
|
+
*
|
|
33
|
+
* beforeEach(() => CaptureEmailDriver.clear())
|
|
34
|
+
*
|
|
35
|
+
* test('signup sends welcome', async () => {
|
|
36
|
+
* await POST('/api/signup', { email: 'a@b.com' })
|
|
37
|
+
* const sent = CaptureEmailDriver.all()
|
|
38
|
+
* expect(sent).toHaveLength(1)
|
|
39
|
+
* expect(sent[0].subject).toContain('Welcome')
|
|
40
|
+
* expect(CaptureEmailDriver.last()?.to).toBe('a@b.com')
|
|
41
|
+
* })
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare interface CapturedMessage extends EmailMessage {
|
|
45
|
+
sentAt: Date
|
|
46
|
+
messageId: string
|
|
47
|
+
}
|
|
48
|
+
export declare class CaptureEmailDriver extends BaseEmailDriver {
|
|
49
|
+
name: string;
|
|
50
|
+
send(message: EmailMessage, _options?: TemplateOptions): Promise<EmailResult>;
|
|
51
|
+
static all(): readonly CapturedMessage[];
|
|
52
|
+
static last(): CapturedMessage | undefined;
|
|
53
|
+
static count(): number;
|
|
54
|
+
static clear(): void;
|
|
55
|
+
}
|
package/dist/drivers/index.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
export * as capture from './capture';
|
|
1
2
|
export * as log from './log';
|
|
2
3
|
export * as mailgun from './mailgun';
|
|
3
4
|
export * as mailtrap from './mailtrap';
|
|
4
|
-
|
|
5
|
+
// `nodemailer` driver removed โ it was a throwing stub that surfaced as a
|
|
6
|
+
// runtime crash on `mail.send()` only after a user had already wired it
|
|
7
|
+
// into config. Use the SMTP driver (`smtp` in MAIL_MAILER) for SMTP-based
|
|
8
|
+
// providers; see stacksjs/stacks#1871 M-7.
|
|
5
9
|
export * as sendgrid from './sendgrid';
|
|
6
10
|
export * as ses from './ses';
|
package/dist/drivers/ses.d.ts
CHANGED
|
@@ -4,6 +4,5 @@ import type { TemplateOptions } from '../template';
|
|
|
4
4
|
export declare class SESDriver extends BaseEmailDriver {
|
|
5
5
|
name: string;
|
|
6
6
|
send(message: EmailMessage, options?: TemplateOptions): Promise<EmailResult>;
|
|
7
|
-
protected formatAddresses(addresses: string | string[] | { address: string, name?: string }[] | undefined): string[];
|
|
8
7
|
}
|
|
9
8
|
export default SESDriver;
|
package/dist/email.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export declare class Email {
|
|
|
29
29
|
constructor(options: Message);
|
|
30
30
|
send(to?: string | string[]): Promise<EmailHandlerResult>;
|
|
31
31
|
}
|
|
32
|
-
declare class Mail {
|
|
32
|
+
export declare class Mail {
|
|
33
33
|
constructor(options?: MailConfig);
|
|
34
34
|
send(message: EmailMessage): Promise<EmailResult>;
|
|
35
35
|
use(driver: string): Mail;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { EmailMessage, EmailResult } from '@stacksjs/types';
|
|
2
|
+
/**
|
|
3
|
+
* Look up a cached EmailResult by idempotency key. Returns the
|
|
4
|
+
* reconstructed result when this key has been seen before, `null`
|
|
5
|
+
* when it hasn't. Degrades to "always null" with a startup warn
|
|
6
|
+
* when the `email_idempotency` dedup table isn't migrated yet.
|
|
7
|
+
*/
|
|
8
|
+
export declare function findEmailByIdempotencyKey(key: string): Promise<EmailResult | null>;
|
|
9
|
+
/**
|
|
10
|
+
* Record a successful send under its idempotency key. No-op when
|
|
11
|
+
* the table doesn't exist (warn-once already fired from the lookup
|
|
12
|
+
* path) and when the result reports failure (failed sends shouldn't
|
|
13
|
+
* lock out retries).
|
|
14
|
+
*
|
|
15
|
+
* Collision (same key inserted concurrently) is intentionally
|
|
16
|
+
* swallowed: the row already exists, which is exactly what the
|
|
17
|
+
* lookup will return next time. Throwing here would mask an
|
|
18
|
+
* otherwise successful send.
|
|
19
|
+
*/
|
|
20
|
+
export declare function recordEmailIdempotency(key: string, message: EmailMessage, result: EmailResult): Promise<void>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
|
+
export type { InlineCssOptions } from './css-inliner';
|
|
2
|
+
export type { DiscoveredMailable, MailablePreview } from './preview';
|
|
1
3
|
export * from './drivers/index';
|
|
2
4
|
export * from './email';
|
|
5
|
+
export * from './idempotency';
|
|
6
|
+
export * from './suppression';
|
|
7
|
+
export * from './unsubscribe';
|
|
8
|
+
export * from './webhook-dedup';
|
|
9
|
+
export * from './webhook-events';
|
|
10
|
+
export * from './webhook-handlers';
|
|
11
|
+
export * from './webhook-signatures';
|
|
3
12
|
export * from './mailable';
|
|
4
13
|
export * from './template';
|
|
5
14
|
export * from './types';
|
|
15
|
+
export { inlineCss, shouldInlineByDefault } from './css-inliner';
|
|
16
|
+
// Dev-only Mailable preview server (stacksjs/stacks#1900 A3).
|
|
17
|
+
export {
|
|
18
|
+
discoverMailables,
|
|
19
|
+
loadSampleProps,
|
|
20
|
+
renderMailablePreview,
|
|
21
|
+
} from './preview';
|
|
22
|
+
export { renderIndexHtml, renderPreviewHtml } from './preview-ui';
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,132 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var
|
|
2
|
+
var yY=Object.defineProperty;var bY=(J)=>J;function vY(J,Y){this[J]=bY.bind(null,Y)}var v=(J,Y)=>{for(var Z in Y)yY(J,Z,{get:Y[Z],enumerable:!0,configurable:!0,set:vY.bind(Y,Z)})};var k=import.meta.require;var SJ={};v(SJ,{CaptureEmailDriver:()=>e});import{config as hY}from"@stacksjs/config";import{log as fY}from"@stacksjs/logging";var FJ=/^[^\s<>"\\\r\n\t]+@[^\s<>"\\\r\n\t]+$/;function MJ(J,Y){if(typeof J!=="string"||!FJ.test(J))throw Error(`Email ${Y} address is malformed or contains forbidden characters: ${JSON.stringify(J)}`)}function CJ(J){if(/[\r\n]/.test(J))throw Error("Email subject contains forbidden line break characters (CR/LF)")}function T(J){if(!J)return;let Y={};for(let[Z,X]of Object.entries(J)){if(typeof X!=="string")continue;if(/[\r\n]/.test(Z)||/[\r\n]/.test(X))continue;Y[Z]=X}return Object.keys(Y).length>0?Y:void 0}class H{config;constructor(J){this.config={maxRetries:J?.maxRetries||3,retryTimeout:J?.retryTimeout||1000,...J}}configure(J){this.config={...this.config,...J}}validateMessage(J){if(!J.from?.address&&!hY.email.from?.address)throw Error("Email sender address is required either in message or config");if(!J.to||Array.isArray(J.to)&&J.to.length===0)throw Error("At least one recipient is required");if(!J.subject)throw Error("Email subject is required");CJ(J.subject);let Y=(X,$)=>{if(!X)return;MJ(X,$)},Z=(X)=>{if(!X)return[];if(typeof X==="string")return[X];if(Array.isArray(X))return X.flatMap((G)=>typeof G==="string"?[G]:G?.address?[G.address]:[]);let $=X;return $.address?[$.address]:[]};if(J.from)Y(J.from.address,"from");for(let X of Z(J.to))Y(X,"to");for(let X of Z(J.cc))Y(X,"cc");for(let X of Z(J.bcc))Y(X,"bcc");return!0}formatAddresses(J){if(!J)return[];if(typeof J==="string")return[J];return J.map((Y)=>{if(typeof Y==="string")return Y;if(!Y.name)return Y.address;return`${/[",()<>[\]:;@\\]/.test(Y.name)?`"${Y.name.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")}"`:Y.name} <${Y.address}>`})}formatAddressList(J){if(!J)return[];if(Array.isArray(J))return this.formatAddresses(J);if(typeof J==="string")return this.formatAddresses(J);return this.formatAddresses([J])}async handleError(J,Y){let Z=J instanceof Error?J:Error(String(J));fY.error(`[${this.name}] Email sending failed`,{error:Z.message,stack:Z.stack,to:Y.to,subject:Y.subject});let X={message:`Email sending failed: ${Z.message}`,success:!1,provider:this.name};if(Y.onError){let $=Y.onError(Z),G=$ instanceof Promise?await $:$;X={...X,...G,success:!1,provider:this.name}}return X}async handleSuccess(J,Y){let Z={message:"Email sent successfully",success:!0,provider:this.name,messageId:Y};try{if(J.handle){let X=J.handle(),$=X instanceof Promise?await X:X;Z={...Z,...$,success:!0,provider:this.name,messageId:Y}}if(J.onSuccess){let X=J.onSuccess(),$=X instanceof Promise?await X:X;Z={...Z,...$,success:!0,provider:this.name,messageId:Y}}}catch(X){return this.handleError(X,J)}return Z}}var h=[],wJ=1;class e extends H{name="capture";async send(J,Y){try{this.validateMessage(J);let Z=new Date,X=`capture-${Z.getTime()}-${wJ++}`;return h.push({...J,sentAt:Z,messageId:X}),this.handleSuccess(J,X)}catch(Z){return this.handleError(Z,J)}}static all(){return h}static last(){return h[h.length-1]}static count(){return h.length}static clear(){h.length=0,wJ=1}}var oJ={};v(oJ,{default:()=>ZZ,LogEmailDriver:()=>g});import{mkdir as sY,writeFile as eY}from"fs/promises";import{join as gJ,resolve as dJ}from"path";import{log as lJ}from"@stacksjs/logging";import{config as VJ}from"@stacksjs/config";import{log as mY}from"@stacksjs/logging";import{fs as P}from"@stacksjs/storage";import{defaultsResourcesPath as vJ,resourcesPath as hJ}from"@stacksjs/path";import{join as qJ}from"path";function JJ(J,Y={}){let{inline:Z=!0,important:X=!0}=Y;if(!Z)return J;let $=J,G=[];$=$.replace(/<style\b[^>]*\bdata-inline=["']false["'][^>]*>[\s\S]*?<\/style>/gi,(W)=>{return G.push(W),`\x00STX_PASSTHROUGH_${G.length-1}\x00`});let K=[];if($=$.replace(/<style\b[^>]*>([\s\S]*?)<\/style>/gi,(W,z)=>{return K.push(z),""}),K.length===0)return kJ($,G);let Q=[],F=[];for(let W of K)for(let z of uY(W)){let{selector:V,declarations:q}=z;if(!V||!q)continue;if(yJ(V))Q.push({selector:V,decls:q});else F.push(`${V} { ${q.map((B)=>`${B.prop}: ${B.value};`).join(" ")} }`)}for(let{selector:W,decls:z}of Q)$=bJ($,W,z,X);if(F.length>0){let W=`<style>
|
|
3
|
+
${F.join(`
|
|
4
|
+
`)}
|
|
5
|
+
</style>`;$=/<\/head>/i.test($)?$.replace(/<\/head>/i,`${W}
|
|
6
|
+
</head>`):`${W}
|
|
7
|
+
${$}`}return kJ($,G)}function kJ(J,Y){if(Y.length===0)return J;return J.replace(/\u0000STX_PASSTHROUGH_(\d+)\u0000/g,(Z,X)=>{return Y[Number.parseInt(X,10)]??""})}function uY(J){let Y=J.replace(/\/\*[\s\S]*?\*\//g,""),Z=[],X=0;while(X<Y.length){let $=Y.indexOf("{",X);if($===-1)break;let G=Y.slice(X,$).trim(),K=1,Q=$+1;while(Q<Y.length&&K>0){let W=Y[Q];if(W==="{")K++;else if(W==="}")K--;Q++}let F=Y.slice($+1,Q-1);if(G.startsWith("@")||F.includes("{"))Z.push({selector:G||"@unknown",declarations:cY(F)});else Z.push({selector:G,declarations:pY(F)});X=Q}return Z}function pY(J){return J.split(";").map((Y)=>Y.trim()).filter(Boolean).map((Y)=>{let Z=Y.indexOf(":");if(Z===-1)return null;return{prop:Y.slice(0,Z).trim(),value:Y.slice(Z+1).trim()}}).filter((Y)=>Y!==null)}function cY(J){return[{prop:"",value:J.trim()}]}function yJ(J){if(!J)return!1;if(J.includes(","))return J.split(",").every((Y)=>yJ(Y.trim()));if(J.startsWith("@"))return!1;if(/[\s>+~:[]/.test(J))return!1;return/^[a-z][a-z0-9-]*?$|^([a-z][a-z0-9-]*)?([.#][a-z][\w-]*)+$/i.test(J)}function bJ(J,Y,Z,X){if(Y.includes(",")){let z=J;for(let V of Y.split(","))z=bJ(z,V.trim(),Z,X);return z}let{tag:$,classes:G,ids:K}=gY(Y),F=new RegExp(`<(${$??"[a-z][a-z0-9-]*"})\\b([^>]*?)(/?)>`,"gi"),W=Z.map((z)=>`${z.prop}:${z.value}${X&&!/!important\b/i.test(z.value)?" !important":""};`).join("");return J.replace(F,(z,V,q)=>{if(!dY(q,G,K))return z;return lY(z,q,W)})}function gY(J){let Y=null,Z=[],X=[],$=0;while($<J.length&&/[a-z0-9-]/i.test(J[$]))Y=(Y??"")+J[$],$++;if(Y==="")Y=null;while($<J.length){let G=J[$];if(G!=="."&&G!=="#")break;let K=$+1;while(K<J.length&&/[\w-]/.test(J[K]))K++;let Q=J.slice($+1,K);if(G===".")Z.push(Q);else X.push(Q);$=K}return{tag:Y,classes:Z,ids:X}}function dY(J,Y,Z){if(Y.length>0){let X=J.match(/\bclass\s*=\s*["']([^"']*)["']/i);if(!X)return!1;let $=X[1].split(/\s+/).filter(Boolean);if(!Y.every((G)=>$.includes(G)))return!1}if(Z.length>0){let X=J.match(/\bid\s*=\s*["']([^"']+)["']/i);if(!X)return!1;if(!Z.every(($)=>X[1]===$))return!1}return!0}function lY(J,Y,Z){let X=Y.match(/\bstyle\s*=\s*["']([^"']*)["']/i);if(X){let G=X[1].trim(),K=`${Z}${G}${G.endsWith(";")||G===""?"":";"}`,Q=Y.replace(X[0],`style="${K}"`);return J.replace(Y,Q)}let $=` style="${Z}"`;return J.replace(/(\s*\/?)>$/,`${$}$1>`)}function zJ(){return(globalThis.process?.env?.APP_ENV??globalThis.process?.env?.NODE_ENV??"").toLowerCase()==="production"}class LJ{value;__safeHtml=!0;constructor(J){this.value=J}}function oY(J){return new LJ(J)}function fJ(){let J=VJ.app.primaryColor||"#3b82f6";return{appName:VJ.app.name||"Stacks",appUrl:VJ.app.url||"https://localhost",primaryColor:J,primaryColorDark:nY(J,15),year:new Date().getFullYear()}}function nY(J,Y){let Z=Number.parseInt(J.replace("#",""),16),X=Math.round(2.55*Y),$=Math.max(0,(Z>>16)-X),G=Math.max(0,(Z>>8&255)-X),K=Math.max(0,(Z&255)-X);return`#${(16777216+$*65536+G*256+K).toString(16).slice(1)}`}function iY(J){return J.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function OJ(J,Y){let Z=J;for(let[X,$]of Object.entries(Y)){let G=X.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),K=new RegExp(`\\{\\{\\s*${G}\\s*\\}\\}`,"g");Z=Z.replace(K,rY($))}return Z}function rY(J){if(J===null||J===void 0)return"";if(J instanceof LJ)return J.value;return iY(String(J))}var uJ=[(J)=>hJ(qJ("emails",J)),(J)=>vJ(qJ("emails",J))];function pJ(J){for(let Y of uJ){if(J.endsWith(".stx")){let $=Y(J);if(P.existsSync($))return{path:$,type:"stx"};continue}if(J.endsWith(".html")){let $=Y(J);if(P.existsSync($))return{path:$,type:"html"};continue}let Z=Y(`${J}.stx`);if(P.existsSync(Z))return{path:Z,type:"stx"};let X=Y(`${J}.html`);if(P.existsSync(X))return{path:X,type:"html"}}return null}function tY(J){let Y=J.endsWith(".html")?J:`${J}.html`;for(let Z of uJ){let X=Z(Y);if(P.existsSync(X))return P.readFileSync(X,"utf-8")}return null}function aY(J){return tY(`layouts/${J}`)}function cJ(J){return J.replace(/<br\s*\/?>/gi,`
|
|
3
8
|
`).replace(/<\/(p|div|h[1-6]|li|tr)>/gi,`
|
|
4
9
|
`).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,`
|
|
5
10
|
|
|
6
|
-
`).trim()}async function
|
|
7
|
-
${
|
|
8
|
-
`)}catch(z){
|
|
9
|
-
`)}function d(J){if(!J)return"";if(typeof J==="string")return J;let U=J;return U.name?`${U.name} <${U.address??""}>`:U.address??""}function n(J){if(Array.isArray(J))return J.map(d).join(", ");return d(J)}function bJ(J){return J.replace(/[&<>"']/g,(U)=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[U])}var vJ=C;var OJ={};T(OJ,{default:()=>hJ,MailgunDriver:()=>b});import{Buffer as zJ}from"buffer";import{config as M}from"@stacksjs/config";import{log as o}from"@stacksjs/logging";class b extends H{name="mailgun";apiKey=null;domain=null;endpoint=null;getConfig(){if(!this.apiKey||!this.domain||!this.endpoint)this.apiKey=M.services.mailgun?.apiKey??"",this.domain=M.services.mailgun?.domain??"",this.endpoint=M.services.mailgun?.endpoint??"api.mailgun.net";return{apiKey:this.apiKey,domain:this.domain,endpoint:this.endpoint}}async send(J,U){let{domain:X}=this.getConfig(),Y={provider:this.name,to:J.to,subject:J.subject,domain:X};o.info("Sending email via Mailgun...",Y);try{this.validateMessage(J);let Z;if(J.template){let F=await V(J.template,U);if(F&&"html"in F)Z=F.html}let W=Z||J.html,K=new FormData,G={address:J.from?.address||M.email.from?.address||"",name:J.from?.name||M.email.from?.name};if(K.append("from",this.formatMailgunAddress(G)),this.formatMailgunAddresses(J.to).forEach((F)=>K.append("to",F)),J.cc)this.formatMailgunAddresses(J.cc).forEach((F)=>K.append("cc",F));if(J.bcc)this.formatMailgunAddresses(J.bcc).forEach((F)=>K.append("bcc",F));if(K.append("subject",J.subject),W)K.append("html",W);if(J.text)K.append("text",J.text);if(J.attachments)J.attachments.forEach((F)=>{let z=typeof F.content==="string"?F.content:this.arrayBufferToBase64(F.content);K.append("attachment",new Blob([z],{type:F.contentType}),F.filename)});let $=await this.sendWithRetry(K);return this.handleSuccess(J,$.id)}catch(Z){return this.handleError(Z,J)}}formatMailgunAddress(J){return J.name?`${J.name} <${J.address}>`:J.address}formatMailgunAddresses(J){if(!J)return[];if(typeof J==="string")return[J];return J.map((U)=>{if(typeof U==="string")return U;if(!U.name)return U.address;return`${/[",()<>[\]:;@\\]/.test(U.name)?`"${U.name.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")}"`:U.name} <${U.address}>`})}arrayBufferToBase64(J){let U="",X=new Uint8Array(J),Y=X.byteLength;for(let Z=0;Z<Y;Z++)U+=String.fromCharCode(X[Z]??0);return typeof btoa==="function"?btoa(U):zJ.from(U).toString("base64")}async sendWithRetry(J,U=1){let{apiKey:X,domain:Y,endpoint:Z}=this.getConfig(),W=`https://${Z}/v3/${Y}/messages`,K=zJ.from(`api:${X}`).toString("base64");try{let G=await fetch(W,{method:"POST",headers:{Authorization:`Basic ${K}`},body:J});if(!G.ok){let F=await G.json();throw Error(`Mailgun API error: ${G.status} - ${JSON.stringify(F)}`)}let $=await G.json();return o.info(`[${this.name}] Email sent successfully`,{attempt:U,messageId:$.id}),$}catch(G){if(U<(M.services.mailgun?.maxRetries??3)){let $=M.services.mailgun?.retryTimeout??1000;return o.warn(`[${this.name}] Email send failed, retrying (${U}/${M.services.mailgun?.maxRetries??3})`),await new Promise((F)=>setTimeout(F,$)),this.sendWithRetry(J,U+1)}throw G}}}var hJ=b;var qJ={};T(qJ,{default:()=>fJ,MailtrapDriver:()=>v});import{Buffer as uJ}from"buffer";import{config as A}from"@stacksjs/config";import{log as m}from"@stacksjs/logging";class v extends H{name="mailtrap";host=null;token=null;inboxId=null;getConfig(){if(this.host===null||this.token===null||this.inboxId===null)this.host=A.services.mailtrap?.host??"https://sandbox.api.mailtrap.io/api/send",this.token=A.services.mailtrap?.token??"",this.inboxId=A.services.mailtrap?.inboxId?Number(A.services.mailtrap.inboxId):void 0;return{host:this.host,token:this.token,inboxId:this.inboxId}}async send(J,U){let{inboxId:X}=this.getConfig(),Y={provider:this.name,to:J.to,subject:J.subject,inboxId:X};m.info("Sending email via Mailtrap...",Y);try{this.validateMessage(J);let Z;if(J.template)Z=await V(J.template,U);let W=Z?.html||J.html,K={from:{email:J.from?.address||A.email.from?.address||"",name:J.from?.name||A.email.from?.name},to:this.formatMailtrapAddresses(J.to),...J.cc&&{cc:this.formatMailtrapAddresses(J.cc)},...J.bcc&&{bcc:this.formatMailtrapAddresses(J.bcc)},subject:J.subject,...W&&{html:W},...J.text&&{text:J.text},...J.attachments&&{attachments:J.attachments.map(($)=>({filename:$.filename,content:typeof $.content==="string"?$.content:this.arrayBufferToBase64($.content),type:$.contentType||"application/octet-stream"}))}},G=await this.sendWithRetry(K);return this.handleSuccess(J,G.message_ids?.[0])}catch(Z){return this.handleError(Z,J)}}formatMailtrapAddresses(J){if(!J)return[];if(typeof J==="string")return[{email:J}];return J.map((U)=>{if(typeof U==="string")return{email:U};return{email:U.address,...U.name&&{name:U.name}}})}arrayBufferToBase64(J){let U="",X=new Uint8Array(J),Y=X.byteLength;for(let Z=0;Z<Y;Z++)U+=String.fromCharCode(X[Z]??0);return typeof btoa==="function"?btoa(U):uJ.from(U).toString("base64")}async sendWithRetry(J,U=1){let{host:X,token:Y,inboxId:Z}=this.getConfig();if(!Z)throw Error("Mailtrap inbox ID is required but not provided. Please set MAILTRAP_INBOX_ID in your environment variables.");let W=`${X}/${Z}`;try{let K=await fetch(W,{method:"POST",headers:{Authorization:`Bearer ${Y}`,"Content-Type":"application/json"},body:JSON.stringify(J)});if(!K.ok){let $=await K.json();throw Error(`Mailtrap API error: ${K.status} - ${JSON.stringify($)}`)}let G=await K.json();return m.info(`[${this.name}] Email sent successfully`,{attempt:U,messageId:G.message_ids?.[0]}),G}catch(K){if(U<(A.services.mailtrap?.maxRetries??3)){let G=A.services.mailtrap?.retryTimeout??1000;return m.warn(`[${this.name}] Email send failed, retrying (${U}/${A.services.mailtrap?.maxRetries??3})`),await new Promise(($)=>setTimeout($,G)),this.sendWithRetry(J,U+1)}throw K}}}var fJ=v;var VJ={};T(VJ,{NodemailerDriver:()=>QJ});class QJ{async send(){throw Error("Nodemailer driver is not yet implemented. Use smtp, ses, sendgrid, mailgun, or mailtrap instead.")}}var HJ={};T(HJ,{default:()=>pJ,SendGridDriver:()=>h});import{Buffer as cJ}from"buffer";import{config as x}from"@stacksjs/config";import{log as r}from"@stacksjs/logging";class h extends H{name="sendgrid";apiKey=null;getApiKey(){if(!this.apiKey)this.apiKey=x.services.sendgrid?.apiKey??"";return this.apiKey}async send(J,U){let X={provider:this.name,to:J.to,subject:J.subject};r.info("Sending email via SendGrid...",X);try{this.validateMessage(J);let Y;if(J.template){let $=await V(J.template,U);if($&&"html"in $)Y=$.html}let Z=Y||J.html,W=[];if(Z)W.push({type:"text/html",value:Z});if(J.text)W.push({type:"text/plain",value:J.text});if(W.length===0)throw Error("Email must have either HTML or text content");let K={personalizations:[{to:this.formatSendGridAddresses(J.to),...J.cc&&{cc:this.formatSendGridAddresses(J.cc)},...J.bcc&&{bcc:this.formatSendGridAddresses(J.bcc)},subject:J.subject}],from:{email:J.from?.address||x.email.from?.address||"",name:J.from?.name||x.email.from?.name},content:W,...J.attachments&&{attachments:J.attachments.map(($)=>({filename:$.filename,content:typeof $.content==="string"?$.content:this.arrayBufferToBase64($.content),type:$.contentType,disposition:"attachment"}))}},G=await this.sendWithRetry(K);return this.handleSuccess(J,G.headers?.get("x-message-id")??void 0)}catch(Y){return this.handleError(Y,J)}}formatSendGridAddresses(J){if(!J)return[];if(typeof J==="string")return[{email:J}];return J.map((U)=>{if(typeof U==="string")return{email:U};return{email:U.address,...U.name&&{name:U.name}}})}arrayBufferToBase64(J){let U="",X=new Uint8Array(J),Y=X.byteLength;for(let Z=0;Z<Y;Z++)U+=String.fromCharCode(X[Z]??0);return typeof btoa==="function"?btoa(U):cJ.from(U).toString("base64")}async sendWithRetry(J,U=1){try{let X=await fetch("https://api.sendgrid.com/v3/mail/send",{method:"POST",headers:{Authorization:`Bearer ${this.getApiKey()}`,"Content-Type":"application/json"},body:JSON.stringify(J)});if(!X.ok){let Y=await X.json(),Z=Error(`SendGrid API error: ${X.status} - ${JSON.stringify(Y)}`);throw Z.status=X.status,Z}return r.info(`[${this.name}] Email sent successfully`,{attempt:U}),X}catch(X){let Y=X?.status;if(!(typeof Y==="number"&&Y>=400&&Y<500&&Y!==429)&&U<(x.services.sendgrid?.maxRetries??3)){let W=x.services.sendgrid?.retryTimeout??1000;return r.warn(`[${this.name}] Email send failed, retrying (${U}/${x.services.sendgrid?.maxRetries??3})`),await new Promise((K)=>setTimeout(K,W)),this.sendWithRetry(J,U+1)}throw X}}}var pJ=h;var BJ={};T(BJ,{default:()=>iJ,SESDriver:()=>u});import{SESClient as lJ}from"@stacksjs/ts-cloud";import{config as D}from"@stacksjs/config";class u extends H{name="ses";client=null;getClient(){if(!this.client)this.client=new lJ(D?.services?.ses?.region||"us-east-1");return this.client}async send(J,U){try{this.validateMessage(J);let X;if(J.template){let K=await V(J.template,U);if(K&&"html"in K)X=K.html}let Y=X||J.html,Z={};if(Y)Z.Html={Charset:D.email.charset||"UTF-8",Data:Y};if(J.text)Z.Text={Charset:D.email.charset||"UTF-8",Data:J.text};if(Object.keys(Z).length===0)throw Error("Email must have either HTML or text content");let W=await this.getClient().sendEmail({FromEmailAddress:this.formatSourceAddress({address:J.from?.address||D.email.from?.address||"",name:J.from?.name||D.email.from?.name}),Destination:{ToAddresses:this.formatAddresses(J.to),CcAddresses:this.formatAddresses(J.cc),BccAddresses:this.formatAddresses(J.bcc)},Content:{Simple:{Subject:{Charset:D.email.charset||"UTF-8",Data:J.subject},Body:Z}}});return this.handleSuccess(J,W.MessageId)}catch(X){return this.handleError(X,J)}}formatSourceAddress(J){return J.name?`${J.name} <${J.address}>`:J.address}formatAddresses(J){if(!J)return[];if(typeof J==="string")return[J];return J.map((U)=>typeof U==="string"?U:U.address)}}var iJ=u;import{config as k}from"@stacksjs/config";import{Buffer as t}from"buffer";import*as f from"tls";import*as wJ from"net";import{config as L}from"@stacksjs/config";import{log as w}from"@stacksjs/logging";function gJ(J){if(/^[\x00-\x7F]*$/.test(J))return J;return`=?UTF-8?B?${t.from(J,"utf-8").toString("base64")}?=`}var nJ=/^[^\s<>"\\\r\n\t]+@[^\s<>"\\\r\n\t]+$/;function LJ(J,U){if(typeof J!=="string"||!nJ.test(J))throw Error(`[smtp] Refusing to send: ${U} envelope address contains forbidden characters or is malformed: ${JSON.stringify(J)}`)}class E extends H{static SMTP_TIMEOUT=30000;name="smtp";smtpConfig=null;getConfig(){if(!this.smtpConfig){let J=L.services.smtp?.encryption;this.smtpConfig={host:L.services.smtp?.host||"127.0.0.1",port:L.services.smtp?.port||587,username:L.services.smtp?.username||"",password:L.services.smtp?.password||"",encryption:J==="tls"?"starttls":J||null}}return this.smtpConfig}async send(J,U){let X=this.getConfig();if(!X.host||X.host==="")throw Error("[SMTP] Host is not configured. Set MAIL_HOST in your .env file.");let Y={provider:this.name,to:J.to,subject:J.subject,host:X.host,port:X.port};w.info("Sending email via SMTP...",Y);try{this.validateMessage(J);let Z;if(J.template){let Q=await V(J.template,U);if(Q&&"html"in Q)Z=Q.html}let W=Z||J.html,K=J.from?.address||L.email.from?.address||"",G=J.from?.name||L.email.from?.name||"",$=this.formatAddresses(J.to),F=`----=_Part_${Date.now()}_${Math.random().toString(36).substring(2)}`,z=this.buildEmailContent({from:G?`${G} <${K}>`:K,to:$.join(", "),cc:J.cc?this.formatAddresses(J.cc).join(", "):void 0,subject:J.subject,text:J.text,html:W,boundary:F}),j=await this.sendViaSMTP(X,K,$,z);return this.handleSuccess(J,j)}catch(Z){return this.handleError(Z,J)}}buildEmailContent(J){let{from:U,to:X,cc:Y,subject:Z,text:W,html:K,boundary:G}=J,$=[];if($.push(`From: ${U}`),$.push(`To: ${X}`),Y)$.push(`Cc: ${Y}`);if($.push(`Subject: ${gJ(Z)}`),$.push("MIME-Version: 1.0"),$.push(`Date: ${new Date().toUTCString()}`),$.push(`Message-ID: <${Date.now()}.${Math.random().toString(36).substring(2)}@${L.email.domain||"localhost"}>`),K&&W)$.push(`Content-Type: multipart/alternative; boundary="${G}"`),$.push(""),$.push(`--${G}`),$.push("Content-Type: text/plain; charset=UTF-8"),$.push("Content-Transfer-Encoding: 7bit"),$.push(""),$.push(W),$.push(""),$.push(`--${G}`),$.push("Content-Type: text/html; charset=UTF-8"),$.push("Content-Transfer-Encoding: 7bit"),$.push(""),$.push(K),$.push(""),$.push(`--${G}--`);else if(K)$.push("Content-Type: text/html; charset=UTF-8"),$.push("Content-Transfer-Encoding: 7bit"),$.push(""),$.push(K);else if(W)$.push("Content-Type: text/plain; charset=UTF-8"),$.push("Content-Transfer-Encoding: 7bit"),$.push(""),$.push(W);return $.join(`\r
|
|
10
|
-
`)}
|
|
11
|
-
`)})},
|
|
12
|
-
|
|
11
|
+
`).trim()}async function I(J,Y={}){let{variables:Z={},layout:X="base",subject:$="",inline:G=zJ()}=Y,K={...fJ(),subject:$,...Z},Q=pJ(J);if(!Q)return console.warn(`[Email Template] Template "${J}" not found`),{html:"",text:""};if(Q.type==="stx")try{let{renderEmail:V}=await import("@stacksjs/stx"),q=await V(Q.path,K,{componentsDir:vJ("components/Email")});return{...q,html:JJ(q.html,{inline:G})}}catch(V){return mY.warn(`[email] STX template rendering failed for ${J}: ${V instanceof Error?V.message:String(V)}`),{html:"",text:""}}let F=P.readFileSync(Q.path,"utf-8");F=OJ(F,K);let W;if(X!==!1){let V=aY(X);if(!V)console.warn(`[Email Template] Layout "${X}" not found, using content only`),W=F;else K.content=oY(F),W=OJ(V,K)}else W=F;W=JJ(W,{inline:G});let z=cJ(W);return{html:W,text:z}}function QX(J,Y={}){let Z={...fJ(),...Y},X=OJ(J,Z),$=cJ(X);return{html:X,text:$}}function FX(J){return pJ(J)!==null}function zX(){let J=hJ("emails");if(!P.existsSync(J))return[];let Y=[];function Z(X,$=""){let G=P.readdirSync(X,{withFileTypes:!0});for(let K of G)if(K.isDirectory()&&K.name!=="layouts")Z(qJ(X,K.name),`${$}${K.name}/`);else if(K.isFile()&&(K.name.endsWith(".html")||K.name.endsWith(".stx"))){let Q=K.name.replace(/\.(html|stx)$/,"");if(!Y.includes(`${$}${Q}`))Y.push(`${$}${Q}`)}}return Z(J),Y}var mJ=100,f=[];class g extends H{name="log";resolveDir(){let J=process.env.LOG_MAIL_DIR;if(J)return dJ(J);return dJ(gJ(import.meta.dir,"..","..","..","..","..","logs","mail"))}async send(J,Y){try{this.validateMessage(J);let Z;if(J.template){let z=await I(J.template,Y);if(z)Z={html:z.html,text:z.text}}let X=Z?.html??J.html,$=Z?.text??J.text,G=new Date,K=(J.subject||"no-subject").replace(/[^\w.-]+/g,"-").slice(0,60),Q=`${G.toISOString().replace(/[:.]/g,"-")}-${K}.html`,F=this.resolveDir();try{await sY(F,{recursive:!0});let z=gJ(F,Q),V=X?X:$?`<pre>${YZ($)}</pre>`:"<em>(empty body)</em>",q=JZ({stamp:G,message:J});await eY(z,`${q}
|
|
12
|
+
${V}
|
|
13
|
+
`)}catch(z){lJ.warn(`[email:log] could not write inspection file: ${z.message}`)}let W=Array.isArray(J.to)?J.to.map((z)=>typeof z==="string"?z:z.address).join(", "):typeof J.to==="string"?J.to:J.to.address;if(lJ.info(`[email:log] would send \u2192 ${W} :: ${J.subject}`),f.push({...J,sentAt:G,rendered:Z}),f.length>mJ)f.splice(0,f.length-mJ);return this.handleSuccess(J,`log-${G.getTime()}`)}catch(Z){return this.handleError(Z,J)}}static captured(){return f}static reset(){f.length=0}}function JZ({stamp:J,message:Y}){return["<!--",` Captured by @stacksjs/email log driver at ${J.toISOString()}`,` From: ${NJ(Y.from)}`,` To: ${UJ(Y.to)}`,Y.cc?` Cc: ${UJ(Y.cc)}`:null,Y.bcc?` Bcc: ${UJ(Y.bcc)}`:null,` Subject: ${Y.subject}`,"-->"].filter(Boolean).join(`
|
|
14
|
+
`)}function NJ(J){if(!J)return"";if(typeof J==="string")return J;let Y=J;return Y.name?`${Y.name} <${Y.address??""}>`:Y.address??""}function UJ(J){if(Array.isArray(J))return J.map(NJ).join(", ");return NJ(J)}function YZ(J){return J.replace(/[&<>"']/g,(Y)=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[Y])}var ZZ=g;var iJ={};v(iJ,{default:()=>XZ,MailgunDriver:()=>d});import{Buffer as nJ}from"buffer";import{config as A}from"@stacksjs/config";import{log as IJ}from"@stacksjs/logging";class d extends H{name="mailgun";apiKey=null;domain=null;endpoint=null;getConfig(){if(!this.apiKey||!this.domain||!this.endpoint)this.apiKey=A.services.mailgun?.apiKey??"",this.domain=A.services.mailgun?.domain??"",this.endpoint=A.services.mailgun?.endpoint??"api.mailgun.net";return{apiKey:this.apiKey,domain:this.domain,endpoint:this.endpoint}}async send(J,Y){let{domain:Z}=this.getConfig(),X={provider:this.name,to:J.to,subject:J.subject,domain:Z};IJ.info("Sending email via Mailgun...",X);try{this.validateMessage(J);let $;if(J.template){let W=await I(J.template,Y);if(W&&"html"in W)$=W.html}let G=$||J.html,K=new FormData,Q={address:J.from?.address||A.email.from?.address||"",name:J.from?.name||A.email.from?.name};if(K.append("from",this.formatMailgunAddress(Q)),this.formatMailgunAddresses(J.to).forEach((W)=>K.append("to",W)),J.cc)this.formatMailgunAddresses(J.cc).forEach((W)=>K.append("cc",W));if(J.bcc)this.formatMailgunAddresses(J.bcc).forEach((W)=>K.append("bcc",W));if(K.append("subject",J.subject),J.replyTo){let W=this.formatMailgunAddresses(Array.isArray(J.replyTo)||typeof J.replyTo==="string"?J.replyTo:[J.replyTo]);if(W.length>0)K.append("h:Reply-To",W.join(", "))}if(J.headers){for(let[W,z]of Object.entries(J.headers))if(typeof z==="string")K.append(`h:${W}`,z)}if(G)K.append("html",G);if(J.text)K.append("text",J.text);if(J.attachments)J.attachments.forEach((W)=>{let z=typeof W.content==="string"?W.content:this.arrayBufferToBase64(W.content);K.append("attachment",new Blob([z],{type:W.contentType}),W.filename)});let F=await this.sendWithRetry(K);return this.handleSuccess(J,F.id)}catch($){return this.handleError($,J)}}formatMailgunAddress(J){return J.name?`${J.name} <${J.address}>`:J.address}formatMailgunAddresses(J){if(!J)return[];if(typeof J==="string")return[J];return J.map((Y)=>{if(typeof Y==="string")return Y;if(!Y.name)return Y.address;return`${/[",()<>[\]:;@\\]/.test(Y.name)?`"${Y.name.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")}"`:Y.name} <${Y.address}>`})}arrayBufferToBase64(J){let Y="",Z=new Uint8Array(J),X=Z.byteLength;for(let $=0;$<X;$++)Y+=String.fromCharCode(Z[$]??0);return typeof btoa==="function"?btoa(Y):nJ.from(Y).toString("base64")}async sendWithRetry(J,Y=1){let{apiKey:Z,domain:X,endpoint:$}=this.getConfig(),G=`https://${$}/v3/${X}/messages`,K=nJ.from(`api:${Z}`).toString("base64");try{let Q=await fetch(G,{method:"POST",headers:{Authorization:`Basic ${K}`},body:J});if(!Q.ok){let W=await Q.json();throw Error(`Mailgun API error: ${Q.status} - ${JSON.stringify(W)}`)}let F=await Q.json();return IJ.info(`[${this.name}] Email sent successfully`,{attempt:Y,messageId:F.id}),F}catch(Q){if(Y<(A.services.mailgun?.maxRetries??3)){let F=A.services.mailgun?.retryTimeout??1000;return IJ.warn(`[${this.name}] Email send failed, retrying (${Y}/${A.services.mailgun?.maxRetries??3})`),await new Promise((W)=>setTimeout(W,F)),this.sendWithRetry(J,Y+1)}throw Q}}}var XZ=d;var rJ={};v(rJ,{default:()=>GZ,MailtrapDriver:()=>l});import{Buffer as $Z}from"buffer";import{config as j}from"@stacksjs/config";import{log as HJ}from"@stacksjs/logging";class l extends H{name="mailtrap";host=null;token=null;inboxId=null;getConfig(){if(this.host===null||this.token===null||this.inboxId===null)this.host=j.services.mailtrap?.host??"https://sandbox.api.mailtrap.io/api/send",this.token=j.services.mailtrap?.token??"",this.inboxId=j.services.mailtrap?.inboxId?Number(j.services.mailtrap.inboxId):void 0;return{host:this.host,token:this.token,inboxId:this.inboxId}}async send(J,Y){let{inboxId:Z}=this.getConfig(),X={provider:this.name,to:J.to,subject:J.subject,inboxId:Z};HJ.info("Sending email via Mailtrap...",X);try{this.validateMessage(J);let $;if(J.template)$=await I(J.template,Y);let G=$?.html||J.html,K=this.firstMailtrapAddress(J.replyTo),Q=T(J.headers),F={from:{email:J.from?.address||j.email.from?.address||"",name:J.from?.name||j.email.from?.name},to:this.formatMailtrapAddresses(J.to),...J.cc&&{cc:this.formatMailtrapAddresses(J.cc)},...J.bcc&&{bcc:this.formatMailtrapAddresses(J.bcc)},...K?{reply_to:K}:{},...Q?{headers:Q}:{},subject:J.subject,...G&&{html:G},...J.text&&{text:J.text},...J.attachments&&{attachments:J.attachments.map((z)=>({filename:z.filename,content:typeof z.content==="string"?z.content:this.arrayBufferToBase64(z.content),type:z.contentType||"application/octet-stream"}))}},W=await this.sendWithRetry(F);return this.handleSuccess(J,W.message_ids?.[0])}catch($){return this.handleError($,J)}}formatMailtrapAddresses(J){if(!J)return[];if(typeof J==="string")return[{email:J}];return J.map((Y)=>{if(typeof Y==="string")return{email:Y};return{email:Y.address,...Y.name&&{name:Y.name}}})}firstMailtrapAddress(J){if(!J)return;if(typeof J==="string")return{email:J};if(Array.isArray(J)){let Y=J[0];if(Y===void 0)return;if(typeof Y==="string")return{email:Y};return{email:Y.address,...Y.name&&{name:Y.name}}}return{email:J.address,...J.name&&{name:J.name}}}arrayBufferToBase64(J){let Y="",Z=new Uint8Array(J),X=Z.byteLength;for(let $=0;$<X;$++)Y+=String.fromCharCode(Z[$]??0);return typeof btoa==="function"?btoa(Y):$Z.from(Y).toString("base64")}async sendWithRetry(J,Y=1){let{host:Z,token:X,inboxId:$}=this.getConfig();if(!$)throw Error("Mailtrap inbox ID is required but not provided. Please set MAILTRAP_INBOX_ID in your environment variables.");let G=`${Z}/${$}`;try{let K=await fetch(G,{method:"POST",headers:{Authorization:`Bearer ${X}`,"Content-Type":"application/json"},body:JSON.stringify(J)});if(!K.ok){let F=await K.json();throw Error(`Mailtrap API error: ${K.status} - ${JSON.stringify(F)}`)}let Q=await K.json();return HJ.info(`[${this.name}] Email sent successfully`,{attempt:Y,messageId:Q.message_ids?.[0]}),Q}catch(K){if(Y<(j.services.mailtrap?.maxRetries??3)){let Q=j.services.mailtrap?.retryTimeout??1000;return HJ.warn(`[${this.name}] Email send failed, retrying (${Y}/${j.services.mailtrap?.maxRetries??3})`),await new Promise((F)=>setTimeout(F,Q)),this.sendWithRetry(J,Y+1)}throw K}}}var GZ=l;var tJ={};v(tJ,{default:()=>WZ,SendGridDriver:()=>m});import{Buffer as KZ}from"buffer";import{config as y}from"@stacksjs/config";import{log as RJ}from"@stacksjs/logging";class m extends H{name="sendgrid";apiKey=null;getApiKey(){if(!this.apiKey)this.apiKey=y.services.sendgrid?.apiKey??"";return this.apiKey}async send(J,Y){let Z={provider:this.name,to:J.to,subject:J.subject};RJ.info("Sending email via SendGrid...",Z);try{this.validateMessage(J);let X;if(J.template){let z=await I(J.template,Y);if(z&&"html"in z)X=z.html}let $=X||J.html,G=[];if($)G.push({type:"text/html",value:$});if(J.text)G.push({type:"text/plain",value:J.text});if(G.length===0)throw Error("Email must have either HTML or text content");let K=this.firstSendGridAddress(J.replyTo),Q=T(J.headers),F={personalizations:[{to:this.formatSendGridAddresses(J.to),...J.cc&&{cc:this.formatSendGridAddresses(J.cc)},...J.bcc&&{bcc:this.formatSendGridAddresses(J.bcc)},subject:J.subject}],from:{email:J.from?.address||y.email.from?.address||"",name:J.from?.name||y.email.from?.name},...K?{reply_to:K}:{},...Q?{headers:Q}:{},content:G,...J.attachments&&{attachments:J.attachments.map((z)=>({filename:z.filename,content:typeof z.content==="string"?z.content:this.arrayBufferToBase64(z.content),type:z.contentType,disposition:"attachment"}))}},W=await this.sendWithRetry(F);return this.handleSuccess(J,W.headers?.get("x-message-id")??void 0)}catch(X){return this.handleError(X,J)}}formatSendGridAddresses(J){if(!J)return[];if(typeof J==="string")return[{email:J}];return J.map((Y)=>{if(typeof Y==="string")return{email:Y};return{email:Y.address,...Y.name&&{name:Y.name}}})}firstSendGridAddress(J){if(!J)return;if(typeof J==="string")return{email:J};if(Array.isArray(J)){let Y=J[0];if(Y===void 0)return;if(typeof Y==="string")return{email:Y};return{email:Y.address,...Y.name&&{name:Y.name}}}return{email:J.address,...J.name&&{name:J.name}}}arrayBufferToBase64(J){let Y="",Z=new Uint8Array(J),X=Z.byteLength;for(let $=0;$<X;$++)Y+=String.fromCharCode(Z[$]??0);return typeof btoa==="function"?btoa(Y):KZ.from(Y).toString("base64")}async sendWithRetry(J,Y=1){try{let Z=await fetch("https://api.sendgrid.com/v3/mail/send",{method:"POST",headers:{Authorization:`Bearer ${this.getApiKey()}`,"Content-Type":"application/json"},body:JSON.stringify(J)});if(!Z.ok){let X=await Z.json(),$=Error(`SendGrid API error: ${Z.status} - ${JSON.stringify(X)}`);throw $.status=Z.status,$}return RJ.info(`[${this.name}] Email sent successfully`,{attempt:Y}),Z}catch(Z){let X=Z?.status;if(!(typeof X==="number"&&X>=400&&X<500&&X!==429)&&Y<(y.services.sendgrid?.maxRetries??3)){let G=y.services.sendgrid?.retryTimeout??1000;return RJ.warn(`[${this.name}] Email send failed, retrying (${Y}/${y.services.sendgrid?.maxRetries??3})`),await new Promise((K)=>setTimeout(K,G)),this.sendWithRetry(J,Y+1)}throw Z}}}var WZ=m;var eJ={};v(eJ,{default:()=>zZ,SESDriver:()=>o});import{config as E}from"@stacksjs/config";import{SESClient as FZ}from"@stacksjs/ts-cloud";import{Buffer as _J}from"buffer";function sJ(J){if(/^[\x00-\x7F]*$/.test(J))return J;return`=?UTF-8?B?${_J.from(J,"utf-8").toString("base64")}?=`}function YJ(J){let{from:Y,to:Z,cc:X,replyTo:$,subject:G,text:K,html:Q,attachments:F,messageIdDomain:W,customHeaders:z}=J,V=[],q=!!(F&&F.length>0),B=`----=_Part_${Date.now()}_${Math.random().toString(36).substring(2)}`;if(V.push(`From: ${Y}`),V.push(`To: ${Z}`),X)V.push(`Cc: ${X}`);if($)V.push(`Reply-To: ${$}`);if(V.push(`Subject: ${sJ(G)}`),V.push("MIME-Version: 1.0"),V.push(`Date: ${new Date().toUTCString()}`),V.push(`Message-ID: <${Date.now()}.${Math.random().toString(36).substring(2)}@${W||"localhost"}>`),z)for(let[N,w]of Object.entries(z))V.push(`${N}: ${w}`);if(q){let N=B,w=`${B}_alt`;V.push(`Content-Type: multipart/mixed; boundary="${N}"`),V.push(""),V.push(`--${N}`),aJ(V,{text:K,html:Q,altBoundary:w});for(let R of F)V.push(`--${N}`),QZ(V,R);V.push(`--${N}--`)}else aJ(V,{text:K,html:Q,altBoundary:B});return V.join(`\r
|
|
15
|
+
`)}function aJ(J,Y){let{text:Z,html:X,altBoundary:$}=Y;if(X&&Z)J.push(`Content-Type: multipart/alternative; boundary="${$}"`),J.push(""),J.push(`--${$}`),J.push("Content-Type: text/plain; charset=UTF-8"),J.push("Content-Transfer-Encoding: 7bit"),J.push(""),J.push(Z),J.push(""),J.push(`--${$}`),J.push("Content-Type: text/html; charset=UTF-8"),J.push("Content-Transfer-Encoding: 7bit"),J.push(""),J.push(X),J.push(""),J.push(`--${$}--`);else if(X)J.push("Content-Type: text/html; charset=UTF-8"),J.push("Content-Transfer-Encoding: 7bit"),J.push(""),J.push(X);else if(Z)J.push("Content-Type: text/plain; charset=UTF-8"),J.push("Content-Transfer-Encoding: 7bit"),J.push(""),J.push(Z)}function QZ(J,Y){let Z=Y.contentType||"application/octet-stream",X=sJ(Y.filename.replace(/\\/g,"\\\\").replace(/"/g,"\\\""));J.push(`Content-Type: ${Z}; name="${X}"`),J.push(`Content-Disposition: attachment; filename="${X}"`),J.push("Content-Transfer-Encoding: base64"),J.push("");let $=typeof Y.content==="string"?_J.from(Y.content,"utf-8").toString("base64"):_J.from(Y.content).toString("base64"),G=$.match(/.{1,76}/g)?.join(`\r
|
|
16
|
+
`)??$;J.push(G),J.push("")}class o extends H{name="ses";client=null;getClient(){if(!this.client){let J=E?.services?.ses,Y=J?.credentials,Z=!!(Y?.accessKeyId&&Y?.secretAccessKey);this.client=new FZ(J?.region||"us-east-1",Z?{accessKeyId:Y.accessKeyId,secretAccessKey:Y.secretAccessKey,sessionToken:Y.sessionToken}:void 0)}return this.client}async send(J,Y){try{this.validateMessage(J);let Z;if(J.template){let N=await I(J.template,Y);if(N&&"html"in N)Z=N.html}let X=Z||J.html;if(!X&&!J.text)throw Error("Email must have either HTML or text content");let $=this.formatSourceAddress({address:J.from?.address||E.email.from?.address||"",name:J.from?.name||E.email.from?.name}),G=this.formatAddresses(J.to),K=this.formatAddresses(J.cc),Q=this.formatAddresses(J.bcc),F=J.replyTo?this.formatAddressList(J.replyTo):[],W=T(J.headers);if(!!(J.attachments&&J.attachments.length>0)||!!W){let N=YJ({from:$,to:G.join(", "),cc:K.length>0?K.join(", "):void 0,replyTo:F.length>0?F.join(", "):void 0,customHeaders:W,subject:J.subject,text:J.text,html:X,attachments:J.attachments,messageIdDomain:E.email.domain}),w=await this.getClient().sendRawEmail({source:$,destinations:[...G,...K,...Q],rawMessage:N});return this.handleSuccess(J,w.MessageId)}let q={};if(X)q.Html={Charset:E.email.charset||"UTF-8",Data:X};if(J.text)q.Text={Charset:E.email.charset||"UTF-8",Data:J.text};let B=await this.getClient().sendEmail({FromEmailAddress:$,Destination:{ToAddresses:G,CcAddresses:K,BccAddresses:Q},...F.length>0?{ReplyToAddresses:F}:{},Content:{Simple:{Subject:{Charset:E.email.charset||"UTF-8",Data:J.subject},Body:q}}});return this.handleSuccess(J,B.MessageId)}catch(Z){return this.handleError(this.enrichSesError(Z),J)}}formatSourceAddress(J){if(!J.name)return J.address;return`${/[",()<>[\]:;@\\]/.test(J.name)?`"${J.name.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")}"`:J.name} <${J.address}>`}enrichSesError(J){let Y=J instanceof Error?J:Error(String(J)),Z=`${Y.message} ${Y.name??""}`.toLowerCase(),X=E?.services?.ses?.region||"us-east-1";if(Z.includes("email address is not verified")||Z.includes("not authorized to send")||Z.includes("messagerejected")&&Z.includes("verified"))return Y.message=`${Y.message}
|
|
17
|
+
|
|
18
|
+
SES sandbox restriction: the From and (in sandbox) every To address must be verified. Verify identities in the SES console under "Verified identities" (region: ${X}), or request production access to lift the recipient restriction.`,Y;if(Z.includes("signaturedoesnotmatch")||Z.includes("invalidclienttokenid")||Z.includes("unable to locate credentials")||Z.includes("the security token included in the request is invalid"))return Y.message=`${Y.message}
|
|
19
|
+
|
|
20
|
+
SES authentication failed. Check AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY (or services.ses.credentials), and confirm the IAM principal has \`ses:SendEmail\` permission for the From identity.`,Y;if(Z.includes("could not be reached")||Z.includes("econnrefused")||Z.includes("enotfound"))return Y.message=`${Y.message}
|
|
21
|
+
|
|
22
|
+
SES endpoint unreachable. The configured region (\`${X}\`) must match the region where the From identity is verified.`,Y;return Y}}var zZ=o;import{config as u}from"@stacksjs/config";import{log as UZ}from"@stacksjs/logging";import{Buffer as JY}from"buffer";import VZ from"process";import*as n from"tls";import*as ZY from"net";import{config as x}from"@stacksjs/config";import{log as _}from"@stacksjs/logging";function YY(J,Y){if(typeof J!=="string"||!FJ.test(J))throw Error(`[smtp] Refusing to send: ${Y} envelope address contains forbidden characters or is malformed: ${JSON.stringify(J)}`)}class b extends H{static SMTP_TIMEOUT=30000;name="smtp";getConfig(){let J=x.services?.smtp,Y=VZ.env,Z=J?.host||Y.MAIL_HOST||"127.0.0.1",X=J?.port||(Y.MAIL_PORT?Number(Y.MAIL_PORT):void 0)||587,$=typeof x.email?.from?.address==="string"?x.email.from.address:"",G=J?.username||Y.MAIL_USERNAME||$||"",K=(G.includes("@")?G.split("@")[0]:G).toUpperCase().replace(/[^A-Z0-9]/g,"_"),Q=J?.password||Y.MAIL_PASSWORD||(K?Y[`MAIL_PASSWORD_${K}`]:void 0)||"",F=J?.encryption??Y.MAIL_ENCRYPTION??null;return{host:Z,port:X,username:G,password:Q,encryption:F==="tls"?"starttls":F||null}}async send(J,Y){let Z=this.getConfig();if(!Z.host||Z.host==="")throw Error("[SMTP] Host is not configured. Set MAIL_HOST in your .env file.");let X={provider:this.name,to:J.to,subject:J.subject,host:Z.host,port:Z.port};_.info("Sending email via SMTP...",X);try{this.validateMessage(J);let $;if(J.template){let q=await I(J.template,Y);if(q&&"html"in q)$=q.html}let G=$||J.html,K=J.from?.address||x.email.from?.address||"",Q=J.from?.name||x.email.from?.name||"",F=this.formatAddresses(J.to),W=this.formatAddressList(J.replyTo),z=YJ({from:Q?`${Q} <${K}>`:K,to:F.join(", "),cc:J.cc?this.formatAddresses(J.cc).join(", "):void 0,replyTo:W.length>0?W.join(", "):void 0,customHeaders:T(J.headers),subject:J.subject,text:J.text,html:G,attachments:J.attachments,messageIdDomain:x.email.domain}),V=await this.sendViaSMTP(Z,K,F,z);return this.handleSuccess(J,V)}catch($){return this.handleError($,J)}}async sendViaSMTP(J,Y,Z,X){return new Promise(($,G)=>{let K=setTimeout(()=>{G(Error(`SMTP connection timed out after ${b.SMTP_TIMEOUT}ms`))},b.SMTP_TIMEOUT),Q=$,F=G;$=(O)=>{clearTimeout(K),Q(O)},G=(O)=>{clearTimeout(K),F(O)};let W,z="",V="",q=[],B=!1,N=!1,w=(O)=>{if(_.debug(`[SMTP] Server: ${O.trim()}`),parseInt(O.substring(0,3),10)>=400){let L=Error(`SMTP Error: ${O.trim()}`);if(q.length>0)q.shift()?.reject(L);return}if(q.length>0)q.shift()?.resolve(O)},R=(O)=>{return new Promise((D,L)=>{q.push({cmd:O,resolve:D,reject:L}),_.debug(`[SMTP] Client: ${O}`),W.write(`${O}\r
|
|
23
|
+
`)})},EJ=(O)=>{z+=O.toString();let D=z.split(`\r
|
|
24
|
+
`);z=D.pop()||"";for(let L of D)if(L.length>=3){if(L.length===3||L[3]===" ")w(L)}},xJ=async()=>{try{await new Promise((L,S)=>{q.push({cmd:"GREETING",resolve:L,reject:S})});let O=await R(`EHLO ${x.email.domain||"localhost"}`);if(J.encryption==="starttls"&&!(W instanceof n.TLSSocket)){await R("STARTTLS");let L=W;L.removeAllListeners("data"),W=await new Promise((S,kY)=>{let a=n.connect({socket:L,host:J.host,servername:J.host},()=>{_.debug("[SMTP] TLS connection established"),S(a)});a.on("error",(s)=>{_.error("[SMTP] TLS socket error:",s),kY(s)}),a.on("data",EJ),a.on("close",(s)=>{_.debug(`[SMTP] TLS socket closed (hadError: ${s})`);while(q.length>0)q.shift()?.reject(Error("TLS connection closed unexpectedly"))})}),await R(`EHLO ${x.email.domain||"localhost"}`)}if(J.username&&J.password)await R("AUTH LOGIN"),await R(JY.from(J.username).toString("base64")),await R(JY.from(J.password).toString("base64"));YY(Y,"MAIL FROM");for(let L of Z)YY(L,"RCPT TO");await R(`MAIL FROM:<${Y}>`);for(let L of Z)await R(`RCPT TO:<${L}>`);await R("DATA"),W.write(`${X}\r
|
|
13
25
|
.\r
|
|
14
|
-
`),await new Promise((
|
|
26
|
+
`),await new Promise((L,S)=>{q.push({cmd:"DATA_END",resolve:L,reject:S})}),N=!0;let D=`${Date.now()}.${Math.random().toString(36).substring(2)}@${J.host}`;try{W.write(`QUIT\r
|
|
27
|
+
`)}catch{}W.end(),$(D)}catch(O){if(N){W.end();return}W.end(),G(O)}};if(J.encryption==="ssl")W=n.connect({host:J.host,port:J.port,servername:J.host},()=>{_.debug(`[SMTP] TLS connected to ${J.host}:${J.port}`),xJ()});else W=ZY.connect({host:J.host,port:J.port},()=>{_.debug(`[SMTP] Connected to ${J.host}:${J.port}`),xJ()});W.on("data",EJ),W.setTimeout(b.SMTP_TIMEOUT),W.on("timeout",()=>{W.destroy(Error(`SMTP socket timed out after ${b.SMTP_TIMEOUT}ms`))}),W.on("error",(O)=>{if(N)return;_.error(`[SMTP] Connection error to ${J.host}:${J.port}:`,O),G(O)}),W.on("close",(O)=>{if(_.debug(`[SMTP] Connection closed (hadError: ${O})`),N)return;while(q.length>0)q.shift()?.reject(Error("Connection closed unexpectedly"))})})}formatAddresses(J){if(!J)return[];if(typeof J==="string")return[J];return J.map((Y)=>{if(typeof Y==="string")return Y;return Y.address})}}import{db as $Y}from"@stacksjs/database";var XY=!1;function GY(){if(XY)return;XY=!0,console.warn("[email/idempotency] email_idempotency table missing \u2014 idempotency keys are accepted but NOT enforced. "+"Run migrations to enable dedup.")}function KY(J){let Y=(J?.message??"").toLowerCase();return Y.includes("no such table")||Y.includes("doesn't exist")||Y.includes("connection closed")||Y.includes("unable to open database")||Y.includes("econnrefused")||Y.includes("connection terminated")||Y.includes("no database")||Y.includes("database connection")}async function WY(J){try{let Y=await $Y.selectFrom("email_idempotency").where("idempotency_key","=",J).selectAll().executeTakeFirst();if(!Y)return null;return{success:Boolean(Y.success),message:`Idempotent replay \u2014 original send recorded ${Y.created_at}`,provider:String(Y.provider??"cache"),messageId:Y.message_id??void 0}}catch(Y){if(KY(Y))return GY(),null;throw Y}}async function QY(J,Y,Z){if(!Z.success)return;let X=Array.isArray(Y.to)?Y.to.map(($)=>typeof $==="string"?$:$.address).join(", "):typeof Y.to==="string"?Y.to:Y.to.address;try{await $Y.insertInto("email_idempotency").values({idempotency_key:J,message_id:Z.messageId??null,recipient:X,subject:Y.subject,provider:Z.provider,success:1,created_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute()}catch($){if(KY($)){GY();return}let G=$?.message??"";if(G.includes("UNIQUE constraint")||G.includes("Duplicate entry"))return;throw $}}import{db as i}from"@stacksjs/database";var FY=!1;function ZJ(){if(FY)return;FY=!0,console.warn("[email/suppression] email_suppressions table missing \u2014 suppression checks accepted but NOT enforced. "+"Run migrations to enable enforcement.")}function qZ(J){let Y=J?.message??"";return Y.includes("no such table")||Y.includes("doesn't exist")}function XJ(J){if(qZ(J))return!0;let Y=(J?.message??"").toLowerCase();return Y.includes("connection closed")||Y.includes("unable to open database")||Y.includes("econnrefused")||Y.includes("connection terminated")||Y.includes("no database")||Y.includes("database connection")}function $J(J){return String(J).trim().toLowerCase()}async function Z9(J,Y){let Z=$J(J);try{let X=i.selectFrom("email_suppressions").where("email","=",Z).select(["email"]);if(Y)X=X.where("type","=",Y);let $=await X.executeTakeFirst();return Boolean($)}catch(X){if(XJ(X))return ZJ(),!1;throw X}}async function OZ(J){let Y=$J(J);try{return await i.selectFrom("email_suppressions").where("email","=",Y).selectAll().execute()??[]}catch(Z){if(XJ(Z))return ZJ(),[];throw Z}}async function zY(J,Y,Z){let X=$J(J),$=new Date().toISOString().slice(0,19).replace("T"," ");try{await i.insertInto("email_suppressions").values({email:X,type:Y,reason:Z??null,created_at:$}).execute()}catch(G){if(XJ(G)){ZJ();return}let K=G?.message??"";if(K.includes("UNIQUE constraint")||K.includes("Duplicate entry"))return;throw G}}async function X9(J,Y){let Z=$J(J);try{await i.deleteFrom("email_suppressions").where("email","=",Z).where("type","=",Y).execute()}catch(X){if(XJ(X)){ZJ();return}throw X}}async function LZ(){try{let{config:J}=await import("@stacksjs/config"),Y=J?.email?.suppressionPolicy;if(Y==="strict"||Y==="transactional-allowed"||Y==="off")return Y}catch{}return"strict"}async function VY(J,Y){let Z=await LZ();if(Z==="off")return null;if(Z==="transactional-allowed"&&Y==="transactional")return null;let X=await OZ(J);if(X.length===0)return null;let $=["unsubscribe","complaint","bounce","manual"];for(let G of $)if(X.some((K)=>K.type===G))return G;return X[0].type}class NZ{name;subject;to;from;template;handle;onError;onSuccess;constructor(J){this.name=J.name,this.subject=J.subject,this.to=J.to,this.from=J.from,this.template=J.template,this.handle=J.handle,this.onError=J.onError,this.onSuccess=J.onSuccess}async renderTemplate(){if(!this.template)return"";try{let{path:J}=await import("@stacksjs/path"),Y=J.resourcesPath(`views/emails/${this.template}.html`),Z=Bun.file(Y);if(await Z.exists())return await Z.text()}catch{}if(this.template.includes("<"))return this.template;return`<p>${this.template}</p>`}async send(J){let Y=J??this.to,Z=Array.isArray(Y)?Y:Y?[Y]:[];if(Z.length===0)throw Error("No recipient specified for email");try{if(await GJ.send({to:Z,from:this.from||{name:u.email.from?.name||"Stacks",address:u.email.from?.address||"no-reply@stacksjs.com"},subject:this.subject,html:await this.renderTemplate()}),this.onSuccess)this.onSuccess();if(this.handle)return this.handle();return{message:"Email sent"}}catch(X){if(this.onError)return this.onError(X instanceof Error?X:Error(String(X)));throw X}}}class PJ{drivers=new Map;defaultDriver;constructor(J={}){this.defaultDriver=J.defaultDriver||u.email.default||"ses",this.registerDefaultDrivers()}registerDefaultDrivers(){this.drivers.set("log",new g),this.drivers.set("ses",new o),this.drivers.set("sendgrid",new m),this.drivers.set("mailgun",new d),this.drivers.set("mailtrap",new l),this.drivers.set("smtp",new b),this.drivers.set("capture",new e)}async send(J){let Y=this.drivers.get(this.defaultDriver);if(!Y){let G=[...this.drivers.keys()].sort().join(", ");throw Error(`Email driver '${this.defaultDriver}' is not registered. Available drivers: [${G}]. Check config.email.default or the MAIL_MAILER environment variable.`)}if(J.idempotencyKey){let G=await WY(J.idempotencyKey);if(G)return G}let Z=await IZ(J);if(Z)return{success:!1,message:`suppressed:${Z}`,provider:"suppression"};let X={name:u.email.from?.name||"Stacks",address:u.email.from?.address||"no-reply@stacksjs.com"},$=await Y.send({...J,from:J.from||X});if(J.idempotencyKey)await QY(J.idempotencyKey,J,$);return $}use(J){if(!this.drivers.has(J))throw Error(`Email driver '${J}' is not available`);return new PJ({defaultDriver:J})}async queue(J){await this.dispatchOrFallback(J,async()=>{let{job:Y}=await import("@stacksjs/queue");await Y("SendEmail",{message:J,driver:this.defaultDriver}).onQueue("emails").dispatch()},{context:"queue"})}async later(J,Y){await this.dispatchOrFallback(Y,async()=>{let{job:Z}=await import("@stacksjs/queue");await Z("SendEmail",{message:Y,driver:this.defaultDriver}).onQueue("emails").delay(J).dispatch()},{context:"later",delaySeconds:J})}async queueOn(J,Y){await this.dispatchOrFallback(Y,async()=>{let{job:Z}=await import("@stacksjs/queue");await Z("SendEmail",{message:Y,driver:this.defaultDriver}).onQueue(J).dispatch()},{context:"queueOn",queueName:J})}async dispatchOrFallback(J,Y,Z){try{await Y();return}catch(X){let $=X instanceof Error?X.message:String(X);UZ.warn("[email] Queue dispatch failed; falling back to synchronous send. "+"Background email pipeline is degraded \u2014 check the queue worker / broker.",{...Z,reason:$})}await this.send(J)}}async function IZ(J){let Y=HZ(J);if(Y.length===0)return null;for(let Z of Y){let X=await VY(Z,J.tag);if(X)return X}return null}function HZ(J){let Y=[],Z=(X)=>{if(typeof X==="string"){Y.push(X);return}if(X&&typeof X==="object"&&"address"in X&&typeof X.address==="string")Y.push(X.address)};for(let X of["to","cc","bcc"]){let $=J[X];if(!$)continue;if(Array.isArray($))for(let G of $)Z(G);else Z($)}return Y}var BJ;function qY(){if(!BJ){let J=u?.email?.default||process.env.MAIL_MAILER||"ses";BJ=new PJ({defaultDriver:J})}return BJ}var GJ=new Proxy({},{get(J,Y){return qY()[Y]},set(J,Y,Z){return qY()[Y]=Z,!0}});import{createHmac as UY,timingSafeEqual as RZ}from"crypto";import KJ from"process";import{Buffer as NY}from"buffer";var _Z=2592000,BZ="/_stacks/email/unsubscribe";function IY(){let J=KJ.env.APP_KEY;if(!J||J.length<16){if(KJ.env.APP_ENV==="production"||KJ.env.NODE_ENV==="production")throw Error("[email/unsubscribe] APP_KEY is missing or too short (need \u226516 chars). Cannot sign unsubscribe URL.")}return J||"stacks-default-key-dev-only-do-not-use-prod"}function OY(J){return J.toString("base64url")}function LY(J){return NY.from(J,"base64url")}function PZ(J,Y=_Z){if(!J)throw Error("[email/unsubscribe] email is required");let Z=Math.floor(Date.now()/1000)+Math.floor(Y),X={email:String(J).trim().toLowerCase(),exp:Z,iss:"stacks"},$=OY(NY.from(JSON.stringify(X))),G=OY(UY("sha256",IY()).update($).digest());return`${$}.${G}`}function _9(J){if(typeof J!=="string")return{valid:!1,reason:"malformed"};let Y=J.split(".");if(Y.length!==2)return{valid:!1,reason:"malformed"};let[Z,X]=Y,$=UY("sha256",IY()).update(Z).digest(),G;try{G=LY(X)}catch{return{valid:!1,reason:"malformed"}}if(G.length!==$.length||!RZ(G,$))return{valid:!1,reason:"bad_signature"};let K;try{K=JSON.parse(LY(Z).toString("utf8"))}catch{return{valid:!1,reason:"malformed"}}if(typeof K.exp!=="number"||Math.floor(Date.now()/1000)>=K.exp)return{valid:!1,reason:"expired"};if(!K.email||typeof K.email!=="string")return{valid:!1,reason:"malformed"};return{valid:!0,email:K.email}}function jZ(J,Y,Z={}){let X=PZ(J,Y),$=(Z.baseUrl||KJ.env.APP_URL||"http://localhost").replace(/\/$/,""),G=(Z.routePrefix||BZ).replace(/\/$/,"");return`${$}${G}/${X}`}function B9(J,Y,Z){return{"List-Unsubscribe":`<${jZ(J,Y,Z)}>`,"List-Unsubscribe-Post":"List-Unsubscribe=One-Click"}}import{db as DZ}from"@stacksjs/database";var HY=!1;function TZ(){if(HY)return;HY=!0,console.warn("[email/webhook-dedup] email_webhook_events table missing \u2014 webhook idempotency NOT enforced. "+"Providers may double-deliver retries; run migrations to enable dedup.")}function AZ(J){let Y=J?.message??"";return Y.includes("no such table")||Y.includes("doesn't exist")}async function r(J,Y){if(!Y)return!0;try{return await DZ.insertInto("email_webhook_events").values({provider:J,event_id:Y,processed_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute(),!0}catch(Z){if(AZ(Z))return TZ(),!0;let X=Z?.message??"";if(X.includes("UNIQUE constraint")||X.includes("Duplicate entry"))return!1;throw Z}}async function p(J,Y){try{let Z=await import("@stacksjs/events").catch(()=>null);if(!Z)return;let X=Z.dispatch;if(typeof X!=="function")return;X(J,Y)}catch{}}async function RY(J){await p("email:bounce-hard",J),await p("email:bounce",J)}async function _Y(J){await p("email:bounce-soft",J),await p("email:bounce",J)}async function BY(J){await p("email:complaint",J)}async function PY(J){await p("email:unsubscribe",J)}function jY(J){switch(J){case"bounce-hard":return"bounce";case"complaint":return"complaint";case"unsubscribe":return"unsubscribe";default:return null}}import{createHmac as EZ,createVerify as TY,timingSafeEqual as jJ}from"crypto";import{Buffer as M}from"buffer";function AY(J){if(!J.signingKey)return{ok:!1,reason:"missing-config"};if(!J.timestamp||!J.token||!J.signature)return{ok:!1,reason:"missing-signature"};let Y=J.toleranceSeconds??300,Z=Number(J.timestamp);if(!Number.isFinite(Z))return{ok:!1,reason:"bad-signature"};let X=Math.floor(Date.now()/1000);if(Math.abs(X-Z)>Y)return{ok:!1,reason:"expired"};let $=EZ("sha256",J.signingKey).update(`${J.timestamp}${J.token}`).digest("hex"),G;try{G=M.from(J.signature,"hex")}catch{return{ok:!1,reason:"bad-signature"}}let K=M.from($,"hex");if(G.length!==K.length)return{ok:!1,reason:"bad-signature"};if(!jJ(G,K))return{ok:!1,reason:"bad-signature"};return{ok:!0}}function EY(J){if(!J.expectedUsername||!J.expectedPassword)return{ok:!1,reason:"missing-config"};if(!J.authorizationHeader||!J.authorizationHeader.startsWith("Basic "))return{ok:!1,reason:"missing-signature"};let Y;try{Y=M.from(J.authorizationHeader.slice(6),"base64").toString("utf8")}catch{return{ok:!1,reason:"bad-signature"}}let[Z,X]=Y.split(":");if(!Z||X===void 0)return{ok:!1,reason:"bad-signature"};let $=DY(Z,J.expectedUsername),G=DY(X,J.expectedPassword);if(!$||!G)return{ok:!1,reason:"bad-signature"};if(J.ipAllowlist&&J.ipAllowlist.length>0){if(!J.sourceIp||!J.ipAllowlist.includes(J.sourceIp))return{ok:!1,reason:"bad-signature"}}return{ok:!0}}function DY(J,Y){let Z=M.from(J,"utf8"),X=M.from(Y,"utf8");if(Z.length!==X.length){let $=M.alloc(Math.max(Z.length,X.length));return jJ($,$),!1}return jJ(Z,X)}var xZ=/^sns\.[a-z0-9-]+\.amazonaws\.com$/;async function MZ(J){let Y=await fetch(J,{redirect:"error"});if(!Y.ok)throw Error(`SNS cert fetch returned ${Y.status}`);return await Y.text()}async function xY(J){let Y=J.message;if(!Y||!Y.Signature||!Y.SigningCertURL)return{ok:!1,reason:"missing-signature"};let Z=J.certUrlHostAllowlist??xZ,X;try{X=new URL(Y.SigningCertURL)}catch{return{ok:!1,reason:"untrusted-cert-url"}}if(X.protocol!=="https:")return{ok:!1,reason:"untrusted-cert-url"};if(!Z.test(X.host))return{ok:!1,reason:"untrusted-cert-url"};let $;try{$=await(J.fetchCert??MZ)(Y.SigningCertURL)}catch{return{ok:!1,reason:"cert-fetch-failed"}}let G=CZ(Y);if(!G)return{ok:!1,reason:"bad-signature"};let K;try{K=M.from(Y.Signature,"base64")}catch{return{ok:!1,reason:"bad-signature"}}let Q=Y.SignatureVersion==="2"?"SHA256":"SHA1",F=TY(`RSA-${Q}`);return F.update(G,"utf8"),F.verify($,K)?{ok:!0}:{ok:!1,reason:"bad-signature"}}function CZ(J){let Y=[];if(J.Type==="Notification"){if(Y.push("Message",J.Message),Y.push("MessageId",J.MessageId),J.Subject!==void 0)Y.push("Subject",J.Subject);Y.push("Timestamp",J.Timestamp),Y.push("TopicArn",J.TopicArn),Y.push("Type",J.Type)}else if(J.Type==="SubscriptionConfirmation"||J.Type==="UnsubscribeConfirmation"){if(Y.push("Message",J.Message),Y.push("MessageId",J.MessageId),!J.SubscribeURL||!J.Token)return null;Y.push("SubscribeURL",J.SubscribeURL),Y.push("Timestamp",J.Timestamp),Y.push("Token",J.Token),Y.push("TopicArn",J.TopicArn),Y.push("Type",J.Type)}else return null;let Z="";for(let X of Y)Z+=`${X}
|
|
28
|
+
`;return Z}function MY(J){if(!J.publicKeyPem)return{ok:!1,reason:"missing-config"};if(!J.signature||!J.timestamp)return{ok:!1,reason:"missing-signature"};let Y=J.toleranceSeconds??300,Z=Number(J.timestamp);if(!Number.isFinite(Z))return{ok:!1,reason:"bad-signature"};if(Math.abs(Math.floor(Date.now()/1000)-Z)>Y)return{ok:!1,reason:"expired"};let X;try{X=M.from(J.signature,"base64")}catch{return{ok:!1,reason:"bad-signature"}}let $=TY("SHA256");return $.update(`${J.timestamp}${J.body}`,"utf8"),$.verify(J.publicKeyPem,X)?{ok:!0}:{ok:!1,reason:"bad-signature"}}var DJ={status:200,body:{ok:!0,processed:!1,reason:"duplicate"}};function WJ(J){return{status:401,body:{ok:!1,reason:J}}}function C(J){return{status:400,body:{ok:!1,reason:J}}}async function c(J,Y){let Z=jY(J);if(Z)await zY(Y.email,Z,Y.reason);switch(J){case"bounce-hard":await RY(Y);break;case"bounce-soft":await _Y(Y);break;case"complaint":await BY(Y);break;case"unsubscribe":await PY(Y);break;case"delivered":break}}async function k9(J,Y){let Z;try{Z=JSON.parse(J)}catch{return C("invalid-json")}let X=Z.signature,$=Z["event-data"];if(!X||!$)return C("missing-fields");let G=AY({timestamp:X.timestamp,token:X.token,signature:X.signature,signingKey:Y.signingKey,toleranceSeconds:Y.toleranceSeconds});if(!G.ok)return WJ(G.reason);if(!await r("mailgun",$.id))return DJ;let Q=wZ($.event,$.severity);if(!Q)return{status:200,body:{ok:!0,processed:!1,reason:"unhandled-event"}};return await c(Q,{email:$.recipient,provider:"mailgun",reason:$.reason,raw:$}),{status:200,body:{ok:!0,processed:!0,classification:Q}}}function wZ(J,Y){switch(J){case"failed":return Y==="temporary"?"bounce-soft":"bounce-hard";case"complained":return"complaint";case"unsubscribed":return"unsubscribe";case"delivered":return"delivered";default:return null}}async function y9(J,Y,Z,X){let $=EY({authorizationHeader:Y,expectedUsername:X.username,expectedPassword:X.password,sourceIp:Z,ipAllowlist:X.ipAllowlist});if(!$.ok)return WJ($.reason);let G;try{G=JSON.parse(J)}catch{return C("invalid-json")}let K=String(G.ID??G.MessageID??""),Q=String(G.Email??G.Recipient??"");if(!Q)return C("missing-recipient");if(!await r("postmark",K))return DJ;let W=SZ(G);if(!W)return{status:200,body:{ok:!0,processed:!1,reason:"unhandled-event"}};return await c(W,{email:Q,provider:"postmark",reason:G.Description,raw:G}),{status:200,body:{ok:!0,processed:!0,classification:W}}}function SZ(J){switch(J.RecordType){case"Bounce":return J.TypeCode===1?"bounce-hard":"bounce-soft";case"SpamComplaint":return"complaint";case"SubscriptionChange":return J.SuppressSending?"unsubscribe":null;case"Delivery":return"delivered";default:return null}}async function b9(J,Y={}){let Z;try{Z=JSON.parse(J)}catch{return C("invalid-json")}let X=await xY({message:Z,certUrlHostAllowlist:Y.certUrlHostAllowlist,fetchCert:Y.fetchCert});if(!X.ok)return WJ(X.reason);if(Z.Type==="SubscriptionConfirmation"){if(Y.autoConfirmSubscriptions!==!1&&Z.SubscribeURL)try{await fetch(Z.SubscribeURL)}catch{}return{status:200,body:{ok:!0,processed:!0,reason:"subscription-confirmed"}}}if(Z.Type==="UnsubscribeConfirmation")return{status:200,body:{ok:!0,processed:!0,reason:"subscription-removed"}};let $;try{$=JSON.parse(Z.Message)}catch{return C("invalid-inner-json")}if(!await r("ses",Z.MessageId))return DJ;let K=[];if($.notificationType==="Bounce"&&$.bounce){let Q=$.bounce.bounceType==="Permanent"?"bounce-hard":"bounce-soft";for(let F of $.bounce.bouncedRecipients)await c(Q,{email:F.emailAddress,provider:"ses",reason:F.diagnosticCode,raw:$}),K.push(Q)}else if($.notificationType==="Complaint"&&$.complaint)for(let Q of $.complaint.complainedRecipients)await c("complaint",{email:Q.emailAddress,provider:"ses",reason:$.complaint.complaintFeedbackType,raw:$}),K.push("complaint");else if($.notificationType==="Delivery"&&$.delivery)for(let Q of $.delivery.recipients)await c("delivered",{email:Q,provider:"ses",raw:$}),K.push("delivered");return{status:200,body:{ok:!0,processed:K.length>0,classification:K[0]}}}async function v9(J,Y,Z,X){let $=MY({body:J,signature:Y,timestamp:Z,publicKeyPem:X.publicKeyPem,toleranceSeconds:X.toleranceSeconds});if(!$.ok)return WJ($.reason);let G;try{G=JSON.parse(J)}catch{return C("invalid-json")}if(!Array.isArray(G))return C("expected-array");let K=0,Q=[];for(let F of G){if(!F.email)continue;let W=F.sg_event_id??`${F.event}:${F.email}:${Date.now()}`;if(!await r("sendgrid",W))continue;let V=kZ(F.event,F.type);if(!V)continue;await c(V,{email:F.email,provider:"sendgrid",reason:F.reason,raw:F}),K++,Q.push(V)}return{status:200,body:{ok:!0,processed:K>0,classification:Q[0]}}}function kZ(J,Y){switch(J){case"bounce":return Y==="blocked"?"bounce-soft":"bounce-hard";case"dropped":return"bounce-hard";case"spamreport":return"complaint";case"unsubscribe":return"unsubscribe";case"group_unsubscribe":return"unsubscribe";case"delivered":return"delivered";default:return null}}import{config as CY}from"@stacksjs/config";class QJ{_to=[];_cc=[];_bcc=[];_replyTo;_from;_subject;_text;_html;_template;_attachments=[];to(J){return this._to=TJ(J),this}cc(J){return this._cc=TJ(J),this}bcc(J){return this._bcc=TJ(J),this}replyTo(J){return this._replyTo=typeof J==="string"?{address:J}:J,this}from(J){return this._from=J,this}subject(J){return this._subject=J,this}text(J){return this._text=J,this}html(J){return this._html=J,this}template(J,...Y){let Z=Y[0]??{};return this._template={name:J,props:Z},this}inspect(){return{to:this._to,cc:this._cc,bcc:this._bcc,from:this._from,replyTo:this._replyTo,subject:this._subject,text:this._text,html:this._html,template:this._template,attachments:this._attachments}}attach(J,Y){return this._attachments.push({filename:Y||bZ(J),content:`__file__:${J}`,encoding:"binary"}),this}attachData(J,Y,Z){return this._attachments.push({filename:Y,content:J,contentType:Z,encoding:typeof J==="string"?"utf8":"binary"}),this}async send(J={}){if(await Promise.resolve(this.build()),this._to.length===0)throw Error("[Mailable] no recipients \u2014 call this.to(...) inside build()");if(!this._subject)throw Error("[Mailable] no subject \u2014 call this.subject(...) inside build()");let Y=this._html,Z=this._text;if(this._template){let G=await I(this._template.name,{variables:this._template.props,subject:this._subject});if(!Y)Y=G.html;if(!Z)Z=G.text}let X={to:this._to,subject:this._subject,from:this._from||yZ(),...this._cc.length?{cc:this._cc}:{},...this._bcc.length?{bcc:this._bcc}:{},...Y?{html:Y}:{},...Z?{text:Z}:{},...this._attachments.length?{attachments:await vZ(this._attachments)}:{}};if(this._replyTo)X.replyTo=this._replyTo;return(J.driver?GJ.use(J.driver):GJ).send(X)}}function TJ(J){let Y=Array.isArray(J)?J:[J];if(Y.some((X)=>typeof X==="object"&&X!==null))return Y.map((X)=>typeof X==="string"?{address:X}:X);return Y}function yZ(){return{name:CY.email.from?.name||"Stacks",address:CY.email.from?.address||"no-reply@stacksjs.com"}}function bZ(J){let Y=Math.max(J.lastIndexOf("/"),J.lastIndexOf("\\"));return Y===-1?J:J.slice(Y+1)}async function vZ(J){return Promise.all(J.map(async(Y)=>{if(typeof Y.content==="string"&&Y.content.startsWith("__file__:")){let Z=Y.content.slice(9),X=Bun.file(Z),$=new Uint8Array(await X.arrayBuffer());return{...Y,content:$,contentType:Y.contentType||X.type||void 0,encoding:"binary"}}return Y}))}import{readdirSync as hZ}from"fs";import{join as fZ}from"path";import{existsSync as wY}from"@stacksjs/storage";import{userEmailsPath as uZ,userMailPath as pZ}from"@stacksjs/path";import{kebabCase as cZ}from"@stacksjs/strings";function gZ(){let J=pZ();if(!wY(J))return[];let Y=hZ(J,{withFileTypes:!0}),Z=[];for(let X of Y){if(!X.isFile())continue;if(!X.name.endsWith(".ts"))continue;if(X.name.endsWith(".d.ts"))continue;if(X.name.endsWith(".test.ts"))continue;if(X.name.endsWith(".spec.ts"))continue;let $=X.name.replace(/\.ts$/,"");Z.push({name:$,path:fZ(J,X.name),slug:cZ($)})}return Z.sort((X,$)=>X.name.localeCompare($.name)),Z}async function SY(J){let Y=uZ(`_previews/${J}.ts`);if(!wY(Y))return null;try{let Z=await import(Y),X=Z.default??Z.props??null;return X&&typeof X==="object"?X:null}catch(Z){return{__preview_error__:`Failed to load sample props: ${Z instanceof Error?Z.message:String(Z)}`}}}async function dZ(J){let Y={inspection:{to:[],cc:[],bcc:[],attachments:[]},html:"",text:"",sampleProps:null};try{let Z=await import(J.path),X=lZ(Z);if(!X)return{...Y,error:`No subclass of \`Mailable\` exported from ${J.path}.`};let $=await SY(J.slug),G=new X($??{});if(!(G instanceof QJ))return{...Y,sampleProps:$,error:"Constructor did not produce a Mailable instance."};await G.build();let K=G.inspect();if(!K.template)return{inspection:K,html:K.html??"",text:K.text??"",sampleProps:$};let Q=await I(K.template.name,{variables:K.template.props,subject:K.subject});return{inspection:K,html:Q.html,text:Q.text,sampleProps:$}}catch(Z){return{...Y,error:Z instanceof Error?`${Z.name}: ${Z.message}`:String(Z)}}}function lZ(J){let Y=[];if(J.default)Y.push(J.default);for(let Z of Object.keys(J))if(Z!=="default")Y.push(J[Z]);for(let Z of Y){if(typeof Z!=="function")continue;if(!(Z.prototype instanceof QJ))continue;return Z}return null}function U(J){return String(J??"").replace(/[&<>"']/g,(Y)=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[Y])}function t(J){if(!J?.length)return"\u2014";return J.map((Y)=>{if(typeof Y==="string")return U(Y);if(Y.name)return`${U(Y.name)} <${U(Y.address)}>`;return U(Y.address)}).join(", ")}function AJ(J,Y){return`<!DOCTYPE html>
|
|
29
|
+
<html lang="en">
|
|
30
|
+
<head>
|
|
31
|
+
<meta charset="UTF-8">
|
|
32
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
33
|
+
<title>${U(J)} \u2014 Stacks Mail Preview</title>
|
|
34
|
+
<style>
|
|
35
|
+
:root { color-scheme: light dark; }
|
|
36
|
+
* { box-sizing: border-box; }
|
|
37
|
+
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: #f4f4f5; color: #111827; line-height: 1.5; }
|
|
38
|
+
@media (prefers-color-scheme: dark) {
|
|
39
|
+
body { background: #0a0a0a; color: #ededed; }
|
|
40
|
+
.panel { background: #1a1a1a; border-color: #2a2a2a; }
|
|
41
|
+
a { color: #818cf8; }
|
|
42
|
+
.muted { color: #9ca3af; }
|
|
43
|
+
pre { background: #2a2a2a; color: #ededed; }
|
|
44
|
+
}
|
|
45
|
+
header { padding: 16px 24px; background: #fff; border-bottom: 1px solid #e5e7eb; }
|
|
46
|
+
@media (prefers-color-scheme: dark) { header { background: #1a1a1a; border-color: #2a2a2a; } }
|
|
47
|
+
header h1 { margin: 0; font-size: 16px; font-weight: 600; }
|
|
48
|
+
header h1 a { color: inherit; text-decoration: none; }
|
|
49
|
+
header .crumb { font-size: 13px; color: #6b7280; }
|
|
50
|
+
main { padding: 24px; max-width: 1200px; margin: 0 auto; }
|
|
51
|
+
.panel { background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px; margin-bottom: 16px; }
|
|
52
|
+
.grid { display: grid; grid-template-columns: 220px 1fr; gap: 16px; }
|
|
53
|
+
.pill { display: inline-block; padding: 2px 8px; font-size: 12px; border-radius: 999px; background: #eef2ff; color: #3730a3; }
|
|
54
|
+
.toolbar { display: flex; gap: 8px; padding: 8px 0; flex-wrap: wrap; align-items: center; }
|
|
55
|
+
.toolbar a { padding: 6px 12px; border: 1px solid #d1d5db; border-radius: 6px; text-decoration: none; color: inherit; font-size: 13px; }
|
|
56
|
+
.toolbar a.active { background: #4f46e5; color: white; border-color: #4f46e5; }
|
|
57
|
+
.muted { color: #6b7280; font-size: 13px; }
|
|
58
|
+
iframe { width: 100%; min-height: 800px; border: 0; background: #fff; border-radius: 8px; }
|
|
59
|
+
iframe.mobile { max-width: 375px; margin: 0 auto; display: block; min-height: 700px; }
|
|
60
|
+
pre { background: #f3f4f6; padding: 12px; border-radius: 6px; overflow: auto; font-size: 12px; line-height: 1.5; max-height: 400px; }
|
|
61
|
+
.meta { display: grid; grid-template-columns: 80px 1fr; gap: 6px 12px; font-size: 13px; }
|
|
62
|
+
.meta dt { color: #6b7280; }
|
|
63
|
+
.meta dd { margin: 0; }
|
|
64
|
+
.empty { text-align: center; padding: 48px 24px; color: #6b7280; }
|
|
65
|
+
ul.list { list-style: none; padding: 0; margin: 0; }
|
|
66
|
+
ul.list li { padding: 12px; border-bottom: 1px solid #e5e7eb; }
|
|
67
|
+
@media (prefers-color-scheme: dark) { ul.list li { border-color: #2a2a2a; } }
|
|
68
|
+
ul.list li:last-child { border-bottom: 0; }
|
|
69
|
+
ul.list a { color: inherit; text-decoration: none; font-weight: 500; }
|
|
70
|
+
ul.list a:hover { color: #4f46e5; }
|
|
71
|
+
.error { background: #fef2f2; border: 1px solid #fecaca; color: #991b1b; padding: 12px 16px; border-radius: 6px; }
|
|
72
|
+
@media (prefers-color-scheme: dark) { .error { background: #1f0a0a; border-color: #7f1d1d; color: #fca5a5; } }
|
|
73
|
+
</style>
|
|
74
|
+
</head>
|
|
75
|
+
<body>
|
|
76
|
+
<header>
|
|
77
|
+
<h1><a href="${"/_stacks/mail/preview"}">Mail Preview</a> <span class="crumb">${U(J)}</span></h1>
|
|
78
|
+
</header>
|
|
79
|
+
<main>${Y}</main>
|
|
80
|
+
</body>
|
|
81
|
+
</html>`}function mZ(J){if(J.length===0)return AJ("Index",`
|
|
82
|
+
<div class="empty panel">
|
|
83
|
+
<p>No Mailables found in <code>app/Mail/</code>.</p>
|
|
84
|
+
<p class="muted">Run <code>./buddy make:mail Welcome</code> to scaffold one.</p>
|
|
85
|
+
</div>
|
|
86
|
+
`);let Y=J.map((Z)=>`
|
|
87
|
+
<li>
|
|
88
|
+
<a href="/_stacks/mail/preview/${U(Z.slug)}">${U(Z.name)}</a>
|
|
89
|
+
<div class="muted">app/Mail/${U(Z.name)}.ts \xB7 template: ${U(Z.slug)}.stx</div>
|
|
90
|
+
</li>
|
|
91
|
+
`).join("");return AJ("Index",`
|
|
92
|
+
<div class="panel">
|
|
93
|
+
<p class="muted">${J.length} mailable${J.length===1?"":"s"} discovered. Add sample props at
|
|
94
|
+
<code>resources/emails/_previews/<slug>.ts</code> for richer previews.</p>
|
|
95
|
+
<ul class="list">${Y}</ul>
|
|
96
|
+
</div>
|
|
97
|
+
`)}function oZ(J,Y,Z="desktop"){let{inspection:X,text:$,sampleProps:G,error:K}=Y,Q=`/_stacks/mail/preview/${J.slug}`,F=`${Q}/raw`,W=K?`<div class="panel"><div class="error"><strong>${U(J.name)} failed to render:</strong> ${U(K)}</div></div>`:"",z=K?"":Z==="text"?`<div class="panel"><pre>${U($||"(no plain-text body)")}</pre></div>`:`<div class="panel"><iframe src="${F}" class="${Z==="mobile"?"mobile":""}" title="${U(J.name)} body" sandbox="allow-same-origin"></iframe></div>`,V=G?`<dd><code>resources/emails/_previews/${U(J.slug)}.ts</code></dd>`:`<dd class="muted">No sample file \u2014 edit <code>resources/emails/_previews/${U(J.slug)}.ts</code> to customize.</dd>`;return AJ(J.name,`
|
|
98
|
+
${W}
|
|
99
|
+
|
|
100
|
+
<div class="grid">
|
|
101
|
+
<aside>
|
|
102
|
+
<div class="panel">
|
|
103
|
+
<dl class="meta">
|
|
104
|
+
<dt>Subject</dt><dd>${U(X.subject||"(no subject)")}</dd>
|
|
105
|
+
<dt>To</dt><dd>${t(X.to)}</dd>
|
|
106
|
+
${X.cc?.length?`<dt>Cc</dt><dd>${t(X.cc)}</dd>`:""}
|
|
107
|
+
${X.bcc?.length?`<dt>Bcc</dt><dd>${t(X.bcc)}</dd>`:""}
|
|
108
|
+
${X.from?`<dt>From</dt><dd>${t([X.from])}</dd>`:""}
|
|
109
|
+
${X.replyTo?`<dt>Reply-To</dt><dd>${t([X.replyTo])}</dd>`:""}
|
|
110
|
+
${X.template?`<dt>Template</dt><dd>${U(X.template.name)}.stx</dd>`:""}
|
|
111
|
+
${X.attachments?.length?`<dt>Attachments</dt><dd>${X.attachments.length}</dd>`:""}
|
|
112
|
+
</dl>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div class="panel">
|
|
116
|
+
<h3 style="margin: 0 0 8px; font-size: 13px; font-weight: 600;">Sample props</h3>
|
|
117
|
+
${V}
|
|
118
|
+
${G?`<pre>${U(JSON.stringify(G,null,2))}</pre>`:""}
|
|
119
|
+
</div>
|
|
120
|
+
</aside>
|
|
121
|
+
|
|
122
|
+
<section>
|
|
123
|
+
<div class="toolbar">
|
|
124
|
+
<a href="${Q}" class="${Z==="desktop"?"active":""}">Desktop</a>
|
|
125
|
+
<a href="${Q}?view=mobile" class="${Z==="mobile"?"active":""}">Mobile</a>
|
|
126
|
+
<a href="${Q}?view=text" class="${Z==="text"?"active":""}">Text</a>
|
|
127
|
+
<span class="muted" style="margin-left: auto;">${U(J.name)}</span>
|
|
128
|
+
</div>
|
|
129
|
+
${z}
|
|
130
|
+
</section>
|
|
131
|
+
</div>
|
|
132
|
+
`)}export{_9 as verifyUnsubscribeToken,xY as verifySesSnsSignature,MY as verifySendgridSignature,EY as verifyPostmarkAuth,AY as verifyMailgunSignature,X9 as unsuppress,FX as templateExists,I as template,jY as suppressionTypeFor,zY as suppress,zJ as shouldInlineByDefault,eJ as ses,tJ as sendgrid,oY as safe,oZ as renderPreviewHtml,dZ as renderMailablePreview,mZ as renderIndexHtml,QX as renderHtml,r as recordWebhookEventOrSkip,QY as recordEmailIdempotency,rJ as mailtrap,iJ as mailgun,GJ as mail,oJ as log,SY as loadSampleProps,zX as listTemplates,Z9 as isSuppressed,JJ as inlineCss,b9 as handleSesWebhook,v9 as handleSendgridWebhook,y9 as handlePostmarkWebhook,k9 as handleMailgunWebhook,OZ as getSuppressions,LZ as getSuppressionPolicy,WY as findEmailByIdempotencyKey,PY as emitEmailUnsubscribe,BY as emitEmailComplaint,_Y as emitEmailBounceSoft,RY as emitEmailBounceHard,gZ as discoverMailables,PZ as createUnsubscribeToken,VY as checkSuppressionFor,SJ as capture,jZ as buildUnsubscribeUrl,B9 as buildListUnsubscribeHeaders,LJ as SafeHtml,QJ as Mailable,PJ as Mail,NZ as Email};
|
package/dist/mailable.d.ts
CHANGED
|
@@ -12,9 +12,30 @@ export declare interface MailableSendOptions {
|
|
|
12
12
|
* Internal stash for template rendering โ populated by {@link Mailable.template}
|
|
13
13
|
* and consumed in {@link Mailable.send} after `build()` resolves.
|
|
14
14
|
*/
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Read-only snapshot of a Mailable's build state โ produced by
|
|
17
|
+
* {@link Mailable.inspect}. Used by the preview server (#1900) so the
|
|
18
|
+
* UI can render what the email WOULD look like without dispatching.
|
|
19
|
+
*
|
|
20
|
+
* Generic over `TProps` so typed Mailables expose typed template
|
|
21
|
+
* props (stacksjs/stacks#1903). Defaults to `Record<string, unknown>`
|
|
22
|
+
* so untyped usages keep working without changes.
|
|
23
|
+
*/
|
|
24
|
+
export declare interface MailableInspection<TProps extends Record<string, unknown> = Record<string, unknown>> {
|
|
25
|
+
to: string[] | EmailAddress[]
|
|
26
|
+
cc: string[] | EmailAddress[]
|
|
27
|
+
bcc: string[] | EmailAddress[]
|
|
28
|
+
from?: EmailAddress
|
|
29
|
+
replyTo?: EmailAddress
|
|
30
|
+
subject?: string
|
|
31
|
+
text?: string
|
|
32
|
+
html?: string
|
|
33
|
+
template?: { name: string, props: TProps }
|
|
34
|
+
attachments: EmailAttachment[]
|
|
35
|
+
}
|
|
36
|
+
declare interface TemplateRef<TProps extends Record<string, unknown> = Record<string, unknown>> {
|
|
16
37
|
name: string
|
|
17
|
-
props:
|
|
38
|
+
props: TProps
|
|
18
39
|
}
|
|
19
40
|
/**
|
|
20
41
|
* Allowed recipient input โ accepts a single address, an array of addresses,
|
|
@@ -22,6 +43,19 @@ declare interface TemplateRef {
|
|
|
22
43
|
* `address` only (no display name).
|
|
23
44
|
*/
|
|
24
45
|
export type MailableAddressInput = string | string[] | EmailAddress | EmailAddress[];
|
|
46
|
+
/**
|
|
47
|
+
* `true` iff `T` is the loose default (`Record<string, unknown>`),
|
|
48
|
+
* i.e. the caller didn't specialize the generic. Used to make
|
|
49
|
+
* `Mailable#template(name)` props-optional in the loose case and
|
|
50
|
+
* props-required when a concrete `TProps` is supplied.
|
|
51
|
+
*/
|
|
52
|
+
declare type IsLooseProps<T> = Record<string, unknown> extends T ? true : false;
|
|
53
|
+
/**
|
|
54
|
+
* Builds the variadic tail of {@link Mailable.template}'s parameter list.
|
|
55
|
+
* Untyped Mailable accepts `.template('name')`; typed `Mailable<P>` must
|
|
56
|
+
* pass props matching `P`.
|
|
57
|
+
*/
|
|
58
|
+
declare type TemplateArgs<T extends Record<string, unknown>> = IsLooseProps<T> extends true ? [props?: T] : [props: T];
|
|
25
59
|
/**
|
|
26
60
|
* Laravel-style class-based email definition. Subclass `Mailable`,
|
|
27
61
|
* implement `build()`, and call `.send()` to dispatch.
|
|
@@ -54,7 +88,7 @@ export type MailableAddressInput = string | string[] | EmailAddress | EmailAddre
|
|
|
54
88
|
* await new WelcomeMail(user).send()
|
|
55
89
|
* ```
|
|
56
90
|
*/
|
|
57
|
-
export declare abstract class Mailable {
|
|
91
|
+
export declare abstract class Mailable<TProps extends Record<string, unknown> = Record<string, unknown>> {
|
|
58
92
|
protected _to: string[] | EmailAddress[];
|
|
59
93
|
protected _cc: string[] | EmailAddress[];
|
|
60
94
|
protected _bcc: string[] | EmailAddress[];
|
|
@@ -63,7 +97,7 @@ export declare abstract class Mailable {
|
|
|
63
97
|
protected _subject?: string;
|
|
64
98
|
protected _text?: string;
|
|
65
99
|
protected _html?: string;
|
|
66
|
-
protected _template?: TemplateRef
|
|
100
|
+
protected _template?: TemplateRef<TProps>;
|
|
67
101
|
protected _attachments: EmailAttachment[];
|
|
68
102
|
abstract build(): this | Promise<this>;
|
|
69
103
|
to(address: MailableAddressInput): this;
|
|
@@ -74,7 +108,8 @@ export declare abstract class Mailable {
|
|
|
74
108
|
subject(s: string): this;
|
|
75
109
|
text(body: string): this;
|
|
76
110
|
html(body: string): this;
|
|
77
|
-
template(name: string,
|
|
111
|
+
template(name: string, ...rest: TemplateArgs<TProps>): this;
|
|
112
|
+
inspect(): MailableInspection<TProps>;
|
|
78
113
|
attach(path: string, name?: string): this;
|
|
79
114
|
attachData(buffer: Uint8Array | string, name: string, mime?: string): this;
|
|
80
115
|
send(options?: MailableSendOptions): Promise<EmailResult>;
|
package/dist/mime.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { EmailAttachment } from '@stacksjs/types';
|
|
2
|
+
/**
|
|
3
|
+
* Encode an SMTP header value with RFC 2047 base64 encoding when it
|
|
4
|
+
* contains non-ASCII characters. Without this, subjects like
|
|
5
|
+
* "Encore d'idรฉes" or "ไฝ ๅฅฝ" produce headers that violate RFC 5322
|
|
6
|
+
* (which mandates 7-bit ASCII for headers) and get mangled or rejected
|
|
7
|
+
* by downstream relays.
|
|
8
|
+
*/
|
|
9
|
+
export declare function encodeRfc2047IfNeeded(value: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Build the full raw bytes for an RFC 5322 / 2045 email message.
|
|
12
|
+
*
|
|
13
|
+
* Returns a single string with CRLF line endings โ both SMTP DATA and
|
|
14
|
+
* SES `SendRawEmail.RawMessage.Data` consume this shape directly.
|
|
15
|
+
*
|
|
16
|
+
* Shape decision tree:
|
|
17
|
+
*
|
|
18
|
+
* no attachments:
|
|
19
|
+
* - both html+text โ multipart/alternative
|
|
20
|
+
* - one of either โ single-part
|
|
21
|
+
* with attachments:
|
|
22
|
+
* - multipart/mixed envelope wrapping the body (alternative or
|
|
23
|
+
* single-part) + one part per attachment
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildMimeMessage(options: MimeMessageOptions): string;
|
|
26
|
+
export declare interface MimeMessageOptions {
|
|
27
|
+
from: string
|
|
28
|
+
to: string
|
|
29
|
+
cc?: string
|
|
30
|
+
replyTo?: string
|
|
31
|
+
subject: string
|
|
32
|
+
text?: string
|
|
33
|
+
html?: string
|
|
34
|
+
attachments?: EmailAttachment[]
|
|
35
|
+
messageIdDomain?: string
|
|
36
|
+
customHeaders?: Record<string, string>
|
|
37
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DiscoveredMailable, MailablePreview } from './preview';
|
|
2
|
+
/**
|
|
3
|
+
* Render the index page listing every discovered Mailable.
|
|
4
|
+
*/
|
|
5
|
+
export declare function renderIndexHtml(mailables: DiscoveredMailable[]): string;
|
|
6
|
+
/**
|
|
7
|
+
* Render the preview page for a single Mailable. `view` controls the
|
|
8
|
+
* iframe width: 'desktop' (default) / 'mobile' / 'text' (renders the
|
|
9
|
+
* plain-text version instead of HTML).
|
|
10
|
+
*/
|
|
11
|
+
export declare function renderPreviewHtml(mailable: DiscoveredMailable, preview: MailablePreview, view?: 'desktop' | 'mobile' | 'text'): string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { MailableInspection } from './mailable';
|
|
2
|
+
/*.ts` and return the discovered Mailables. Non-`.ts`
|
|
3
|
+
* files (test files, `.d.ts`, etc.) are skipped.
|
|
4
|
+
*
|
|
5
|
+
* Filesystem scan is synchronous + cheap (one directory, no recursion)
|
|
6
|
+
* โ fine for a dev-only preview surface; not a hot path.
|
|
7
|
+
*/
|
|
8
|
+
export declare function discoverMailables(): DiscoveredMailable[];
|
|
9
|
+
/**
|
|
10
|
+
* Look up sample props for a Mailable. Convention:
|
|
11
|
+
*
|
|
12
|
+
* resources/emails/_previews/<slug>.ts
|
|
13
|
+
* โ default export is the props object passed to `new Mailable(props)`
|
|
14
|
+
*
|
|
15
|
+
* Returns `null` when no sample file is present โ the caller falls back
|
|
16
|
+
* to instantiating with `{}` so apps without samples still render
|
|
17
|
+
* (defaulted props in the Mailable's build() show their fallback values).
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadSampleProps(slug: string): Promise<Record<string, unknown> | null>;
|
|
20
|
+
/**
|
|
21
|
+
* Render a Mailable to its preview payload. Imports the source file,
|
|
22
|
+
* picks the first exported subclass of `Mailable`, instantiates with
|
|
23
|
+
* sample props (or `{}` when none exist), runs `build()`, and resolves
|
|
24
|
+
* the bound template through `template()` from this same package.
|
|
25
|
+
*
|
|
26
|
+
* Errors at any step (import failure, no Mailable export, build()
|
|
27
|
+
* throws, template not found) are returned as `error` on the preview
|
|
28
|
+
* payload rather than thrown โ the route layer renders them as part
|
|
29
|
+
* of the UI so the user sees what broke.
|
|
30
|
+
*/
|
|
31
|
+
export declare function renderMailablePreview(mailable: DiscoveredMailable): Promise<MailablePreview>;
|
|
32
|
+
/**
|
|
33
|
+
* One Mailable discovered under `app/Mail/`.
|
|
34
|
+
*/
|
|
35
|
+
export declare interface DiscoveredMailable {
|
|
36
|
+
name: string
|
|
37
|
+
path: string
|
|
38
|
+
slug: string
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The fully-rendered preview of one Mailable. Returned by
|
|
42
|
+
* {@link renderMailablePreview} and consumed by the preview HTML.
|
|
43
|
+
*/
|
|
44
|
+
export declare interface MailablePreview {
|
|
45
|
+
inspection: MailableInspection
|
|
46
|
+
html: string
|
|
47
|
+
text: string
|
|
48
|
+
sampleProps: Record<string, unknown> | null
|
|
49
|
+
error?: string
|
|
50
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is the address suppressed? Pass a specific `type` to check only
|
|
3
|
+
* one kind (e.g. unsubscribe-only โ bounces from the same address
|
|
4
|
+
* don't count). Omit to match any suppression.
|
|
5
|
+
*
|
|
6
|
+
* Returns `false` when the table doesn't exist yet โ apps that
|
|
7
|
+
* haven't run the migration aren't broken by the new behavior, and
|
|
8
|
+
* a one-shot warn lets the operator know the table is missing.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isSuppressed(email: string, type?: SuppressionType): Promise<boolean>;
|
|
11
|
+
/**
|
|
12
|
+
* Look up the full suppression record(s) for an address โ useful
|
|
13
|
+
* for admin tools that want to show the reason/timestamp alongside
|
|
14
|
+
* the suppression status. Returns an empty array on missing-table
|
|
15
|
+
* (same warn-once degrade).
|
|
16
|
+
*/
|
|
17
|
+
export declare function getSuppressions(email: string): Promise<SuppressionRecord[]>;
|
|
18
|
+
/**
|
|
19
|
+
* Record a suppression. Idempotent โ a duplicate (email, type)
|
|
20
|
+
* pair silently no-ops (the unique constraint catches it).
|
|
21
|
+
*
|
|
22
|
+
* Called by:
|
|
23
|
+
* - the framework's bounce/complaint webhook handlers (#1881)
|
|
24
|
+
* - the unsubscribe route handler (this PR)
|
|
25
|
+
* - admin tooling (`SuppressionType: 'manual'`)
|
|
26
|
+
*/
|
|
27
|
+
export declare function suppress(email: string, type: SuppressionType, reason?: string): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Remove a suppression record (admin recovery, user-initiated
|
|
30
|
+
* resubscribe). Idempotent โ removing something that isn't there
|
|
31
|
+
* is a no-op.
|
|
32
|
+
*/
|
|
33
|
+
export declare function unsuppress(email: string, type: SuppressionType): Promise<void>;
|
|
34
|
+
export declare function getSuppressionPolicy(): Promise<SuppressionPolicy>;
|
|
35
|
+
/**
|
|
36
|
+
* Decide whether a `mail.send()` should proceed for a given
|
|
37
|
+
* recipient. Returns `null` to indicate "allowed"; otherwise
|
|
38
|
+
* returns the matched suppression type so the caller can surface
|
|
39
|
+
* it in the error message.
|
|
40
|
+
*
|
|
41
|
+
* Called from `Mail.send()` after idempotency lookup, before the
|
|
42
|
+
* driver dispatch.
|
|
43
|
+
*/
|
|
44
|
+
export declare function checkSuppressionFor(email: string, tag: 'transactional' | 'broadcast' | undefined): Promise<SuppressionType | null>;
|
|
45
|
+
export declare interface SuppressionRecord {
|
|
46
|
+
email: string
|
|
47
|
+
type: SuppressionType
|
|
48
|
+
reason: string | null
|
|
49
|
+
created_at: string
|
|
50
|
+
}
|
|
51
|
+
export type SuppressionType = 'bounce' | 'complaint' | 'unsubscribe' | 'manual';
|
|
52
|
+
/**
|
|
53
|
+
* Suppression-policy resolution for `mail.send()` (stacksjs/stacks#1880).
|
|
54
|
+
*
|
|
55
|
+
* Reads the policy from `config.email.suppressionPolicy` with a
|
|
56
|
+
* sensible default and decides whether the message should be
|
|
57
|
+
* allowed through given its tag.
|
|
58
|
+
*
|
|
59
|
+
* Policy semantics:
|
|
60
|
+
* - `'strict'` โ block all sends to suppressed addresses
|
|
61
|
+
* - `'transactional-allowed'` โ block broadcasts; allow `tag: 'transactional'`
|
|
62
|
+
* - `'off'` โ never block (table is only used for tracking)
|
|
63
|
+
*
|
|
64
|
+
* Default is `'strict'` โ the safest behavior for compliance, and
|
|
65
|
+
* apps that don't run the migration are unaffected because the
|
|
66
|
+
* lookup falls through to "not suppressed" when the table is
|
|
67
|
+
* missing.
|
|
68
|
+
*/
|
|
69
|
+
export type SuppressionPolicy = 'strict' | 'transactional-allowed' | 'off';
|
package/dist/template.d.ts
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mark a string as pre-rendered HTML so {@link replaceVariables} splices
|
|
3
|
+
* it in verbatim instead of escaping. Use ONLY for content that you
|
|
4
|
+
* authored (or that came from a trusted renderer like the framework's
|
|
5
|
+
* own layout-slot resolution) โ never for user input.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* mail.send({
|
|
10
|
+
* template: 'invoice',
|
|
11
|
+
* variables: {
|
|
12
|
+
* // User-supplied โ escaped automatically (good)
|
|
13
|
+
* userName: req.input('name'),
|
|
14
|
+
* // Pre-rendered HTML you built yourself โ opt out of escaping
|
|
15
|
+
* invoiceTable: safe(renderInvoiceTable(rows)),
|
|
16
|
+
* },
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function safe(html: string): SafeHtml;
|
|
1
21
|
/**
|
|
2
22
|
* Render an email template with optional layout
|
|
3
23
|
*
|
|
@@ -6,7 +26,15 @@
|
|
|
6
26
|
* server scripts, etc.). When an .html template is found, it uses
|
|
7
27
|
* simple {{ variable }} replacement with layout wrapping.
|
|
8
28
|
*
|
|
9
|
-
*
|
|
29
|
+
* Templates resolve from userland `resources/emails/` first, then
|
|
30
|
+
* fall back to the framework-shipped defaults in
|
|
31
|
+
* `storage/framework/defaults/resources/emails/` โ so the prebaked
|
|
32
|
+
* mailers (password-reset, password-changed, email-verification)
|
|
33
|
+
* work out of the box on a default install while any userland file
|
|
34
|
+
* with the same name always wins (stacksjs/stacks#1944).
|
|
35
|
+
*
|
|
36
|
+
* Within each directory, .stx templates are preferred over .html
|
|
37
|
+
* when both exist.
|
|
10
38
|
*
|
|
11
39
|
* @example
|
|
12
40
|
* ```typescript
|
|
@@ -48,8 +76,24 @@ export declare interface TemplateOptions {
|
|
|
48
76
|
variables?: TemplateVariables
|
|
49
77
|
layout?: string | false
|
|
50
78
|
subject?: string
|
|
79
|
+
inline?: boolean
|
|
51
80
|
}
|
|
52
81
|
/** Allowed types for email template variable values */
|
|
53
|
-
export type TemplateVariableValue = string | number | boolean | undefined | null;
|
|
82
|
+
export type TemplateVariableValue = string | number | boolean | undefined | null | SafeHtml;
|
|
54
83
|
/** Map of variable names to their values for template replacement */
|
|
55
84
|
export type TemplateVariables = Record<string, TemplateVariableValue>;
|
|
85
|
+
/**
|
|
86
|
+
* Marker wrapper for variable values that contain pre-rendered HTML and
|
|
87
|
+
* should NOT be escaped during {@link replaceVariables}. Constructed via
|
|
88
|
+
* the {@link safe} helper.
|
|
89
|
+
*
|
|
90
|
+
* Anything that isn't a `SafeHtml` instance (or `safe`-marked) is treated
|
|
91
|
+
* as untrusted text and runs through HTML escaping โ this is the M-1
|
|
92
|
+
* fix for stacksjs/stacks#1871 (template XSS via unescaped variable
|
|
93
|
+
* interpolation).
|
|
94
|
+
*/
|
|
95
|
+
export declare class SafeHtml {
|
|
96
|
+
readonly __safeHtml: unknown;
|
|
97
|
+
public readonly value: string;
|
|
98
|
+
constructor(value: string);
|
|
99
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mint a signed unsubscribe token for `email`. Default expiry is
|
|
3
|
+
* 30 days โ long enough that the link in an archived email still
|
|
4
|
+
* works months later, short enough that a leaked URL doesn't grant
|
|
5
|
+
* indefinite control. Tighter caps available via `ttlSeconds`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createUnsubscribeToken(email: string, ttlSeconds?: number): string;
|
|
8
|
+
/**
|
|
9
|
+
* Verify a signed unsubscribe token. Returns the email + a
|
|
10
|
+
* discriminated outcome โ callers map invalid results to a 400/410
|
|
11
|
+
* response and the success case writes the suppression record.
|
|
12
|
+
*/
|
|
13
|
+
export declare function verifyUnsubscribeToken(token: string): UnsubscribeVerification;
|
|
14
|
+
/**
|
|
15
|
+
* Build the full opt-out URL for `email`. Combines the configured
|
|
16
|
+
* route prefix (`email.unsubscribeRoute`, defaults to
|
|
17
|
+
* `/_stacks/email/unsubscribe`) with the app's public URL (`APP_URL`
|
|
18
|
+
* env var) and the signed token.
|
|
19
|
+
*
|
|
20
|
+
* Pass the result into email bodies / `List-Unsubscribe` headers โ
|
|
21
|
+
* see {@link buildListUnsubscribeHeaders} for RFC 8058
|
|
22
|
+
* (one-click) compatibility.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildUnsubscribeUrl(email: string, ttlSeconds?: number, options?: { baseUrl?: string, routePrefix?: string }): string;
|
|
25
|
+
/**
|
|
26
|
+
* Build a `List-Unsubscribe` / `List-Unsubscribe-Post` header
|
|
27
|
+
* pair (RFC 8058). Gmail/Apple Mail use these for the native
|
|
28
|
+
* "Unsubscribe" button โ without them the user has to find your
|
|
29
|
+
* footer link.
|
|
30
|
+
*
|
|
31
|
+
* Returns a map suitable for passing into `EmailMessage.headers`
|
|
32
|
+
* (or merging with existing headers).
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildListUnsubscribeHeaders(email: string, ttlSeconds?: number, options?: { baseUrl?: string, routePrefix?: string }): Record<string, string>;
|
|
35
|
+
export declare interface UnsubscribeVerification {
|
|
36
|
+
valid: boolean
|
|
37
|
+
reason?: 'malformed' | 'bad_signature' | 'expired'
|
|
38
|
+
email?: string
|
|
39
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Throw if `addr` isn't a clean envelope address. The error message
|
|
3
|
+
* includes the role (`to` / `cc` / etc.) so log scrapers can grep for
|
|
4
|
+
* the offending slot without parsing the rest.
|
|
5
|
+
*/
|
|
6
|
+
export declare function assertEnvelopeAddress(addr: unknown, role: string): void;
|
|
7
|
+
/**
|
|
8
|
+
* Reject subject lines containing CR or LF โ they become header
|
|
9
|
+
* fields on the wire, and a newline in the value lets an attacker
|
|
10
|
+
* inject additional headers (BCC leak, Reply-To override).
|
|
11
|
+
*
|
|
12
|
+
* Centralized here so the check applies uniformly across drivers
|
|
13
|
+
* (stacksjs/stacks#1871 M-6).
|
|
14
|
+
*/
|
|
15
|
+
export declare function assertHeaderSafeSubject(subject: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Filter a `message.headers` map down to entries whose value is a
|
|
18
|
+
* string and whose name/value contain no CR/LF (header injection
|
|
19
|
+
* vector). Returns undefined when nothing usable remains so caller-
|
|
20
|
+
* sites can spread the result without sending an empty `headers: {}`
|
|
21
|
+
* payload field.
|
|
22
|
+
*
|
|
23
|
+
* Used by every driver that consumes {@link EmailMessage.headers}
|
|
24
|
+
* (stacksjs/stacks#1871 M-5). Centralizing the CR/LF guard ensures
|
|
25
|
+
* the SES `Headers` slot, the SendGrid `headers` field, and the
|
|
26
|
+
* Mailtrap `headers` map all reject the same injection-shaped values.
|
|
27
|
+
*/
|
|
28
|
+
export declare function filterStringHeaders(headers: Record<string, string> | undefined): Record<string, string> | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Email validation primitives shared by every driver + the base
|
|
31
|
+
* driver class. Pulled into a module so the `ENVELOPE_ADDRESS` regex
|
|
32
|
+
* lives in exactly one place โ the audit (stacksjs/stacks#1871 M-6)
|
|
33
|
+
* called out a copy in `drivers/base.ts` and a copy in `drivers/smtp.ts`,
|
|
34
|
+
* and they were already starting to drift.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* RFC 5321-ish envelope-address shape. Intentionally tighter than the
|
|
38
|
+
* full RFC because the broader form (display names, comments, source
|
|
39
|
+
* routes) has no business in the envelope slots that hit the wire as
|
|
40
|
+
* raw header values: `to` / `cc` / `bcc` / `from` / `replyTo`.
|
|
41
|
+
*
|
|
42
|
+
* Rejects:
|
|
43
|
+
* - whitespace, including CR / LF / tab (header-injection vectors)
|
|
44
|
+
* - angle brackets / quotes / backslashes (header parser confusion)
|
|
45
|
+
*
|
|
46
|
+
* Requires a single `@` separating local part and domain.
|
|
47
|
+
*/
|
|
48
|
+
export declare const ENVELOPE_ADDRESS: unknown;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Record an event-id as processed. Returns `true` if this is the
|
|
3
|
+
* first time we've seen the id (caller should process), `false`
|
|
4
|
+
* if it's a duplicate (caller should ack-and-skip).
|
|
5
|
+
*
|
|
6
|
+
* When the table doesn't exist, falls through to "always first" +
|
|
7
|
+
* warn-once โ apps without the migration still work; they just
|
|
8
|
+
* double-process if the provider retries.
|
|
9
|
+
*/
|
|
10
|
+
export declare function recordWebhookEventOrSkip(provider: 'mailgun' | 'postmark' | 'ses' | 'sendgrid', eventId: string): Promise<boolean>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SuppressionType } from './suppression';
|
|
2
|
+
/** Fired when a provider reports a hard bounce. */
|
|
3
|
+
export declare function emitEmailBounceHard(payload: EmailEventPayload): Promise<void>;
|
|
4
|
+
/** Fired when a provider reports a soft bounce (transient). */
|
|
5
|
+
export declare function emitEmailBounceSoft(payload: EmailEventPayload): Promise<void>;
|
|
6
|
+
/** Fired when a provider reports a complaint (user marked spam). */
|
|
7
|
+
export declare function emitEmailComplaint(payload: EmailEventPayload): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Fired when the user clicks the framework's signed unsubscribe URL
|
|
10
|
+
* AND when a provider's "unsubscribed" webhook event arrives. Both
|
|
11
|
+
* sources land in the same listener so apps only have to wire up
|
|
12
|
+
* one code path.
|
|
13
|
+
*/
|
|
14
|
+
export declare function emitEmailUnsubscribe(payload: EmailEventPayload): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Map a classified event to the suppression type that should be
|
|
17
|
+
* recorded. Soft bounces are intentionally NOT auto-suppressed โ
|
|
18
|
+
* they're transient and the next send is likely to succeed.
|
|
19
|
+
*/
|
|
20
|
+
export declare function suppressionTypeFor(classification: EmailEventClassification): SuppressionType | null;
|
|
21
|
+
/**
|
|
22
|
+
* Payload shape every email-event listener receives. Always
|
|
23
|
+
* includes the recipient + provider + the provider's raw event
|
|
24
|
+
* payload so listeners can branch on provider-specific fields
|
|
25
|
+
* without re-parsing.
|
|
26
|
+
*/
|
|
27
|
+
export declare interface EmailEventPayload {
|
|
28
|
+
email: string
|
|
29
|
+
provider: 'mailgun' | 'postmark' | 'ses' | 'sendgrid'
|
|
30
|
+
reason?: string
|
|
31
|
+
raw: unknown
|
|
32
|
+
}
|
|
33
|
+
export type EmailBounceType = 'hard' | 'soft';
|
|
34
|
+
export type EmailEventClassification = 'bounce-hard' | 'bounce-soft' | 'complaint' | 'unsubscribe' | 'delivered';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { EmailEventClassification } from './webhook-events';
|
|
2
|
+
export declare function handleMailgunWebhook(rawBody: string, config: MailgunWebhookConfig): Promise<WebhookResult>;
|
|
3
|
+
export declare function handlePostmarkWebhook(rawBody: string, authorizationHeader: string | null, sourceIp: string | undefined, config: PostmarkWebhookConfig): Promise<WebhookResult>;
|
|
4
|
+
export declare function handleSesWebhook(rawBody: string, config?: SesWebhookConfig): Promise<WebhookResult>;
|
|
5
|
+
export declare function handleSendgridWebhook(rawBody: string, signatureHeader: string | null, timestampHeader: string | null, config: SendgridWebhookConfig): Promise<WebhookResult>;
|
|
6
|
+
export declare interface WebhookResult {
|
|
7
|
+
status: number
|
|
8
|
+
body: { ok: boolean, reason?: string, processed?: boolean, classification?: EmailEventClassification }
|
|
9
|
+
}
|
|
10
|
+
export declare interface MailgunWebhookConfig {
|
|
11
|
+
signingKey: string
|
|
12
|
+
toleranceSeconds?: number
|
|
13
|
+
}
|
|
14
|
+
export declare interface PostmarkWebhookConfig {
|
|
15
|
+
username: string
|
|
16
|
+
password: string
|
|
17
|
+
ipAllowlist?: ReadonlyArray<string>
|
|
18
|
+
}
|
|
19
|
+
export declare interface SesWebhookConfig {
|
|
20
|
+
certUrlHostAllowlist?: RegExp
|
|
21
|
+
fetchCert?: (url: string) => Promise<string>
|
|
22
|
+
autoConfirmSubscriptions?: boolean
|
|
23
|
+
}
|
|
24
|
+
export declare interface SendgridWebhookConfig {
|
|
25
|
+
publicKeyPem: string
|
|
26
|
+
toleranceSeconds?: number
|
|
27
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify a Mailgun webhook signature. Mailgun signs
|
|
3
|
+
* `${timestamp}${token}` with HMAC-SHA256; the hex digest is
|
|
4
|
+
* compared in constant time against the `signature` field.
|
|
5
|
+
*
|
|
6
|
+
* The timestamp is checked against wall clock with a configurable
|
|
7
|
+
* tolerance (default 5 minutes) to reject replays. Constant-time
|
|
8
|
+
* compare prevents signature-timing oracles.
|
|
9
|
+
*/
|
|
10
|
+
export declare function verifyMailgunSignature(input: MailgunSignatureInput): SignatureVerification;
|
|
11
|
+
/**
|
|
12
|
+
* Verify a Postmark webhook. Postmark uses HTTP Basic Auth (the app
|
|
13
|
+
* configures the username + password when registering the webhook in
|
|
14
|
+
* the Postmark dashboard); they don't sign the body. The auth check
|
|
15
|
+
* is constant-time. Optional IP-allowlist check rejects requests
|
|
16
|
+
* from outside Postmark's published source IPs.
|
|
17
|
+
*/
|
|
18
|
+
export declare function verifyPostmarkAuth(input: PostmarkAuthInput): SignatureVerification;
|
|
19
|
+
/**
|
|
20
|
+
* Verify an SNS message signature (SES uses SNS for delivery). The
|
|
21
|
+
* cert URL host MUST match `sns.<region>.amazonaws.com` โ any other
|
|
22
|
+
* host is treated as untrusted (defense against SSRF via crafted
|
|
23
|
+
* SigningCertURL).
|
|
24
|
+
*
|
|
25
|
+
* Note: this function intentionally only does the structural +
|
|
26
|
+
* cert-URL check + signature verify. SubscriptionConfirmation
|
|
27
|
+
* handling (responding to `SubscribeURL` to complete the topic
|
|
28
|
+
* binding) is the route handler's job since it's a one-time setup
|
|
29
|
+
* action distinct from per-event verification.
|
|
30
|
+
*/
|
|
31
|
+
export declare function verifySesSnsSignature(input: SesSnsSignatureInput): Promise<SignatureVerification>;
|
|
32
|
+
/**
|
|
33
|
+
* Verify a SendGrid Event Webhook signature. SendGrid signs
|
|
34
|
+
* `${timestamp}${body}` with ECDSA-SHA256 using the public key from
|
|
35
|
+
* their signed-webhook setup page. Signature is base64 in the
|
|
36
|
+
* `X-Twilio-Email-Event-Webhook-Signature` header.
|
|
37
|
+
*/
|
|
38
|
+
export declare function verifySendgridSignature(input: SendgridSignatureInput): SignatureVerification;
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// Mailgun
|
|
41
|
+
// =============================================================================
|
|
42
|
+
export declare interface MailgunSignatureInput {
|
|
43
|
+
timestamp: string
|
|
44
|
+
token: string
|
|
45
|
+
signature: string
|
|
46
|
+
signingKey: string
|
|
47
|
+
toleranceSeconds?: number
|
|
48
|
+
}
|
|
49
|
+
// =============================================================================
|
|
50
|
+
// Postmark
|
|
51
|
+
// =============================================================================
|
|
52
|
+
export declare interface PostmarkAuthInput {
|
|
53
|
+
authorizationHeader: string | null | undefined
|
|
54
|
+
expectedUsername: string
|
|
55
|
+
expectedPassword: string
|
|
56
|
+
sourceIp?: string
|
|
57
|
+
ipAllowlist?: ReadonlyArray<string>
|
|
58
|
+
}
|
|
59
|
+
// =============================================================================
|
|
60
|
+
// SES (via SNS)
|
|
61
|
+
// =============================================================================
|
|
62
|
+
export declare interface SesSnsSignatureInput {
|
|
63
|
+
message: {
|
|
64
|
+
Type: 'Notification' | 'SubscriptionConfirmation' | 'UnsubscribeConfirmation'
|
|
65
|
+
MessageId: string
|
|
66
|
+
TopicArn: string
|
|
67
|
+
Subject?: string
|
|
68
|
+
Message: string
|
|
69
|
+
Timestamp: string
|
|
70
|
+
SignatureVersion: string
|
|
71
|
+
Signature: string
|
|
72
|
+
SigningCertURL: string
|
|
73
|
+
Token?: string
|
|
74
|
+
SubscribeURL?: string
|
|
75
|
+
[key: string]: unknown
|
|
76
|
+
}
|
|
77
|
+
fetchCert?: (url: string) => Promise<string>
|
|
78
|
+
certUrlHostAllowlist?: RegExp
|
|
79
|
+
}
|
|
80
|
+
// =============================================================================
|
|
81
|
+
// SendGrid
|
|
82
|
+
// =============================================================================
|
|
83
|
+
export declare interface SendgridSignatureInput {
|
|
84
|
+
body: string
|
|
85
|
+
signature: string | null | undefined
|
|
86
|
+
timestamp: string | null | undefined
|
|
87
|
+
publicKeyPem: string
|
|
88
|
+
toleranceSeconds?: number
|
|
89
|
+
}
|
|
90
|
+
export type SignatureVerification = | { ok: true }
|
|
91
|
+
| { ok: false, reason: 'missing-config' | 'missing-signature' | 'bad-signature' | 'expired' | 'untrusted-cert-url' | 'cert-fetch-failed' }
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/email",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.70.
|
|
4
|
+
"version": "0.70.53",
|
|
5
5
|
"description": "The Stacks Email integration. Painlessly create & manage your inboxes, templates, and send emails.",
|
|
6
6
|
"author": "Chris Breuer",
|
|
7
7
|
"contributors": [
|
|
@@ -50,13 +50,13 @@
|
|
|
50
50
|
"prepublishOnly": "bun run build"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@stacksjs/ts-cloud": "^0.
|
|
53
|
+
"@stacksjs/ts-cloud": "^0.7.12"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@stacksjs/cli": "
|
|
57
|
-
"@stacksjs/config": "
|
|
56
|
+
"@stacksjs/cli": "0.70.53",
|
|
57
|
+
"@stacksjs/config": "0.70.53",
|
|
58
58
|
"better-dx": "^0.2.12",
|
|
59
|
-
"@stacksjs/error-handling": "
|
|
60
|
-
"@stacksjs/types": "
|
|
59
|
+
"@stacksjs/error-handling": "0.70.53",
|
|
60
|
+
"@stacksjs/types": "0.70.53"
|
|
61
61
|
}
|
|
62
62
|
}
|