From f92c9fc2247f4a4ecd65b9e582e5d112a37e39b1 Mon Sep 17 00:00:00 2001 From: Stephanie Swaney Date: Mon, 23 Jun 2025 15:05:07 -0500 Subject: [PATCH 1/4] fix: Add optional config for ses mail options with SourceArn, FromArn, ConfigurationSetName Signed-off-by: Stephanie Swaney --- .changeset/tender-wolves-strive.md | 5 ++ .../README.md | 5 ++ .../config.d.ts | 17 ++++++ .../NotificationsEmailProcessor.test.ts | 58 +++++++++++++++++++ .../processor/NotificationsEmailProcessor.ts | 20 +++++++ 5 files changed, 105 insertions(+) create mode 100644 .changeset/tender-wolves-strive.md diff --git a/.changeset/tender-wolves-strive.md b/.changeset/tender-wolves-strive.md new file mode 100644 index 0000000000..abc4c01c65 --- /dev/null +++ b/.changeset/tender-wolves-strive.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend-module-email': patch +--- + +Add optional config for ses mail options with SourceArn, FromArn, ConfigurationSetName diff --git a/plugins/notifications-backend-module-email/README.md b/plugins/notifications-backend-module-email/README.md index ed5bc22af3..229f30a465 100644 --- a/plugins/notifications-backend-module-email/README.md +++ b/plugins/notifications-backend-module-email/README.md @@ -79,6 +79,11 @@ notifications: # Who to send email for broadcast notifications broadcastConfig: receiver: 'users' + # Optional SES config + # sesConfig: + # SourceArn: 'arn:aws:ses:us-west-2:123456789012:identity/example.com' + # FromArn: 'arn:aws:ses:us-west-2:123456789012:identity/example.com' + # ConfigurationSetName: 'custom-config' # How many emails to send concurrently, defaults to 2 concurrencyLimit: 10 # How much to throttle between emails, defaults to 100ms diff --git a/plugins/notifications-backend-module-email/config.d.ts b/plugins/notifications-backend-module-email/config.d.ts index 97a6495246..d4c2ba06a3 100644 --- a/plugins/notifications-backend-module-email/config.d.ts +++ b/plugins/notifications-backend-module-email/config.d.ts @@ -132,6 +132,23 @@ export interface Config { */ receiverEmails?: string[]; }; + /** + * Optional SES config for mail options. Allows for delegated sender + */ + sesConfig?: { + /** + * ARN of the identity to use as the source of the email + */ + SourceArn?: string; + /** + * ARN of the identity to use for the "From"/sender address of the email + */ + FromArn?: string; + /** + * Name of the configuration set to use when sending email via ses + */ + ConfigurationSetName?: string; + }; cache?: { /** * Email cache TTL, defaults to 1 hour diff --git a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.test.ts b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.test.ts index 1408abcf9c..7b609d4543 100644 --- a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.test.ts +++ b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.test.ts @@ -442,4 +442,62 @@ describe('NotificationsEmailProcessor', () => { to: 'mock@backstage.io', }); }); + + it('should send email with ses config', async () => { + const SES_SENDMAIL_CONFIG = { + app: { + baseUrl: 'https://example.org', + }, + notifications: { + processors: { + email: { + transportConfig: { + transport: 'ses', + region: 'us-west-2', + }, + sender: 'backstage@backstage.io', + replyTo: 'no-reply@backstage.io', + sesConfig: { + SourceArn: 'arn:aws:ses:us-west-2:123456789012:identity/example.com', + FromArn: 'arn:aws:ses:us-west-2:123456789012:identity/example.com', + } + }, + }, + }, + }; + (createTransport as jest.Mock).mockReturnValue(mockTransport); + const processor = new NotificationsEmailProcessor( + logger, + mockServices.rootConfig({ data: SES_SENDMAIL_CONFIG }), + catalogServiceMock({ entities: [ DEFAULT_ENTITIES_RESPONSE.items[ 0 ] ] }), + auth, + ); + + await processor.postProcess( + { + origin: 'plugin', + id: '1234', + user: 'user:default/mock', + created: new Date(), + payload: { title: 'notification' }, + }, + { + recipients: { type: 'entity', entityRef: 'user:default/mock' }, + payload: { title: 'notification' }, + }, + ); + + expect(sendmailMock).toHaveBeenCalledWith({ + from: 'backstage@backstage.io', + html: '

https://example.org/notifications

', + replyTo: 'no-reply@backstage.io', + subject: 'notification', + text: 'https://example.org/notifications', + to: 'mock@backstage.io', + ses: { + SourceArn: 'arn:aws:ses:us-west-2:123456789012:identity/example.com', + FromArn: 'arn:aws:ses:us-west-2:123456789012:identity/example.com', + } + }); + }); }); diff --git a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts index 3496d431e3..c081dbe40e 100644 --- a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts +++ b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts @@ -52,6 +52,7 @@ export class NotificationsEmailProcessor implements NotificationProcessor { private readonly transportConfig: Config; private readonly sender: string; private readonly replyTo?: string; + private readonly sesConfig?: Config; private readonly cacheTtl: number; private readonly concurrencyLimit: number; private readonly throttleInterval: number; @@ -76,6 +77,7 @@ export class NotificationsEmailProcessor implements NotificationProcessor { emailProcessorConfig.getOptionalConfig('broadcastConfig'); this.sender = emailProcessorConfig.getString('sender'); this.replyTo = emailProcessorConfig.getOptionalString('replyTo'); + this.sesConfig = emailProcessorConfig.getOptionalConfig('sesConfig'); this.concurrencyLimit = emailProcessorConfig.getOptionalNumber('concurrencyLimit') ?? 2; this.throttleInterval = emailProcessorConfig.has('throttleInterval') @@ -292,6 +294,22 @@ export class NotificationsEmailProcessor implements NotificationProcessor { return contentParts.join('\n\n'); } + private async getSesOptions() { + if (!this.sesConfig) { + return undefined; + } + const ses: Record = {}; + const sourceArn = this.sesConfig.getOptionalString('SourceArn'); + const fromArn = this.sesConfig.getOptionalString('FromArn'); + const configurationSetName = this.sesConfig.getOptionalString('ConfigurationSetName'); + + if (sourceArn) ses.SourceArn = sourceArn; + if (fromArn) ses.FromArn = fromArn; + if (configurationSetName) ses.ConfigurationSetName = configurationSetName; + + return Object.keys(ses).length > 0 ? ses : undefined; + } + private async sendPlainEmail(notification: Notification, emails: string[]) { const mailOptions = { from: this.sender, @@ -299,6 +317,7 @@ export class NotificationsEmailProcessor implements NotificationProcessor { html: this.getHtmlContent(notification), text: this.getTextContent(notification), replyTo: this.replyTo, + ses: await this.getSesOptions() }; await this.sendMails(mailOptions, emails); @@ -316,6 +335,7 @@ export class NotificationsEmailProcessor implements NotificationProcessor { html: await this.templateRenderer?.getHtml?.(notification), text: await this.templateRenderer?.getText?.(notification), replyTo: this.replyTo, + ses: await this.getSesOptions(), }; await this.sendMails(mailOptions, emails); From 94a30fed5aec5dc61d66c8c681dcb3be30621523 Mon Sep 17 00:00:00 2001 From: Stephanie Swaney Date: Mon, 23 Jun 2025 16:59:05 -0500 Subject: [PATCH 2/4] chore: prettier fixes Signed-off-by: Stephanie Swaney --- .../processor/NotificationsEmailProcessor.test.ts | 12 +++++++----- .../src/processor/NotificationsEmailProcessor.ts | 6 ++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.test.ts b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.test.ts index 7b609d4543..0b758a50f5 100644 --- a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.test.ts +++ b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.test.ts @@ -458,9 +458,11 @@ describe('NotificationsEmailProcessor', () => { sender: 'backstage@backstage.io', replyTo: 'no-reply@backstage.io', sesConfig: { - SourceArn: 'arn:aws:ses:us-west-2:123456789012:identity/example.com', - FromArn: 'arn:aws:ses:us-west-2:123456789012:identity/example.com', - } + SourceArn: + 'arn:aws:ses:us-west-2:123456789012:identity/example.com', + FromArn: + 'arn:aws:ses:us-west-2:123456789012:identity/example.com', + }, }, }, }, @@ -469,7 +471,7 @@ describe('NotificationsEmailProcessor', () => { const processor = new NotificationsEmailProcessor( logger, mockServices.rootConfig({ data: SES_SENDMAIL_CONFIG }), - catalogServiceMock({ entities: [ DEFAULT_ENTITIES_RESPONSE.items[ 0 ] ] }), + catalogServiceMock({ entities: [DEFAULT_ENTITIES_RESPONSE.items[0]] }), auth, ); @@ -497,7 +499,7 @@ describe('NotificationsEmailProcessor', () => { ses: { SourceArn: 'arn:aws:ses:us-west-2:123456789012:identity/example.com', FromArn: 'arn:aws:ses:us-west-2:123456789012:identity/example.com', - } + }, }); }); }); diff --git a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts index c081dbe40e..461b8ed2f1 100644 --- a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts +++ b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts @@ -301,7 +301,9 @@ export class NotificationsEmailProcessor implements NotificationProcessor { const ses: Record = {}; const sourceArn = this.sesConfig.getOptionalString('SourceArn'); const fromArn = this.sesConfig.getOptionalString('FromArn'); - const configurationSetName = this.sesConfig.getOptionalString('ConfigurationSetName'); + const configurationSetName = this.sesConfig.getOptionalString( + 'ConfigurationSetName', + ); if (sourceArn) ses.SourceArn = sourceArn; if (fromArn) ses.FromArn = fromArn; @@ -317,7 +319,7 @@ export class NotificationsEmailProcessor implements NotificationProcessor { html: this.getHtmlContent(notification), text: this.getTextContent(notification), replyTo: this.replyTo, - ses: await this.getSesOptions() + ses: await this.getSesOptions(), }; await this.sendMails(mailOptions, emails); From 990edb5274800ec4ad6f6587a1f59687aa30e726 Mon Sep 17 00:00:00 2001 From: Stephanie Swaney Date: Tue, 24 Jun 2025 09:02:50 -0500 Subject: [PATCH 3/4] fix: use convention for config variables inside sesConfig Signed-off-by: Stephanie Swaney --- plugins/notifications-backend-module-email/README.md | 6 +++--- plugins/notifications-backend-module-email/config.d.ts | 6 +++--- .../src/processor/NotificationsEmailProcessor.test.ts | 4 ++-- .../src/processor/NotificationsEmailProcessor.ts | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/plugins/notifications-backend-module-email/README.md b/plugins/notifications-backend-module-email/README.md index 229f30a465..c1ead48876 100644 --- a/plugins/notifications-backend-module-email/README.md +++ b/plugins/notifications-backend-module-email/README.md @@ -81,9 +81,9 @@ notifications: receiver: 'users' # Optional SES config # sesConfig: - # SourceArn: 'arn:aws:ses:us-west-2:123456789012:identity/example.com' - # FromArn: 'arn:aws:ses:us-west-2:123456789012:identity/example.com' - # ConfigurationSetName: 'custom-config' + # sourceArn: 'arn:aws:ses:us-west-2:123456789012:identity/example.com' + # fromArn: 'arn:aws:ses:us-west-2:123456789012:identity/example.com' + # configurationSetName: 'custom-config' # How many emails to send concurrently, defaults to 2 concurrencyLimit: 10 # How much to throttle between emails, defaults to 100ms diff --git a/plugins/notifications-backend-module-email/config.d.ts b/plugins/notifications-backend-module-email/config.d.ts index d4c2ba06a3..7116826c5d 100644 --- a/plugins/notifications-backend-module-email/config.d.ts +++ b/plugins/notifications-backend-module-email/config.d.ts @@ -139,15 +139,15 @@ export interface Config { /** * ARN of the identity to use as the source of the email */ - SourceArn?: string; + sourceArn?: string; /** * ARN of the identity to use for the "From"/sender address of the email */ - FromArn?: string; + fromArn?: string; /** * Name of the configuration set to use when sending email via ses */ - ConfigurationSetName?: string; + configurationSetName?: string; }; cache?: { /** diff --git a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.test.ts b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.test.ts index 0b758a50f5..ebe1b90f35 100644 --- a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.test.ts +++ b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.test.ts @@ -458,9 +458,9 @@ describe('NotificationsEmailProcessor', () => { sender: 'backstage@backstage.io', replyTo: 'no-reply@backstage.io', sesConfig: { - SourceArn: + sourceArn: 'arn:aws:ses:us-west-2:123456789012:identity/example.com', - FromArn: + fromArn: 'arn:aws:ses:us-west-2:123456789012:identity/example.com', }, }, diff --git a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts index 461b8ed2f1..39a0c5da02 100644 --- a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts +++ b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts @@ -299,10 +299,10 @@ export class NotificationsEmailProcessor implements NotificationProcessor { return undefined; } const ses: Record = {}; - const sourceArn = this.sesConfig.getOptionalString('SourceArn'); - const fromArn = this.sesConfig.getOptionalString('FromArn'); + const sourceArn = this.sesConfig.getOptionalString('sourceArn'); + const fromArn = this.sesConfig.getOptionalString('fromArn'); const configurationSetName = this.sesConfig.getOptionalString( - 'ConfigurationSetName', + 'configurationSetName', ); if (sourceArn) ses.SourceArn = sourceArn; From 3520a64a4613393ebc9024ad534c68a15acbccc9 Mon Sep 17 00:00:00 2001 From: sswane <36286083+sswane@users.noreply.github.com> Date: Tue, 24 Jun 2025 09:50:35 -0500 Subject: [PATCH 4/4] Update .changeset/tender-wolves-strive.md to match updated options Co-authored-by: Ben Lambert Signed-off-by: sswane <36286083+sswane@users.noreply.github.com> --- .changeset/tender-wolves-strive.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tender-wolves-strive.md b/.changeset/tender-wolves-strive.md index abc4c01c65..63de61a2f9 100644 --- a/.changeset/tender-wolves-strive.md +++ b/.changeset/tender-wolves-strive.md @@ -2,4 +2,4 @@ '@backstage/plugin-notifications-backend-module-email': patch --- -Add optional config for ses mail options with SourceArn, FromArn, ConfigurationSetName +Add optional config for `ses` mail options with `sourceArn`, `fromArn`, `configurationSetName`