52: Email send script#16
Conversation
Graphite Automations"Request reviewers once CI passes" took an action on this PR • (05/30/26)2 reviewers were added to this PR based on Henry Chen's automation. |
| const data = fs.readFileSync(csv, "utf-8"); | ||
|
|
||
| const lines = data.split("\n").map(line => line.trim()).filter(line => line.length > 0); | ||
| const csvArray = lines.map(line => line.split(",").map(cell => cell.trim())); |
There was a problem hiding this comment.
Critical CSV parsing bug. Using split(",") will incorrectly parse CSV fields that contain commas within quotes. The test data includes fields like "Tahmid is a senior at Hunter College & has been interning at Integral Ad Science for close to a year now, as well as running various software projects for Patina. Tahmid likes to eat good food :)" which contains commas. This will cause:
- Incorrect column counts
- Misaligned field mappings
- Runtime errors when accessing
row[headerIndex['Email']](line 43) due to wrong indices
Fix: Use a proper CSV parsing library:
import { parse } from 'csv-parse/sync';
// Then replace lines 29-30 with:
const csvArray = parse(data, {
skip_empty_lines: true,
trim: true
});| const csvArray = lines.map(line => line.split(",").map(cell => cell.trim())); | |
| const csvArray = parse(data, { | |
| skip_empty_lines: true, | |
| trim: true | |
| }); | |
Spotted by Graphite
Is this helpful? React 👍 or 👎 to let us know.
| row[headerIndex['Topics']], | ||
| row[headerIndex['Anything']] | ||
| ); | ||
| usersMap[email] = user; |
There was a problem hiding this comment.
Critical Bug: Incorrect Map usage
Using bracket notation on a Map object doesn't add entries to the Map. This creates a regular object property instead, so the Map will remain empty and subsequent lookups will fail.
// Wrong:
usersMap[email] = user;
// Correct:
usersMap.set(email, user);| usersMap[email] = user; | |
| usersMap.set(email, user); |
Spotted by Graphite
Is this helpful? React 👍 or 👎 to let us know.
| const pair = new Pair( | ||
| row[0], //fullNameA | ||
| row[1], //fullNameB | ||
| row[2], //emailA | ||
| row[3] //emailB | ||
| ); |
There was a problem hiding this comment.
Critical Bug: CSV column order mismatch
The pairings CSV has columns in order fullNameA,emailA,fullNameB,emailB but the code assumes fullNameA,fullNameB,emailA,emailB. This causes names and emails to be swapped incorrectly:
row[1](emailA) is assigned tofullNameBrow[2](fullNameB) is assigned toemailA
// Current (wrong):
const pair = new Pair(
row[0], //fullNameA
row[1], //fullNameB - actually emailA in CSV
row[2], //emailA - actually fullNameB in CSV
row[3] //emailB
);
// Should be:
const pair = new Pair(
row[0], //fullNameA
row[2], //fullNameB
row[1], //emailA
row[3] //emailB
);| const pair = new Pair( | |
| row[0], //fullNameA | |
| row[1], //fullNameB | |
| row[2], //emailA | |
| row[3] //emailB | |
| ); | |
| const pair = new Pair( | |
| row[0], //fullNameA | |
| row[2], //fullNameB | |
| row[1], //emailA | |
| row[3] //emailB | |
| ); | |
Spotted by Graphite
Is this helpful? React 👍 or 👎 to let us know.
There was a problem hiding this comment.
Yeah It shouldn't be row 0 2 1 3 lmao
There was a problem hiding this comment.
We want the email functionality on the backend side in Java.
There was a problem hiding this comment.
Usually, the naming convention for pure JS/TS files is all lowercase.
React components in tsx files are the ones that should be in PascalCase.
Also almost all the code should be in TS or TSX, not JS.
| pairings.push(pair); | ||
|
|
||
| } | ||
| }catch (err){ |
There was a problem hiding this comment.
I feel like the autoformatter isn't working. Yeah the CI test is failing.
We should be adding prettier --write to the git precommit hooks so that we don't have to remember to run it.
There was a problem hiding this comment.
Casually leaking Tahmid's and Andrew's emails 😅
| const pair = new Pair( | ||
| row[0], //fullNameA | ||
| row[1], //fullNameB | ||
| row[2], //emailA | ||
| row[3] //emailB | ||
| ); |
There was a problem hiding this comment.
Yeah It shouldn't be row 0 2 1 3 lmao
| const SENDER_NAME = "Patina Network" | ||
| const SENDER_EMAIL = "bella.patinanetwork@gmail.com" | ||
| const APP_PASSWORD = "" | ||
| const USERS_FILE = "js/src/app/user/admin/emails/users-test.csv" |
There was a problem hiding this comment.
We'll need to move this from being read as a file to a user input in the future.
| const PAIRINGS_FILE = "js/src/app/user/admin/emails/pairings-test.csv" | ||
|
|
||
| //classes | ||
| class User { |
There was a problem hiding this comment.
These should likely be interfaces instead of classes. Classes should pretty rarely be used in general, especially not for pure data objects.
| return requiredHeaders.every(header => headers.includes(header)); //returns true if all requiredheaders are present, false otherwise | ||
| } | ||
|
|
||
| function splitCSVLine(line){ //split lines w/ commas as deliminators (manually implmented to handle commas within quotes) |
There was a problem hiding this comment.
We probably should be using a library to parse the CSVs. Isn't commas within quotes part of the usual CSV standard?
| await fetch("/api/emails/send-users", { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify(userMap) | ||
| }); |
There was a problem hiding this comment.
Serializing a Map with JSON.stringify(userMap) will produce an empty object {}. Map objects are not directly JSON-serializable and will result in {} being sent to the API.
Fix: Convert the Map to an object or array before stringifying:
await fetch("/api/emails/send-users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(Object.fromEntries(userMap))
// OR: body: JSON.stringify(Array.from(userMap.entries()))
});| await fetch("/api/emails/send-users", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(userMap) | |
| }); | |
| await fetch("/api/emails/send-users", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(Object.fromEntries(userMap)) | |
| }); | |
Spotted by Graphite
Is this helpful? React 👍 or 👎 to let us know.
| parseUserFile(USER_FILE); | ||
| parsePairingFile(PAIR_FILE); |
There was a problem hiding this comment.
Calling async functions at module top-level without await will result in unhandled promise rejections if these functions fail. The functions will execute but errors won't be caught, and the module will continue loading even if the operations fail.
Fix: Either wrap in an async IIFE with error handling or use top-level await:
(async () => {
try {
await parseUserFile(USER_FILE);
await parsePairingFile(PAIR_FILE);
} catch (error) {
console.error('Failed to parse files:', error);
}
})();| parseUserFile(USER_FILE); | |
| parsePairingFile(PAIR_FILE); | |
| (async () => { | |
| try { | |
| await parseUserFile(USER_FILE); | |
| await parsePairingFile(PAIR_FILE); | |
| } catch (error) { | |
| console.error('Failed to parse files:', error); | |
| } | |
| })(); | |
Spotted by Graphite
Is this helpful? React 👍 or 👎 to let us know.
| const results = Papa.parse(userFile, config); | ||
| const userMap = new Map<string, User>(); | ||
|
|
||
| for (const userData of results.data) { | ||
| const user: User = { | ||
| name: userData.Name, | ||
| email: userData.Email, | ||
| intro: userData.Intro, | ||
| linkedin: userData.LinkedIn, | ||
| industry: userData.Industry, | ||
| preferences: userData.Preferences, | ||
| topics: userData.Topics, | ||
| anything: userData.Anything | ||
| }; | ||
|
|
||
| userMap.set(user.email, user); |
There was a problem hiding this comment.
Critical: No error checking or type safety on parsed data
Papa.parse() returns an object with errors array that is not checked. Additionally, userData properties are accessed without validation, which will create User objects with undefined values if column names don't match exactly.
const results = Papa.parse(userFile, config);
// results.errors is never checked!
for (const userData of results.data) {
// userData.Name could be undefined if CSV column is named differently
const user: User = {
name: userData.Name, // No validation!Add error handling and validate that required columns exist before accessing properties.
| const results = Papa.parse(userFile, config); | |
| const userMap = new Map<string, User>(); | |
| for (const userData of results.data) { | |
| const user: User = { | |
| name: userData.Name, | |
| email: userData.Email, | |
| intro: userData.Intro, | |
| linkedin: userData.LinkedIn, | |
| industry: userData.Industry, | |
| preferences: userData.Preferences, | |
| topics: userData.Topics, | |
| anything: userData.Anything | |
| }; | |
| userMap.set(user.email, user); | |
| const results = Papa.parse(userFile, config); | |
| if (results.errors && results.errors.length > 0) { | |
| console.error("CSV parsing errors:", results.errors); | |
| throw new Error(`Failed to parse CSV: ${results.errors.map(e => e.message).join(", ")}`); | |
| } | |
| const userMap = new Map<string, User>(); | |
| for (const userData of results.data) { | |
| if (!userData.Email || !userData.Name) { | |
| console.warn("Skipping row with missing required fields:", userData); | |
| continue; | |
| } | |
| const user: User = { | |
| name: userData.Name, | |
| email: userData.Email, | |
| intro: userData.Intro, | |
| linkedin: userData.LinkedIn, | |
| industry: userData.Industry, | |
| preferences: userData.Preferences, | |
| topics: userData.Topics, | |
| anything: userData.Anything | |
| }; | |
| userMap.set(user.email, user); | |
Spotted by Graphite
Is this helpful? React 👍 or 👎 to let us know.
| await fetch("/api/emails/send-pairings", { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify(pairings) | ||
| }); |
There was a problem hiding this comment.
The fetch call has no error handling. If the API request fails (network error, 4xx/5xx status), the function will silently continue and return the pairings without any indication of failure. This could lead to the application appearing to work while emails are never sent.
const response = await fetch("/api/emails/send-pairings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(pairings)
});
if (!response.ok) {
throw new Error(`Failed to send pairings: ${response.statusText}`);
}| await fetch("/api/emails/send-pairings", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(pairings) | |
| }); | |
| const response = await fetch("/api/emails/send-pairings", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(pairings) | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`Failed to send pairings: ${response.statusText}`); | |
| } | |
Spotted by Graphite
Is this helpful? React 👍 or 👎 to let us know.
|
|
||
| Element supportText = doc.getElementById("input-supportEmail-innerText"); | ||
| assertNotNull(supportText, "Missing element: input-supportEmail-innerText"); | ||
| assertEquals("codebloom@patinanetwork.org", supportText.text(), "supportEmail text not set"); |
There was a problem hiding this comment.
Test expects "codebloom@patinanetwork.org" but the input on line 20 is "patchats@patinanetwork.org". Additionally, the implementation on line 28 of ReactEmailTemplaterImpl.java prepends "mailto:" to the supportEmail, so the actual text would be "mailto:patchats@patinanetwork.org". This test will fail.
assertEquals("mailto:" + supportEmail, supportText.text(), "supportEmail text not set");| assertEquals("codebloom@patinanetwork.org", supportText.text(), "supportEmail text not set"); | |
| assertEquals("mailto:" + supportEmail, supportText.text(), "supportEmail text not set"); |
Spotted by Graphite
Is this helpful? React 👍 or 👎 to let us know.
|




No description provided.