UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

175 lines 5.79 kB
/** * @beignet/core/mail * * Shared mail port and test adapters for Beignet applications. */ import { createProvider, createProviderInstrumentation, } from "../providers/index.js"; /** * Error thrown by mail helpers and provider adapters. */ export class MailDeliveryError extends Error { /** * Provider name when known. */ provider; /** * Original provider error when available. */ cause; constructor(args) { super(args.message); this.name = "MailDeliveryError"; this.provider = args.provider; this.cause = args.cause; } } /** * Normalize a single address or address list into an array. * * This helper does not validate email syntax. */ export function normalizeMailAddressList(addresses) { if (addresses === undefined) return undefined; return Array.isArray(addresses) ? [...addresses] : [addresses]; } function normalizeOptionalMailAddressList(addresses) { const normalized = normalizeMailAddressList(addresses); return normalized && normalized.length > 0 ? normalized : undefined; } /** * Format one address for providers that accept RFC-like address strings. * * Rejects carriage returns and line feeds so address values cannot inject * additional mail header lines. This helper does not validate email syntax. */ export function formatMailAddress(address) { if (typeof address === "string") { assertMailAddressLineSafe(address); return address; } assertMailAddressLineSafe(address.email); if (!address.name) return address.email; assertMailAddressLineSafe(address.name); const escapedName = address.name.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); return `"${escapedName}" <${address.email}>`; } function assertMailAddressLineSafe(value) { if (/[\r\n]/.test(value)) { throw new MailDeliveryError({ message: "Mail addresses must not include carriage returns or line feeds.", }); } } /** * Format one or more addresses for providers that accept string address fields. */ export function formatMailAddressList(addresses) { const normalized = normalizeMailAddressList(addresses) ?? []; const formatted = normalized.map(formatMailAddress); return formatted.length === 1 ? formatted[0] : formatted; } /** * Normalize a mail message and apply a default sender. * * Throws when the message has no recipients. */ export function normalizeMailMessage(message, options = {}) { const to = normalizeMailAddressList(message.to) ?? []; if (to.length === 0) { throw new MailDeliveryError({ message: "Cannot send email without at least one recipient.", }); } return { ...message, to, from: message.from ?? options.defaultFrom, cc: normalizeOptionalMailAddressList(message.cc), bcc: normalizeOptionalMailAddressList(message.bcc), replyTo: normalizeOptionalMailAddressList(message.replyTo), text: "text" in message ? message.text : undefined, html: "html" in message ? message.html : undefined, }; } /** * Create an in-memory mailer for tests, local development, and examples. * * The memory mailer does not send real email or validate address syntax. */ export function createMemoryMailer(options = {}) { return createMemoryMailerInternal(options); } function createMemoryMailerInternal(options, onDelivery) { const deliveries = []; const now = options.now ?? (() => new Date()); const id = options.id ?? (() => crypto.randomUUID()); return { get deliveries() { return deliveries; }, async send(message) { const startedAt = Date.now(); const delivery = { id: id(), message: normalizeMailMessage(message, { defaultFrom: options.defaultFrom, }), sentAt: now(), }; deliveries.push(delivery); onDelivery?.(delivery, Date.now() - startedAt); await options.onSend?.(delivery); return { id: delivery.id, provider: "memory", }; }, clear() { deliveries.length = 0; }, }; } /** * Create a provider that contributes an in-memory mailer. * * Use it as the dev-default `mailer` port in `server/providers.ts` until a * real mail provider such as Resend or SMTP takes over. Deliveries are * captured in memory and recorded as `mail.sent` devtools events through the * `mail` watcher when an instrumentation port is installed. */ export function createMemoryMailerProvider(options = {}) { const { name = "memory-mailer", onSend, ...mailerOptions } = options; return createProvider({ name, setup({ ports }) { const instrumentation = createProviderInstrumentation(ports, { providerName: name, watcher: "mail", }); const mailer = createMemoryMailerInternal({ ...mailerOptions, onSend, }, (delivery, durationMs) => { instrumentation.custom({ name: "mail.sent", label: "Mail sent", summary: delivery.message.subject, details: { to: delivery.message.to, subject: delivery.message.subject, id: delivery.id, durationMs, }, }); }); return { ports: { mailer, }, }; }, }); } //# sourceMappingURL=index.js.map