feat: add application productivity foundations
Quality / quality (ubuntu-latest) (push) Failing after 22s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-23 11:13:03 +05:30
parent 46195462c3
commit 64ab20cc95
42 changed files with 1533 additions and 40 deletions
+88
View File
@@ -0,0 +1,88 @@
import type { DefinedJob } from "@wrnexus/queue";
export interface MailMessage {
from?: string;
to: string | readonly string[];
subject: string;
html?: string;
text?: string;
headers?: Record<string, string>;
}
export interface MailDriver {
send(message: MailMessage): Promise<unknown>;
test?(): Promise<{ ok: boolean; error?: string }>;
}
export interface MailTemplate<T = Record<string, unknown>> {
subject(data: T): string;
html?(data: T): string;
text?(data: T): string;
}
export interface MailOptions {
driver: MailDriver;
from?: string;
sandbox?: { enabled?: boolean; allowlist?: readonly string[] };
}
export function defineMail(options: MailOptions) {
const allowed = new Set(
(options.sandbox?.allowlist ?? []).map((value) => value.trim().toLowerCase()),
);
const assertAllowed = (recipients: readonly string[]) => {
if (!options.sandbox?.enabled) return;
const denied = recipients.find((recipient) => !allowed.has(recipient.trim().toLowerCase()));
if (denied) throw new Error(`WRN-MAIL-SANDBOX: recipient '${denied}' is not allow-listed`);
};
return {
async send(message: MailMessage) {
const recipients = typeof message.to === "string" ? [message.to] : [...message.to];
assertAllowed(recipients);
return options.driver.send({ ...message, from: message.from ?? options.from });
},
async render<T>(template: MailTemplate<T>, data: T, to: string | readonly string[]) {
return {
from: options.from,
to,
subject: template.subject(data),
html: template.html?.(data),
text: template.text?.(data),
} satisfies MailMessage;
},
test: () => options.driver.test?.() ?? Promise.resolve({ ok: true }),
};
}
export function defineMailTemplate<T>(template: MailTemplate<T>): MailTemplate<T> {
return template;
}
export function interpolateTemplate(source: string, data: Record<string, unknown>): string {
return source.replace(/\{\{\s*([A-Za-z_][A-Za-z0-9_.]*)\s*\}\}/g, (_match, path: string) => {
let value: unknown = data;
for (const part of path.split("."))
value =
value && typeof value === "object" ? (value as Record<string, unknown>)[part] : undefined;
return value == null ? "" : String(value);
});
}
export function storedMailTemplate(template: {
subject: string;
html?: string;
text?: string;
}): MailTemplate<Record<string, unknown>> {
return {
subject: (data) => interpolateTemplate(template.subject, data),
html:
template.html === undefined ? undefined : (data) => interpolateTemplate(template.html!, data),
text:
template.text === undefined ? undefined : (data) => interpolateTemplate(template.text!, data),
};
}
/** Queue a fully rendered message without coupling the mail package to one queue backend. */
export function queuedMail<T extends MailMessage>(job: Pick<DefinedJob<T>, "add">) {
return { send: (message: T) => job.add(message) };
}