Merge pull request #24322 from drodil/email_notification_module
feat: add email notification processor
This commit is contained in:
@@ -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,72 @@
|
||||
# @backstage/plugin-notifications-backend-module-email
|
||||
|
||||
Adds support for sending Backstage notifications as emails to users.
|
||||
|
||||
Supports sending emails using `SMTP`, `SES`, or `sendmail`.
|
||||
|
||||
## Customizing email content
|
||||
|
||||
The email content can be customized with the `notificationsEmailTemplateExtensionPoint`. When you create
|
||||
this extension, you can set the custom `NotificationTemplateRenderer` to the module. To modify the contents,
|
||||
override the `getSubject`, `getHtml` and `getText` methods.
|
||||
|
||||
```ts
|
||||
import { notificationsEmailTemplateExtensionPoint } from '@backstage/plugin-notifications-backend-module-email';
|
||||
import { Notification } from '@backstage/plugin-notifications-common';
|
||||
|
||||
export const notificationsModuleEmailDecorator = createBackendModule({
|
||||
pluginId: 'notifications',
|
||||
moduleId: 'email.templates',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {
|
||||
emailTemplates: notificationsEmailTemplateExtensionPoint,
|
||||
},
|
||||
async init({ emailTemplates }) {
|
||||
emailTemplates.setTemplateRenderer({
|
||||
getSubject(notification) {
|
||||
return `New notification from ${notification.source}`;
|
||||
},
|
||||
getText(notification) {
|
||||
return notification.content;
|
||||
},
|
||||
getHtml(notification) {
|
||||
return `<p>${notification.content}</p>`;
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Example configuration:
|
||||
|
||||
```yaml
|
||||
notifications:
|
||||
processors:
|
||||
email:
|
||||
# Transport config, see options at `config.d.ts`
|
||||
transportConfig:
|
||||
transport: 'smtp'
|
||||
hostname: 'my-smtp-server'
|
||||
port: 587
|
||||
secure: false
|
||||
username: 'my-username'
|
||||
password: 'my-password'
|
||||
# The email sender address
|
||||
sender: 'sender@mycompany.com'
|
||||
replyTo: 'no-reply@mycompany.com'
|
||||
# Who to get email for broadcast notifications
|
||||
broadcastConfig:
|
||||
receiver: 'users'
|
||||
# How many emails to send concurrently, defaults to 2
|
||||
concurrencyLimit: 10
|
||||
# Cache configuration for email addresses
|
||||
# This is to prevent unnecessary calls to the catalog
|
||||
cache:
|
||||
ttl:
|
||||
days: 1
|
||||
```
|
||||
|
||||
See `config.d.ts` for more options for configuration.
|
||||
@@ -0,0 +1,32 @@
|
||||
## 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';
|
||||
import { ExtensionPoint } from '@backstage/backend-plugin-api';
|
||||
import { Notification as Notification_2 } from '@backstage/plugin-notifications-common';
|
||||
|
||||
// @public (undocumented)
|
||||
export interface NotificationsEmailTemplateExtensionPoint {
|
||||
// (undocumented)
|
||||
setTemplateRenderer(renderer: NotificationTemplateRenderer): void;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const notificationsEmailTemplateExtensionPoint: ExtensionPoint<NotificationsEmailTemplateExtensionPoint>;
|
||||
|
||||
// @public (undocumented)
|
||||
const notificationsModuleEmail: () => BackendFeature;
|
||||
export default notificationsModuleEmail;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface NotificationTemplateRenderer {
|
||||
// (undocumented)
|
||||
getHtml?(notification: Notification_2): string;
|
||||
// (undocumented)
|
||||
getSubject?(notification: Notification_2): string;
|
||||
// (undocumented)
|
||||
getText?(notification: Notification_2): string;
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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 { HumanDuration } from '@backstage/types';
|
||||
|
||||
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;
|
||||
/**
|
||||
* AWS account ID to use
|
||||
*/
|
||||
accountId?: string;
|
||||
/**
|
||||
* AWS region to use
|
||||
*/
|
||||
region?: 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;
|
||||
/**
|
||||
* Optional reply-to address
|
||||
*/
|
||||
replyTo?: string;
|
||||
/**
|
||||
* Concurrency limit for email sending, defaults to 2
|
||||
*/
|
||||
concurrencyLimit?: number;
|
||||
/**
|
||||
* Throttle duration between email sending, defaults to 100ms
|
||||
*/
|
||||
throttleInterval?: HumanDuration;
|
||||
/**
|
||||
* 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[];
|
||||
};
|
||||
cache?: {
|
||||
/**
|
||||
* Email cache TTL, defaults to 1 hour
|
||||
*/
|
||||
ttl?: HumanDuration;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"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.550.0",
|
||||
"@aws-sdk/types": "^3.347.0",
|
||||
"@backstage/backend-common": "workspace:^",
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/catalog-client": "workspace:^",
|
||||
"@backstage/catalog-model": "workspace:^",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/integration-aws-node": "workspace:^",
|
||||
"@backstage/plugin-notifications-common": "workspace:^",
|
||||
"@backstage/plugin-notifications-node": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"lodash": "^4.17.21",
|
||||
"nodemailer": "^6.9.13",
|
||||
"p-throttle": "^6.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@types/nodemailer": "^6.4.14"
|
||||
},
|
||||
"configSchema": "config.d.ts"
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 { createExtensionPoint } from '@backstage/backend-plugin-api';
|
||||
import { Notification } from '@backstage/plugin-notifications-common';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface NotificationTemplateRenderer {
|
||||
getSubject?(notification: Notification): string;
|
||||
getText?(notification: Notification): string;
|
||||
getHtml?(notification: Notification): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface NotificationsEmailTemplateExtensionPoint {
|
||||
setTemplateRenderer(renderer: NotificationTemplateRenderer): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const notificationsEmailTemplateExtensionPoint =
|
||||
createExtensionPoint<NotificationsEmailTemplateExtensionPoint>({
|
||||
id: 'notifications.email.templates',
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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';
|
||||
export * from './extensions';
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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';
|
||||
import {
|
||||
notificationsEmailTemplateExtensionPoint,
|
||||
NotificationTemplateRenderer,
|
||||
} from './extensions';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const notificationsModuleEmail = createBackendModule({
|
||||
pluginId: 'notifications',
|
||||
moduleId: 'email',
|
||||
register(reg) {
|
||||
let templateRenderer: NotificationTemplateRenderer | undefined;
|
||||
reg.registerExtensionPoint(notificationsEmailTemplateExtensionPoint, {
|
||||
setTemplateRenderer(renderer) {
|
||||
if (templateRenderer) {
|
||||
throw new Error(`Email template renderer was already registered`);
|
||||
}
|
||||
templateRenderer = renderer;
|
||||
},
|
||||
});
|
||||
|
||||
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,
|
||||
templateRenderer,
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
+349
@@ -0,0 +1,349 @@
|
||||
/*
|
||||
* 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', async () => {
|
||||
const processor = new NotificationsEmailProcessor(
|
||||
logger,
|
||||
new ConfigReader({
|
||||
notifications: {
|
||||
processors: {
|
||||
email: {
|
||||
transport: {
|
||||
transport: 'smtp',
|
||||
hostname: 'localhost',
|
||||
port: 465,
|
||||
secure: true,
|
||||
requireTls: false,
|
||||
},
|
||||
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(processor).toBeInstanceOf(NotificationsEmailProcessor);
|
||||
expect(createTransport as jest.Mock).toHaveBeenCalledWith({
|
||||
host: 'localhost',
|
||||
port: 465,
|
||||
requireTLS: false,
|
||||
secure: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create ses transport', async () => {
|
||||
const processor = new NotificationsEmailProcessor(
|
||||
logger,
|
||||
new ConfigReader({
|
||||
notifications: {
|
||||
processors: {
|
||||
email: {
|
||||
transport: {
|
||||
transport: 'ses',
|
||||
region: 'us-west-2',
|
||||
},
|
||||
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(processor).toBeInstanceOf(NotificationsEmailProcessor);
|
||||
expect(createTransport as jest.Mock).toHaveBeenCalledWith({
|
||||
SES: expect.anything(),
|
||||
});
|
||||
});
|
||||
|
||||
it('should create sendmail transport', async () => {
|
||||
const processor = new NotificationsEmailProcessor(
|
||||
logger,
|
||||
new ConfigReader({
|
||||
notifications: {
|
||||
processors: {
|
||||
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(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: {
|
||||
processors: {
|
||||
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: '',
|
||||
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: {
|
||||
processors: {
|
||||
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: '',
|
||||
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: {
|
||||
processors: {
|
||||
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: '',
|
||||
to: 'broadcast@backstage.io',
|
||||
});
|
||||
});
|
||||
});
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
* 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, readDurationFromConfig } from '@backstage/config';
|
||||
import { durationToMilliseconds } from '@backstage/types';
|
||||
import {
|
||||
CATALOG_FILTER_EXISTS,
|
||||
CatalogClient,
|
||||
} from '@backstage/catalog-client';
|
||||
import { Notification } from '@backstage/plugin-notifications-common';
|
||||
import {
|
||||
createSendmailTransport,
|
||||
createSesTransport,
|
||||
createSmtpTransport,
|
||||
} from './transports';
|
||||
import { UserEntity } from '@backstage/catalog-model';
|
||||
import { compact } from 'lodash';
|
||||
import { DefaultAwsCredentialsManager } from '@backstage/integration-aws-node';
|
||||
import { NotificationTemplateRenderer } from '../extensions';
|
||||
import Mail from 'nodemailer/lib/mailer';
|
||||
import pThrottle from 'p-throttle';
|
||||
|
||||
export class NotificationsEmailProcessor implements NotificationProcessor {
|
||||
private transporter: any;
|
||||
private readonly broadcastConfig?: Config;
|
||||
private readonly transportConfig: Config;
|
||||
private readonly sender: string;
|
||||
private readonly replyTo?: string;
|
||||
private readonly cacheTtl: number;
|
||||
private readonly concurrencyLimit: number;
|
||||
private readonly throttleInterval: number;
|
||||
|
||||
constructor(
|
||||
private readonly logger: LoggerService,
|
||||
private readonly config: Config,
|
||||
private readonly catalog: CatalogClient,
|
||||
private readonly auth: AuthService,
|
||||
private readonly cache?: CacheService,
|
||||
private readonly templateRenderer?: NotificationTemplateRenderer,
|
||||
) {
|
||||
const emailProcessorConfig = config.getConfig(
|
||||
'notifications.processors.email',
|
||||
);
|
||||
this.transportConfig = emailProcessorConfig.getConfig('transport');
|
||||
this.broadcastConfig =
|
||||
emailProcessorConfig.getOptionalConfig('broadcastConfig');
|
||||
this.sender = emailProcessorConfig.getString('sender');
|
||||
this.replyTo = emailProcessorConfig.getOptionalString('replyTo');
|
||||
this.concurrencyLimit =
|
||||
emailProcessorConfig.getOptionalNumber('concurrencyLimit') ?? 2;
|
||||
const throttleConfig =
|
||||
emailProcessorConfig.getOptionalConfig('throttleInterval');
|
||||
this.throttleInterval = throttleConfig
|
||||
? durationToMilliseconds(readDurationFromConfig(throttleConfig))
|
||||
: 100;
|
||||
const cacheConfig = emailProcessorConfig.getOptionalConfig('cache.ttl');
|
||||
this.cacheTtl = cacheConfig
|
||||
? durationToMilliseconds(readDurationFromConfig(cacheConfig))
|
||||
: 3_600_000;
|
||||
}
|
||||
|
||||
private async getTransporter() {
|
||||
if (this.transporter) {
|
||||
return this.transporter;
|
||||
}
|
||||
const transport = this.transportConfig.getString('transport');
|
||||
if (transport === 'smtp') {
|
||||
this.transporter = createSmtpTransport(this.transportConfig);
|
||||
} else if (transport === 'ses') {
|
||||
const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(
|
||||
this.config,
|
||||
);
|
||||
this.transporter = await createSesTransport(
|
||||
this.transportConfig,
|
||||
awsCredentialsManager,
|
||||
);
|
||||
} else if (transport === 'sendmail') {
|
||||
this.transporter = createSendmailTransport(this.transportConfig);
|
||||
} else {
|
||||
throw new Error(`Unsupported transport: ${transport}`);
|
||||
}
|
||||
return this.transporter;
|
||||
}
|
||||
|
||||
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<string[]>('user-emails:all');
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const { token } = await this.auth.getPluginRequestToken({
|
||||
onBehalfOf: await this.auth.getOwnServiceCredentials(),
|
||||
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, {
|
||||
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 { token } = await this.auth.getPluginRequestToken({
|
||||
onBehalfOf: await this.auth.getOwnServiceCredentials(),
|
||||
targetPluginId: 'catalog',
|
||||
});
|
||||
const entity = await this.catalog.getEntityByRef(entityRef, { token });
|
||||
const ret: string[] = [];
|
||||
if (entity) {
|
||||
const userEntity = entity as UserEntity;
|
||||
if (userEntity.spec.profile?.email) {
|
||||
ret.push(userEntity.spec.profile.email);
|
||||
}
|
||||
}
|
||||
|
||||
await this.cache?.set(`user-emails:${entityRef}`, ret, {
|
||||
ttl: this.cacheTtl,
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private async sendMail(options: Mail.Options) {
|
||||
try {
|
||||
await this.transporter.sendMail(options);
|
||||
} catch (e) {
|
||||
this.logger.error(`Failed to send email to ${options.to}: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendMails(options: Mail.Options, emails: string[]) {
|
||||
const throttle = pThrottle({
|
||||
limit: this.concurrencyLimit,
|
||||
interval: this.throttleInterval,
|
||||
});
|
||||
|
||||
const throttled = throttle((opts: Mail.Options) => this.sendMail(opts));
|
||||
await Promise.all(
|
||||
emails.map(email => throttled({ ...options, to: email })),
|
||||
);
|
||||
}
|
||||
|
||||
private async sendPlainEmail(notification: Notification, emails: string[]) {
|
||||
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: `<p>${contentParts.join('<br/>')}</p>`,
|
||||
text: contentParts.join('\n\n'),
|
||||
replyTo: this.replyTo,
|
||||
};
|
||||
|
||||
await this.sendMails(mailOptions, emails);
|
||||
}
|
||||
|
||||
private async sendTemplateEmail(
|
||||
notification: Notification,
|
||||
emails: string[],
|
||||
) {
|
||||
const mailOptions = {
|
||||
from: this.sender,
|
||||
subject:
|
||||
this.templateRenderer?.getSubject?.(notification) ??
|
||||
notification.payload.title,
|
||||
html: this.templateRenderer?.getHtml?.(notification),
|
||||
text: this.templateRenderer?.getText?.(notification),
|
||||
replyTo: this.replyTo,
|
||||
};
|
||||
|
||||
await this.sendMails(mailOptions, emails);
|
||||
}
|
||||
|
||||
async postProcess(
|
||||
notification: Notification,
|
||||
options: NotificationSendOptions,
|
||||
): Promise<void> {
|
||||
this.transporter = await this.getTransporter();
|
||||
|
||||
let emails: string[] = [];
|
||||
try {
|
||||
emails = await this.getRecipientEmails(notification, options);
|
||||
} catch (e) {
|
||||
this.logger.error(`Failed to resolve recipient emails: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (emails.length === 0) {
|
||||
this.logger.info(
|
||||
`No email recipients found for notification: ${notification.id}, skipping`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.templateRenderer) {
|
||||
await this.sendPlainEmail(notification, emails);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.sendTemplateEmail(notification, emails);
|
||||
}
|
||||
}
|
||||
@@ -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 { Config } from '@backstage/config';
|
||||
|
||||
export const createSendmailTransport = (config: Config) => {
|
||||
return createTransport({
|
||||
sendmail: true,
|
||||
newline: config.getOptionalString('newline') ?? 'unix',
|
||||
path: config.getOptionalString('path') ?? '/usr/sbin/sendmail',
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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 { SendRawEmailCommand, SES } from '@aws-sdk/client-ses';
|
||||
import { Config } from '@backstage/config';
|
||||
import { AwsCredentialsManager } from '@backstage/integration-aws-node';
|
||||
|
||||
export const createSesTransport = async (
|
||||
config: Config,
|
||||
credentialsManager: AwsCredentialsManager,
|
||||
) => {
|
||||
const credentials = await credentialsManager.getCredentialProvider({
|
||||
accountId: config.getOptionalString('accountId'),
|
||||
});
|
||||
const ses = new SES([
|
||||
{
|
||||
apiVersion: config.getOptionalString('apiVersion') ?? '2010-12-01',
|
||||
credentials: credentials.sdkCredentialProvider,
|
||||
region: config.getOptionalString('region'),
|
||||
},
|
||||
]);
|
||||
return createTransport({
|
||||
SES: { ses, aws: { SendRawEmailCommand } },
|
||||
});
|
||||
};
|
||||
@@ -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 { createTransport } from 'nodemailer';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
export const createSmtpTransport = (config: Config) => {
|
||||
const username = config.getOptionalString('username');
|
||||
const password = config.getOptionalString('password');
|
||||
|
||||
return createTransport({
|
||||
host: config.getString('hostname'),
|
||||
port: config.getNumber('port'),
|
||||
secure: config.getOptionalBoolean('secure') ?? false,
|
||||
requireTLS: config.getOptionalBoolean('requireTls') ?? false,
|
||||
auth: username && password ? { user: username, pass: password } : undefined,
|
||||
});
|
||||
};
|
||||
@@ -599,6 +599,55 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@aws-sdk/client-ses@npm:^3.550.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"
|
||||
@@ -6124,6 +6173,30 @@ __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.550.0
|
||||
"@aws-sdk/types": ^3.347.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/integration-aws-node": "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
|
||||
p-throttle: ^6.1.0
|
||||
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"
|
||||
@@ -16224,6 +16297,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"
|
||||
@@ -32423,6 +32505,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"
|
||||
@@ -33348,6 +33437,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"p-throttle@npm:^6.1.0":
|
||||
version: 6.1.0
|
||||
resolution: "p-throttle@npm:6.1.0"
|
||||
checksum: c1947cca8844564c3d86f8c09067add5e7398b87898cb1f1aea5c48d2c211590d039a316d28a4b0d95364ffa0213c01dff6bf71175c468eab0e381f77715dbdb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"p-timeout@npm:^3.2.0":
|
||||
version: 3.2.0
|
||||
resolution: "p-timeout@npm:3.2.0"
|
||||
|
||||
Reference in New Issue
Block a user