diff --git a/app/emails/mail.server.ts b/app/emails/mail.server.ts index c0845fc0d..69e469e8c 100644 --- a/app/emails/mail.server.ts +++ b/app/emails/mail.server.ts @@ -38,8 +38,8 @@ export const sendEmail = async ({ try { // send mail with defined transport object await transporter.sendMail({ - from: from || SMTP_FROM || `"Shelf" `, // sender address - ...(replyTo && { replyTo }), // reply to + from: from || SMTP_FROM || `"Shelf" `, // sender address + replyTo: replyTo || "support@shelf.nu", // reply to to, // list of receivers subject, // Subject line text, // plain text body @@ -80,3 +80,37 @@ export const sendEmail = async ({ // Preview only available when sending through an Ethereal account // console.log("Preview URL: %s", nodemailer.getTestMessageUrl(info)); }; + +/** Utility function to add delay between operations */ +async function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** Process emails in batches with rate limiting + * @param emails - Array of email configurations to send + * @param batchSize - Number of emails to process per batch (default: 2) + * @param delayMs - Milliseconds to wait between batches (default: 1000ms) + */ +export async function sendEmailsWithRateLimit( + emails: Array<{ + to: string; + subject: string; + text: string; + html: string; + }>, + batchSize = 2, + delayMs = 1100 +): Promise { + for (let i = 0; i < emails.length; i += batchSize) { + // Process emails in batches of specified size + const batch = emails.slice(i, i + batchSize); + + // Send emails in current batch concurrently + await Promise.all(batch.map((email) => sendEmail(email))); + + // If there are more emails to process, add delay before next batch + if (i + batchSize < emails.length) { + await delay(delayMs); + } + } +} diff --git a/app/modules/booking/service.server.ts b/app/modules/booking/service.server.ts index a4b394cf2..de7b894e8 100644 --- a/app/modules/booking/service.server.ts +++ b/app/modules/booking/service.server.ts @@ -10,7 +10,7 @@ import type { } from "@prisma/client"; import { db } from "~/database/db.server"; import { bookingUpdatesTemplateString } from "~/emails/bookings-updates-template"; -import { sendEmail } from "~/emails/mail.server"; +import { sendEmail, sendEmailsWithRateLimit } from "~/emails/mail.server"; import { getStatusClasses, isOneDayEvent } from "~/utils/calendar"; import { getDateTimeFormat } from "~/utils/client-hints"; import { calcTimeDifference } from "~/utils/date-fns"; @@ -1317,38 +1317,31 @@ export async function bulkDeleteBookings({ bookingsWithSchedulerReference.map((booking) => cancelScheduler(booking)) ); - /** Sending mails to required users */ - await Promise.all( - bookingsToSendEmail.map((b) => { - const subject = `Booking deleted (${b.name}) - shelf.nu`; - const text = deletedBookingEmailContent({ - bookingName: b.name, - assetsCount: b.assets.length, - custodian: - `${b.custodianUser?.firstName} ${b.custodianUser?.lastName}` || - (b.custodianTeamMember?.name as string), - from: b.from as Date, - to: b.to as Date, - bookingId: b.id, - hints, - }); - - const html = bookingUpdatesTemplateString({ - booking: b, - heading: `Your booking as been deleted: "${b.name}"`, - assetCount: b.assets.length, - hints, - hideViewButton: true, - }); + const emailConfigs = bookingsToSendEmail.map((b) => ({ + to: b.custodianUser?.email ?? "", + subject: `Booking deleted (${b.name}) - shelf.nu`, + text: deletedBookingEmailContent({ + bookingName: b.name, + assetsCount: b.assets.length, + custodian: + `${b.custodianUser?.firstName} ${b.custodianUser?.lastName}` || + (b.custodianTeamMember?.name as string), + from: b.from as Date, + to: b.to as Date, + bookingId: b.id, + hints, + }), + html: bookingUpdatesTemplateString({ + booking: b, + heading: `Your booking as been deleted: "${b.name}"`, + assetCount: b.assets.length, + hints, + hideViewButton: true, + }), + })); - return sendEmail({ - to: b.custodianUser?.email ?? "", - subject, - text, - html, - }); - }) - ); + // Send emails with rate limiting + return await sendEmailsWithRateLimit(emailConfigs); } catch (cause) { const message = cause instanceof ShelfError diff --git a/app/routes/_layout+/account-details.general.tsx b/app/routes/_layout+/account-details.general.tsx index 44d699915..a88671225 100644 --- a/app/routes/_layout+/account-details.general.tsx +++ b/app/routes/_layout+/account-details.general.tsx @@ -182,7 +182,7 @@ export async function action({ context, request }: ActionFunctionArgs) { } void sendEmail({ - to: ADMIN_EMAIL || `"Shelf" `, + to: ADMIN_EMAIL || `"Shelf" `, subject: "Delete account request", text: `User with id ${userId} and email ${payload.email} has requested to delete their account. \n User: ${SERVER_URL}/admin-dashboard/${userId} \n\n Reason: ${reason}\n\n`, });