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
|
|
@ -10,10 +10,12 @@ import { renderTemplateText, sendMail } from '../../core/mail.js';
|
|||
import type { KcModule } from '../../core/module.js';
|
||||
|
||||
const email = z.string().email().max(254).transform((s) => s.toLowerCase());
|
||||
const CATEGORIES = ['kundencenter', 'stoerungsticket', 'rechnungen'] as const;
|
||||
const settingsView = (s: any) => ({
|
||||
host: s.host, port: s.port, secureMode: s.secure_mode, username: s.username, hasPassword: !!s.secrets_enc,
|
||||
fromName: s.from_name, fromEmail: s.from_email, configured: !!s.host && !!s.from_email, updatedAt: s.updated_at,
|
||||
});
|
||||
const senderView = (p: any) => ({ category: p.category, name: p.name, fromName: p.from_name, fromEmail: p.from_email, updatedAt: p.updated_at });
|
||||
/** Beispielwerte für die Testvorschau, je Platzhaltername – fällt auf den Platzhalternamen selbst zurück. */
|
||||
const SAMPLE: Record<string, string> = {
|
||||
name: 'Max Mustermann', inviteLink: 'https://kundencenter.example/einladung?token=…', validDays: '7',
|
||||
|
|
@ -52,12 +54,25 @@ export const mailModule: KcModule = {
|
|||
const a = requirePermission(req, 'email.write');
|
||||
const { to } = z.object({ to: email }).parse(req.body);
|
||||
const status = await sendMail(to, 'test', 'Testmail vom Kundencenter', 'Diese Testmail bestätigt, dass die SMTP-Einstellungen des Kundencenters funktionieren.\n');
|
||||
await audit({ actorType: 'user', actorId: a.user.id, action: 'email.settings.test', resourceType: 'smtp_settings', resourceId: '1', result: status === 'sent' ? 'success' : 'failure', correlationId: req.correlationId, ip: clientIp(req), after: { to, status } });
|
||||
return { status };
|
||||
const error = status === 'failed' ? (await one('SELECT error FROM mail_log WHERE to_email = ? AND template = ? ORDER BY id DESC LIMIT 1', [to, 'test']))?.error ?? null : null;
|
||||
await audit({ actorType: 'user', actorId: a.user.id, action: 'email.settings.test', resourceType: 'smtp_settings', resourceId: '1', result: status === 'sent' ? 'success' : 'failure', correlationId: req.correlationId, ip: clientIp(req), after: { to, status, error } });
|
||||
return { status, error };
|
||||
});
|
||||
|
||||
// ---- Absenderprofile: eigener Absendername/-adresse je Bereich, leer = globaler SMTP-Absender ----------
|
||||
app.get('/admin/email/senders', async (req) => { requirePermission(req, 'email.read'); return (await query('SELECT * FROM mail_sender_profiles ORDER BY category')).map(senderView); });
|
||||
app.put('/admin/email/senders/:category', async (req) => {
|
||||
const a = requirePermission(req, 'email.write');
|
||||
const { category } = z.object({ category: z.enum(CATEGORIES) }).parse(req.params);
|
||||
const b = z.object({ fromName: z.string().trim().max(150).nullable(), fromEmail: z.string().trim().max(254).nullable() }).parse(req.body);
|
||||
if (b.fromEmail && !z.string().email().safeParse(b.fromEmail).success) throw badRequest('Absenderadresse ist keine gültige E-Mail-Adresse', 'BAD_FROM_EMAIL');
|
||||
await run('UPDATE mail_sender_profiles SET from_name = ?, from_email = ?, updated_by = ? WHERE category = ?', [b.fromName, b.fromEmail, a.user.id, category]);
|
||||
await audit({ actorType: 'user', actorId: a.user.id, action: 'email.sender.update', resourceType: 'mail_sender_profile', resourceId: category, correlationId: req.correlationId, ip: clientIp(req), after: b });
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
// ---- Mailvorlagen: Definition (Auslöser + verfügbare Daten) ist fest, Betreff/Text/aktiv sind editierbar ----
|
||||
const templateView = (t: any) => ({ key: t.key, name: t.name, trigger: t.trigger_text, variables: typeof t.variables_json === 'string' ? JSON.parse(t.variables_json) : t.variables_json, subject: t.subject, body: t.body, enabled: !!t.enabled, updatedAt: t.updated_at });
|
||||
const templateView = (t: any) => ({ key: t.key, name: t.name, trigger: t.trigger_text, category: t.category, variables: typeof t.variables_json === 'string' ? JSON.parse(t.variables_json) : t.variables_json, subject: t.subject, body: t.body, enabled: !!t.enabled, updatedAt: t.updated_at });
|
||||
app.get('/admin/email/templates', async (req) => { requirePermission(req, 'email.read'); return (await query('SELECT * FROM mail_templates ORDER BY name')).map(templateView); });
|
||||
app.get('/admin/email/templates/:key', async (req) => {
|
||||
requirePermission(req, 'email.read'); const { key } = z.object({ key: z.string().max(60) }).parse(req.params);
|
||||
|
|
@ -82,9 +97,10 @@ export const mailModule: KcModule = {
|
|||
const t = await one('SELECT * FROM mail_templates WHERE `key` = ?', [key]); if (!t) throw notFound();
|
||||
const vars: string[] = typeof t.variables_json === 'string' ? JSON.parse(t.variables_json) : t.variables_json;
|
||||
const sample = Object.fromEntries(vars.map((v) => [v, SAMPLE[v] ?? `[${v}]`]));
|
||||
const status = await sendMail(to, `${key}:test`, renderTemplateText(t.subject, sample), renderTemplateText(t.body, sample));
|
||||
await audit({ actorType: 'user', actorId: a.user.id, action: 'email.template.test', resourceType: 'mail_template', resourceId: key, result: status === 'sent' ? 'success' : 'failure', correlationId: req.correlationId, ip: clientIp(req), after: { to, status } });
|
||||
return { status };
|
||||
const status = await sendMail(to, `${key}:test`, renderTemplateText(t.subject, sample), renderTemplateText(t.body, sample), t.category);
|
||||
const error = status === 'failed' ? (await one('SELECT error FROM mail_log WHERE to_email = ? AND template = ? ORDER BY id DESC LIMIT 1', [to, `${key}:test`]))?.error ?? null : null;
|
||||
await audit({ actorType: 'user', actorId: a.user.id, action: 'email.template.test', resourceType: 'mail_template', resourceId: key, result: status === 'sent' ? 'success' : 'failure', correlationId: req.correlationId, ip: clientIp(req), after: { to, status, error } });
|
||||
return { status, error };
|
||||
});
|
||||
|
||||
// ---- Protokoll der letzten Sendeversuche (kein Inhalt, nur Metadaten) --------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue