54 lines
3 KiB
TypeScript
54 lines
3 KiB
TypeScript
|
|
import nodemailer from 'nodemailer';
|
||
|
|
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 }
|
||
|
|
|
||
|
|
/** 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 };
|
||
|
|
}
|
||
|
|
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). */
|
||
|
|
export async function sendMail(to: string, template: string, subject: string, text: string): 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']);
|
||
|
|
return 'not_configured';
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const transport = await transportFor(cfg);
|
||
|
|
await transport.sendMail({ from: cfg.fromName ? `"${cfg.fromName.replace(/"/g, "'")}" <${cfg.fromEmail}>` : cfg.fromEmail, to, subject, text });
|
||
|
|
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));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Sendet anhand einer in der Datenbank hinterlegten Vorlage (Einstellungen > E-Mail > Vorlagen). */
|
||
|
|
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));
|
||
|
|
}
|