imapflow 2.0.8 → 2.1.1

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,32 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.1.1](https://github.com/postalsys/imapflow/compare/v2.1.0...v2.1.1) (2026-09-28)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * settle every pending connect on close, refuse empty search operands, keep NIL envelope rows, prefer top-level special-use folders ([27a93cc](https://github.com/postalsys/imapflow/commit/27a93ccd292fc95842bcc27cdc76e0cb0de613ba))
9
+
10
+ ## [2.1.0](https://github.com/postalsys/imapflow/compare/v2.0.8...v2.1.0) (2026-09-27)
11
+
12
+
13
+ ### Features
14
+
15
+ * export ImapFlowErrorCode with the error codes the library sets ([50c909c](https://github.com/postalsys/imapflow/commit/50c909c30a8cda4731c397f1a77c3bf3d2d17102))
16
+ * support `await using` through Symbol.asyncDispose ([baadd4b](https://github.com/postalsys/imapflow/commit/baadd4bd73a8d9c46aab2966beb55698a42e71b2))
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * accept a number or string uidValidity for QRESYNC ([e87d0c2](https://github.com/postalsys/imapflow/commit/e87d0c216e48e2dda23d94a7c942682ec28258ca))
22
+ * give a special-use type to its next candidate when the best is taken ([5b0d357](https://github.com/postalsys/imapflow/commit/5b0d357ec1f30cd7b43a7ad06e3a22a9ac244bad))
23
+ * keep a bracketed IPv6 host in a REFERRAL URL ([1e90b70](https://github.com/postalsys/imapflow/commit/1e90b706f72c54e4d1397e11e0c0b2aabc2f2549))
24
+ * keep auto-IDLE off a socket handed over by unbind() ([a47b3ae](https://github.com/postalsys/imapflow/commit/a47b3ae99e5e1e00fd8b804a8072aeccc34c6a5a))
25
+ * parse a chunk of many literals in a loop instead of recursing ([db4c704](https://github.com/postalsys/imapflow/commit/db4c704291054cff5a9d87386a093d113ea69fc3))
26
+ * process a chunk that arrives while the input loop is winding down ([c73f3ea](https://github.com/postalsys/imapflow/commit/c73f3ea18f5557abf91414a0faf98da7150e3f4f)), closes [#408](https://github.com/postalsys/imapflow/issues/408)
27
+ * read the last extension field of a BODYSTRUCTURE part ([d5cf6c0](https://github.com/postalsys/imapflow/commit/d5cf6c0282cf35df2de7eb153fed24ecd09448d3))
28
+ * reject connect() right away on a BYE greeting ([dc5d80e](https://github.com/postalsys/imapflow/commit/dc5d80e6f6efdebe92f461e23f41dc64bce6c4aa))
29
+
3
30
  ## [2.0.8](https://github.com/postalsys/imapflow/compare/v2.0.7...v2.0.8) (2026-09-27)
4
31
 
5
32
 
package/README.md CHANGED
@@ -68,6 +68,8 @@ const main = async () => {
68
68
  main().catch(console.error);
69
69
  ```
70
70
 
71
+ 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`.
72
+
71
73
  See the [Quick Start guide](https://imapflow.com/docs/getting-started/quick-start) for more examples, including Gmail, Outlook, and Yahoo configuration.
72
74
 
73
75
  ## Documentation
@@ -151,10 +151,12 @@ async function list(connection, reference, mailbox, options) {
151
151
  if (!untagged.attributes || !untagged.attributes.length) {
152
152
  return;
153
153
  }
154
+ // A name sent as a literal arrives as a Buffer, so convert it once for both fields
155
+ let rawPath = ((untagged.attributes[2] && untagged.attributes[2].value) || '').toString();
154
156
  let entry = {
155
157
  // Decode from modified UTF-7 wire format and normalize the path
156
- path: (0, tools_js_1.normalizePath)(connection, (0, tools_js_1.decodePath)(connection, ((untagged.attributes[2] && untagged.attributes[2].value) || ''))),
157
- pathAsListed: ((untagged.attributes[2] && untagged.attributes[2].value) || ''),
158
+ path: (0, tools_js_1.normalizePath)(connection, (0, tools_js_1.decodePath)(connection, rawPath)),
159
+ pathAsListed: rawPath,
158
160
  flags: new Set((0, tools_js_1.getStringList)(untagged.attributes[0])),
159
161
  delimiter: (untagged.attributes[1] && untagged.attributes[1].value),
160
162
  listed: true
@@ -382,9 +384,11 @@ async function list(connection, reference, mailbox, options) {
382
384
  if (!untagged.attributes || !untagged.attributes.length) {
383
385
  return;
384
386
  }
387
+ // A name sent as a literal arrives as a Buffer, so convert it once for both fields
388
+ let rawPath = ((untagged.attributes[2] && untagged.attributes[2].value) || '').toString();
385
389
  let entry = {
386
- path: (0, tools_js_1.normalizePath)(connection, (0, tools_js_1.decodePath)(connection, ((untagged.attributes[2] && untagged.attributes[2].value) || ''))),
387
- pathAsListed: ((untagged.attributes[2] && untagged.attributes[2].value) || ''),
390
+ path: (0, tools_js_1.normalizePath)(connection, (0, tools_js_1.decodePath)(connection, rawPath)),
391
+ pathAsListed: rawPath,
388
392
  flags: new Set((0, tools_js_1.getStringList)(untagged.attributes[0])),
389
393
  delimiter: (untagged.attributes[1] && untagged.attributes[1].value),
390
394
  subscribed: true
@@ -447,23 +451,35 @@ async function list(connection, reference, mailbox, options) {
447
451
  connection.log.warn({ msg: 'Failed to request subscription info', err, cid: connection.id });
448
452
  }
449
453
  }
450
- // Resolve special-use conflicts: for each type, pick the best candidate
451
- // based on source priority (user > extension > name), then alphabetically.
452
- // Only the winning entry gets the specialUse property set.
453
- for (let type of Object.keys(specialUseMatches)) {
454
- let sortedEntries = specialUseMatches[type].sort((a, b) => {
455
- let aSource = SOURCE_SORT_ORDER.indexOf(a.source);
456
- let bSource = SOURCE_SORT_ORDER.indexOf(b.source);
457
- if (aSource === bSource) {
458
- return a.entry.path.localeCompare(b.entry.path);
459
- }
454
+ // Resolve special-use conflicts. Each type goes to one mailbox and each mailbox gets
455
+ // at most one type. Candidates are taken in priority order across all types (user >
456
+ // extension > name, then alphabetically), so a mailbox claimed by a stronger match
457
+ // leaves its other type to that type's next candidate instead of to nobody. Within a
458
+ // source a shallower mailbox wins before the alphabetical order is consulted, so that
459
+ // INBOX.Sent is preferred over INBOX.Archive.Sent.
460
+ let candidates = Object.entries(specialUseMatches).flatMap(([type, matches]) => matches.map(match => ({ type, ...match })));
461
+ candidates.sort((a, b) => {
462
+ let aSource = SOURCE_SORT_ORDER.indexOf(a.source);
463
+ let bSource = SOURCE_SORT_ORDER.indexOf(b.source);
464
+ if (aSource !== bSource) {
460
465
  return aSource - bSource;
461
- });
462
- if (!sortedEntries[0].entry.specialUse) {
463
- let source = sortedEntries[0].source;
464
- sortedEntries[0].entry.specialUse = type;
465
- sortedEntries[0].entry.specialUseSource = PUBLIC_SOURCE[source] || source;
466
466
  }
467
+ // parent is set on every listed entry before the candidates are ranked
468
+ let aDepth = a.entry.parent.length;
469
+ let bDepth = b.entry.parent.length;
470
+ if (aDepth !== bDepth) {
471
+ return aDepth - bDepth;
472
+ }
473
+ return a.entry.path.localeCompare(b.entry.path);
474
+ });
475
+ let assignedTypes = new Set();
476
+ for (let { type, entry, source } of candidates) {
477
+ if (assignedTypes.has(type) || entry.specialUse) {
478
+ continue;
479
+ }
480
+ entry.specialUse = type;
481
+ entry.specialUseSource = PUBLIC_SOURCE[source] || source;
482
+ assignedTypes.add(type);
467
483
  }
468
484
  // No source answered, so "not subscribed" was never actually reported for any of
469
485
  // these folders - the state is unknown, not false. Reporting the whole listing as
@@ -95,12 +95,16 @@ async function select(connection, pathInput, options) {
95
95
  // QRESYNC (RFC 7162): allows efficient mailbox resynchronization by sending
96
96
  // the last known UIDVALIDITY and HIGHESTMODSEQ. Server responds with only
97
97
  // the changes (new flags, expunged UIDs) since that point.
98
+ // The caller may pass UIDVALIDITY as a bigint, a number or a string. It is parsed once,
99
+ // so the value that is sent is also the one checked against the server's below, and a
100
+ // value that is not a plain decimal skips QRESYNC instead of reaching the server.
101
+ let uidValidity = options.uidValidity !== undefined ? (0, tools_js_1.parseBigIntValue)(String(options.uidValidity)) : false;
98
102
  let extraArgs = [];
99
- if (connection.enabled.has('QRESYNC') && options.changedSince && options.uidValidity) {
103
+ if (connection.enabled.has('QRESYNC') && options.changedSince && uidValidity) {
100
104
  extraArgs.push([
101
105
  { type: 'ATOM', value: 'QRESYNC' },
102
106
  [
103
- { type: 'ATOM', value: options.uidValidity?.toString() },
107
+ { type: 'ATOM', value: uidValidity.toString() },
104
108
  { type: 'ATOM', value: options.changedSince.toString() }
105
109
  ]
106
110
  ]);
@@ -205,7 +209,7 @@ async function select(connection, pathInput, options) {
205
209
  // QRESYNC results are only valid if UIDVALIDITY matches, HIGHESTMODSEQ is
206
210
  // present, and the mailbox supports mod-sequences. If any condition fails,
207
211
  // the client cannot trust the incremental updates and must do a full resync.
208
- if (map.qresync && (options.uidValidity !== map.uidValidity || !map.highestModseq || map.noModseq)) {
212
+ if (map.qresync && (uidValidity !== map.uidValidity || !map.highestModseq || map.noModseq)) {
209
213
  map.qresync = false;
210
214
  }
211
215
  // Transition mailbox state: save previous mailbox reference, temporarily
@@ -1,11 +1,60 @@
1
1
  import type { ImapResponse } from './handler/types.js';
2
+ /**
3
+ * The `code` values ImapFlow sets on the errors it raises, so they can be matched without
4
+ * string literals: `if (err.code === ImapFlowErrorCode.NoConnection)`.
5
+ *
6
+ * Parser failures use `ParserError` followed by a number (`ParserError11`) and are not listed
7
+ * one by one, test them with `err.code?.startsWith('ParserError')`. Errors from the socket, TLS
8
+ * or DNS layer pass through with Node's own code (`ECONNREFUSED`, `ENOTFOUND`, ...).
9
+ */
10
+ export declare const ImapFlowErrorCode: {
11
+ readonly NoConnection: "NoConnection";
12
+ readonly EConnectionClosed: "EConnectionClosed";
13
+ readonly StateLogout: "StateLogout";
14
+ readonly ClosedAfterConnectText: "ClosedAfterConnectText";
15
+ readonly ClosedAfterConnectTLS: "ClosedAfterConnectTLS";
16
+ readonly CONNECT_TIMEOUT: "CONNECT_TIMEOUT";
17
+ readonly GREETING_TIMEOUT: "GREETING_TIMEOUT";
18
+ readonly UPGRADE_TIMEOUT: "UPGRADE_TIMEOUT";
19
+ readonly ETIMEOUT: "ETIMEOUT";
20
+ readonly LockTimeout: "LockTimeout";
21
+ readonly ETHROTTLE: "ETHROTTLE";
22
+ readonly UnexpectedTag: "UnexpectedTag";
23
+ readonly InvalidResponse: "InvalidResponse";
24
+ readonly ResponseProcessingFailed: "ResponseProcessingFailed";
25
+ readonly STARTTLS_INJECTION: "STARTTLS_INJECTION";
26
+ readonly COMPRESS_TRAILING_DATA: "COMPRESS_TRAILING_DATA";
27
+ readonly PollFailed: "PollFailed";
28
+ readonly NotFound: "NotFound";
29
+ readonly MissingServerExtension: "MissingServerExtension";
30
+ readonly ParserError: "ParserError";
31
+ readonly ParserErrorExchange: "ParserErrorExchange";
32
+ readonly MAX_IMAP_NESTING_REACHED: "MAX_IMAP_NESTING_REACHED";
33
+ readonly LineTooLarge: "LineTooLarge";
34
+ readonly LiteralTooLarge: "LiteralTooLarge";
35
+ readonly ResponseTooLarge: "ResponseTooLarge";
36
+ readonly InvalidStringValue: "InvalidStringValue";
37
+ readonly InvalidTokenValue: "InvalidTokenValue";
38
+ readonly InvalidTextValue: "InvalidTextValue";
39
+ readonly InvalidSequenceSet: "InvalidSequenceSet";
40
+ readonly InvalidSearchQuery: "InvalidSearchQuery";
41
+ readonly DownloadOverflow: "DownloadOverflow";
42
+ readonly DownloadIncomplete: "DownloadIncomplete";
43
+ readonly ProxyError: "ProxyError";
44
+ readonly EPROXY: "EPROXY";
45
+ readonly UnsupportedProxyAddress: "UnsupportedProxyAddress";
46
+ readonly ERR_INVALID_URL: "ERR_INVALID_URL";
47
+ readonly InstanceReused: "InstanceReused";
48
+ };
49
+ /** One of the {@link ImapFlowErrorCode} values */
50
+ export type ImapFlowErrorCode = (typeof ImapFlowErrorCode)[keyof typeof ImapFlowErrorCode];
2
51
  /**
3
52
  * An Error raised by ImapFlow, with the extra properties the library attaches to describe
4
53
  * the failure. Every property is optional: which ones are present depends on where the
5
54
  * error came from.
6
55
  */
7
56
  export interface ImapFlowError extends Error {
8
- /** Error code, e.g. 'NoConnection', 'ETIMEOUT', 'LockTimeout' or a parser error code */
57
+ /** Error code, one of {@link ImapFlowErrorCode}, a parser error code or a code from Node */
9
58
  code?: string | undefined;
10
59
  /** Connection id the error belongs to */
11
60
  cid?: string | undefined;
@@ -1,6 +1,61 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AuthenticationFailure = void 0;
3
+ exports.AuthenticationFailure = exports.ImapFlowErrorCode = void 0;
4
+ /**
5
+ * The `code` values ImapFlow sets on the errors it raises, so they can be matched without
6
+ * string literals: `if (err.code === ImapFlowErrorCode.NoConnection)`.
7
+ *
8
+ * Parser failures use `ParserError` followed by a number (`ParserError11`) and are not listed
9
+ * one by one, test them with `err.code?.startsWith('ParserError')`. Errors from the socket, TLS
10
+ * or DNS layer pass through with Node's own code (`ECONNREFUSED`, `ENOTFOUND`, ...).
11
+ */
12
+ exports.ImapFlowErrorCode = {
13
+ // the connection is gone, the command was not (or can no longer be) completed
14
+ NoConnection: 'NoConnection',
15
+ EConnectionClosed: 'EConnectionClosed',
16
+ StateLogout: 'StateLogout',
17
+ ClosedAfterConnectText: 'ClosedAfterConnectText',
18
+ ClosedAfterConnectTLS: 'ClosedAfterConnectTLS',
19
+ // timeouts
20
+ CONNECT_TIMEOUT: 'CONNECT_TIMEOUT',
21
+ GREETING_TIMEOUT: 'GREETING_TIMEOUT',
22
+ UPGRADE_TIMEOUT: 'UPGRADE_TIMEOUT',
23
+ ETIMEOUT: 'ETIMEOUT',
24
+ LockTimeout: 'LockTimeout',
25
+ // the server
26
+ ETHROTTLE: 'ETHROTTLE',
27
+ UnexpectedTag: 'UnexpectedTag',
28
+ InvalidResponse: 'InvalidResponse',
29
+ ResponseProcessingFailed: 'ResponseProcessingFailed',
30
+ STARTTLS_INJECTION: 'STARTTLS_INJECTION',
31
+ COMPRESS_TRAILING_DATA: 'COMPRESS_TRAILING_DATA',
32
+ PollFailed: 'PollFailed',
33
+ NotFound: 'NotFound',
34
+ MissingServerExtension: 'MissingServerExtension',
35
+ // response parsing and size limits
36
+ ParserError: 'ParserError',
37
+ ParserErrorExchange: 'ParserErrorExchange',
38
+ MAX_IMAP_NESTING_REACHED: 'MAX_IMAP_NESTING_REACHED',
39
+ LineTooLarge: 'LineTooLarge',
40
+ LiteralTooLarge: 'LiteralTooLarge',
41
+ ResponseTooLarge: 'ResponseTooLarge',
42
+ // invalid values in a command
43
+ InvalidStringValue: 'InvalidStringValue',
44
+ InvalidTokenValue: 'InvalidTokenValue',
45
+ InvalidTextValue: 'InvalidTextValue',
46
+ InvalidSequenceSet: 'InvalidSequenceSet',
47
+ InvalidSearchQuery: 'InvalidSearchQuery',
48
+ // download()
49
+ DownloadOverflow: 'DownloadOverflow',
50
+ DownloadIncomplete: 'DownloadIncomplete',
51
+ // proxy connections
52
+ ProxyError: 'ProxyError',
53
+ EPROXY: 'EPROXY',
54
+ UnsupportedProxyAddress: 'UnsupportedProxyAddress',
55
+ ERR_INVALID_URL: 'ERR_INVALID_URL',
56
+ // API misuse
57
+ InstanceReused: 'InstanceReused'
58
+ };
4
59
  /**
5
60
  * Error subclass thrown when IMAP authentication fails.
6
61
  */
@@ -145,13 +145,24 @@ export declare class ImapStream extends Transform {
145
145
  * pushed downstream as a readable object.
146
146
  *
147
147
  * @param chunk - The raw data chunk to process.
148
- * @param startPos - The byte offset within the chunk to start processing from.
149
148
  */
150
- processInputChunk(chunk: Buffer, startPos?: number | undefined): Promise<void>;
149
+ processInputChunk(chunk: Buffer): Promise<void>;
150
+ /**
151
+ * Processes the chunk from `startPos` until the parser state changes or the chunk ends.
152
+ *
153
+ * @returns The offset to continue from after a state switch, or `null` when done with the chunk.
154
+ */
155
+ processChunkSegment(chunk: Buffer, startPos: number): Promise<number | null>;
151
156
  /**
152
157
  * Drains the input queue by processing each queued chunk sequentially.
153
158
  * Yields to the event loop every 10 chunks to prevent CPU blocking on
154
159
  * large bursts of incoming data.
160
+ *
161
+ * The `processingInput` guard is cleared in the same synchronous step that finds the queue
162
+ * empty. Clearing it later (in a promise handler) leaves a gap of a few microtasks where a
163
+ * chunk delivered by the writable side is queued but no loop is started for it, so its
164
+ * transform callback is never called and the socket is never read again. Workers deliver
165
+ * the next chunk inside that gap.
155
166
  */
156
167
  processInput(): Promise<void>;
157
168
  /**
@@ -1,11 +1,8 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.ImapStream = void 0;
7
4
  const node_stream_1 = require("node:stream");
8
- const logger_js_1 = __importDefault(require("../logger.js"));
5
+ const logger_js_1 = require("../logger.js");
9
6
  const limits_js_1 = require("./limits.js");
10
7
  const LINE = 0x01;
11
8
  const LITERAL = 0x02;
@@ -36,13 +33,12 @@ class ImapStream extends node_stream_1.Transform {
36
33
  });
37
34
  this.options = options || {};
38
35
  this.cid = this.options.cid;
39
- this.log =
40
- this.options.logger && typeof this.options.logger === 'object'
41
- ? this.options.logger
42
- : logger_js_1.default.child({
43
- component: 'imap-connection',
44
- cid: this.cid
45
- });
36
+ if (this.options.logger && typeof this.options.logger === 'object') {
37
+ this.log = this.options.logger;
38
+ }
39
+ else {
40
+ this.log = (0, logger_js_1.createConnectionLogger)({ cid: this.cid, logRaw: this.options.logRaw });
41
+ }
46
42
  this.readBytesCounter = 0;
47
43
  // Maximum length of a single line (response without a literal). Bounds the line buffer
48
44
  // so a server that never sends a line terminator cannot exhaust memory.
@@ -223,12 +219,23 @@ class ImapStream extends node_stream_1.Transform {
223
219
  * pushed downstream as a readable object.
224
220
  *
225
221
  * @param chunk - The raw data chunk to process.
226
- * @param startPos - The byte offset within the chunk to start processing from.
227
222
  */
228
- async processInputChunk(chunk, startPos) {
229
- startPos = startPos || 0;
223
+ async processInputChunk(chunk) {
224
+ // Every state switch hands back the offset to resume from instead of recursing, so a
225
+ // chunk packed with thousands of small literals can not exhaust the call stack
226
+ let nextPos = 0;
227
+ while (nextPos !== null) {
228
+ nextPos = await this.processChunkSegment(chunk, nextPos);
229
+ }
230
+ }
231
+ /**
232
+ * Processes the chunk from `startPos` until the parser state changes or the chunk ends.
233
+ *
234
+ * @returns The offset to continue from after a state switch, or `null` when done with the chunk.
235
+ */
236
+ async processChunkSegment(chunk, startPos) {
230
237
  if (this.destroyed || startPos >= chunk.length) {
231
- return;
238
+ return null;
232
239
  }
233
240
  switch (this.state) {
234
241
  case LINE: {
@@ -240,7 +247,7 @@ class ImapStream extends node_stream_1.Transform {
240
247
  // TCP chunk boundaries happen to fall.
241
248
  let segment = chunk.subarray(lineStart, i + 1);
242
249
  if (!this.checkLineLength(this.lineBytes + segment.length)) {
243
- return;
250
+ return null;
244
251
  }
245
252
  this.lineBuffer.push(segment);
246
253
  lineStart = i + 1;
@@ -252,18 +259,18 @@ class ImapStream extends node_stream_1.Transform {
252
259
  // would otherwise be emitted as part of the rejected command.
253
260
  let isLiteralMarker = this.checkLiteralMarker(line);
254
261
  if (this.destroyed) {
255
- return;
262
+ return null;
256
263
  }
257
264
  // Count the line itself and, for a literal marker, the declared
258
265
  // literal bytes against the cumulative per-response budget, so a
259
266
  // response assembled from many tokens stays bounded as a whole
260
267
  if (!this.checkResponseSize(line.length + (isLiteralMarker ? this.literalWaiting : 0))) {
261
- return;
268
+ return null;
262
269
  }
263
270
  this.inputBuffer.push(line);
264
271
  if (isLiteralMarker) {
265
272
  // switch into literal mode and start over
266
- return await this.processInputChunk(chunk, lineStart);
273
+ return lineStart;
267
274
  }
268
275
  // reached end of command input, emit it
269
276
  let payload = this.inputBuffer.length === 1 ? this.inputBuffer[0] : Buffer.concat(this.inputBuffer);
@@ -297,7 +304,7 @@ class ImapStream extends node_stream_1.Transform {
297
304
  });
298
305
  this.pendingPush = null;
299
306
  if (this.destroyed) {
300
- return;
307
+ return null;
301
308
  }
302
309
  }
303
310
  }
@@ -313,7 +320,7 @@ class ImapStream extends node_stream_1.Transform {
313
320
  // while a server streams a line that never terminates - only the much
314
321
  // larger line cap would hold it back.
315
322
  if (!this.checkLineLength(this.lineBytes + tail.length) || !this.checkResponseSize(this.lineBytes + tail.length, true)) {
316
- return;
323
+ return null;
317
324
  }
318
325
  this.lineBytes += tail.length;
319
326
  this.lineBuffer.push(tail);
@@ -331,33 +338,45 @@ class ImapStream extends node_stream_1.Transform {
331
338
  this.literalBuffer = [];
332
339
  this.state = LINE;
333
340
  if (remainingInChunk > bytesToRead) {
334
- return await this.processInputChunk(chunk, startPos + bytesToRead);
341
+ return startPos + bytesToRead;
335
342
  }
336
343
  }
337
344
  break;
338
345
  }
339
346
  }
347
+ return null;
340
348
  }
341
349
  /**
342
350
  * Drains the input queue by processing each queued chunk sequentially.
343
351
  * Yields to the event loop every 10 chunks to prevent CPU blocking on
344
352
  * large bursts of incoming data.
353
+ *
354
+ * The `processingInput` guard is cleared in the same synchronous step that finds the queue
355
+ * empty. Clearing it later (in a promise handler) leaves a gap of a few microtasks where a
356
+ * chunk delivered by the writable side is queued but no loop is started for it, so its
357
+ * transform callback is never called and the socket is never read again. Workers deliver
358
+ * the next chunk inside that gap.
345
359
  */
346
360
  async processInput() {
347
- let data;
348
- let processedCount = 0;
349
- while (!this.destroyed && (data = this.inputQueue.shift())) {
350
- this.activeInput = data;
351
- await this.processInputChunk(data.chunk);
352
- this.activeInput = null;
353
- // mark chunk as processed
354
- this.releaseInput(data);
355
- // Yield to event loop every 10 chunks to prevent CPU blocking
356
- processedCount++;
357
- if (processedCount % 10 === 0) {
358
- await new Promise(resolve => setImmediate(resolve));
361
+ try {
362
+ let data;
363
+ let processedCount = 0;
364
+ while (!this.destroyed && (data = this.inputQueue.shift())) {
365
+ this.activeInput = data;
366
+ await this.processInputChunk(data.chunk);
367
+ this.activeInput = null;
368
+ // mark chunk as processed
369
+ this.releaseInput(data);
370
+ // Yield to event loop every 10 chunks to prevent CPU blocking
371
+ processedCount++;
372
+ if (processedCount % 10 === 0) {
373
+ await new Promise(resolve => setImmediate(resolve));
374
+ }
359
375
  }
360
376
  }
377
+ finally {
378
+ this.processingInput = false;
379
+ }
361
380
  }
362
381
  /**
363
382
  * Transform stream implementation. Receives raw data chunks from the writable side,
@@ -397,9 +416,7 @@ class ImapStream extends node_stream_1.Transform {
397
416
  this.inputQueue.push({ chunk, next });
398
417
  if (!this.processingInput) {
399
418
  this.processingInput = true;
400
- this.processInput()
401
- .catch(err => this.failStream(err))
402
- .finally(() => (this.processingInput = false));
419
+ this.processInput().catch(err => this.failStream(err));
403
420
  }
404
421
  }
405
422
  /**
@@ -327,15 +327,21 @@ class TokenParser {
327
327
  this.currentNode = this.createNode(this.currentNode, this.pos + i + 10);
328
328
  // just call this an ATOM, even though IMAPURL might be more correct
329
329
  this.currentNode.type = 'ATOM';
330
- // jump i to the ']'
331
- i = this.str.indexOf(']', i + 10);
332
- if (i < 0) {
333
- // Malformed REFERRAL with no closing ']'. Consume the rest
334
- // of the string (there is no ']' to exclude) instead of
335
- // computing a negative-index substring, which would yield
336
- // garbage.
337
- i = this.str.length;
330
+ // jump i to the ']' that closes the section. The URL itself can
331
+ // hold a bracketed IPv6 host (imap://[::1]/INBOX), so brackets
332
+ // opened inside the URL are matched before the closing one.
333
+ let depth = 0;
334
+ for (i = i + 10; i < this.str.length; i++) {
335
+ let urlChr = this.str.charAt(i);
336
+ if (urlChr === '[') {
337
+ depth++;
338
+ }
339
+ else if (urlChr === ']' && depth-- === 0) {
340
+ break;
341
+ }
338
342
  }
343
+ // A malformed REFERRAL with no closing ']' leaves i at the end of
344
+ // the string, so the URL takes the rest of it.
339
345
  this.currentNode.endPos = this.pos + i - 1;
340
346
  this.currentNode.value = this.str.substring(this.currentNode.startPos - this.pos, this.currentNode.endPos - this.pos + 1);
341
347
  this.currentNode = this.currentNode.parentNode;
@@ -9,7 +9,7 @@ import { AuthenticationFailure } from './errors.js';
9
9
  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 } from './types.js';
10
10
  export type * from './types.js';
11
11
  export type { ImapFlowError } from './errors.js';
12
- export { AuthenticationFailure } from './errors.js';
12
+ export { AuthenticationFailure, ImapFlowErrorCode } from './errors.js';
13
13
  export type { ImapAttribute, ImapAttributeList, ImapAttributeNode, ImapResponse } from './handler/types.js';
14
14
  declare const stateValues: {
15
15
  readonly NOT_AUTHENTICATED: 1;
@@ -61,6 +61,17 @@ export interface ImapFlow {
61
61
  prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this;
62
62
  emit<K extends keyof ImapFlowEvents>(event: K, ...args: ImapFlowEvents[K]): boolean;
63
63
  emit(event: string | symbol, ...args: any[]): boolean;
64
+ /**
65
+ * Logs out and closes the connection when the scope of an `await using` declaration ends.
66
+ * Never throws, the connection is closed whether LOGOUT succeeds or not. Only present on
67
+ * runtimes that define `Symbol.asyncDispose` (Node.js 20.4 and newer).
68
+ *
69
+ * @example
70
+ * await using client = new ImapFlow({...});
71
+ * await client.connect();
72
+ * // client.logout() runs automatically when the scope exits, even on a throw
73
+ */
74
+ [Symbol.asyncDispose](): Promise<void>;
64
75
  }
65
76
  /**
66
77
  * IMAP client class for accessing IMAP mailboxes
@@ -592,7 +603,8 @@ export declare class ImapFlow extends EventEmitter {
592
603
  getMailboxLock(path: string | string[], options?: MailboxLockOptions | undefined): Promise<MailboxLockObject>;
593
604
  /**
594
605
  * Detaches sockets from the IMAP pipeline. Useful for upgrading the connection
595
- * (e.g., STARTTLS) or transferring socket ownership.
606
+ * (e.g., STARTTLS) or transferring socket ownership. Call it while the connection is not
607
+ * idling: an IDLE in progress is not broken first, so the server still expects `DONE`.
596
608
  *
597
609
  * @returns Socket objects: `readSocket` is the read socket (inflated socket if compression is enabled, raw socket otherwise),
598
610
  * `writeSocket` the write socket and `socket` the raw underlying socket (same as readSocket/writeSocket when compression is disabled)
@@ -603,81 +615,6 @@ export declare class ImapFlow extends EventEmitter {
603
615
  socket: ImapSocket;
604
616
  };
605
617
  }
606
- /**
607
- * Connection close event. **NB!** ImapFlow does not handle reconnects automatically.
608
- * So whenever a 'close' event occurs you must create a new connection yourself.
609
- *
610
- * @event ImapFlow#close
611
- */
612
- /**
613
- * Error event. In most cases getting an error event also means that connection is closed
614
- * and pending operations should return with a failure.
615
- *
616
- * @event ImapFlow#error
617
- * @example
618
- * client.on('error', err=>{
619
- * console.log(`Error occurred: ${err.message}`);
620
- * });
621
- */
622
- /**
623
- * Message count in currently opened mailbox changed
624
- *
625
- * @event ImapFlow#exists
626
- * @example
627
- * client.on('exists', data=>{
628
- * console.log(`Message count in "${data.path}" is ${data.count}`);
629
- * });
630
- */
631
- /**
632
- * Deleted message sequence number in currently opened mailbox. One event is fired for every deleted email.
633
- *
634
- * @event ImapFlow#expunge
635
- * @example
636
- * client.on('expunge', data=>{
637
- * console.log(`Message #${data.seq} was deleted from "${data.path}"`);
638
- * });
639
- */
640
- /**
641
- * Flags were updated for a message. Not all servers fire this event.
642
- *
643
- * @event ImapFlow#flags
644
- * @example
645
- * client.on('flags', data=>{
646
- * console.log(`Flag set for #${data.seq} is now "${Array.from(data.flags).join(', ')}"`);
647
- * });
648
- */
649
- /**
650
- * Mailbox was opened
651
- *
652
- * @event ImapFlow#mailboxOpen
653
- * @example
654
- * client.on('mailboxOpen', mailbox => {
655
- * console.log(`Mailbox ${mailbox.path} was opened`);
656
- * });
657
- */
658
- /**
659
- * Mailbox was closed
660
- *
661
- * Emitted both when a selected mailbox is closed explicitly, by `mailboxClose()` or by
662
- * selecting a different mailbox, and when the connection itself goes away while a mailbox
663
- * was still selected, whether through a clean logout or a lost transport. The transition is
664
- * reported once per selected mailbox, before the `close` event.
665
- *
666
- * @event ImapFlow#mailboxClose
667
- * @example
668
- * client.on('mailboxClose', mailbox => {
669
- * console.log(`Mailbox ${mailbox.path} was closed`);
670
- * });
671
- */
672
- /**
673
- * Log event if `emitLogs=true`
674
- *
675
- * @event ImapFlow#log
676
- * @example
677
- * client.on('log', entry => {
678
- * console.log(`${entry.cid} ${entry.msg}`);
679
- * });
680
- */
681
618
  declare const imapflow: {
682
619
  ImapFlow: typeof ImapFlow;
683
620
  AuthenticationFailure: typeof AuthenticationFailure;