Automatische Mails im neuen Design (HTML mit Textfassung)

Alle 22 automatischen Mails gehen jetzt als HTML nach den Vorlagen aus
server/mail-templates raus, dazu eine aus dem HTML erzeugte Textfassung
(multipart/alternative). Werte werden HTML-sicher eingesetzt, optionale
Blöcke (WENN/ENDE) je nach Fall ein- oder ausgeblendet.

- Neu: services/mailTemplates.js (Vorlagen lesen, Platzhalter, Blöcke,
  Textfassung); mailer.js nimmt { template, values, blocks }.
- K11 "Passwort geändert" war nicht im Design-Export: aus den Bausteinen
  von K02 zusammengesetzt.
- T09 (Widerruf, intern): Fälle "kein Abo gefunden" und "Stripe-Kündigung
  fehlgeschlagen" ergänzt; bisher stand dort immer "auf Kündigung gesetzt".
- Einheitlich: Datum TT.MM.JJJJ, Beträge deutsch (T04 "50,00 €"),
  "WEBklar" statt "Webklar", Anrede mit Kundennamen, Team-Absender
  "WEBklar Kundenportal", Betreff T05 "Kündigung (Stripe)".
- Logo für die Mails: public/webklar-logo.png (192 px, aus dem Design-SVG).

Tests: 52 neue (jede Vorlage: Kopf, Platzhalter, Blöcke, Rendern,
Escaping, Textfassung), npm test 97/97 auf Node 20.19.5; alle 40 Auslöser
einmal durch den Server geschickt (Appwrite-Mock, Stripe-Double,
SMTP-Falle mit STARTTLS).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-09-27 14:37:50 +00:00
1 parent e3daffffa3
commit 18c3cdf3e5
11 files changed
+657 -316

No files matched your search

+42 -21
View File
@@ -1,5 +1,10 @@
import nodemailer from 'nodemailer'
import { config } from '../config.js'
import { renderMail } from './mailTemplates.js'
// Absendernamen wie im Design: Kundenmails "WEBklar", Hinweise ans Team "WEBklar Kundenportal"
const CUSTOMER_SENDER = 'WEBklar'
const TEAM_SENDER = 'WEBklar Kundenportal'
let transporter = null
@@ -18,6 +23,13 @@ function getTransporter() {
return transporter
}
/** Vorlage rendern (server/mail-templates); Unstimmigkeiten landen im Log, die Mail geht trotzdem raus. */
function render({ template, values = {}, blocks = {} }) {
const mail = renderMail(template, values, blocks)
for (const problem of mail.problems) console.warn(`[mailer] Vorlage ${template}: ${problem}`)
return mail
}
// Throttle: pro Kunde maximal eine Benachrichtigung je Zeitfenster,
// damit ein aktiver Chat nicht pro Nachricht eine Mail ausloest.
const lastNotifyByCustomer = new Map() // customerId -> timestamp
@@ -37,18 +49,16 @@ export async function sendChatNotification(customer, text) {
if (now - last < throttleMs) return { sent: false, reason: 'throttled' }
lastNotifyByCustomer.set(customer.$id, now)
const name = customer.name || customer.email || 'Kunde'
const mail = render({
template: 'T01',
values: { kunde_name: customer.name || customer.email || 'Kunde', nachricht: text },
})
await mailer.sendMail({
from: `"WEBklar Kundenbereich" <${config.smtp.from}>`,
from: `"${TEAM_SENDER}" <${config.smtp.from}>`,
to: config.chatNotify.emails.join(', '),
subject: `Neue Portalnachricht von ${name}`,
text: [
`${name} hat im Kundenportal geschrieben:`,
'',
text,
'',
'Antworten: https://project.webklar.com/admin.html (Tab Chat)',
].join('\n'),
subject: mail.subject,
text: mail.text,
html: mail.html,
})
return { sent: true }
} catch (err) {
@@ -61,16 +71,22 @@ export async function sendChatNotification(customer, text) {
// Wartungs-Abo: Vertrags-/Kuendigungs-/Widerrufsbestaetigungen
// ---------------------------------------------------------------------------
/** Mail an die Webklar-Admins (Abo-Ereignisse). Fehler werden nur geloggt. */
export async function sendAdminNotice(subject, lines) {
/**
* Hinweis an das WEBklar-Team (Abo-Ereignisse) aus einer T-Vorlage; der Betreff steht in der Vorlage.
* Fehler werden nur geloggt.
* @param {{ template: string, values?: object, blocks?: object }} message
*/
export async function sendAdminNotice(message) {
try {
const mailer = getTransporter()
if (!mailer || !config.chatNotify.emails.length) return { sent: false, reason: 'nicht konfiguriert' }
const mail = render(message)
await mailer.sendMail({
from: `"WEBklar Kundenbereich" <${config.smtp.from}>`,
from: `"${TEAM_SENDER}" <${config.smtp.from}>`,
to: config.chatNotify.emails.join(', '),
subject: `[Abo] ${subject}`,
text: lines.join('\n'),
subject: mail.subject,
text: mail.text,
html: mail.html,
})
return { sent: true }
} catch (err) {
@@ -80,20 +96,25 @@ export async function sendAdminNotice(subject, lines) {
}
/**
* Mail an Kunden. `attachments` fuer die Vertragsbestaetigung auf dauerhaftem
* Datentraeger (§ 312f BGB): Bedingungen als HTML-Anhang.
* Mail an Kunden aus einer K-Vorlage (HTML mit Textfassung), Kopie per BCC ans Team.
* `attachments` fuer die Vertragsbestaetigung auf dauerhaftem Datentraeger (§ 312f BGB):
* Vertragsunterlagen als PDF.
* Ergebnis: nodemailer-Info (messageId, Antwort des Mailservers, abgelehnte Empfaenger).
* @param {string} to
* @param {{ template: string, values?: object, blocks?: object, attachments?: object[] }} message
*/
export async function sendCustomerMail(to, subject, lines, attachments = []) {
export async function sendCustomerMail(to, { attachments = [], ...message }) {
const mailer = getTransporter()
if (!mailer) throw new Error('SMTP nicht konfiguriert')
const mail = render(message)
return mailer.sendMail({
from: `"WEBklar" <${config.smtp.from}>`,
from: `"${CUSTOMER_SENDER}" <${config.smtp.from}>`,
replyTo: config.smtp.replyTo || undefined,
to,
bcc: config.chatNotify.emails.join(', ') || undefined,
subject,
text: lines.join('\n'),
subject: mail.subject,
text: mail.text,
html: mail.html,
attachments,
})
}