emailengine-app 2.68.0 → 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.
- package/.github/codeql/codeql-config.yml +16 -0
- package/.github/workflows/codeql.yml +102 -0
- package/.github/workflows/deploy.yml +8 -0
- package/.github/workflows/release.yaml +4 -0
- package/.github/workflows/test.yml +3 -0
- package/CHANGELOG.md +49 -0
- package/SECURITY.md +80 -0
- package/SECURITY.txt +27 -0
- package/config/default.toml +2 -0
- package/data/google-crawlers.json +13 -1
- package/lib/account.js +62 -25
- package/lib/api-routes/account-routes.js +493 -75
- package/lib/api-routes/blocklist-routes.js +337 -0
- package/lib/api-routes/delivery-test-routes.js +321 -0
- package/lib/api-routes/export-routes.js +1 -12
- package/lib/api-routes/gateway-routes.js +376 -0
- package/lib/api-routes/license-routes.js +142 -0
- package/lib/api-routes/mailbox-routes.js +318 -0
- package/lib/api-routes/message-routes.js +21 -129
- package/lib/api-routes/oauth2-app-routes.js +631 -0
- package/lib/api-routes/outbox-routes.js +173 -0
- package/lib/api-routes/pubsub-routes.js +98 -0
- package/lib/api-routes/route-helpers.js +45 -0
- package/lib/api-routes/settings-routes.js +331 -0
- package/lib/api-routes/stats-routes.js +77 -0
- package/lib/api-routes/submit-routes.js +472 -0
- package/lib/api-routes/template-routes.js +7 -55
- package/lib/api-routes/token-routes.js +297 -0
- package/lib/api-routes/webhook-route-routes.js +152 -0
- package/lib/email-client/gmail-client.js +14 -0
- package/lib/email-client/imap/mailbox.js +34 -11
- package/lib/email-client/imap/subconnection.js +20 -12
- package/lib/email-client/imap/sync-operations.js +130 -2
- package/lib/email-client/imap-client.js +116 -58
- package/lib/email-client/outlook-client.js +85 -13
- package/lib/export.js +60 -19
- package/lib/imapproxy/imap-core/lib/commands/starttls.js +18 -0
- package/lib/imapproxy/imap-core/lib/imap-command.js +7 -2
- package/lib/imapproxy/imap-core/lib/imap-connection.js +113 -23
- package/lib/imapproxy/imap-core/lib/imap-server.js +25 -1
- package/lib/imapproxy/imap-core/lib/imap-stream.js +26 -0
- package/lib/imapproxy/imap-server.js +92 -29
- package/lib/message-port-stream.js +113 -16
- package/lib/reject-worker-calls.js +42 -0
- package/lib/routes-ui.js +37 -8778
- package/lib/schemas.js +26 -1
- package/lib/tools.js +73 -0
- package/lib/ui-routes/account-routes.js +40 -210
- package/lib/ui-routes/admin-config-routes.js +913 -487
- package/lib/ui-routes/admin-entities-routes.js +1 -0
- package/lib/ui-routes/auth-routes.js +1339 -0
- package/lib/ui-routes/dashboard-routes.js +188 -0
- package/lib/ui-routes/document-store-routes.js +800 -0
- package/lib/ui-routes/export-routes.js +217 -0
- package/lib/ui-routes/internals-routes.js +354 -0
- package/lib/ui-routes/network-config-routes.js +759 -0
- package/lib/ui-routes/{oauth-routes.js → oauth-config-routes.js} +371 -91
- package/lib/ui-routes/route-helpers.js +316 -0
- package/lib/ui-routes/smtp-test-routes.js +236 -0
- package/lib/ui-routes/unsubscribe-routes.js +234 -0
- package/lib/webhook-request.js +36 -0
- package/package.json +17 -17
- package/sbom.json +1 -1
- package/server.js +217 -19
- package/static/licenses.html +52 -182
- package/translations/messages.pot +131 -151
- package/views/dashboard.hbs +7 -26
- package/views/internals/index.hbs +15 -0
- package/views/tokens/index.hbs +9 -0
- package/workers/api.js +198 -4401
- package/workers/export.js +87 -54
- package/workers/imap.js +29 -13
- package/workers/submit.js +20 -11
- 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;
|