feat: SMTP-Stack mit konfigurierbaren Mailvorlagen
Bisher gab es nur zwei fest im Code verdrahtete E-Mails (Einladung,
Passwort-Reset) und die SMTP-Verbindung kam ausschließlich aus
Umgebungsvariablen. Jetzt:
- SMTP-Verbindung unter Einstellungen > E-Mail konfigurierbar (Host,
Port, Verschlüsselung, Zugangsdaten verschlüsselt gespeichert,
Absender), mit Testmail-Versand. Die Mail-Logik wandert dafür nach
@kc/platform/mail, damit API und Worker sie gemeinsam nutzen.
- Mailvorlagen-Verwaltung: jedes Ereignis hat eine feste "Definition"
(Name, Auslöser, verfügbare Platzhalter/Daten), Betreff/Text sind
editierbar, je Vorlage einzeln aktivierbar, mit Testversand anhand
von Beispieldaten. Unbekannte Platzhalter werden beim Speichern
abgelehnt.
- Neue Ereignisse verdrahtet: Ticket erstellt (Bestätigung an Kunde),
Personal antwortet auf Ticket (Benachrichtigung an Kunde), Rechnung
ausgestellt, sowie die zuvor zurückgestellte Funktion "Panel-
Zugangsdaten per E-Mail senden" nach einem Passwort-Reset. Diese
laufen über die Jobqueue (mail.template), analog zu discord.notify.
- Versandprotokoll (letzte 100 Versuche, ohne Inhalte) einsehbar unter
Einstellungen > E-Mail.
- Migration 022, Rechte email.read (Admin+) / email.write (Superadmin).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-28 12:53:29 +02:00
import nodemailer from 'nodemailer' ;
import { one , run } from './db.js' ;
import { decrypt } from './crypto.js' ;
2026-09-28 23:00:31 +02:00
import { config } from './config.js' ;
feat: SMTP-Stack mit konfigurierbaren Mailvorlagen
Bisher gab es nur zwei fest im Code verdrahtete E-Mails (Einladung,
Passwort-Reset) und die SMTP-Verbindung kam ausschließlich aus
Umgebungsvariablen. Jetzt:
- SMTP-Verbindung unter Einstellungen > E-Mail konfigurierbar (Host,
Port, Verschlüsselung, Zugangsdaten verschlüsselt gespeichert,
Absender), mit Testmail-Versand. Die Mail-Logik wandert dafür nach
@kc/platform/mail, damit API und Worker sie gemeinsam nutzen.
- Mailvorlagen-Verwaltung: jedes Ereignis hat eine feste "Definition"
(Name, Auslöser, verfügbare Platzhalter/Daten), Betreff/Text sind
editierbar, je Vorlage einzeln aktivierbar, mit Testversand anhand
von Beispieldaten. Unbekannte Platzhalter werden beim Speichern
abgelehnt.
- Neue Ereignisse verdrahtet: Ticket erstellt (Bestätigung an Kunde),
Personal antwortet auf Ticket (Benachrichtigung an Kunde), Rechnung
ausgestellt, sowie die zuvor zurückgestellte Funktion "Panel-
Zugangsdaten per E-Mail senden" nach einem Passwort-Reset. Diese
laufen über die Jobqueue (mail.template), analog zu discord.notify.
- Versandprotokoll (letzte 100 Versuche, ohne Inhalte) einsehbar unter
Einstellungen > E-Mail.
- Migration 022, Rechte email.read (Admin+) / email.write (Superadmin).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-28 12:53:29 +02:00
export interface SmtpConfig { host : string ; port : number ; secureMode : 'none' | 'starttls' | 'tls' ; username : string | null ; fromName : string | null ; fromEmail : string }
2026-09-28 13:16:42 +02:00
export type MailCategory = 'kundencenter' | 'stoerungsticket' | 'rechnungen' ;
feat: SMTP-Stack mit konfigurierbaren Mailvorlagen
Bisher gab es nur zwei fest im Code verdrahtete E-Mails (Einladung,
Passwort-Reset) und die SMTP-Verbindung kam ausschließlich aus
Umgebungsvariablen. Jetzt:
- SMTP-Verbindung unter Einstellungen > E-Mail konfigurierbar (Host,
Port, Verschlüsselung, Zugangsdaten verschlüsselt gespeichert,
Absender), mit Testmail-Versand. Die Mail-Logik wandert dafür nach
@kc/platform/mail, damit API und Worker sie gemeinsam nutzen.
- Mailvorlagen-Verwaltung: jedes Ereignis hat eine feste "Definition"
(Name, Auslöser, verfügbare Platzhalter/Daten), Betreff/Text sind
editierbar, je Vorlage einzeln aktivierbar, mit Testversand anhand
von Beispieldaten. Unbekannte Platzhalter werden beim Speichern
abgelehnt.
- Neue Ereignisse verdrahtet: Ticket erstellt (Bestätigung an Kunde),
Personal antwortet auf Ticket (Benachrichtigung an Kunde), Rechnung
ausgestellt, sowie die zuvor zurückgestellte Funktion "Panel-
Zugangsdaten per E-Mail senden" nach einem Passwort-Reset. Diese
laufen über die Jobqueue (mail.template), analog zu discord.notify.
- Versandprotokoll (letzte 100 Versuche, ohne Inhalte) einsehbar unter
Einstellungen > E-Mail.
- Migration 022, Rechte email.read (Admin+) / email.write (Superadmin).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-28 12:53:29 +02:00
/** Lädt die SMTP-Verbindung aus der Datenbank (Einstellungen > E-Mail). Kein Host/Absender gesetzt = nicht eingerichtet. */
export async function loadSmtpConfig ( ) : Promise < SmtpConfig | null > {
const s = await one ( 'SELECT * FROM smtp_settings WHERE id = 1' ) ;
if ( ! s ? . host || ! s ? . from_email ) return null ;
return { host : s.host , port : s.port , secureMode : s.secure_mode , username : s.username , fromName : s.from_name , fromEmail : s.from_email } ;
}
2026-09-28 13:16:42 +02:00
/** Absenderprofil je Bereich (Kundencenter/Störungsticket/Rechnungen); leere Felder fallen auf den globalen SMTP-Absender zurück. */
async function fromFor ( cfg : SmtpConfig , category? : MailCategory ) : Promise < { name : string | null ; email : string } > {
if ( ! category ) return { name : cfg.fromName , email : cfg.fromEmail } ;
const p = await one ( 'SELECT from_name, from_email FROM mail_sender_profiles WHERE category = ?' , [ category ] ) ;
return { name : p?.from_name ? ? cfg . fromName , email : p?.from_email ? ? cfg . fromEmail } ;
}
feat: SMTP-Stack mit konfigurierbaren Mailvorlagen
Bisher gab es nur zwei fest im Code verdrahtete E-Mails (Einladung,
Passwort-Reset) und die SMTP-Verbindung kam ausschließlich aus
Umgebungsvariablen. Jetzt:
- SMTP-Verbindung unter Einstellungen > E-Mail konfigurierbar (Host,
Port, Verschlüsselung, Zugangsdaten verschlüsselt gespeichert,
Absender), mit Testmail-Versand. Die Mail-Logik wandert dafür nach
@kc/platform/mail, damit API und Worker sie gemeinsam nutzen.
- Mailvorlagen-Verwaltung: jedes Ereignis hat eine feste "Definition"
(Name, Auslöser, verfügbare Platzhalter/Daten), Betreff/Text sind
editierbar, je Vorlage einzeln aktivierbar, mit Testversand anhand
von Beispieldaten. Unbekannte Platzhalter werden beim Speichern
abgelehnt.
- Neue Ereignisse verdrahtet: Ticket erstellt (Bestätigung an Kunde),
Personal antwortet auf Ticket (Benachrichtigung an Kunde), Rechnung
ausgestellt, sowie die zuvor zurückgestellte Funktion "Panel-
Zugangsdaten per E-Mail senden" nach einem Passwort-Reset. Diese
laufen über die Jobqueue (mail.template), analog zu discord.notify.
- Versandprotokoll (letzte 100 Versuche, ohne Inhalte) einsehbar unter
Einstellungen > E-Mail.
- Migration 022, Rechte email.read (Admin+) / email.write (Superadmin).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-28 12:53:29 +02:00
async function smtpPassword ( ) : Promise < string | null > {
const s = await one ( 'SELECT secrets_enc FROM smtp_settings WHERE id = 1' ) ;
return s ? . secrets_enc ? ( JSON . parse ( decrypt ( s . secrets_enc ) ) . password ? ? null ) : null ;
}
async function transportFor ( cfg : SmtpConfig ) {
return nodemailer . createTransport ( {
host : cfg.host , port : cfg.port , secure : cfg.secureMode === 'tls' , requireTLS : cfg.secureMode === 'starttls' ,
auth : cfg.username ? { user : cfg.username , pass : ( await smtpPassword ( ) ) ? ? '' } : undefined ,
} ) ;
}
/** Versand mit Protokoll. Ohne SMTP-Konfiguration wird nur "not_configured" protokolliert (Inhalt bewusst nicht gespeichert). */
2026-09-28 23:00:31 +02:00
export async function sendMail ( to : string , template : string , subject : string , text : string , opts : { category? : MailCategory ; html? : string } = { } ) : Promise < 'sent' | 'failed' | 'not_configured' > {
feat: SMTP-Stack mit konfigurierbaren Mailvorlagen
Bisher gab es nur zwei fest im Code verdrahtete E-Mails (Einladung,
Passwort-Reset) und die SMTP-Verbindung kam ausschließlich aus
Umgebungsvariablen. Jetzt:
- SMTP-Verbindung unter Einstellungen > E-Mail konfigurierbar (Host,
Port, Verschlüsselung, Zugangsdaten verschlüsselt gespeichert,
Absender), mit Testmail-Versand. Die Mail-Logik wandert dafür nach
@kc/platform/mail, damit API und Worker sie gemeinsam nutzen.
- Mailvorlagen-Verwaltung: jedes Ereignis hat eine feste "Definition"
(Name, Auslöser, verfügbare Platzhalter/Daten), Betreff/Text sind
editierbar, je Vorlage einzeln aktivierbar, mit Testversand anhand
von Beispieldaten. Unbekannte Platzhalter werden beim Speichern
abgelehnt.
- Neue Ereignisse verdrahtet: Ticket erstellt (Bestätigung an Kunde),
Personal antwortet auf Ticket (Benachrichtigung an Kunde), Rechnung
ausgestellt, sowie die zuvor zurückgestellte Funktion "Panel-
Zugangsdaten per E-Mail senden" nach einem Passwort-Reset. Diese
laufen über die Jobqueue (mail.template), analog zu discord.notify.
- Versandprotokoll (letzte 100 Versuche, ohne Inhalte) einsehbar unter
Einstellungen > E-Mail.
- Migration 022, Rechte email.read (Admin+) / email.write (Superadmin).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-28 12:53:29 +02:00
const cfg = await loadSmtpConfig ( ) ;
if ( ! cfg ) {
await run ( 'INSERT INTO mail_log (to_email, template, status) VALUES (?,?,?)' , [ to , template , 'not_configured' ] ) ;
return 'not_configured' ;
}
try {
const transport = await transportFor ( cfg ) ;
2026-09-28 23:00:31 +02:00
const from = await fromFor ( cfg , opts . category ) ;
await transport . sendMail ( { from : from . name ? ` " ${ from . name . replace ( /"/g , "'" ) } " < ${ from . email } > ` : from . email , to , subject , text , html : opts.html } ) ;
feat: SMTP-Stack mit konfigurierbaren Mailvorlagen
Bisher gab es nur zwei fest im Code verdrahtete E-Mails (Einladung,
Passwort-Reset) und die SMTP-Verbindung kam ausschließlich aus
Umgebungsvariablen. Jetzt:
- SMTP-Verbindung unter Einstellungen > E-Mail konfigurierbar (Host,
Port, Verschlüsselung, Zugangsdaten verschlüsselt gespeichert,
Absender), mit Testmail-Versand. Die Mail-Logik wandert dafür nach
@kc/platform/mail, damit API und Worker sie gemeinsam nutzen.
- Mailvorlagen-Verwaltung: jedes Ereignis hat eine feste "Definition"
(Name, Auslöser, verfügbare Platzhalter/Daten), Betreff/Text sind
editierbar, je Vorlage einzeln aktivierbar, mit Testversand anhand
von Beispieldaten. Unbekannte Platzhalter werden beim Speichern
abgelehnt.
- Neue Ereignisse verdrahtet: Ticket erstellt (Bestätigung an Kunde),
Personal antwortet auf Ticket (Benachrichtigung an Kunde), Rechnung
ausgestellt, sowie die zuvor zurückgestellte Funktion "Panel-
Zugangsdaten per E-Mail senden" nach einem Passwort-Reset. Diese
laufen über die Jobqueue (mail.template), analog zu discord.notify.
- Versandprotokoll (letzte 100 Versuche, ohne Inhalte) einsehbar unter
Einstellungen > E-Mail.
- Migration 022, Rechte email.read (Admin+) / email.write (Superadmin).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-28 12:53:29 +02:00
await run ( 'INSERT INTO mail_log (to_email, template, status) VALUES (?,?,?)' , [ to , template , 'sent' ] ) ;
return 'sent' ;
} catch ( e ) {
await run ( 'INSERT INTO mail_log (to_email, template, status, error) VALUES (?,?,?,?)' , [ to , template , 'failed' , String ( ( e as Error ) . message ) . slice ( 0 , 300 ) ] ) ;
return 'failed' ;
}
}
/** Ersetzt {{platzhalter}} durch die übergebenen Werte; unbekannte Platzhalter bleiben unverändert stehen (fällt in der Vorschau auf). */
export function renderTemplateText ( tpl : string , vars : Record < string , string > ) : string {
return tpl . replace ( /\{\{(\w+)\}\}/g , ( m , k : string ) = > ( k in vars ? vars [ k ] ! : m ) ) ;
}
2026-09-28 23:00:31 +02:00
const escapeHtml = ( s : string ) : string = > s . replace ( /&/g , '&' ) . replace ( /</g , '<' ) . replace ( />/g , '>' ) . replace ( /"/g , '"' ) ;
/** Beschriftung des Schaltflächen-Buttons, je nach Variablenname (endet auf "Link"). */
const LINK_LABEL : Record < string , string > = { inviteLink : 'Zugang einrichten' , resetLink : 'Passwort zurücksetzen' , ticketLink : 'Ticket ansehen' , invoiceLink : 'Rechnung ansehen' } ;
/ * *
* Baut aus dem ( Klartext - ) Vorlagentext eine einfache , mandantenfarbige HTML - Mail . Eine Variable , deren Name auf
* "Link" endet , wird nicht als nackte URL im Text belassen , sondern als Schaltfläche ans Ende gesetzt ( der
* Klartext - Teil der Mail behält den Link weiterhin als sichtbare URL , für Mailprogramme ohne HTML - Ansicht ) .
* /
export function renderTemplateHtml ( tpl : string , vars : Record < string , string > ) : string {
const linkKey = Object . keys ( vars ) . find ( ( k ) = > /Link$/ . test ( k ) && vars [ k ] ) ;
const bodyVars = linkKey ? { . . . vars , [ linkKey ] : '' } : vars ;
const rendered = tpl . replace ( /\{\{(\w+)\}\}/g , ( m , k : string ) = > ( k in bodyVars ? escapeHtml ( bodyVars [ k ] ! ) : m ) ) ;
const paragraphs = rendered . split ( /\n{2,}/ ) . map ( ( p ) = > p . trim ( ) ) . filter ( Boolean ) . map ( ( p ) = > ` <p style="margin:0 0 16px;"> ${ p . replace ( /\n/g , '<br>' ) } </p> ` ) . join ( '' ) ;
const button = linkKey ? ` <p style="margin:24px 0 0;"><a href=" ${ escapeHtml ( vars [ linkKey ] ! ) } " style="display:inline-block;background:#3b6fd8;color:#ffffff;text-decoration:none;padding:12px 24px;border-radius:8px;font-weight:600;"> ${ LINK_LABEL [ linkKey ] ? ? 'Öffnen' } </a></p> ` : '' ;
return ` <!doctype html><html><body style="margin:0;padding:32px 16px;background:#f4f6fa;font-family:-apple-system,Segoe UI,Roboto,sans-serif;">
< table role = "presentation" width = "100%" style = "max-width:520px;margin:0 auto;background:#ffffff;border:1px solid #e3e8f0;border-radius:12px;" >
< tr > < td style = "padding:28px 32px;" >
< div style = "font-weight:700;font-size:18px;color:#173568;margin-bottom:20px;" > Kundencenter < / div >
< div style = "color:#172033;font-size:15px;line-height:1.55;" > $ { paragraphs } $ { button } < / div >
< / td > < / tr > < / table >
< / body > < / html > ` ;
}
2026-09-28 13:16:42 +02:00
/** Sendet anhand einer in der Datenbank hinterlegten Vorlage (Einstellungen > E-Mail > Vorlagen). Der Absender richtet sich nach dem Bereich der Vorlage (Kundencenter/Störungsticket/Rechnungen). */
feat: SMTP-Stack mit konfigurierbaren Mailvorlagen
Bisher gab es nur zwei fest im Code verdrahtete E-Mails (Einladung,
Passwort-Reset) und die SMTP-Verbindung kam ausschließlich aus
Umgebungsvariablen. Jetzt:
- SMTP-Verbindung unter Einstellungen > E-Mail konfigurierbar (Host,
Port, Verschlüsselung, Zugangsdaten verschlüsselt gespeichert,
Absender), mit Testmail-Versand. Die Mail-Logik wandert dafür nach
@kc/platform/mail, damit API und Worker sie gemeinsam nutzen.
- Mailvorlagen-Verwaltung: jedes Ereignis hat eine feste "Definition"
(Name, Auslöser, verfügbare Platzhalter/Daten), Betreff/Text sind
editierbar, je Vorlage einzeln aktivierbar, mit Testversand anhand
von Beispieldaten. Unbekannte Platzhalter werden beim Speichern
abgelehnt.
- Neue Ereignisse verdrahtet: Ticket erstellt (Bestätigung an Kunde),
Personal antwortet auf Ticket (Benachrichtigung an Kunde), Rechnung
ausgestellt, sowie die zuvor zurückgestellte Funktion "Panel-
Zugangsdaten per E-Mail senden" nach einem Passwort-Reset. Diese
laufen über die Jobqueue (mail.template), analog zu discord.notify.
- Versandprotokoll (letzte 100 Versuche, ohne Inhalte) einsehbar unter
Einstellungen > E-Mail.
- Migration 022, Rechte email.read (Admin+) / email.write (Superadmin).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-28 12:53:29 +02:00
export async function sendTemplateMail ( to : string , key : string , vars : Record < string , string > ) : Promise < 'sent' | 'failed' | 'not_configured' | 'disabled' | 'no_template' > {
const t = await one ( 'SELECT * FROM mail_templates WHERE `key` = ?' , [ key ] ) ;
if ( ! t ) return 'no_template' ;
if ( ! t . enabled ) return 'disabled' ;
2026-09-28 23:00:31 +02:00
return sendMail ( to , key , renderTemplateText ( t . subject , vars ) , renderTemplateText ( t . body , vars ) , { category : t.category as MailCategory , html : renderTemplateHtml ( t . body , vars ) } ) ;
feat: SMTP-Stack mit konfigurierbaren Mailvorlagen
Bisher gab es nur zwei fest im Code verdrahtete E-Mails (Einladung,
Passwort-Reset) und die SMTP-Verbindung kam ausschließlich aus
Umgebungsvariablen. Jetzt:
- SMTP-Verbindung unter Einstellungen > E-Mail konfigurierbar (Host,
Port, Verschlüsselung, Zugangsdaten verschlüsselt gespeichert,
Absender), mit Testmail-Versand. Die Mail-Logik wandert dafür nach
@kc/platform/mail, damit API und Worker sie gemeinsam nutzen.
- Mailvorlagen-Verwaltung: jedes Ereignis hat eine feste "Definition"
(Name, Auslöser, verfügbare Platzhalter/Daten), Betreff/Text sind
editierbar, je Vorlage einzeln aktivierbar, mit Testversand anhand
von Beispieldaten. Unbekannte Platzhalter werden beim Speichern
abgelehnt.
- Neue Ereignisse verdrahtet: Ticket erstellt (Bestätigung an Kunde),
Personal antwortet auf Ticket (Benachrichtigung an Kunde), Rechnung
ausgestellt, sowie die zuvor zurückgestellte Funktion "Panel-
Zugangsdaten per E-Mail senden" nach einem Passwort-Reset. Diese
laufen über die Jobqueue (mail.template), analog zu discord.notify.
- Versandprotokoll (letzte 100 Versuche, ohne Inhalte) einsehbar unter
Einstellungen > E-Mail.
- Migration 022, Rechte email.read (Admin+) / email.write (Superadmin).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-28 12:53:29 +02:00
}
2026-09-28 23:00:31 +02:00
export const baseUrl = config . baseUrl ;