/**
* 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 `
`;
}
/** 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 = `|
|
`;
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 };
}