import { Context } from 'effect';
import { DataStore } from '@voltro/database';
import { Effect } from 'effect';
import { Layer } from 'effect';
import { PluginEgressDeclaration } from '@voltro/protocol';
import { PluginHttpRouteRequest } from '@voltro/protocol';
import { Schema } from 'effect';
import { VoltroPlugin } from '@voltro/protocol';
import { YieldableError } from 'effect/Cause';
/** Any template regardless of its props type — for registries/collections.
* `P` is invariant across `EmailTemplate` (it appears in the `props` Schema
* and in `preview`), so no single `EmailTemplate
` is a common supertype.
* This widens every `P`-dependent member to its most permissive accepting
* form: function params to `never` (accepts any concrete-prop function via
* contravariance), `props` to `Schema.Schema.Any`, `preview` to `unknown`. */
export declare interface AnyEmailTemplate {
readonly name: string;
readonly subject: (props: never) => string;
readonly props: Schema.Top;
readonly render: (props: never) => unknown;
readonly locale?: string | undefined;
readonly preview?: unknown;
}
/**
* One personalized message in a batch. `to` is a single recipient; the rest
* of the fields (subject, html/text OR template+props) are per-recipient, so
* every entry can carry its own template variables. The service applies the
* default `from`, allowlist, and suppression to each entry, then hands the
* whole set to the provider's native batch endpoint (Resend/SendGrid) or
* loops `send` for transports without one.
*/
export declare type BatchMessage = MailMessage | TemplateMessage;
export declare const buildMailService: (options: MailServiceOptions) => MailServiceShape;
/** Build the service AND expose its scheduler (for the flush loop). Most
* consumers want `buildMailService` (the shape only). */
export declare const buildMailServiceWithScheduler: (options: MailServiceOptions) => BuiltMailService;
/** The built service plus the scheduler driving its hold-and-flush loop. The
* plugin forks `scheduler.tick` on an interval in `onActivate`. */
export declare interface BuiltMailService {
readonly service: MailServiceShape;
readonly scheduler: Scheduler;
}
/** Drop everything — for tests. */
export declare const clearGlobalTemplates: () => void;
/** Drop everything in the in-process buffer. */
export declare const clearMailBuffer: () => void;
export declare const consoleProvider: (log?: (message: string, fields?: Record) => void) => MailProvider;
export declare const createOutbox: (max?: number) => Outbox;
/** Define a template. Infers the props type from the `props` Schema so
* `subject` / `render` receive the decoded shape. */
export declare const defineEmail: (template: {
readonly name: string;
readonly props: S;
readonly subject: (props: Schema.Schema.Type) => string;
readonly render: (props: Schema.Schema.Type) => unknown;
readonly locale?: string;
/** Realistic example props for the dashboard preview. */
readonly preview?: Schema.Schema.Type;
}) => EmailTemplate>;
/**
* A React-Email template. `props` is an effect/Schema validated before
* render; `subject` derives the line from the decoded props; `render`
* returns the React element (typed `unknown` here so the plugin doesn't
* hard-depend on react's types). `locale` tags a per-language variant
* (`welcome.de.email.tsx` → `locale: 'de'`), looked up with fallback to
* the base template.
*/
export declare interface EmailTemplate {
readonly name: string;
readonly subject: (props: P) => string;
readonly props: Schema.Codec
| Schema.Top;
readonly render: (props: P) => unknown;
readonly locale?: string;
/** Realistic example props — pre-fills the dashboard preview box (and
* documents what the template expects). Falls back to a schema-derived
* skeleton when absent. */
readonly preview?: P;
}
export declare const globalTemplateCount: () => number;
/** Apply events to the suppression store: bounce + complaint → suppress the
* address (per tenant). Returns the parsed events for logging/auditing. */
export declare const handleMailEvents: (provider: string, payload: unknown, suppression: SuppressionStore, tenantId?: string | null) => Effect.Effect>;
/** Stable-ish held-entry id (avoids Math.random, mirrors providers' synthId).
* When the message carries an `idempotencyKey`, that keys the hold so a
* replayed schedule doesn't double-park. */
export declare const heldId: (message: MailMessage) => string;
/** A message parked until its `dueAt`. Carries the fully-formed raw message
* the service already resolved (default-from applied, template rendered). */
export declare interface HeldMessage {
/** Opaque id for the held entry (dedupes re-enqueue). */
readonly id: string;
/** epoch-ms the message becomes due. */
readonly dueAt: number;
/** The raw message to hand back to `send` when due (never a TemplateMessage
* — the service renders templates BEFORE holding, so the flush is a plain
* provider send). */
readonly message: MailMessage;
}
export declare interface IdempotencyStore {
/** Atomically claim `key` for `tenantId`. First caller wins; replays lose. */
readonly reserve: (tenantId: string | null, key: string) => Effect.Effect;
/** Stamp the winning send's result so a later replay returns it verbatim. */
readonly record: (tenantId: string | null, key: string, result: SendResult) => Effect.Effect;
readonly activate?: (env: NodeJS.ProcessEnv) => Promise;
readonly deactivate?: () => Promise;
/** Receives the app's `DataStore` at `bindDataStore`, for a store kept in the
* app's own database. */
readonly bind?: (store: DataStore) => Promise;
}
/** Structural guard: recognise a `defineEmail(...)` result among a module's
* exports (name + subject fn + render fn + a props schema). */
export declare const isEmailTemplate: (v: unknown) => v is AnyEmailTemplate;
export declare interface LettermintOptions {
/** Project API token (`lm_…`), sent as `x-lettermint-token`. */
readonly projectToken: string;
/** Default `https://api.lettermint.co/v1`. */
readonly baseUrl?: string;
/** Route slug per stream. Default: the project's default route (its
* transactional one) for `transactional`, `broadcast` for `broadcast`. */
readonly routes?: {
readonly transactional?: string;
readonly broadcast?: string;
};
}
export declare const lettermintProvider: (options: LettermintOptions) => MailProvider;
/** Names (+ locale variants) of every globally-registered template. */
export declare const listGlobalTemplates: () => ReadonlyArray<{
readonly name: string;
readonly locale?: string;
}>;
/** Look up a globally-registered template, locale-variant first. */
export declare const lookupGlobalTemplate: (name: string, locale: string | undefined) => AnyEmailTemplate | undefined;
/** A file attached to a message. `content` is the raw bytes as a base64
* string (transport-agnostic — every adapter maps it to its own shape). */
export declare interface MailAttachment {
/** Displayed filename, e.g. `invoice.pdf`. */
readonly filename: string;
/** File bytes as a base64-encoded string. */
readonly content: string;
/** MIME type, e.g. `application/pdf`. Defaults per-provider when omitted. */
readonly contentType?: string;
}
/** What mail egress a provider choice declares. */
export declare const mailEgressOf: (options: {
readonly provider?: MailProviderName | MailProvider;
readonly env?: Readonly>;
}) => ReadonlyArray;
/**
* Mail failure. `transient` drives retry — HTTP 408 / 429 / 5xx and network
* failures are retryable; other HTTP 4xx are not. SMTP instead retries 4xx
* temporary replies, never 5xx permanent replies (RFC 5321 §4.2.1). Surfaced
* typed so handlers can `Effect.catchTag('MailError', …)`.
*/
export declare class MailError extends MailError_base {
}
declare const MailError_base: Schema.Class;
}>, YieldableError>;
export declare interface MailEvent {
readonly type: MailEventType;
readonly email: string;
readonly provider: string;
readonly messageId?: string;
/** The provider's id for this event — the key to act on a redelivered
* webhook only once. */
readonly eventId?: string;
/** What the message was sent with as `metadata`, returned by the provider. */
readonly metadata?: Readonly>;
readonly raw: unknown;
}
/** `unsubscribed`: the recipient left a broadcast through the provider's own
* unsubscribe (a provider that `managesUnsubscribe`). It is NOT suppressed
* app-wide — it ended one list, not the person's transactional mail.
* `unsuppressed`: the provider lifted a suppression (someone removed it in
* its dashboard) — lifted here too, or the app would keep dropping mail the
* provider would now deliver. */
export declare type MailEventType = 'delivered' | 'bounced' | 'complained' | 'unsubscribed' | 'unsuppressed' | 'opened' | 'clicked' | 'unknown';
export declare interface MailgunOptions {
readonly apiKey: string;
readonly domain: string;
/** EU region → `https://api.eu.mailgun.net`. Default: US. */
readonly baseUrl?: string;
}
export declare const mailgunProvider: (options: MailgunOptions) => MailProvider;
/** A message to send. `from` is optional here — the service fills it
* from the plugin's configured default before handing it to a provider. */
export declare interface MailMessage {
readonly to: string | ReadonlyArray;
readonly from?: string;
readonly replyTo?: string;
readonly cc?: string | ReadonlyArray;
readonly bcc?: string | ReadonlyArray;
readonly subject: string;
readonly html?: string;
readonly text?: string;
readonly headers?: Readonly>;
/** Provider-side tags/labels where supported (Resend, SendGrid). */
readonly tags?: ReadonlyArray;
/** File attachments. Mapped to each transport's native attachment shape
* (Resend/Postmark/SendGrid/SMTP). `content` is base64. */
readonly attachments?: ReadonlyArray;
/** Scopes the suppression check (bounced/complained addresses) to a
* tenant. Omit for app-global suppression. */
readonly tenantId?: string | null;
/** Deliver at (or after) this instant instead of now. Providers that
* support scheduling natively (Resend, SendGrid, Mailgun) receive it on
* the wire; for the rest the service HOLDS the message and flushes it
* once the time arrives (`scheduleFallback`). A time in the past sends
* immediately. */
readonly scheduledAt?: Date;
/** De-duplicates a send across webhook/retry replays OUTSIDE a workflow
* step. The first send with a given key delivers; a replay with the same
* key is a no-op returning the recorded result. Scoped per tenant. */
readonly idempotencyKey?: string;
/** `transactional` (default) or `broadcast`. See `MailStream`. */
readonly stream?: MailStream;
/** How a recipient leaves a broadcast. See `MailUnsubscribe`. */
readonly unsubscribe?: MailUnsubscribe;
/** Key/value pairs the provider stores with the message and returns on its
* delivery events (`MailEvent.metadata`) — how a bounce finds its way back
* to the row that caused it. Never added to the mail itself. Lettermint and
* Postmark carry it; other providers ignore it. */
readonly metadata?: Readonly>;
}
/**
* The mail plugin, with the service it built — the same one handlers yield as
* `MailService`. For wiring that happens in `app.config.ts`, before any
* handler runs: `authRoutesPlugin({ sendEmail: mailSender(mail.service) })`.
*/
export declare type MailPlugin = VoltroPlugin & {
readonly service: MailServiceShape;
};
export declare const mailPlugin: (options?: MailPluginOptions) => MailPlugin;
export declare interface MailPluginOptions {
/** `'lettermint' | 'resend' | 'postmark' | 'sendgrid' | 'smtp' | 'console' | 'memory'`
* (built from env) or a `MailProvider`. Default: `MAIL_PROVIDER` env, else
* `'console'`. */
readonly provider?: MailProviderName | MailProvider;
/** API key/token for string providers (else read from per-provider env). */
readonly apiKey?: string;
/** Default `From` when a message omits it. */
readonly from?: string;
/** Default `Reply-To`. */
readonly replyTo?: string;
/** Dev safety: outside production, only send to these addresses. Defaults
* to the comma-separated `MAIL_ALLOWLIST` env. */
readonly allowlist?: ReadonlyArray;
/** Transient-failure retries (429 / 5xx / network). Default 3. */
readonly attempts?: number;
/** Registered `*.email.tsx` templates (from `defineEmail`), enabling
* `mail.send({ template, props })`. */
readonly templates?: ReadonlyArray;
/** Suppression backend for bounced/complained addresses: `'memory'`
* (default, single-node), `'postgres'` (the app's own postgres connection),
* or a custom `SuppressionStore`. */
readonly suppression?: 'memory' | 'postgres' | SuppressionStore;
/** Per-send idempotency backend (webhook-retry double-send guard):
* `'memory'` (default, single-node), `'postgres'` (the app's own
* postgres connection), or a custom `IdempotencyStore`. Engaged only for a
* send carrying `idempotencyKey`. */
readonly idempotency?: 'memory' | 'postgres' | IdempotencyStore;
/** Backing store for HELD scheduled messages (providers without native
* send-later — console/memory/SMTP/SES). Default: in-memory. A custom
* `ScheduleStore` makes scheduled sends durable + cross-replica. */
readonly schedule?: ScheduleStore;
/** How often (ms) the scheduled-send flush loop wakes to deliver due held
* messages. Default 1000. Ignored when the provider schedules natively. */
readonly scheduleTickMs?: number;
/** Mount the provider's delivery-event webhook. Needs a provider with a
* `webhook` verifier (Lettermint). See `MailWebhookOptions`. */
readonly webhook?: MailWebhookOptions;
/**
* Namespace for this plugin's inspect endpoints. Default `mail`.
*
* Set it when your app already publishes under that name — an exact tag
* collision is fatal at codegen, and this is the way out. Orthogonal to
* `name` below: `alias` REPLACES the namespace, `name` distinguishes two
* installations within it.
*
* The dashboard panel follows: the plugin's inspect endpoints keep a mount
* under its CANONICAL name alongside the aliased one, so aliasing does not
* take the panel away. The one case it cannot cover is two installs of this
* plugin — one canonical name, two panels — which get no shared mount at
* all, on purpose. Read `inspectSlug` from `/_voltro/inspect/plugins` to
* reach a specific install.
*/
readonly alias?: string;
/**
* Discriminator for a SECOND installation of this plugin, when one app runs
* two (`@voltro/plugin-mail#analytics`). Not a rename — for that use `alias`.
*/
readonly name?: string;
}
/** A transport. Receives a fully-resolved message; returns the send result
* or fails with a (possibly transient) MailError.
*
* Two OPTIONAL capabilities let a provider skip the service's generic
* fallbacks:
* - `supportsScheduling`: the provider maps `message.scheduledAt` onto its
* own send-later field (Resend/SendGrid/Mailgun). When absent/false the
* service holds the message and flushes it at the due time.
* - `sendBatch`: one provider call for N personalized messages
* (Resend/SendGrid). When absent the service loops `send`. */
export declare interface MailProvider {
readonly name: string;
readonly send: (message: ResolvedMessage) => Effect.Effect;
/** True if `send` honours `message.scheduledAt` natively (no service-side
* hold-and-flush needed). */
readonly supportsScheduling?: boolean;
/** One request for N personalized messages. Each keeps its own subject /
* body / template vars. Returns one `SendResult` per input, in order. */
readonly sendBatch?: (messages: ReadonlyArray) => Effect.Effect, MailError>;
/** True if the provider adds its own unsubscribe link and one-click headers
* to broadcast mail and suppresses whoever uses them (Lettermint's
* broadcast route). The service then sends no `List-Unsubscribe` of its own
* — two competing unsubscribe mechanisms would each only half work. */
readonly managesUnsubscribe?: boolean;
/** Verifies and parses the provider's delivery-event webhook. Present →
* `mailPlugin({ webhook })` can mount the endpoint. */
readonly webhook?: MailProviderWebhook;
}
export declare type MailProviderName = 'lettermint' | 'resend' | 'postmark' | 'sendgrid' | 'ses' | 'mailgun' | 'smtp' | 'console' | 'memory';
/** A provider's delivery-event webhook: signature check, then normalisation. */
export declare interface MailProviderWebhook {
/** Verify the signature over the RAW body and decode it. Fails with
* `MailWebhookError` for a missing, stale or forged signature. */
readonly verify: (headers: Readonly>, rawBody: string, secret: string, nowSeconds?: number) => Effect.Effect;
}
export declare interface MailSendRecord {
readonly id: string;
readonly to: ReadonlyArray;
readonly from: string;
readonly subject: string;
/** Provider that handled it (`resend`/`memory`/`skipped`/`suppressed`/…). */
readonly provider: string;
readonly ok: boolean;
readonly error?: string;
/** epoch-ms when the send completed. */
readonly ts: number;
/** Template name when sent via `send({ template })`. */
readonly template?: string;
}
export declare class MailService extends MailService_base {
}
declare const MailService_base: Context.ServiceClass;
export declare const mailServiceLayer: (options: MailServiceOptions) => Layer.Layer;
export declare interface MailServiceOptions {
readonly provider: MailProvider;
readonly from?: string;
readonly replyTo?: string;
/** Dev safety: when set + not production, only these recipients are sent to. */
readonly allowlist?: ReadonlyArray;
/** Transient-failure retries. Default 3. */
readonly attempts?: number;
readonly production?: boolean;
/** Registered `*.email.tsx` templates, for `send({ template, props })`. */
readonly templates?: ReadonlyArray;
/** Suppression list (bounced/complained addresses). Default: in-memory. */
readonly suppression?: SuppressionStore;
/** Per-send idempotency store (webhook-retry double-send guard). Default:
* in-memory. Only engaged for a send carrying an `idempotencyKey`. */
readonly idempotency?: IdempotencyStore;
/** Backing store for HELD scheduled messages (providers without native
* send-later). Default: in-memory. */
readonly schedule?: ScheduleStore;
/** Called after every send (success, failure, or skipped) — feeds the
* dev outbox the dashboard reads. */
readonly onSent?: (record: MailSendRecord) => void;
}
/** The service handlers consume: `const mail = yield* MailService`. */
export declare interface MailServiceShape {
readonly send: (message: MailMessage | TemplateMessage) => Effect.Effect;
/** Send N personalized messages (each with its own subject / body /
* template vars) in one provider batch call where the transport supports
* it, else looped `send`. Returns one `SendResult` per input, in order.
* Allowlist, suppression, default-`from`, scheduling and idempotency
* apply per entry, exactly as for a single `send`. */
readonly sendBatch: (messages: ReadonlyArray) => Effect.Effect, MailError>;
/** Mark an address as undeliverable/opted-out (per tenant). Future sends
* to it are dropped. Called automatically on bounce/complaint events. */
readonly suppress: (tenantId: string | null, email: string, reason: string) => Effect.Effect;
/** Remove an address from the suppression list. */
readonly unsuppress: (tenantId: string | null, email: string) => Effect.Effect;
/** Is this address currently suppressed for the tenant? */
readonly isSuppressed: (tenantId: string | null, email: string) => Effect.Effect;
/** The provider behind the service. `managesUnsubscribe` tells a broadcast
* sender whether the provider already puts an unsubscribe link in the mail
* (then the body needs none of its own). Absent on a hand-built service. */
readonly provider?: {
readonly name: string;
readonly managesUnsubscribe: boolean;
};
}
/**
* Which kind of mail this is. `transactional` (the default) is one-to-one mail
* a person's own action triggered — an invitation, a receipt, a password
* reset. `broadcast` is one-to-many mail — a newsletter, a campaign. Providers
* that separate the two (Lettermint routes, Postmark message streams) send
* them from different infrastructure, so a newsletter's complaints never cost
* a password reset its inbox placement.
*/
export declare type MailStream = 'transactional' | 'broadcast';
/**
* How a recipient leaves a broadcast. `oneClickUrl` is the HTTPS endpoint a
* mail client POSTs `List-Unsubscribe=One-Click` to (RFC 8058); the service
* turns it into the `List-Unsubscribe` + `List-Unsubscribe-Post` headers.
* Required on a broadcast unless the provider unsubscribes recipients itself
* (`MailProvider.managesUnsubscribe`).
*/
export declare interface MailUnsubscribe {
readonly oneClickUrl: string;
/** Optional `mailto:` alternative listed beside the URL. */
readonly mailto?: string;
}
/** A webhook that failed verification — answer 401, never process it. */
export declare class MailWebhookError extends MailWebhookError_base {
}
declare const MailWebhookError_base: Schema.Class, YieldableError>;
/**
* The provider's delivery-event webhook, mounted by the plugin. Bounces and
* spam complaints suppress the address in the plugin's OWN suppression store
* — the one every send checks — then `onEvent` sees every event.
*/
export declare interface MailWebhookOptions {
/** Signing secret. Default: `MAIL_WEBHOOK_SECRET`. Without one the endpoint
* answers 503, and a production boot refuses. */
readonly secret?: string;
/** Default `/webhooks/mail`. */
readonly path?: string;
/**
* Every verified event, after suppression — an unsubscribe to record, a
* bounce to mark on the row the message's `metadata` names. A failure
* answers 500 and the provider redelivers, so act idempotently
* (`event.eventId`). `store` is the app's boot store, as on any plugin route.
*/
readonly onEvent?: (event: MailEvent, ctx: {
readonly store: PluginHttpRouteRequest['store'];
}) => Effect.Effect | Promise | void;
}
export declare const makeScheduler: (options: SchedulerOptions) => Scheduler;
export declare const memoryIdempotencyStore: () => IdempotencyStore;
export declare const memoryProvider: () => MailProvider;
export declare const memoryScheduleStore: () => ScheduleStore;
export declare const memorySuppressionStore: () => SuppressionStore;
export declare interface Outbox {
readonly record: (r: MailSendRecord) => void;
/** Newest first. */
readonly list: () => ReadonlyArray;
readonly clear: () => void;
}
/** Normalise a provider webhook payload to MailEvents. Unknown shapes → []. */
export declare const parseMailEvent: (provider: string, payload: unknown) => ReadonlyArray;
/**
* Dedup in the app's own database: `_voltro_mail_idempotency`, declared through
* the plugin's `extendSchema` (`storeTables.ts`) and written through the app's
* `DataStore`, bound at `bindDataStore`. `reserve` is an `insertIgnore` on the
* (tenant, key) primary key, and its outcome comes from the store's own insert
* decision — exactly one replica sees `inserted`, so exactly one send fires
* fleet-wide. Unlike the suppression store this does NOT fail open: if the
* database is unreachable a reserve reports `won: false` (skip) so a blip can
* never turn into a double-send — the safer failure for an at-least-once
* webhook.
*/
export declare const postgresIdempotencyStore: () => IdempotencyStore;
/**
* The suppression list in the app's own database: `_voltro_mail_suppression`,
* declared through the plugin's `extendSchema` (`storeTables.ts`) and written
* through the app's `DataStore`, bound at `bindDataStore`. Multi-node correct on
* every dialect the app runs on; nothing opens a connection of its own.
*/
export declare const postgresSuppressionStore: () => SuppressionStore;
export declare const postmarkProvider: (options: {
readonly serverToken: string;
readonly streams?: {
readonly transactional?: string;
readonly broadcast?: string;
};
}) => MailProvider;
/** Stable snapshot of everything the memory provider has "sent". */
export declare const readMailBuffer: () => ReadonlyArray;
/** Register CLI-discovered (or hand-collected) templates. Idempotent per key. */
export declare const registerEmailTemplates: (templates: ReadonlyArray) => void;
export declare interface RenderedEmail {
readonly subject: string;
readonly html: string;
readonly text: string;
}
export declare const renderTemplate: (tpl: AnyEmailTemplate, rawProps: unknown) => Effect.Effect;
export declare const resendProvider: (options: {
readonly apiKey: string;
}) => MailProvider;
/** Outcome of reserving an idempotency key. */
export declare interface Reservation {
/** True → this caller won the key and MUST send. False → a prior send owns
* it; the caller skips and returns `prior` if present. */
readonly won: boolean;
/** The recorded result of the winning send, when a replay finds it. */
readonly prior?: SendResult;
}
/** What a provider receives — `from` is guaranteed resolved by the service.
* `scheduledAt` survives only for providers that advertise
* `supportsScheduling` (the service strips + holds it otherwise). */
export declare interface ResolvedMessage extends MailMessage {
readonly from: string;
}
/** Resolve the configured provider. A `MailProvider` object passes through;
* a string (or `MAIL_PROVIDER` env) builds one from per-provider env.
* Throws a clear config error on a missing key — surfaces at boot. */
export declare const resolveProvider: (options: ResolveProviderOptions) => MailProvider;
export declare interface ResolveProviderOptions {
readonly provider?: MailProviderName | MailProvider;
readonly apiKey?: string;
readonly env?: NodeJS.ProcessEnv;
}
/**
* The scheduling runner: decides whether a message ships now or is held, and
* drives the flush loop. `flush` is the real send (the service's provider
* send, post-resolution) — the runner calls it for due messages.
*/
export declare interface Scheduler {
/** True if a message must be held (has a future `scheduledAt` AND the
* provider can't schedule natively). */
readonly shouldHold: (scheduledAt: Date | undefined) => boolean;
/** Park a message for later flush. */
readonly hold: (message: MailMessage) => Effect.Effect;
/** One flush pass: claim due messages and send each. Returns the count
* flushed. Safe to call on an interval. */
readonly tick: (now?: number) => Effect.Effect;
/** How many messages are currently held. */
readonly pending: () => Effect.Effect;
}
export declare interface SchedulerOptions {
readonly store: ScheduleStore;
/** True when the resolved provider schedules natively (Resend/SendGrid/
* Mailgun) — then `shouldHold` is always false. */
readonly providerSchedules: boolean;
/** The real send for a due message (strips `scheduledAt` first so the flush
* doesn't loop). Returns whatever the provider returns; errors are logged
* and swallowed so one bad message can't stall the loop. */
readonly flush: (message: MailMessage) => Effect.Effect;
}
/**
* Backing store for held (scheduled) messages. The default is in-memory
* (single-node); a durable store makes scheduled sends survive a restart and
* flush from exactly one replica. `claimDue` MUST be atomic per entry so two
* replicas never both flush the same message.
*/
export declare interface ScheduleStore {
/** Park a message until `dueAt`. Idempotent on `id`. */
readonly hold: (entry: HeldMessage) => Effect.Effect;
/** Atomically claim + remove every entry due at/before `now`, returning
* them for flush. A claimed entry is gone from the store (a crash after
* claim but before send loses that one message — the durable store trades
* that for cross-replica exactly-once; the workflow-step path is the
* crash-proof option). */
readonly claimDue: (now: number) => Effect.Effect>;
/** Count of currently-held messages (for the inspect surface / tests). */
readonly size: () => Effect.Effect;
}
export declare const sendgridProvider: (options: {
readonly apiKey: string;
}) => MailProvider;
export declare interface SendResult {
/** Provider message id (or a synthetic id for console/memory/skipped). */
readonly id: string;
/** Which provider handled it (`'resend'`, `'memory'`, `'skipped'`, …). */
readonly provider: string;
}
export declare interface SesOptions {
readonly region: string;
readonly accessKeyId: string;
readonly secretAccessKey: string;
readonly sessionToken?: string;
}
export declare const sesProvider: (options: SesOptions) => MailProvider;
export declare interface SmtpOptions {
/** `smtp(s)://user:pass@host:port` — takes precedence over discrete fields. */
readonly url?: string;
readonly host?: string;
readonly port?: number;
readonly secure?: boolean;
readonly auth?: {
readonly user: string;
readonly pass: string;
};
}
export declare const smtpProvider: (options: SmtpOptions) => MailProvider;
export declare interface SuppressionStore {
readonly isSuppressed: (tenantId: string | null, email: string) => Effect.Effect;
readonly suppress: (tenantId: string | null, email: string, reason: string) => Effect.Effect;
readonly unsuppress: (tenantId: string | null, email: string) => Effect.Effect;
readonly activate?: (env: NodeJS.ProcessEnv) => Promise;
readonly deactivate?: () => Promise;
/** Receives the app's `DataStore` at `bindDataStore`, for a store kept in the
* app's own database. */
readonly bind?: (store: DataStore) => Promise;
}
/** Send a registered template instead of raw html/text. */
export declare interface TemplateMessage {
readonly to: string | ReadonlyArray;
readonly template: string;
readonly props?: unknown;
/** Picks `.` if registered, else falls back to ``. */
readonly locale?: string;
readonly from?: string;
readonly replyTo?: string;
readonly cc?: string | ReadonlyArray;
readonly bcc?: string | ReadonlyArray;
readonly headers?: Readonly>;
readonly tags?: ReadonlyArray;
readonly attachments?: ReadonlyArray;
readonly tenantId?: string | null;
/** See `MailMessage.scheduledAt`. */
readonly scheduledAt?: Date;
/** See `MailMessage.idempotencyKey`. */
readonly idempotencyKey?: string;
/** See `MailMessage.stream`. */
readonly stream?: MailStream;
/** See `MailMessage.unsubscribe`. */
readonly unsubscribe?: MailUnsubscribe;
/** See `MailMessage.metadata`. */
readonly metadata?: Readonly>;
}
/** Lettermint signs `${t}.${rawBody}` with HMAC-SHA256 under the webhook
* secret: `x-lettermint-signature: t=,v1=` (several `v1` during a
* secret rotation), `x-lettermint-delivery: ` must equal `t`. */
export declare const verifyLettermintSignature: (headers: Readonly>, rawBody: string, secret: string, nowSeconds?: number, toleranceSeconds?: number) => Effect.Effect;
export { VoltroPlugin }
export { }