imapflow 1.5.0 → 1.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.
Files changed (36) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +15 -0
  3. package/CLAUDE.md +1 -1
  4. package/lib/commands/idle.js +197 -104
  5. package/lib/commands/list.js +15 -7
  6. package/lib/commands/quota.js +3 -0
  7. package/lib/commands/select.js +5 -0
  8. package/lib/commands/status.js +4 -0
  9. package/lib/connection-deadline.js +98 -0
  10. package/lib/handler/imap-compiler.js +8 -5
  11. package/lib/handler/imap-stream.js +141 -50
  12. package/lib/handler/limits.js +43 -0
  13. package/lib/handler/token-parser.js +31 -1
  14. package/lib/imap-flow.d.ts +33 -5
  15. package/lib/imap-flow.js +575 -281
  16. package/lib/proxy-connection.js +393 -98
  17. package/lib/special-use.js +660 -51
  18. package/lib/tools.js +17 -0
  19. package/package.json +2 -2
  20. package/test/commands-branches-test.js +17 -1
  21. package/test/commands-integration-test.js +24 -2
  22. package/test/fixtures/fake-timers.js +115 -0
  23. package/test/handler-branches-test.js +0 -25
  24. package/test/idle-polling-test.js +349 -0
  25. package/test/imap-flow-compress-test.js +12 -0
  26. package/test/imap-flow-coverage-test.js +3 -3
  27. package/test/imap-flow-internals-test.js +23 -0
  28. package/test/imap-flow-proxy-paths-test.js +151 -0
  29. package/test/imap-flow-secure-test.js +159 -0
  30. package/test/imap-flow-server-test.js +149 -0
  31. package/test/parser-limits-test.js +274 -0
  32. package/test/proxy-connection-test.js +553 -442
  33. package/test/reliability-improvements-test.js +87 -0
  34. package/test/special-use-test.js +337 -0
  35. package/test/tag-correlation-test.js +333 -0
  36. package/test/timer-policy-test.js +214 -0
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Transform = require('stream').Transform;
4
4
  const logger = require('../logger');
5
+ const { MAX_LITERAL_SIZE, MAX_LINE_SIZE, normalizeLimit, createLiteralTooLargeError } = require('./limits');
5
6
 
6
7
  const LINE = 0x01;
7
8
  const LITERAL = 0x02;
@@ -13,14 +14,6 @@ const NUM_9 = 0x39;
13
14
  const CURLY_OPEN = 0x7b;
14
15
  const CURLY_CLOSE = 0x7d;
15
16
 
16
- // Maximum allowed literal size: 1GB (1073741824 bytes)
17
- const MAX_LITERAL_SIZE = 1024 * 1024 * 1024;
18
-
19
- // Default maximum length of a single line (a response without a literal). Matches the literal cap:
20
- // large literal-free responses (e.g. big SEARCH/LIST results) are legitimate, so this bound exists
21
- // only to stop a server that never sends a line terminator, not to constrain normal traffic.
22
- const MAX_LINE_SIZE = MAX_LITERAL_SIZE;
23
-
24
17
  /**
25
18
  * A Transform stream that parses raw IMAP protocol data from a socket into structured
26
19
  * command/response objects. Reads binary input, splits it into lines delimited by LF,
@@ -42,10 +35,15 @@ class ImapStream extends Transform {
42
35
  * @param {number} [options.maxLineLength] - Maximum allowed length (in bytes) of a single
43
36
  * line (a response without a literal). Defaults to MAX_LITERAL_SIZE (1GB). Guards against a
44
37
  * malicious or broken server that never sends a line terminator, which would otherwise grow
45
- * the internal line buffer without bound.
38
+ * the internal line buffer without bound. The line terminator counts toward the limit, and a
39
+ * line exactly at the limit is accepted. Exceeding it is terminal: the stream is destroyed
40
+ * with a `LineTooLarge` error and no further input is parsed.
46
41
  * @param {number} [options.maxLiteralSize] - Maximum allowed size (in bytes) of a single
47
42
  * literal block. Defaults to MAX_LITERAL_SIZE (1GB). Lower it to bound peak memory
48
- * allocation against a malicious or broken server announcing an oversized literal.
43
+ * allocation against a malicious or broken server announcing an oversized literal. A literal
44
+ * exactly at the limit is accepted. Exceeding it is terminal: the stream is destroyed with a
45
+ * `LiteralTooLarge` error, the marker line is not emitted, and no byte of the rejected
46
+ * literal body is parsed as protocol.
49
47
  */
50
48
  constructor(options) {
51
49
  super({
@@ -68,16 +66,12 @@ class ImapStream extends Transform {
68
66
  this.readBytesCounter = 0;
69
67
 
70
68
  // Maximum length of a single line (response without a literal). Bounds the line buffer
71
- // so a server that never sends a line terminator cannot exhaust memory. A non-negative
72
- // integer is honored as-is (including 0); anything else falls back to the default, so an
73
- // explicit 0 is not silently swallowed into the 1GB default the way `|| MAX_LINE_SIZE` was.
74
- this.maxLineLength = Number.isInteger(this.options.maxLineLength) && this.options.maxLineLength >= 0 ? this.options.maxLineLength : MAX_LINE_SIZE;
69
+ // so a server that never sends a line terminator cannot exhaust memory.
70
+ this.maxLineLength = normalizeLimit(this.options.maxLineLength, MAX_LINE_SIZE);
75
71
 
76
72
  // Maximum size of a single literal block. Bounds peak memory allocation so a server
77
- // announcing an oversized literal cannot exhaust memory. As above, a non-negative integer
78
- // (including an explicit 0, meaning "reject all non-empty literals") is honored as-is.
79
- this.maxLiteralSize =
80
- Number.isInteger(this.options.maxLiteralSize) && this.options.maxLiteralSize >= 0 ? this.options.maxLiteralSize : MAX_LITERAL_SIZE;
73
+ // announcing an oversized literal cannot exhaust memory.
74
+ this.maxLiteralSize = normalizeLimit(this.options.maxLiteralSize, MAX_LITERAL_SIZE);
81
75
 
82
76
  this.state = LINE;
83
77
  this.literalWaiting = 0;
@@ -92,6 +86,52 @@ class ImapStream extends Transform {
92
86
 
93
87
  this.processingInput = false;
94
88
  this.inputQueue = []; // unprocessed input chunks
89
+ this.activeInput = null; // chunk currently being processed (already shifted off inputQueue)
90
+
91
+ // Resolver of the in-flight push() backpressure promise, so destruction can settle it
92
+ // instead of leaving processInput() awaiting a consumer that will never read again.
93
+ this.pendingPush = null;
94
+ }
95
+
96
+ /**
97
+ * Terminally fails the stream. Used for response limit violations and for any other
98
+ * error raised while parsing.
99
+ *
100
+ * The stream is destroyed instead of only emitting `error`: emitting on a Transform leaves
101
+ * it running, so the caller would keep scanning the rejected payload and could emit it as
102
+ * protocol (an oversized literal body contains attacker-chosen CRLF delimited lines).
103
+ * Destroying stops all parsing, drops the offending line, and releases every queued
104
+ * transform callback exactly once (see `_destroy()`).
105
+ *
106
+ * `destroyed` (set synchronously by destroy()) is the single liveness flag every other path
107
+ * checks, so a second failure attempt is a no-op and nothing is parsed after the first.
108
+ *
109
+ * @param {Error} err - The error to destroy the stream with.
110
+ * @returns {boolean} Always false, so callers can `return this.failStream(err)`.
111
+ */
112
+ failStream(err) {
113
+ if (this.destroyed) {
114
+ return false;
115
+ }
116
+ this.destroy(err);
117
+ return false;
118
+ }
119
+
120
+ /**
121
+ * Releases a queued input chunk's transform callback exactly once, signalling the writable
122
+ * side that the chunk was consumed. The mirror image of ImapFlow's releaseStreamData(), which
123
+ * releases the readable items this stream pushes downstream.
124
+ *
125
+ * @param {Object} item - Queue entry holding the chunk and its transform callback.
126
+ */
127
+ releaseInput(item) {
128
+ if (!item || item.released) {
129
+ return;
130
+ }
131
+ item.released = true;
132
+ if (typeof item.next === 'function') {
133
+ item.next();
134
+ }
95
135
  }
96
136
 
97
137
  /**
@@ -137,12 +177,7 @@ class ImapStream extends Transform {
137
177
  const literalSize = Number(Buffer.from(numBytes).toString());
138
178
 
139
179
  if (literalSize > this.maxLiteralSize) {
140
- const err = new Error(`Literal size ${literalSize} exceeds maximum allowed size of ${this.maxLiteralSize} bytes`);
141
- err.code = 'LiteralTooLarge';
142
- err.literalSize = literalSize;
143
- err.maxSize = this.maxLiteralSize;
144
- this.emit('error', err);
145
- return false;
180
+ return this.failStream(createLiteralTooLargeError(literalSize, this.maxLiteralSize));
146
181
  }
147
182
 
148
183
  this.state = LITERAL;
@@ -154,6 +189,25 @@ class ImapStream extends Transform {
154
189
  return false;
155
190
  }
156
191
 
192
+ /**
193
+ * Enforces the configured line-length cap for a projected line length. The projected length
194
+ * covers every byte of the line, the line terminator included, whether or not the line was
195
+ * split across input chunks. A line exactly at the limit is accepted.
196
+ *
197
+ * @param {number} lineLength - Total length the current line would reach.
198
+ * @returns {boolean} True if the line is within the limit, false if the stream was failed.
199
+ */
200
+ checkLineLength(lineLength) {
201
+ if (lineLength <= this.maxLineLength) {
202
+ return true;
203
+ }
204
+ const err = new Error(`Line length ${lineLength} exceeds maximum allowed size of ${this.maxLineLength} bytes`);
205
+ err.code = 'LineTooLarge';
206
+ err.lineLength = lineLength;
207
+ err.maxSize = this.maxLineLength;
208
+ return this.failStream(err);
209
+ }
210
+
157
211
  /**
158
212
  * Processes a single input chunk of raw data. In LINE state, scans for LF-terminated
159
213
  * lines and checks for literal markers. In LITERAL state, collects the expected number
@@ -166,7 +220,7 @@ class ImapStream extends Transform {
166
220
  */
167
221
  async processInputChunk(chunk, startPos) {
168
222
  startPos = startPos || 0;
169
- if (startPos >= chunk.length) {
223
+ if (this.destroyed || startPos >= chunk.length) {
170
224
  return;
171
225
  }
172
226
 
@@ -175,19 +229,34 @@ class ImapStream extends Transform {
175
229
  let lineStart = startPos;
176
230
  for (let i = startPos, len = chunk.length; i < len; i++) {
177
231
  if (chunk[i] === LF) {
178
- // line end found
179
- this.lineBuffer.push(chunk.slice(lineStart, i + 1));
232
+ // line end found. Measure the completed line (terminator included) before
233
+ // concatenating or emitting anything, so the cap does not depend on where
234
+ // TCP chunk boundaries happen to fall.
235
+ let segment = chunk.slice(lineStart, i + 1);
236
+ if (!this.checkLineLength(this.lineBytes + segment.length)) {
237
+ return;
238
+ }
239
+
240
+ this.lineBuffer.push(segment);
180
241
  lineStart = i + 1;
181
242
 
182
- let line = Buffer.concat(this.lineBuffer);
243
+ let line = this.lineBuffer.length === 1 ? this.lineBuffer[0] : Buffer.concat(this.lineBuffer);
183
244
 
184
- this.inputBuffer.push(line);
185
245
  this.lineBuffer = [];
186
246
  this.lineBytes = 0;
187
247
 
188
- // try to detect if this is a literal start
189
- if (this.checkLiteralMarker(line)) {
190
- // switch into line mode and start over
248
+ // try to detect if this is a literal start. An oversized literal fails the
249
+ // stream, so the marker line must not be buffered before the check - it
250
+ // would otherwise be emitted as part of the rejected command.
251
+ let isLiteralMarker = this.checkLiteralMarker(line);
252
+ if (this.destroyed) {
253
+ return;
254
+ }
255
+
256
+ this.inputBuffer.push(line);
257
+
258
+ if (isLiteralMarker) {
259
+ // switch into literal mode and start over
191
260
  return await this.processInputChunk(chunk, lineStart);
192
261
  }
193
262
 
@@ -215,27 +284,29 @@ class ImapStream extends Transform {
215
284
  // boundaries can read it from the pushed object.
216
285
  let trailingAfterLine = lineStart < chunk.length || this.inputQueue.length > 0;
217
286
  await new Promise(resolve => {
287
+ // Tracked so destruction can settle the wait instead of leaving
288
+ // this loop (and the chunk's transform callback) pending forever
289
+ // when the consumer stops reading.
290
+ this.pendingPush = resolve;
218
291
  this.push({ payload, literals, next: resolve, trailingAfterLine });
219
292
  });
293
+ this.pendingPush = null;
294
+
295
+ if (this.destroyed) {
296
+ return;
297
+ }
220
298
  }
221
299
  }
222
300
  }
223
301
  }
224
302
  if (lineStart < chunk.length) {
225
303
  // No line terminator was found in the remaining bytes; carry the tail over to
226
- // the next chunk. Enforce the line-length cap here, since this is the only
227
- // path that grows the line buffer across chunks.
304
+ // the next chunk after measuring the line it belongs to.
228
305
  let tail = chunk.slice(lineStart);
229
- let lineLength = this.lineBytes + tail.length;
230
- if (lineLength > this.maxLineLength) {
231
- const err = new Error(`Line length ${lineLength} exceeds maximum allowed size of ${this.maxLineLength} bytes`);
232
- err.code = 'LineTooLarge';
233
- err.lineLength = lineLength;
234
- err.maxSize = this.maxLineLength;
235
- this.emit('error', err);
306
+ if (!this.checkLineLength(this.lineBytes + tail.length)) {
236
307
  return;
237
308
  }
238
- this.lineBytes = lineLength;
309
+ this.lineBytes += tail.length;
239
310
  this.lineBuffer.push(tail);
240
311
  }
241
312
  break;
@@ -273,10 +344,12 @@ class ImapStream extends Transform {
273
344
  async processInput() {
274
345
  let data;
275
346
  let processedCount = 0;
276
- while ((data = this.inputQueue.shift())) {
347
+ while (!this.destroyed && (data = this.inputQueue.shift())) {
348
+ this.activeInput = data;
277
349
  await this.processInputChunk(data.chunk);
350
+ this.activeInput = null;
278
351
  // mark chunk as processed
279
- data.next();
352
+ this.releaseInput(data);
280
353
 
281
354
  // Yield to event loop every 10 chunks to prevent CPU blocking
282
355
  processedCount++;
@@ -317,6 +390,12 @@ class ImapStream extends Transform {
317
390
  });
318
391
  }
319
392
 
393
+ // A terminal parser failure must not accept any more protocol input, even if the
394
+ // transport delivers a chunk that was already in flight.
395
+ if (this.destroyed) {
396
+ return next();
397
+ }
398
+
320
399
  // Queue the chunk for async processing. The 'next' callback serves as
321
400
  // backpressure: it is called only after this chunk is fully processed,
322
401
  // which signals the writable side that more data can be accepted.
@@ -325,7 +404,7 @@ class ImapStream extends Transform {
325
404
  if (!this.processingInput) {
326
405
  this.processingInput = true;
327
406
  this.processInput()
328
- .catch(err => this.emit('error', err))
407
+ .catch(err => this.failStream(err))
329
408
  .finally(() => (this.processingInput = false));
330
409
  }
331
410
  }
@@ -347,18 +426,30 @@ class ImapStream extends Transform {
347
426
  * @param {Function} callback - Callback to signal destruction completion.
348
427
  */
349
428
  _destroy(err, callback) {
429
+ // Destruction is the single release point for parser-owned callbacks, so a terminal
430
+ // failure can never leave the writable side or the processing loop waiting.
350
431
  this.inputBuffer = [];
351
432
  this.lineBuffer = [];
352
433
  this.lineBytes = 0;
353
434
  this.literalBuffer = [];
354
435
  this.literals = [];
355
- // Clear inputQueue and call any pending callbacks
436
+
437
+ // Settle an in-flight push() wait so processInput() can unwind
438
+ if (typeof this.pendingPush === 'function') {
439
+ const resolve = this.pendingPush;
440
+ this.pendingPush = null;
441
+ resolve();
442
+ }
443
+
444
+ // Release the chunk currently being processed, then everything still queued.
445
+ // releaseInput() is idempotent, so the processing loop releasing the same chunk
446
+ // afterwards is a no-op.
447
+ this.releaseInput(this.activeInput);
448
+ this.activeInput = null;
356
449
  while (this.inputQueue.length) {
357
- const item = this.inputQueue.shift();
358
- if (typeof item.next === 'function') {
359
- item.next();
360
- }
450
+ this.releaseInput(this.inputQueue.shift());
361
451
  }
452
+
362
453
  callback(err);
363
454
  }
364
455
  }
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ // Shared response-size limits for the IMAP parser. Kept in one place so the streaming parser
4
+ // (ImapStream) and the standalone token parser cannot drift apart, and so the documented
5
+ // defaults in imap-flow.d.ts describe both paths.
6
+
7
+ // Maximum allowed literal size: 1GB (1073741824 bytes)
8
+ const MAX_LITERAL_SIZE = 1024 * 1024 * 1024;
9
+
10
+ // Default maximum length of a single line (a response without a literal). Matches the literal cap:
11
+ // large literal-free responses (e.g. big SEARCH/LIST results) are legitimate, so this bound exists
12
+ // only to stop a server that never sends a line terminator, not to constrain normal traffic.
13
+ const MAX_LINE_SIZE = MAX_LITERAL_SIZE;
14
+
15
+ /**
16
+ * Normalizes a configured size limit. A non-negative integer is honored as-is (including 0, which
17
+ * means "reject anything non-empty"); anything else falls back to the default, so an explicit 0 is
18
+ * not silently swallowed the way `value || DEFAULT` would swallow it.
19
+ *
20
+ * @param {*} value - The configured value.
21
+ * @param {number} defaultValue - Fallback when the value is not a usable limit.
22
+ * @returns {number} The normalized limit.
23
+ */
24
+ const normalizeLimit = (value, defaultValue) => (Number.isInteger(value) && value >= 0 ? value : defaultValue);
25
+
26
+ /**
27
+ * Builds the `LiteralTooLarge` error. One shape for every place a literal is refused, so callers
28
+ * can rely on `code`, `literalSize` and `maxSize` regardless of which parser rejected it.
29
+ *
30
+ * @param {number} literalSize - The declared literal size.
31
+ * @param {number} maxSize - The bound that was exceeded.
32
+ * @param {string} [reason] - What the bound was, when it is not the configured maximum.
33
+ * @returns {Error} The error to emit or throw.
34
+ */
35
+ const createLiteralTooLargeError = (literalSize, maxSize, reason) => {
36
+ const err = new Error(`Literal size ${literalSize} exceeds ${reason || `maximum allowed size of ${maxSize} bytes`}`);
37
+ err.code = 'LiteralTooLarge';
38
+ err.literalSize = literalSize;
39
+ err.maxSize = maxSize;
40
+ return err;
41
+ };
42
+
43
+ module.exports = { MAX_LITERAL_SIZE, MAX_LINE_SIZE, normalizeLimit, createLiteralTooLargeError };
@@ -3,6 +3,7 @@
3
3
  'use strict';
4
4
 
5
5
  const imapFormalSyntax = require('./imap-formal-syntax');
6
+ const { MAX_LITERAL_SIZE, normalizeLimit, createLiteralTooLargeError } = require('./limits');
6
7
 
7
8
  const STATE_ATOM = 0x001;
8
9
  const STATE_LITERAL = 0x002;
@@ -34,12 +35,18 @@ class TokenParser {
34
35
  * @param {Object} [options] - Parser options.
35
36
  * @param {boolean} [options.literalPlus] - Whether the LITERAL+ extension is in use.
36
37
  * @param {Array<Buffer>} [options.literals] - Pre-parsed literal values from the input stream.
38
+ * @param {number} [options.maxLiteralSize] - Maximum size (in bytes) of a literal parsed inline
39
+ * from the input, i.e. when no pre-parsed literal buffers were supplied. Defaults to 1GB.
37
40
  */
38
41
  constructor(parent, startPos, str, options) {
39
42
  this.str = (str || '').toString();
40
43
  this.options = options || {};
41
44
  this.parent = parent;
42
45
 
46
+ // Same normalization and default as the streaming parser, so a direct user of this parser
47
+ // gets the same bound (an explicit 0 means "reject any non-empty inline literal").
48
+ this.maxLiteralSize = normalizeLimit(this.options.maxLiteralSize, MAX_LITERAL_SIZE);
49
+
43
50
  this.tree = this.currentNode = this.createNode();
44
51
  this.pos = startPos || 0;
45
52
 
@@ -611,8 +618,31 @@ class TokenParser {
611
618
  this.state = STATE_NORMAL;
612
619
  checkSP();
613
620
  } else {
621
+ // No pre-parsed literal buffers were supplied, so the literal is read
622
+ // inline from this input and its declared length decides an
623
+ // allocation. ImapStream always supplies buffers (and has already
624
+ // enforced its own cap), so this branch means the parser is being used
625
+ // directly and the declared length is untrusted: bound it before
626
+ // allocating anything.
627
+ // Two bounds apply: the configured maximum, and the bytes actually
628
+ // available here - an inline literal has to be present in the input
629
+ // being parsed, so a longer declaration can never be satisfied and
630
+ // must not reserve memory for itself.
631
+ let available = this.str.length - i - 1;
632
+ let literalLength = this.currentNode.literalLength;
633
+ if (literalLength > this.maxLiteralSize || literalLength > available) {
634
+ let overMax = literalLength > this.maxLiteralSize;
635
+ let error = createLiteralTooLargeError(
636
+ literalLength,
637
+ overMax ? this.maxLiteralSize : available,
638
+ overMax ? null : `the ${available} bytes available in the input`
639
+ );
640
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
641
+ throw error;
642
+ }
643
+
614
644
  this.currentNode.started = true;
615
- // Allocate expected size buffer. Max size check is already performed
645
+ // Allocate expected size buffer.
616
646
  // Maybe should use allocUnsafe instead?
617
647
  this.currentNode.chBuffer = Buffer.alloc(this.currentNode.literalLength);
618
648
  this.currentNode.chPos = 0;
@@ -42,7 +42,22 @@ export interface ImapFlowOptions {
42
42
  verifyOnly?: boolean;
43
43
  /** If true and verifyOnly is set, lists mailboxes */
44
44
  includeMailboxes?: boolean;
45
- /** Proxy URL. Supports HTTP CONNECT (http:, https:) and SOCKS (socks:, socks4:, socks5:) proxies */
45
+ /**
46
+ * Proxy URL. Supports HTTP CONNECT (http:, https:) and SOCKS (socks:, socks4:, socks4a:,
47
+ * socks5:) proxies. IPv6 proxy endpoints are given in URL form, e.g. `socks5://[2001:db8::1]:1080`.
48
+ *
49
+ * DNS behaviour depends on the proxy protocol:
50
+ * - `http:`/`https:` - the destination hostname is sent to the proxy unresolved
51
+ * - `socks4:` - destination hostnames are resolved locally to IPv4 (SOCKS4 has no IPv6
52
+ * destination address type; IPv6 destinations are rejected)
53
+ * - `socks4a:` - destination hostnames are sent to the proxy for remote DNS (IPv6
54
+ * destinations are rejected)
55
+ * - `socks:`/`socks5:` - destination hostnames are sent to the proxy for remote DNS, IPv4
56
+ * and IPv6 literals are passed through
57
+ *
58
+ * The proxy endpoint itself is never resolved by ImapFlow; a hostname endpoint is handed to
59
+ * Node as-is. Proxy DNS and negotiation run inside `connectionTimeout`.
60
+ */
46
61
  proxy?: string;
47
62
  /** If true, then use QRESYNC instead of CONDSTORE. EXPUNGE notifications will include UID instead of sequence number */
48
63
  qresync?: boolean;
@@ -56,7 +71,11 @@ export interface ImapFlowOptions {
56
71
  disableAutoEnable?: boolean;
57
72
  /** If true, do not enable IMAP4rev2 mode even if the server supports it */
58
73
  disableIMAP4rev2?: boolean;
59
- /** How long to wait for the connection to be established. Defaults to 90 seconds */
74
+ /**
75
+ * How long to wait for a usable transport, covering DNS resolution, proxy negotiation and the
76
+ * TCP/TLS handshake as a single budget. Defaults to 90 seconds. An expiry in any of those
77
+ * phases rejects with error code `CONNECT_TIMEOUT`.
78
+ */
60
79
  connectionTimeout?: number;
61
80
  /** How long to wait for the greeting. Defaults to 16 seconds */
62
81
  greetingTimeout?: number;
@@ -65,12 +84,17 @@ export interface ImapFlowOptions {
65
84
  /**
66
85
  * Maximum allowed length in bytes of a single response line (a response without a literal).
67
86
  * Guards against a malicious or broken server that never sends a line terminator. Defaults to
68
- * 1GB.
87
+ * 1GB. The line terminator counts towards the limit and a line exactly at the limit is
88
+ * accepted. Exceeding it is terminal: the connection fails with error code `LineTooLarge` and
89
+ * no further input is parsed.
69
90
  */
70
91
  maxLineLength?: number;
71
92
  /**
72
93
  * Maximum allowed size in bytes of a single IMAP literal block. Bounds peak memory allocation
73
- * against a malicious or broken server announcing an oversized literal. Defaults to 1GB.
94
+ * against a malicious or broken server announcing an oversized literal. Defaults to 1GB. A
95
+ * literal exactly at the limit is accepted. Exceeding it is terminal: the connection fails
96
+ * with error code `LiteralTooLarge`, and neither the marker line nor any byte of the rejected
97
+ * literal is interpreted as protocol.
74
98
  */
75
99
  maxLiteralSize?: number;
76
100
  /**
@@ -184,6 +208,8 @@ export interface ListResponse {
184
208
  flags: Set<string>;
185
209
  /** One of special-use flags (if applicable) */
186
210
  specialUse?: string;
211
+ /** How specialUse was determined: "user" (from specialUseHints), "extension" (SPECIAL-USE or XLIST flag reported by the server) or "name" (matched against known localized folder names) */
212
+ specialUseSource?: 'user' | 'extension' | 'name';
187
213
  /** True if mailbox was found from the output of LIST command */
188
214
  listed: boolean;
189
215
  /** True if the mailbox is subscribed - reported by LSUB or by LIST RETURN (SUBSCRIBED) on LIST-EXTENDED/IMAP4rev2 servers */
@@ -222,6 +248,8 @@ export interface ListOptions {
222
248
  junk?: string;
223
249
  /** Path to "Drafts" folder */
224
250
  drafts?: string;
251
+ /** Path to "Archive" folder */
252
+ archive?: string;
225
253
  };
226
254
  }
227
255
 
@@ -875,7 +903,7 @@ export class ImapFlow extends EventEmitter {
875
903
  /** Mailbox was opened */
876
904
  on(event: 'mailboxOpen', listener: (mailbox: MailboxObject) => void): this;
877
905
 
878
- /** Mailbox was closed */
906
+ /** Mailbox was closed, either explicitly or because the connection went away while a mailbox was still selected */
879
907
  on(event: 'mailboxClose', listener: (mailbox: MailboxObject) => void): this;
880
908
 
881
909
  /** Log event if emitLogs=true */