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 { }