import { env } from "cloudflare:workers"; import { publicUrl } from "./events/emit"; import type { QueueMessage } from "./events/emit"; export type EmailQueueMessage = Extract; type EmailContent = { subject: string; text: string; html: string }; const HTML_ESCAPES: Record = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }; function escapeHtml(value: string): string { return value.replace(/[&<>"']/g, (char) => HTML_ESCAPES[char] ?? char); } function forgeName(): string { return env.FORGE_NAME || "gitfor"; } function layout(heading: string, paragraphs: string[], action: { label: string; url: string } | null): string { const body = paragraphs.map((text) => `

${escapeHtml(text)}

`).join(""); const button = action ? `

${escapeHtml(action.label)}

${escapeHtml(action.url)}

` : ""; return `
${escapeHtml(forgeName())}

${escapeHtml(heading)}

${body}${button}
`; } function render(subject: string, paragraphs: string[], action: { label: string; url: string } | null): EmailContent { const text = [...paragraphs, ...(action ? [`${action.label}: ${action.url}`] : [])].join("\n\n"); return { subject: `[${forgeName()}] ${subject}`, text, html: layout(subject, paragraphs, action) }; } export const emailTemplates = { verifyEmail(name: string, url: string): EmailContent { return render( "Verify your email address", [`Hi ${name}, confirm this address to finish setting up your account.`, "The link expires in 24 hours."], { label: "Verify email", url }, ); }, passwordReset(name: string, url: string): EmailContent { return render( "Reset your password", [ `Hi ${name}, someone asked to reset the password for your account.`, "The link expires in one hour. If this wasn't you, you can ignore this email.", ], { label: "Choose a new password", url }, ); }, securityNotice(name: string, event: string): EmailContent { return render( "Security alert", [`Hi ${name}, ${event}.`, "If this wasn't you, reset your password and review your security settings."], { label: "Review security settings", url: publicUrl("/settings/security") }, ); }, notification(title: string, reason: string, url: string): EmailContent { return render( title, [ `You are receiving this because of: ${reason.replace(/_/g, " ")}.`, "Manage notification settings in your account.", ], { label: "View on the forge", url: publicUrl(url) }, ); }, }; /** Queues an email; the Queue consumer delivers it through the Email Service binding with retries. */ export async function queueEmail(to: string, content: EmailContent): Promise { const message: EmailQueueMessage = { kind: "email.send", to, ...content }; await env.EVENTS.send(message); } /** Email Service only sends from verified domains, so an unset or localhost sender means email is disabled. */ export function emailConfigured(): boolean { const domain = env.EMAIL_FROM.match(/@([^>\s]+)/)?.[1] ?? ""; return domain !== "" && domain !== "localhost" && domain.includes("."); } export async function deliverEmail(message: EmailQueueMessage): Promise { if (!emailConfigured()) { console.warn( "Email disabled: set EMAIL_FROM to an address on a verified Email Service domain; dropping", message.subject, ); return; } await env.EMAIL.send({ to: message.to, from: env.EMAIL_FROM, subject: message.subject, text: message.text, html: message.html, }); }