mailery 0.0.0 → 0.1.0
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/README.md +103 -11
- package/dist/admin/spa/index-B6riKdpB.css +1 -0
- package/dist/admin/spa/index-C_FJL_E_.js +41 -0
- package/dist/admin/spa/index-C_FJL_E_.js.map +1 -0
- package/dist/admin/spa/index.html +16 -0
- package/dist/admin/spa/template-editor-Cx1ky7rr.js +502 -0
- package/dist/admin/spa/template-editor-Cx1ky7rr.js.map +1 -0
- package/dist/index.cjs +3691 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +242 -2
- package/dist/index.d.ts +242 -2
- package/dist/index.js +3667 -3
- package/dist/index.js.map +1 -1
- package/dist/null-7gnz1V98.d.cts +959 -0
- package/dist/null-7gnz1V98.d.ts +959 -0
- package/dist/testing.cjs +16565 -2
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +63 -2
- package/dist/testing.d.ts +63 -2
- package/dist/testing.js +16555 -2
- package/dist/testing.js.map +1 -1
- package/package.json +38 -5
- package/dist/admin/static/.gitkeep +0 -0
- package/dist/admin/views/.gitkeep +0 -0
package/dist/index.d.cts
CHANGED
|
@@ -1,3 +1,243 @@
|
|
|
1
|
-
|
|
1
|
+
import { C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, N as NormalizedEvent, c as Mailer, T as TemplateDoc, d as SuppressionScope } from './null-7gnz1V98.cjs';
|
|
2
|
+
export { e as AuditLogDoc, B as BroadcastDoc, f as BroadcastStatus, g as CircuitBreakerThresholds, h as Collections, i as ContactTagDoc, E as EventDoc, F as FlowDoc, j as FlowGoal, k as FlowRunDoc, l as FlowRunStatus, m as FlowStep, n as FlowVersionDoc, H as HealthDoc, o as HealthStatus, L as LeadDoc, p as MailerConfig, q as NullProvider, O as OutboxDoc, P as Predicate, R as RedisOptions, r as SegmentDefinition, s as SegmentFilter, t as SendDoc, u as SendStatus, v as SubscriptionDoc, w as SubscriptionStatus, x as SuppressionDoc, y as SuppressionReason, z as TemplateKind, D as TemplateVersionDoc, W as WebhookEventDoc, G as ensureIndexes, I as getCollections } from './null-7gnz1V98.cjs';
|
|
3
|
+
import { Db, Filter } from 'mongodb';
|
|
4
|
+
import { Request, Router } from 'express';
|
|
5
|
+
import Handlebars from 'handlebars';
|
|
6
|
+
import 'ioredis';
|
|
7
|
+
import 'zod';
|
|
2
8
|
|
|
3
|
-
|
|
9
|
+
/**
|
|
10
|
+
* MongoContactAdapter — reads contacts directly from the host's `users`
|
|
11
|
+
* collection (read-mostly; optional narrow tag writes). Mailer never
|
|
12
|
+
* duplicates identity data — the host is the source of truth.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
interface MongoContactAdapterOptions {
|
|
16
|
+
db: Db;
|
|
17
|
+
collection: string;
|
|
18
|
+
emailField?: string;
|
|
19
|
+
idField?: string;
|
|
20
|
+
/** Path to the tags array on the user document. */
|
|
21
|
+
tagsField?: string;
|
|
22
|
+
/** When true, mailer writes tags via `$addToSet` / `$pull` on the user doc. */
|
|
23
|
+
tagsWritable?: boolean;
|
|
24
|
+
/** 'strings' = ['vip','beta'], 'objects' = [{ name: 'vip' }]. */
|
|
25
|
+
tagsArrayShape?: 'strings' | 'objects';
|
|
26
|
+
/** Customize the projection mailer sees. Defaults pick up email + tags + a few common fields. */
|
|
27
|
+
toContact?: (userDoc: any) => Contact;
|
|
28
|
+
/** Customize how AdapterFilter becomes a Mongo query. */
|
|
29
|
+
translateFilter?: (filter: AdapterFilter) => Filter<any>;
|
|
30
|
+
/** Per-call default for query() limit. */
|
|
31
|
+
batchSize?: number;
|
|
32
|
+
}
|
|
33
|
+
declare class MongoContactAdapter implements ContactAdapter {
|
|
34
|
+
private readonly col;
|
|
35
|
+
private readonly emailField;
|
|
36
|
+
private readonly idField;
|
|
37
|
+
private readonly tagsField;
|
|
38
|
+
private readonly tagsWritable;
|
|
39
|
+
private readonly tagsArrayShape;
|
|
40
|
+
private readonly toContactFn;
|
|
41
|
+
private readonly translateFilterFn;
|
|
42
|
+
private readonly batchSize;
|
|
43
|
+
constructor(opts: MongoContactAdapterOptions);
|
|
44
|
+
getById(externalId: string): Promise<Contact | null>;
|
|
45
|
+
getByEmail(email: string): Promise<Contact | null>;
|
|
46
|
+
getBatch(externalIds: string[]): Promise<Map<string, Contact>>;
|
|
47
|
+
query(filter: AdapterFilter, opts: {
|
|
48
|
+
limit: number;
|
|
49
|
+
cursor?: string;
|
|
50
|
+
}): Promise<{
|
|
51
|
+
contacts: Contact[];
|
|
52
|
+
nextCursor?: string;
|
|
53
|
+
}>;
|
|
54
|
+
count(filter: AdapterFilter): Promise<number>;
|
|
55
|
+
addTags?: (externalId: string, tags: string[]) => Promise<void>;
|
|
56
|
+
removeTags?: (externalId: string, tags: string[]) => Promise<void>;
|
|
57
|
+
private idFilter;
|
|
58
|
+
private defaultToContact;
|
|
59
|
+
private defaultTranslateFilter;
|
|
60
|
+
private addTagsImpl;
|
|
61
|
+
private removeTagsImpl;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* SendGridProvider — sends mail through @sendgrid/mail, verifies the Event
|
|
66
|
+
* Webhook signature with the ECDSA public key configured in SendGrid, and
|
|
67
|
+
* normalizes inbound event payloads into our shared shape.
|
|
68
|
+
*
|
|
69
|
+
* Setup steps required on the SendGrid side (one-time):
|
|
70
|
+
* 1. Authenticate sender domain (SPF, DKIM, DMARC).
|
|
71
|
+
* 2. Configure event webhook → POST to https://your.host/m/webhooks/sendgrid
|
|
72
|
+
* 3. Enable: delivered, open, click, bounce, dropped, spamreport, unsubscribe
|
|
73
|
+
* 4. Generate Signed Event Webhook key → store as webhookVerificationKey
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
interface SendGridProviderOptions {
|
|
77
|
+
apiKey: string;
|
|
78
|
+
/** ECDSA public key (PEM) from SendGrid → Settings → Mail Settings → Signed Event Webhook. */
|
|
79
|
+
webhookVerificationKey?: string;
|
|
80
|
+
/** Send-rate cap per second (BullMQ group limiter consults this). */
|
|
81
|
+
sendRatePerSecond?: number;
|
|
82
|
+
/** Sandbox mode bypasses actual delivery — useful in dev/test. */
|
|
83
|
+
sandbox?: boolean;
|
|
84
|
+
}
|
|
85
|
+
declare class SendGridProvider implements MailProvider {
|
|
86
|
+
private readonly opts;
|
|
87
|
+
readonly name = "sendgrid";
|
|
88
|
+
readonly sendRatePerSecond: number;
|
|
89
|
+
constructor(opts: SendGridProviderOptions);
|
|
90
|
+
send(args: SendArgs): Promise<SendResult>;
|
|
91
|
+
verifyWebhook(rawBody: Buffer, headers: Record<string, string>): Promise<boolean>;
|
|
92
|
+
parseWebhookEvents(payload: unknown): NormalizedEvent[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Admin router — serves the prebuilt React SPA + REST endpoints the SPA
|
|
97
|
+
* consumes. Mount inside a host Express app, gated by host auth.
|
|
98
|
+
*
|
|
99
|
+
* app.use('/admin/mailer', requireAdmin, createAdminRouter(mailer))
|
|
100
|
+
*
|
|
101
|
+
* REST routes under /api/* are documented in plans/14-admin-api.md.
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
interface AdminRouterOptions {
|
|
105
|
+
/** Override path to the built SPA. Defaults to `dist/admin/spa` shipped with the package. */
|
|
106
|
+
spaDir?: string;
|
|
107
|
+
/** Resolve actor metadata from a Request. Defaults to `human:${req.user?.email || 'anonymous'}`. */
|
|
108
|
+
getActor?: (req: Request) => string;
|
|
109
|
+
}
|
|
110
|
+
declare function createAdminRouter(mailer: Mailer, opts?: AdminRouterOptions): Router;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Public router — endpoints that must be reachable by email clients and
|
|
114
|
+
* provider webhooks. Mount under your tracking base path (default `/m`).
|
|
115
|
+
*
|
|
116
|
+
* app.use('/m', createPublicRouter(mailer))
|
|
117
|
+
*
|
|
118
|
+
* Routes:
|
|
119
|
+
* GET /open/:sendId.png — open pixel (records open, returns 1×1 PNG)
|
|
120
|
+
* GET /click/:sendId/:linkId — click redirect (records click, 302 → target)
|
|
121
|
+
* GET /unsub/:token — confirmation page (one-click POST link)
|
|
122
|
+
* POST /unsub/:token — RFC 8058 one-click unsubscribe
|
|
123
|
+
* POST /webhooks/:provider — inbound provider event webhook
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
interface PublicRouterOptions {
|
|
127
|
+
/**
|
|
128
|
+
* Path on disk where unsubscribe events fall back to when Mongo is degraded.
|
|
129
|
+
* Defaults to /tmp/mailery-pending-unsubs.jsonl.
|
|
130
|
+
*/
|
|
131
|
+
pendingUnsubsPath?: string;
|
|
132
|
+
}
|
|
133
|
+
declare function createPublicRouter(mailer: Mailer, opts?: PublicRouterOptions): Router;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Template render pipeline.
|
|
137
|
+
*
|
|
138
|
+
* authorMjml + handlebarsContext
|
|
139
|
+
* ↓ Handlebars render → MJML with substituted vars
|
|
140
|
+
* ↓ mjml-core compile → HTML
|
|
141
|
+
* ↓ html-to-text derive → plain text alternative
|
|
142
|
+
* ↓ applyTracking(sendId) → tracked HTML with rewritten links + open pixel
|
|
143
|
+
*
|
|
144
|
+
* Subject and preheader run through Handlebars too. Plain text is auto-derived
|
|
145
|
+
* unless the template explicitly overrides it.
|
|
146
|
+
*/
|
|
147
|
+
|
|
148
|
+
interface CompileResult {
|
|
149
|
+
html: string;
|
|
150
|
+
plainText: string;
|
|
151
|
+
errors: Array<{
|
|
152
|
+
line?: number;
|
|
153
|
+
message: string;
|
|
154
|
+
tagName?: string;
|
|
155
|
+
formattedMessage?: string;
|
|
156
|
+
}>;
|
|
157
|
+
}
|
|
158
|
+
/** Compile MJML → HTML, then derive plain text. Called at template publish time. */
|
|
159
|
+
declare function compileTemplate(mjml: string): Promise<CompileResult>;
|
|
160
|
+
/** Auto-derive plain text from compiled HTML. */
|
|
161
|
+
declare function derivePlaintext(html: string): string;
|
|
162
|
+
interface RenderContext {
|
|
163
|
+
contact: Contact;
|
|
164
|
+
vars: Record<string, unknown>;
|
|
165
|
+
/** URL the recipient hits to one-click unsubscribe. */
|
|
166
|
+
unsubscribeUrl: string;
|
|
167
|
+
/** URL to view this email in a browser (when implemented). */
|
|
168
|
+
viewInBrowserUrl?: string;
|
|
169
|
+
/** URL for the preference center (when implemented). */
|
|
170
|
+
preferenceCenterUrl?: string;
|
|
171
|
+
/** Configured sender postal address (CAN-SPAM). */
|
|
172
|
+
senderAddress?: string;
|
|
173
|
+
}
|
|
174
|
+
interface RenderedTemplate {
|
|
175
|
+
subject: string;
|
|
176
|
+
preheader: string;
|
|
177
|
+
html: string;
|
|
178
|
+
plainText: string;
|
|
179
|
+
fromName: string;
|
|
180
|
+
fromEmail: string;
|
|
181
|
+
replyTo: string | null;
|
|
182
|
+
}
|
|
183
|
+
interface RenderOptions {
|
|
184
|
+
/** Extra Handlebars helpers contributed by the host. */
|
|
185
|
+
helpers?: Record<string, Handlebars.HelperDelegate>;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Render a published template against a contact + vars context. Returns the
|
|
189
|
+
* substituted subject/preheader/html/plainText. Tracking is NOT applied here —
|
|
190
|
+
* that step needs the send id and runs separately via `applyTracking`.
|
|
191
|
+
*/
|
|
192
|
+
declare function renderTemplate(template: TemplateDoc, ctx: RenderContext, opts?: RenderOptions): Promise<RenderedTemplate>;
|
|
193
|
+
interface TrackingOptions {
|
|
194
|
+
sendId: string;
|
|
195
|
+
publicUrl: string;
|
|
196
|
+
trackOpens: boolean;
|
|
197
|
+
trackClicks: boolean;
|
|
198
|
+
/** URL that must NOT be rewritten (e.g. the resolved unsubscribe URL). */
|
|
199
|
+
preserveUrls?: string[];
|
|
200
|
+
}
|
|
201
|
+
interface TrackingResult {
|
|
202
|
+
html: string;
|
|
203
|
+
/** linkId → original URL map; persist on `mailer_sends.links` for click resolution. */
|
|
204
|
+
links: Array<{
|
|
205
|
+
linkId: string;
|
|
206
|
+
url: string;
|
|
207
|
+
}>;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Rewrite `<a href>` in `html` to /m/click/<sendId>/<linkId>?... and append an
|
|
211
|
+
* open pixel. Returns the modified HTML plus the link map to persist on the
|
|
212
|
+
* send document.
|
|
213
|
+
*/
|
|
214
|
+
declare function applyTracking(html: string, opts: TrackingOptions): TrackingResult;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* HMAC-signed tokens for unsubscribe + preference-center URLs.
|
|
218
|
+
*
|
|
219
|
+
* Format: base64url(payload) '.' base64url(hmac)
|
|
220
|
+
* payload = JSON.stringify({ e: email, s: scope, x: expiresAtMs })
|
|
221
|
+
* hmac = HMAC-SHA256(secret, payload)
|
|
222
|
+
*
|
|
223
|
+
* Tokens expire because long-lived signed URLs are a liability.
|
|
224
|
+
*/
|
|
225
|
+
|
|
226
|
+
interface UnsubscribeTokenPayload {
|
|
227
|
+
email: string;
|
|
228
|
+
scope: SuppressionScope;
|
|
229
|
+
expiresAt: Date;
|
|
230
|
+
}
|
|
231
|
+
declare function signUnsubscribeToken(payload: UnsubscribeTokenPayload, secret: string): string;
|
|
232
|
+
declare function verifyUnsubscribeToken(token: string, secret: string, now?: Date): UnsubscribeTokenPayload | null;
|
|
233
|
+
/**
|
|
234
|
+
* sha256 hex digest, used for storing email hashes (GDPR forget) and dedup helpers.
|
|
235
|
+
*/
|
|
236
|
+
declare function sha256Hex(input: string): string;
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Public exports for the `mailery` package.
|
|
240
|
+
*/
|
|
241
|
+
declare const VERSION = "0.1.0";
|
|
242
|
+
|
|
243
|
+
export { AdapterFilter, type AdminRouterOptions, Contact, ContactAdapter, MailProvider, Mailer, MongoContactAdapter, type MongoContactAdapterOptions, NormalizedEvent, type PublicRouterOptions, SendArgs, SendGridProvider, type SendGridProviderOptions, SendResult, SuppressionScope, TemplateDoc, VERSION, applyTracking, compileTemplate, createAdminRouter, createPublicRouter, derivePlaintext, renderTemplate, sha256Hex, signUnsubscribeToken, verifyUnsubscribeToken };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,243 @@
|
|
|
1
|
-
|
|
1
|
+
import { C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, N as NormalizedEvent, c as Mailer, T as TemplateDoc, d as SuppressionScope } from './null-7gnz1V98.js';
|
|
2
|
+
export { e as AuditLogDoc, B as BroadcastDoc, f as BroadcastStatus, g as CircuitBreakerThresholds, h as Collections, i as ContactTagDoc, E as EventDoc, F as FlowDoc, j as FlowGoal, k as FlowRunDoc, l as FlowRunStatus, m as FlowStep, n as FlowVersionDoc, H as HealthDoc, o as HealthStatus, L as LeadDoc, p as MailerConfig, q as NullProvider, O as OutboxDoc, P as Predicate, R as RedisOptions, r as SegmentDefinition, s as SegmentFilter, t as SendDoc, u as SendStatus, v as SubscriptionDoc, w as SubscriptionStatus, x as SuppressionDoc, y as SuppressionReason, z as TemplateKind, D as TemplateVersionDoc, W as WebhookEventDoc, G as ensureIndexes, I as getCollections } from './null-7gnz1V98.js';
|
|
3
|
+
import { Db, Filter } from 'mongodb';
|
|
4
|
+
import { Request, Router } from 'express';
|
|
5
|
+
import Handlebars from 'handlebars';
|
|
6
|
+
import 'ioredis';
|
|
7
|
+
import 'zod';
|
|
2
8
|
|
|
3
|
-
|
|
9
|
+
/**
|
|
10
|
+
* MongoContactAdapter — reads contacts directly from the host's `users`
|
|
11
|
+
* collection (read-mostly; optional narrow tag writes). Mailer never
|
|
12
|
+
* duplicates identity data — the host is the source of truth.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
interface MongoContactAdapterOptions {
|
|
16
|
+
db: Db;
|
|
17
|
+
collection: string;
|
|
18
|
+
emailField?: string;
|
|
19
|
+
idField?: string;
|
|
20
|
+
/** Path to the tags array on the user document. */
|
|
21
|
+
tagsField?: string;
|
|
22
|
+
/** When true, mailer writes tags via `$addToSet` / `$pull` on the user doc. */
|
|
23
|
+
tagsWritable?: boolean;
|
|
24
|
+
/** 'strings' = ['vip','beta'], 'objects' = [{ name: 'vip' }]. */
|
|
25
|
+
tagsArrayShape?: 'strings' | 'objects';
|
|
26
|
+
/** Customize the projection mailer sees. Defaults pick up email + tags + a few common fields. */
|
|
27
|
+
toContact?: (userDoc: any) => Contact;
|
|
28
|
+
/** Customize how AdapterFilter becomes a Mongo query. */
|
|
29
|
+
translateFilter?: (filter: AdapterFilter) => Filter<any>;
|
|
30
|
+
/** Per-call default for query() limit. */
|
|
31
|
+
batchSize?: number;
|
|
32
|
+
}
|
|
33
|
+
declare class MongoContactAdapter implements ContactAdapter {
|
|
34
|
+
private readonly col;
|
|
35
|
+
private readonly emailField;
|
|
36
|
+
private readonly idField;
|
|
37
|
+
private readonly tagsField;
|
|
38
|
+
private readonly tagsWritable;
|
|
39
|
+
private readonly tagsArrayShape;
|
|
40
|
+
private readonly toContactFn;
|
|
41
|
+
private readonly translateFilterFn;
|
|
42
|
+
private readonly batchSize;
|
|
43
|
+
constructor(opts: MongoContactAdapterOptions);
|
|
44
|
+
getById(externalId: string): Promise<Contact | null>;
|
|
45
|
+
getByEmail(email: string): Promise<Contact | null>;
|
|
46
|
+
getBatch(externalIds: string[]): Promise<Map<string, Contact>>;
|
|
47
|
+
query(filter: AdapterFilter, opts: {
|
|
48
|
+
limit: number;
|
|
49
|
+
cursor?: string;
|
|
50
|
+
}): Promise<{
|
|
51
|
+
contacts: Contact[];
|
|
52
|
+
nextCursor?: string;
|
|
53
|
+
}>;
|
|
54
|
+
count(filter: AdapterFilter): Promise<number>;
|
|
55
|
+
addTags?: (externalId: string, tags: string[]) => Promise<void>;
|
|
56
|
+
removeTags?: (externalId: string, tags: string[]) => Promise<void>;
|
|
57
|
+
private idFilter;
|
|
58
|
+
private defaultToContact;
|
|
59
|
+
private defaultTranslateFilter;
|
|
60
|
+
private addTagsImpl;
|
|
61
|
+
private removeTagsImpl;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* SendGridProvider — sends mail through @sendgrid/mail, verifies the Event
|
|
66
|
+
* Webhook signature with the ECDSA public key configured in SendGrid, and
|
|
67
|
+
* normalizes inbound event payloads into our shared shape.
|
|
68
|
+
*
|
|
69
|
+
* Setup steps required on the SendGrid side (one-time):
|
|
70
|
+
* 1. Authenticate sender domain (SPF, DKIM, DMARC).
|
|
71
|
+
* 2. Configure event webhook → POST to https://your.host/m/webhooks/sendgrid
|
|
72
|
+
* 3. Enable: delivered, open, click, bounce, dropped, spamreport, unsubscribe
|
|
73
|
+
* 4. Generate Signed Event Webhook key → store as webhookVerificationKey
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
interface SendGridProviderOptions {
|
|
77
|
+
apiKey: string;
|
|
78
|
+
/** ECDSA public key (PEM) from SendGrid → Settings → Mail Settings → Signed Event Webhook. */
|
|
79
|
+
webhookVerificationKey?: string;
|
|
80
|
+
/** Send-rate cap per second (BullMQ group limiter consults this). */
|
|
81
|
+
sendRatePerSecond?: number;
|
|
82
|
+
/** Sandbox mode bypasses actual delivery — useful in dev/test. */
|
|
83
|
+
sandbox?: boolean;
|
|
84
|
+
}
|
|
85
|
+
declare class SendGridProvider implements MailProvider {
|
|
86
|
+
private readonly opts;
|
|
87
|
+
readonly name = "sendgrid";
|
|
88
|
+
readonly sendRatePerSecond: number;
|
|
89
|
+
constructor(opts: SendGridProviderOptions);
|
|
90
|
+
send(args: SendArgs): Promise<SendResult>;
|
|
91
|
+
verifyWebhook(rawBody: Buffer, headers: Record<string, string>): Promise<boolean>;
|
|
92
|
+
parseWebhookEvents(payload: unknown): NormalizedEvent[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Admin router — serves the prebuilt React SPA + REST endpoints the SPA
|
|
97
|
+
* consumes. Mount inside a host Express app, gated by host auth.
|
|
98
|
+
*
|
|
99
|
+
* app.use('/admin/mailer', requireAdmin, createAdminRouter(mailer))
|
|
100
|
+
*
|
|
101
|
+
* REST routes under /api/* are documented in plans/14-admin-api.md.
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
interface AdminRouterOptions {
|
|
105
|
+
/** Override path to the built SPA. Defaults to `dist/admin/spa` shipped with the package. */
|
|
106
|
+
spaDir?: string;
|
|
107
|
+
/** Resolve actor metadata from a Request. Defaults to `human:${req.user?.email || 'anonymous'}`. */
|
|
108
|
+
getActor?: (req: Request) => string;
|
|
109
|
+
}
|
|
110
|
+
declare function createAdminRouter(mailer: Mailer, opts?: AdminRouterOptions): Router;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Public router — endpoints that must be reachable by email clients and
|
|
114
|
+
* provider webhooks. Mount under your tracking base path (default `/m`).
|
|
115
|
+
*
|
|
116
|
+
* app.use('/m', createPublicRouter(mailer))
|
|
117
|
+
*
|
|
118
|
+
* Routes:
|
|
119
|
+
* GET /open/:sendId.png — open pixel (records open, returns 1×1 PNG)
|
|
120
|
+
* GET /click/:sendId/:linkId — click redirect (records click, 302 → target)
|
|
121
|
+
* GET /unsub/:token — confirmation page (one-click POST link)
|
|
122
|
+
* POST /unsub/:token — RFC 8058 one-click unsubscribe
|
|
123
|
+
* POST /webhooks/:provider — inbound provider event webhook
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
interface PublicRouterOptions {
|
|
127
|
+
/**
|
|
128
|
+
* Path on disk where unsubscribe events fall back to when Mongo is degraded.
|
|
129
|
+
* Defaults to /tmp/mailery-pending-unsubs.jsonl.
|
|
130
|
+
*/
|
|
131
|
+
pendingUnsubsPath?: string;
|
|
132
|
+
}
|
|
133
|
+
declare function createPublicRouter(mailer: Mailer, opts?: PublicRouterOptions): Router;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Template render pipeline.
|
|
137
|
+
*
|
|
138
|
+
* authorMjml + handlebarsContext
|
|
139
|
+
* ↓ Handlebars render → MJML with substituted vars
|
|
140
|
+
* ↓ mjml-core compile → HTML
|
|
141
|
+
* ↓ html-to-text derive → plain text alternative
|
|
142
|
+
* ↓ applyTracking(sendId) → tracked HTML with rewritten links + open pixel
|
|
143
|
+
*
|
|
144
|
+
* Subject and preheader run through Handlebars too. Plain text is auto-derived
|
|
145
|
+
* unless the template explicitly overrides it.
|
|
146
|
+
*/
|
|
147
|
+
|
|
148
|
+
interface CompileResult {
|
|
149
|
+
html: string;
|
|
150
|
+
plainText: string;
|
|
151
|
+
errors: Array<{
|
|
152
|
+
line?: number;
|
|
153
|
+
message: string;
|
|
154
|
+
tagName?: string;
|
|
155
|
+
formattedMessage?: string;
|
|
156
|
+
}>;
|
|
157
|
+
}
|
|
158
|
+
/** Compile MJML → HTML, then derive plain text. Called at template publish time. */
|
|
159
|
+
declare function compileTemplate(mjml: string): Promise<CompileResult>;
|
|
160
|
+
/** Auto-derive plain text from compiled HTML. */
|
|
161
|
+
declare function derivePlaintext(html: string): string;
|
|
162
|
+
interface RenderContext {
|
|
163
|
+
contact: Contact;
|
|
164
|
+
vars: Record<string, unknown>;
|
|
165
|
+
/** URL the recipient hits to one-click unsubscribe. */
|
|
166
|
+
unsubscribeUrl: string;
|
|
167
|
+
/** URL to view this email in a browser (when implemented). */
|
|
168
|
+
viewInBrowserUrl?: string;
|
|
169
|
+
/** URL for the preference center (when implemented). */
|
|
170
|
+
preferenceCenterUrl?: string;
|
|
171
|
+
/** Configured sender postal address (CAN-SPAM). */
|
|
172
|
+
senderAddress?: string;
|
|
173
|
+
}
|
|
174
|
+
interface RenderedTemplate {
|
|
175
|
+
subject: string;
|
|
176
|
+
preheader: string;
|
|
177
|
+
html: string;
|
|
178
|
+
plainText: string;
|
|
179
|
+
fromName: string;
|
|
180
|
+
fromEmail: string;
|
|
181
|
+
replyTo: string | null;
|
|
182
|
+
}
|
|
183
|
+
interface RenderOptions {
|
|
184
|
+
/** Extra Handlebars helpers contributed by the host. */
|
|
185
|
+
helpers?: Record<string, Handlebars.HelperDelegate>;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Render a published template against a contact + vars context. Returns the
|
|
189
|
+
* substituted subject/preheader/html/plainText. Tracking is NOT applied here —
|
|
190
|
+
* that step needs the send id and runs separately via `applyTracking`.
|
|
191
|
+
*/
|
|
192
|
+
declare function renderTemplate(template: TemplateDoc, ctx: RenderContext, opts?: RenderOptions): Promise<RenderedTemplate>;
|
|
193
|
+
interface TrackingOptions {
|
|
194
|
+
sendId: string;
|
|
195
|
+
publicUrl: string;
|
|
196
|
+
trackOpens: boolean;
|
|
197
|
+
trackClicks: boolean;
|
|
198
|
+
/** URL that must NOT be rewritten (e.g. the resolved unsubscribe URL). */
|
|
199
|
+
preserveUrls?: string[];
|
|
200
|
+
}
|
|
201
|
+
interface TrackingResult {
|
|
202
|
+
html: string;
|
|
203
|
+
/** linkId → original URL map; persist on `mailer_sends.links` for click resolution. */
|
|
204
|
+
links: Array<{
|
|
205
|
+
linkId: string;
|
|
206
|
+
url: string;
|
|
207
|
+
}>;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Rewrite `<a href>` in `html` to /m/click/<sendId>/<linkId>?... and append an
|
|
211
|
+
* open pixel. Returns the modified HTML plus the link map to persist on the
|
|
212
|
+
* send document.
|
|
213
|
+
*/
|
|
214
|
+
declare function applyTracking(html: string, opts: TrackingOptions): TrackingResult;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* HMAC-signed tokens for unsubscribe + preference-center URLs.
|
|
218
|
+
*
|
|
219
|
+
* Format: base64url(payload) '.' base64url(hmac)
|
|
220
|
+
* payload = JSON.stringify({ e: email, s: scope, x: expiresAtMs })
|
|
221
|
+
* hmac = HMAC-SHA256(secret, payload)
|
|
222
|
+
*
|
|
223
|
+
* Tokens expire because long-lived signed URLs are a liability.
|
|
224
|
+
*/
|
|
225
|
+
|
|
226
|
+
interface UnsubscribeTokenPayload {
|
|
227
|
+
email: string;
|
|
228
|
+
scope: SuppressionScope;
|
|
229
|
+
expiresAt: Date;
|
|
230
|
+
}
|
|
231
|
+
declare function signUnsubscribeToken(payload: UnsubscribeTokenPayload, secret: string): string;
|
|
232
|
+
declare function verifyUnsubscribeToken(token: string, secret: string, now?: Date): UnsubscribeTokenPayload | null;
|
|
233
|
+
/**
|
|
234
|
+
* sha256 hex digest, used for storing email hashes (GDPR forget) and dedup helpers.
|
|
235
|
+
*/
|
|
236
|
+
declare function sha256Hex(input: string): string;
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Public exports for the `mailery` package.
|
|
240
|
+
*/
|
|
241
|
+
declare const VERSION = "0.1.0";
|
|
242
|
+
|
|
243
|
+
export { AdapterFilter, type AdminRouterOptions, Contact, ContactAdapter, MailProvider, Mailer, MongoContactAdapter, type MongoContactAdapterOptions, NormalizedEvent, type PublicRouterOptions, SendArgs, SendGridProvider, type SendGridProviderOptions, SendResult, SuppressionScope, TemplateDoc, VERSION, applyTracking, compileTemplate, createAdminRouter, createPublicRouter, derivePlaintext, renderTemplate, sha256Hex, signUnsubscribeToken, verifyUnsubscribeToken };
|