67 lines
2.6 KiB
TypeScript
67 lines
2.6 KiB
TypeScript
interface SendEmailInput {
|
|
to: string;
|
|
subject: string;
|
|
html: string;
|
|
text?: string;
|
|
}
|
|
|
|
const FROM = process.env.EMAIL_FROM ?? "Podcast Distribution AI <noreply@podcastdistributionai.com>";
|
|
|
|
/**
|
|
* 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<void> {
|
|
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, """)
|
|
.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
|
|
? `<a href="${escapeHtml(safeUrl(cta.url))}" style="display:inline-block;background:#7c3aed;color:#fff;text-decoration:none;padding:12px 20px;border-radius:8px;font-weight:600;margin-top:16px">${escapeHtml(cta.label)}</a>`
|
|
: "";
|
|
return `
|
|
<div style="font-family:Inter,Arial,sans-serif;max-width:480px;margin:0 auto;padding:24px;color:#0a0a0a">
|
|
<h1 style="font-size:20px;margin:0 0 12px">🎙️ Podcast Distribution AI</h1>
|
|
<h2 style="font-size:18px;margin:0 0 12px">${escapeHtml(title)}</h2>
|
|
<div style="font-size:14px;line-height:1.6;color:#404040">${body}</div>
|
|
${button}
|
|
<p style="font-size:12px;color:#a3a3a3;margin-top:32px">If you didn't request this, you can ignore this email.</p>
|
|
</div>`;
|
|
}
|