emailengine-app 2.69.0 → 2.71.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/.github/workflows/test.yml +73 -12
- package/.ncurc.js +3 -3
- package/CHANGELOG.md +37 -0
- package/Gruntfile.js +21 -23
- package/bin/emailengine.js +8 -1
- package/config/default.toml +5 -0
- package/config/test.toml +5 -0
- package/data/google-crawlers.json +1 -1
- package/getswagger.sh +44 -4
- package/gettext-extract.js +163 -0
- package/lib/account.js +104 -72
- 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 +291 -145
- 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/document-store.js +22 -1
- package/lib/email-client/base-client.js +31 -8
- package/lib/email-client/gmail-client.js +119 -112
- package/lib/email-client/imap/mailbox.js +2 -2
- 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 +3 -6
- package/lib/email-client/outlook-client.js +49 -62
- package/lib/export.js +49 -1
- package/lib/feature-flags.js +8 -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/license-beacon.js +367 -0
- package/lib/logger.js +35 -22
- 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/routes-ui.js +2 -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 +28 -6
- package/lib/ui-routes/account-routes.js +7 -4
- package/lib/ui-routes/admin-config-routes.js +20 -6
- package/lib/ui-routes/document-store-routes.js +7 -1
- 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 +23 -19
- package/sbom.json +1 -1
- package/server.js +38 -31
- package/static/licenses.html +171 -391
- package/translations/de.mo +0 -0
- package/translations/de.po +154 -142
- package/translations/et.mo +0 -0
- package/translations/et.po +129 -131
- package/translations/fr.mo +0 -0
- package/translations/fr.po +133 -136
- package/translations/ja.mo +0 -0
- package/translations/ja.po +126 -129
- package/translations/messages.pot +107 -107
- package/translations/nl.mo +0 -0
- package/translations/nl.po +128 -130
- package/translations/pl.mo +0 -0
- package/translations/pl.po +125 -128
- package/update-info.sh +19 -1
- package/views/config/logging.hbs +48 -0
- package/views/dashboard.hbs +22 -0
- package/workers/api.js +33 -37
- package/workers/documents.js +2 -22
- package/workers/export.js +73 -92
- 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
|
@@ -11,13 +11,52 @@ const {
|
|
|
11
11
|
googleProjectIdSchema,
|
|
12
12
|
googleWorkspaceAccountsSchema,
|
|
13
13
|
googleTopicNameSchema,
|
|
14
|
-
googleSubscriptionNameSchema
|
|
14
|
+
googleSubscriptionNameSchema,
|
|
15
|
+
errorResponses
|
|
15
16
|
} = require('../schemas');
|
|
16
|
-
const { handleError, flattenOAuthAppMeta } = require('./route-helpers');
|
|
17
|
+
const { handleError, throwNotFound, flattenOAuthAppMeta } = require('./route-helpers');
|
|
18
|
+
|
|
19
|
+
// Stored fields that are returned on OAuth2 application objects in addition to the basic fields
|
|
20
|
+
const oauth2AppExtraFields = {
|
|
21
|
+
extraScopes: Joi.array()
|
|
22
|
+
.items(Joi.string().example('https://graph.microsoft.com/.default').label('ExtraScopeEntry'))
|
|
23
|
+
.description('Additional OAuth2 scopes requested for this app')
|
|
24
|
+
.label('AppExtraScopes'),
|
|
25
|
+
skipScopes: Joi.array()
|
|
26
|
+
.items(Joi.string().example('SMTP.Send').label('SkipScopeEntry'))
|
|
27
|
+
.description('OAuth2 scopes excluded from the defaults for this app')
|
|
28
|
+
.label('AppSkipScopes'),
|
|
29
|
+
baseScopes: oauthCreateSchema.baseScopes.description('OAuth2 base scopes for this app').label('AppBaseScopes'),
|
|
30
|
+
pubSubApp: oauthCreateSchema.pubSubApp.description('Cloud Pub/Sub app ID used for Gmail change notifications').label('AppPubSubApp'),
|
|
31
|
+
authMethod: Joi.string().example('serviceKey').description('Authentication method for Gmail service accounts'),
|
|
32
|
+
cloud: Joi.string().example('global').description('Azure cloud type for Outlook OAuth2 applications'),
|
|
33
|
+
tenant: Joi.string()
|
|
34
|
+
.example('f8cdef31-a31e-4b4a-93e4-5f571e91255a')
|
|
35
|
+
.description('Deprecated and unused directory tenant value. Use the authority field instead'),
|
|
36
|
+
externalAccount: Joi.string().example('******').description('External account identifier for 2-legged OAuth2 applications. Actual value is not revealed.'),
|
|
37
|
+
accessToken: Joi.string().example('******').description('Access token for app-based authentication. Actual value is not revealed.'),
|
|
38
|
+
pubSubTopic: Joi.string().example('projects/project-name/topics/ee-pub-12345').description('Cloud Pub/Sub topic name for Gmail change notifications'),
|
|
39
|
+
pubSubSubscription: Joi.string()
|
|
40
|
+
.example('projects/project-name/subscriptions/ee-sub-12345')
|
|
41
|
+
.description('Cloud Pub/Sub subscription name for Gmail change notifications'),
|
|
42
|
+
pubSubIamPolicy: Joi.boolean().example(true).description('Whether the IAM policy for the Cloud Pub/Sub topic has been set up')
|
|
43
|
+
};
|
|
17
44
|
|
|
18
45
|
async function init(args) {
|
|
19
46
|
const { server, call, CORS_CONFIG, OAuth2ProviderSchema } = args;
|
|
20
47
|
|
|
48
|
+
// Notify the worker when an app create/update changed Pub/Sub resources, and strip the
|
|
49
|
+
// internal marker from the API response
|
|
50
|
+
let applyPubSubUpdates = async result => {
|
|
51
|
+
if (result && result.pubsubUpdates) {
|
|
52
|
+
if (Object.keys(result.pubsubUpdates).length > 0) {
|
|
53
|
+
await call({ cmd: 'googlePubSub', app: result.id });
|
|
54
|
+
}
|
|
55
|
+
delete result.pubsubUpdates;
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
};
|
|
59
|
+
|
|
21
60
|
server.route({
|
|
22
61
|
method: 'GET',
|
|
23
62
|
path: '/v1/oauth2',
|
|
@@ -55,7 +94,11 @@ async function init(args) {
|
|
|
55
94
|
notes: 'Lists registered OAuth2 applications',
|
|
56
95
|
tags: ['api', 'OAuth2 Applications'],
|
|
57
96
|
|
|
58
|
-
plugins: {
|
|
97
|
+
plugins: {
|
|
98
|
+
'hapi-swagger': {
|
|
99
|
+
responses: errorResponses(400, 401, 403, 429, 500)
|
|
100
|
+
}
|
|
101
|
+
},
|
|
59
102
|
|
|
60
103
|
auth: {
|
|
61
104
|
strategy: 'api-token',
|
|
@@ -109,7 +152,10 @@ async function init(args) {
|
|
|
109
152
|
.falsy('N', 'false', 0, '')
|
|
110
153
|
.example(true)
|
|
111
154
|
.description('`true` for older OAuth2 apps set via the settings endpoint'),
|
|
112
|
-
created: Joi.date()
|
|
155
|
+
created: Joi.date()
|
|
156
|
+
.iso()
|
|
157
|
+
.example('2021-02-17T13:43:18.860Z')
|
|
158
|
+
.description('The time this entry was added. Not present for legacy apps'),
|
|
113
159
|
updated: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this entry was updated'),
|
|
114
160
|
includeInListing: Joi.boolean()
|
|
115
161
|
.truthy('Y', 'true', '1', 'on')
|
|
@@ -153,6 +199,8 @@ async function init(args) {
|
|
|
153
199
|
.example('******')
|
|
154
200
|
.description('PEM formatted service secret for 2-legged OAuth2 applications. Actual value is not revealed.'),
|
|
155
201
|
|
|
202
|
+
...oauth2AppExtraFields,
|
|
203
|
+
|
|
156
204
|
lastError: lastErrorSchema.allow(null),
|
|
157
205
|
pubSubError: pubSubErrorSchema.allow(null)
|
|
158
206
|
}).label('OAuth2ResponseItem')
|
|
@@ -171,6 +219,9 @@ async function init(args) {
|
|
|
171
219
|
async handler(request) {
|
|
172
220
|
try {
|
|
173
221
|
let app = await oauth2Apps.get(request.params.app);
|
|
222
|
+
if (!app) {
|
|
223
|
+
throwNotFound();
|
|
224
|
+
}
|
|
174
225
|
|
|
175
226
|
// remove secrets
|
|
176
227
|
for (let secretKey of ['clientSecret', 'serviceKey', 'accessToken', 'externalAccount']) {
|
|
@@ -199,6 +250,12 @@ async function init(args) {
|
|
|
199
250
|
notes: 'Returns stored information about an OAuth2 application. Secrets are not included.',
|
|
200
251
|
tags: ['api', 'OAuth2 Applications'],
|
|
201
252
|
|
|
253
|
+
plugins: {
|
|
254
|
+
'hapi-swagger': {
|
|
255
|
+
responses: errorResponses(400, 401, 403, 404, 429, 500)
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
|
|
202
259
|
auth: {
|
|
203
260
|
strategy: 'api-token',
|
|
204
261
|
mode: 'required'
|
|
@@ -236,7 +293,7 @@ async function init(args) {
|
|
|
236
293
|
.falsy('N', 'false', 0, '')
|
|
237
294
|
.example(true)
|
|
238
295
|
.description('`true` for older OAuth2 apps set via the settings endpoint'),
|
|
239
|
-
created: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this entry was added')
|
|
296
|
+
created: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this entry was added. Not present for legacy apps'),
|
|
240
297
|
updated: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this entry was updated'),
|
|
241
298
|
includeInListing: Joi.boolean()
|
|
242
299
|
.truthy('Y', 'true', '1', 'on')
|
|
@@ -283,6 +340,8 @@ async function init(args) {
|
|
|
283
340
|
.example(12)
|
|
284
341
|
.description('The number of accounts registered with this application. Not available for legacy apps.'),
|
|
285
342
|
|
|
343
|
+
...oauth2AppExtraFields,
|
|
344
|
+
|
|
286
345
|
lastError: lastErrorSchema.allow(null),
|
|
287
346
|
pubSubError: pubSubErrorSchema.allow(null)
|
|
288
347
|
}).label('ApplicationResponse'),
|
|
@@ -298,13 +357,7 @@ async function init(args) {
|
|
|
298
357
|
async handler(request) {
|
|
299
358
|
try {
|
|
300
359
|
let result = await oauth2Apps.create(request.payload);
|
|
301
|
-
|
|
302
|
-
if (result && result.pubsubUpdates && Object.keys(result.pubsubUpdates).length > 0) {
|
|
303
|
-
await call({ cmd: 'googlePubSub', app: result.id });
|
|
304
|
-
delete result.pubsubUpdates;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
return result;
|
|
360
|
+
return await applyPubSubUpdates(result);
|
|
308
361
|
} catch (err) {
|
|
309
362
|
handleError(request, err);
|
|
310
363
|
}
|
|
@@ -315,7 +368,11 @@ async function init(args) {
|
|
|
315
368
|
notes: 'Registers a new OAuth2 application for a specific provider',
|
|
316
369
|
tags: ['api', 'OAuth2 Applications'],
|
|
317
370
|
|
|
318
|
-
plugins: {
|
|
371
|
+
plugins: {
|
|
372
|
+
'hapi-swagger': {
|
|
373
|
+
responses: errorResponses(400, 401, 403, 429, 500)
|
|
374
|
+
}
|
|
375
|
+
},
|
|
319
376
|
|
|
320
377
|
auth: {
|
|
321
378
|
strategy: 'api-token',
|
|
@@ -351,13 +408,7 @@ async function init(args) {
|
|
|
351
408
|
async handler(request) {
|
|
352
409
|
try {
|
|
353
410
|
let result = await oauth2Apps.update(request.params.app, request.payload);
|
|
354
|
-
|
|
355
|
-
if (result && result.pubsubUpdates && Object.keys(result.pubsubUpdates).length > 0) {
|
|
356
|
-
await call({ cmd: 'googlePubSub', app: result.id });
|
|
357
|
-
delete result.pubsubUpdates;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
return result;
|
|
411
|
+
return await applyPubSubUpdates(result);
|
|
361
412
|
} catch (err) {
|
|
362
413
|
handleError(request, err);
|
|
363
414
|
}
|
|
@@ -367,7 +418,11 @@ async function init(args) {
|
|
|
367
418
|
notes: 'Updates OAuth2 application information',
|
|
368
419
|
tags: ['api', 'OAuth2 Applications'],
|
|
369
420
|
|
|
370
|
-
plugins: {
|
|
421
|
+
plugins: {
|
|
422
|
+
'hapi-swagger': {
|
|
423
|
+
responses: errorResponses(400, 401, 403, 404, 429, 500)
|
|
424
|
+
}
|
|
425
|
+
},
|
|
371
426
|
|
|
372
427
|
auth: {
|
|
373
428
|
strategy: 'api-token',
|
|
@@ -481,7 +536,9 @@ async function init(args) {
|
|
|
481
536
|
|
|
482
537
|
response: {
|
|
483
538
|
schema: Joi.object({
|
|
484
|
-
id: Joi.string().max(256).required().example('example').description('OAuth2 app ID')
|
|
539
|
+
id: Joi.string().max(256).required().example('example').description('OAuth2 app ID'),
|
|
540
|
+
updated: Joi.boolean().example(true).description('Was the application updated'),
|
|
541
|
+
legacy: Joi.boolean().example(false).description('`true` for older OAuth2 apps set via the settings endpoint')
|
|
485
542
|
}).label('UpdateOAuthAppResponse'),
|
|
486
543
|
failAction: 'log'
|
|
487
544
|
}
|
|
@@ -512,7 +569,11 @@ async function init(args) {
|
|
|
512
569
|
notes: 'Delete OAuth2 application data',
|
|
513
570
|
tags: ['api', 'OAuth2 Applications'],
|
|
514
571
|
|
|
515
|
-
plugins: {
|
|
572
|
+
plugins: {
|
|
573
|
+
'hapi-swagger': {
|
|
574
|
+
responses: errorResponses(400, 401, 403, 404, 429, 500)
|
|
575
|
+
}
|
|
576
|
+
},
|
|
516
577
|
|
|
517
578
|
auth: {
|
|
518
579
|
strategy: 'api-token',
|
|
@@ -537,6 +598,7 @@ async function init(args) {
|
|
|
537
598
|
schema: Joi.object({
|
|
538
599
|
id: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
|
|
539
600
|
deleted: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Was the OAuth2 application deleted'),
|
|
601
|
+
legacy: Joi.boolean().example(false).description('`true` for older OAuth2 apps set via the settings endpoint'),
|
|
540
602
|
accounts: Joi.number()
|
|
541
603
|
.integer()
|
|
542
604
|
.example(12)
|
|
@@ -566,7 +628,11 @@ async function init(args) {
|
|
|
566
628
|
notes: 'Runs the provider authentication chain step by step and reports which steps pass or fail, with hints for fixing failures. For service-account apps an optional mailbox address enables the delegation and live mailbox checks.',
|
|
567
629
|
tags: ['api', 'OAuth2 Applications'],
|
|
568
630
|
|
|
569
|
-
plugins: {
|
|
631
|
+
plugins: {
|
|
632
|
+
'hapi-swagger': {
|
|
633
|
+
responses: errorResponses(400, 401, 403, 404, 429, 500)
|
|
634
|
+
}
|
|
635
|
+
},
|
|
570
636
|
|
|
571
637
|
auth: {
|
|
572
638
|
strategy: 'api-token',
|
|
@@ -6,7 +6,7 @@ const logger = require('../logger');
|
|
|
6
6
|
const outbox = require('../outbox');
|
|
7
7
|
const { failAction } = require('../tools');
|
|
8
8
|
const { handleError } = require('./route-helpers');
|
|
9
|
-
const { outboxEntrySchema } = require('../schemas');
|
|
9
|
+
const { outboxEntrySchema, errorResponses } = require('../schemas');
|
|
10
10
|
|
|
11
11
|
async function init(args) {
|
|
12
12
|
const { server, CORS_CONFIG } = args;
|
|
@@ -28,7 +28,11 @@ async function init(args) {
|
|
|
28
28
|
notes: 'Lists messages in the Outbox',
|
|
29
29
|
tags: ['api', 'Outbox'],
|
|
30
30
|
|
|
31
|
-
plugins: {
|
|
31
|
+
plugins: {
|
|
32
|
+
'hapi-swagger': {
|
|
33
|
+
responses: errorResponses(400, 401, 403, 429, 500)
|
|
34
|
+
}
|
|
35
|
+
},
|
|
32
36
|
|
|
33
37
|
auth: {
|
|
34
38
|
strategy: 'api-token',
|
|
@@ -93,7 +97,11 @@ async function init(args) {
|
|
|
93
97
|
notes: 'Gets a queued message in the Outbox',
|
|
94
98
|
tags: ['api', 'Outbox'],
|
|
95
99
|
|
|
96
|
-
plugins: {
|
|
100
|
+
plugins: {
|
|
101
|
+
'hapi-swagger': {
|
|
102
|
+
responses: errorResponses(400, 401, 403, 404, 429, 500)
|
|
103
|
+
}
|
|
104
|
+
},
|
|
97
105
|
|
|
98
106
|
auth: {
|
|
99
107
|
strategy: 'api-token',
|
|
@@ -139,7 +147,11 @@ async function init(args) {
|
|
|
139
147
|
notes: 'Remove a message from the outbox',
|
|
140
148
|
tags: ['api', 'Outbox'],
|
|
141
149
|
|
|
142
|
-
plugins: {
|
|
150
|
+
plugins: {
|
|
151
|
+
'hapi-swagger': {
|
|
152
|
+
responses: errorResponses(400, 401, 403, 429, 500)
|
|
153
|
+
}
|
|
154
|
+
},
|
|
143
155
|
|
|
144
156
|
auth: {
|
|
145
157
|
strategy: 'api-token',
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const Joi = require('joi');
|
|
4
4
|
const { failAction } = require('../tools');
|
|
5
5
|
const { oauth2Apps } = require('../oauth2-apps');
|
|
6
|
-
const { pubSubErrorSchema } = require('../schemas');
|
|
6
|
+
const { pubSubErrorSchema, errorResponses } = require('../schemas');
|
|
7
7
|
const { handleError, flattenOAuthAppMeta } = require('./route-helpers');
|
|
8
8
|
|
|
9
9
|
async function init(args) {
|
|
@@ -38,7 +38,11 @@ async function init(args) {
|
|
|
38
38
|
notes: 'Lists Pub/Sub enabled OAuth2 applications and their subscription status',
|
|
39
39
|
tags: ['api', 'OAuth2 Applications'],
|
|
40
40
|
|
|
41
|
-
plugins: {
|
|
41
|
+
plugins: {
|
|
42
|
+
'hapi-swagger': {
|
|
43
|
+
responses: errorResponses(400, 401, 403, 429, 500)
|
|
44
|
+
}
|
|
45
|
+
},
|
|
42
46
|
|
|
43
47
|
auth: {
|
|
44
48
|
strategy: 'api-token',
|
|
@@ -79,10 +83,10 @@ async function init(args) {
|
|
|
79
83
|
id: Joi.string().max(256).required().example('AAABhaBPHscAAAAH').description('OAuth2 application ID'),
|
|
80
84
|
name: Joi.string().allow(null).max(256).example('My Gmail App').description('Display name for the app'),
|
|
81
85
|
lastError: Joi.object({
|
|
82
|
-
response: Joi.string().example('Enable the Cloud Pub/Sub API').description('
|
|
86
|
+
response: Joi.string().example('Enable the Cloud Pub/Sub API').description('Error message')
|
|
83
87
|
})
|
|
84
88
|
.allow(null)
|
|
85
|
-
.description('
|
|
89
|
+
.description('Last Pub/Sub related error for this app - either a setup error or an OAuth2 token renewal failure')
|
|
86
90
|
.label('PubSubSetupError'),
|
|
87
91
|
pubSubError: pubSubErrorSchema.allow(null)
|
|
88
92
|
}).label('PubSubAppStatus')
|
|
@@ -21,9 +21,22 @@ function handleError(request, err) {
|
|
|
21
21
|
if (err.code) {
|
|
22
22
|
error.output.payload.code = err.code;
|
|
23
23
|
}
|
|
24
|
+
if (err.details) {
|
|
25
|
+
error.output.payload.details = err.details;
|
|
26
|
+
}
|
|
24
27
|
throw error;
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
// Throws the canonical 404 error used when an entity getter returns a falsy value. Matches the
|
|
31
|
+
// error shape that the lib-level update() methods (templates, webhooks, oauth2-apps) already throw
|
|
32
|
+
// for missing documents, so API clients see the same error for both code paths.
|
|
33
|
+
function throwNotFound(message = 'Document was not found') {
|
|
34
|
+
let err = new Error(message);
|
|
35
|
+
err.code = 'NotFound';
|
|
36
|
+
err.statusCode = 404;
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
|
|
27
40
|
// Strips the internal `meta` field from an OAuth2 application object before returning it to the API
|
|
28
41
|
// client, surfacing any authentication or Pub/Sub error messages as `lastError`/`pubSubError`.
|
|
29
42
|
// Pure function: it mutates the passed object and closes over no module state.
|
|
@@ -42,4 +55,4 @@ function flattenOAuthAppMeta(app) {
|
|
|
42
55
|
}
|
|
43
56
|
}
|
|
44
57
|
|
|
45
|
-
module.exports = { handleError, flattenOAuthAppMeta };
|
|
58
|
+
module.exports = { handleError, throwNotFound, flattenOAuthAppMeta };
|
|
@@ -7,7 +7,28 @@ const consts = require('../consts');
|
|
|
7
7
|
const { REDIS_PREFIX } = consts;
|
|
8
8
|
const { failAction } = require('../tools');
|
|
9
9
|
const { handleError } = require('./route-helpers');
|
|
10
|
-
const { settingsSchema, settingsQuerySchema } = require('../schemas');
|
|
10
|
+
const { settingsSchema, settingsQuerySchema, errorResponses } = require('../schemas');
|
|
11
|
+
|
|
12
|
+
// Response variant of the settings schema. Secret values are masked and returned as booleans,
|
|
13
|
+
// any setting that has never been set is returned as null, and the virtual eventTypes key is
|
|
14
|
+
// not part of the stored settings.
|
|
15
|
+
const settingsOutputSchema = {};
|
|
16
|
+
for (let key of Object.keys(settingsSchema)) {
|
|
17
|
+
if (settings.encryptedKeys.includes(key)) {
|
|
18
|
+
settingsOutputSchema[key] = Joi.boolean()
|
|
19
|
+
.allow(null)
|
|
20
|
+
.example(true)
|
|
21
|
+
.description('Whether a value is set for this setting. Secret values are never returned, only a boolean marker');
|
|
22
|
+
} else {
|
|
23
|
+
// Use a distinct label for the nullable response variant, otherwise the generated
|
|
24
|
+
// OpenAPI spec would contain suffixed duplicates of the request-side components
|
|
25
|
+
settingsOutputSchema[key] = settingsSchema[key].allow(null).label(`${key}Response`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
settingsOutputSchema.eventTypes = Joi.array()
|
|
29
|
+
.items(Joi.string().example('messageNew').label('EventTypeEntry'))
|
|
30
|
+
.description('Supported webhook event types')
|
|
31
|
+
.label('EventTypesList');
|
|
11
32
|
|
|
12
33
|
async function init(args) {
|
|
13
34
|
const { server, notify, CORS_CONFIG } = args;
|
|
@@ -22,13 +43,8 @@ async function init(args) {
|
|
|
22
43
|
if (request.query[key]) {
|
|
23
44
|
if (key === 'eventTypes') {
|
|
24
45
|
values[key] = Object.keys(consts)
|
|
25
|
-
.
|
|
26
|
-
|
|
27
|
-
return consts[key];
|
|
28
|
-
}
|
|
29
|
-
return false;
|
|
30
|
-
})
|
|
31
|
-
.map(key => key);
|
|
46
|
+
.filter(key => /_NOTIFY?/.test(key))
|
|
47
|
+
.map(key => consts[key]);
|
|
32
48
|
continue;
|
|
33
49
|
}
|
|
34
50
|
|
|
@@ -50,6 +66,12 @@ async function init(args) {
|
|
|
50
66
|
notes: 'List setting values for specific keys',
|
|
51
67
|
tags: ['api', 'Settings'],
|
|
52
68
|
|
|
69
|
+
plugins: {
|
|
70
|
+
'hapi-swagger': {
|
|
71
|
+
responses: errorResponses(400, 401, 403, 429, 500)
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
|
|
53
75
|
auth: {
|
|
54
76
|
strategy: 'api-token',
|
|
55
77
|
mode: 'required'
|
|
@@ -68,7 +90,7 @@ async function init(args) {
|
|
|
68
90
|
},
|
|
69
91
|
|
|
70
92
|
response: {
|
|
71
|
-
schema: Joi.object(
|
|
93
|
+
schema: Joi.object(settingsOutputSchema).label('SettingsQueryResponse'),
|
|
72
94
|
failAction: 'log'
|
|
73
95
|
}
|
|
74
96
|
}
|
|
@@ -81,19 +103,9 @@ async function init(args) {
|
|
|
81
103
|
async handler(request) {
|
|
82
104
|
let updated = [];
|
|
83
105
|
for (let key of Object.keys(request.payload)) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
request.payload.serviceUrl = url.origin;
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
case 'webhooksEnabled':
|
|
92
|
-
if (!request.payload.webhooksEnabled) {
|
|
93
|
-
// clear error message (if exists)
|
|
94
|
-
await settings.clear('webhookErrorFlag');
|
|
95
|
-
}
|
|
96
|
-
break;
|
|
106
|
+
if (key === 'webhooksEnabled' && !request.payload.webhooksEnabled) {
|
|
107
|
+
// clear error message (if exists)
|
|
108
|
+
await settings.clear('webhookErrorFlag');
|
|
97
109
|
}
|
|
98
110
|
|
|
99
111
|
await settings.set(key, request.payload[key]);
|
|
@@ -110,7 +122,11 @@ async function init(args) {
|
|
|
110
122
|
notes: 'Set setting values for specific keys',
|
|
111
123
|
tags: ['api', 'Settings'],
|
|
112
124
|
|
|
113
|
-
plugins: {
|
|
125
|
+
plugins: {
|
|
126
|
+
'hapi-swagger': {
|
|
127
|
+
responses: errorResponses(400, 401, 403, 429, 500)
|
|
128
|
+
}
|
|
129
|
+
},
|
|
114
130
|
|
|
115
131
|
auth: {
|
|
116
132
|
strategy: 'api-token',
|
|
@@ -160,7 +176,7 @@ async function init(args) {
|
|
|
160
176
|
|
|
161
177
|
if (resActive[0] || resDelayed[0] || resPaused[0] || resWaiting[0]) {
|
|
162
178
|
// counting failed
|
|
163
|
-
let err = new Error('Failed to count queue
|
|
179
|
+
let err = new Error('Failed to count queue length');
|
|
164
180
|
err.statusCode = 500;
|
|
165
181
|
throw err;
|
|
166
182
|
}
|
|
@@ -184,6 +200,12 @@ async function init(args) {
|
|
|
184
200
|
notes: 'Show queue status and current state',
|
|
185
201
|
tags: ['api', 'Settings'],
|
|
186
202
|
|
|
203
|
+
plugins: {
|
|
204
|
+
'hapi-swagger': {
|
|
205
|
+
responses: errorResponses(400, 401, 403, 429, 500)
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
|
|
187
209
|
auth: {
|
|
188
210
|
strategy: 'api-token',
|
|
189
211
|
mode: 'required'
|
|
@@ -278,7 +300,11 @@ async function init(args) {
|
|
|
278
300
|
notes: 'Set queue settings',
|
|
279
301
|
tags: ['api', 'Settings'],
|
|
280
302
|
|
|
281
|
-
plugins: {
|
|
303
|
+
plugins: {
|
|
304
|
+
'hapi-swagger': {
|
|
305
|
+
responses: errorResponses(400, 401, 403, 429, 500)
|
|
306
|
+
}
|
|
307
|
+
},
|
|
282
308
|
|
|
283
309
|
auth: {
|
|
284
310
|
strategy: 'api-token',
|
|
@@ -4,6 +4,7 @@ const Joi = require('joi');
|
|
|
4
4
|
const { redis } = require('../db');
|
|
5
5
|
const { failAction, getStats } = require('../tools');
|
|
6
6
|
const { MAX_DAYS_STATS } = require('../consts');
|
|
7
|
+
const { errorResponses } = require('../schemas');
|
|
7
8
|
const packageData = require('../../package.json');
|
|
8
9
|
|
|
9
10
|
async function init(args) {
|
|
@@ -21,6 +22,12 @@ async function init(args) {
|
|
|
21
22
|
description: 'Return server stats',
|
|
22
23
|
tags: ['api', 'Stats'],
|
|
23
24
|
|
|
25
|
+
plugins: {
|
|
26
|
+
'hapi-swagger': {
|
|
27
|
+
responses: errorResponses(400, 401, 403, 429, 500)
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
|
|
24
31
|
auth: {
|
|
25
32
|
strategy: 'api-token',
|
|
26
33
|
mode: 'required'
|
|
@@ -54,20 +61,47 @@ async function init(args) {
|
|
|
54
61
|
license: Joi.string().example(packageData.license).description('EmailEngine license'),
|
|
55
62
|
accounts: Joi.number().integer().example(26).description('Number of registered accounts'),
|
|
56
63
|
node: Joi.string().example('16.10.0').description('Node.js Version'),
|
|
57
|
-
redis: Joi.string()
|
|
64
|
+
redis: Joi.string()
|
|
65
|
+
.example('6.2.4')
|
|
66
|
+
.description('Redis version. Can include the server software in parentheses, or an error message if the version lookup failed'),
|
|
67
|
+
redisSoftware: Joi.string().example('redis').description('Redis-compatible server software name'),
|
|
68
|
+
redisCluster: Joi.boolean().example(false).description('Whether Redis is running in cluster mode'),
|
|
69
|
+
redisWarnings: Joi.array()
|
|
70
|
+
.items(
|
|
71
|
+
Joi.object({
|
|
72
|
+
key: Joi.string().example('maxmemory-policy').description('Warning identifier'),
|
|
73
|
+
color: Joi.string().example('warning').description('Severity indicator'),
|
|
74
|
+
title: Joi.string().example('Unsafe Redis eviction policy').description('Warning title'),
|
|
75
|
+
details: Joi.array().items(Joi.string()).description('Warning details')
|
|
76
|
+
})
|
|
77
|
+
.unknown()
|
|
78
|
+
.label('RedisWarningEntry')
|
|
79
|
+
)
|
|
80
|
+
.description('Warnings about the Redis configuration')
|
|
81
|
+
.label('RedisWarnings'),
|
|
82
|
+
redisPing: Joi.number().description('Redis latency in milliseconds'),
|
|
83
|
+
imapflow: Joi.string().example('1.0.188').description('ImapFlow version'),
|
|
84
|
+
bullmq: Joi.string().example('5.0.0').description('BullMQ version'),
|
|
85
|
+
arch: Joi.string().example('arm64').description('CPU architecture of the host'),
|
|
86
|
+
build: Joi.object().unknown().description('Build information for the running EmailEngine instance').label('BuildInfo'),
|
|
87
|
+
queues: Joi.object().unknown().description('Job counters for the notify, submit, and documents queues').label('QueueStats'),
|
|
58
88
|
connections: Joi.object({
|
|
59
89
|
init: Joi.number().integer().example(2).description('Accounts not yet initialized'),
|
|
60
90
|
connected: Joi.number().integer().example(8).description('Successfully connected accounts'),
|
|
61
91
|
connecting: Joi.number().integer().example(7).description('Connection is being established'),
|
|
92
|
+
syncing: Joi.number().integer().example(1).description('Accounts that are currently syncing'),
|
|
62
93
|
authenticationError: Joi.number().integer().example(3).description('Authentication failed'),
|
|
63
94
|
connectError: Joi.number().integer().example(5).description('Connection failed due to technical error'),
|
|
64
95
|
unset: Joi.number().integer().example(0).description('Accounts without valid IMAP settings'),
|
|
65
|
-
disconnected: Joi.number().integer().example(1).description('IMAP connection was closed')
|
|
96
|
+
disconnected: Joi.number().integer().example(1).description('IMAP connection was closed'),
|
|
97
|
+
paused: Joi.number().integer().example(0).description('Accounts that are paused'),
|
|
98
|
+
unassigned: Joi.number().integer().example(0).description('Accounts not assigned to any worker')
|
|
66
99
|
})
|
|
100
|
+
.unknown()
|
|
67
101
|
.description('Counts of accounts in different connection states')
|
|
68
102
|
.label('ConnectionsStats'),
|
|
69
103
|
counters: Joi.object().label('CounterStats').unknown()
|
|
70
|
-
}).label('
|
|
104
|
+
}).label('StatsResponse'),
|
|
71
105
|
failAction: 'log'
|
|
72
106
|
}
|
|
73
107
|
}
|
|
@@ -15,7 +15,9 @@ const {
|
|
|
15
15
|
accountIdSchema,
|
|
16
16
|
templateSchemas,
|
|
17
17
|
settingsSchema,
|
|
18
|
-
ipSchema
|
|
18
|
+
ipSchema,
|
|
19
|
+
threadIdSchema,
|
|
20
|
+
errorResponses
|
|
19
21
|
} = require('../schemas');
|
|
20
22
|
|
|
21
23
|
async function init(args) {
|
|
@@ -65,7 +67,11 @@ async function init(args) {
|
|
|
65
67
|
notes: 'Submit message for delivery. If reference message ID is provided then EmailEngine adds all headers and flags required for a reply/forward automatically.',
|
|
66
68
|
tags: ['api', 'Submit'],
|
|
67
69
|
|
|
68
|
-
plugins: {
|
|
70
|
+
plugins: {
|
|
71
|
+
'hapi-swagger': {
|
|
72
|
+
responses: errorResponses(400, 401, 403, 404, 429, 500, 503)
|
|
73
|
+
}
|
|
74
|
+
},
|
|
69
75
|
|
|
70
76
|
auth: {
|
|
71
77
|
strategy: 'api-token',
|
|
@@ -378,9 +384,9 @@ async function init(args) {
|
|
|
378
384
|
message: Joi.string()
|
|
379
385
|
.base64({ paddingRequired: false, urlSafe: true })
|
|
380
386
|
.max(256)
|
|
381
|
-
.required()
|
|
382
387
|
.example('AAAAAQAACnA')
|
|
383
|
-
.description('Referenced message ID'),
|
|
388
|
+
.description('Referenced message ID. Not present when only a thread ID was referenced'),
|
|
389
|
+
threadId: threadIdSchema.description('Referenced thread ID (Gmail API accounts only)').label('SubmitResponseReferenceThreadId'),
|
|
384
390
|
documentStore: Joi.boolean()
|
|
385
391
|
.example(true)
|
|
386
392
|
.description('Was the message data loaded from the Document Store')
|
|
@@ -395,47 +401,34 @@ async function init(args) {
|
|
|
395
401
|
preview: Joi.string()
|
|
396
402
|
.base64()
|
|
397
403
|
.example('Q29udGVudC1UeXBlOiBtdWx0aX...')
|
|
398
|
-
.description('Base64 encoded RFC822 email if a preview was requested')
|
|
404
|
+
.description('Base64 encoded RFC822 email if a preview was requested. Not returned for mail-merge submissions.')
|
|
399
405
|
.label('ResponsePreview'),
|
|
400
406
|
|
|
407
|
+
idempotency: Joi.object({
|
|
408
|
+
key: idempotencyKeySchema.example('submit-12345').description('Idempotency key from the request').label('ResponseIdempotencyKey'),
|
|
409
|
+
status: Joi.string()
|
|
410
|
+
.valid('MISS', 'HIT')
|
|
411
|
+
.example('MISS')
|
|
412
|
+
.description('Whether the response was generated now (MISS) or returned from the idempotency cache (HIT)')
|
|
413
|
+
})
|
|
414
|
+
.description('Idempotency info, present when an Idempotency-Key header was used')
|
|
415
|
+
.label('ResponseIdempotency'),
|
|
416
|
+
|
|
401
417
|
mailMerge: Joi.array()
|
|
402
418
|
.items(
|
|
403
419
|
Joi.object({
|
|
404
|
-
success: Joi.boolean()
|
|
405
|
-
.example(true)
|
|
406
|
-
.description('Was the referenced message processed successfully')
|
|
407
|
-
.label('ResponseReferenceSuccess'),
|
|
420
|
+
success: Joi.boolean().example(true).description('Was the message queued successfully').label('ResponseReferenceSuccess'),
|
|
408
421
|
to: addressSchema.label('ToAddressSingle'),
|
|
409
422
|
messageId: Joi.string().max(996).example('<test123@example.com>').description('Message ID'),
|
|
410
|
-
queueId: Joi.string()
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
.description('Referenced message ID'),
|
|
420
|
-
documentStore: Joi.boolean()
|
|
421
|
-
.example(true)
|
|
422
|
-
.description('Was the message data loaded from the Document Store')
|
|
423
|
-
.label('ResponseDocumentStore')
|
|
424
|
-
.meta({ swaggerHidden: true }),
|
|
425
|
-
success: Joi.boolean()
|
|
426
|
-
.example(true)
|
|
427
|
-
.description('Was the referenced message processed successfully')
|
|
428
|
-
.label('ResponseReferenceSuccess'),
|
|
429
|
-
error: Joi.string()
|
|
430
|
-
.example('Referenced message was not found')
|
|
431
|
-
.description('An error message if referenced message processing failed')
|
|
432
|
-
})
|
|
433
|
-
.description('Reference info if referencing was requested')
|
|
434
|
-
.label('ResponseReference'),
|
|
435
|
-
sendAt: Joi.date()
|
|
436
|
-
.iso()
|
|
437
|
-
.example('2021-07-08T07:06:34.336Z')
|
|
438
|
-
.description('Send message at specified time. Overrides message level `sendAt` value.'),
|
|
423
|
+
queueId: Joi.string().example('d41f0423195f271f').description('Queue identifier for the scheduled email'),
|
|
424
|
+
sendAt: Joi.date().iso().example('2021-07-08T07:06:34.336Z').description('Scheduled send time for this recipient'),
|
|
425
|
+
error: Joi.string().example('Failed to queue message').description('Error message if queueing failed for this recipient'),
|
|
426
|
+
code: Joi.string().example('EENVELOPE').description('Error code if queueing failed for this recipient'),
|
|
427
|
+
statusCode: Joi.number()
|
|
428
|
+
.integer()
|
|
429
|
+
.allow(null)
|
|
430
|
+
.example(500)
|
|
431
|
+
.description('Error status code if queueing failed for this recipient'),
|
|
439
432
|
skipped: Joi.object({
|
|
440
433
|
reason: Joi.string().example('unsubscribe').description('Why this message was skipped'),
|
|
441
434
|
listId: Joi.string().example('test-list')
|
|
@@ -452,10 +445,6 @@ async function init(args) {
|
|
|
452
445
|
},
|
|
453
446
|
messageId: '<19b9c433-d428-f6d8-1d00-d666ebcadfc4@ekiri.ee>',
|
|
454
447
|
queueId: '1812477338914c8372a',
|
|
455
|
-
reference: {
|
|
456
|
-
message: 'AAAAAQAACnA',
|
|
457
|
-
success: true
|
|
458
|
-
},
|
|
459
448
|
sendAt: '2021-07-08T07:06:34.336Z'
|
|
460
449
|
})
|
|
461
450
|
.unknown()
|