Skip to content

Commit d795299

Browse files
authored
[Alerting] set correct parameter for unauthented email action (#63086) (#63411)
PR #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/).
1 parent bb22529 commit d795299

File tree

4 files changed

+299
-16
lines changed

4 files changed

+299
-16
lines changed

x-pack/plugins/actions/server/builtin_action_types/email.test.ts

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,14 @@ describe('execute()', () => {
255255
services,
256256
};
257257
sendEmailMock.mockReset();
258-
await actionType.executor(executorOptions);
258+
const result = await actionType.executor(executorOptions);
259+
expect(result).toMatchInlineSnapshot(`
260+
Object {
261+
"actionId": "some-id",
262+
"data": undefined,
263+
"status": "ok",
264+
}
265+
`);
259266
expect(sendEmailMock.mock.calls[0][1]).toMatchInlineSnapshot(`
260267
Object {
261268
"content": Object {
@@ -282,4 +289,102 @@ describe('execute()', () => {
282289
}
283290
`);
284291
});
292+
293+
test('parameters are as expected with no auth', async () => {
294+
const config: ActionTypeConfigType = {
295+
service: null,
296+
host: 'a host',
297+
port: 42,
298+
secure: true,
299+
from: 'bob@example.com',
300+
};
301+
const secrets: ActionTypeSecretsType = {
302+
user: null,
303+
password: null,
304+
};
305+
const params: ActionParamsType = {
306+
to: ['jim@example.com'],
307+
cc: ['james@example.com'],
308+
bcc: ['jimmy@example.com'],
309+
subject: 'the subject',
310+
message: 'a message to you',
311+
};
312+
313+
const actionId = 'some-id';
314+
const executorOptions: ActionTypeExecutorOptions = {
315+
actionId,
316+
config,
317+
params,
318+
secrets,
319+
services,
320+
};
321+
sendEmailMock.mockReset();
322+
await actionType.executor(executorOptions);
323+
expect(sendEmailMock.mock.calls[0][1]).toMatchInlineSnapshot(`
324+
Object {
325+
"content": Object {
326+
"message": "a message to you",
327+
"subject": "the subject",
328+
},
329+
"routing": Object {
330+
"bcc": Array [
331+
"jimmy@example.com",
332+
],
333+
"cc": Array [
334+
"james@example.com",
335+
],
336+
"from": "bob@example.com",
337+
"to": Array [
338+
"jim@example.com",
339+
],
340+
},
341+
"transport": Object {
342+
"host": "a host",
343+
"port": 42,
344+
"secure": true,
345+
},
346+
}
347+
`);
348+
});
349+
350+
test('returns expected result when an error is thrown', async () => {
351+
const config: ActionTypeConfigType = {
352+
service: null,
353+
host: 'a host',
354+
port: 42,
355+
secure: true,
356+
from: 'bob@example.com',
357+
};
358+
const secrets: ActionTypeSecretsType = {
359+
user: null,
360+
password: null,
361+
};
362+
const params: ActionParamsType = {
363+
to: ['jim@example.com'],
364+
cc: ['james@example.com'],
365+
bcc: ['jimmy@example.com'],
366+
subject: 'the subject',
367+
message: 'a message to you',
368+
};
369+
370+
const actionId = 'some-id';
371+
const executorOptions: ActionTypeExecutorOptions = {
372+
actionId,
373+
config,
374+
params,
375+
secrets,
376+
services,
377+
};
378+
sendEmailMock.mockReset();
379+
sendEmailMock.mockRejectedValue(new Error('wops'));
380+
const result = await actionType.executor(executorOptions);
381+
expect(result).toMatchInlineSnapshot(`
382+
Object {
383+
"actionId": "some-id",
384+
"message": "error sending email",
385+
"serviceMessage": "wops",
386+
"status": "error",
387+
}
388+
`);
389+
});
285390
});

x-pack/plugins/actions/server/builtin_action_types/email.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n';
99
import { schema, TypeOf } from '@kbn/config-schema';
1010
import nodemailerGetService from 'nodemailer/lib/well-known';
1111

12-
import { sendEmail, JSON_TRANSPORT_SERVICE } from './lib/send_email';
12+
import { sendEmail, JSON_TRANSPORT_SERVICE, SendEmailOptions, Transport } from './lib/send_email';
1313
import { portSchema } from './lib/schemas';
1414
import { Logger } from '../../../../../src/core/server';
1515
import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types';
@@ -143,7 +143,7 @@ async function executor(
143143
const secrets = execOptions.secrets as ActionTypeSecretsType;
144144
const params = execOptions.params as ActionParamsType;
145145

146-
const transport: any = {};
146+
const transport: Transport = {};
147147

148148
if (secrets.user != null) {
149149
transport.user = secrets.user;
@@ -155,12 +155,13 @@ async function executor(
155155
if (config.service !== null) {
156156
transport.service = config.service;
157157
} else {
158-
transport.host = config.host;
159-
transport.port = config.port;
158+
// already validated service or host/port is not null ...
159+
transport.host = config.host!;
160+
transport.port = config.port!;
160161
transport.secure = getSecureValue(config.secure, config.port);
161162
}
162163

163-
const sendEmailOptions = {
164+
const sendEmailOptions: SendEmailOptions = {
164165
transport,
165166
routing: {
166167
from: config.from,
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
jest.mock('nodemailer', () => ({
8+
createTransport: jest.fn(),
9+
}));
10+
11+
import { Logger } from '../../../../../../src/core/server';
12+
import { sendEmail } from './send_email';
13+
import { loggingServiceMock } from '../../../../../../src/core/server/mocks';
14+
import nodemailer from 'nodemailer';
15+
16+
const createTransportMock = nodemailer.createTransport as jest.Mock;
17+
const sendMailMockResult = { result: 'does not matter' };
18+
const sendMailMock = jest.fn();
19+
20+
const mockLogger = loggingServiceMock.create().get() as jest.Mocked<Logger>;
21+
22+
describe('send_email module', () => {
23+
beforeEach(() => {
24+
jest.resetAllMocks();
25+
createTransportMock.mockReturnValue({ sendMail: sendMailMock });
26+
sendMailMock.mockResolvedValue(sendMailMockResult);
27+
});
28+
29+
test('handles authenticated email using service', async () => {
30+
const sendEmailOptions = getSendEmailOptions();
31+
const result = await sendEmail(mockLogger, sendEmailOptions);
32+
expect(result).toBe(sendMailMockResult);
33+
expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
34+
Array [
35+
Object {
36+
"auth": Object {
37+
"pass": "changeme",
38+
"user": "elastic",
39+
},
40+
"service": "whatever",
41+
},
42+
]
43+
`);
44+
expect(sendMailMock.mock.calls[0]).toMatchInlineSnapshot(`
45+
Array [
46+
Object {
47+
"bcc": Array [],
48+
"cc": Array [
49+
"bob@example.com",
50+
"robert@example.com",
51+
],
52+
"from": "fred@example.com",
53+
"html": "<p>a message</p>
54+
",
55+
"subject": "a subject",
56+
"text": "a message",
57+
"to": Array [
58+
"jim@example.com",
59+
],
60+
},
61+
]
62+
`);
63+
});
64+
65+
test('handles unauthenticated email using not secure host/port', async () => {
66+
const sendEmailOptions = getSendEmailOptions();
67+
delete sendEmailOptions.transport.service;
68+
delete sendEmailOptions.transport.user;
69+
delete sendEmailOptions.transport.password;
70+
sendEmailOptions.transport.host = 'example.com';
71+
sendEmailOptions.transport.port = 1025;
72+
const result = await sendEmail(mockLogger, sendEmailOptions);
73+
expect(result).toBe(sendMailMockResult);
74+
expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
75+
Array [
76+
Object {
77+
"host": "example.com",
78+
"port": 1025,
79+
"secure": false,
80+
"tls": Object {
81+
"rejectUnauthorized": false,
82+
},
83+
},
84+
]
85+
`);
86+
expect(sendMailMock.mock.calls[0]).toMatchInlineSnapshot(`
87+
Array [
88+
Object {
89+
"bcc": Array [],
90+
"cc": Array [
91+
"bob@example.com",
92+
"robert@example.com",
93+
],
94+
"from": "fred@example.com",
95+
"html": "<p>a message</p>
96+
",
97+
"subject": "a subject",
98+
"text": "a message",
99+
"to": Array [
100+
"jim@example.com",
101+
],
102+
},
103+
]
104+
`);
105+
});
106+
107+
test('handles unauthenticated email using secure host/port', async () => {
108+
const sendEmailOptions = getSendEmailOptions();
109+
delete sendEmailOptions.transport.service;
110+
delete sendEmailOptions.transport.user;
111+
delete sendEmailOptions.transport.password;
112+
sendEmailOptions.transport.host = 'example.com';
113+
sendEmailOptions.transport.port = 1025;
114+
sendEmailOptions.transport.secure = true;
115+
const result = await sendEmail(mockLogger, sendEmailOptions);
116+
expect(result).toBe(sendMailMockResult);
117+
expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
118+
Array [
119+
Object {
120+
"host": "example.com",
121+
"port": 1025,
122+
"secure": true,
123+
},
124+
]
125+
`);
126+
expect(sendMailMock.mock.calls[0]).toMatchInlineSnapshot(`
127+
Array [
128+
Object {
129+
"bcc": Array [],
130+
"cc": Array [
131+
"bob@example.com",
132+
"robert@example.com",
133+
],
134+
"from": "fred@example.com",
135+
"html": "<p>a message</p>
136+
",
137+
"subject": "a subject",
138+
"text": "a message",
139+
"to": Array [
140+
"jim@example.com",
141+
],
142+
},
143+
]
144+
`);
145+
});
146+
147+
test('passes nodemailer exceptions to caller', async () => {
148+
const sendEmailOptions = getSendEmailOptions();
149+
150+
sendMailMock.mockReset();
151+
sendMailMock.mockRejectedValue(new Error('wops'));
152+
153+
await expect(sendEmail(mockLogger, sendEmailOptions)).rejects.toThrow('wops');
154+
});
155+
});
156+
157+
function getSendEmailOptions(): any {
158+
return {
159+
content: {
160+
message: 'a message',
161+
subject: 'a subject',
162+
},
163+
routing: {
164+
from: 'fred@example.com',
165+
to: ['jim@example.com'],
166+
cc: ['bob@example.com', 'robert@example.com'],
167+
bcc: [],
168+
},
169+
transport: {
170+
service: 'whatever',
171+
user: 'elastic',
172+
password: 'changeme',
173+
},
174+
};
175+
}

x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,30 @@ import { Logger } from '../../../../../../src/core/server';
1414
// an email "service" which doesn't actually send, just returns what it would send
1515
export const JSON_TRANSPORT_SERVICE = '__json';
1616

17-
interface SendEmailOptions {
17+
export interface SendEmailOptions {
1818
transport: Transport;
1919
routing: Routing;
2020
content: Content;
2121
}
2222

2323
// config validation ensures either service is set or host/port are set
24-
interface Transport {
25-
user: string;
26-
password: string;
24+
export interface Transport {
25+
user?: string;
26+
password?: string;
2727
service?: string; // see: https://nodemailer.com/smtp/well-known/
2828
host?: string;
2929
port?: number;
3030
secure?: boolean; // see: https://nodemailer.com/smtp/#tls-options
3131
}
3232

33-
interface Routing {
33+
export interface Routing {
3434
from: string;
3535
to: string[];
3636
cc: string[];
3737
bcc: string[];
3838
}
3939

40-
interface Content {
40+
export interface Content {
4141
subject: string;
4242
message: string;
4343
}
@@ -49,12 +49,14 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom
4949
const { from, to, cc, bcc } = routing;
5050
const { subject, message } = content;
5151

52-
const transportConfig: Record<string, any> = {
53-
auth: {
52+
const transportConfig: Record<string, any> = {};
53+
54+
if (user != null && password != null) {
55+
transportConfig.auth = {
5456
user,
5557
pass: password,
56-
},
57-
};
58+
};
59+
}
5860

5961
if (service === JSON_TRANSPORT_SERVICE) {
6062
transportConfig.jsonTransport = true;

0 commit comments

Comments
 (0)