imapflow 1.0.193 → 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/lib/imap-flow.js CHANGED
@@ -369,7 +369,8 @@ class ImapFlow extends EventEmitter {
369
369
  this.streamer.on('error', err => {
370
370
  if (['Z_BUF_ERROR', 'ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'EHOSTUNREACH'].includes(err.code)) {
371
371
  // just close the connection, usually nothing but noise
372
- return setImmediate(() => this.close());
372
+ this.closeAfter();
373
+ return;
373
374
  }
374
375
 
375
376
  this.log.error({ err, cid: this.id });
@@ -386,7 +387,7 @@ class ImapFlow extends EventEmitter {
386
387
  }
387
388
  err._connId = err._connId || this.id;
388
389
 
389
- setImmediate(() => this.close());
390
+ this.closeAfter();
390
391
  this.emit('error', err);
391
392
  }
392
393
 
@@ -494,7 +495,7 @@ class ImapFlow extends EventEmitter {
494
495
 
495
496
  let options = data.options || {};
496
497
 
497
- 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 });
498
499
 
499
500
  this.write(this.commandParts.shift());
500
501
 
@@ -571,6 +572,7 @@ class ImapFlow extends EventEmitter {
571
572
 
572
573
  async reader() {
573
574
  let data;
575
+ let processedCount = 0;
574
576
  while ((data = this.streamer.read()) !== null) {
575
577
  let parsed;
576
578
 
@@ -736,6 +738,12 @@ class ImapFlow extends EventEmitter {
736
738
  }
737
739
 
738
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
+ }
739
747
  }
740
748
  }
741
749
 
@@ -764,16 +772,8 @@ class ImapFlow extends EventEmitter {
764
772
  this.log.error({ err, cid: this.id });
765
773
  this.emitError(err);
766
774
  });
767
- this._socketClose =
768
- this._socketClose ||
769
- (() => {
770
- this.close();
771
- });
772
- this._socketEnd =
773
- this._socketEnd ||
774
- (() => {
775
- this.close();
776
- });
775
+ this._socketClose = this._socketClose || (() => this.close());
776
+ this._socketEnd = this._socketEnd || (() => this.close());
777
777
 
778
778
  /**
779
779
  * Socket timeout event handler.
@@ -892,11 +892,17 @@ class ImapFlow extends EventEmitter {
892
892
  return; // was not able to negotiate compression
893
893
  }
894
894
 
895
- // create deflate/inflate streams
895
+ // create deflate/inflate streams with rate limiting options
896
896
  this._deflate = zlib.createDeflateRaw({
897
- 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
898
905
  });
899
- this._inflate = zlib.createInflateRaw();
900
906
 
901
907
  // route incoming socket via inflate stream
902
908
  this.socket.unpipe(this.streamer);
@@ -906,8 +912,10 @@ class ImapFlow extends EventEmitter {
906
912
  this.streamer.emit('error', err);
907
913
  });
908
914
 
909
- // route outgoing socket via deflate stream
910
- 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
+ });
911
919
 
912
920
  this.writeSocket.destroySoon = () => {
913
921
  try {
@@ -928,22 +936,34 @@ class ImapFlow extends EventEmitter {
928
936
  // we need to force flush deflated data to socket so we can't
929
937
  // use normal pipes for this.writeSocket -> this._deflate -> this.socket
930
938
  let reading = false;
931
- let readNext = () => {
932
- reading = true;
939
+ let processedChunks = 0;
940
+ let readNext = async () => {
941
+ try {
942
+ reading = true;
943
+ processedChunks = 0;
944
+
945
+ let chunk;
946
+ while ((chunk = this.writeSocket.read()) !== null) {
947
+ if (this._deflate && this._deflate.write(chunk) === false) {
948
+ return this._deflate.once('drain', readNext);
949
+ }
933
950
 
934
- let chunk;
935
- while ((chunk = this.writeSocket.read()) !== null) {
936
- if (this._deflate && this._deflate.write(chunk) === false) {
937
- return this._deflate.once('drain', readNext);
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
+ }
938
956
  }
939
- }
940
957
 
941
- // flush data to socket
942
- if (this._deflate) {
943
- this._deflate.flush();
944
- }
958
+ // flush data to socket
959
+ if (this._deflate) {
960
+ this._deflate.flush();
961
+ }
945
962
 
946
- reading = false;
963
+ reading = false;
964
+ } catch (ex) {
965
+ this.emitError(ex);
966
+ }
947
967
  };
948
968
 
949
969
  this.writeSocket.on('readable', () => {
@@ -1024,7 +1044,7 @@ class ImapFlow extends EventEmitter {
1024
1044
  // don't care anymore
1025
1045
  return;
1026
1046
  }
1027
- setImmediate(() => this.close());
1047
+ this.closeAfter();
1028
1048
  this.upgrading = false;
1029
1049
  err.tlsFailed = true;
1030
1050
  reject(err);
@@ -1034,7 +1054,7 @@ class ImapFlow extends EventEmitter {
1034
1054
  if (!this.upgrading) {
1035
1055
  return;
1036
1056
  }
1037
- setImmediate(() => this.close());
1057
+ this.closeAfter();
1038
1058
  let err = new Error('Failed to upgrade connection in required time');
1039
1059
  err.tlsFailed = true;
1040
1060
  err.code = 'UPGRADE_TIMEOUT';
@@ -1043,30 +1063,34 @@ class ImapFlow extends EventEmitter {
1043
1063
 
1044
1064
  this.upgrading = true;
1045
1065
  this.socket = tls.connect(opts, () => {
1046
- clearTimeout(this.upgradeTimeout);
1047
- if (this.isClosed) {
1048
- // not sure if this is possible?
1049
- return this.close();
1050
- }
1066
+ try {
1067
+ clearTimeout(this.upgradeTimeout);
1068
+ if (this.isClosed) {
1069
+ // not sure if this is possible?
1070
+ return this.close();
1071
+ }
1051
1072
 
1052
- this.secureConnection = true;
1053
- this.upgrading = false;
1054
- this.streamer.secureConnection = true;
1055
- this.socket.pipe(this.streamer);
1056
- this.tls = typeof this.socket.getCipher === 'function' ? this.socket.getCipher() : false;
1057
- if (this.tls) {
1058
- this.tls.authorized = this.socket.authorized;
1059
- this.log.info({
1060
- src: 'tls',
1061
- msg: 'Established TLS session',
1062
- cid: this.id,
1063
- authorized: this.tls.authorized,
1064
- algo: this.tls.standardName || this.tls.name,
1065
- version: this.tls.version
1066
- });
1067
- }
1073
+ this.secureConnection = true;
1074
+ this.upgrading = false;
1075
+ this.streamer.secureConnection = true;
1076
+ this.socket.pipe(this.streamer);
1077
+ this.tls = typeof this.socket.getCipher === 'function' ? this.socket.getCipher() : false;
1078
+ if (this.tls) {
1079
+ this.tls.authorized = this.socket.authorized;
1080
+ this.log.info({
1081
+ src: 'tls',
1082
+ msg: 'Established TLS session',
1083
+ cid: this.id,
1084
+ authorized: this.tls.authorized,
1085
+ algo: this.tls.standardName || this.tls.name,
1086
+ version: this.tls.version
1087
+ });
1088
+ }
1068
1089
 
1069
- return resolve(true);
1090
+ return resolve(true);
1091
+ } catch (ex) {
1092
+ this.emitError(ex);
1093
+ }
1070
1094
  });
1071
1095
 
1072
1096
  this.writeSocket = this.socket;
@@ -1214,7 +1238,7 @@ class ImapFlow extends EventEmitter {
1214
1238
  return reject(err);
1215
1239
  }
1216
1240
 
1217
- setImmediate(() => this.close());
1241
+ this.closeAfter();
1218
1242
  });
1219
1243
  }
1220
1244
 
@@ -1517,55 +1541,62 @@ class ImapFlow extends EventEmitter {
1517
1541
  connectionTimeout: this.options.connectionTimeout || CONNECT_TIMEOUT
1518
1542
  };
1519
1543
  this.log.error({ err, cid: this.id });
1520
- setImmediate(() => this.close());
1544
+ this.closeAfter();
1521
1545
  reject(err);
1522
1546
  }, this.options.connectionTimeout || CONNECT_TIMEOUT);
1523
1547
 
1524
1548
  let onConnect = () => {
1525
- clearTimeout(this.connectTimeout);
1526
- this.socket.setKeepAlive(true, 5 * 1000);
1527
- this.socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT);
1528
-
1529
- this.greetingTimeout = setTimeout(() => {
1530
- let err = new Error(`Failed to receive greeting from server in required time${!this.secureConnection ? '. Maybe should use TLS?' : ''}`);
1531
- err.code = 'GREETING_TIMEOUT';
1532
- err.details = {
1533
- greetingTimeout: this.options.greetingTimeout || GREETING_TIMEOUT
1549
+ try {
1550
+ clearTimeout(this.connectTimeout);
1551
+ this.socket.setKeepAlive(true, 5 * 1000);
1552
+ this.socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT);
1553
+
1554
+ this.greetingTimeout = setTimeout(() => {
1555
+ let err = new Error(
1556
+ `Failed to receive greeting from server in required time${!this.secureConnection ? '. Maybe should use TLS?' : ''}`
1557
+ );
1558
+ err.code = 'GREETING_TIMEOUT';
1559
+ err.details = {
1560
+ greetingTimeout: this.options.greetingTimeout || GREETING_TIMEOUT
1561
+ };
1562
+ this.log.error({ err, cid: this.id });
1563
+ this.closeAfter();
1564
+ reject(err);
1565
+ }, this.options.greetingTimeout || GREETING_TIMEOUT);
1566
+
1567
+ this.tls = typeof this.socket.getCipher === 'function' ? this.socket.getCipher() : false;
1568
+
1569
+ let logInfo = {
1570
+ src: 'connection',
1571
+ msg: `Established ${this.tls ? 'secure ' : ''}TCP connection`,
1572
+ cid: this.id,
1573
+ secure: !!this.tls,
1574
+ host: this.host,
1575
+ servername: this.servername,
1576
+ port: this.socket.remotePort,
1577
+ address: this.socket.remoteAddress,
1578
+ localAddress: this.socket.localAddress,
1579
+ localPort: this.socket.localPort
1534
1580
  };
1535
- this.log.error({ err, cid: this.id });
1536
- setImmediate(() => this.close());
1537
- reject(err);
1538
- }, this.options.greetingTimeout || GREETING_TIMEOUT);
1539
-
1540
- this.tls = typeof this.socket.getCipher === 'function' ? this.socket.getCipher() : false;
1541
-
1542
- let logInfo = {
1543
- src: 'connection',
1544
- msg: `Established ${this.tls ? 'secure ' : ''}TCP connection`,
1545
- cid: this.id,
1546
- secure: !!this.tls,
1547
- host: this.host,
1548
- servername: this.servername,
1549
- port: this.socket.remotePort,
1550
- address: this.socket.remoteAddress,
1551
- localAddress: this.socket.localAddress,
1552
- localPort: this.socket.localPort
1553
- };
1554
1581
 
1555
- if (this.tls) {
1556
- logInfo.authorized = this.tls.authorized = this.socket.authorized;
1557
- logInfo.algo = this.tls.standardName || this.tls.name;
1558
- logInfo.version = this.tls.version;
1559
- }
1582
+ if (this.tls) {
1583
+ logInfo.authorized = this.tls.authorized = this.socket.authorized;
1584
+ logInfo.algo = this.tls.standardName || this.tls.name;
1585
+ logInfo.version = this.tls.version;
1586
+ }
1560
1587
 
1561
- this.log.info(logInfo);
1588
+ this.log.info(logInfo);
1562
1589
 
1563
- this.setSocketHandlers();
1564
- this.socket.pipe(this.streamer);
1590
+ this.setSocketHandlers();
1591
+ this.socket.pipe(this.streamer);
1565
1592
 
1566
- // executed by initial "* OK"
1567
- this.initialResolve = resolve;
1568
- this.initialReject = reject;
1593
+ // executed by initial "* OK"
1594
+ this.initialResolve = resolve;
1595
+ this.initialReject = reject;
1596
+ } catch (ex) {
1597
+ // connect failed
1598
+ reject(ex);
1599
+ }
1569
1600
  };
1570
1601
 
1571
1602
  if (socket) {
@@ -1588,7 +1619,7 @@ class ImapFlow extends EventEmitter {
1588
1619
  this.socket.on('error', err => {
1589
1620
  clearTimeout(this.connectTimeout);
1590
1621
  clearTimeout(this.greetingTimeout);
1591
- setImmediate(() => this.close());
1622
+ this.closeAfter();
1592
1623
  this.log.error({ err, cid: this.id });
1593
1624
  reject(err);
1594
1625
  });
@@ -1611,6 +1642,15 @@ class ImapFlow extends EventEmitter {
1611
1642
  return await this.run('LOGOUT');
1612
1643
  }
1613
1644
 
1645
+ /**
1646
+ * Close the TCP connection.
1647
+ * Unlike `close()`, return immediately from this function, allowing the
1648
+ * caller function to proceed, and run `close()` function afterwards.
1649
+ */
1650
+ closeAfter() {
1651
+ setImmediate(() => this.close());
1652
+ }
1653
+
1614
1654
  /**
1615
1655
  * Closes TCP connection without notifying the server.
1616
1656
  *
@@ -1621,101 +1661,114 @@ class ImapFlow extends EventEmitter {
1621
1661
  * client.close();
1622
1662
  */
1623
1663
  close() {
1624
- // clear pending timers
1625
- clearTimeout(this.idleStartTimer);
1626
- clearTimeout(this.upgradeTimeout);
1627
- clearTimeout(this.connectTimeout);
1628
- clearTimeout(this.greetingTimeout);
1629
-
1630
- this.usable = false;
1631
- this.idling = false;
1632
-
1633
- if (typeof this.initialReject === 'function' && !this.options.verifyOnly) {
1664
+ try {
1665
+ // clear pending timers
1666
+ clearTimeout(this.idleStartTimer);
1667
+ clearTimeout(this.upgradeTimeout);
1668
+ clearTimeout(this.connectTimeout);
1634
1669
  clearTimeout(this.greetingTimeout);
1635
- let reject = this.initialReject;
1636
- this.initialResolve = false;
1637
- this.initialReject = false;
1638
- let err = new Error('Unexpected close');
1639
- err.code = `ClosedAfterConnect${this.secureConnection ? 'TLS' : 'Text'}`;
1640
- // still has to go through the logic below
1641
- setImmediate(() => reject(err));
1642
- }
1643
1670
 
1644
- if (typeof this.preCheck === 'function') {
1645
- this.preCheck().catch(err => this.log.warn({ err, cid: this.id }));
1646
- }
1671
+ this.usable = false;
1672
+ this.idling = false;
1647
1673
 
1648
- // reject command that is currently processed
1649
- if (this.currentRequest && this.requestTagMap.has(this.currentRequest.tag)) {
1650
- let request = this.requestTagMap.get(this.currentRequest.tag);
1651
- if (request) {
1652
- this.requestTagMap.delete(request.tag);
1653
- const error = new Error('Connection not available');
1654
- error.code = 'NoConnection';
1655
- request.reject(error);
1674
+ if (typeof this.initialReject === 'function' && !this.options.verifyOnly) {
1675
+ clearTimeout(this.greetingTimeout);
1676
+ let reject = this.initialReject;
1677
+ this.initialResolve = false;
1678
+ this.initialReject = false;
1679
+ let err = new Error('Unexpected close');
1680
+ err.code = `ClosedAfterConnect${this.secureConnection ? 'TLS' : 'Text'}`;
1681
+ // still has to go through the logic below
1682
+ setImmediate(() => reject(err));
1683
+ }
1684
+
1685
+ if (typeof this.preCheck === 'function') {
1686
+ this.preCheck().catch(err => this.log.warn({ err, cid: this.id }));
1656
1687
  }
1657
- this.currentRequest = false;
1658
- }
1659
1688
 
1660
- // reject all other pending commands
1661
- while (this.requestQueue.length) {
1662
- let req = this.requestQueue.shift();
1663
- if (req && this.requestTagMap.has(req.tag)) {
1664
- let request = this.requestTagMap.get(req.tag);
1689
+ // reject command that is currently processed
1690
+ if (this.currentRequest && this.requestTagMap.has(this.currentRequest.tag)) {
1691
+ let request = this.requestTagMap.get(this.currentRequest.tag);
1665
1692
  if (request) {
1666
1693
  this.requestTagMap.delete(request.tag);
1667
1694
  const error = new Error('Connection not available');
1668
1695
  error.code = 'NoConnection';
1669
1696
  request.reject(error);
1670
1697
  }
1698
+ this.currentRequest = false;
1699
+ }
1700
+
1701
+ // reject all other pending commands
1702
+ while (this.requestQueue.length) {
1703
+ let req = this.requestQueue.shift();
1704
+ if (req && this.requestTagMap.has(req.tag)) {
1705
+ let request = this.requestTagMap.get(req.tag);
1706
+ if (request) {
1707
+ this.requestTagMap.delete(request.tag);
1708
+ const error = new Error('Connection not available');
1709
+ error.code = 'NoConnection';
1710
+ request.reject(error);
1711
+ }
1712
+ }
1671
1713
  }
1672
- }
1673
1714
 
1674
- this.state = this.states.LOGOUT;
1675
- if (this.isClosed) {
1676
- return;
1677
- }
1678
- this.isClosed = true;
1715
+ // cleanup compression streams if they exist
1716
+ if (this._inflate) {
1717
+ try {
1718
+ this._inflate.unpipe();
1719
+ this._inflate.destroy();
1720
+ this._inflate = null;
1721
+ } catch (err) {
1722
+ this.log.error({ err, info: 'Failed to destroy inflate stream', cid: this.id });
1723
+ }
1724
+ }
1679
1725
 
1680
- if (this.writeSocket && !this.writeSocket.destroyed) {
1681
- try {
1682
- this.writeSocket.destroy();
1683
- } catch (err) {
1684
- this.log.error({ err, cid: this.id });
1726
+ if (this._deflate) {
1727
+ try {
1728
+ this._deflate.unpipe();
1729
+ this._deflate.destroy();
1730
+ this._deflate = null;
1731
+ } catch (err) {
1732
+ this.log.error({ err, info: 'Failed to destroy deflate stream', cid: this.id });
1733
+ }
1685
1734
  }
1686
- }
1687
1735
 
1688
- // cleanup compression streams if they exist
1689
- if (this._inflate) {
1690
- try {
1691
- this._inflate.unpipe();
1692
- this._inflate.destroy();
1693
- this._inflate = null;
1694
- } catch (err) {
1695
- this.log.error({ err, info: 'Failed to destroy inflate stream', cid: this.id });
1736
+ this.state = this.states.LOGOUT;
1737
+ if (this.isClosed) {
1738
+ return;
1696
1739
  }
1697
- }
1698
1740
 
1699
- if (this._deflate) {
1700
- try {
1701
- this._deflate.unpipe();
1702
- this._deflate.destroy();
1703
- this._deflate = null;
1704
- } catch (err) {
1705
- this.log.error({ err, info: 'Failed to destroy deflate stream', cid: this.id });
1741
+ if (this.socket && !this.socket.destroyed && this.writeSocket !== this.socket) {
1742
+ try {
1743
+ this.socket.destroy();
1744
+ } catch (err) {
1745
+ this.log.error({ err, cid: this.id });
1746
+ }
1706
1747
  }
1707
- }
1748
+ this.isClosed = true;
1708
1749
 
1709
- if (this.socket && !this.socket.destroyed && this.writeSocket !== this.socket) {
1710
- try {
1711
- this.socket.destroy();
1712
- } catch (err) {
1713
- this.log.error({ err, cid: this.id });
1750
+ if (this.writeSocket && !this.writeSocket.destroyed) {
1751
+ try {
1752
+ this.writeSocket.destroy();
1753
+ } catch (err) {
1754
+ this.log.error({ err, cid: this.id });
1755
+ }
1756
+ }
1757
+
1758
+ if (this.socket && !this.socket.destroyed && this.writeSocket !== this.socket) {
1759
+ try {
1760
+ this.socket.destroy();
1761
+ } catch (err) {
1762
+ this.log.error({ err, cid: this.id });
1763
+ }
1714
1764
  }
1715
- }
1716
1765
 
1717
- this.log.trace({ msg: 'Connection closed', cid: this.id });
1718
- this.emit('close');
1766
+ this.log.trace({ msg: 'Connection closed', cid: this.id });
1767
+ this.emit('close');
1768
+ } catch (ex) {
1769
+ // close failed
1770
+ this.log.error(ex);
1771
+ }
1719
1772
  }
1720
1773
 
1721
1774
  /**
@@ -2973,7 +3026,18 @@ class ImapFlow extends EventEmitter {
2973
3026
  };
2974
3027
 
2975
3028
  setImmediate(() => {
2976
- if (!writeChunk(chunk)) {
3029
+ let writeResult;
3030
+ try {
3031
+ writeResult = writeChunk(chunk);
3032
+ } catch (err) {
3033
+ stream.emit('error', err);
3034
+ if (!fetchAborted && stream && !stream.destroyed) {
3035
+ stream.end();
3036
+ }
3037
+ return;
3038
+ }
3039
+
3040
+ if (!writeResult) {
2977
3041
  // Initial chunk filled the buffer, wait for drain
2978
3042
  stream.once('drain', () => {
2979
3043
  if (!fetchAborted) {
@@ -3172,126 +3236,133 @@ class ImapFlow extends EventEmitter {
3172
3236
  return result;
3173
3237
  }
3174
3238
 
3175
- async processLocks(force) {
3176
- if (!force) {
3177
- // Atomic test-and-set to prevent race condition
3178
- const wasProcessing = this.processingLock;
3179
- this.processingLock = true;
3180
-
3181
- if (wasProcessing) {
3182
- // Another processor is already running, reset and exit
3183
- this.processingLock = wasProcessing;
3184
- this.log.trace({
3185
- msg: 'Mailbox locking queued',
3186
- path: this.mailbox && this.mailbox.path,
3187
- pending: this.locks.length,
3188
- idling: this.idling,
3189
- activeLock: this.currentLock
3190
- ? {
3191
- lockId: this.currentLock.lockId,
3192
- ...(this.currentLock.options?.description && { description: this.currentLock.options?.description })
3193
- }
3194
- : null
3195
- });
3196
- return;
3197
- }
3198
- } else {
3199
- this.processingLock = true;
3239
+ async processLocks() {
3240
+ // Atomic test-and-set to prevent race condition
3241
+ const wasProcessing = this.processingLock;
3242
+ if (wasProcessing) {
3243
+ // Another processor is already running, just exit
3244
+ this.log.trace({
3245
+ msg: 'Mailbox locking queued',
3246
+ path: this.mailbox && this.mailbox.path,
3247
+ pending: this.locks.length,
3248
+ idling: this.idling,
3249
+ activeLock: this.currentLock
3250
+ ? {
3251
+ lockId: this.currentLock.lockId,
3252
+ ...(this.currentLock.options?.description && { description: this.currentLock.options?.description })
3253
+ }
3254
+ : null
3255
+ });
3256
+ return;
3200
3257
  }
3258
+ this.processingLock = true;
3201
3259
 
3202
3260
  try {
3203
- if (!this.locks.length) {
3204
- this.log.trace({
3205
- msg: 'Mailbox locking queue processed',
3206
- idling: this.idling
3207
- });
3208
- return;
3209
- }
3210
-
3211
- const release = () => {
3212
- if (this.currentLock) {
3261
+ // Process all locks in queue until empty
3262
+ let processedCount = 0;
3263
+ while (this.locks.length > 0) {
3264
+ if (!this.locks.length) {
3213
3265
  this.log.trace({
3214
- msg: 'Mailbox lock released',
3215
- lockId: this.currentLock.lockId,
3216
- path: this.mailbox && this.mailbox.path,
3217
- pending: this.locks.length,
3266
+ msg: 'Mailbox locking queue processed',
3218
3267
  idling: this.idling
3219
3268
  });
3220
- this.currentLock = false;
3269
+ return;
3221
3270
  }
3222
- this.processLocks(true).catch(err => this.log.error({ err, cid: this.id }));
3223
- };
3224
3271
 
3225
- const lock = this.locks.shift();
3226
- const { resolve, reject, path, options, lockId } = lock;
3227
-
3228
- if (!this.usable || !this.socket || this.socket.destroyed) {
3229
- this.log.trace({ msg: 'Failed to acquire mailbox lock', path, lockId, idling: this.idling });
3230
- // reject all
3231
- let error = new Error('Connection not available');
3232
- error.code = 'NoConnection';
3233
- reject(error);
3234
- // Process remaining locks
3235
- await this.processLocks(true);
3236
- return;
3237
- }
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
+ }
3238
3277
 
3239
- if (this.mailbox && this.mailbox.path === path && !!this.mailbox.readOnly === !!options.readOnly) {
3240
- // nothing to do here, already selected
3241
- this.log.trace({
3242
- msg: 'Mailbox lock acquired [existing]',
3243
- path,
3244
- lockId,
3245
- idling: this.idling,
3246
- ...(options.description && { description: options.description })
3247
- });
3248
- this.currentLock = lock;
3249
- return resolve({ path, release });
3250
- } else {
3251
- try {
3252
- // Try to open. Throws if mailbox does not exists or can't open
3253
- await this.mailboxOpen(path, options);
3278
+ const release = () => {
3279
+ if (this.currentLock) {
3280
+ this.log.trace({
3281
+ msg: 'Mailbox lock released',
3282
+ lockId: this.currentLock.lockId,
3283
+ path: this.mailbox && this.mailbox.path,
3284
+ pending: this.locks.length,
3285
+ idling: this.idling
3286
+ });
3287
+ this.currentLock = false;
3288
+ }
3289
+ // Use setImmediate to avoid stack overflow
3290
+ setImmediate(() => {
3291
+ this.processLocks().catch(err => this.log.error({ err, cid: this.id }));
3292
+ });
3293
+ };
3294
+
3295
+ const lock = this.locks.shift();
3296
+ const { resolve, reject, path, options, lockId } = lock;
3297
+
3298
+ if (!this.usable || !this.socket || this.socket.destroyed) {
3299
+ this.log.trace({ msg: 'Failed to acquire mailbox lock', path, lockId, idling: this.idling });
3300
+ let error = new Error('Connection not available');
3301
+ error.code = 'NoConnection';
3302
+ reject(error);
3303
+ continue; // Process next lock in queue
3304
+ }
3305
+
3306
+ if (this.mailbox && this.mailbox.path === path && !!this.mailbox.readOnly === !!options.readOnly) {
3307
+ // nothing to do here, already selected
3254
3308
  this.log.trace({
3255
- msg: 'Mailbox lock acquired [selected]',
3309
+ msg: 'Mailbox lock acquired [existing]',
3256
3310
  path,
3257
3311
  lockId,
3258
3312
  idling: this.idling,
3259
3313
  ...(options.description && { description: options.description })
3260
3314
  });
3261
3315
  this.currentLock = lock;
3262
- return resolve({ path, release });
3263
- } catch (err) {
3264
- if (err.responseStatus === 'NO') {
3265
- try {
3266
- let folders = await this.run('LIST', '', path, { listOnly: true });
3267
- if (!folders || !folders.length) {
3268
- err.mailboxMissing = true;
3316
+ resolve({ path, release });
3317
+ break; // Wait for this lock to be released
3318
+ } else {
3319
+ try {
3320
+ // Try to open. Throws if mailbox does not exists or can't open
3321
+ await this.mailboxOpen(path, options);
3322
+ this.log.trace({
3323
+ msg: 'Mailbox lock acquired [selected]',
3324
+ path,
3325
+ lockId,
3326
+ idling: this.idling,
3327
+ ...(options.description && { description: options.description })
3328
+ });
3329
+ this.currentLock = lock;
3330
+ resolve({ path, release });
3331
+ break; // Wait for this lock to be released
3332
+ } catch (err) {
3333
+ if (err.responseStatus === 'NO') {
3334
+ try {
3335
+ let folders = await this.run('LIST', '', path, { listOnly: true });
3336
+ if (!folders || !folders.length) {
3337
+ err.mailboxMissing = true;
3338
+ }
3339
+ } catch (E) {
3340
+ this.log.trace({ msg: 'Failed to verify failed mailbox', path, err: E });
3269
3341
  }
3270
- } catch (E) {
3271
- this.log.trace({ msg: 'Failed to verify failed mailbox', path, err: E });
3272
3342
  }
3273
- }
3274
3343
 
3275
- this.log.trace({
3276
- msg: 'Failed to acquire mailbox lock',
3277
- path,
3278
- lockId,
3279
- idling: this.idling,
3280
- ...(options.description && { description: options.description }),
3281
- err
3282
- });
3283
- reject(err);
3284
- // Process remaining locks
3285
- await this.processLocks(true);
3344
+ this.log.trace({
3345
+ msg: 'Failed to acquire mailbox lock',
3346
+ path,
3347
+ lockId,
3348
+ idling: this.idling,
3349
+ ...(options.description && { description: options.description }),
3350
+ err
3351
+ });
3352
+ reject(err);
3353
+ // Continue to next lock in queue
3354
+ }
3286
3355
  }
3287
3356
  }
3288
3357
  } finally {
3289
3358
  this.processingLock = false;
3290
3359
 
3291
3360
  // Check if new locks were added while we were processing
3292
- if (this.locks.length && !this.processingLock) {
3293
- // Recursively process any new locks that were added
3294
- this.processLocks(true).catch(err => this.log.error({ err, cid: this.id }));
3361
+ if (this.locks.length && !this.currentLock) {
3362
+ // Process any remaining locks
3363
+ setImmediate(() => {
3364
+ this.processLocks().catch(err => this.log.error({ err, cid: this.id }));
3365
+ });
3295
3366
  }
3296
3367
  }
3297
3368
  }