/** * Auth mail templates for the Supabase Send Email hook (WL-05, WL-13). * * Swedish-only, like the other user-facing mail templates in lib/email/ * (see .claude/rules/i18n.md): these are transactional mails, not UI chrome, * so they deliberately do not go through next-intl. * * The appName parameter carries the brand of the requesting host (resolved * via resolveBrandByHost from the redirect_to origin); the default is the * platform name, so unbranded hosts render canonical mail. */ import { escapeHtml, sanitizeSubjectLine } from './user-text' export type AuthEmailActionType = | 'signup' | 'recovery' | 'magiclink' | 'invite' | 'email_change' | 'email_change_current' | 'reauthentication' | 'bankid_signup' interface AuthEmailContent { subject: string heading: string body: (appName: string) => string cta: string } const CONTENT: Record = { signup: { subject: 'Bekräfta din e-postadress', heading: 'Bekräfta din e-postadress', body: (appName) => `Klicka på knappen nedan för att bekräfta din e-postadress och slutföra din registrering hos ${appName}.`, cta: 'Bekräfta e-postadress', }, recovery: { subject: 'Återställ ditt lösenord', heading: 'Återställ ditt lösenord', body: (appName) => `Vi har tagit emot en begäran om att återställa lösenordet för ditt konto hos ${appName}. Klicka på knappen nedan för att välja ett nytt lösenord.`, cta: 'Återställ lösenord', }, magiclink: { subject: 'Din inloggningslänk', heading: 'Logga in', body: (appName) => `Klicka på knappen nedan för att logga in hos ${appName}.`, cta: 'Logga in', }, invite: { subject: 'Du har blivit inbjuden', heading: 'Du har blivit inbjuden', body: (appName) => `Du har blivit inbjuden till ${appName}. Klicka på knappen nedan för att skapa ditt konto.`, cta: 'Acceptera inbjudan', }, email_change: { subject: 'Bekräfta din nya e-postadress', heading: 'Bekräfta din nya e-postadress', body: (appName) => `Klicka på knappen nedan för att bekräfta din nya e-postadress hos ${appName}. Av säkerhetsskäl skickas två mail, ett till din nya adress och ett till din nuvarande. Bytet slutförs först när du klickat på länken i båda.`, cta: 'Bekräfta ny e-postadress', }, email_change_current: { subject: 'Godkänn ändrad e-postadress', heading: 'Godkänn ändrad e-postadress', body: (appName) => `En ändring av e-postadressen för ditt konto hos ${appName} har begärts. Klicka på knappen nedan för att godkänna ändringen från din nuvarande adress. Av säkerhetsskäl skickas två mail, ett till din nuvarande adress och ett till din nya. Bytet slutförs först när du klickat på länken i båda.`, cta: 'Godkänn ändringen', }, reauthentication: { subject: 'Din verifieringskod', heading: 'Din verifieringskod', body: (appName) => `Ange koden nedan för att bekräfta din identitet hos ${appName}.`, cta: '', }, // Sent by the BankID signup (extensions/general/tic) to the address the // person typed, and again when a still-unconfirmed identity tries to log // in. Not a Supabase hook type: the link is minted by generateLink and only // ever travels by mail. The copy says plainly that the account was opened // with BankID and that ignoring the mail leaves it inactive, so a stranger // whose address was typed by mistake (or on purpose) is not nudged into // activating someone else's BankID login. bankid_signup: { subject: 'Bekräfta din e-postadress', heading: 'Bekräfta din e-postadress', body: (appName) => `Ett konto hos ${appName} har skapats med BankID och den här e-postadressen. Klicka på knappen nedan för att bekräfta att adressen är din och aktivera kontot. Om det inte var du som skapade kontot kan du bortse från det här meddelandet: kontot förblir inaktivt och kan inte användas för att logga in.`, cta: 'Bekräfta e-postadress', }, } // Availability first: an action type this module does not know (Supabase can // add new mail classes) still produces a usable mail with the verify link // rather than dropping the send. const FALLBACK_CONTENT: AuthEmailContent = { subject: 'Bekräfta din åtgärd', heading: 'Bekräfta din åtgärd', body: (appName) => `Klicka på knappen nedan för att fortsätta hos ${appName}.`, cta: 'Fortsätt', } const IGNORE_NOTE = 'Om du inte begärde detta kan du bortse från det här meddelandet.' export interface AuthEmailInput { actionType: string appName: string /** Verify URL on the originating host (token_hash flow). */ actionUrl?: string /** One-time code, for reauthentication mail (no link). */ otpCode?: string } export interface AuthEmail { subject: string html: string text: string } function contentFor(actionType: string): AuthEmailContent { return (CONTENT as Record)[actionType] ?? FALLBACK_CONTENT } export function buildAuthEmail(input: AuthEmailInput): AuthEmail { const content = contentFor(input.actionType) const appName = input.appName const safeAppName = escapeHtml(appName) const body = content.body(appName) const action = input.otpCode ? `
${escapeHtml(input.otpCode)}
` : input.actionUrl ? `
${escapeHtml(content.cta)}
` : '' const html = ` ${escapeHtml(content.subject)}

${safeAppName.toUpperCase()}

${escapeHtml(content.heading)}

${escapeHtml(body)}

${action}

${IGNORE_NOTE}

` let text = `${content.heading}\n\n${body}\n\n` if (input.otpCode) { text += `Kod: ${input.otpCode}\n\n` } else if (input.actionUrl) { text += `${content.cta}: ${input.actionUrl}\n\n` } text += IGNORE_NOTE return { subject: sanitizeSubjectLine(content.subject), html, text, } }