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/.github/workflows/test.yml +1 -1
- package/CHANGELOG.md +21 -0
- package/lib/commands/fetch.js +17 -4
- package/lib/handler/imap-stream.js +7 -0
- package/lib/imap-flow.js +336 -265
- package/package.json +8 -8
- 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
|
|
|
@@ -494,7 +495,7 @@ class ImapFlow extends EventEmitter {
|
|
|
494
495
|
|
|
495
496
|
let options = data.options || {};
|
|
496
497
|
|
|
497
|
-
this.log.debug({ src: '
|
|
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
|
-
|
|
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
|
|
932
|
-
|
|
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
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
958
|
+
// flush data to socket
|
|
959
|
+
if (this._deflate) {
|
|
960
|
+
this._deflate.flush();
|
|
961
|
+
}
|
|
945
962
|
|
|
946
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
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
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1544
|
+
this.closeAfter();
|
|
1521
1545
|
reject(err);
|
|
1522
1546
|
}, this.options.connectionTimeout || CONNECT_TIMEOUT);
|
|
1523
1547
|
|
|
1524
1548
|
let onConnect = () => {
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
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
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
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
|
-
|
|
1588
|
+
this.log.info(logInfo);
|
|
1562
1589
|
|
|
1563
|
-
|
|
1564
|
-
|
|
1590
|
+
this.setSocketHandlers();
|
|
1591
|
+
this.socket.pipe(this.streamer);
|
|
1565
1592
|
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
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
|
-
|
|
1645
|
-
this.
|
|
1646
|
-
}
|
|
1671
|
+
this.usable = false;
|
|
1672
|
+
this.idling = false;
|
|
1647
1673
|
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
this.
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
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
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
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
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
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
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
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
|
-
|
|
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 });
|
|
1736
|
+
this.state = this.states.LOGOUT;
|
|
1737
|
+
if (this.isClosed) {
|
|
1738
|
+
return;
|
|
1696
1739
|
}
|
|
1697
|
-
}
|
|
1698
1740
|
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
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
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
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
|
-
|
|
1718
|
-
|
|
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
|
-
|
|
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(
|
|
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;
|
|
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
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
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
|
|
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
|
-
|
|
3269
|
+
return;
|
|
3221
3270
|
}
|
|
3222
|
-
this.processLocks(true).catch(err => this.log.error({ err, cid: this.id }));
|
|
3223
|
-
};
|
|
3224
3271
|
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
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
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
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 [
|
|
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
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
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
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
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.
|
|
3293
|
-
//
|
|
3294
|
-
|
|
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
|
}
|