Email sending, templates, and notifications.
Supports multiple email providers via HTTP APIs: Resend, Postmark, SendGrid, and Mailgun.
Emails are rendered using Liquid templates with support for custom confirmation and admin notification templates.
Build SVG ticket data from an email entry (non-PII only)
Build the data object exposed to Liquid templates. Rows booked through a
package head the email by the package's name (listing_names); an order may
carry several bundles beside plain rows. hidePackageMembers (set for the
buyer's confirmation, not the admin notification) collapses each HIDDEN
package's member rows into one package row so members aren't revealed —
whatever else the order carries beside them.
Generate SVG ticket attachments for the buyer's row groups: each HIDDEN package collapses to a single package-level SVG so the attachments don't reveal the member listings the email body hides — whatever else the order carries beside the bundle. Callers without package rows pass one group per entry (buyerEntryGroups builds the real thing).
The single-row summary a hidden package collapses to for buyers: the bundle's summed price and quantity plus the widest member's dated stay (hiding members must not lose the date the buyer booked). Shared by the email body row and the SVG ticket, so the two can never disagree.
Host (everything after the last @) of a validated address. The ValidEmail
type guarantees a host is present, so there is no empty-host case to handle
and the compiler forbids passing a raw, unvalidated string.
Local part (everything before the last @) of a validated address.
The email config sending should use: the site's own settings when complete, otherwise the host-level config. Null when neither is configured — callers treat that as "email is off".
Read email config from DB settings. Falls back to business email for fromAddress. Returns null if not configured or the from address is invalid.
Get host-level email config. Uses test override if set, otherwise reads env vars.
Whether a string is a valid email (trimmed) per EmailSchema.
Parse and normalize a candidate email, returning the branded ValidEmail when it is valid or null otherwise. Use this in preference to isValidEmail when the validated address needs to be carried onward in a type-safe way.
Render all 3 parts (subject, html, text) using custom templates with fallback to defaults
Render a single Liquid template string with the given data
For testing: reset the engine (so filters can be re-registered after currency changes)
For testing: reset host email config to read from env vars.
Send a bulk email via the configured provider. Every supported provider has a
batch endpoint, so this works for any EmailConfig. Chunks recipients to the
provider's batch limit and POSTs each chunk; logs (never throws) on a non-OK
batch, whose recipients then count as failed. Each batch's provider response
is captured so the caller can relay it to the sender.
Send a single email via the configured provider. Logs errors, never throws. Returns HTTP status or undefined on non-HTTP errors.
Send an error notification to the configured ntfy URL Returns a promise so callers can await delivery if needed. Delivery failures are logged locally (via logErrorLocal) but never throw.
Send registration confirmation + admin notification emails. Entries is an array because one registration can cover multiple listings. Silently skips if email is not configured. Attaches one SVG ticket per entry to the confirmation email.
Send a test email to the business email address. Returns HTTP status or undefined on non-HTTP errors.
For testing: set host email config directly. Bypasses env vars to avoid races.
Updates the business email in the database. Pass empty string to clear the business email. Email is encrypted at rest.
Validate a Liquid template by parsing it (no rendering). Returns null if valid, or an error message string if invalid.
What the provider returned for one batch: HTTP status, ok flag, and the raw response body. Providers reply with queued message IDs (or rejection reasons), so the body is kept to surface back to the sender and the log.
A bulk send: shared template (html/text may contain the placeholder) + recipients.
One bulk recipient: address plus its unsubscribe URL (marketing sends only).
Outcome of a bulk send: recipients attempted, batches sent, recipients in failed batches, and the raw per-batch provider responses.
One buyer-facing row group: a HIDDEN package's rows gather behind its name; every other row stands alone.
Listing data needed for registration pipeline (extends webhook listing with display + assignment fields)
Union of all supported email provider keys, derived from the PROVIDERS map
Data object passed to Liquid templates
An email address that has passed validation (a non-empty host containing at least one dot) and been normalized (trimmed, lowercased).
Placeholder in a bulk template marking where each recipient's unsubscribe URL goes.
Group an order's entries for buyer-facing rendering (the confirmation body and its SVG tickets): each hidden package's rows collapse into one group sitting where its first row was, so a mixed order conceals every hidden bundle while its other rows render normally.
Display labels for email providers — keys must match EmailProvider
Format-only email schema: validates an address exactly as typed, without trimming or lowercasing. Used by field validators that check raw user input (see validateEmail in #templates/fields.ts).
Picklist schema for the supported email providers. Its options are derived
from the PROVIDERS map so the two can never drift, and it mirrors the
string-union picklists in types.ts (ContactFieldSchema, PaymentProviderSchema
…) — EmailProviderSchema.options + v.is replace the previous hand-rolled
Set + .has() guard.
Canonical email schema used across the app: local@host.tld. valibot's
email action guarantees a non-empty local part and a host containing at
least one dot. The input is trimmed and lowercased before validation, and the
output is branded as ValidEmail so a value can only be produced by passing
validation. All email validation that needs a normalized, carry-onward value
goes through this (see isValidEmail / parseEmail).
Type guard: checks if a string is a valid EmailProvider
The buyer's summed price (minor units) across an order's entries.
The bundle's summed booked quantity across an order's entries.
Valid provider names (the picklist options), derived from the PROVIDERS map
Usage
import * as mod from "docs/email.ts";