// Email transport layer: provider configuration, SMTP setup, and the low-level // sendEmail router. No template rendering or DB logging happens here. import nodemailer from 'nodemailer'; import type { Transporter } from 'nodemailer'; // ==================== Types ==================== export interface SendEmailOptions { to: string | string[]; subject: string; html: string; text?: string; replyTo?: string; } export interface SendEmailResult { success: boolean; messageId?: string; error?: string; } export type EmailProvider = 'resend' | 'smtp' | 'console'; // ==================== Provider Configuration ==================== function getEmailProvider(): EmailProvider { const provider = (process.env.EMAIL_PROVIDER || 'console').toLowerCase(); if (provider === 'resend' || provider === 'smtp' || provider === 'console') { return provider; } console.warn(`[Email] Unknown provider "${provider}", falling back to console`); return 'console'; } function getFromEmail(): string { return process.env.EMAIL_FROM || 'noreply@spanglish.com'; } function getFromName(): string { return process.env.EMAIL_FROM_NAME || 'Spanglish'; } /** Provider info for diagnostics endpoints. */ export function getProviderInfo(): { provider: EmailProvider; configured: boolean } { const provider = getEmailProvider(); let configured = false; switch (provider) { case 'resend': configured = !!(process.env.EMAIL_API_KEY || process.env.RESEND_API_KEY); break; case 'smtp': configured = !!process.env.SMTP_HOST; break; case 'console': configured = true; break; } return { provider, configured }; } // ==================== SMTP Configuration ==================== interface SMTPConfig { host: string; port: number; secure: boolean; auth?: { user: string; pass: string; }; } function getSMTPConfig(): SMTPConfig | null { const host = process.env.SMTP_HOST; const port = parseInt(process.env.SMTP_PORT || '587'); const user = process.env.SMTP_USER; const pass = process.env.SMTP_PASS; const secure = process.env.SMTP_SECURE === 'true' || port === 465; if (!host) { return null; } const config: SMTPConfig = { host, port, secure, }; if (user && pass) { config.auth = { user, pass }; } return config; } // Cached SMTP transporter let smtpTransporter: Transporter | null = null; function getSMTPTransporter(): Transporter | null { if (smtpTransporter) { return smtpTransporter; } const config = getSMTPConfig(); if (!config) { console.error('[Email] SMTP configuration missing'); return null; } smtpTransporter = nodemailer.createTransport({ host: config.host, port: config.port, secure: config.secure, auth: config.auth, // Additional options for better deliverability pool: true, maxConnections: 5, maxMessages: 100, // TLS options tls: { rejectUnauthorized: process.env.SMTP_TLS_REJECT_UNAUTHORIZED !== 'false', }, }); // Verify connection configuration smtpTransporter.verify((error, success) => { if (error) { console.error('[Email] SMTP connection verification failed:', error.message); } else { console.log('[Email] SMTP server is ready to send emails'); } }); return smtpTransporter; } // ==================== Email Providers ==================== /** * Send email using Resend API */ async function sendWithResend(options: SendEmailOptions): Promise { const apiKey = process.env.EMAIL_API_KEY || process.env.RESEND_API_KEY; const fromEmail = getFromEmail(); const fromName = getFromName(); if (!apiKey) { console.error('[Email] Resend API key not configured'); return { success: false, error: 'Resend API key not configured' }; } try { const response = await fetch('https://api.resend.com/emails', { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ from: `${fromName} <${fromEmail}>`, to: Array.isArray(options.to) ? options.to : [options.to], subject: options.subject, html: options.html, text: options.text, reply_to: options.replyTo, }), }); const data = await response.json(); if (!response.ok) { console.error('[Email] Resend API error:', data); return { success: false, error: data.message || data.error || 'Failed to send email' }; } console.log('[Email] Email sent via Resend:', data.id); return { success: true, messageId: data.id }; } catch (error: any) { console.error('[Email] Resend error:', error); return { success: false, error: error.message || 'Failed to send email via Resend' }; } } /** * Send email using SMTP (Nodemailer) */ async function sendWithSMTP(options: SendEmailOptions): Promise { const transporter = getSMTPTransporter(); if (!transporter) { return { success: false, error: 'SMTP not configured' }; } const fromEmail = getFromEmail(); const fromName = getFromName(); try { const info = await transporter.sendMail({ from: `"${fromName}" <${fromEmail}>`, to: Array.isArray(options.to) ? options.to.join(', ') : options.to, replyTo: options.replyTo, subject: options.subject, html: options.html, text: options.text, }); console.log('[Email] Email sent via SMTP:', info.messageId); return { success: true, messageId: info.messageId }; } catch (error: any) { console.error('[Email] SMTP error:', error); return { success: false, error: error.message || 'Failed to send email via SMTP' }; } } /** * Console logger for development/testing (no actual email sent) */ async function sendWithConsole(options: SendEmailOptions): Promise { const to = Array.isArray(options.to) ? options.to.join(', ') : options.to; console.log('\n========================================'); console.log('[Email] Console Mode - Email Preview'); console.log('========================================'); console.log(`To: ${to}`); console.log(`Subject: ${options.subject}`); console.log(`Reply-To: ${options.replyTo || 'N/A'}`); console.log('----------------------------------------'); console.log('HTML Body (truncated):'); console.log(options.html?.substring(0, 500) + '...'); console.log('========================================\n'); return { success: true, messageId: `console-${Date.now()}` }; } // Mask an email address for logs: keep first char + domain (e.g. j***@example.com). function maskEmail(email: string): string { const [local, domain] = String(email).split('@'); if (!domain) return '***'; const head = local.slice(0, 1); return `${head}***@${domain}`; } /** * Main send function that routes to the appropriate provider */ export async function sendEmail(options: SendEmailOptions): Promise { const provider = getEmailProvider(); const recipientCount = Array.isArray(options.to) ? options.to.length : 1; const sample = Array.isArray(options.to) ? options.to[0] : options.to; console.log(`[Email] Sending email via ${provider} to ${maskEmail(sample)}${recipientCount > 1 ? ` (+${recipientCount - 1} more)` : ''}`); switch (provider) { case 'resend': return sendWithResend(options); case 'smtp': return sendWithSMTP(options); case 'console': default: return sendWithConsole(options); } } /** * Test email configuration by sending a test email */ export async function testConnection(to: string): Promise { const { provider, configured } = getProviderInfo(); if (!configured) { return { success: false, error: `Email provider "${provider}" is not configured` }; } return sendEmail({ to, subject: 'Spanglish - Email Test', html: `

Email Configuration Test

This is a test email from your Spanglish platform.

Provider: ${provider}

Timestamp: ${new Date().toISOString()}

If you received this email, your email configuration is working correctly!

`, text: `Email Configuration Test\n\nProvider: ${provider}\nTimestamp: ${new Date().toISOString()}\n\nIf you received this email, your email configuration is working correctly!`, }); }