interface SendEmailInput { to: string; subject: string; html: string; text?: string; } const FROM = process.env.EMAIL_FROM ?? "Podcast Distribution AI "; /** * Send a transactional email via Resend when configured; otherwise log to the * console (useful in local dev before RESEND_API_KEY is set). */ export async function sendEmail({ to, subject, html, text }: SendEmailInput): Promise { const apiKey = process.env.RESEND_API_KEY; if (!apiKey) { console.info(`[email:dev] To: ${to} | Subject: ${subject}\n${text ?? html}`); return; } const { Resend } = await import("resend"); const resend = new Resend(apiKey); const { error } = await resend.emails.send({ from: FROM, to, subject, html, text }); if (error) throw new Error(`Resend error: ${error.message}`); } /** Escape text for safe interpolation into HTML/attribute contexts. */ function escapeHtml(value: string): string { return value .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } /** Allow only http/https/mailto URLs; fall back to "#" for anything else (e.g. javascript:). */ function safeUrl(url: string): string { try { const scheme = new URL(url).protocol; if (scheme === "http:" || scheme === "https:" || scheme === "mailto:") return url; } catch { // Not a parseable absolute URL — reject. } return "#"; } /** * Minimal branded wrapper so transactional emails share a consistent look. * * NOTE: `body` is interpolated as TRUSTED raw HTML and is intentionally NOT escaped. * Callers must only ever pass static, trusted markup — never user-supplied input. * `title` and `cta.label`/`cta.url` are escaped/validated for defense in depth. */ export function emailLayout(title: string, body: string, cta?: { label: string; url: string }) { const button = cta ? `${escapeHtml(cta.label)}` : ""; return `

🎙️ Podcast Distribution AI

${escapeHtml(title)}

${body}
${button}

If you didn't request this, you can ignore this email.

`; }