imapflow 2.5.8 → 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.
Files changed (43) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +9 -0
  3. package/dist/cjs/commands/append.js +6 -3
  4. package/dist/cjs/commands/fetch.d.ts +2 -1
  5. package/dist/cjs/commands/fetch.js +8 -2
  6. package/dist/cjs/commands/idle.js +7 -0
  7. package/dist/cjs/commands/select.js +17 -6
  8. package/dist/cjs/commands/status.js +19 -5
  9. package/dist/cjs/download.js +3 -3
  10. package/dist/cjs/errors.d.ts +3 -1
  11. package/dist/cjs/errors.js +4 -1
  12. package/dist/cjs/handler/imap-parser.js +38 -6
  13. package/dist/cjs/handler/limits.d.ts +1 -0
  14. package/dist/cjs/handler/limits.js +8 -1
  15. package/dist/cjs/handler/token-parser.d.ts +8 -2
  16. package/dist/cjs/handler/token-parser.js +55 -16
  17. package/dist/cjs/handler/types.d.ts +5 -2
  18. package/dist/cjs/imap-flow.d.ts +38 -2
  19. package/dist/cjs/imap-flow.js +99 -15
  20. package/dist/cjs/package-info.d.ts +1 -1
  21. package/dist/cjs/package-info.js +1 -1
  22. package/dist/cjs/types.d.ts +51 -7
  23. package/dist/esm/commands/append.js +6 -3
  24. package/dist/esm/commands/fetch.d.ts +2 -1
  25. package/dist/esm/commands/fetch.js +8 -2
  26. package/dist/esm/commands/idle.js +7 -0
  27. package/dist/esm/commands/select.js +17 -6
  28. package/dist/esm/commands/status.js +19 -5
  29. package/dist/esm/download.js +3 -3
  30. package/dist/esm/errors.d.ts +3 -1
  31. package/dist/esm/errors.js +4 -1
  32. package/dist/esm/handler/imap-parser.js +38 -6
  33. package/dist/esm/handler/limits.d.ts +1 -0
  34. package/dist/esm/handler/limits.js +7 -0
  35. package/dist/esm/handler/token-parser.d.ts +8 -2
  36. package/dist/esm/handler/token-parser.js +56 -17
  37. package/dist/esm/handler/types.d.ts +5 -2
  38. package/dist/esm/imap-flow.d.ts +38 -2
  39. package/dist/esm/imap-flow.js +99 -15
  40. package/dist/esm/package-info.d.ts +1 -1
  41. package/dist/esm/package-info.js +1 -1
  42. package/dist/esm/types.d.ts +51 -7
  43. package/package.json +3 -3
@@ -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
  }
@@ -35,6 +35,7 @@ export declare const ImapFlowErrorCode: {
35
35
  readonly LineTooLarge: "LineTooLarge";
36
36
  readonly LiteralTooLarge: "LiteralTooLarge";
37
37
  readonly ResponseTooLarge: "ResponseTooLarge";
38
+ readonly TooManyTokens: "TooManyTokens";
38
39
  readonly TooManyResponses: "TooManyResponses";
39
40
  readonly InvalidStringValue: "InvalidStringValue";
40
41
  readonly InvalidTokenValue: "InvalidTokenValue";
@@ -50,6 +51,7 @@ export declare const ImapFlowErrorCode: {
50
51
  readonly ERR_INVALID_URL: "ERR_INVALID_URL";
51
52
  readonly UnsupportedTransportOption: "UnsupportedTransportOption";
52
53
  readonly InstanceReused: "InstanceReused";
54
+ readonly NoMailboxSelected: "NoMailboxSelected";
53
55
  };
54
56
  /** One of the {@link ImapFlowErrorCode} values */
55
57
  export type ImapFlowErrorCode = (typeof ImapFlowErrorCode)[keyof typeof ImapFlowErrorCode];
@@ -117,7 +119,7 @@ export interface ImapFlowError extends Error {
117
119
  lineLength?: number | undefined;
118
120
  /** The size of a rejected response */
119
121
  responseSize?: number | undefined;
120
- /** The bound that was exceeded */
122
+ /** The bound that was exceeded (bytes, or tokens for TooManyTokens) */
121
123
  maxSize?: number | undefined;
122
124
  /** OAuth error details from the server, for XOAUTH2 authentication failures */
123
125
  oauthError?: any;
@@ -39,6 +39,7 @@ export const ImapFlowErrorCode = {
39
39
  LineTooLarge: 'LineTooLarge',
40
40
  LiteralTooLarge: 'LiteralTooLarge',
41
41
  ResponseTooLarge: 'ResponseTooLarge',
42
+ TooManyTokens: 'TooManyTokens',
42
43
  // one command received more untagged responses than are kept, see MAX_COLLECTED_RESPONSES
43
44
  TooManyResponses: 'TooManyResponses',
44
45
  // invalid values in a command
@@ -59,7 +60,9 @@ export const ImapFlowErrorCode = {
59
60
  // the transport option
60
61
  UnsupportedTransportOption: 'UnsupportedTransportOption',
61
62
  // API misuse
62
- InstanceReused: 'InstanceReused'
63
+ InstanceReused: 'InstanceReused',
64
+ // a message command (FETCH) was called while no mailbox was selected
65
+ NoMailboxSelected: 'NoMailboxSelected'
63
66
  };
64
67
  /**
65
68
  * Error subclass thrown when IMAP authentication fails.
@@ -3,8 +3,8 @@ import { ParserInstance } from './parser-instance.js';
3
3
  import { createImapError } from '../errors.js';
4
4
  import { boundedInput } from './limits.js';
5
5
  // The codes the parser raises on purpose: the numbered ParserErrorN codes, ParserErrorExchange,
6
- // the nesting limit and the inline literal size limit
7
- const PARSER_ERROR_CODES = /^(?:ParserError\d+|ParserErrorExchange|MAX_IMAP_NESTING_REACHED|LiteralTooLarge)$/;
6
+ // the nesting limit, the inline literal size limit and the token limit
7
+ const PARSER_ERROR_CODES = /^(?:ParserError\d+|ParserErrorExchange|MAX_IMAP_NESTING_REACHED|LiteralTooLarge|TooManyTokens)$/;
8
8
  /**
9
9
  * Anything the parser throws other than one of its own coded errors - a TypeError from a parser
10
10
  * bug, a RangeError from runaway recursion or a bad Buffer offset (which carries a Node code), a
@@ -75,6 +75,39 @@ function readSearchNumbers(str) {
75
75
  numbers.push(value);
76
76
  return numbers;
77
77
  }
78
+ // The CONDSTORE list that ends a SEARCH answer to a MODSEQ query (RFC 7162 section 3.1.5)
79
+ const RE_SEARCH_MODSEQ = /^ \(MODSEQ (\d{1,19})\) *$/i;
80
+ /**
81
+ * Reads an untagged SEARCH response without the tokenizer, see readSearchNumbers(): the numbers,
82
+ * and the MODSEQ value of a trailing CONDSTORE list. A MODSEQ query on a large mailbox matches as
83
+ * many messages as any other, so its answer must not fall back to the tokenizer either.
84
+ *
85
+ * @param str - The rest of the line after the command
86
+ * @returns The numbers, with the MODSEQ list as the attributes the tokenizer would have made of
87
+ * it, or null when the line is left to the tokenizer
88
+ */
89
+ function readSearchResponse(str) {
90
+ // the last character tells whether there is a list to look for, without scanning the line
91
+ if (!str.trimEnd().endsWith(')')) {
92
+ const numbers = readSearchNumbers(str);
93
+ return numbers && { numbers };
94
+ }
95
+ const open = str.lastIndexOf(' (');
96
+ const match = open < 0 ? null : RE_SEARCH_MODSEQ.exec(str.slice(open));
97
+ if (!match) {
98
+ return null;
99
+ }
100
+ const numbers = readSearchNumbers(str.slice(0, open));
101
+ return (numbers && {
102
+ numbers,
103
+ attributes: [
104
+ [
105
+ { type: 'ATOM', value: 'MODSEQ' },
106
+ { type: 'ATOM', value: match[1] }
107
+ ]
108
+ ]
109
+ });
110
+ }
78
111
  /**
79
112
  * Parses a raw IMAP command or response buffer into a structured object.
80
113
  * Handles edge cases such as null-byte-padded responses from buggy servers and
@@ -130,10 +163,9 @@ export default async function parser(command, options) {
130
163
  response.command += ' ' + (await parserInstance.getElement(imapFormalSyntax.command()));
131
164
  }
132
165
  if (response.tag === '*' && (response.command || '').toUpperCase() === 'SEARCH') {
133
- const numbers = readSearchNumbers(parserInstance.remainder);
134
- if (numbers) {
135
- response.numbers = numbers;
136
- return response;
166
+ const search = readSearchResponse(parserInstance.remainder);
167
+ if (search) {
168
+ return Object.assign(response, search);
137
169
  }
138
170
  }
139
171
  if (parserInstance.remainder.trim().length) {
@@ -2,6 +2,7 @@ import { type ImapFlowError } from '../errors.js';
2
2
  export declare const MAX_LITERAL_SIZE: number;
3
3
  export declare const MAX_LINE_SIZE: number;
4
4
  export declare const MAX_RESPONSE_SIZE: number;
5
+ export declare const MAX_RESPONSE_TOKENS = 1000000;
5
6
  export declare const ERROR_CONTEXT_LENGTH = 1024;
6
7
  /**
7
8
  * The input a parse error carries: a bounded prefix with the full length.
@@ -18,6 +18,13 @@ export const MAX_LINE_SIZE = MAX_LITERAL_SIZE;
18
18
  // of exactly the maximum permitted size impossible to receive. Configuring both limits calls for
19
19
  // the same headroom - set maxResponseSize above maxLiteralSize, not equal to it.
20
20
  export const MAX_RESPONSE_SIZE = 2 * MAX_LITERAL_SIZE;
21
+ // Default maximum number of tokens (atoms, strings, literals, lists and the other parse tree
22
+ // nodes) in a single response. The byte limits bound what a server sends, not what parsing it
23
+ // costs: every token becomes a tree node and an attribute object, around a hundred bytes of heap
24
+ // for a two-byte "1 ", so a line of many small tokens costs fifty times its size. A legitimate
25
+ // response stays far below this (the long one, a SEARCH answer of plain numbers, is read without
26
+ // the tokenizer, see parser()), so the bound only stops a server that floods one response.
27
+ export const MAX_RESPONSE_TOKENS = 1000000;
21
28
  // How much of the offending input a parse error carries along. The error travels whole into log
22
29
  // entries (pino copies every property of a logged error) and into the rejection of the command
23
30
  // the line belonged to, and the line can be as long as the configured line cap.
@@ -7,7 +7,8 @@ import type { ParserInstance } from './parser-instance.js';
7
7
  * (ATOM, string, LITERAL, SEQUENCE, LIST, SECTION, PARTIAL).
8
8
  */
9
9
  export interface TokenNode {
10
- childNodes: TokenNode[];
10
+ /** Created with the first child, a leaf token does not carry an empty array */
11
+ childNodes?: TokenNode[] | undefined;
11
12
  type: string | false;
12
13
  value: string | Buffer;
13
14
  isClosed: boolean;
@@ -44,6 +45,9 @@ export declare class TokenParser {
44
45
  options: ParserOptions;
45
46
  parent: TokenParserParent | ParserInstance;
46
47
  maxLiteralSize: number;
48
+ maxResponseTokens: number;
49
+ /** Nodes created so far, checked against maxResponseTokens */
50
+ tokenCount: number;
47
51
  tree: TokenNode;
48
52
  currentNode: TokenNode;
49
53
  pos: number;
@@ -62,6 +66,7 @@ export declare class TokenParser {
62
66
  * @param options.literals - Pre-parsed literal values from the input stream.
63
67
  * @param options.maxLiteralSize - Maximum size (in bytes) of a literal parsed inline
64
68
  * from the input, i.e. when no pre-parsed literal buffers were supplied. Defaults to 1GB.
69
+ * @param options.maxResponseTokens - Maximum number of parse tree nodes, see MAX_RESPONSE_TOKENS.
65
70
  */
66
71
  constructor(parent: TokenParserParent | ParserInstance, startPos?: number | undefined, str?: string | null | undefined, options?: ParserOptions | undefined);
67
72
  /**
@@ -93,7 +98,8 @@ export declare class TokenParser {
93
98
  * @param parentNode - The parent node to attach this node to. If omitted, creates a root node.
94
99
  * @param startPos - The starting position of this node in the original input string.
95
100
  * @returns The newly created node with childNodes, type, value, and isClosed properties.
96
- * @throws {Error} If the nesting depth exceeds MAX_NODE_DEPTH outside a FETCH data item.
101
+ * @throws {Error} If the nesting depth exceeds MAX_NODE_DEPTH outside a FETCH data item, or
102
+ * the response has more than maxResponseTokens nodes.
97
103
  */
98
104
  createNode(parentNode?: TokenNode | undefined, startPos?: number | undefined): TokenNode;
99
105
  /**
@@ -1,6 +1,6 @@
1
1
  /* eslint new-cap: 0 */
2
2
  import imapFormalSyntax from './imap-formal-syntax.js';
3
- import { MAX_LITERAL_SIZE, ERROR_CONTEXT_LENGTH, boundedInput, normalizeLimit, createLiteralTooLargeError } from './limits.js';
3
+ import { MAX_LITERAL_SIZE, MAX_RESPONSE_TOKENS, ERROR_CONTEXT_LENGTH, boundedInput, normalizeLimit, createLiteralTooLargeError } from './limits.js';
4
4
  import { createImapError } from '../errors.js';
5
5
  const STATE_ATOM = 0x001;
6
6
  const STATE_LITERAL = 0x002;
@@ -39,6 +39,7 @@ export class TokenParser {
39
39
  * @param options.literals - Pre-parsed literal values from the input stream.
40
40
  * @param options.maxLiteralSize - Maximum size (in bytes) of a literal parsed inline
41
41
  * from the input, i.e. when no pre-parsed literal buffers were supplied. Defaults to 1GB.
42
+ * @param options.maxResponseTokens - Maximum number of parse tree nodes, see MAX_RESPONSE_TOKENS.
42
43
  */
43
44
  constructor(parent, startPos, str, options) {
44
45
  this.str = (str || '').toString();
@@ -47,6 +48,8 @@ export class TokenParser {
47
48
  // Same normalization and default as the streaming parser, so a direct user of this parser
48
49
  // gets the same bound (an explicit 0 means "reject any non-empty inline literal").
49
50
  this.maxLiteralSize = normalizeLimit(this.options.maxLiteralSize, MAX_LITERAL_SIZE);
51
+ this.maxResponseTokens = normalizeLimit(this.options.maxResponseTokens, MAX_RESPONSE_TOKENS);
52
+ this.tokenCount = 0;
50
53
  this.truncatedAttributes = [];
51
54
  this.tree = this.currentNode = this.createNode();
52
55
  this.pos = startPos || 0;
@@ -83,7 +86,9 @@ export class TokenParser {
83
86
  await this.processString();
84
87
  const attributes = [];
85
88
  let branch = attributes;
86
- let walk = async (node) => {
89
+ // Synchronous on purpose: the recursion is bounded by MAX_NODE_DEPTH, and an async walk
90
+ // allocated a promise per token, which a line of many small tokens multiplies
91
+ let walk = (node) => {
87
92
  let curBranch = branch;
88
93
  let elm;
89
94
  let partial;
@@ -140,12 +145,16 @@ export class TokenParser {
140
145
  branch[branch.length - 1].partial = partial;
141
146
  break;
142
147
  }
143
- for (let childNode of node.childNodes) {
144
- await walk(childNode);
148
+ // Each child is released once it is converted, so the tree and the attributes built
149
+ // from it are not both held whole at the peak of a line of many tokens
150
+ let childNodes = node.childNodes;
151
+ for (let i = 0; childNodes && i < childNodes.length; i++) {
152
+ walk(childNodes[i]);
153
+ childNodes[i] = null;
145
154
  }
146
155
  branch = curBranch;
147
156
  };
148
- await walk(this.tree);
157
+ walk(this.tree);
149
158
  return attributes;
150
159
  }
151
160
  /**
@@ -156,11 +165,20 @@ export class TokenParser {
156
165
  * @param parentNode - The parent node to attach this node to. If omitted, creates a root node.
157
166
  * @param startPos - The starting position of this node in the original input string.
158
167
  * @returns The newly created node with childNodes, type, value, and isClosed properties.
159
- * @throws {Error} If the nesting depth exceeds MAX_NODE_DEPTH outside a FETCH data item.
168
+ * @throws {Error} If the nesting depth exceeds MAX_NODE_DEPTH outside a FETCH data item, or
169
+ * the response has more than maxResponseTokens nodes.
160
170
  */
161
171
  createNode(parentNode, startPos) {
172
+ // The root node is not a token of the response
173
+ if (parentNode && ++this.tokenCount > this.maxResponseTokens) {
174
+ throw createImapError(`Response has more than ${this.maxResponseTokens} tokens`, 'TooManyTokens', {
175
+ maxSize: this.maxResponseTokens,
176
+ parserContext: { ...boundedInput(this.str), pos: startPos }
177
+ });
178
+ }
162
179
  let node = {
163
- childNodes: [],
180
+ // declared up front, so a node keeps one shape when its first child arrives
181
+ childNodes: undefined,
164
182
  type: false,
165
183
  value: '',
166
184
  isClosed: true,
@@ -193,7 +211,7 @@ export class TokenParser {
193
211
  node.startPos = startPos;
194
212
  }
195
213
  if (parentNode) {
196
- parentNode.childNodes.push(node);
214
+ (parentNode.childNodes ||= []).push(node);
197
215
  }
198
216
  return node;
199
217
  }
@@ -205,7 +223,7 @@ export class TokenParser {
205
223
  */
206
224
  isFetchResponse() {
207
225
  let command = this.parent.command;
208
- let first = this.tree.childNodes[0];
226
+ let first = this.tree.childNodes?.[0];
209
227
  return (typeof command === 'string' &&
210
228
  /^\d+$/.test(command) &&
211
229
  !!first &&
@@ -231,7 +249,7 @@ export class TokenParser {
231
249
  // The item is the last child of the list while it is being parsed, so its key is the
232
250
  // child before it. A key is an atom (BODYSTRUCTURE, ENVELOPE, BODY); anything else means
233
251
  // the row itself is malformed, and the key is reported as unknown.
234
- let siblings = item.parentNode ? item.parentNode.childNodes : [];
252
+ let siblings = item.parentNode?.childNodes || [];
235
253
  let key = siblings[siblings.length - 2];
236
254
  this.truncatedAttributes.push(key && key.type === 'ATOM' && typeof key.value === 'string' ? key.value.toUpperCase() : '?');
237
255
  }
@@ -259,13 +277,26 @@ export class TokenParser {
259
277
  // character, which is quadratic in the token length, and the server decides that length:
260
278
  // a 600 KB ESEARCH result took seconds of event loop time
261
279
  let tokenLength = 0;
280
+ // Where the token starts in the input, and whether its value still is that run of the
281
+ // input. A token is read as a run of characters, so its value is sliced from the input
282
+ // instead of grown with +=: each += of a long token adds a cons string of some twenty
283
+ // bytes per character, which the server multiplies with a long token
284
+ let tokenStart = 0;
285
+ let tokenSliced = true;
286
+ // Where the quoted string being read starts, and whether it holds an escape
287
+ let stringStart = 0;
288
+ let stringEscaped = false;
262
289
  let tokenLast = '';
263
290
  let tokenPrev = '';
264
291
  let tokenDigitsOnly = true;
265
292
  let tokenHasStar = false;
266
293
  let tokenHasDot = false;
267
294
  const appendToToken = (c) => {
268
- this.currentNode.value += c;
295
+ if (!tokenLength) {
296
+ tokenStart = i;
297
+ }
298
+ tokenSliced = tokenSliced && i === tokenStart + tokenLength && this.str.charAt(i) === c;
299
+ this.currentNode.value = tokenSliced ? this.str.slice(tokenStart, i + 1) : this.currentNode.value + c;
269
300
  tokenLength++;
270
301
  tokenPrev = tokenLast;
271
302
  tokenLast = c;
@@ -289,6 +320,7 @@ export class TokenParser {
289
320
  const startToken = (first) => {
290
321
  this.currentNode.value = '';
291
322
  tokenLength = 0;
323
+ tokenSliced = true;
292
324
  tokenLast = tokenPrev = '';
293
325
  tokenDigitsOnly = true;
294
326
  tokenHasStar = tokenHasDot = false;
@@ -299,11 +331,14 @@ export class TokenParser {
299
331
  // ImapStream supplies one buffer per literal marker it framed, {0} included. A marker
300
332
  // without a buffer means the line and its literals do not match up, so fail with a parser
301
333
  // error instead of reading a missing buffer or shifting every later literal by one
334
+ // Read by index: shift() moves every remaining buffer, which made a response of many
335
+ // literals quadratic to parse
336
+ let literalIndex = 0;
302
337
  const takeLiteral = (literals) => {
303
- if (!literals.length) {
338
+ if (literalIndex >= literals.length) {
304
339
  throw this.parserError(`Literal without data at position ${this.pos + i} [E35]`, 'ParserError35', this.pos + i, chr);
305
340
  }
306
- return literals.shift();
341
+ return literals[literalIndex++];
307
342
  };
308
343
  // Any ATOM supported char starts a new Atom sequence, otherwise throw an error
309
344
  // Allow \ as the first char for atom to support system flags
@@ -330,6 +365,8 @@ export class TokenParser {
330
365
  this.currentNode = this.createNode(this.currentNode, this.pos + i);
331
366
  this.currentNode.type = 'string';
332
367
  this.state = STATE_STRING;
368
+ stringStart = i + 1;
369
+ stringEscaped = false;
333
370
  this.currentNode.isClosed = false;
334
371
  break;
335
372
  // ( starts a new list
@@ -560,8 +597,11 @@ export class TokenParser {
560
597
  appendToToken(chr);
561
598
  break;
562
599
  case STATE_STRING:
563
- // DQUOTE ends the string sequence
600
+ // DQUOTE ends the string sequence. Its value is taken from the input in one
601
+ // piece, for the same reason tokens are sliced, see appendToToken()
564
602
  if (chr === '"') {
603
+ let value = this.str.slice(stringStart, i);
604
+ this.currentNode.value = stringEscaped ? value.replace(/\\([\s\S])/g, '$1') : value;
565
605
  this.currentNode.endPos = this.pos + i;
566
606
  this.currentNode.isClosed = true;
567
607
  this.currentNode = this.currentNode.parentNode;
@@ -569,15 +609,14 @@ export class TokenParser {
569
609
  checkSP();
570
610
  break;
571
611
  }
572
- // \ Escapes the following char
612
+ // \ Escapes the following char, which is skipped here so it can not end the string
573
613
  if (chr === '\\') {
574
614
  i++;
575
615
  if (i >= len) {
576
616
  throw this.parserError(`Unexpected end of input at position ${this.pos + i} [E18]`, 'ParserError18', this.pos + i);
577
617
  }
578
- chr = this.str.charAt(i);
618
+ stringEscaped = true;
579
619
  }
580
- this.currentNode.value += chr;
581
620
  break;
582
621
  case STATE_PARTIAL:
583
622
  if (chr === '>') {
@@ -38,8 +38,9 @@ export interface ImapResponse {
38
38
  /** Parsed attributes of the response */
39
39
  attributes?: ImapAttributeList | undefined;
40
40
  /**
41
- * The numbers of an untagged SEARCH response (`* SEARCH 2 5 9`), set instead of `attributes`
42
- * when the line holds nothing else, see parser()
41
+ * The numbers of an untagged SEARCH response (`* SEARCH 2 5 9`), set when the line holds
42
+ * nothing else but a CONDSTORE `(MODSEQ n)` list, which is then the only entry of
43
+ * `attributes`, see parser()
43
44
  */
44
45
  numbers?: number[] | undefined;
45
46
  /** Number of leading NUL bytes stripped from the line, if any */
@@ -79,6 +80,8 @@ export interface ParserOptions {
79
80
  literals?: Buffer[] | undefined;
80
81
  /** Maximum size in bytes of a literal parsed inline from the input */
81
82
  maxLiteralSize?: number | undefined;
83
+ /** Maximum number of tokens in the response, see MAX_RESPONSE_TOKENS */
84
+ maxResponseTokens?: number | undefined;
82
85
  }
83
86
  /**
84
87
  * One assembled response as pushed by ImapStream: the line payload, its literals and the
@@ -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