const { SESClient, SendEmailCommand } = require("@aws-sdk/client-ses"); const fs = require("fs").promises; const path = require("path"); const { getAWSConfig } = require("../config/aws"); const { User } = require("../models"); class EmailService { constructor() { this.sesClient = null; this.initialized = false; this.templates = new Map(); } async initialize() { if (this.initialized) return; try { // Use centralized AWS configuration with credential profiles const awsConfig = getAWSConfig(); this.sesClient = new SESClient(awsConfig); await this.loadEmailTemplates(); this.initialized = true; console.log("SES Email Service initialized successfully"); } catch (error) { console.error("Failed to initialize SES Email Service:", error); throw error; } } async loadEmailTemplates() { const templatesDir = path.join(__dirname, "..", "templates", "emails"); try { const templateFiles = [ "conditionCheckReminder.html", "rentalConfirmation.html", "lateReturnCS.html", "damageReportCS.html", "lostItemCS.html", ]; for (const templateFile of templateFiles) { try { const templatePath = path.join(templatesDir, templateFile); const templateContent = await fs.readFile(templatePath, "utf-8"); const templateName = path.basename(templateFile, ".html"); this.templates.set(templateName, templateContent); } catch (error) { console.warn(`Template ${templateFile} not found, will use fallback`); } } console.log(`Loaded ${this.templates.size} email templates`); } catch (error) { console.warn("Templates directory not found, using fallback templates"); } } /** * Convert HTML to plain text for email fallback * Strips HTML tags and formats content for plain text email clients */ htmlToPlainText(html) { return html // Remove style and script tags and their content .replace(/]*>[\s\S]*?<\/style>/gi, '') .replace(/]*>[\s\S]*?<\/script>/gi, '') // Convert common HTML elements to text equivalents .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, '• ') // Remove remaining HTML tags .replace(/<[^>]+>/g, '') // Decode HTML entities .replace(/ /g, ' ') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, "'") // Remove emojis and special characters that don't render well in plain text .replace(/[\u{1F600}-\u{1F64F}]/gu, '') // Emoticons .replace(/[\u{1F300}-\u{1F5FF}]/gu, '') // Misc Symbols and Pictographs .replace(/[\u{1F680}-\u{1F6FF}]/gu, '') // Transport and Map .replace(/[\u{2600}-\u{26FF}]/gu, '') // Misc symbols .replace(/[\u{2700}-\u{27BF}]/gu, '') // Dingbats .replace(/[\u{FE00}-\u{FE0F}]/gu, '') // Variation Selectors .replace(/[\u{1F900}-\u{1F9FF}]/gu, '') // Supplemental Symbols and Pictographs .replace(/[\u{1FA70}-\u{1FAFF}]/gu, '') // Symbols and Pictographs Extended-A // Clean up excessive whitespace .replace(/\n\s*\n\s*\n/g, '\n\n') .trim(); } async sendEmail(to, subject, htmlContent, textContent = null) { if (!this.initialized) { await this.initialize(); } if (!process.env.EMAIL_ENABLED || process.env.EMAIL_ENABLED !== "true") { console.log("Email sending disabled in environment"); return { success: true, messageId: "disabled" }; } // Auto-generate plain text from HTML if not provided if (!textContent) { textContent = this.htmlToPlainText(htmlContent); } // Use friendly sender name format for better recognition const fromName = process.env.SES_FROM_NAME || "RentAll"; const fromEmail = process.env.SES_FROM_EMAIL; 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: htmlContent, Charset: "UTF-8", }, Text: { Data: textContent, Charset: "UTF-8", }, }, }, }; if (process.env.SES_REPLY_TO_EMAIL) { params.ReplyToAddresses = [process.env.SES_REPLY_TO_EMAIL]; } try { const command = new SendEmailCommand(params); const result = await this.sesClient.send(command); console.log( `Email sent successfully to ${to}, MessageId: ${result.MessageId}` ); return { success: true, messageId: result.MessageId }; } catch (error) { console.error("Failed to send email:", error); return { success: false, error: error.message }; } } renderTemplate(templateName, variables = {}) { let template = this.templates.get(templateName); if (!template) { template = this.getFallbackTemplate(templateName); } let rendered = template; Object.keys(variables).forEach((key) => { const regex = new RegExp(`{{${key}}}`, "g"); rendered = rendered.replace(regex, variables[key] || ""); }); return rendered; } getFallbackTemplate(templateName) { const baseTemplate = ` {{title}}
    {{content}}
    `; const templates = { conditionCheckReminder: baseTemplate.replace( "{{content}}", `

    {{title}}

    {{message}}

    Rental Item: {{itemName}}

    Deadline: {{deadline}}

    Please complete this condition check as soon as possible to ensure proper documentation.

    ` ), rentalConfirmation: baseTemplate.replace( "{{content}}", `

    Hi {{recipientName}},

    {{title}}

    {{message}}

    Item: {{itemName}}

    Rental Period: {{startDate}} to {{endDate}}

    Thank you for using RentAll!

    ` ), }; return ( templates[templateName] || baseTemplate.replace( "{{content}}", `

    {{title}}

    {{message}}

    ` ) ); } async sendConditionCheckReminder(userEmail, notification, rental) { const variables = { title: notification.title, message: notification.message, itemName: rental?.item?.name || "Unknown Item", deadline: notification.metadata?.deadline ? new Date(notification.metadata.deadline).toLocaleDateString() : "Not specified", }; const htmlContent = this.renderTemplate( "conditionCheckReminder", variables ); return await this.sendEmail( userEmail, `RentAll: ${notification.title}`, htmlContent ); } async sendRentalConfirmation(userEmail, notification, rental, recipientName = null) { const itemName = rental?.item?.name || "Unknown Item"; const variables = { recipientName: recipientName || "there", title: notification.title, message: notification.message, itemName: itemName, startDate: rental?.startDateTime ? new Date(rental.startDateTime).toLocaleDateString() : "Not specified", endDate: rental?.endDateTime ? new Date(rental.endDateTime).toLocaleDateString() : "Not specified", }; const htmlContent = this.renderTemplate("rentalConfirmation", variables); // Use clear, transactional subject line with item name const subject = `Rental Confirmation - ${itemName}`; return await this.sendEmail( userEmail, subject, htmlContent ); } async sendTemplateEmail(toEmail, subject, templateName, variables = {}) { const htmlContent = this.renderTemplate(templateName, variables); return await this.sendEmail(toEmail, subject, htmlContent); } async sendLateReturnToCustomerService(rental, lateCalculation) { try { // Get owner and renter details const owner = await User.findByPk(rental.ownerId); const renter = await User.findByPk(rental.renterId); if (!owner || !renter) { console.error("Owner or renter not found for late return notification"); return; } // Format dates const scheduledEnd = new Date(rental.endDateTime).toLocaleString(); const actualReturn = new Date( rental.actualReturnDateTime ).toLocaleString(); // Send email to customer service await this.sendTemplateEmail( process.env.CUSTOMER_SUPPORT_EMAIL, "Late Return Detected - Action Required", "lateReturnCS", { rentalId: rental.id, itemName: rental.item.name, ownerName: owner.name, ownerEmail: owner.email, renterName: renter.name, renterEmail: renter.email, scheduledEnd, actualReturn, hoursLate: lateCalculation.lateHours.toFixed(1), lateFee: lateCalculation.lateFee.toFixed(2), } ); console.log( `Late return notification sent to customer service for rental ${rental.id}` ); } catch (error) { console.error( "Failed to send late return notification to customer service:", error ); } } async sendDamageReportToCustomerService( rental, damageAssessment, lateCalculation = null ) { try { // Get owner and renter details const owner = await User.findByPk(rental.ownerId); const renter = await User.findByPk(rental.renterId); if (!owner || !renter) { console.error( "Owner or renter not found for damage report notification" ); return; } // Calculate total fees const damageFee = damageAssessment.feeCalculation.amount; const lateFee = lateCalculation?.lateFee || 0; const totalFees = damageFee + lateFee; // Determine fee type description let feeTypeDescription = ""; if (damageAssessment.feeCalculation.type === "repair") { feeTypeDescription = "Repair Cost"; } else if (damageAssessment.feeCalculation.type === "replacement") { feeTypeDescription = "Replacement Cost"; } else { feeTypeDescription = "Damage Assessment Fee"; } // Send email to customer service await this.sendTemplateEmail( process.env.CUSTOMER_SUPPORT_EMAIL, "Damage Report Filed - Action Required", "damageReportCS", { rentalId: rental.id, itemName: rental.item.name, ownerName: `${owner.firstName} ${owner.lastName}`, ownerEmail: owner.email, renterName: `${renter.firstName} ${renter.lastName}`, renterEmail: renter.email, damageDescription: damageAssessment.description, canBeFixed: damageAssessment.canBeFixed ? "Yes" : "No", repairCost: damageAssessment.repairCost ? damageAssessment.repairCost.toFixed(2) : "N/A", needsReplacement: damageAssessment.needsReplacement ? "Yes" : "No", replacementCost: damageAssessment.replacementCost ? damageAssessment.replacementCost.toFixed(2) : "N/A", feeTypeDescription, damageFee: damageFee.toFixed(2), lateFee: lateFee.toFixed(2), totalFees: totalFees.toFixed(2), hasProofOfOwnership: damageAssessment.proofOfOwnership && damageAssessment.proofOfOwnership.length > 0 ? "Yes" : "No", } ); console.log( `Damage report notification sent to customer service for rental ${rental.id}` ); } catch (error) { console.error( "Failed to send damage report notification to customer service:", error ); } } async sendLostItemToCustomerService(rental) { try { // Get owner and renter details const owner = await User.findByPk(rental.ownerId); const renter = await User.findByPk(rental.renterId); if (!owner || !renter) { console.error("Owner or renter not found for lost item notification"); return; } // Format dates const reportedAt = new Date(rental.itemLostReportedAt).toLocaleString(); const scheduledReturnDate = new Date(rental.endDateTime).toLocaleString(); // Send email to customer service await this.sendTemplateEmail( process.env.CUSTOMER_SUPPORT_EMAIL, "Lost Item Claim Filed - Action Required", "lostItemCS", { rentalId: rental.id, itemName: rental.item.name, ownerName: `${owner.firstName} ${owner.lastName}`, ownerEmail: owner.email, renterName: `${renter.firstName} ${renter.lastName}`, renterEmail: renter.email, reportedAt, scheduledReturnDate, replacementCost: parseFloat(rental.item.replacementCost).toFixed(2), } ); console.log( `Lost item notification sent to customer service for rental ${rental.id}` ); } catch (error) { console.error( "Failed to send lost item notification to customer service:", error ); } } async sendRentalConfirmationEmails(rental) { const results = { ownerEmailSent: false, renterEmailSent: false, }; try { // Get owner and renter details const owner = await User.findByPk(rental.ownerId, { attributes: ["email", "firstName"], }); const renter = await User.findByPk(rental.renterId, { attributes: ["email", "firstName"], }); // Create notification data for owner const ownerNotification = { type: "rental_confirmed", title: "Rental Confirmed", message: `Your "${rental.item.name}" has been confirmed for rental.`, rentalId: rental.id, userId: rental.ownerId, metadata: { rentalStart: rental.startDateTime }, }; // Create notification data for renter const renterNotification = { type: "rental_confirmed", title: "Rental Confirmed", message: `Your rental of "${rental.item.name}" has been confirmed.`, rentalId: rental.id, userId: rental.renterId, metadata: { rentalStart: rental.startDateTime }, }; // Send email to owner - independent error handling if (owner?.email) { try { const ownerResult = await this.sendRentalConfirmation( owner.email, ownerNotification, rental, owner.firstName ); if (ownerResult.success) { console.log( `Rental confirmation email sent to owner: ${owner.email}` ); results.ownerEmailSent = true; } else { console.error( `Failed to send rental confirmation email to owner (${owner.email}):`, ownerResult.error ); } } catch (error) { console.error( `Failed to send rental confirmation email to owner (${owner.email}):`, error.message ); } } // Send email to renter - independent error handling if (renter?.email) { try { const renterResult = await this.sendRentalConfirmation( renter.email, renterNotification, rental, renter.firstName ); if (renterResult.success) { console.log( `Rental confirmation email sent to renter: ${renter.email}` ); results.renterEmailSent = true; } else { console.error( `Failed to send rental confirmation email to renter (${renter.email}):`, renterResult.error ); } } catch (error) { console.error( `Failed to send rental confirmation email to renter (${renter.email}):`, error.message ); } } } catch (error) { console.error( "Error fetching user data for rental confirmation emails:", error ); } return results; } } module.exports = new EmailService();