/** * 验证码模板(纯函数):data → { subject, html, text }。 * 本模板不生成、不校验验证码——只渲染 auth 传入的 `code` 与 `ttlMin`。 * * @typedef {Object} VerifyCodeData * @property {string} code 验证码(auth 生成) * @property {number} ttlMin 有效期(分钟) * @property {'en'|'zh'|'ja'|'ko'|'es'|'pt'} locale */ import { EmailError } from '../map-error.js' const COPY = { en: { subject: (code) => `VOKO verification code: ${code}`, heading: 'Verify your VOKO email', intro: 'Use the code below to continue. Do not share it with anyone.', ttl: (min) => `This code expires in ${min} minute${min === 1 ? '' : 's'}.`, ignore: 'If you did not request this, you can safely ignore this email.', }, zh: { subject: (code) => `VOKO 验证码:${code}`, heading: '验证您的 VOKO 邮箱', intro: '请使用下方验证码继续操作,请勿向任何人泄露。', ttl: (min) => `该验证码将在 ${min} 分钟后失效。`, ignore: '如果这不是您本人的操作,请忽略本邮件。', }, ja: { subject: (code) => `VOKO認証コード:${code}`, heading: 'VOKOのメールアドレスを認証', intro: '続行するには、以下のコードを使用してください。他の人と共有しないでください。', ttl: (min) => `このコードは${min}分後に無効になります。`, ignore: 'このリクエストに心当たりがない場合は、このメールを無視してください。', }, ko: { subject: (code) => `VOKO 인증 코드: ${code}`, heading: 'VOKO 이메일 인증', intro: '계속하려면 아래 코드를 사용하세요. 다른 사람과 공유하지 마세요.', ttl: (min) => `이 코드는 ${min}분 후에 만료됩니다.`, ignore: '본인이 요청하지 않았다면 이 이메일을 무시하세요.', }, es: { subject: (code) => `Código de verificación de VOKO: ${code}`, heading: 'Verifica tu correo de VOKO', intro: 'Usa el siguiente código para continuar. No lo compartas con nadie.', ttl: (min) => `Este código caduca en ${min} minuto${min === 1 ? '' : 's'}.`, ignore: 'Si no solicitaste este código, puedes ignorar este correo.', }, pt: { subject: (code) => `Código de verificação do VOKO: ${code}`, heading: 'Verifique seu e-mail do VOKO', intro: 'Use o código abaixo para continuar. Não o compartilhe com ninguém.', ttl: (min) => `Este código expira em ${min} minuto${min === 1 ? '' : 's'}.`, ignore: 'Se você não solicitou este código, ignore este e-mail.', }, } /** * @param {VerifyCodeData} data * @returns {{ subject: string, html: string, text: string }} */ export function render(data) { if (!data || typeof data !== 'object') { throw new EmailError({ provider: 'template', reason: 'badTemplate', retryable: false, message: 'verify-code: data required' }) } const { code, ttlMin } = data if (typeof code !== 'string' || code.trim() === '') { throw new EmailError({ provider: 'template', reason: 'badTemplate', retryable: false, message: 'verify-code: missing field "code"' }) } if (typeof ttlMin !== 'number' || !Number.isFinite(ttlMin) || ttlMin <= 0) { throw new EmailError({ provider: 'template', reason: 'badTemplate', retryable: false, message: 'verify-code: missing or invalid field "ttlMin"' }) } const c = COPY[data.locale] if (!c) { throw new EmailError({ provider: 'template', reason: 'badTemplate', retryable: false, message: `verify-code: unsupported locale: ${String(data.locale)}` }) } const subject = c.subject(code) const ttlLine = c.ttl(ttlMin) const text = [c.heading, '', c.intro, '', code, '', ttlLine, '', c.ignore].join('\n') const html = [ '
', `${esc(c.intro)}
`, `${esc(code)}
`, `${esc(ttlLine)}
`, `${esc(c.ignore)}
`, '', ].join('') return { subject, html, text } } function esc(s) { return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') }