puffin-app/server/utils/emailService.js

189 lines
5.6 KiB
JavaScript
Raw Normal View History

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 || 'admin@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) {
const subject = `New Order: ${orderDetails.tons} tons CO₂ - $${(orderDetails.totalAmount / 100).toFixed(2)}`;
const adminEmail = process.env.ADMIN_EMAIL || 'admin@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: (orderDetails.totalAmount / 100).toFixed(2),
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');
}