Skip to content

Commit 0a884b8

Browse files
Send internal emails to ACM email (#244)
If that email doesn't exist, then it will be forwarded to the Illinois email. --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
1 parent 9a6831b commit 0a884b8

File tree

8 files changed

+293
-27
lines changed

8 files changed

+293
-27
lines changed

src/api/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"pino": "^9.6.0",
5959
"pluralize": "^8.0.0",
6060
"qrcode": "^1.5.4",
61+
"sanitize-html": "^2.17.0",
6162
"stripe": "^18.0.0",
6263
"uuid": "^11.1.0",
6364
"zod": "^4.0.14",
@@ -67,9 +68,10 @@
6768
"@tsconfig/node22": "^22.0.1",
6869
"@types/aws-lambda": "^8.10.152",
6970
"@types/qrcode": "^1.5.5",
71+
"@types/sanitize-html": "^2.16.0",
7072
"esbuild-copy-static-files": "^0.1.0",
7173
"nodemon": "^3.1.10",
7274
"pino-pretty": "^13.1.1",
7375
"yaml": "^2.8.0"
7476
}
75-
}
77+
}

src/api/routes/apiKey.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
import * as z from "zod/v4";
2525
import { AvailableSQSFunctions, SQSPayload } from "common/types/sqsMessage.js";
2626
import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
27+
import { getAllUserEmails } from "common/utils.js";
2728

2829
const apiKeyRoute: FastifyPluginAsync = async (fastify, _options) => {
2930
await fastify.register(rateLimiter, {
@@ -96,7 +97,7 @@ const apiKeyRoute: FastifyPluginAsync = async (fastify, _options) => {
9697
reqId: request.id,
9798
},
9899
payload: {
99-
to: [request.username!],
100+
to: getAllUserEmails(request.username),
100101
subject: "Important: API Key Created",
101102
content: `
102103
This email confirms that an API key for the Core API has been generated from your account.
@@ -203,7 +204,7 @@ If you did not create this API key, please secure your account and notify the AC
203204
reqId: request.id,
204205
},
205206
payload: {
206-
to: [request.username!],
207+
to: getAllUserEmails(request.username),
207208
subject: "Important: API Key Deleted",
208209
content: `
209210
This email confirms that an API key for the Core API has been deleted from your account.

src/api/routes/iam.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { AvailableSQSFunctions } from "common/types/sqsMessage.js";
4444
import { SendMessageBatchCommand, SQSClient } from "@aws-sdk/client-sqs";
4545
import { randomUUID } from "crypto";
4646
import { getKey, setKey } from "api/functions/redisCache.js";
47+
import { getAllUserEmails } from "common/utils.js";
4748

4849
const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
4950
const getAuthorizedClients = async () => {
@@ -456,7 +457,7 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
456457
reqId: request.id,
457458
},
458459
payload: {
459-
to: [x],
460+
to: getAllUserEmails(x),
460461
subject: "You have been added to an access group",
461462
content: `
462463
Hello,
@@ -478,7 +479,7 @@ No action is required from you at this time.
478479
reqId: request.id,
479480
},
480481
payload: {
481-
to: [x],
482+
to: getAllUserEmails(x),
482483
subject: "You have been removed from an access group",
483484
content: `
484485
Hello,

src/api/routes/roomRequests.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { buildAuditLogTransactPut } from "api/functions/auditLog.js";
3232
import { Modules } from "common/modules.js";
3333
import {
3434
generateProjectionParams,
35+
getAllUserEmails,
3536
getDefaultFilteringQuerystring,
3637
nonEmptyCommaSeparatedStringSchema,
3738
} from "common/utils.js";
@@ -142,7 +143,7 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
142143
reqId: request.id,
143144
},
144145
payload: {
145-
to: [originalRequestor],
146+
to: getAllUserEmails(originalRequestor),
146147
subject: "Room Reservation Request Status Change",
147148
content: `Your Room Reservation Request has been been moved to status "${formatStatus(request.body.status)}". Please visit the management portal for more details.`,
148149
callToActionButton: {

src/api/routes/stripe.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import rawbody from "fastify-raw-body";
4444
import { AvailableSQSFunctions, SQSPayload } from "common/types/sqsMessage.js";
4545
import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
4646
import * as z from "zod/v4";
47+
import { getAllUserEmails } from "common/utils.js";
4748

4849
const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
4950
await fastify.register(rawbody, {
@@ -412,7 +413,7 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
412413
reqId: request.id,
413414
},
414415
payload: {
415-
to: [unmarshalledEntry.userId],
416+
to: getAllUserEmails(unmarshalledEntry.userId),
416417
subject: `Payment Failed for Invoice ${unmarshalledEntry.invoiceId}`,
417418
content: `
418419
A ${paidInFull ? "full" : "partial"} payment for Invoice ${unmarshalledEntry.invoiceId} (${withCurrency} paid by ${name}, ${email}) <b>has failed.</b>
@@ -565,7 +566,7 @@ Please ask the payee to try again, perhaps with a different payment method, or c
565566
reqId: request.id,
566567
},
567568
payload: {
568-
to: [unmarshalledEntry.userId],
569+
to: getAllUserEmails(unmarshalledEntry.userId),
569570
subject: `Payment Pending for Invoice ${unmarshalledEntry.invoiceId}`,
570571
content: `
571572
ACM @ UIUC has received intent of ${paidInFull ? "full" : "partial"} payment for Invoice ${unmarshalledEntry.invoiceId} (${withCurrency} paid by ${name}, ${email}).
@@ -610,7 +611,7 @@ Please contact Officer Board with any questions.
610611
reqId: request.id,
611612
},
612613
payload: {
613-
to: [unmarshalledEntry.userId],
614+
to: getAllUserEmails(unmarshalledEntry.userId),
614615
cc: [
615616
notificationRecipients[fastify.runEnvironment]
616617
.Treasurer,

src/api/sqs/handlers/emailNotifications.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { createAuditLogEntry } from "api/functions/auditLog.js";
66
import { Modules } from "common/modules.js";
77
import Handlebars from "handlebars";
88
import emailTemplate from "./templates/notification.js";
9+
import sanitizeHtml from "sanitize-html";
910

1011
Handlebars.registerHelper("nl2br", (text) => {
1112
let nl2br = `${text}`.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, "$1<br>$2");
@@ -16,17 +17,22 @@ Handlebars.registerHelper("nl2br", (text) => {
1617
const compiledTemplate = Handlebars.compile(emailTemplate);
1718

1819
const stripHtml = (html: string): string => {
19-
return html
20-
.replace(/<[^>]*>/g, "") // Remove HTML tags
21-
.replace(/&nbsp;/g, " ") // Replace non-breaking spaces
22-
.replace(/\s+/g, " ") // Normalize whitespace
23-
.trim();
20+
// Remove all HTML tags and attributes, then normalize whitespace and trim
21+
const sanitized = sanitizeHtml(html, {
22+
allowedTags: [],
23+
allowedAttributes: {},
24+
});
25+
return sanitized.replace(/\s+/g, " ").trim();
2426
};
2527

2628
export const emailNotificationsHandler: SQSHandlerFunction<
2729
AvailableSQSFunctions.EmailNotifications
2830
> = async (payload, metadata, logger) => {
2931
const { to, cc, bcc, content, subject } = payload;
32+
if (to.length + (cc || []).length + (bcc || []).length === 0) {
33+
logger.warn("Found no message recipients. Exiting without calling SES.");
34+
return;
35+
}
3036
const senderEmailAddress = `notifications@${currentEnvironmentConfig.EmailDomain}`;
3137
const senderEmail = `ACM @ UIUC <${senderEmailAddress}>`;
3238
logger.info("Constructing email...");
@@ -59,17 +65,16 @@ export const emailNotificationsHandler: SQSHandlerFunction<
5965
},
6066
},
6167
});
62-
const logPromise = createAuditLogEntry({
68+
const sesClient = new SESClient({ region: genericConfig.AwsRegion });
69+
const response = await sesClient.send(command);
70+
logger.info("Sent!");
71+
await createAuditLogEntry({
6372
entry: {
6473
module: Modules.EMAIL_NOTIFICATION,
6574
actor: metadata.initiator,
66-
target: to.join(";"),
75+
target: [...to, ...(bcc || []), ...(cc || [])].join(";"),
6776
message: `Sent email notification with subject "${subject}".`,
6877
},
6978
});
70-
const sesClient = new SESClient({ region: genericConfig.AwsRegion });
71-
const response = await sesClient.send(command);
72-
logger.info("Sent!");
73-
await logPromise;
7479
return response;
7580
};

src/common/utils.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,10 @@ export const getDefaultFilteringQuerystring = ({ defaultSelect }: GetDefaultFilt
5353
})
5454
};
5555
};
56+
57+
export const getAllUserEmails = (username?: string) => {
58+
if (!username) {
59+
return [];
60+
}
61+
return [username.replace("@illinois.edu", "@acm.illinois.edu")]
62+
}

0 commit comments

Comments
 (0)