imapflow 2.4.1 → 2.4.3
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 +16 -0
- package/dist/cjs/errors.d.ts +1 -0
- package/dist/cjs/errors.js +1 -0
- package/dist/cjs/imap-flow.js +47 -6
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/esm/errors.d.ts +1 -0
- package/dist/esm/errors.js +1 -0
- package/dist/esm/imap-flow.js +47 -6
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.4.3](https://github.com/postalsys/imapflow/compare/v2.4.2...v2.4.3) (2026-10-15)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* fail the command on a NO or BAD status line without a tag ([5a38e17](https://github.com/postalsys/imapflow/commit/5a38e171ad8405583b090872076f464c4dc6f0e2))
|
|
9
|
+
* send ID after STARTTLS instead of before it ([7d6f37a](https://github.com/postalsys/imapflow/commit/7d6f37a4e8eb2f9b0f921f6b004a1ba5e45b1867))
|
|
10
|
+
* send ID after STARTTLS instead of before it ([197ecdc](https://github.com/postalsys/imapflow/commit/197ecdc49f2f64c45ee98c4e4284534662e26942))
|
|
11
|
+
|
|
12
|
+
## [2.4.2](https://github.com/postalsys/imapflow/compare/v2.4.1...v2.4.2) (2026-10-14)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **deps:** update libmime to 5.4.8 and mailsplit to 5.4.21 ([8eafb46](https://github.com/postalsys/imapflow/commit/8eafb46f858893670dda1be115899b8b761391ab))
|
|
18
|
+
|
|
3
19
|
## [2.4.1](https://github.com/postalsys/imapflow/compare/v2.4.0...v2.4.1) (2026-10-14)
|
|
4
20
|
|
|
5
21
|
|
package/dist/cjs/errors.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export declare const ImapFlowErrorCode: {
|
|
|
20
20
|
readonly LockTimeout: "LockTimeout";
|
|
21
21
|
readonly ETHROTTLE: "ETHROTTLE";
|
|
22
22
|
readonly UnexpectedTag: "UnexpectedTag";
|
|
23
|
+
readonly MissingTag: "MissingTag";
|
|
23
24
|
readonly InvalidResponse: "InvalidResponse";
|
|
24
25
|
readonly ResponseProcessingFailed: "ResponseProcessingFailed";
|
|
25
26
|
readonly STARTTLS_INJECTION: "STARTTLS_INJECTION";
|
package/dist/cjs/errors.js
CHANGED
|
@@ -26,6 +26,7 @@ exports.ImapFlowErrorCode = {
|
|
|
26
26
|
// the server
|
|
27
27
|
ETHROTTLE: 'ETHROTTLE',
|
|
28
28
|
UnexpectedTag: 'UnexpectedTag',
|
|
29
|
+
MissingTag: 'MissingTag',
|
|
29
30
|
InvalidResponse: 'InvalidResponse',
|
|
30
31
|
ResponseProcessingFailed: 'ResponseProcessingFailed',
|
|
31
32
|
STARTTLS_INJECTION: 'STARTTLS_INJECTION',
|
package/dist/cjs/imap-flow.js
CHANGED
|
@@ -781,7 +781,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
781
781
|
*/
|
|
782
782
|
rejectUnparsedCompletion(payload, parserError) {
|
|
783
783
|
if (!this.currentRequest || !this.currentRequest.sent) {
|
|
784
|
-
return;
|
|
784
|
+
return false;
|
|
785
785
|
}
|
|
786
786
|
// Prefer the tag the parser had already extracted before it failed - it went
|
|
787
787
|
// through the same leading-NUL workaround as every parsed response. Fall back
|
|
@@ -793,11 +793,45 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
793
793
|
tag = match && match[1];
|
|
794
794
|
}
|
|
795
795
|
if (!tag || tag !== this.currentRequest.tag) {
|
|
796
|
-
return;
|
|
796
|
+
return false;
|
|
797
797
|
}
|
|
798
798
|
let err = new Error('Failed to parse the server response for this command');
|
|
799
799
|
err.code = parserError.code || 'ParserError';
|
|
800
800
|
err.parserError = parserError;
|
|
801
|
+
this.failInFlight(err);
|
|
802
|
+
return true;
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Fails the in-flight command when the server answered it with a NO or BAD status line that
|
|
806
|
+
* has no tag at all (Namecheap Private Email: `BAD "Must issue a STARTTLS command first"`).
|
|
807
|
+
* Only one command is ever on the wire, and leaving it pending would stall the connection
|
|
808
|
+
* until the socket timeout. Tags are uppercase hex, so a command can carry the tag BAD:
|
|
809
|
+
* callers check for the command's own tag first.
|
|
810
|
+
*
|
|
811
|
+
* @param payload - Raw bytes of the line.
|
|
812
|
+
* @returns `true` when the line failed the in-flight command
|
|
813
|
+
* @internal
|
|
814
|
+
*/
|
|
815
|
+
rejectTaglessCompletion(payload) {
|
|
816
|
+
if (!this.currentRequest || !this.currentRequest.sent) {
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
let match = payload.toString('latin1', 0, 1024).match(/^\0*(BAD|NO)(?:\s+([^]*))?$/i);
|
|
820
|
+
if (!match) {
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
let err = (0, errors_js_1.createImapError)('Command failed', 'MissingTag', {
|
|
824
|
+
command: this.currentRequest.command,
|
|
825
|
+
responseStatus: match[1].toUpperCase(),
|
|
826
|
+
responseText: (match[2] || '').trim().replace(/^"(.*)"$/, '$1')
|
|
827
|
+
});
|
|
828
|
+
this.log.warn({ msg: 'Server sent a status response without a tag', err, cid: this.id });
|
|
829
|
+
this.failInFlight(err);
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
// Rejects the in-flight command and moves on to the next queued one
|
|
833
|
+
/** @internal */
|
|
834
|
+
failInFlight(err) {
|
|
801
835
|
this.rejectCurrentRequest(err);
|
|
802
836
|
this.trySend().catch(sendErr => (0, tools_js_1.logConnectionError)(this, 'Failed to dispatch command', sendErr));
|
|
803
837
|
}
|
|
@@ -847,7 +881,9 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
847
881
|
// timeout fires. The tag is recovered from the raw bytes (a tag is
|
|
848
882
|
// ASTRING-CHAR only, so it survives whatever made the rest unparseable) and
|
|
849
883
|
// the command is failed with the parser error instead of hanging.
|
|
850
|
-
this.rejectUnparsedCompletion(data.payload, err)
|
|
884
|
+
if (!this.rejectUnparsedCompletion(data.payload, err)) {
|
|
885
|
+
this.rejectTaglessCompletion(data.payload);
|
|
886
|
+
}
|
|
851
887
|
return true;
|
|
852
888
|
}
|
|
853
889
|
if (parsed.tag && !['*', '+'].includes(parsed.tag) && parsed.command) {
|
|
@@ -952,7 +988,8 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
952
988
|
// actually written to the socket (invariant 2), so the three cases below are kept apart:
|
|
953
989
|
// the active command completes, a command that has not been written yet is proof of
|
|
954
990
|
// desynchronization (queued behind another command, or current but not yet on the wire),
|
|
955
|
-
// and an entirely unknown tag is recorded but tolerated
|
|
991
|
+
// and an entirely unknown tag is recorded but tolerated, unless the line is a NO or BAD
|
|
992
|
+
// that came without a tag (see rejectTaglessCompletion()).
|
|
956
993
|
if (parsed.tag && !['*', '+'].includes(parsed.tag)) {
|
|
957
994
|
if (this.currentRequest && this.currentRequest.tag === parsed.tag && this.currentRequest.sent) {
|
|
958
995
|
let request = this.takeCurrentRequest();
|
|
@@ -990,7 +1027,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
990
1027
|
this.failProtocol(err);
|
|
991
1028
|
return false;
|
|
992
1029
|
}
|
|
993
|
-
else {
|
|
1030
|
+
else if (!this.rejectTaglessCompletion(data.payload)) {
|
|
994
1031
|
this.countUnknownTag(parsed.tag);
|
|
995
1032
|
}
|
|
996
1033
|
}
|
|
@@ -1266,10 +1303,14 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1266
1303
|
/** @internal */
|
|
1267
1304
|
async startSession() {
|
|
1268
1305
|
await this.run('CAPABILITY');
|
|
1306
|
+
await this.upgradeToSTARTTLS();
|
|
1307
|
+
// ID goes after STARTTLS: the client info should not travel in cleartext, and some
|
|
1308
|
+
// servers refuse every command but STARTTLS before the upgrade with an untagged BAD
|
|
1309
|
+
// the client can not tie to ID, which then waits for the socket timeout (Namecheap
|
|
1310
|
+
// Private Email). The capability list was refreshed by the upgrade.
|
|
1269
1311
|
if (this.capabilities.has('ID')) {
|
|
1270
1312
|
this.idRequested = await this.run('ID', this.clientInfo);
|
|
1271
1313
|
}
|
|
1272
|
-
await this.upgradeToSTARTTLS();
|
|
1273
1314
|
await this.authenticate();
|
|
1274
1315
|
if ((!this.idRequested || Object.keys(this.idRequested).length < 2) && this.capabilities.has('ID')) {
|
|
1275
1316
|
// re-request ID after LOGIN
|
package/dist/cjs/package-info.js
CHANGED
package/dist/esm/errors.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export declare const ImapFlowErrorCode: {
|
|
|
20
20
|
readonly LockTimeout: "LockTimeout";
|
|
21
21
|
readonly ETHROTTLE: "ETHROTTLE";
|
|
22
22
|
readonly UnexpectedTag: "UnexpectedTag";
|
|
23
|
+
readonly MissingTag: "MissingTag";
|
|
23
24
|
readonly InvalidResponse: "InvalidResponse";
|
|
24
25
|
readonly ResponseProcessingFailed: "ResponseProcessingFailed";
|
|
25
26
|
readonly STARTTLS_INJECTION: "STARTTLS_INJECTION";
|
package/dist/esm/errors.js
CHANGED
|
@@ -22,6 +22,7 @@ export const ImapFlowErrorCode = {
|
|
|
22
22
|
// the server
|
|
23
23
|
ETHROTTLE: 'ETHROTTLE',
|
|
24
24
|
UnexpectedTag: 'UnexpectedTag',
|
|
25
|
+
MissingTag: 'MissingTag',
|
|
25
26
|
InvalidResponse: 'InvalidResponse',
|
|
26
27
|
ResponseProcessingFailed: 'ResponseProcessingFailed',
|
|
27
28
|
STARTTLS_INJECTION: 'STARTTLS_INJECTION',
|
package/dist/esm/imap-flow.js
CHANGED
|
@@ -740,7 +740,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
740
740
|
*/
|
|
741
741
|
rejectUnparsedCompletion(payload, parserError) {
|
|
742
742
|
if (!this.currentRequest || !this.currentRequest.sent) {
|
|
743
|
-
return;
|
|
743
|
+
return false;
|
|
744
744
|
}
|
|
745
745
|
// Prefer the tag the parser had already extracted before it failed - it went
|
|
746
746
|
// through the same leading-NUL workaround as every parsed response. Fall back
|
|
@@ -752,11 +752,45 @@ export class ImapFlow extends EventEmitter {
|
|
|
752
752
|
tag = match && match[1];
|
|
753
753
|
}
|
|
754
754
|
if (!tag || tag !== this.currentRequest.tag) {
|
|
755
|
-
return;
|
|
755
|
+
return false;
|
|
756
756
|
}
|
|
757
757
|
let err = new Error('Failed to parse the server response for this command');
|
|
758
758
|
err.code = parserError.code || 'ParserError';
|
|
759
759
|
err.parserError = parserError;
|
|
760
|
+
this.failInFlight(err);
|
|
761
|
+
return true;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Fails the in-flight command when the server answered it with a NO or BAD status line that
|
|
765
|
+
* has no tag at all (Namecheap Private Email: `BAD "Must issue a STARTTLS command first"`).
|
|
766
|
+
* Only one command is ever on the wire, and leaving it pending would stall the connection
|
|
767
|
+
* until the socket timeout. Tags are uppercase hex, so a command can carry the tag BAD:
|
|
768
|
+
* callers check for the command's own tag first.
|
|
769
|
+
*
|
|
770
|
+
* @param payload - Raw bytes of the line.
|
|
771
|
+
* @returns `true` when the line failed the in-flight command
|
|
772
|
+
* @internal
|
|
773
|
+
*/
|
|
774
|
+
rejectTaglessCompletion(payload) {
|
|
775
|
+
if (!this.currentRequest || !this.currentRequest.sent) {
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
let match = payload.toString('latin1', 0, 1024).match(/^\0*(BAD|NO)(?:\s+([^]*))?$/i);
|
|
779
|
+
if (!match) {
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
let err = createImapError('Command failed', 'MissingTag', {
|
|
783
|
+
command: this.currentRequest.command,
|
|
784
|
+
responseStatus: match[1].toUpperCase(),
|
|
785
|
+
responseText: (match[2] || '').trim().replace(/^"(.*)"$/, '$1')
|
|
786
|
+
});
|
|
787
|
+
this.log.warn({ msg: 'Server sent a status response without a tag', err, cid: this.id });
|
|
788
|
+
this.failInFlight(err);
|
|
789
|
+
return true;
|
|
790
|
+
}
|
|
791
|
+
// Rejects the in-flight command and moves on to the next queued one
|
|
792
|
+
/** @internal */
|
|
793
|
+
failInFlight(err) {
|
|
760
794
|
this.rejectCurrentRequest(err);
|
|
761
795
|
this.trySend().catch(sendErr => logConnectionError(this, 'Failed to dispatch command', sendErr));
|
|
762
796
|
}
|
|
@@ -806,7 +840,9 @@ export class ImapFlow extends EventEmitter {
|
|
|
806
840
|
// timeout fires. The tag is recovered from the raw bytes (a tag is
|
|
807
841
|
// ASTRING-CHAR only, so it survives whatever made the rest unparseable) and
|
|
808
842
|
// the command is failed with the parser error instead of hanging.
|
|
809
|
-
this.rejectUnparsedCompletion(data.payload, err)
|
|
843
|
+
if (!this.rejectUnparsedCompletion(data.payload, err)) {
|
|
844
|
+
this.rejectTaglessCompletion(data.payload);
|
|
845
|
+
}
|
|
810
846
|
return true;
|
|
811
847
|
}
|
|
812
848
|
if (parsed.tag && !['*', '+'].includes(parsed.tag) && parsed.command) {
|
|
@@ -911,7 +947,8 @@ export class ImapFlow extends EventEmitter {
|
|
|
911
947
|
// actually written to the socket (invariant 2), so the three cases below are kept apart:
|
|
912
948
|
// the active command completes, a command that has not been written yet is proof of
|
|
913
949
|
// desynchronization (queued behind another command, or current but not yet on the wire),
|
|
914
|
-
// and an entirely unknown tag is recorded but tolerated
|
|
950
|
+
// and an entirely unknown tag is recorded but tolerated, unless the line is a NO or BAD
|
|
951
|
+
// that came without a tag (see rejectTaglessCompletion()).
|
|
915
952
|
if (parsed.tag && !['*', '+'].includes(parsed.tag)) {
|
|
916
953
|
if (this.currentRequest && this.currentRequest.tag === parsed.tag && this.currentRequest.sent) {
|
|
917
954
|
let request = this.takeCurrentRequest();
|
|
@@ -949,7 +986,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
949
986
|
this.failProtocol(err);
|
|
950
987
|
return false;
|
|
951
988
|
}
|
|
952
|
-
else {
|
|
989
|
+
else if (!this.rejectTaglessCompletion(data.payload)) {
|
|
953
990
|
this.countUnknownTag(parsed.tag);
|
|
954
991
|
}
|
|
955
992
|
}
|
|
@@ -1225,10 +1262,14 @@ export class ImapFlow extends EventEmitter {
|
|
|
1225
1262
|
/** @internal */
|
|
1226
1263
|
async startSession() {
|
|
1227
1264
|
await this.run('CAPABILITY');
|
|
1265
|
+
await this.upgradeToSTARTTLS();
|
|
1266
|
+
// ID goes after STARTTLS: the client info should not travel in cleartext, and some
|
|
1267
|
+
// servers refuse every command but STARTTLS before the upgrade with an untagged BAD
|
|
1268
|
+
// the client can not tie to ID, which then waits for the socket timeout (Namecheap
|
|
1269
|
+
// Private Email). The capability list was refreshed by the upgrade.
|
|
1228
1270
|
if (this.capabilities.has('ID')) {
|
|
1229
1271
|
this.idRequested = await this.run('ID', this.clientInfo);
|
|
1230
1272
|
}
|
|
1231
|
-
await this.upgradeToSTARTTLS();
|
|
1232
1273
|
await this.authenticate();
|
|
1233
1274
|
if ((!this.idRequested || Object.keys(this.idRequested).length < 2) && this.capabilities.has('ID')) {
|
|
1234
1275
|
// re-request ID after LOGIN
|
package/dist/esm/package-info.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imapflow",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.3",
|
|
4
4
|
"description": "IMAP Client for Node",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/imap-flow.js",
|
|
@@ -81,14 +81,14 @@
|
|
|
81
81
|
"types-node-legacy": "npm:@types/node@20.0.0",
|
|
82
82
|
"typescript": "6.0.3",
|
|
83
83
|
"typescript-eslint": "8.71.2",
|
|
84
|
-
"wrangler": "4.
|
|
84
|
+
"wrangler": "4.151.0"
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
|
-
"@zone-eu/mailsplit": "5.4.
|
|
87
|
+
"@zone-eu/mailsplit": "5.4.21",
|
|
88
88
|
"encoding-japanese": "2.4.0",
|
|
89
89
|
"iconv-lite": "0.7.3",
|
|
90
90
|
"libbase64": "1.3.3",
|
|
91
|
-
"libmime": "5.4.
|
|
91
|
+
"libmime": "5.4.8",
|
|
92
92
|
"libqp": "2.1.2",
|
|
93
93
|
"pino": "10.3.1",
|
|
94
94
|
"socks": "2.8.10"
|