import nodemailer from 'nodemailer'; import handlebars from 'handlebars'; import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Helper function to format currency with commas function formatCurrency(amount) { const num = typeof amount === 'number' ? (amount / 100).toFixed(2) : parseFloat(amount).toFixed(2); return num.replace(/\B(?=(\d{3})+(?!\d))/g, ','); } // Create transporter with connection pooling const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST || 'mail.puffinoffset.com', port: parseInt(process.env.SMTP_PORT) || 587, secure: process.env.SMTP_SECURE === 'true', // true for 465, false for other ports auth: { user: process.env.SMTP_USER || 'noreply@puffinoffset.com', pass: process.env.SMTP_PASSWORD }, pool: true, maxConnections: 5, maxMessages: 100, rateDelta: 1000, rateLimit: 10 }); // Verify connection configuration on startup export async function verifyConnection() { try { await transporter.verify(); console.log('✅ SMTP server is ready to send emails'); return true; } catch (error) { console.error('❌ SMTP connection failed:', error.message); return false; } } // Load and compile template async function loadTemplate(templateName) { try { const templatePath = path.join(__dirname, '..', 'templates', `${templateName}.hbs`); const templateSource = await fs.readFile(templatePath, 'utf-8'); return handlebars.compile(templateSource); } catch (error) { console.error(`Failed to load template ${templateName}:`, error.message); throw new Error(`Template ${templateName} not found`); } } // Send email using template export async function sendTemplateEmail(to, subject, templateName, data) { try { // Load and compile template const template = await loadTemplate(templateName); const html = template(data); // Create plain text version (strip HTML tags) const text = html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim(); // Send email const info = await transporter.sendMail({ from: `"${process.env.SMTP_FROM_NAME || 'Puffin Offset'}" <${process.env.SMTP_FROM_EMAIL || 'noreply@puffinoffset.com'}>`, to, subject, text, html }); console.log(`✉️ Email sent: ${info.messageId} to ${to}`); return { success: true, messageId: info.messageId, accepted: info.accepted, rejected: info.rejected }; } catch (error) { console.error('❌ Failed to send email:', error.message); throw error; } } // Send receipt email export async function sendReceiptEmail(customerEmail, orderDetails) { const subject = `Order Confirmation - ${orderDetails.tons} tons CO₂ Offset`; // Prepare template data const templateData = { tons: orderDetails.tons, portfolioId: orderDetails.portfolioId, baseAmount: formatCurrency(orderDetails.baseAmount), processingFee: formatCurrency(orderDetails.processingFee), totalAmount: formatCurrency(orderDetails.totalAmount), orderId: orderDetails.orderId, stripeSessionId: orderDetails.stripeSessionId, date: new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) }; // Add portfolio projects if available if (orderDetails.projects && orderDetails.projects.length > 0) { templateData.projects = orderDetails.projects; } // Add carbon comparisons if available if (orderDetails.comparisons && orderDetails.comparisons.length > 0) { templateData.comparisons = orderDetails.comparisons; } return await sendTemplateEmail( customerEmail, subject, 'receipt', templateData ); } // Send contact form email export async function sendContactEmail(contactData) { const subject = `Contact Form: ${contactData.name}`; const adminEmail = process.env.ADMIN_EMAIL || 'matt@puffinoffset.com'; return await sendTemplateEmail( adminEmail, subject, 'contact', { name: contactData.name, email: contactData.email, phone: contactData.phone || 'Not provided', company: contactData.company || 'Not provided', message: contactData.message, timestamp: new Date().toLocaleString('en-US', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' }) } ); } // Send admin notification for new order export async function sendAdminNotification(orderDetails, customerEmail) { // totalAmount is already in dollars (converted before calling this function) const totalAmount = typeof orderDetails.totalAmount === 'string' ? orderDetails.totalAmount : (orderDetails.totalAmount / 100).toFixed(2); // Format amount with commas for subject line const formattedAmount = parseFloat(totalAmount).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','); const subject = `New Order: ${orderDetails.tons} tons CO₂ - $${formattedAmount}`; const adminEmail = process.env.ADMIN_EMAIL || 'matt@puffinoffset.com'; // Check if admin notifications are enabled if (process.env.ADMIN_NOTIFY_ON_ORDER === 'false') { console.log('ℹ️ Admin notifications disabled, skipping'); return { success: true, skipped: true }; } return await sendTemplateEmail( adminEmail, subject, 'admin-notification', { tons: orderDetails.tons, portfolioId: orderDetails.portfolioId, totalAmount: totalAmount, orderId: orderDetails.orderId, customerEmail, timestamp: new Date().toLocaleString('en-US', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' }) } ); } // Close transporter (for graceful shutdown) export function closeTransporter() { transporter.close(); console.log('📪 Email transporter closed'); }