imapflow 2.7.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,12 @@
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
+
3
10
  ## [2.7.0](https://github.com/postalsys/imapflow/compare/v2.6.0...v2.7.0) (2026-10-21)
4
11
 
5
12
 
@@ -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';
@@ -378,8 +378,10 @@ export declare class ImapFlow extends EventEmitter {
378
378
  * return until IDLE is finished which means you would have to call some other command out of scope.
379
379
  *
380
380
  * Unlike auto-IDLE, a direct call does not wait for the connection to be free: a mailbox lock
381
- * 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.
382
383
  *
384
+ * @param options IDLE options
383
385
  * @returns `false` if IDLE failed, `undefined` otherwise
384
386
  *
385
387
  * @example
@@ -387,7 +389,7 @@ export declare class ImapFlow extends EventEmitter {
387
389
  *
388
390
  * await client.idle();
389
391
  */
390
- idle(): Promise<boolean | undefined>;
392
+ idle(options?: IdleOptions | undefined): Promise<boolean | undefined>;
391
393
  /**
392
394
  * Sets flags for a message or message range
393
395
  *
@@ -302,6 +302,7 @@ 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 = [];
305
306
  this.idlingAtClose = false;
306
307
  this.emitLogs = !!this.options.emitLogs;
307
308
  this.lo = 0;
@@ -2187,6 +2188,14 @@ class ImapFlow extends node_events_1.EventEmitter {
2187
2188
  connectionBusy() {
2188
2189
  return !!(this.currentLock || this.locks.length || this.currentRequest || this.requestQueue.length || this._openDownloads);
2189
2190
  }
2191
+ /** @internal */
2192
+ wakeIdleWaiters() {
2193
+ if (this._idleWaiters.length) {
2194
+ for (let { resolve } of this._idleWaiters.splice(0)) {
2195
+ resolve();
2196
+ }
2197
+ }
2198
+ }
2190
2199
  // Timer process-liveness policy: connection establishment and greeting deadlines keep the
2191
2200
  // process alive, because a caller is waiting on connect() to settle. Background timers
2192
2201
  // (auto-IDLE, IDLE restart, fallback polling, throttle back-off, the held-lock diagnostic) are
@@ -2194,6 +2203,9 @@ class ImapFlow extends node_events_1.EventEmitter {
2194
2203
  // explicitly on close().
2195
2204
  /** @internal */
2196
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();
2197
2209
  (0, tools_js_1.clearTimer)(this.idleStartTimer);
2198
2210
  if (this.options.disableAutoIdle || !this.usable || this.state !== this.states.SELECTED) {
2199
2211
  return;
@@ -2501,6 +2513,12 @@ class ImapFlow extends node_events_1.EventEmitter {
2501
2513
  }
2502
2514
  this.closeRequests();
2503
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
+ }
2504
2522
  this.closeStreams();
2505
2523
  // clear socket handlers
2506
2524
  this.clearSocketHandlers();
@@ -2941,8 +2959,10 @@ class ImapFlow extends node_events_1.EventEmitter {
2941
2959
  * return until IDLE is finished which means you would have to call some other command out of scope.
2942
2960
  *
2943
2961
  * Unlike auto-IDLE, a direct call does not wait for the connection to be free: a mailbox lock
2944
- * 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.
2945
2964
  *
2965
+ * @param options IDLE options
2946
2966
  * @returns `false` if IDLE failed, `undefined` otherwise
2947
2967
  *
2948
2968
  * @example
@@ -2950,7 +2970,13 @@ class ImapFlow extends node_events_1.EventEmitter {
2950
2970
  *
2951
2971
  * await client.idle();
2952
2972
  */
2953
- 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
+ }
2954
2980
  if (!this.idling) {
2955
2981
  return await this.run('IDLE', this.maxIdleTime);
2956
2982
  }
@@ -1,3 +1,3 @@
1
1
  export declare const name = "imapflow";
2
- export declare const version = "2.7.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.7.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';
@@ -378,8 +378,10 @@ export declare class ImapFlow extends EventEmitter {
378
378
  * return until IDLE is finished which means you would have to call some other command out of scope.
379
379
  *
380
380
  * Unlike auto-IDLE, a direct call does not wait for the connection to be free: a mailbox lock
381
- * 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.
382
383
  *
384
+ * @param options IDLE options
383
385
  * @returns `false` if IDLE failed, `undefined` otherwise
384
386
  *
385
387
  * @example
@@ -387,7 +389,7 @@ export declare class ImapFlow extends EventEmitter {
387
389
  *
388
390
  * await client.idle();
389
391
  */
390
- idle(): Promise<boolean | undefined>;
392
+ idle(options?: IdleOptions | undefined): Promise<boolean | undefined>;
391
393
  /**
392
394
  * Sets flags for a message or message range
393
395
  *
@@ -259,6 +259,7 @@ 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 = [];
262
263
  this.idlingAtClose = false;
263
264
  this.emitLogs = !!this.options.emitLogs;
264
265
  this.lo = 0;
@@ -2144,6 +2145,14 @@ export class ImapFlow extends EventEmitter {
2144
2145
  connectionBusy() {
2145
2146
  return !!(this.currentLock || this.locks.length || this.currentRequest || this.requestQueue.length || this._openDownloads);
2146
2147
  }
2148
+ /** @internal */
2149
+ wakeIdleWaiters() {
2150
+ if (this._idleWaiters.length) {
2151
+ for (let { resolve } of this._idleWaiters.splice(0)) {
2152
+ resolve();
2153
+ }
2154
+ }
2155
+ }
2147
2156
  // Timer process-liveness policy: connection establishment and greeting deadlines keep the
2148
2157
  // process alive, because a caller is waiting on connect() to settle. Background timers
2149
2158
  // (auto-IDLE, IDLE restart, fallback polling, throttle back-off, the held-lock diagnostic) are
@@ -2151,6 +2160,9 @@ export class ImapFlow extends EventEmitter {
2151
2160
  // explicitly on close().
2152
2161
  /** @internal */
2153
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();
2154
2166
  clearTimer(this.idleStartTimer);
2155
2167
  if (this.options.disableAutoIdle || !this.usable || this.state !== this.states.SELECTED) {
2156
2168
  return;
@@ -2458,6 +2470,12 @@ export class ImapFlow extends EventEmitter {
2458
2470
  }
2459
2471
  this.closeRequests();
2460
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
+ }
2461
2479
  this.closeStreams();
2462
2480
  // clear socket handlers
2463
2481
  this.clearSocketHandlers();
@@ -2898,8 +2916,10 @@ export class ImapFlow extends EventEmitter {
2898
2916
  * return until IDLE is finished which means you would have to call some other command out of scope.
2899
2917
  *
2900
2918
  * Unlike auto-IDLE, a direct call does not wait for the connection to be free: a mailbox lock
2901
- * 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.
2902
2921
  *
2922
+ * @param options IDLE options
2903
2923
  * @returns `false` if IDLE failed, `undefined` otherwise
2904
2924
  *
2905
2925
  * @example
@@ -2907,7 +2927,13 @@ export class ImapFlow extends EventEmitter {
2907
2927
  *
2908
2928
  * await client.idle();
2909
2929
  */
2910
- 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
+ }
2911
2937
  if (!this.idling) {
2912
2938
  return await this.run('IDLE', this.maxIdleTime);
2913
2939
  }
@@ -1,3 +1,3 @@
1
1
  export declare const name = "imapflow";
2
- export declare const version = "2.7.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.7.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.7.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",