imapflow 1.0.194 → 1.0.195
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/test.yml +1 -1
- package/CHANGELOG.md +13 -0
- package/lib/commands/fetch.js +17 -4
- package/lib/handler/imap-stream.js +7 -0
- package/lib/imap-flow.js +37 -7
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.195](https://github.com/postalsys/imapflow/compare/v1.0.194...v1.0.195) (2025-08-26)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* Add async yielding to compression readNext() to prevent CPU blocking ([18e806d](https://github.com/postalsys/imapflow/commit/18e806d5485defe6d4a0ef3eb502b17f3b2d8c2d))
|
|
9
|
+
* Add async yielding to lock processing loop ([fbf1d9a](https://github.com/postalsys/imapflow/commit/fbf1d9a3aa93b4733a123553d8be0b1785f4135e))
|
|
10
|
+
* Add async yielding to processInput() to prevent CPU blocking ([6b619ce](https://github.com/postalsys/imapflow/commit/6b619ced453c3ee8556e18646ad476ad82a68fbf))
|
|
11
|
+
* Add async yielding to reader() method to prevent CPU blocking ([40574e3](https://github.com/postalsys/imapflow/commit/40574e322be27d8b3cdd1b32416e8395d7e395b5))
|
|
12
|
+
* Add exponential backoff to FETCH retry logic ([fb99e2d](https://github.com/postalsys/imapflow/commit/fb99e2d26c9cb29e69b6d33083ef101264b4b039))
|
|
13
|
+
* Add rate limiting for compression operations ([f54c1d0](https://github.com/postalsys/imapflow/commit/f54c1d0caf1bd8cc41a0779e12c915adbb55f346))
|
|
14
|
+
* Correct log source indicator for client commands ([bb3ea09](https://github.com/postalsys/imapflow/commit/bb3ea09c3135c131fc8bedd733311aa3fe176261))
|
|
15
|
+
|
|
3
16
|
## [1.0.194](https://github.com/postalsys/imapflow/compare/v1.0.193...v1.0.194) (2025-08-13)
|
|
4
17
|
|
|
5
18
|
|
package/lib/commands/fetch.js
CHANGED
|
@@ -16,7 +16,10 @@ module.exports = async (connection, range, query, options) => {
|
|
|
16
16
|
const commandKey = connection.capabilities.has('BINARY') && options.binary && !connection.disableBinary ? 'BINARY' : 'BODY';
|
|
17
17
|
|
|
18
18
|
let retryCount = 0;
|
|
19
|
-
|
|
19
|
+
const maxRetries = 4;
|
|
20
|
+
const baseDelay = 1000; // Start with 1 second delay
|
|
21
|
+
|
|
22
|
+
while (retryCount < maxRetries) {
|
|
20
23
|
let messages = {
|
|
21
24
|
count: 0,
|
|
22
25
|
list: []
|
|
@@ -189,15 +192,25 @@ module.exports = async (connection, range, query, options) => {
|
|
|
189
192
|
return messages;
|
|
190
193
|
} catch (err) {
|
|
191
194
|
if (err.code === 'ETHROTTLE') {
|
|
192
|
-
//
|
|
195
|
+
// Calculate exponential backoff delay
|
|
196
|
+
const backoffDelay = Math.min(baseDelay * Math.pow(2, retryCount), 30000); // Cap at 30 seconds
|
|
197
|
+
|
|
198
|
+
// Use throttle reset time if provided and longer than backoff
|
|
199
|
+
const delay = err.throttleReset && err.throttleReset > backoffDelay ? err.throttleReset : backoffDelay;
|
|
200
|
+
|
|
193
201
|
connection.log.warn({
|
|
194
|
-
msg: 'Retrying throttled request',
|
|
202
|
+
msg: 'Retrying throttled request with exponential backoff',
|
|
195
203
|
cid: connection.id,
|
|
196
204
|
code: err.code,
|
|
197
205
|
response: err.responseText,
|
|
198
206
|
throttleReset: err.throttleReset,
|
|
199
|
-
retryCount
|
|
207
|
+
retryCount,
|
|
208
|
+
delayMs: delay
|
|
200
209
|
});
|
|
210
|
+
|
|
211
|
+
// Wait before retrying
|
|
212
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
213
|
+
|
|
201
214
|
retryCount++;
|
|
202
215
|
continue;
|
|
203
216
|
}
|
|
@@ -201,10 +201,17 @@ class ImapStream extends Transform {
|
|
|
201
201
|
|
|
202
202
|
async processInput() {
|
|
203
203
|
let data;
|
|
204
|
+
let processedCount = 0;
|
|
204
205
|
while ((data = this.inputQueue.shift())) {
|
|
205
206
|
await this.processInputChunk(data.chunk);
|
|
206
207
|
// mark chunk as processed
|
|
207
208
|
data.next();
|
|
209
|
+
|
|
210
|
+
// Yield to event loop every 10 chunks to prevent CPU blocking
|
|
211
|
+
processedCount++;
|
|
212
|
+
if (processedCount % 10 === 0) {
|
|
213
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
214
|
+
}
|
|
208
215
|
}
|
|
209
216
|
}
|
|
210
217
|
|
package/lib/imap-flow.js
CHANGED
|
@@ -495,7 +495,7 @@ class ImapFlow extends EventEmitter {
|
|
|
495
495
|
|
|
496
496
|
let options = data.options || {};
|
|
497
497
|
|
|
498
|
-
this.log.debug({ src: '
|
|
498
|
+
this.log.debug({ src: 'c', msg: logCompiled.toString(), cid: this.id, comment: options.comment });
|
|
499
499
|
|
|
500
500
|
this.write(this.commandParts.shift());
|
|
501
501
|
|
|
@@ -572,6 +572,7 @@ class ImapFlow extends EventEmitter {
|
|
|
572
572
|
|
|
573
573
|
async reader() {
|
|
574
574
|
let data;
|
|
575
|
+
let processedCount = 0;
|
|
575
576
|
while ((data = this.streamer.read()) !== null) {
|
|
576
577
|
let parsed;
|
|
577
578
|
|
|
@@ -737,6 +738,12 @@ class ImapFlow extends EventEmitter {
|
|
|
737
738
|
}
|
|
738
739
|
|
|
739
740
|
data.next();
|
|
741
|
+
|
|
742
|
+
// Yield to event loop every 10 processed messages to prevent CPU blocking
|
|
743
|
+
processedCount++;
|
|
744
|
+
if (processedCount % 10 === 0) {
|
|
745
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
746
|
+
}
|
|
740
747
|
}
|
|
741
748
|
}
|
|
742
749
|
|
|
@@ -885,11 +892,17 @@ class ImapFlow extends EventEmitter {
|
|
|
885
892
|
return; // was not able to negotiate compression
|
|
886
893
|
}
|
|
887
894
|
|
|
888
|
-
// create deflate/inflate streams
|
|
895
|
+
// create deflate/inflate streams with rate limiting options
|
|
889
896
|
this._deflate = zlib.createDeflateRaw({
|
|
890
|
-
windowBits: 15
|
|
897
|
+
windowBits: 15,
|
|
898
|
+
level: zlib.constants.Z_DEFAULT_COMPRESSION, // Use default compression level (6)
|
|
899
|
+
memLevel: 8, // Memory usage level (8 is default)
|
|
900
|
+
strategy: zlib.constants.Z_DEFAULT_STRATEGY,
|
|
901
|
+
chunkSize: 16 * 1024 // Process in 16KB chunks to prevent CPU blocking
|
|
902
|
+
});
|
|
903
|
+
this._inflate = zlib.createInflateRaw({
|
|
904
|
+
chunkSize: 16 * 1024 // Process in 16KB chunks to prevent CPU blocking
|
|
891
905
|
});
|
|
892
|
-
this._inflate = zlib.createInflateRaw();
|
|
893
906
|
|
|
894
907
|
// route incoming socket via inflate stream
|
|
895
908
|
this.socket.unpipe(this.streamer);
|
|
@@ -899,8 +912,10 @@ class ImapFlow extends EventEmitter {
|
|
|
899
912
|
this.streamer.emit('error', err);
|
|
900
913
|
});
|
|
901
914
|
|
|
902
|
-
// route outgoing socket via deflate stream
|
|
903
|
-
this.writeSocket = new PassThrough(
|
|
915
|
+
// route outgoing socket via deflate stream with rate limiting
|
|
916
|
+
this.writeSocket = new PassThrough({
|
|
917
|
+
highWaterMark: 64 * 1024 // 64KB buffer limit to prevent excessive memory usage
|
|
918
|
+
});
|
|
904
919
|
|
|
905
920
|
this.writeSocket.destroySoon = () => {
|
|
906
921
|
try {
|
|
@@ -921,15 +936,23 @@ class ImapFlow extends EventEmitter {
|
|
|
921
936
|
// we need to force flush deflated data to socket so we can't
|
|
922
937
|
// use normal pipes for this.writeSocket -> this._deflate -> this.socket
|
|
923
938
|
let reading = false;
|
|
924
|
-
let
|
|
939
|
+
let processedChunks = 0;
|
|
940
|
+
let readNext = async () => {
|
|
925
941
|
try {
|
|
926
942
|
reading = true;
|
|
943
|
+
processedChunks = 0;
|
|
927
944
|
|
|
928
945
|
let chunk;
|
|
929
946
|
while ((chunk = this.writeSocket.read()) !== null) {
|
|
930
947
|
if (this._deflate && this._deflate.write(chunk) === false) {
|
|
931
948
|
return this._deflate.once('drain', readNext);
|
|
932
949
|
}
|
|
950
|
+
|
|
951
|
+
// Yield to event loop every 100 chunks to prevent CPU blocking
|
|
952
|
+
processedChunks++;
|
|
953
|
+
if (processedChunks % 100 === 0) {
|
|
954
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
955
|
+
}
|
|
933
956
|
}
|
|
934
957
|
|
|
935
958
|
// flush data to socket
|
|
@@ -3236,6 +3259,7 @@ class ImapFlow extends EventEmitter {
|
|
|
3236
3259
|
|
|
3237
3260
|
try {
|
|
3238
3261
|
// Process all locks in queue until empty
|
|
3262
|
+
let processedCount = 0;
|
|
3239
3263
|
while (this.locks.length > 0) {
|
|
3240
3264
|
if (!this.locks.length) {
|
|
3241
3265
|
this.log.trace({
|
|
@@ -3245,6 +3269,12 @@ class ImapFlow extends EventEmitter {
|
|
|
3245
3269
|
return;
|
|
3246
3270
|
}
|
|
3247
3271
|
|
|
3272
|
+
// Yield to event loop periodically to prevent CPU blocking
|
|
3273
|
+
processedCount++;
|
|
3274
|
+
if (processedCount % 5 === 0) {
|
|
3275
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3248
3278
|
const release = () => {
|
|
3249
3279
|
if (this.currentLock) {
|
|
3250
3280
|
this.log.trace({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imapflow",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.195",
|
|
4
4
|
"description": "IMAP Client for Node",
|
|
5
5
|
"main": "lib/imap-flow.js",
|
|
6
6
|
"types": "lib/imap-flow.d.ts",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"@babel/eslint-parser": "7.28.0",
|
|
32
32
|
"@babel/eslint-plugin": "7.27.1",
|
|
33
33
|
"@babel/plugin-syntax-class-properties": "7.12.13",
|
|
34
|
-
"@babel/preset-env": "7.28.
|
|
35
|
-
"@types/node": "24.
|
|
34
|
+
"@babel/preset-env": "7.28.3",
|
|
35
|
+
"@types/node": "24.3.0",
|
|
36
36
|
"eslint": "8.57.0",
|
|
37
37
|
"eslint-config-nodemailer": "1.2.0",
|
|
38
38
|
"eslint-config-prettier": "9.1.0",
|
|
@@ -42,18 +42,18 @@
|
|
|
42
42
|
"grunt-eslint": "24.3.0",
|
|
43
43
|
"imapflow-jsdoc-template": "3.4.0-imapflow.3",
|
|
44
44
|
"jsdoc": "4.0.4",
|
|
45
|
-
"st": "3.0.
|
|
45
|
+
"st": "3.0.3",
|
|
46
46
|
"typescript": "5.9.2"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"encoding-japanese": "2.2.0",
|
|
50
|
-
"iconv-lite": "0.
|
|
50
|
+
"iconv-lite": "0.7.0",
|
|
51
51
|
"libbase64": "1.3.0",
|
|
52
52
|
"libmime": "5.3.7",
|
|
53
53
|
"libqp": "2.1.1",
|
|
54
54
|
"mailsplit": "5.4.6",
|
|
55
55
|
"nodemailer": "7.0.5",
|
|
56
|
-
"pino": "9.
|
|
56
|
+
"pino": "9.9.0",
|
|
57
57
|
"socks": "2.8.7"
|
|
58
58
|
}
|
|
59
59
|
}
|