import {sanitizeEmailHtml} from './sanitize-email-html';
interface Mailer {
send(options: {to: string; subject: string; html: string; text?: string}): Promise;
}
interface GenerateEmailContent {
(options: {template: string; data: Record}): Promise<{html: string; text?: string}>;
}
export interface NotificationEmailDeps {
mailer: Mailer;
generateEmailContent: GenerateEmailContent;
getSiteUrl: () => string;
}
export interface NotificationEmailMessage {
/**
* Resolved recipient addresses. The service does not decide who receives
* the email; callers compose recipients via a separate audience helper.
*/
to: string[];
subject: string;
/**
* Untrusted HTML content. Sanitised before being rendered into the shell,
* so the email cannot ship scripts, event handlers or unsafe links even
* if the upstream source is compromised.
*/
content: string;
}
/**
* Renders a notification email in the shared shell and sends it to each
* recipient. Recipient resolution is a separate concern handled by the caller.
*/
export class NotificationEmailService {
private readonly mailer: Mailer;
private readonly generateEmailContent: GenerateEmailContent;
private readonly getSiteUrl: () => string;
constructor(deps: NotificationEmailDeps) {
this.mailer = deps.mailer;
this.generateEmailContent = deps.generateEmailContent;
this.getSiteUrl = deps.getSiteUrl;
}
async send({to, subject, content}: NotificationEmailMessage): Promise {
if (!to.length) {
return;
}
const message = sanitizeEmailHtml(content);
const siteUrl = this.getSiteUrl();
for (const recipient of to) {
const {html, text} = await this.generateEmailContent({
template: 'notification',
data: {message, siteUrl, recipientEmail: recipient}
});
await this.mailer.send({
to: recipient,
subject,
html,
...(text ? {text} : {})
});
}
}
}