emailengine-app 2.63.4 → 2.65.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 (59) hide show
  1. package/.github/workflows/test.yml +4 -0
  2. package/CHANGELOG.md +70 -0
  3. package/copy-static-files.sh +1 -1
  4. package/data/google-crawlers.json +1 -1
  5. package/eslint.config.js +2 -0
  6. package/lib/account.js +13 -9
  7. package/lib/api-routes/account-routes.js +7 -1
  8. package/lib/consts.js +17 -1
  9. package/lib/email-client/gmail/gmail-api.js +1 -12
  10. package/lib/email-client/imap-client.js +5 -3
  11. package/lib/email-client/outlook/graph-api.js +9 -15
  12. package/lib/email-client/outlook-client.js +406 -177
  13. package/lib/export.js +17 -0
  14. package/lib/imapproxy/imap-server.js +3 -2
  15. package/lib/oauth/gmail.js +12 -1
  16. package/lib/oauth/outlook.js +99 -1
  17. package/lib/oauth/pubsub/google.js +253 -85
  18. package/lib/oauth2-apps.js +620 -389
  19. package/lib/outbox.js +1 -1
  20. package/lib/routes-ui.js +193 -238
  21. package/lib/schemas.js +189 -12
  22. package/lib/ui-routes/account-routes.js +7 -2
  23. package/lib/ui-routes/admin-entities-routes.js +3 -3
  24. package/lib/ui-routes/oauth-routes.js +27 -175
  25. package/package.json +21 -21
  26. package/sbom.json +1 -1
  27. package/server.js +54 -22
  28. package/static/licenses.html +30 -90
  29. package/translations/de.mo +0 -0
  30. package/translations/de.po +54 -42
  31. package/translations/en.mo +0 -0
  32. package/translations/en.po +55 -43
  33. package/translations/et.mo +0 -0
  34. package/translations/et.po +54 -42
  35. package/translations/fr.mo +0 -0
  36. package/translations/fr.po +54 -42
  37. package/translations/ja.mo +0 -0
  38. package/translations/ja.po +54 -42
  39. package/translations/messages.pot +93 -71
  40. package/translations/nl.mo +0 -0
  41. package/translations/nl.po +54 -42
  42. package/translations/pl.mo +0 -0
  43. package/translations/pl.po +54 -42
  44. package/views/config/oauth/app.hbs +12 -0
  45. package/views/config/oauth/edit.hbs +2 -0
  46. package/views/config/oauth/index.hbs +4 -1
  47. package/views/config/oauth/new.hbs +2 -0
  48. package/views/config/oauth/subscriptions.hbs +175 -0
  49. package/views/error.hbs +4 -4
  50. package/views/partials/oauth_form.hbs +179 -4
  51. package/views/partials/oauth_tabs.hbs +8 -0
  52. package/views/partials/scope_info.hbs +10 -0
  53. package/workers/api.js +174 -96
  54. package/workers/documents.js +1 -0
  55. package/workers/export.js +6 -2
  56. package/workers/imap.js +33 -49
  57. package/workers/smtp.js +1 -0
  58. package/workers/submit.js +1 -0
  59. package/workers/webhooks.js +42 -30
package/workers/imap.js CHANGED
@@ -37,7 +37,7 @@ const { GmailClient } = require('../lib/email-client/gmail-client');
37
37
  const { OutlookClient } = require('../lib/email-client/outlook-client');
38
38
  const { BaseClient } = require('../lib/email-client/base-client');
39
39
  const { Account } = require('../lib/account');
40
- const { oauth2Apps } = require('../lib/oauth2-apps');
40
+ const { oauth2Apps, isApiBasedApp } = require('../lib/oauth2-apps');
41
41
  const { redis, notifyQueue, submitQueue, documentsQueue, getFlowProducer } = require('../lib/db');
42
42
  const { MessagePortWritable } = require('../lib/message-port-stream');
43
43
  const { getESClient } = require('../lib/document-store');
@@ -78,10 +78,6 @@ class ConnectionHandler {
78
78
  this.mids = 0;
79
79
 
80
80
  this.accounts = new Map();
81
-
82
- // Reconnection metrics tracking
83
- this.reconnectMetrics = new Map(); // Track metrics per account
84
- this.metricsWindow = 60000; // 1-minute window
85
81
  }
86
82
 
87
83
  async init() {
@@ -213,7 +209,7 @@ class ConnectionHandler {
213
209
  oauth2App = await oauth2Apps.get(accountData.oauth2.provider);
214
210
  }
215
211
 
216
- if (oauth2App && oauth2App.baseScopes === 'api') {
212
+ if (isApiBasedApp(oauth2App)) {
217
213
  // Use API instead of IMAP
218
214
 
219
215
  switch (oauth2App.provider) {
@@ -236,6 +232,7 @@ class ConnectionHandler {
236
232
  accountObject.logger = accountObject.connection.logger;
237
233
  break;
238
234
 
235
+ case 'outlookService':
239
236
  case 'outlook':
240
237
  accountObject.connection = new OutlookClient(account, {
241
238
  runIndex,
@@ -639,6 +636,34 @@ class ConnectionHandler {
639
636
  return await accountData.connection.externalNotify(message);
640
637
  }
641
638
 
639
+ async subscriptionLifecycle(message) {
640
+ if (!this.accounts.has(message.account)) {
641
+ throw NO_ACTIVE_HANDLER_RESP_ERR;
642
+ }
643
+
644
+ let accountData = this.accounts.get(message.account);
645
+ if (!accountData.connection) {
646
+ throw NO_ACTIVE_HANDLER_RESP_ERR;
647
+ }
648
+
649
+ let connection = accountData.connection;
650
+
651
+ switch (message.event) {
652
+ case 'reauthorizationRequired':
653
+ return await connection.renewSubscription({ force: true });
654
+
655
+ case 'subscriptionRemoved':
656
+ // Clear stored subscription since MS deleted it server-side, under lock
657
+ logger.info({ msg: 'Handling subscriptionRemoved lifecycle event', account: message.account });
658
+ await connection.ensureSubscription({ clearExisting: true });
659
+ return true;
660
+
661
+ default:
662
+ logger.warn({ msg: 'Unknown subscription lifecycle event', event: message.event, account: message.account });
663
+ return false;
664
+ }
665
+ }
666
+
642
667
  async getQuota(message) {
643
668
  if (!this.accounts.has(message.account)) {
644
669
  throw NO_ACTIVE_HANDLER_RESP_ERR;
@@ -725,49 +750,6 @@ class ConnectionHandler {
725
750
  };
726
751
  }
727
752
 
728
- /**
729
- * Track reconnection attempts for monitoring (without blocking)
730
- * @param {string} account - Account identifier
731
- */
732
- trackReconnection(account) {
733
- const now = Date.now();
734
- const metrics = this.reconnectMetrics.get(account) || {
735
- attempts: [],
736
- warnings: 0
737
- };
738
-
739
- // Clean old attempts outside window
740
- metrics.attempts = metrics.attempts.filter(t => now - t < this.metricsWindow);
741
- metrics.attempts.push(now);
742
-
743
- // Log warning if excessive reconnections
744
- if (metrics.attempts.length > 20) {
745
- // More than 20 per minute
746
- metrics.warnings++;
747
- logger.warn({
748
- msg: 'Excessive reconnection rate detected',
749
- account,
750
- rate: `${metrics.attempts.length}/min`,
751
- totalWarnings: metrics.warnings
752
- });
753
-
754
- // Emit metrics for monitoring/alerting
755
- try {
756
- parentPort.postMessage({
757
- cmd: 'metrics',
758
- key: 'imap.reconnect.excessive',
759
- method: 'inc',
760
- args: [1],
761
- meta: { account }
762
- });
763
- } catch (err) {
764
- logger.error({ msg: 'Failed to send metrics', err });
765
- }
766
- }
767
-
768
- this.reconnectMetrics.set(account, metrics);
769
- }
770
-
771
753
  async getAttachment(message) {
772
754
  if (!this.accounts.has(message.account)) {
773
755
  throw NO_ACTIVE_HANDLER_RESP_ERR;
@@ -892,6 +874,7 @@ class ConnectionHandler {
892
874
  case 'uploadMessage':
893
875
  case 'subconnections':
894
876
  case 'externalNotify':
877
+ case 'subscriptionLifecycle':
895
878
  case 'listSignatures':
896
879
  return await this[message.cmd](message);
897
880
 
@@ -940,6 +923,7 @@ class ConnectionHandler {
940
923
  err.statusCode = 504;
941
924
  err.code = 'Timeout';
942
925
  err.ttl = ttl;
926
+ this.callQueue.delete(mid);
943
927
  reject(err);
944
928
  }, ttl);
945
929
 
package/workers/smtp.js CHANGED
@@ -77,6 +77,7 @@ async function call(message, transferList) {
77
77
  err.statusCode = 504;
78
78
  err.code = 'Timeout';
79
79
  err.ttl = ttl;
80
+ callQueue.delete(mid);
80
81
  reject(err);
81
82
  }, ttl);
82
83
 
package/workers/submit.js CHANGED
@@ -85,6 +85,7 @@ async function call(message, transferList) {
85
85
  err.statusCode = 504;
86
86
  err.code = 'Timeout';
87
87
  err.ttl = ttl;
88
+ callQueue.delete(mid);
88
89
  reject(err);
89
90
  }, ttl);
90
91
 
@@ -67,6 +67,7 @@ async function call(message, transferList) {
67
67
  err.statusCode = 504;
68
68
  err.code = 'Timeout';
69
69
  err.ttl = ttl;
70
+ callQueue.delete(mid);
70
71
  reject(err);
71
72
  }, ttl);
72
73
 
@@ -117,6 +118,14 @@ async function onCommand(command) {
117
118
  case 'googlePubSub':
118
119
  await googlePubSub.update(command.app);
119
120
  return true;
121
+ case 'googlePubSubRemove':
122
+ googlePubSub.remove(command.app);
123
+ return true;
124
+ case 'close':
125
+ clearTimeout(startRetryTimer);
126
+ googlePubSub.stopAll();
127
+ await notifyWorker.close(true);
128
+ return true;
120
129
  default:
121
130
  logger.debug({ msg: 'Unhandled command', command });
122
131
  return 999;
@@ -132,6 +141,18 @@ setInterval(() => {
132
141
  }
133
142
  }, 10 * 1000).unref();
134
143
 
144
+ // Clean up Pub/Sub instances when parent port closes
145
+ parentPort.on('close', () => {
146
+ clearTimeout(startRetryTimer);
147
+ googlePubSub.stopAll();
148
+ // notifyWorker.close() may throw synchronously if not yet initialized
149
+ try {
150
+ notifyWorker.close(true).catch(() => {});
151
+ } catch {
152
+ // ignore
153
+ }
154
+ });
155
+
135
156
  // Send initial ready signal
136
157
  parentPort.postMessage({ cmd: 'ready' });
137
158
 
@@ -461,28 +482,6 @@ const notifyWorker = new Worker(
461
482
  status: 'success'
462
483
  });
463
484
  } catch (err) {
464
- /*
465
- // do not disable by default
466
- if (err.status === 410) {
467
- // disable webhook
468
- logger.error({
469
- msg: 'Webhooks were disabled by server',
470
- action: 'webhook',
471
- queue: job.queue.name,
472
- code: 'disabled_by_server',
473
- job: job.id,
474
- webhooks,
475
- accountWebhooks: !!accountWebhooks,
476
- event: job.name,
477
- status: err.status,
478
- account: job.data.account,
479
- route: customRoute && customRoute.id,
480
- err
481
- });
482
- await settings.set('webhooksEnabled', false);
483
- return;
484
- }
485
- */
486
485
  logger.error({
487
486
  msg: 'Failed posting webhook',
488
487
  action: 'webhook',
@@ -606,13 +605,26 @@ notifyWorker.on('failed', async job => {
606
605
  });
607
606
  });
608
607
 
609
- googlePubSub
610
- .start()
611
- .then(() => {
612
- logger.info({ msg: 'Started processing Google pub/sub' });
613
- })
614
- .catch(err => {
615
- logger.fatal({ msg: 'Failed to start processing Google pub/sub', err });
616
- });
608
+ let startRetryTimer = null;
609
+
610
+ (function startGooglePubSub(attempt) {
611
+ googlePubSub
612
+ .start()
613
+ .then(() => {
614
+ logger.info({ msg: 'Started processing Google pub/sub' });
615
+ })
616
+ .catch(err => {
617
+ let maxNormalAttempts = 20;
618
+ let delay;
619
+ if (attempt < maxNormalAttempts) {
620
+ delay = Math.min(5000 * Math.pow(2, Math.min(attempt, 10)), 60000);
621
+ logger.error({ msg: 'Failed to start processing Google pub/sub', err, attempt: attempt + 1, retryMs: delay });
622
+ } else {
623
+ delay = 5 * 60 * 1000;
624
+ logger.warn({ msg: 'Failed to start processing Google pub/sub (reduced frequency)', err, attempt: attempt + 1, retryMs: delay });
625
+ }
626
+ startRetryTimer = setTimeout(() => startGooglePubSub(attempt + 1), delay);
627
+ });
628
+ })(0);
617
629
 
618
630
  logger.info({ msg: 'Started Webhooks worker thread', version: packageData.version });