fix: Add optional config for ses mail options with SourceArn, FromArn, ConfigurationSetName

Signed-off-by: Stephanie Swaney <stephanie.swaney.ddk6@statefarm.com>
This commit is contained in:
Stephanie Swaney
2025-06-23 15:05:07 -05:00
parent 0d0c24ee33
commit f92c9fc224
5 changed files with 105 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-notifications-backend-module-email': patch
---
Add optional config for ses mail options with SourceArn, FromArn, ConfigurationSetName
@@ -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
+17
View File
@@ -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
@@ -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: '<p><a href="https://example.org/notifications">https://example.org/notifications</a></p>',
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',
}
});
});
});
@@ -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<string, string> = {};
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);