205 lines
5.0 KiB
JavaScript
205 lines
5.0 KiB
JavaScript
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,
|
|
});
|
|
}
|
|
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(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
|
|
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
|
|
.replace(/<br\s*\/?>/gi, "\n")
|
|
.replace(/<\/p>/gi, "\n\n")
|
|
.replace(/<\/div>/gi, "\n")
|
|
.replace(/<\/li>/gi, "\n")
|
|
.replace(/<\/h[1-6]>/gi, "\n\n")
|
|
.replace(/<li>/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<string>} 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,
|
|
};
|