emailengine-app 2.69.0 → 2.70.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/workflows/deploy.yml +6 -3
- package/.github/workflows/release.yaml +2 -0
- package/CHANGELOG.md +19 -0
- package/Gruntfile.js +3 -1
- package/data/google-crawlers.json +1 -1
- package/getswagger.sh +40 -4
- package/gettext-extract.js +163 -0
- package/lib/account.js +73 -47
- package/lib/api-routes/account-routes.js +231 -71
- package/lib/api-routes/blocklist-routes.js +25 -18
- package/lib/api-routes/chat-routes.js +32 -14
- package/lib/api-routes/delivery-test-routes.js +30 -5
- package/lib/api-routes/export-routes.js +27 -2
- package/lib/api-routes/gateway-routes.js +63 -12
- package/lib/api-routes/license-routes.js +18 -4
- package/lib/api-routes/mailbox-routes.js +33 -7
- package/lib/api-routes/message-routes.js +200 -58
- package/lib/api-routes/oauth2-app-routes.js +90 -24
- package/lib/api-routes/outbox-routes.js +16 -4
- package/lib/api-routes/pubsub-routes.js +8 -4
- package/lib/api-routes/route-helpers.js +14 -1
- package/lib/api-routes/settings-routes.js +51 -25
- package/lib/api-routes/stats-routes.js +37 -3
- package/lib/api-routes/submit-routes.js +31 -42
- package/lib/api-routes/template-routes.js +54 -21
- package/lib/api-routes/token-routes.js +67 -67
- package/lib/api-routes/webhook-route-routes.js +37 -8
- package/lib/autodetect-imap-settings.js +0 -2
- package/lib/consts.js +5 -0
- package/lib/email-client/base-client.js +28 -6
- package/lib/email-client/gmail-client.js +119 -112
- package/lib/email-client/imap/subconnection.js +0 -1
- package/lib/email-client/imap/sync-operations.js +1 -1
- package/lib/email-client/imap-client.js +36 -17
- package/lib/email-client/notification-handler.js +1 -4
- package/lib/email-client/outlook-client.js +49 -62
- package/lib/export.js +37 -1
- package/lib/feature-flags.js +2 -2
- package/lib/gateway.js +4 -9
- package/lib/get-raw-email.js +5 -5
- package/lib/imapproxy/imap-core/lib/imap-connection.js +0 -1
- package/lib/logger.js +24 -21
- package/lib/metrics-collector.js +0 -2
- package/lib/oauth2-apps.js +13 -4
- package/lib/outbox.js +24 -40
- package/lib/redis-operations.js +1 -1
- package/lib/schemas.js +403 -83
- package/lib/sentry.js +139 -0
- package/lib/settings.js +9 -3
- package/lib/stream-encrypt.js +1 -1
- package/lib/templates.js +1 -1
- package/lib/tokens.js +5 -3
- package/lib/tools.js +2 -4
- package/lib/ui-routes/account-routes.js +7 -4
- package/lib/ui-routes/admin-config-routes.js +16 -3
- package/lib/ui-routes/oauth-config-routes.js +0 -2
- package/lib/ui-routes/route-helpers.js +0 -2
- package/lib/ui-routes/unsubscribe-routes.js +0 -2
- package/lib/webhooks.js +8 -4
- package/package.json +9 -8
- package/sbom.json +1 -1
- package/server.js +8 -23
- package/static/licenses.html +152 -292
- package/translations/messages.pot +122 -122
- package/update-info.sh +19 -1
- package/views/config/logging.hbs +48 -0
- package/workers/api.js +11 -32
- package/workers/documents.js +2 -22
- package/workers/export.js +16 -50
- package/workers/imap-proxy.js +3 -23
- package/workers/imap.js +2 -22
- package/workers/smtp.js +2 -22
- package/workers/submit.js +6 -24
- package/workers/webhooks.js +2 -22
|
@@ -6,9 +6,12 @@ const getSecret = require('../get-secret');
|
|
|
6
6
|
const { templates } = require('../templates');
|
|
7
7
|
const Joi = require('joi');
|
|
8
8
|
const { failAction } = require('../tools');
|
|
9
|
-
const { handleError } = require('./route-helpers');
|
|
9
|
+
const { handleError, throwNotFound } = require('./route-helpers');
|
|
10
10
|
|
|
11
|
-
const { templateSchemas, accountIdSchema } = require('../schemas');
|
|
11
|
+
const { templateSchemas, accountIdSchema, errorResponses } = require('../schemas');
|
|
12
|
+
|
|
13
|
+
// Template owner field shared by all template response schemas
|
|
14
|
+
const templateAccountIdSchema = accountIdSchema.required().allow(null).description('Account ID. Null for public templates');
|
|
12
15
|
|
|
13
16
|
async function init(args) {
|
|
14
17
|
const { server, call, CORS_CONFIG } = args;
|
|
@@ -52,7 +55,11 @@ async function init(args) {
|
|
|
52
55
|
notes: 'Create a new stored template. Templates can be used when sending emails as the content of the message.',
|
|
53
56
|
tags: ['api', 'Templates'],
|
|
54
57
|
|
|
55
|
-
plugins: {
|
|
58
|
+
plugins: {
|
|
59
|
+
'hapi-swagger': {
|
|
60
|
+
responses: errorResponses(400, 401, 403, 404, 429, 500)
|
|
61
|
+
}
|
|
62
|
+
},
|
|
56
63
|
|
|
57
64
|
auth: {
|
|
58
65
|
strategy: 'api-token',
|
|
@@ -104,7 +111,7 @@ async function init(args) {
|
|
|
104
111
|
response: {
|
|
105
112
|
schema: Joi.object({
|
|
106
113
|
created: Joi.boolean().description('Was the template created or not'),
|
|
107
|
-
account:
|
|
114
|
+
account: templateAccountIdSchema,
|
|
108
115
|
id: Joi.string().max(256).required().example('example').description('Template ID')
|
|
109
116
|
}).label('CreateTemplateResponse'),
|
|
110
117
|
failAction: 'log'
|
|
@@ -136,7 +143,11 @@ async function init(args) {
|
|
|
136
143
|
notes: 'Update a stored template.',
|
|
137
144
|
tags: ['api', 'Templates'],
|
|
138
145
|
|
|
139
|
-
plugins: {
|
|
146
|
+
plugins: {
|
|
147
|
+
'hapi-swagger': {
|
|
148
|
+
responses: errorResponses(400, 401, 403, 404, 429, 500)
|
|
149
|
+
}
|
|
150
|
+
},
|
|
140
151
|
|
|
141
152
|
auth: {
|
|
142
153
|
strategy: 'api-token',
|
|
@@ -182,7 +193,7 @@ async function init(args) {
|
|
|
182
193
|
response: {
|
|
183
194
|
schema: Joi.object({
|
|
184
195
|
updated: Joi.boolean().description('Was the template updated or not'),
|
|
185
|
-
account:
|
|
196
|
+
account: templateAccountIdSchema,
|
|
186
197
|
id: Joi.string().max(256).required().example('example').description('Template ID')
|
|
187
198
|
}).label('UpdateTemplateResponse'),
|
|
188
199
|
failAction: 'log'
|
|
@@ -213,7 +224,11 @@ async function init(args) {
|
|
|
213
224
|
notes: 'Lists stored templates for the account',
|
|
214
225
|
tags: ['api', 'Templates'],
|
|
215
226
|
|
|
216
|
-
plugins: {
|
|
227
|
+
plugins: {
|
|
228
|
+
'hapi-swagger': {
|
|
229
|
+
responses: errorResponses(400, 401, 403, 404, 429, 500)
|
|
230
|
+
}
|
|
231
|
+
},
|
|
217
232
|
|
|
218
233
|
auth: {
|
|
219
234
|
strategy: 'api-token',
|
|
@@ -246,7 +261,7 @@ async function init(args) {
|
|
|
246
261
|
|
|
247
262
|
response: {
|
|
248
263
|
schema: Joi.object({
|
|
249
|
-
account:
|
|
264
|
+
account: templateAccountIdSchema,
|
|
250
265
|
total: Joi.number().integer().example(120).description('How many matching entries').label('TotalNumber'),
|
|
251
266
|
page: Joi.number().integer().example(0).description('Current page (0-based index)').label('PageNumber'),
|
|
252
267
|
pages: Joi.number().integer().example(24).description('Total page count').label('PagesNumber'),
|
|
@@ -284,7 +299,11 @@ async function init(args) {
|
|
|
284
299
|
|
|
285
300
|
async handler(request) {
|
|
286
301
|
try {
|
|
287
|
-
|
|
302
|
+
let templateData = await templates.get(request.params.template);
|
|
303
|
+
if (!templateData) {
|
|
304
|
+
throwNotFound();
|
|
305
|
+
}
|
|
306
|
+
return templateData;
|
|
288
307
|
} catch (err) {
|
|
289
308
|
handleError(request, err);
|
|
290
309
|
}
|
|
@@ -295,7 +314,11 @@ async function init(args) {
|
|
|
295
314
|
notes: 'Retrieve template content and information',
|
|
296
315
|
tags: ['api', 'Templates'],
|
|
297
316
|
|
|
298
|
-
plugins: {
|
|
317
|
+
plugins: {
|
|
318
|
+
'hapi-swagger': {
|
|
319
|
+
responses: errorResponses(400, 401, 403, 404, 429, 500)
|
|
320
|
+
}
|
|
321
|
+
},
|
|
299
322
|
|
|
300
323
|
auth: {
|
|
301
324
|
strategy: 'api-token',
|
|
@@ -317,7 +340,7 @@ async function init(args) {
|
|
|
317
340
|
|
|
318
341
|
response: {
|
|
319
342
|
schema: Joi.object({
|
|
320
|
-
account:
|
|
343
|
+
account: templateAccountIdSchema,
|
|
321
344
|
id: Joi.string().max(256).required().example('AAABgS-UcAYAAAABAA').description('Template ID'),
|
|
322
345
|
name: Joi.string().max(256).example('Transaction receipt').description('Name of the template').label('TemplateName').required(),
|
|
323
346
|
description: Joi.string()
|
|
@@ -337,12 +360,7 @@ async function init(args) {
|
|
|
337
360
|
subject: templateSchemas.subject,
|
|
338
361
|
text: templateSchemas.text,
|
|
339
362
|
html: templateSchemas.html,
|
|
340
|
-
previewText: templateSchemas.previewText
|
|
341
|
-
format: Joi.string()
|
|
342
|
-
.valid('html', 'markdown')
|
|
343
|
-
.default('html')
|
|
344
|
-
.description('Markup language for HTML ("html" or "markdown")')
|
|
345
|
-
.label('TemplateContentFormat')
|
|
363
|
+
previewText: templateSchemas.previewText
|
|
346
364
|
}).label('RequestTemplateContent')
|
|
347
365
|
}).label('AccountTemplateResponse'),
|
|
348
366
|
failAction: 'log'
|
|
@@ -366,7 +384,11 @@ async function init(args) {
|
|
|
366
384
|
notes: 'Delete a stored template',
|
|
367
385
|
tags: ['api', 'Templates'],
|
|
368
386
|
|
|
369
|
-
plugins: {
|
|
387
|
+
plugins: {
|
|
388
|
+
'hapi-swagger': {
|
|
389
|
+
responses: errorResponses(400, 401, 403, 429, 500)
|
|
390
|
+
}
|
|
391
|
+
},
|
|
370
392
|
|
|
371
393
|
auth: {
|
|
372
394
|
strategy: 'api-token',
|
|
@@ -390,7 +412,7 @@ async function init(args) {
|
|
|
390
412
|
response: {
|
|
391
413
|
schema: Joi.object({
|
|
392
414
|
deleted: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Was the template deleted'),
|
|
393
|
-
account:
|
|
415
|
+
account: templateAccountIdSchema,
|
|
394
416
|
id: Joi.string().max(256).required().example('AAABgS-UcAYAAAABAA').description('Template ID')
|
|
395
417
|
}).label('DeleteTemplateResponse'),
|
|
396
418
|
failAction: 'log'
|
|
@@ -406,6 +428,13 @@ async function init(args) {
|
|
|
406
428
|
let accountObject = new Account({ redis, account: request.params.account, call, secret: await getSecret() });
|
|
407
429
|
|
|
408
430
|
try {
|
|
431
|
+
if (!request.query.force) {
|
|
432
|
+
let err = new Error('Set the force=true query parameter to flush account templates');
|
|
433
|
+
err.code = 'ForceRequired';
|
|
434
|
+
err.statusCode = 400;
|
|
435
|
+
throw err;
|
|
436
|
+
}
|
|
437
|
+
|
|
409
438
|
// throws if account does not exist
|
|
410
439
|
await accountObject.loadAccountData();
|
|
411
440
|
|
|
@@ -419,7 +448,11 @@ async function init(args) {
|
|
|
419
448
|
notes: 'Delete all stored templates for an account',
|
|
420
449
|
tags: ['api', 'Templates'],
|
|
421
450
|
|
|
422
|
-
plugins: {
|
|
451
|
+
plugins: {
|
|
452
|
+
'hapi-swagger': {
|
|
453
|
+
responses: errorResponses(400, 401, 403, 404, 429, 500)
|
|
454
|
+
}
|
|
455
|
+
},
|
|
423
456
|
|
|
424
457
|
auth: {
|
|
425
458
|
strategy: 'api-token',
|
|
@@ -451,7 +484,7 @@ async function init(args) {
|
|
|
451
484
|
|
|
452
485
|
response: {
|
|
453
486
|
schema: Joi.object({
|
|
454
|
-
|
|
487
|
+
flushed: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Were the templates flushed'),
|
|
455
488
|
account: accountIdSchema.required()
|
|
456
489
|
}).label('FlushTemplatesResponse'),
|
|
457
490
|
failAction: 'log'
|
|
@@ -7,7 +7,50 @@ const getSecret = require('../get-secret');
|
|
|
7
7
|
const tokens = require('../tokens');
|
|
8
8
|
const { failAction } = require('../tools');
|
|
9
9
|
const { handleError } = require('./route-helpers');
|
|
10
|
-
const { accountIdSchema, tokenRestrictionsSchema, ipSchema, tokenIdSchema } = require('../schemas');
|
|
10
|
+
const { accountIdSchema, tokenRestrictionsSchema, ipSchema, tokenIdSchema, errorResponses } = require('../schemas');
|
|
11
|
+
|
|
12
|
+
const tokenAccessSchema = Joi.object({
|
|
13
|
+
time: Joi.date().iso().allow(null).example('2021-02-17T13:43:18.860Z').description('Last time this token was used. Null if the token has never been used'),
|
|
14
|
+
ip: ipSchema.allow(null).description('IP address of the last request that used this token. Null if the address was not available')
|
|
15
|
+
})
|
|
16
|
+
.unknown()
|
|
17
|
+
.description('Token usage information')
|
|
18
|
+
.label('TokenAccess');
|
|
19
|
+
|
|
20
|
+
const tokenScopesSchema = Joi.array()
|
|
21
|
+
.items(Joi.string().example('api').label('TokenScopeEntry'))
|
|
22
|
+
.description('Scopes this token is valid for')
|
|
23
|
+
.label('TokenScopes');
|
|
24
|
+
|
|
25
|
+
const tokenMetadataSchema = Joi.string()
|
|
26
|
+
.empty('')
|
|
27
|
+
.max(1024 * 1024)
|
|
28
|
+
.custom((value, helpers) => {
|
|
29
|
+
try {
|
|
30
|
+
// check if parsing fails
|
|
31
|
+
JSON.parse(value);
|
|
32
|
+
return value;
|
|
33
|
+
} catch (err) {
|
|
34
|
+
return helpers.message('Metadata must be a valid JSON string');
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
.example('{"example": "value"}')
|
|
38
|
+
.description('Related metadata in JSON format')
|
|
39
|
+
.label('JsonMetaData');
|
|
40
|
+
|
|
41
|
+
// Both token listings (root and account) return the same item shape from tokens.list(),
|
|
42
|
+
// the account listing just adds the account ID
|
|
43
|
+
const tokenListItemFields = {
|
|
44
|
+
description: Joi.string().empty('').trim().max(1024).example('Token description').description('Token description'),
|
|
45
|
+
metadata: tokenMetadataSchema,
|
|
46
|
+
ip: ipSchema.description('IP address of the requester').label('TokenIP'),
|
|
47
|
+
remoteAddress: ipSchema.description('IP address of the client that created the token').label('TokenRemoteAddress'),
|
|
48
|
+
scopes: tokenScopesSchema,
|
|
49
|
+
restrictions: tokenRestrictionsSchema,
|
|
50
|
+
created: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this token was created'),
|
|
51
|
+
access: tokenAccessSchema,
|
|
52
|
+
id: tokenIdSchema
|
|
53
|
+
};
|
|
11
54
|
|
|
12
55
|
async function init(args) {
|
|
13
56
|
const { server, call, CORS_CONFIG } = args;
|
|
@@ -42,7 +85,11 @@ async function init(args) {
|
|
|
42
85
|
notes: 'Provisions a new access token for an account',
|
|
43
86
|
tags: ['api', 'Access Tokens'],
|
|
44
87
|
|
|
45
|
-
plugins: {
|
|
88
|
+
plugins: {
|
|
89
|
+
'hapi-swagger': {
|
|
90
|
+
responses: errorResponses(400, 401, 403, 404, 429, 500)
|
|
91
|
+
}
|
|
92
|
+
},
|
|
46
93
|
|
|
47
94
|
auth: {
|
|
48
95
|
strategy: 'api-token',
|
|
@@ -73,21 +120,7 @@ async function init(args) {
|
|
|
73
120
|
)
|
|
74
121
|
.label('Scopes'),
|
|
75
122
|
|
|
76
|
-
metadata:
|
|
77
|
-
.empty('')
|
|
78
|
-
.max(1024 * 1024)
|
|
79
|
-
.custom((value, helpers) => {
|
|
80
|
-
try {
|
|
81
|
-
// check if parsing fails
|
|
82
|
-
JSON.parse(value);
|
|
83
|
-
return value;
|
|
84
|
-
} catch (err) {
|
|
85
|
-
return helpers.message('Metadata must be a valid JSON string');
|
|
86
|
-
}
|
|
87
|
-
})
|
|
88
|
-
.example('{"example": "value"}')
|
|
89
|
-
.description('Related metadata in JSON format')
|
|
90
|
-
.label('JsonMetaData'),
|
|
123
|
+
metadata: tokenMetadataSchema,
|
|
91
124
|
|
|
92
125
|
restrictions: tokenRestrictionsSchema,
|
|
93
126
|
|
|
@@ -120,7 +153,11 @@ async function init(args) {
|
|
|
120
153
|
notes: 'Delete an access token',
|
|
121
154
|
tags: ['api', 'Access Tokens'],
|
|
122
155
|
|
|
123
|
-
plugins: {
|
|
156
|
+
plugins: {
|
|
157
|
+
'hapi-swagger': {
|
|
158
|
+
responses: errorResponses(400, 401, 403, 429, 500)
|
|
159
|
+
}
|
|
160
|
+
},
|
|
124
161
|
|
|
125
162
|
auth: {
|
|
126
163
|
strategy: 'api-token',
|
|
@@ -168,7 +205,11 @@ async function init(args) {
|
|
|
168
205
|
notes: 'Lists access tokens registered for root access',
|
|
169
206
|
tags: ['api', 'Access Tokens'],
|
|
170
207
|
|
|
171
|
-
plugins: {
|
|
208
|
+
plugins: {
|
|
209
|
+
'hapi-swagger': {
|
|
210
|
+
responses: errorResponses(401, 403, 429, 500)
|
|
211
|
+
}
|
|
212
|
+
},
|
|
172
213
|
|
|
173
214
|
auth: {
|
|
174
215
|
strategy: 'api-token',
|
|
@@ -187,31 +228,7 @@ async function init(args) {
|
|
|
187
228
|
|
|
188
229
|
response: {
|
|
189
230
|
schema: Joi.object({
|
|
190
|
-
tokens: Joi.array()
|
|
191
|
-
.items(
|
|
192
|
-
Joi.object({
|
|
193
|
-
account: accountIdSchema.required(),
|
|
194
|
-
description: Joi.string().empty('').trim().max(1024).required().example('Token description').description('Token description'),
|
|
195
|
-
metadata: Joi.string()
|
|
196
|
-
.empty('')
|
|
197
|
-
.max(1024 * 1024)
|
|
198
|
-
.custom((value, helpers) => {
|
|
199
|
-
try {
|
|
200
|
-
// check if parsing fails
|
|
201
|
-
JSON.parse(value);
|
|
202
|
-
return value;
|
|
203
|
-
} catch (err) {
|
|
204
|
-
return helpers.message('Metadata must be a valid JSON string');
|
|
205
|
-
}
|
|
206
|
-
})
|
|
207
|
-
.example('{"example": "value"}')
|
|
208
|
-
.description('Related metadata in JSON format')
|
|
209
|
-
.label('JsonMetaData'),
|
|
210
|
-
ip: ipSchema.description('IP address of the requester').label('TokenIP'),
|
|
211
|
-
id: tokenIdSchema
|
|
212
|
-
}).label('RootTokensItem')
|
|
213
|
-
)
|
|
214
|
-
.label('RootTokensEntries')
|
|
231
|
+
tokens: Joi.array().items(Joi.object(tokenListItemFields).label('RootTokensItem')).label('RootTokensEntries')
|
|
215
232
|
}).label('RootTokensResponse'),
|
|
216
233
|
failAction: 'log'
|
|
217
234
|
}
|
|
@@ -236,7 +253,11 @@ async function init(args) {
|
|
|
236
253
|
notes: 'Lists access tokens registered for an account',
|
|
237
254
|
tags: ['api', 'Access Tokens'],
|
|
238
255
|
|
|
239
|
-
plugins: {
|
|
256
|
+
plugins: {
|
|
257
|
+
'hapi-swagger': {
|
|
258
|
+
responses: errorResponses(400, 401, 403, 429, 500)
|
|
259
|
+
}
|
|
260
|
+
},
|
|
240
261
|
|
|
241
262
|
auth: {
|
|
242
263
|
strategy: 'api-token',
|
|
@@ -262,28 +283,7 @@ async function init(args) {
|
|
|
262
283
|
.items(
|
|
263
284
|
Joi.object({
|
|
264
285
|
account: accountIdSchema.required(),
|
|
265
|
-
|
|
266
|
-
metadata: Joi.string()
|
|
267
|
-
.empty('')
|
|
268
|
-
.max(1024 * 1024)
|
|
269
|
-
.custom((value, helpers) => {
|
|
270
|
-
try {
|
|
271
|
-
// check if parsing fails
|
|
272
|
-
JSON.parse(value);
|
|
273
|
-
return value;
|
|
274
|
-
} catch (err) {
|
|
275
|
-
return helpers.message('Metadata must be a valid JSON string');
|
|
276
|
-
}
|
|
277
|
-
})
|
|
278
|
-
.example('{"example": "value"}')
|
|
279
|
-
.description('Related metadata in JSON format')
|
|
280
|
-
.label('JsonMetaData'),
|
|
281
|
-
|
|
282
|
-
restrictions: tokenRestrictionsSchema,
|
|
283
|
-
|
|
284
|
-
ip: ipSchema.description('IP address of the requester').label('TokenIP'),
|
|
285
|
-
|
|
286
|
-
id: tokenIdSchema
|
|
286
|
+
...tokenListItemFields
|
|
287
287
|
}).label('AccountTokensItem')
|
|
288
288
|
)
|
|
289
289
|
.label('AccountTokensEntries')
|
|
@@ -3,8 +3,20 @@
|
|
|
3
3
|
const Joi = require('joi');
|
|
4
4
|
const { webhooks: Webhooks } = require('../webhooks');
|
|
5
5
|
const { failAction } = require('../tools');
|
|
6
|
-
const { handleError } = require('./route-helpers');
|
|
7
|
-
const { settingsSchema } = require('../schemas');
|
|
6
|
+
const { handleError, throwNotFound } = require('./route-helpers');
|
|
7
|
+
const { settingsSchema, errorResponses } = require('../schemas');
|
|
8
|
+
|
|
9
|
+
const webhookErrorFlagSchema = Joi.object({
|
|
10
|
+
message: Joi.string().example('Request failed with status 500').description('Error message from the last failed delivery')
|
|
11
|
+
})
|
|
12
|
+
.unknown()
|
|
13
|
+
.allow(null)
|
|
14
|
+
.description('Information about the last webhook delivery error. Null if no errors have been registered')
|
|
15
|
+
.label('WebhookRouteErrorFlag');
|
|
16
|
+
|
|
17
|
+
const webhookCustomHeadersSchema = settingsSchema.webhooksCustomHeaders
|
|
18
|
+
.description('Custom HTTP headers added to webhook requests for this route')
|
|
19
|
+
.label('WebhookRouteCustomHeaders');
|
|
8
20
|
|
|
9
21
|
async function init(args) {
|
|
10
22
|
const { server, CORS_CONFIG } = args;
|
|
@@ -26,7 +38,11 @@ async function init(args) {
|
|
|
26
38
|
notes: 'List custom webhook routes',
|
|
27
39
|
tags: ['api', 'Webhooks'],
|
|
28
40
|
|
|
29
|
-
plugins: {
|
|
41
|
+
plugins: {
|
|
42
|
+
'hapi-swagger': {
|
|
43
|
+
responses: errorResponses(400, 401, 403, 429, 500)
|
|
44
|
+
}
|
|
45
|
+
},
|
|
30
46
|
|
|
31
47
|
auth: {
|
|
32
48
|
strategy: 'api-token',
|
|
@@ -76,7 +92,9 @@ async function init(args) {
|
|
|
76
92
|
updated: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this route was last updated'),
|
|
77
93
|
enabled: Joi.boolean().example(true).description('Is the route enabled').label('WebhookRouteEnabled'),
|
|
78
94
|
targetUrl: settingsSchema.webhooks,
|
|
79
|
-
tcount: Joi.number().integer().example(123).description('How many times this route has been applied')
|
|
95
|
+
tcount: Joi.number().integer().example(123).description('How many times this route has been applied'),
|
|
96
|
+
webhookErrorFlag: webhookErrorFlagSchema,
|
|
97
|
+
customHeaders: webhookCustomHeadersSchema
|
|
80
98
|
}).label('WebhookRoutesListEntry')
|
|
81
99
|
)
|
|
82
100
|
.label('WebhookRoutesList')
|
|
@@ -92,7 +110,11 @@ async function init(args) {
|
|
|
92
110
|
|
|
93
111
|
async handler(request) {
|
|
94
112
|
try {
|
|
95
|
-
|
|
113
|
+
let webhookRouteData = await Webhooks.get(request.params.webhookRoute);
|
|
114
|
+
if (!webhookRouteData) {
|
|
115
|
+
throwNotFound();
|
|
116
|
+
}
|
|
117
|
+
return webhookRouteData;
|
|
96
118
|
} catch (err) {
|
|
97
119
|
handleError(request, err);
|
|
98
120
|
}
|
|
@@ -103,7 +125,11 @@ async function init(args) {
|
|
|
103
125
|
notes: 'Retrieve webhook route content and information',
|
|
104
126
|
tags: ['api', 'Webhooks'],
|
|
105
127
|
|
|
106
|
-
plugins: {
|
|
128
|
+
plugins: {
|
|
129
|
+
'hapi-swagger': {
|
|
130
|
+
responses: errorResponses(400, 401, 403, 404, 429, 500)
|
|
131
|
+
}
|
|
132
|
+
},
|
|
107
133
|
|
|
108
134
|
auth: {
|
|
109
135
|
strategy: 'api-token',
|
|
@@ -138,9 +164,12 @@ async function init(args) {
|
|
|
138
164
|
enabled: Joi.boolean().example(true).description('Is the route enabled').label('WebhookRouteEnabled'),
|
|
139
165
|
targetUrl: settingsSchema.webhooks,
|
|
140
166
|
tcount: Joi.number().integer().example(123).description('How many times this route has been applied'),
|
|
167
|
+
v: Joi.number().integer().example(1).description('Internal version counter, increased on every update'),
|
|
168
|
+
webhookErrorFlag: webhookErrorFlagSchema,
|
|
169
|
+
customHeaders: webhookCustomHeadersSchema,
|
|
141
170
|
content: Joi.object({
|
|
142
|
-
fn: Joi.string().example('return true;').description('Filter function'),
|
|
143
|
-
map: Joi.string().example('payload.ts = Date.now(); return payload;').description('Mapping function')
|
|
171
|
+
fn: Joi.string().allow(null).example('return true;').description('Filter function. Null if not set'),
|
|
172
|
+
map: Joi.string().allow(null).example('payload.ts = Date.now(); return payload;').description('Mapping function. Null if not set')
|
|
144
173
|
}).label('WebhookRouteContent')
|
|
145
174
|
}).label('WebhookRouteResponse'),
|
|
146
175
|
failAction: 'log'
|
package/lib/consts.js
CHANGED
|
@@ -107,6 +107,11 @@ module.exports = {
|
|
|
107
107
|
|
|
108
108
|
DEFAULT_MAX_LOG_LINES: 10000,
|
|
109
109
|
|
|
110
|
+
// Shared Sentry instance run by the EmailEngine developers. Used as the error
|
|
111
|
+
// reporting target when error reporting is enabled without a custom DSN. A DSN
|
|
112
|
+
// is a write-only credential, it can only be used to submit events.
|
|
113
|
+
COMMUNITY_SENTRY_DSN: 'https://bdd958f3e813a488904b0f254e0bb8a8@sentry.emailengine.dev/3',
|
|
114
|
+
|
|
110
115
|
PDKDF2_ITERATIONS: 600000,
|
|
111
116
|
PDKDF2_SALT_SIZE: 16,
|
|
112
117
|
PDKDF2_DIGEST: 'sha256', // 'sha512', 'sha256' or 'sha1'
|
|
@@ -240,6 +240,33 @@ class BaseClient {
|
|
|
240
240
|
return rid;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Normalizes an IMAP-style flag update request (add/delete/set) into plain add/delete
|
|
245
|
+
* flag lists. If `set` is present then it replaces the state of the flags the backend
|
|
246
|
+
* supports and add/delete are ignored, matching the IMAP backend behavior.
|
|
247
|
+
* @param {Object} [flags] - Flag update request ({ add, delete, set })
|
|
248
|
+
* @param {string[]} supportedFlags - Flags the backend can represent
|
|
249
|
+
* @returns {Object} Normalized flag lists ({ add: string[], delete: string[] })
|
|
250
|
+
*/
|
|
251
|
+
normalizeFlagUpdates(flags, supportedFlags) {
|
|
252
|
+
if (flags?.set) {
|
|
253
|
+
// If set exists then ignore add/delete calls
|
|
254
|
+
let setFlags = [].concat(flags.set);
|
|
255
|
+
let addFlags = [];
|
|
256
|
+
let deleteFlags = [];
|
|
257
|
+
for (let flag of supportedFlags) {
|
|
258
|
+
if (setFlags.includes(flag)) {
|
|
259
|
+
addFlags.push(flag);
|
|
260
|
+
} else {
|
|
261
|
+
deleteFlags.push(flag);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return { add: addFlags, delete: deleteFlags };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return { add: [].concat(flags?.add || []), delete: [].concat(flags?.delete || []) };
|
|
268
|
+
}
|
|
269
|
+
|
|
243
270
|
// Redis key generators for different data types
|
|
244
271
|
|
|
245
272
|
getAccountKey() {
|
|
@@ -721,7 +748,7 @@ class BaseClient {
|
|
|
721
748
|
}
|
|
722
749
|
|
|
723
750
|
// use existing response
|
|
724
|
-
switch (idempotencyData
|
|
751
|
+
switch (idempotencyData?.status) {
|
|
725
752
|
case 'completed':
|
|
726
753
|
// Return cached result
|
|
727
754
|
idempotencyData.returnValue = Object.assign({}, idempotencyData.result, {
|
|
@@ -3194,11 +3221,6 @@ class BaseClient {
|
|
|
3194
3221
|
async handleSubmitError(err, context) {
|
|
3195
3222
|
const { smtpSettings, networkRouting, gatewayData, gatewayObject, data, jobData, queueId, envelope } = context;
|
|
3196
3223
|
|
|
3197
|
-
// Handle permanent failures
|
|
3198
|
-
if (err.responseCode >= 500 && jobData.opts?.attempts <= jobData.attemptsMade) {
|
|
3199
|
-
jobData.nextAttempt = false;
|
|
3200
|
-
}
|
|
3201
|
-
|
|
3202
3224
|
// Build SMTP status from error
|
|
3203
3225
|
const smtpStatus = SmtpErrorBuilder.buildStatus(err, smtpSettings, networkRouting);
|
|
3204
3226
|
|