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
@@ -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: accountIdSchema.required(),
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: accountIdSchema.required(),
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: accountIdSchema.required(),
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
- return await templates.get(request.params.template);
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: accountIdSchema.required(),
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: accountIdSchema.required(),
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
- deleted: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Were the templates flushed'),
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: Joi.string()
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
- description: Joi.string().empty('').trim().max(1024).required().example('Token description').description('Token description'),
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
- return await Webhooks.get(request.params.webhookRoute);
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'
@@ -709,8 +709,6 @@ async function resolveUsingAutodiscovery(email, domain, source) {
709
709
  });
710
710
  });
711
711
 
712
- resp = resp.filter(entry => entry.account);
713
-
714
712
  return { imap, smtp, _source: source || 'autodiscover' };
715
713
  }
716
714
 
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'
@@ -1,12 +1,33 @@
1
1
  'use strict';
2
2
 
3
+ const config = require('@zone-eu/wild-config');
3
4
  const settings = require('./settings');
4
5
  const { Client: ElasticSearch } = require('@elastic/elasticsearch');
5
6
  const { ensureIndex } = require('./es');
7
+ const { hasEnvValue, readEnvValue, getBoolean } = require('./tools');
8
+
9
+ // Deployment-level gate for the deprecated Document Store feature. When this is false the
10
+ // "documents" worker is not spawned, document-store-only endpoints are not registered, and
11
+ // all runtime document-store code takes its existing "disabled" path. Set via the
12
+ // --documentStore.enabled CLI flag / [documentStore] enabled config or EENGINE_DOCUMENT_STORE_ENABLED.
13
+ const documentStoreFeatureEnabled = hasEnvValue('EENGINE_DOCUMENT_STORE_ENABLED')
14
+ ? getBoolean(readEnvValue('EENGINE_DOCUMENT_STORE_ENABLED'))
15
+ : getBoolean(config.documentStore && config.documentStore.enabled);
16
+
17
+ // Effective runtime state: the feature must be available (gate) AND enabled in settings.
18
+ // When the gate is off this is always false, so callers reuse the already-tested
19
+ // "documentStoreEnabled is false" code paths.
20
+ const isDocumentStoreEnabled = async () => documentStoreFeatureEnabled && !!(await settings.get('documentStoreEnabled'));
6
21
 
7
22
  const clientCache = { version: -1, config: false, client: false, index: false };
8
23
 
9
24
  const getESClient = async logger => {
25
+ // Feature gate is off: behave exactly as a disabled document store. Checked before any
26
+ // Redis access so disabled deployments do not pay a round-trip on every getESClient call.
27
+ if (!documentStoreFeatureEnabled) {
28
+ return clientCache;
29
+ }
30
+
10
31
  const documentStoreVersion = (await settings.get('documentStoreVersion')) || 0;
11
32
  if (clientCache.version === documentStoreVersion) {
12
33
  return clientCache;
@@ -51,4 +72,4 @@ const getESClient = async logger => {
51
72
  return clientCache;
52
73
  };
53
74
 
54
- module.exports = { getESClient };
75
+ module.exports = { getESClient, documentStoreFeatureEnabled, isDocumentStoreEnabled };
@@ -4,6 +4,7 @@ const { threadId: workerThreadId } = require('worker_threads');
4
4
  const crypto = require('crypto');
5
5
  const logger = require('../logger');
6
6
  const settings = require('../settings');
7
+ const { isDocumentStoreEnabled } = require('../document-store');
7
8
  const msgpack = require('msgpack5')();
8
9
  const { templates } = require('../templates');
9
10
  const { Gateway } = require('../gateway');
@@ -240,6 +241,33 @@ class BaseClient {
240
241
  return rid;
241
242
  }
242
243
 
244
+ /**
245
+ * Normalizes an IMAP-style flag update request (add/delete/set) into plain add/delete
246
+ * flag lists. If `set` is present then it replaces the state of the flags the backend
247
+ * supports and add/delete are ignored, matching the IMAP backend behavior.
248
+ * @param {Object} [flags] - Flag update request ({ add, delete, set })
249
+ * @param {string[]} supportedFlags - Flags the backend can represent
250
+ * @returns {Object} Normalized flag lists ({ add: string[], delete: string[] })
251
+ */
252
+ normalizeFlagUpdates(flags, supportedFlags) {
253
+ if (flags?.set) {
254
+ // If set exists then ignore add/delete calls
255
+ let setFlags = [].concat(flags.set);
256
+ let addFlags = [];
257
+ let deleteFlags = [];
258
+ for (let flag of supportedFlags) {
259
+ if (setFlags.includes(flag)) {
260
+ addFlags.push(flag);
261
+ } else {
262
+ deleteFlags.push(flag);
263
+ }
264
+ }
265
+ return { add: addFlags, delete: deleteFlags };
266
+ }
267
+
268
+ return { add: [].concat(flags?.add || []), delete: [].concat(flags?.delete || []) };
269
+ }
270
+
243
271
  // Redis key generators for different data types
244
272
 
245
273
  getAccountKey() {
@@ -721,7 +749,7 @@ class BaseClient {
721
749
  }
722
750
 
723
751
  // use existing response
724
- switch (idempotencyData.status) {
752
+ switch (idempotencyData?.status) {
725
753
  case 'completed':
726
754
  // Return cached result
727
755
  idempotencyData.returnValue = Object.assign({}, idempotencyData.result, {
@@ -1049,7 +1077,7 @@ class BaseClient {
1049
1077
  // Resolve reference and update reference/in-reply-to headers
1050
1078
  if (data.reference && data.reference.message) {
1051
1079
  // Try document store first if enabled
1052
- if (data.reference.documentStore && (await settings.get('documentStoreEnabled'))) {
1080
+ if (data.reference.documentStore && (await isDocumentStoreEnabled())) {
1053
1081
  try {
1054
1082
  referencedMessage = await this.accountObject.getMessage(data.reference.message, {
1055
1083
  documentStore: true,
@@ -1551,7 +1579,7 @@ class BaseClient {
1551
1579
  // Resolve reference and update reference/in-reply-to headers
1552
1580
  if (data.reference && data.reference.message) {
1553
1581
  // Try document store first if enabled
1554
- if (data.reference.documentStore && (await settings.get('documentStoreEnabled'))) {
1582
+ if (data.reference.documentStore && (await isDocumentStoreEnabled())) {
1555
1583
  try {
1556
1584
  referencedMessage = await this.accountObject.getMessage(data.reference.message, {
1557
1585
  documentStore: true,
@@ -3194,11 +3222,6 @@ class BaseClient {
3194
3222
  async handleSubmitError(err, context) {
3195
3223
  const { smtpSettings, networkRouting, gatewayData, gatewayObject, data, jobData, queueId, envelope } = context;
3196
3224
 
3197
- // Handle permanent failures
3198
- if (err.responseCode >= 500 && jobData.opts?.attempts <= jobData.attemptsMade) {
3199
- jobData.nextAttempt = false;
3200
- }
3201
-
3202
3225
  // Build SMTP status from error
3203
3226
  const smtpStatus = SmtpErrorBuilder.buildStatus(err, smtpSettings, networkRouting);
3204
3227