@voltro/plugin-mail 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.
@@ -0,0 +1,544 @@
1
+ import { Context } from 'effect';
2
+ import { Effect } from 'effect';
3
+ import { Layer } from 'effect';
4
+ import { Schema } from 'effect';
5
+ import { VoltroPlugin } from '@voltro/protocol';
6
+
7
+ /** Any template regardless of its props type — for registries/collections.
8
+ * `P` is invariant across `EmailTemplate` (it appears in the `props` Schema
9
+ * and in `preview`), so no single `EmailTemplate<P>` is a common supertype.
10
+ * This widens every `P`-dependent member to its most permissive accepting
11
+ * form: function params to `never` (accepts any concrete-prop function via
12
+ * contravariance), `props` to `Schema.Schema.Any`, `preview` to `unknown`. */
13
+ export declare interface AnyEmailTemplate {
14
+ readonly name: string;
15
+ readonly subject: (props: never) => string;
16
+ readonly props: Schema.Schema.Any;
17
+ readonly render: (props: never) => unknown;
18
+ readonly locale?: string | undefined;
19
+ readonly preview?: unknown;
20
+ }
21
+
22
+ /**
23
+ * One personalized message in a batch. `to` is a single recipient; the rest
24
+ * of the fields (subject, html/text OR template+props) are per-recipient, so
25
+ * every entry can carry its own template variables. The service applies the
26
+ * default `from`, allowlist, and suppression to each entry, then hands the
27
+ * whole set to the provider's native batch endpoint (Resend/SendGrid) or
28
+ * loops `send` for transports without one.
29
+ */
30
+ export declare type BatchMessage = MailMessage | TemplateMessage;
31
+
32
+ export declare const buildMailService: (options: MailServiceOptions) => MailServiceShape;
33
+
34
+ /** Build the service AND expose its scheduler (for the flush loop). Most
35
+ * consumers want `buildMailService` (the shape only). */
36
+ export declare const buildMailServiceWithScheduler: (options: MailServiceOptions) => BuiltMailService;
37
+
38
+ /** The built service plus the scheduler driving its hold-and-flush loop. The
39
+ * plugin forks `scheduler.tick` on an interval in `onActivate`. */
40
+ export declare interface BuiltMailService {
41
+ readonly service: MailServiceShape;
42
+ readonly scheduler: Scheduler;
43
+ }
44
+
45
+ /** Drop everything — for tests. */
46
+ export declare const clearGlobalTemplates: () => void;
47
+
48
+ /** Drop everything in the in-process buffer. */
49
+ export declare const clearMailBuffer: () => void;
50
+
51
+ export declare const consoleProvider: (log?: (message: string, fields?: Record<string, unknown>) => void) => MailProvider;
52
+
53
+ export declare const createOutbox: (max?: number) => Outbox;
54
+
55
+ /** Define a template. Infers the props type from the `props` Schema so
56
+ * `subject` / `render` receive the decoded shape. */
57
+ export declare const defineEmail: <S extends Schema.Schema.Any>(template: {
58
+ readonly name: string;
59
+ readonly props: S;
60
+ readonly subject: (props: Schema.Schema.Type<S>) => string;
61
+ readonly render: (props: Schema.Schema.Type<S>) => unknown;
62
+ readonly locale?: string;
63
+ /** Realistic example props for the dashboard preview. */
64
+ readonly preview?: Schema.Schema.Type<S>;
65
+ }) => EmailTemplate<Schema.Schema.Type<S>>;
66
+
67
+ /**
68
+ * A React-Email template. `props` is an effect/Schema validated before
69
+ * render; `subject` derives the line from the decoded props; `render`
70
+ * returns the React element (typed `unknown` here so the plugin doesn't
71
+ * hard-depend on react's types). `locale` tags a per-language variant
72
+ * (`welcome.de.email.tsx` → `locale: 'de'`), looked up with fallback to
73
+ * the base template.
74
+ */
75
+ export declare interface EmailTemplate<P = unknown> {
76
+ readonly name: string;
77
+ readonly subject: (props: P) => string;
78
+ readonly props: Schema.Schema<P, P, never> | Schema.Schema.Any;
79
+ readonly render: (props: P) => unknown;
80
+ readonly locale?: string;
81
+ /** Realistic example props — pre-fills the dashboard preview box (and
82
+ * documents what the template expects). Falls back to a schema-derived
83
+ * skeleton when absent. */
84
+ readonly preview?: P;
85
+ }
86
+
87
+ export declare const globalTemplateCount: () => number;
88
+
89
+ /** Apply events to the suppression store: bounce + complaint → suppress the
90
+ * address (per tenant). Returns the parsed events for logging/auditing. */
91
+ export declare const handleMailEvents: (provider: string, payload: unknown, suppression: SuppressionStore, tenantId?: string | null) => Effect.Effect<ReadonlyArray<MailEvent>>;
92
+
93
+ /** Stable-ish held-entry id (avoids Math.random, mirrors providers' synthId).
94
+ * When the message carries an `idempotencyKey`, that keys the hold so a
95
+ * replayed schedule doesn't double-park. */
96
+ export declare const heldId: (message: MailMessage) => string;
97
+
98
+ /** A message parked until its `dueAt`. Carries the fully-formed raw message
99
+ * the service already resolved (default-from applied, template rendered). */
100
+ export declare interface HeldMessage {
101
+ /** Opaque id for the held entry (dedupes re-enqueue). */
102
+ readonly id: string;
103
+ /** epoch-ms the message becomes due. */
104
+ readonly dueAt: number;
105
+ /** The raw message to hand back to `send` when due (never a TemplateMessage
106
+ * — the service renders templates BEFORE holding, so the flush is a plain
107
+ * provider send). */
108
+ readonly message: MailMessage;
109
+ }
110
+
111
+ export declare interface IdempotencyStore {
112
+ /** Atomically claim `key` for `tenantId`. First caller wins; replays lose. */
113
+ readonly reserve: (tenantId: string | null, key: string) => Effect.Effect<Reservation>;
114
+ /** Stamp the winning send's result so a later replay returns it verbatim. */
115
+ readonly record: (tenantId: string | null, key: string, result: SendResult) => Effect.Effect<void>;
116
+ readonly activate?: (env: NodeJS.ProcessEnv) => Promise<void>;
117
+ readonly deactivate?: () => Promise<void>;
118
+ }
119
+
120
+ /** Structural guard: recognise a `defineEmail(...)` result among a module's
121
+ * exports (name + subject fn + render fn + a props schema). */
122
+ export declare const isEmailTemplate: (v: unknown) => v is AnyEmailTemplate;
123
+
124
+ /** Names (+ locale variants) of every globally-registered template. */
125
+ export declare const listGlobalTemplates: () => ReadonlyArray<{
126
+ readonly name: string;
127
+ readonly locale?: string;
128
+ }>;
129
+
130
+ /** Look up a globally-registered template, locale-variant first. */
131
+ export declare const lookupGlobalTemplate: (name: string, locale: string | undefined) => AnyEmailTemplate | undefined;
132
+
133
+ /** A file attached to a message. `content` is the raw bytes as a base64
134
+ * string (transport-agnostic — every adapter maps it to its own shape). */
135
+ export declare interface MailAttachment {
136
+ /** Displayed filename, e.g. `invoice.pdf`. */
137
+ readonly filename: string;
138
+ /** File bytes as a base64-encoded string. */
139
+ readonly content: string;
140
+ /** MIME type, e.g. `application/pdf`. Defaults per-provider when omitted. */
141
+ readonly contentType?: string;
142
+ }
143
+
144
+ /**
145
+ * Mail failure. `transient` drives retry — `true` for 429 / 5xx / network
146
+ * blips (worth retrying), `false` for 4xx / config errors (don't). Surfaced
147
+ * typed so handlers can `Effect.catchTag('MailError', …)`.
148
+ */
149
+ export declare class MailError extends MailError_base {
150
+ }
151
+
152
+ declare const MailError_base: Schema.TaggedErrorClass<MailError, "MailError", {
153
+ readonly _tag: Schema.tag<"MailError">;
154
+ } & {
155
+ provider: typeof Schema.String;
156
+ message: typeof Schema.String;
157
+ transient: typeof Schema.Boolean;
158
+ status: Schema.optional<typeof Schema.Number>;
159
+ }>;
160
+
161
+ export declare interface MailEvent {
162
+ readonly type: MailEventType;
163
+ readonly email: string;
164
+ readonly provider: string;
165
+ readonly messageId?: string;
166
+ readonly raw: unknown;
167
+ }
168
+
169
+ export declare type MailEventType = 'delivered' | 'bounced' | 'complained' | 'opened' | 'clicked' | 'unknown';
170
+
171
+ export declare interface MailgunOptions {
172
+ readonly apiKey: string;
173
+ readonly domain: string;
174
+ /** EU region → `https://api.eu.mailgun.net`. Default: US. */
175
+ readonly baseUrl?: string;
176
+ }
177
+
178
+ export declare const mailgunProvider: (options: MailgunOptions) => MailProvider;
179
+
180
+ /** A message to send. `from` is optional here — the service fills it
181
+ * from the plugin's configured default before handing it to a provider. */
182
+ export declare interface MailMessage {
183
+ readonly to: string | ReadonlyArray<string>;
184
+ readonly from?: string;
185
+ readonly replyTo?: string;
186
+ readonly cc?: string | ReadonlyArray<string>;
187
+ readonly bcc?: string | ReadonlyArray<string>;
188
+ readonly subject: string;
189
+ readonly html?: string;
190
+ readonly text?: string;
191
+ readonly headers?: Readonly<Record<string, string>>;
192
+ /** Provider-side tags/labels where supported (Resend, SendGrid). */
193
+ readonly tags?: ReadonlyArray<string>;
194
+ /** File attachments. Mapped to each transport's native attachment shape
195
+ * (Resend/Postmark/SendGrid/SMTP). `content` is base64. */
196
+ readonly attachments?: ReadonlyArray<MailAttachment>;
197
+ /** Scopes the suppression check (bounced/complained addresses) to a
198
+ * tenant. Omit for app-global suppression. */
199
+ readonly tenantId?: string | null;
200
+ /** Deliver at (or after) this instant instead of now. Providers that
201
+ * support scheduling natively (Resend, SendGrid, Mailgun) receive it on
202
+ * the wire; for the rest the service HOLDS the message and flushes it
203
+ * once the time arrives (`scheduleFallback`). A time in the past sends
204
+ * immediately. */
205
+ readonly scheduledAt?: Date;
206
+ /** De-duplicates a send across webhook/retry replays OUTSIDE a workflow
207
+ * step. The first send with a given key delivers; a replay with the same
208
+ * key is a no-op returning the recorded result. Scoped per tenant. */
209
+ readonly idempotencyKey?: string;
210
+ }
211
+
212
+ export declare const mailPlugin: (options?: MailPluginOptions) => VoltroPlugin;
213
+
214
+ export declare interface MailPluginOptions {
215
+ /** `'resend' | 'postmark' | 'sendgrid' | 'smtp' | 'console' | 'memory'`
216
+ * (built from env) or a `MailProvider`. Default: `MAIL_PROVIDER` env, else
217
+ * `'console'`. */
218
+ readonly provider?: MailProviderName | MailProvider;
219
+ /** API key/token for string providers (else read from per-provider env). */
220
+ readonly apiKey?: string;
221
+ /** Default `From` when a message omits it. */
222
+ readonly from?: string;
223
+ /** Default `Reply-To`. */
224
+ readonly replyTo?: string;
225
+ /** Dev safety: outside production, only send to these addresses. Defaults
226
+ * to the comma-separated `MAIL_ALLOWLIST` env. */
227
+ readonly allowlist?: ReadonlyArray<string>;
228
+ /** Transient-failure retries (429 / 5xx / network). Default 3. */
229
+ readonly attempts?: number;
230
+ /** Registered `*.email.tsx` templates (from `defineEmail`), enabling
231
+ * `mail.send({ template, props })`. */
232
+ readonly templates?: ReadonlyArray<AnyEmailTemplate>;
233
+ /** Suppression backend for bounced/complained addresses: `'memory'`
234
+ * (default, single-node), `'postgres'` (own pool from `DB_*`/`PG_*` env),
235
+ * or a custom `SuppressionStore`. */
236
+ readonly suppression?: 'memory' | 'postgres' | SuppressionStore;
237
+ /** Per-send idempotency backend (webhook-retry double-send guard):
238
+ * `'memory'` (default, single-node), `'postgres'` (own pool from
239
+ * `DB_*`/`PG_*` env), or a custom `IdempotencyStore`. Engaged only for a
240
+ * send carrying `idempotencyKey`. */
241
+ readonly idempotency?: 'memory' | 'postgres' | IdempotencyStore;
242
+ /** Backing store for HELD scheduled messages (providers without native
243
+ * send-later — console/memory/SMTP/SES). Default: in-memory. A custom
244
+ * `ScheduleStore` makes scheduled sends durable + cross-replica. */
245
+ readonly schedule?: ScheduleStore;
246
+ /** How often (ms) the scheduled-send flush loop wakes to deliver due held
247
+ * messages. Default 1000. Ignored when the provider schedules natively. */
248
+ readonly scheduleTickMs?: number;
249
+ /** Disambiguates multiple instances of this plugin in one app. */
250
+ readonly name?: string;
251
+ }
252
+
253
+ /** A transport. Receives a fully-resolved message; returns the send result
254
+ * or fails with a (possibly transient) MailError.
255
+ *
256
+ * Two OPTIONAL capabilities let a provider skip the service's generic
257
+ * fallbacks:
258
+ * - `supportsScheduling`: the provider maps `message.scheduledAt` onto its
259
+ * own send-later field (Resend/SendGrid/Mailgun). When absent/false the
260
+ * service holds the message and flushes it at the due time.
261
+ * - `sendBatch`: one provider call for N personalized messages
262
+ * (Resend/SendGrid). When absent the service loops `send`. */
263
+ export declare interface MailProvider {
264
+ readonly name: string;
265
+ readonly send: (message: ResolvedMessage) => Effect.Effect<SendResult, MailError>;
266
+ /** True if `send` honours `message.scheduledAt` natively (no service-side
267
+ * hold-and-flush needed). */
268
+ readonly supportsScheduling?: boolean;
269
+ /** One request for N personalized messages. Each keeps its own subject /
270
+ * body / template vars. Returns one `SendResult` per input, in order. */
271
+ readonly sendBatch?: (messages: ReadonlyArray<ResolvedMessage>) => Effect.Effect<ReadonlyArray<SendResult>, MailError>;
272
+ }
273
+
274
+ export declare type MailProviderName = 'resend' | 'postmark' | 'sendgrid' | 'ses' | 'mailgun' | 'smtp' | 'console' | 'memory';
275
+
276
+ export declare interface MailSendRecord {
277
+ readonly id: string;
278
+ readonly to: ReadonlyArray<string>;
279
+ readonly from: string;
280
+ readonly subject: string;
281
+ /** Provider that handled it (`resend`/`memory`/`skipped`/`suppressed`/…). */
282
+ readonly provider: string;
283
+ readonly ok: boolean;
284
+ readonly error?: string;
285
+ /** epoch-ms when the send completed. */
286
+ readonly ts: number;
287
+ /** Template name when sent via `send({ template })`. */
288
+ readonly template?: string;
289
+ }
290
+
291
+ export declare class MailService extends MailService_base {
292
+ }
293
+
294
+ declare const MailService_base: Context.TagClass<MailService, "@voltro/plugin-mail/MailService", MailServiceShape>;
295
+
296
+ export declare const mailServiceLayer: (options: MailServiceOptions) => Layer.Layer<MailService>;
297
+
298
+ export declare interface MailServiceOptions {
299
+ readonly provider: MailProvider;
300
+ readonly from?: string;
301
+ readonly replyTo?: string;
302
+ /** Dev safety: when set + not production, only these recipients are sent to. */
303
+ readonly allowlist?: ReadonlyArray<string>;
304
+ /** Transient-failure retries. Default 3. */
305
+ readonly attempts?: number;
306
+ readonly production?: boolean;
307
+ /** Registered `*.email.tsx` templates, for `send({ template, props })`. */
308
+ readonly templates?: ReadonlyArray<AnyEmailTemplate>;
309
+ /** Suppression list (bounced/complained addresses). Default: in-memory. */
310
+ readonly suppression?: SuppressionStore;
311
+ /** Per-send idempotency store (webhook-retry double-send guard). Default:
312
+ * in-memory. Only engaged for a send carrying an `idempotencyKey`. */
313
+ readonly idempotency?: IdempotencyStore;
314
+ /** Backing store for HELD scheduled messages (providers without native
315
+ * send-later). Default: in-memory. */
316
+ readonly schedule?: ScheduleStore;
317
+ /** Called after every send (success, failure, or skipped) — feeds the
318
+ * dev outbox the dashboard reads. */
319
+ readonly onSent?: (record: MailSendRecord) => void;
320
+ }
321
+
322
+ /** The service handlers consume: `const mail = yield* MailService`. */
323
+ export declare interface MailServiceShape {
324
+ readonly send: (message: MailMessage | TemplateMessage) => Effect.Effect<SendResult, MailError>;
325
+ /** Send N personalized messages (each with its own subject / body /
326
+ * template vars) in one provider batch call where the transport supports
327
+ * it, else looped `send`. Returns one `SendResult` per input, in order.
328
+ * Allowlist, suppression, default-`from`, scheduling and idempotency
329
+ * apply per entry, exactly as for a single `send`. */
330
+ readonly sendBatch: (messages: ReadonlyArray<BatchMessage>) => Effect.Effect<ReadonlyArray<SendResult>, MailError>;
331
+ /** Mark an address as undeliverable/opted-out (per tenant). Future sends
332
+ * to it are dropped. Called automatically on bounce/complaint events. */
333
+ readonly suppress: (tenantId: string | null, email: string, reason: string) => Effect.Effect<void>;
334
+ /** Remove an address from the suppression list. */
335
+ readonly unsuppress: (tenantId: string | null, email: string) => Effect.Effect<void>;
336
+ /** Is this address currently suppressed for the tenant? */
337
+ readonly isSuppressed: (tenantId: string | null, email: string) => Effect.Effect<boolean>;
338
+ }
339
+
340
+ export declare const makeScheduler: (options: SchedulerOptions) => Scheduler;
341
+
342
+ export declare const memoryIdempotencyStore: () => IdempotencyStore;
343
+
344
+ export declare const memoryProvider: () => MailProvider;
345
+
346
+ export declare const memoryScheduleStore: () => ScheduleStore;
347
+
348
+ export declare const memorySuppressionStore: () => SuppressionStore;
349
+
350
+ export declare interface Outbox {
351
+ readonly record: (r: MailSendRecord) => void;
352
+ /** Newest first. */
353
+ readonly list: () => ReadonlyArray<MailSendRecord>;
354
+ readonly clear: () => void;
355
+ }
356
+
357
+ /** Normalise a provider webhook payload to MailEvents. Unknown shapes → []. */
358
+ export declare const parseMailEvent: (provider: string, payload: unknown) => ReadonlyArray<MailEvent>;
359
+
360
+ export declare interface PostgresIdempotencyOptions {
361
+ readonly table?: string;
362
+ readonly maxConnections?: number;
363
+ }
364
+
365
+ /**
366
+ * Postgres dedup store. `reserve` is an atomic
367
+ * `INSERT … ON CONFLICT (tenant, key) DO NOTHING RETURNING` — exactly one
368
+ * replica sees the insert, so exactly one send fires fleet-wide. Unlike the
369
+ * suppression store this does NOT fail open: if the dedup DB is unreachable a
370
+ * reserve reports `won: false` (skip) so a blip can never turn into a
371
+ * double-send — the safer failure for an at-least-once webhook.
372
+ */
373
+ export declare const postgresIdempotencyStore: (options?: PostgresIdempotencyOptions) => IdempotencyStore;
374
+
375
+ export declare interface PostgresSuppressionOptions {
376
+ readonly table?: string;
377
+ readonly maxConnections?: number;
378
+ }
379
+
380
+ export declare const postgresSuppressionStore: (options?: PostgresSuppressionOptions) => SuppressionStore;
381
+
382
+ export declare const postmarkProvider: (options: {
383
+ readonly serverToken: string;
384
+ }) => MailProvider;
385
+
386
+ /** Stable snapshot of everything the memory provider has "sent". */
387
+ export declare const readMailBuffer: () => ReadonlyArray<ResolvedMessage>;
388
+
389
+ /** Register CLI-discovered (or hand-collected) templates. Idempotent per key. */
390
+ export declare const registerEmailTemplates: (templates: ReadonlyArray<AnyEmailTemplate>) => void;
391
+
392
+ export declare interface RenderedEmail {
393
+ readonly subject: string;
394
+ readonly html: string;
395
+ readonly text: string;
396
+ }
397
+
398
+ export declare const renderTemplate: (tpl: AnyEmailTemplate, rawProps: unknown) => Effect.Effect<RenderedEmail, MailError>;
399
+
400
+ export declare const resendProvider: (options: {
401
+ readonly apiKey: string;
402
+ }) => MailProvider;
403
+
404
+ /** Outcome of reserving an idempotency key. */
405
+ export declare interface Reservation {
406
+ /** True → this caller won the key and MUST send. False → a prior send owns
407
+ * it; the caller skips and returns `prior` if present. */
408
+ readonly won: boolean;
409
+ /** The recorded result of the winning send, when a replay finds it. */
410
+ readonly prior?: SendResult;
411
+ }
412
+
413
+ /** What a provider receives — `from` is guaranteed resolved by the service.
414
+ * `scheduledAt` survives only for providers that advertise
415
+ * `supportsScheduling` (the service strips + holds it otherwise). */
416
+ export declare interface ResolvedMessage extends MailMessage {
417
+ readonly from: string;
418
+ }
419
+
420
+ /** Resolve the configured provider. A `MailProvider` object passes through;
421
+ * a string (or `MAIL_PROVIDER` env) builds one from per-provider env.
422
+ * Throws a clear config error on a missing key — surfaces at boot. */
423
+ export declare const resolveProvider: (options: ResolveProviderOptions) => MailProvider;
424
+
425
+ export declare interface ResolveProviderOptions {
426
+ readonly provider?: MailProviderName | MailProvider;
427
+ readonly apiKey?: string;
428
+ readonly env?: NodeJS.ProcessEnv;
429
+ }
430
+
431
+ /**
432
+ * The scheduling runner: decides whether a message ships now or is held, and
433
+ * drives the flush loop. `flush` is the real send (the service's provider
434
+ * send, post-resolution) — the runner calls it for due messages.
435
+ */
436
+ export declare interface Scheduler {
437
+ /** True if a message must be held (has a future `scheduledAt` AND the
438
+ * provider can't schedule natively). */
439
+ readonly shouldHold: (scheduledAt: Date | undefined) => boolean;
440
+ /** Park a message for later flush. */
441
+ readonly hold: (message: MailMessage) => Effect.Effect<void>;
442
+ /** One flush pass: claim due messages and send each. Returns the count
443
+ * flushed. Safe to call on an interval. */
444
+ readonly tick: (now?: number) => Effect.Effect<number>;
445
+ /** How many messages are currently held. */
446
+ readonly pending: () => Effect.Effect<number>;
447
+ }
448
+
449
+ export declare interface SchedulerOptions {
450
+ readonly store: ScheduleStore;
451
+ /** True when the resolved provider schedules natively (Resend/SendGrid/
452
+ * Mailgun) — then `shouldHold` is always false. */
453
+ readonly providerSchedules: boolean;
454
+ /** The real send for a due message (strips `scheduledAt` first so the flush
455
+ * doesn't loop). Returns whatever the provider returns; errors are logged
456
+ * and swallowed so one bad message can't stall the loop. */
457
+ readonly flush: (message: MailMessage) => Effect.Effect<unknown, unknown>;
458
+ }
459
+
460
+ /**
461
+ * Backing store for held (scheduled) messages. The default is in-memory
462
+ * (single-node); a durable store makes scheduled sends survive a restart and
463
+ * flush from exactly one replica. `claimDue` MUST be atomic per entry so two
464
+ * replicas never both flush the same message.
465
+ */
466
+ export declare interface ScheduleStore {
467
+ /** Park a message until `dueAt`. Idempotent on `id`. */
468
+ readonly hold: (entry: HeldMessage) => Effect.Effect<void>;
469
+ /** Atomically claim + remove every entry due at/before `now`, returning
470
+ * them for flush. A claimed entry is gone from the store (a crash after
471
+ * claim but before send loses that one message — the durable store trades
472
+ * that for cross-replica exactly-once; the workflow-step path is the
473
+ * crash-proof option). */
474
+ readonly claimDue: (now: number) => Effect.Effect<ReadonlyArray<HeldMessage>>;
475
+ /** Count of currently-held messages (for the inspect surface / tests). */
476
+ readonly size: () => Effect.Effect<number>;
477
+ }
478
+
479
+ export declare const sendgridProvider: (options: {
480
+ readonly apiKey: string;
481
+ }) => MailProvider;
482
+
483
+ export declare interface SendResult {
484
+ /** Provider message id (or a synthetic id for console/memory/skipped). */
485
+ readonly id: string;
486
+ /** Which provider handled it (`'resend'`, `'memory'`, `'skipped'`, …). */
487
+ readonly provider: string;
488
+ }
489
+
490
+ export declare interface SesOptions {
491
+ readonly region: string;
492
+ readonly accessKeyId: string;
493
+ readonly secretAccessKey: string;
494
+ readonly sessionToken?: string;
495
+ }
496
+
497
+ export declare const sesProvider: (options: SesOptions) => MailProvider;
498
+
499
+ export declare interface SmtpOptions {
500
+ /** `smtp(s)://user:pass@host:port` — takes precedence over discrete fields. */
501
+ readonly url?: string;
502
+ readonly host?: string;
503
+ readonly port?: number;
504
+ readonly secure?: boolean;
505
+ readonly auth?: {
506
+ readonly user: string;
507
+ readonly pass: string;
508
+ };
509
+ }
510
+
511
+ export declare const smtpProvider: (options: SmtpOptions) => MailProvider;
512
+
513
+ export declare interface SuppressionStore {
514
+ readonly isSuppressed: (tenantId: string | null, email: string) => Effect.Effect<boolean>;
515
+ readonly suppress: (tenantId: string | null, email: string, reason: string) => Effect.Effect<void>;
516
+ readonly unsuppress: (tenantId: string | null, email: string) => Effect.Effect<void>;
517
+ readonly activate?: (env: NodeJS.ProcessEnv) => Promise<void>;
518
+ readonly deactivate?: () => Promise<void>;
519
+ }
520
+
521
+ /** Send a registered template instead of raw html/text. */
522
+ export declare interface TemplateMessage {
523
+ readonly to: string | ReadonlyArray<string>;
524
+ readonly template: string;
525
+ readonly props?: unknown;
526
+ /** Picks `<name>.<locale>` if registered, else falls back to `<name>`. */
527
+ readonly locale?: string;
528
+ readonly from?: string;
529
+ readonly replyTo?: string;
530
+ readonly cc?: string | ReadonlyArray<string>;
531
+ readonly bcc?: string | ReadonlyArray<string>;
532
+ readonly headers?: Readonly<Record<string, string>>;
533
+ readonly tags?: ReadonlyArray<string>;
534
+ readonly attachments?: ReadonlyArray<MailAttachment>;
535
+ readonly tenantId?: string | null;
536
+ /** See `MailMessage.scheduledAt`. */
537
+ readonly scheduledAt?: Date;
538
+ /** See `MailMessage.idempotencyKey`. */
539
+ readonly idempotencyKey?: string;
540
+ }
541
+
542
+ export { VoltroPlugin }
543
+
544
+ export { }
package/dist/index.js ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@voltro/plugin-mail",
3
+ "version": "0.1.0",
4
+ "description": "Transactional email plugin — one MailProvider contract, multiple adapters (Resend / Postmark / SendGrid / SMTP + console/memory for dev/test). Exposes a MailService via the plugin services layer with transient retry and a dev allowlist.",
5
+ "keywords": [
6
+ "voltro",
7
+ "typescript",
8
+ "framework"
9
+ ],
10
+ "license": "SEE LICENSE IN LICENSE",
11
+ "homepage": "https://voltro.dev",
12
+ "bugs": {
13
+ "email": "support@voltro.dev"
14
+ },
15
+ "author": {
16
+ "name": "Voltro UG",
17
+ "url": "https://voltro.dev"
18
+ },
19
+ "type": "module",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js",
24
+ "default": "./dist/index.js"
25
+ }
26
+ },
27
+ "main": "./dist/index.js",
28
+ "module": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "sideEffects": false,
31
+ "engines": {
32
+ "node": ">=24.0.0"
33
+ },
34
+ "dependencies": {
35
+ "@effect/platform": "^0.96.2",
36
+ "@effect/sql": "^0.51.1",
37
+ "@effect/sql-pg": "^0.52.1",
38
+ "@voltro/env": "0.1.0",
39
+ "@voltro/logger": "0.1.0",
40
+ "@voltro/protocol": "0.1.0"
41
+ },
42
+ "optionalDependencies": {
43
+ "nodemailer": "'>=9.0.1'"
44
+ },
45
+ "peerDependencies": {
46
+ "@react-email/render": "^2.0.8",
47
+ "effect": "^3.21.4",
48
+ "react": "^19.0.0",
49
+ "react-dom": "^19.0.0"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "react": {
53
+ "optional": true
54
+ },
55
+ "react-dom": {
56
+ "optional": true
57
+ },
58
+ "@react-email/render": {
59
+ "optional": true
60
+ }
61
+ },
62
+ "publishConfig": {
63
+ "access": "public"
64
+ }
65
+ }