imapflow 1.6.0 → 1.6.2
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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +14 -0
- package/lib/commands/list.js +10 -9
- package/lib/imap-flow.js +9 -1
- package/package.json +3 -3
- package/test/commands-integration-test.js +35 -1
- package/test/imap-flow-server-test.js +37 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.6.2](https://github.com/postalsys/imapflow/compare/v1.6.1...v1.6.2) (2026-07-28)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* never fall back to LSUB on an IMAP4rev2 session ([329f588](https://github.com/postalsys/imapflow/commit/329f58829592a3c9a9f69fa74387596a3e55eac3))
|
|
9
|
+
|
|
10
|
+
## [1.6.1](https://github.com/postalsys/imapflow/compare/v1.6.0...v1.6.1) (2026-07-27)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* keep the authentication result on a verifyOnly connection ([8665c64](https://github.com/postalsys/imapflow/commit/8665c642f16988c630a779fa796c5801532ffd4a))
|
|
16
|
+
|
|
3
17
|
## [1.6.0](https://github.com/postalsys/imapflow/compare/v1.5.0...v1.6.0) (2026-07-27)
|
|
4
18
|
|
|
5
19
|
|
package/lib/commands/list.js
CHANGED
|
@@ -478,20 +478,21 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
478
478
|
response.next();
|
|
479
479
|
};
|
|
480
480
|
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
|
|
481
|
+
// Never sent on a rev2 session - LSUB is not part of that protocol version, and
|
|
482
|
+
// some servers break the rest of the session over the rejection, so this is
|
|
483
|
+
// decided up front rather than left to the skipLsub latch below. On rev1 it is
|
|
484
|
+
// skipped when RETURN (SUBSCRIBED) already answered. Safety net: if the extended
|
|
485
|
+
// LIST was accepted but not a single mailbox came back subscribed, assume the
|
|
486
|
+
// server silently ignored the option and fall back to LSUB anyway (an account
|
|
487
|
+
// with no subscriptions legitimately looks the same).
|
|
488
|
+
let needsLsub = !isRev2Active(connection) && (!successStage.subscribed || !entries.some(entry => entry.subscribed));
|
|
488
489
|
if (needsLsub && !connection.skipLsub) {
|
|
489
490
|
try {
|
|
490
491
|
await runLsub();
|
|
491
492
|
} catch (err) {
|
|
492
493
|
if (isRejectedCommand(err)) {
|
|
493
|
-
// Tagged BAD: the server does not
|
|
494
|
-
//
|
|
494
|
+
// Tagged BAD: the server does not implement LSUB despite advertising
|
|
495
|
+
// rev1 - skip it for the rest of this connection
|
|
495
496
|
connection.skipLsub = true;
|
|
496
497
|
} else if (err.responseStatus !== 'NO' || err.code === 'ETHROTTLE') {
|
|
497
498
|
// Transport failures and throttling: rethrow, every follow-up
|
package/lib/imap-flow.js
CHANGED
|
@@ -2292,12 +2292,20 @@ class ImapFlow extends EventEmitter {
|
|
|
2292
2292
|
// live server state. Cleared during the first close only, so repeated close() calls
|
|
2293
2293
|
// stay idempotent and cannot emit an event twice.
|
|
2294
2294
|
// `byeReason` is deliberately kept: it explains why the session ended.
|
|
2295
|
+
//
|
|
2296
|
+
// `authenticated` is kept for a verifyOnly connection, where it is the result rather
|
|
2297
|
+
// than live state. That mode authenticates, optionally lists, and logs out before
|
|
2298
|
+
// connect() resolves, so clearing it here left every caller reading `false` off a
|
|
2299
|
+
// connection that had just authenticated successfully - there is no later moment at
|
|
2300
|
+
// which the answer could be read, and such a client is never reconnected.
|
|
2295
2301
|
let closedMailbox = false;
|
|
2296
2302
|
if (!this.isClosed) {
|
|
2297
2303
|
closedMailbox = this.mailbox;
|
|
2298
2304
|
this.mailbox = false;
|
|
2299
2305
|
this.currentSelectCommand = false;
|
|
2300
|
-
this.
|
|
2306
|
+
if (!this.options.verifyOnly) {
|
|
2307
|
+
this.authenticated = false;
|
|
2308
|
+
}
|
|
2301
2309
|
this.preCheck = false;
|
|
2302
2310
|
}
|
|
2303
2311
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imapflow",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.2",
|
|
4
4
|
"description": "IMAP Client for Node",
|
|
5
5
|
"main": "lib/imap-flow.js",
|
|
6
6
|
"types": "lib/imap-flow.d.ts",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"homepage": "https://imapflow.com/",
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@eslint/js": "10.0.1",
|
|
33
|
-
"@types/node": "26.1.
|
|
33
|
+
"@types/node": "26.1.2",
|
|
34
34
|
"c8": "12.0.0",
|
|
35
|
-
"eslint": "10.
|
|
35
|
+
"eslint": "10.8.0",
|
|
36
36
|
"eslint-config-nodemailer": "1.2.0",
|
|
37
37
|
"eslint-config-prettier": "10.1.8",
|
|
38
38
|
"grunt": "1.6.2",
|
|
@@ -4168,7 +4168,7 @@ module.exports['Commands: list survives LSUB rejection'] = async test => {
|
|
|
4168
4168
|
exec: async (cmd, attrs, opts) => {
|
|
4169
4169
|
if (cmd === 'LSUB') {
|
|
4170
4170
|
lsubCalls++;
|
|
4171
|
-
// e.g. Exchange
|
|
4171
|
+
// e.g. Exchange responds "BAD Command Argument Error"
|
|
4172
4172
|
throw commandError('Command failed', 'BAD');
|
|
4173
4173
|
}
|
|
4174
4174
|
if (cmd === 'LIST' && opts && opts.untagged && opts.untagged.LIST) {
|
|
@@ -4304,6 +4304,40 @@ module.exports['Commands: list retries with plain LIST when RETURN is rejected']
|
|
|
4304
4304
|
test.done();
|
|
4305
4305
|
};
|
|
4306
4306
|
|
|
4307
|
+
module.exports['Commands: list never falls back to LSUB on a rev2 session'] = async test => {
|
|
4308
|
+
let lsubCalled = false;
|
|
4309
|
+
// An Exchange-alike that advertises both revisions and negotiates rev2 via ENABLE,
|
|
4310
|
+
// having already proved it rejects RETURN (SUBSCRIBED). The listing therefore
|
|
4311
|
+
// carries no subscription state - and the LSUB that rev1 would fall back to is not
|
|
4312
|
+
// part of rev2, so asking anyway only risks upsetting the session.
|
|
4313
|
+
const connection = createMockConnection({
|
|
4314
|
+
state: 3,
|
|
4315
|
+
capabilities: new Map([
|
|
4316
|
+
['IMAP4rev2', true],
|
|
4317
|
+
['IMAP4rev1', true]
|
|
4318
|
+
]),
|
|
4319
|
+
enabled: new Set(['IMAP4REV2']),
|
|
4320
|
+
skipListSubscribedArg: true,
|
|
4321
|
+
exec: async (cmd, attrs, opts) => {
|
|
4322
|
+
if (cmd === 'LSUB') {
|
|
4323
|
+
lsubCalled = true;
|
|
4324
|
+
}
|
|
4325
|
+
if (cmd === 'LIST' && opts && opts.untagged && opts.untagged.LIST) {
|
|
4326
|
+
await opts.untagged.LIST({
|
|
4327
|
+
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'Folder1' }]
|
|
4328
|
+
});
|
|
4329
|
+
}
|
|
4330
|
+
return { next: () => {} };
|
|
4331
|
+
}
|
|
4332
|
+
});
|
|
4333
|
+
|
|
4334
|
+
const result = await listCommand(connection, '', '*');
|
|
4335
|
+
test.equal(lsubCalled, false);
|
|
4336
|
+
// No subscription source was available, so nothing claims the folder is subscribed
|
|
4337
|
+
test.ok(!result.find(entry => entry.path === 'Folder1').subscribed);
|
|
4338
|
+
test.done();
|
|
4339
|
+
};
|
|
4340
|
+
|
|
4307
4341
|
module.exports['Commands: list does not retry extended LIST on transport errors'] = async test => {
|
|
4308
4342
|
let listCalls = 0;
|
|
4309
4343
|
const connection = createMockConnection({
|
|
@@ -615,6 +615,43 @@ module.exports['Server: verifyOnly connect lists mailboxes and logs out'] = asyn
|
|
|
615
615
|
test.done();
|
|
616
616
|
};
|
|
617
617
|
|
|
618
|
+
module.exports['Server: verifyOnly keeps the authentication result after logging out'] = async test => {
|
|
619
|
+
// The whole point of verifyOnly is to report whether the credentials work. The mode logs out
|
|
620
|
+
// before connect() resolves, so if close() cleared `authenticated` the caller would read false
|
|
621
|
+
// off a connection that had just authenticated, with no later moment to read the real answer.
|
|
622
|
+
let server = createServer();
|
|
623
|
+
let port = await listen(server);
|
|
624
|
+
let client = makeClient(port, { verifyOnly: true, includeMailboxes: true });
|
|
625
|
+
client.on('error', () => {});
|
|
626
|
+
|
|
627
|
+
await client.connect();
|
|
628
|
+
test.ok(client.authenticated, 'authentication result survives the verifyOnly logout');
|
|
629
|
+
|
|
630
|
+
client.close();
|
|
631
|
+
test.ok(client.authenticated, 'and survives an explicit close as well');
|
|
632
|
+
|
|
633
|
+
server.close();
|
|
634
|
+
test.done();
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
module.exports['Server: an ordinary session clears the authentication state on close'] = async test => {
|
|
638
|
+
// The counterpart: for a session that is not verifyOnly, `authenticated` describes live state
|
|
639
|
+
// and must not survive the connection, or reconnect logic reads it as still signed in.
|
|
640
|
+
let server = createServer();
|
|
641
|
+
let port = await listen(server);
|
|
642
|
+
let client = makeClient(port);
|
|
643
|
+
client.on('error', () => {});
|
|
644
|
+
|
|
645
|
+
await client.connect();
|
|
646
|
+
test.ok(client.authenticated, 'authenticated while the session is up');
|
|
647
|
+
|
|
648
|
+
client.close();
|
|
649
|
+
test.equal(client.authenticated, false, 'cleared once the session ends');
|
|
650
|
+
|
|
651
|
+
server.close();
|
|
652
|
+
test.done();
|
|
653
|
+
};
|
|
654
|
+
|
|
618
655
|
module.exports['Server: ID is re-requested after login when first response is sparse'] = async test => {
|
|
619
656
|
let idCalls = 0;
|
|
620
657
|
let server = createServer({
|