emailengine-app 2.63.1 → 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 +20 -0
- package/data/google-crawlers.json +1 -1
- package/lib/email-client/base-client.js +22 -0
- package/lib/email-client/gmail/gmail-api.js +35 -0
- package/lib/email-client/outlook/graph-api.js +31 -0
- package/lib/oauth/pubsub/google.js +12 -1
- package/lib/oauth2-apps.js +35 -0
- package/package.json +6 -6
- package/sbom.json +1 -1
- package/server.js +43 -9
- package/static/licenses.html +39 -29
- package/translations/messages.pot +9 -9
- package/workers/api.js +39 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
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
|
+
|
|
14
|
+
## [2.63.2](https://github.com/postalsys/emailengine/compare/v2.63.1...v2.63.2) (2026-03-03)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* harden static file route with path confinement and pkg-only guard ([19cd5f6](https://github.com/postalsys/emailengine/commit/19cd5f632baa7fb37f5d639f234c5374b371ab93))
|
|
20
|
+
* prevent EISDIR crash on static subdirectory requests in pkg ([4d5ef81](https://github.com/postalsys/emailengine/commit/4d5ef81679e4fd7f5e8a55a260f2c29f99938ba4))
|
|
21
|
+
* prevent infinite loop and add retry backoff in account assignment ([f8b9e53](https://github.com/postalsys/emailengine/commit/f8b9e533a821d1c34a869270fa489437ac80da93))
|
|
22
|
+
|
|
3
23
|
## [2.63.1](https://github.com/postalsys/emailengine/compare/v2.63.0...v2.63.1) (2026-02-26)
|
|
4
24
|
|
|
5
25
|
|
|
@@ -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 (
|
|
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
|
}
|
package/lib/oauth2-apps.js
CHANGED
|
@@ -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.
|
|
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.
|
|
63
|
-
"@postalsys/email-text-tools": "2.4.
|
|
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.
|
|
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,9 +82,9 @@
|
|
|
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.
|
|
85
|
+
"imapflow": "1.2.12",
|
|
86
86
|
"ioredfour": "1.4.0",
|
|
87
|
-
"ioredis": "5.
|
|
87
|
+
"ioredis": "5.10.0",
|
|
88
88
|
"ipaddr.js": "2.3.0",
|
|
89
89
|
"joi": "17.13.3",
|
|
90
90
|
"jquery": "4.0.0",
|