emailengine-app 2.68.1 → 2.69.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.github/workflows/deploy.yml +2 -0
  2. package/.github/workflows/release.yaml +4 -0
  3. package/CHANGELOG.md +40 -0
  4. package/config/default.toml +2 -0
  5. package/data/google-crawlers.json +7 -1
  6. package/lib/account.js +62 -25
  7. package/lib/api-routes/account-routes.js +493 -75
  8. package/lib/api-routes/blocklist-routes.js +337 -0
  9. package/lib/api-routes/delivery-test-routes.js +321 -0
  10. package/lib/api-routes/export-routes.js +1 -12
  11. package/lib/api-routes/gateway-routes.js +376 -0
  12. package/lib/api-routes/license-routes.js +142 -0
  13. package/lib/api-routes/mailbox-routes.js +318 -0
  14. package/lib/api-routes/message-routes.js +21 -129
  15. package/lib/api-routes/oauth2-app-routes.js +631 -0
  16. package/lib/api-routes/outbox-routes.js +173 -0
  17. package/lib/api-routes/pubsub-routes.js +98 -0
  18. package/lib/api-routes/route-helpers.js +45 -0
  19. package/lib/api-routes/settings-routes.js +331 -0
  20. package/lib/api-routes/stats-routes.js +77 -0
  21. package/lib/api-routes/submit-routes.js +472 -0
  22. package/lib/api-routes/template-routes.js +7 -55
  23. package/lib/api-routes/token-routes.js +297 -0
  24. package/lib/api-routes/webhook-route-routes.js +152 -0
  25. package/lib/email-client/gmail-client.js +14 -0
  26. package/lib/email-client/imap/mailbox.js +34 -11
  27. package/lib/email-client/imap/subconnection.js +20 -12
  28. package/lib/email-client/imap/sync-operations.js +130 -2
  29. package/lib/email-client/imap-client.js +116 -58
  30. package/lib/email-client/outlook-client.js +85 -13
  31. package/lib/export.js +60 -19
  32. package/lib/imapproxy/imap-core/lib/commands/starttls.js +18 -0
  33. package/lib/imapproxy/imap-core/lib/imap-command.js +6 -1
  34. package/lib/imapproxy/imap-core/lib/imap-connection.js +106 -23
  35. package/lib/imapproxy/imap-core/lib/imap-server.js +24 -0
  36. package/lib/imapproxy/imap-core/lib/imap-stream.js +26 -0
  37. package/lib/message-port-stream.js +113 -16
  38. package/lib/reject-worker-calls.js +42 -0
  39. package/lib/routes-ui.js +37 -8778
  40. package/lib/schemas.js +26 -1
  41. package/lib/tools.js +68 -0
  42. package/lib/ui-routes/account-routes.js +40 -210
  43. package/lib/ui-routes/admin-config-routes.js +913 -487
  44. package/lib/ui-routes/admin-entities-routes.js +1 -0
  45. package/lib/ui-routes/auth-routes.js +1339 -0
  46. package/lib/ui-routes/dashboard-routes.js +188 -0
  47. package/lib/ui-routes/document-store-routes.js +800 -0
  48. package/lib/ui-routes/export-routes.js +217 -0
  49. package/lib/ui-routes/internals-routes.js +354 -0
  50. package/lib/ui-routes/network-config-routes.js +759 -0
  51. package/lib/ui-routes/{oauth-routes.js → oauth-config-routes.js} +371 -91
  52. package/lib/ui-routes/route-helpers.js +316 -0
  53. package/lib/ui-routes/smtp-test-routes.js +236 -0
  54. package/lib/ui-routes/unsubscribe-routes.js +234 -0
  55. package/lib/webhook-request.js +36 -0
  56. package/package.json +8 -8
  57. package/sbom.json +1 -1
  58. package/server.js +214 -16
  59. package/static/licenses.html +12 -12
  60. package/translations/messages.pot +129 -149
  61. package/views/dashboard.hbs +7 -26
  62. package/views/internals/index.hbs +15 -0
  63. package/views/tokens/index.hbs +9 -0
  64. package/workers/api.js +198 -4401
  65. package/workers/export.js +87 -54
  66. package/workers/imap.js +29 -13
  67. package/workers/submit.js +20 -11
  68. package/workers/webhooks.js +6 -20
@@ -0,0 +1,337 @@
1
+ 'use strict';
2
+
3
+ const Boom = require('@hapi/boom');
4
+ const Joi = require('joi');
5
+ const { redis } = require('../db');
6
+ const { Account } = require('../account');
7
+ const getSecret = require('../get-secret');
8
+ const { lists } = require('../lists');
9
+ const { failAction } = require('../tools');
10
+ const { handleError } = require('./route-helpers');
11
+ const { accountIdSchema } = require('../schemas');
12
+ const { REDIS_PREFIX } = require('../consts');
13
+
14
+ async function init(args) {
15
+ const { server, call, CORS_CONFIG } = args;
16
+
17
+ server.route({
18
+ method: 'GET',
19
+ path: '/v1/blocklists',
20
+
21
+ async handler(request) {
22
+ try {
23
+ return await lists.list(request.query.page, request.query.pageSize);
24
+ } catch (err) {
25
+ handleError(request, err);
26
+ }
27
+ },
28
+
29
+ options: {
30
+ description: 'List blocklists',
31
+ notes: 'List blocklists with blocked addresses',
32
+ tags: ['api', 'Blocklists'],
33
+
34
+ plugins: {},
35
+
36
+ auth: {
37
+ strategy: 'api-token',
38
+ mode: 'required'
39
+ },
40
+ cors: CORS_CONFIG,
41
+
42
+ validate: {
43
+ options: {
44
+ stripUnknown: false,
45
+ abortEarly: false,
46
+ convert: true
47
+ },
48
+ failAction,
49
+
50
+ query: Joi.object({
51
+ page: Joi.number()
52
+ .integer()
53
+ .min(0)
54
+ .max(1024 * 1024)
55
+ .default(0)
56
+ .example(0)
57
+ .description('Page number (zero indexed, so use 0 for first page)')
58
+ .label('PageNumber'),
59
+ pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
60
+ }).label('PageListsRequest')
61
+ },
62
+
63
+ response: {
64
+ schema: Joi.object({
65
+ total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
66
+ page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
67
+ pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
68
+
69
+ blocklists: Joi.array()
70
+ .items(
71
+ Joi.object({
72
+ listId: Joi.string().max(256).required().example('example').description('List ID'),
73
+ count: Joi.number().integer().example(12).description('Count of blocked addresses in this list')
74
+ }).label('BlocklistsResponseItem')
75
+ )
76
+ .label('BlocklistsEntries')
77
+ }).label('BlocklistsResponse'),
78
+ failAction: 'log'
79
+ }
80
+ }
81
+ });
82
+
83
+ server.route({
84
+ method: 'GET',
85
+ path: '/v1/blocklist/{listId}',
86
+
87
+ async handler(request) {
88
+ try {
89
+ return await lists.listContent(request.params.listId, request.query.page, request.query.pageSize);
90
+ } catch (err) {
91
+ handleError(request, err);
92
+ }
93
+ },
94
+
95
+ options: {
96
+ description: 'List blocklist entries',
97
+ notes: 'List blocked addresses for a list',
98
+ tags: ['api', 'Blocklists'],
99
+
100
+ plugins: {},
101
+
102
+ auth: {
103
+ strategy: 'api-token',
104
+ mode: 'required'
105
+ },
106
+ cors: CORS_CONFIG,
107
+
108
+ validate: {
109
+ options: {
110
+ stripUnknown: false,
111
+ abortEarly: false,
112
+ convert: true
113
+ },
114
+ failAction,
115
+
116
+ params: Joi.object({
117
+ listId: Joi.string()
118
+ .hostname()
119
+ .example('test-list')
120
+ .description('List ID. Must use a subdomain name format. Lists are registered ad-hoc, so a new identifier defines a new list.')
121
+ .label('ListID')
122
+ .required()
123
+ }).label('BlocklistListRequest'),
124
+
125
+ query: Joi.object({
126
+ page: Joi.number()
127
+ .integer()
128
+ .min(0)
129
+ .max(1024 * 1024)
130
+ .default(0)
131
+ .example(0)
132
+ .description('Page number (zero indexed, so use 0 for first page)')
133
+ .label('PageNumber'),
134
+ pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
135
+ }).label('PageListsRequest')
136
+ },
137
+
138
+ response: {
139
+ schema: Joi.object({
140
+ listId: Joi.string().max(256).required().example('example').description('List ID'),
141
+ total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
142
+ page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
143
+ pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
144
+ addresses: Joi.array()
145
+ .items(
146
+ Joi.object({
147
+ recipient: Joi.string().email().example('user@example.com').description('Listed email address').required(),
148
+ account: accountIdSchema.required().required(),
149
+ messageId: Joi.string().example('<test123@example.com>').description('Message ID'),
150
+ source: Joi.string().example('api').description('Which mechanism was used to add the entry'),
151
+ reason: Joi.string().example('api').description('Why this entry was added'),
152
+ remoteAddress: Joi.string()
153
+ .ip({
154
+ version: ['ipv4', 'ipv6'],
155
+ cidr: 'optional'
156
+ })
157
+ .description('Which IP address triggered the entry'),
158
+ userAgent: Joi.string().example('Mozilla/5.0 (Macintosh)').description('Which user agent triggered the entry'),
159
+ created: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this entry was added or updated').required()
160
+ }).label('BlocklistListResponseItem')
161
+ )
162
+ .label('BlocklistListEntries')
163
+ }).label('BlocklistListResponse'),
164
+ failAction: 'log'
165
+ }
166
+ }
167
+ });
168
+
169
+ server.route({
170
+ method: 'POST',
171
+ path: '/v1/blocklist/{listId}',
172
+ async handler(request) {
173
+ let accountObject = new Account({
174
+ redis,
175
+ account: request.payload.account,
176
+ call,
177
+ secret: await getSecret(),
178
+ timeout: request.headers['x-ee-timeout']
179
+ });
180
+
181
+ try {
182
+ // throws if account does not exist
183
+ await accountObject.loadAccountData();
184
+
185
+ let added = await redis.eeListAdd(
186
+ `${REDIS_PREFIX}lists:unsub:lists`,
187
+ `${REDIS_PREFIX}lists:unsub:entries:${request.params.listId}`,
188
+ request.params.listId,
189
+ request.payload.recipient.toLowerCase().trim(),
190
+ JSON.stringify({
191
+ recipient: request.payload.recipient,
192
+ account: request.payload.account,
193
+ source: 'api',
194
+ reason: request.payload.reason,
195
+ remoteAddress: request.app.ip,
196
+ userAgent: request.headers['user-agent'],
197
+ created: new Date().toISOString()
198
+ })
199
+ );
200
+
201
+ return {
202
+ success: true,
203
+ added: !!added
204
+ };
205
+ } catch (err) {
206
+ request.logger.error({ msg: 'API request failed', err });
207
+ if (Boom.isBoom(err)) {
208
+ throw err;
209
+ }
210
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
211
+ if (err.code) {
212
+ error.output.payload.code = err.code;
213
+ }
214
+ if (err.details) {
215
+ error.output.payload.details = err.details;
216
+ }
217
+ throw error;
218
+ }
219
+ },
220
+ options: {
221
+ description: 'Add to blocklist',
222
+ notes: 'Add an email address to a blocklist',
223
+ tags: ['api', 'Blocklists'],
224
+
225
+ auth: {
226
+ strategy: 'api-token',
227
+ mode: 'required'
228
+ },
229
+ cors: CORS_CONFIG,
230
+
231
+ validate: {
232
+ options: {
233
+ stripUnknown: false,
234
+ abortEarly: false,
235
+ convert: true
236
+ },
237
+ failAction,
238
+
239
+ params: Joi.object({
240
+ listId: Joi.string()
241
+ .hostname()
242
+ .example('test-list')
243
+ .description('List ID. Must use a subdomain name format. Lists are registered ad-hoc, so a new identifier defines a new list.')
244
+ .label('ListID')
245
+ .required()
246
+ }).label('BlocklistListRequest'),
247
+
248
+ payload: Joi.object({
249
+ account: accountIdSchema.required(),
250
+ recipient: Joi.string().empty('').email().example('user@example.com').description('Email address to add to the list').required(),
251
+ reason: Joi.string().empty('').default('block').description('Identifier for the blocking reason')
252
+ }).label('BlocklistListAddPayload')
253
+ },
254
+
255
+ response: {
256
+ schema: Joi.object({
257
+ success: Joi.boolean().example(true).description('Was the request successful').label('BlocklistListAddSuccess'),
258
+ added: Joi.boolean().example(true).description('Was the address added to the list')
259
+ }).label('BlocklistListAddResponse'),
260
+ failAction: 'log'
261
+ }
262
+ }
263
+ });
264
+
265
+ server.route({
266
+ method: 'DELETE',
267
+ path: '/v1/blocklist/{listId}',
268
+
269
+ async handler(request) {
270
+ try {
271
+ let exists = await redis.hexists(`${REDIS_PREFIX}lists:unsub:lists`, request.params.listId);
272
+ if (!exists) {
273
+ let message = 'Requested blocklist was not found';
274
+ let error = Boom.boomify(new Error(message), { statusCode: 404 });
275
+ throw error;
276
+ }
277
+
278
+ let deleted = await redis.eeListRemove(
279
+ `${REDIS_PREFIX}lists:unsub:lists`,
280
+ `${REDIS_PREFIX}lists:unsub:entries:${request.params.listId}`,
281
+ request.params.listId,
282
+ request.query.recipient.toLowerCase().trim()
283
+ );
284
+
285
+ return {
286
+ deleted: !!deleted
287
+ };
288
+ } catch (err) {
289
+ handleError(request, err);
290
+ }
291
+ },
292
+ options: {
293
+ description: 'Remove from blocklist',
294
+ notes: 'Delete a blocked email address from a list',
295
+ tags: ['api', 'Blocklists'],
296
+
297
+ plugins: {},
298
+
299
+ auth: {
300
+ strategy: 'api-token',
301
+ mode: 'required'
302
+ },
303
+ cors: CORS_CONFIG,
304
+
305
+ validate: {
306
+ options: {
307
+ stripUnknown: false,
308
+ abortEarly: false,
309
+ convert: true
310
+ },
311
+ failAction,
312
+
313
+ params: Joi.object({
314
+ listId: Joi.string()
315
+ .hostname()
316
+ .example('test-list')
317
+ .description('List ID. Must use a subdomain name format. Lists are registered ad-hoc, so a new identifier defines a new list.')
318
+ .label('ListID')
319
+ .required()
320
+ }).label('BlocklistListRequest'),
321
+
322
+ query: Joi.object({
323
+ recipient: Joi.string().empty('').email().example('user@example.com').description('Email address to remove from the list').required()
324
+ }).label('RecipientQuery')
325
+ },
326
+
327
+ response: {
328
+ schema: Joi.object({
329
+ deleted: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Was the address removed from the list')
330
+ }).label('DeleteBlocklistResponse'),
331
+ failAction: 'log'
332
+ }
333
+ }
334
+ });
335
+ }
336
+
337
+ module.exports = init;
@@ -0,0 +1,321 @@
1
+ 'use strict';
2
+
3
+ const Boom = require('@hapi/boom');
4
+ const Joi = require('joi');
5
+ const { fetch: fetchCmd } = require('undici');
6
+ const { redis } = require('../db');
7
+ const { Account } = require('../account');
8
+ const { Gateway } = require('../gateway');
9
+ const getSecret = require('../get-secret');
10
+ const { failAction, httpAgent } = require('../tools');
11
+ const { accountIdSchema } = require('../schemas');
12
+ const { REDIS_PREFIX } = require('../consts');
13
+ const packageData = require('../../package.json');
14
+
15
+ async function init(args) {
16
+ const { server, call, CORS_CONFIG, SMTP_TEST_HOST } = args;
17
+
18
+ server.route({
19
+ method: 'POST',
20
+ path: '/v1/delivery-test/account/{account}',
21
+ async handler(request) {
22
+ let accountObject = new Account({
23
+ redis,
24
+ account: request.params.account,
25
+ call,
26
+ secret: await getSecret(),
27
+ timeout: request.headers['x-ee-timeout']
28
+ });
29
+
30
+ try {
31
+ // throws if account does not exist
32
+ let accountData = await accountObject.loadAccountData();
33
+
34
+ request.logger.info({ msg: 'Requested SMTP delivery test', account: request.params.account });
35
+
36
+ let headers = {
37
+ 'Content-Type': 'application/json',
38
+ 'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage})`
39
+ };
40
+
41
+ let res = await fetchCmd(`${SMTP_TEST_HOST}/test-address`, {
42
+ method: 'post',
43
+ body: JSON.stringify({
44
+ version: packageData.version,
45
+ requestor: '@postalsys/emailengine-app'
46
+ }),
47
+ headers,
48
+ dispatcher: httpAgent.retry
49
+ });
50
+
51
+ if (!res.ok) {
52
+ let err = new Error(`Invalid response: ${res.status} ${res.statusText}`);
53
+ err.statusCode = res.status;
54
+
55
+ try {
56
+ err.details = await res.json();
57
+ } catch (err) {
58
+ // ignore
59
+ }
60
+
61
+ throw err;
62
+ }
63
+
64
+ let testAccount = await res.json();
65
+ if (!testAccount || !testAccount.user) {
66
+ let err = new Error(`Invalid test account`);
67
+ err.statusCode = 500;
68
+
69
+ try {
70
+ err.details = testAccount;
71
+ } catch (err) {
72
+ // ignore
73
+ }
74
+
75
+ throw err;
76
+ }
77
+
78
+ if (request.payload.gateway) {
79
+ // try to load the gateway, throws if not set
80
+ let gatewayObject = new Gateway({ redis, gateway: request.payload.gateway, call, secret: await getSecret() });
81
+ await gatewayObject.loadGatewayData();
82
+ }
83
+
84
+ try {
85
+ let now = new Date().toISOString();
86
+ let queueResponse = await accountObject.queueMessage(
87
+ {
88
+ account: accountData.account,
89
+ subject: `Delivery test ${now}`,
90
+ text: `Hello
91
+
92
+ This is an automated email to test deliverability settings. If you see this email, you can safely delete it.
93
+
94
+ ${now}`,
95
+ html: `<p>Hello</p>
96
+ <p>This is an automated email to test deliverability settings. If you see this email, you can safely delete it.</p>
97
+ <p>${now}</p>`,
98
+ from: {
99
+ name: accountData.name,
100
+ address: accountData.email
101
+ },
102
+ to: [{ name: 'Delivery Test Server', address: testAccount.address }],
103
+ copy: false,
104
+ gateway: request.payload.gateway,
105
+ feedbackKey: `${REDIS_PREFIX}test-send:${testAccount.user}`,
106
+ deliveryAttempts: 1
107
+ },
108
+ { source: 'test' }
109
+ );
110
+
111
+ return {
112
+ success: !!queueResponse.queueId,
113
+ deliveryTest: testAccount.user
114
+ };
115
+ } catch (err) {
116
+ return {
117
+ error: err.message
118
+ };
119
+ }
120
+ } catch (err) {
121
+ request.logger.error({ msg: 'API request failed', err });
122
+ if (Boom.isBoom(err)) {
123
+ throw err;
124
+ }
125
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
126
+ if (err.code) {
127
+ error.output.payload.code = err.code;
128
+ }
129
+ if (err.details) {
130
+ error.output.payload.details = err.details;
131
+ }
132
+ throw error;
133
+ }
134
+ },
135
+ options: {
136
+ description: 'Create delivery test',
137
+ notes: 'Initiate a delivery test',
138
+ tags: ['api', 'Delivery Test'],
139
+
140
+ auth: {
141
+ strategy: 'api-token',
142
+ mode: 'required'
143
+ },
144
+ cors: CORS_CONFIG,
145
+
146
+ validate: {
147
+ options: {
148
+ stripUnknown: false,
149
+ abortEarly: false,
150
+ convert: true
151
+ },
152
+ failAction,
153
+
154
+ params: Joi.object({
155
+ account: accountIdSchema.required()
156
+ }),
157
+
158
+ payload: Joi.object({
159
+ gateway: Joi.string().allow(false, null).empty('').max(256).example(false).description('Optional gateway ID').label('DeliveryTestGateway')
160
+ }).label('DeliveryStartRequest')
161
+ },
162
+
163
+ response: {
164
+ schema: Joi.object({
165
+ success: Joi.boolean().example(true).description('Was the test started').label('ResponseDeliveryStartSuccess'),
166
+ deliveryTest: Joi.string()
167
+ .guid({
168
+ version: ['uuidv4', 'uuidv5']
169
+ })
170
+ .example('6420a6ad-7f82-4e4f-8112-82a9dad1f34d')
171
+ .description('Test ID')
172
+ }).label('DeliveryStartResponse'),
173
+ failAction: 'log'
174
+ }
175
+ }
176
+ });
177
+
178
+ server.route({
179
+ method: 'GET',
180
+ path: '/v1/delivery-test/check/{deliveryTest}',
181
+ async handler(request) {
182
+ try {
183
+ request.logger.info({ msg: 'Requested SMTP delivery test check', deliveryTest: request.params.deliveryTest });
184
+
185
+ let deliveryStatus = (await redis.hgetall(`${REDIS_PREFIX}test-send:${request.params.deliveryTest}`)) || {};
186
+ if (deliveryStatus.success === 'false') {
187
+ let err = new Error(`Failed to deliver email`);
188
+ err.statusCode = 500;
189
+ err.details = deliveryStatus;
190
+ throw err;
191
+ }
192
+
193
+ let headers = {
194
+ 'Content-Type': 'application/json',
195
+ 'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage})`
196
+ };
197
+
198
+ let res = await fetchCmd(`${SMTP_TEST_HOST}/test-address/${request.params.deliveryTest}`, {
199
+ method: 'get',
200
+ headers,
201
+ dispatcher: httpAgent.retry
202
+ });
203
+
204
+ if (!res.ok) {
205
+ let err = new Error(`Invalid response: ${res.status} ${res.statusText}`);
206
+ err.statusCode = res.status;
207
+
208
+ try {
209
+ err.details = await res.json();
210
+ } catch (err) {
211
+ // ignore
212
+ }
213
+
214
+ throw err;
215
+ }
216
+
217
+ let testResponse = await res.json();
218
+
219
+ let success = testResponse && testResponse.status === 'success'; //Default
220
+
221
+ if (testResponse && success) {
222
+ let mainSig =
223
+ testResponse.dkim &&
224
+ testResponse.dkim.results &&
225
+ testResponse.dkim.results.find(entry => entry && entry.status && entry.status.result === 'pass' && entry.status.aligned);
226
+
227
+ if (!mainSig) {
228
+ mainSig =
229
+ testResponse.dkim &&
230
+ testResponse.dkim.results &&
231
+ testResponse.dkim.results.find(entry => entry && entry.status && entry.status.result === 'pass');
232
+ }
233
+
234
+ if (!mainSig) {
235
+ mainSig = testResponse.dkim && testResponse.dkim.results && testResponse.dkim.results[0];
236
+ }
237
+
238
+ testResponse.mainSig = mainSig || {
239
+ status: {
240
+ result: 'none'
241
+ }
242
+ };
243
+
244
+ if (testResponse.spf && testResponse.spf.status && testResponse.spf.status.comment) {
245
+ testResponse.spf.status.comment = testResponse.spf.status.comment.replace(/^[^:\s]+:s*/, '');
246
+ }
247
+ }
248
+
249
+ if (testResponse) {
250
+ if (testResponse.status === 'success') {
251
+ delete testResponse.status;
252
+ }
253
+ delete testResponse.user;
254
+ }
255
+
256
+ return Object.assign({ success }, testResponse || {});
257
+ } catch (err) {
258
+ request.logger.error({ msg: 'API request failed', err });
259
+ if (Boom.isBoom(err)) {
260
+ throw err;
261
+ }
262
+ let error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
263
+ if (err.code) {
264
+ error.output.payload.code = err.code;
265
+ }
266
+ if (err.details) {
267
+ error.output.payload.details = err.details;
268
+ }
269
+ throw error;
270
+ }
271
+ },
272
+ options: {
273
+ description: 'Check test status',
274
+ notes: 'Check delivery test status',
275
+ tags: ['api', 'Delivery Test'],
276
+
277
+ auth: {
278
+ strategy: 'api-token',
279
+ mode: 'required'
280
+ },
281
+ cors: CORS_CONFIG,
282
+
283
+ validate: {
284
+ options: {
285
+ stripUnknown: false,
286
+ abortEarly: false,
287
+ convert: true
288
+ },
289
+ failAction,
290
+
291
+ params: Joi.object({
292
+ deliveryTest: Joi.string()
293
+ .guid({
294
+ version: ['uuidv4', 'uuidv5']
295
+ })
296
+ .example('6420a6ad-7f82-4e4f-8112-82a9dad1f34d')
297
+ .required()
298
+ .description('Test ID')
299
+ }).label('DeliveryCheckParams')
300
+ },
301
+
302
+ response: {
303
+ schema: Joi.object({
304
+ success: Joi.boolean().example(true).description('Was the test completed').label('ResponseDeliveryCheckSuccess'),
305
+ dkim: Joi.object().unknown().description('DKIM results').label('DkimResults'),
306
+ spf: Joi.object().unknown().description('SPF results').label('SpfResults'),
307
+ dmarc: Joi.object().unknown().description('DMARC results').label('DmarcResults'),
308
+ bimi: Joi.object().unknown().description('BIMI results').label('BimiResults'),
309
+ arc: Joi.object().unknown().description('ARC results').label('ArcResults'),
310
+ mainSig: Joi.object()
311
+ .unknown()
312
+ .description('Primary DKIM signature. `status.aligned` should be set, otherwise DKIM check should not be considered as passed.')
313
+ .label('MainSignature')
314
+ }).label('DeliveryCheckResponse'),
315
+ failAction: 'log'
316
+ }
317
+ }
318
+ });
319
+ }
320
+
321
+ module.exports = init;
@@ -8,18 +8,7 @@ const { failAction } = require('../tools');
8
8
  const { accountIdSchema, exportRequestSchema, exportStatusSchema, exportListSchema, exportIdSchema } = require('../schemas');
9
9
  const getSecret = require('../get-secret');
10
10
  const { createDecryptStream } = require('../stream-encrypt');
11
-
12
- function handleError(request, err) {
13
- request.logger.error({ msg: 'API request failed', err });
14
- if (Boom.isBoom(err)) {
15
- throw err;
16
- }
17
- const error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
18
- if (err.code) {
19
- error.output.payload.code = err.code;
20
- }
21
- throw error;
22
- }
11
+ const { handleError } = require('./route-helpers');
23
12
 
24
13
  async function init(args) {
25
14
  const { server, CORS_CONFIG } = args;