imapflow 1.0.193 → 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/CHANGELOG.md +8 -0
- package/lib/imap-flow.js +299 -258
- package/package.json +5 -5
- package/test/connection-edge-cases-test.js +629 -0
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
|
-
|
|
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
|
-
|
|
390
|
+
this.closeAfter();
|
|
390
391
|
this.emit('error', err);
|
|
391
392
|
}
|
|
392
393
|
|
|
@@ -764,16 +765,8 @@ class ImapFlow extends EventEmitter {
|
|
|
764
765
|
this.log.error({ err, cid: this.id });
|
|
765
766
|
this.emitError(err);
|
|
766
767
|
});
|
|
767
|
-
this._socketClose =
|
|
768
|
-
|
|
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.
|
|
@@ -929,21 +922,25 @@ class ImapFlow extends EventEmitter {
|
|
|
929
922
|
// use normal pipes for this.writeSocket -> this._deflate -> this.socket
|
|
930
923
|
let reading = false;
|
|
931
924
|
let readNext = () => {
|
|
932
|
-
|
|
925
|
+
try {
|
|
926
|
+
reading = true;
|
|
933
927
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
+
}
|
|
938
933
|
}
|
|
939
|
-
}
|
|
940
934
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
935
|
+
// flush data to socket
|
|
936
|
+
if (this._deflate) {
|
|
937
|
+
this._deflate.flush();
|
|
938
|
+
}
|
|
945
939
|
|
|
946
|
-
|
|
940
|
+
reading = false;
|
|
941
|
+
} catch (ex) {
|
|
942
|
+
this.emitError(ex);
|
|
943
|
+
}
|
|
947
944
|
};
|
|
948
945
|
|
|
949
946
|
this.writeSocket.on('readable', () => {
|
|
@@ -1024,7 +1021,7 @@ class ImapFlow extends EventEmitter {
|
|
|
1024
1021
|
// don't care anymore
|
|
1025
1022
|
return;
|
|
1026
1023
|
}
|
|
1027
|
-
|
|
1024
|
+
this.closeAfter();
|
|
1028
1025
|
this.upgrading = false;
|
|
1029
1026
|
err.tlsFailed = true;
|
|
1030
1027
|
reject(err);
|
|
@@ -1034,7 +1031,7 @@ class ImapFlow extends EventEmitter {
|
|
|
1034
1031
|
if (!this.upgrading) {
|
|
1035
1032
|
return;
|
|
1036
1033
|
}
|
|
1037
|
-
|
|
1034
|
+
this.closeAfter();
|
|
1038
1035
|
let err = new Error('Failed to upgrade connection in required time');
|
|
1039
1036
|
err.tlsFailed = true;
|
|
1040
1037
|
err.code = 'UPGRADE_TIMEOUT';
|
|
@@ -1043,30 +1040,34 @@ class ImapFlow extends EventEmitter {
|
|
|
1043
1040
|
|
|
1044
1041
|
this.upgrading = true;
|
|
1045
1042
|
this.socket = tls.connect(opts, () => {
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1043
|
+
try {
|
|
1044
|
+
clearTimeout(this.upgradeTimeout);
|
|
1045
|
+
if (this.isClosed) {
|
|
1046
|
+
// not sure if this is possible?
|
|
1047
|
+
return this.close();
|
|
1048
|
+
}
|
|
1051
1049
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
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
|
+
}
|
|
1068
1066
|
|
|
1069
|
-
|
|
1067
|
+
return resolve(true);
|
|
1068
|
+
} catch (ex) {
|
|
1069
|
+
this.emitError(ex);
|
|
1070
|
+
}
|
|
1070
1071
|
});
|
|
1071
1072
|
|
|
1072
1073
|
this.writeSocket = this.socket;
|
|
@@ -1214,7 +1215,7 @@ class ImapFlow extends EventEmitter {
|
|
|
1214
1215
|
return reject(err);
|
|
1215
1216
|
}
|
|
1216
1217
|
|
|
1217
|
-
|
|
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
|
-
|
|
1521
|
+
this.closeAfter();
|
|
1521
1522
|
reject(err);
|
|
1522
1523
|
}, this.options.connectionTimeout || CONNECT_TIMEOUT);
|
|
1523
1524
|
|
|
1524
1525
|
let onConnect = () => {
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
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
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
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
|
-
|
|
1565
|
+
this.log.info(logInfo);
|
|
1562
1566
|
|
|
1563
|
-
|
|
1564
|
-
|
|
1567
|
+
this.setSocketHandlers();
|
|
1568
|
+
this.socket.pipe(this.streamer);
|
|
1565
1569
|
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
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
|
-
|
|
1645
|
-
this.
|
|
1646
|
-
}
|
|
1648
|
+
this.usable = false;
|
|
1649
|
+
this.idling = false;
|
|
1647
1650
|
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
this.
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
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
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
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
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
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
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
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
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
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
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
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
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
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
|
+
}
|
|
1714
1733
|
}
|
|
1715
|
-
}
|
|
1716
1734
|
|
|
1717
|
-
|
|
1718
|
-
|
|
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
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
|
|
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
|
-
|
|
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(
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
this.
|
|
3184
|
-
this.
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
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
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
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
|
|
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
|
-
|
|
3245
|
+
return;
|
|
3221
3246
|
}
|
|
3222
|
-
this.processLocks(true).catch(err => this.log.error({ err, cid: this.id }));
|
|
3223
|
-
};
|
|
3224
3247
|
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
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
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
path,
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
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 [
|
|
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
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
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
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
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.
|
|
3293
|
-
//
|
|
3294
|
-
|
|
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
|
}
|