From 62d84a1a453050f25ecf92fb8e1d76adee64c91e Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Mon, 10 Feb 2025 16:59:38 +0100 Subject: [PATCH 1/5] Initial implementation of Azure Communication Service transport Signed-off-by: Jonathan Mezach --- .../config.d.ts | 12 +++ .../package.json | 2 + .../processor/NotificationsEmailProcessor.ts | 3 + .../src/processor/transports/azure.ts | 81 +++++++++++++++++ .../src/processor/transports/index.ts | 1 + yarn.lock | 87 ++++++++++++++----- 6 files changed, 163 insertions(+), 23 deletions(-) create mode 100644 plugins/notifications-backend-module-email/src/processor/transports/azure.ts diff --git a/plugins/notifications-backend-module-email/config.d.ts b/plugins/notifications-backend-module-email/config.d.ts index eaed835485..bce7a4b332 100644 --- a/plugins/notifications-backend-module-email/config.d.ts +++ b/plugins/notifications-backend-module-email/config.d.ts @@ -83,6 +83,18 @@ export interface Config { | { /** Only for debugging, disables the actual sending of emails */ transport: 'stream'; + } + | { + transport: 'azure'; + /** + * Azure Communication Services endpoint + */ + endpoint: string; + /** + * Optional Azure Communication Services access key + * @visibility secret + */ + accessKey?: string; }; /** * Sender email address diff --git a/plugins/notifications-backend-module-email/package.json b/plugins/notifications-backend-module-email/package.json index dd3e4dbb78..5494832979 100644 --- a/plugins/notifications-backend-module-email/package.json +++ b/plugins/notifications-backend-module-email/package.json @@ -36,6 +36,8 @@ "dependencies": { "@aws-sdk/client-ses": "^3.550.0", "@aws-sdk/types": "^3.347.0", + "@azure/communication-email": "^1.0.0", + "@azure/identity": "^4.0.0", "@backstage/backend-plugin-api": "workspace:^", "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", diff --git a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts index c88dda2ac0..d165973424 100644 --- a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts +++ b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts @@ -32,6 +32,7 @@ import { NotificationProcessorFilters, } from '@backstage/plugin-notifications-common'; import { + createAzureTransport, createSendmailTransport, createSesTransport, createSmtpTransport, @@ -117,6 +118,8 @@ export class NotificationsEmailProcessor implements NotificationProcessor { this.transporter = createSendmailTransport(this.transportConfig); } else if (transport === 'stream') { this.transporter = createStreamTransport(); + } else if (transport === 'azure') { + this.transporter = createAzureTransport(this.transportConfig); } else { throw new Error(`Unsupported transport: ${transport}`); } diff --git a/plugins/notifications-backend-module-email/src/processor/transports/azure.ts b/plugins/notifications-backend-module-email/src/processor/transports/azure.ts new file mode 100644 index 0000000000..d00d8c9574 --- /dev/null +++ b/plugins/notifications-backend-module-email/src/processor/transports/azure.ts @@ -0,0 +1,81 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createTransport } from 'nodemailer'; +import { + EmailClient, + EmailMessage, + EmailRecipients, +} from '@azure/communication-email'; +import { DefaultAzureCredential } from '@azure/identity'; +import { Config } from '@backstage/config'; +import MailMessage from 'nodemailer/lib/mailer/mail-message'; + +export const createAzureTransport = async (config: Config) => { + const accessKey = config.getOptionalString('accessKey'); + const credentials = + accessKey === undefined ? new DefaultAzureCredential() : { key: accessKey }; + const emailClient = new EmailClient( + config.getString('endpoint'), + credentials, + ); + const transport = { + name: 'azure', + version: '1.0.0', + send: async ( + mail: MailMessage, + callback: (err: Error | null, info: {} | null) => Promise, + ) => { + const envelope = mail.data.envelope || mail.message.getEnvelope(); + const from = + typeof envelope.from === 'string' + ? envelope.from + : config.getString('senderAddress'); + const to = + typeof envelope.to === 'string' ? [envelope.to] : envelope.to ?? []; + const recipients: EmailRecipients = { + to: to.map(address => ({ address })), + }; + + const content = { + subject: mail.message.getHeader('Subject'), + html: + typeof mail.data.html === 'string' + ? mail.data.html + : mail.data.html?.toString('utf-8'), + plainText: + typeof mail.data.text === 'string' + ? mail.data.text + : mail.data.text?.toString('utf-8') ?? 'No content', + }; + + const emailMessage: EmailMessage = { + senderAddress: from, + recipients: recipients, + content: content, + }; + + try { + const poller = await emailClient.beginSend(emailMessage); + const response = await poller.pollUntilDone(); + callback(null, response); + } catch (error) { + console.error(error); + callback(error, null); + } + }, + }; + return createTransport(transport); +}; diff --git a/plugins/notifications-backend-module-email/src/processor/transports/index.ts b/plugins/notifications-backend-module-email/src/processor/transports/index.ts index 9a679f3723..2a6b37ffd1 100644 --- a/plugins/notifications-backend-module-email/src/processor/transports/index.ts +++ b/plugins/notifications-backend-module-email/src/processor/transports/index.ts @@ -17,3 +17,4 @@ export { createSmtpTransport } from './smtp'; export { createSesTransport } from './ses'; export { createSendmailTransport } from './sendmail'; export { createStreamTransport } from './stream'; +export { createAzureTransport } from './azure'; diff --git a/yarn.lock b/yarn.lock index 074909bfa3..0b05f3639a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1656,18 +1656,50 @@ __metadata: languageName: node linkType: hard -"@azure/core-auth@npm:^1.4.0, @azure/core-auth@npm:^1.5.0": - version: 1.5.0 - resolution: "@azure/core-auth@npm:1.5.0" +"@azure/communication-common@npm:^2.2.0": + version: 2.3.1 + resolution: "@azure/communication-common@npm:2.3.1" dependencies: "@azure/abort-controller": ^1.0.0 - "@azure/core-util": ^1.1.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 - checksum: 11c5ba072902693435dc2930e2fdfe2ff34836f4c2d6c87c6ac0566d48dc49157ebf49f4478cd3784dc0c4d57b502d3a12d74ea29f416725204a6e1aa937ef78 + checksum: de01172479bd0d498bccfd9906e9be7d57cf2b4c58159f140e573fd940c537fe0eba8d9fa8ee63469cb5c9f0c8d70bcac7afb330b514b4097a49df746367a255 languageName: node linkType: hard -"@azure/core-client@npm:^1.3.0, @azure/core-client@npm:^1.6.2, @azure/core-client@npm:^1.9.2": +"@azure/communication-email@npm:^1.0.0": + version: 1.0.0 + resolution: "@azure/communication-email@npm:1.0.0" + 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 + checksum: d939c729b303bc1bf7272b7f54850114f3cc90760cc0f796bbc8e01cd2834d0b28b33bd27b9ce590db691df94fbc23b10e39fa3c5d82e4b000a523616b5f12ef + languageName: node + linkType: hard + +"@azure/core-auth@npm:^1.3.0, @azure/core-auth@npm:^1.4.0, @azure/core-auth@npm:^1.5.0, @azure/core-auth@npm:^1.8.0": + version: 1.9.0 + resolution: "@azure/core-auth@npm:1.9.0" + dependencies: + "@azure/abort-controller": ^2.0.0 + "@azure/core-util": ^1.11.0 + tslib: ^2.6.2 + checksum: 4050112188db093c5e01caca0175708c767054c0cea4202430ff43ee42a16430235752ccc0002caea1796c8f01b4f6369c878762bf4c1b2f61af1b7ac13182fc + languageName: node + linkType: hard + +"@azure/core-client@npm:^1.3.0, @azure/core-client@npm:^1.3.2, @azure/core-client@npm:^1.6.2, @azure/core-client@npm:^1.9.2": version: 1.9.2 resolution: "@azure/core-client@npm:1.9.2" dependencies: @@ -1693,15 +1725,15 @@ __metadata: languageName: node linkType: hard -"@azure/core-lro@npm:^2.2.0": - version: 2.5.4 - resolution: "@azure/core-lro@npm:2.5.4" +"@azure/core-lro@npm:^2.2.0, @azure/core-lro@npm:^2.5.0": + version: 2.7.2 + resolution: "@azure/core-lro@npm:2.7.2" dependencies: - "@azure/abort-controller": ^1.0.0 + "@azure/abort-controller": ^2.0.0 "@azure/core-util": ^1.2.0 "@azure/logger": ^1.0.0 - tslib: ^2.2.0 - checksum: f048b99850e8497b557cf661c2f8a384ea1227de6ea0c0e1436653851c3932e28a05056a380f7c20ebc51e4c6d7bd15d7dfabc6ecca80eddb9dc3e3339df9519 + tslib: ^2.6.2 + checksum: dc2e5bbb004a86704bcf584422cd099b7a6beef57ce6501afacced65f4f3b5fbba57a2439f701687237867552a661fd6568f8b3c9e3eacdfd9039004772f85b0 languageName: node linkType: hard @@ -1714,19 +1746,19 @@ __metadata: languageName: node linkType: hard -"@azure/core-rest-pipeline@npm:^1.1.0, @azure/core-rest-pipeline@npm:^1.10.1, @azure/core-rest-pipeline@npm:^1.3.0, @azure/core-rest-pipeline@npm:^1.9.1": - version: 1.16.1 - resolution: "@azure/core-rest-pipeline@npm:1.16.1" +"@azure/core-rest-pipeline@npm:^1.1.0, @azure/core-rest-pipeline@npm:^1.10.1, @azure/core-rest-pipeline@npm:^1.3.0, @azure/core-rest-pipeline@npm:^1.3.2, @azure/core-rest-pipeline@npm:^1.8.0, @azure/core-rest-pipeline@npm:^1.9.1": + version: 1.19.0 + resolution: "@azure/core-rest-pipeline@npm:1.19.0" dependencies: "@azure/abort-controller": ^2.0.0 - "@azure/core-auth": ^1.4.0 + "@azure/core-auth": ^1.8.0 "@azure/core-tracing": ^1.0.1 - "@azure/core-util": ^1.9.0 + "@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 - checksum: ab2c5b715c4ebb9f6554f12d8e804833be29db0f23868adc092f4841aa8c7f140a6821dbcd9fa046bfb31e435085f171a87dab3f97d851cf9a62127d042fa75e + checksum: e8d7631152d7af27b949f3da792a22a92a633b5744604f90e02e56e0b888e031f15f5d3384d7ca65ae851492ad0e33972f000b99600b081ceb0eb66e294454c1 languageName: node linkType: hard @@ -1739,13 +1771,13 @@ __metadata: languageName: node linkType: hard -"@azure/core-util@npm:^1.1.0, @azure/core-util@npm:^1.2.0, @azure/core-util@npm:^1.3.0, @azure/core-util@npm:^1.6.1, @azure/core-util@npm:^1.9.0": - version: 1.9.0 - resolution: "@azure/core-util@npm:1.9.0" +"@azure/core-util@npm:^1.0.0, @azure/core-util@npm:^1.11.0, @azure/core-util@npm:^1.2.0, @azure/core-util@npm:^1.3.0, @azure/core-util@npm:^1.6.1": + version: 1.11.0 + resolution: "@azure/core-util@npm:1.11.0" dependencies: "@azure/abort-controller": ^2.0.0 tslib: ^2.6.2 - checksum: 9246dc5bd246e7b94883ea8130fce04e2f22abd1e94afcff7a3e92a4c2da5e9b382dbf89a606b21d70bc8b01c7c89c84e803ca9da27f78d87f72bdff91ec7380 + checksum: 91e3ec329d9eddaa66be5efb1785dad68dcb48dd779fca36e39db041673230510158ff5ca9ccef9f19c3e4d8e9af29f66a367cfc31a7b94d2541f80ef94ec797 languageName: node linkType: hard @@ -7042,6 +7074,8 @@ __metadata: dependencies: "@aws-sdk/client-ses": ^3.550.0 "@aws-sdk/types": ^3.347.0 + "@azure/communication-email": ^1.0.0 + "@azure/identity": ^4.0.0 "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-client": "workspace:^" @@ -34548,6 +34582,13 @@ __metadata: languageName: node linkType: hard +"jwt-decode@npm:^4.0.0": + version: 4.0.0 + resolution: "jwt-decode@npm:4.0.0" + checksum: 390e2edcb31a92e86c8cbdd1edeea4c0d62acd371f8a8f0a8878e499390c0ecf4c658b365c4e941e4ef37d0170e4ca650aaa49f99a45c0b9695a235b210154b0 + languageName: node + linkType: hard + "keygrip@npm:~1.1.0": version: 1.1.0 resolution: "keygrip@npm:1.1.0" @@ -45769,7 +45810,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^1.11.1, tslib@npm:^1.14.1, tslib@npm:^1.8.1, tslib@npm:^1.9.0": +"tslib@npm:^1.11.1, tslib@npm:^1.14.1, tslib@npm:^1.8.1, tslib@npm:^1.9.0, tslib@npm:^1.9.3": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: dbe628ef87f66691d5d2959b3e41b9ca0045c3ee3c7c7b906cc1e328b39f199bb1ad9e671c39025bd56122ac57dfbf7385a94843b1cc07c60a4db74795829acd From c717b36cba1f3c46c254fee98087943b94c7641f Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Tue, 11 Feb 2025 08:05:17 +0100 Subject: [PATCH 2/5] Update README Signed-off-by: Jonathan Mezach --- plugins/notifications-backend-module-email/README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/notifications-backend-module-email/README.md b/plugins/notifications-backend-module-email/README.md index 3ccee8f1e8..ed5bc22af3 100644 --- a/plugins/notifications-backend-module-email/README.md +++ b/plugins/notifications-backend-module-email/README.md @@ -2,7 +2,7 @@ Adds support for sending Backstage notifications as emails to users. -Supports sending emails using `SMTP`, `SES`, `sendmail`, or `stream` (for debugging purposes). +Supports sending emails using `SMTP`, `SES`, `azure`, `sendmail`, or `stream` (for debugging purposes). ## Customizing email content @@ -61,6 +61,12 @@ notifications: # accessKeyId: 'my-access-key # region: 'us-west-2' + # Azure Communication Service + # transportConfig: + # transport: 'azure' + # endpoint: 'https://my-endpoint.communication.azure.com' + # accessKey: 'my-access-key' Optional: if not provided, Managed Identity will be used + # sendmail # transportConfig: # transport: 'sendmail' From 6259aa91f298212605bc12a543258ce91455af91 Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Tue, 11 Feb 2025 08:07:16 +0100 Subject: [PATCH 3/5] Add changeset Signed-off-by: Jonathan Mezach --- .changeset/fuzzy-bugs-hunt.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fuzzy-bugs-hunt.md diff --git a/.changeset/fuzzy-bugs-hunt.md b/.changeset/fuzzy-bugs-hunt.md new file mode 100644 index 0000000000..404cee1732 --- /dev/null +++ b/.changeset/fuzzy-bugs-hunt.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend-module-email': minor +--- + +Add transport for Azure Communication Service From 23cec17d8fd23752c6ccbd1d994a12b7d39db22e Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Tue, 18 Feb 2025 09:05:25 +0100 Subject: [PATCH 4/5] Update plugins/notifications-backend-module-email/src/processor/transports/azure.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Jonathan Mezach --- .../src/processor/transports/azure.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/notifications-backend-module-email/src/processor/transports/azure.ts b/plugins/notifications-backend-module-email/src/processor/transports/azure.ts index d00d8c9574..9961e0cea8 100644 --- a/plugins/notifications-backend-module-email/src/processor/transports/azure.ts +++ b/plugins/notifications-backend-module-email/src/processor/transports/azure.ts @@ -72,7 +72,6 @@ export const createAzureTransport = async (config: Config) => { const response = await poller.pollUntilDone(); callback(null, response); } catch (error) { - console.error(error); callback(error, null); } }, From f8708c435a8b771232c3a82da839440e9bebc7d5 Mon Sep 17 00:00:00 2001 From: Jonathan Mezach Date: Tue, 18 Feb 2025 09:05:31 +0100 Subject: [PATCH 5/5] Update .changeset/fuzzy-bugs-hunt.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Jonathan Mezach --- .changeset/fuzzy-bugs-hunt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fuzzy-bugs-hunt.md b/.changeset/fuzzy-bugs-hunt.md index 404cee1732..768c0b60b0 100644 --- a/.changeset/fuzzy-bugs-hunt.md +++ b/.changeset/fuzzy-bugs-hunt.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-notifications-backend-module-email': minor +'@backstage/plugin-notifications-backend-module-email': patch --- Add transport for Azure Communication Service