imapflow 2.8.0 → 2.9.0

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.9.0](https://github.com/postalsys/imapflow/compare/v2.8.0...v2.9.0) (2026-10-22)
4
+
5
+
6
+ ### Features
7
+
8
+ * closedByClient tells a close the application asked for from one the server made, and auto-IDLE never waits longer than maxIdleTime ([5339c12](https://github.com/postalsys/imapflow/commit/5339c12895a759621b2e30bbb07521dba7b39929))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * logout() marks the close as the application's before the LOGOUT exchange, and closeAfter() is tested ([03602d2](https://github.com/postalsys/imapflow/commit/03602d2afa2dc8e243735ad6ede8403b733ed4b0))
14
+
3
15
  ## [2.8.0](https://github.com/postalsys/imapflow/compare/v2.7.0...v2.8.0) (2026-10-21)
4
16
 
5
17
 
@@ -45,7 +45,7 @@ async function compress(connection) {
45
45
  // STARTTLS guard does (starttls.ts records a flag, upgradeToSTARTTLS decides):
46
46
  // only the command module holds the response before its backpressure release,
47
47
  // so only it can order teardown ahead of that release.
48
- connection.closeAfter();
48
+ connection.closeConnectionAfter();
49
49
  response.next();
50
50
  throw error;
51
51
  }
@@ -157,8 +157,17 @@ export declare class ImapFlow extends EventEmitter {
157
157
  * what tells a server dropping an idle session from one dropping a session mid-command
158
158
  */
159
159
  idlingAtClose: boolean;
160
+ /**
161
+ * Whether the application ended the connection (`close()`, `closeAfter()`, `logout()`, disposing
162
+ * the client), `false` while it is open and when the server, the network or an error ended it.
163
+ * Tells a session the server dropped from one the application closed, also for an application
164
+ * that closes a connection only to open a new one
165
+ */
166
+ closedByClient: boolean;
160
167
  /** Whether log entries are also emitted as 'log' events, see the `emitLogs` option */
161
168
  emitLogs: boolean;
169
+ /** How often IDLE is restarted, in milliseconds (the `maxIdleTime` option), `false` for never */
170
+ maxIdleTime: number | false;
162
171
  /** Hash algorithm for the fallback message id, see `ImapFlowOptions.idHashAlgorithm` */
163
172
  idHashAlgorithm: string;
164
173
  /** The personal namespace, from the NAMESPACE command */
@@ -304,6 +304,8 @@ class ImapFlow extends node_events_1.EventEmitter {
304
304
  this.lastClientWrite = null;
305
305
  this._idleWaiters = [];
306
306
  this.idlingAtClose = false;
307
+ this.closedByClient = false;
308
+ this._closePending = false;
307
309
  this.emitLogs = !!this.options.emitLogs;
308
310
  this.lo = 0;
309
311
  this.untaggedHandlers = {};
@@ -315,6 +317,11 @@ class ImapFlow extends node_events_1.EventEmitter {
315
317
  this.idRequested = false;
316
318
  this.maxIdleTime = this.options.maxIdleTime || false;
317
319
  this.autoIdleDelay = normalizeAutoIdleDelay(this.options.autoIdleDelay, this.socketTimeout, this.log, this.id);
320
+ // A server that needs IDLE restarted this often can cut the silence before the first IDLE just
321
+ // as well: the connection is never left quiet for longer than maxIdleTime
322
+ if (this.maxIdleTime && this.autoIdleDelay > this.maxIdleTime) {
323
+ this.autoIdleDelay = this.maxIdleTime;
324
+ }
318
325
  this._lastPollAt = 0;
319
326
  this._openDownloads = 0;
320
327
  this._recoveryPending = false;
@@ -334,7 +341,7 @@ class ImapFlow extends node_events_1.EventEmitter {
334
341
  // than emitting an error event to the caller.
335
342
  this._streamerErrorHandler = (err) => {
336
343
  if (['Z_BUF_ERROR', 'ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'EHOSTUNREACH'].includes(err.code)) {
337
- this.closeAfter();
344
+ this.closeConnectionAfter();
338
345
  return;
339
346
  }
340
347
  this.log.error({ err, cid: this.id });
@@ -355,7 +362,7 @@ class ImapFlow extends node_events_1.EventEmitter {
355
362
  // only awaited connect(), as an unhandled 'error' that throws.
356
363
  if (this._errorReported) {
357
364
  this.log.debug({ msg: 'Suppressed error after the connection already reported one', err, cid: this.id });
358
- this.closeAfter();
365
+ this.closeConnectionAfter();
359
366
  return;
360
367
  }
361
368
  // During a STARTTLS handshake the upgrade owns the single error path (its settle()
@@ -373,7 +380,7 @@ class ImapFlow extends node_events_1.EventEmitter {
373
380
  return;
374
381
  }
375
382
  this.upgrading = false;
376
- this.closeAfter();
383
+ this.closeConnectionAfter();
377
384
  return;
378
385
  }
379
386
  // While the initial connect promise is still pending it owns error reporting:
@@ -384,12 +391,12 @@ class ImapFlow extends node_events_1.EventEmitter {
384
391
  this.initialResolve = false;
385
392
  this.initialReject = false;
386
393
  this._errorReported = true;
387
- this.closeAfter();
394
+ this.closeConnectionAfter();
388
395
  reject(err);
389
396
  return;
390
397
  }
391
398
  this._errorReported = true;
392
- this.closeAfter();
399
+ this.closeConnectionAfter();
393
400
  this.emit('error', err);
394
401
  }
395
402
  /** @internal */
@@ -415,7 +422,7 @@ class ImapFlow extends node_events_1.EventEmitter {
415
422
  }
416
423
  if (this.writeSocket.destroyed) {
417
424
  this.log.error({ msg: 'Write socket destroyed', cid: this.id });
418
- this.close();
425
+ this.closeConnection();
419
426
  return;
420
427
  }
421
428
  // Append CRLF only to the final part of a command. When sending literals,
@@ -1265,8 +1272,8 @@ class ImapFlow extends node_events_1.EventEmitter {
1265
1272
  this.log.error({ err, cid: this.id });
1266
1273
  this.emitError(err);
1267
1274
  });
1268
- this._socketClose = this._socketClose || (() => this.close());
1269
- this._socketEnd = this._socketEnd || (() => this.close());
1275
+ this._socketClose = this._socketClose || (() => this.closeConnection());
1276
+ this._socketEnd = this._socketEnd || (() => this.closeConnection());
1270
1277
  /**
1271
1278
  * Socket timeout event handler.
1272
1279
  *
@@ -1320,7 +1327,7 @@ class ImapFlow extends node_events_1.EventEmitter {
1320
1327
  .catch(err => {
1321
1328
  this.log.warn({ msg: 'Connection recovery failed after timeout', err, cid: this.id });
1322
1329
  if (!this.isClosed) {
1323
- this.close();
1330
+ this.closeConnection();
1324
1331
  }
1325
1332
  })
1326
1333
  .finally(() => {
@@ -1631,7 +1638,7 @@ class ImapFlow extends node_events_1.EventEmitter {
1631
1638
  // is rejected there instead (with a generic TLS error rather than STARTTLS_INJECTION).
1632
1639
  const failSTARTTLSInjection = () => {
1633
1640
  let err = (0, errors_js_1.createImapError)('Server sent data after the STARTTLS response and before the TLS handshake; possible plaintext-injection attack', 'STARTTLS_INJECTION', { tlsFailed: true });
1634
- this.closeAfter();
1641
+ this.closeConnectionAfter();
1635
1642
  return err;
1636
1643
  };
1637
1644
  // Check 1: the parser saw more input already buffered right after the tagged OK
@@ -1699,7 +1706,7 @@ class ImapFlow extends node_events_1.EventEmitter {
1699
1706
  // Preserve the original error, marked as a TLS failure so callers can tell
1700
1707
  // an upgrade failure from an ordinary command failure.
1701
1708
  err.tlsFailed = true;
1702
- this.closeAfter();
1709
+ this.closeConnectionAfter();
1703
1710
  return reject(err);
1704
1711
  }
1705
1712
  resolve(result);
@@ -1889,7 +1896,7 @@ class ImapFlow extends node_events_1.EventEmitter {
1889
1896
  .catch(err => {
1890
1897
  // The transport goes with the failed attempt: the instance can not be reused, and
1891
1898
  // an open socket would only trip the inactivity watchdog later
1892
- this.closeAfter();
1899
+ this.closeConnectionAfter();
1893
1900
  if (typeof this.initialReject === 'function') {
1894
1901
  this.log.error({ err, cid: this.id });
1895
1902
  (0, tools_js_1.clearTimer)(this.greetingTimeout);
@@ -1939,7 +1946,7 @@ class ImapFlow extends node_events_1.EventEmitter {
1939
1946
  // that answers a LOGOUT is the exception: its tagged completion closes the connection.
1940
1947
  const answersLogout = !!this.currentRequest && this.currentRequest.command.toUpperCase() === 'LOGOUT';
1941
1948
  if (!this.greetingReceived || !answersLogout) {
1942
- this.closeAfter();
1949
+ this.closeConnectionAfter();
1943
1950
  }
1944
1951
  }
1945
1952
  // Drops every capability-derived field together - the counterpart of
@@ -2188,10 +2195,15 @@ class ImapFlow extends node_events_1.EventEmitter {
2188
2195
  connectionBusy() {
2189
2196
  return !!(this.currentLock || this.locks.length || this.currentRequest || this.requestQueue.length || this._openDownloads);
2190
2197
  }
2198
+ // Settles every idle({ whenFree: true }) call waiting for the connection: resolved to re-check
2199
+ // it, or rejected when the connection is gone
2191
2200
  /** @internal */
2192
- wakeIdleWaiters() {
2193
- if (this._idleWaiters.length) {
2194
- for (let { resolve } of this._idleWaiters.splice(0)) {
2201
+ settleIdleWaiters(err) {
2202
+ for (let { resolve, reject } of this._idleWaiters.splice(0)) {
2203
+ if (err) {
2204
+ reject(err);
2205
+ }
2206
+ else {
2195
2207
  resolve();
2196
2208
  }
2197
2209
  }
@@ -2205,7 +2217,7 @@ class ImapFlow extends node_events_1.EventEmitter {
2205
2217
  autoidle() {
2206
2218
  // every place the connection may have become free calls this, so it is where waiting
2207
2219
  // idle({ whenFree: true }) calls re-check it, also with auto-IDLE turned off
2208
- this.wakeIdleWaiters();
2220
+ this.settleIdleWaiters();
2209
2221
  (0, tools_js_1.clearTimer)(this.idleStartTimer);
2210
2222
  if (this.options.disableAutoIdle || !this.usable || this.state !== this.states.SELECTED) {
2211
2223
  return;
@@ -2313,7 +2325,7 @@ class ImapFlow extends node_events_1.EventEmitter {
2313
2325
  this.connectTimeout = setTimeout(() => {
2314
2326
  let err = deadline.error();
2315
2327
  this.log.error({ err, cid: this.id });
2316
- this.closeAfter();
2328
+ this.closeConnectionAfter();
2317
2329
  reject(err);
2318
2330
  }, deadline.remaining());
2319
2331
  let onConnect = () => {
@@ -2340,7 +2352,7 @@ class ImapFlow extends node_events_1.EventEmitter {
2340
2352
  }
2341
2353
  });
2342
2354
  this.log.error({ err, cid: this.id });
2343
- this.closeAfter();
2355
+ this.closeConnectionAfter();
2344
2356
  reject(err);
2345
2357
  }, this.options.greetingTimeout || GREETING_TIMEOUT);
2346
2358
  const connected = this.socket;
@@ -2394,7 +2406,7 @@ class ImapFlow extends node_events_1.EventEmitter {
2394
2406
  catch (err) {
2395
2407
  // refused up front: the connection deadline must not stay armed
2396
2408
  (0, tools_js_1.clearTimer)(this.connectTimeout);
2397
- this.closeAfter();
2409
+ this.closeConnectionAfter();
2398
2410
  this.log.error({ err, cid: this.id });
2399
2411
  reject(err);
2400
2412
  return;
@@ -2408,7 +2420,7 @@ class ImapFlow extends node_events_1.EventEmitter {
2408
2420
  this._connectErrorHandler = (err) => {
2409
2421
  (0, tools_js_1.clearTimer)(this.connectTimeout);
2410
2422
  (0, tools_js_1.clearTimer)(this.greetingTimeout);
2411
- this.closeAfter();
2423
+ this.closeConnectionAfter();
2412
2424
  this.log.error({ err, cid: this.id });
2413
2425
  reject(err);
2414
2426
  };
@@ -2426,6 +2438,9 @@ class ImapFlow extends node_events_1.EventEmitter {
2426
2438
  * await client.logout();
2427
2439
  */
2428
2440
  async logout() {
2441
+ // marked before the exchange: a server may answer with BYE and drop the socket before the
2442
+ // tagged OK, which closes the connection from the socket's side first
2443
+ this.markClosedByClient();
2429
2444
  return await this.run('LOGOUT');
2430
2445
  }
2431
2446
  /**
@@ -2434,7 +2449,21 @@ class ImapFlow extends node_events_1.EventEmitter {
2434
2449
  * caller function to proceed, and run `close()` function afterwards.
2435
2450
  */
2436
2451
  closeAfter() {
2437
- setImmediate(() => this.close());
2452
+ this.markClosedByClient();
2453
+ this.closeConnectionAfter();
2454
+ }
2455
+ // The deferred close ImapFlow itself starts (a server BYE, an error), which closedByClient leaves alone
2456
+ /** @internal */
2457
+ closeConnectionAfter() {
2458
+ this._closePending = true;
2459
+ setImmediate(() => this.closeConnection());
2460
+ }
2461
+ // The application asked for the close, unless the connection is already closed or closing
2462
+ /** @internal */
2463
+ markClosedByClient() {
2464
+ if (!this.isClosed && !this._closePending) {
2465
+ this.closedByClient = true;
2466
+ }
2438
2467
  }
2439
2468
  // Connection-scoped wrapper around the shared stamping helper; see buildConnectionError().
2440
2469
  /** @internal */
@@ -2470,6 +2499,13 @@ class ImapFlow extends node_events_1.EventEmitter {
2470
2499
  * client.close();
2471
2500
  */
2472
2501
  close() {
2502
+ this.markClosedByClient();
2503
+ this.closeConnection();
2504
+ }
2505
+ // The close itself, for the application's close() and for the closes ImapFlow starts on its own
2506
+ // (the socket ending, a write to a destroyed socket, a failed timeout recovery)
2507
+ /** @internal */
2508
+ closeConnection() {
2473
2509
  try {
2474
2510
  this.closeTimers();
2475
2511
  this.usable = false;
@@ -2514,10 +2550,7 @@ class ImapFlow extends node_events_1.EventEmitter {
2514
2550
  this.closeRequests();
2515
2551
  this.closeLocks();
2516
2552
  if (this._idleWaiters.length) {
2517
- let waiterError = this.createNoConnectionError(this.byeReason, { rejectedFrom: 'idleWait', command: 'IDLE' });
2518
- for (let { reject } of this._idleWaiters.splice(0)) {
2519
- reject(waiterError);
2520
- }
2553
+ this.settleIdleWaiters(this.createNoConnectionError(this.byeReason, { rejectedFrom: 'idleWait', command: 'IDLE' }));
2521
2554
  }
2522
2555
  this.closeStreams();
2523
2556
  // clear socket handlers
@@ -1,3 +1,3 @@
1
1
  export declare const name = "imapflow";
2
- export declare const version = "2.8.0";
2
+ export declare const version = "2.9.0";
3
3
  export declare const homepage = "https://imapflow.com/";
@@ -3,5 +3,5 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.homepage = exports.version = exports.name = void 0;
5
5
  exports.name = "imapflow";
6
- exports.version = "2.8.0";
6
+ exports.version = "2.9.0";
7
7
  exports.homepage = "https://imapflow.com/";
@@ -42,7 +42,7 @@ export default async function compress(connection) {
42
42
  // STARTTLS guard does (starttls.ts records a flag, upgradeToSTARTTLS decides):
43
43
  // only the command module holds the response before its backpressure release,
44
44
  // so only it can order teardown ahead of that release.
45
- connection.closeAfter();
45
+ connection.closeConnectionAfter();
46
46
  response.next();
47
47
  throw error;
48
48
  }
@@ -157,8 +157,17 @@ export declare class ImapFlow extends EventEmitter {
157
157
  * what tells a server dropping an idle session from one dropping a session mid-command
158
158
  */
159
159
  idlingAtClose: boolean;
160
+ /**
161
+ * Whether the application ended the connection (`close()`, `closeAfter()`, `logout()`, disposing
162
+ * the client), `false` while it is open and when the server, the network or an error ended it.
163
+ * Tells a session the server dropped from one the application closed, also for an application
164
+ * that closes a connection only to open a new one
165
+ */
166
+ closedByClient: boolean;
160
167
  /** Whether log entries are also emitted as 'log' events, see the `emitLogs` option */
161
168
  emitLogs: boolean;
169
+ /** How often IDLE is restarted, in milliseconds (the `maxIdleTime` option), `false` for never */
170
+ maxIdleTime: number | false;
162
171
  /** Hash algorithm for the fallback message id, see `ImapFlowOptions.idHashAlgorithm` */
163
172
  idHashAlgorithm: string;
164
173
  /** The personal namespace, from the NAMESPACE command */
@@ -261,6 +261,8 @@ export class ImapFlow extends EventEmitter {
261
261
  this.lastClientWrite = null;
262
262
  this._idleWaiters = [];
263
263
  this.idlingAtClose = false;
264
+ this.closedByClient = false;
265
+ this._closePending = false;
264
266
  this.emitLogs = !!this.options.emitLogs;
265
267
  this.lo = 0;
266
268
  this.untaggedHandlers = {};
@@ -272,6 +274,11 @@ export class ImapFlow extends EventEmitter {
272
274
  this.idRequested = false;
273
275
  this.maxIdleTime = this.options.maxIdleTime || false;
274
276
  this.autoIdleDelay = normalizeAutoIdleDelay(this.options.autoIdleDelay, this.socketTimeout, this.log, this.id);
277
+ // A server that needs IDLE restarted this often can cut the silence before the first IDLE just
278
+ // as well: the connection is never left quiet for longer than maxIdleTime
279
+ if (this.maxIdleTime && this.autoIdleDelay > this.maxIdleTime) {
280
+ this.autoIdleDelay = this.maxIdleTime;
281
+ }
275
282
  this._lastPollAt = 0;
276
283
  this._openDownloads = 0;
277
284
  this._recoveryPending = false;
@@ -291,7 +298,7 @@ export class ImapFlow extends EventEmitter {
291
298
  // than emitting an error event to the caller.
292
299
  this._streamerErrorHandler = (err) => {
293
300
  if (['Z_BUF_ERROR', 'ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'EHOSTUNREACH'].includes(err.code)) {
294
- this.closeAfter();
301
+ this.closeConnectionAfter();
295
302
  return;
296
303
  }
297
304
  this.log.error({ err, cid: this.id });
@@ -312,7 +319,7 @@ export class ImapFlow extends EventEmitter {
312
319
  // only awaited connect(), as an unhandled 'error' that throws.
313
320
  if (this._errorReported) {
314
321
  this.log.debug({ msg: 'Suppressed error after the connection already reported one', err, cid: this.id });
315
- this.closeAfter();
322
+ this.closeConnectionAfter();
316
323
  return;
317
324
  }
318
325
  // During a STARTTLS handshake the upgrade owns the single error path (its settle()
@@ -330,7 +337,7 @@ export class ImapFlow extends EventEmitter {
330
337
  return;
331
338
  }
332
339
  this.upgrading = false;
333
- this.closeAfter();
340
+ this.closeConnectionAfter();
334
341
  return;
335
342
  }
336
343
  // While the initial connect promise is still pending it owns error reporting:
@@ -341,12 +348,12 @@ export class ImapFlow extends EventEmitter {
341
348
  this.initialResolve = false;
342
349
  this.initialReject = false;
343
350
  this._errorReported = true;
344
- this.closeAfter();
351
+ this.closeConnectionAfter();
345
352
  reject(err);
346
353
  return;
347
354
  }
348
355
  this._errorReported = true;
349
- this.closeAfter();
356
+ this.closeConnectionAfter();
350
357
  this.emit('error', err);
351
358
  }
352
359
  /** @internal */
@@ -372,7 +379,7 @@ export class ImapFlow extends EventEmitter {
372
379
  }
373
380
  if (this.writeSocket.destroyed) {
374
381
  this.log.error({ msg: 'Write socket destroyed', cid: this.id });
375
- this.close();
382
+ this.closeConnection();
376
383
  return;
377
384
  }
378
385
  // Append CRLF only to the final part of a command. When sending literals,
@@ -1222,8 +1229,8 @@ export class ImapFlow extends EventEmitter {
1222
1229
  this.log.error({ err, cid: this.id });
1223
1230
  this.emitError(err);
1224
1231
  });
1225
- this._socketClose = this._socketClose || (() => this.close());
1226
- this._socketEnd = this._socketEnd || (() => this.close());
1232
+ this._socketClose = this._socketClose || (() => this.closeConnection());
1233
+ this._socketEnd = this._socketEnd || (() => this.closeConnection());
1227
1234
  /**
1228
1235
  * Socket timeout event handler.
1229
1236
  *
@@ -1277,7 +1284,7 @@ export class ImapFlow extends EventEmitter {
1277
1284
  .catch(err => {
1278
1285
  this.log.warn({ msg: 'Connection recovery failed after timeout', err, cid: this.id });
1279
1286
  if (!this.isClosed) {
1280
- this.close();
1287
+ this.closeConnection();
1281
1288
  }
1282
1289
  })
1283
1290
  .finally(() => {
@@ -1588,7 +1595,7 @@ export class ImapFlow extends EventEmitter {
1588
1595
  // is rejected there instead (with a generic TLS error rather than STARTTLS_INJECTION).
1589
1596
  const failSTARTTLSInjection = () => {
1590
1597
  let err = createImapError('Server sent data after the STARTTLS response and before the TLS handshake; possible plaintext-injection attack', 'STARTTLS_INJECTION', { tlsFailed: true });
1591
- this.closeAfter();
1598
+ this.closeConnectionAfter();
1592
1599
  return err;
1593
1600
  };
1594
1601
  // Check 1: the parser saw more input already buffered right after the tagged OK
@@ -1656,7 +1663,7 @@ export class ImapFlow extends EventEmitter {
1656
1663
  // Preserve the original error, marked as a TLS failure so callers can tell
1657
1664
  // an upgrade failure from an ordinary command failure.
1658
1665
  err.tlsFailed = true;
1659
- this.closeAfter();
1666
+ this.closeConnectionAfter();
1660
1667
  return reject(err);
1661
1668
  }
1662
1669
  resolve(result);
@@ -1846,7 +1853,7 @@ export class ImapFlow extends EventEmitter {
1846
1853
  .catch(err => {
1847
1854
  // The transport goes with the failed attempt: the instance can not be reused, and
1848
1855
  // an open socket would only trip the inactivity watchdog later
1849
- this.closeAfter();
1856
+ this.closeConnectionAfter();
1850
1857
  if (typeof this.initialReject === 'function') {
1851
1858
  this.log.error({ err, cid: this.id });
1852
1859
  clearTimer(this.greetingTimeout);
@@ -1896,7 +1903,7 @@ export class ImapFlow extends EventEmitter {
1896
1903
  // that answers a LOGOUT is the exception: its tagged completion closes the connection.
1897
1904
  const answersLogout = !!this.currentRequest && this.currentRequest.command.toUpperCase() === 'LOGOUT';
1898
1905
  if (!this.greetingReceived || !answersLogout) {
1899
- this.closeAfter();
1906
+ this.closeConnectionAfter();
1900
1907
  }
1901
1908
  }
1902
1909
  // Drops every capability-derived field together - the counterpart of
@@ -2145,10 +2152,15 @@ export class ImapFlow extends EventEmitter {
2145
2152
  connectionBusy() {
2146
2153
  return !!(this.currentLock || this.locks.length || this.currentRequest || this.requestQueue.length || this._openDownloads);
2147
2154
  }
2155
+ // Settles every idle({ whenFree: true }) call waiting for the connection: resolved to re-check
2156
+ // it, or rejected when the connection is gone
2148
2157
  /** @internal */
2149
- wakeIdleWaiters() {
2150
- if (this._idleWaiters.length) {
2151
- for (let { resolve } of this._idleWaiters.splice(0)) {
2158
+ settleIdleWaiters(err) {
2159
+ for (let { resolve, reject } of this._idleWaiters.splice(0)) {
2160
+ if (err) {
2161
+ reject(err);
2162
+ }
2163
+ else {
2152
2164
  resolve();
2153
2165
  }
2154
2166
  }
@@ -2162,7 +2174,7 @@ export class ImapFlow extends EventEmitter {
2162
2174
  autoidle() {
2163
2175
  // every place the connection may have become free calls this, so it is where waiting
2164
2176
  // idle({ whenFree: true }) calls re-check it, also with auto-IDLE turned off
2165
- this.wakeIdleWaiters();
2177
+ this.settleIdleWaiters();
2166
2178
  clearTimer(this.idleStartTimer);
2167
2179
  if (this.options.disableAutoIdle || !this.usable || this.state !== this.states.SELECTED) {
2168
2180
  return;
@@ -2270,7 +2282,7 @@ export class ImapFlow extends EventEmitter {
2270
2282
  this.connectTimeout = setTimeout(() => {
2271
2283
  let err = deadline.error();
2272
2284
  this.log.error({ err, cid: this.id });
2273
- this.closeAfter();
2285
+ this.closeConnectionAfter();
2274
2286
  reject(err);
2275
2287
  }, deadline.remaining());
2276
2288
  let onConnect = () => {
@@ -2297,7 +2309,7 @@ export class ImapFlow extends EventEmitter {
2297
2309
  }
2298
2310
  });
2299
2311
  this.log.error({ err, cid: this.id });
2300
- this.closeAfter();
2312
+ this.closeConnectionAfter();
2301
2313
  reject(err);
2302
2314
  }, this.options.greetingTimeout || GREETING_TIMEOUT);
2303
2315
  const connected = this.socket;
@@ -2351,7 +2363,7 @@ export class ImapFlow extends EventEmitter {
2351
2363
  catch (err) {
2352
2364
  // refused up front: the connection deadline must not stay armed
2353
2365
  clearTimer(this.connectTimeout);
2354
- this.closeAfter();
2366
+ this.closeConnectionAfter();
2355
2367
  this.log.error({ err, cid: this.id });
2356
2368
  reject(err);
2357
2369
  return;
@@ -2365,7 +2377,7 @@ export class ImapFlow extends EventEmitter {
2365
2377
  this._connectErrorHandler = (err) => {
2366
2378
  clearTimer(this.connectTimeout);
2367
2379
  clearTimer(this.greetingTimeout);
2368
- this.closeAfter();
2380
+ this.closeConnectionAfter();
2369
2381
  this.log.error({ err, cid: this.id });
2370
2382
  reject(err);
2371
2383
  };
@@ -2383,6 +2395,9 @@ export class ImapFlow extends EventEmitter {
2383
2395
  * await client.logout();
2384
2396
  */
2385
2397
  async logout() {
2398
+ // marked before the exchange: a server may answer with BYE and drop the socket before the
2399
+ // tagged OK, which closes the connection from the socket's side first
2400
+ this.markClosedByClient();
2386
2401
  return await this.run('LOGOUT');
2387
2402
  }
2388
2403
  /**
@@ -2391,7 +2406,21 @@ export class ImapFlow extends EventEmitter {
2391
2406
  * caller function to proceed, and run `close()` function afterwards.
2392
2407
  */
2393
2408
  closeAfter() {
2394
- setImmediate(() => this.close());
2409
+ this.markClosedByClient();
2410
+ this.closeConnectionAfter();
2411
+ }
2412
+ // The deferred close ImapFlow itself starts (a server BYE, an error), which closedByClient leaves alone
2413
+ /** @internal */
2414
+ closeConnectionAfter() {
2415
+ this._closePending = true;
2416
+ setImmediate(() => this.closeConnection());
2417
+ }
2418
+ // The application asked for the close, unless the connection is already closed or closing
2419
+ /** @internal */
2420
+ markClosedByClient() {
2421
+ if (!this.isClosed && !this._closePending) {
2422
+ this.closedByClient = true;
2423
+ }
2395
2424
  }
2396
2425
  // Connection-scoped wrapper around the shared stamping helper; see buildConnectionError().
2397
2426
  /** @internal */
@@ -2427,6 +2456,13 @@ export class ImapFlow extends EventEmitter {
2427
2456
  * client.close();
2428
2457
  */
2429
2458
  close() {
2459
+ this.markClosedByClient();
2460
+ this.closeConnection();
2461
+ }
2462
+ // The close itself, for the application's close() and for the closes ImapFlow starts on its own
2463
+ // (the socket ending, a write to a destroyed socket, a failed timeout recovery)
2464
+ /** @internal */
2465
+ closeConnection() {
2430
2466
  try {
2431
2467
  this.closeTimers();
2432
2468
  this.usable = false;
@@ -2471,10 +2507,7 @@ export class ImapFlow extends EventEmitter {
2471
2507
  this.closeRequests();
2472
2508
  this.closeLocks();
2473
2509
  if (this._idleWaiters.length) {
2474
- let waiterError = this.createNoConnectionError(this.byeReason, { rejectedFrom: 'idleWait', command: 'IDLE' });
2475
- for (let { reject } of this._idleWaiters.splice(0)) {
2476
- reject(waiterError);
2477
- }
2510
+ this.settleIdleWaiters(this.createNoConnectionError(this.byeReason, { rejectedFrom: 'idleWait', command: 'IDLE' }));
2478
2511
  }
2479
2512
  this.closeStreams();
2480
2513
  // clear socket handlers
@@ -1,3 +1,3 @@
1
1
  export declare const name = "imapflow";
2
- export declare const version = "2.8.0";
2
+ export declare const version = "2.9.0";
3
3
  export declare const homepage = "https://imapflow.com/";
@@ -1,4 +1,4 @@
1
1
  // Generated by scripts/build.js from package.json. Do not edit by hand.
2
2
  export const name = "imapflow";
3
- export const version = "2.8.0";
3
+ export const version = "2.9.0";
4
4
  export const homepage = "https://imapflow.com/";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "2.8.0",
3
+ "version": "2.9.0",
4
4
  "description": "IMAP Client for Node",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/imap-flow.js",