From 3568986c63e4a6dd29dec723e05275705faa9320 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Mon, 13 Apr 2020 21:09:33 -0400 Subject: [PATCH] [Alerting] set correct parameter for unauthented email action (#63086) (#63412) PR https://github.com/elastic/kibana/pull/60839 added support for unauthenticated emails, but didn't actually do enough to make it work. This PR completes that support, and adds some tests. You can do manual testing now with [maildev](http://maildev.github.io/maildev/). --- .../server/builtin_action_types/email.test.ts | 107 ++++++++++- .../server/builtin_action_types/email.ts | 11 +- .../lib/send_email.test.ts | 175 ++++++++++++++++++ .../builtin_action_types/lib/send_email.ts | 22 ++- 4 files changed, 299 insertions(+), 16 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index 469df4fd86e2c..658f8f3fd8cf9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -255,7 +255,14 @@ describe('execute()', () => { services, }; sendEmailMock.mockReset(); - await actionType.executor(executorOptions); + const result = await actionType.executor(executorOptions); + expect(result).toMatchInlineSnapshot(` + Object { + "actionId": "some-id", + "data": undefined, + "status": "ok", + } + `); expect(sendEmailMock.mock.calls[0][1]).toMatchInlineSnapshot(` Object { "content": Object { @@ -282,4 +289,102 @@ describe('execute()', () => { } `); }); + + test('parameters are as expected with no auth', async () => { + const config: ActionTypeConfigType = { + service: null, + host: 'a host', + port: 42, + secure: true, + from: 'bob@example.com', + }; + const secrets: ActionTypeSecretsType = { + user: null, + password: null, + }; + const params: ActionParamsType = { + to: ['jim@example.com'], + cc: ['james@example.com'], + bcc: ['jimmy@example.com'], + subject: 'the subject', + message: 'a message to you', + }; + + const actionId = 'some-id'; + const executorOptions: ActionTypeExecutorOptions = { + actionId, + config, + params, + secrets, + services, + }; + sendEmailMock.mockReset(); + await actionType.executor(executorOptions); + expect(sendEmailMock.mock.calls[0][1]).toMatchInlineSnapshot(` + Object { + "content": Object { + "message": "a message to you", + "subject": "the subject", + }, + "routing": Object { + "bcc": Array [ + "jimmy@example.com", + ], + "cc": Array [ + "james@example.com", + ], + "from": "bob@example.com", + "to": Array [ + "jim@example.com", + ], + }, + "transport": Object { + "host": "a host", + "port": 42, + "secure": true, + }, + } + `); + }); + + test('returns expected result when an error is thrown', async () => { + const config: ActionTypeConfigType = { + service: null, + host: 'a host', + port: 42, + secure: true, + from: 'bob@example.com', + }; + const secrets: ActionTypeSecretsType = { + user: null, + password: null, + }; + const params: ActionParamsType = { + to: ['jim@example.com'], + cc: ['james@example.com'], + bcc: ['jimmy@example.com'], + subject: 'the subject', + message: 'a message to you', + }; + + const actionId = 'some-id'; + const executorOptions: ActionTypeExecutorOptions = { + actionId, + config, + params, + secrets, + services, + }; + sendEmailMock.mockReset(); + sendEmailMock.mockRejectedValue(new Error('wops')); + const result = await actionType.executor(executorOptions); + expect(result).toMatchInlineSnapshot(` + Object { + "actionId": "some-id", + "message": "error sending email", + "serviceMessage": "wops", + "status": "error", + } + `); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.ts b/x-pack/plugins/actions/server/builtin_action_types/email.ts index 7992920fdfcb4..ca8d089ad2946 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import nodemailerGetService from 'nodemailer/lib/well-known'; -import { sendEmail, JSON_TRANSPORT_SERVICE } from './lib/send_email'; +import { sendEmail, JSON_TRANSPORT_SERVICE, SendEmailOptions, Transport } from './lib/send_email'; import { portSchema } from './lib/schemas'; import { Logger } from '../../../../../src/core/server'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; @@ -143,7 +143,7 @@ async function executor( const secrets = execOptions.secrets as ActionTypeSecretsType; const params = execOptions.params as ActionParamsType; - const transport: any = {}; + const transport: Transport = {}; if (secrets.user != null) { transport.user = secrets.user; @@ -155,12 +155,13 @@ async function executor( if (config.service !== null) { transport.service = config.service; } else { - transport.host = config.host; - transport.port = config.port; + // already validated service or host/port is not null ... + transport.host = config.host!; + transport.port = config.port!; transport.secure = getSecureValue(config.secure, config.port); } - const sendEmailOptions = { + const sendEmailOptions: SendEmailOptions = { transport, routing: { from: config.from, diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts new file mode 100644 index 0000000000000..42160dc2fc22b --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts @@ -0,0 +1,175 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('nodemailer', () => ({ + createTransport: jest.fn(), +})); + +import { Logger } from '../../../../../../src/core/server'; +import { sendEmail } from './send_email'; +import { loggingServiceMock } from '../../../../../../src/core/server/mocks'; +import nodemailer from 'nodemailer'; + +const createTransportMock = nodemailer.createTransport as jest.Mock; +const sendMailMockResult = { result: 'does not matter' }; +const sendMailMock = jest.fn(); + +const mockLogger = loggingServiceMock.create().get() as jest.Mocked; + +describe('send_email module', () => { + beforeEach(() => { + jest.resetAllMocks(); + createTransportMock.mockReturnValue({ sendMail: sendMailMock }); + sendMailMock.mockResolvedValue(sendMailMockResult); + }); + + test('handles authenticated email using service', async () => { + const sendEmailOptions = getSendEmailOptions(); + const result = await sendEmail(mockLogger, sendEmailOptions); + expect(result).toBe(sendMailMockResult); + expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "auth": Object { + "pass": "changeme", + "user": "elastic", + }, + "service": "whatever", + }, + ] + `); + expect(sendMailMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "bcc": Array [], + "cc": Array [ + "bob@example.com", + "robert@example.com", + ], + "from": "fred@example.com", + "html": "

a message

+ ", + "subject": "a subject", + "text": "a message", + "to": Array [ + "jim@example.com", + ], + }, + ] + `); + }); + + test('handles unauthenticated email using not secure host/port', async () => { + const sendEmailOptions = getSendEmailOptions(); + delete sendEmailOptions.transport.service; + delete sendEmailOptions.transport.user; + delete sendEmailOptions.transport.password; + sendEmailOptions.transport.host = 'example.com'; + sendEmailOptions.transport.port = 1025; + const result = await sendEmail(mockLogger, sendEmailOptions); + expect(result).toBe(sendMailMockResult); + expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "host": "example.com", + "port": 1025, + "secure": false, + "tls": Object { + "rejectUnauthorized": false, + }, + }, + ] + `); + expect(sendMailMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "bcc": Array [], + "cc": Array [ + "bob@example.com", + "robert@example.com", + ], + "from": "fred@example.com", + "html": "

a message

+ ", + "subject": "a subject", + "text": "a message", + "to": Array [ + "jim@example.com", + ], + }, + ] + `); + }); + + test('handles unauthenticated email using secure host/port', async () => { + const sendEmailOptions = getSendEmailOptions(); + delete sendEmailOptions.transport.service; + delete sendEmailOptions.transport.user; + delete sendEmailOptions.transport.password; + sendEmailOptions.transport.host = 'example.com'; + sendEmailOptions.transport.port = 1025; + sendEmailOptions.transport.secure = true; + const result = await sendEmail(mockLogger, sendEmailOptions); + expect(result).toBe(sendMailMockResult); + expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "host": "example.com", + "port": 1025, + "secure": true, + }, + ] + `); + expect(sendMailMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "bcc": Array [], + "cc": Array [ + "bob@example.com", + "robert@example.com", + ], + "from": "fred@example.com", + "html": "

a message

+ ", + "subject": "a subject", + "text": "a message", + "to": Array [ + "jim@example.com", + ], + }, + ] + `); + }); + + test('passes nodemailer exceptions to caller', async () => { + const sendEmailOptions = getSendEmailOptions(); + + sendMailMock.mockReset(); + sendMailMock.mockRejectedValue(new Error('wops')); + + await expect(sendEmail(mockLogger, sendEmailOptions)).rejects.toThrow('wops'); + }); +}); + +function getSendEmailOptions(): any { + return { + content: { + message: 'a message', + subject: 'a subject', + }, + routing: { + from: 'fred@example.com', + to: ['jim@example.com'], + cc: ['bob@example.com', 'robert@example.com'], + bcc: [], + }, + transport: { + service: 'whatever', + user: 'elastic', + password: 'changeme', + }, + }; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts index 47d7aff8022ce..ffbf7485a8b0b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts @@ -14,30 +14,30 @@ import { Logger } from '../../../../../../src/core/server'; // an email "service" which doesn't actually send, just returns what it would send export const JSON_TRANSPORT_SERVICE = '__json'; -interface SendEmailOptions { +export interface SendEmailOptions { transport: Transport; routing: Routing; content: Content; } // config validation ensures either service is set or host/port are set -interface Transport { - user: string; - password: string; +export interface Transport { + user?: string; + password?: string; service?: string; // see: https://nodemailer.com/smtp/well-known/ host?: string; port?: number; secure?: boolean; // see: https://nodemailer.com/smtp/#tls-options } -interface Routing { +export interface Routing { from: string; to: string[]; cc: string[]; bcc: string[]; } -interface Content { +export interface Content { subject: string; message: string; } @@ -49,12 +49,14 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom const { from, to, cc, bcc } = routing; const { subject, message } = content; - const transportConfig: Record = { - auth: { + const transportConfig: Record = {}; + + if (user != null && password != null) { + transportConfig.auth = { user, pass: password, - }, - }; + }; + } if (service === JSON_TRANSPORT_SERVICE) { transportConfig.jsonTransport = true;