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) --------------------------
|
||||
|
|
|
|||
|
|
@ -5,9 +5,12 @@ import { useSession } from '@/lib/session';
|
|||
import { Alert, Field, fmt } from '@/components/ui';
|
||||
|
||||
interface Settings { host: string | null; port: number; secureMode: 'none' | 'starttls' | 'tls'; username: string | null; hasPassword: boolean; fromName: string | null; fromEmail: string | null; configured: boolean; updatedAt: string }
|
||||
interface Template { key: string; name: string; trigger: string; variables: string[]; subject: string; body: string; enabled: boolean; updatedAt: string }
|
||||
type Category = 'kundencenter' | 'stoerungsticket' | 'rechnungen';
|
||||
interface Template { key: string; name: string; trigger: string; category: Category; variables: string[]; subject: string; body: string; enabled: boolean; updatedAt: string }
|
||||
interface Sender { category: Category; name: string; fromName: string | null; fromEmail: string | null; updatedAt: string }
|
||||
interface LogRow { id: number; to: string; template: string; status: string; error: string | null; createdAt: string }
|
||||
const SECURE_LABEL: Record<string, string> = { none: 'Keine Verschlüsselung', starttls: 'STARTTLS (empfohlen, meist Port 587)', tls: 'TLS/SSL direkt (meist Port 465)' };
|
||||
const CATEGORY_LABEL: Record<Category, string> = { kundencenter: 'Kundencenter', stoerungsticket: 'Störungsticket', rechnungen: 'Rechnungen' };
|
||||
const STATUS: Record<string, { cls: string; icon: string; label: string }> = {
|
||||
sent: { cls: 'ok', icon: '✓', label: 'Gesendet' }, failed: { cls: 'err', icon: '✕', label: 'Fehlgeschlagen' },
|
||||
not_configured: { cls: 'warn', icon: '▲', label: 'SMTP nicht eingerichtet' }, disabled: { cls: 'lock', icon: '⊘', label: 'Vorlage deaktiviert' }, no_template: { cls: 'err', icon: '✕', label: 'Vorlage fehlt' },
|
||||
|
|
@ -30,7 +33,7 @@ function SmtpForm({ s, w, onSaved }: { s: Settings; w: boolean; onSaved: () => v
|
|||
}
|
||||
async function sendTest() {
|
||||
setTestBusy(true); setMsg(null);
|
||||
try { const r = await api<{ status: string }>('POST', '/admin/email/settings/test', { to: testTo }); setMsg({ k: r.status === 'sent' ? 'ok' : 'err', t: r.status === 'sent' ? `Testmail an ${testTo} gesendet.` : `Status: ${STATUS[r.status]?.label ?? r.status}` }); }
|
||||
try { const r = await api<{ status: string; error: string | null }>('POST', '/admin/email/settings/test', { to: testTo }); setMsg({ k: r.status === 'sent' ? 'ok' : 'err', t: r.status === 'sent' ? `Testmail an ${testTo} gesendet.` : `Status: ${STATUS[r.status]?.label ?? r.status}${r.error ? ` – ${r.error}` : ''}` }); }
|
||||
catch (x) { setMsg({ k: 'err', t: errMsg(x) }); } finally { setTestBusy(false); }
|
||||
}
|
||||
return (<div className="card">
|
||||
|
|
@ -57,6 +60,31 @@ function SmtpForm({ s, w, onSaved }: { s: Settings; w: boolean; onSaved: () => v
|
|||
</div>);
|
||||
}
|
||||
|
||||
/** Absenderprofile je Bereich: eigener Name/Adresse statt des globalen SMTP-Absenders, leer = globaler Absender wird verwendet. */
|
||||
function SenderRow({ p, w, fallbackName, fallbackEmail, onSaved }: { p: Sender; w: boolean; fallbackName: string; fallbackEmail: string; onSaved: () => void }) {
|
||||
const [busy, setBusy] = useState(false); const [msg, setMsg] = useState<{ k: 'ok' | 'err'; t: string } | null>(null);
|
||||
async function save(e: FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault(); if (!w) return; setBusy(true); setMsg(null);
|
||||
const f = new FormData(e.currentTarget); const v = (k: string) => (String(f.get(k) ?? '').trim() || null);
|
||||
try { await api('PUT', `/admin/email/senders/${p.category}`, { fromName: v('fromName'), fromEmail: v('fromEmail') }); setMsg({ k: 'ok', t: 'Gespeichert.' }); onSaved(); }
|
||||
catch (x) { setMsg({ k: 'err', t: errMsg(x) }); } finally { setBusy(false); }
|
||||
}
|
||||
return (<form onSubmit={save} className="cols" style={{ alignItems: 'end', marginBottom: 12, paddingBottom: 12, borderBottom: '1px solid var(--border)' }}>
|
||||
<div><strong>{CATEGORY_LABEL[p.category]}</strong><p className="small muted" style={{ margin: '2px 0 0' }}>{p.name}</p>{msg && <p className={`small ${msg.k === 'ok' ? '' : 'muted'}`} style={{ color: msg.k === 'ok' ? 'var(--ok)' : 'var(--err)', margin: '4px 0 0' }}>{msg.t}</p>}</div>
|
||||
<Field id={`sn-${p.category}`} label="Absendername"><input id={`sn-${p.category}`} name="fromName" defaultValue={p.fromName ?? ''} placeholder={fallbackName} disabled={!w} /></Field>
|
||||
<Field id={`se-${p.category}`} label="Absenderadresse"><input id={`se-${p.category}`} name="fromEmail" type="email" defaultValue={p.fromEmail ?? ''} placeholder={fallbackEmail} disabled={!w} /></Field>
|
||||
{w && <button className="btn small" type="submit" disabled={busy} style={{ marginBottom: 16 }}>{busy ? '…' : 'Speichern'}</button>}
|
||||
</form>);
|
||||
}
|
||||
function SenderProfiles({ senders, s, w, onSaved }: { senders: Sender[]; s: Settings; w: boolean; onSaved: () => void }) {
|
||||
const fallbackName = s.fromName || 'globaler Absender oben'; const fallbackEmail = s.fromEmail || 'globaler Absender oben';
|
||||
return (<div className="card">
|
||||
<h2>Absenderprofile</h2>
|
||||
<p className="muted small">Für Kundencenter-Zugang, Störungstickets und Rechnungen kann jeweils ein eigener Absender hinterlegt werden (z. B. <code className="mono">support@…</code> für Tickets, <code className="mono">rechnung@…</code> für Rechnungen). Leer gelassene Felder verwenden den globalen SMTP-Absender oben.</p>
|
||||
{senders.map((p) => <SenderRow key={p.category} p={p} w={w} fallbackName={fallbackName} fallbackEmail={fallbackEmail} onSaved={onSaved} />)}
|
||||
</div>);
|
||||
}
|
||||
|
||||
/** Eine Vorlage: feste Definition (Auslöser, verfügbare Daten) + editierbarer Betreff/Text. */
|
||||
function TemplateCard({ t, w, onSaved }: { t: Template; w: boolean; onSaved: () => void }) {
|
||||
const [open, setOpen] = useState(false); const [busy, setBusy] = useState(false); const [msg, setMsg] = useState<{ k: 'ok' | 'err'; t: string } | null>(null);
|
||||
|
|
@ -69,12 +97,12 @@ function TemplateCard({ t, w, onSaved }: { t: Template; w: boolean; onSaved: ()
|
|||
}
|
||||
async function sendTest() {
|
||||
setTestBusy(true); setMsg(null);
|
||||
try { const r = await api<{ status: string }>('POST', `/admin/email/templates/${t.key}/test`, { to: testTo }); setMsg({ k: r.status === 'sent' ? 'ok' : 'err', t: r.status === 'sent' ? `Testmail mit Beispieldaten an ${testTo} gesendet.` : `Status: ${STATUS[r.status]?.label ?? r.status}` }); }
|
||||
try { const r = await api<{ status: string; error: string | null }>('POST', `/admin/email/templates/${t.key}/test`, { to: testTo }); setMsg({ k: r.status === 'sent' ? 'ok' : 'err', t: r.status === 'sent' ? `Testmail mit Beispieldaten an ${testTo} gesendet.` : `Status: ${STATUS[r.status]?.label ?? r.status}${r.error ? ` – ${r.error}` : ''}` }); }
|
||||
catch (x) { setMsg({ k: 'err', t: errMsg(x) }); } finally { setTestBusy(false); }
|
||||
}
|
||||
return (<div className="card">
|
||||
<div className="row between">
|
||||
<div><h3 style={{ margin: 0 }}>{t.name}</h3><p className="small muted" style={{ margin: '4px 0 0' }}>{t.trigger}</p></div>
|
||||
<div><h3 style={{ margin: 0 }}>{t.name}</h3><p className="small muted" style={{ margin: '4px 0 0' }}>{t.trigger}</p><span className="badge info" style={{ marginTop: 6 }}>{CATEGORY_LABEL[t.category]}</span></div>
|
||||
<div className="row">{!t.enabled && <span className="badge lock"><span aria-hidden="true">⊘</span>Deaktiviert</span>}<button className="btn small" aria-expanded={open} onClick={() => setOpen(!open)}>{open ? 'Schließen' : 'Bearbeiten'}</button></div>
|
||||
</div>
|
||||
{open && <div style={{ marginTop: 16 }}>
|
||||
|
|
@ -96,9 +124,10 @@ function TemplateCard({ t, w, onSaved }: { t: Template; w: boolean; onSaved: ()
|
|||
|
||||
export default function EmailSettings() {
|
||||
const { can } = useSession(); const w = can('email.write');
|
||||
const [s, setS] = useState<Settings | null>(null); const [tpls, setTpls] = useState<Template[] | null>(null); const [log, setLog] = useState<LogRow[] | null>(null); const [err, setErr] = useState('');
|
||||
const [s, setS] = useState<Settings | null>(null); const [senders, setSenders] = useState<Sender[] | null>(null); const [tpls, setTpls] = useState<Template[] | null>(null); const [log, setLog] = useState<LogRow[] | null>(null); const [err, setErr] = useState('');
|
||||
const load = useCallback(() => {
|
||||
api<Settings>('GET', '/admin/email/settings').then(setS).catch((e) => setErr(errMsg(e)));
|
||||
api<Sender[]>('GET', '/admin/email/senders').then(setSenders).catch((e) => setErr(errMsg(e)));
|
||||
api<Template[]>('GET', '/admin/email/templates').then(setTpls).catch((e) => setErr(errMsg(e)));
|
||||
api<LogRow[]>('GET', '/admin/email/log').then(setLog).catch(() => undefined);
|
||||
}, []);
|
||||
|
|
@ -108,6 +137,7 @@ export default function EmailSettings() {
|
|||
{err && <Alert kind="err">{err}</Alert>}
|
||||
{!w && <Alert kind="info">Nur Superadministratoren können die SMTP-Verbindung und Mailvorlagen ändern. Sie können hier den aktuellen Stand und das Protokoll einsehen.</Alert>}
|
||||
{s ? <SmtpForm s={s} w={w} onSaved={load} /> : <p className="muted" role="status">Wird geladen …</p>}
|
||||
{s && senders && <SenderProfiles senders={senders} s={s} w={w} onSaved={load} />}
|
||||
<h2 style={{ marginTop: 32 }}>Mailvorlagen</h2>
|
||||
<p className="muted small" style={{ marginTop: -8 }}>Jede Vorlage wird bei einem festen Ereignis im Kundencenter verwendet. Betreff und Text können angepasst werden; Platzhalter wie <code className="mono">{'{{name}}'}</code> werden beim Versand durch die tatsächlichen Daten ersetzt. Welche Platzhalter je Vorlage zur Verfügung stehen, steht direkt unter „Bearbeiten“.</p>
|
||||
{tpls === null ? <p className="muted" role="status">Wird geladen …</p> : tpls.map((t) => <TemplateCard key={t.key} t={t} w={w} onSaved={load} />)}
|
||||
|
|
|
|||
19
migrations/023_mail_sender_profiles.sql
Normal file
19
migrations/023_mail_sender_profiles.sql
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
-- Getrennte Absender-Identitäten je Bereich (Kundencenter, Störungsticket, Rechnungen), optional abweichend
|
||||
-- vom globalen SMTP-Absender (smtp_settings.from_name/from_email). Leer = globaler Absender wird verwendet.
|
||||
CREATE TABLE mail_sender_profiles (
|
||||
category ENUM('kundencenter','stoerungsticket','rechnungen') PRIMARY KEY,
|
||||
name VARCHAR(150) NOT NULL,
|
||||
from_name VARCHAR(150) NULL,
|
||||
from_email VARCHAR(254) NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
updated_by CHAR(36) NULL REFERENCES users(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
INSERT INTO mail_sender_profiles (category, name) VALUES
|
||||
('kundencenter', 'Kundencenter (Zugang, Passwort, Zugangsdaten)'),
|
||||
('stoerungsticket', 'Störungsticket (Support)'),
|
||||
('rechnungen', 'Rechnungen');
|
||||
|
||||
ALTER TABLE mail_templates ADD COLUMN category ENUM('kundencenter','stoerungsticket','rechnungen') NOT NULL DEFAULT 'kundencenter';
|
||||
UPDATE mail_templates SET category = 'kundencenter' WHERE `key` IN ('invite','password_reset','panel_credentials');
|
||||
UPDATE mail_templates SET category = 'stoerungsticket' WHERE `key` IN ('ticket_created','ticket_message');
|
||||
UPDATE mail_templates SET category = 'rechnungen' WHERE `key` IN ('invoice_issued');
|
||||
|
|
@ -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