import nodemailer from 'nodemailer'; import prisma from '@prisma/prisma'; interface EmailTemplate { subject: string; template: JSX.Element; } interface SendResult { success: boolean; messageId?: string; error?: string; } // Create nodemailer transporter for Postfix container const createTransporter = () => { // Default to 'postfix' for Docker networking (service name in docker-compose) // Override with EMAIL_HOST for different setups const host = process.env.EMAIL_HOST || 'postfix'; const port = parseInt(process.env.EMAIL_PORT || '25', 10); return nodemailer.createTransport({ host, port, secure: false, // true for 465, false for other ports tls: { rejectUnauthorized: false }, // Connection pooling for better performance pool: true, maxConnections: 5, maxMessages: 100 }); }; // Render React component to HTML string using dynamic import const renderTemplate = async (template: JSX.Element): Promise => { // Dynamic import to avoid Next.js bundling issues with react-dom/server const { renderToStaticMarkup } = await import('react-dom/server'); const html = renderToStaticMarkup(template); return ` ${html} `; }; // Generate plain text version from HTML (basic extraction) const htmlToPlainText = (html: string): string => { return html .replace(/]*>[\s\S]*?<\/style>/gi, '') .replace(/]*>[\s\S]*?<\/script>/gi, '') .replace(/<[^>]+>/g, '') .replace(/ /g, ' ') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, "'") .replace(/\s+/g, ' ') .trim(); }; // Log email send attempt to database const logEmailSend = async ( recipient: string, subject: string, status: 'sent' | 'failed', messageId?: string, errorMessage?: string ) => { try { await prisma.emailLog.create({ data: { recipient, subject, status, messageId, errorMessage } }); } catch (error) { console.error('Failed to log email send:', error); } }; // Send a single email const sendSingleEmail = async ( transporter: nodemailer.Transporter, recipient: string, subject: string, htmlContent: string, textContent: string ): Promise => { const fromAddress = process.env.EMAIL_FROM || process.env.RESEND_FROM; const fromName = process.env.NEXT_PUBLIC_BRAND_NAME || 'HackerNews Newsletter'; try { const info = await transporter.sendMail({ from: `"${fromName}" <${fromAddress}>`, to: recipient, subject, text: textContent, html: htmlContent, headers: { 'List-Unsubscribe': `<${process.env.HOME_URL}/unsubscribe>`, 'X-Newsletter-ID': `hackernews-${Date.now()}` } }); await logEmailSend(recipient, subject, 'sent', info.messageId); return { success: true, messageId: info.messageId }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; await logEmailSend(recipient, subject, 'failed', undefined, errorMessage); return { success: false, error: errorMessage }; } }; // Main sender function - maintains same API as Resend version export async function sender( recipients: string[], { subject, template }: EmailTemplate ): Promise { const fromAddress = process.env.EMAIL_FROM || process.env.RESEND_FROM; if (!fromAddress) { throw new Error('EMAIL_FROM or RESEND_FROM environment variable is not set'); } if (recipients.length === 0) { console.info(`${subject} email skipped for having zero recipients`); return true; } const transporter = createTransporter(); // Render template to HTML const htmlContent = await renderTemplate(template); const textContent = htmlToPlainText(htmlContent); // Add small delay between sends to avoid overwhelming mail server const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); let successCount = 0; let failCount = 0; const errors: Array<{ email: string; error: string }> = []; for (const recipient of recipients) { const result = await sendSingleEmail( transporter, recipient, subject, htmlContent, textContent ); if (result.success) { successCount++; } else { failCount++; errors.push({ email: recipient, error: result.error || 'Unknown error' }); } // Add 100ms delay between sends if (recipients.length > 1) { await delay(100); } } // Close the transporter pool transporter.close(); if (errors.length > 0) { console.error('Email send errors:', errors); } console.info( `${subject} email: ${successCount} sent, ${failCount} failed out of ${recipients.length} recipients` ); // Return true if at least one email was sent successfully return successCount > 0; } // Verify transporter connection (useful for health checks) export async function verifyMailer(): Promise { const transporter = createTransporter(); try { await transporter.verify(); console.log('Email server connection verified'); transporter.close(); return true; } catch (error) { console.error('Email server connection failed:', error); transporter.close(); return false; } }