714f731ac9
Neue Felder loginHintDirectory/loginHintLocal (Platzhalter, Pflicht, max. 1000), Migration 20260930170000 (nullable, leer = Standard aus @tessera/shared). Knopf Passwort festlegen und Gueltigkeitshinweis bleiben fest; local-no-link wird nie erzeugt und bleibt fest. Vorschau springt beim Bearbeiten auf die passende Kontoart. Lokal im Browser nachgewiesen. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
420 lines
20 KiB
TypeScript
420 lines
20 KiB
TypeScript
/**
|
||
* 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`.
|
||
*
|
||
* Eigene Vorlage (Administrator → Willkommensmail): Betreff, Ueberschrift,
|
||
* Einleitung, die zwei Anmeldehinweise (Verzeichniskonto / lokales Konto)
|
||
* und Abschluss kommen als `texts` herein (Vorlage des Mandanten
|
||
* des ZIEL-Benutzers, sonst `DEFAULT_WELCOME_MAIL_TEXTS` aus
|
||
* `@tessera/shared`). Platzhalter (`{{name}}`, `{{vorname}}`,
|
||
* `{{benutzername}}`, `{{email}}`, `{{adresse}}`, `{{firma}}`) werden auf dem
|
||
* REINEN Text ersetzt und erst danach escaped. Leerzeile = neuer Absatz,
|
||
* einfacher Umbruch = `<br>`. Alles andere bleibt fest.
|
||
*
|
||
* 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.
|
||
*/
|
||
|
||
import {
|
||
DEFAULT_WELCOME_MAIL_TEXTS,
|
||
isWelcomeMailPlaceholder,
|
||
WELCOME_MAIL_PLACEHOLDER_RE,
|
||
type WelcomeMailPlaceholder,
|
||
type WelcomeMailTexts,
|
||
} from '@tessera/shared';
|
||
|
||
/** 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 }
|
||
/**
|
||
* Lokales Konto ohne Link. Wird heute nirgends erzeugt (`WelcomeMailService`
|
||
* legt fuer lokale Konten immer einen Token an); deshalb bleibt ihr Hinweis
|
||
* fest und ist nicht Teil der Vorlage.
|
||
*/
|
||
| { kind: 'local-no-link' }
|
||
/**
|
||
* Nur Testmail aus Administrator → Willkommensmail: derselbe Baustein wie
|
||
* `local`, aber OHNE Token — der Knopf fuehrt zur Anmeldeseite und ein
|
||
* Hinweis sagt, was er in der echten Mail tut.
|
||
*/
|
||
| { kind: 'local-example' };
|
||
|
||
export interface WelcomeMailInput {
|
||
/** Anzeigename, sonst Benutzername. */
|
||
name: string;
|
||
username: string;
|
||
/** E-Mail-Adresse des Empfaengers (Platzhalter `{{email}}`). */
|
||
email: string;
|
||
/** Name des Mandanten (Platzhalter `{{firma}}`). */
|
||
tenantName: string;
|
||
/** Oeffentliche Basisadresse der Web-Oberflaeche, ohne abschliessenden Schraegstrich. */
|
||
appUrl: string;
|
||
account: WelcomeMailAccount;
|
||
/** `cid:...` im Versand, `data:` in der Vorschau, `null` = Textkopf. */
|
||
headerImageSrc: string | null;
|
||
/** Eigene Vorlage des Mandanten; fehlt sie, gelten die Standardtexte. */
|
||
texts?: WelcomeMailTexts | null;
|
||
/** Hinweisleiste ganz oben (nur Testmail), reiner Text. */
|
||
notice?: string | null;
|
||
}
|
||
|
||
export interface RenderedWelcomeMail {
|
||
subject: string;
|
||
text: string;
|
||
html: string;
|
||
}
|
||
|
||
export const WELCOME_MAIL_SUBJECT = DEFAULT_WELCOME_MAIL_TEXTS.subject;
|
||
|
||
const FOOTER = 'Diese E-Mail wurde von Tessera im Auftrag Ihres Administrators versendet.';
|
||
const EXAMPLE_LINK_NOTE =
|
||
'Beispiel-Link: In der echten Willkommensmail führt dieser Knopf zu einem persönlichen Link, mit dem der neue Benutzer sein Passwort festlegt. In dieser Testmail öffnet er nur die Anmeldeseite.';
|
||
|
||
/** Werte der Platzhalter, noch NICHT escaped (Ersetzung laeuft auf reinem Text). */
|
||
export type WelcomeMailPlaceholderValues = Record<WelcomeMailPlaceholder, string>;
|
||
|
||
/** Erstes Wort des Anzeigenamens, sonst der Benutzername. */
|
||
export function firstNameOf(displayName: string | null | undefined, username: string): string {
|
||
const first = (displayName ?? '').trim().split(/\s+/)[0];
|
||
return first || username;
|
||
}
|
||
|
||
/**
|
||
* Setzt die Platzhalter in einen REINEN Text ein (Gross-/Kleinschreibung
|
||
* egal). Unbekannte bleiben stehen — gespeicherte Vorlagen enthalten keine,
|
||
* das prueft die API beim Speichern. Das Ergebnis ist weiter reiner Text und
|
||
* wird erst beim Einbau ins HTML escaped.
|
||
*/
|
||
export function applyWelcomeMailPlaceholders(
|
||
text: string,
|
||
values: WelcomeMailPlaceholderValues,
|
||
): string {
|
||
return text.replace(WELCOME_MAIL_PLACEHOLDER_RE, (whole, name: string) =>
|
||
isWelcomeMailPlaceholder(name) ? values[name.toLowerCase() as WelcomeMailPlaceholder] : whole,
|
||
);
|
||
}
|
||
|
||
/** Zeilenenden vereinheitlichen, Leerraum an den Raendern entfernen. */
|
||
function normalizeText(value: string): string {
|
||
return value.replace(/\r\n?/g, '\n').trim();
|
||
}
|
||
|
||
/** Einzeilig (Betreff, Ueberschrift): jeder Umbruch/Leerraum-Lauf wird ein Leerzeichen. */
|
||
function singleLine(value: string): string {
|
||
return value.replace(/\s+/g, ' ').trim();
|
||
}
|
||
|
||
/** Absaetze: Leerzeile trennt, einfacher Umbruch bleibt im Absatz. */
|
||
function paragraphsOf(value: string): string[] {
|
||
const text = normalizeText(value);
|
||
if (!text) return [];
|
||
return text
|
||
.split(/\n[ \t]*\n+/)
|
||
.map((part) => part.trim())
|
||
.filter(Boolean);
|
||
}
|
||
|
||
export function escapeHtml(value: string): string {
|
||
return value
|
||
.replace(/&/g, '&')
|
||
.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 : '';
|
||
}
|
||
|
||
/**
|
||
* Anmeldehinweis je Kontoart als REINER Text (Platzhalter noch nicht
|
||
* ersetzt). Verzeichnis- und lokale Konten nehmen den Text der Vorlage; ist
|
||
* er leer oder fehlt er (aeltere Aufrufer), gilt der Standardtext.
|
||
*/
|
||
function loginHintText(account: WelcomeMailAccount, texts: Partial<WelcomeMailTexts>): string {
|
||
const pick = (field: 'loginHintDirectory' | 'loginHintLocal') => {
|
||
const value = texts[field];
|
||
return typeof value === 'string' && value.trim() ? value : DEFAULT_WELCOME_MAIL_TEXTS[field];
|
||
};
|
||
switch (account.kind) {
|
||
case 'directory':
|
||
return pick('loginHintDirectory');
|
||
case 'local':
|
||
case 'local-example':
|
||
return pick('loginHintLocal');
|
||
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 `<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse:separate;">
|
||
<tr><td align="center" bgcolor="${bg}" style="background-color:${bg};border-radius:6px;mso-padding-alt:14px 30px;">
|
||
<a href="${escapeHtml(href)}" target="_blank" style="display:inline-block;padding:14px 30px;font-family:${FONT};font-size:16px;line-height:20px;font-weight:600;color:${fg};text-decoration:none;border-radius:6px;">${escapeHtml(label)}</a>
|
||
</td></tr></table>`;
|
||
}
|
||
|
||
/** 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 `<td width="9" height="9" ${bg}width:9px;height:9px;font-size:1px;line-height:9px;mso-line-height-rule:exactly;"> </td>`;
|
||
}
|
||
|
||
/** Luecke zwischen Kacheln. */
|
||
const GAP = '<td width="3" style="width:3px;font-size:1px;line-height:1px;"> </td>';
|
||
|
||
/**
|
||
* 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<string | null>) =>
|
||
`<tr>${cells.map((c, i) => (i > 0 ? GAP : '') + tile(c)).join('')}</tr>`;
|
||
const spacer = `<tr><td colspan="5" height="3" style="height:3px;font-size:1px;line-height:3px;mso-line-height-rule:exactly;"> </td></tr>`;
|
||
const olive = '#9c9440';
|
||
return `<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse:separate;">
|
||
<tr><td bgcolor="#111214" style="background-color:#111214;border:1px solid #3a3d44;border-radius:10px;padding:10px 10px 10px 10px;">
|
||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse:collapse;">
|
||
${row([olive, olive, C.yellow])}${spacer}${row([null, olive, null])}${spacer}${row([null, olive, null])}
|
||
</table>
|
||
</td></tr></table>`;
|
||
}
|
||
|
||
/**
|
||
* 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:`, 600x40), der ins Weiss der Karte auslaeuft. Die Zelle um das
|
||
* Bild traegt die dunkle Kopffarbe (zweite Rueckmeldung des Nutzers: sein
|
||
* Outlook zeigt das CID-Bild nicht, vorher stand dort eine "riesengrosse
|
||
* weisse Luecke"): fehlt das Bild, wirkt der Kopf nur etwas hoeher.
|
||
*/
|
||
function headerRow(src: string | null): string {
|
||
const bar = `<tr><td bgcolor="${C.header}" style="background-color:${C.header};border-radius:12px 12px 0 0;padding:22px 32px 14px;">
|
||
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr>
|
||
<td valign="middle" style="padding:0 14px 0 0;">${logoMark()}</td>
|
||
<td valign="middle" style="font-family:${FONT};font-size:26px;line-height:32px;font-weight:700;color:#ffffff;letter-spacing:-0.5px;">Tessera</td>
|
||
</tr></table>
|
||
</td></tr>`;
|
||
const wave = src
|
||
? `<tr><td bgcolor="${C.header}" style="background-color:${C.header};line-height:0;font-size:0;">
|
||
<img src="${escapeHtml(src)}" width="600" height="40" alt="" style="display:block;width:100%;max-width:600px;height:auto;border:0;outline:none;text-decoration:none;">
|
||
</td></tr>`
|
||
: `<tr><td bgcolor="${C.header}" height="4" style="background-color:${C.header};height:4px;font-size:1px;line-height:4px;border-bottom:3px solid ${C.yellow};"> </td></tr>`;
|
||
return bar + wave;
|
||
}
|
||
|
||
/**
|
||
* Baut die Willkommensmail. Die sechs Texte (Betreff, Ueberschrift,
|
||
* Einleitung, Anmeldehinweis je Kontoart, Abschluss) kommen aus der eigenen
|
||
* Vorlage des Mandanten, sonst
|
||
* aus `DEFAULT_WELCOME_MAIL_TEXTS`. Sie sind REINER Text: Platzhalter werden
|
||
* auf dem Text ersetzt, erst danach wird alles escaped — HTML aus der
|
||
* Vorlage oder aus einem Benutzerwert erscheint als Text, nie als Markup.
|
||
* Kopf, Kasten Adresse/Benutzername, Knopf "Passwort festlegen" mit
|
||
* Gueltigkeitshinweis, Knopf "Zu Tessera" und Fusszeile sind feste Bausteine
|
||
* und nicht Teil der Vorlage.
|
||
*/
|
||
export function renderWelcomeMail(input: WelcomeMailInput): RenderedWelcomeMail {
|
||
const base = safeUrl(input.appUrl.replace(/\/+$/, ''));
|
||
const loginUrl = `${base}/login`;
|
||
const texts = input.texts ?? DEFAULT_WELCOME_MAIL_TEXTS;
|
||
|
||
const values: WelcomeMailPlaceholderValues = {
|
||
name: input.name,
|
||
vorname: firstNameOf(input.name, input.username),
|
||
benutzername: input.username,
|
||
email: input.email,
|
||
adresse: base,
|
||
firma: input.tenantName,
|
||
};
|
||
const fill = (value: string) => applyWelcomeMailPlaceholders(value, values);
|
||
|
||
const subject = singleLine(fill(texts.subject)) || WELCOME_MAIL_SUBJECT;
|
||
const heading = singleLine(fill(texts.heading));
|
||
const introParagraphs = paragraphsOf(fill(texts.intro));
|
||
const hintParagraphs = paragraphsOf(fill(loginHintText(input.account, texts)));
|
||
const closingParagraphs = paragraphsOf(fill(texts.closing));
|
||
const notice = input.notice ? singleLine(input.notice) : '';
|
||
|
||
// ── Text-Alternative ───────────────────────────────────────────────────
|
||
const textLines: string[] = [];
|
||
if (notice) textLines.push(`[${notice}]`, '');
|
||
if (heading) textLines.push(heading, '');
|
||
for (const para of introParagraphs) textLines.push(para, '');
|
||
textLines.push('Ihre Zugangsdaten', `Adresse: ${base}`, `Benutzername: ${input.username}`);
|
||
for (const para of hintParagraphs) textLines.push('', para);
|
||
if (input.account.kind === 'local') {
|
||
textLines.push(
|
||
'',
|
||
'Passwort festlegen:',
|
||
input.account.setPasswordUrl,
|
||
validityText(input.account.validHours),
|
||
);
|
||
} else if (input.account.kind === 'local-example') {
|
||
textLines.push('', 'Passwort festlegen:', loginUrl, EXAMPLE_LINK_NOTE);
|
||
}
|
||
textLines.push('', 'Zu Tessera:', loginUrl);
|
||
for (const para of closingParagraphs) textLines.push('', para);
|
||
textLines.push('', '--', FOOTER);
|
||
const text = textLines.join('\n');
|
||
|
||
// ── HTML ───────────────────────────────────────────────────────────────
|
||
const username = escapeHtml(input.username);
|
||
const baseHtml = escapeHtml(base);
|
||
|
||
const p = (content: string, extra = '') =>
|
||
`<p style="margin:0 0 16px;font-family:${FONT};font-size:16px;line-height:25px;color:${C.body};${extra}">${content}</p>`;
|
||
|
||
/** Reiner Text -> escaped, einfache Umbrueche als <br>. */
|
||
const para = (value: string) => escapeHtml(value).replace(/\n/g, '<br>');
|
||
|
||
const label = (content: string) =>
|
||
`<div style="font-family:${FONT};font-size:12px;line-height:16px;font-weight:600;letter-spacing:0.06em;text-transform:uppercase;color:${C.muted};">${content}</div>`;
|
||
|
||
let accountBlock = hintParagraphs
|
||
.map((part, i) => p(para(part), i === 0 ? 'margin:24px 0 16px;' : ''))
|
||
.join('\n');
|
||
if (input.account.kind === 'local') {
|
||
const setUrl = safeUrl(input.account.setPasswordUrl);
|
||
accountBlock += `${button(setUrl, 'Passwort festlegen', C.ink, '#ffffff')}
|
||
<p style="margin:12px 0 0;font-family:${FONT};font-size:13px;line-height:20px;color:${C.muted};">${escapeHtml(validityText(input.account.validHours))}</p>`;
|
||
} else if (input.account.kind === 'local-example') {
|
||
accountBlock += `${button(loginUrl, 'Passwort festlegen', C.ink, '#ffffff')}
|
||
<p style="margin:12px 0 0;font-family:${FONT};font-size:13px;line-height:20px;color:${C.muted};">${escapeHtml(EXAMPLE_LINK_NOTE)}</p>`;
|
||
}
|
||
|
||
const noticeRow = notice
|
||
? `<tr><td style="padding:0 0 12px;"><table role="presentation" width="100%" border="0" cellpadding="0" cellspacing="0"><tr><td bgcolor="#fff8c2" style="background-color:#fff8c2;border:1px solid #e6d74c;border-radius:8px;padding:10px 16px;font-family:${FONT};font-size:13px;line-height:20px;color:${C.ink};">${escapeHtml(notice)}</td></tr></table></td></tr>
|
||
`
|
||
: '';
|
||
|
||
const headingHtml = heading
|
||
? `<h1 style="margin:0 0 16px;font-family:${FONT};font-size:24px;line-height:32px;font-weight:700;color:${C.ink};">${escapeHtml(heading)}</h1>
|
||
`
|
||
: '';
|
||
const introHtml = introParagraphs.map((part) => p(para(part))).join('\n');
|
||
|
||
const closingHtml = closingParagraphs.length
|
||
? `<table role="presentation" width="100%" border="0" cellpadding="0" cellspacing="0"><tr><td style="border-top:1px solid ${C.line};padding-top:20px;">
|
||
${closingParagraphs
|
||
.map((part, i) =>
|
||
p(
|
||
para(part),
|
||
`font-size:14px;line-height:22px;margin:0${i < closingParagraphs.length - 1 ? ' 0 12px' : ''};`,
|
||
),
|
||
)
|
||
.join('\n')}
|
||
</td></tr></table>`
|
||
: '';
|
||
|
||
const html = `<!DOCTYPE html>
|
||
<html lang="de" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||
<meta name="color-scheme" content="light">
|
||
<meta name="supported-color-schemes" content="light">
|
||
<title>${escapeHtml(subject)}</title>
|
||
<!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]-->
|
||
<style>
|
||
a { color: ${C.link}; }
|
||
@media only screen and (max-width: 620px) {
|
||
.tsr-pad { padding-left: 24px !important; padding-right: 24px !important; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body style="margin:0;padding:0;background-color:${C.page};-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
|
||
<div style="display:none;max-height:0;overflow:hidden;mso-hide:all;font-size:1px;line-height:1px;color:${C.page};">Ihr Zugang zu Tessera ist eingerichtet – hier finden Sie Adresse und Benutzername.</div>
|
||
<table role="presentation" width="100%" border="0" cellpadding="0" cellspacing="0" bgcolor="${C.page}" style="background-color:${C.page};">
|
||
<tr><td align="center" style="padding:32px 12px;">
|
||
<!--[if mso]><table role="presentation" width="600" border="0" cellpadding="0" cellspacing="0"><tr><td><![endif]-->
|
||
<table role="presentation" width="100%" border="0" cellpadding="0" cellspacing="0" style="width:100%;max-width:600px;border-collapse:separate;">
|
||
${noticeRow}${headerRow(input.headerImageSrc)}
|
||
<tr><td class="tsr-pad" bgcolor="${C.card}" style="background-color:${C.card};padding:24px 40px 12px;">
|
||
${headingHtml}${introHtml}
|
||
<table role="presentation" width="100%" border="0" cellpadding="0" cellspacing="0" style="border-collapse:separate;margin:8px 0 0;">
|
||
<tr><td bgcolor="${C.well}" style="background-color:${C.well};border:1px solid ${C.line};border-left:4px solid ${C.yellow};border-radius:8px;padding:18px 22px;">
|
||
${label('Adresse')}
|
||
<div style="margin:4px 0 14px;font-family:${FONT};font-size:16px;line-height:24px;font-weight:600;"><a href="${escapeHtml(loginUrl)}" target="_blank" style="color:${C.link};text-decoration:none;">${baseHtml}</a></div>
|
||
${label('Benutzername')}
|
||
<div style="margin:4px 0 0;font-family:Consolas,'SF Mono',Menlo,'Courier New',monospace;font-size:16px;line-height:24px;font-weight:600;color:${C.ink};">${username}</div>
|
||
</td></tr></table>
|
||
${accountBlock}
|
||
</td></tr>
|
||
<tr><td class="tsr-pad" bgcolor="${C.card}" align="left" style="background-color:${C.card};padding:20px 40px 8px;">
|
||
${button(loginUrl, 'Zu Tessera', C.yellow, C.ink)}
|
||
</td></tr>
|
||
<tr><td class="tsr-pad" bgcolor="${C.card}" style="background-color:${C.card};padding:20px 40px 36px;border-radius:0 0 12px 12px;">
|
||
${closingHtml}
|
||
</td></tr>
|
||
<tr><td align="center" style="padding:20px 24px 0;font-family:${FONT};font-size:12px;line-height:18px;color:${C.muted};">${escapeHtml(FOOTER)}</td></tr>
|
||
</table>
|
||
<!--[if mso]></td></tr></table><![endif]-->
|
||
</td></tr>
|
||
</table>
|
||
</body>
|
||
</html>`;
|
||
|
||
return { subject, text, html };
|
||
}
|