feat: separate Absenderprofile für Kundencenter/Störungsticket/Rechnungen
Bisher gab es nur einen globalen SMTP-Absender für alle Mailvorlagen. Jetzt kann je Bereich ein eigener Absendername/-adresse hinterlegt werden (z. B. support@… für Tickets, rechnung@… für Rechnungen); leer gelassene Felder fallen weiter auf den globalen SMTP-Absender zurück. Jede Vorlage ist fest einem Bereich zugeordnet (Migration 023: mail_templates.category, neue Tabelle mail_sender_profiles mit den drei Zeilen kundencenter/stoerungsticket/rechnungen). Außerdem: der Testmail-Versand (SMTP-Verbindung wie auch je Vorlage) zeigt jetzt den konkreten Fehler aus dem Mail-Protokoll an (z. B. "535 5.7.8 authentication failed") statt nur "Fehlgeschlagen". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
bbece6ca5f
commit
7d2b2947f4
4 changed files with 88 additions and 15 deletions
|
|
@ -3,6 +3,7 @@ import { one, run } from './db.js';
|
|||
import { decrypt } from './crypto.js';
|
||||
|
||||
export interface SmtpConfig { host: string; port: number; secureMode: 'none' | 'starttls' | 'tls'; username: string | null; fromName: string | null; fromEmail: string }
|
||||
export type MailCategory = 'kundencenter' | 'stoerungsticket' | 'rechnungen';
|
||||
|
||||
/** Lädt die SMTP-Verbindung aus der Datenbank (Einstellungen > E-Mail). Kein Host/Absender gesetzt = nicht eingerichtet. */
|
||||
export async function loadSmtpConfig(): Promise<SmtpConfig | null> {
|
||||
|
|
@ -10,6 +11,12 @@ export async function loadSmtpConfig(): Promise<SmtpConfig | null> {
|
|||
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 };
|
||||
}
|
||||
/** 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 };
|
||||
}
|
||||
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;
|
||||
|
|
@ -22,7 +29,7 @@ async function transportFor(cfg: SmtpConfig) {
|
|||
}
|
||||
|
||||
/** Versand mit Protokoll. Ohne SMTP-Konfiguration wird nur "not_configured" protokolliert (Inhalt bewusst nicht gespeichert). */
|
||||
export async function sendMail(to: string, template: string, subject: string, text: string): Promise<'sent' | 'failed' | 'not_configured'> {
|
||||
export async function sendMail(to: string, template: string, subject: string, text: string, category?: MailCategory): Promise<'sent' | 'failed' | 'not_configured'> {
|
||||
const cfg = await loadSmtpConfig();
|
||||
if (!cfg) {
|
||||
await run('INSERT INTO mail_log (to_email, template, status) VALUES (?,?,?)', [to, template, 'not_configured']);
|
||||
|
|
@ -30,7 +37,8 @@ export async function sendMail(to: string, template: string, subject: string, te
|
|||
}
|
||||
try {
|
||||
const transport = await transportFor(cfg);
|
||||
await transport.sendMail({ from: cfg.fromName ? `"${cfg.fromName.replace(/"/g, "'")}" <${cfg.fromEmail}>` : cfg.fromEmail, to, subject, text });
|
||||
const from = await fromFor(cfg, category);
|
||||
await transport.sendMail({ from: from.name ? `"${from.name.replace(/"/g, "'")}" <${from.email}>` : from.email, to, subject, text });
|
||||
await run('INSERT INTO mail_log (to_email, template, status) VALUES (?,?,?)', [to, template, 'sent']);
|
||||
return 'sent';
|
||||
} catch (e) {
|
||||
|
|
@ -44,10 +52,10 @@ export function renderTemplateText(tpl: string, vars: Record<string, string>): s
|
|||
return tpl.replace(/\{\{(\w+)\}\}/g, (m, k: string) => (k in vars ? vars[k]! : m));
|
||||
}
|
||||
|
||||
/** Sendet anhand einer in der Datenbank hinterlegten Vorlage (Einstellungen > E-Mail > Vorlagen). */
|
||||
/** 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). */
|
||||
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';
|
||||
return sendMail(to, key, renderTemplateText(t.subject, vars), renderTemplateText(t.body, vars));
|
||||
return sendMail(to, key, renderTemplateText(t.subject, vars), renderTemplateText(t.body, vars), t.category as MailCategory);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue