puffin-app/server/utils/emailService.js
Matt 0b66378423
All checks were successful
Build and Push Docker Images / docker (push) Successful in 58s
Update admin email to matt@puffinoffset.com
- Update default admin email from admin@ to matt@puffinoffset.com
- Add ADMIN_EMAIL environment variable to docker-compose.yml
- Add complete Email Configuration section to .env.example
- Admin email receives contact form submissions and order notifications

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-31 21:01:57 +01:00

189 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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