@voltro/plugin-mail 0.80.0 → 0.82.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 +1610 -6
- package/README.md +1 -1
- package/THIRD-PARTY-NOTICES.md +2 -2
- package/dist/index.d.ts +152 -5
- package/dist/index.js +489 -283
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @voltro/plugin-mail
|
|
4
4
|
|
|
5
|
-
**Transactional email
|
|
5
|
+
**Transactional and broadcast email — one MailProvider contract, multiple adapters (Lettermint / Resend / Postmark / SendGrid / SES / Mailgun / SMTP + console/memory for dev/test). Exposes a MailService via the plugin services layer with transient retry, suppression, a mounted delivery-event webhook and a dev allowlist.**
|
|
6
6
|
|
|
7
7
|
[📖 Documentation](https://docs.voltro.dev/docs/plugins/mail) · [voltro.dev](https://voltro.dev) · [Voltro Cloud](https://voltro.cloud)
|
|
8
8
|
|
package/THIRD-PARTY-NOTICES.md
CHANGED
|
@@ -4348,7 +4348,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
|
4348
4348
|
THE SOFTWARE.
|
|
4349
4349
|
```
|
|
4350
4350
|
|
|
4351
|
-
## nodemailer@
|
|
4351
|
+
## nodemailer@10.0.11
|
|
4352
4352
|
|
|
4353
4353
|
License: MIT-0
|
|
4354
4354
|
|
|
@@ -5155,7 +5155,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
5155
5155
|
SOFTWARE.
|
|
5156
5156
|
```
|
|
5157
5157
|
|
|
5158
|
-
## undici@8.
|
|
5158
|
+
## undici@8.11.2
|
|
5159
5159
|
|
|
5160
5160
|
License: MIT
|
|
5161
5161
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Context } from 'effect';
|
|
2
2
|
import { Effect } from 'effect';
|
|
3
3
|
import { Layer } from 'effect';
|
|
4
|
+
import { PluginEgressDeclaration } from '@voltro/protocol';
|
|
5
|
+
import { PluginHttpRouteRequest } from '@voltro/protocol';
|
|
4
6
|
import { Schema } from 'effect';
|
|
5
7
|
import { VoltroPlugin } from '@voltro/protocol';
|
|
6
8
|
|
|
@@ -121,6 +123,21 @@ export declare interface IdempotencyStore {
|
|
|
121
123
|
* exports (name + subject fn + render fn + a props schema). */
|
|
122
124
|
export declare const isEmailTemplate: (v: unknown) => v is AnyEmailTemplate;
|
|
123
125
|
|
|
126
|
+
export declare interface LettermintOptions {
|
|
127
|
+
/** Project API token (`lm_…`), sent as `x-lettermint-token`. */
|
|
128
|
+
readonly projectToken: string;
|
|
129
|
+
/** Default `https://api.lettermint.co/v1`. */
|
|
130
|
+
readonly baseUrl?: string;
|
|
131
|
+
/** Route slug per stream. Default: the project's default route (its
|
|
132
|
+
* transactional one) for `transactional`, `broadcast` for `broadcast`. */
|
|
133
|
+
readonly routes?: {
|
|
134
|
+
readonly transactional?: string;
|
|
135
|
+
readonly broadcast?: string;
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export declare const lettermintProvider: (options: LettermintOptions) => MailProvider;
|
|
140
|
+
|
|
124
141
|
/** Names (+ locale variants) of every globally-registered template. */
|
|
125
142
|
export declare const listGlobalTemplates: () => ReadonlyArray<{
|
|
126
143
|
readonly name: string;
|
|
@@ -141,6 +158,12 @@ export declare interface MailAttachment {
|
|
|
141
158
|
readonly contentType?: string;
|
|
142
159
|
}
|
|
143
160
|
|
|
161
|
+
/** What mail egress a provider choice declares. */
|
|
162
|
+
export declare const mailEgressOf: (options: {
|
|
163
|
+
readonly provider?: MailProviderName | MailProvider;
|
|
164
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
165
|
+
}) => ReadonlyArray<PluginEgressDeclaration>;
|
|
166
|
+
|
|
144
167
|
/**
|
|
145
168
|
* Mail failure. `transient` drives retry — HTTP 408 / 429 / 5xx and network
|
|
146
169
|
* failures are retryable; other HTTP 4xx are not. SMTP instead retries 4xx
|
|
@@ -164,10 +187,21 @@ export declare interface MailEvent {
|
|
|
164
187
|
readonly email: string;
|
|
165
188
|
readonly provider: string;
|
|
166
189
|
readonly messageId?: string;
|
|
190
|
+
/** The provider's id for this event — the key to act on a redelivered
|
|
191
|
+
* webhook only once. */
|
|
192
|
+
readonly eventId?: string;
|
|
193
|
+
/** What the message was sent with as `metadata`, returned by the provider. */
|
|
194
|
+
readonly metadata?: Readonly<Record<string, string>>;
|
|
167
195
|
readonly raw: unknown;
|
|
168
196
|
}
|
|
169
197
|
|
|
170
|
-
|
|
198
|
+
/** `unsubscribed`: the recipient left a broadcast through the provider's own
|
|
199
|
+
* unsubscribe (a provider that `managesUnsubscribe`). It is NOT suppressed
|
|
200
|
+
* app-wide — it ended one list, not the person's transactional mail.
|
|
201
|
+
* `unsuppressed`: the provider lifted a suppression (someone removed it in
|
|
202
|
+
* its dashboard) — lifted here too, or the app would keep dropping mail the
|
|
203
|
+
* provider would now deliver. */
|
|
204
|
+
export declare type MailEventType = 'delivered' | 'bounced' | 'complained' | 'unsubscribed' | 'unsuppressed' | 'opened' | 'clicked' | 'unknown';
|
|
171
205
|
|
|
172
206
|
export declare interface MailgunOptions {
|
|
173
207
|
readonly apiKey: string;
|
|
@@ -208,12 +242,30 @@ export declare interface MailMessage {
|
|
|
208
242
|
* step. The first send with a given key delivers; a replay with the same
|
|
209
243
|
* key is a no-op returning the recorded result. Scoped per tenant. */
|
|
210
244
|
readonly idempotencyKey?: string;
|
|
245
|
+
/** `transactional` (default) or `broadcast`. See `MailStream`. */
|
|
246
|
+
readonly stream?: MailStream;
|
|
247
|
+
/** How a recipient leaves a broadcast. See `MailUnsubscribe`. */
|
|
248
|
+
readonly unsubscribe?: MailUnsubscribe;
|
|
249
|
+
/** Key/value pairs the provider stores with the message and returns on its
|
|
250
|
+
* delivery events (`MailEvent.metadata`) — how a bounce finds its way back
|
|
251
|
+
* to the row that caused it. Never added to the mail itself. Lettermint and
|
|
252
|
+
* Postmark carry it; other providers ignore it. */
|
|
253
|
+
readonly metadata?: Readonly<Record<string, string>>;
|
|
211
254
|
}
|
|
212
255
|
|
|
213
|
-
|
|
256
|
+
/**
|
|
257
|
+
* The mail plugin, with the service it built — the same one handlers yield as
|
|
258
|
+
* `MailService`. For wiring that happens in `app.config.ts`, before any
|
|
259
|
+
* handler runs: `authRoutesPlugin({ sendEmail: mailSender(mail.service) })`.
|
|
260
|
+
*/
|
|
261
|
+
export declare type MailPlugin = VoltroPlugin & {
|
|
262
|
+
readonly service: MailServiceShape;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
export declare const mailPlugin: (options?: MailPluginOptions) => MailPlugin;
|
|
214
266
|
|
|
215
267
|
export declare interface MailPluginOptions {
|
|
216
|
-
/** `'resend' | 'postmark' | 'sendgrid' | 'smtp' | 'console' | 'memory'`
|
|
268
|
+
/** `'lettermint' | 'resend' | 'postmark' | 'sendgrid' | 'smtp' | 'console' | 'memory'`
|
|
217
269
|
* (built from env) or a `MailProvider`. Default: `MAIL_PROVIDER` env, else
|
|
218
270
|
* `'console'`. */
|
|
219
271
|
readonly provider?: MailProviderName | MailProvider;
|
|
@@ -247,7 +299,9 @@ export declare interface MailPluginOptions {
|
|
|
247
299
|
/** How often (ms) the scheduled-send flush loop wakes to deliver due held
|
|
248
300
|
* messages. Default 1000. Ignored when the provider schedules natively. */
|
|
249
301
|
readonly scheduleTickMs?: number;
|
|
250
|
-
/**
|
|
302
|
+
/** Mount the provider's delivery-event webhook. Needs a provider with a
|
|
303
|
+
* `webhook` verifier (Lettermint). See `MailWebhookOptions`. */
|
|
304
|
+
readonly webhook?: MailWebhookOptions;
|
|
251
305
|
/**
|
|
252
306
|
* Namespace for this plugin's inspect endpoints. Default `mail`.
|
|
253
307
|
*
|
|
@@ -290,9 +344,24 @@ export declare interface MailProvider {
|
|
|
290
344
|
/** One request for N personalized messages. Each keeps its own subject /
|
|
291
345
|
* body / template vars. Returns one `SendResult` per input, in order. */
|
|
292
346
|
readonly sendBatch?: (messages: ReadonlyArray<ResolvedMessage>) => Effect.Effect<ReadonlyArray<SendResult>, MailError>;
|
|
347
|
+
/** True if the provider adds its own unsubscribe link and one-click headers
|
|
348
|
+
* to broadcast mail and suppresses whoever uses them (Lettermint's
|
|
349
|
+
* broadcast route). The service then sends no `List-Unsubscribe` of its own
|
|
350
|
+
* — two competing unsubscribe mechanisms would each only half work. */
|
|
351
|
+
readonly managesUnsubscribe?: boolean;
|
|
352
|
+
/** Verifies and parses the provider's delivery-event webhook. Present →
|
|
353
|
+
* `mailPlugin({ webhook })` can mount the endpoint. */
|
|
354
|
+
readonly webhook?: MailProviderWebhook;
|
|
293
355
|
}
|
|
294
356
|
|
|
295
|
-
export declare type MailProviderName = 'resend' | 'postmark' | 'sendgrid' | 'ses' | 'mailgun' | 'smtp' | 'console' | 'memory';
|
|
357
|
+
export declare type MailProviderName = 'lettermint' | 'resend' | 'postmark' | 'sendgrid' | 'ses' | 'mailgun' | 'smtp' | 'console' | 'memory';
|
|
358
|
+
|
|
359
|
+
/** A provider's delivery-event webhook: signature check, then normalisation. */
|
|
360
|
+
export declare interface MailProviderWebhook {
|
|
361
|
+
/** Verify the signature over the RAW body and decode it. Fails with
|
|
362
|
+
* `MailWebhookError` for a missing, stale or forged signature. */
|
|
363
|
+
readonly verify: (headers: Readonly<Record<string, string | undefined>>, rawBody: string, secret: string, nowSeconds?: number) => Effect.Effect<unknown, MailWebhookError>;
|
|
364
|
+
}
|
|
296
365
|
|
|
297
366
|
export declare interface MailSendRecord {
|
|
298
367
|
readonly id: string;
|
|
@@ -356,6 +425,69 @@ export declare interface MailServiceShape {
|
|
|
356
425
|
readonly unsuppress: (tenantId: string | null, email: string) => Effect.Effect<void>;
|
|
357
426
|
/** Is this address currently suppressed for the tenant? */
|
|
358
427
|
readonly isSuppressed: (tenantId: string | null, email: string) => Effect.Effect<boolean>;
|
|
428
|
+
/** The provider behind the service. `managesUnsubscribe` tells a broadcast
|
|
429
|
+
* sender whether the provider already puts an unsubscribe link in the mail
|
|
430
|
+
* (then the body needs none of its own). Absent on a hand-built service. */
|
|
431
|
+
readonly provider?: {
|
|
432
|
+
readonly name: string;
|
|
433
|
+
readonly managesUnsubscribe: boolean;
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Which kind of mail this is. `transactional` (the default) is one-to-one mail
|
|
439
|
+
* a person's own action triggered — an invitation, a receipt, a password
|
|
440
|
+
* reset. `broadcast` is one-to-many mail — a newsletter, a campaign. Providers
|
|
441
|
+
* that separate the two (Lettermint routes, Postmark message streams) send
|
|
442
|
+
* them from different infrastructure, so a newsletter's complaints never cost
|
|
443
|
+
* a password reset its inbox placement.
|
|
444
|
+
*/
|
|
445
|
+
export declare type MailStream = 'transactional' | 'broadcast';
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* How a recipient leaves a broadcast. `oneClickUrl` is the HTTPS endpoint a
|
|
449
|
+
* mail client POSTs `List-Unsubscribe=One-Click` to (RFC 8058); the service
|
|
450
|
+
* turns it into the `List-Unsubscribe` + `List-Unsubscribe-Post` headers.
|
|
451
|
+
* Required on a broadcast unless the provider unsubscribes recipients itself
|
|
452
|
+
* (`MailProvider.managesUnsubscribe`).
|
|
453
|
+
*/
|
|
454
|
+
export declare interface MailUnsubscribe {
|
|
455
|
+
readonly oneClickUrl: string;
|
|
456
|
+
/** Optional `mailto:` alternative listed beside the URL. */
|
|
457
|
+
readonly mailto?: string;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** A webhook that failed verification — answer 401, never process it. */
|
|
461
|
+
export declare class MailWebhookError extends MailWebhookError_base {
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
declare const MailWebhookError_base: Schema.TaggedErrorClass<MailWebhookError, "MailWebhookError", {
|
|
465
|
+
readonly _tag: Schema.tag<"MailWebhookError">;
|
|
466
|
+
} & {
|
|
467
|
+
provider: typeof Schema.String;
|
|
468
|
+
message: typeof Schema.String;
|
|
469
|
+
}>;
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* The provider's delivery-event webhook, mounted by the plugin. Bounces and
|
|
473
|
+
* spam complaints suppress the address in the plugin's OWN suppression store
|
|
474
|
+
* — the one every send checks — then `onEvent` sees every event.
|
|
475
|
+
*/
|
|
476
|
+
export declare interface MailWebhookOptions {
|
|
477
|
+
/** Signing secret. Default: `MAIL_WEBHOOK_SECRET`. Without one the endpoint
|
|
478
|
+
* answers 503, and a production boot refuses. */
|
|
479
|
+
readonly secret?: string;
|
|
480
|
+
/** Default `/webhooks/mail`. */
|
|
481
|
+
readonly path?: string;
|
|
482
|
+
/**
|
|
483
|
+
* Every verified event, after suppression — an unsubscribe to record, a
|
|
484
|
+
* bounce to mark on the row the message's `metadata` names. A failure
|
|
485
|
+
* answers 500 and the provider redelivers, so act idempotently
|
|
486
|
+
* (`event.eventId`). `store` is the app's boot store, as on any plugin route.
|
|
487
|
+
*/
|
|
488
|
+
readonly onEvent?: (event: MailEvent, ctx: {
|
|
489
|
+
readonly store: PluginHttpRouteRequest['store'];
|
|
490
|
+
}) => Effect.Effect<void, unknown> | Promise<void> | void;
|
|
359
491
|
}
|
|
360
492
|
|
|
361
493
|
export declare const makeScheduler: (options: SchedulerOptions) => Scheduler;
|
|
@@ -402,6 +534,10 @@ export declare const postgresSuppressionStore: (options?: PostgresSuppressionOpt
|
|
|
402
534
|
|
|
403
535
|
export declare const postmarkProvider: (options: {
|
|
404
536
|
readonly serverToken: string;
|
|
537
|
+
readonly streams?: {
|
|
538
|
+
readonly transactional?: string;
|
|
539
|
+
readonly broadcast?: string;
|
|
540
|
+
};
|
|
405
541
|
}) => MailProvider;
|
|
406
542
|
|
|
407
543
|
/** Stable snapshot of everything the memory provider has "sent". */
|
|
@@ -558,8 +694,19 @@ export declare interface TemplateMessage {
|
|
|
558
694
|
readonly scheduledAt?: Date;
|
|
559
695
|
/** See `MailMessage.idempotencyKey`. */
|
|
560
696
|
readonly idempotencyKey?: string;
|
|
697
|
+
/** See `MailMessage.stream`. */
|
|
698
|
+
readonly stream?: MailStream;
|
|
699
|
+
/** See `MailMessage.unsubscribe`. */
|
|
700
|
+
readonly unsubscribe?: MailUnsubscribe;
|
|
701
|
+
/** See `MailMessage.metadata`. */
|
|
702
|
+
readonly metadata?: Readonly<Record<string, string>>;
|
|
561
703
|
}
|
|
562
704
|
|
|
705
|
+
/** Lettermint signs `${t}.${rawBody}` with HMAC-SHA256 under the webhook
|
|
706
|
+
* secret: `x-lettermint-signature: t=<unix>,v1=<hex>` (several `v1` during a
|
|
707
|
+
* secret rotation), `x-lettermint-delivery: <unix>` must equal `t`. */
|
|
708
|
+
export declare const verifyLettermintSignature: (headers: Readonly<Record<string, string | undefined>>, rawBody: string, secret: string, nowSeconds?: number, toleranceSeconds?: number) => Effect.Effect<unknown, MailWebhookError>;
|
|
709
|
+
|
|
563
710
|
export { VoltroPlugin }
|
|
564
711
|
|
|
565
712
|
export { }
|