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 let testTransport = null /** * Nur für Tests (F25): alle Mails über diesen Transport statt per SMTP, z. B. mailSink() aus * test/support/mailSink.js (nodemailer jsonTransport, gesendete Mails gesammelt). null schaltet zurück. * Gerendert wird wie im Betrieb; so lassen sich Vorlagen, Blöcke und Anhänge in Routentests prüfen. */ export function setMailTransportForTests(transport) { testTransport = transport || null } function getTransporter() { if (testTransport) return testTransport 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, }) } /** * Mail an eine einzelne Person im Team aus einer T-Vorlage (z. B. T15 Push-Zeitmail), ohne BCC. * Wirft bei fehlendem SMTP oder Versandfehler - der Aufrufer entscheidet, ob das schlimm ist. * @param {string} to * @param {{ template: string, values?: object, blocks?: object }} message */ export async function sendStaffMail(to, message) { const mailer = getTransporter() if (!mailer) throw new Error('SMTP nicht konfiguriert') const mail = render(message) return mailer.sendMail({ from: `"${TEAM_SENDER}" <${config.smtp.from}>`, to, subject: mail.subject, text: mail.text, html: mail.html, }) }