imapflow 1.0.192 → 1.0.194

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
@@ -4,10 +4,6 @@
4
4
  * @module imapflow
5
5
  */
6
6
 
7
- // TODO:
8
- // * Use buffers for compiled commands
9
- // * OAuth2 authentication
10
-
11
7
  const tls = require('tls');
12
8
  const net = require('net');
13
9
  const crypto = require('crypto');
@@ -373,11 +369,11 @@ class ImapFlow extends EventEmitter {
373
369
  this.streamer.on('error', err => {
374
370
  if (['Z_BUF_ERROR', 'ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'EHOSTUNREACH'].includes(err.code)) {
375
371
  // just close the connection, usually nothing but noise
376
- return setImmediate(() => this.close());
372
+ this.closeAfter();
373
+ return;
377
374
  }
378
375
 
379
376
  this.log.error({ err, cid: this.id });
380
- setImmediate(() => this.close());
381
377
  this.emitError(err);
382
378
  });
383
379
 
@@ -386,6 +382,12 @@ class ImapFlow extends EventEmitter {
386
382
  }
387
383
 
388
384
  emitError(err) {
385
+ if (!err) {
386
+ return;
387
+ }
388
+ err._connId = err._connId || this.id;
389
+
390
+ this.closeAfter();
389
391
  this.emit('error', err);
390
392
  }
391
393
 
@@ -761,19 +763,10 @@ class ImapFlow extends EventEmitter {
761
763
  this._socketError ||
762
764
  (err => {
763
765
  this.log.error({ err, cid: this.id });
764
- setImmediate(() => this.close());
765
766
  this.emitError(err);
766
767
  });
767
- this._socketClose =
768
- this._socketClose ||
769
- (() => {
770
- this.close();
771
- });
772
- this._socketEnd =
773
- this._socketEnd ||
774
- (() => {
775
- this.close();
776
- });
768
+ this._socketClose = this._socketClose || (() => this.close());
769
+ this._socketEnd = this._socketEnd || (() => this.close());
777
770
 
778
771
  /**
779
772
  * Socket timeout event handler.
@@ -790,6 +783,10 @@ class ImapFlow extends EventEmitter {
790
783
  err.code = 'ETIMEOUT';
791
784
 
792
785
  if (this.idling) {
786
+ if (!this.usable || !this.socket || this.socket.destroyed) {
787
+ this.emitError(err);
788
+ return;
789
+ }
793
790
  // Attempt to recover IDLE connections
794
791
  this.run('NOOP')
795
792
  .then(() => this.idle())
@@ -797,8 +794,7 @@ class ImapFlow extends EventEmitter {
797
794
  } else {
798
795
  // Close immediately for non-IDLE operations
799
796
  this.log.debug({ msg: 'Socket timeout', cid: this.id });
800
- this.emit('error', err);
801
- this.close();
797
+ this.emitError(err);
802
798
  }
803
799
  });
804
800
 
@@ -926,21 +922,25 @@ class ImapFlow extends EventEmitter {
926
922
  // use normal pipes for this.writeSocket -> this._deflate -> this.socket
927
923
  let reading = false;
928
924
  let readNext = () => {
929
- reading = true;
925
+ try {
926
+ reading = true;
930
927
 
931
- let chunk;
932
- while ((chunk = this.writeSocket.read()) !== null) {
933
- if (this._deflate && this._deflate.write(chunk) === false) {
934
- return this._deflate.once('drain', readNext);
928
+ let chunk;
929
+ while ((chunk = this.writeSocket.read()) !== null) {
930
+ if (this._deflate && this._deflate.write(chunk) === false) {
931
+ return this._deflate.once('drain', readNext);
932
+ }
935
933
  }
936
- }
937
934
 
938
- // flush data to socket
939
- if (this._deflate) {
940
- this._deflate.flush();
941
- }
935
+ // flush data to socket
936
+ if (this._deflate) {
937
+ this._deflate.flush();
938
+ }
942
939
 
943
- reading = false;
940
+ reading = false;
941
+ } catch (ex) {
942
+ this.emitError(ex);
943
+ }
944
944
  };
945
945
 
946
946
  this.writeSocket.on('readable', () => {
@@ -1021,7 +1021,7 @@ class ImapFlow extends EventEmitter {
1021
1021
  // don't care anymore
1022
1022
  return;
1023
1023
  }
1024
- setImmediate(() => this.close());
1024
+ this.closeAfter();
1025
1025
  this.upgrading = false;
1026
1026
  err.tlsFailed = true;
1027
1027
  reject(err);
@@ -1031,7 +1031,7 @@ class ImapFlow extends EventEmitter {
1031
1031
  if (!this.upgrading) {
1032
1032
  return;
1033
1033
  }
1034
- setImmediate(() => this.close());
1034
+ this.closeAfter();
1035
1035
  let err = new Error('Failed to upgrade connection in required time');
1036
1036
  err.tlsFailed = true;
1037
1037
  err.code = 'UPGRADE_TIMEOUT';
@@ -1040,30 +1040,34 @@ class ImapFlow extends EventEmitter {
1040
1040
 
1041
1041
  this.upgrading = true;
1042
1042
  this.socket = tls.connect(opts, () => {
1043
- clearTimeout(this.upgradeTimeout);
1044
- if (this.isClosed) {
1045
- // not sure if this is possible?
1046
- return this.close();
1047
- }
1043
+ try {
1044
+ clearTimeout(this.upgradeTimeout);
1045
+ if (this.isClosed) {
1046
+ // not sure if this is possible?
1047
+ return this.close();
1048
+ }
1048
1049
 
1049
- this.secureConnection = true;
1050
- this.upgrading = false;
1051
- this.streamer.secureConnection = true;
1052
- this.socket.pipe(this.streamer);
1053
- this.tls = typeof this.socket.getCipher === 'function' ? this.socket.getCipher() : false;
1054
- if (this.tls) {
1055
- this.tls.authorized = this.socket.authorized;
1056
- this.log.info({
1057
- src: 'tls',
1058
- msg: 'Established TLS session',
1059
- cid: this.id,
1060
- authorized: this.tls.authorized,
1061
- algo: this.tls.standardName || this.tls.name,
1062
- version: this.tls.version
1063
- });
1064
- }
1050
+ this.secureConnection = true;
1051
+ this.upgrading = false;
1052
+ this.streamer.secureConnection = true;
1053
+ this.socket.pipe(this.streamer);
1054
+ this.tls = typeof this.socket.getCipher === 'function' ? this.socket.getCipher() : false;
1055
+ if (this.tls) {
1056
+ this.tls.authorized = this.socket.authorized;
1057
+ this.log.info({
1058
+ src: 'tls',
1059
+ msg: 'Established TLS session',
1060
+ cid: this.id,
1061
+ authorized: this.tls.authorized,
1062
+ algo: this.tls.standardName || this.tls.name,
1063
+ version: this.tls.version
1064
+ });
1065
+ }
1065
1066
 
1066
- return resolve(true);
1067
+ return resolve(true);
1068
+ } catch (ex) {
1069
+ this.emitError(ex);
1070
+ }
1067
1071
  });
1068
1072
 
1069
1073
  this.writeSocket = this.socket;
@@ -1175,10 +1179,7 @@ class ImapFlow extends EventEmitter {
1175
1179
  }
1176
1180
 
1177
1181
  // ALWAYS emit the error so users can handle it
1178
- setImmediate(() => {
1179
- this.emit('error', err);
1180
- this.close();
1181
- });
1182
+ this.emitError(err);
1182
1183
  });
1183
1184
  }
1184
1185
 
@@ -1214,7 +1215,7 @@ class ImapFlow extends EventEmitter {
1214
1215
  return reject(err);
1215
1216
  }
1216
1217
 
1217
- setImmediate(() => this.close());
1218
+ this.closeAfter();
1218
1219
  });
1219
1220
  }
1220
1221
 
@@ -1517,55 +1518,62 @@ class ImapFlow extends EventEmitter {
1517
1518
  connectionTimeout: this.options.connectionTimeout || CONNECT_TIMEOUT
1518
1519
  };
1519
1520
  this.log.error({ err, cid: this.id });
1520
- setImmediate(() => this.close());
1521
+ this.closeAfter();
1521
1522
  reject(err);
1522
1523
  }, this.options.connectionTimeout || CONNECT_TIMEOUT);
1523
1524
 
1524
1525
  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
1526
+ try {
1527
+ clearTimeout(this.connectTimeout);
1528
+ this.socket.setKeepAlive(true, 5 * 1000);
1529
+ this.socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT);
1530
+
1531
+ this.greetingTimeout = setTimeout(() => {
1532
+ let err = new Error(
1533
+ `Failed to receive greeting from server in required time${!this.secureConnection ? '. Maybe should use TLS?' : ''}`
1534
+ );
1535
+ err.code = 'GREETING_TIMEOUT';
1536
+ err.details = {
1537
+ greetingTimeout: this.options.greetingTimeout || GREETING_TIMEOUT
1538
+ };
1539
+ this.log.error({ err, cid: this.id });
1540
+ this.closeAfter();
1541
+ reject(err);
1542
+ }, this.options.greetingTimeout || GREETING_TIMEOUT);
1543
+
1544
+ this.tls = typeof this.socket.getCipher === 'function' ? this.socket.getCipher() : false;
1545
+
1546
+ let logInfo = {
1547
+ src: 'connection',
1548
+ msg: `Established ${this.tls ? 'secure ' : ''}TCP connection`,
1549
+ cid: this.id,
1550
+ secure: !!this.tls,
1551
+ host: this.host,
1552
+ servername: this.servername,
1553
+ port: this.socket.remotePort,
1554
+ address: this.socket.remoteAddress,
1555
+ localAddress: this.socket.localAddress,
1556
+ localPort: this.socket.localPort
1534
1557
  };
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
1558
 
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
- }
1559
+ if (this.tls) {
1560
+ logInfo.authorized = this.tls.authorized = this.socket.authorized;
1561
+ logInfo.algo = this.tls.standardName || this.tls.name;
1562
+ logInfo.version = this.tls.version;
1563
+ }
1560
1564
 
1561
- this.log.info(logInfo);
1565
+ this.log.info(logInfo);
1562
1566
 
1563
- this.setSocketHandlers();
1564
- this.socket.pipe(this.streamer);
1567
+ this.setSocketHandlers();
1568
+ this.socket.pipe(this.streamer);
1565
1569
 
1566
- // executed by initial "* OK"
1567
- this.initialResolve = resolve;
1568
- this.initialReject = reject;
1570
+ // executed by initial "* OK"
1571
+ this.initialResolve = resolve;
1572
+ this.initialReject = reject;
1573
+ } catch (ex) {
1574
+ // connect failed
1575
+ reject(ex);
1576
+ }
1569
1577
  };
1570
1578
 
1571
1579
  if (socket) {
@@ -1588,7 +1596,7 @@ class ImapFlow extends EventEmitter {
1588
1596
  this.socket.on('error', err => {
1589
1597
  clearTimeout(this.connectTimeout);
1590
1598
  clearTimeout(this.greetingTimeout);
1591
- setImmediate(() => this.close());
1599
+ this.closeAfter();
1592
1600
  this.log.error({ err, cid: this.id });
1593
1601
  reject(err);
1594
1602
  });
@@ -1611,6 +1619,15 @@ class ImapFlow extends EventEmitter {
1611
1619
  return await this.run('LOGOUT');
1612
1620
  }
1613
1621
 
1622
+ /**
1623
+ * Close the TCP connection.
1624
+ * Unlike `close()`, return immediately from this function, allowing the
1625
+ * caller function to proceed, and run `close()` function afterwards.
1626
+ */
1627
+ closeAfter() {
1628
+ setImmediate(() => this.close());
1629
+ }
1630
+
1614
1631
  /**
1615
1632
  * Closes TCP connection without notifying the server.
1616
1633
  *
@@ -1621,101 +1638,114 @@ class ImapFlow extends EventEmitter {
1621
1638
  * client.close();
1622
1639
  */
1623
1640
  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) {
1641
+ try {
1642
+ // clear pending timers
1643
+ clearTimeout(this.idleStartTimer);
1644
+ clearTimeout(this.upgradeTimeout);
1645
+ clearTimeout(this.connectTimeout);
1634
1646
  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
1647
 
1644
- if (typeof this.preCheck === 'function') {
1645
- this.preCheck().catch(err => this.log.warn({ err, cid: this.id }));
1646
- }
1648
+ this.usable = false;
1649
+ this.idling = false;
1647
1650
 
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);
1651
+ if (typeof this.initialReject === 'function' && !this.options.verifyOnly) {
1652
+ clearTimeout(this.greetingTimeout);
1653
+ let reject = this.initialReject;
1654
+ this.initialResolve = false;
1655
+ this.initialReject = false;
1656
+ let err = new Error('Unexpected close');
1657
+ err.code = `ClosedAfterConnect${this.secureConnection ? 'TLS' : 'Text'}`;
1658
+ // still has to go through the logic below
1659
+ setImmediate(() => reject(err));
1656
1660
  }
1657
- this.currentRequest = false;
1658
- }
1659
1661
 
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);
1662
+ if (typeof this.preCheck === 'function') {
1663
+ this.preCheck().catch(err => this.log.warn({ err, cid: this.id }));
1664
+ }
1665
+
1666
+ // reject command that is currently processed
1667
+ if (this.currentRequest && this.requestTagMap.has(this.currentRequest.tag)) {
1668
+ let request = this.requestTagMap.get(this.currentRequest.tag);
1665
1669
  if (request) {
1666
1670
  this.requestTagMap.delete(request.tag);
1667
1671
  const error = new Error('Connection not available');
1668
1672
  error.code = 'NoConnection';
1669
1673
  request.reject(error);
1670
1674
  }
1675
+ this.currentRequest = false;
1676
+ }
1677
+
1678
+ // reject all other pending commands
1679
+ while (this.requestQueue.length) {
1680
+ let req = this.requestQueue.shift();
1681
+ if (req && this.requestTagMap.has(req.tag)) {
1682
+ let request = this.requestTagMap.get(req.tag);
1683
+ if (request) {
1684
+ this.requestTagMap.delete(request.tag);
1685
+ const error = new Error('Connection not available');
1686
+ error.code = 'NoConnection';
1687
+ request.reject(error);
1688
+ }
1689
+ }
1671
1690
  }
1672
- }
1673
1691
 
1674
- this.state = this.states.LOGOUT;
1675
- if (this.isClosed) {
1676
- return;
1677
- }
1678
- this.isClosed = true;
1692
+ // cleanup compression streams if they exist
1693
+ if (this._inflate) {
1694
+ try {
1695
+ this._inflate.unpipe();
1696
+ this._inflate.destroy();
1697
+ this._inflate = null;
1698
+ } catch (err) {
1699
+ this.log.error({ err, info: 'Failed to destroy inflate stream', cid: this.id });
1700
+ }
1701
+ }
1679
1702
 
1680
- if (this.writeSocket && !this.writeSocket.destroyed) {
1681
- try {
1682
- this.writeSocket.destroy();
1683
- } catch (err) {
1684
- this.log.error({ err, cid: this.id });
1703
+ if (this._deflate) {
1704
+ try {
1705
+ this._deflate.unpipe();
1706
+ this._deflate.destroy();
1707
+ this._deflate = null;
1708
+ } catch (err) {
1709
+ this.log.error({ err, info: 'Failed to destroy deflate stream', cid: this.id });
1710
+ }
1685
1711
  }
1686
- }
1687
1712
 
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 });
1713
+ this.state = this.states.LOGOUT;
1714
+ if (this.isClosed) {
1715
+ return;
1696
1716
  }
1697
- }
1698
1717
 
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 });
1718
+ if (this.socket && !this.socket.destroyed && this.writeSocket !== this.socket) {
1719
+ try {
1720
+ this.socket.destroy();
1721
+ } catch (err) {
1722
+ this.log.error({ err, cid: this.id });
1723
+ }
1706
1724
  }
1707
- }
1725
+ this.isClosed = true;
1708
1726
 
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 });
1727
+ if (this.writeSocket && !this.writeSocket.destroyed) {
1728
+ try {
1729
+ this.writeSocket.destroy();
1730
+ } catch (err) {
1731
+ this.log.error({ err, cid: this.id });
1732
+ }
1733
+ }
1734
+
1735
+ if (this.socket && !this.socket.destroyed && this.writeSocket !== this.socket) {
1736
+ try {
1737
+ this.socket.destroy();
1738
+ } catch (err) {
1739
+ this.log.error({ err, cid: this.id });
1740
+ }
1714
1741
  }
1715
- }
1716
1742
 
1717
- this.log.trace({ msg: 'Connection closed', cid: this.id });
1718
- this.emit('close');
1743
+ this.log.trace({ msg: 'Connection closed', cid: this.id });
1744
+ this.emit('close');
1745
+ } catch (ex) {
1746
+ // close failed
1747
+ this.log.error(ex);
1748
+ }
1719
1749
  }
1720
1750
 
1721
1751
  /**
@@ -2973,7 +3003,18 @@ class ImapFlow extends EventEmitter {
2973
3003
  };
2974
3004
 
2975
3005
  setImmediate(() => {
2976
- if (!writeChunk(chunk)) {
3006
+ let writeResult;
3007
+ try {
3008
+ writeResult = writeChunk(chunk);
3009
+ } catch (err) {
3010
+ stream.emit('error', err);
3011
+ if (!fetchAborted && stream && !stream.destroyed) {
3012
+ stream.end();
3013
+ }
3014
+ return;
3015
+ }
3016
+
3017
+ if (!writeResult) {
2977
3018
  // Initial chunk filled the buffer, wait for drain
2978
3019
  stream.once('drain', () => {
2979
3020
  if (!fetchAborted) {
@@ -3172,126 +3213,126 @@ class ImapFlow extends EventEmitter {
3172
3213
  return result;
3173
3214
  }
3174
3215
 
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;
3216
+ async processLocks() {
3217
+ // Atomic test-and-set to prevent race condition
3218
+ const wasProcessing = this.processingLock;
3219
+ if (wasProcessing) {
3220
+ // Another processor is already running, just exit
3221
+ this.log.trace({
3222
+ msg: 'Mailbox locking queued',
3223
+ path: this.mailbox && this.mailbox.path,
3224
+ pending: this.locks.length,
3225
+ idling: this.idling,
3226
+ activeLock: this.currentLock
3227
+ ? {
3228
+ lockId: this.currentLock.lockId,
3229
+ ...(this.currentLock.options?.description && { description: this.currentLock.options?.description })
3230
+ }
3231
+ : null
3232
+ });
3233
+ return;
3200
3234
  }
3235
+ this.processingLock = true;
3201
3236
 
3202
3237
  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) {
3238
+ // Process all locks in queue until empty
3239
+ while (this.locks.length > 0) {
3240
+ if (!this.locks.length) {
3213
3241
  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,
3242
+ msg: 'Mailbox locking queue processed',
3218
3243
  idling: this.idling
3219
3244
  });
3220
- this.currentLock = false;
3245
+ return;
3221
3246
  }
3222
- this.processLocks(true).catch(err => this.log.error({ err, cid: this.id }));
3223
- };
3224
3247
 
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
- }
3248
+ const release = () => {
3249
+ if (this.currentLock) {
3250
+ this.log.trace({
3251
+ msg: 'Mailbox lock released',
3252
+ lockId: this.currentLock.lockId,
3253
+ path: this.mailbox && this.mailbox.path,
3254
+ pending: this.locks.length,
3255
+ idling: this.idling
3256
+ });
3257
+ this.currentLock = false;
3258
+ }
3259
+ // Use setImmediate to avoid stack overflow
3260
+ setImmediate(() => {
3261
+ this.processLocks().catch(err => this.log.error({ err, cid: this.id }));
3262
+ });
3263
+ };
3238
3264
 
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);
3265
+ const lock = this.locks.shift();
3266
+ const { resolve, reject, path, options, lockId } = lock;
3267
+
3268
+ if (!this.usable || !this.socket || this.socket.destroyed) {
3269
+ this.log.trace({ msg: 'Failed to acquire mailbox lock', path, lockId, idling: this.idling });
3270
+ let error = new Error('Connection not available');
3271
+ error.code = 'NoConnection';
3272
+ reject(error);
3273
+ continue; // Process next lock in queue
3274
+ }
3275
+
3276
+ if (this.mailbox && this.mailbox.path === path && !!this.mailbox.readOnly === !!options.readOnly) {
3277
+ // nothing to do here, already selected
3254
3278
  this.log.trace({
3255
- msg: 'Mailbox lock acquired [selected]',
3279
+ msg: 'Mailbox lock acquired [existing]',
3256
3280
  path,
3257
3281
  lockId,
3258
3282
  idling: this.idling,
3259
3283
  ...(options.description && { description: options.description })
3260
3284
  });
3261
3285
  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;
3286
+ resolve({ path, release });
3287
+ break; // Wait for this lock to be released
3288
+ } else {
3289
+ try {
3290
+ // Try to open. Throws if mailbox does not exists or can't open
3291
+ await this.mailboxOpen(path, options);
3292
+ this.log.trace({
3293
+ msg: 'Mailbox lock acquired [selected]',
3294
+ path,
3295
+ lockId,
3296
+ idling: this.idling,
3297
+ ...(options.description && { description: options.description })
3298
+ });
3299
+ this.currentLock = lock;
3300
+ resolve({ path, release });
3301
+ break; // Wait for this lock to be released
3302
+ } catch (err) {
3303
+ if (err.responseStatus === 'NO') {
3304
+ try {
3305
+ let folders = await this.run('LIST', '', path, { listOnly: true });
3306
+ if (!folders || !folders.length) {
3307
+ err.mailboxMissing = true;
3308
+ }
3309
+ } catch (E) {
3310
+ this.log.trace({ msg: 'Failed to verify failed mailbox', path, err: E });
3269
3311
  }
3270
- } catch (E) {
3271
- this.log.trace({ msg: 'Failed to verify failed mailbox', path, err: E });
3272
3312
  }
3273
- }
3274
3313
 
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);
3314
+ this.log.trace({
3315
+ msg: 'Failed to acquire mailbox lock',
3316
+ path,
3317
+ lockId,
3318
+ idling: this.idling,
3319
+ ...(options.description && { description: options.description }),
3320
+ err
3321
+ });
3322
+ reject(err);
3323
+ // Continue to next lock in queue
3324
+ }
3286
3325
  }
3287
3326
  }
3288
3327
  } finally {
3289
3328
  this.processingLock = false;
3290
3329
 
3291
3330
  // 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 }));
3331
+ if (this.locks.length && !this.currentLock) {
3332
+ // Process any remaining locks
3333
+ setImmediate(() => {
3334
+ this.processLocks().catch(err => this.log.error({ err, cid: this.id }));
3335
+ });
3295
3336
  }
3296
3337
  }
3297
3338
  }