/** * The alpha sign-up confirmation email, in the site's voice and look: warm paper, warm black, * one neo-grotesque, hairlines, one square button. Email clients ignore most CSS, so the layout * is tables with inline styles, the type falls back from Inter Tight to Helvetica and Arial, and * the logo travels as an inline attachment (cid) rather than a remote image. * * Colours are the tokens from src/styles/tokens.css, written out: email has no custom properties. * The hairline is --line (ink at 14 %) flattened onto paper. */ import { fileURLToPath } from 'node:url'; const C = { paper: '#E9E5DF', ink: '#1A1A18', muted: '#57564F', line: '#CCC9C3', }; const FONT = "'Inter Tight', Inter, 'Helvetica Neue', Helvetica, Arial, sans-serif"; const LOGO_CID = 'logo@ciphercell.de'; const LOGO = { path: fileURLToPath(new URL('./logo.png', import.meta.url)), width: 140, height: 32 }; const COPY = { en: { subject: 'Confirm your CipherVerifier alpha registration', preheader: 'One click confirms your registration for the CipherVerifier alpha.', title: 'Confirm your registration.', body: (email) => `You registered ${email} for the CipherVerifier alpha. Confirm the address, and we will email you once, when the alpha is released. Nothing else.`, button: 'Confirm registration', expiry: 'The link is valid for 7 days. If you did not register, ignore this email. Your address is not stored.', fallback: 'If the button does not work, open this address in your browser:', }, de: { subject: 'Bitte bestätigen Sie Ihre Registrierung für die CipherVerifier-Alpha', preheader: 'Ein Klick bestätigt Ihre Registrierung für die Alpha von CipherVerifier.', title: 'Registrierung bestätigen.', body: (email) => `Sie haben ${email} für die Alpha von CipherVerifier registriert. Bestätigen Sie die Adresse, und wir schreiben Ihnen einmal, sobald die Alpha erscheint. Sonst nichts.`, button: 'Registrierung bestätigen', expiry: 'Der Link ist 7 Tage gültig. Wenn Sie sich nicht registriert haben, ignorieren Sie diese E-Mail. Ihre Adresse wird nicht gespeichert.', fallback: 'Falls der Button nicht funktioniert, öffnen Sie diese Adresse im Browser:', }, }; const ADDRESS = ['CipherCell', 'Innstr. 43, 94032 Passau']; const SITE_LABEL = 'www.ciphercell.de'; const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]); /** { subject, text, html, attachments } for nodemailer. */ export function confirmMail({ lang, email, link, site }) { const t = COPY[lang] ?? COPY.en; const text = [ t.title, '', t.body(email), '', `${t.button}:`, link, '', t.expiry, '', '-- ', ...ADDRESS, SITE_LABEL, ].join('\n'); const p = (content, { size = 16, color = C.ink, lineHeight = 1.55, extra = '' } = {}) => `

${content}

`; const row = (content, padding) => `${content}`; const rule = ` `; const html = ` ${esc(t.subject)}
${esc(t.preheader)}
${row(`CipherCell`, '0 0 20px')} ${rule} ${row(p(esc(t.title), { size: 34, lineHeight: 1.08, extra: 'font-weight:500;letter-spacing:-0.02em;' }), '48px 0 20px')} ${row(p(esc(t.body(email))), '0 0 32px')} ${row(`
${esc(t.button)}
`, '0 0 32px')} ${row(p(esc(t.expiry), { size: 14, color: C.muted, lineHeight: 1.4 }), '0 0 16px')} ${row(p(`${esc(t.fallback)}
${esc(link)}`, { size: 14, color: C.muted, lineHeight: 1.4 }), '0 0 48px')} ${rule} ${row(p(`${ADDRESS.map(esc).join('
')}
${SITE_LABEL}`, { size: 14, color: C.muted, lineHeight: 1.4 }), '20px 0 0')}
`; return { subject: t.subject, text, html, attachments: [{ filename: 'ciphercell.png', path: LOGO.path, cid: LOGO_CID }], }; }