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.
@@ -8,7 +8,7 @@ jobs:
8
8
  test:
9
9
  strategy:
10
10
  matrix:
11
- node: [16.x, 18.x]
11
+ node: [20.x, 22.x, 24.x]
12
12
  os: [ubuntu-latest]
13
13
  runs-on: ${{ matrix.os }}
14
14
  steps:
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
 
@@ -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
- while (retryCount < 4) {
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
- // retrying
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: 's', msg: logCompiled.toString(), cid: this.id, comment: options.comment });
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 readNext = () => {
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.194",
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.0",
35
- "@types/node": "24.2.1",
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.2",
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.6.3",
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.8.0",
56
+ "pino": "9.9.0",
57
57
  "socks": "2.8.7"
58
58
  }
59
59
  }