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.
@@ -76,8 +76,13 @@ export interface ImapFlowOptions {
76
76
  emitLogs?: boolean | undefined;
77
77
  /** If true, then logs out automatically after successful authentication */
78
78
  verifyOnly?: boolean | undefined;
79
- /** If true and verifyOnly is set, lists mailboxes */
79
+ /** If true and verifyOnly is set, lists mailboxes. The listing is in `client.mailboxList` once `connect()` resolved */
80
80
  includeMailboxes?: boolean | undefined;
81
+ /**
82
+ * Data of the application's own to keep with the client, such as the account the connection
83
+ * belongs to. ImapFlow never reads it, it is the initial value of `client.context`
84
+ */
85
+ context?: unknown;
81
86
  /**
82
87
  * Proxy URL. Supports HTTP CONNECT (http:, https:) and SOCKS (socks:, socks4:, socks4a:,
83
88
  * socks5:) proxies. IPv6 proxy endpoints are given in URL form, e.g. `socks5://[2001:db8::1]:1080`.
@@ -789,6 +794,22 @@ export interface ExistsEvent {
789
794
  /** Message count before this update */
790
795
  prevCount: number;
791
796
  }
797
+ /**
798
+ * Payload of the `highestModseq` event: the HIGHESTMODSEQ of the selected mailbox changed (RFC
799
+ * 7162), so flags or other message metadata changed on the server. Emitted when a poll on a
800
+ * server without IDLE (`missingIdleCommand` STATUS or SELECT) or a STATUS or re-SELECT of the
801
+ * open mailbox reports a new value, which is when the changes themselves are not delivered as
802
+ * `flags` events. A consumer resynchronizes with `fetch(range, { flags: true }, { changedSince:
803
+ * prevHighestModseq })`.
804
+ */
805
+ export interface HighestModseqEvent {
806
+ /** Mailbox path */
807
+ path: string;
808
+ /** The new HIGHESTMODSEQ value */
809
+ highestModseq: bigint;
810
+ /** The value before this update, undefined when none was known */
811
+ prevHighestModseq?: bigint | undefined;
812
+ }
792
813
  export interface FlagsEvent {
793
814
  /** Mailbox path */
794
815
  path: string;
@@ -888,7 +909,10 @@ export interface TlsInfo extends CipherNameAndProtocol {
888
909
  export interface ImapFlowEvents {
889
910
  /** Connection close event */
890
911
  close: [];
891
- /** Error event */
912
+ /**
913
+ * Error event. Emitted at most once per connection, the connection closes after it. An error
914
+ * while `connect()` is pending rejects `connect()` instead and is not emitted
915
+ */
892
916
  error: [error: Error];
893
917
  /** Message count in currently opened mailbox changed */
894
918
  exists: [data: ExistsEvent];
@@ -896,7 +920,13 @@ export interface ImapFlowEvents {
896
920
  expunge: [data: ExpungeEvent];
897
921
  /** Flags were updated for a message */
898
922
  flags: [data: FlagsEvent];
899
- /** Mailbox was opened */
923
+ /** The HIGHESTMODSEQ of the selected mailbox changed without the changes being reported, see {@link HighestModseqEvent} */
924
+ highestModseq: [data: HighestModseqEvent];
925
+ /**
926
+ * Mailbox was opened. Also emitted, after `mailboxClose` for the old mailbox object, when the
927
+ * open mailbox turned out to have a new UIDVALIDITY (a re-SELECT, or a STATUS poll on a server
928
+ * without IDLE, which then selects the mailbox again): every cached UID of it is invalid
929
+ */
900
930
  mailboxOpen: [mailbox: MailboxObject];
901
931
  /** Mailbox was closed, either explicitly or because the connection went away while a mailbox was still selected */
902
932
  mailboxClose: [mailbox: MailboxObject];
@@ -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>;
@@ -7,11 +7,17 @@ import { createImapError } from '../errors.js';
7
7
  * @param range - Message sequence number or UID range
8
8
  * @param query - Fetch query specifying which data to retrieve (e.g., flags, envelope, bodyStructure, headers, source, bodyParts)
9
9
  * @param options - Fetch options
10
- * @returns Object with message count and list, or undefined if not in SELECTED state
10
+ * @returns Object with message count and list, or undefined when the range is empty
11
+ * @throws NoMailboxSelected when not in SELECTED state (NoConnection once the connection is gone)
11
12
  */
12
13
  export default async function fetch(connection, range, query, options) {
13
14
  let mailbox = getSelectedMailbox(connection);
14
- if (!mailbox || !range) {
15
+ if (!mailbox) {
16
+ // queued behind a CLOSE or a failed SELECT: rejecting keeps fetch() from yielding
17
+ // nothing as if the mailbox were empty
18
+ throw connection.createNoMailboxError('FETCH');
19
+ }
20
+ if (!range) {
15
21
  // nothing to do here
16
22
  return;
17
23
  }
@@ -212,6 +212,7 @@ async function pollOnce(connection, session) {
212
212
  // HIGHESTMODSEQ is filtered out again unless the server advertises CONDSTORE, so a
213
213
  // CONDSTORE session keeps mailbox.highestModseq current without asking a plain
214
214
  // server for an item it does not know.
215
+ let polled = connection.mailbox;
215
216
  let status = await connection.runInternal('STATUS', path, {
216
217
  messages: true,
217
218
  uidNext: true,
@@ -222,6 +223,12 @@ async function pollOnce(connection, session) {
222
223
  if (!status) {
223
224
  throw createImapError('STATUS poll failed', 'PollFailed');
224
225
  }
226
+ // STATUS can only tell that the UIDs changed, the selected view is stale then: select
227
+ // the mailbox again, which reports it as closed and opened anew with the new state
228
+ if (polled && connection.mailbox === polled && status.uidValidity && polled.uidValidity && status.uidValidity !== polled.uidValidity) {
229
+ connection.log.info({ msg: 'UIDVALIDITY changed, selecting the mailbox again', path, cid: connection.id });
230
+ await connection.runInternal('SELECT', path, { readOnly: session.selectCommand.command === 'EXAMINE' });
231
+ }
225
232
  break;
226
233
  }
227
234
  case 'NOOP':
@@ -211,8 +211,13 @@ export default async function select(connection, pathInput, options) {
211
211
  let currentMailbox = connection.mailbox;
212
212
  connection.mailbox = false;
213
213
  // Emit mailboxClose if we're switching from a different mailbox.
214
- // Re-selecting the same mailbox (e.g., for resync) does not trigger close/open.
215
- if (currentMailbox && currentMailbox.path !== path) {
214
+ // Re-selecting the same mailbox (e.g., for resync) does not trigger close/open, unless
215
+ // it came back with a new UIDVALIDITY: the old view and every UID in it are gone then,
216
+ // which is reported the way a fresh mailboxOpen() of another mailbox would be.
217
+ let reopened = !currentMailbox ||
218
+ currentMailbox.path !== path ||
219
+ (!!currentMailbox.uidValidity && !!map.uidValidity && currentMailbox.uidValidity !== map.uidValidity);
220
+ if (currentMailbox && reopened) {
216
221
  emitSafe(connection, 'mailboxClose', currentMailbox);
217
222
  }
218
223
  connection.mailbox = map;
@@ -220,13 +225,19 @@ export default async function select(connection, pathInput, options) {
220
225
  // re-issues the SELECT to detect changes on servers without IDLE support).
221
226
  connection.currentSelectCommand = selectCommand;
222
227
  connection.state = connection.states.SELECTED;
223
- if (!currentMailbox || currentMailbox.path !== path) {
228
+ if (reopened || !currentMailbox) {
224
229
  emitSafe(connection, 'mailboxOpen', connection.mailbox);
225
230
  }
226
- else if (typeof map.exists === 'number' && map.exists !== currentMailbox.exists) {
231
+ else {
227
232
  // A re-SELECT of the open mailbox (the SELECT polling fallback) gets its EXISTS here
228
- // instead of in the global handler, so it is reported the same way
229
- emitSafe(connection, 'exists', { path, count: map.exists, prevCount: currentMailbox.exists });
233
+ // instead of in the global handler, so it is reported the same way. Flag changes are
234
+ // not reported by SELECT, a new HIGHESTMODSEQ is the cue to resync them.
235
+ if (typeof map.exists === 'number' && map.exists !== currentMailbox.exists) {
236
+ emitSafe(connection, 'exists', { path, count: map.exists, prevCount: currentMailbox.exists });
237
+ }
238
+ if (map.highestModseq && map.highestModseq !== currentMailbox.highestModseq) {
239
+ emitSafe(connection, 'highestModseq', { path, highestModseq: map.highestModseq, prevHighestModseq: currentMailbox.highestModseq });
240
+ }
230
241
  }
231
242
  response.next();
232
243
  return map;
@@ -14,8 +14,13 @@ const MAILBOX_UPDATERS = {
14
14
  uidNext: (value, mailbox) => {
15
15
  mailbox.uidNext = value;
16
16
  },
17
- highestModseq: (value, mailbox) => {
18
- mailbox.highestModseq = value;
17
+ highestModseq: (value, mailbox, connection, path) => {
18
+ let prevHighestModseq = mailbox.highestModseq;
19
+ if (prevHighestModseq !== value) {
20
+ mailbox.highestModseq = value;
21
+ // the changes behind it are not reported by STATUS, the event is the cue to resync
22
+ emitSafe(connection, 'highestModseq', { path, highestModseq: value, prevHighestModseq });
23
+ }
19
24
  }
20
25
  };
21
26
  /**
@@ -65,13 +70,22 @@ export default async function status(connection, path, query) {
65
70
  if (!list) {
66
71
  return;
67
72
  }
73
+ let updates = [];
68
74
  parseStatusList(list, (key, value) => {
69
75
  map[key] = value;
70
- let updater = MAILBOX_UPDATERS[key];
71
- if (currentMailbox && updater) {
72
- updater(value, currentMailbox, connection, path);
76
+ if (currentMailbox && MAILBOX_UPDATERS[key]) {
77
+ updates.push([key, value]);
73
78
  }
74
79
  });
80
+ // Under a new UIDVALIDITY the counts describe a mailbox the selected view no
81
+ // longer matches, so they are not applied to it; the STATUS poll selects the
82
+ // mailbox again instead, which reports it as opened anew
83
+ if (currentMailbox && map.uidValidity && currentMailbox.uidValidity && map.uidValidity !== currentMailbox.uidValidity) {
84
+ return;
85
+ }
86
+ for (let [key, value] of updates) {
87
+ MAILBOX_UPDATERS[key](value, currentMailbox, connection, path);
88
+ }
75
89
  }
76
90
  }
77
91
  });
@@ -229,9 +229,9 @@ export async function downloadMessage(client, range, part, options) {
229
229
  };
230
230
  let response = await fetchExpected(client, range, query, downloadOptions, expected);
231
231
  if (!response) {
232
- // An empty answer also comes from a connection that closed while the consumer was
233
- // paused between chunks (fetchOne() finds no mailbox then), and that is not a
234
- // missing message: a caller that retries would skip it
232
+ // An empty answer from a connection that closed while the consumer was paused
233
+ // between chunks is not a missing message: a caller that retries would skip it
234
+ // (fetchOne() itself rejects with NoConnection when it finds no mailbox then)
235
235
  if (!client.usable) {
236
236
  throw client.createNoConnectionError(false, { rejectedFrom: 'download' });
237
237
  }
@@ -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];
@@ -60,7 +60,9 @@ export const ImapFlowErrorCode = {
60
60
  // the transport option
61
61
  UnsupportedTransportOption: 'UnsupportedTransportOption',
62
62
  // API misuse
63
- InstanceReused: 'InstanceReused'
63
+ InstanceReused: 'InstanceReused',
64
+ // a message command (FETCH) was called while no mailbox was selected
65
+ NoMailboxSelected: 'NoMailboxSelected'
64
66
  };
65
67
  /**
66
68
  * 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
@@ -168,6 +168,18 @@ export class ImapFlow extends EventEmitter {
168
168
  * Current module version as a static class property
169
169
  */
170
170
  static { this.version = packageInfo.version; }
171
+ /**
172
+ * The mailboxes a `verifyOnly` connection with `includeMailboxes` listed before it logged out,
173
+ * `undefined` otherwise
174
+ *
175
+ * @example
176
+ * let client = new ImapFlow({ ..., verifyOnly: true, includeMailboxes: true });
177
+ * await client.connect();
178
+ * console.log(client.mailboxList.map(mailbox => mailbox.path));
179
+ */
180
+ get mailboxList() {
181
+ return this._mailboxList;
182
+ }
171
183
  /**
172
184
  * Creates a client for one IMAP connection. Nothing is sent before `connect()` is called
173
185
  */
@@ -219,6 +231,7 @@ export class ImapFlow extends EventEmitter {
219
231
  this._throttleWaits = new Set();
220
232
  this._upgradeReject = null;
221
233
  this.isClosed = false;
234
+ this._errorReported = false;
222
235
  this.states = states;
223
236
  this.state = this.states.NOT_AUTHENTICATED;
224
237
  this.lockCounter = 0;
@@ -243,6 +256,9 @@ export class ImapFlow extends EventEmitter {
243
256
  this.mailbox = false;
244
257
  this.currentSelectCommand = false;
245
258
  this.idling = false;
259
+ this.context = this.options.context;
260
+ this.connectedAt = null;
261
+ this.lastClientWrite = null;
246
262
  this.emitLogs = !!this.options.emitLogs;
247
263
  this.lo = 0;
248
264
  this.untaggedHandlers = {};
@@ -288,6 +304,15 @@ export class ImapFlow extends EventEmitter {
288
304
  return;
289
305
  }
290
306
  err._connId = err._connId || this.id;
307
+ // One error per connection: emitError() closes the connection, and until the deferred
308
+ // close() runs a second socket, deflate or parser error may still arrive. Emitting it as
309
+ // well would reach a consumer that removed its listener after the first one, or one that
310
+ // only awaited connect(), as an unhandled 'error' that throws.
311
+ if (this._errorReported) {
312
+ this.log.debug({ msg: 'Suppressed error after the connection already reported one', err, cid: this.id });
313
+ this.closeAfter();
314
+ return;
315
+ }
291
316
  // During a STARTTLS handshake the upgrade owns the single error path (its settle()
292
317
  // helper). Route the error there so a streamer-originated failure is surfaced with its
293
318
  // real code (instead of a generic ClosedAfterConnect*) and cannot hang a verifyOnly
@@ -298,6 +323,7 @@ export class ImapFlow extends EventEmitter {
298
323
  this._upgradeReject = null;
299
324
  if (typeof reject === 'function') {
300
325
  // settle() clears the upgrade timer and flags, and closes the connection
326
+ this._errorReported = true;
301
327
  reject(err);
302
328
  return;
303
329
  }
@@ -312,10 +338,12 @@ export class ImapFlow extends EventEmitter {
312
338
  let reject = this.initialReject;
313
339
  this.initialResolve = false;
314
340
  this.initialReject = false;
341
+ this._errorReported = true;
315
342
  this.closeAfter();
316
343
  reject(err);
317
344
  return;
318
345
  }
346
+ this._errorReported = true;
319
347
  this.closeAfter();
320
348
  this.emit('error', err);
321
349
  }
@@ -369,6 +397,7 @@ export class ImapFlow extends EventEmitter {
369
397
  else {
370
398
  return false;
371
399
  }
400
+ this.lastClientWrite = Date.now();
372
401
  for (let data of frames) {
373
402
  if (this.logRaw) {
374
403
  // Client frames of an authentication exchange carry credentials: the LOGIN
@@ -1259,7 +1288,9 @@ export class ImapFlow extends EventEmitter {
1259
1288
  }
1260
1289
  });
1261
1290
  const socket = this.socket;
1262
- socket.once('error', this._socketError);
1291
+ // on(), not once(): with compression the deflate and writeSocket errors are forwarded
1292
+ // onto this socket, and a second one before close() must not find it without a listener
1293
+ socket.on('error', this._socketError);
1263
1294
  socket.once('close', this._socketClose);
1264
1295
  socket.once('end', this._socketEnd);
1265
1296
  socket.on('tlsClientError', this._socketError);
@@ -2238,6 +2269,7 @@ export class ImapFlow extends EventEmitter {
2238
2269
  }
2239
2270
  try {
2240
2271
  clearTimer(this.connectTimeout);
2272
+ this.connectedAt = Date.now();
2241
2273
  // ImapFlow now owns the socket; drop the proxy's early error handler
2242
2274
  // (its "before connection setup" message no longer applies).
2243
2275
  detachEarlyErrorHandler(socket);
@@ -2363,6 +2395,15 @@ export class ImapFlow extends EventEmitter {
2363
2395
  }
2364
2396
  return error;
2365
2397
  }
2398
+ // What a message command called without a selected mailbox rejects with: NoMailboxSelected,
2399
+ // or NoConnection once the connection is gone, as that is why the mailbox is missing then
2400
+ /** @internal */
2401
+ createNoMailboxError(command) {
2402
+ if (!this.usable) {
2403
+ return this.createNoConnectionError(this.byeReason, { rejectedFrom: 'noMailbox', command });
2404
+ }
2405
+ return createImapError('No mailbox is selected', 'NoMailboxSelected', { command });
2406
+ }
2366
2407
  /**
2367
2408
  * Closes TCP connection without notifying the server.
2368
2409
  *
@@ -2803,8 +2844,14 @@ export class ImapFlow extends EventEmitter {
2803
2844
  // LIST. This sets mailboxMissing on the error so the caller can distinguish
2804
2845
  // "doesn't exist" from other failures, for getMailboxLock() callers as well.
2805
2846
  try {
2806
- let folders = await this.run('LIST', '', normalizePath(this, path), { listOnly: true });
2807
- if (!folders || !folders.length) {
2847
+ let normalized = normalizePath(this, path);
2848
+ let folders = await this.run('LIST', '', normalized, { listOnly: true });
2849
+ // The path is the LIST pattern, so a % or * in a name also matches other
2850
+ // mailboxes, and an entry marked \NonExistent (RFC 5258) does not exist either
2851
+ let exists = Array.isArray(folders) &&
2852
+ folders.some(entry => comparePaths(this, entry.path, normalized) &&
2853
+ ![...(entry.flags || [])].some(flag => typeof flag === 'string' && flag.toUpperCase() === '\\NONEXISTENT'));
2854
+ if (!exists) {
2808
2855
  err.mailboxMissing = true;
2809
2856
  }
2810
2857
  }
@@ -2847,6 +2894,9 @@ export class ImapFlow extends EventEmitter {
2847
2894
  * otherwise IDLE is started by default on connection inactivity. NB! If `idle()` is called manually then it does not
2848
2895
  * return until IDLE is finished which means you would have to call some other command out of scope.
2849
2896
  *
2897
+ * 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.
2899
+ *
2850
2900
  * @returns `false` if IDLE failed, `undefined` otherwise
2851
2901
  *
2852
2902
  * @example
@@ -3100,6 +3150,9 @@ export class ImapFlow extends EventEmitter {
3100
3150
  * @param query Fetch query
3101
3151
  * @param options Fetch options
3102
3152
  * @yields Message data object
3153
+ * @throws An error with the code `NoMailboxSelected` when no mailbox is selected (for example
3154
+ * after `mailboxClose()` or a failed `mailboxOpen()`), or `NoConnection` once the connection
3155
+ * is gone. Earlier releases yielded nothing in that case, as for an empty mailbox
3103
3156
  *
3104
3157
  * @example
3105
3158
  * let mailbox = await client.mailboxOpen('INBOX');
@@ -3113,8 +3166,8 @@ export class ImapFlow extends EventEmitter {
3113
3166
  async *fetch(range, query, options) {
3114
3167
  options = options || {};
3115
3168
  if (!this.mailbox) {
3116
- // no mailbox selected, nothing to do
3117
- return;
3169
+ // an empty result would look like an empty mailbox, for example right after close()
3170
+ throw this.createNoMailboxError('FETCH');
3118
3171
  }
3119
3172
  let resolved = await this.resolveRange(range, options);
3120
3173
  if (!resolved) {
@@ -3224,6 +3277,7 @@ export class ImapFlow extends EventEmitter {
3224
3277
  * @param query Fetch query
3225
3278
  * @param options Fetch options
3226
3279
  * @returns Array of Message data object
3280
+ * @throws An error with the code `NoMailboxSelected` when no mailbox is selected, see fetch()
3227
3281
  *
3228
3282
  * @example
3229
3283
  * let mailbox = await client.mailboxOpen('INBOX');
@@ -3247,7 +3301,9 @@ export class ImapFlow extends EventEmitter {
3247
3301
  * @param seq Single UID or sequence number of the message to fetch for
3248
3302
  * @param query Fetch query
3249
3303
  * @param options Fetch options
3250
- * @returns Message data object
3304
+ * @returns Message data object, `false` when the message was not found
3305
+ * @throws An error with the code `NoMailboxSelected` when no mailbox is selected, see fetch().
3306
+ * Earlier releases resolved `undefined` then
3251
3307
  *
3252
3308
  * @example
3253
3309
  * let mailbox = await client.mailboxOpen('INBOX');
@@ -3257,8 +3313,9 @@ export class ImapFlow extends EventEmitter {
3257
3313
  */
3258
3314
  async fetchOne(seq, query, options) {
3259
3315
  if (!this.mailbox) {
3260
- // no mailbox selected, nothing to do
3261
- return;
3316
+ // undefined used to be returned here, indistinguishable from a missing message for a
3317
+ // caller that only tests the result
3318
+ throw this.createNoMailboxError('FETCH');
3262
3319
  }
3263
3320
  if (seq === '*') {
3264
3321
  if (!this.mailbox.exists) {
@@ -3436,6 +3493,9 @@ export class ImapFlow extends EventEmitter {
3436
3493
  return;
3437
3494
  }
3438
3495
  this.processingLock = true;
3496
+ // The lock taken off the queue and not settled yet. A pass that throws out of its loop
3497
+ // rejects it, its caller would wait forever otherwise: it is in the queue no longer.
3498
+ let unsettled = null;
3439
3499
  try {
3440
3500
  // Process all locks in queue until empty
3441
3501
  let processedCount = 0;
@@ -3452,6 +3512,7 @@ export class ImapFlow extends EventEmitter {
3452
3512
  await new Promise(resolve => setImmediate(resolve));
3453
3513
  }
3454
3514
  const lock = this.locks.shift();
3515
+ unsettled = lock;
3455
3516
  const { resolve, reject, path, options, lockId } = lock;
3456
3517
  // From here on the grant/reject path owns the outcome; the acquire
3457
3518
  // timer must not race with resolution.
@@ -3517,6 +3578,7 @@ export class ImapFlow extends EventEmitter {
3517
3578
  };
3518
3579
  if (!this.usable || !this.socket || this.socket.destroyed) {
3519
3580
  this.log.trace({ msg: 'Failed to acquire mailbox lock', path, lockId, idling: this.idling });
3581
+ unsettled = null;
3520
3582
  reject(this.createNoConnectionError(false, { rejectedFrom: 'mailboxLock', path }));
3521
3583
  continue; // Process next lock in queue
3522
3584
  }
@@ -3529,6 +3591,7 @@ export class ImapFlow extends EventEmitter {
3529
3591
  this.currentLock = lock;
3530
3592
  armHeldTimer();
3531
3593
  this.autoidle();
3594
+ unsettled = null;
3532
3595
  resolve({ path, release });
3533
3596
  };
3534
3597
  if (this.mailbox && this.mailbox.path === path && !!this.mailbox.readOnly === !!options.readOnly) {
@@ -3571,23 +3634,35 @@ export class ImapFlow extends EventEmitter {
3571
3634
  ...(options.description && { description: options.description }),
3572
3635
  err
3573
3636
  });
3637
+ unsettled = null;
3574
3638
  reject(err);
3575
3639
  // Continue to next lock in queue
3576
3640
  }
3577
3641
  }
3578
3642
  }
3643
+ catch (err) {
3644
+ if (unsettled) {
3645
+ let lock = unsettled;
3646
+ unsettled = null;
3647
+ // granted but not handed out (the grant itself failed): nobody can release it
3648
+ if (this.currentLock === lock) {
3649
+ clearTimer(lock.heldWarnTimer);
3650
+ lock.heldWarnTimer = null;
3651
+ this.currentLock = false;
3652
+ }
3653
+ lock.reject(err);
3654
+ }
3655
+ throw err;
3656
+ }
3579
3657
  finally {
3580
3658
  this.processingLock = false;
3581
- // New locks may have been queued while we were processing (e.g.,
3582
- // a lock that failed immediately and the next getMailboxLock call
3583
- // arrived before we finished). Schedule another run if needed.
3584
- /* c8 ignore start */ // requires a lock to be enqueued during an in-flight processLocks pass; not reproducible deterministically
3659
+ // A pass that ended with locks queued and none held (it threw out of its loop) would
3660
+ // leave them waiting with nothing to start another pass. Schedule one.
3585
3661
  if (this.locks.length && !this.currentLock) {
3586
3662
  setImmediate(() => {
3587
3663
  this.processLocks().catch(err => this.log.error({ err, cid: this.id }));
3588
3664
  });
3589
3665
  }
3590
- /* c8 ignore stop */
3591
3666
  }
3592
3667
  }
3593
3668
  /**
@@ -3781,7 +3856,8 @@ export class ImapFlow extends EventEmitter {
3781
3856
  */
3782
3857
  /**
3783
3858
  * Error event. In most cases getting an error event also means that connection is closed
3784
- * and pending operations should return with a failure.
3859
+ * and pending operations should return with a failure. Emitted at most once per connection,
3860
+ * later errors while the connection is closing are only logged.
3785
3861
  *
3786
3862
  * @event ImapFlow#error
3787
3863
  * @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/";
@@ -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.5.9";
3
+ export const version = "2.6.0";
4
4
  export const homepage = "https://imapflow.com/";