139 lines
5.2 KiB
TypeScript
139 lines
5.2 KiB
TypeScript
|
|
import { Resend } from "resend";
|
||
|
|
|
||
|
|
const FONT_STACK =
|
||
|
|
"'Barlow', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";
|
||
|
|
|
||
|
|
const FROM_DEFAULT = "Brackt <noreply@brackt.com>";
|
||
|
|
|
||
|
|
// Lazily initialized so import doesn't throw in test environments without RESEND_API_KEY.
|
||
|
|
let _resend: Resend | null = null;
|
||
|
|
function getResend(): Resend {
|
||
|
|
if (!_resend) {
|
||
|
|
const key = process.env.RESEND_API_KEY;
|
||
|
|
if (!key) throw new Error("RESEND_API_KEY is not set");
|
||
|
|
_resend = new Resend(key);
|
||
|
|
}
|
||
|
|
return _resend;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function sendEmail(opts: {
|
||
|
|
to: string;
|
||
|
|
subject: string;
|
||
|
|
html: string;
|
||
|
|
text?: string;
|
||
|
|
replyTo?: string;
|
||
|
|
from?: string;
|
||
|
|
}): Promise<{ error: Error | null }> {
|
||
|
|
const resend = getResend();
|
||
|
|
const { error } = await resend.emails.send({
|
||
|
|
from: opts.from ?? FROM_DEFAULT,
|
||
|
|
to: opts.to,
|
||
|
|
subject: opts.subject,
|
||
|
|
html: opts.html,
|
||
|
|
...(opts.text ? { text: opts.text } : {}),
|
||
|
|
...(opts.replyTo ? { replyTo: opts.replyTo } : {}),
|
||
|
|
});
|
||
|
|
return { error: error ? new Error(error.message) : null };
|
||
|
|
}
|
||
|
|
|
||
|
|
export function escapeHtml(s: string): string {
|
||
|
|
return s
|
||
|
|
.replace(/&/g, "&")
|
||
|
|
.replace(/</g, "<")
|
||
|
|
.replace(/>/g, ">")
|
||
|
|
.replace(/"/g, """)
|
||
|
|
.replace(/'/g, "'");
|
||
|
|
}
|
||
|
|
|
||
|
|
export function emailParagraph(html: string): string {
|
||
|
|
return `<p style="margin:0 0 16px;font-family:${FONT_STACK};font-size:16px;line-height:1.6;color:#ffffff;">${html}</p>`;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function emailButton(href: string, text: string): string {
|
||
|
|
return `
|
||
|
|
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:24px 0;">
|
||
|
|
<tr>
|
||
|
|
<td bgcolor="#adf661" style="border-radius:6px;">
|
||
|
|
<a href="${escapeHtml(href)}" target="_blank" style="display:inline-block;padding:12px 28px;color:#14171e;font-family:${FONT_STACK};font-size:16px;font-weight:700;text-decoration:none;border-radius:6px;">${text}</a>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
</table>`;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function wrapInEmailTemplate(
|
||
|
|
content: string,
|
||
|
|
preheader?: string
|
||
|
|
): string {
|
||
|
|
const appUrl = process.env.APP_URL ?? "https://brackt.com";
|
||
|
|
const logoUrl = `${appUrl}/email-logo.png`;
|
||
|
|
const year = new Date().getFullYear();
|
||
|
|
|
||
|
|
const preheaderHtml = preheader
|
||
|
|
? `<span style="display:none;max-height:0;overflow:hidden;mso-hide:all;">${escapeHtml(preheader)} ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌</span>`
|
||
|
|
: "";
|
||
|
|
|
||
|
|
return `<!DOCTYPE html>
|
||
|
|
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||
|
|
<head>
|
||
|
|
<meta charset="utf-8">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
|
|
<meta name="color-scheme" content="light dark">
|
||
|
|
<meta name="supported-color-schemes" content="light dark">
|
||
|
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||
|
|
<!--[if gte mso 9]>
|
||
|
|
<xml>
|
||
|
|
<o:OfficeDocumentSettings>
|
||
|
|
<o:AllowPNG/>
|
||
|
|
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||
|
|
</o:OfficeDocumentSettings>
|
||
|
|
</xml>
|
||
|
|
<![endif]-->
|
||
|
|
<style>
|
||
|
|
@import url('https://fonts.googleapis.com/css2?family=Barlow:wght@400;600;700&display=swap');
|
||
|
|
body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
|
||
|
|
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
|
||
|
|
img { -ms-interpolation-mode: bicubic; border: 0; outline: none; text-decoration: none; }
|
||
|
|
body { margin: 0; padding: 0; background-color: #14171e; color-scheme: light dark; }
|
||
|
|
a[x-apple-data-detectors] { color: inherit !important; text-decoration: none !important; }
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body style="margin:0;padding:0;background-color:#14171e;">
|
||
|
|
${preheaderHtml}
|
||
|
|
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" bgcolor="#14171e" style="background-color:#14171e;">
|
||
|
|
<tr>
|
||
|
|
<td align="center" style="padding:40px 16px;">
|
||
|
|
<!--[if mso]>
|
||
|
|
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="600"><tr><td>
|
||
|
|
<![endif]-->
|
||
|
|
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="max-width:600px;background-color:#1c1f26;border-radius:8px;border:1px solid rgba(255,255,255,0.1);">
|
||
|
|
<!-- Header -->
|
||
|
|
<tr>
|
||
|
|
<td align="center" style="padding:32px 40px 24px;border-bottom:1px solid rgba(255,255,255,0.08);">
|
||
|
|
<img src="${logoUrl}" width="48" height="62" alt="Brackt" style="display:block;width:48px;height:62px;">
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
<!-- Content -->
|
||
|
|
<tr>
|
||
|
|
<td style="padding:32px 40px;">
|
||
|
|
${content}
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
<!-- Footer -->
|
||
|
|
<tr>
|
||
|
|
<td align="center" style="padding:24px 40px;border-top:1px solid rgba(255,255,255,0.08);">
|
||
|
|
<p style="margin:0;font-family:${FONT_STACK};font-size:12px;line-height:1.5;color:rgba(255,255,255,0.45);">
|
||
|
|
© ${year} Brackt. All rights reserved.
|
||
|
|
</p>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
</table>
|
||
|
|
<!--[if mso]>
|
||
|
|
</td></tr></table>
|
||
|
|
<![endif]-->
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
</table>
|
||
|
|
</body>
|
||
|
|
</html>`;
|
||
|
|
}
|