@beignet/core
Version:
Core framework primitives for Beignet
175 lines • 5.79 kB
JavaScript
/**
* @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