Files
Webklar-Kundenbereich/server/services/mailer.js
T
ClaudeandClaude Opus 5.5 60baeb3271 Mailversand: Antwortadresse, Absendername WEBklar, nur mit STARTTLS
- SMTP_REPLY_TO (z. B. support@webklar.com) setzt Reply-To auf allen
  Kundenmails, damit Antworten nicht im Versandpostfach (noreply@) landen
- Absendername einheitlich "WEBklar" (Kunden) bzw. "WEBklar Kundenbereich"
  (interne Hinweise) statt "Webklar"/"Webklar Kundenportal"
- requireTLS auf Port 587: der neue Mailserver (Hetzner) nimmt die
  Anmeldung auch unverschluesselt an; ohne STARTTLS wird jetzt nicht gesendet

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 12:12:37 +00:00

100 lines
3.5 KiB
JavaScript

import nodemailer from 'nodemailer'
import { config } from '../config.js'
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
}
// 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 name = customer.name || customer.email || 'Kunde'
await mailer.sendMail({
from: `"WEBklar Kundenbereich" <${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'),
})
return { sent: true }
} catch (err) {
console.error('[mailer] Chat-Benachrichtigung fehlgeschlagen:', err.message)
return { sent: false, reason: err.message }
}
}
// ---------------------------------------------------------------------------
// Wartungs-Abo: Vertrags-/Kuendigungs-/Widerrufsbestaetigungen
// ---------------------------------------------------------------------------
/** Mail an die Webklar-Admins (Abo-Ereignisse). Fehler werden nur geloggt. */
export async function sendAdminNotice(subject, lines) {
try {
const mailer = getTransporter()
if (!mailer || !config.chatNotify.emails.length) return { sent: false, reason: 'nicht konfiguriert' }
await mailer.sendMail({
from: `"WEBklar Kundenbereich" <${config.smtp.from}>`,
to: config.chatNotify.emails.join(', '),
subject: `[Abo] ${subject}`,
text: lines.join('\n'),
})
return { sent: true }
} catch (err) {
console.error('[mailer] Admin-Hinweis fehlgeschlagen:', err.message)
return { sent: false, reason: err.message }
}
}
/**
* Mail an Kunden. `attachments` fuer die Vertragsbestaetigung auf dauerhaftem
* Datentraeger (§ 312f BGB): Bedingungen als HTML-Anhang.
* Ergebnis: nodemailer-Info (messageId, Antwort des Mailservers, abgelehnte Empfaenger).
*/
export async function sendCustomerMail(to, subject, lines, attachments = []) {
const mailer = getTransporter()
if (!mailer) throw new Error('SMTP nicht konfiguriert')
return mailer.sendMail({
from: `"WEBklar" <${config.smtp.from}>`,
replyTo: config.smtp.replyTo || undefined,
to,
bcc: config.chatNotify.emails.join(', ') || undefined,
subject,
text: lines.join('\n'),
attachments,
})
}