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
package/workers/imap.js CHANGED
@@ -7,7 +7,7 @@ const logger = require('../lib/logger');
7
7
 
8
8
  const { REDIS_PREFIX } = require('../lib/consts');
9
9
 
10
- const { getDuration, getBoolean, emitChangeEvent, readEnvValue, hasEnvValue, threadStats } = require('../lib/tools');
10
+ const { getDuration, getBoolean, emitChangeEvent, readEnvValue, hasEnvValue, threadStats, reloadHttpProxyAgent } = require('../lib/tools');
11
11
 
12
12
  const Bugsnag = require('@bugsnag/js');
13
13
  if (readEnvValue('BUGSNAG_API_KEY')) {
@@ -452,6 +452,34 @@ class ConnectionHandler {
452
452
  return await accountData.connection.getMessage(message.message, message.options);
453
453
  }
454
454
 
455
+ async getMessages(message) {
456
+ if (!this.accounts.has(message.account)) {
457
+ throw NO_ACTIVE_HANDLER_RESP_ERR;
458
+ }
459
+
460
+ let accountData = this.accounts.get(message.account);
461
+ if (!accountData.connection) {
462
+ throw NO_ACTIVE_HANDLER_RESP_ERR;
463
+ }
464
+
465
+ // Use batch method if available (Gmail/Outlook API clients)
466
+ if (typeof accountData.connection.getMessages === 'function') {
467
+ return await accountData.connection.getMessages(message.messageIds, message.options);
468
+ }
469
+
470
+ // Fallback to sequential fetching for IMAP
471
+ const results = [];
472
+ for (const messageId of message.messageIds) {
473
+ try {
474
+ const msg = await accountData.connection.getMessage(messageId, message.options);
475
+ results.push({ messageId, data: msg, error: null });
476
+ } catch (err) {
477
+ results.push({ messageId, data: null, error: { message: err.message, code: err.code } });
478
+ }
479
+ }
480
+ return results;
481
+ }
482
+
455
483
  async updateMessage(message) {
456
484
  if (!this.accounts.has(message.account)) {
457
485
  throw NO_ACTIVE_HANDLER_RESP_ERR;
@@ -798,6 +826,10 @@ class ConnectionHandler {
798
826
 
799
827
  switch (message.cmd) {
800
828
  case 'settings':
829
+ if (message.data && ('httpProxyEnabled' in message.data || 'httpProxyUrl' in message.data)) {
830
+ reloadHttpProxyAgent().catch(err => logger.error({ msg: 'Failed to reload HTTP proxy agent', err }));
831
+ }
832
+
801
833
  if (message.data && message.data.logs) {
802
834
  for (let [account, accountObject] of this.accounts) {
803
835
  // update log handling
@@ -841,6 +873,7 @@ class ConnectionHandler {
841
873
  case 'listMessages':
842
874
  case 'getText':
843
875
  case 'getMessage':
876
+ case 'getMessages':
844
877
  case 'updateMessage':
845
878
  case 'updateMessages':
846
879
  case 'listMailboxes':
package/workers/submit.js CHANGED
@@ -7,7 +7,7 @@ const config = require('@zone-eu/wild-config');
7
7
  const logger = require('../lib/logger');
8
8
 
9
9
  const { REDIS_PREFIX } = require('../lib/consts');
10
- const { getDuration, readEnvValue, threadStats } = require('../lib/tools');
10
+ const { getDuration, readEnvValue, threadStats, reloadHttpProxyAgent } = require('../lib/tools');
11
11
  const { webhooks: Webhooks } = require('../lib/webhooks');
12
12
  const settings = require('../lib/settings');
13
13
 
@@ -62,6 +62,16 @@ const SUBMIT_QC = (readEnvValue('EENGINE_SUBMIT_QC') && Number(readEnvValue('EEN
62
62
 
63
63
  const SUBMIT_DELAY = getDuration(readEnvValue('EENGINE_SUBMIT_DELAY') || config.submitDelay) || null;
64
64
 
65
+ const NON_RETRYABLE_CODES = new Set([
66
+ 'EAUTH', // authentication failed
67
+ 'ENOAUTH', // no credentials provided
68
+ 'EOAUTH2', // OAuth2 token failure
69
+ 'ETLS', // TLS handshake failed
70
+ 'EENVELOPE', // invalid sender/recipients
71
+ 'EMESSAGE', // message content error
72
+ 'EPROTOCOL' // SMTP protocol mismatch
73
+ ]);
74
+
65
75
  let callQueue = new Map();
66
76
  let mids = 0;
67
77
 
@@ -295,9 +305,11 @@ const submitWorker = new Worker(
295
305
  // ignore
296
306
  }
297
307
 
298
- if (err.statusCode >= 500 && job.attemptsMade < job.opts.attempts) {
308
+ const isPermanentSmtp = err.statusCode >= 500 && err.statusCode !== 503;
309
+ const isPermanentCode = NON_RETRYABLE_CODES.has(err.code);
310
+ if ((isPermanentSmtp || isPermanentCode) && job.attemptsMade < job.opts.attempts) {
299
311
  try {
300
- // do not retry after 5xx error
312
+ // do not retry after 5xx error (except 503 which is transient)
301
313
  await job.discard();
302
314
  logger.info({
303
315
  msg: 'Job discarded',
@@ -305,9 +317,6 @@ const submitWorker = new Worker(
305
317
  queueId: job.data.queueId
306
318
  });
307
319
  } catch (E) {
308
- // ignore
309
- logger.error({ msg: 'Failed to discard job', account: queueEntry.account, queueId: job.data.queueId, err: E });
310
-
311
320
  logger.error({
312
321
  msg: 'Failed to discard job',
313
322
  action: 'submit',
@@ -329,6 +338,17 @@ const submitWorker = new Worker(
329
338
  {
330
339
  concurrency: SUBMIT_QC,
331
340
 
341
+ // Lock duration must exceed SMTP socket timeout (2 min) to prevent
342
+ // jobs from being marked stalled during normal email delivery
343
+ lockDuration: 3 * 60 * 1000, // 3 minutes
344
+
345
+ // Check for stalled jobs every 60 seconds
346
+ stalledInterval: 60 * 1000,
347
+
348
+ // Allow jobs to recover from stalled state up to 3 times before failing
349
+ // This handles transient Redis latency or connection issues
350
+ maxStalledCount: 3,
351
+
332
352
  limiter: SUBMIT_DELAY
333
353
  ? {
334
354
  max: 1,
@@ -474,6 +494,13 @@ parentPort.on('message', message => {
474
494
  });
475
495
  });
476
496
  }
497
+
498
+ if (message && message.cmd === 'settings') {
499
+ let d = message.data || {};
500
+ if ('httpProxyEnabled' in d || 'httpProxyUrl' in d) {
501
+ reloadHttpProxyAgent().catch(err => logger.error({ msg: 'Failed to reload HTTP proxy agent', err }));
502
+ }
503
+ }
477
504
  });
478
505
 
479
506
  logger.info({ msg: 'Started SMTP submission worker thread', version: packageData.version });
@@ -10,7 +10,7 @@ const { webhooks: Webhooks } = require('../lib/webhooks');
10
10
 
11
11
  const { GooglePubSub } = require('../lib/oauth/pubsub/google');
12
12
 
13
- const { readEnvValue, threadStats, getDuration, retryAgent, getServiceSecret } = require('../lib/tools');
13
+ const { readEnvValue, threadStats, getDuration, httpAgent, getServiceSecret, reloadHttpProxyAgent } = require('../lib/tools');
14
14
 
15
15
  const Bugsnag = require('@bugsnag/js');
16
16
  if (readEnvValue('BUGSNAG_API_KEY')) {
@@ -176,6 +176,13 @@ parentPort.on('message', message => {
176
176
  });
177
177
  });
178
178
  }
179
+
180
+ if (message && message.cmd === 'settings') {
181
+ let d = message.data || {};
182
+ if ('httpProxyEnabled' in d || 'httpProxyUrl' in d) {
183
+ reloadHttpProxyAgent().catch(err => logger.error({ msg: 'Failed to reload HTTP proxy agent', err }));
184
+ }
185
+ }
179
186
  });
180
187
 
181
188
  const notifyWorker = new Worker(
@@ -293,7 +300,7 @@ const notifyWorker = new Worker(
293
300
  {
294
301
  let filteredSubData = {};
295
302
  let isPartial = false;
296
- for (let dataKey of Object.keys(job.data.data)) {
303
+ for (let dataKey of Object.keys(job.data.data || {})) {
297
304
  switch (dataKey) {
298
305
  case 'id':
299
306
  case 'uid':
@@ -402,7 +409,7 @@ const notifyWorker = new Worker(
402
409
  method: 'post',
403
410
  body,
404
411
  headers,
405
- dispatcher: retryAgent
412
+ dispatcher: httpAgent.retry
406
413
  });
407
414
  duration = Date.now() - start;
408
415
  } catch (err) {
@@ -546,7 +553,16 @@ route: customRoute && customRoute.id,
546
553
  },
547
554
  Object.assign(
548
555
  {
549
- concurrency: Number(NOTIFY_QC) || 1
556
+ concurrency: Number(NOTIFY_QC) || 1,
557
+
558
+ // Webhook HTTP requests have 90s timeout, lock should exceed this
559
+ lockDuration: 3 * 60 * 1000, // 3 minutes
560
+
561
+ // Check for stalled jobs every 60 seconds
562
+ stalledInterval: 60 * 1000,
563
+
564
+ // Allow jobs to recover from stalled state up to 3 times
565
+ maxStalledCount: 3
550
566
  },
551
567
  queueConf || {}
552
568
  )