imapflow 2.2.5 → 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.
- package/CHANGELOG.md +17 -0
- package/dist/cjs/commands/append.js +10 -3
- package/dist/cjs/commands/authenticate.js +27 -18
- package/dist/cjs/commands/close.d.ts +12 -1
- package/dist/cjs/commands/close.js +4 -2
- package/dist/cjs/commands/copyuid-parser.js +16 -2
- package/dist/cjs/commands/delete.js +2 -1
- package/dist/cjs/commands/esearch-parser.js +8 -2
- package/dist/cjs/commands/fetch.js +35 -9
- package/dist/cjs/commands/id.js +8 -1
- package/dist/cjs/commands/idle.js +15 -5
- package/dist/cjs/commands/list.js +10 -1
- package/dist/cjs/commands/login.js +5 -1
- package/dist/cjs/commands/logout.js +7 -0
- package/dist/cjs/commands/namespace.js +7 -3
- package/dist/cjs/commands/quota.js +3 -1
- package/dist/cjs/commands/rename.js +2 -1
- package/dist/cjs/commands/select.js +5 -0
- package/dist/cjs/commands/status.js +6 -1
- package/dist/cjs/commands/store.d.ts +1 -1
- package/dist/cjs/commands/store.js +1 -2
- package/dist/cjs/download.js +82 -91
- package/dist/cjs/handler/imap-compiler.js +45 -29
- package/dist/cjs/handler/limits.d.ts +11 -0
- package/dist/cjs/handler/limits.js +16 -1
- package/dist/cjs/handler/parser-instance.d.ts +10 -0
- package/dist/cjs/handler/parser-instance.js +25 -10
- package/dist/cjs/handler/token-parser.js +162 -78
- package/dist/cjs/imap-flow.js +235 -89
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/cjs/proxy-connection.js +7 -7
- package/dist/cjs/search-compiler.js +37 -16
- package/dist/cjs/special-use.js +10 -5
- package/dist/cjs/tools.d.ts +10 -1
- package/dist/cjs/tools.js +30 -3
- package/dist/cjs/types.d.ts +19 -4
- package/dist/esm/commands/append.js +11 -4
- package/dist/esm/commands/authenticate.js +28 -19
- package/dist/esm/commands/close.d.ts +12 -1
- package/dist/esm/commands/close.js +5 -3
- package/dist/esm/commands/copyuid-parser.js +16 -2
- package/dist/esm/commands/delete.js +2 -1
- package/dist/esm/commands/esearch-parser.js +8 -2
- package/dist/esm/commands/fetch.js +35 -9
- package/dist/esm/commands/id.js +9 -2
- package/dist/esm/commands/idle.js +16 -6
- package/dist/esm/commands/list.js +10 -1
- package/dist/esm/commands/login.js +6 -2
- package/dist/esm/commands/logout.js +7 -0
- package/dist/esm/commands/namespace.js +7 -3
- package/dist/esm/commands/quota.js +3 -1
- package/dist/esm/commands/rename.js +2 -1
- package/dist/esm/commands/select.js +5 -0
- package/dist/esm/commands/status.js +7 -2
- package/dist/esm/commands/store.d.ts +1 -1
- package/dist/esm/commands/store.js +1 -2
- package/dist/esm/download.js +82 -91
- package/dist/esm/handler/imap-compiler.js +45 -29
- package/dist/esm/handler/limits.d.ts +11 -0
- package/dist/esm/handler/limits.js +14 -0
- package/dist/esm/handler/parser-instance.d.ts +10 -0
- package/dist/esm/handler/parser-instance.js +25 -10
- package/dist/esm/handler/token-parser.js +163 -79
- package/dist/esm/imap-flow.js +235 -89
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/dist/esm/proxy-connection.js +7 -7
- package/dist/esm/search-compiler.js +37 -16
- package/dist/esm/special-use.js +10 -5
- package/dist/esm/tools.d.ts +10 -1
- package/dist/esm/tools.js +29 -3
- package/dist/esm/types.d.ts +19 -4
- package/package.json +3 -2
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/* eslint new-cap: 0 */
|
|
2
2
|
import imapFormalSyntax from './imap-formal-syntax.js';
|
|
3
|
-
import { MAX_LITERAL_SIZE, normalizeLimit, createLiteralTooLargeError } from './limits.js';
|
|
3
|
+
import { MAX_LITERAL_SIZE, ERROR_CONTEXT_LENGTH, boundedInput, normalizeLimit, createLiteralTooLargeError } from './limits.js';
|
|
4
4
|
const STATE_ATOM = 0x001;
|
|
5
5
|
const STATE_LITERAL = 0x002;
|
|
6
6
|
const STATE_NORMAL = 0x003;
|
|
7
7
|
const STATE_PARTIAL = 0x004;
|
|
8
8
|
const STATE_SEQUENCE = 0x005;
|
|
9
9
|
const STATE_STRING = 0x006;
|
|
10
|
-
const RE_DIGITS = /^\d+$/;
|
|
11
10
|
const RE_SINGLE_DIGIT = /^\d$/;
|
|
12
11
|
// Prevents stack overflow from maliciously crafted deeply-nested IMAP input (e.g., (((((...))))))
|
|
13
12
|
const MAX_NODE_DEPTH = 25;
|
|
@@ -68,7 +67,7 @@ export class TokenParser {
|
|
|
68
67
|
if (!node.isClosed) {
|
|
69
68
|
let error = new Error(`Unexpected end of input at position ${this.pos + this.str.length - 1} [E9]`);
|
|
70
69
|
error.code = 'ParserError9';
|
|
71
|
-
error.parserContext = {
|
|
70
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + this.str.length - 1 };
|
|
72
71
|
throw error;
|
|
73
72
|
}
|
|
74
73
|
let type = (node.type || '').toString().toUpperCase();
|
|
@@ -142,7 +141,8 @@ export class TokenParser {
|
|
|
142
141
|
if (node.depth > MAX_NODE_DEPTH) {
|
|
143
142
|
let error = new Error('Too much nesting in IMAP string');
|
|
144
143
|
error.code = 'MAX_IMAP_NESTING_REACHED';
|
|
145
|
-
error
|
|
144
|
+
// A bounded prefix only, the error travels whole into logs and rejections
|
|
145
|
+
error._imapStr = this.str.slice(0, ERROR_CONTEXT_LENGTH);
|
|
146
146
|
throw error;
|
|
147
147
|
}
|
|
148
148
|
if (typeof startPos === 'number') {
|
|
@@ -169,6 +169,63 @@ export class TokenParser {
|
|
|
169
169
|
i++;
|
|
170
170
|
}
|
|
171
171
|
};
|
|
172
|
+
// Whether chr is the closing delimiter of node: ")" ends a LIST, "]" ends a SECTION
|
|
173
|
+
const closesNode = (c, node) => !!node && ((c === ')' && node.type === 'LIST') || (c === ']' && node.type === 'SECTION'));
|
|
174
|
+
// Facts about the ATOM, SEQUENCE or PARTIAL token being read, kept up to date as its
|
|
175
|
+
// characters are appended. Reading them back from the value instead (value.at(-1),
|
|
176
|
+
// value.includes('*'), a regex test) makes V8 flatten the string built with += on every
|
|
177
|
+
// character, which is quadratic in the token length, and the server decides that length:
|
|
178
|
+
// a 600 KB ESEARCH result took seconds of event loop time
|
|
179
|
+
let tokenLength = 0;
|
|
180
|
+
let tokenLast = '';
|
|
181
|
+
let tokenPrev = '';
|
|
182
|
+
let tokenDigitsOnly = true;
|
|
183
|
+
let tokenHasStar = false;
|
|
184
|
+
let tokenHasDot = false;
|
|
185
|
+
const appendToToken = (c) => {
|
|
186
|
+
this.currentNode.value += c;
|
|
187
|
+
tokenLength++;
|
|
188
|
+
tokenPrev = tokenLast;
|
|
189
|
+
tokenLast = c;
|
|
190
|
+
tokenDigitsOnly = tokenDigitsOnly && RE_SINGLE_DIGIT.test(c);
|
|
191
|
+
tokenHasStar = tokenHasStar || c === '*';
|
|
192
|
+
tokenHasDot = tokenHasDot || c === '.';
|
|
193
|
+
};
|
|
194
|
+
// A digit-led token is only a guess at a sequence set. ":" and "," are ATOM-CHARs, so a
|
|
195
|
+
// server sends a mailbox name, keyword or label such as "2024:Q1" or "1,a" unquoted, and
|
|
196
|
+
// rejecting it would drop the whole response line: the mailbox missing from LIST, the
|
|
197
|
+
// message missing from FETCH. When the sequence grammar breaks, such a token is read on as
|
|
198
|
+
// an atom, and the atom rules decide whether the current character is acceptable. A token
|
|
199
|
+
// holding "*" can not be an atom ("*" is not an ATOM-CHAR), so it keeps the sequence errors
|
|
200
|
+
const continueAsAtom = () => {
|
|
201
|
+
this.currentNode.type = 'ATOM';
|
|
202
|
+
this.state = STATE_ATOM;
|
|
203
|
+
// read the current character again, in STATE_ATOM
|
|
204
|
+
i--;
|
|
205
|
+
};
|
|
206
|
+
// Starts the value of the current node as a new token, with its first character if any
|
|
207
|
+
const startToken = (first) => {
|
|
208
|
+
this.currentNode.value = '';
|
|
209
|
+
tokenLength = 0;
|
|
210
|
+
tokenLast = tokenPrev = '';
|
|
211
|
+
tokenDigitsOnly = true;
|
|
212
|
+
tokenHasStar = tokenHasDot = false;
|
|
213
|
+
if (first) {
|
|
214
|
+
appendToToken(first);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
// ImapStream supplies one buffer per literal marker it framed, {0} included. A marker
|
|
218
|
+
// without a buffer means the line and its literals do not match up, so fail with a parser
|
|
219
|
+
// error instead of reading a missing buffer or shifting every later literal by one
|
|
220
|
+
const takeLiteral = (literals) => {
|
|
221
|
+
if (!literals.length) {
|
|
222
|
+
let error = new Error(`Literal without data at position ${this.pos + i} [E35]`);
|
|
223
|
+
error.code = 'ParserError35';
|
|
224
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i, chr };
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
return literals.shift();
|
|
228
|
+
};
|
|
172
229
|
// Any ATOM supported char starts a new Atom sequence, otherwise throw an error
|
|
173
230
|
// Allow \ as the first char for atom to support system flags
|
|
174
231
|
// Allow % to support LIST '' %
|
|
@@ -179,12 +236,12 @@ export class TokenParser {
|
|
|
179
236
|
if (!imapFormalSyntax['ATOM-CHAR']().includes(chr) && chr !== '\\' && chr !== '%' && chr.charCodeAt(0) < 0x80) {
|
|
180
237
|
let error = new Error(`Unexpected char at position ${this.pos + i} [E13: ${JSON.stringify(chr)}]`);
|
|
181
238
|
error.code = 'ParserError13';
|
|
182
|
-
error.parserContext = {
|
|
239
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i, chr };
|
|
183
240
|
throw error;
|
|
184
241
|
}
|
|
185
242
|
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
|
186
243
|
this.currentNode.type = 'ATOM';
|
|
187
|
-
|
|
244
|
+
startToken(chr);
|
|
188
245
|
this.state = STATE_ATOM;
|
|
189
246
|
};
|
|
190
247
|
for (i = 0, len = this.str.length; i < len; i++) {
|
|
@@ -210,7 +267,7 @@ export class TokenParser {
|
|
|
210
267
|
if (this.currentNode.type !== 'LIST') {
|
|
211
268
|
let error = new Error(`Unexpected list terminator ) at position ${this.pos + i} [E10]`);
|
|
212
269
|
error.code = 'ParserError10';
|
|
213
|
-
error.parserContext = {
|
|
270
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i, chr };
|
|
214
271
|
throw error;
|
|
215
272
|
}
|
|
216
273
|
this.currentNode.isClosed = true;
|
|
@@ -223,7 +280,7 @@ export class TokenParser {
|
|
|
223
280
|
if (this.currentNode.type !== 'SECTION') {
|
|
224
281
|
let error = new Error(`Unexpected section terminator ] at position ${this.pos + i} [E11]`);
|
|
225
282
|
error.code = 'ParserError11';
|
|
226
|
-
error.parserContext = {
|
|
283
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i, chr };
|
|
227
284
|
throw error;
|
|
228
285
|
}
|
|
229
286
|
this.currentNode.isClosed = true;
|
|
@@ -239,12 +296,13 @@ export class TokenParser {
|
|
|
239
296
|
if (this.str.charAt(i - 1) !== ']') {
|
|
240
297
|
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
|
241
298
|
this.currentNode.type = 'ATOM';
|
|
242
|
-
|
|
299
|
+
startToken(chr);
|
|
243
300
|
this.state = STATE_ATOM;
|
|
244
301
|
}
|
|
245
302
|
else {
|
|
246
303
|
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
|
247
304
|
this.currentNode.type = 'PARTIAL';
|
|
305
|
+
startToken();
|
|
248
306
|
this.state = STATE_PARTIAL;
|
|
249
307
|
this.currentNode.isClosed = false;
|
|
250
308
|
}
|
|
@@ -254,17 +312,19 @@ export class TokenParser {
|
|
|
254
312
|
case '~': {
|
|
255
313
|
let nextChr = this.str.charAt(i + 1);
|
|
256
314
|
if (nextChr !== '{') {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
315
|
+
// '~' is an ATOM-CHAR itself, so when it does not start a literal8
|
|
316
|
+
// it starts an atom, including the one-character atom "~" that a
|
|
317
|
+
// space, the end of the line or a closing delimiter ends
|
|
318
|
+
if (imapFormalSyntax['ATOM-CHAR']().includes(nextChr) ||
|
|
319
|
+
nextChr === ' ' ||
|
|
320
|
+
nextChr === '' ||
|
|
321
|
+
closesNode(nextChr, this.currentNode)) {
|
|
322
|
+
startAtom();
|
|
263
323
|
break;
|
|
264
324
|
}
|
|
265
325
|
let error = new Error(`Unexpected literal8 marker at position ${this.pos + i} [E12]`);
|
|
266
326
|
error.code = 'ParserError12';
|
|
267
|
-
error.parserContext = {
|
|
327
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i, chr };
|
|
268
328
|
throw error;
|
|
269
329
|
}
|
|
270
330
|
// Mark the next literal as literal8 type; consumed when '{' is encountered
|
|
@@ -285,7 +345,7 @@ export class TokenParser {
|
|
|
285
345
|
case '*':
|
|
286
346
|
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
|
287
347
|
this.currentNode.type = 'SEQUENCE';
|
|
288
|
-
|
|
348
|
+
startToken(chr);
|
|
289
349
|
this.currentNode.isClosed = false;
|
|
290
350
|
this.state = STATE_SEQUENCE;
|
|
291
351
|
break;
|
|
@@ -366,8 +426,7 @@ export class TokenParser {
|
|
|
366
426
|
break;
|
|
367
427
|
}
|
|
368
428
|
// ')' or ']' terminates the atom AND closes the enclosing LIST or SECTION
|
|
369
|
-
if (this.currentNode.parentNode
|
|
370
|
-
((chr === ')' && this.currentNode.parentNode.type === 'LIST') || (chr === ']' && this.currentNode.parentNode.type === 'SECTION'))) {
|
|
429
|
+
if (closesNode(chr, this.currentNode.parentNode)) {
|
|
371
430
|
this.currentNode.endPos = this.pos + i - 1;
|
|
372
431
|
this.currentNode = this.currentNode.parentNode;
|
|
373
432
|
this.currentNode.isClosed = true;
|
|
@@ -403,14 +462,16 @@ export class TokenParser {
|
|
|
403
462
|
}
|
|
404
463
|
// If the atom so far is all digits and we see ',' or ':', it is actually
|
|
405
464
|
// a sequence set (e.g., "1:5" or "1,3,5"), so reclassify and switch state
|
|
406
|
-
if ((chr === ',' || chr === ':') &&
|
|
465
|
+
if ((chr === ',' || chr === ':') && tokenLength && tokenDigitsOnly) {
|
|
407
466
|
this.currentNode.type = 'SEQUENCE';
|
|
408
467
|
this.currentNode.isClosed = true;
|
|
409
468
|
this.state = STATE_SEQUENCE;
|
|
410
469
|
}
|
|
411
470
|
// [ starts a section group for this element
|
|
412
471
|
// Allowed only for selected elements, otherwise falls through to regular ATOM processing
|
|
413
|
-
if (chr === '[' &&
|
|
472
|
+
if (chr === '[' &&
|
|
473
|
+
tokenLength <= 11 &&
|
|
474
|
+
['BODY', 'BODY.PEEK', 'BINARY', 'BINARY.PEEK'].includes(this.currentNode.value.toUpperCase())) {
|
|
414
475
|
this.currentNode.endPos = this.pos + i;
|
|
415
476
|
this.currentNode = this.createNode(this.currentNode.parentNode, this.pos + i);
|
|
416
477
|
this.currentNode.type = 'SECTION';
|
|
@@ -423,19 +484,19 @@ export class TokenParser {
|
|
|
423
484
|
chr.charCodeAt(0) < 0x80 && // allow 8bit (presumably unicode) bytes
|
|
424
485
|
chr !== ']' &&
|
|
425
486
|
!(chr === '*' && this.currentNode.value === '\\') &&
|
|
426
|
-
(!this.parent || !this.parent.command || !['NO', 'BAD', 'OK'].includes(this.parent.command))) {
|
|
487
|
+
(!this.parent || !this.parent.command || !['NO', 'BAD', 'OK'].includes(this.parent.command.toUpperCase()))) {
|
|
427
488
|
let error = new Error(`Unexpected char at position ${this.pos + i} [E16: ${JSON.stringify(chr)}]`);
|
|
428
489
|
error.code = 'ParserError16';
|
|
429
|
-
error.parserContext = {
|
|
490
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i, chr };
|
|
430
491
|
throw error;
|
|
431
492
|
}
|
|
432
493
|
else if (this.currentNode.value === '\\*') {
|
|
433
494
|
let error = new Error(`Unexpected char at position ${this.pos + i} [E17: ${JSON.stringify(chr)}]`);
|
|
434
495
|
error.code = 'ParserError17';
|
|
435
|
-
error.parserContext = {
|
|
496
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i, chr };
|
|
436
497
|
throw error;
|
|
437
498
|
}
|
|
438
|
-
|
|
499
|
+
appendToToken(chr);
|
|
439
500
|
break;
|
|
440
501
|
case STATE_STRING:
|
|
441
502
|
// DQUOTE ends the string sequence
|
|
@@ -453,7 +514,7 @@ export class TokenParser {
|
|
|
453
514
|
if (i >= len) {
|
|
454
515
|
let error = new Error(`Unexpected end of input at position ${this.pos + i} [E18]`);
|
|
455
516
|
error.code = 'ParserError18';
|
|
456
|
-
error.parserContext = {
|
|
517
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i };
|
|
457
518
|
throw error;
|
|
458
519
|
}
|
|
459
520
|
chr = this.str.charAt(i);
|
|
@@ -462,10 +523,10 @@ export class TokenParser {
|
|
|
462
523
|
break;
|
|
463
524
|
case STATE_PARTIAL:
|
|
464
525
|
if (chr === '>') {
|
|
465
|
-
if (
|
|
526
|
+
if (tokenLast === '.') {
|
|
466
527
|
let error = new Error(`Unexpected end of partial at position ${this.pos + i} [E19]`);
|
|
467
528
|
error.code = 'ParserError19';
|
|
468
|
-
error.parserContext = {
|
|
529
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i, chr };
|
|
469
530
|
throw error;
|
|
470
531
|
}
|
|
471
532
|
this.currentNode.endPos = this.pos + i;
|
|
@@ -475,25 +536,25 @@ export class TokenParser {
|
|
|
475
536
|
checkSP();
|
|
476
537
|
break;
|
|
477
538
|
}
|
|
478
|
-
if (chr === '.' && (
|
|
539
|
+
if (chr === '.' && (!tokenLength || tokenHasDot)) {
|
|
479
540
|
let error = new Error(`Unexpected partial separator . at position ${this.pos + i} [E20]`);
|
|
480
541
|
error.code = 'ParserError20';
|
|
481
|
-
error.parserContext = {
|
|
542
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i, chr };
|
|
482
543
|
throw error;
|
|
483
544
|
}
|
|
484
545
|
if (!imapFormalSyntax.DIGIT().includes(chr) && chr !== '.') {
|
|
485
546
|
let error = new Error(`Unexpected char at position ${this.pos + i} [E21: ${JSON.stringify(chr)}]`);
|
|
486
547
|
error.code = 'ParserError21';
|
|
487
|
-
error.parserContext = {
|
|
548
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i, chr };
|
|
488
549
|
throw error;
|
|
489
550
|
}
|
|
490
|
-
if (
|
|
551
|
+
if (tokenLast === '0' && (tokenLength === 1 || tokenPrev === '.') && chr !== '.') {
|
|
491
552
|
let error = new Error(`Invalid partial at position ${this.pos + i} [E22: ${JSON.stringify(chr)}]`);
|
|
492
553
|
error.code = 'ParserError22';
|
|
493
|
-
error.parserContext = {
|
|
554
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i, chr };
|
|
494
555
|
throw error;
|
|
495
556
|
}
|
|
496
|
-
|
|
557
|
+
appendToToken(chr);
|
|
497
558
|
break;
|
|
498
559
|
case STATE_LITERAL:
|
|
499
560
|
if (this.currentNode.started) {
|
|
@@ -525,7 +586,7 @@ export class TokenParser {
|
|
|
525
586
|
if (!('literalLength' in this.currentNode)) {
|
|
526
587
|
let error = new Error(`Unexpected literal prefix end char } at position ${this.pos + i} [E23]`);
|
|
527
588
|
error.code = 'ParserError23';
|
|
528
|
-
error.parserContext = {
|
|
589
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i, chr };
|
|
529
590
|
throw error;
|
|
530
591
|
}
|
|
531
592
|
if (this.str.charAt(i + 1) === '\n') {
|
|
@@ -537,19 +598,17 @@ export class TokenParser {
|
|
|
537
598
|
else {
|
|
538
599
|
let error = new Error(`Unexpected char at position ${this.pos + i} [E24: ${JSON.stringify(chr)}]`);
|
|
539
600
|
error.code = 'ParserError24';
|
|
540
|
-
error.parserContext = {
|
|
601
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i, chr };
|
|
541
602
|
throw error;
|
|
542
603
|
}
|
|
543
604
|
this.currentNode.literalLength = Number(this.currentNode.literalLength);
|
|
544
605
|
if (!this.currentNode.literalLength) {
|
|
545
606
|
// special case where literal content length is 0
|
|
546
607
|
// close the node right away, do not wait for additional input
|
|
547
|
-
if (this.options.literals
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
|
|
551
|
-
// with their markers instead of shifting by one
|
|
552
|
-
this.currentNode.value = this.options.literals.shift();
|
|
608
|
+
if (this.options.literals) {
|
|
609
|
+
// consume the queue entry of the {0} marker too, so later
|
|
610
|
+
// literals in the same response stay aligned with their markers
|
|
611
|
+
this.currentNode.value = takeLiteral(this.options.literals);
|
|
553
612
|
}
|
|
554
613
|
this.currentNode.endPos = this.pos + i;
|
|
555
614
|
this.currentNode.isClosed = true;
|
|
@@ -559,7 +618,7 @@ export class TokenParser {
|
|
|
559
618
|
}
|
|
560
619
|
else if (this.options.literals) {
|
|
561
620
|
// use the next precached literal values
|
|
562
|
-
this.currentNode.value = this.options.literals
|
|
621
|
+
this.currentNode.value = takeLiteral(this.options.literals);
|
|
563
622
|
// only APPEND arguments are kept as Buffers
|
|
564
623
|
/*
|
|
565
624
|
if ((this.parent.command || '').toString().toUpperCase() !== 'APPEND') {
|
|
@@ -589,7 +648,7 @@ export class TokenParser {
|
|
|
589
648
|
if (literalLength > this.maxLiteralSize || literalLength > available) {
|
|
590
649
|
let overMax = literalLength > this.maxLiteralSize;
|
|
591
650
|
let error = createLiteralTooLargeError(literalLength, overMax ? this.maxLiteralSize : available, overMax ? null : `the ${available} bytes available in the input`);
|
|
592
|
-
error.parserContext = {
|
|
651
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i, chr };
|
|
593
652
|
throw error;
|
|
594
653
|
}
|
|
595
654
|
this.currentNode.started = true;
|
|
@@ -603,95 +662,120 @@ export class TokenParser {
|
|
|
603
662
|
if (!imapFormalSyntax.DIGIT().includes(chr)) {
|
|
604
663
|
let error = new Error(`Unexpected char at position ${this.pos + i} [E25: ${JSON.stringify(chr)}]`);
|
|
605
664
|
error.code = 'ParserError25';
|
|
606
|
-
error.parserContext = {
|
|
665
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i, chr };
|
|
607
666
|
throw error;
|
|
608
667
|
}
|
|
609
668
|
if (this.currentNode.literalLength === '0') {
|
|
610
669
|
let error = new Error(`Invalid literal at position ${this.pos + i} [E26]`);
|
|
611
670
|
error.code = 'ParserError26';
|
|
612
|
-
error.parserContext = {
|
|
671
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i, chr };
|
|
613
672
|
throw error;
|
|
614
673
|
}
|
|
615
674
|
this.currentNode.literalLength = (this.currentNode.literalLength || '') + chr;
|
|
616
675
|
break;
|
|
617
|
-
case STATE_SEQUENCE:
|
|
618
|
-
// space
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
676
|
+
case STATE_SEQUENCE: {
|
|
677
|
+
// A space ends the sequence set, and so does the closing delimiter of the
|
|
678
|
+
// enclosing list or section ("PARTIAL (1:100 5,7,9)", "[COPYUID 1 1:3 4,5]")
|
|
679
|
+
let closesParent = closesNode(chr, this.currentNode.parentNode);
|
|
680
|
+
if (chr === ' ' || closesParent) {
|
|
681
|
+
if (!RE_SINGLE_DIGIT.test(tokenLast) && tokenLast !== '*') {
|
|
682
|
+
if (!tokenHasStar) {
|
|
683
|
+
// a dangling separator, "10:" or "1,"
|
|
684
|
+
continueAsAtom();
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
let error = new Error(`Unexpected end of sequence at position ${this.pos + i} [E27: ${JSON.stringify(chr)}]`);
|
|
622
688
|
error.code = 'ParserError27';
|
|
623
|
-
error.parserContext = {
|
|
689
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i, chr };
|
|
624
690
|
throw error;
|
|
625
691
|
}
|
|
626
|
-
if (this.currentNode.value !== '*' &&
|
|
627
|
-
this.
|
|
628
|
-
this.currentNode.value.at(-2) !== ':') {
|
|
629
|
-
let error = new Error(`Unexpected whitespace at position ${this.pos + i} [E28]`);
|
|
692
|
+
if (this.currentNode.value !== '*' && tokenLast === '*' && tokenPrev !== ':') {
|
|
693
|
+
let error = new Error(`Unexpected end of sequence at position ${this.pos + i} [E28: ${JSON.stringify(chr)}]`);
|
|
630
694
|
error.code = 'ParserError28';
|
|
631
|
-
error.parserContext = {
|
|
695
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i, chr };
|
|
632
696
|
throw error;
|
|
633
697
|
}
|
|
634
698
|
this.currentNode.isClosed = true;
|
|
635
699
|
this.currentNode.endPos = this.pos + i - 1;
|
|
636
700
|
this.currentNode = this.currentNode.parentNode;
|
|
637
701
|
this.state = STATE_NORMAL;
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
this.currentNode.endPos = this.pos + i;
|
|
645
|
-
this.currentNode = this.currentNode.parentNode;
|
|
646
|
-
this.state = STATE_NORMAL;
|
|
647
|
-
checkSP();
|
|
702
|
+
if (closesParent) {
|
|
703
|
+
this.currentNode.isClosed = true;
|
|
704
|
+
this.currentNode.endPos = this.pos + i;
|
|
705
|
+
this.currentNode = this.currentNode.parentNode;
|
|
706
|
+
checkSP();
|
|
707
|
+
}
|
|
648
708
|
break;
|
|
649
709
|
}
|
|
650
710
|
if (chr === ':') {
|
|
651
|
-
if (!RE_SINGLE_DIGIT.test(
|
|
711
|
+
if (!RE_SINGLE_DIGIT.test(tokenLast) && tokenLast !== '*') {
|
|
712
|
+
if (!tokenHasStar) {
|
|
713
|
+
continueAsAtom();
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
652
716
|
let error = new Error(`Unexpected range separator : at position ${this.pos + i} [E29]`);
|
|
653
717
|
error.code = 'ParserError29';
|
|
654
|
-
error.parserContext = {
|
|
718
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i, chr };
|
|
655
719
|
throw error;
|
|
656
720
|
}
|
|
657
721
|
}
|
|
658
722
|
else if (chr === '*') {
|
|
659
|
-
if (![',', ':'].includes(
|
|
723
|
+
if (![',', ':'].includes(tokenLast)) {
|
|
660
724
|
let error = new Error(`Unexpected range wildcard at position ${this.pos + i} [E30]`);
|
|
661
725
|
error.code = 'ParserError30';
|
|
662
|
-
error.parserContext = {
|
|
726
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i, chr };
|
|
663
727
|
throw error;
|
|
664
728
|
}
|
|
665
729
|
}
|
|
666
730
|
else if (chr === ',') {
|
|
667
|
-
if (!RE_SINGLE_DIGIT.test(
|
|
731
|
+
if (!RE_SINGLE_DIGIT.test(tokenLast) && tokenLast !== '*') {
|
|
732
|
+
if (!tokenHasStar) {
|
|
733
|
+
continueAsAtom();
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
668
736
|
let error = new Error(`Unexpected sequence separator , at position ${this.pos + i} [E31]`);
|
|
669
737
|
error.code = 'ParserError31';
|
|
670
|
-
error.parserContext = {
|
|
738
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i, chr };
|
|
671
739
|
throw error;
|
|
672
740
|
}
|
|
673
|
-
if (
|
|
741
|
+
if (tokenLast === '*' && tokenPrev !== ':') {
|
|
674
742
|
let error = new Error(`Unexpected sequence separator , at position ${this.pos + i} [E32]`);
|
|
675
743
|
error.code = 'ParserError32';
|
|
676
|
-
error.parserContext = {
|
|
744
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i, chr };
|
|
677
745
|
throw error;
|
|
678
746
|
}
|
|
679
747
|
}
|
|
680
748
|
else if (!RE_SINGLE_DIGIT.test(chr)) {
|
|
749
|
+
if (!tokenHasStar) {
|
|
750
|
+
continueAsAtom();
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
681
753
|
let error = new Error(`Unexpected char at position ${this.pos + i} [E33: ${JSON.stringify(chr)}]`);
|
|
682
754
|
error.code = 'ParserError33';
|
|
683
|
-
error.parserContext = {
|
|
755
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i, chr };
|
|
684
756
|
throw error;
|
|
685
757
|
}
|
|
686
|
-
if (RE_SINGLE_DIGIT.test(chr) &&
|
|
758
|
+
if (RE_SINGLE_DIGIT.test(chr) && tokenLast === '*') {
|
|
687
759
|
let error = new Error(`Unexpected number at position ${this.pos + i} [E34: ${JSON.stringify(chr)}]`);
|
|
688
760
|
error.code = 'ParserError34';
|
|
689
|
-
error.parserContext = {
|
|
761
|
+
error.parserContext = { ...boundedInput(this.str), pos: this.pos + i, chr };
|
|
690
762
|
throw error;
|
|
691
763
|
}
|
|
692
|
-
|
|
764
|
+
appendToToken(chr);
|
|
693
765
|
break;
|
|
766
|
+
}
|
|
694
767
|
}
|
|
695
768
|
}
|
|
769
|
+
// the same applies to a digit-led token that ends the input on a dangling separator ("10:")
|
|
770
|
+
if (this.state === STATE_SEQUENCE && (tokenLast === ':' || tokenLast === ',') && !tokenHasStar) {
|
|
771
|
+
this.currentNode.type = 'ATOM';
|
|
772
|
+
}
|
|
773
|
+
// A star-led sequence set ("*:4") is only closed by a space or a closing delimiter, so one
|
|
774
|
+
// that ends the input is closed here, under the same rules a space applies to it. A bare "*"
|
|
775
|
+
// is left to getAttributes(), which reads it as an atom
|
|
776
|
+
if (this.state === STATE_SEQUENCE && !this.currentNode.isClosed && (RE_SINGLE_DIGIT.test(tokenLast) || (tokenLast === '*' && tokenPrev === ':'))) {
|
|
777
|
+
this.currentNode.isClosed = true;
|
|
778
|
+
this.currentNode.endPos = this.pos + this.str.length - 1;
|
|
779
|
+
}
|
|
696
780
|
}
|
|
697
781
|
}
|