From af4c408bdaba345b7b8ba997feacbfec39bc98d2 Mon Sep 17 00:00:00 2001 From: Eric Bolikowski Date: Wed, 19 Mar 2025 09:18:19 +0100 Subject: [PATCH 1/4] wip --- apps/nestjs-api/src/email/lib/email/email.js | 65 ++++++++++++++++---- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/apps/nestjs-api/src/email/lib/email/email.js b/apps/nestjs-api/src/email/lib/email/email.js index 6aeb1e221..2edc8eb29 100644 --- a/apps/nestjs-api/src/email/lib/email/email.js +++ b/apps/nestjs-api/src/email/lib/email/email.js @@ -4,26 +4,65 @@ import { buildFrontendUrl } from '../build-frontend-url' const Rx = require('rxjs') const mjml2html = require('mjml') -const nodemailer = require('nodemailer') const fs = require('fs') const path = require('path') +const { EmailClient } = require('@azure/communication-email') -const transporter = nodemailer.createTransport({ - host: 'smtp.googlemail.com', - port: 465, - secure: true, - auth: { - user: 'career@redi-school.org', - pass: process.env.NX_GWORKSPACE_EMAIL_PASSWORD, - }, -}) +const connectionString = + process.env.AZURE_COMMUNICATION_CONNECTION_STRING || + 'endpoint=https://red-platform-email-sending.germany.communication.azure.com/;accesskey=A49CnzJqxCFl6YZNidtQpj1wKNutAY66nhKaAU8S8lENpE13Y7FZJQQJ99BAACULyCpc62JTAAAAAZCS8U4P' +const emailClient = new EmailClient(connectionString) + +// Helper function to extract email address from formatted string like "Name " +const extractEmailAddress = (formattedEmail) => { + const matches = formattedEmail.match(/<(.+)>/) + return matches ? matches[1] : formattedEmail +} + +// Helper function to convert email recipients to Azure format +const formatRecipients = (emailAddresses) => { + if (!emailAddresses) return [] + return Array.isArray(emailAddresses) + ? emailAddresses.map((email) => ({ address: extractEmailAddress(email) })) + : [{ address: extractEmailAddress(emailAddresses) }] +} const isProductionOrDemonstration = () => ['production', 'demonstration', 'staging'].includes(process.env.NODE_ENV) -export const sendMjmlEmail = Rx.bindNodeCallback( - transporter.sendMail.bind(transporter) -) +async function _sendMjmlEmail(emailParams) { + console.log('emailParams', emailParams) + const emailMessage = { + senderAddress: extractEmailAddress(emailParams.from), + content: { + subject: emailParams.subject, + html: emailParams.html, + }, + recipients: { + to: formatRecipients(emailParams.to), + bcc: formatRecipients(emailParams.bcc), + }, + } + + try { + const poller = await emailClient.beginSend(emailMessage) + const result = await poller.pollUntilDone() + return result + } catch (error) { + return [error] + } +} + +export const sendMjmlEmail = (emailParams) => + Rx.from(_sendMjmlEmail(emailParams)) + +console.log('sending test email') +sendMjmlEmail({ + to: 'eric@binarylights.com', + subject: 'Test', + html: 'Test', + rediLocation: 'MALMO', +}).subscribe() // TODO: I'm a duplicate of getSenderDetails in apps/api/lib/email/email.js, keep me in sync const getSenderDetails = (rediLocation) => { From 7db9e5e3e9c9c68be34e781c1b6619946c80b3bd Mon Sep 17 00:00:00 2001 From: Eric Bolikowski Date: Wed, 19 Mar 2025 09:41:02 +0100 Subject: [PATCH 2/4] finish refactor to send emails via azure --- apps/nestjs-api/src/email/lib/email/email.js | 63 +++++++------------- 1 file changed, 21 insertions(+), 42 deletions(-) diff --git a/apps/nestjs-api/src/email/lib/email/email.js b/apps/nestjs-api/src/email/lib/email/email.js index 2edc8eb29..59af1b7c3 100644 --- a/apps/nestjs-api/src/email/lib/email/email.js +++ b/apps/nestjs-api/src/email/lib/email/email.js @@ -13,56 +13,35 @@ const connectionString = 'endpoint=https://red-platform-email-sending.germany.communication.azure.com/;accesskey=A49CnzJqxCFl6YZNidtQpj1wKNutAY66nhKaAU8S8lENpE13Y7FZJQQJ99BAACULyCpc62JTAAAAAZCS8U4P' const emailClient = new EmailClient(connectionString) -// Helper function to extract email address from formatted string like "Name " -const extractEmailAddress = (formattedEmail) => { - const matches = formattedEmail.match(/<(.+)>/) - return matches ? matches[1] : formattedEmail -} - -// Helper function to convert email recipients to Azure format const formatRecipients = (emailAddresses) => { if (!emailAddresses) return [] - return Array.isArray(emailAddresses) - ? emailAddresses.map((email) => ({ address: extractEmailAddress(email) })) - : [{ address: extractEmailAddress(emailAddresses) }] + if (typeof emailAddresses === 'string') return [{ address: emailAddresses }] + return emailAddresses.map((email) => ({ address: email })) } const isProductionOrDemonstration = () => ['production', 'demonstration', 'staging'].includes(process.env.NODE_ENV) -async function _sendMjmlEmail(emailParams) { - console.log('emailParams', emailParams) - const emailMessage = { - senderAddress: extractEmailAddress(emailParams.from), - content: { - subject: emailParams.subject, - html: emailParams.html, - }, - recipients: { - to: formatRecipients(emailParams.to), - bcc: formatRecipients(emailParams.bcc), - }, - } - - try { - const poller = await emailClient.beginSend(emailMessage) - const result = await poller.pollUntilDone() - return result - } catch (error) { - return [error] - } -} - export const sendMjmlEmail = (emailParams) => - Rx.from(_sendMjmlEmail(emailParams)) - -console.log('sending test email') -sendMjmlEmail({ - to: 'eric@binarylights.com', - subject: 'Test', - html: 'Test', - rediLocation: 'MALMO', -}).subscribe() + Rx.from( + (async function iifeSendEmail() { + const emailMessage = { + senderAddress: 'career@redi-school.org', + content: { + subject: emailParams.subject, + html: emailParams.html, + }, + recipients: { + to: formatRecipients(emailParams.to), + bcc: formatRecipients(emailParams.bcc), + }, + } + + const poller = await emailClient.beginSend(emailMessage) + const result = await poller.pollUntilDone() + return result + })() + ) // TODO: I'm a duplicate of getSenderDetails in apps/api/lib/email/email.js, keep me in sync const getSenderDetails = (rediLocation) => { From ff763bbe43974878e3c9fe90317d01f3ba9cd5d2 Mon Sep 17 00:00:00 2001 From: Eric Bolikowski Date: Mon, 24 Mar 2025 08:53:57 +0100 Subject: [PATCH 3/4] experimental implementation of azure email sending --- apps/nestjs-api/src/email/lib/email/email.js | 2 +- loopback-standalone/lib/email/email.js | 44 ++++-- package.json | 1 + yarn.lock | 141 +++++++++++++++++++ 4 files changed, 174 insertions(+), 14 deletions(-) diff --git a/apps/nestjs-api/src/email/lib/email/email.js b/apps/nestjs-api/src/email/lib/email/email.js index 59af1b7c3..7ad368d54 100644 --- a/apps/nestjs-api/src/email/lib/email/email.js +++ b/apps/nestjs-api/src/email/lib/email/email.js @@ -50,7 +50,7 @@ const getSenderDetails = (rediLocation) => { ? 'ReDI Malmö Team' : 'ReDI Talent Success Team' const senderEmail = isMalmoLocation - ? 'career@redi-school.org' // TODO: set back to career-sweden when we send email via Azure + ? 'career-sweden@redi-school.org' // TODO: set back to career-sweden when we send email via Azure : 'career@redi-school.org' return { senderName, senderEmail } } diff --git a/loopback-standalone/lib/email/email.js b/loopback-standalone/lib/email/email.js index fb792b2a8..a4da0ae92 100644 --- a/loopback-standalone/lib/email/email.js +++ b/loopback-standalone/lib/email/email.js @@ -2,29 +2,47 @@ const Rx = require('rxjs') const mjml2html = require('mjml') -const nodemailer = require('nodemailer') const fs = require('fs') const path = require('path') +const { EmailClient } = require('@azure/communication-email') const { buildFrontendUrl } = require('../build-frontend-url') const { buildBackendUrl } = require('../build-backend-url') -const transporter = nodemailer.createTransport({ - host: 'smtp.googlemail.com', - port: 465, - secure: true, - auth: { - user: 'career@redi-school.org', - pass: process.env.NX_GWORKSPACE_EMAIL_PASSWORD, - }, -}) +const connectionString = + process.env.AZURE_COMMUNICATION_CONNECTION_STRING || + 'endpoint=https://red-platform-email-sending.germany.communication.azure.com/;accesskey=A49CnzJqxCFl6YZNidtQpj1wKNutAY66nhKaAU8S8lENpE13Y7FZJQQJ99BAACULyCpc62JTAAAAAZCS8U4P' +const emailClient = new EmailClient(connectionString) + +const formatRecipients = (emailAddresses) => { + if (!emailAddresses) return [] + if (typeof emailAddresses === 'string') return [{ address: emailAddresses }] + return emailAddresses.map((email) => ({ address: email })) +} const isProductionOrDemonstration = () => ['production', 'demonstration', 'staging'].includes(process.env.NODE_ENV) -const sendMjmlEmail = Rx.bindNodeCallback( - transporter.sendMail.bind(transporter) -) +export const sendMjmlEmail = (emailParams) => + Rx.from( + (async function iifeSendEmail() { + const emailMessage = { + senderAddress: 'career@redi-school.org', + content: { + subject: emailParams.subject, + html: emailParams.html, + }, + recipients: { + to: formatRecipients(emailParams.to), + bcc: formatRecipients(emailParams.bcc), + }, + } + + const poller = await emailClient.beginSend(emailMessage) + const result = await poller.pollUntilDone() + return result + })() + ) // TODO: I'm a duplicate of getSenderDetails in apps/nestjs-api/src/email/lib/email/email.js, keep me in sync const getSenderDetails = (rediLocation) => { diff --git a/package.json b/package.json index 0d96f25c1..84d828422 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ }, "private": true, "dependencies": { + "@azure/communication-email": "^1.0.0", "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", "@formbricks/js": "^2.1.0", diff --git a/yarn.lock b/yarn.lock index 34a0ab6d6..2444d5ed3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -164,6 +164,116 @@ dependencies: xss "^1.0.8" +"@azure/abort-controller@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-1.1.0.tgz#788ee78457a55af8a1ad342acb182383d2119249" + integrity sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw== + dependencies: + tslib "^2.2.0" + +"@azure/abort-controller@^2.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-2.1.2.tgz#42fe0ccab23841d9905812c58f1082d27784566d" + integrity sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA== + dependencies: + tslib "^2.6.2" + +"@azure/communication-common@^2.2.0": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@azure/communication-common/-/communication-common-2.3.1.tgz#2a847a2d9c7e7b479db4f90e0a46484cd86179f7" + integrity sha512-6ZQt20iMZbyckQn4m1TDwiDv3Fzyt1h4lnQ1szBBns2x3VQY9XHbnskPtvUdwK/HT+c/1PoUwof3toy1AIznbQ== + dependencies: + "@azure/abort-controller" "^1.0.0" + "@azure/core-auth" "^1.3.0" + "@azure/core-rest-pipeline" "^1.3.2" + "@azure/core-tracing" "^1.0.0" + "@azure/core-util" "^1.0.0" + events "^3.0.0" + jwt-decode "^4.0.0" + tslib "^2.2.0" + +"@azure/communication-email@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@azure/communication-email/-/communication-email-1.0.0.tgz#7a15d0438e2438aba1953202890d3908b49f41b3" + integrity sha512-aY/qE3u4gadd6I895WOJPXrbKaPqeFDxGOK5xgAAqHkqNadI+hCp/D59q5Kfcj5Qcxal6mLm1GwZ1Cka0x4KZw== + dependencies: + "@azure/communication-common" "^2.2.0" + "@azure/core-auth" "^1.3.0" + "@azure/core-client" "^1.3.2" + "@azure/core-lro" "^2.5.0" + "@azure/core-rest-pipeline" "^1.8.0" + "@azure/logger" "^1.0.0" + tslib "^1.9.3" + uuid "^8.3.2" + +"@azure/core-auth@^1.3.0", "@azure/core-auth@^1.4.0", "@azure/core-auth@^1.8.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@azure/core-auth/-/core-auth-1.9.0.tgz#ac725b03fabe3c892371065ee9e2041bee0fd1ac" + integrity sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-util" "^1.11.0" + tslib "^2.6.2" + +"@azure/core-client@^1.3.2": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@azure/core-client/-/core-client-1.9.2.tgz#6fc69cee2816883ab6c5cdd653ee4f2ff9774f74" + integrity sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-auth" "^1.4.0" + "@azure/core-rest-pipeline" "^1.9.1" + "@azure/core-tracing" "^1.0.0" + "@azure/core-util" "^1.6.1" + "@azure/logger" "^1.0.0" + tslib "^2.6.2" + +"@azure/core-lro@^2.5.0": + version "2.7.2" + resolved "https://registry.yarnpkg.com/@azure/core-lro/-/core-lro-2.7.2.tgz#787105027a20e45c77651a98b01a4d3b01b75a08" + integrity sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-util" "^1.2.0" + "@azure/logger" "^1.0.0" + tslib "^2.6.2" + +"@azure/core-rest-pipeline@^1.3.2", "@azure/core-rest-pipeline@^1.8.0", "@azure/core-rest-pipeline@^1.9.1": + version "1.19.0" + resolved "https://registry.yarnpkg.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.19.0.tgz#4cc60d3f2ee68cf0ef379851b4ed175f7932c8c5" + integrity sha512-bM3308LRyg5g7r3Twprtqww0R/r7+GyVxj4BafcmVPo4WQoGt5JXuaqxHEFjw2o3rvFZcUPiqJMg6WuvEEeVUA== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-auth" "^1.8.0" + "@azure/core-tracing" "^1.0.1" + "@azure/core-util" "^1.11.0" + "@azure/logger" "^1.0.0" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.0" + tslib "^2.6.2" + +"@azure/core-tracing@^1.0.0", "@azure/core-tracing@^1.0.1": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@azure/core-tracing/-/core-tracing-1.2.0.tgz#7be5d53c3522d639cf19042cbcdb19f71bc35ab2" + integrity sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg== + dependencies: + tslib "^2.6.2" + +"@azure/core-util@^1.0.0", "@azure/core-util@^1.11.0", "@azure/core-util@^1.2.0", "@azure/core-util@^1.6.1": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.11.0.tgz#f530fc67e738aea872fbdd1cc8416e70219fada7" + integrity sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g== + dependencies: + "@azure/abort-controller" "^2.0.0" + tslib "^2.6.2" + +"@azure/logger@^1.0.0": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@azure/logger/-/logger-1.1.4.tgz#223cbf2b424dfa66478ce9a4f575f59c6f379768" + integrity sha512-4IXXzcCdLdlXuCG+8UKEwLA1T1NHqUfanhXYHiQTn+6sfWCZXduqbtXDGceg3Ce5QxTGo7EqmbV6Bi+aqKuClQ== + dependencies: + tslib "^2.6.2" + "@babel/code-frame@7.12.11": version "7.12.11" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz" @@ -6281,6 +6391,11 @@ agent-base@6: dependencies: debug "4" +agent-base@^7.1.0, agent-base@^7.1.2: + version "7.1.3" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.3.tgz#29435eb821bc4194633a5b89e5bc4703bafc25a1" + integrity sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw== + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz" @@ -12334,6 +12449,14 @@ http-proxy-agent@^5.0.0: agent-base "6" debug "4" +http-proxy-agent@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" + integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + http-proxy-middleware@^2.0.3: version "2.0.4" resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz" @@ -12430,6 +12553,14 @@ https-proxy-agent@^5.0.1: agent-base "6" debug "4" +https-proxy-agent@^7.0.0: + version "7.0.6" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" + integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== + dependencies: + agent-base "^7.1.2" + debug "4" + human-signals@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz" @@ -14395,6 +14526,11 @@ jws@^3.2.2: jwa "^1.4.1" safe-buffer "^5.0.1" +jwt-decode@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b" + integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA== + keypress@~0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/keypress/-/keypress-0.2.1.tgz" @@ -21245,6 +21381,11 @@ tslib@^2, tslib@~2.4.0: resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== +tslib@^2.6.2: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tsutils@^3.17.1, tsutils@^3.21.0: version "3.21.0" resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" From d3d31076324117d8f24140a05c6abfbb7726e364 Mon Sep 17 00:00:00 2001 From: Eric Bolikowski Date: Mon, 24 Mar 2025 08:59:23 +0100 Subject: [PATCH 4/4] throw error on missing env --- apps/nestjs-api/src/email/lib/email/email.js | 6 +++--- loopback-standalone/lib/email/email.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/nestjs-api/src/email/lib/email/email.js b/apps/nestjs-api/src/email/lib/email/email.js index 7ad368d54..93a00395c 100644 --- a/apps/nestjs-api/src/email/lib/email/email.js +++ b/apps/nestjs-api/src/email/lib/email/email.js @@ -8,9 +8,9 @@ const fs = require('fs') const path = require('path') const { EmailClient } = require('@azure/communication-email') -const connectionString = - process.env.AZURE_COMMUNICATION_CONNECTION_STRING || - 'endpoint=https://red-platform-email-sending.germany.communication.azure.com/;accesskey=A49CnzJqxCFl6YZNidtQpj1wKNutAY66nhKaAU8S8lENpE13Y7FZJQQJ99BAACULyCpc62JTAAAAAZCS8U4P' +const connectionString = process.env.AZURE_COMMUNICATION_CONNECTION_STRING +if (!connectionString) + throw new Error('AZURE_COMMUNICATION_CONNECTION_STRING env var not set') const emailClient = new EmailClient(connectionString) const formatRecipients = (emailAddresses) => { diff --git a/loopback-standalone/lib/email/email.js b/loopback-standalone/lib/email/email.js index a4da0ae92..b79523467 100644 --- a/loopback-standalone/lib/email/email.js +++ b/loopback-standalone/lib/email/email.js @@ -9,9 +9,9 @@ const { EmailClient } = require('@azure/communication-email') const { buildFrontendUrl } = require('../build-frontend-url') const { buildBackendUrl } = require('../build-backend-url') -const connectionString = - process.env.AZURE_COMMUNICATION_CONNECTION_STRING || - 'endpoint=https://red-platform-email-sending.germany.communication.azure.com/;accesskey=A49CnzJqxCFl6YZNidtQpj1wKNutAY66nhKaAU8S8lENpE13Y7FZJQQJ99BAACULyCpc62JTAAAAAZCS8U4P' +const connectionString = process.env.AZURE_COMMUNICATION_CONNECTION_STRING +if (!connectionString) + throw new Error('AZURE_COMMUNICATION_CONNECTION_STRING env var not set') const emailClient = new EmailClient(connectionString) const formatRecipients = (emailAddresses) => {