imapflow 2.3.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/SECURITY.md +126 -55
  3. package/dist/cjs/commands/append.d.ts +1 -1
  4. package/dist/cjs/commands/append.js +15 -4
  5. package/dist/cjs/commands/compress.js +2 -2
  6. package/dist/cjs/commands/idle.js +2 -3
  7. package/dist/cjs/commands/status.js +2 -4
  8. package/dist/cjs/connection-deadline.d.ts +1 -1
  9. package/dist/cjs/connection-deadline.js +2 -4
  10. package/dist/cjs/download.js +22 -19
  11. package/dist/cjs/errors.d.ts +15 -0
  12. package/dist/cjs/errors.js +22 -0
  13. package/dist/cjs/handler/imap-compiler.d.ts +4 -4
  14. package/dist/cjs/handler/imap-compiler.js +18 -18
  15. package/dist/cjs/handler/imap-parser.js +28 -3
  16. package/dist/cjs/handler/imap-stream.js +9 -8
  17. package/dist/cjs/handler/limits.d.ts +1 -1
  18. package/dist/cjs/handler/limits.js +5 -7
  19. package/dist/cjs/handler/parser-instance.js +23 -38
  20. package/dist/cjs/handler/token-parser.d.ts +12 -0
  21. package/dist/cjs/handler/token-parser.js +44 -104
  22. package/dist/cjs/imap-flow.d.ts +4 -3
  23. package/dist/cjs/imap-flow.js +72 -72
  24. package/dist/cjs/package-info.d.ts +1 -1
  25. package/dist/cjs/package-info.js +1 -1
  26. package/dist/cjs/proxy-connection.js +2 -5
  27. package/dist/cjs/search-compiler.js +2 -3
  28. package/dist/cjs/tools.d.ts +2 -2
  29. package/dist/cjs/tools.js +8 -11
  30. package/dist/esm/commands/append.d.ts +1 -1
  31. package/dist/esm/commands/append.js +15 -4
  32. package/dist/esm/commands/compress.js +2 -2
  33. package/dist/esm/commands/idle.js +2 -3
  34. package/dist/esm/commands/status.js +2 -4
  35. package/dist/esm/connection-deadline.d.ts +1 -1
  36. package/dist/esm/connection-deadline.js +2 -4
  37. package/dist/esm/download.js +22 -19
  38. package/dist/esm/errors.d.ts +15 -0
  39. package/dist/esm/errors.js +21 -0
  40. package/dist/esm/handler/imap-compiler.d.ts +4 -4
  41. package/dist/esm/handler/imap-compiler.js +18 -18
  42. package/dist/esm/handler/imap-parser.js +28 -3
  43. package/dist/esm/handler/imap-stream.js +9 -8
  44. package/dist/esm/handler/limits.d.ts +1 -1
  45. package/dist/esm/handler/limits.js +5 -7
  46. package/dist/esm/handler/parser-instance.js +23 -38
  47. package/dist/esm/handler/token-parser.d.ts +12 -0
  48. package/dist/esm/handler/token-parser.js +44 -104
  49. package/dist/esm/imap-flow.d.ts +4 -3
  50. package/dist/esm/imap-flow.js +73 -73
  51. package/dist/esm/package-info.d.ts +1 -1
  52. package/dist/esm/package-info.js +1 -1
  53. package/dist/esm/proxy-connection.js +2 -5
  54. package/dist/esm/search-compiler.js +2 -3
  55. package/dist/esm/tools.d.ts +2 -2
  56. package/dist/esm/tools.js +9 -12
  57. package/package.json +6 -6
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const imap_formal_syntax_js_1 = __importDefault(require("./imap-formal-syntax.js"));
8
+ const errors_js_1 = require("../errors.js");
8
9
  // A single element of a sequence-set as defined by the RFC 9051 grammar: a number
9
10
  // or a range, where "*" stands for the largest number in use. Digit strings are not
10
11
  // range-checked here (a server rejects "0" or an overlong number on its own); the
@@ -46,13 +47,11 @@ const CRLF = /[\r\n]/;
46
47
  */
47
48
  const quoteString = (value) => {
48
49
  if (NOT_QUOTABLE.test(value)) {
49
- let error = new Error('Unquotable character in IMAP string value');
50
- error.code = 'InvalidStringValue';
51
- throw error;
50
+ throw (0, errors_js_1.createImapError)('Unquotable character in IMAP string value', 'InvalidStringValue');
52
51
  }
53
52
  return '"' + value.replace(/["\\]/g, char => '\\' + char) + '"';
54
53
  };
55
- async function compiler(response, options) {
54
+ function compiler(response, options) {
56
55
  let { asArray, isLogging, literalPlus, literalMinus } = options || {};
57
56
  const respParts = [];
58
57
  // Formats an entry (string, number or Buffer) into the Buffer that is written to
@@ -67,9 +66,7 @@ async function compiler(response, options) {
67
66
  const emitEntry = (entry, opts) => {
68
67
  let { returnEmpty, raw } = opts || {};
69
68
  if (!raw && !isLogging && (typeof entry === 'string' || Buffer.isBuffer(entry)) && CRLF.test(entry.toString('latin1'))) {
70
- let error = new Error('Line terminator in IMAP token');
71
- error.code = 'InvalidTokenValue';
72
- throw error;
69
+ throw (0, errors_js_1.createImapError)('Line terminator in IMAP token', 'InvalidTokenValue');
73
70
  }
74
71
  if (typeof entry === 'string') {
75
72
  return Buffer.from(entry);
@@ -93,7 +90,7 @@ async function compiler(response, options) {
93
90
  // Set right after the compiler writes "(" or "[" itself, so the first element inside gets no
94
91
  // leading space
95
92
  let afterOpener = false;
96
- let walk = async (node, options) => {
93
+ let walk = (node, options) => {
97
94
  options = options || {};
98
95
  // Add a space separator when:
99
96
  // - The previous token was a LITERAL. Literal data ends exactly at its declared length, so
@@ -125,7 +122,7 @@ async function compiler(response, options) {
125
122
  if (subArray && !Array.isArray(child)) {
126
123
  subArray = false;
127
124
  }
128
- await walk(child, { subArray });
125
+ walk(child, { subArray });
129
126
  }
130
127
  resp.push(emitEntry(')'));
131
128
  afterOpener = false;
@@ -159,6 +156,11 @@ async function compiler(response, options) {
159
156
  resp.push(emitEntry(logPlaceholder(node.value.length, 'literal')));
160
157
  }
161
158
  else {
159
+ // The size marker must match the bytes emitEntry() writes, and it writes
160
+ // nothing for a value that is not a Buffer or a string
161
+ if (node.value && !Buffer.isBuffer(node.value) && typeof node.value !== 'string') {
162
+ throw (0, errors_js_1.createImapError)('Literal value must be a Buffer or a string', 'InvalidTokenValue');
163
+ }
162
164
  // The literal size marker counts octets - string values are written as
163
165
  // UTF-8, so their UTF-16 .length would undercount multi-byte characters
164
166
  let literalLength = !node.value ? 0 : Buffer.isBuffer(node.value) ? node.value.length : Buffer.byteLength(node.value.toString());
@@ -213,9 +215,7 @@ async function compiler(response, options) {
213
215
  if (!isLogging) {
214
216
  val = node.value === null || node.value === undefined ? '' : node.value.toString();
215
217
  if (!isValidSequenceSet(val)) {
216
- let error = new Error('Invalid sequence set value');
217
- error.code = 'InvalidSequenceSet';
218
- throw error;
218
+ throw (0, errors_js_1.createImapError)('Invalid sequence set value', 'InvalidSequenceSet');
219
219
  }
220
220
  resp.push(emitEntry(val, { raw: true }));
221
221
  }
@@ -230,9 +230,7 @@ async function compiler(response, options) {
230
230
  // this check runs first only to raise the more specific InvalidTextValue code.
231
231
  if (node.value) {
232
232
  if (!isLogging && CRLF.test(node.value.toString())) {
233
- let error = new Error('Line terminator in IMAP text value');
234
- error.code = 'InvalidTextValue';
235
- throw error;
233
+ throw (0, errors_js_1.createImapError)('Line terminator in IMAP text value', 'InvalidTextValue');
236
234
  }
237
235
  resp.push(emitEntry(node.value));
238
236
  }
@@ -267,7 +265,7 @@ async function compiler(response, options) {
267
265
  resp.push(emitEntry('['));
268
266
  afterOpener = true;
269
267
  for (let child of node.section) {
270
- await walk(child);
268
+ walk(child);
271
269
  }
272
270
  resp.push(emitEntry(']'));
273
271
  afterOpener = false;
@@ -286,13 +284,15 @@ async function compiler(response, options) {
286
284
  if (response.attributes) {
287
285
  let attributes = Array.isArray(response.attributes) ? response.attributes : [].concat(response.attributes);
288
286
  for (let child of attributes) {
289
- await walk(child);
287
+ walk(child);
290
288
  }
291
289
  }
292
290
  if (resp.length) {
293
291
  respParts.push(resp);
294
292
  }
295
- const compiled = respParts.map(part => Buffer.concat(part));
293
+ // A part with a single entry (the data of a synchronizing literal) is used as is: copying a
294
+ // large message only to send it doubles its memory
295
+ const compiled = respParts.map(part => (part.length === 1 ? part[0] : Buffer.concat(part)));
296
296
  // without asArray there is a single part, returned as is instead of copied
297
297
  return asArray ? compiled : compiled.length === 1 ? compiled[0] : Buffer.concat(compiled);
298
298
  }
@@ -6,6 +6,31 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.default = parser;
7
7
  const imap_formal_syntax_js_1 = __importDefault(require("./imap-formal-syntax.js"));
8
8
  const parser_instance_js_1 = require("./parser-instance.js");
9
+ const errors_js_1 = require("../errors.js");
10
+ const limits_js_1 = require("./limits.js");
11
+ // The codes the parser raises on purpose: the numbered ParserErrorN codes, ParserErrorExchange,
12
+ // the nesting limit and the inline literal size limit
13
+ const PARSER_ERROR_CODES = /^(?:ParserError\d+|ParserErrorExchange|MAX_IMAP_NESTING_REACHED|LiteralTooLarge)$/;
14
+ /**
15
+ * Anything the parser throws other than one of its own coded errors - a TypeError from a parser
16
+ * bug, a RangeError from runaway recursion or a bad Buffer offset (which carries a Node code), a
17
+ * thrown non-Error - is re-raised as a coded `ParserErrorInternal`, so callers that match on
18
+ * codes still handle it and settle the command the line belonged to. The fuzz suite treats this
19
+ * code as a failure, so the guard does not hide parser bugs from the tests.
20
+ *
21
+ * @param err - The thrown value
22
+ * @param input - The line that was being parsed
23
+ * @returns The error to rethrow
24
+ */
25
+ function asParserError(err, input) {
26
+ if (err instanceof Error && PARSER_ERROR_CODES.test(err.code || '')) {
27
+ return err;
28
+ }
29
+ return (0, errors_js_1.createImapError)(`Unexpected parser failure: ${err instanceof Error ? err.message : String(err)}`, 'ParserErrorInternal', {
30
+ parserContext: (0, limits_js_1.boundedInput)(input.toString()),
31
+ ...(err instanceof Error ? { _err: err } : {})
32
+ });
33
+ }
9
34
  /**
10
35
  * Parses a raw IMAP command or response buffer into a structured object.
11
36
  * Handles edge cases such as null-byte-padded responses from buggy servers and
@@ -26,7 +51,7 @@ async function parser(command, options) {
26
51
  // Workaround for buggy IMAP servers that pad responses with leading NUL (\x00) bytes.
27
52
  // Some servers (observed in the wild) prepend null bytes to their output, which would
28
53
  // cause parsing to fail. We strip them and note how many were removed for diagnostics.
29
- if (command[0] === 0) {
54
+ if (Buffer.isBuffer(command) && command[0] === 0) {
30
55
  // find the first non null byte and trim
31
56
  let firstNonNull = -1;
32
57
  for (let i = 0; i < command.length; i++) {
@@ -39,7 +64,7 @@ async function parser(command, options) {
39
64
  // All bytes are null, treat as a BAD response
40
65
  return { tag: '*', command: 'BAD', attributes: [] };
41
66
  }
42
- command = command.slice(firstNonNull);
67
+ command = command.subarray(firstNonNull);
43
68
  nullBytesRemoved = firstNonNull;
44
69
  }
45
70
  const parserInstance = new parser_instance_js_1.ParserInstance(command, options);
@@ -71,7 +96,7 @@ async function parser(command, options) {
71
96
  }
72
97
  }
73
98
  catch (err) {
74
- let error = err;
99
+ let error = asParserError(err, command);
75
100
  if (error.code === 'ParserErrorExchange' && error.parserContext && error.parserContext.value) {
76
101
  return error.parserContext.value;
77
102
  }
@@ -4,6 +4,7 @@ exports.ImapStream = void 0;
4
4
  const node_stream_1 = require("node:stream");
5
5
  const logger_js_1 = require("../logger.js");
6
6
  const limits_js_1 = require("./limits.js");
7
+ const errors_js_1 = require("../errors.js");
7
8
  const LINE = 0x01;
8
9
  const LITERAL = 0x02;
9
10
  const LF = 0x0a;
@@ -180,10 +181,10 @@ class ImapStream extends node_stream_1.Transform {
180
181
  if (lineLength <= this.maxLineLength) {
181
182
  return true;
182
183
  }
183
- const err = new Error(`Line length ${lineLength} exceeds maximum allowed size of ${this.maxLineLength} bytes`);
184
- err.code = 'LineTooLarge';
185
- err.lineLength = lineLength;
186
- err.maxSize = this.maxLineLength;
184
+ const err = (0, errors_js_1.createImapError)(`Line length ${lineLength} exceeds maximum allowed size of ${this.maxLineLength} bytes`, 'LineTooLarge', {
185
+ lineLength,
186
+ maxSize: this.maxLineLength
187
+ });
187
188
  return this.failStream(err);
188
189
  }
189
190
  /**
@@ -206,10 +207,10 @@ class ImapStream extends node_stream_1.Transform {
206
207
  }
207
208
  return true;
208
209
  }
209
- const err = new Error(`Response size ${total} exceeds maximum allowed size of ${this.maxResponseSize} bytes`);
210
- err.code = 'ResponseTooLarge';
211
- err.responseSize = total;
212
- err.maxSize = this.maxResponseSize;
210
+ const err = (0, errors_js_1.createImapError)(`Response size ${total} exceeds maximum allowed size of ${this.maxResponseSize} bytes`, 'ResponseTooLarge', {
211
+ responseSize: total,
212
+ maxSize: this.maxResponseSize
213
+ });
213
214
  return this.failStream(err);
214
215
  }
215
216
  /**
@@ -1,4 +1,4 @@
1
- import type { ImapFlowError } from '../errors.js';
1
+ 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;
@@ -4,6 +4,7 @@
4
4
  // defaults in the ImapFlowOptions type describe both paths.
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createLiteralTooLargeError = exports.normalizeLimit = exports.boundedInput = exports.ERROR_CONTEXT_LENGTH = exports.MAX_RESPONSE_SIZE = exports.MAX_LINE_SIZE = exports.MAX_LITERAL_SIZE = void 0;
7
+ const errors_js_1 = require("../errors.js");
7
8
  // Maximum allowed literal size: 1GB (1073741824 bytes)
8
9
  exports.MAX_LITERAL_SIZE = 1024 * 1024 * 1024;
9
10
  // Default maximum length of a single line (a response without a literal). Matches the literal cap:
@@ -56,11 +57,8 @@ exports.normalizeLimit = normalizeLimit;
56
57
  * @param reason - What the bound was, when it is not the configured maximum.
57
58
  * @returns The error to emit or throw.
58
59
  */
59
- const createLiteralTooLargeError = (literalSize, maxSize, reason) => {
60
- const err = new Error(`Literal size ${literalSize} exceeds ${reason || `maximum allowed size of ${maxSize} bytes`}`);
61
- err.code = 'LiteralTooLarge';
62
- err.literalSize = literalSize;
63
- err.maxSize = maxSize;
64
- return err;
65
- };
60
+ const createLiteralTooLargeError = (literalSize, maxSize, reason) => (0, errors_js_1.createImapError)(`Literal size ${literalSize} exceeds ${reason || `maximum allowed size of ${maxSize} bytes`}`, 'LiteralTooLarge', {
61
+ literalSize,
62
+ maxSize
63
+ });
66
64
  exports.createLiteralTooLargeError = createLiteralTooLargeError;
@@ -8,6 +8,7 @@ exports.ParserInstance = void 0;
8
8
  const imap_formal_syntax_js_1 = __importDefault(require("./imap-formal-syntax.js"));
9
9
  const token_parser_js_1 = require("./token-parser.js");
10
10
  const limits_js_1 = require("./limits.js");
11
+ const errors_js_1 = require("../errors.js");
11
12
  /**
12
13
  * Parses a single IMAP response line into its structural components: tag, command,
13
14
  * and attributes. Handles status responses (OK, NO, BAD, PREAUTH, BYE) with their
@@ -143,10 +144,7 @@ class ParserInstance {
143
144
  async getElement(syntax) {
144
145
  let match, element, errPos;
145
146
  if (/^\s/.test(this.remainder)) {
146
- let error = new Error(`Unexpected whitespace at position ${this.pos} [E1]`);
147
- error.code = 'ParserError1';
148
- error.parserContext = this.errorContext();
149
- throw error;
147
+ throw (0, errors_js_1.createImapError)(`Unexpected whitespace at position ${this.pos} [E1]`, 'ParserError1', { parserContext: this.errorContext() });
150
148
  }
151
149
  if ((match = this.remainder.match(/^\s*[^\s]+(?=\s|$)/))) {
152
150
  element = match[0];
@@ -156,29 +154,24 @@ class ParserInstance {
156
154
  // "Server Unavailable." instead of a proper IMAP tagged/untagged response.
157
155
  // We detect this specific pattern and convert it into a synthetic BAD response
158
156
  // so the rest of the parser can handle it gracefully.
159
- let error = new Error(`Server returned an error: ${this.input}`);
160
- error.code = 'ParserErrorExchange';
161
- error.parserContext = {
162
- ...this.errorContext(element),
163
- value: {
164
- tag: '*',
165
- command: 'BAD',
166
- attributes: [{ type: 'TEXT', value: this.input }]
157
+ throw (0, errors_js_1.createImapError)(`Server returned an error: ${this.input}`, 'ParserErrorExchange', {
158
+ parserContext: {
159
+ ...this.errorContext(element),
160
+ value: {
161
+ tag: '*',
162
+ command: 'BAD',
163
+ attributes: [{ type: 'TEXT', value: this.input }]
164
+ }
167
165
  }
168
- };
169
- throw error;
166
+ });
170
167
  }
171
- let error = new Error(`Unexpected char at position ${this.pos + errPos} [E2: ${JSON.stringify(element.charAt(errPos))}]`);
172
- error.code = 'ParserError2';
173
- error.parserContext = this.errorContext(element);
174
- throw error;
168
+ throw (0, errors_js_1.createImapError)(`Unexpected char at position ${this.pos + errPos} [E2: ${JSON.stringify(element.charAt(errPos))}]`, 'ParserError2', {
169
+ parserContext: this.errorContext(element)
170
+ });
175
171
  }
176
172
  }
177
173
  else {
178
- let error = new Error(`Unexpected end of input at position ${this.pos} [E3]`);
179
- error.code = 'ParserError3';
180
- error.parserContext = this.errorContext();
181
- throw error;
174
+ throw (0, errors_js_1.createImapError)(`Unexpected end of input at position ${this.pos} [E3]`, 'ParserError3', { parserContext: this.errorContext() });
182
175
  }
183
176
  this.pos += match[0].length;
184
177
  this.remainder = this.remainder.slice(match[0].length);
@@ -196,16 +189,12 @@ class ParserInstance {
196
189
  // special case, empty + response
197
190
  return;
198
191
  }
199
- let error = new Error(`Unexpected end of input at position ${this.pos} [E4]`);
200
- error.code = 'ParserError4';
201
- error.parserContext = this.errorContext();
202
- throw error;
192
+ throw (0, errors_js_1.createImapError)(`Unexpected end of input at position ${this.pos} [E4]`, 'ParserError4', { parserContext: this.errorContext() });
203
193
  }
204
194
  if (imap_formal_syntax_js_1.default.verify(this.remainder.charAt(0), imap_formal_syntax_js_1.default.SP()) >= 0) {
205
- let error = new Error(`Unexpected char at position ${this.pos} [E5: ${JSON.stringify(this.remainder.charAt(0))}]`);
206
- error.code = 'ParserError5';
207
- error.parserContext = this.errorContext(this.remainder);
208
- throw error;
195
+ throw (0, errors_js_1.createImapError)(`Unexpected char at position ${this.pos} [E5: ${JSON.stringify(this.remainder.charAt(0))}]`, 'ParserError5', {
196
+ parserContext: this.errorContext(this.remainder)
197
+ });
209
198
  }
210
199
  this.pos++;
211
200
  this.remainder = this.remainder.slice(1);
@@ -220,16 +209,12 @@ class ParserInstance {
220
209
  */
221
210
  async getAttributes() {
222
211
  if (!this.remainder.length) {
223
- let error = new Error(`Unexpected end of input at position ${this.pos} [E6]`);
224
- error.code = 'ParserError6';
225
- error.parserContext = this.errorContext();
226
- throw error;
212
+ throw (0, errors_js_1.createImapError)(`Unexpected end of input at position ${this.pos} [E6]`, 'ParserError6', { parserContext: this.errorContext() });
227
213
  }
228
214
  if (/^\s/.test(this.remainder)) {
229
- let error = new Error(`Unexpected whitespace at position ${this.pos} [E7]`);
230
- error.code = 'ParserError7';
231
- error.parserContext = this.errorContext(this.remainder);
232
- throw error;
215
+ throw (0, errors_js_1.createImapError)(`Unexpected whitespace at position ${this.pos} [E7]`, 'ParserError7', {
216
+ parserContext: this.errorContext(this.remainder)
217
+ });
233
218
  }
234
219
  const tokenParser = new token_parser_js_1.TokenParser(this, this.pos, this.remainder, this.options);
235
220
  return await tokenParser.getAttributes();
@@ -1,3 +1,4 @@
1
+ import { type ImapFlowError, type ParserErrorCode } from '../errors.js';
1
2
  import type { ImapAttributeList, ParserOptions } from './types.js';
2
3
  import type { ParserInstance } from './parser-instance.js';
3
4
  /**
@@ -58,6 +59,17 @@ export declare class TokenParser {
58
59
  * from the input, i.e. when no pre-parsed literal buffers were supplied. Defaults to 1GB.
59
60
  */
60
61
  constructor(parent: TokenParserParent | ParserInstance, startPos?: number | undefined, str?: string | null | undefined, options?: ParserOptions | undefined);
62
+ /**
63
+ * Builds a numbered parser error that carries a bounded copy of the input and the position
64
+ * (and character, when there is one) where parsing failed.
65
+ *
66
+ * @param message - The error message
67
+ * @param code - The numbered parser error code
68
+ * @param pos - Position of the failure in the original input
69
+ * @param chr - The offending character
70
+ * @returns The error to throw
71
+ */
72
+ parserError(message: string, code: ParserErrorCode, pos: number, chr?: string | undefined): ImapFlowError;
61
73
  /**
62
74
  * Processes the input string and returns the parsed attributes as a flat array of typed objects.
63
75
  * Each attribute is an object with a `type` (e.g., "ATOM", "STRING", "LITERAL", "SEQUENCE")