imapflow 1.6.1 → 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.
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "1.6.1"
2
+ ".": "1.6.2"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
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
+
3
10
  ## [1.6.1](https://github.com/postalsys/imapflow/compare/v1.6.0...v1.6.1) (2026-07-27)
4
11
 
5
12
 
@@ -478,20 +478,21 @@ module.exports = async (connection, reference, mailbox, options) => {
478
478
  response.next();
479
479
  };
480
480
 
481
- // Skipped when RETURN (SUBSCRIBED) already provided subscription state or when
482
- // this connection's server already rejected LSUB once. Safety net: if the
483
- // extended LIST was accepted but not a single mailbox came back subscribed on a
484
- // non-rev2 session, assume the server silently ignored RETURN (SUBSCRIBED) and
485
- // fall back to LSUB anyway (a rev2 session has no LSUB to fall back to, and an
486
- // account without any subscriptions legitimately looks the same).
487
- let needsLsub = !successStage.subscribed || (!isRev2Active(connection) && !entries.some(entry => entry.subscribed));
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 recognize the command (IMAP4rev2
494
- // removed LSUB) - skip LSUB for the rest of this connection
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.6.1",
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.1",
33
+ "@types/node": "26.1.2",
34
34
  "c8": "12.0.0",
35
- "eslint": "10.7.0",
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 in IMAP4rev2 mode responds "BAD Command Argument Error"
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({