From 9a28261e5aeeae4c20c5d7da86b5bc0c1333bba5 Mon Sep 17 00:00:00 2001 From: Diptayan Jash Date: Thu, 18 Sep 2025 00:56:11 +0530 Subject: [PATCH 1/2] concurrent emailing --- beacon/app/api/SendEmails/route.js | 243 ++++++++++++++++++----------- 1 file changed, 156 insertions(+), 87 deletions(-) diff --git a/beacon/app/api/SendEmails/route.js b/beacon/app/api/SendEmails/route.js index 047fe9e..27ebba3 100644 --- a/beacon/app/api/SendEmails/route.js +++ b/beacon/app/api/SendEmails/route.js @@ -3,6 +3,7 @@ import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses'; import * as XLSX from 'xlsx'; import { promises as fs } from 'fs'; import path from 'path'; +import pLimit from 'p-limit'; export async function POST(request) { // No tempDir needed; use in-memory buffers @@ -112,94 +113,162 @@ export async function POST(request) { const results = []; let processedCount = 0; - for (const row of data) { - try { - processedCount++; - console.log(`Processing email ${processedCount}/${data.length} for ${row.Email}`); - - // Skip rows with missing email or name - if (!row.Email || !row.Name) { - results.push({ - email: row.Email || 'Unknown', - name: row.Name || 'Unknown', - status: 'skipped', - message: 'Missing email or name' - }); - continue; - } - - // Replace placeholders in template - let personalizedTemplate = template; - - // Replace {{Recipient_name}} with the actual name - personalizedTemplate = personalizedTemplate.replace(/{{Recipient_name}}/g, row.Name); - - // Replace any other placeholders that might exist - Object.keys(row).forEach(key => { - const placeholder = new RegExp(`{{${key}}}`, 'g'); - personalizedTemplate = personalizedTemplate.replace(placeholder, row[key]); - }); - - const isPlainTextOnly = false; // We always send as HTML now since rich text editor produces HTML - - // Create SES email parameters - const emailParams = { - Source: senderEmail, - Destination: { - ToAddresses: [row.Email], - }, - Message: { - Subject: { - Data: subject, - Charset: 'UTF-8', - }, - Body: { - Html: { - Data: personalizedTemplate, - Charset: 'UTF-8', + // Instead of for...of + const limit = pLimit(12); // 12 concurrent sends (SES default rate) + + // for (const row of data) { + // try { + // processedCount++; + // console.log(`Processing email ${processedCount}/${data.length} for ${row.Email}`); + + // // Skip rows with missing email or name + // if (!row.Email || !row.Name) { + // results.push({ + // email: row.Email || 'Unknown', + // name: row.Name || 'Unknown', + // status: 'skipped', + // message: 'Missing email or name' + // }); + // continue; + // } + + // // Replace placeholders in template + // let personalizedTemplate = template; + + // // Replace {{Recipient_name}} with the actual name + // personalizedTemplate = personalizedTemplate.replace(/{{Recipient_name}}/g, row.Name); + + // // Replace any other placeholders that might exist + // Object.keys(row).forEach(key => { + // const placeholder = new RegExp(`{{${key}}}`, 'g'); + // personalizedTemplate = personalizedTemplate.replace(placeholder, row[key]); + // }); + + // const isPlainTextOnly = false; // We always send as HTML now since rich text editor produces HTML + + // // Create SES email parameters + // const emailParams = { + // Source: senderEmail, + // Destination: { + // ToAddresses: [row.Email], + // }, + // Message: { + // Subject: { + // Data: subject, + // Charset: 'UTF-8', + // }, + // Body: { + // Html: { + // Data: personalizedTemplate, + // Charset: 'UTF-8', + // }, + // }, + // }, + // }; + + // // Send email + // const command = new SendEmailCommand(emailParams); + // const response = await sesClient.send(command); + + // results.push({ + // email: row.Email, + // name: row.Name, + // status: 'success', + // message: 'Email sent successfully', + // messageId: response.MessageId + // }); + + // // Add a small delay between emails to avoid rate limiting + // if (processedCount < data.length) { + // await new Promise(resolve => setTimeout(resolve, 100)); + // } + + // } catch (error) { + // console.error(`Error sending email to ${row.Email}:`, error); + + // // Provide specific error messages + // let errorMessage = error.message || 'Unknown error occurred'; + // if (error.message.includes('Email address is not verified')) { + // errorMessage = 'Email address not verified in AWS SES'; + // } else if (error.message.includes('AccessDenied')) { + // errorMessage = 'AWS SES permission denied - check IAM permissions'; + // } else if (error.message.includes('InvalidParameterValue')) { + // errorMessage = 'Invalid email format or parameters'; + // } + + // results.push({ + // email: row.Email, + // name: row.Name, + // status: 'error', + // message: errorMessage + // }); + // } + // } + + await Promise.all( + data.map((row, idx) => + limit(async () => { + try { + processedCount++; + console.log(`Processing email ${processedCount}/${data.length} for ${row.Email}`); + + if (!row.Email || !row.Name) { + results.push({ + email: row.Email || 'Unknown', + name: row.Name || 'Unknown', + status: 'skipped', + message: 'Missing email or name' + }); + return; + } + + // Replace placeholders + let personalizedTemplate = template.replace(/{{Recipient_name}}/g, row.Name); + Object.keys(row).forEach(key => { + const placeholder = new RegExp(`{{${key}}}`, 'g'); + personalizedTemplate = personalizedTemplate.replace(placeholder, row[key]); + }); + + const emailParams = { + Source: senderEmail, + Destination: { ToAddresses: [row.Email] }, + Message: { + Subject: { Data: subject, Charset: 'UTF-8' }, + Body: { Html: { Data: personalizedTemplate, Charset: 'UTF-8' } } }, - }, - }, - }; - - // Send email - const command = new SendEmailCommand(emailParams); - const response = await sesClient.send(command); - - results.push({ - email: row.Email, - name: row.Name, - status: 'success', - message: 'Email sent successfully', - messageId: response.MessageId - }); - - // Add a small delay between emails to avoid rate limiting - if (processedCount < data.length) { - await new Promise(resolve => setTimeout(resolve, 100)); - } - - } catch (error) { - console.error(`Error sending email to ${row.Email}:`, error); - - // Provide specific error messages - let errorMessage = error.message || 'Unknown error occurred'; - if (error.message.includes('Email address is not verified')) { - errorMessage = 'Email address not verified in AWS SES'; - } else if (error.message.includes('AccessDenied')) { - errorMessage = 'AWS SES permission denied - check IAM permissions'; - } else if (error.message.includes('InvalidParameterValue')) { - errorMessage = 'Invalid email format or parameters'; - } - - results.push({ - email: row.Email, - name: row.Name, - status: 'error', - message: errorMessage - }); - } - } + }; + + const command = new SendEmailCommand(emailParams); + const response = await sesClient.send(command); + + results.push({ + email: row.Email, + name: row.Name, + status: 'success', + message: 'Email sent successfully', + messageId: response.MessageId + }); + + } catch (error) { + console.error(`Error sending email to ${row.Email}:`, error); + let errorMessage = error.message || 'Unknown error occurred'; + if (error.message.includes('Email address is not verified')) { + errorMessage = 'Email address not verified in AWS SES'; + } else if (error.message.includes('AccessDenied')) { + errorMessage = 'AWS SES permission denied - check IAM permissions'; + } else if (error.message.includes('InvalidParameterValue')) { + errorMessage = 'Invalid email format or parameters'; + } + results.push({ + email: row.Email, + name: row.Name, + status: 'error', + message: error.message || 'Unknown error occurred' + }); + } + }) + ) + ); // Calculate summary const successCount = results.filter(r => r.status === 'success').length; From 62386ad4e258e12fff6e796f1f6cd50bb9232bec Mon Sep 17 00:00:00 2001 From: Diptayan Jash Date: Mon, 13 Oct 2025 17:45:50 +0530 Subject: [PATCH 2/2] templates --- beacon/public/sample-data.xlsx | Bin 9177 -> 9192 bytes beacon/templates/RSVP.html | 176 +++++++++++++ beacon/templates/onboarding.html | 169 +++++++++++++ beacon/templates/postpone.html | 311 +++++++++++++++++++++++ beacon/templates/task-invite-fixed.html | 0 beacon/templates/update.html | 320 ++++++++++++++++++++++++ 6 files changed, 976 insertions(+) create mode 100644 beacon/templates/RSVP.html create mode 100644 beacon/templates/onboarding.html create mode 100644 beacon/templates/postpone.html delete mode 100644 beacon/templates/task-invite-fixed.html create mode 100644 beacon/templates/update.html diff --git a/beacon/public/sample-data.xlsx b/beacon/public/sample-data.xlsx index b82cfe6ae2fddb17dbf127509bf0852fe73c67a6..451439dc02fbd03fbbc448e71ad5c6526da75320 100644 GIT binary patch delta 2575 zcmY*bcTm$=7flFNS|Xta2!!4=bY!JT3rGzD(iTLjG$U1tKajF?kx-;$X-1kTN)?3A zS?L0@paP+YAR;YNmH;36?d_P`v{r(f!je)*B;J^c#B<@`8h{A2_8E`YaOD^?WEvpzD(^VQw!~AhGwhk2 z74DNdv|B7#dp@VMQNzA>4q|J$J3R@Wux5o?3CBA$74ulj5{9x>f}}e)tb^ig=;vtL zCF8`unbS}W4#O={N)9IAC6lNB?Q50){E+Q@-lghZp}7B)46#y6Doh*A<#`s zmKk7b4Hj_xt_mLq4&Mt|Xl>T|8{gPeRo^er=HMR3l~pX|+F!~b_Yp@2YI@scl@k%i zivZ$=+9Vvq{4p-)Wfowzp&TrBD|HC{s)4L3bpkI92{PqeyV{jmc(FNdN71cEY9iUB z^_l+@EPBVdF^FVvqT8Hxm;p^R>TgAm(%8pTy)TaVctTSp4nr|x0WHx%F_Ouq%I1-p zY)H7GH)RVhUWR_qC(*g)t$B{EyAy|QN$l?=E0HjnI+xq|fZkDZsuA{y_tHSx`O85G z3>kYG6YMQtF=vc}FtLJy_2+5m0w2o9Ky{KUxpWp2UK=_9cIR2F|TOE-}jD~HA zaL?Zv0s_e~fDIJ@Sl;N>WXjijo~$3D+4-D$cX55ZYVm0ygUP1{?L1M-vU+QL z-eWqw`_O}9>T&A~*RgaE=pn2#J~>o=j|kNXc5&yq$(|l!Pq8sD$^VPU*v2r!Vm6hC z8Wis`-Z+S{_{)c9y3xK`+Fl*D5K`AAGQWOET=|Y zwDpD33hT5I_w3$WYoVsER66eRyT1h~fKe(ia%u(9*C~sC4oPgACiFF-$AVgGr;(#In z(x@nb$s+Ooq3b#O(rX3_BkPOTf=Hc>G;ujA?^${XC=1)@vGWy*(9q4>o)`AVqSEM}9pBDN|Lt>XhZ0>*M5@r{I!*W0VRX3m zy`r^(iCU9jUf2lMAS>M!MXX{>Q%aMm)a6`9@>98Y(#8ghy7Vg1rq_&tpgt>Uo-J3pcC*r;)=@zeCAx%L7Qu)cnEXlAmxvH+=l6+?a? zdr*HnPyOb{ZSEg+gMje45Vkrec9U~#o*6>O#j}j0fI%QQ2tSYF0^Bl&&CZe%w~2Wj z+Yq`~FLbipoJi7TcZ_C0Z#RZ;KGzM2C2!!YW2%g z%ox5`**e*a3}H>~eHiK_?n`Fiwb`Tepi`us7U}O}fNJF?Fn&pT9~1AplTir2 zC&cj?Cd83Ct|&K! z+fQf;PEP!fjRc7a17ES0*kAw;f&%mVP9nr9lrcoLtuAVDz2gq}^8eXN_tsDf(5jn6h=YMy^DZ>JcixnW z%+Xxyo(f@He3@p#WGRyHM=CPFG$N+A!Ds3VnbgEmc(6TP7zk+We+;Y7A?|}4q@=Tj z_@K)%i`kNs3OWxCdyuHn>rZH9WayaQN9&YP4kTC97p+CG4CpyZc)8c4 zqg5j5GV?kvWpQv@%K>@8qv1))O98%2YNBq@V%NnVxrZ@Rf(`W|9+L&Iq(9x6q5Qf` z=d+X_(ZhiVxyfs+gjyZ z;b+Tl(XvtQ)@SCFokG*|NnBulXy>a~>%M>8VMFLnM!YaFj&g}nteg1`-x5jW1=(*E z^G*KuLD&WsFfwne#%bz!W4hYS6Ep>ug_A}`%6>gn?jwP{kA_DFiA6t`RGprn8szUk zRg5qeoQR(kmP7n^VV&vfpVNxxN9ltt@UAFr7Jn`fD9ra_V9;G(cl;KL6Y<~H`{&Ks xr4T=dl0f`drT^w@@j@ahh@VA0Q!qUZ2n72#4I0JcL^Q$r_*xMWda&>h>VHv!q~QPn delta 2553 zcmY*bXHXLe6AitJp$Y^-SMVqa(m|vsQVb>ZYAB(1asm@HuQyk67K&ZbFvELVgdW|zc7V~u^nk#(Dte$qI;!RUqIdOoQvjgf zpAuPZ32myJQ)s-j!n#m?45gE;h?vV@}0$uLv z8U4i#*sJiX(X2>uouu1epO~HD-)x;_s7N+oj0|o{t4LANRnPrm*s=$93Z%}d#r}!a zP{5eFX3b0;wJSlGxwl7N0Ni&UyH3Y*3PuC5gHgPnQiXhboqLudhcW%~TYkCgwK)M0 zfke^!$qnLFWMErt@tM8p+kvPSV9spQ*qK1b6LQHg;p%VX_nr$qI|imV5#TurS0pGUBwv|7}(;_8nVPJHssF)L4^Lu!V3&dl>A z?Q}LmB!aogY~dzWt{BUuz4R7Y%KZUgN9~;U5M!Y@8f7KCY1uQcbTf+UOTc!DeW`H4 zb5FEW_OyG^n&R;cl-V>Br^Cv4 zfB&=fLiHQlD@+aH8=!MVM-hXXd*UYVPgH0s#EN z0RV^s@XE5{7$P1PF$LjG-D263Vx1A341ZW~5^rLX(r8IvqTRa`$qm|gD2$=+Kn`EJ-W}5%E4q`uC9~an za1>jwa_G1Wkd?#o2ygkUTghNoyZRxoz!Dn`%T@PoXneQgzkRk&j9AHk?I8@8`*DYb zv^xQ*32VeAIE3)1f~CRt?g?R*QBn^yDyq9(SDe?9$Rm3xl*ZAhD@MBY*8ukDsVepfwquVQM1+nEaF`sD5pw@A^ytvx@iB%mFI~+zH#)b<^4J4f+@czQmj` z?URdOMb3so4YY-xfCv-1iD`L`FUoZIHJ)At6$q4|U_RmV4Nj>m<7nEl+8ZZCmlT3T zo>%vlFRhaf>eI|-;~5=K8wkmUmNM85xOsoDROSxAvzM;cyJcZaV$8@lqKnm^{PLI+ z>Cl!uddQ%4G^!;89(kJtB<#9NTtR-ub!=?b$I_xNLiBXDX1)~YwMHlpD6L1KhGtDE zqzDd3&Iwh1Ip}1@rUf?B2~@d?H>aXdCN@JrR+jn37R7<-^Fx$9f?r zo2P6wkep6Ni0+A&ge2A=Cp&aMlxV(j7!e_Gx@g+CKznVMkGM^a$kOSZ$ zatfI7oMB5ADDd|!>=tlw)BicvJg5omu-u(7&LNsJ8T!#rAV+cLxi*-Uhwa&IB4&0= zf51P1+Y4>0s#=F7tr-K?0wvtpB94~w6M!YMpxtbXZWFd9+V^*hxd!iVIr#sP9w?;B zz34`D>n{%Ac4^7GJLQ+IiwHwDsgn$g72IE}9_c^As3Qz~)Bm)~id__WnPj1I5POSw z{^<&Ib>GNC?dqEs*>$n#P}_lr<=Et*iDoSh8oZA!KCb3wyY=hs_U{5P!8kp~{(ImB zB=ubONZenEAI|5SsN*(FsYO<)C(E%ilUxR^8V9;)Xo)>z zgrpgw11( zD)i^lL`I;Lq>U*YC6NUC>z_qLS08ZhnGflale8Uq=6KUB%}GR@PS5t#T1xib>~BuG zk^N&~4YNT~`&omN*6Y(D?zdi*1{I$V_x*7!YG||Rzfta8*5nCg`WkjcxG{Xb!h?Xl z8UA&25arb-wBH>DZRN8DbDKcS6m`r*zjG{W;4s(WLmx-drhSs~ar^Ncl z+VH=~USqPsN68xThKT?GaiMB4sQ6GMo*^p?`mfmj$vF-3@f)%iK>wxbzwsixnw%Es h=T1+B%6>{?{(r-OZhW$wA(IxqQ%;WktMuQ{{{Z4{t&acz diff --git a/beacon/templates/RSVP.html b/beacon/templates/RSVP.html new file mode 100644 index 0000000..6a629c6 --- /dev/null +++ b/beacon/templates/RSVP.html @@ -0,0 +1,176 @@ + + + + + + + + Figma Fiesta 2.0 — RSVP + + + + +
+
+
+
GitHub Community SRM presents
+

Figma Fiesta 2.0

+
+ + + + Figma Fiesta 2.0 — event poster + + +
+

Hello {{Recipient_name}},

+ +

You’re invited to Figma Fiesta 2.0 — a creative design event hosted by + GitHub Community SRM. Join us for an exciting session filled with design insights + and fun! +

+ +
+ Date: 14th October
+ Time: 8:30 AM - 5:00 PM
+ Venue: iMac Lab C, Tech Park 1, SRM Institute of Science and Technology +
+ + +

+ RSVP before 8 PM tonight!
+ Only 60 seats are available. Entry is strictly limited to RSVP’d participants.
+ Upon registration, you will receive a QR ticket via email. +

+ + +

+ RSVP Now +

+ +

RSVP quickly to secure your spot! Only registered attendees with a QR ticket will be + allowed entry.

+
+ + +
+
+ + + \ No newline at end of file diff --git a/beacon/templates/onboarding.html b/beacon/templates/onboarding.html new file mode 100644 index 0000000..605ebeb --- /dev/null +++ b/beacon/templates/onboarding.html @@ -0,0 +1,169 @@ + + + + + + + + + + 🎉 Congratulations – You’re Selected for GitHub Community SRM! + + + + + +
Congratulations! You’ve been selected for GitHub Community SRM. Join our WhatsApp group for + next steps.
+ + + + + +
+ + + + + + + + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/beacon/templates/postpone.html b/beacon/templates/postpone.html new file mode 100644 index 0000000..6e3ae91 --- /dev/null +++ b/beacon/templates/postpone.html @@ -0,0 +1,311 @@ + + + + + + + + + + Important Update: Interviews Postponed + + + + + +
Interviews postponed to Monday & Tuesday (15th and 16th Sep). If you missed your slot, you + can come on these days.
+ + + + + + + + + \ No newline at end of file diff --git a/beacon/templates/task-invite-fixed.html b/beacon/templates/task-invite-fixed.html deleted file mode 100644 index e69de29..0000000 diff --git a/beacon/templates/update.html b/beacon/templates/update.html new file mode 100644 index 0000000..fc5e780 --- /dev/null +++ b/beacon/templates/update.html @@ -0,0 +1,320 @@ + + + + + + + + + + Follow-up: Interview Postponement — Walk-in Details + + + + + +
Follow-up: Walk-in on 15–16 Sep from 5:20 PM. If you’ve already interviewed, no need to come. +
+ + + + + + + + + \ No newline at end of file