From 46c150f6dfd6faa80e7ad3e6c063802d90069206 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Thu, 26 Mar 2026 08:09:35 +0000 Subject: [PATCH] feat: implement background email sending with improved user feedback --- .../AdminDashboard/src/Pages/More/Email.tsx | 3 +- .../src/Pages/SendEmail/Index.tsx | 3 +- .../Notification/API/BroadcastEmail.ts | 129 ++++++++++++------ 3 files changed, 87 insertions(+), 48 deletions(-) diff --git a/App/FeatureSet/AdminDashboard/src/Pages/More/Email.tsx b/App/FeatureSet/AdminDashboard/src/Pages/More/Email.tsx index 876cc14019..b3125321a0 100644 --- a/App/FeatureSet/AdminDashboard/src/Pages/More/Email.tsx +++ b/App/FeatureSet/AdminDashboard/src/Pages/More/Email.tsx @@ -94,9 +94,8 @@ const MoreEmail: FunctionComponent = (): ReactElement => { throw new Error("Failed to send emails."); } - const data: JSONObject = response.data as JSONObject; setSuccess( - `Emails sent successfully. Total users: ${data["totalUsers"]}, Sent: ${data["sentCount"]}, Errors: ${data["errorCount"]}`, + "Broadcast email job has been started. Emails will be sent in the background.", ); } catch (err) { setError(API.getFriendlyMessage(err)); diff --git a/App/FeatureSet/AdminDashboard/src/Pages/SendEmail/Index.tsx b/App/FeatureSet/AdminDashboard/src/Pages/SendEmail/Index.tsx index a02409ad52..b5491b2900 100644 --- a/App/FeatureSet/AdminDashboard/src/Pages/SendEmail/Index.tsx +++ b/App/FeatureSet/AdminDashboard/src/Pages/SendEmail/Index.tsx @@ -51,9 +51,8 @@ const SendEmail: FunctionComponent = (): ReactElement => { throw new Error("Failed to send emails."); } - const data: JSONObject = response.data as JSONObject; setSendAllSuccess( - `Emails sent successfully. Total users: ${data["totalUsers"]}, Sent: ${data["sentCount"]}, Errors: ${data["errorCount"]}`, + "Broadcast email job has been started. Emails will be sent in the background.", ); } catch (err) { setSendAllError(API.getFriendlyMessage(err)); diff --git a/App/FeatureSet/Notification/API/BroadcastEmail.ts b/App/FeatureSet/Notification/API/BroadcastEmail.ts index 5d3b6503f2..eb7c714259 100644 --- a/App/FeatureSet/Notification/API/BroadcastEmail.ts +++ b/App/FeatureSet/Notification/API/BroadcastEmail.ts @@ -19,6 +19,79 @@ import User from "Common/Models/DatabaseModels/User"; const router: ExpressRouter = Express.getRouter(); +const BATCH_SIZE: number = 100; + +async function sendBroadcastEmailsInBackground(data: { + subject: string; + htmlMessage: string; +}): Promise { + let skip: number = 0; + let sentCount: number = 0; + let errorCount: number = 0; + let totalUsers: number = 0; + + try { + while (true) { + const users: Array = await UserService.findBy({ + query: {}, + select: { + email: true, + }, + skip: skip, + limit: BATCH_SIZE, + props: { + isRoot: true, + }, + }); + + if (users.length === 0) { + break; + } + + totalUsers += users.length; + + for (const user of users) { + if (!user.email) { + continue; + } + + try { + const mail: EmailMessage = { + templateType: EmailTemplateType.SimpleMessage, + toEmail: user.email, + subject: data.subject, + vars: { + subject: data.subject, + message: data.htmlMessage, + }, + body: "", + }; + + await MailService.send(mail); + sentCount++; + } catch (err) { + errorCount++; + logger.error( + `Failed to send broadcast email to ${user.email.toString()}: ${err}`, + ); + } + } + + if (users.length < BATCH_SIZE) { + break; + } + + skip += users.length; + } + + logger.info( + `Broadcast email completed. Total users: ${totalUsers}, Sent: ${sentCount}, Errors: ${errorCount}`, + ); + } catch (err) { + logger.error(`Broadcast email background job failed: ${err}`); + } +} + router.post( "/send-test", MasterAdminAuthorization.isAuthorizedMasterAdminMiddleware, @@ -85,56 +158,24 @@ router.post( throw new BadDataException("Message is required"); } - const users: Array = await UserService.findAllBy({ - query: {}, - select: { - email: true, - }, - skip: 0, - props: { - isRoot: true, - }, - }); - const htmlMessage: string = await Markdown.convertToHTML( message, MarkdownContentType.Email, ); - let sentCount: number = 0; - let errorCount: number = 0; + // Send response immediately so the request doesn't timeout. + // Emails are sent in the background. + Response.sendJsonObjectResponse(req, res, { + message: + "Broadcast email job has been started. Emails will be sent in the background.", + }); - for (const user of users) { - if (!user.email) { - continue; - } - - try { - const mail: EmailMessage = { - templateType: EmailTemplateType.SimpleMessage, - toEmail: user.email, - subject: subject, - vars: { - subject: subject, - message: htmlMessage, - }, - body: "", - }; - - await MailService.send(mail); - sentCount++; - } catch (err) { - errorCount++; - logger.error( - `Failed to send broadcast email to ${user.email.toString()}: ${err}`, - ); - } - } - - return Response.sendJsonObjectResponse(req, res, { - totalUsers: users.length, - sentCount: sentCount, - errorCount: errorCount, + // Process emails in the background after the response is sent. + sendBroadcastEmailsInBackground({ + subject, + htmlMessage, + }).catch((err: Error) => { + logger.error(`Broadcast email background job failed: ${err}`); }); } catch (err) { return next(err);