import nodemailer from 'nodemailer'; import { getSystemConfig } from './config'; export interface InlineAttachment { filename: string content: Buffer cid: string } interface MailOptions { to: string; subject: string; html: string; attachments?: InlineAttachment[]; } interface MailResult { success: boolean; messageId?: string; error?: string; } type EmailProvider = 'auto' | 'resend' | 'smtp'; /** * Send email. * - 'auto': try Resend first (if key set), fall back to SMTP on failure * - 'smtp': force SMTP only * - 'resend': force Resend only * Supports inline image attachments via cid: references in HTML. */ export async function sendEmail({ to, subject, html, attachments }: MailOptions, provider: EmailProvider = 'auto'): Promise { const config = await getSystemConfig(); const resendKey = config.RESEND_API_KEY || process.env.RESEND_API_KEY; // Force SMTP if (provider === 'smtp') { return sendViaSMTP(config, { to, subject, html, attachments }); } // Force Resend (no fallback) if (provider === 'resend') { if (!resendKey) return { success: false, error: 'No Resend API key configured' }; return sendViaResend(resendKey, { to, subject, html, attachments }); } // Auto: try Resend, fall back to SMTP if (resendKey) { const result = await sendViaResend(resendKey, { to, subject, html, attachments }); if (result.success) return result; console.warn('[Mail] Resend failed, falling back to SMTP:', result.error); return sendViaSMTP(config, { to, subject, html, attachments }); } return sendViaSMTP(config, { to, subject, html, attachments }); } async function sendViaResend(apiKey: string, { to, subject, html, attachments }: MailOptions): Promise { try { const { Resend } = await import('resend'); const resend = new Resend(apiKey); const from = process.env.NEXTAUTH_URL ? `Memento ` : 'Memento '; // Resend supports attachments with inline content const resendAttachments = attachments?.map(att => ({ filename: att.filename, content: att.content.toString('base64'), content_type: att.filename.endsWith('.png') ? 'image/png' : 'image/jpeg', disposition: 'inline' as const, content_id: att.cid, })); const { data, error } = await resend.emails.send({ from, to, subject, html, attachments: resendAttachments, }); if (error) { return { success: false, error: error.message }; } return { success: true, messageId: data?.id }; } catch (error: any) { return { success: false, error: `Resend: ${error.message}` }; } } async function sendViaSMTP(config: Record, { to, subject, html, attachments }: MailOptions): Promise { const host = config.SMTP_HOST || process.env.SMTP_HOST; const port = parseInt(config.SMTP_PORT || process.env.SMTP_PORT || '587'); const user = (config.SMTP_USER || process.env.SMTP_USER || '').trim(); const pass = (config.SMTP_PASS || process.env.SMTP_PASS || '').trim(); const from = config.SMTP_FROM || process.env.SMTP_FROM || 'noreply@memento.app'; if (!host) { return { success: false, error: 'SMTP host is not configured' }; } const forceSecure = config.SMTP_SECURE === 'true'; const isPort465 = port === 465; const secure = forceSecure || isPort465; const ignoreCerts = config.SMTP_IGNORE_CERT === 'true'; const transporter = nodemailer.createTransport({ host, port, secure, auth: { user, pass }, family: 4, authMethod: 'LOGIN', connectionTimeout: 10000, tls: { rejectUnauthorized: !ignoreCerts, ciphers: ignoreCerts ? 'SSLv3' : undefined } } as any); try { await transporter.verify(); // Build nodemailer inline attachments with cid const smtpAttachments = attachments?.map(att => ({ filename: att.filename, content: att.content, cid: att.cid, })); const info = await transporter.sendMail({ from: `"Memento" <${from}>`, to, subject, html, attachments: smtpAttachments, }); return { success: true, messageId: info.messageId }; } catch (error: any) { console.error('SMTP error:', error); return { success: false, error: `SMTP: ${error.message} (Code: ${error.code})` }; } }