emailengine-app 2.63.3 → 2.64.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/CHANGELOG.md +66 -0
- package/data/google-crawlers.json +1 -1
- package/eslint.config.js +2 -0
- package/lib/account.js +6 -2
- package/lib/consts.js +17 -1
- package/lib/email-client/gmail/gmail-api.js +1 -12
- package/lib/email-client/imap/mailbox.js +24 -6
- package/lib/email-client/imap/sync-operations.js +17 -5
- package/lib/email-client/imap-client.js +25 -16
- package/lib/email-client/outlook/graph-api.js +7 -13
- package/lib/email-client/outlook-client.js +363 -167
- package/lib/imapproxy/imap-server.js +1 -0
- package/lib/oauth/gmail.js +12 -1
- package/lib/oauth/pubsub/google.js +253 -85
- package/lib/oauth2-apps.js +554 -377
- package/lib/routes-ui.js +186 -91
- package/lib/schemas.js +18 -1
- package/lib/tools.js +6 -0
- package/lib/ui-routes/account-routes.js +1 -1
- package/lib/ui-routes/admin-entities-routes.js +3 -3
- package/lib/ui-routes/oauth-routes.js +9 -3
- package/package.json +13 -13
- package/sbom.json +1 -1
- package/server.js +54 -22
- package/static/licenses.html +39 -29
- 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 +74 -52
- 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/index.hbs +2 -0
- package/views/config/oauth/subscriptions.hbs +175 -0
- package/views/error.hbs +4 -4
- package/views/partials/oauth_tabs.hbs +8 -0
- package/workers/api.js +174 -96
- package/workers/documents.js +1 -0
- package/workers/imap.js +30 -47
- package/workers/smtp.js +1 -0
- package/workers/submit.js +1 -0
- package/workers/webhooks.js +42 -30
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,71 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.64.0](https://github.com/postalsys/emailengine/compare/v2.63.4...v2.64.0) (2026-03-16)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add configurable Gmail Pub/Sub subscription TTL setting ([ca33e7f](https://github.com/postalsys/emailengine/commit/ca33e7f9101b3375d7e0001cdbe1d7a41a9442d3))
|
|
9
|
+
* add Gmail Subscriptions tab to OAuth config page ([3bd30bf](https://github.com/postalsys/emailengine/commit/3bd30bf0bbc26600eae40d42e95ef7976257e4f2))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* add .catch() to fire-and-forget setMeta and track error fingerprints ([672a03e](https://github.com/postalsys/emailengine/commit/672a03e2524bcaa26c4d3c908fa828f4e636999b))
|
|
15
|
+
* add lock to del() to prevent race with ensurePubsub, fix pubSubApp cleanup ([27f2bdd](https://github.com/postalsys/emailengine/commit/27f2bdd4a9e3d7e913e7f4bed96084a278bb6312))
|
|
16
|
+
* always notify webhook workers when Pub/Sub app config changes ([e9f276a](https://github.com/postalsys/emailengine/commit/e9f276ab528ad384bcbf5c370a0d0cb22e084cc5))
|
|
17
|
+
* auto-recover expired Gmail Pub/Sub subscriptions and expose status via API ([7961ceb](https://github.com/postalsys/emailengine/commit/7961ceb8ce81688e5c3bb435a6c33ed3882afd7b))
|
|
18
|
+
* avoid redundant Redis call and concurrent backfill races in Pub/Sub setup ([05b02af](https://github.com/postalsys/emailengine/commit/05b02afc1c14de1b8fa4e272d26da2056280731a))
|
|
19
|
+
* broaden Pub/Sub notification condition in oauth-routes.js ([cbe1380](https://github.com/postalsys/emailengine/commit/cbe13800ab9460c41f5dcdcd4d7ae50f8e5e75e3))
|
|
20
|
+
* clean up Pub/Sub Redis keys on deletion, refresh stale instances, and localize UI labels ([3b35fa1](https://github.com/postalsys/emailengine/commit/3b35fa19a6a8024a60d2ed21e9fcd69fda349481))
|
|
21
|
+
* clear stale pubSubFlag after restart and consolidate backfill push ([f67961f](https://github.com/postalsys/emailengine/commit/f67961fbc590aa586ef40b65c2189b5dab4d89c0))
|
|
22
|
+
* correct pubSubApp property name in del() and add cleanup tests ([016ae0a](https://github.com/postalsys/emailengine/commit/016ae0a98bc52eeee2bd1ec4f4d0ed58e92c507d))
|
|
23
|
+
* correct typo in Pub/Sub schema version log message ([2cc8e08](https://github.com/postalsys/emailengine/commit/2cc8e085463ed550e341b027e592b5351784182e))
|
|
24
|
+
* eliminate redundant Redis ops in Pub/Sub pull loop and backfill ([c8ebe39](https://github.com/postalsys/emailengine/commit/c8ebe393744eee46c3d19b5be413badd8ab315d6))
|
|
25
|
+
* fix lifecycle event races, lock TTL, and stale clearExisting in MS Graph subscriptions ([3f3cbac](https://github.com/postalsys/emailengine/commit/3f3cbac843c4c221644492855d7c1beb41cd1da0))
|
|
26
|
+
* fix missing renewal retry, clock-skew gap, and incomplete pipeline error check ([8a8b5b6](https://github.com/postalsys/emailengine/commit/8a8b5b68feb3edd7c984707a85ddb6d146739020))
|
|
27
|
+
* fix off-by-one retry cap and simplify MS Graph subscription code ([aa9afbc](https://github.com/postalsys/emailengine/commit/aa9afbc5aea20d1bf775e59dbfd82842c4f8b701))
|
|
28
|
+
* fix Pub/Sub deletion race, add 429 handling, batch ACKs, and lock ensurePubsub ([744c354](https://github.com/postalsys/emailengine/commit/744c354a062f63eedcec8de1a324a5d13519b9a7))
|
|
29
|
+
* fix retry boundary, lock races, and dropped lifecycle events in MS Graph subscriptions ([d62741d](https://github.com/postalsys/emailengine/commit/d62741d2a2aafc47d831482b71ff24fa4c9d1883))
|
|
30
|
+
* fix silent 401/403 error suppression, floating promise, and recovery loop in Pub/Sub ([406211c](https://github.com/postalsys/emailengine/commit/406211cbd62f7d3f1b811fbec81767f319219c63))
|
|
31
|
+
* fix stale subscription state blocking recovery and retry count persisting across reconnects ([60b77d5](https://github.com/postalsys/emailengine/commit/60b77d5d7574abf728c5e1f015c0cecddd5d9fdd))
|
|
32
|
+
* fix subscription loss on subscriptionRemoved, lifecycle webhook timeout, and retry gaps ([62988ab](https://github.com/postalsys/emailengine/commit/62988ab3b690ed98583fa01557a9c4145427e3ae))
|
|
33
|
+
* guard fire-and-forget setMeta calls and add Pub/Sub graceful shutdown ([45f7b64](https://github.com/postalsys/emailengine/commit/45f7b64d7692b220ff14ec0e1c0e9deea84731de))
|
|
34
|
+
* guard releaseLock against undefined, add 429 handling to Pub/Sub deletion ([d67ebe1](https://github.com/postalsys/emailengine/commit/d67ebe138ca9663796eacc5a1c6b5b73e721e6c4))
|
|
35
|
+
* handle TimeoutError from AbortSignal.timeout in Pub/Sub pull loop ([c0e3036](https://github.com/postalsys/emailengine/commit/c0e303641e4eb9f6cdca3c584861052b2bd91148))
|
|
36
|
+
* harden MS Graph subscription lifecycle, locking, cleanup, and error recovery ([8210056](https://github.com/postalsys/emailengine/commit/8210056f7e7b00977fb5487323ff1b8f9f4b7a99))
|
|
37
|
+
* harden Pub/Sub deletion, IAM policy, and worker timeout cleanup ([0be7c3e](https://github.com/postalsys/emailengine/commit/0be7c3eb04f3381306284e1e3790f7c3cc28cb5a))
|
|
38
|
+
* harden Pub/Sub pull loop resilience and fix projectId typo ([8124328](https://github.com/postalsys/emailengine/commit/81243287f339b5fe40cddd0e1d127514d2e11f4a))
|
|
39
|
+
* harden Pub/Sub pull loop, message ACK, list pairing, and shutdown cleanup ([9e9775f](https://github.com/postalsys/emailengine/commit/9e9775f6df97af42bdb8b799c2345a4564f8a77a))
|
|
40
|
+
* harden Pub/Sub pull loop, worker coordination, and OAuth app lifecycle ([529b705](https://github.com/postalsys/emailengine/commit/529b7055dd4aeb0b1d80fabff3f139cf23f00252))
|
|
41
|
+
* harden Pub/Sub shutdown, loop scheduling, and abort lifecycle ([71b3ab4](https://github.com/postalsys/emailengine/commit/71b3ab4625fa3304ae7a4413a3b0a64276948d7f))
|
|
42
|
+
* harden Pub/Sub shutdown, transient error handling, and input validation ([cfc6e0b](https://github.com/postalsys/emailengine/commit/cfc6e0bb73902600b6eecc6bc955af85f06d9b0f))
|
|
43
|
+
* localize error page strings using translation helper ([609ebb9](https://github.com/postalsys/emailengine/commit/609ebb94d1e662ac661f7efe22f6608d7823fb85))
|
|
44
|
+
* move pubsub status from unauthenticated /health to GET /v1/pubsub/status ([f6597ad](https://github.com/postalsys/emailengine/commit/f6597addff4de1c23fbd88964702bc2425f14d0c))
|
|
45
|
+
* prevent oscillating recovery loop for Pub/Sub apps missing googleProjectId ([8bdbc39](https://github.com/postalsys/emailengine/commit/8bdbc39c790b7097b02459b558328c18584585a2))
|
|
46
|
+
* reduce Pub/Sub recovery log noise and respect backoff delay ([63cd78f](https://github.com/postalsys/emailengine/commit/63cd78fcfd0c12934e488f6936109653371f0899))
|
|
47
|
+
* remove dead circuit breaker code from IMAP and webhooks workers ([da9295b](https://github.com/postalsys/emailengine/commit/da9295b951d20a5817116195689dd3c964b3d996))
|
|
48
|
+
* remove Pub/Sub circuit breaker, fix 401/403 recovery handling ([b7017f3](https://github.com/postalsys/emailengine/commit/b7017f34478dbb3bcedfb9a1e098a8a45cb0f83d))
|
|
49
|
+
* reorder OAuth app deletion to prevent pull-loop gap, add startLoop tests ([129f96c](https://github.com/postalsys/emailengine/commit/129f96c99ff9ff2552fa1c1962b1e0a652b8837e))
|
|
50
|
+
* replace custom Redis locks with ioredfour in Outlook subscription code ([019eaa1](https://github.com/postalsys/emailengine/commit/019eaa1f26be504d127cb6c8a05dfe50d8a47b71))
|
|
51
|
+
* resolve lint errors, fix TTL null guard, and extract Pub/Sub constants ([4949431](https://github.com/postalsys/emailengine/commit/4949431c8e05f06c5aafc3dfe93876ea8a2875b9))
|
|
52
|
+
* resolve livelock, timeout, and state corruption in MS Graph subscription lifecycle ([745334e](https://github.com/postalsys/emailengine/commit/745334e8c55fcf4b0b3fc6adef2b37f6cf9aea61))
|
|
53
|
+
* retry transient errors during Pub/Sub resource deletion and log dropped messages ([b9d9de5](https://github.com/postalsys/emailengine/commit/b9d9de56a50b37e0163ea4e9a954d65bc305004c))
|
|
54
|
+
* show OAuth apps with failed Pub/Sub setup in subscriptions list ([14124cd](https://github.com/postalsys/emailengine/commit/14124cd29baf107f1f82dff48695bc51cae4fd97))
|
|
55
|
+
* stop Pub/Sub instances on OAuth2 app deletion and harden lifecycle ([bc0ff75](https://github.com/postalsys/emailengine/commit/bc0ff758cea8d8713485e3a28e81b16ee56d1dc7))
|
|
56
|
+
* surface TTL reconciliation failures to operators via ttlWarning meta flag ([6e5d5c5](https://github.com/postalsys/emailengine/commit/6e5d5c5a97fb9aaff2662f3fe2f90c8226359b4a))
|
|
57
|
+
* update imapflow to 1.2.15 to fix unhandled rejection crashes ([494a3f8](https://github.com/postalsys/emailengine/commit/494a3f8cd9e71bc1fc1683bea2fc92474cb33206))
|
|
58
|
+
|
|
59
|
+
## [2.63.4](https://github.com/postalsys/emailengine/compare/v2.63.3...v2.63.4) (2026-03-09)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
### Bug Fixes
|
|
63
|
+
|
|
64
|
+
* guard null imapClient dereferences during async operations ([671bcee](https://github.com/postalsys/emailengine/commit/671bcee4e81c7c0d6421617c3e25ec6906cb1abb))
|
|
65
|
+
* prevent null dereference crash in getImapConnection during connection drops ([6a356fd](https://github.com/postalsys/emailengine/commit/6a356fd20e1412a070c26c093108c4ea2a722a5c))
|
|
66
|
+
* throw instead of silent return in select() null guard, revert redundant optional chaining ([cb7db50](https://github.com/postalsys/emailengine/commit/cb7db5024e0b00f8a773328dd5e0009067bed0b6))
|
|
67
|
+
* update tests for getCurrentListing null guard changes ([a644f9c](https://github.com/postalsys/emailengine/commit/a644f9c1c3f3e4cbce388e43bfa319f11f4a469c))
|
|
68
|
+
|
|
3
69
|
## [2.63.3](https://github.com/postalsys/emailengine/compare/v2.63.2...v2.63.3) (2026-03-05)
|
|
4
70
|
|
|
5
71
|
|
package/eslint.config.js
CHANGED
package/lib/account.js
CHANGED
|
@@ -2374,7 +2374,9 @@ class Account {
|
|
|
2374
2374
|
});
|
|
2375
2375
|
}
|
|
2376
2376
|
} finally {
|
|
2377
|
-
|
|
2377
|
+
if (flushLock?.success) {
|
|
2378
|
+
await lock.releaseLock(flushLock);
|
|
2379
|
+
}
|
|
2378
2380
|
}
|
|
2379
2381
|
}
|
|
2380
2382
|
|
|
@@ -2490,7 +2492,9 @@ class Account {
|
|
|
2490
2492
|
|
|
2491
2493
|
throw err;
|
|
2492
2494
|
} finally {
|
|
2493
|
-
|
|
2495
|
+
if (renewLock?.success) {
|
|
2496
|
+
await lock.releaseLock(renewLock);
|
|
2497
|
+
}
|
|
2494
2498
|
}
|
|
2495
2499
|
}
|
|
2496
2500
|
|
package/lib/consts.js
CHANGED
|
@@ -199,11 +199,27 @@ module.exports = {
|
|
|
199
199
|
OUTLOOK_EXPIRATION_RENEW_TIME: 24 * 60 * 60 * 1000, // Renew when less than 24 hours remain
|
|
200
200
|
|
|
201
201
|
// MS Graph API retry and rate limiting settings
|
|
202
|
-
OUTLOOK_SUBSCRIPTION_LOCK_TTL: 60, //
|
|
202
|
+
OUTLOOK_SUBSCRIPTION_LOCK_TTL: 4 * 60 * 1000, // ms - lock TTL for subscription operations (ioredfour uses ms)
|
|
203
203
|
OUTLOOK_MAX_BATCH_SIZE: 20, // MS Graph batch request limit
|
|
204
204
|
OUTLOOK_MAX_RETRY_ATTEMPTS: 3, // Maximum retry attempts for rate-limited requests
|
|
205
205
|
OUTLOOK_RETRY_BASE_DELAY: 30, // seconds - base delay for exponential backoff
|
|
206
206
|
OUTLOOK_RETRY_MAX_DELAY: 120, // seconds - maximum delay between retries
|
|
207
|
+
OUTLOOK_CLOCK_SKEW_BUFFER: 60 * 1000, // ms - buffer for clock skew between EmailEngine and MS Graph servers
|
|
208
|
+
|
|
209
|
+
// Google Pub/Sub subscription defaults
|
|
210
|
+
GMAIL_PUBSUB_DEFAULT_EXPIRATION_TTL: '2678400s', // 31 days in seconds (Google's default)
|
|
211
|
+
GMAIL_PUBSUB_ACK_DEADLINE_SECONDS: 30,
|
|
212
|
+
// Transient network error codes that indicate a retry-worthy connection failure
|
|
213
|
+
TRANSIENT_NETWORK_CODES: new Set([
|
|
214
|
+
'ENOTFOUND',
|
|
215
|
+
'EAI_AGAIN',
|
|
216
|
+
'ETIMEDOUT',
|
|
217
|
+
'ECONNRESET',
|
|
218
|
+
'ECONNREFUSED',
|
|
219
|
+
'UND_ERR_SOCKET',
|
|
220
|
+
'UND_ERR_CONNECT_TIMEOUT',
|
|
221
|
+
'UND_ERR_HEADERS_TIMEOUT'
|
|
222
|
+
]),
|
|
207
223
|
|
|
208
224
|
generateWebhookTable() {
|
|
209
225
|
let entries = [];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { metricsMeta } = require('../base-client');
|
|
4
|
+
const { TRANSIENT_NETWORK_CODES } = require('../../consts');
|
|
4
5
|
|
|
5
6
|
// Gmail API configuration
|
|
6
7
|
const GMAIL_API_BASE = 'https://gmail.googleapis.com';
|
|
@@ -12,18 +13,6 @@ const LIST_BATCH_SIZE = 10;
|
|
|
12
13
|
const MAX_RETRY_ATTEMPTS = 3;
|
|
13
14
|
const RETRY_BASE_DELAY = 1000; // 1 second base delay
|
|
14
15
|
|
|
15
|
-
// Network-level errors that are transient and should be retried
|
|
16
|
-
const TRANSIENT_NETWORK_CODES = new Set([
|
|
17
|
-
'ENOTFOUND',
|
|
18
|
-
'EAI_AGAIN',
|
|
19
|
-
'ETIMEDOUT',
|
|
20
|
-
'ECONNRESET',
|
|
21
|
-
'ECONNREFUSED',
|
|
22
|
-
'UND_ERR_SOCKET',
|
|
23
|
-
'UND_ERR_CONNECT_TIMEOUT',
|
|
24
|
-
'UND_ERR_HEADERS_TIMEOUT'
|
|
25
|
-
]);
|
|
26
|
-
|
|
27
16
|
// Gmail API error code mapping to internal error codes
|
|
28
17
|
// https://developers.google.com/gmail/api/reference/rest#error-codes
|
|
29
18
|
const GMAIL_ERROR_MAP = {
|
|
@@ -94,7 +94,10 @@ class Mailbox {
|
|
|
94
94
|
getMailboxStatus(connectionClient) {
|
|
95
95
|
connectionClient = connectionClient || this.connection.imapClient;
|
|
96
96
|
if (!connectionClient) {
|
|
97
|
-
|
|
97
|
+
let err = new Error('IMAP connection not available');
|
|
98
|
+
err.code = 'IMAPConnectionClosing';
|
|
99
|
+
err.statusCode = 503;
|
|
100
|
+
throw err;
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
let mailboxInfo = connectionClient.mailbox;
|
|
@@ -490,6 +493,12 @@ class Mailbox {
|
|
|
490
493
|
* @param {Boolean} skipIdle - Don't start IDLE after selecting
|
|
491
494
|
*/
|
|
492
495
|
async select(skipIdle) {
|
|
496
|
+
if (!this.connection.imapClient) {
|
|
497
|
+
let err = new Error('IMAP connection not available');
|
|
498
|
+
err.code = 'IMAPConnectionClosing';
|
|
499
|
+
err.statusCode = 503;
|
|
500
|
+
throw err;
|
|
501
|
+
}
|
|
493
502
|
const currentLock = this.connection.imapClient.currentLock;
|
|
494
503
|
// Avoid interfering with any active operations
|
|
495
504
|
if (currentLock) {
|
|
@@ -520,7 +529,7 @@ class Mailbox {
|
|
|
520
529
|
|
|
521
530
|
// Check if we still need to select after getting the lock
|
|
522
531
|
// Another operation might have already selected this mailbox while we were waiting
|
|
523
|
-
if (this.connection.imapClient
|
|
532
|
+
if (this.connection.imapClient?.mailbox && this.connection.imapClient.mailbox.path === this.path) {
|
|
524
533
|
this.logger.trace({
|
|
525
534
|
msg: 'Mailbox already selected after lock acquired',
|
|
526
535
|
path: this.path
|
|
@@ -551,7 +560,10 @@ class Mailbox {
|
|
|
551
560
|
connectionClient = connectionClient || this.connection.imapClient;
|
|
552
561
|
|
|
553
562
|
if (!connectionClient) {
|
|
554
|
-
|
|
563
|
+
let err = new Error('IMAP connection not available');
|
|
564
|
+
err.code = 'IMAPConnectionClosing';
|
|
565
|
+
err.statusCode = 503;
|
|
566
|
+
throw err;
|
|
555
567
|
}
|
|
556
568
|
|
|
557
569
|
let lock = await connectionClient.getMailboxLock(this.path, options || {});
|
|
@@ -613,7 +625,13 @@ class Mailbox {
|
|
|
613
625
|
return false;
|
|
614
626
|
})
|
|
615
627
|
.then(() => this.select())
|
|
616
|
-
.catch(err =>
|
|
628
|
+
.catch(err => {
|
|
629
|
+
if (err.code === 'IMAPConnectionClosing') {
|
|
630
|
+
this.logger.debug({ msg: 'Sync skipped, connection closing', err });
|
|
631
|
+
} else {
|
|
632
|
+
this.logger.error({ msg: 'Sync error', err });
|
|
633
|
+
}
|
|
634
|
+
});
|
|
617
635
|
}, 1000);
|
|
618
636
|
}
|
|
619
637
|
|
|
@@ -1090,7 +1108,7 @@ class Mailbox {
|
|
|
1090
1108
|
|
|
1091
1109
|
// Resolve Gmail category for inbox messages
|
|
1092
1110
|
if (
|
|
1093
|
-
this.connection.imapClient
|
|
1111
|
+
this.connection.imapClient?.capabilities?.has('X-GM-EXT-1') &&
|
|
1094
1112
|
this.isAllMail &&
|
|
1095
1113
|
messageInfo.labels &&
|
|
1096
1114
|
messageInfo.labels.includes('\\Inbox') &&
|
|
@@ -2069,7 +2087,7 @@ class Mailbox {
|
|
|
2069
2087
|
}
|
|
2070
2088
|
|
|
2071
2089
|
// Partial sync if CONDSTORE indicates only new messages or flag changes
|
|
2072
|
-
if (canUseCondstorePartialSync(this.connection.imapClient, storedStatus, mailboxStatus)) {
|
|
2090
|
+
if (this.connection.imapClient && canUseCondstorePartialSync(this.connection.imapClient, storedStatus, mailboxStatus)) {
|
|
2073
2091
|
return await this.partialSync(storedStatus);
|
|
2074
2092
|
}
|
|
2075
2093
|
|
|
@@ -197,7 +197,10 @@ class SyncOperations {
|
|
|
197
197
|
this.mailbox.syncing = true;
|
|
198
198
|
try {
|
|
199
199
|
if (!this.connection.imapClient) {
|
|
200
|
-
|
|
200
|
+
let err = new Error('IMAP connection not available');
|
|
201
|
+
err.code = 'IMAPConnectionClosing';
|
|
202
|
+
err.statusCode = 503;
|
|
203
|
+
throw err;
|
|
201
204
|
}
|
|
202
205
|
|
|
203
206
|
let knownUidNext = typeof storedStatus.uidNext === 'number' ? storedStatus.uidNext || 1 : 1;
|
|
@@ -351,7 +354,10 @@ class SyncOperations {
|
|
|
351
354
|
this.mailbox.syncing = true;
|
|
352
355
|
try {
|
|
353
356
|
if (!this.connection.imapClient) {
|
|
354
|
-
|
|
357
|
+
let err = new Error('IMAP connection not available');
|
|
358
|
+
err.code = 'IMAPConnectionClosing';
|
|
359
|
+
err.statusCode = 503;
|
|
360
|
+
throw err;
|
|
355
361
|
}
|
|
356
362
|
|
|
357
363
|
let fields = { uid: true, flags: true, modseq: true, emailId: true, labels: true, internalDate: true };
|
|
@@ -378,7 +384,10 @@ class SyncOperations {
|
|
|
378
384
|
let imapClient = this.connection.imapClient;
|
|
379
385
|
if (!imapClient || !imapClient.usable) {
|
|
380
386
|
this.logger.error({ msg: 'IMAP client not available for partial sync' });
|
|
381
|
-
|
|
387
|
+
let err = new Error('IMAP connection not available');
|
|
388
|
+
err.code = 'IMAPConnectionClosing';
|
|
389
|
+
err.statusCode = 503;
|
|
390
|
+
throw err;
|
|
382
391
|
}
|
|
383
392
|
|
|
384
393
|
try {
|
|
@@ -556,7 +565,10 @@ class SyncOperations {
|
|
|
556
565
|
const imapClient = this.connection.imapClient;
|
|
557
566
|
if (!imapClient || !imapClient.usable) {
|
|
558
567
|
this.logger.error({ msg: 'IMAP client not available for FETCH' });
|
|
559
|
-
|
|
568
|
+
let err = new Error('IMAP connection not available');
|
|
569
|
+
err.code = 'IMAPConnectionClosing';
|
|
570
|
+
err.statusCode = 503;
|
|
571
|
+
throw err;
|
|
560
572
|
}
|
|
561
573
|
|
|
562
574
|
try {
|
|
@@ -728,7 +740,7 @@ class SyncOperations {
|
|
|
728
740
|
|
|
729
741
|
// Verify we're still on the correct mailbox after the delay
|
|
730
742
|
// Another operation might have changed the mailbox while we were waiting
|
|
731
|
-
const currentMailbox = this.connection.imapClient
|
|
743
|
+
const currentMailbox = this.connection.imapClient?.mailbox;
|
|
732
744
|
if (!currentMailbox || currentMailbox.path !== this.mailbox.path) {
|
|
733
745
|
this.logger.error({
|
|
734
746
|
msg: 'Mailbox changed during retry delay, aborting sync',
|
|
@@ -191,6 +191,12 @@ class IMAPClient extends BaseClient {
|
|
|
191
191
|
let syncing = this.syncing || ['init', 'connecting', 'syncing'].includes(this.state);
|
|
192
192
|
if (!noPool && (!syncing || !allowSecondary)) {
|
|
193
193
|
// Return the primary connection for most operations
|
|
194
|
+
if (!this.imapClient) {
|
|
195
|
+
let err = new Error('IMAP connection not available');
|
|
196
|
+
err.code = 'IMAPConnectionClosing';
|
|
197
|
+
err.statusCode = 503;
|
|
198
|
+
throw err;
|
|
199
|
+
}
|
|
194
200
|
return this.imapClient;
|
|
195
201
|
}
|
|
196
202
|
|
|
@@ -202,14 +208,19 @@ class IMAPClient extends BaseClient {
|
|
|
202
208
|
if (connectionClient && connectionClient.usable) {
|
|
203
209
|
connectionOptions.connectionClient = connectionClient;
|
|
204
210
|
return connectionClient;
|
|
205
|
-
} else {
|
|
206
|
-
// fall back to default connection
|
|
207
|
-
return this.imapClient;
|
|
208
211
|
}
|
|
209
212
|
} catch (err) {
|
|
210
213
|
this.logger.error({ msg: 'Failed to acquire command connection', reason, err });
|
|
211
|
-
return this.imapClient;
|
|
212
214
|
}
|
|
215
|
+
|
|
216
|
+
// Fall back to primary connection
|
|
217
|
+
if (!this.imapClient) {
|
|
218
|
+
let err = new Error('IMAP connection not available');
|
|
219
|
+
err.code = 'IMAPConnectionClosing';
|
|
220
|
+
err.statusCode = 503;
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
return this.imapClient;
|
|
213
224
|
}
|
|
214
225
|
|
|
215
226
|
/**
|
|
@@ -324,9 +335,11 @@ class IMAPClient extends BaseClient {
|
|
|
324
335
|
return commandClient;
|
|
325
336
|
} finally {
|
|
326
337
|
// Always release the lock
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
338
|
+
if (connectLock?.success) {
|
|
339
|
+
this.logger.debug({ msg: 'Releasing connection lock', lockKey, index: connectLock.index });
|
|
340
|
+
await lock.releaseLock(connectLock);
|
|
341
|
+
this.logger.debug({ msg: 'Released connection lock', lockKey, index: connectLock.index });
|
|
342
|
+
}
|
|
330
343
|
}
|
|
331
344
|
}
|
|
332
345
|
|
|
@@ -535,14 +548,6 @@ class IMAPClient extends BaseClient {
|
|
|
535
548
|
this.checkIMAPConnection(connectionOptions);
|
|
536
549
|
|
|
537
550
|
const connectionClient = await this.getImapConnection(connectionOptions, 'getCurrentListing');
|
|
538
|
-
if (!connectionClient) {
|
|
539
|
-
if (this.imapClient) {
|
|
540
|
-
this.imapClient.close();
|
|
541
|
-
}
|
|
542
|
-
let error = new Error('Failed to get connection');
|
|
543
|
-
error.code = 'ConnectionError';
|
|
544
|
-
throw error;
|
|
545
|
-
}
|
|
546
551
|
|
|
547
552
|
let accountData = await this.accountObject.loadAccountData();
|
|
548
553
|
|
|
@@ -802,7 +807,11 @@ class IMAPClient extends BaseClient {
|
|
|
802
807
|
try {
|
|
803
808
|
await mailbox.onOpen(event);
|
|
804
809
|
} catch (err) {
|
|
805
|
-
|
|
810
|
+
if (err.code === 'IMAPConnectionClosing') {
|
|
811
|
+
imapClient.log.debug({ msg: 'Open skipped, connection closing', err });
|
|
812
|
+
} else {
|
|
813
|
+
imapClient.log.error({ msg: 'Open error', err });
|
|
814
|
+
}
|
|
806
815
|
}
|
|
807
816
|
});
|
|
808
817
|
|
|
@@ -2,23 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
const { metricsMeta } = require('../base-client');
|
|
4
4
|
|
|
5
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
OUTLOOK_MAX_BATCH_SIZE,
|
|
7
|
+
OUTLOOK_MAX_RETRY_ATTEMPTS,
|
|
8
|
+
OUTLOOK_RETRY_BASE_DELAY,
|
|
9
|
+
OUTLOOK_RETRY_MAX_DELAY,
|
|
10
|
+
TRANSIENT_NETWORK_CODES
|
|
11
|
+
} = require('../../consts');
|
|
6
12
|
|
|
7
13
|
// Maximum number of operations in a single batch request to Microsoft Graph API
|
|
8
14
|
const MAX_BATCH_SIZE = OUTLOOK_MAX_BATCH_SIZE;
|
|
9
15
|
|
|
10
|
-
// Network-level errors that are transient and should be retried
|
|
11
|
-
const TRANSIENT_NETWORK_CODES = new Set([
|
|
12
|
-
'ENOTFOUND',
|
|
13
|
-
'EAI_AGAIN',
|
|
14
|
-
'ETIMEDOUT',
|
|
15
|
-
'ECONNRESET',
|
|
16
|
-
'ECONNREFUSED',
|
|
17
|
-
'UND_ERR_SOCKET',
|
|
18
|
-
'UND_ERR_CONNECT_TIMEOUT',
|
|
19
|
-
'UND_ERR_HEADERS_TIMEOUT'
|
|
20
|
-
]);
|
|
21
|
-
|
|
22
16
|
// MS Graph API error code mapping to internal error codes
|
|
23
17
|
// https://learn.microsoft.com/en-us/graph/errors
|
|
24
18
|
const GRAPH_ERROR_MAP = {
|