imapflow 2.2.6 → 2.2.7

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 (72) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/cjs/commands/append.js +10 -3
  3. package/dist/cjs/commands/authenticate.js +27 -18
  4. package/dist/cjs/commands/close.d.ts +12 -1
  5. package/dist/cjs/commands/close.js +4 -2
  6. package/dist/cjs/commands/delete.js +2 -1
  7. package/dist/cjs/commands/esearch-parser.js +8 -2
  8. package/dist/cjs/commands/fetch.js +35 -9
  9. package/dist/cjs/commands/id.js +8 -1
  10. package/dist/cjs/commands/idle.js +15 -5
  11. package/dist/cjs/commands/list.js +10 -1
  12. package/dist/cjs/commands/login.js +5 -1
  13. package/dist/cjs/commands/logout.js +7 -0
  14. package/dist/cjs/commands/namespace.js +7 -3
  15. package/dist/cjs/commands/quota.js +3 -1
  16. package/dist/cjs/commands/rename.js +2 -1
  17. package/dist/cjs/commands/select.js +5 -0
  18. package/dist/cjs/commands/status.js +6 -1
  19. package/dist/cjs/commands/store.d.ts +1 -1
  20. package/dist/cjs/commands/store.js +1 -2
  21. package/dist/cjs/download.js +82 -91
  22. package/dist/cjs/handler/imap-compiler.js +19 -10
  23. package/dist/cjs/handler/limits.d.ts +11 -0
  24. package/dist/cjs/handler/limits.js +16 -1
  25. package/dist/cjs/handler/parser-instance.d.ts +10 -0
  26. package/dist/cjs/handler/parser-instance.js +25 -10
  27. package/dist/cjs/handler/token-parser.js +36 -28
  28. package/dist/cjs/imap-flow.js +235 -89
  29. package/dist/cjs/package-info.d.ts +1 -1
  30. package/dist/cjs/package-info.js +1 -1
  31. package/dist/cjs/proxy-connection.js +7 -7
  32. package/dist/cjs/search-compiler.js +30 -13
  33. package/dist/cjs/special-use.js +10 -5
  34. package/dist/cjs/tools.d.ts +10 -1
  35. package/dist/cjs/tools.js +30 -3
  36. package/dist/cjs/types.d.ts +19 -4
  37. package/dist/esm/commands/append.js +11 -4
  38. package/dist/esm/commands/authenticate.js +28 -19
  39. package/dist/esm/commands/close.d.ts +12 -1
  40. package/dist/esm/commands/close.js +5 -3
  41. package/dist/esm/commands/delete.js +2 -1
  42. package/dist/esm/commands/esearch-parser.js +8 -2
  43. package/dist/esm/commands/fetch.js +35 -9
  44. package/dist/esm/commands/id.js +9 -2
  45. package/dist/esm/commands/idle.js +16 -6
  46. package/dist/esm/commands/list.js +10 -1
  47. package/dist/esm/commands/login.js +6 -2
  48. package/dist/esm/commands/logout.js +7 -0
  49. package/dist/esm/commands/namespace.js +7 -3
  50. package/dist/esm/commands/quota.js +3 -1
  51. package/dist/esm/commands/rename.js +2 -1
  52. package/dist/esm/commands/select.js +5 -0
  53. package/dist/esm/commands/status.js +7 -2
  54. package/dist/esm/commands/store.d.ts +1 -1
  55. package/dist/esm/commands/store.js +1 -2
  56. package/dist/esm/download.js +82 -91
  57. package/dist/esm/handler/imap-compiler.js +19 -10
  58. package/dist/esm/handler/limits.d.ts +11 -0
  59. package/dist/esm/handler/limits.js +14 -0
  60. package/dist/esm/handler/parser-instance.d.ts +10 -0
  61. package/dist/esm/handler/parser-instance.js +25 -10
  62. package/dist/esm/handler/token-parser.js +37 -29
  63. package/dist/esm/imap-flow.js +235 -89
  64. package/dist/esm/package-info.d.ts +1 -1
  65. package/dist/esm/package-info.js +1 -1
  66. package/dist/esm/proxy-connection.js +7 -7
  67. package/dist/esm/search-compiler.js +30 -13
  68. package/dist/esm/special-use.js +10 -5
  69. package/dist/esm/tools.d.ts +10 -1
  70. package/dist/esm/tools.js +29 -3
  71. package/dist/esm/types.d.ts +19 -4
  72. package/package.json +1 -1
@@ -15,6 +15,9 @@ const libbase64_1 = __importDefault(require("libbase64"));
15
15
  const mailsplit_1 = require("@zone-eu/mailsplit");
16
16
  const flowed_decoder_js_1 = __importDefault(require("@zone-eu/mailsplit/lib/flowed-decoder.js"));
17
17
  const limited_passthrough_js_1 = require("./limited-passthrough.js");
18
+ // What a wait on the head stream listens for: it can take more input, it failed, or it went
19
+ // away (a consumer destroying it closes it without a 'drain')
20
+ const DRAIN_WAIT_EVENTS = ['drain', 'error', 'close'];
18
21
  const tools_js_1 = require("./tools.js");
19
22
  /**
20
23
  * Implements ImapFlow.download(), see its documentation
@@ -54,7 +57,8 @@ async function downloadMessage(client, range, part, options) {
54
57
  range = uid;
55
58
  downloadOptions.uid = true;
56
59
  }
57
- if (!response.bodyStructure.childNodes) {
60
+ // bodyStructure is unset when the server sent BODYSTRUCTURE NIL
61
+ if (!response.bodyStructure?.childNodes) {
58
62
  // single text message
59
63
  part = 'TEXT';
60
64
  }
@@ -297,11 +301,54 @@ async function downloadMessage(client, range, part, options) {
297
301
  // at the bound still gets its terminating chunk. Infinity when the server reported no
298
302
  // size, which leaves the loop bounded by maxBytes alone.
299
303
  let maxTotalBytes = (0, limited_passthrough_js_1.normalizeByteLimit)(meta.expectedSize ? meta.expectedSize * 2 + chunkSize : 0);
300
- // Fetch remaining chunks in a loop, writing each to the decoder stream.
301
- // Stops when the server returns a short chunk (< chunkSize), answers with more than the
302
- // requested window, the byte limiter is satisfied, or the consumer destroys the output
303
- // stream. Throws when the ceiling above is crossed.
304
- let fetchAllParts = async () => {
304
+ // Resolves once the head stream can take more input, rejects when it fails, and resolves
305
+ // when it goes away. finish() is the listener itself: 'drain' and 'close' emit no arguments,
306
+ // 'error' emits the error, and removal needs no separate handler references. It removes
307
+ // only the listeners this wait installed - removeAllListeners('error') also took off the
308
+ // forwarder pipeStage() attached to the head stream when the pipeline was built, and the
309
+ // head must keep that forwarder for the life of the download or a chunk failure has nowhere
310
+ // to go.
311
+ let waitForDrain = () => new Promise((resolve, reject) => {
312
+ const finish = (err) => {
313
+ for (let event of DRAIN_WAIT_EVENTS) {
314
+ stream.removeListener(event, finish);
315
+ }
316
+ /* c8 ignore next 2 */ // stream error during a backpressure drain wait is timing-dependent
317
+ if (err) {
318
+ reject(err);
319
+ }
320
+ else {
321
+ resolve();
322
+ }
323
+ };
324
+ for (let event of DRAIN_WAIT_EVENTS) {
325
+ stream.once(event, finish);
326
+ }
327
+ });
328
+ // Writes a chunk and waits out the backpressure. A base64 decoder defers its write callback,
329
+ // so a chunk the size of its buffer is never taken synchronously and the head chunk waits
330
+ // here as much as any other. A stream failure during the wait is thrown unless the download
331
+ // was already aborted (the consumer destroyed the stream).
332
+ let writeAndDrain = async (chunk) => {
333
+ if (writeChunk(chunk) !== false) {
334
+ return;
335
+ }
336
+ try {
337
+ await waitForDrain();
338
+ /* c8 ignore next 5 */ // re-throw path only triggers on a stream error mid-drain, which is timing-dependent
339
+ }
340
+ catch (err) {
341
+ if (!fetchAborted) {
342
+ throw err;
343
+ }
344
+ }
345
+ };
346
+ // Writes the head chunk, then fetches the remaining chunks in a loop, writing each to the
347
+ // decoder stream. Stops when the server returns a short chunk (< chunkSize), answers with
348
+ // more than the requested window, the byte limiter is satisfied, or the consumer destroys
349
+ // the output stream. Throws when the ceiling above is crossed.
350
+ let fetchAllParts = async (head) => {
351
+ await writeAndDrain(head);
305
352
  while (hasMore && !isLimited() && !fetchAborted) {
306
353
  if (processed >= maxTotalBytes) {
307
354
  // Loud on purpose. Everything written downstream by this point holds
@@ -314,7 +361,9 @@ async function downloadMessage(client, range, part, options) {
314
361
  throw err;
315
362
  }
316
363
  let { response, chunk } = await getNextPart();
317
- if (fetchAborted) {
364
+ // A consumer that gave up while the chunk was in flight: its 'close' may still be a
365
+ // tick away from setting fetchAborted, so the stream's own flag is checked as well
366
+ if (fetchAborted || output.destroyed) {
318
367
  break;
319
368
  }
320
369
  if (response === false) {
@@ -329,48 +378,7 @@ async function downloadMessage(client, range, part, options) {
329
378
  if (!chunk) {
330
379
  break;
331
380
  }
332
- // Handle backpressure
333
- if (writeChunk(chunk) === false) {
334
- // Wait for drain event before continuing
335
- try {
336
- await new Promise((resolve, reject) => {
337
- // finish() is the listener itself, as settle() is for the TLS upgrade:
338
- // 'drain' and 'close' emit no arguments, 'error' emits the error, and
339
- // removal needs no separate handler references. It removes only the
340
- // three listeners this wait installed - removeAllListeners('error')
341
- // also took off the forwarder pipeStage() attached to the head stream
342
- // when the pipeline was built, and the head must keep that forwarder
343
- // for the life of the download or a chunk failure has nowhere to go.
344
- const finish = (err) => {
345
- for (let event of ['drain', 'error', 'close']) {
346
- stream.removeListener(event, finish);
347
- }
348
- /* c8 ignore next 2 */ // stream error during a backpressure drain wait is timing-dependent
349
- if (err) {
350
- reject(err);
351
- }
352
- else {
353
- resolve();
354
- }
355
- };
356
- stream.once('drain', finish);
357
- stream.once('error', finish);
358
- stream.once('close', finish);
359
- });
360
- /* c8 ignore start */ // re-throw path only triggers on a stream error mid-drain, which is timing-dependent
361
- }
362
- catch (err) {
363
- // Re-throw only if not aborted
364
- if (!fetchAborted) {
365
- throw err;
366
- }
367
- }
368
- /* c8 ignore stop */
369
- // Check if we should abort after waiting
370
- if (fetchAborted) {
371
- break;
372
- }
373
- }
381
+ await writeAndDrain(chunk);
374
382
  }
375
383
  };
376
384
  // A download is a sequence of chunk FETCHes with a backpressure wait in between. Those
@@ -389,13 +397,10 @@ async function downloadMessage(client, range, part, options) {
389
397
  client.autoidle();
390
398
  }
391
399
  };
392
- // Kick off the download pipeline asynchronously. The first chunk was
393
- // already fetched above (to get metadata); write it to the decoder
394
- // stream and then fetch remaining chunks via fetchAllParts().
395
- // setImmediate ensures the caller gets the {meta, content} return
396
- // value before streaming begins.
397
- let runFetchAllParts = () => {
398
- fetchAllParts()
400
+ // Runs the download pipeline with the head chunk fetched above (for its metadata): it is
401
+ // written to the decoder stream and the remaining chunks follow
402
+ let runFetchAllParts = (head) => {
403
+ fetchAllParts(head)
399
404
  .catch(err => {
400
405
  if (!fetchAborted && stream && !stream.destroyed) {
401
406
  stream.emit('error', err);
@@ -427,36 +432,8 @@ async function downloadMessage(client, range, part, options) {
427
432
  // for a routine-looking connection code.
428
433
  .catch(err => client.log.error({ msg: 'Failed to fail the download stream', err, cid: client.id }));
429
434
  };
430
- setImmediate(() => {
431
- let writeResult;
432
- try {
433
- writeResult = writeChunk(chunk);
434
- }
435
- catch (err) {
436
- stream.emit('error', err);
437
- finishDownload();
438
- /* c8 ignore next 3 */ // emitting the error above triggers cleanup (fetchAborted=true), so this end() guard is already false here
439
- if (!fetchAborted && stream && !stream.destroyed) {
440
- stream.end();
441
- }
442
- return;
443
- }
444
- /* c8 ignore next 9 */ // `stream` is piped to the limiter before this runs, so the head write drains synchronously and always returns true (verified for chunkSize up to 8MB); the drain-wait branch is unreachable
445
- if (!writeResult) {
446
- // Initial chunk filled the buffer, wait for drain
447
- stream.once('drain', () => {
448
- if (!fetchAborted) {
449
- runFetchAllParts();
450
- }
451
- else {
452
- finishDownload();
453
- }
454
- });
455
- }
456
- else {
457
- runFetchAllParts();
458
- }
459
- });
435
+ // Deferred so the caller gets the {meta, content} return value before streaming begins
436
+ setImmediate(() => runFetchAllParts(chunk));
460
437
  return {
461
438
  meta,
462
439
  content: output
@@ -476,14 +453,16 @@ async function downloadMessageParts(client, range, parts, options) {
476
453
  // no mailbox selected, nothing to do
477
454
  return {};
478
455
  }
479
- let downloadOptions = Object.assign({
480
- chunkSize: 64 * 1024,
481
- maxBytes: Infinity
482
- }, options || {});
456
+ let downloadOptions = options || {};
457
+ // Asked as a partial fetch so at most maxBytes of each part crosses the wire, and enforced
458
+ // again on the answer for servers that ignore the partial specifier
459
+ let maxBytes = (0, limited_passthrough_js_1.normalizeByteLimit)(downloadOptions.maxBytes);
483
460
  let query = { bodyParts: [] };
484
461
  for (let part of parts) {
485
462
  query.bodyParts.push(part + '.mime');
486
- query.bodyParts.push(part);
463
+ // The partial specifier carries a 32-bit length (RFC 9051 "number"), so a cap beyond
464
+ // that is applied on the answer alone
465
+ query.bodyParts.push(maxBytes > 0xffffffff ? part : { key: part, start: 0, maxLength: maxBytes });
487
466
  }
488
467
  let response = await client.fetchOne(range, query, downloadOptions);
489
468
  if (!response || !response.bodyParts) {
@@ -501,6 +480,18 @@ async function downloadMessageParts(client, range, parts, options) {
501
480
  if (keyParts.length === 1) {
502
481
  // content
503
482
  let key = keyParts[0];
483
+ if (content.length > maxBytes) {
484
+ // The server ignored the partial specifier and sent the whole part, the quirk
485
+ // download() tolerates the same way: keep what was asked for
486
+ client.log.warn({
487
+ msg: 'Server returned more than the requested window, truncating the part',
488
+ part: key,
489
+ maxBytes,
490
+ received: content.length,
491
+ cid: client.id
492
+ });
493
+ content = content.subarray(0, maxBytes);
494
+ }
504
495
  if (!data[key]) {
505
496
  data[key] = { content };
506
497
  }
@@ -24,6 +24,10 @@ const safeNumber = (value) => {
24
24
  let num = Math.round(Number(value));
25
25
  return Number.isSafeInteger(num) && num >= 0 ? num : 0;
26
26
  };
27
+ // Log output stands in a placeholder for any value longer than this, so a log entry never
28
+ // carries a copy of a large token (a message body, a long unquoted server token)
29
+ const LOG_VALUE_LIMIT = 100;
30
+ const logPlaceholder = (length, kind) => `"(* ${length}B ${kind} *)"`;
27
31
  // Characters that cannot appear in an IMAP quoted string: CR and LF terminate a
28
32
  // command line, and NUL is outside the CHAR production entirely. A value carrying
29
33
  // any of them has to be sent as a literal, so quoting it is never correct.
@@ -132,8 +136,8 @@ async function compiler(response, options) {
132
136
  return;
133
137
  }
134
138
  if (typeof node === 'string' || Buffer.isBuffer(node)) {
135
- if (isLogging && node.length > 100) {
136
- resp.push(emitEntry('"(* ' + node.length + 'B string *)"'));
139
+ if (isLogging && node.length > LOG_VALUE_LIMIT) {
140
+ resp.push(emitEntry(logPlaceholder(node.length, 'string')));
137
141
  }
138
142
  else {
139
143
  resp.push(emitEntry(isLogging ? JSON.stringify(node.toString()) : quoteString(node.toString())));
@@ -152,7 +156,7 @@ async function compiler(response, options) {
152
156
  switch (node.type.toUpperCase()) {
153
157
  case 'LITERAL':
154
158
  if (isLogging) {
155
- resp.push(emitEntry('"(* ' + node.value.length + 'B literal *)"'));
159
+ resp.push(emitEntry(logPlaceholder(node.value.length, 'literal')));
156
160
  }
157
161
  else {
158
162
  // The literal size marker counts octets - string values are written as
@@ -184,8 +188,8 @@ async function compiler(response, options) {
184
188
  }
185
189
  break;
186
190
  case 'STRING':
187
- if (isLogging && node.value.length > 100) {
188
- resp.push(emitEntry('"(* ' + node.value.length + 'B string *)"'));
191
+ if (isLogging && node.value.length > LOG_VALUE_LIMIT) {
192
+ resp.push(emitEntry(logPlaceholder(node.value.length, 'string')));
189
193
  }
190
194
  else {
191
195
  val = (node.value || '').toString();
@@ -243,11 +247,16 @@ async function compiler(response, options) {
243
247
  case 'SECTION':
244
248
  val = (node.value || '').toString();
245
249
  if (!node.section || val) {
246
- // Verify the value contains only valid ATOM-CHAR characters.
247
- // Strip a leading backslash before checking (system flags like \Seen start with '\').
248
- // If any character fails verification, fall back to an IMAP quoted string
249
- // (JSON.stringify is used only for log output, where values are display-escaped).
250
- if (node.value === '' || imap_formal_syntax_js_1.default.verify(val.charAt(0) === '\\' ? val.slice(1) : val, imap_formal_syntax_js_1.default['ATOM-CHAR']()) >= 0) {
250
+ if (isLogging && val.length > LOG_VALUE_LIMIT) {
251
+ // A server can put a line's worth of bytes in one unquoted token
252
+ val = logPlaceholder(val.length, 'atom');
253
+ }
254
+ else if (node.value === '' ||
255
+ imap_formal_syntax_js_1.default.verify(val.charAt(0) === '\\' ? val.slice(1) : val, imap_formal_syntax_js_1.default['ATOM-CHAR']()) >= 0) {
256
+ // An empty value, or one with a character outside ATOM-CHAR (checked
257
+ // past a leading backslash, as system flags like \Seen carry one), goes
258
+ // out as an IMAP quoted string. JSON.stringify is used only for log
259
+ // output, where values are display-escaped.
251
260
  val = isLogging ? JSON.stringify(val) : quoteString(val);
252
261
  }
253
262
  resp.push(emitEntry(val));
@@ -2,6 +2,17 @@ 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 ERROR_CONTEXT_LENGTH = 1024;
6
+ /**
7
+ * The input a parse error carries: a bounded prefix with the full length.
8
+ *
9
+ * @param input - The input that failed to parse
10
+ * @returns The bounded prefix and the full length
11
+ */
12
+ export declare const boundedInput: (input: string) => {
13
+ input: string;
14
+ inputLength: number;
15
+ };
5
16
  /**
6
17
  * Normalizes a configured size limit. A non-negative integer is honored as-is (including 0, which
7
18
  * means "reject anything non-empty"), and `Infinity` disables the limit; anything else falls back
@@ -3,7 +3,7 @@
3
3
  // (ImapStream) and the standalone token parser cannot drift apart, and so the documented
4
4
  // defaults in the ImapFlowOptions type describe both paths.
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.createLiteralTooLargeError = exports.normalizeLimit = exports.MAX_RESPONSE_SIZE = exports.MAX_LINE_SIZE = exports.MAX_LITERAL_SIZE = void 0;
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
7
  // Maximum allowed literal size: 1GB (1073741824 bytes)
8
8
  exports.MAX_LITERAL_SIZE = 1024 * 1024 * 1024;
9
9
  // Default maximum length of a single line (a response without a literal). Matches the literal cap:
@@ -20,6 +20,21 @@ exports.MAX_LINE_SIZE = exports.MAX_LITERAL_SIZE;
20
20
  // of exactly the maximum permitted size impossible to receive. Configuring both limits calls for
21
21
  // the same headroom - set maxResponseSize above maxLiteralSize, not equal to it.
22
22
  exports.MAX_RESPONSE_SIZE = 2 * exports.MAX_LITERAL_SIZE;
23
+ // How much of the offending input a parse error carries along. The error travels whole into log
24
+ // entries (pino copies every property of a logged error) and into the rejection of the command
25
+ // the line belonged to, and the line can be as long as the configured line cap.
26
+ exports.ERROR_CONTEXT_LENGTH = 1024;
27
+ /**
28
+ * The input a parse error carries: a bounded prefix with the full length.
29
+ *
30
+ * @param input - The input that failed to parse
31
+ * @returns The bounded prefix and the full length
32
+ */
33
+ const boundedInput = (input) => ({
34
+ input: input.slice(0, exports.ERROR_CONTEXT_LENGTH),
35
+ inputLength: input.length
36
+ });
37
+ exports.boundedInput = boundedInput;
23
38
  /**
24
39
  * Normalizes a configured size limit. A non-negative integer is honored as-is (including 0, which
25
40
  * means "reject anything non-empty"), and `Infinity` disables the limit; anything else falls back
@@ -21,6 +21,16 @@ export declare class ParserInstance {
21
21
  * @param options.literals - Pre-parsed literal values from the stream.
22
22
  */
23
23
  constructor(input?: Buffer | string | null | undefined, options?: ParserOptions | undefined);
24
+ /**
25
+ * The context a parse error carries: a bounded prefix of the input (and of the element that
26
+ * failed, when there is one) with their full lengths, and the position.
27
+ *
28
+ * @param element - The element that failed to parse
29
+ * @returns The context to attach to the error
30
+ */
31
+ errorContext(element?: string | undefined): {
32
+ [key: string]: unknown;
33
+ };
24
34
  /**
25
35
  * Extracts and returns the IMAP tag from the beginning of the response.
26
36
  * The tag is typically "*" for untagged responses, "+" for continuation requests,
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
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
+ const limits_js_1 = require("./limits.js");
10
11
  /**
11
12
  * Parses a single IMAP response line into its structural components: tag, command,
12
13
  * and attributes. Handles status responses (OK, NO, BAD, PREAUTH, BYE) with their
@@ -27,6 +28,22 @@ class ParserInstance {
27
28
  this.remainder = this.input;
28
29
  this.pos = 0;
29
30
  }
31
+ /**
32
+ * The context a parse error carries: a bounded prefix of the input (and of the element that
33
+ * failed, when there is one) with their full lengths, and the position.
34
+ *
35
+ * @param element - The element that failed to parse
36
+ * @returns The context to attach to the error
37
+ */
38
+ errorContext(element) {
39
+ let context = { ...(0, limits_js_1.boundedInput)(this.input), pos: this.pos };
40
+ if (element !== undefined) {
41
+ let bounded = (0, limits_js_1.boundedInput)(element);
42
+ context.element = bounded.input;
43
+ context.elementLength = bounded.inputLength;
44
+ }
45
+ return context;
46
+ }
30
47
  /**
31
48
  * Extracts and returns the IMAP tag from the beginning of the response.
32
49
  * The tag is typically "*" for untagged responses, "+" for continuation requests,
@@ -128,7 +145,7 @@ class ParserInstance {
128
145
  if (/^\s/.test(this.remainder)) {
129
146
  let error = new Error(`Unexpected whitespace at position ${this.pos} [E1]`);
130
147
  error.code = 'ParserError1';
131
- error.parserContext = { input: this.input, pos: this.pos };
148
+ error.parserContext = this.errorContext();
132
149
  throw error;
133
150
  }
134
151
  if ((match = this.remainder.match(/^\s*[^\s]+(?=\s|$)/))) {
@@ -142,9 +159,7 @@ class ParserInstance {
142
159
  let error = new Error(`Server returned an error: ${this.input}`);
143
160
  error.code = 'ParserErrorExchange';
144
161
  error.parserContext = {
145
- input: this.input,
146
- element,
147
- pos: this.pos,
162
+ ...this.errorContext(element),
148
163
  value: {
149
164
  tag: '*',
150
165
  command: 'BAD',
@@ -155,14 +170,14 @@ class ParserInstance {
155
170
  }
156
171
  let error = new Error(`Unexpected char at position ${this.pos + errPos} [E2: ${JSON.stringify(element.charAt(errPos))}]`);
157
172
  error.code = 'ParserError2';
158
- error.parserContext = { input: this.input, element, pos: this.pos };
173
+ error.parserContext = this.errorContext(element);
159
174
  throw error;
160
175
  }
161
176
  }
162
177
  else {
163
178
  let error = new Error(`Unexpected end of input at position ${this.pos} [E3]`);
164
179
  error.code = 'ParserError3';
165
- error.parserContext = { input: this.input, pos: this.pos };
180
+ error.parserContext = this.errorContext();
166
181
  throw error;
167
182
  }
168
183
  this.pos += match[0].length;
@@ -183,13 +198,13 @@ class ParserInstance {
183
198
  }
184
199
  let error = new Error(`Unexpected end of input at position ${this.pos} [E4]`);
185
200
  error.code = 'ParserError4';
186
- error.parserContext = { input: this.input, pos: this.pos };
201
+ error.parserContext = this.errorContext();
187
202
  throw error;
188
203
  }
189
204
  if (imap_formal_syntax_js_1.default.verify(this.remainder.charAt(0), imap_formal_syntax_js_1.default.SP()) >= 0) {
190
205
  let error = new Error(`Unexpected char at position ${this.pos} [E5: ${JSON.stringify(this.remainder.charAt(0))}]`);
191
206
  error.code = 'ParserError5';
192
- error.parserContext = { input: this.input, element: this.remainder, pos: this.pos };
207
+ error.parserContext = this.errorContext(this.remainder);
193
208
  throw error;
194
209
  }
195
210
  this.pos++;
@@ -207,13 +222,13 @@ class ParserInstance {
207
222
  if (!this.remainder.length) {
208
223
  let error = new Error(`Unexpected end of input at position ${this.pos} [E6]`);
209
224
  error.code = 'ParserError6';
210
- error.parserContext = { input: this.input, pos: this.pos };
225
+ error.parserContext = this.errorContext();
211
226
  throw error;
212
227
  }
213
228
  if (/^\s/.test(this.remainder)) {
214
229
  let error = new Error(`Unexpected whitespace at position ${this.pos} [E7]`);
215
230
  error.code = 'ParserError7';
216
- error.parserContext = { input: this.input, element: this.remainder, pos: this.pos };
231
+ error.parserContext = this.errorContext(this.remainder);
217
232
  throw error;
218
233
  }
219
234
  const tokenParser = new token_parser_js_1.TokenParser(this, this.pos, this.remainder, this.options);