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/CHANGELOG.md +15 -0
- package/lib/imap-flow.js +310 -269
- package/package.json +5 -5
- package/test/authentication-test.js +83 -0
- package/test/commands-test.js +73 -0
- package/test/connection-edge-cases-test.js +629 -0
- package/test/connection-test.js +162 -0
- package/test/integration-test.js +83 -0
- package/test/search-compiler-test.js +81 -0
- package/test/special-use-test.js +49 -0
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
925
|
+
try {
|
|
926
|
+
reading = true;
|
|
930
927
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
935
|
+
// flush data to socket
|
|
936
|
+
if (this._deflate) {
|
|
937
|
+
this._deflate.flush();
|
|
938
|
+
}
|
|
942
939
|
|
|
943
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
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
|
-
|
|
1718
|
-
|
|
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
|
}
|