/**
* 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}
|
|
| ${esc(opts.title)} |
${opts.blocks.map((b) => `| ${b} | `).join('')}
| ${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]) => `| ${esc(k)} | ${esc(v)} |
`).join('')}
` : '';
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) => `| ${r.href ? `${esc(r.label)}` : esc(r.label)} | ${esc(r.value)} | ${esc(r.delta ?? '')} |
`).join('')}
`);
}
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')] }),
};
}