// Template DB access, seeding, and the core template/custom send + logging logic. import { db, dbGet, emailTemplates, emailLogs } from '../../db/index.js'; import { eq } from 'drizzle-orm'; import { getNow, generateId } from '../utils.js'; import { replaceTemplateVariables, wrapInBaseTemplate, defaultTemplates } from '../emailTemplates.js'; import { sendEmail } from './transport.js'; import { getCommonVariables } from './formatting.js'; /** * Get a template by slug */ export async function getTemplate(slug: string): Promise { const template = await dbGet( (db as any) .select() .from(emailTemplates) .where(eq((emailTemplates as any).slug, slug)) ); return template || null; } /** * Seed default templates if they don't exist, and update system templates with latest content */ export async function seedDefaultTemplates(): Promise { console.log('[Email] Checking for default templates...'); for (const template of defaultTemplates) { const existing = await getTemplate(template.slug); const now = getNow(); if (!existing) { console.log(`[Email] Creating template: ${template.name}`); await (db as any).insert(emailTemplates).values({ id: generateId(), name: template.name, slug: template.slug, subject: template.subject, subjectEs: template.subjectEs, bodyHtml: template.bodyHtml, bodyHtmlEs: template.bodyHtmlEs, bodyText: template.bodyText, bodyTextEs: template.bodyTextEs, description: template.description, variables: JSON.stringify(template.variables), isSystem: template.isSystem ? 1 : 0, isActive: 1, createdAt: now, updatedAt: now, }); } else if (existing.isSystem) { // Update system templates with latest content from defaults console.log(`[Email] Updating system template: ${template.name}`); await (db as any) .update(emailTemplates) .set({ subject: template.subject, subjectEs: template.subjectEs, bodyHtml: template.bodyHtml, bodyHtmlEs: template.bodyHtmlEs, bodyText: template.bodyText, bodyTextEs: template.bodyTextEs, description: template.description, variables: JSON.stringify(template.variables), updatedAt: now, }) .where(eq((emailTemplates as any).slug, template.slug)); } } console.log('[Email] Default templates check complete'); } /** * Send an email using a template */ export async function sendTemplateEmail(params: { templateSlug: string; to: string; toName?: string; variables: Record; locale?: string; eventId?: string; sentBy?: string; }): Promise<{ success: boolean; logId?: string; error?: string }> { const { templateSlug, to, toName, variables, locale = 'en', eventId, sentBy } = params; // Get template const template = await getTemplate(templateSlug); if (!template) { return { success: false, error: `Template "${templateSlug}" not found` }; } // Build variables const allVariables = { ...getCommonVariables(), lang: locale, ...variables, }; // Get localized content const subject = locale === 'es' && template.subjectEs ? template.subjectEs : template.subject; const bodyHtml = locale === 'es' && template.bodyHtmlEs ? template.bodyHtmlEs : template.bodyHtml; const bodyText = locale === 'es' && template.bodyTextEs ? template.bodyTextEs : template.bodyText; // Replace variables const finalSubject = replaceTemplateVariables(subject, allVariables); const finalBodyContent = replaceTemplateVariables(bodyHtml, allVariables, true); const finalBodyHtml = wrapInBaseTemplate(finalBodyContent, { ...allVariables, subject: finalSubject }); const finalBodyText = bodyText ? replaceTemplateVariables(bodyText, allVariables) : undefined; // Create log entry const logId = generateId(); const now = getNow(); await (db as any).insert(emailLogs).values({ id: logId, templateId: template.id, eventId: eventId || null, recipientEmail: to, recipientName: toName || null, subject: finalSubject, bodyHtml: finalBodyHtml, status: 'pending', sentBy: sentBy || null, createdAt: now, }); // Send email const result = await sendEmail({ to, subject: finalSubject, html: finalBodyHtml, text: finalBodyText, }); // Update log with result if (result.success) { await (db as any) .update(emailLogs) .set({ status: 'sent', sentAt: getNow(), }) .where(eq((emailLogs as any).id, logId)); } else { await (db as any) .update(emailLogs) .set({ status: 'failed', errorMessage: result.error, }) .where(eq((emailLogs as any).id, logId)); } return { success: result.success, logId, error: result.error }; } /** * Send a custom email (not from template) */ export async function sendCustomEmail(params: { to: string; toName?: string; subject: string; bodyHtml: string; bodyText?: string; replyTo?: string; eventId?: string; sentBy?: string | null; }): Promise<{ success: boolean; logId?: string; error?: string }> { const { to: rawTo, toName, subject: rawSubject, bodyHtml, bodyText, replyTo: rawReplyTo, eventId, sentBy = null } = params; // Strip CR/LF from header-bound values to prevent email header injection // (e.g. an attacker-supplied subject/replyTo smuggling extra headers/recipients). const stripHeader = (v?: string) => (v ? v.replace(/[\r\n]+/g, ' ').trim() : v); const to = stripHeader(rawTo) as string; const subject = stripHeader(rawSubject) as string; const replyTo = stripHeader(rawReplyTo); const allVariables = { ...getCommonVariables(), subject, }; const finalBodyHtml = wrapInBaseTemplate(bodyHtml, allVariables); // Create log entry const logId = generateId(); const now = getNow(); await (db as any).insert(emailLogs).values({ id: logId, templateId: null, eventId: eventId || null, recipientEmail: to, recipientName: toName || null, subject, bodyHtml: finalBodyHtml, status: 'pending', sentBy: sentBy || null, createdAt: now, }); // Send email const result = await sendEmail({ to, subject, html: finalBodyHtml, text: bodyText, replyTo, }); // Update log if (result.success) { await (db as any) .update(emailLogs) .set({ status: 'sent', sentAt: getNow(), }) .where(eq((emailLogs as any).id, logId)); } else { await (db as any) .update(emailLogs) .set({ status: 'failed', errorMessage: result.error, }) .where(eq((emailLogs as any).id, logId)); } return { success: result.success, logId, error: result.error }; } /** * Resend an email from an existing log entry */ export async function resendFromLog(logId: string): Promise<{ success: boolean; error?: string }> { const log = await dbGet( (db as any).select().from(emailLogs).where(eq((emailLogs as any).id, logId)) ); if (!log) { return { success: false, error: 'Email log not found' }; } if (!log.bodyHtml || !log.subject || !log.recipientEmail) { return { success: false, error: 'Email log missing required data to resend' }; } const result = await sendEmail({ to: log.recipientEmail, subject: log.subject, html: log.bodyHtml, text: undefined, }); const now = getNow(); const currentResendAttempts = (log.resendAttempts ?? 0) + 1; if (result.success) { await (db as any) .update(emailLogs) .set({ status: 'sent', sentAt: now, errorMessage: null, resendAttempts: currentResendAttempts, lastResentAt: now, }) .where(eq((emailLogs as any).id, logId)); } else { await (db as any) .update(emailLogs) .set({ status: 'failed', errorMessage: result.error, resendAttempts: currentResendAttempts, lastResentAt: now, }) .where(eq((emailLogs as any).id, logId)); } return { success: result.success, error: result.error, }; }