imapflow 1.2.7 → 1.2.9

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 (54) 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 +3 -3
  5. package/lib/charsets.js +15 -0
  6. package/lib/commands/append.js +62 -54
  7. package/lib/commands/authenticate.js +99 -52
  8. package/lib/commands/capability.js +12 -2
  9. package/lib/commands/close.js +11 -1
  10. package/lib/commands/compress.js +10 -1
  11. package/lib/commands/copy.js +18 -1
  12. package/lib/commands/create.js +15 -2
  13. package/lib/commands/delete.js +10 -1
  14. package/lib/commands/enable.js +12 -1
  15. package/lib/commands/expunge.js +18 -2
  16. package/lib/commands/fetch.js +39 -4
  17. package/lib/commands/id.js +22 -3
  18. package/lib/commands/idle.js +39 -4
  19. package/lib/commands/list.js +86 -48
  20. package/lib/commands/login.js +12 -1
  21. package/lib/commands/logout.js +11 -2
  22. package/lib/commands/move.js +17 -1
  23. package/lib/commands/namespace.js +32 -2
  24. package/lib/commands/noop.js +6 -1
  25. package/lib/commands/quota.js +33 -14
  26. package/lib/commands/rename.js +13 -1
  27. package/lib/commands/search.js +16 -1
  28. package/lib/commands/select.js +76 -33
  29. package/lib/commands/starttls.js +6 -1
  30. package/lib/commands/status.js +64 -52
  31. package/lib/commands/store.js +27 -4
  32. package/lib/commands/subscribe.js +7 -1
  33. package/lib/commands/unsubscribe.js +7 -1
  34. package/lib/handler/imap-compiler.js +44 -2
  35. package/lib/handler/imap-formal-syntax.js +51 -3
  36. package/lib/handler/imap-handler.js +8 -0
  37. package/lib/handler/imap-parser.js +23 -2
  38. package/lib/handler/imap-stream.js +84 -31
  39. package/lib/handler/parser-instance.js +61 -1
  40. package/lib/handler/token-parser.js +66 -9
  41. package/lib/imap-commands.js +11 -0
  42. package/lib/imap-flow.d.ts +6 -0
  43. package/lib/imap-flow.js +164 -42
  44. package/lib/jp-decoder.js +10 -0
  45. package/lib/limited-passthrough.js +12 -5
  46. package/lib/proxy-connection.js +18 -12
  47. package/lib/search-compiler.js +3 -11
  48. package/lib/special-use.js +23 -16
  49. package/lib/tools.js +218 -13
  50. package/package.json +4 -11
  51. package/test/commands-integration-test.js +33 -0
  52. package/test/special-use-test.js +32 -0
  53. package/assets/favicon.ico +0 -0
  54. package/jsdoc.json +0 -28
@@ -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