imapflow 2.5.9 → 2.6.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,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.6.0](https://github.com/postalsys/imapflow/compare/v2.5.9...v2.6.0) (2026-10-21)
4
+
5
+
6
+ ### Features
7
+
8
+ * expose mailboxList, context, connectedAt and lastClientWrite on the client ([c054bd3](https://github.com/postalsys/imapflow/commit/c054bd33ab0068531c64d613718233d3a1eff9e6))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * emit the error event at most once per connection ([434e6eb](https://github.com/postalsys/imapflow/commit/434e6eb84021892703d02a63dc28da4a8bf086d6))
14
+ * let idle() wait for a free connection before starting IDLE ([4a3d7df](https://github.com/postalsys/imapflow/commit/4a3d7df2116321997a966e0856d7035ed2b60992))
15
+ * reject a mailbox lock when the processLocks pass handling it throws ([a207cd0](https://github.com/postalsys/imapflow/commit/a207cd093535025524ff6066aed092779b0ff569))
16
+ * reject fetch() without a selected mailbox instead of returning nothing ([78036b8](https://github.com/postalsys/imapflow/commit/78036b806b9f70e70a78fa2bc668e340e63297af))
17
+ * report UIDVALIDITY and HIGHESTMODSEQ changes found by the polling fallback ([0f4c306](https://github.com/postalsys/imapflow/commit/0f4c306259ffc9bb973d4cb54f790707c100cc94))
18
+ * require an exact LIST match before treating a failed SELECT mailbox as existing ([99a5db0](https://github.com/postalsys/imapflow/commit/99a5db09a02da721b230fd56bb7d1b240cbb2a0f))
19
+ * start IDLE right away again when idle() is called inside a mailbox lock ([018967d](https://github.com/postalsys/imapflow/commit/018967ded2d6e44801c4af8e211b10870e0f2f44))
20
+
3
21
  ## [2.5.9](https://github.com/postalsys/imapflow/compare/v2.5.8...v2.5.9) (2026-10-20)
4
22
 
5
23
 
package/README.md CHANGED
@@ -128,6 +128,15 @@ main().catch(console.error);
128
128
 
129
129
  An `ImapFlow` instance holds a single connection and cannot reconnect. Once the connection has closed, or after `connect()` has been called once, create a new instance to connect again. Calling `connect()` a second time on the same instance throws an error with the code `InstanceReused`.
130
130
 
131
+ ### Connection state
132
+
133
+ Besides `mailbox`, `usable`, `authenticated` and `capabilities`, a client exposes:
134
+
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
+ - `client.connectedAt` - when the TCP or TLS connection was established, in milliseconds since the epoch, `null` before that.
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.mailboxList` - with `verifyOnly` and `includeMailboxes`, the mailboxes listed before the connection logged out; `undefined` otherwise.
139
+
131
140
  See the [Quick Start guide](https://imapflow.com/docs/getting-started/quick-start) for more examples, including Gmail, Outlook, and Yahoo configuration.
132
141
 
133
142
  ## Documentation
@@ -25,6 +25,7 @@ export interface FetchCommandResult {
25
25
  * @param range - Message sequence number or UID range
26
26
  * @param query - Fetch query specifying which data to retrieve (e.g., flags, envelope, bodyStructure, headers, source, bodyParts)
27
27
  * @param options - Fetch options
28
- * @returns Object with message count and list, or undefined if not in SELECTED state
28
+ * @returns Object with message count and list, or undefined when the range is empty
29
+ * @throws NoMailboxSelected when not in SELECTED state (NoConnection once the connection is gone)
29
30
  */
30
31
  export default function fetch(connection: ImapFlow, range: string, query: FetchQueryObject, options?: FetchCommandOptions | undefined): Promise<FetchCommandResult | undefined>;
@@ -10,11 +10,17 @@ const errors_js_1 = require("../errors.js");
10
10
  * @param range - Message sequence number or UID range
11
11
  * @param query - Fetch query specifying which data to retrieve (e.g., flags, envelope, bodyStructure, headers, source, bodyParts)
12
12
  * @param options - Fetch options
13
- * @returns Object with message count and list, or undefined if not in SELECTED state
13
+ * @returns Object with message count and list, or undefined when the range is empty
14
+ * @throws NoMailboxSelected when not in SELECTED state (NoConnection once the connection is gone)
14
15
  */
15
16
  async function fetch(connection, range, query, options) {
16
17
  let mailbox = (0, tools_js_1.getSelectedMailbox)(connection);
17
- if (!mailbox || !range) {
18
+ if (!mailbox) {
19
+ // queued behind a CLOSE or a failed SELECT: rejecting keeps fetch() from yielding
20
+ // nothing as if the mailbox were empty
21
+ throw connection.createNoMailboxError('FETCH');
22
+ }
23
+ if (!range) {
18
24
  // nothing to do here
19
25
  return;
20
26
  }
@@ -215,6 +215,7 @@ async function pollOnce(connection, session) {
215
215
  // HIGHESTMODSEQ is filtered out again unless the server advertises CONDSTORE, so a
216
216
  // CONDSTORE session keeps mailbox.highestModseq current without asking a plain
217
217
  // server for an item it does not know.
218
+ let polled = connection.mailbox;
218
219
  let status = await connection.runInternal('STATUS', path, {
219
220
  messages: true,
220
221
  uidNext: true,
@@ -225,6 +226,12 @@ async function pollOnce(connection, session) {
225
226
  if (!status) {
226
227
  throw (0, errors_js_1.createImapError)('STATUS poll failed', 'PollFailed');
227
228
  }
229
+ // STATUS can only tell that the UIDs changed, the selected view is stale then: select
230
+ // the mailbox again, which reports it as closed and opened anew with the new state
231
+ if (polled && connection.mailbox === polled && status.uidValidity && polled.uidValidity && status.uidValidity !== polled.uidValidity) {
232
+ connection.log.info({ msg: 'UIDVALIDITY changed, selecting the mailbox again', path, cid: connection.id });
233
+ await connection.runInternal('SELECT', path, { readOnly: session.selectCommand.command === 'EXAMINE' });
234
+ }
228
235
  break;
229
236
  }
230
237
  case 'NOOP':
@@ -214,8 +214,13 @@ async function select(connection, pathInput, options) {
214
214
  let currentMailbox = connection.mailbox;
215
215
  connection.mailbox = false;
216
216
  // Emit mailboxClose if we're switching from a different mailbox.
217
- // Re-selecting the same mailbox (e.g., for resync) does not trigger close/open.
218
- if (currentMailbox && currentMailbox.path !== path) {
217
+ // Re-selecting the same mailbox (e.g., for resync) does not trigger close/open, unless
218
+ // it came back with a new UIDVALIDITY: the old view and every UID in it are gone then,
219
+ // which is reported the way a fresh mailboxOpen() of another mailbox would be.
220
+ let reopened = !currentMailbox ||
221
+ currentMailbox.path !== path ||
222
+ (!!currentMailbox.uidValidity && !!map.uidValidity && currentMailbox.uidValidity !== map.uidValidity);
223
+ if (currentMailbox && reopened) {
219
224
  (0, tools_js_1.emitSafe)(connection, 'mailboxClose', currentMailbox);
220
225
  }
221
226
  connection.mailbox = map;
@@ -223,13 +228,19 @@ async function select(connection, pathInput, options) {
223
228
  // re-issues the SELECT to detect changes on servers without IDLE support).
224
229
  connection.currentSelectCommand = selectCommand;
225
230
  connection.state = connection.states.SELECTED;
226
- if (!currentMailbox || currentMailbox.path !== path) {
231
+ if (reopened || !currentMailbox) {
227
232
  (0, tools_js_1.emitSafe)(connection, 'mailboxOpen', connection.mailbox);
228
233
  }
229
- else if (typeof map.exists === 'number' && map.exists !== currentMailbox.exists) {
234
+ else {
230
235
  // A re-SELECT of the open mailbox (the SELECT polling fallback) gets its EXISTS here
231
- // instead of in the global handler, so it is reported the same way
232
- (0, tools_js_1.emitSafe)(connection, 'exists', { path, count: map.exists, prevCount: currentMailbox.exists });
236
+ // instead of in the global handler, so it is reported the same way. Flag changes are
237
+ // not reported by SELECT, a new HIGHESTMODSEQ is the cue to resync them.
238
+ if (typeof map.exists === 'number' && map.exists !== currentMailbox.exists) {
239
+ (0, tools_js_1.emitSafe)(connection, 'exists', { path, count: map.exists, prevCount: currentMailbox.exists });
240
+ }
241
+ if (map.highestModseq && map.highestModseq !== currentMailbox.highestModseq) {
242
+ (0, tools_js_1.emitSafe)(connection, 'highestModseq', { path, highestModseq: map.highestModseq, prevHighestModseq: currentMailbox.highestModseq });
243
+ }
233
244
  }
234
245
  response.next();
235
246
  return map;
@@ -17,8 +17,13 @@ const MAILBOX_UPDATERS = {
17
17
  uidNext: (value, mailbox) => {
18
18
  mailbox.uidNext = value;
19
19
  },
20
- highestModseq: (value, mailbox) => {
21
- mailbox.highestModseq = value;
20
+ highestModseq: (value, mailbox, connection, path) => {
21
+ let prevHighestModseq = mailbox.highestModseq;
22
+ if (prevHighestModseq !== value) {
23
+ mailbox.highestModseq = value;
24
+ // the changes behind it are not reported by STATUS, the event is the cue to resync
25
+ (0, tools_js_1.emitSafe)(connection, 'highestModseq', { path, highestModseq: value, prevHighestModseq });
26
+ }
22
27
  }
23
28
  };
24
29
  /**
@@ -68,13 +73,22 @@ async function status(connection, path, query) {
68
73
  if (!list) {
69
74
  return;
70
75
  }
76
+ let updates = [];
71
77
  (0, status_fields_js_1.parseStatusList)(list, (key, value) => {
72
78
  map[key] = value;
73
- let updater = MAILBOX_UPDATERS[key];
74
- if (currentMailbox && updater) {
75
- updater(value, currentMailbox, connection, path);
79
+ if (currentMailbox && MAILBOX_UPDATERS[key]) {
80
+ updates.push([key, value]);
76
81
  }
77
82
  });
83
+ // Under a new UIDVALIDITY the counts describe a mailbox the selected view no
84
+ // longer matches, so they are not applied to it; the STATUS poll selects the
85
+ // mailbox again instead, which reports it as opened anew
86
+ if (currentMailbox && map.uidValidity && currentMailbox.uidValidity && map.uidValidity !== currentMailbox.uidValidity) {
87
+ return;
88
+ }
89
+ for (let [key, value] of updates) {
90
+ MAILBOX_UPDATERS[key](value, currentMailbox, connection, path);
91
+ }
78
92
  }
79
93
  }
80
94
  });
@@ -236,9 +236,9 @@ async function downloadMessage(client, range, part, options) {
236
236
  };
237
237
  let response = await fetchExpected(client, range, query, downloadOptions, expected);
238
238
  if (!response) {
239
- // An empty answer also comes from a connection that closed while the consumer was
240
- // paused between chunks (fetchOne() finds no mailbox then), and that is not a
241
- // missing message: a caller that retries would skip it
239
+ // An empty answer from a connection that closed while the consumer was paused
240
+ // between chunks is not a missing message: a caller that retries would skip it
241
+ // (fetchOne() itself rejects with NoConnection when it finds no mailbox then)
242
242
  if (!client.usable) {
243
243
  throw client.createNoConnectionError(false, { rejectedFrom: 'download' });
244
244
  }
@@ -51,6 +51,7 @@ export declare const ImapFlowErrorCode: {
51
51
  readonly ERR_INVALID_URL: "ERR_INVALID_URL";
52
52
  readonly UnsupportedTransportOption: "UnsupportedTransportOption";
53
53
  readonly InstanceReused: "InstanceReused";
54
+ readonly NoMailboxSelected: "NoMailboxSelected";
54
55
  };
55
56
  /** One of the {@link ImapFlowErrorCode} values */
56
57
  export type ImapFlowErrorCode = (typeof ImapFlowErrorCode)[keyof typeof ImapFlowErrorCode];
@@ -64,7 +64,9 @@ exports.ImapFlowErrorCode = {
64
64
  // the transport option
65
65
  UnsupportedTransportOption: 'UnsupportedTransportOption',
66
66
  // API misuse
67
- InstanceReused: 'InstanceReused'
67
+ InstanceReused: 'InstanceReused',
68
+ // a message command (FETCH) was called while no mailbox was selected
69
+ NoMailboxSelected: 'NoMailboxSelected'
68
70
  };
69
71
  /**
70
72
  * Error subclass thrown when IMAP authentication fails.
@@ -134,6 +134,23 @@ export declare class ImapFlow extends EventEmitter {
134
134
  * Is current mailbox idling (`true`) or not (`false`)
135
135
  */
136
136
  idling: boolean;
137
+ /**
138
+ * Data of the application's own, kept with the client and never read by ImapFlow. Starts as
139
+ * the `context` option; cast it to the type the application stores there
140
+ */
141
+ context: unknown;
142
+ /**
143
+ * When the TCP (or TLS) connection to the server was established, in milliseconds since the
144
+ * epoch, `null` before that. Kept after the connection closed
145
+ */
146
+ connectedAt: number | null;
147
+ /**
148
+ * When the client last wrote to the server (a command, a literal, the DONE that ends IDLE, an
149
+ * authentication response), in milliseconds since the epoch, `null` before the first write.
150
+ * Servers drop sessions that have not sent anything for a while, the IDLE ones included
151
+ * (RFC 9051 allows 30 minutes), and this is what their timer measures
152
+ */
153
+ lastClientWrite: number | null;
137
154
  /** Whether log entries are also emitted as 'log' events, see the `emitLogs` option */
138
155
  emitLogs: boolean;
139
156
  /** Hash algorithm for the fallback message id, see `ImapFlowOptions.idHashAlgorithm` */
@@ -148,6 +165,16 @@ export declare class ImapFlow extends EventEmitter {
148
165
  byeReason: string | undefined;
149
166
  /** Negotiated TLS session details, `false` for a cleartext connection */
150
167
  tls: TlsInfo | false | undefined;
168
+ /**
169
+ * The mailboxes a `verifyOnly` connection with `includeMailboxes` listed before it logged out,
170
+ * `undefined` otherwise
171
+ *
172
+ * @example
173
+ * let client = new ImapFlow({ ..., verifyOnly: true, includeMailboxes: true });
174
+ * await client.connect();
175
+ * console.log(client.mailboxList.map(mailbox => mailbox.path));
176
+ */
177
+ get mailboxList(): ListResponse[] | undefined;
151
178
  /**
152
179
  * Creates a client for one IMAP connection. Nothing is sent before `connect()` is called
153
180
  */
@@ -344,6 +371,9 @@ export declare class ImapFlow extends EventEmitter {
344
371
  * otherwise IDLE is started by default on connection inactivity. NB! If `idle()` is called manually then it does not
345
372
  * return until IDLE is finished which means you would have to call some other command out of scope.
346
373
  *
374
+ * 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.
376
+ *
347
377
  * @returns `false` if IDLE failed, `undefined` otherwise
348
378
  *
349
379
  * @example
@@ -505,6 +535,9 @@ export declare class ImapFlow extends EventEmitter {
505
535
  * @param query Fetch query
506
536
  * @param options Fetch options
507
537
  * @yields Message data object
538
+ * @throws An error with the code `NoMailboxSelected` when no mailbox is selected (for example
539
+ * after `mailboxClose()` or a failed `mailboxOpen()`), or `NoConnection` once the connection
540
+ * is gone. Earlier releases yielded nothing in that case, as for an empty mailbox
508
541
  *
509
542
  * @example
510
543
  * let mailbox = await client.mailboxOpen('INBOX');
@@ -526,6 +559,7 @@ export declare class ImapFlow extends EventEmitter {
526
559
  * @param query Fetch query
527
560
  * @param options Fetch options
528
561
  * @returns Array of Message data object
562
+ * @throws An error with the code `NoMailboxSelected` when no mailbox is selected, see fetch()
529
563
  *
530
564
  * @example
531
565
  * let mailbox = await client.mailboxOpen('INBOX');
@@ -542,7 +576,9 @@ export declare class ImapFlow extends EventEmitter {
542
576
  * @param seq Single UID or sequence number of the message to fetch for
543
577
  * @param query Fetch query
544
578
  * @param options Fetch options
545
- * @returns Message data object
579
+ * @returns Message data object, `false` when the message was not found
580
+ * @throws An error with the code `NoMailboxSelected` when no mailbox is selected, see fetch().
581
+ * Earlier releases resolved `undefined` then
546
582
  *
547
583
  * @example
548
584
  * let mailbox = await client.mailboxOpen('INBOX');
@@ -550,7 +586,7 @@ export declare class ImapFlow extends EventEmitter {
550
586
  * let lastMsg = await client.fetchOne('*', {uid: true})
551
587
  * console.log(lastMsg.uid);
552
588
  */
553
- fetchOne(seq: SequenceString, query: FetchQueryObject, options?: FetchOptions | undefined): Promise<FetchMessageObject | false | undefined>;
589
+ fetchOne(seq: SequenceString, query: FetchQueryObject, options?: FetchOptions | undefined): Promise<FetchMessageObject | false>;
554
590
  /**
555
591
  * Download either full rfc822 formatted message or a specific bodystructure part as a Stream.
556
592
  * Bodystructure parts are decoded so the resulting stream is a binary file. Text content
@@ -211,6 +211,18 @@ class ImapFlow extends node_events_1.EventEmitter {
211
211
  * Current module version as a static class property
212
212
  */
213
213
  static { this.version = packageInfo.version; }
214
+ /**
215
+ * The mailboxes a `verifyOnly` connection with `includeMailboxes` listed before it logged out,
216
+ * `undefined` otherwise
217
+ *
218
+ * @example
219
+ * let client = new ImapFlow({ ..., verifyOnly: true, includeMailboxes: true });
220
+ * await client.connect();
221
+ * console.log(client.mailboxList.map(mailbox => mailbox.path));
222
+ */
223
+ get mailboxList() {
224
+ return this._mailboxList;
225
+ }
214
226
  /**
215
227
  * Creates a client for one IMAP connection. Nothing is sent before `connect()` is called
216
228
  */
@@ -262,6 +274,7 @@ class ImapFlow extends node_events_1.EventEmitter {
262
274
  this._throttleWaits = new Set();
263
275
  this._upgradeReject = null;
264
276
  this.isClosed = false;
277
+ this._errorReported = false;
265
278
  this.states = states;
266
279
  this.state = this.states.NOT_AUTHENTICATED;
267
280
  this.lockCounter = 0;
@@ -286,6 +299,9 @@ class ImapFlow extends node_events_1.EventEmitter {
286
299
  this.mailbox = false;
287
300
  this.currentSelectCommand = false;
288
301
  this.idling = false;
302
+ this.context = this.options.context;
303
+ this.connectedAt = null;
304
+ this.lastClientWrite = null;
289
305
  this.emitLogs = !!this.options.emitLogs;
290
306
  this.lo = 0;
291
307
  this.untaggedHandlers = {};
@@ -331,6 +347,15 @@ class ImapFlow extends node_events_1.EventEmitter {
331
347
  return;
332
348
  }
333
349
  err._connId = err._connId || this.id;
350
+ // One error per connection: emitError() closes the connection, and until the deferred
351
+ // close() runs a second socket, deflate or parser error may still arrive. Emitting it as
352
+ // well would reach a consumer that removed its listener after the first one, or one that
353
+ // only awaited connect(), as an unhandled 'error' that throws.
354
+ if (this._errorReported) {
355
+ this.log.debug({ msg: 'Suppressed error after the connection already reported one', err, cid: this.id });
356
+ this.closeAfter();
357
+ return;
358
+ }
334
359
  // During a STARTTLS handshake the upgrade owns the single error path (its settle()
335
360
  // helper). Route the error there so a streamer-originated failure is surfaced with its
336
361
  // real code (instead of a generic ClosedAfterConnect*) and cannot hang a verifyOnly
@@ -341,6 +366,7 @@ class ImapFlow extends node_events_1.EventEmitter {
341
366
  this._upgradeReject = null;
342
367
  if (typeof reject === 'function') {
343
368
  // settle() clears the upgrade timer and flags, and closes the connection
369
+ this._errorReported = true;
344
370
  reject(err);
345
371
  return;
346
372
  }
@@ -355,10 +381,12 @@ class ImapFlow extends node_events_1.EventEmitter {
355
381
  let reject = this.initialReject;
356
382
  this.initialResolve = false;
357
383
  this.initialReject = false;
384
+ this._errorReported = true;
358
385
  this.closeAfter();
359
386
  reject(err);
360
387
  return;
361
388
  }
389
+ this._errorReported = true;
362
390
  this.closeAfter();
363
391
  this.emit('error', err);
364
392
  }
@@ -412,6 +440,7 @@ class ImapFlow extends node_events_1.EventEmitter {
412
440
  else {
413
441
  return false;
414
442
  }
443
+ this.lastClientWrite = Date.now();
415
444
  for (let data of frames) {
416
445
  if (this.logRaw) {
417
446
  // Client frames of an authentication exchange carry credentials: the LOGIN
@@ -1302,7 +1331,9 @@ class ImapFlow extends node_events_1.EventEmitter {
1302
1331
  }
1303
1332
  });
1304
1333
  const socket = this.socket;
1305
- socket.once('error', this._socketError);
1334
+ // on(), not once(): with compression the deflate and writeSocket errors are forwarded
1335
+ // onto this socket, and a second one before close() must not find it without a listener
1336
+ socket.on('error', this._socketError);
1306
1337
  socket.once('close', this._socketClose);
1307
1338
  socket.once('end', this._socketEnd);
1308
1339
  socket.on('tlsClientError', this._socketError);
@@ -2281,6 +2312,7 @@ class ImapFlow extends node_events_1.EventEmitter {
2281
2312
  }
2282
2313
  try {
2283
2314
  (0, tools_js_1.clearTimer)(this.connectTimeout);
2315
+ this.connectedAt = Date.now();
2284
2316
  // ImapFlow now owns the socket; drop the proxy's early error handler
2285
2317
  // (its "before connection setup" message no longer applies).
2286
2318
  (0, proxy_connection_js_1.detachEarlyErrorHandler)(socket);
@@ -2406,6 +2438,15 @@ class ImapFlow extends node_events_1.EventEmitter {
2406
2438
  }
2407
2439
  return error;
2408
2440
  }
2441
+ // What a message command called without a selected mailbox rejects with: NoMailboxSelected,
2442
+ // or NoConnection once the connection is gone, as that is why the mailbox is missing then
2443
+ /** @internal */
2444
+ createNoMailboxError(command) {
2445
+ if (!this.usable) {
2446
+ return this.createNoConnectionError(this.byeReason, { rejectedFrom: 'noMailbox', command });
2447
+ }
2448
+ return (0, errors_js_1.createImapError)('No mailbox is selected', 'NoMailboxSelected', { command });
2449
+ }
2409
2450
  /**
2410
2451
  * Closes TCP connection without notifying the server.
2411
2452
  *
@@ -2846,8 +2887,14 @@ class ImapFlow extends node_events_1.EventEmitter {
2846
2887
  // LIST. This sets mailboxMissing on the error so the caller can distinguish
2847
2888
  // "doesn't exist" from other failures, for getMailboxLock() callers as well.
2848
2889
  try {
2849
- let folders = await this.run('LIST', '', (0, tools_js_1.normalizePath)(this, path), { listOnly: true });
2850
- if (!folders || !folders.length) {
2890
+ let normalized = (0, tools_js_1.normalizePath)(this, path);
2891
+ let folders = await this.run('LIST', '', normalized, { listOnly: true });
2892
+ // The path is the LIST pattern, so a % or * in a name also matches other
2893
+ // mailboxes, and an entry marked \NonExistent (RFC 5258) does not exist either
2894
+ let exists = Array.isArray(folders) &&
2895
+ folders.some(entry => (0, tools_js_1.comparePaths)(this, entry.path, normalized) &&
2896
+ ![...(entry.flags || [])].some(flag => typeof flag === 'string' && flag.toUpperCase() === '\\NONEXISTENT'));
2897
+ if (!exists) {
2851
2898
  err.mailboxMissing = true;
2852
2899
  }
2853
2900
  }
@@ -2890,6 +2937,9 @@ class ImapFlow extends node_events_1.EventEmitter {
2890
2937
  * otherwise IDLE is started by default on connection inactivity. NB! If `idle()` is called manually then it does not
2891
2938
  * return until IDLE is finished which means you would have to call some other command out of scope.
2892
2939
  *
2940
+ * 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.
2942
+ *
2893
2943
  * @returns `false` if IDLE failed, `undefined` otherwise
2894
2944
  *
2895
2945
  * @example
@@ -3143,6 +3193,9 @@ class ImapFlow extends node_events_1.EventEmitter {
3143
3193
  * @param query Fetch query
3144
3194
  * @param options Fetch options
3145
3195
  * @yields Message data object
3196
+ * @throws An error with the code `NoMailboxSelected` when no mailbox is selected (for example
3197
+ * after `mailboxClose()` or a failed `mailboxOpen()`), or `NoConnection` once the connection
3198
+ * is gone. Earlier releases yielded nothing in that case, as for an empty mailbox
3146
3199
  *
3147
3200
  * @example
3148
3201
  * let mailbox = await client.mailboxOpen('INBOX');
@@ -3156,8 +3209,8 @@ class ImapFlow extends node_events_1.EventEmitter {
3156
3209
  async *fetch(range, query, options) {
3157
3210
  options = options || {};
3158
3211
  if (!this.mailbox) {
3159
- // no mailbox selected, nothing to do
3160
- return;
3212
+ // an empty result would look like an empty mailbox, for example right after close()
3213
+ throw this.createNoMailboxError('FETCH');
3161
3214
  }
3162
3215
  let resolved = await this.resolveRange(range, options);
3163
3216
  if (!resolved) {
@@ -3267,6 +3320,7 @@ class ImapFlow extends node_events_1.EventEmitter {
3267
3320
  * @param query Fetch query
3268
3321
  * @param options Fetch options
3269
3322
  * @returns Array of Message data object
3323
+ * @throws An error with the code `NoMailboxSelected` when no mailbox is selected, see fetch()
3270
3324
  *
3271
3325
  * @example
3272
3326
  * let mailbox = await client.mailboxOpen('INBOX');
@@ -3290,7 +3344,9 @@ class ImapFlow extends node_events_1.EventEmitter {
3290
3344
  * @param seq Single UID or sequence number of the message to fetch for
3291
3345
  * @param query Fetch query
3292
3346
  * @param options Fetch options
3293
- * @returns Message data object
3347
+ * @returns Message data object, `false` when the message was not found
3348
+ * @throws An error with the code `NoMailboxSelected` when no mailbox is selected, see fetch().
3349
+ * Earlier releases resolved `undefined` then
3294
3350
  *
3295
3351
  * @example
3296
3352
  * let mailbox = await client.mailboxOpen('INBOX');
@@ -3300,8 +3356,9 @@ class ImapFlow extends node_events_1.EventEmitter {
3300
3356
  */
3301
3357
  async fetchOne(seq, query, options) {
3302
3358
  if (!this.mailbox) {
3303
- // no mailbox selected, nothing to do
3304
- return;
3359
+ // undefined used to be returned here, indistinguishable from a missing message for a
3360
+ // caller that only tests the result
3361
+ throw this.createNoMailboxError('FETCH');
3305
3362
  }
3306
3363
  if (seq === '*') {
3307
3364
  if (!this.mailbox.exists) {
@@ -3479,6 +3536,9 @@ class ImapFlow extends node_events_1.EventEmitter {
3479
3536
  return;
3480
3537
  }
3481
3538
  this.processingLock = true;
3539
+ // The lock taken off the queue and not settled yet. A pass that throws out of its loop
3540
+ // rejects it, its caller would wait forever otherwise: it is in the queue no longer.
3541
+ let unsettled = null;
3482
3542
  try {
3483
3543
  // Process all locks in queue until empty
3484
3544
  let processedCount = 0;
@@ -3495,6 +3555,7 @@ class ImapFlow extends node_events_1.EventEmitter {
3495
3555
  await new Promise(resolve => setImmediate(resolve));
3496
3556
  }
3497
3557
  const lock = this.locks.shift();
3558
+ unsettled = lock;
3498
3559
  const { resolve, reject, path, options, lockId } = lock;
3499
3560
  // From here on the grant/reject path owns the outcome; the acquire
3500
3561
  // timer must not race with resolution.
@@ -3560,6 +3621,7 @@ class ImapFlow extends node_events_1.EventEmitter {
3560
3621
  };
3561
3622
  if (!this.usable || !this.socket || this.socket.destroyed) {
3562
3623
  this.log.trace({ msg: 'Failed to acquire mailbox lock', path, lockId, idling: this.idling });
3624
+ unsettled = null;
3563
3625
  reject(this.createNoConnectionError(false, { rejectedFrom: 'mailboxLock', path }));
3564
3626
  continue; // Process next lock in queue
3565
3627
  }
@@ -3572,6 +3634,7 @@ class ImapFlow extends node_events_1.EventEmitter {
3572
3634
  this.currentLock = lock;
3573
3635
  armHeldTimer();
3574
3636
  this.autoidle();
3637
+ unsettled = null;
3575
3638
  resolve({ path, release });
3576
3639
  };
3577
3640
  if (this.mailbox && this.mailbox.path === path && !!this.mailbox.readOnly === !!options.readOnly) {
@@ -3614,23 +3677,35 @@ class ImapFlow extends node_events_1.EventEmitter {
3614
3677
  ...(options.description && { description: options.description }),
3615
3678
  err
3616
3679
  });
3680
+ unsettled = null;
3617
3681
  reject(err);
3618
3682
  // Continue to next lock in queue
3619
3683
  }
3620
3684
  }
3621
3685
  }
3686
+ catch (err) {
3687
+ if (unsettled) {
3688
+ let lock = unsettled;
3689
+ unsettled = null;
3690
+ // granted but not handed out (the grant itself failed): nobody can release it
3691
+ if (this.currentLock === lock) {
3692
+ (0, tools_js_1.clearTimer)(lock.heldWarnTimer);
3693
+ lock.heldWarnTimer = null;
3694
+ this.currentLock = false;
3695
+ }
3696
+ lock.reject(err);
3697
+ }
3698
+ throw err;
3699
+ }
3622
3700
  finally {
3623
3701
  this.processingLock = false;
3624
- // New locks may have been queued while we were processing (e.g.,
3625
- // a lock that failed immediately and the next getMailboxLock call
3626
- // arrived before we finished). Schedule another run if needed.
3627
- /* c8 ignore start */ // requires a lock to be enqueued during an in-flight processLocks pass; not reproducible deterministically
3702
+ // A pass that ended with locks queued and none held (it threw out of its loop) would
3703
+ // leave them waiting with nothing to start another pass. Schedule one.
3628
3704
  if (this.locks.length && !this.currentLock) {
3629
3705
  setImmediate(() => {
3630
3706
  this.processLocks().catch(err => this.log.error({ err, cid: this.id }));
3631
3707
  });
3632
3708
  }
3633
- /* c8 ignore stop */
3634
3709
  }
3635
3710
  }
3636
3711
  /**
@@ -3825,7 +3900,8 @@ exports.ImapFlow = ImapFlow;
3825
3900
  */
3826
3901
  /**
3827
3902
  * Error event. In most cases getting an error event also means that connection is closed
3828
- * and pending operations should return with a failure.
3903
+ * and pending operations should return with a failure. Emitted at most once per connection,
3904
+ * later errors while the connection is closing are only logged.
3829
3905
  *
3830
3906
  * @event ImapFlow#error
3831
3907
  * @example
@@ -1,3 +1,3 @@
1
1
  export declare const name = "imapflow";
2
- export declare const version = "2.5.9";
2
+ export declare const version = "2.6.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.5.9";
6
+ exports.version = "2.6.0";
7
7
  exports.homepage = "https://imapflow.com/";