imapflow 1.2.8 → 1.2.10

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 (58) hide show
  1. package/.ncurc.js +1 -1
  2. package/.release-please-manifest.json +1 -1
  3. package/CHANGELOG.md +14 -0
  4. package/README.md +36 -58
  5. package/eslint.config.js +18 -16
  6. package/lib/charsets.js +15 -0
  7. package/lib/commands/append.js +62 -54
  8. package/lib/commands/authenticate.js +99 -52
  9. package/lib/commands/capability.js +12 -2
  10. package/lib/commands/close.js +11 -1
  11. package/lib/commands/compress.js +10 -1
  12. package/lib/commands/copy.js +18 -1
  13. package/lib/commands/create.js +15 -2
  14. package/lib/commands/delete.js +10 -1
  15. package/lib/commands/enable.js +12 -1
  16. package/lib/commands/expunge.js +18 -2
  17. package/lib/commands/fetch.js +39 -4
  18. package/lib/commands/id.js +22 -3
  19. package/lib/commands/idle.js +39 -4
  20. package/lib/commands/list.js +86 -48
  21. package/lib/commands/login.js +12 -1
  22. package/lib/commands/logout.js +11 -2
  23. package/lib/commands/move.js +17 -1
  24. package/lib/commands/namespace.js +32 -2
  25. package/lib/commands/noop.js +6 -1
  26. package/lib/commands/quota.js +33 -14
  27. package/lib/commands/rename.js +13 -1
  28. package/lib/commands/search.js +16 -1
  29. package/lib/commands/select.js +76 -33
  30. package/lib/commands/starttls.js +6 -1
  31. package/lib/commands/status.js +64 -52
  32. package/lib/commands/store.js +27 -4
  33. package/lib/commands/subscribe.js +7 -1
  34. package/lib/commands/unsubscribe.js +7 -1
  35. package/lib/handler/imap-compiler.js +44 -2
  36. package/lib/handler/imap-formal-syntax.js +51 -3
  37. package/lib/handler/imap-handler.js +8 -0
  38. package/lib/handler/imap-parser.js +23 -2
  39. package/lib/handler/imap-stream.js +84 -31
  40. package/lib/handler/parser-instance.js +61 -1
  41. package/lib/handler/token-parser.js +66 -9
  42. package/lib/imap-commands.js +11 -0
  43. package/lib/imap-flow.d.ts +6 -0
  44. package/lib/imap-flow.js +175 -46
  45. package/lib/jp-decoder.js +10 -0
  46. package/lib/limited-passthrough.js +12 -5
  47. package/lib/proxy-connection.js +18 -12
  48. package/lib/search-compiler.js +3 -11
  49. package/lib/special-use.js +23 -16
  50. package/lib/tools.js +218 -13
  51. package/package.json +5 -17
  52. package/test/commands-integration-test.js +33 -0
  53. package/test/connection-edge-cases-test.js +105 -0
  54. package/test/special-use-test.js +32 -0
  55. package/.babelrc +0 -6
  56. package/.eslintrc +0 -16
  57. package/assets/favicon.ico +0 -0
  58. package/jsdoc.json +0 -28
@@ -16,7 +16,25 @@ const CURLY_CLOSE = 0x7d;
16
16
  // Maximum allowed literal size: 1GB (1073741824 bytes)
17
17
  const MAX_LITERAL_SIZE = 1024 * 1024 * 1024;
18
18
 
19
+ /**
20
+ * A Transform stream that parses raw IMAP protocol data from a socket into structured
21
+ * command/response objects. Reads binary input, splits it into lines delimited by LF,
22
+ * extracts literal data blocks based on IMAP literal size markers (e.g., "{123}\r\n"),
23
+ * and emits each complete command as a readable object containing the payload Buffer
24
+ * and any associated literal Buffers. Enforces a maximum literal size of 1GB.
25
+ *
26
+ * @extends Transform
27
+ */
19
28
  class ImapStream extends Transform {
29
+ /**
30
+ * Creates a new ImapStream instance.
31
+ *
32
+ * @param {Object} [options] - Stream options.
33
+ * @param {string} [options.cid] - Connection identifier used for logging.
34
+ * @param {Object} [options.logger] - A pino-compatible logger instance. If not provided, a default child logger is created.
35
+ * @param {boolean} [options.logRaw] - If true, logs raw socket data at trace level.
36
+ * @param {boolean} [options.secureConnection] - Whether the connection uses TLS.
37
+ */
20
38
  constructor(options) {
21
39
  super({
22
40
  //writableHighWaterMark: 3,
@@ -51,6 +69,15 @@ class ImapStream extends Transform {
51
69
  this.inputQueue = []; // unprocessed input chunks
52
70
  }
53
71
 
72
+ /**
73
+ * Checks whether the given line buffer ends with an IMAP literal size marker
74
+ * (e.g., "{123}\r\n"). If a valid marker is found and the literal size is within
75
+ * the allowed maximum, switches the stream state to LITERAL mode and records
76
+ * the expected number of literal bytes.
77
+ *
78
+ * @param {Buffer} line - The line buffer to check for a trailing literal marker.
79
+ * @returns {boolean} True if a valid literal marker was found and literal state was activated, false otherwise.
80
+ */
54
81
  checkLiteralMarker(line) {
55
82
  if (!line || !line.length) {
56
83
  return false;
@@ -58,23 +85,22 @@ class ImapStream extends Transform {
58
85
 
59
86
  let pos = line.length - 1;
60
87
 
61
- if (line[pos] === LF) {
62
- pos--;
63
- } else {
88
+ if (line[pos] !== LF) {
64
89
  return false;
65
90
  }
91
+ pos--;
92
+
66
93
  if (pos >= 0 && line[pos] === CR) {
67
94
  pos--;
68
95
  }
69
- if (pos < 0) {
70
- return false;
71
- }
72
96
 
73
- if (!pos || line[pos] !== CURLY_CLOSE) {
97
+ if (pos < 0 || !pos || line[pos] !== CURLY_CLOSE) {
74
98
  return false;
75
99
  }
76
100
  pos--;
77
101
 
102
+ // Scan backwards through the line to find an IMAP literal marker: {size}\r\n
103
+ // The format is: '{' followed by one or more ASCII digits followed by '}'
78
104
  let numBytes = [];
79
105
  for (; pos > 0; pos--) {
80
106
  let c = line[pos];
@@ -103,6 +129,16 @@ class ImapStream extends Transform {
103
129
  return false;
104
130
  }
105
131
 
132
+ /**
133
+ * Processes a single input chunk of raw data. In LINE state, scans for LF-terminated
134
+ * lines and checks for literal markers. In LITERAL state, collects the expected number
135
+ * of literal bytes. When a complete command (with all its literals) is assembled, it is
136
+ * pushed downstream as a readable object.
137
+ *
138
+ * @param {Buffer} chunk - The raw data chunk to process.
139
+ * @param {number} [startPos=0] - The byte offset within the chunk to start processing from.
140
+ * @returns {Promise<void>}
141
+ */
106
142
  async processInputChunk(chunk, startPos) {
107
143
  startPos = startPos || 0;
108
144
  if (startPos >= chunk.length) {
@@ -164,41 +200,34 @@ class ImapStream extends Transform {
164
200
  }
165
201
 
166
202
  case LITERAL: {
167
- // exactly until end of chunk
168
- if (chunk.length === startPos + this.literalWaiting) {
169
- if (!startPos) {
170
- this.literalBuffer.push(chunk);
171
- } else {
172
- this.literalBuffer.push(chunk.slice(startPos));
173
- }
203
+ const remainingInChunk = chunk.length - startPos;
204
+ const bytesToRead = Math.min(remainingInChunk, this.literalWaiting);
205
+ const partial = startPos === 0 && bytesToRead === chunk.length ? chunk : chunk.slice(startPos, startPos + bytesToRead);
174
206
 
175
- this.literalWaiting -= chunk.length;
176
- this.literals.push(Buffer.concat(this.literalBuffer));
177
- this.literalBuffer = [];
178
- this.state = LINE;
207
+ this.literalBuffer.push(partial);
208
+ this.literalWaiting -= bytesToRead;
179
209
 
180
- return;
181
- } else if (chunk.length > startPos + this.literalWaiting) {
182
- let partial = chunk.slice(startPos, startPos + this.literalWaiting);
183
- this.literalBuffer.push(partial);
184
- startPos += partial.length;
185
- this.literalWaiting -= partial.length;
210
+ if (this.literalWaiting === 0) {
186
211
  this.literals.push(Buffer.concat(this.literalBuffer));
187
212
  this.literalBuffer = [];
188
213
  this.state = LINE;
189
214
 
190
- return await this.processInputChunk(chunk, startPos);
191
- } else {
192
- let partial = chunk.slice(startPos);
193
- this.literalBuffer.push(partial);
194
- startPos += partial.length;
195
- this.literalWaiting -= partial.length;
196
- return;
215
+ if (remainingInChunk > bytesToRead) {
216
+ return await this.processInputChunk(chunk, startPos + bytesToRead);
217
+ }
197
218
  }
219
+ break;
198
220
  }
199
221
  }
200
222
  }
201
223
 
224
+ /**
225
+ * Drains the input queue by processing each queued chunk sequentially.
226
+ * Yields to the event loop every 10 chunks to prevent CPU blocking on
227
+ * large bursts of incoming data.
228
+ *
229
+ * @returns {Promise<void>}
230
+ */
202
231
  async processInput() {
203
232
  let data;
204
233
  let processedCount = 0;
@@ -215,6 +244,15 @@ class ImapStream extends Transform {
215
244
  }
216
245
  }
217
246
 
247
+ /**
248
+ * Transform stream implementation. Receives raw data chunks from the writable side,
249
+ * converts strings to Buffers, tracks total bytes read, optionally logs raw data,
250
+ * and queues the chunk for asynchronous processing.
251
+ *
252
+ * @param {Buffer|string} chunk - The incoming data chunk.
253
+ * @param {string} encoding - The encoding if chunk is a string.
254
+ * @param {Function} next - Callback to signal that this chunk has been consumed.
255
+ */
218
256
  _transform(chunk, encoding, next) {
219
257
  if (typeof chunk === 'string') {
220
258
  chunk = Buffer.from(chunk, encoding);
@@ -237,6 +275,9 @@ class ImapStream extends Transform {
237
275
  });
238
276
  }
239
277
 
278
+ // Queue the chunk for async processing. The 'next' callback serves as
279
+ // backpressure: it is called only after this chunk is fully processed,
280
+ // which signals the writable side that more data can be accepted.
240
281
  if (chunk && chunk.length) {
241
282
  this.inputQueue.push({ chunk, next });
242
283
  }
@@ -249,10 +290,22 @@ class ImapStream extends Transform {
249
290
  }
250
291
  }
251
292
 
293
+ /**
294
+ * Flush implementation called when the writable side ends. Signals completion immediately.
295
+ *
296
+ * @param {Function} next - Callback to signal flush completion.
297
+ */
252
298
  _flush(next) {
253
299
  next();
254
300
  }
255
301
 
302
+ /**
303
+ * Destroy implementation for cleanup. Clears all internal buffers, drains the input queue
304
+ * by invoking pending callbacks, and forwards the error (if any) to the callback.
305
+ *
306
+ * @param {Error|null} err - The error that caused destruction, or null.
307
+ * @param {Function} callback - Callback to signal destruction completion.
308
+ */
256
309
  _destroy(err, callback) {
257
310
  this.inputBuffer = [];
258
311
  this.lineBuffer = [];
@@ -6,7 +6,20 @@ const imapFormalSyntax = require('./imap-formal-syntax');
6
6
 
7
7
  const { TokenParser } = require('./token-parser');
8
8
 
9
+ /**
10
+ * Parses a single IMAP response line into its structural components: tag, command,
11
+ * and attributes. Handles status responses (OK, NO, BAD, PREAUTH, BYE) with their
12
+ * human-readable text and response codes, as well as continuation responses ("+").
13
+ */
9
14
  class ParserInstance {
15
+ /**
16
+ * Creates a new ParserInstance for parsing an IMAP response line.
17
+ *
18
+ * @param {Buffer|string} input - The raw IMAP response line to parse.
19
+ * @param {Object} [options] - Parser options passed through to the TokenParser for attribute parsing.
20
+ * @param {boolean} [options.literalPlus] - Whether the LITERAL+ extension is in use.
21
+ * @param {Array<Buffer>} [options.literals] - Pre-parsed literal values from the stream.
22
+ */
10
23
  constructor(input, options) {
11
24
  this.input = (input || '').toString();
12
25
  this.options = options || {};
@@ -14,6 +27,14 @@ class ParserInstance {
14
27
  this.pos = 0;
15
28
  }
16
29
 
30
+ /**
31
+ * Extracts and returns the IMAP tag from the beginning of the response.
32
+ * The tag is typically "*" for untagged responses, "+" for continuation requests,
33
+ * or a client-assigned command tag like "A1".
34
+ *
35
+ * @returns {Promise<string>} The parsed tag string.
36
+ * @throws {Error} If the tag contains invalid characters.
37
+ */
17
38
  async getTag() {
18
39
  if (!this.tag) {
19
40
  this.tag = await this.getElement(imapFormalSyntax.tag() + '*+', true);
@@ -21,6 +42,15 @@ class ParserInstance {
21
42
  return this.tag;
22
43
  }
23
44
 
45
+ /**
46
+ * Extracts and returns the IMAP command or response name from the input.
47
+ * For continuation responses (tag "+"), returns an empty string and stores
48
+ * the remainder as human-readable text. For status responses (OK, NO, BAD,
49
+ * PREAUTH, BYE), separates the optional response code from the human-readable text.
50
+ *
51
+ * @returns {Promise<string>} The parsed command string.
52
+ * @throws {Error} If the command contains invalid characters or input ends unexpectedly.
53
+ */
24
54
  async getCommand() {
25
55
  if (this.tag === '+') {
26
56
  // special case
@@ -34,6 +64,10 @@ class ParserInstance {
34
64
  this.command = await this.getElement(imapFormalSyntax.command());
35
65
  }
36
66
 
67
+ // Status responses have the format: TAG OK/NO/BAD [response-code] human-readable text
68
+ // Example: * OK [CAPABILITY IMAP4rev1] Server ready
69
+ // Example: A1 NO [AUTHENTICATIONFAILED] Invalid credentials
70
+ // We need to separate the optional [response-code] from the human-readable text.
37
71
  switch ((this.command || '').toString().toUpperCase()) {
38
72
  case 'OK':
39
73
  case 'NO':
@@ -69,6 +103,14 @@ class ParserInstance {
69
103
  return this.command;
70
104
  }
71
105
 
106
+ /**
107
+ * Extracts the next whitespace-delimited element from the input and validates it
108
+ * against the given syntax character set. Advances the parser position past the element.
109
+ *
110
+ * @param {string} syntax - A string of allowed characters for the element (as returned by imap-formal-syntax methods).
111
+ * @returns {Promise<string>} The extracted element string.
112
+ * @throws {Error} If the element contains characters not in the syntax set, or if input ends unexpectedly.
113
+ */
72
114
  async getElement(syntax) {
73
115
  let match, element, errPos;
74
116
 
@@ -83,7 +125,10 @@ class ParserInstance {
83
125
  element = match[0];
84
126
  if ((errPos = imapFormalSyntax.verify(element, syntax)) >= 0) {
85
127
  if (this.tag === 'Server' && element === 'Unavailable.') {
86
- // Exchange error
128
+ // Microsoft Exchange sometimes sends a non-standard response
129
+ // "Server Unavailable." instead of a proper IMAP tagged/untagged response.
130
+ // We detect this specific pattern and convert it into a synthetic BAD response
131
+ // so the rest of the parser can handle it gracefully.
87
132
  let error = new Error(`Server returned an error: ${this.input}`);
88
133
  error.code = 'ParserErrorExchange';
89
134
  error.parserContext = {
@@ -117,6 +162,13 @@ class ParserInstance {
117
162
  return element;
118
163
  }
119
164
 
165
+ /**
166
+ * Consumes a single space character from the current position in the input.
167
+ * Advances the parser position by one.
168
+ *
169
+ * @returns {Promise<void>}
170
+ * @throws {Error} If the current character is not a space, or if input has ended unexpectedly.
171
+ */
120
172
  async getSpace() {
121
173
  if (!this.remainder.length) {
122
174
  if (this.tag === '+' && this.pos === 1) {
@@ -141,6 +193,14 @@ class ParserInstance {
141
193
  this.remainder = this.remainder.substr(1);
142
194
  }
143
195
 
196
+ /**
197
+ * Parses the remaining input as IMAP attributes using the TokenParser.
198
+ * This handles complex structures including nested lists, literals, strings,
199
+ * atoms, sections, sequences, and partial ranges.
200
+ *
201
+ * @returns {Promise<Array>} A promise that resolves to an array of parsed attribute objects.
202
+ * @throws {Error} If the input contains unexpected whitespace, invalid characters, or ends unexpectedly.
203
+ */
144
204
  async getAttributes() {
145
205
  if (!this.remainder.length) {
146
206
  let error = new Error(`Unexpected end of input at position ${this.pos} [E6]`);
@@ -15,9 +15,27 @@ const STATE_TEXT = 0x007;
15
15
  const RE_DIGITS = /^\d+$/;
16
16
  const RE_SINGLE_DIGIT = /^\d$/;
17
17
 
18
+ // Prevents stack overflow from maliciously crafted deeply-nested IMAP input (e.g., (((((...))))))
18
19
  const MAX_NODE_DEPTH = 25;
19
20
 
21
+ /**
22
+ * Tokenizes an IMAP attribute string into a tree of typed nodes.
23
+ * Handles all IMAP data types: atoms, quoted strings, literals (including literal8),
24
+ * sequences, lists (parenthesized groups), sections (bracketed groups), and partial ranges.
25
+ * Enforces a maximum nesting depth of {@link MAX_NODE_DEPTH} to prevent stack overflow
26
+ * from malicious input.
27
+ */
20
28
  class TokenParser {
29
+ /**
30
+ * Creates a new TokenParser.
31
+ *
32
+ * @param {ParserInstance} parent - The parent ParserInstance that owns this token parser. Used to access the parsed command for context-sensitive parsing.
33
+ * @param {number} startPos - The starting position offset in the original input, used for error reporting.
34
+ * @param {string} str - The attribute string to tokenize.
35
+ * @param {Object} [options] - Parser options.
36
+ * @param {boolean} [options.literalPlus] - Whether the LITERAL+ extension is in use.
37
+ * @param {Array<Buffer>} [options.literals] - Pre-parsed literal values from the input stream.
38
+ */
21
39
  constructor(parent, startPos, str, options) {
22
40
  this.str = (str || '').toString();
23
41
  this.options = options || {};
@@ -31,6 +49,15 @@ class TokenParser {
31
49
  this.state = STATE_NORMAL;
32
50
  }
33
51
 
52
+ /**
53
+ * Processes the input string and returns the parsed attributes as a flat array of typed objects.
54
+ * Each attribute is an object with a `type` (e.g., "ATOM", "STRING", "LITERAL", "SEQUENCE")
55
+ * and a `value` property. Lists are represented as nested arrays. Sections and partials are
56
+ * attached as properties on the preceding attribute object.
57
+ *
58
+ * @returns {Promise<Array>} A promise that resolves to an array of parsed attribute objects and nested arrays.
59
+ * @throws {Error} If the input contains syntax errors or unclosed nodes.
60
+ */
34
61
  async getAttributes() {
35
62
  await this.processString();
36
63
 
@@ -108,6 +135,16 @@ class TokenParser {
108
135
  return attributes;
109
136
  }
110
137
 
138
+ /**
139
+ * Creates a new node in the parse tree. Each node represents a token or structural
140
+ * element (e.g., atom, string, literal, list, section, partial). The node is automatically
141
+ * appended to the parent's childNodes array if a parent is provided.
142
+ *
143
+ * @param {Object} [parentNode] - The parent node to attach this node to. If omitted, creates a root node.
144
+ * @param {number} [startPos] - The starting position of this node in the original input string.
145
+ * @returns {Object} The newly created node with childNodes, type, value, and isClosed properties.
146
+ * @throws {Error} If the nesting depth exceeds MAX_NODE_DEPTH.
147
+ */
111
148
  createNode(parentNode, startPos) {
112
149
  let node = {
113
150
  childNodes: [],
@@ -141,6 +178,15 @@ class TokenParser {
141
178
  return node;
142
179
  }
143
180
 
181
+ /**
182
+ * Processes the entire input string character by character using a state machine.
183
+ * Transitions between states (NORMAL, ATOM, STRING, LITERAL, SEQUENCE, PARTIAL, TEXT)
184
+ * based on the current character and builds the parse tree. This is the main parsing
185
+ * loop that drives the tokenization.
186
+ *
187
+ * @returns {Promise<void>}
188
+ * @throws {Error} If the input contains unexpected characters, unclosed structures, or other syntax errors.
189
+ */
144
190
  async processString() {
145
191
  let chr, i, len;
146
192
 
@@ -203,8 +249,11 @@ class TokenParser {
203
249
  checkSP();
204
250
  break;
205
251
 
206
- // < starts a new partial
252
+ // < starts a new partial byte range (e.g., BODY[]<0.1024>)
207
253
  case '<':
254
+ // '<' is only a partial range marker when it immediately follows ']',
255
+ // which occurs in BODY[section]<origin.length> responses.
256
+ // In all other contexts, '<' is treated as the start of an ATOM.
208
257
  if (this.str.charAt(i - 1) !== ']') {
209
258
  this.currentNode = this.createNode(this.currentNode, this.pos + i);
210
259
  this.currentNode.type = 'ATOM';
@@ -218,12 +267,13 @@ class TokenParser {
218
267
  }
219
268
  break;
220
269
 
221
- // binary literal8
270
+ // literal8 (RFC 3516): uses ~{size} prefix instead of {size}
271
+ // literal8 allows binary data containing NUL bytes, unlike regular literals
222
272
  case '~': {
223
273
  let nextChr = this.str.charAt(i + 1);
224
274
  if (nextChr !== '{') {
225
275
  if (imapFormalSyntax['ATOM-CHAR']().indexOf(nextChr) >= 0) {
226
- // treat as ATOM
276
+ // '~' not followed by '{' but followed by an ATOM char: treat as ATOM
227
277
  this.currentNode = this.createNode(this.currentNode, this.pos + i);
228
278
  this.currentNode.type = 'ATOM';
229
279
  this.currentNode.value = chr;
@@ -236,14 +286,16 @@ class TokenParser {
236
286
  error.parserContext = { input: this.str, pos: this.pos + i, chr };
237
287
  throw error;
238
288
  }
289
+ // Mark the next literal as literal8 type; consumed when '{' is encountered
239
290
  this.expectedLiteralType = 'literal8';
240
291
  break;
241
292
  }
242
293
 
243
- // { starts a new literal
294
+ // { starts a new literal (regular {size}\r\n or literal8 ~{size}\r\n)
244
295
  case '{':
245
296
  this.currentNode = this.createNode(this.currentNode, this.pos + i);
246
297
  this.currentNode.type = 'LITERAL';
298
+ // Use literal8 type if '~' was seen immediately before, otherwise standard literal
247
299
  this.currentNode.literalType = this.expectedLiteralType || 'literal';
248
300
  this.expectedLiteralType = false;
249
301
  this.state = STATE_LITERAL;
@@ -267,6 +319,7 @@ class TokenParser {
267
319
  // [ starts section
268
320
  case '[':
269
321
  // If it is the *first* element after response command, then process as a response argument list
322
+ // Status responses (OK/NO/BAD/BYE/PREAUTH) use [code] for response codes
270
323
  if (['OK', 'NO', 'BAD', 'BYE', 'PREAUTH'].includes(this.parent.command.toUpperCase()) && this.currentNode === this.tree) {
271
324
  this.currentNode.endPos = this.pos + i;
272
325
 
@@ -278,10 +331,10 @@ class TokenParser {
278
331
  this.currentNode.isClosed = false;
279
332
  this.state = STATE_NORMAL;
280
333
 
281
- // RFC2221 defines a response code REFERRAL whose payload is an
282
- // RFC2192/RFC5092 imapurl that we will try to parse as an ATOM but
283
- // fail quite badly at parsing. Since the imapurl is such a unique
284
- // (and crazy) term, we just specialize that case here.
334
+ // RFC 2221 REFERRAL special case: the payload is an RFC 2192/RFC 5092
335
+ // IMAP URL (e.g., imap://user@host/mailbox) which contains characters
336
+ // that would break normal ATOM parsing (colons, slashes, etc.).
337
+ // We handle this by consuming everything up to ']' as a single ATOM value.
285
338
  if (this.str.substr(i + 1, 9).toUpperCase() === 'REFERRAL ') {
286
339
  // create the REFERRAL atom
287
340
  this.currentNode = this.createNode(this.currentNode, this.pos + i + 1);
@@ -332,6 +385,8 @@ class TokenParser {
332
385
  break;
333
386
 
334
387
  case STATE_ATOM:
388
+ // An atom is terminated by: space, closing delimiter of parent node,
389
+ // or encountering a '[' that starts a section for BODY/BINARY commands.
335
390
  // space finishes an atom
336
391
  if (chr === ' ') {
337
392
  this.currentNode.endPos = this.pos + i - 1;
@@ -340,7 +395,7 @@ class TokenParser {
340
395
  break;
341
396
  }
342
397
 
343
- //
398
+ // ')' or ']' terminates the atom AND closes the enclosing LIST or SECTION
344
399
  if (
345
400
  this.currentNode.parentNode &&
346
401
  ((chr === ')' && this.currentNode.parentNode.type === 'LIST') || (chr === ']' && this.currentNode.parentNode.type === 'SECTION'))
@@ -357,6 +412,8 @@ class TokenParser {
357
412
  break;
358
413
  }
359
414
 
415
+ // If the atom so far is all digits and we see ',' or ':', it is actually
416
+ // a sequence set (e.g., "1:5" or "1,3,5"), so reclassify and switch state
360
417
  if ((chr === ',' || chr === ':') && RE_DIGITS.test(this.currentNode.value)) {
361
418
  this.currentNode.type = 'SEQUENCE';
362
419
  this.currentNode.isClosed = true;
@@ -2,6 +2,17 @@
2
2
 
3
3
  'use strict';
4
4
 
5
+ /**
6
+ * IMAP command registry. Maps IMAP command names (uppercase strings) to their
7
+ * corresponding implementation modules from the `lib/commands/` directory.
8
+ *
9
+ * Each entry maps a command name (e.g., "FETCH", "SELECT", "IDLE") to a module
10
+ * that exports functions for building the command request and processing the
11
+ * server response. This Map is used by the main ImapFlow client to look up
12
+ * and execute IMAP commands.
13
+ *
14
+ * @type {Map<string, Object>}
15
+ */
5
16
  module.exports = new Map([
6
17
  ['ID', require('./commands/id.js')],
7
18
  ['CAPABILITY', require('./commands/capability.js')],
@@ -761,6 +761,12 @@ export class ImapFlow extends EventEmitter {
761
761
  /** Opens a mailbox if not already open and returns a lock */
762
762
  getMailboxLock(path: string | string[], options?: MailboxOpenOptions): Promise<MailboxLockObject>;
763
763
 
764
+ /** Returns byte counters for the current connection, optionally resets them */
765
+ stats(reset?: boolean): { sent: number; received: number };
766
+
767
+ /** Detaches sockets from the IMAP pipeline, returns read and write sockets */
768
+ unbind(): { readSocket: any; writeSocket: any };
769
+
764
770
  /** Connection close event */
765
771
  on(event: 'close', listener: () => void): this;
766
772