emailengine-app 2.63.2 → 2.63.3

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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.63.3](https://github.com/postalsys/emailengine/compare/v2.63.2...v2.63.3) (2026-03-05)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * handle undici HeadersTimeoutError as transient in Pub/Sub and OAuth2 paths ([8b3b698](https://github.com/postalsys/emailengine/commit/8b3b69880ab09119df4ca377d085db261eae2763))
9
+ * prevent IMAP worker crash on ImapFlow unhandled rejection during IDLE recovery ([86ebb02](https://github.com/postalsys/emailengine/commit/86ebb02c321ffa758966af1957509c10e20a35fc))
10
+ * prevent transient network errors during OAuth2 token refresh from being misclassified as auth failures ([38fa212](https://github.com/postalsys/emailengine/commit/38fa212092847b727ac1d02541103c032a983dc7))
11
+ * retry transient network errors in Gmail and Outlook API request functions ([08ea0da](https://github.com/postalsys/emailengine/commit/08ea0daecaec73c58a91ac07df5d0f0ffc083a2f))
12
+ * treat DNS errors as transient in Google Pub/Sub polling loop ([ec33673](https://github.com/postalsys/emailengine/commit/ec33673b47242fd7156b4641c9c4924a33eaf9e5))
13
+
3
14
  ## [2.63.2](https://github.com/postalsys/emailengine/compare/v2.63.1...v2.63.2) (2026-03-03)
4
15
 
5
16
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "creationTime": "2026-03-02T15:45:59.000000",
2
+ "creationTime": "2026-03-04T15:45:55.000000",
3
3
  "prefixes": [
4
4
  {
5
5
  "ipv6Prefix": "2001:4860:4801:2008::/64"
@@ -538,6 +538,28 @@ class BaseClient {
538
538
  });
539
539
  accessToken = accountData.oauth2.accessToken;
540
540
  } catch (err) {
541
+ // Transient network errors are not authentication failures
542
+ let isNetworkError =
543
+ err.code === 'ENOTFOUND' ||
544
+ err.code === 'ETIMEDOUT' ||
545
+ err.code === 'ECONNREFUSED' ||
546
+ err.code === 'ECONNRESET' ||
547
+ err.code === 'EAI_AGAIN' ||
548
+ err.code === 'UND_ERR_CONNECT_TIMEOUT' ||
549
+ err.code === 'UND_ERR_SOCKET' ||
550
+ err.code === 'UND_ERR_HEADERS_TIMEOUT';
551
+
552
+ if (isNetworkError) {
553
+ ctx.logger.error({
554
+ msg: 'Network error during OAuth2 token refresh',
555
+ account: accountObject.account,
556
+ code: err.code,
557
+ err
558
+ });
559
+ // Do not mark as authenticationFailed, let the caller handle as a connection error
560
+ throw err;
561
+ }
562
+
541
563
  err.authenticationFailed = true;
542
564
  let notifyData = {
543
565
  response: err.message,
@@ -12,6 +12,18 @@ const LIST_BATCH_SIZE = 10;
12
12
  const MAX_RETRY_ATTEMPTS = 3;
13
13
  const RETRY_BASE_DELAY = 1000; // 1 second base delay
14
14
 
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
+
15
27
  // Gmail API error code mapping to internal error codes
16
28
  // https://developers.google.com/gmail/api/reference/rest#error-codes
17
29
  const GMAIL_ERROR_MAP = {
@@ -117,6 +129,29 @@ async function request(context, url, method, payload, options = {}) {
117
129
  } catch (err) {
118
130
  lastError = err;
119
131
 
132
+ // Check if this is a transient network error
133
+ if (TRANSIENT_NETWORK_CODES.has(err.code) && attempt < maxRetries) {
134
+ const delay = RETRY_BASE_DELAY * Math.pow(2, attempt) + Math.random() * 500;
135
+
136
+ context.logger.warn({
137
+ msg: 'Transient network error during Gmail API request, retrying',
138
+ account: context.account,
139
+ attempt: attempt + 1,
140
+ maxRetries,
141
+ delay,
142
+ code: err.code
143
+ });
144
+
145
+ metricsMeta({ account: context.account }, context.logger, 'oauth2ApiRequest', 'inc', {
146
+ status: 'network_error',
147
+ provider: 'gmail',
148
+ statusCode: '0'
149
+ });
150
+
151
+ await new Promise(resolve => setTimeout(resolve, delay));
152
+ continue;
153
+ }
154
+
120
155
  // Check if this is a rate limit error
121
156
  if (isRateLimitError(err) && attempt < maxRetries) {
122
157
  const delay = calculateRetryDelay(err, attempt);
@@ -7,6 +7,18 @@ const { OUTLOOK_MAX_BATCH_SIZE, OUTLOOK_MAX_RETRY_ATTEMPTS, OUTLOOK_RETRY_BASE_D
7
7
  // Maximum number of operations in a single batch request to Microsoft Graph API
8
8
  const MAX_BATCH_SIZE = OUTLOOK_MAX_BATCH_SIZE;
9
9
 
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
+
10
22
  // MS Graph API error code mapping to internal error codes
11
23
  // https://learn.microsoft.com/en-us/graph/errors
12
24
  const GRAPH_ERROR_MAP = {
@@ -163,6 +175,25 @@ async function requestWithRetry(context, url, method, payload, options = {}) {
163
175
  try {
164
176
  return await request(context, url, method, payload, options);
165
177
  } catch (err) {
178
+ // Retry on transient network errors
179
+ if (TRANSIENT_NETWORK_CODES.has(err.code) && attempt < maxRetries) {
180
+ lastError = err;
181
+ const delay = Math.min(OUTLOOK_RETRY_BASE_DELAY * Math.pow(2, attempt), OUTLOOK_RETRY_MAX_DELAY);
182
+
183
+ context.logger.warn({
184
+ msg: 'Transient network error during Graph API request, retrying',
185
+ account: context.account,
186
+ attempt: attempt + 1,
187
+ maxRetries,
188
+ delay,
189
+ code: err.code,
190
+ url
191
+ });
192
+
193
+ await new Promise(resolve => setTimeout(resolve, delay * 1000));
194
+ continue;
195
+ }
196
+
166
197
  // Only retry on 429 (rate limit) errors
167
198
  if (err.oauthRequest?.status !== 429 || attempt === maxRetries) {
168
199
  throw err;
@@ -203,7 +203,18 @@ class PubSubInstance {
203
203
  await oauth2Apps.setMeta(this.app, { pubSubFlag: null });
204
204
  } catch (err) {
205
205
  // Transient network errors are expected for long-polling connections
206
- if (['ETIMEDOUT', 'ECONNRESET', 'ECONNREFUSED', 'UND_ERR_SOCKET', 'UND_ERR_CONNECT_TIMEOUT'].includes(err.code)) {
206
+ if (
207
+ [
208
+ 'ENOTFOUND',
209
+ 'EAI_AGAIN',
210
+ 'ETIMEDOUT',
211
+ 'ECONNRESET',
212
+ 'ECONNREFUSED',
213
+ 'UND_ERR_SOCKET',
214
+ 'UND_ERR_CONNECT_TIMEOUT',
215
+ 'UND_ERR_HEADERS_TIMEOUT'
216
+ ].includes(err.code)
217
+ ) {
207
218
  logger.warn({ msg: 'Transient error pulling subscription messages', app: this.app, code: err.code });
208
219
  return;
209
220
  }
@@ -11,6 +11,17 @@ const Lock = require('ioredfour');
11
11
  const getSecret = require('./get-secret');
12
12
  const { parentPort } = require('worker_threads');
13
13
 
14
+ const TRANSIENT_NETWORK_CODES = new Set([
15
+ 'ENOTFOUND',
16
+ 'EAI_AGAIN',
17
+ 'ETIMEDOUT',
18
+ 'ECONNRESET',
19
+ 'ECONNREFUSED',
20
+ 'UND_ERR_SOCKET',
21
+ 'UND_ERR_CONNECT_TIMEOUT',
22
+ 'UND_ERR_HEADERS_TIMEOUT'
23
+ ]);
24
+
14
25
  /**
15
26
  * Record metrics for OAuth2 token operations
16
27
  * Works in both main thread and worker threads
@@ -922,6 +933,10 @@ class OAuth2AppsHandler {
922
933
  {name: 'projects/...'}
923
934
  */
924
935
  } catch (err) {
936
+ if (TRANSIENT_NETWORK_CODES.has(err.code)) {
937
+ logger.warn({ msg: 'Network error checking Pub/Sub topic', app: appData.id, code: err.code });
938
+ throw err;
939
+ }
925
940
  switch (err?.oauthRequest?.response?.error?.code) {
926
941
  case 403:
927
942
  // no permissions
@@ -964,6 +979,10 @@ class OAuth2AppsHandler {
964
979
 
965
980
  results.pubSubTopic = topicName;
966
981
  } catch (err) {
982
+ if (TRANSIENT_NETWORK_CODES.has(err.code)) {
983
+ logger.warn({ msg: 'Network error creating Pub/Sub topic', app: appData.id, code: err.code });
984
+ throw err;
985
+ }
967
986
  switch (err?.oauthRequest?.response?.error?.code) {
968
987
  case 403:
969
988
  // no permissions
@@ -999,6 +1018,10 @@ class OAuth2AppsHandler {
999
1018
  {name: 'projects/...'}
1000
1019
  */
1001
1020
  } catch (err) {
1021
+ if (TRANSIENT_NETWORK_CODES.has(err.code)) {
1022
+ logger.warn({ msg: 'Network error checking Pub/Sub subscription', app: appData.id, code: err.code });
1023
+ throw err;
1024
+ }
1002
1025
  switch (err?.oauthRequest?.response?.error?.code) {
1003
1026
  case 403:
1004
1027
  // no permissions
@@ -1042,6 +1065,10 @@ class OAuth2AppsHandler {
1042
1065
 
1043
1066
  results.pubSubSubscription = subscriptionName;
1044
1067
  } catch (err) {
1068
+ if (TRANSIENT_NETWORK_CODES.has(err.code)) {
1069
+ logger.warn({ msg: 'Network error creating Pub/Sub subscription', app: appData.id, code: err.code });
1070
+ throw err;
1071
+ }
1045
1072
  switch (err?.oauthRequest?.response?.error?.code) {
1046
1073
  case 403:
1047
1074
  // no permissions
@@ -1087,6 +1114,10 @@ class OAuth2AppsHandler {
1087
1114
  }
1088
1115
  */
1089
1116
  } catch (err) {
1117
+ if (TRANSIENT_NETWORK_CODES.has(err.code)) {
1118
+ logger.warn({ msg: 'Network error checking Pub/Sub IAM policy', app: appData.id, code: err.code });
1119
+ throw err;
1120
+ }
1090
1121
  switch (err?.oauthRequest?.response?.error?.code) {
1091
1122
  case 403:
1092
1123
  // no permissions
@@ -1130,6 +1161,10 @@ class OAuth2AppsHandler {
1130
1161
  { partial: true }
1131
1162
  );
1132
1163
  } catch (err) {
1164
+ if (TRANSIENT_NETWORK_CODES.has(err.code)) {
1165
+ logger.warn({ msg: 'Network error setting Pub/Sub IAM policy', app: appData.id, code: err.code });
1166
+ throw err;
1167
+ }
1133
1168
  switch (err?.oauthRequest?.response?.error?.code) {
1134
1169
  case 403:
1135
1170
  // no permissions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emailengine-app",
3
- "version": "2.63.2",
3
+ "version": "2.63.3",
4
4
  "private": false,
5
5
  "productTitle": "EmailEngine",
6
6
  "description": "Email Sync Engine",
@@ -59,8 +59,8 @@
59
59
  "@postalsys/bounce-classifier": "^2.0.0",
60
60
  "@postalsys/certs": "1.0.12",
61
61
  "@postalsys/ee-client": "1.3.0",
62
- "@postalsys/email-ai-tools": "1.11.3",
63
- "@postalsys/email-text-tools": "2.4.1",
62
+ "@postalsys/email-ai-tools": "1.11.4",
63
+ "@postalsys/email-text-tools": "2.4.2",
64
64
  "@postalsys/gettext": "4.1.1",
65
65
  "@postalsys/joi-messages": "1.0.5",
66
66
  "@postalsys/templates": "2.0.0",
@@ -68,7 +68,7 @@
68
68
  "@zone-eu/wild-config": "1.7.3",
69
69
  "ace-builds": "1.43.6",
70
70
  "base32.js": "0.1.0",
71
- "bullmq": "5.70.1",
71
+ "bullmq": "5.70.2",
72
72
  "compare-versions": "6.1.1",
73
73
  "dotenv": "17.3.1",
74
74
  "encoding-japanese": "2.2.0",
@@ -82,7 +82,7 @@
82
82
  "html-to-text": "9.0.5",
83
83
  "ical.js": "1.5.0",
84
84
  "iconv-lite": "0.7.2",
85
- "imapflow": "1.2.11",
85
+ "imapflow": "1.2.12",
86
86
  "ioredfour": "1.4.0",
87
87
  "ioredis": "5.10.0",
88
88
  "ipaddr.js": "2.3.0",