From 629f0055a12c8c1d4e20775da58e403036d3c7b4 Mon Sep 17 00:00:00 2001
From: jackiettran <41605212+jackiettran@users.noreply.github.com>
Date: Fri, 14 Nov 2025 15:51:25 -0500
Subject: [PATCH] integrated email is forum posts/comments
---
backend/routes/forum.js | 139 +++++-
backend/services/emailService.js | 402 +++++++++++++++---
.../forumAnswerAcceptedToCommentAuthor.html | 312 ++++++++++++++
.../emails/forumCommentToPostAuthor.html | 271 ++++++++++++
.../emails/forumReplyToCommentAuthor.html | 303 +++++++++++++
.../forumThreadActivityToParticipant.html | 281 ++++++++++++
6 files changed, 1647 insertions(+), 61 deletions(-)
create mode 100644 backend/templates/emails/forumAnswerAcceptedToCommentAuthor.html
create mode 100644 backend/templates/emails/forumCommentToPostAuthor.html
create mode 100644 backend/templates/emails/forumReplyToCommentAuthor.html
create mode 100644 backend/templates/emails/forumThreadActivityToParticipant.html
diff --git a/backend/routes/forum.js b/backend/routes/forum.js
index d212c0a..4d5f385 100644
--- a/backend/routes/forum.js
+++ b/backend/routes/forum.js
@@ -4,6 +4,7 @@ const { ForumPost, ForumComment, PostTag, User } = require('../models');
const { authenticateToken } = require('../middleware/auth');
const { uploadForumPostImages, uploadForumCommentImages } = require('../middleware/upload');
const logger = require('../utils/logger');
+const emailService = require('../services/emailService');
const router = express.Router();
// Helper function to build nested comment tree
@@ -468,6 +469,46 @@ router.patch('/posts/:id/accept-answer', authenticateToken, async (req, res) =>
status: commentId ? 'answered' : 'open'
});
+ // Send email notification if marking an answer (not unmarking)
+ if (commentId) {
+ (async () => {
+ try {
+ const comment = await ForumComment.findByPk(commentId, {
+ include: [
+ {
+ model: User,
+ as: 'author',
+ attributes: ['id', 'username', 'firstName', 'lastName', 'email']
+ }
+ ]
+ });
+
+ const postAuthor = await User.findByPk(req.user.id, {
+ attributes: ['id', 'username', 'firstName', 'lastName', 'email']
+ });
+
+ // Only send email if not marking your own comment as answer
+ if (comment && comment.authorId !== req.user.id) {
+ await emailService.sendForumAnswerAcceptedNotification(
+ comment.author,
+ postAuthor,
+ post,
+ comment
+ );
+ }
+ } catch (emailError) {
+ // Email errors don't block answer marking
+ logger.error("Failed to send answer accepted notification email", {
+ error: emailError.message,
+ stack: emailError.stack,
+ commentId: commentId,
+ postId: req.params.id
+ });
+ console.error("Email notification error:", emailError);
+ }
+ })();
+ }
+
const updatedPost = await ForumPost.findByPk(post.id, {
include: [
{
@@ -540,11 +581,107 @@ router.post('/posts/:id/comments', authenticateToken, uploadForumCommentImages,
{
model: User,
as: 'author',
- attributes: ['id', 'username', 'firstName', 'lastName']
+ attributes: ['id', 'username', 'firstName', 'lastName', 'email']
}
]
});
+ // Send email notifications (non-blocking)
+ (async () => {
+ try {
+ const commenter = commentWithDetails.author;
+ const notifiedUserIds = new Set();
+
+ // Reload post with author details for email
+ const postWithAuthor = await ForumPost.findByPk(req.params.id, {
+ include: [
+ {
+ model: User,
+ as: 'author',
+ attributes: ['id', 'username', 'firstName', 'lastName', 'email']
+ }
+ ]
+ });
+
+ // If this is a reply, send reply notification to parent comment author
+ if (parentCommentId) {
+ const parentComment = await ForumComment.findByPk(parentCommentId, {
+ include: [
+ {
+ model: User,
+ as: 'author',
+ attributes: ['id', 'username', 'firstName', 'lastName', 'email']
+ }
+ ]
+ });
+
+ // Send reply notification if not replying to yourself
+ if (parentComment && parentComment.authorId !== req.user.id) {
+ await emailService.sendForumReplyNotification(
+ parentComment.author,
+ commenter,
+ postWithAuthor,
+ commentWithDetails,
+ parentComment
+ );
+ notifiedUserIds.add(parentComment.authorId);
+ }
+ } else {
+ // Send comment notification to post author if not commenting on your own post
+ if (postWithAuthor.authorId !== req.user.id) {
+ await emailService.sendForumCommentNotification(
+ postWithAuthor.author,
+ commenter,
+ postWithAuthor,
+ commentWithDetails
+ );
+ notifiedUserIds.add(postWithAuthor.authorId);
+ }
+ }
+
+ // Get all unique participants who have commented on this post (excluding commenter and already notified)
+ const participants = await ForumComment.findAll({
+ where: {
+ postId: req.params.id,
+ authorId: {
+ [Op.notIn]: [req.user.id, ...Array.from(notifiedUserIds)]
+ },
+ isDeleted: false
+ },
+ attributes: ['authorId'],
+ include: [
+ {
+ model: User,
+ as: 'author',
+ attributes: ['id', 'username', 'firstName', 'lastName', 'email']
+ }
+ ],
+ group: ['ForumComment.authorId', 'author.id']
+ });
+
+ // Send thread activity notifications to all unique participants
+ for (const participant of participants) {
+ if (participant.author) {
+ await emailService.sendForumThreadActivityNotification(
+ participant.author,
+ commenter,
+ postWithAuthor,
+ commentWithDetails
+ );
+ }
+ }
+ } catch (emailError) {
+ // Email errors don't block comment creation
+ logger.error("Failed to send forum comment notification emails", {
+ error: emailError.message,
+ stack: emailError.stack,
+ commentId: comment.id,
+ postId: req.params.id
+ });
+ console.error("Email notification error:", emailError);
+ }
+ })();
+
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Forum comment created", {
postId: req.params.id,
diff --git a/backend/services/emailService.js b/backend/services/emailService.js
index 441f0d4..1ddac48 100644
--- a/backend/services/emailService.js
+++ b/backend/services/emailService.js
@@ -55,6 +55,10 @@ class EmailService {
"feedbackConfirmationToUser.html",
"feedbackNotificationToAdmin.html",
"newMessageToUser.html",
+ "forumCommentToPostAuthor.html",
+ "forumReplyToCommentAuthor.html",
+ "forumAnswerAcceptedToCommentAuthor.html",
+ "forumThreadActivityToParticipant.html",
];
for (const templateFile of templateFiles) {
@@ -65,14 +69,23 @@ class EmailService {
this.templates.set(templateName, templateContent);
console.log(`✓ Loaded template: ${templateName}`);
} catch (error) {
- console.error(`✗ Failed to load template ${templateFile}:`, error.message);
- console.error(` Template path: ${path.join(templatesDir, templateFile)}`);
+ console.error(
+ `✗ Failed to load template ${templateFile}:`,
+ error.message
+ );
+ console.error(
+ ` Template path: ${path.join(templatesDir, templateFile)}`
+ );
}
}
- console.log(`Loaded ${this.templates.size} of ${templateFiles.length} email templates`);
+ console.log(
+ `Loaded ${this.templates.size} of ${templateFiles.length} email templates`
+ );
} catch (error) {
- console.warn("Templates directory not found, using fallback templates");
+ console.error("Failed to load email templates:", error);
+ console.error("Templates directory:", templatesDir);
+ console.error("Error stack:", error.stack);
}
}
@@ -178,19 +191,39 @@ class EmailService {
}
}
- renderTemplate(templateName, variables = {}) {
+ async renderTemplate(templateName, variables = {}) {
+ // Ensure service is initialized before rendering
+ if (!this.initialized) {
+ console.log(`Email service not initialized yet, initializing now...`);
+ await this.initialize();
+ }
+
let template = this.templates.get(templateName);
if (!template) {
+ console.error(`Template not found: ${templateName}`);
+ console.error(
+ `Available templates: ${Array.from(this.templates.keys()).join(", ")}`
+ );
+ console.error(`Stack trace:`, new Error().stack);
+ console.log(`Using fallback template for: ${templateName}`);
template = this.getFallbackTemplate(templateName);
+ } else {
+ console.log(`✓ Template found: ${templateName}`);
}
let rendered = template;
- Object.keys(variables).forEach((key) => {
- const regex = new RegExp(`{{${key}}}`, "g");
- rendered = rendered.replace(regex, variables[key] || "");
- });
+ try {
+ Object.keys(variables).forEach((key) => {
+ const regex = new RegExp(`{{${key}}}`, "g");
+ rendered = rendered.replace(regex, variables[key] || "");
+ });
+ } catch (error) {
+ console.error(`Error rendering template ${templateName}:`, error);
+ console.error(`Stack trace:`, error.stack);
+ console.error(`Variables provided:`, Object.keys(variables));
+ }
return rendered;
}
@@ -504,7 +537,7 @@ class EmailService {
: "Not specified",
};
- const htmlContent = this.renderTemplate(
+ const htmlContent = await this.renderTemplate(
"conditionCheckReminderToUser",
variables
);
@@ -543,25 +576,26 @@ class EmailService {
let paymentSection = "";
if (isRenter) {
const totalAmount = parseFloat(rental.totalAmount) || 0;
- const isPaidRental = totalAmount > 0 && rental.paymentStatus === 'paid';
+ const isPaidRental = totalAmount > 0 && rental.paymentStatus === "paid";
if (isPaidRental) {
// Format payment method display
let paymentMethodDisplay = "Payment method on file";
if (rental.paymentMethodBrand && rental.paymentMethodLast4) {
- const brandCapitalized = rental.paymentMethodBrand.charAt(0).toUpperCase() +
- rental.paymentMethodBrand.slice(1);
+ const brandCapitalized =
+ rental.paymentMethodBrand.charAt(0).toUpperCase() +
+ rental.paymentMethodBrand.slice(1);
paymentMethodDisplay = `${brandCapitalized} ending in ${rental.paymentMethodLast4}`;
}
const chargedAtFormatted = rental.chargedAt
? new Date(rental.chargedAt).toLocaleString("en-US", {
dateStyle: "medium",
- timeStyle: "short"
+ timeStyle: "short",
})
: new Date().toLocaleString("en-US", {
dateStyle: "medium",
- timeStyle: "short"
+ timeStyle: "short",
});
// Build payment receipt section HTML
@@ -583,7 +617,9 @@ class EmailService {
| Transaction ID |
- ${rental.stripePaymentIntentId || "N/A"} |
+ ${
+ rental.stripePaymentIntentId || "N/A"
+ } |
| Transaction Date |
@@ -606,7 +642,10 @@ class EmailService {
variables.paymentSection = paymentSection;
- const htmlContent = this.renderTemplate("rentalConfirmationToUser", variables);
+ const htmlContent = await this.renderTemplate(
+ "rentalConfirmationToUser",
+ variables
+ );
// Use clear, transactional subject line with item name
const subject = `Rental Confirmation - ${itemName}`;
@@ -623,7 +662,10 @@ class EmailService {
verificationUrl: verificationUrl,
};
- const htmlContent = this.renderTemplate("emailVerificationToUser", variables);
+ const htmlContent = await this.renderTemplate(
+ "emailVerificationToUser",
+ variables
+ );
return await this.sendEmail(
user.email,
@@ -633,11 +675,6 @@ class EmailService {
}
async sendAlphaInvitation(email, code) {
- // Ensure service is initialized before rendering template
- if (!this.initialized) {
- await this.initialize();
- }
-
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
const variables = {
@@ -648,7 +685,10 @@ class EmailService {
message: `You've been invited to join our exclusive alpha testing program. Use the code ${code} to unlock access and be among the first to experience our platform.`,
};
- const htmlContent = this.renderTemplate("alphaInvitationToUser", variables);
+ const htmlContent = await this.renderTemplate(
+ "alphaInvitationToUser",
+ variables
+ );
return await this.sendEmail(
email,
@@ -666,7 +706,10 @@ class EmailService {
resetUrl: resetUrl,
};
- const htmlContent = this.renderTemplate("passwordResetToUser", variables);
+ const htmlContent = await this.renderTemplate(
+ "passwordResetToUser",
+ variables
+ );
return await this.sendEmail(
user.email,
@@ -687,7 +730,10 @@ class EmailService {
timestamp: timestamp,
};
- const htmlContent = this.renderTemplate("passwordChangedToUser", variables);
+ const htmlContent = await this.renderTemplate(
+ "passwordChangedToUser",
+ variables
+ );
return await this.sendEmail(
user.email,
@@ -744,7 +790,10 @@ class EmailService {
approveUrl: approveUrl,
};
- const htmlContent = this.renderTemplate("rentalRequestToOwner", variables);
+ const htmlContent = await this.renderTemplate(
+ "rentalRequestToOwner",
+ variables
+ );
return await this.sendEmail(
owner.email,
@@ -797,7 +846,7 @@ class EmailService {
viewRentalsUrl: viewRentalsUrl,
};
- const htmlContent = this.renderTemplate(
+ const htmlContent = await this.renderTemplate(
"rentalRequestConfirmationToRenter",
variables
);
@@ -819,9 +868,7 @@ class EmailService {
});
if (!renter) {
- console.error(
- "Renter not found for rental decline notification"
- );
+ console.error("Renter not found for rental decline notification");
return { success: false, error: "Renter not found" };
}
@@ -867,7 +914,7 @@ class EmailService {
totalAmount: totalAmount.toFixed(2),
};
- const htmlContent = this.renderTemplate(
+ const htmlContent = await this.renderTemplate(
"rentalDeclinedToRenter",
variables
);
@@ -920,11 +967,16 @@ class EmailService {
earningsDashboardUrl: earningsDashboardUrl,
};
- const htmlContent = this.renderTemplate("payoutReceivedToOwner", variables);
+ const htmlContent = await this.renderTemplate(
+ "payoutReceivedToOwner",
+ variables
+ );
return await this.sendEmail(
owner.email,
- `Earnings Received - $${payoutAmount.toFixed(2)} for ${rental.item?.name || "Your Item"}`,
+ `Earnings Received - $${payoutAmount.toFixed(2)} for ${
+ rental.item?.name || "Your Item"
+ }`,
htmlContent
);
}
@@ -994,7 +1046,9 @@ class EmailService {
additionalInfo = `
Full Refund Processed
-
You will receive a full refund of $${refundInfo.amount.toFixed(2)}. The refund will appear in your account within 5-10 business days.
+
You will receive a full refund of $${refundInfo.amount.toFixed(
+ 2
+ )}. The refund will appear in your account within 5-10 business days.
Browse Other Items
@@ -1035,7 +1089,9 @@ class EmailService {
Refund Information
$${refundInfo.amount.toFixed(2)}
-
Refund Amount: $${refundInfo.amount.toFixed(2)} (${refundPercentage}% of total)
+
Refund Amount: $${refundInfo.amount.toFixed(
+ 2
+ )} (${refundPercentage}% of total)
Reason: ${refundInfo.reason}
Processing Time: Refunds typically appear within 5-10 business days.
@@ -1063,7 +1119,7 @@ class EmailService {
refundSection: refundSection,
};
- const confirmationHtml = this.renderTemplate(
+ const confirmationHtml = await this.renderTemplate(
"rentalCancellationConfirmationToUser",
confirmationVariables
);
@@ -1099,7 +1155,7 @@ class EmailService {
additionalInfo: additionalInfo,
};
- const notificationHtml = this.renderTemplate(
+ const notificationHtml = await this.renderTemplate(
"rentalCancellationNotificationToUser",
notificationVariables
);
@@ -1112,7 +1168,9 @@ class EmailService {
if (notificationResult.success) {
console.log(
- `Cancellation notification email sent to ${cancelledBy === "owner" ? "renter" : "owner"}: ${notificationRecipient}`
+ `Cancellation notification email sent to ${
+ cancelledBy === "owner" ? "renter" : "owner"
+ }: ${notificationRecipient}`
);
results.notificationEmailSent = true;
}
@@ -1130,7 +1188,7 @@ class EmailService {
}
async sendTemplateEmail(toEmail, subject, templateName, variables = {}) {
- const htmlContent = this.renderTemplate(templateName, variables);
+ const htmlContent = await this.renderTemplate(templateName, variables);
return await this.sendEmail(toEmail, subject, htmlContent);
}
@@ -1413,7 +1471,7 @@ class EmailService {
viewItemUrl: `${frontendUrl}/items/${item.id}`,
};
- const htmlContent = this.renderTemplate(
+ const htmlContent = await this.renderTemplate(
"firstListingCelebrationToOwner",
variables
);
@@ -1428,7 +1486,12 @@ class EmailService {
// Fetch owner details
const owner = await User.findByPk(rental.ownerId, {
- attributes: ["email", "firstName", "lastName", "stripeConnectedAccountId"],
+ attributes: [
+ "email",
+ "firstName",
+ "lastName",
+ "stripeConnectedAccountId",
+ ],
});
// Fetch renter details
@@ -1487,7 +1550,9 @@ class EmailService {
stripeSection = `
⚠️ Action Required: Set Up Your Earnings Account
-
To receive your payout of \$${payoutAmount.toFixed(2)} when this rental completes, you need to set up your earnings account.
+
To receive your payout of \$${payoutAmount.toFixed(
+ 2
+ )} when this rental completes, you need to set up your earnings account.
Set Up Earnings to Get Paid
@@ -1511,19 +1576,23 @@ class EmailService {
stripeSection = `
✓ Earnings Account Active
-
Your earnings account is set up. You'll automatically receive \$${payoutAmount.toFixed(2)} when this rental completes.
+
Your earnings account is set up. You'll automatically receive \$${payoutAmount.toFixed(
+ 2
+ )} when this rental completes.
View your earnings dashboard →
`;
}
// Format delivery method for display
- const deliveryMethodDisplay = rental.deliveryMethod === "delivery" ? "Delivery" : "Pickup";
+ const deliveryMethodDisplay =
+ rental.deliveryMethod === "delivery" ? "Delivery" : "Pickup";
const variables = {
ownerName: owner.firstName || "there",
itemName: rental.item?.name || "your item",
- renterName: `${renter.firstName} ${renter.lastName}`.trim() || "The renter",
+ renterName:
+ `${renter.firstName} ${renter.lastName}`.trim() || "The renter",
startDate: rental.startDateTime
? new Date(rental.startDateTime).toLocaleString("en-US", {
dateStyle: "medium",
@@ -1543,7 +1612,7 @@ class EmailService {
rentalDetailsUrl: `${frontendUrl}/owning?rentalId=${rental.id}`,
};
- const htmlContent = this.renderTemplate(
+ const htmlContent = await this.renderTemplate(
"rentalApprovalConfirmationToOwner",
variables
);
@@ -1563,7 +1632,12 @@ class EmailService {
try {
// Fetch owner details with Stripe info
const owner = await User.findByPk(rental.ownerId, {
- attributes: ["email", "firstName", "lastName", "stripeConnectedAccountId"],
+ attributes: [
+ "email",
+ "firstName",
+ "lastName",
+ "stripeConnectedAccountId",
+ ],
});
// Fetch renter details
@@ -1572,9 +1646,7 @@ class EmailService {
});
if (!owner || !renter) {
- console.error(
- "Owner or renter not found for rental completion emails"
- );
+ console.error("Owner or renter not found for rental completion emails");
return { success: false, error: "User not found" };
}
@@ -1641,7 +1713,7 @@ class EmailService {
browseItemsUrl: `${frontendUrl}/`,
};
- const renterHtmlContent = this.renderTemplate(
+ const renterHtmlContent = await this.renderTemplate(
"rentalCompletionThankYouToRenter",
renterVariables
);
@@ -1709,7 +1781,9 @@ class EmailService {
stripeSection = `
⚠️ Action Required: Set Up Your Earnings Account
-
To receive your payout of \$${payoutAmount.toFixed(2)}, you need to set up your earnings account.
+
To receive your payout of \$${payoutAmount.toFixed(
+ 2
+ )}, you need to set up your earnings account.
Set Up Earnings to Get Paid
@@ -1733,7 +1807,9 @@ class EmailService {
stripeSection = `
✓ Earnings Account Active
-
Your earnings account is set up. You'll automatically receive \$${payoutAmount.toFixed(2)} when the rental period ends.
+
Your earnings account is set up. You'll automatically receive \$${payoutAmount.toFixed(
+ 2
+ )} when the rental period ends.
View your earnings dashboard →
`;
@@ -1744,7 +1820,8 @@ class EmailService {
const ownerVariables = {
ownerName: owner.firstName || "there",
itemName: rental.item?.name || "your item",
- renterName: `${renter.firstName} ${renter.lastName}`.trim() || "The renter",
+ renterName:
+ `${renter.firstName} ${renter.lastName}`.trim() || "The renter",
startDate: startDate,
endDate: endDate,
returnedDate: returnedDate,
@@ -1753,7 +1830,7 @@ class EmailService {
owningUrl: `${frontendUrl}/owning`,
};
- const ownerHtmlContent = this.renderTemplate(
+ const ownerHtmlContent = await this.renderTemplate(
"rentalCompletionCongratsToOwner",
ownerVariables
);
@@ -1802,7 +1879,7 @@ class EmailService {
year: new Date().getFullYear(),
};
- const htmlContent = this.renderTemplate(
+ const htmlContent = await this.renderTemplate(
"feedbackConfirmationToUser",
variables
);
@@ -1815,7 +1892,8 @@ class EmailService {
}
async sendFeedbackNotificationToAdmin(user, feedback) {
- const adminEmail = process.env.FEEDBACK_EMAIL || process.env.CUSTOMER_SUPPORT_EMAIL;
+ const adminEmail =
+ process.env.FEEDBACK_EMAIL || process.env.CUSTOMER_SUPPORT_EMAIL;
if (!adminEmail) {
console.warn("No admin email configured for feedback notifications");
@@ -1839,7 +1917,7 @@ class EmailService {
year: new Date().getFullYear(),
};
- const htmlContent = this.renderTemplate(
+ const htmlContent = await this.renderTemplate(
"feedbackNotificationToAdmin",
variables
);
@@ -1870,7 +1948,10 @@ class EmailService {
timestamp: timestamp,
};
- const htmlContent = this.renderTemplate("newMessageToUser", variables);
+ const htmlContent = await this.renderTemplate(
+ "newMessageToUser",
+ variables
+ );
const subject = `New message from ${sender.firstName} ${sender.lastName}`;
@@ -1888,6 +1969,207 @@ class EmailService {
return { success: false, error: error.message };
}
}
+
+ async sendForumCommentNotification(postAuthor, commenter, post, comment) {
+ try {
+ const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
+ const postUrl = `${frontendUrl}/forum/posts/${post.id}`;
+
+ const timestamp = new Date(comment.createdAt).toLocaleString("en-US", {
+ dateStyle: "medium",
+ timeStyle: "short",
+ });
+
+ const variables = {
+ postAuthorName: postAuthor.firstName || "there",
+ commenterName:
+ `${commenter.firstName} ${commenter.lastName}`.trim() || "Someone",
+ postTitle: post.title,
+ commentContent: comment.content,
+ postUrl: postUrl,
+ timestamp: timestamp,
+ };
+
+ const htmlContent = await this.renderTemplate(
+ "forumCommentToPostAuthor",
+ variables
+ );
+
+ const subject = `${commenter.firstName} ${commenter.lastName} commented on your post`;
+
+ const result = await this.sendEmail(
+ postAuthor.email,
+ subject,
+ htmlContent
+ );
+
+ if (result.success) {
+ console.log(
+ `Forum comment notification email sent to ${postAuthor.email}`
+ );
+ }
+
+ return result;
+ } catch (error) {
+ console.error("Failed to send forum comment notification email:", error);
+ return { success: false, error: error.message };
+ }
+ }
+
+ async sendForumReplyNotification(
+ commentAuthor,
+ replier,
+ post,
+ reply,
+ parentComment
+ ) {
+ try {
+ const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
+ const postUrl = `${frontendUrl}/forum/posts/${post.id}`;
+
+ const timestamp = new Date(reply.createdAt).toLocaleString("en-US", {
+ dateStyle: "medium",
+ timeStyle: "short",
+ });
+
+ const variables = {
+ commentAuthorName: commentAuthor.firstName || "there",
+ replierName:
+ `${replier.firstName} ${replier.lastName}`.trim() || "Someone",
+ postTitle: post.title,
+ parentCommentContent: parentComment.content,
+ replyContent: reply.content,
+ postUrl: postUrl,
+ timestamp: timestamp,
+ };
+
+ const htmlContent = await this.renderTemplate(
+ "forumReplyToCommentAuthor",
+ variables
+ );
+
+ const subject = `${replier.firstName} ${replier.lastName} replied to your comment`;
+
+ const result = await this.sendEmail(
+ commentAuthor.email,
+ subject,
+ htmlContent
+ );
+
+ if (result.success) {
+ console.log(
+ `Forum reply notification email sent to ${commentAuthor.email}`
+ );
+ }
+
+ return result;
+ } catch (error) {
+ console.error("Failed to send forum reply notification email:", error);
+ return { success: false, error: error.message };
+ }
+ }
+
+ async sendForumAnswerAcceptedNotification(
+ commentAuthor,
+ postAuthor,
+ post,
+ comment
+ ) {
+ try {
+ const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
+ const postUrl = `${frontendUrl}/forum/posts/${post.id}`;
+
+ const variables = {
+ commentAuthorName: commentAuthor.firstName || "there",
+ postAuthorName:
+ `${postAuthor.firstName} ${postAuthor.lastName}`.trim() || "Someone",
+ postTitle: post.title,
+ commentContent: comment.content,
+ postUrl: postUrl,
+ };
+
+ const htmlContent = await this.renderTemplate(
+ "forumAnswerAcceptedToCommentAuthor",
+ variables
+ );
+
+ const subject = `Your comment was marked as the accepted answer!`;
+
+ const result = await this.sendEmail(
+ commentAuthor.email,
+ subject,
+ htmlContent
+ );
+
+ if (result.success) {
+ console.log(
+ `Forum answer accepted notification email sent to ${commentAuthor.email}`
+ );
+ }
+
+ return result;
+ } catch (error) {
+ console.error(
+ "Failed to send forum answer accepted notification email:",
+ error
+ );
+ return { success: false, error: error.message };
+ }
+ }
+
+ async sendForumThreadActivityNotification(
+ participant,
+ commenter,
+ post,
+ comment
+ ) {
+ try {
+ const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
+ const postUrl = `${frontendUrl}/forum/posts/${post.id}`;
+
+ const timestamp = new Date(comment.createdAt).toLocaleString("en-US", {
+ dateStyle: "medium",
+ timeStyle: "short",
+ });
+
+ const variables = {
+ participantName: participant.firstName || "there",
+ commenterName:
+ `${commenter.firstName} ${commenter.lastName}`.trim() || "Someone",
+ postTitle: post.title,
+ commentContent: comment.content,
+ postUrl: postUrl,
+ timestamp: timestamp,
+ };
+
+ const htmlContent = await this.renderTemplate(
+ "forumThreadActivityToParticipant",
+ variables
+ );
+
+ const subject = `New activity on a post you're following`;
+
+ const result = await this.sendEmail(
+ participant.email,
+ subject,
+ htmlContent
+ );
+
+ if (result.success) {
+ console.log(
+ `Forum thread activity notification email sent to ${participant.email}`
+ );
+ }
+
+ return result;
+ } catch (error) {
+ console.error(
+ "Failed to send forum thread activity notification email:",
+ error
+ );
+ return { success: false, error: error.message };
+ }
+ }
}
module.exports = new EmailService();
diff --git a/backend/templates/emails/forumAnswerAcceptedToCommentAuthor.html b/backend/templates/emails/forumAnswerAcceptedToCommentAuthor.html
new file mode 100644
index 0000000..fc28f06
--- /dev/null
+++ b/backend/templates/emails/forumAnswerAcceptedToCommentAuthor.html
@@ -0,0 +1,312 @@
+
+
+
+
+
+
+
Your Comment Was Marked as the Answer
+
+
+
+
+
+
+
+
Hi {{commentAuthorName}},
+
+
Your comment was marked as the accepted answer!
+
+
+
✓
+
Great job helping the community!
+
{{postAuthorName}} marked your comment as the accepted answer
+
+
+
Your helpful comment successfully answered this question:
+
+
+
+
+
+
View Post
+
+
Thank you for contributing your knowledge and helping others in the RentAll community!
+
+
+
Keep it up! Your contributions make RentAll a better place for everyone. Continue sharing your expertise and helping fellow community members.
+
+
+
+
+
+
+
diff --git a/backend/templates/emails/forumCommentToPostAuthor.html b/backend/templates/emails/forumCommentToPostAuthor.html
new file mode 100644
index 0000000..56ddeeb
--- /dev/null
+++ b/backend/templates/emails/forumCommentToPostAuthor.html
@@ -0,0 +1,271 @@
+
+
+
+
+
+
+
New Comment on Your Post
+
+
+
+
+
+
+
+
Hi {{postAuthorName}},
+
+
{{commenterName}} commented on your post
+
+
Someone just commented on your forum post:
+
+
+
+
+
+
View Post & Reply
+
+
Click the button above to see the full discussion and respond to this comment.
+
+
+
Tip: Engaging with commenters helps build a vibrant community and provides better answers for everyone.
+
+
+
+
+
+
+
diff --git a/backend/templates/emails/forumReplyToCommentAuthor.html b/backend/templates/emails/forumReplyToCommentAuthor.html
new file mode 100644
index 0000000..e0baf20
--- /dev/null
+++ b/backend/templates/emails/forumReplyToCommentAuthor.html
@@ -0,0 +1,303 @@
+
+
+
+
+
+
+
New Reply to Your Comment
+
+
+
+
+
+
+
+
Hi {{commentAuthorName}},
+
+
{{replierName}} replied to your comment
+
+
Someone just replied to your comment in the forum:
+
+
+
+
+
+
+
{{replierName}}
+
{{replyContent}}
+
Posted {{timestamp}}
+
+
+
View Reply & Respond
+
+
Click the button above to see the full discussion and continue the conversation.
+
+
+
Tip: Thoughtful replies help create meaningful discussions and build community connections.
+
+
+
+
+
+
+
diff --git a/backend/templates/emails/forumThreadActivityToParticipant.html b/backend/templates/emails/forumThreadActivityToParticipant.html
new file mode 100644
index 0000000..32514b8
--- /dev/null
+++ b/backend/templates/emails/forumThreadActivityToParticipant.html
@@ -0,0 +1,281 @@
+
+
+
+
+
+
+
New Activity on a Forum Post You Follow
+
+
+
+
+
+
+
+
Hi {{participantName}},
+
+
New activity on a post you're following
+
+
{{commenterName}} just commented on a forum post you've participated in:
+
+
+
Post You're Following
+
{{postTitle}}
+
+
+
+
+
View Discussion
+
+
Click the button above to see the full conversation and join the discussion.
+
+
+
Stay engaged: You're receiving this because you've commented on this post. Keep the conversation going!
+
+
+
+
+
+
+