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.
Files changed (97) hide show
  1. package/.github/workflows/deploy.yml +6 -3
  2. package/.github/workflows/release.yaml +2 -0
  3. package/.github/workflows/test.yml +73 -12
  4. package/.ncurc.js +3 -3
  5. package/CHANGELOG.md +37 -0
  6. package/Gruntfile.js +21 -23
  7. package/bin/emailengine.js +8 -1
  8. package/config/default.toml +5 -0
  9. package/config/test.toml +5 -0
  10. package/data/google-crawlers.json +1 -1
  11. package/getswagger.sh +44 -4
  12. package/gettext-extract.js +163 -0
  13. package/lib/account.js +104 -72
  14. package/lib/api-routes/account-routes.js +231 -71
  15. package/lib/api-routes/blocklist-routes.js +25 -18
  16. package/lib/api-routes/chat-routes.js +32 -14
  17. package/lib/api-routes/delivery-test-routes.js +30 -5
  18. package/lib/api-routes/export-routes.js +27 -2
  19. package/lib/api-routes/gateway-routes.js +63 -12
  20. package/lib/api-routes/license-routes.js +18 -4
  21. package/lib/api-routes/mailbox-routes.js +33 -7
  22. package/lib/api-routes/message-routes.js +291 -145
  23. package/lib/api-routes/oauth2-app-routes.js +90 -24
  24. package/lib/api-routes/outbox-routes.js +16 -4
  25. package/lib/api-routes/pubsub-routes.js +8 -4
  26. package/lib/api-routes/route-helpers.js +14 -1
  27. package/lib/api-routes/settings-routes.js +51 -25
  28. package/lib/api-routes/stats-routes.js +37 -3
  29. package/lib/api-routes/submit-routes.js +31 -42
  30. package/lib/api-routes/template-routes.js +54 -21
  31. package/lib/api-routes/token-routes.js +67 -67
  32. package/lib/api-routes/webhook-route-routes.js +37 -8
  33. package/lib/autodetect-imap-settings.js +0 -2
  34. package/lib/consts.js +5 -0
  35. package/lib/document-store.js +22 -1
  36. package/lib/email-client/base-client.js +31 -8
  37. package/lib/email-client/gmail-client.js +119 -112
  38. package/lib/email-client/imap/mailbox.js +2 -2
  39. package/lib/email-client/imap/subconnection.js +0 -1
  40. package/lib/email-client/imap/sync-operations.js +1 -1
  41. package/lib/email-client/imap-client.js +36 -17
  42. package/lib/email-client/notification-handler.js +3 -6
  43. package/lib/email-client/outlook-client.js +49 -62
  44. package/lib/export.js +49 -1
  45. package/lib/feature-flags.js +8 -2
  46. package/lib/gateway.js +4 -9
  47. package/lib/get-raw-email.js +5 -5
  48. package/lib/imapproxy/imap-core/lib/imap-connection.js +0 -1
  49. package/lib/license-beacon.js +367 -0
  50. package/lib/logger.js +35 -22
  51. package/lib/metrics-collector.js +0 -2
  52. package/lib/oauth2-apps.js +13 -4
  53. package/lib/outbox.js +24 -40
  54. package/lib/redis-operations.js +1 -1
  55. package/lib/routes-ui.js +2 -1
  56. package/lib/schemas.js +403 -83
  57. package/lib/sentry.js +139 -0
  58. package/lib/settings.js +9 -3
  59. package/lib/stream-encrypt.js +1 -1
  60. package/lib/templates.js +1 -1
  61. package/lib/tokens.js +5 -3
  62. package/lib/tools.js +28 -6
  63. package/lib/ui-routes/account-routes.js +7 -4
  64. package/lib/ui-routes/admin-config-routes.js +20 -6
  65. package/lib/ui-routes/document-store-routes.js +7 -1
  66. package/lib/ui-routes/oauth-config-routes.js +0 -2
  67. package/lib/ui-routes/route-helpers.js +0 -2
  68. package/lib/ui-routes/unsubscribe-routes.js +0 -2
  69. package/lib/webhooks.js +8 -4
  70. package/package.json +23 -19
  71. package/sbom.json +1 -1
  72. package/server.js +38 -31
  73. package/static/licenses.html +171 -391
  74. package/translations/de.mo +0 -0
  75. package/translations/de.po +154 -142
  76. package/translations/et.mo +0 -0
  77. package/translations/et.po +129 -131
  78. package/translations/fr.mo +0 -0
  79. package/translations/fr.po +133 -136
  80. package/translations/ja.mo +0 -0
  81. package/translations/ja.po +126 -129
  82. package/translations/messages.pot +107 -107
  83. package/translations/nl.mo +0 -0
  84. package/translations/nl.po +128 -130
  85. package/translations/pl.mo +0 -0
  86. package/translations/pl.po +125 -128
  87. package/update-info.sh +19 -1
  88. package/views/config/logging.hbs +48 -0
  89. package/views/dashboard.hbs +22 -0
  90. package/workers/api.js +33 -37
  91. package/workers/documents.js +2 -22
  92. package/workers/export.js +73 -92
  93. package/workers/imap-proxy.js +3 -23
  94. package/workers/imap.js +2 -22
  95. package/workers/smtp.js +2 -22
  96. package/workers/submit.js +6 -24
  97. 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().iso().example('2021-02-17T13:43:18.860Z').description('The time this entry was added').required(),
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').required(),
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('Setup error message')
86
+ response: Joi.string().example('Enable the Cloud Pub/Sub API').description('Error message')
83
87
  })
84
88
  .allow(null)
85
- .description('Setup error from ensurePubsub, if any')
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
- .map(key => {
26
- if (/_NOTIFY?/.test(key)) {
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(settingsSchema).label('SettingsQueryResponse'),
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
- switch (key) {
85
- case 'serviceUrl': {
86
- let url = new URL(request.payload.serviceUrl);
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 lengtho');
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().example('6.2.4').description('Redis Version'),
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('SettingsResponse'),
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
- .example('d41f0423195f271f')
412
- .description('Queue identifier for scheduled email. Not present for bulk messages.'),
413
- reference: Joi.object({
414
- message: Joi.string()
415
- .base64({ paddingRequired: false, urlSafe: true })
416
- .max(256)
417
- .required()
418
- .example('AAAAAQAACnA')
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()