import nodemailer from 'nodemailer'; import type { Transporter } from 'nodemailer'; import { supabaseAdmin } from './supabase'; export interface SmtpConfig { host: string; port: number; secure: boolean; username: string; password: string; from_address: string; from_name: string; } export interface SendEmailOptions { to: string; subject: string; html: string; text?: string; recipientId?: string; recipientName?: string; templateKey?: string; emailType?: string; sentBy?: string; } /** * Get SMTP configuration from app_settings table, with fallback to environment variables * This allows welcome emails to work using the same SMTP as GoTrue when app_settings isn't configured */ export async function getSmtpConfig(): Promise { // First try to get from app_settings const { data: settings } = await supabaseAdmin .from('app_settings') .select('setting_key, setting_value') .eq('category', 'email'); const config: Record = {}; if (settings && settings.length > 0) { for (const s of settings) { // Parse the value - it might be JSON stringified or plain let value = s.setting_value; if (typeof value === 'string') { // Remove surrounding quotes if present value = value.replace(/^"|"$/g, ''); } config[s.setting_key] = value as string; } } // Check if app_settings has valid SMTP config if (config.smtp_host && config.smtp_username && config.smtp_password) { return { host: config.smtp_host, port: parseInt(config.smtp_port || '587'), secure: config.smtp_secure === 'true' || parseInt(config.smtp_port || '587') === 465, username: config.smtp_username, password: config.smtp_password, from_address: config.smtp_from_address || 'noreply@monacousa.org', from_name: config.smtp_from_name || 'Monaco USA' }; } // Fall back to environment variables (same as GoTrue SMTP settings) const envHost = process.env.SMTP_HOST || process.env.GOTRUE_SMTP_HOST; const envUser = process.env.SMTP_USER || process.env.GOTRUE_SMTP_USER; const envPass = process.env.SMTP_PASS || process.env.GOTRUE_SMTP_PASS; if (envHost && envUser && envPass) { const envPort = process.env.SMTP_PORT || process.env.GOTRUE_SMTP_PORT || '587'; return { host: envHost, port: parseInt(envPort), secure: parseInt(envPort) === 465, username: envUser, password: envPass, from_address: process.env.SMTP_ADMIN_EMAIL || process.env.GOTRUE_SMTP_ADMIN_EMAIL || 'noreply@monacousa.org', from_name: process.env.SMTP_SENDER_NAME || process.env.GOTRUE_SMTP_SENDER_NAME || 'Monaco USA' }; } return null; } /** * Create a nodemailer transporter with the configured SMTP settings */ export async function createTransporter(): Promise { const config = await getSmtpConfig(); if (!config) { console.error('SMTP configuration not found or incomplete'); return null; } return nodemailer.createTransport({ host: config.host, port: config.port, secure: config.secure, auth: { user: config.username, pass: config.password } }); } /** * Send an email using the configured SMTP settings */ export async function sendEmail(options: SendEmailOptions): Promise<{ success: boolean; error?: string; messageId?: string }> { const config = await getSmtpConfig(); if (!config) { return { success: false, error: 'SMTP not configured. Please configure email settings first.' }; } const transporter = await createTransporter(); if (!transporter) { return { success: false, error: 'Failed to create email transporter' }; } try { const result = await transporter.sendMail({ from: `"${config.from_name}" <${config.from_address}>`, to: options.to, subject: options.subject, html: options.html, text: options.text || stripHtml(options.html) }); // Log to email_logs table await supabaseAdmin.from('email_logs').insert({ recipient_id: options.recipientId || null, recipient_email: options.to, recipient_name: options.recipientName || null, template_key: options.templateKey || null, subject: options.subject, email_type: options.emailType || 'manual', status: 'sent', provider: 'smtp', provider_message_id: result.messageId, sent_by: options.sentBy || null, sent_at: new Date().toISOString() }); return { success: true, messageId: result.messageId }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; console.error('Email send error:', error); // Log failed attempt await supabaseAdmin.from('email_logs').insert({ recipient_id: options.recipientId || null, recipient_email: options.to, recipient_name: options.recipientName || null, template_key: options.templateKey || null, subject: options.subject, email_type: options.emailType || 'manual', status: 'failed', provider: 'smtp', error_message: errorMessage, sent_by: options.sentBy || null }); return { success: false, error: errorMessage }; } } /** * Send a templated email with variable substitution * Templates should contain content only (no full HTML wrapper) - will be wrapped automatically */ export async function sendTemplatedEmail( templateKey: string, to: string, variables: Record, options?: { recipientId?: string; recipientName?: string; sentBy?: string; baseUrl?: string; } ): Promise<{ success: boolean; error?: string; messageId?: string }> { // Fetch template from database const { data: template, error: templateError } = await supabaseAdmin .from('email_templates') .select('*') .eq('template_key', templateKey) .eq('is_active', true) .single(); if (templateError || !template) { return { success: false, error: `Email template "${templateKey}" not found or inactive` }; } // Get site URL for logo const baseUrl = options?.baseUrl || process.env.SITE_URL || 'https://monacousa.org'; const logoUrl = `${baseUrl}/MONACOUSA-Flags_376x376.png`; // Add default variables const allVariables: Record = { logo_url: logoUrl, site_url: baseUrl, ...variables }; // Replace variables in subject and body let subject = template.subject; let bodyContent = template.body_html; let text = template.body_text || ''; for (const [key, value] of Object.entries(allVariables)) { const regex = new RegExp(`{{${key}}}`, 'g'); subject = subject.replace(regex, value); bodyContent = bodyContent.replace(regex, value); text = text.replace(regex, value); } // Extract title from template or use subject // Look for title in template metadata or first h2 tag let emailTitle = template.email_title || subject; // Try to extract from first h2 in content const h2Match = bodyContent.match(/]*>([^<]+)<\/h2>/i); if (h2Match) { emailTitle = h2Match[1].replace(/{{[^}]+}}/g, '').trim(); } // Check if template already has full HTML wrapper (legacy templates) const hasFullWrapper = bodyContent.includes('/g, `` ); } else { // Content-only template - wrap with Monaco template html = wrapInMonacoTemplate({ title: emailTitle, content: bodyContent, logoUrl }); } return sendEmail({ to, subject, html, text: text || undefined, recipientId: options?.recipientId, recipientName: options?.recipientName, templateKey, emailType: template.category, sentBy: options?.sentBy }); } /** * Test SMTP connection and optionally send a test email */ export async function testSmtpConnection( sendTo?: string, sentBy?: string ): Promise<{ success: boolean; error?: string }> { const config = await getSmtpConfig(); if (!config) { return { success: false, error: 'SMTP not configured. Please configure and save email settings first.' }; } const transporter = await createTransporter(); if (!transporter) { return { success: false, error: 'Failed to create email transporter' }; } try { // Verify connection await transporter.verify(); // If a recipient is provided, send a test email if (sendTo) { const testContent = `

This is a test email from your Monaco USA Portal.

✓ Configuration Verified

Your SMTP settings are working correctly!

Sent at ${new Date().toLocaleString()}

`; const result = await sendEmail({ to: sendTo, subject: 'Monaco USA Portal - SMTP Test Email', html: wrapInMonacoTemplate({ title: 'SMTP Test Successful!', content: testContent }), emailType: 'test', sentBy }); if (!result.success) { return { success: false, error: result.error }; } } return { success: true }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; console.error('SMTP test error:', error); return { success: false, error: `SMTP connection failed: ${errorMessage}` }; } } // S3-hosted background image URL matching login screen const EMAIL_BACKGROUND_IMAGE_URL = 'https://s3.monacousa.org/public/monaco_high_res.jpg'; /** * Wrap email content in Monaco-branded template * This creates a consistent look matching the login page styling with background image */ export function wrapInMonacoTemplate(options: { title: string; content: string; logoUrl?: string; backgroundImageUrl?: string; }): string { const baseUrl = process.env.SITE_URL || 'http://localhost:7453'; const logoUrl = options.logoUrl || `${baseUrl}/MONACOUSA-Flags_376x376.png`; const bgImageUrl = options.backgroundImageUrl || EMAIL_BACKGROUND_IMAGE_URL; return `
Monaco USA

Monaco USA

Americans in Monaco

${options.title}

${options.content}

© 2026 Monaco USA. All rights reserved.

`; } /** * Strip HTML tags from a string to create plain text version */ function stripHtml(html: string): string { return html .replace(/<[^>]*>/g, '') .replace(/ /g, ' ') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, "'") .replace(/\s+/g, ' ') .trim(); }