@voltro/plugin-mail 0.81.0 → 0.83.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/CHANGELOG.md +1806 -6
- package/README.md +1 -1
- package/THIRD-PARTY-NOTICES.md +3 -465
- package/dist/index.d.ts +179 -26
- package/dist/index.js +642 -580
- package/package.json +6 -6
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Context } from 'effect';
|
|
2
|
+
import { DataStore } from '@voltro/database';
|
|
2
3
|
import { Effect } from 'effect';
|
|
3
4
|
import { Layer } from 'effect';
|
|
5
|
+
import { PluginEgressDeclaration } from '@voltro/protocol';
|
|
6
|
+
import { PluginHttpRouteRequest } from '@voltro/protocol';
|
|
4
7
|
import { Schema } from 'effect';
|
|
5
8
|
import { VoltroPlugin } from '@voltro/protocol';
|
|
6
9
|
|
|
@@ -115,12 +118,30 @@ export declare interface IdempotencyStore {
|
|
|
115
118
|
readonly record: (tenantId: string | null, key: string, result: SendResult) => Effect.Effect<void>;
|
|
116
119
|
readonly activate?: (env: NodeJS.ProcessEnv) => Promise<void>;
|
|
117
120
|
readonly deactivate?: () => Promise<void>;
|
|
121
|
+
/** Receives the app's `DataStore` at `bindDataStore`, for a store kept in the
|
|
122
|
+
* app's own database. */
|
|
123
|
+
readonly bind?: (store: DataStore) => Promise<void>;
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
/** Structural guard: recognise a `defineEmail(...)` result among a module's
|
|
121
127
|
* exports (name + subject fn + render fn + a props schema). */
|
|
122
128
|
export declare const isEmailTemplate: (v: unknown) => v is AnyEmailTemplate;
|
|
123
129
|
|
|
130
|
+
export declare interface LettermintOptions {
|
|
131
|
+
/** Project API token (`lm_…`), sent as `x-lettermint-token`. */
|
|
132
|
+
readonly projectToken: string;
|
|
133
|
+
/** Default `https://api.lettermint.co/v1`. */
|
|
134
|
+
readonly baseUrl?: string;
|
|
135
|
+
/** Route slug per stream. Default: the project's default route (its
|
|
136
|
+
* transactional one) for `transactional`, `broadcast` for `broadcast`. */
|
|
137
|
+
readonly routes?: {
|
|
138
|
+
readonly transactional?: string;
|
|
139
|
+
readonly broadcast?: string;
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export declare const lettermintProvider: (options: LettermintOptions) => MailProvider;
|
|
144
|
+
|
|
124
145
|
/** Names (+ locale variants) of every globally-registered template. */
|
|
125
146
|
export declare const listGlobalTemplates: () => ReadonlyArray<{
|
|
126
147
|
readonly name: string;
|
|
@@ -141,6 +162,12 @@ export declare interface MailAttachment {
|
|
|
141
162
|
readonly contentType?: string;
|
|
142
163
|
}
|
|
143
164
|
|
|
165
|
+
/** What mail egress a provider choice declares. */
|
|
166
|
+
export declare const mailEgressOf: (options: {
|
|
167
|
+
readonly provider?: MailProviderName | MailProvider;
|
|
168
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
169
|
+
}) => ReadonlyArray<PluginEgressDeclaration>;
|
|
170
|
+
|
|
144
171
|
/**
|
|
145
172
|
* Mail failure. `transient` drives retry — HTTP 408 / 429 / 5xx and network
|
|
146
173
|
* failures are retryable; other HTTP 4xx are not. SMTP instead retries 4xx
|
|
@@ -164,10 +191,21 @@ export declare interface MailEvent {
|
|
|
164
191
|
readonly email: string;
|
|
165
192
|
readonly provider: string;
|
|
166
193
|
readonly messageId?: string;
|
|
194
|
+
/** The provider's id for this event — the key to act on a redelivered
|
|
195
|
+
* webhook only once. */
|
|
196
|
+
readonly eventId?: string;
|
|
197
|
+
/** What the message was sent with as `metadata`, returned by the provider. */
|
|
198
|
+
readonly metadata?: Readonly<Record<string, string>>;
|
|
167
199
|
readonly raw: unknown;
|
|
168
200
|
}
|
|
169
201
|
|
|
170
|
-
|
|
202
|
+
/** `unsubscribed`: the recipient left a broadcast through the provider's own
|
|
203
|
+
* unsubscribe (a provider that `managesUnsubscribe`). It is NOT suppressed
|
|
204
|
+
* app-wide — it ended one list, not the person's transactional mail.
|
|
205
|
+
* `unsuppressed`: the provider lifted a suppression (someone removed it in
|
|
206
|
+
* its dashboard) — lifted here too, or the app would keep dropping mail the
|
|
207
|
+
* provider would now deliver. */
|
|
208
|
+
export declare type MailEventType = 'delivered' | 'bounced' | 'complained' | 'unsubscribed' | 'unsuppressed' | 'opened' | 'clicked' | 'unknown';
|
|
171
209
|
|
|
172
210
|
export declare interface MailgunOptions {
|
|
173
211
|
readonly apiKey: string;
|
|
@@ -208,12 +246,30 @@ export declare interface MailMessage {
|
|
|
208
246
|
* step. The first send with a given key delivers; a replay with the same
|
|
209
247
|
* key is a no-op returning the recorded result. Scoped per tenant. */
|
|
210
248
|
readonly idempotencyKey?: string;
|
|
249
|
+
/** `transactional` (default) or `broadcast`. See `MailStream`. */
|
|
250
|
+
readonly stream?: MailStream;
|
|
251
|
+
/** How a recipient leaves a broadcast. See `MailUnsubscribe`. */
|
|
252
|
+
readonly unsubscribe?: MailUnsubscribe;
|
|
253
|
+
/** Key/value pairs the provider stores with the message and returns on its
|
|
254
|
+
* delivery events (`MailEvent.metadata`) — how a bounce finds its way back
|
|
255
|
+
* to the row that caused it. Never added to the mail itself. Lettermint and
|
|
256
|
+
* Postmark carry it; other providers ignore it. */
|
|
257
|
+
readonly metadata?: Readonly<Record<string, string>>;
|
|
211
258
|
}
|
|
212
259
|
|
|
213
|
-
|
|
260
|
+
/**
|
|
261
|
+
* The mail plugin, with the service it built — the same one handlers yield as
|
|
262
|
+
* `MailService`. For wiring that happens in `app.config.ts`, before any
|
|
263
|
+
* handler runs: `authRoutesPlugin({ sendEmail: mailSender(mail.service) })`.
|
|
264
|
+
*/
|
|
265
|
+
export declare type MailPlugin = VoltroPlugin & {
|
|
266
|
+
readonly service: MailServiceShape;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export declare const mailPlugin: (options?: MailPluginOptions) => MailPlugin;
|
|
214
270
|
|
|
215
271
|
export declare interface MailPluginOptions {
|
|
216
|
-
/** `'resend' | 'postmark' | 'sendgrid' | 'smtp' | 'console' | 'memory'`
|
|
272
|
+
/** `'lettermint' | 'resend' | 'postmark' | 'sendgrid' | 'smtp' | 'console' | 'memory'`
|
|
217
273
|
* (built from env) or a `MailProvider`. Default: `MAIL_PROVIDER` env, else
|
|
218
274
|
* `'console'`. */
|
|
219
275
|
readonly provider?: MailProviderName | MailProvider;
|
|
@@ -232,12 +288,12 @@ export declare interface MailPluginOptions {
|
|
|
232
288
|
* `mail.send({ template, props })`. */
|
|
233
289
|
readonly templates?: ReadonlyArray<AnyEmailTemplate>;
|
|
234
290
|
/** Suppression backend for bounced/complained addresses: `'memory'`
|
|
235
|
-
* (default, single-node), `'postgres'` (
|
|
291
|
+
* (default, single-node), `'postgres'` (the app's own postgres connection),
|
|
236
292
|
* or a custom `SuppressionStore`. */
|
|
237
293
|
readonly suppression?: 'memory' | 'postgres' | SuppressionStore;
|
|
238
294
|
/** Per-send idempotency backend (webhook-retry double-send guard):
|
|
239
|
-
* `'memory'` (default, single-node), `'postgres'` (
|
|
240
|
-
*
|
|
295
|
+
* `'memory'` (default, single-node), `'postgres'` (the app's own
|
|
296
|
+
* postgres connection), or a custom `IdempotencyStore`. Engaged only for a
|
|
241
297
|
* send carrying `idempotencyKey`. */
|
|
242
298
|
readonly idempotency?: 'memory' | 'postgres' | IdempotencyStore;
|
|
243
299
|
/** Backing store for HELD scheduled messages (providers without native
|
|
@@ -247,7 +303,9 @@ export declare interface MailPluginOptions {
|
|
|
247
303
|
/** How often (ms) the scheduled-send flush loop wakes to deliver due held
|
|
248
304
|
* messages. Default 1000. Ignored when the provider schedules natively. */
|
|
249
305
|
readonly scheduleTickMs?: number;
|
|
250
|
-
/**
|
|
306
|
+
/** Mount the provider's delivery-event webhook. Needs a provider with a
|
|
307
|
+
* `webhook` verifier (Lettermint). See `MailWebhookOptions`. */
|
|
308
|
+
readonly webhook?: MailWebhookOptions;
|
|
251
309
|
/**
|
|
252
310
|
* Namespace for this plugin's inspect endpoints. Default `mail`.
|
|
253
311
|
*
|
|
@@ -290,9 +348,24 @@ export declare interface MailProvider {
|
|
|
290
348
|
/** One request for N personalized messages. Each keeps its own subject /
|
|
291
349
|
* body / template vars. Returns one `SendResult` per input, in order. */
|
|
292
350
|
readonly sendBatch?: (messages: ReadonlyArray<ResolvedMessage>) => Effect.Effect<ReadonlyArray<SendResult>, MailError>;
|
|
351
|
+
/** True if the provider adds its own unsubscribe link and one-click headers
|
|
352
|
+
* to broadcast mail and suppresses whoever uses them (Lettermint's
|
|
353
|
+
* broadcast route). The service then sends no `List-Unsubscribe` of its own
|
|
354
|
+
* — two competing unsubscribe mechanisms would each only half work. */
|
|
355
|
+
readonly managesUnsubscribe?: boolean;
|
|
356
|
+
/** Verifies and parses the provider's delivery-event webhook. Present →
|
|
357
|
+
* `mailPlugin({ webhook })` can mount the endpoint. */
|
|
358
|
+
readonly webhook?: MailProviderWebhook;
|
|
293
359
|
}
|
|
294
360
|
|
|
295
|
-
export declare type MailProviderName = 'resend' | 'postmark' | 'sendgrid' | 'ses' | 'mailgun' | 'smtp' | 'console' | 'memory';
|
|
361
|
+
export declare type MailProviderName = 'lettermint' | 'resend' | 'postmark' | 'sendgrid' | 'ses' | 'mailgun' | 'smtp' | 'console' | 'memory';
|
|
362
|
+
|
|
363
|
+
/** A provider's delivery-event webhook: signature check, then normalisation. */
|
|
364
|
+
export declare interface MailProviderWebhook {
|
|
365
|
+
/** Verify the signature over the RAW body and decode it. Fails with
|
|
366
|
+
* `MailWebhookError` for a missing, stale or forged signature. */
|
|
367
|
+
readonly verify: (headers: Readonly<Record<string, string | undefined>>, rawBody: string, secret: string, nowSeconds?: number) => Effect.Effect<unknown, MailWebhookError>;
|
|
368
|
+
}
|
|
296
369
|
|
|
297
370
|
export declare interface MailSendRecord {
|
|
298
371
|
readonly id: string;
|
|
@@ -356,6 +429,69 @@ export declare interface MailServiceShape {
|
|
|
356
429
|
readonly unsuppress: (tenantId: string | null, email: string) => Effect.Effect<void>;
|
|
357
430
|
/** Is this address currently suppressed for the tenant? */
|
|
358
431
|
readonly isSuppressed: (tenantId: string | null, email: string) => Effect.Effect<boolean>;
|
|
432
|
+
/** The provider behind the service. `managesUnsubscribe` tells a broadcast
|
|
433
|
+
* sender whether the provider already puts an unsubscribe link in the mail
|
|
434
|
+
* (then the body needs none of its own). Absent on a hand-built service. */
|
|
435
|
+
readonly provider?: {
|
|
436
|
+
readonly name: string;
|
|
437
|
+
readonly managesUnsubscribe: boolean;
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Which kind of mail this is. `transactional` (the default) is one-to-one mail
|
|
443
|
+
* a person's own action triggered — an invitation, a receipt, a password
|
|
444
|
+
* reset. `broadcast` is one-to-many mail — a newsletter, a campaign. Providers
|
|
445
|
+
* that separate the two (Lettermint routes, Postmark message streams) send
|
|
446
|
+
* them from different infrastructure, so a newsletter's complaints never cost
|
|
447
|
+
* a password reset its inbox placement.
|
|
448
|
+
*/
|
|
449
|
+
export declare type MailStream = 'transactional' | 'broadcast';
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* How a recipient leaves a broadcast. `oneClickUrl` is the HTTPS endpoint a
|
|
453
|
+
* mail client POSTs `List-Unsubscribe=One-Click` to (RFC 8058); the service
|
|
454
|
+
* turns it into the `List-Unsubscribe` + `List-Unsubscribe-Post` headers.
|
|
455
|
+
* Required on a broadcast unless the provider unsubscribes recipients itself
|
|
456
|
+
* (`MailProvider.managesUnsubscribe`).
|
|
457
|
+
*/
|
|
458
|
+
export declare interface MailUnsubscribe {
|
|
459
|
+
readonly oneClickUrl: string;
|
|
460
|
+
/** Optional `mailto:` alternative listed beside the URL. */
|
|
461
|
+
readonly mailto?: string;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** A webhook that failed verification — answer 401, never process it. */
|
|
465
|
+
export declare class MailWebhookError extends MailWebhookError_base {
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
declare const MailWebhookError_base: Schema.TaggedErrorClass<MailWebhookError, "MailWebhookError", {
|
|
469
|
+
readonly _tag: Schema.tag<"MailWebhookError">;
|
|
470
|
+
} & {
|
|
471
|
+
provider: typeof Schema.String;
|
|
472
|
+
message: typeof Schema.String;
|
|
473
|
+
}>;
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* The provider's delivery-event webhook, mounted by the plugin. Bounces and
|
|
477
|
+
* spam complaints suppress the address in the plugin's OWN suppression store
|
|
478
|
+
* — the one every send checks — then `onEvent` sees every event.
|
|
479
|
+
*/
|
|
480
|
+
export declare interface MailWebhookOptions {
|
|
481
|
+
/** Signing secret. Default: `MAIL_WEBHOOK_SECRET`. Without one the endpoint
|
|
482
|
+
* answers 503, and a production boot refuses. */
|
|
483
|
+
readonly secret?: string;
|
|
484
|
+
/** Default `/webhooks/mail`. */
|
|
485
|
+
readonly path?: string;
|
|
486
|
+
/**
|
|
487
|
+
* Every verified event, after suppression — an unsubscribe to record, a
|
|
488
|
+
* bounce to mark on the row the message's `metadata` names. A failure
|
|
489
|
+
* answers 500 and the provider redelivers, so act idempotently
|
|
490
|
+
* (`event.eventId`). `store` is the app's boot store, as on any plugin route.
|
|
491
|
+
*/
|
|
492
|
+
readonly onEvent?: (event: MailEvent, ctx: {
|
|
493
|
+
readonly store: PluginHttpRouteRequest['store'];
|
|
494
|
+
}) => Effect.Effect<void, unknown> | Promise<void> | void;
|
|
359
495
|
}
|
|
360
496
|
|
|
361
497
|
export declare const makeScheduler: (options: SchedulerOptions) => Scheduler;
|
|
@@ -378,30 +514,33 @@ export declare interface Outbox {
|
|
|
378
514
|
/** Normalise a provider webhook payload to MailEvents. Unknown shapes → []. */
|
|
379
515
|
export declare const parseMailEvent: (provider: string, payload: unknown) => ReadonlyArray<MailEvent>;
|
|
380
516
|
|
|
381
|
-
export declare interface PostgresIdempotencyOptions {
|
|
382
|
-
readonly table?: string;
|
|
383
|
-
readonly maxConnections?: number;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
517
|
/**
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
518
|
+
* Dedup in the app's own database: `_voltro_mail_idempotency`, declared through
|
|
519
|
+
* the plugin's `extendSchema` (`storeTables.ts`) and written through the app's
|
|
520
|
+
* `DataStore`, bound at `bindDataStore`. `reserve` is an `insertIgnore` on the
|
|
521
|
+
* (tenant, key) primary key, and its outcome comes from the store's own insert
|
|
522
|
+
* decision — exactly one replica sees `inserted`, so exactly one send fires
|
|
523
|
+
* fleet-wide. Unlike the suppression store this does NOT fail open: if the
|
|
524
|
+
* database is unreachable a reserve reports `won: false` (skip) so a blip can
|
|
525
|
+
* never turn into a double-send — the safer failure for an at-least-once
|
|
526
|
+
* webhook.
|
|
393
527
|
*/
|
|
394
|
-
export declare const postgresIdempotencyStore: (
|
|
395
|
-
|
|
396
|
-
export declare interface PostgresSuppressionOptions {
|
|
397
|
-
readonly table?: string;
|
|
398
|
-
readonly maxConnections?: number;
|
|
399
|
-
}
|
|
528
|
+
export declare const postgresIdempotencyStore: () => IdempotencyStore;
|
|
400
529
|
|
|
401
|
-
|
|
530
|
+
/**
|
|
531
|
+
* The suppression list in the app's own database: `_voltro_mail_suppression`,
|
|
532
|
+
* declared through the plugin's `extendSchema` (`storeTables.ts`) and written
|
|
533
|
+
* through the app's `DataStore`, bound at `bindDataStore`. Multi-node correct on
|
|
534
|
+
* every dialect the app runs on; nothing opens a connection of its own.
|
|
535
|
+
*/
|
|
536
|
+
export declare const postgresSuppressionStore: () => SuppressionStore;
|
|
402
537
|
|
|
403
538
|
export declare const postmarkProvider: (options: {
|
|
404
539
|
readonly serverToken: string;
|
|
540
|
+
readonly streams?: {
|
|
541
|
+
readonly transactional?: string;
|
|
542
|
+
readonly broadcast?: string;
|
|
543
|
+
};
|
|
405
544
|
}) => MailProvider;
|
|
406
545
|
|
|
407
546
|
/** Stable snapshot of everything the memory provider has "sent". */
|
|
@@ -537,6 +676,9 @@ export declare interface SuppressionStore {
|
|
|
537
676
|
readonly unsuppress: (tenantId: string | null, email: string) => Effect.Effect<void>;
|
|
538
677
|
readonly activate?: (env: NodeJS.ProcessEnv) => Promise<void>;
|
|
539
678
|
readonly deactivate?: () => Promise<void>;
|
|
679
|
+
/** Receives the app's `DataStore` at `bindDataStore`, for a store kept in the
|
|
680
|
+
* app's own database. */
|
|
681
|
+
readonly bind?: (store: DataStore) => Promise<void>;
|
|
540
682
|
}
|
|
541
683
|
|
|
542
684
|
/** Send a registered template instead of raw html/text. */
|
|
@@ -558,8 +700,19 @@ export declare interface TemplateMessage {
|
|
|
558
700
|
readonly scheduledAt?: Date;
|
|
559
701
|
/** See `MailMessage.idempotencyKey`. */
|
|
560
702
|
readonly idempotencyKey?: string;
|
|
703
|
+
/** See `MailMessage.stream`. */
|
|
704
|
+
readonly stream?: MailStream;
|
|
705
|
+
/** See `MailMessage.unsubscribe`. */
|
|
706
|
+
readonly unsubscribe?: MailUnsubscribe;
|
|
707
|
+
/** See `MailMessage.metadata`. */
|
|
708
|
+
readonly metadata?: Readonly<Record<string, string>>;
|
|
561
709
|
}
|
|
562
710
|
|
|
711
|
+
/** Lettermint signs `${t}.${rawBody}` with HMAC-SHA256 under the webhook
|
|
712
|
+
* secret: `x-lettermint-signature: t=<unix>,v1=<hex>` (several `v1` during a
|
|
713
|
+
* secret rotation), `x-lettermint-delivery: <unix>` must equal `t`. */
|
|
714
|
+
export declare const verifyLettermintSignature: (headers: Readonly<Record<string, string | undefined>>, rawBody: string, secret: string, nowSeconds?: number, toleranceSeconds?: number) => Effect.Effect<unknown, MailWebhookError>;
|
|
715
|
+
|
|
563
716
|
export { VoltroPlugin }
|
|
564
717
|
|
|
565
718
|
export { }
|