/** * welcome-mail.template.ts — Inhalt und Gestaltung der Willkommensmail. * * Reine Funktion ohne Abhaengigkeiten: bekommt die fertigen Werte * (Anzeigename, Benutzername, Adresse, Anmeldeweg) und liefert Betreff, * Text-Alternative und HTML. Versand und Kopfbild-Anhang erledigt * `MailService.sendWelcomeMail`, die Entscheidung "wer bekommt welchen * Anmeldehinweis" `WelcomeMailService`. * * E-Mail-tauglich gebaut, weil Outlook (Word-Darstellung), Gmail und Apple * Mail sehr unterschiedlich darstellen: * - Tabellenlayout, alle Stile inline, hoechstens 600 px breit; * - keine externen Ressourcen, keine Web-Fonts (Systemschriften); * - Kopf: Bildmarke und Schriftzug als HTML (erscheinen immer), darunter * die Duenen-Welle als schmaler PNG-Streifen (`headerImageSrc`, im * Versand `cid:`), weil SVG und CSS-Hintergruende in Outlook nicht * erscheinen; fehlt das Bild, bleibt nur ein schmaler Abschluss; * - Knoepfe als Tabelle mit Hintergrundfarbe in der Zelle ("bulletproof"), * Outlook ignoriert Innenabstaende und Rundungen am Link selbst. * * Sicherheit: jeder eingesetzte Wert laeuft durch `escapeHtml`; Links werden * nur als http(s) uebernommen. Ein Kennwort steht NIE in der Mail — lokale * Konten bekommen einen Link zum Festlegen (Token wie beim * "Passwort vergessen"-Weg), verzeichnisgefuehrte den Hinweis auf das * Windows-Passwort. */ /** Farben aus dem Design "Mosaik" (globals.css / brand.ts). */ const C = { page: '#eceef1', card: '#ffffff', ink: '#1a1d21', body: '#3d4450', muted: '#6b7280', line: '#e3e5e9', well: '#f7f7f5', yellow: '#ffed00', link: '#1d5fc2', header: '#1a1c20', } as const; const FONT = "'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', Arial, sans-serif"; /** Anmeldeweg des Empfaengers — entscheidet den Hinweis in der Mail. */ export type WelcomeMailAccount = | { kind: 'directory' } | { kind: 'local'; setPasswordUrl: string; validHours: number } | { kind: 'local-no-link' }; export interface WelcomeMailInput { /** Anzeigename, sonst Benutzername. */ name: string; username: string; /** Oeffentliche Basisadresse der Web-Oberflaeche, ohne abschliessenden Schraegstrich. */ appUrl: string; account: WelcomeMailAccount; /** `cid:...` im Versand, relativer Pfad in der Vorschau, `null` = Textkopf. */ headerImageSrc: string | null; } export interface RenderedWelcomeMail { subject: string; text: string; html: string; } export const WELCOME_MAIL_SUBJECT = 'Willkommen bei Tessera'; const FOOTER = 'Diese E-Mail wurde von Tessera im Auftrag Ihres Administrators versendet.'; const INTRO = 'für Sie wurde ein Zugang zu Tessera eingerichtet – Ihrer zentralen Plattform für Werkzeuge und Abläufe im Unternehmen. Alles, was Sie für Ihre tägliche Arbeit brauchen, finden Sie dort an einem Ort.'; const DESKTOP_HINT = 'Tessera gibt es auch als Desktop-App – den Download finden Sie auf der Anmeldeseite.'; export function escapeHtml(value: string): string { return value .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /** Nur http(s)-Adressen gelangen in ein href; alles andere wird leer. */ function safeUrl(value: string): string { return /^https?:\/\//i.test(value) ? value : ''; } function loginHintText(account: WelcomeMailAccount): string { switch (account.kind) { case 'directory': return 'Melden Sie sich mit Ihrem Benutzernamen und Ihrem gewohnten Windows-Passwort an.'; case 'local': return 'Bevor Sie sich zum ersten Mal anmelden, legen Sie bitte Ihr persönliches Passwort fest.'; case 'local-no-link': return 'Ihr Startpasswort erhalten Sie von Ihrem Administrator.'; } } function validityText(hours: number): string { const span = hours % 24 === 0 && hours >= 24 ? hours === 24 ? '1 Tag' : `${hours / 24} Tage` : hours === 1 ? '1 Stunde' : `${hours} Stunden`; return `Der Link ist ${span} gültig und nur einmal verwendbar. Ist er abgelaufen, fordern Sie auf der Anmeldeseite über „Passwort vergessen?“ einfach einen neuen an.`; } /** Knopf als Tabelle: Farbe an der Zelle, damit Outlook ihn als Flaeche zeigt. */ function button(href: string, label: string, bg: string, fg: string): string { return `
${escapeHtml(label)}
`; } /** Eine Kachel der Bildmarke: feste Zelle, Hoehe auch in Outlook exakt. */ function tile(color: string | null): string { const bg = color ? `bgcolor="${color}" style="background-color:${color};` : 'style="'; return ` `; } /** Luecke zwischen Kacheln. */ const GAP = ' '; /** * Bildmarke als HTML (quick-260930): das Kachel-"T" aus Tabellenzellen — * oben drei Kacheln (die dritte gelb, im Original gedreht), darunter zwei in * der Mitte —, auf dunkler Grundplatte mit heller Kontur wie in der App. * Braucht kein Bild und erscheint deshalb in jedem Mailprogramm. */ function logoMark(): string { const row = (cells: Array) => `${cells.map((c, i) => (i > 0 ? GAP : '') + tile(c)).join('')}`; const spacer = ` `; const olive = '#9c9440'; return `
${row([olive, olive, C.yellow])}${spacer}${row([null, olive, null])}${spacer}${row([null, olive, null])}
`; } /** * Kopf der Mail (quick-260930, Rueckmeldung des Nutzers: in Outlook "ein * riesiger schwarzer Fleck, kein Logo"). Vorher steckten Logo und * Schriftzug in EINEM 150 px hohen Bild; zeigt ein Mailprogramm das * eingebettete Bild nicht an, blieb nur die dunkle Flaeche. Jetzt: * - Bildmarke (HTML-Kacheln) und Schriftzug "Tessera" (echter Text) in einer * niedrigen dunklen Leiste — erscheinen immer; * - darunter die Duenen-Welle als schmaler Bildstreifen (`src`, im Versand * `cid:`), der ins Weiss der Karte auslaeuft. Fehlt er, bleibt nur weisser * Abstand, kein dunkler Block. */ function headerRow(src: string | null): string { const bar = `
${logoMark()} Tessera
`; const wave = src ? ` ` : ` `; return bar + wave; } export function renderWelcomeMail(input: WelcomeMailInput): RenderedWelcomeMail { const base = safeUrl(input.appUrl.replace(/\/+$/, '')); const loginUrl = `${base}/login`; const hint = loginHintText(input.account); // ── Text-Alternative ─────────────────────────────────────────────────── const textLines = [ `Guten Tag ${input.name},`, '', INTRO, '', 'Ihre Zugangsdaten', `Adresse: ${base}`, `Benutzername: ${input.username}`, '', hint, ]; if (input.account.kind === 'local') { textLines.push( '', 'Passwort festlegen:', input.account.setPasswordUrl, validityText(input.account.validHours), ); } textLines.push( '', 'Zu Tessera:', loginUrl, '', DESKTOP_HINT, '', 'Viel Erfolg mit Tessera!', '', '--', FOOTER, ); const text = textLines.join('\n'); // ── HTML ─────────────────────────────────────────────────────────────── const name = escapeHtml(input.name); const username = escapeHtml(input.username); const baseHtml = escapeHtml(base); const p = (content: string, extra = '') => `

${content}

`; const label = (content: string) => `
${content}
`; let accountBlock = p(escapeHtml(hint), 'margin:24px 0 16px;'); if (input.account.kind === 'local') { const setUrl = safeUrl(input.account.setPasswordUrl); accountBlock += `${button(setUrl, 'Passwort festlegen', C.ink, '#ffffff')}

${escapeHtml(validityText(input.account.validHours))}

`; } const html = ` ${escapeHtml(WELCOME_MAIL_SUBJECT)}
Ihr Zugang zu Tessera ist eingerichtet – hier finden Sie Adresse und Benutzername.
${headerRow(input.headerImageSrc)}

Willkommen bei Tessera, ${name}!

${p(escapeHtml(INTRO.charAt(0).toUpperCase() + INTRO.slice(1)))}
${label('Adresse')} ${label('Benutzername')}
${username}
${accountBlock}
${button(loginUrl, 'Zu Tessera', C.yellow, C.ink)}
${p(`Tipp: ${escapeHtml(DESKTOP_HINT)}`, `font-size:14px;line-height:22px;margin:0 0 12px;`)} ${p('Viel Erfolg mit Tessera!', 'font-size:14px;line-height:22px;margin:0;')}
${escapeHtml(FOOTER)}
`; return { subject: WELCOME_MAIL_SUBJECT, text, html }; }