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.
- package/.github/workflows/test.yml +4 -0
- package/CHANGELOG.md +70 -0
- package/copy-static-files.sh +1 -1
- package/data/google-crawlers.json +1 -1
- package/eslint.config.js +2 -0
- package/lib/account.js +13 -9
- package/lib/api-routes/account-routes.js +7 -1
- package/lib/consts.js +17 -1
- package/lib/email-client/gmail/gmail-api.js +1 -12
- package/lib/email-client/imap-client.js +5 -3
- package/lib/email-client/outlook/graph-api.js +9 -15
- package/lib/email-client/outlook-client.js +406 -177
- package/lib/export.js +17 -0
- package/lib/imapproxy/imap-server.js +3 -2
- package/lib/oauth/gmail.js +12 -1
- package/lib/oauth/outlook.js +99 -1
- package/lib/oauth/pubsub/google.js +253 -85
- package/lib/oauth2-apps.js +620 -389
- package/lib/outbox.js +1 -1
- package/lib/routes-ui.js +193 -238
- package/lib/schemas.js +189 -12
- package/lib/ui-routes/account-routes.js +7 -2
- package/lib/ui-routes/admin-entities-routes.js +3 -3
- package/lib/ui-routes/oauth-routes.js +27 -175
- package/package.json +21 -21
- package/sbom.json +1 -1
- package/server.js +54 -22
- package/static/licenses.html +30 -90
- package/translations/de.mo +0 -0
- package/translations/de.po +54 -42
- package/translations/en.mo +0 -0
- package/translations/en.po +55 -43
- package/translations/et.mo +0 -0
- package/translations/et.po +54 -42
- package/translations/fr.mo +0 -0
- package/translations/fr.po +54 -42
- package/translations/ja.mo +0 -0
- package/translations/ja.po +54 -42
- package/translations/messages.pot +93 -71
- package/translations/nl.mo +0 -0
- package/translations/nl.po +54 -42
- package/translations/pl.mo +0 -0
- package/translations/pl.po +54 -42
- package/views/config/oauth/app.hbs +12 -0
- package/views/config/oauth/edit.hbs +2 -0
- package/views/config/oauth/index.hbs +4 -1
- package/views/config/oauth/new.hbs +2 -0
- package/views/config/oauth/subscriptions.hbs +175 -0
- package/views/error.hbs +4 -4
- package/views/partials/oauth_form.hbs +179 -4
- package/views/partials/oauth_tabs.hbs +8 -0
- package/views/partials/scope_info.hbs +10 -0
- package/workers/api.js +174 -96
- package/workers/documents.js +1 -0
- package/workers/export.js +6 -2
- package/workers/imap.js +33 -49
- package/workers/smtp.js +1 -0
- package/workers/submit.js +1 -0
- 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
|
|
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
package/workers/submit.js
CHANGED
package/workers/webhooks.js
CHANGED
|
@@ -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
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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 });
|