Files
Webklar-Kundenbereich/server/services/mailer.js
T
ClaudeandClaude Opus 5.5 18c3cdf3e5 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>
2026-09-27 14:37:50 +00:00

121 lines
4.4 KiB
JavaScript

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
function getTransporter() {
if (!config.smtp.host || !config.smtp.user) return null
if (!transporter) {
transporter = nodemailer.createTransport({
host: config.smtp.host,
port: config.smtp.port,
secure: config.smtp.secure,
// Port 587: ohne STARTTLS nicht senden (Hetzner erlaubt die Anmeldung auch unverschluesselt)
requireTLS: !config.smtp.secure,
auth: { user: config.smtp.user, pass: config.smtp.pass },
})
}
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
/**
* Benachrichtigt die Admins per E-Mail ueber eine neue Portal-Chatnachricht.
* Fehler werden nur geloggt - der Chat darf am Mailversand nie scheitern.
*/
export async function sendChatNotification(customer, text) {
try {
const mailer = getTransporter()
if (!mailer || !config.chatNotify.emails.length) return { sent: false, reason: 'nicht konfiguriert' }
const now = Date.now()
const last = lastNotifyByCustomer.get(customer.$id) || 0
const throttleMs = config.chatNotify.throttleMin * 60 * 1000
if (now - last < throttleMs) return { sent: false, reason: 'throttled' }
lastNotifyByCustomer.set(customer.$id, now)
const mail = render({
template: 'T01',
values: { kunde_name: customer.name || customer.email || 'Kunde', nachricht: text },
})
await mailer.sendMail({
from: `"${TEAM_SENDER}" <${config.smtp.from}>`,
to: config.chatNotify.emails.join(', '),
subject: mail.subject,
text: mail.text,
html: mail.html,
})
return { sent: true }
} catch (err) {
console.error('[mailer] Chat-Benachrichtigung fehlgeschlagen:', err.message)
return { sent: false, reason: err.message }
}
}
// ---------------------------------------------------------------------------
// Wartungs-Abo: Vertrags-/Kuendigungs-/Widerrufsbestaetigungen
// ---------------------------------------------------------------------------
/**
* 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: `"${TEAM_SENDER}" <${config.smtp.from}>`,
to: config.chatNotify.emails.join(', '),
subject: mail.subject,
text: mail.text,
html: mail.html,
})
return { sent: true }
} catch (err) {
console.error('[mailer] Admin-Hinweis fehlgeschlagen:', err.message)
return { sent: false, reason: err.message }
}
}
/**
* 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, { attachments = [], ...message }) {
const mailer = getTransporter()
if (!mailer) throw new Error('SMTP nicht konfiguriert')
const mail = render(message)
return mailer.sendMail({
from: `"${CUSTOMER_SENDER}" <${config.smtp.from}>`,
replyTo: config.smtp.replyTo || undefined,
to,
bcc: config.chatNotify.emails.join(', ') || undefined,
subject: mail.subject,
text: mail.text,
html: mail.html,
attachments,
})
}