/** * Branded transactional templates: text + HTML for every message. Neutral palette, one accent, * no images (deliverability), numbers always with context. Keep copy short and factual. */ const SITE = () => (process.env.NEXT_PUBLIC_SITE_URL ?? 'https://www.rareindex.io').replace(/\/$/, ''); function esc(s: string): string { return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } export interface Rendered { subject: string; html: string; text: string; } function layout(opts: { title: string; preheader?: string; blocks: string[]; footer?: string }): string { const pre = opts.preheader ? `${esc(opts.preheader)}` : ''; return `${esc(opts.title)} ${pre}
${opts.blocks.map((b) => ``).join('')}
R RareIndex
${esc(opts.title)}
${b}
${opts.footer ?? `Sent by RareIndex · rareindex.io. Valuations are estimates derived from observed public data; listing prices are not confirmed transactions.`}

RareIndex does not authenticate items. Manage e-mail preferences in account settings.

`; } function codeBlock(code: string): string { return `
${esc(code)}
`; } function button(href: string, label: string): string { return `${esc(label)}`; } export function verificationEmail(p: { code: string; minutes: number }): Rendered { const subject = `${p.code} is your RareIndex verification code`; return { subject, text: `Your RareIndex verification code is ${p.code}. It expires in ${p.minutes} minutes.\n\nIf you did not create an account, ignore this message.`, html: layout({ title: 'Confirm your e-mail', preheader: `Code ${p.code} · expires in ${p.minutes} min`, blocks: [`Enter this code to activate your RareIndex account. It expires in ${p.minutes} minutes.`, codeBlock(p.code), `If you did not create an account, you can ignore this message.`] }), }; } export function mfaCodeEmail(p: { code: string; minutes: number; ip?: string | null; userAgent?: string | null }): Rendered { const ctx = [p.ip ? `IP ${esc(p.ip)}` : null, p.userAgent ? esc(p.userAgent.slice(0, 80)) : null].filter(Boolean).join(' · '); return { subject: `${p.code} is your RareIndex sign-in code`, text: `Your sign-in code is ${p.code} (valid ${p.minutes} minutes).${ctx ? `\nSign-in attempt: ${ctx}` : ''}\n\nIf this was not you, change your password immediately.`, html: layout({ title: 'Your sign-in code', preheader: `Code ${p.code}`, blocks: [`Use this code to finish signing in. Valid for ${p.minutes} minutes.`, codeBlock(p.code), ctx ? `Attempt: ${ctx}` : '', `If this was not you, secure your account now.`] }), }; } export function newLoginEmail(p: { when: Date; ip?: string | null; userAgent?: string | null; method: string }): Rendered { const line = `${p.when.toUTCString()}${p.ip ? ` · IP ${p.ip}` : ''}${p.userAgent ? ` · ${p.userAgent.slice(0, 90)}` : ''} · ${p.method}`; return { subject: 'New sign-in to your RareIndex account', text: `A new sign-in to your RareIndex account was completed.\n${line}\n\nIf this was not you, reset your password and review trusted devices: ${SITE()}/account/security`, html: layout({ title: 'New sign-in', blocks: [`A new sign-in to your account was completed from a device we had not seen before.`, `${esc(line)}`, `If this was not you, reset your password and revoke devices.`, button(`${SITE()}/account/security`, 'Review security')] }), }; } export function passwordResetEmail(p: { code: string; link: string; minutes: number }): Rendered { return { subject: 'Reset your RareIndex password', text: `Reset your password: ${p.link}\n\nOr enter code ${p.code}. The link and code expire in ${p.minutes} minutes. If you did not request this, ignore this message.`, html: layout({ title: 'Reset your password', preheader: `Code ${p.code}`, blocks: [`Click the button or enter the code below. Expires in ${p.minutes} minutes.`, button(p.link, 'Choose a new password'), codeBlock(p.code), `If you did not request a reset, nothing changes.`] }), }; } export function changeEmailEmail(p: { code: string; minutes: number; newEmail: string }): Rendered { return { subject: `${p.code} — confirm your new RareIndex e-mail`, text: `Confirm ${p.newEmail} as your new e-mail with code ${p.code} (valid ${p.minutes} minutes).`, html: layout({ title: 'Confirm your new e-mail', blocks: [`Enter this code to switch your account to ${esc(p.newEmail)}.`, codeBlock(p.code)] }), }; } export interface AlertMail { title: string; body: string; href: string; facts?: Array<[string, string]>; } export function alertEmail(p: AlertMail): Rendered { const facts = p.facts?.length ? `${p.facts.map(([k, v]) => ``).join('')}
${esc(k)}${esc(v)}
` : ''; return { subject: `Alert · ${p.title}`, text: `${p.title}\n${p.body}\n${(p.facts ?? []).map(([k, v]) => `${k}: ${v}`).join('\n')}\n\n${p.href}`, html: layout({ title: p.title, preheader: p.body, blocks: [esc(p.body), facts, button(p.href, 'Open in RareIndex')] }), }; } export interface DigestSection { heading: string; rows: Array<{ label: string; value: string; delta?: string; href?: string }>; } export function digestEmail(p: { name: string | null; period: string; sections: DigestSection[]; portfolio?: { valueUsd: string; change: string } | null }): Rendered { const blocks: string[] = [`${p.name ? `Hi ${esc(p.name)}, here` : 'Here'} is your ${esc(p.period)} RareIndex digest.`]; if (p.portfolio) blocks.push(`
Collection value
${esc(p.portfolio.valueUsd)} ${esc(p.portfolio.change)} this period
`); for (const s of p.sections) { if (!s.rows.length) continue; blocks.push(`
${esc(s.heading)}
${s.rows.map((r) => ``).join('')}
${r.href ? `${esc(r.label)}` : esc(r.label)}${esc(r.value)}${esc(r.delta ?? '')}
`); } blocks.push(button(`${SITE()}/collections`, 'Open my RareIndex')); const text = [`Your ${p.period} RareIndex digest`, p.portfolio ? `Collection value ${p.portfolio.valueUsd} (${p.portfolio.change})` : '', ...p.sections.flatMap((s) => [``, s.heading.toUpperCase(), ...s.rows.map((r) => `- ${r.label}: ${r.value} ${r.delta ?? ''}`)])].join('\n'); return { subject: `Your ${p.period} RareIndex digest`, text, html: layout({ title: `${p.period[0]?.toUpperCase()}${p.period.slice(1)} digest`, blocks }) }; } export function accountDeletionEmail(p: { purgeAt: Date }): Rendered { return { subject: 'Your RareIndex account is scheduled for deletion', text: `Your account will be permanently deleted on ${p.purgeAt.toUTCString()}. Sign in before then to cancel.`, html: layout({ title: 'Account deletion scheduled', blocks: [`Your account and all private data will be permanently deleted on ${esc(p.purgeAt.toUTCString())}.`, `Signing in before that date cancels the deletion.`, button(`${SITE()}/login`, 'Sign in to keep my account')] }), }; }