-
Notifications
You must be signed in to change notification settings - Fork 1
concurrent emailing #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
82ea19e
2bf4cd7
4a30826
192e778
c14725a
daa7bdc
9a28261
62386ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||||||||||||||||||||||||||||||||||||||||
| // }); | ||||||||||||||||||||||||||||||||||||||||||||
| // } | ||||||||||||||||||||||||||||||||||||||||||||
| // } | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+119
to
+206
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Remove commented‑out legacy loop. Large commented blocks add noise and risk divergence. Delete this block entirely from the PR. 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+186
to
+207
|
||||||||||||||||||||||||||||||||||||||||||||
| // } 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 | |
| // }); | |
| // } | |
| // } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don’t log full recipient emails (PII). Mask before logging.
Avoid emitting raw addresses to logs.
- console.log(`Processing email ${processedCount}/${data.length} for ${row.Email}`);
+ const maskedEmail = typeof row.Email === 'string'
+ ? row.Email.replace(/(.{2}).+(@.+)/, '$1***$2')
+ : 'Unknown';
+ console.log(`Processing email ${processedCount}/${data.length} for ${maskedEmail}`);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| console.log(`Processing email ${processedCount}/${data.length} for ${row.Email}`); | |
| const maskedEmail = typeof row.Email === 'string' | |
| ? row.Email.replace(/(.{2}).+(@.+)/, '$1***$2') | |
| : 'Unknown'; | |
| console.log(`Processing email ${processedCount}/${data.length} for ${maskedEmail}`); |
🤖 Prompt for AI Agents
In beacon/app/api/SendEmails/route.js around line 213 the code logs full
recipient emails (PII) with console.log(`Processing email
${processedCount}/${data.length} for ${row.Email}`); — replace this with a
masked form of the email before logging (e.g., keep first character and domain,
replace intervening characters with asterisks or show only the domain and a
short hashed/tokenized prefix), and ensure no other logs emit the raw row.Email;
update the logging statement to use the masked value and add a small helper or
inline logic to perform the masking consistently.
Copilot
AI
Sep 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The processedCount variable is being incremented concurrently without synchronization, which can lead to race conditions and inaccurate counts. Consider using an atomic counter or removing this logging since the concurrent processing makes the count unreliable for progress tracking.
| processedCount++; | |
| console.log(`Processing email ${processedCount}/${data.length} for ${row.Email}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Escape placeholder keys and avoid inserting "undefined" into the template.
Unescaped regex keys can mis‑match; undefined values render as the string "undefined".
- 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 safe = v => (v == null ? '' : String(v));
+ let personalizedTemplate = template.replace(/{{Recipient_name}}/g, safe(row.Name));
+ for (const [key, value] of Object.entries(row)) {
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ personalizedTemplate = personalizedTemplate
+ .replace(new RegExp(`{{${escaped}}}`, 'g'), safe(value));
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // 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]); | |
| }); | |
| // Replace placeholders | |
| const safe = v => (v == null ? '' : String(v)); | |
| let personalizedTemplate = template.replace(/{{Recipient_name}}/g, safe(row.Name)); | |
| for (const [key, value] of Object.entries(row)) { | |
| const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
| personalizedTemplate = personalizedTemplate | |
| .replace(new RegExp(`{{${escaped}}}`, 'g'), safe(value)); | |
| } |
🤖 Prompt for AI Agents
In beacon/app/api/SendEmails/route.js around lines 225 to 231, the loop builds a
RegExp from object keys without escaping special characters and blindly inserts
row[key] which can become the string "undefined"; update it to escape regex
metacharacters in the key before constructing the RegExp (so keys like "a.b" or
"name?" match literally) and only replace when row[key] is not null/undefined
(use an empty string or a safe default for falsy values), converting non-string
values to string explicitly before replacement.
Copilot
AI
Sep 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The errorMessage variable that was constructed with specific error handling logic is being ignored. Line 266 should use errorMessage instead of error.message || 'Unknown error occurred' to preserve the enhanced error messages for AWS SES issues.
| message: error.message || 'Unknown error occurred' | |
| message: errorMessage |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use the mapped errorMessage and guard when error.message is undefined; also normalize matching.
Current code computes errorMessage but returns raw error.message and can throw on .includes when message is absent.
- 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')) {
+ console.error(`Error sending email to ${row.Email}:`, error);
+ const rawMsg = String(error?.message ?? '');
+ const msg = rawMsg.toLowerCase();
+ let errorMessage = rawMsg || 'Unknown error occurred';
+ if (msg.includes('not verified')) {
errorMessage = 'Email address not verified in AWS SES';
- } else if (error.message.includes('AccessDenied')) {
+ } else if (msg.includes('accessdenied')) {
errorMessage = 'AWS SES permission denied - check IAM permissions';
- } else if (error.message.includes('InvalidParameterValue')) {
+ } else if (msg.includes('invalidparametervalue')) {
errorMessage = 'Invalid email format or parameters';
+ } else if (msg.includes('throttl')) {
+ errorMessage = 'AWS SES rate limit exceeded - reduce concurrency or request a higher limit';
}
- results.push({
- email: row.Email,
- name: row.Name,
- status: 'error',
- message: error.message || 'Unknown error occurred'
- });
+ results[idx] = {
+ email: row.Email,
+ name: row.Name,
+ status: 'error',
+ message: errorMessage
+ };Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In beacon/app/api/SendEmails/route.js around lines 252-267, the code computes a
mapped errorMessage but still uses error.message and calls .includes on it which
can throw if undefined; fix by first extracting a safe string like const rawMsg
= (error && error.message) ? String(error.message) : '' , normalize it to
lowerCase for matching, run includes checks against the normalized string to set
errorMessage cases, then use the computed errorMessage when pushing results and
in the console.error (falling back to a generic message if empty).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Confirm p-limit is declared and compatible with ESM/Next runtime.
Ensure dependency is present (v4+ is ESM) and not duplicated via subdeps.
🏁 Script executed:
Length of output: 123
🏁 Script executed:
Length of output: 10533
🏁 Script executed:
Length of output: 16037
Declare p-limit in package.json and choose an ESM-compatible version
🤖 Prompt for AI Agents