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);