feat: add email notification processor

Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
Heikki Hellgren
2024-04-17 13:51:53 +03:00
parent 41d5c56e18
commit dbf269686a
18 changed files with 1128 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-notifications-backend-module-email': patch
---
Allow sending notifications by email with the new notifications module
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
@@ -0,0 +1,26 @@
# @backstage/plugin-notifications-backend-module-email
Adds support for sending Backstage notifications as emails to users.
Supports sending emails using SMTP, SES, or sendmail.
## Example configuration:
```yaml
notifications:
processors:
email:
transportConfig:
transport: 'smtp'
hostname: 'my-smtp-server'
port: 587
secure: false
username: 'my-username'
password: 'my-password'
sender: 'sender@mycompany.com'
replyTo: 'no-reply@mycompany.com'
broadcastConfig:
receiver: 'user'
```
See `config.d.ts` for more options for configuration.
@@ -0,0 +1,11 @@
## API Report File for "@backstage/plugin-notifications-backend-module-email"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
// @public (undocumented)
const notificationsModuleEmail: () => BackendFeature;
export default notificationsModuleEmail;
```
@@ -0,0 +1,12 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-notifications-backend-module-email
title: '@backstage/plugin-notifications-backend-module-email'
description: >-
The email-notification-processor backend module for the notifications
plugin.
spec:
lifecycle: experimental
type: backstage-backend-plugin-module
owner: maintainers
+120
View File
@@ -0,0 +1,120 @@
/*
* 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.
*/
export interface Config {
/**
* Configuration options for notifications-backend-module-email */
notifications: {
processors: {
email: {
/**
* Transport to use for sending emails */
transportConfig:
| {
transport: 'smtp';
/**
* SMTP server hostname
*/
hostname: string;
/**
* SMTP server port
*/
port: number;
/**
* Use secure connection for SMTP, defaults to false
*/
secure?: boolean;
/**
* Require TLS for SMTP connection, defaults to false
*/
requireTls?: boolean;
/**
* SMTP username
*/
username?: string;
/**
* SMTP password
* @visibility secret
*/
password?: string;
}
| {
transport: 'ses';
/**
* SES ApiVersion to use, defaults to 2010-12-01
*/
apiVersion?: string;
/**
* SES region to use
*/
region?: string;
/**
* AWS access key id
*/
accessKeyId?: string;
/**
* AWS secret access key
* @visibility secret
*/
secretAccessKey?: string;
}
| {
transport: 'sendmail';
/**
* Sendmail binary path, defaults to /usr/sbin/sendmail
*/
path?: string;
/**
* Newline style, defaults to 'unix'
*/
newline?: 'unix' | 'windows';
};
/**
* Sender email address
*/
sender: string;
/**
* Email format, defaults to HTML
*/
format?: 'html' | 'text';
/**
* Optional reply-to address
*/
replyTo?: string;
/**
* Configuration for broadcast notifications
*/
broadcastConfig?: {
/**
* Receiver of the broadcast notifications:
* none - skips sending
* users - sends to all users in backstage, might have performance impact
* config - sends to the emails specified in the config
*/
receiver: 'none' | 'users' | 'config';
/**
* Broadcast notification receivers when receiver is set to config
*/
receiverEmails?: string[];
};
/**
* Email cache TTL, defaults to 1 hour
*/
cacheTtl?: number;
};
};
};
}
@@ -0,0 +1,54 @@
{
"name": "@backstage/plugin-notifications-backend-module-email",
"version": "0.0.0",
"description": "The email backend module for the notifications plugin.",
"backstage": {
"role": "backend-plugin-module"
},
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/notifications-backend-module-email"
},
"license": "Apache-2.0",
"main": "src/index.ts",
"types": "src/index.ts",
"files": [
"dist",
"config.d.ts"
],
"scripts": {
"build": "backstage-cli package build",
"clean": "backstage-cli package clean",
"lint": "backstage-cli package lint",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack",
"start": "backstage-cli package start",
"test": "backstage-cli package test"
},
"dependencies": {
"@aws-sdk/client-ses": "^3.556.0",
"@aws-sdk/credential-provider-node": "^3.350.0",
"@backstage/backend-common": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/catalog-client": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/plugin-notifications-common": "workspace:^",
"@backstage/plugin-notifications-node": "workspace:^",
"@backstage/types": "workspace:^",
"lodash": "^4.17.21",
"nodemailer": "^6.9.13"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@types/nodemailer": "^6.4.14"
},
"configSchema": "config.d.ts"
}
@@ -0,0 +1,23 @@
/*
* 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.
*/
/**
* The email backend module for the notifications plugin.
*
* @packageDocumentation
*/
export { notificationsModuleEmail as default } from './module';
@@ -0,0 +1,57 @@
/*
* 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 {
coreServices,
createBackendModule,
} from '@backstage/backend-plugin-api';
import { CatalogClient } from '@backstage/catalog-client';
import { notificationsProcessingExtensionPoint } from '@backstage/plugin-notifications-node';
import { NotificationsEmailProcessor } from './processor';
/**
* @public
*/
export const notificationsModuleEmail = createBackendModule({
pluginId: 'notifications',
moduleId: 'email',
register(reg) {
reg.registerInit({
deps: {
config: coreServices.rootConfig,
notifications: notificationsProcessingExtensionPoint,
discovery: coreServices.discovery,
logger: coreServices.logger,
auth: coreServices.auth,
cache: coreServices.cache,
},
async init({ config, notifications, discovery, logger, auth, cache }) {
const catalogClient = new CatalogClient({
discoveryApi: discovery,
});
notifications.addProcessor(
new NotificationsEmailProcessor(
logger,
config,
catalogClient,
auth,
cache,
),
);
},
});
},
});
@@ -0,0 +1,295 @@
/*
* 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 { mockServices } from '@backstage/backend-test-utils';
import { NotificationsEmailProcessor } from './NotificationsEmailProcessor';
import { ConfigReader } from '@backstage/config';
import { JsonArray } from '@backstage/types';
import { CatalogClient } from '@backstage/catalog-client';
import { createTransport } from 'nodemailer';
const sendmailMock = jest.fn();
const mockTransport = {
sendMail: sendmailMock,
};
jest.mock('nodemailer', () => ({
...jest.requireActual('nodemailer'),
createTransport: jest.fn(),
}));
describe('NotificationsEmailProcessor', () => {
const logger = mockServices.logger.mock();
const auth = mockServices.auth();
const getEntityRefMock = jest.fn();
const getEntitiesMock = jest.fn();
const mockCatalogClient: Partial<CatalogClient> = {
getEntityByRef: getEntityRefMock,
getEntities: getEntitiesMock,
};
beforeEach(() => {
jest.resetAllMocks();
});
it('should create smtp transport', () => {
const processor = new NotificationsEmailProcessor(
logger,
new ConfigReader({
notifications: {
email: {
transport: {
transport: 'smtp',
hostname: 'localhost',
port: 465,
secure: true,
requireTls: false,
},
sender: 'backstage@backstage.io',
},
},
}),
mockCatalogClient as unknown as CatalogClient,
auth,
);
expect(processor).toBeInstanceOf(NotificationsEmailProcessor);
expect(createTransport as jest.Mock).toHaveBeenCalledWith({
host: 'localhost',
port: 465,
requireTLS: false,
secure: true,
});
});
it('should create ses transport', () => {
const processor = new NotificationsEmailProcessor(
logger,
new ConfigReader({
notifications: {
email: {
transport: {
transport: 'ses',
region: 'us-west-2',
},
sender: 'backstage@backstage.io',
},
},
}),
mockCatalogClient as unknown as CatalogClient,
auth,
);
expect(processor).toBeInstanceOf(NotificationsEmailProcessor);
expect(createTransport as jest.Mock).toHaveBeenCalledWith({
SES: expect.anything(),
});
});
it('should create sendmail transport', () => {
const processor = new NotificationsEmailProcessor(
logger,
new ConfigReader({
notifications: {
email: {
transport: {
transport: 'sendmail',
path: '/usr/local/bin/sendmail',
},
sender: 'backstage@backstage.io',
},
},
}),
mockCatalogClient as unknown as CatalogClient,
auth,
);
expect(processor).toBeInstanceOf(NotificationsEmailProcessor);
expect(createTransport as jest.Mock).toHaveBeenCalledWith({
sendmail: true,
path: '/usr/local/bin/sendmail',
newline: 'unix',
});
});
it('should send user email', async () => {
(createTransport as jest.Mock).mockReturnValue(mockTransport);
getEntityRefMock.mockResolvedValue({
kind: 'User',
spec: {
profile: {
email: 'mock@backstage.io',
},
},
});
const processor = new NotificationsEmailProcessor(
logger,
new ConfigReader({
notifications: {
email: {
transport: {
transport: 'sendmail',
path: '/usr/local/bin/sendmail',
},
sender: 'backstage@backstage.io',
},
},
}),
mockCatalogClient as unknown as CatalogClient,
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></p>',
replyTo: undefined,
subject: 'notification',
text: undefined,
to: 'mock@backstage.io',
});
});
it('should send email to all', async () => {
(createTransport as jest.Mock).mockReturnValue(mockTransport);
getEntitiesMock.mockResolvedValue({
items: [
{
kind: 'User',
spec: {
profile: {
email: 'mock@backstage.io',
},
},
},
],
});
const processor = new NotificationsEmailProcessor(
logger,
new ConfigReader({
notifications: {
email: {
transport: {
transport: 'sendmail',
path: '/usr/local/bin/sendmail',
},
sender: 'backstage@backstage.io',
broadcastConfig: {
receiver: 'users',
},
},
},
}),
mockCatalogClient as unknown as CatalogClient,
auth,
);
await processor.postProcess(
{
origin: 'plugin',
id: '1234',
user: null,
created: new Date(),
payload: { title: 'notification' },
},
{
recipients: { type: 'broadcast' },
payload: { title: 'notification' },
},
);
expect(sendmailMock).toHaveBeenCalledWith({
from: 'backstage@backstage.io',
html: '<p></p>',
replyTo: undefined,
subject: 'notification',
text: undefined,
to: 'mock@backstage.io',
});
});
it('should send email to configured addresses', async () => {
(createTransport as jest.Mock).mockReturnValue(mockTransport);
getEntitiesMock.mockResolvedValue({
items: [
{
kind: 'User',
spec: {
profile: {
email: 'mock@backstage.io',
},
},
},
],
});
const processor = new NotificationsEmailProcessor(
logger,
new ConfigReader({
notifications: {
email: {
transport: {
transport: 'sendmail',
path: '/usr/local/bin/sendmail',
},
sender: 'backstage@backstage.io',
broadcastConfig: {
receiver: 'config',
receiverEmails: ['broadcast@backstage.io'] as JsonArray,
},
},
},
}),
mockCatalogClient as unknown as CatalogClient,
auth,
);
await processor.postProcess(
{
origin: 'plugin',
id: '1234',
user: null,
created: new Date(),
payload: { title: 'notification' },
},
{
recipients: { type: 'broadcast' },
payload: { title: 'notification' },
},
);
expect(sendmailMock).toHaveBeenCalledWith({
from: 'backstage@backstage.io',
html: '<p></p>',
replyTo: undefined,
subject: 'notification',
text: undefined,
to: 'broadcast@backstage.io',
});
});
});
@@ -0,0 +1,229 @@
/*
* 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 {
NotificationProcessor,
NotificationSendOptions,
} from '@backstage/plugin-notifications-node';
import {
AuthService,
CacheService,
LoggerService,
} from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
import { JsonArray } from '@backstage/types';
import {
CATALOG_FILTER_EXISTS,
CatalogClient,
} from '@backstage/catalog-client';
import { Notification } from '@backstage/plugin-notifications-common';
import { createSendmailTransport, createSmtpTransport } from './transports';
import { createSesTransport } from './transports/ses';
import { UserEntity } from '@backstage/catalog-model';
import { compact } from 'lodash';
export class NotificationsEmailProcessor implements NotificationProcessor {
private readonly transportter: any;
private readonly broadcastConfig?: Config;
private readonly sender: string;
private readonly format: string;
private readonly replyTo?: string;
private readonly cacheTtl: number;
constructor(
private readonly logger: LoggerService,
config: Config,
private readonly catalog: CatalogClient,
private readonly auth: AuthService,
private readonly cache?: CacheService,
) {
const transportConfig = config.getConfig('notifications.email.transport');
const transport = transportConfig.getString('transport');
if (transport === 'smtp') {
this.transportter = createSmtpTransport({
transport: 'smtp',
hostname: transportConfig.getString('hostname'),
port: transportConfig.getNumber('port'),
secure: transportConfig.getOptionalBoolean('secure'),
requireTls: transportConfig.getOptionalBoolean('requireTls'),
username: transportConfig.getOptionalString('username'),
password: transportConfig.getOptionalString('password'),
});
} else if (transport === 'ses') {
this.transportter = createSesTransport({
transport: 'ses',
apiVersion: transportConfig.getOptionalString('apiVersion'),
region: transportConfig.getOptionalString('region'),
accessKeyId: transportConfig.getOptionalString('accessKeyId'),
secretAccessKey: transportConfig.getOptionalString('secretAccessKey'),
});
} else if (transport === 'sendmail') {
this.transportter = createSendmailTransport({
transport: 'sendmail',
path: transportConfig.getOptionalString('path'),
newline: transportConfig.getOptionalString('newline'),
});
} else {
throw new Error(`Unsupported transport: ${transport}`);
}
this.broadcastConfig = config.getOptionalConfig(
'notifications.email.broadcastConfig',
);
this.sender = config.getString('notifications.email.sender');
this.format =
config.getOptionalString('notifications.email.format') ?? 'html';
this.replyTo = config.getOptionalString('notifications.email.replyTo');
this.cacheTtl =
config.getOptionalNumber('notifications.email.cacheTtl') ?? 3_600_000;
}
getName(): string {
return 'Email';
}
private async getBroadcastEmails(): Promise<string[]> {
if (!this.broadcastConfig) {
return [];
}
const receiver = this.broadcastConfig.getString('receiver');
if (receiver === 'none') {
return [];
}
if (receiver === 'config') {
return (
this.broadcastConfig.getOptionalStringArray('receiverEmails') ?? []
);
}
if (receiver === 'users') {
const cached = await this.cache?.get<JsonArray>('user-emails:all');
if (cached) {
return cached as string[];
}
const credentials = await this.auth.getOwnServiceCredentials();
const { token } = await this.auth.getPluginRequestToken({
onBehalfOf: credentials,
targetPluginId: 'catalog',
});
const entities = await this.catalog.getEntities(
{
filter: [
{ kind: 'user', 'spec.profile.email': CATALOG_FILTER_EXISTS },
],
fields: ['spec.profile.email'],
},
{ token },
);
const ret = compact([
...new Set(
entities.items.map(entity => {
return (entity as UserEntity)?.spec.profile?.email;
}),
),
]);
await this.cache?.set('user-emails:all', ret as JsonArray, {
ttl: this.cacheTtl,
});
return ret;
}
throw new Error(`Unsupported broadcast receiver: ${receiver}`);
}
private async getUserEmail(entityRef: string): Promise<string[]> {
const cached = await this.cache?.get<string>(`user-emails:${entityRef}`);
if (cached) {
return [cached];
}
const credentials = await this.auth.getOwnServiceCredentials();
const { token } = await this.auth.getPluginRequestToken({
onBehalfOf: credentials,
targetPluginId: 'catalog',
});
const entity = await this.catalog.getEntityByRef(entityRef, { token });
if (!entity) {
return [];
}
const userEntity = entity as UserEntity;
if (!userEntity.spec.profile?.email) {
return [];
}
await this.cache?.set(
`user-emails:${entityRef}`,
userEntity.spec.profile.email,
{ ttl: this.cacheTtl },
);
return [userEntity.spec.profile.email];
}
private async getRecipientEmails(
notification: Notification,
options: NotificationSendOptions,
) {
if (options.recipients.type === 'broadcast' || notification.user === null) {
return await this.getBroadcastEmails();
}
return await this.getUserEmail(notification.user);
}
async postProcess(
notification: Notification,
options: NotificationSendOptions,
): Promise<void> {
let emails: string[] = [];
try {
emails = await this.getRecipientEmails(notification, options);
} catch (e) {
this.logger.error(`Failed to resolve recipient emails: ${e}`);
return;
}
// TODO: add template support for content either HTML or text
const contentParts: string[] = [];
if (notification.payload.description) {
contentParts.push(`${notification.payload.description}`);
}
if (notification.payload.link) {
contentParts.push(`${notification.payload.link}`);
}
const mailOptions = {
from: this.sender,
subject: notification.payload.title,
html:
this.format === 'html'
? `<p>${contentParts.join('<br/>')}</p>`
: undefined,
text: this.format === 'text' ? contentParts.join('\n\n') : undefined,
replyTo: this.replyTo,
};
for (const email of emails) {
try {
await this.transportter.sendMail({ ...mailOptions, to: email });
} catch (e) {
this.logger.error(`Failed to send email to ${email}: ${e}`);
}
}
}
}
@@ -0,0 +1,16 @@
/*
* 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.
*/
export { NotificationsEmailProcessor } from './NotificationsEmailProcessor';
@@ -0,0 +1,18 @@
/*
* 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.
*/
export { createSmtpTransport } from './smtp';
export { createSesTransport } from './ses';
export { createSendmailTransport } from './sendmail';
@@ -0,0 +1,25 @@
/*
* 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 { SendmailTransportConfig } from '../../types';
export const createSendmailTransport = (config: SendmailTransportConfig) => {
return createTransport({
sendmail: true,
newline: config.newline ?? 'unix',
path: config.path ?? '/usr/sbin/sendmail',
});
};
@@ -0,0 +1,39 @@
/*
* 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 { SesTransportConfig } from '../../types';
import { createTransport } from 'nodemailer';
import * as aws from '@aws-sdk/client-ses';
import { defaultProvider } from '@aws-sdk/credential-provider-node';
export const createSesTransport = (config: SesTransportConfig) => {
const ses = new aws.SES([
{
region: config.region,
apiVersion: config.apiVersion ?? '2010-12-01',
defaultProvider,
credentials:
config.accessKeyId && config.secretAccessKey
? {
accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey,
}
: undefined,
},
]);
return createTransport({
SES: { ses, aws },
});
};
@@ -0,0 +1,30 @@
/*
* 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 { SmtpTransportConfig } from '../../types';
import { createTransport } from 'nodemailer';
export const createSmtpTransport = (config: SmtpTransportConfig) => {
return createTransport({
host: config.hostname,
port: config.port,
secure: config.secure ?? false,
requireTLS: config.requireTls ?? false,
auth:
config.username && config.password
? { user: config.username, pass: config.password }
: undefined,
});
};
@@ -0,0 +1,80 @@
/*
* 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.
*/
export interface TransportConfig {
transport: 'smtp' | 'ses' | 'sendmail';
}
export interface SmtpTransportConfig extends TransportConfig {
transport: 'smtp';
/**
* SMTP server hostname
*/
hostname: string;
/**
* SMTP server port
*/
port: number;
/**
* Use secure connection for SMTP, defaults to false
*/
secure?: boolean;
/**
* Require TLS for SMTP connection, defaults to false
*/
requireTls?: boolean;
/**
* SMTP username
*/
username?: string;
/**
* SMTP password
* @visibility secret
*/
password?: string;
}
export interface SesTransportConfig extends TransportConfig {
transport: 'ses';
/**
* SES ApiVersion to use, defaults to 2010-12-01
*/
apiVersion?: string;
/**
* SES region to use
*/
region?: string;
/**
* AWS access key id
*/
accessKeyId?: string;
/**
* AWS secret access key
* @visibility secret
*/
secretAccessKey?: string;
}
export interface SendmailTransportConfig extends TransportConfig {
transport: 'sendmail';
/**
* Sendmail binary path, defaults to /usr/sbin/sendmail
*/
path?: string;
/**
* Newline style, defaults to 'unix'
*/
newline?: string;
}
+87
View File
@@ -599,6 +599,55 @@ __metadata:
languageName: node
linkType: hard
"@aws-sdk/client-ses@npm:^3.556.0":
version: 3.556.0
resolution: "@aws-sdk/client-ses@npm:3.556.0"
dependencies:
"@aws-crypto/sha256-browser": 3.0.0
"@aws-crypto/sha256-js": 3.0.0
"@aws-sdk/client-sts": 3.556.0
"@aws-sdk/core": 3.556.0
"@aws-sdk/credential-provider-node": 3.556.0
"@aws-sdk/middleware-host-header": 3.535.0
"@aws-sdk/middleware-logger": 3.535.0
"@aws-sdk/middleware-recursion-detection": 3.535.0
"@aws-sdk/middleware-user-agent": 3.540.0
"@aws-sdk/region-config-resolver": 3.535.0
"@aws-sdk/types": 3.535.0
"@aws-sdk/util-endpoints": 3.540.0
"@aws-sdk/util-user-agent-browser": 3.535.0
"@aws-sdk/util-user-agent-node": 3.535.0
"@smithy/config-resolver": ^2.2.0
"@smithy/core": ^1.4.2
"@smithy/fetch-http-handler": ^2.5.0
"@smithy/hash-node": ^2.2.0
"@smithy/invalid-dependency": ^2.2.0
"@smithy/middleware-content-length": ^2.2.0
"@smithy/middleware-endpoint": ^2.5.1
"@smithy/middleware-retry": ^2.3.1
"@smithy/middleware-serde": ^2.3.0
"@smithy/middleware-stack": ^2.2.0
"@smithy/node-config-provider": ^2.3.0
"@smithy/node-http-handler": ^2.5.0
"@smithy/protocol-http": ^3.3.0
"@smithy/smithy-client": ^2.5.1
"@smithy/types": ^2.12.0
"@smithy/url-parser": ^2.2.0
"@smithy/util-base64": ^2.3.0
"@smithy/util-body-length-browser": ^2.2.0
"@smithy/util-body-length-node": ^2.3.0
"@smithy/util-defaults-mode-browser": ^2.2.1
"@smithy/util-defaults-mode-node": ^2.3.1
"@smithy/util-endpoints": ^1.2.0
"@smithy/util-middleware": ^2.2.0
"@smithy/util-retry": ^2.2.0
"@smithy/util-utf8": ^2.3.0
"@smithy/util-waiter": ^2.2.0
tslib: ^2.6.2
checksum: 913108e79061185faae51711b121df99da624f988f530af23c3e4270299be60771be83f507dc8ed4af4051765077ea038edab19e4d0d32e159c4e67faf5fc9f8
languageName: node
linkType: hard
"@aws-sdk/client-sqs@npm:^3.350.0":
version: 3.556.0
resolution: "@aws-sdk/client-sqs@npm:3.556.0"
@@ -5982,6 +6031,28 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-notifications-backend-module-email@workspace:plugins/notifications-backend-module-email":
version: 0.0.0-use.local
resolution: "@backstage/plugin-notifications-backend-module-email@workspace:plugins/notifications-backend-module-email"
dependencies:
"@aws-sdk/client-ses": ^3.556.0
"@aws-sdk/credential-provider-node": ^3.350.0
"@backstage/backend-common": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/catalog-client": "workspace:^"
"@backstage/catalog-model": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/plugin-notifications-common": "workspace:^"
"@backstage/plugin-notifications-node": "workspace:^"
"@backstage/types": "workspace:^"
"@types/nodemailer": ^6.4.14
lodash: ^4.17.21
nodemailer: ^6.9.13
languageName: unknown
linkType: soft
"@backstage/plugin-notifications-backend@workspace:^, @backstage/plugin-notifications-backend@workspace:plugins/notifications-backend":
version: 0.0.0-use.local
resolution: "@backstage/plugin-notifications-backend@workspace:plugins/notifications-backend"
@@ -16064,6 +16135,15 @@ __metadata:
languageName: node
linkType: hard
"@types/nodemailer@npm:^6.4.14":
version: 6.4.14
resolution: "@types/nodemailer@npm:6.4.14"
dependencies:
"@types/node": "*"
checksum: 5f61f01dd736b17f431d1e8b320322f86460604b45df947fc4bc8999d7c7719405e349f7abba86e4fb100a464a30b52615d00dac03d9cb37562ff04487ebd310
languageName: node
linkType: hard
"@types/normalize-package-data@npm:^2.4.0":
version: 2.4.1
resolution: "@types/normalize-package-data@npm:2.4.1"
@@ -32254,6 +32334,13 @@ __metadata:
languageName: node
linkType: hard
"nodemailer@npm:^6.9.13":
version: 6.9.13
resolution: "nodemailer@npm:6.9.13"
checksum: 1b591ef480be2ff69480127cbff819e6593b1ef263b6f920e1a4e83e40280582daf7a14a809ef92f9828e2a70bdb3ce22b11924e209f2afe4975f9ff37e08e9d
languageName: node
linkType: hard
"nodemon@npm:^3.0.1":
version: 3.1.0
resolution: "nodemon@npm:3.1.0"