const { SESClient, SendEmailCommand } = require("@aws-sdk/client-ses"); const fs = require("fs").promises; const path = require("path"); let sesClient = null; /** * Get or create an SES client. * Reuses client across Lambda invocations for better performance. */ function getSESClient() { if (!sesClient) { sesClient = new SESClient({ region: process.env.AWS_REGION || "us-east-1", }); } return sesClient; } /** * Convert HTML to plain text for email fallback. * @param {string} html - HTML content to convert * @returns {string} Plain text version */ function htmlToPlainText(html) { return html .replace(/]*>[\s\S]*?<\/style>/gi, "") .replace(/]*>[\s\S]*?<\/script>/gi, "") .replace(//gi, "\n") .replace(/<\/p>/gi, "\n\n") .replace(/<\/div>/gi, "\n") .replace(/<\/li>/gi, "\n") .replace(/<\/h[1-6]>/gi, "\n\n") .replace(/
  • /gi, "- ") .replace(/<[^>]+>/g, "") .replace(/ /g, " ") .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, '"') .replace(/'/g, "'") .replace(/\n\s*\n\s*\n/g, "\n\n") .trim(); } /** * Render a template by replacing {{variable}} placeholders with values. * @param {string} template - Template string with {{placeholders}} * @param {Object} variables - Key-value pairs to substitute * @returns {string} Rendered template */ function renderTemplate(template, variables = {}) { let rendered = template; Object.keys(variables).forEach((key) => { const regex = new RegExp(`{{${key}}}`, "g"); rendered = rendered.replace(regex, variables[key] ?? ""); }); return rendered; } /** * Load a template file from disk. * @param {string} templatePath - Absolute path to the template file * @returns {Promise} Template content */ async function loadTemplate(templatePath) { try { return await fs.readFile(templatePath, "utf-8"); } catch (error) { console.error(JSON.stringify({ level: "error", message: "Failed to load email template", templatePath, error: error.message, })); throw error; } } /** * Send an email using AWS SES. * @param {string|string[]} to - Recipient email address(es) * @param {string} subject - Email subject line * @param {string} htmlBody - HTML content of the email * @param {string|null} textBody - Plain text content (auto-generated if not provided) * @returns {Promise<{success: boolean, messageId?: string, error?: string}>} */ async function sendEmail(to, subject, htmlBody, textBody = null) { // Check if email sending is enabled if (process.env.EMAIL_ENABLED !== "true") { console.log(JSON.stringify({ level: "info", message: "Email sending disabled, skipping", to, subject, })); return { success: true, messageId: "disabled" }; } const client = getSESClient(); // Auto-generate plain text from HTML if not provided const plainText = textBody || htmlToPlainText(htmlBody); // Build sender address with friendly name const fromName = process.env.SES_FROM_NAME || "Village Share"; const fromEmail = process.env.SES_FROM_EMAIL; if (!fromEmail) { throw new Error("SES_FROM_EMAIL environment variable is required"); } const source = `${fromName} <${fromEmail}>`; const params = { Source: source, Destination: { ToAddresses: Array.isArray(to) ? to : [to], }, Message: { Subject: { Data: subject, Charset: "UTF-8", }, Body: { Html: { Data: htmlBody, Charset: "UTF-8", }, Text: { Data: plainText, Charset: "UTF-8", }, }, }, }; // Add reply-to if configured if (process.env.SES_REPLY_TO_EMAIL) { params.ReplyToAddresses = [process.env.SES_REPLY_TO_EMAIL]; } try { const command = new SendEmailCommand(params); const result = await client.send(command); console.log(JSON.stringify({ level: "info", message: "Email sent successfully", to, subject, messageId: result.MessageId, })); return { success: true, messageId: result.MessageId }; } catch (error) { console.error(JSON.stringify({ level: "error", message: "Failed to send email", to, subject, error: error.message, })); return { success: false, error: error.message }; } } /** * Format a date for email display. * @param {Date|string} date - Date to format * @returns {string} Formatted date string */ function formatEmailDate(date) { const dateObj = typeof date === "string" ? new Date(date) : date; return dateObj.toLocaleString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric", hour: "numeric", minute: "2-digit", hour12: true, timeZone: "America/New_York", }); } module.exports = { sendEmail, loadTemplate, renderTemplate, htmlToPlainText, formatEmailDate, };