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
@@ -80,3 +80,7 @@ jobs:
80
80
  GMAIL_SENDONLY_CLIENT_SECRET: ${{ secrets.GMAIL_SENDONLY_CLIENT_SECRET }}
81
81
  GMAIL_SENDONLY_ACCOUNT_EMAIL: ${{ secrets.GMAIL_SENDONLY_ACCOUNT_EMAIL }}
82
82
  GMAIL_SENDONLY_ACCOUNT_REFRESH: ${{ secrets.GMAIL_SENDONLY_ACCOUNT_REFRESH }}
83
+ OUTLOOK_SERVICE_CLIENT_ID: ${{ secrets.OUTLOOK_SERVICE_CLIENT_ID }}
84
+ OUTLOOK_SERVICE_TENANT_ID: ${{ secrets.OUTLOOK_SERVICE_TENANT_ID }}
85
+ OUTLOOK_SERVICE_CLIENT_SECRET: ${{ secrets.OUTLOOK_SERVICE_CLIENT_SECRET }}
86
+ OUTLOOK_SERVICE_ACCOUNT_EMAIL: ${{ secrets.OUTLOOK_SERVICE_ACCOUNT_EMAIL }}
package/CHANGELOG.md CHANGED
@@ -1,5 +1,75 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.65.0](https://github.com/postalsys/emailengine/compare/v2.64.0...v2.65.0) (2026-03-23)
4
+
5
+
6
+ ### Features
7
+
8
+ * add Outlook Service (client_credentials) provider for app-only Microsoft 365 access ([#587](https://github.com/postalsys/emailengine/issues/587)) ([5f906cd](https://github.com/postalsys/emailengine/commit/5f906cd540564afe2dc2024b4e17c2d5d9483ed2))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * fix export TTL expiration, cancellation cleanup, and expose truncated field ([019c05c](https://github.com/postalsys/emailengine/commit/019c05c941842ae28668711e8f5c2e8a85d3b84b))
14
+ * include failed jobs in /v1/outbox response ([b393676](https://github.com/postalsys/emailengine/commit/b393676f2f877046d27b06d518a03dd256d5b7d9))
15
+ * switch pkg targets from node22 to node24 to fix Windows builds ([12522a7](https://github.com/postalsys/emailengine/commit/12522a7b92d72684af49cd70c1c6d4bcafe1d4af))
16
+
17
+ ## [2.64.0](https://github.com/postalsys/emailengine/compare/v2.63.4...v2.64.0) (2026-03-16)
18
+
19
+
20
+ ### Features
21
+
22
+ * add configurable Gmail Pub/Sub subscription TTL setting ([ca33e7f](https://github.com/postalsys/emailengine/commit/ca33e7f9101b3375d7e0001cdbe1d7a41a9442d3))
23
+ * add Gmail Subscriptions tab to OAuth config page ([3bd30bf](https://github.com/postalsys/emailengine/commit/3bd30bf0bbc26600eae40d42e95ef7976257e4f2))
24
+
25
+
26
+ ### Bug Fixes
27
+
28
+ * add .catch() to fire-and-forget setMeta and track error fingerprints ([672a03e](https://github.com/postalsys/emailengine/commit/672a03e2524bcaa26c4d3c908fa828f4e636999b))
29
+ * add lock to del() to prevent race with ensurePubsub, fix pubSubApp cleanup ([27f2bdd](https://github.com/postalsys/emailengine/commit/27f2bdd4a9e3d7e913e7f4bed96084a278bb6312))
30
+ * always notify webhook workers when Pub/Sub app config changes ([e9f276a](https://github.com/postalsys/emailengine/commit/e9f276ab528ad384bcbf5c370a0d0cb22e084cc5))
31
+ * auto-recover expired Gmail Pub/Sub subscriptions and expose status via API ([7961ceb](https://github.com/postalsys/emailengine/commit/7961ceb8ce81688e5c3bb435a6c33ed3882afd7b))
32
+ * avoid redundant Redis call and concurrent backfill races in Pub/Sub setup ([05b02af](https://github.com/postalsys/emailengine/commit/05b02afc1c14de1b8fa4e272d26da2056280731a))
33
+ * broaden Pub/Sub notification condition in oauth-routes.js ([cbe1380](https://github.com/postalsys/emailengine/commit/cbe13800ab9460c41f5dcdcd4d7ae50f8e5e75e3))
34
+ * clean up Pub/Sub Redis keys on deletion, refresh stale instances, and localize UI labels ([3b35fa1](https://github.com/postalsys/emailengine/commit/3b35fa19a6a8024a60d2ed21e9fcd69fda349481))
35
+ * clear stale pubSubFlag after restart and consolidate backfill push ([f67961f](https://github.com/postalsys/emailengine/commit/f67961fbc590aa586ef40b65c2189b5dab4d89c0))
36
+ * correct pubSubApp property name in del() and add cleanup tests ([016ae0a](https://github.com/postalsys/emailengine/commit/016ae0a98bc52eeee2bd1ec4f4d0ed58e92c507d))
37
+ * correct typo in Pub/Sub schema version log message ([2cc8e08](https://github.com/postalsys/emailengine/commit/2cc8e085463ed550e341b027e592b5351784182e))
38
+ * eliminate redundant Redis ops in Pub/Sub pull loop and backfill ([c8ebe39](https://github.com/postalsys/emailengine/commit/c8ebe393744eee46c3d19b5be413badd8ab315d6))
39
+ * fix lifecycle event races, lock TTL, and stale clearExisting in MS Graph subscriptions ([3f3cbac](https://github.com/postalsys/emailengine/commit/3f3cbac843c4c221644492855d7c1beb41cd1da0))
40
+ * fix missing renewal retry, clock-skew gap, and incomplete pipeline error check ([8a8b5b6](https://github.com/postalsys/emailengine/commit/8a8b5b68feb3edd7c984707a85ddb6d146739020))
41
+ * fix off-by-one retry cap and simplify MS Graph subscription code ([aa9afbc](https://github.com/postalsys/emailengine/commit/aa9afbc5aea20d1bf775e59dbfd82842c4f8b701))
42
+ * fix Pub/Sub deletion race, add 429 handling, batch ACKs, and lock ensurePubsub ([744c354](https://github.com/postalsys/emailengine/commit/744c354a062f63eedcec8de1a324a5d13519b9a7))
43
+ * fix retry boundary, lock races, and dropped lifecycle events in MS Graph subscriptions ([d62741d](https://github.com/postalsys/emailengine/commit/d62741d2a2aafc47d831482b71ff24fa4c9d1883))
44
+ * fix silent 401/403 error suppression, floating promise, and recovery loop in Pub/Sub ([406211c](https://github.com/postalsys/emailengine/commit/406211cbd62f7d3f1b811fbec81767f319219c63))
45
+ * fix stale subscription state blocking recovery and retry count persisting across reconnects ([60b77d5](https://github.com/postalsys/emailengine/commit/60b77d5d7574abf728c5e1f015c0cecddd5d9fdd))
46
+ * fix subscription loss on subscriptionRemoved, lifecycle webhook timeout, and retry gaps ([62988ab](https://github.com/postalsys/emailengine/commit/62988ab3b690ed98583fa01557a9c4145427e3ae))
47
+ * guard fire-and-forget setMeta calls and add Pub/Sub graceful shutdown ([45f7b64](https://github.com/postalsys/emailengine/commit/45f7b64d7692b220ff14ec0e1c0e9deea84731de))
48
+ * guard releaseLock against undefined, add 429 handling to Pub/Sub deletion ([d67ebe1](https://github.com/postalsys/emailengine/commit/d67ebe138ca9663796eacc5a1c6b5b73e721e6c4))
49
+ * handle TimeoutError from AbortSignal.timeout in Pub/Sub pull loop ([c0e3036](https://github.com/postalsys/emailengine/commit/c0e303641e4eb9f6cdca3c584861052b2bd91148))
50
+ * harden MS Graph subscription lifecycle, locking, cleanup, and error recovery ([8210056](https://github.com/postalsys/emailengine/commit/8210056f7e7b00977fb5487323ff1b8f9f4b7a99))
51
+ * harden Pub/Sub deletion, IAM policy, and worker timeout cleanup ([0be7c3e](https://github.com/postalsys/emailengine/commit/0be7c3eb04f3381306284e1e3790f7c3cc28cb5a))
52
+ * harden Pub/Sub pull loop resilience and fix projectId typo ([8124328](https://github.com/postalsys/emailengine/commit/81243287f339b5fe40cddd0e1d127514d2e11f4a))
53
+ * harden Pub/Sub pull loop, message ACK, list pairing, and shutdown cleanup ([9e9775f](https://github.com/postalsys/emailengine/commit/9e9775f6df97af42bdb8b799c2345a4564f8a77a))
54
+ * harden Pub/Sub pull loop, worker coordination, and OAuth app lifecycle ([529b705](https://github.com/postalsys/emailengine/commit/529b7055dd4aeb0b1d80fabff3f139cf23f00252))
55
+ * harden Pub/Sub shutdown, loop scheduling, and abort lifecycle ([71b3ab4](https://github.com/postalsys/emailengine/commit/71b3ab4625fa3304ae7a4413a3b0a64276948d7f))
56
+ * harden Pub/Sub shutdown, transient error handling, and input validation ([cfc6e0b](https://github.com/postalsys/emailengine/commit/cfc6e0bb73902600b6eecc6bc955af85f06d9b0f))
57
+ * localize error page strings using translation helper ([609ebb9](https://github.com/postalsys/emailengine/commit/609ebb94d1e662ac661f7efe22f6608d7823fb85))
58
+ * move pubsub status from unauthenticated /health to GET /v1/pubsub/status ([f6597ad](https://github.com/postalsys/emailengine/commit/f6597addff4de1c23fbd88964702bc2425f14d0c))
59
+ * prevent oscillating recovery loop for Pub/Sub apps missing googleProjectId ([8bdbc39](https://github.com/postalsys/emailengine/commit/8bdbc39c790b7097b02459b558328c18584585a2))
60
+ * reduce Pub/Sub recovery log noise and respect backoff delay ([63cd78f](https://github.com/postalsys/emailengine/commit/63cd78fcfd0c12934e488f6936109653371f0899))
61
+ * remove dead circuit breaker code from IMAP and webhooks workers ([da9295b](https://github.com/postalsys/emailengine/commit/da9295b951d20a5817116195689dd3c964b3d996))
62
+ * remove Pub/Sub circuit breaker, fix 401/403 recovery handling ([b7017f3](https://github.com/postalsys/emailengine/commit/b7017f34478dbb3bcedfb9a1e098a8a45cb0f83d))
63
+ * reorder OAuth app deletion to prevent pull-loop gap, add startLoop tests ([129f96c](https://github.com/postalsys/emailengine/commit/129f96c99ff9ff2552fa1c1962b1e0a652b8837e))
64
+ * replace custom Redis locks with ioredfour in Outlook subscription code ([019eaa1](https://github.com/postalsys/emailengine/commit/019eaa1f26be504d127cb6c8a05dfe50d8a47b71))
65
+ * resolve lint errors, fix TTL null guard, and extract Pub/Sub constants ([4949431](https://github.com/postalsys/emailengine/commit/4949431c8e05f06c5aafc3dfe93876ea8a2875b9))
66
+ * resolve livelock, timeout, and state corruption in MS Graph subscription lifecycle ([745334e](https://github.com/postalsys/emailengine/commit/745334e8c55fcf4b0b3fc6adef2b37f6cf9aea61))
67
+ * retry transient errors during Pub/Sub resource deletion and log dropped messages ([b9d9de5](https://github.com/postalsys/emailengine/commit/b9d9de56a50b37e0163ea4e9a954d65bc305004c))
68
+ * show OAuth apps with failed Pub/Sub setup in subscriptions list ([14124cd](https://github.com/postalsys/emailengine/commit/14124cd29baf107f1f82dff48695bc51cae4fd97))
69
+ * stop Pub/Sub instances on OAuth2 app deletion and harden lifecycle ([bc0ff75](https://github.com/postalsys/emailengine/commit/bc0ff758cea8d8713485e3a28e81b16ee56d1dc7))
70
+ * surface TTL reconciliation failures to operators via ttlWarning meta flag ([6e5d5c5](https://github.com/postalsys/emailengine/commit/6e5d5c5a97fb9aaff2662f3fe2f90c8226359b4a))
71
+ * update imapflow to 1.2.15 to fix unhandled rejection crashes ([494a3f8](https://github.com/postalsys/emailengine/commit/494a3f8cd9e71bc1fc1683bea2fc92474cb33206))
72
+
3
73
  ## [2.63.4](https://github.com/postalsys/emailengine/compare/v2.63.3...v2.63.4) (2026-03-09)
4
74
 
5
75
 
@@ -24,7 +24,7 @@ cp node_modules/ace-builds/src-min/ext-searchbox.js static/js/ace/ext-searchbox.
24
24
 
25
25
  cp node_modules/\@postalsys/ee-client/index.js static/js/ee-client.js
26
26
 
27
- wget https://developers.google.com/static/search/apis/ipranges/special-crawlers.json -O data/google-crawlers.json
27
+ wget https://developers.google.com/static/crawling/ipranges/special-crawlers.json -O data/google-crawlers.json
28
28
  node -e 'console.log("Google crawlers updated: "+require("./data/google-crawlers.json").creationTime);'
29
29
 
30
30
  # brew install gh
@@ -1,5 +1,5 @@
1
1
  {
2
- "creationTime": "2026-03-06T15:46:26.000000",
2
+ "creationTime": "2026-03-20T15:46:03.000000",
3
3
  "prefixes": [
4
4
  {
5
5
  "ipv6Prefix": "2001:4860:4801:2008::/64"
package/eslint.config.js CHANGED
@@ -27,6 +27,8 @@ module.exports = [
27
27
  clearTimeout: 'readonly',
28
28
  setInterval: 'readonly',
29
29
  clearInterval: 'readonly',
30
+ AbortController: 'readonly',
31
+ AbortSignal: 'readonly',
30
32
  URL: 'readonly',
31
33
  URLSearchParams: 'readonly',
32
34
  structuredClone: 'readonly',
package/lib/account.js CHANGED
@@ -18,7 +18,7 @@ const { MessageChannel } = require('worker_threads');
18
18
  const { MessagePortReadable } = require('./message-port-stream');
19
19
  const { deepStrictEqual, strictEqual } = require('assert');
20
20
  const { encrypt, decrypt } = require('./encrypt');
21
- const { oauth2Apps, LEGACY_KEYS } = require('./oauth2-apps');
21
+ const { oauth2Apps, LEGACY_KEYS, isApiBasedApp } = require('./oauth2-apps');
22
22
  const settings = require('./settings');
23
23
  const redisScanDelete = require('./redis-scan-delete');
24
24
  const { customAlphabet } = require('nanoid');
@@ -175,7 +175,7 @@ class Account {
175
175
  oauthApps.set(accountData.oauth2.provider, app || null);
176
176
  if (app) {
177
177
  accountData.type = app.provider;
178
- if (app.baseScopes === 'api') {
178
+ if (isApiBasedApp(app)) {
179
179
  accountData.isApi = true;
180
180
  }
181
181
 
@@ -203,7 +203,7 @@ class Account {
203
203
  app = await oauth2Apps.get(delegatedAccountData.oauth2.provider);
204
204
  }
205
205
  oauthApps.set(delegatedAccountData.oauth2.provider, app || null);
206
- if (app && app.baseScopes === 'api') {
206
+ if (isApiBasedApp(app)) {
207
207
  accountData.isApi = true;
208
208
  }
209
209
  }
@@ -686,7 +686,7 @@ class Account {
686
686
  if (accountData.oauth2 && accountData.oauth2.provider) {
687
687
  let app = await oauth2Apps.get(accountData.oauth2.provider);
688
688
  if (app) {
689
- if (app.baseScopes === 'api') {
689
+ if (isApiBasedApp(app)) {
690
690
  accountData.isApi = true;
691
691
  }
692
692
 
@@ -709,7 +709,7 @@ class Account {
709
709
  let app = await oauth2Apps.get(delegatedAccountData.oauth2.provider);
710
710
  if (app) {
711
711
  accountData._app = app;
712
- if (app.baseScopes === 'api') {
712
+ if (isApiBasedApp(app)) {
713
713
  accountData.isApi = true;
714
714
  }
715
715
  }
@@ -2374,7 +2374,9 @@ class Account {
2374
2374
  });
2375
2375
  }
2376
2376
  } finally {
2377
- await lock.releaseLock(flushLock);
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
- await lock.releaseLock(renewLock);
2495
+ if (renewLock?.success) {
2496
+ await lock.releaseLock(renewLock);
2497
+ }
2494
2498
  }
2495
2499
  }
2496
2500
 
@@ -2576,7 +2580,7 @@ class Account {
2576
2580
  let delegatedAccountData = this.unserializeAccountData(delegatedAccountRow);
2577
2581
  if (delegatedAccountData?.oauth2?.provider) {
2578
2582
  let app = await oauth2Apps.get(delegatedAccountData.oauth2.provider);
2579
- return app?.baseScopes === 'api';
2583
+ return isApiBasedApp(app);
2580
2584
  } else {
2581
2585
  return false;
2582
2586
  }
@@ -2589,7 +2593,7 @@ class Account {
2589
2593
 
2590
2594
  if (accountData.oauth2?.provider) {
2591
2595
  let app = await oauth2Apps.get(accountData.oauth2.provider);
2592
- return app?.baseScopes === 'api';
2596
+ return isApiBasedApp(app);
2593
2597
  }
2594
2598
  return false;
2595
2599
  }
@@ -4,7 +4,7 @@ const crypto = require('crypto');
4
4
  const { redis } = require('../db');
5
5
  const { Account } = require('../account');
6
6
  const getSecret = require('../get-secret');
7
- const { oauth2Apps } = require('../oauth2-apps');
7
+ const { oauth2Apps, SERVICE_ACCOUNT_PROVIDERS } = require('../oauth2-apps');
8
8
  const Boom = require('@hapi/boom');
9
9
  const Joi = require('joi');
10
10
  const { failAction } = require('../tools');
@@ -67,6 +67,12 @@ async function init(args) {
67
67
  // redirect to OAuth2 consent screen
68
68
 
69
69
  const oAuth2Client = await oauth2Apps.getClient(request.payload.oauth2.provider);
70
+
71
+ // Service providers use client_credentials - no interactive authorization
72
+ if (SERVICE_ACCOUNT_PROVIDERS.has(oAuth2Client.provider)) {
73
+ throw Boom.badRequest('Application-only OAuth providers do not support interactive authorization');
74
+ }
75
+
70
76
  const nonce = crypto.randomBytes(NONCE_BYTES).toString('base64url');
71
77
 
72
78
  const accountData = request.payload;
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, // seconds - lock TTL for subscription renewal
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 = {
@@ -335,9 +335,11 @@ class IMAPClient extends BaseClient {
335
335
  return commandClient;
336
336
  } finally {
337
337
  // Always release the lock
338
- this.logger.debug({ msg: 'Releasing connection lock', lockKey, index: connectLock.index });
339
- await lock.releaseLock(connectLock);
340
- this.logger.debug({ msg: 'Released connection lock', lockKey, index: connectLock.index });
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
+ }
341
343
  }
342
344
  }
343
345
 
@@ -2,23 +2,17 @@
2
2
 
3
3
  const { metricsMeta } = require('../base-client');
4
4
 
5
- const { OUTLOOK_MAX_BATCH_SIZE, OUTLOOK_MAX_RETRY_ATTEMPTS, OUTLOOK_RETRY_BASE_DELAY, OUTLOOK_RETRY_MAX_DELAY } = require('../../consts');
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 = {
@@ -100,7 +94,7 @@ async function request(context, url, method, payload, options = {}) {
100
94
  // Track successful API request
101
95
  metricsMeta({ account: context.account }, context.logger, 'oauth2ApiRequest', 'inc', {
102
96
  status: 'success',
103
- provider: 'outlook',
97
+ provider: context.oAuth2Client?.provider || 'outlook',
104
98
  statusCode: '200'
105
99
  });
106
100
  } catch (err) {
@@ -108,7 +102,7 @@ async function request(context, url, method, payload, options = {}) {
108
102
  const statusCode = String(err.oauthRequest?.status || 0);
109
103
  metricsMeta({ account: context.account }, context.logger, 'oauth2ApiRequest', 'inc', {
110
104
  status: 'failure',
111
- provider: 'outlook',
105
+ provider: context.oAuth2Client?.provider || 'outlook',
112
106
  statusCode
113
107
  });
114
108