From 2202a4a231eb15e81fedc6ac551fb550f1bce2e6 Mon Sep 17 00:00:00 2001 From: Paul Goldschmidt Date: Wed, 10 Jun 2026 23:12:23 +0200 Subject: [PATCH 1/3] add test email function --- functions/package-lock.json | 21 +++++++++++ functions/package.json | 4 +- functions/src/functions/hourlyTestEmail.ts | 44 ++++++++++++++++++++++ functions/src/index.ts | 1 + 4 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 functions/src/functions/hourlyTestEmail.ts diff --git a/functions/package-lock.json b/functions/package-lock.json index 723434e..8df65cc 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -17,6 +17,7 @@ "jspdf": "^4.2.1", "jspdf-autotable": "^5.0.2", "luxon": "^3.7.2", + "nodemailer": "^8.0.11", "openai": "^6.17.0", "zod": "^3.25.76" }, @@ -29,6 +30,7 @@ "@types/luxon": "^3.4.2", "@types/mocha": "^10.0.7", "@types/node": "^22", + "@types/nodemailer": "^8.0.0", "@types/sinon": "^21.0.0", "@typescript-eslint/eslint-plugin": "^8.54.0", "@typescript-eslint/parser": "^8.54.0", @@ -3536,6 +3538,16 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/nodemailer": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.0.tgz", + "integrity": "sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/pako": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", @@ -10632,6 +10644,15 @@ "license": "MIT", "peer": true }, + "node_modules/nodemailer": { + "version": "8.0.11", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.11.tgz", + "integrity": "sha512-nrO/pDAUKl+wXX+lx16tDLbnm0fW6sK/x8mgohaCpg+CdCEl482bD4tCuAZk2DyliruiNTIZxRCoWkDqJEnAiA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", diff --git a/functions/package.json b/functions/package.json index fc8f51e..79667fd 100644 --- a/functions/package.json +++ b/functions/package.json @@ -34,6 +34,7 @@ "jspdf": "^4.2.1", "jspdf-autotable": "^5.0.2", "luxon": "^3.7.2", + "nodemailer": "^8.0.11", "openai": "^6.17.0", "zod": "^3.25.76" }, @@ -46,6 +47,7 @@ "@types/luxon": "^3.4.2", "@types/mocha": "^10.0.7", "@types/node": "^22", + "@types/nodemailer": "^8.0.0", "@types/sinon": "^21.0.0", "@typescript-eslint/eslint-plugin": "^8.54.0", "@typescript-eslint/parser": "^8.54.0", @@ -70,4 +72,4 @@ "serialize-javascript": "^7.0.4", "http-proxy-agent": "^7.0.2" } -} \ No newline at end of file +} diff --git a/functions/src/functions/hourlyTestEmail.ts b/functions/src/functions/hourlyTestEmail.ts new file mode 100644 index 0000000..45613d5 --- /dev/null +++ b/functions/src/functions/hourlyTestEmail.ts @@ -0,0 +1,44 @@ +// This source file is part of the MyHeart Counts project +// +// SPDX-FileCopyrightText: 2026 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-License-Identifier: MIT + +import { logger } from "firebase-functions/v2"; +import { onSchedule } from "firebase-functions/v2/scheduler"; +import nodemailer from "nodemailer"; +import { defaultServiceAccount } from "./helpers.js"; + +const senderAddress = "myheartcounts@stanford.edu"; +const recipientAddress = "goldschmidt@stanford.edu"; + +export const sendTestEmail = async (): Promise => { + // GCP blocks outbound port 25, so we default to 587 with STARTTLS + const transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST ?? "smtp.stanford.edu", + port: Number(process.env.SMTP_PORT ?? 587), + secure: false, + }); + + const info = await transporter.sendMail({ + from: `"MyHeart Counts" <${senderAddress}>`, + to: recipientAddress, + subject: "MyHeart Counts SMTP relay test", + text: `This is an automated hourly test email from the MyHeart Counts Firebase functions, sent at ${new Date().toISOString()}.`, + }); + logger.info(`Test email sent to ${recipientAddress}: ${info.response}`); +}; + +export const hourlyTestEmail = onSchedule( + { + schedule: "every 1 hours", + timeZone: "UTC", + serviceAccount: defaultServiceAccount, + }, + async (_event) => { + try { + await sendTestEmail(); + } catch (error) { + logger.error("Failed to send test email:", error); + } + }, +); diff --git a/functions/src/index.ts b/functions/src/index.ts index 2552dd2..a91e9ee 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -64,3 +64,4 @@ export * from "./functions/processUserDeletions.js"; // trigger is back and backfill has populated the target docs. // export * from "./functions/processPendingHealthSampleDeletions.js"; export * from "./functions/backfillExtendedActivityNudgesOptIn.js"; +export * from "./functions/hourlyTestEmail.js"; From 4dd1559e97791eaf239d10474b0aedb001689ceb Mon Sep 17 00:00:00 2001 From: Paul Goldschmidt Date: Thu, 11 Jun 2026 11:44:55 +0200 Subject: [PATCH 2/3] get egress IP --- functions/src/functions/hourlyTestEmail.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/functions/src/functions/hourlyTestEmail.ts b/functions/src/functions/hourlyTestEmail.ts index 45613d5..b8b4a44 100644 --- a/functions/src/functions/hourlyTestEmail.ts +++ b/functions/src/functions/hourlyTestEmail.ts @@ -11,7 +11,20 @@ import { defaultServiceAccount } from "./helpers.js"; const senderAddress = "myheartcounts@stanford.edu"; const recipientAddress = "goldschmidt@stanford.edu"; +const getEgressIp = async (): Promise => { + try { + const response = await fetch("https://api.ipify.org"); + return await response.text(); + } catch (error) { + logger.warn("Failed to determine egress IP:", error); + return "unknown"; + } +}; + export const sendTestEmail = async (): Promise => { + const egressIp = await getEgressIp(); + logger.info(`Sending test email from egress IP ${egressIp}`); + // GCP blocks outbound port 25, so we default to 587 with STARTTLS const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST ?? "smtp.stanford.edu", @@ -23,9 +36,11 @@ export const sendTestEmail = async (): Promise => { from: `"MyHeart Counts" <${senderAddress}>`, to: recipientAddress, subject: "MyHeart Counts SMTP relay test", - text: `This is an automated hourly test email from the MyHeart Counts Firebase functions, sent at ${new Date().toISOString()}.`, + text: `This is an automated hourly test email from the MyHeart Counts Firebase functions, sent at ${new Date().toISOString()} from egress IP ${egressIp}.`, }); - logger.info(`Test email sent to ${recipientAddress}: ${info.response}`); + logger.info( + `Test email sent to ${recipientAddress} from egress IP ${egressIp}: ${info.response}`, + ); }; export const hourlyTestEmail = onSchedule( From 6b51b6b6a2b917bbcc13030a75e536fbb422f95e Mon Sep 17 00:00:00 2001 From: Paul Goldschmidt Date: Thu, 11 Jun 2026 21:21:00 +0200 Subject: [PATCH 3/3] add runs_on_labels to use ubuntu runners --- .github/workflows/build-and-test.yml | 2 ++ .github/workflows/monthly-markdown-link-check.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index a76019c..4badfaf 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -21,6 +21,8 @@ jobs: markdownlinkcheck: name: Markdown Link Check uses: StanfordBDHG/.github/.github/workflows/markdown-link-check.yml@v2 + with: + runs_on_labels: '["ubuntu-latest"]' lint: name: Lint runs-on: ubuntu-latest diff --git a/.github/workflows/monthly-markdown-link-check.yml b/.github/workflows/monthly-markdown-link-check.yml index b89cccd..ca9ef42 100644 --- a/.github/workflows/monthly-markdown-link-check.yml +++ b/.github/workflows/monthly-markdown-link-check.yml @@ -13,3 +13,5 @@ jobs: markdown_link_check: name: Markdown Link Check uses: StanfordBDHG/.github/.github/workflows/markdown-link-check.yml@v2 + with: + runs_on_labels: '["ubuntu-latest"]'