emailengine-app 2.61.5 → 2.62.1

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 (65) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/data/google-crawlers.json +1 -1
  3. package/lib/account.js +20 -7
  4. package/lib/api-routes/account-routes.js +28 -5
  5. package/lib/api-routes/chat-routes.js +1 -1
  6. package/lib/api-routes/export-routes.js +316 -0
  7. package/lib/api-routes/message-routes.js +28 -23
  8. package/lib/api-routes/template-routes.js +28 -7
  9. package/lib/arf-detect.js +1 -1
  10. package/lib/autodetect-imap-settings.js +5 -5
  11. package/lib/consts.js +16 -0
  12. package/lib/db.js +3 -0
  13. package/lib/email-client/base-client.js +6 -4
  14. package/lib/email-client/gmail-client.js +205 -35
  15. package/lib/email-client/imap/mailbox.js +99 -8
  16. package/lib/email-client/imap/subconnection.js +5 -5
  17. package/lib/email-client/imap-client.js +76 -19
  18. package/lib/email-client/message-builder.js +3 -1
  19. package/lib/email-client/notification-handler.js +12 -9
  20. package/lib/email-client/outlook-client.js +364 -73
  21. package/lib/email-client/smtp-pool-manager.js +1 -1
  22. package/lib/export.js +528 -0
  23. package/lib/oauth/gmail.js +24 -16
  24. package/lib/oauth/mail-ru.js +26 -13
  25. package/lib/oauth/outlook.js +29 -19
  26. package/lib/oauth/pubsub/google.js +5 -0
  27. package/lib/routes-ui.js +268 -9
  28. package/lib/schemas.js +274 -81
  29. package/lib/stream-encrypt.js +263 -0
  30. package/lib/sub-script.js +2 -2
  31. package/lib/tools.js +194 -12
  32. package/lib/ui-routes/account-routes.js +23 -0
  33. package/lib/ui-routes/admin-config-routes.js +13 -6
  34. package/lib/ui-routes/admin-entities-routes.js +18 -0
  35. package/lib/webhooks.js +16 -20
  36. package/package.json +20 -20
  37. package/sbom.json +1 -1
  38. package/server.js +66 -7
  39. package/static/js/ace/ace.js +1 -1
  40. package/static/js/ace/ext-language_tools.js +1 -1
  41. package/static/licenses.html +118 -149
  42. package/translations/de.mo +0 -0
  43. package/translations/de.po +63 -36
  44. package/translations/en.mo +0 -0
  45. package/translations/en.po +64 -37
  46. package/translations/et.mo +0 -0
  47. package/translations/et.po +63 -36
  48. package/translations/fr.mo +0 -0
  49. package/translations/fr.po +63 -36
  50. package/translations/ja.mo +0 -0
  51. package/translations/ja.po +63 -36
  52. package/translations/messages.pot +84 -51
  53. package/translations/nl.mo +0 -0
  54. package/translations/nl.po +63 -36
  55. package/translations/pl.mo +0 -0
  56. package/translations/pl.po +63 -36
  57. package/views/accounts/account.hbs +375 -2
  58. package/views/config/network.hbs +45 -0
  59. package/views/config/service.hbs +35 -0
  60. package/workers/api.js +130 -47
  61. package/workers/documents.js +3 -2
  62. package/workers/export.js +933 -0
  63. package/workers/imap.js +34 -1
  64. package/workers/submit.js +33 -6
  65. package/workers/webhooks.js +20 -4
@@ -147,7 +147,8 @@ async function init(args) {
147
147
  .lowercase()
148
148
  .valid('html', 'plain', '*')
149
149
  .example('*')
150
- .description('Which text content to return, use * for all. By default text content is not returned.'),
150
+ .description('Which text content to return, use * for all. By default text content is not returned.')
151
+ .label('MessageTextType'),
151
152
 
152
153
  webSafeHtml: Joi.boolean()
153
154
  .truthy('Y', 'true', '1')
@@ -310,9 +311,9 @@ async function init(args) {
310
311
  }),
311
312
 
312
313
  contentType: Joi.string().lowercase().max(256).example('image/gif'),
313
- contentDisposition: Joi.string().lowercase().valid('inline', 'attachment'),
314
+ contentDisposition: Joi.string().lowercase().valid('inline', 'attachment').label('MsgUploadContentDisposition'),
314
315
  cid: Joi.string().max(256).example('unique-image-id@localhost').description('Content-ID value for embedded images'),
315
- encoding: Joi.string().valid('base64').default('base64'),
316
+ encoding: Joi.string().valid('base64').default('base64').label('MsgUploadEncoding'),
316
317
 
317
318
  reference: Joi.string()
318
319
  .base64({ paddingRequired: false, urlSafe: true })
@@ -322,6 +323,7 @@ async function init(args) {
322
323
  .description(
323
324
  'References an existing attachment by its ID instead of providing new attachment content. If this field is set, the `content` field must not be included. If not set, the `content` field is required.'
324
325
  )
326
+ .label('MsgUploadReference')
325
327
  }).label('UploadAttachment')
326
328
  )
327
329
  .description('List of attachments')
@@ -430,15 +432,15 @@ async function init(args) {
430
432
  response: {
431
433
  schema: Joi.object({
432
434
  flags: Joi.object({
433
- add: Joi.array().items(Joi.string()).example(['\\Seen', '\\Flagged']),
434
- delete: Joi.array().items(Joi.string()).example(['\\Draft']),
435
- set: Joi.array().items(Joi.string()).example(['\\Seen'])
436
- }).label('FlagResponse'),
435
+ add: Joi.array().items(Joi.string().label('FlagEntry')).example(['\\Seen', '\\Flagged']).label('FlagAddList'),
436
+ delete: Joi.array().items(Joi.string().label('FlagEntry')).example(['\\Draft']).label('FlagDeleteList'),
437
+ set: Joi.array().items(Joi.string().label('FlagEntry')).example(['\\Seen']).label('FlagSetList')
438
+ }).label('FlagUpdateResponse'),
437
439
  labels: Joi.object({
438
- add: Joi.array().items(Joi.string()).example(['Label1', 'Label2']),
439
- delete: Joi.array().items(Joi.string()).example(['Label3']),
440
- set: Joi.array().items(Joi.string()).example(['Label1'])
441
- }).label('FlagResponse')
440
+ add: Joi.array().items(Joi.string().label('LabelEntry')).example(['Label1', 'Label2']).label('LabelAddList'),
441
+ delete: Joi.array().items(Joi.string().label('LabelEntry')).example(['Label3']).label('LabelDeleteList'),
442
+ set: Joi.array().items(Joi.string().label('LabelEntry')).example(['Label1']).label('LabelSetList')
443
+ }).label('LabelUpdateResponse')
442
444
  }).label('MessageUpdateResponse'),
443
445
  failAction: 'log'
444
446
  }
@@ -510,16 +512,16 @@ async function init(args) {
510
512
  response: {
511
513
  schema: Joi.object({
512
514
  flags: Joi.object({
513
- add: Joi.array().items(Joi.string()).example(['\\Seen', '\\Flagged']),
514
- delete: Joi.array().items(Joi.string()).example(['\\Draft']),
515
- set: Joi.array().items(Joi.string()).example(['\\Seen'])
516
- }).label('FlagResponse'),
515
+ add: Joi.array().items(Joi.string().label('BulkFlagEntry')).example(['\\Seen', '\\Flagged']).label('BulkFlagAddList'),
516
+ delete: Joi.array().items(Joi.string().label('BulkFlagEntry')).example(['\\Draft']).label('BulkFlagDeleteList'),
517
+ set: Joi.array().items(Joi.string().label('BulkFlagEntry')).example(['\\Seen']).label('BulkFlagSetList')
518
+ }).label('BulkFlagUpdateResponse'),
517
519
  labels: Joi.object({
518
- add: Joi.array().items(Joi.string()).example(['Label1', 'Label2']),
519
- delete: Joi.array().items(Joi.string()).example(['Label3']),
520
- set: Joi.array().items(Joi.string()).example(['Label1'])
521
- }).label('FlagResponse')
522
- }).label('MessageUpdateResponse'),
520
+ add: Joi.array().items(Joi.string().label('BulkLabelEntry')).example(['Label1', 'Label2']).label('BulkLabelAddList'),
521
+ delete: Joi.array().items(Joi.string().label('BulkLabelEntry')).example(['Label3']).label('BulkLabelDeleteList'),
522
+ set: Joi.array().items(Joi.string().label('BulkLabelEntry')).example(['Label1']).label('BulkLabelSetList')
523
+ }).label('BulkLabelUpdateResponse')
524
+ }).label('BulkMessageUpdateResponse'),
523
525
  failAction: 'log'
524
526
  }
525
527
  }
@@ -759,7 +761,9 @@ async function init(args) {
759
761
  moved: Joi.object({
760
762
  destination: Joi.string().required().example('Trash').description('Trash folder path').label('TrashPath'),
761
763
  message: Joi.string().required().example('AAAAAwAAAWg').description('Message ID in Trash').label('TrashMessageId')
762
- }).description('Present if message was moved to Trash')
764
+ })
765
+ .description('Present if message was moved to Trash')
766
+ .label('MessageMovedToTrash')
763
767
  }).label('MessageDeleteResponse'),
764
768
  failAction: 'log'
765
769
  }
@@ -1083,7 +1087,7 @@ async function init(args) {
1083
1087
  .unknown()
1084
1088
  .meta({ swaggerHidden: true })
1085
1089
  })
1086
- .label('SearchQuery')
1090
+ .label('MessageSearchPayload')
1087
1091
  .example({
1088
1092
  search: {
1089
1093
  unseen: true,
@@ -1281,7 +1285,8 @@ async function init(args) {
1281
1285
  .valid('html', 'plain', '*')
1282
1286
  .default('*')
1283
1287
  .example('*')
1284
- .description('Which text content to return, use * for all. By default all contents are returned.'),
1288
+ .description('Which text content to return, use * for all. By default all contents are returned.')
1289
+ .label('TextSearchTextType'),
1285
1290
  documentStore: documentStoreSchema.default(false)
1286
1291
  }),
1287
1292
 
@@ -93,7 +93,11 @@ async function init(args) {
93
93
  .example('Something about the template')
94
94
  .description('Optional description of the template')
95
95
  .label('TemplateDescription'),
96
- format: Joi.string().valid('html', 'markdown').default('html').description('Markup language for HTML ("html" or "markdown")'),
96
+ format: Joi.string()
97
+ .valid('html', 'markdown')
98
+ .default('html')
99
+ .description('Markup language for HTML ("html" or "markdown")')
100
+ .label('TemplateFormat'),
97
101
  content: Joi.object({
98
102
  subject: templateSchemas.subject,
99
103
  text: templateSchemas.text,
@@ -176,7 +180,12 @@ async function init(args) {
176
180
  .example('Something about the template')
177
181
  .description('Optional description of the template')
178
182
  .label('TemplateDescription'),
179
- format: Joi.string().empty('').valid('html', 'markdown').default('html').description('Markup language for HTML ("html" or "markdown")'),
183
+ format: Joi.string()
184
+ .empty('')
185
+ .valid('html', 'markdown')
186
+ .default('html')
187
+ .description('Markup language for HTML ("html" or "markdown")')
188
+ .label('TemplateUpdateFormat'),
180
189
  content: Joi.object({
181
190
  subject: templateSchemas.subject,
182
191
  text: templateSchemas.text,
@@ -277,7 +286,11 @@ async function init(args) {
277
286
  .example('Something about the template')
278
287
  .description('Optional description of the template')
279
288
  .label('TemplateDescription'),
280
- format: Joi.string().valid('html', 'markdown').default('html').description('Markup language for HTML ("html" or "markdown")'),
289
+ format: Joi.string()
290
+ .valid('html', 'markdown')
291
+ .default('html')
292
+ .description('Markup language for HTML ("html" or "markdown")')
293
+ .label('TemplateListFormat'),
281
294
  created: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this template was created'),
282
295
  updated: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this template was last updated')
283
296
  }).label('AccountTemplate')
@@ -345,7 +358,11 @@ async function init(args) {
345
358
  .example('Something about the template')
346
359
  .description('Optional description of the template')
347
360
  .label('TemplateDescription'),
348
- format: Joi.string().valid('html', 'markdown').default('html').description('Markup language for HTML ("html" or "markdown")'),
361
+ format: Joi.string()
362
+ .valid('html', 'markdown')
363
+ .default('html')
364
+ .description('Markup language for HTML ("html" or "markdown")')
365
+ .label('TemplateResponseFormat'),
349
366
  created: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this template was created'),
350
367
  updated: Joi.date().iso().example('2021-02-17T13:43:18.860Z').description('The time this template was last updated'),
351
368
  content: Joi.object({
@@ -353,7 +370,11 @@ async function init(args) {
353
370
  text: templateSchemas.text,
354
371
  html: templateSchemas.html,
355
372
  previewText: templateSchemas.previewText,
356
- format: Joi.string().valid('html', 'markdown').default('html').description('Markup language for HTML ("html" or "markdown")')
373
+ format: Joi.string()
374
+ .valid('html', 'markdown')
375
+ .default('html')
376
+ .description('Markup language for HTML ("html" or "markdown")')
377
+ .label('TemplateContentFormat')
357
378
  }).label('RequestTemplateContent')
358
379
  }).label('AccountTemplateResponse'),
359
380
  failAction: 'log'
@@ -411,7 +432,7 @@ async function init(args) {
411
432
  deleted: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Was the template deleted'),
412
433
  account: accountIdSchema.required(),
413
434
  id: Joi.string().max(256).required().example('AAABgS-UcAYAAAABAA').description('Template ID')
414
- }).label('DeleteTemplateRequestResponse'),
435
+ }).label('DeleteTemplateResponse'),
415
436
  failAction: 'log'
416
437
  }
417
438
  }
@@ -480,7 +501,7 @@ async function init(args) {
480
501
  schema: Joi.object({
481
502
  deleted: Joi.boolean().truthy('Y', 'true', '1').falsy('N', 'false', 0).default(true).description('Were the templates flushed'),
482
503
  account: accountIdSchema.required()
483
- }).label('DeleteTemplateRequestResponse'),
504
+ }).label('FlushTemplatesResponse'),
484
505
  failAction: 'log'
485
506
  }
486
507
  }
package/lib/arf-detect.js CHANGED
@@ -27,7 +27,7 @@ const arfDetect = async messageInfo => {
27
27
 
28
28
  let returnPath;
29
29
 
30
- for (let attachment of messageInfo.attachments) {
30
+ for (let attachment of messageInfo.attachments || []) {
31
31
  switch (attachment.contentType.toLowerCase()) {
32
32
  case 'message/feedback-report': {
33
33
  // found a feedback report
@@ -20,7 +20,7 @@ const parseXml = util.promisify((xml, cb) => {
20
20
  });
21
21
 
22
22
  const { fetch: fetchCmd } = require('undici');
23
- const { retryAgent } = require('./tools');
23
+ const { httpAgent } = require('./tools');
24
24
 
25
25
  const RESOLV_TIMEOUT = 5 * 1000;
26
26
 
@@ -191,7 +191,7 @@ async function resolveUsingMozillaDirectory(email, domain, source) {
191
191
  headers: {
192
192
  'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage})`
193
193
  },
194
- dispatcher: retryAgent
194
+ dispatcher: httpAgent.retry
195
195
  });
196
196
 
197
197
  if (!res.ok) {
@@ -216,7 +216,7 @@ async function resolveUsingAutoconfig(email, domain, source) {
216
216
  headers: {
217
217
  'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage})`
218
218
  },
219
- dispatcher: retryAgent
219
+ dispatcher: httpAgent.retry
220
220
  });
221
221
 
222
222
  if (!res.ok) {
@@ -241,7 +241,7 @@ async function resolveUsingWellKnown(email, domain, source) {
241
241
  headers: {
242
242
  'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage})`
243
243
  },
244
- dispatcher: retryAgent
244
+ dispatcher: httpAgent.retry
245
245
  });
246
246
 
247
247
  if (!res.ok) {
@@ -640,7 +640,7 @@ async function resolveUsingAutodiscovery(email, domain, source) {
640
640
  'Content-type': 'application/xml'
641
641
  },
642
642
  body,
643
- dispatcher: retryAgent
643
+ dispatcher: httpAgent.retry
644
644
  });
645
645
 
646
646
  if (!res.ok) {
package/lib/consts.js CHANGED
@@ -87,6 +87,22 @@ module.exports = {
87
87
  LIST_SUBSCRIBE_NOTIFY: 'listSubscribe',
88
88
  LIST_SUBSCRIBE_DESCRIPTION: 'Recipient Subscribed - A recipient re-subscribed to a list',
89
89
 
90
+ EXPORT_COMPLETED_NOTIFY: 'exportCompleted',
91
+ EXPORT_COMPLETED_DESCRIPTION: 'Export Completed - A bulk message export has completed successfully',
92
+
93
+ EXPORT_FAILED_NOTIFY: 'exportFailed',
94
+ EXPORT_FAILED_DESCRIPTION: 'Export Failed - A bulk message export has failed',
95
+
96
+ // Export defaults
97
+ DEFAULT_EXPORT_MAX_AGE: 24 * 60 * 60 * 1000, // 24 hours
98
+ DEFAULT_EXPORT_MAX_SIZE: 10 * 1024 * 1024 * 1024, // 10GB
99
+ DEFAULT_EXPORT_MAX_MESSAGES: 500000,
100
+ DEFAULT_EXPORT_MAX_CONCURRENT: 2,
101
+ DEFAULT_EXPORT_MAX_GLOBAL_CONCURRENT: 8,
102
+ DEFAULT_EXPORT_MAX_MESSAGE_SIZE: 50 * 1024 * 1024, // 50MB
103
+ DEFAULT_GMAIL_EXPORT_BATCH_SIZE: 10,
104
+ DEFAULT_OUTLOOK_EXPORT_BATCH_SIZE: 20,
105
+
90
106
  MAX_DAYS_STATS: 7,
91
107
 
92
108
  DEFAULT_MAX_LOG_LINES: 10000,
package/lib/db.js CHANGED
@@ -107,12 +107,14 @@ const reqisQueue = new Redis(REDIS_CONF);
107
107
 
108
108
  module.exports.queueConf = {
109
109
  connection: reqisQueue,
110
+ sharedConnection: true, // Prevent BullMQ from closing shared connection
110
111
  prefix: `${REDIS_PREFIX}bull`
111
112
  };
112
113
 
113
114
  const notifyQueue = new Queue('notify', module.exports.queueConf);
114
115
  const submitQueue = new Queue('submit', module.exports.queueConf);
115
116
  const documentsQueue = new Queue('documents', module.exports.queueConf);
117
+ const exportQueue = new Queue('export', module.exports.queueConf);
116
118
 
117
119
  const zExpungeScript = fs.readFileSync(pathlib.join(__dirname, '/lua/z-expunge.lua'), 'utf-8');
118
120
  const zSetScript = fs.readFileSync(pathlib.join(__dirname, 'lua/z-set.lua'), 'utf-8');
@@ -215,6 +217,7 @@ module.exports.redis = redis;
215
217
  module.exports.notifyQueue = notifyQueue;
216
218
  module.exports.submitQueue = submitQueue;
217
219
  module.exports.documentsQueue = documentsQueue;
220
+ module.exports.exportQueue = exportQueue;
218
221
 
219
222
  // do not set up the flow producer by default
220
223
  module.exports.getFlowProducer = () => new FlowProducer(module.exports.queueConf /*, Redis*/);
@@ -1984,7 +1984,7 @@ class BaseClient {
1984
1984
  // Store message in Redis
1985
1985
  await this.redis.hsetBuffer(`${REDIS_PREFIX}iaq:${this.account}`, queueId, msgEntry);
1986
1986
 
1987
- let queueKeep = (await settings.get('queueKeep')) || true;
1987
+ let queueKeep = (await settings.get('queueKeep')) ?? true;
1988
1988
 
1989
1989
  // Configure delivery retry settings
1990
1990
  let defaultDeliveryAttempts = await settings.get('deliveryAttempts');
@@ -2007,14 +2007,16 @@ class BaseClient {
2007
2007
 
2008
2008
  // Configure queue job options
2009
2009
  let queueName = 'queued';
2010
+ let retention = typeof queueKeep === 'number' ? { age: 24 * 3600, count: queueKeep } : queueKeep;
2010
2011
  let jobOpts = {
2011
2012
  jobId: queueId,
2012
- removeOnComplete: queueKeep,
2013
- removeOnFail: queueKeep,
2013
+ removeOnComplete: retention,
2014
+ removeOnFail: retention,
2014
2015
  attempts: typeof deliveryAttempts === 'number' ? deliveryAttempts : defaultDeliveryAttempts,
2015
2016
  backoff: {
2016
2017
  type: 'exponential',
2017
- delay: 5000
2018
+ delay: 5000,
2019
+ jitter: 0.2 // 20% randomization to prevent thundering herd
2018
2020
  }
2019
2021
  };
2020
2022