emailengine-app 2.65.0 → 2.67.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/deploy.yml +8 -8
- package/.github/workflows/release.yaml +9 -9
- package/.github/workflows/test.yml +2 -2
- package/CHANGELOG.md +53 -0
- package/bin/emailengine.js +3 -0
- package/data/google-crawlers.json +7 -1
- package/lib/account.js +35 -29
- package/lib/consts.js +5 -0
- package/lib/email-client/gmail-client.js +23 -27
- package/lib/email-client/imap/mailbox.js +46 -19
- package/lib/email-client/imap/sync-operations.js +51 -19
- package/lib/email-client/imap-client.js +28 -5
- package/lib/email-client/outlook-client.js +155 -1
- package/lib/oauth/gmail.js +52 -1
- package/lib/passkeys.js +206 -0
- package/lib/routes-ui.js +522 -21
- package/lib/ui-routes/oauth-routes.js +6 -1
- package/package.json +13 -11
- package/sbom.json +1 -1
- package/static/js/login-passkey.js +75 -0
- package/static/js/passkey-register.js +107 -0
- package/static/licenses.html +238 -38
- package/static/vendor/handlebars/handlebars.min-v4.7.9.js +29 -0
- package/static/vendor/simplewebauthn/browser.min.js +2 -0
- package/translations/de.mo +0 -0
- package/translations/de.po +91 -53
- package/translations/en.mo +0 -0
- package/translations/en.po +84 -52
- package/translations/et.mo +0 -0
- package/translations/et.po +95 -60
- package/translations/fr.mo +0 -0
- package/translations/fr.po +102 -65
- package/translations/ja.mo +0 -0
- package/translations/ja.po +93 -57
- package/translations/messages.pot +101 -76
- package/translations/nl.mo +0 -0
- package/translations/nl.po +92 -56
- package/translations/pl.mo +0 -0
- package/translations/pl.po +106 -70
- package/views/account/login.hbs +35 -25
- package/views/account/password.hbs +4 -4
- package/views/account/security.hbs +101 -12
- package/views/account/totp.hbs +3 -3
- package/views/config/oauth/app.hbs +25 -0
- package/views/layout/app.hbs +2 -2
- package/views/layout/login.hbs +6 -1
- package/views/oauth-scope-error.hbs +29 -0
- package/workers/api.js +81 -3
- package/workers/imap.js +4 -0
- package/static/vendor/handlebars/handlebars.min-v4.7.7.js +0 -29
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const { compareExisting, calculateFetchBackoff, readEnvValue } = require('../../tools');
|
|
5
5
|
const config = require('@zone-eu/wild-config');
|
|
6
|
-
const { DEFAULT_FETCH_BATCH_SIZE } = require('../../consts');
|
|
6
|
+
const { DEFAULT_FETCH_BATCH_SIZE, FETCH_RETRY_MAX_TIME, FETCH_RETRY_MIN_ATTEMPTS } = require('../../consts');
|
|
7
7
|
|
|
8
8
|
// Configurable batch size for fetching messages (default: 250)
|
|
9
9
|
const FETCH_BATCH_SIZE = Number(readEnvValue('EENGINE_FETCH_BATCH_SIZE') || config.service.fetchBatchSize) || DEFAULT_FETCH_BATCH_SIZE;
|
|
@@ -11,6 +11,15 @@ const FETCH_BATCH_SIZE = Number(readEnvValue('EENGINE_FETCH_BATCH_SIZE') || conf
|
|
|
11
11
|
// Do not check for flag updates using full sync more often than this value (30 minutes)
|
|
12
12
|
const FULL_SYNC_DELAY = 30 * 60 * 1000;
|
|
13
13
|
|
|
14
|
+
function checkFetchRetryTimeout(retryCount, startTime) {
|
|
15
|
+
if (retryCount >= FETCH_RETRY_MIN_ATTEMPTS && Date.now() - startTime > FETCH_RETRY_MAX_TIME) {
|
|
16
|
+
const err = new Error('FETCH retry time limit exceeded');
|
|
17
|
+
err.code = 'FetchRetryTimeout';
|
|
18
|
+
err.statusCode = 504;
|
|
19
|
+
throw err;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
14
23
|
/**
|
|
15
24
|
* Calculates the next range of sequence numbers to fetch based on the last fetched range
|
|
16
25
|
* @param {Number} totalMessages - Total number of messages in the mailbox
|
|
@@ -234,6 +243,7 @@ class SyncOperations {
|
|
|
234
243
|
// Fetch messages with retry logic
|
|
235
244
|
let fetchCompleted = false;
|
|
236
245
|
let fetchRetryCount = 0;
|
|
246
|
+
let fetchStartTime = Date.now();
|
|
237
247
|
|
|
238
248
|
while (!fetchCompleted) {
|
|
239
249
|
try {
|
|
@@ -312,9 +322,11 @@ class SyncOperations {
|
|
|
312
322
|
|
|
313
323
|
// Retry with exponential backoff
|
|
314
324
|
if (!imapClient.usable) {
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
325
|
+
// connection closed, throw so callers know sync was interrupted
|
|
326
|
+
const connErr = new Error('IMAP connection closed during sync');
|
|
327
|
+
connErr.code = 'IMAPConnectionClosing';
|
|
328
|
+
connErr.statusCode = 503;
|
|
329
|
+
throw connErr;
|
|
318
330
|
}
|
|
319
331
|
|
|
320
332
|
const fetchRetryDelay = calculateFetchBackoff(++fetchRetryCount);
|
|
@@ -322,10 +334,14 @@ class SyncOperations {
|
|
|
322
334
|
await new Promise(r => setTimeout(r, fetchRetryDelay));
|
|
323
335
|
|
|
324
336
|
if (!imapClient.usable) {
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
337
|
+
// connection closed, throw so callers know sync was interrupted
|
|
338
|
+
const connErr = new Error('IMAP connection closed during sync');
|
|
339
|
+
connErr.code = 'IMAPConnectionClosing';
|
|
340
|
+
connErr.statusCode = 503;
|
|
341
|
+
throw connErr;
|
|
328
342
|
}
|
|
343
|
+
|
|
344
|
+
checkFetchRetryTimeout(fetchRetryCount, fetchStartTime);
|
|
329
345
|
}
|
|
330
346
|
}
|
|
331
347
|
}
|
|
@@ -379,6 +395,7 @@ class SyncOperations {
|
|
|
379
395
|
// only fetch messages if there are some
|
|
380
396
|
let fetchCompleted = false;
|
|
381
397
|
let fetchRetryCount = 0;
|
|
398
|
+
let fetchStartTime = Date.now();
|
|
382
399
|
while (!fetchCompleted) {
|
|
383
400
|
// Get fresh imapClient reference inside retry loop
|
|
384
401
|
let imapClient = this.connection.imapClient;
|
|
@@ -461,9 +478,11 @@ class SyncOperations {
|
|
|
461
478
|
|
|
462
479
|
// Retry with exponential backoff
|
|
463
480
|
if (!imapClient.usable) {
|
|
464
|
-
//
|
|
465
|
-
|
|
466
|
-
|
|
481
|
+
// connection closed, throw so callers know sync was interrupted
|
|
482
|
+
const connErr = new Error('IMAP connection closed during sync');
|
|
483
|
+
connErr.code = 'IMAPConnectionClosing';
|
|
484
|
+
connErr.statusCode = 503;
|
|
485
|
+
throw connErr;
|
|
467
486
|
}
|
|
468
487
|
|
|
469
488
|
const fetchRetryDelay = calculateFetchBackoff(++fetchRetryCount);
|
|
@@ -471,10 +490,14 @@ class SyncOperations {
|
|
|
471
490
|
await new Promise(r => setTimeout(r, fetchRetryDelay));
|
|
472
491
|
|
|
473
492
|
if (!imapClient.usable) {
|
|
474
|
-
//
|
|
475
|
-
|
|
476
|
-
|
|
493
|
+
// connection closed, throw so callers know sync was interrupted
|
|
494
|
+
const connErr = new Error('IMAP connection closed during sync');
|
|
495
|
+
connErr.code = 'IMAPConnectionClosing';
|
|
496
|
+
connErr.statusCode = 503;
|
|
497
|
+
throw connErr;
|
|
477
498
|
}
|
|
499
|
+
|
|
500
|
+
checkFetchRetryTimeout(fetchRetryCount, fetchStartTime);
|
|
478
501
|
}
|
|
479
502
|
}
|
|
480
503
|
}
|
|
@@ -545,6 +568,8 @@ class SyncOperations {
|
|
|
545
568
|
let range = false;
|
|
546
569
|
let lastHighestUid = 0;
|
|
547
570
|
let batchNumber = 0;
|
|
571
|
+
let fetchStartTime = Date.now();
|
|
572
|
+
let totalFetchRetryCount = 0;
|
|
548
573
|
// process messages in batches
|
|
549
574
|
while ((range = getFetchRange(mailboxStatus.messages, range))) {
|
|
550
575
|
batchNumber++;
|
|
@@ -703,9 +728,11 @@ class SyncOperations {
|
|
|
703
728
|
});
|
|
704
729
|
|
|
705
730
|
if (!imapClient.usable) {
|
|
706
|
-
//
|
|
707
|
-
|
|
708
|
-
|
|
731
|
+
// connection closed, throw so callers know sync was interrupted
|
|
732
|
+
const connErr = new Error('IMAP connection closed during sync');
|
|
733
|
+
connErr.code = 'IMAPConnectionClosing';
|
|
734
|
+
connErr.statusCode = 503;
|
|
735
|
+
throw connErr;
|
|
709
736
|
}
|
|
710
737
|
|
|
711
738
|
try {
|
|
@@ -728,14 +755,17 @@ class SyncOperations {
|
|
|
728
755
|
}
|
|
729
756
|
|
|
730
757
|
// Retry with exponential backoff
|
|
758
|
+
totalFetchRetryCount++;
|
|
731
759
|
const fetchRetryDelay = calculateFetchBackoff(++fetchRetryCount);
|
|
732
760
|
this.logger.error({ msg: `FETCH failed, retrying in ${Math.round(fetchRetryDelay / 1000)}s`, loopId, batchNumber });
|
|
733
761
|
await new Promise(r => setTimeout(r, fetchRetryDelay));
|
|
734
762
|
|
|
735
763
|
if (!imapClient.usable) {
|
|
736
|
-
//
|
|
737
|
-
|
|
738
|
-
|
|
764
|
+
// connection closed, throw so callers know sync was interrupted
|
|
765
|
+
const connErr = new Error('IMAP connection closed during sync');
|
|
766
|
+
connErr.code = 'IMAPConnectionClosing';
|
|
767
|
+
connErr.statusCode = 503;
|
|
768
|
+
throw connErr;
|
|
739
769
|
}
|
|
740
770
|
|
|
741
771
|
// Verify we're still on the correct mailbox after the delay
|
|
@@ -764,6 +794,8 @@ class SyncOperations {
|
|
|
764
794
|
newMessages: mailboxStatus.messages,
|
|
765
795
|
range
|
|
766
796
|
});
|
|
797
|
+
|
|
798
|
+
checkFetchRetryTimeout(totalFetchRetryCount, fetchStartTime);
|
|
767
799
|
}
|
|
768
800
|
}
|
|
769
801
|
}
|
|
@@ -299,7 +299,9 @@ class IMAPClient extends BaseClient {
|
|
|
299
299
|
|
|
300
300
|
// Set up error handling for the command connection
|
|
301
301
|
const onErr = err => {
|
|
302
|
-
|
|
302
|
+
// Idle timeout (ETIMEOUT) is expected - the command client has no keepalive
|
|
303
|
+
const logLevel = err.code === 'ETIMEOUT' ? 'debug' : 'warn';
|
|
304
|
+
commandClient?.log[logLevel]({ msg: 'IMAP connection error', cid: commandCid, channel: 'command', account: this.account, err });
|
|
303
305
|
commandClient.close();
|
|
304
306
|
this.commandClient = null;
|
|
305
307
|
};
|
|
@@ -605,9 +607,15 @@ class IMAPClient extends BaseClient {
|
|
|
605
607
|
})
|
|
606
608
|
.filter(entry => entry);
|
|
607
609
|
|
|
610
|
+
// Build lookup map from stored listing for O(1) access
|
|
611
|
+
const storedListingMap = new Map();
|
|
612
|
+
for (const entry of storedListing) {
|
|
613
|
+
storedListingMap.set(normalizePath(entry.path), entry);
|
|
614
|
+
}
|
|
615
|
+
|
|
608
616
|
// compare listings to detect changes
|
|
609
617
|
for (let mailbox of listing) {
|
|
610
|
-
let existingMailbox =
|
|
618
|
+
let existingMailbox = storedListingMap.get(normalizePath(mailbox.path));
|
|
611
619
|
if (!existingMailbox) {
|
|
612
620
|
// found new!
|
|
613
621
|
mailbox.isNew = true;
|
|
@@ -622,8 +630,9 @@ class IMAPClient extends BaseClient {
|
|
|
622
630
|
}
|
|
623
631
|
|
|
624
632
|
// Check for deleted mailboxes
|
|
633
|
+
const listingPathSet = new Set(listing.map(mailbox => normalizePath(mailbox.path)));
|
|
625
634
|
for (let entry of storedListing) {
|
|
626
|
-
if (!
|
|
635
|
+
if (!listingPathSet.has(normalizePath(entry.path))) {
|
|
627
636
|
// found deleted!
|
|
628
637
|
await this.clearMailboxEntry(entry);
|
|
629
638
|
hasChanges = true;
|
|
@@ -1207,9 +1216,14 @@ class IMAPClient extends BaseClient {
|
|
|
1207
1216
|
}
|
|
1208
1217
|
|
|
1209
1218
|
// Provider-specific workarounds
|
|
1210
|
-
if (
|
|
1211
|
-
//
|
|
1219
|
+
if (/\.rambler\.ru$/i.test(imapConfig.host)) {
|
|
1220
|
+
// Rambler supports IDLE but needs it restarted frequently
|
|
1221
|
+
imapConfig.maxIdleTime = 55 * 1000;
|
|
1222
|
+
} else if (/\.163\.com$/i.test(imapConfig.host)) {
|
|
1223
|
+
// 163.com (Coremail) does not support IDLE and ignores NOOP for idle timer reset.
|
|
1224
|
+
// Use STATUS as the keepalive command - it counts as real activity for the server.
|
|
1212
1225
|
imapConfig.maxIdleTime = 55 * 1000;
|
|
1226
|
+
imapConfig.missingIdleCommand = 'STATUS';
|
|
1213
1227
|
} else if (/\.yahoo\.com$/i.test(imapConfig.host)) {
|
|
1214
1228
|
// Special case for Yahoo. Break IDLE at least once every three minutes
|
|
1215
1229
|
imapConfig.maxIdleTime = 3 * 60 * 1000;
|
|
@@ -2461,6 +2475,9 @@ class IMAPClient extends BaseClient {
|
|
|
2461
2475
|
const mailboxes = [];
|
|
2462
2476
|
|
|
2463
2477
|
const listing = await this.getCurrentListing(false, { allowSecondary: true });
|
|
2478
|
+
// Capture reference so we can close it after the await-heavy loop below
|
|
2479
|
+
// without risking closure of a different command client obtained by a concurrent caller
|
|
2480
|
+
const commandClientForListing = this.commandClient;
|
|
2464
2481
|
|
|
2465
2482
|
// Process each configured subconnection
|
|
2466
2483
|
for (const path of accountData.subconnections || []) {
|
|
@@ -2588,6 +2605,12 @@ class IMAPClient extends BaseClient {
|
|
|
2588
2605
|
await subconnection.init();
|
|
2589
2606
|
}
|
|
2590
2607
|
|
|
2608
|
+
// Close the command client if it is still the same instance we captured above
|
|
2609
|
+
if (commandClientForListing && this.commandClient === commandClientForListing) {
|
|
2610
|
+
this.commandClient.close();
|
|
2611
|
+
this.commandClient = null;
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2591
2614
|
return this.subconnections.length;
|
|
2592
2615
|
}
|
|
2593
2616
|
|
|
@@ -2875,7 +2875,7 @@ class OutlookClient extends BaseClient {
|
|
|
2875
2875
|
return this.oAuth2Client;
|
|
2876
2876
|
}
|
|
2877
2877
|
let accountData = await (this.delegatedAccountObject || this.accountObject).loadAccountData();
|
|
2878
|
-
this.oAuth2Client = await oauth2Apps.getClient(accountData.oauth2.
|
|
2878
|
+
this.oAuth2Client = await oauth2Apps.getClient(accountData.oauth2?.provider || accountData._app?.id, {
|
|
2879
2879
|
logger: this.logger,
|
|
2880
2880
|
logRaw: this.options.logRaw
|
|
2881
2881
|
});
|
|
@@ -3857,8 +3857,11 @@ class OutlookClient extends BaseClient {
|
|
|
3857
3857
|
async processHistory() {
|
|
3858
3858
|
let event;
|
|
3859
3859
|
let newMessageOptions;
|
|
3860
|
+
let processedAny = false;
|
|
3860
3861
|
|
|
3861
3862
|
while ((event = await this.accountObject.pullQueueEvent()) !== null) {
|
|
3863
|
+
processedAny = true;
|
|
3864
|
+
|
|
3862
3865
|
switch (event.type) {
|
|
3863
3866
|
case 'updated':
|
|
3864
3867
|
await this.processUpdatedMessage(event.message);
|
|
@@ -3900,6 +3903,157 @@ class OutlookClient extends BaseClient {
|
|
|
3900
3903
|
break;
|
|
3901
3904
|
}
|
|
3902
3905
|
}
|
|
3906
|
+
|
|
3907
|
+
if (processedAny) {
|
|
3908
|
+
await this._updateLastNotificationTime();
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3912
|
+
async _updateLastNotificationTime() {
|
|
3913
|
+
try {
|
|
3914
|
+
await this.redis.hset(this.getAccountKey(), 'outlookLastNotification', Date.now().toString());
|
|
3915
|
+
} catch (err) {
|
|
3916
|
+
this.logger.error({ msg: 'Failed to update last notification time', err });
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
|
|
3920
|
+
/**
|
|
3921
|
+
* Recover messages missed due to failed webhook delivery.
|
|
3922
|
+
* Triggered by MS Graph 'missed' lifecycle event.
|
|
3923
|
+
* Queries recent messages and processes any not already seen via normal webhooks.
|
|
3924
|
+
* @returns {boolean} Whether recovery ran (false if debounced)
|
|
3925
|
+
*/
|
|
3926
|
+
async syncMissedMessages() {
|
|
3927
|
+
// Debounce: skip if recovery already ran recently
|
|
3928
|
+
const cooldownKey = `${REDIS_PREFIX}iam:${this.account}:missedRecovery`;
|
|
3929
|
+
const acquired = await this.redis.set(cooldownKey, '1', 'EX', 300, 'NX');
|
|
3930
|
+
if (!acquired) {
|
|
3931
|
+
this.logger.debug({ msg: 'Missed notification recovery skipped (cooldown active)', account: this.account });
|
|
3932
|
+
return false;
|
|
3933
|
+
}
|
|
3934
|
+
|
|
3935
|
+
// Determine lookback window
|
|
3936
|
+
let sinceTime;
|
|
3937
|
+
const lastNotification = await this.redis.hget(this.getAccountKey(), 'outlookLastNotification');
|
|
3938
|
+
if (lastNotification) {
|
|
3939
|
+
// 2-minute safety margin before last known good notification
|
|
3940
|
+
sinceTime = new Date(Number(lastNotification) - 2 * 60 * 1000);
|
|
3941
|
+
} else {
|
|
3942
|
+
sinceTime = new Date(Date.now() - 30 * 60 * 1000);
|
|
3943
|
+
}
|
|
3944
|
+
|
|
3945
|
+
this.logger.info({
|
|
3946
|
+
msg: 'Recovering missed notifications',
|
|
3947
|
+
account: this.account,
|
|
3948
|
+
since: sinceTime.toISOString()
|
|
3949
|
+
});
|
|
3950
|
+
|
|
3951
|
+
let messages = [];
|
|
3952
|
+
let reqUrl = `/${this.oauth2UserPath}/messages`;
|
|
3953
|
+
let reqPayload = {
|
|
3954
|
+
$filter: `receivedDateTime gt ${sinceTime.toISOString()}`,
|
|
3955
|
+
$select: 'id,parentFolderId',
|
|
3956
|
+
$top: 250,
|
|
3957
|
+
$orderby: 'receivedDateTime desc'
|
|
3958
|
+
};
|
|
3959
|
+
|
|
3960
|
+
try {
|
|
3961
|
+
while (reqUrl) {
|
|
3962
|
+
let pageResult = await this.request(reqUrl, 'get', reqPayload);
|
|
3963
|
+
|
|
3964
|
+
if (pageResult?.value?.length) {
|
|
3965
|
+
messages.push(...pageResult.value);
|
|
3966
|
+
if (messages.length >= 1000) {
|
|
3967
|
+
messages.length = 1000;
|
|
3968
|
+
break;
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
|
|
3972
|
+
if (pageResult['@odata.nextLink']) {
|
|
3973
|
+
reqUrl = pageResult['@odata.nextLink'];
|
|
3974
|
+
reqPayload = null; // nextLink includes all query params
|
|
3975
|
+
} else {
|
|
3976
|
+
reqUrl = null;
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
} catch (err) {
|
|
3980
|
+
this.logger.error({ msg: 'Failed to query recent messages for missed notification recovery', account: this.account, err });
|
|
3981
|
+
await this.redis.del(cooldownKey);
|
|
3982
|
+
return false;
|
|
3983
|
+
}
|
|
3984
|
+
|
|
3985
|
+
if (!messages.length) {
|
|
3986
|
+
this.logger.debug({ msg: 'No recent messages found during missed notification recovery', account: this.account });
|
|
3987
|
+
return true;
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3990
|
+
this.logger.info({
|
|
3991
|
+
msg: 'Found recent messages for missed notification recovery',
|
|
3992
|
+
account: this.account,
|
|
3993
|
+
count: messages.length
|
|
3994
|
+
});
|
|
3995
|
+
|
|
3996
|
+
let newMessageOptions = await this.getMessageFetchOptions();
|
|
3997
|
+
newMessageOptions.showPath = true;
|
|
3998
|
+
|
|
3999
|
+
// Pre-resolve unique folder IDs once to avoid per-message Redis lookups
|
|
4000
|
+
let folderMap = new Map();
|
|
4001
|
+
let cachedListing = await this.getCachedMailboxListing();
|
|
4002
|
+
if (cachedListing) {
|
|
4003
|
+
for (let msg of messages) {
|
|
4004
|
+
if (msg.parentFolderId && !folderMap.has(msg.parentFolderId)) {
|
|
4005
|
+
let folder = cachedListing.find(entry => entry.id === msg.parentFolderId);
|
|
4006
|
+
if (folder) {
|
|
4007
|
+
folderMap.set(msg.parentFolderId, folder);
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
|
|
4013
|
+
let recoveredCount = 0;
|
|
4014
|
+
|
|
4015
|
+
for (const msg of messages) {
|
|
4016
|
+
try {
|
|
4017
|
+
// Pre-filter: check rollingBucketLock BEFORE the expensive getMessage() call
|
|
4018
|
+
let preFilterChecked = false;
|
|
4019
|
+
let folder = folderMap.get(msg.parentFolderId);
|
|
4020
|
+
if (folder) {
|
|
4021
|
+
let recentlySeen = await this.rollingBucketLock(`${msg.id}:created`, folder.pathName);
|
|
4022
|
+
if (recentlySeen) {
|
|
4023
|
+
continue;
|
|
4024
|
+
}
|
|
4025
|
+
preFilterChecked = true;
|
|
4026
|
+
}
|
|
4027
|
+
|
|
4028
|
+
let messageData = await this.prepareNewMessage(msg.id, newMessageOptions);
|
|
4029
|
+
if (!messageData) {
|
|
4030
|
+
continue;
|
|
4031
|
+
}
|
|
4032
|
+
|
|
4033
|
+
if (!preFilterChecked) {
|
|
4034
|
+
let recentlySeen = await this.rollingBucketLock(`${messageData.id}:created`, messageData.path);
|
|
4035
|
+
if (recentlySeen) {
|
|
4036
|
+
continue;
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
|
|
4040
|
+
await this.processNew(messageData, newMessageOptions);
|
|
4041
|
+
recoveredCount++;
|
|
4042
|
+
} catch (err) {
|
|
4043
|
+
this.logger.error({ msg: 'Failed to process message during missed notification recovery', emailId: msg.id, account: this.account, err });
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
this.logger.info({
|
|
4048
|
+
msg: 'Missed notification recovery completed',
|
|
4049
|
+
account: this.account,
|
|
4050
|
+
checked: messages.length,
|
|
4051
|
+
recovered: recoveredCount
|
|
4052
|
+
});
|
|
4053
|
+
|
|
4054
|
+
await this._updateLastNotificationTime();
|
|
4055
|
+
|
|
4056
|
+
return true;
|
|
3903
4057
|
}
|
|
3904
4058
|
|
|
3905
4059
|
/**
|
package/lib/oauth/gmail.js
CHANGED
|
@@ -20,6 +20,8 @@ const GMAIL_API_SCOPES = {
|
|
|
20
20
|
labels: 'https://www.googleapis.com/auth/gmail.labels'
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
const OPENID_SCOPES = ['openid', 'email', 'profile'];
|
|
24
|
+
|
|
23
25
|
const EXPOSE_PARTIAL_SECRET_KEY_REGEX = /Unauthorized/i;
|
|
24
26
|
|
|
25
27
|
const checkForFlags = (err, isPrincipal) => {
|
|
@@ -171,7 +173,7 @@ class GmailOauth {
|
|
|
171
173
|
// Service accounts use JWT-based authentication and don't need OpenID scopes
|
|
172
174
|
// These are non-privileged scopes that provide email and profile info for 3-legged OAuth
|
|
173
175
|
if (!opts.serviceClient) {
|
|
174
|
-
const openidScopes =
|
|
176
|
+
const openidScopes = OPENID_SCOPES;
|
|
175
177
|
|
|
176
178
|
// Add OpenID scopes if not already present
|
|
177
179
|
for (let scope of openidScopes) {
|
|
@@ -203,6 +205,7 @@ class GmailOauth {
|
|
|
203
205
|
this.setFlag = opts.setFlag;
|
|
204
206
|
|
|
205
207
|
this.tokenUrl = `https://oauth2.googleapis.com/token`;
|
|
208
|
+
this.revokeUrl = `https://oauth2.googleapis.com/revoke`;
|
|
206
209
|
}
|
|
207
210
|
|
|
208
211
|
generateAuthUrl(opts) {
|
|
@@ -675,8 +678,56 @@ class GmailOauth {
|
|
|
675
678
|
const signature = crypto.createSign('RSA-SHA256').update(encodedPayload).sign(this.serviceKey);
|
|
676
679
|
return [encodedPayload, Buffer.from(signature).toString('base64url')].join('.');
|
|
677
680
|
}
|
|
681
|
+
|
|
682
|
+
async revokeToken(token) {
|
|
683
|
+
const fetchOpts = {
|
|
684
|
+
method: 'post',
|
|
685
|
+
headers: {
|
|
686
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
687
|
+
'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage})`
|
|
688
|
+
},
|
|
689
|
+
body: new URLSearchParams({ token }).toString()
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
try {
|
|
693
|
+
let res = await fetchCmd(this.revokeUrl, Object.assign(fetchOpts, { dispatcher: httpAgent.retry }));
|
|
694
|
+
|
|
695
|
+
let responseText;
|
|
696
|
+
try {
|
|
697
|
+
responseText = await res.text();
|
|
698
|
+
} catch (err) {
|
|
699
|
+
// ignore
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (this.logger) {
|
|
703
|
+
this.logger.info({
|
|
704
|
+
msg: 'OAuth2 token revocation request',
|
|
705
|
+
action: 'oauth2Fetch',
|
|
706
|
+
fn: 'revokeToken',
|
|
707
|
+
url: this.revokeUrl,
|
|
708
|
+
success: !!res.ok,
|
|
709
|
+
status: res.status
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (!res.ok) {
|
|
714
|
+
if (this.logger) {
|
|
715
|
+
this.logger.warn({
|
|
716
|
+
msg: 'OAuth2 token revocation failed',
|
|
717
|
+
status: res.status,
|
|
718
|
+
response: responseText
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
} catch (err) {
|
|
723
|
+
if (this.logger) {
|
|
724
|
+
this.logger.error({ msg: 'OAuth2 token revocation error', err });
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
678
728
|
}
|
|
679
729
|
|
|
680
730
|
module.exports.GmailOauth = GmailOauth;
|
|
681
731
|
module.exports.GMAIL_SCOPES = GMAIL_SCOPES;
|
|
682
732
|
module.exports.GMAIL_API_SCOPES = GMAIL_API_SCOPES;
|
|
733
|
+
module.exports.OPENID_SCOPES = OPENID_SCOPES;
|