imapflow 2.6.0 → 2.8.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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.8.0](https://github.com/postalsys/imapflow/compare/v2.7.0...v2.8.0) (2026-10-21)
4
+
5
+
6
+ ### Features
7
+
8
+ * idle({ whenFree: true }) waits for the connection to be free before starting IDLE ([791c24c](https://github.com/postalsys/imapflow/commit/791c24ca607b60585ef619269abb0aa1f400f525))
9
+
10
+ ## [2.7.0](https://github.com/postalsys/imapflow/compare/v2.6.0...v2.7.0) (2026-10-21)
11
+
12
+
13
+ ### Features
14
+
15
+ * expose idlingAtClose so a consumer can tell a session dropped in IDLE ([eb5ac6c](https://github.com/postalsys/imapflow/commit/eb5ac6c5307a735d4f2526c8e8a266e488dbe17f))
16
+
3
17
  ## [2.6.0](https://github.com/postalsys/imapflow/compare/v2.5.9...v2.6.0) (2026-10-21)
4
18
 
5
19
 
package/README.md CHANGED
@@ -135,6 +135,7 @@ Besides `mailbox`, `usable`, `authenticated` and `capabilities`, a client expose
135
135
  - `client.context` - data of the application's own, never read by ImapFlow. It starts as the `context` option (`new ImapFlow({ ..., context: { account: 'a1' } })`) and can be replaced at any time. Its type is `unknown`, cast it to what you store there.
136
136
  - `client.connectedAt` - when the TCP or TLS connection was established, in milliseconds since the epoch, `null` before that.
137
137
  - `client.lastClientWrite` - when the client last wrote to the server (a command, a literal, the `DONE` that ends IDLE), in milliseconds since the epoch, `null` before the first write. Useful for staying ahead of server inactivity timers.
138
+ - `client.idlingAtClose` - whether the connection was in IDLE (or the polling fallback) when it closed. `idling` is already cleared when the `close` event fires, so this tells a server dropping an idle session from one dropping a session mid-command.
138
139
  - `client.mailboxList` - with `verifyOnly` and `includeMailboxes`, the mailboxes listed before the connection logged out; `undefined` otherwise.
139
140
 
140
141
  See the [Quick Start guide](https://imapflow.com/docs/getting-started/quick-start) for more examples, including Gmail, Outlook, and Yahoo configuration.
@@ -4,7 +4,7 @@
4
4
  import { EventEmitter } from 'node:events';
5
5
  import { PassThrough, type Readable } from 'node:stream';
6
6
  import { AuthenticationFailure } from './errors.js';
7
- import type { AppendResponseObject, CopyResponseObject, DownloadManyOptions, DownloadManyResult, DownloadObject, DownloadNotFound, DownloadOptions, ESearchResult, FetchMessageObject, FetchOptions, FetchQueryObject, IdInfoObject, ImapFlowEvents, ImapFlowOptions, InternalLogger, ListOptions, ListResponse, ListTreeResponse, MailboxCreateResponse, MailboxDeleteResponse, MailboxLockObject, MailboxLockOptions, MailboxObject, MailboxOpenOptions, MailboxRenameResponse, MessageRange, MessageRangeOptions, NamespaceObject, NamespacesObject, QuotaResponse, SearchObject, SearchOptions, SearchReturnOption, SequenceString, StatusObject, StatusQuery, StoreOptions, TlsInfo, ImapTransportSocket } from './types.js';
7
+ import type { AppendResponseObject, CopyResponseObject, DownloadManyOptions, DownloadManyResult, DownloadObject, DownloadNotFound, DownloadOptions, ESearchResult, FetchMessageObject, FetchOptions, FetchQueryObject, IdInfoObject, ImapFlowEvents, ImapFlowOptions, InternalLogger, ListOptions, ListResponse, ListTreeResponse, MailboxCreateResponse, MailboxDeleteResponse, MailboxLockObject, MailboxLockOptions, IdleOptions, MailboxObject, MailboxOpenOptions, MailboxRenameResponse, MessageRange, MessageRangeOptions, NamespaceObject, NamespacesObject, QuotaResponse, SearchObject, SearchOptions, SearchReturnOption, SequenceString, StatusObject, StatusQuery, StoreOptions, TlsInfo, ImapTransportSocket } from './types.js';
8
8
  export type * from './types.js';
9
9
  export type { ImapFlowError } from './errors.js';
10
10
  export { AuthenticationFailure, ImapFlowErrorCode } from './errors.js';
@@ -151,6 +151,12 @@ export declare class ImapFlow extends EventEmitter {
151
151
  * (RFC 9051 allows 30 minutes), and this is what their timer measures
152
152
  */
153
153
  lastClientWrite: number | null;
154
+ /**
155
+ * Whether the connection was in IDLE (or the polling fallback) when it closed, `false` while it
156
+ * is open. `idling` itself is already cleared by the time the 'close' event fires, so this is
157
+ * what tells a server dropping an idle session from one dropping a session mid-command
158
+ */
159
+ idlingAtClose: boolean;
154
160
  /** Whether log entries are also emitted as 'log' events, see the `emitLogs` option */
155
161
  emitLogs: boolean;
156
162
  /** Hash algorithm for the fallback message id, see `ImapFlowOptions.idHashAlgorithm` */
@@ -372,8 +378,10 @@ export declare class ImapFlow extends EventEmitter {
372
378
  * return until IDLE is finished which means you would have to call some other command out of scope.
373
379
  *
374
380
  * Unlike auto-IDLE, a direct call does not wait for the connection to be free: a mailbox lock
375
- * holder may idle inside its own lock, and idle() can not tell it from another caller.
381
+ * holder may idle inside its own lock, and idle() can not tell it from another caller. Code that
382
+ * holds no lock passes `{ whenFree: true }` to wait for the connection to be free first.
376
383
  *
384
+ * @param options IDLE options
377
385
  * @returns `false` if IDLE failed, `undefined` otherwise
378
386
  *
379
387
  * @example
@@ -381,7 +389,7 @@ export declare class ImapFlow extends EventEmitter {
381
389
  *
382
390
  * await client.idle();
383
391
  */
384
- idle(): Promise<boolean | undefined>;
392
+ idle(options?: IdleOptions | undefined): Promise<boolean | undefined>;
385
393
  /**
386
394
  * Sets flags for a message or message range
387
395
  *
@@ -302,6 +302,8 @@ class ImapFlow extends node_events_1.EventEmitter {
302
302
  this.context = this.options.context;
303
303
  this.connectedAt = null;
304
304
  this.lastClientWrite = null;
305
+ this._idleWaiters = [];
306
+ this.idlingAtClose = false;
305
307
  this.emitLogs = !!this.options.emitLogs;
306
308
  this.lo = 0;
307
309
  this.untaggedHandlers = {};
@@ -2186,6 +2188,14 @@ class ImapFlow extends node_events_1.EventEmitter {
2186
2188
  connectionBusy() {
2187
2189
  return !!(this.currentLock || this.locks.length || this.currentRequest || this.requestQueue.length || this._openDownloads);
2188
2190
  }
2191
+ /** @internal */
2192
+ wakeIdleWaiters() {
2193
+ if (this._idleWaiters.length) {
2194
+ for (let { resolve } of this._idleWaiters.splice(0)) {
2195
+ resolve();
2196
+ }
2197
+ }
2198
+ }
2189
2199
  // Timer process-liveness policy: connection establishment and greeting deadlines keep the
2190
2200
  // process alive, because a caller is waiting on connect() to settle. Background timers
2191
2201
  // (auto-IDLE, IDLE restart, fallback polling, throttle back-off, the held-lock diagnostic) are
@@ -2193,6 +2203,9 @@ class ImapFlow extends node_events_1.EventEmitter {
2193
2203
  // explicitly on close().
2194
2204
  /** @internal */
2195
2205
  autoidle() {
2206
+ // every place the connection may have become free calls this, so it is where waiting
2207
+ // idle({ whenFree: true }) calls re-check it, also with auto-IDLE turned off
2208
+ this.wakeIdleWaiters();
2196
2209
  (0, tools_js_1.clearTimer)(this.idleStartTimer);
2197
2210
  if (this.options.disableAutoIdle || !this.usable || this.state !== this.states.SELECTED) {
2198
2211
  return;
@@ -2464,6 +2477,8 @@ class ImapFlow extends node_events_1.EventEmitter {
2464
2477
  // poll or IDLE that unwinds after this point sees that it no longer owns the flag and
2465
2478
  // leaves it alone (see claimIdling() in commands/idle.ts).
2466
2479
  this._idleSession = null;
2480
+ // a later close() finds idling already cleared and keeps what the first one saw
2481
+ this.idlingAtClose = this.idlingAtClose || this.idling;
2467
2482
  this.idling = false;
2468
2483
  this.closeConnectSteps();
2469
2484
  if (typeof this.preCheck === 'function') {
@@ -2498,6 +2513,12 @@ class ImapFlow extends node_events_1.EventEmitter {
2498
2513
  }
2499
2514
  this.closeRequests();
2500
2515
  this.closeLocks();
2516
+ 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
+ }
2521
+ }
2501
2522
  this.closeStreams();
2502
2523
  // clear socket handlers
2503
2524
  this.clearSocketHandlers();
@@ -2938,8 +2959,10 @@ class ImapFlow extends node_events_1.EventEmitter {
2938
2959
  * return until IDLE is finished which means you would have to call some other command out of scope.
2939
2960
  *
2940
2961
  * Unlike auto-IDLE, a direct call does not wait for the connection to be free: a mailbox lock
2941
- * holder may idle inside its own lock, and idle() can not tell it from another caller.
2962
+ * holder may idle inside its own lock, and idle() can not tell it from another caller. Code that
2963
+ * holds no lock passes `{ whenFree: true }` to wait for the connection to be free first.
2942
2964
  *
2965
+ * @param options IDLE options
2943
2966
  * @returns `false` if IDLE failed, `undefined` otherwise
2944
2967
  *
2945
2968
  * @example
@@ -2947,7 +2970,13 @@ class ImapFlow extends node_events_1.EventEmitter {
2947
2970
  *
2948
2971
  * await client.idle();
2949
2972
  */
2950
- async idle() {
2973
+ async idle(options) {
2974
+ // Re-checked on every wake, as another IDLE may have started meanwhile
2975
+ while (options?.whenFree && !this.idling && this.usable && this.connectionBusy()) {
2976
+ await new Promise((resolve, reject) => {
2977
+ this._idleWaiters.push({ resolve, reject });
2978
+ });
2979
+ }
2951
2980
  if (!this.idling) {
2952
2981
  return await this.run('IDLE', this.maxIdleTime);
2953
2982
  }
@@ -1,3 +1,3 @@
1
1
  export declare const name = "imapflow";
2
- export declare const version = "2.6.0";
2
+ export declare const version = "2.8.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.6.0";
6
+ exports.version = "2.8.0";
7
7
  exports.homepage = "https://imapflow.com/";
@@ -745,6 +745,16 @@ export interface MailboxOpenOptions {
745
745
  /** QRESYNC (RFC 7162): the UIDVALIDITY known from the previous session, required with `changedSince` */
746
746
  uidValidity?: bigint | number | string | undefined;
747
747
  }
748
+ export interface IdleOptions {
749
+ /**
750
+ * Wait until the connection is free before starting IDLE: no mailbox lock held or queued, no
751
+ * command running or queued, no download open. Use it from code that does not hold a mailbox
752
+ * lock, so IDLE does not land between another caller's commands. A lock holder must not set it,
753
+ * it would wait for its own release. The returned promise rejects with `NoConnection` when the
754
+ * connection closes while waiting.
755
+ */
756
+ whenFree?: boolean | undefined;
757
+ }
748
758
  export interface MailboxLockOptions extends MailboxOpenOptions {
749
759
  /**
750
760
  * Optional timeout in milliseconds to wait for the lock to be granted.
@@ -4,7 +4,7 @@
4
4
  import { EventEmitter } from 'node:events';
5
5
  import { PassThrough, type Readable } from 'node:stream';
6
6
  import { AuthenticationFailure } from './errors.js';
7
- import type { AppendResponseObject, CopyResponseObject, DownloadManyOptions, DownloadManyResult, DownloadObject, DownloadNotFound, DownloadOptions, ESearchResult, FetchMessageObject, FetchOptions, FetchQueryObject, IdInfoObject, ImapFlowEvents, ImapFlowOptions, InternalLogger, ListOptions, ListResponse, ListTreeResponse, MailboxCreateResponse, MailboxDeleteResponse, MailboxLockObject, MailboxLockOptions, MailboxObject, MailboxOpenOptions, MailboxRenameResponse, MessageRange, MessageRangeOptions, NamespaceObject, NamespacesObject, QuotaResponse, SearchObject, SearchOptions, SearchReturnOption, SequenceString, StatusObject, StatusQuery, StoreOptions, TlsInfo, ImapTransportSocket } from './types.js';
7
+ import type { AppendResponseObject, CopyResponseObject, DownloadManyOptions, DownloadManyResult, DownloadObject, DownloadNotFound, DownloadOptions, ESearchResult, FetchMessageObject, FetchOptions, FetchQueryObject, IdInfoObject, ImapFlowEvents, ImapFlowOptions, InternalLogger, ListOptions, ListResponse, ListTreeResponse, MailboxCreateResponse, MailboxDeleteResponse, MailboxLockObject, MailboxLockOptions, IdleOptions, MailboxObject, MailboxOpenOptions, MailboxRenameResponse, MessageRange, MessageRangeOptions, NamespaceObject, NamespacesObject, QuotaResponse, SearchObject, SearchOptions, SearchReturnOption, SequenceString, StatusObject, StatusQuery, StoreOptions, TlsInfo, ImapTransportSocket } from './types.js';
8
8
  export type * from './types.js';
9
9
  export type { ImapFlowError } from './errors.js';
10
10
  export { AuthenticationFailure, ImapFlowErrorCode } from './errors.js';
@@ -151,6 +151,12 @@ export declare class ImapFlow extends EventEmitter {
151
151
  * (RFC 9051 allows 30 minutes), and this is what their timer measures
152
152
  */
153
153
  lastClientWrite: number | null;
154
+ /**
155
+ * Whether the connection was in IDLE (or the polling fallback) when it closed, `false` while it
156
+ * is open. `idling` itself is already cleared by the time the 'close' event fires, so this is
157
+ * what tells a server dropping an idle session from one dropping a session mid-command
158
+ */
159
+ idlingAtClose: boolean;
154
160
  /** Whether log entries are also emitted as 'log' events, see the `emitLogs` option */
155
161
  emitLogs: boolean;
156
162
  /** Hash algorithm for the fallback message id, see `ImapFlowOptions.idHashAlgorithm` */
@@ -372,8 +378,10 @@ export declare class ImapFlow extends EventEmitter {
372
378
  * return until IDLE is finished which means you would have to call some other command out of scope.
373
379
  *
374
380
  * Unlike auto-IDLE, a direct call does not wait for the connection to be free: a mailbox lock
375
- * holder may idle inside its own lock, and idle() can not tell it from another caller.
381
+ * holder may idle inside its own lock, and idle() can not tell it from another caller. Code that
382
+ * holds no lock passes `{ whenFree: true }` to wait for the connection to be free first.
376
383
  *
384
+ * @param options IDLE options
377
385
  * @returns `false` if IDLE failed, `undefined` otherwise
378
386
  *
379
387
  * @example
@@ -381,7 +389,7 @@ export declare class ImapFlow extends EventEmitter {
381
389
  *
382
390
  * await client.idle();
383
391
  */
384
- idle(): Promise<boolean | undefined>;
392
+ idle(options?: IdleOptions | undefined): Promise<boolean | undefined>;
385
393
  /**
386
394
  * Sets flags for a message or message range
387
395
  *
@@ -259,6 +259,8 @@ export class ImapFlow extends EventEmitter {
259
259
  this.context = this.options.context;
260
260
  this.connectedAt = null;
261
261
  this.lastClientWrite = null;
262
+ this._idleWaiters = [];
263
+ this.idlingAtClose = false;
262
264
  this.emitLogs = !!this.options.emitLogs;
263
265
  this.lo = 0;
264
266
  this.untaggedHandlers = {};
@@ -2143,6 +2145,14 @@ export class ImapFlow extends EventEmitter {
2143
2145
  connectionBusy() {
2144
2146
  return !!(this.currentLock || this.locks.length || this.currentRequest || this.requestQueue.length || this._openDownloads);
2145
2147
  }
2148
+ /** @internal */
2149
+ wakeIdleWaiters() {
2150
+ if (this._idleWaiters.length) {
2151
+ for (let { resolve } of this._idleWaiters.splice(0)) {
2152
+ resolve();
2153
+ }
2154
+ }
2155
+ }
2146
2156
  // Timer process-liveness policy: connection establishment and greeting deadlines keep the
2147
2157
  // process alive, because a caller is waiting on connect() to settle. Background timers
2148
2158
  // (auto-IDLE, IDLE restart, fallback polling, throttle back-off, the held-lock diagnostic) are
@@ -2150,6 +2160,9 @@ export class ImapFlow extends EventEmitter {
2150
2160
  // explicitly on close().
2151
2161
  /** @internal */
2152
2162
  autoidle() {
2163
+ // every place the connection may have become free calls this, so it is where waiting
2164
+ // idle({ whenFree: true }) calls re-check it, also with auto-IDLE turned off
2165
+ this.wakeIdleWaiters();
2153
2166
  clearTimer(this.idleStartTimer);
2154
2167
  if (this.options.disableAutoIdle || !this.usable || this.state !== this.states.SELECTED) {
2155
2168
  return;
@@ -2421,6 +2434,8 @@ export class ImapFlow extends EventEmitter {
2421
2434
  // poll or IDLE that unwinds after this point sees that it no longer owns the flag and
2422
2435
  // leaves it alone (see claimIdling() in commands/idle.ts).
2423
2436
  this._idleSession = null;
2437
+ // a later close() finds idling already cleared and keeps what the first one saw
2438
+ this.idlingAtClose = this.idlingAtClose || this.idling;
2424
2439
  this.idling = false;
2425
2440
  this.closeConnectSteps();
2426
2441
  if (typeof this.preCheck === 'function') {
@@ -2455,6 +2470,12 @@ export class ImapFlow extends EventEmitter {
2455
2470
  }
2456
2471
  this.closeRequests();
2457
2472
  this.closeLocks();
2473
+ 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
+ }
2478
+ }
2458
2479
  this.closeStreams();
2459
2480
  // clear socket handlers
2460
2481
  this.clearSocketHandlers();
@@ -2895,8 +2916,10 @@ export class ImapFlow extends EventEmitter {
2895
2916
  * return until IDLE is finished which means you would have to call some other command out of scope.
2896
2917
  *
2897
2918
  * Unlike auto-IDLE, a direct call does not wait for the connection to be free: a mailbox lock
2898
- * holder may idle inside its own lock, and idle() can not tell it from another caller.
2919
+ * holder may idle inside its own lock, and idle() can not tell it from another caller. Code that
2920
+ * holds no lock passes `{ whenFree: true }` to wait for the connection to be free first.
2899
2921
  *
2922
+ * @param options IDLE options
2900
2923
  * @returns `false` if IDLE failed, `undefined` otherwise
2901
2924
  *
2902
2925
  * @example
@@ -2904,7 +2927,13 @@ export class ImapFlow extends EventEmitter {
2904
2927
  *
2905
2928
  * await client.idle();
2906
2929
  */
2907
- async idle() {
2930
+ async idle(options) {
2931
+ // Re-checked on every wake, as another IDLE may have started meanwhile
2932
+ while (options?.whenFree && !this.idling && this.usable && this.connectionBusy()) {
2933
+ await new Promise((resolve, reject) => {
2934
+ this._idleWaiters.push({ resolve, reject });
2935
+ });
2936
+ }
2908
2937
  if (!this.idling) {
2909
2938
  return await this.run('IDLE', this.maxIdleTime);
2910
2939
  }
@@ -1,3 +1,3 @@
1
1
  export declare const name = "imapflow";
2
- export declare const version = "2.6.0";
2
+ export declare const version = "2.8.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.6.0";
3
+ export const version = "2.8.0";
4
4
  export const homepage = "https://imapflow.com/";
@@ -745,6 +745,16 @@ export interface MailboxOpenOptions {
745
745
  /** QRESYNC (RFC 7162): the UIDVALIDITY known from the previous session, required with `changedSince` */
746
746
  uidValidity?: bigint | number | string | undefined;
747
747
  }
748
+ export interface IdleOptions {
749
+ /**
750
+ * Wait until the connection is free before starting IDLE: no mailbox lock held or queued, no
751
+ * command running or queued, no download open. Use it from code that does not hold a mailbox
752
+ * lock, so IDLE does not land between another caller's commands. A lock holder must not set it,
753
+ * it would wait for its own release. The returned promise rejects with `NoConnection` when the
754
+ * connection closes while waiting.
755
+ */
756
+ whenFree?: boolean | undefined;
757
+ }
748
758
  export interface MailboxLockOptions extends MailboxOpenOptions {
749
759
  /**
750
760
  * Optional timeout in milliseconds to wait for the lock to be granted.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "2.6.0",
3
+ "version": "2.8.0",
4
4
  "description": "IMAP Client for Node",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/imap-flow.js",