imapflow 1.0.154 → 1.0.156

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.156](https://github.com/postalsys/imapflow/compare/v1.0.155...v1.0.156) (2024-03-07)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * micro-optimizations for parser ([3451ad2](https://github.com/postalsys/imapflow/commit/3451ad235532ca1a64a829f9f2f8e77bcef3a17d))
9
+
10
+ ## [1.0.155](https://github.com/postalsys/imapflow/compare/v1.0.154...v1.0.155) (2024-03-06)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **exchange:** handle special error response from exchange ([3ed4198](https://github.com/postalsys/imapflow/commit/3ed4198c1b5504ab0b46926fec179c682988b2e3))
16
+
3
17
  ## [1.0.154](https://github.com/postalsys/imapflow/compare/v1.0.153...v1.0.154) (2024-03-05)
4
18
 
5
19
 
@@ -24,29 +24,37 @@ module.exports = async (command, options) => {
24
24
  const parser = new ParserInstance(command, options);
25
25
  const response = {};
26
26
 
27
- response.tag = await parser.getTag();
28
- await parser.getSpace();
29
- response.command = await parser.getCommand();
27
+ try {
28
+ response.tag = await parser.getTag();
30
29
 
31
- if (nullBytesRemoved) {
32
- response.nullBytesRemoved = nullBytesRemoved;
33
- }
34
-
35
- if (['UID', 'AUTHENTICATE'].indexOf((response.command || '').toUpperCase()) >= 0) {
36
30
  await parser.getSpace();
37
- response.command += ' ' + (await parser.getElement(imapFormalSyntax.command()));
38
- }
31
+ response.command = await parser.getCommand();
39
32
 
40
- if (parser.remainder.trim().length) {
41
- await parser.getSpace();
42
- response.attributes = await parser.getAttributes();
43
- }
33
+ if (nullBytesRemoved) {
34
+ response.nullBytesRemoved = nullBytesRemoved;
35
+ }
44
36
 
45
- if (parser.humanReadable) {
46
- response.attributes = (response.attributes || []).concat({
47
- type: 'TEXT',
48
- value: parser.humanReadable
49
- });
37
+ if (['UID', 'AUTHENTICATE'].indexOf((response.command || '').toUpperCase()) >= 0) {
38
+ await parser.getSpace();
39
+ response.command += ' ' + (await parser.getElement(imapFormalSyntax.command()));
40
+ }
41
+
42
+ if (parser.remainder.trim().length) {
43
+ await parser.getSpace();
44
+ response.attributes = await parser.getAttributes();
45
+ }
46
+
47
+ if (parser.humanReadable) {
48
+ response.attributes = (response.attributes || []).concat({
49
+ type: 'TEXT',
50
+ value: parser.humanReadable
51
+ });
52
+ }
53
+ } catch (err) {
54
+ if (err.code === 'ParserErrorExchange' && err.parserContext && err.parserContext.value) {
55
+ return err.parserContext.value;
56
+ }
57
+ throw err;
50
58
  }
51
59
 
52
60
  return response;
@@ -73,16 +73,42 @@ class ParserInstance {
73
73
  let match, element, errPos;
74
74
 
75
75
  if (this.remainder.match(/^\s/)) {
76
- throw new Error(`Unexpected whitespace at position ${this.pos} [E1]`);
76
+ let error = new Error(`Unexpected whitespace at position ${this.pos} [E1]`);
77
+ error.code = 'ParserError1';
78
+ error.parserContext = { input: this.input, pos: this.pos };
79
+ throw error;
77
80
  }
78
81
 
79
82
  if ((match = this.remainder.match(/^\s*[^\s]+(?=\s|$)/))) {
80
83
  element = match[0];
81
84
  if ((errPos = imapFormalSyntax.verify(element, syntax)) >= 0) {
82
- throw new Error(`Unexpected char at position ${this.pos + errPos} [E2: ${JSON.stringify(element.charAt(errPos))}]`);
85
+ if (this.tag === 'Server' && element === 'Unavailable.') {
86
+ // Exchange error
87
+ let error = new Error(`Server returned an error: ${this.input}`);
88
+ error.code = 'ParserErrorExchange';
89
+ error.parserContext = {
90
+ input: this.input,
91
+ element,
92
+ pos: this.pos,
93
+ value: {
94
+ tag: '*',
95
+ command: 'BAD',
96
+ attributes: [{ type: 'TEXT', value: this.input }]
97
+ }
98
+ };
99
+ throw error;
100
+ }
101
+
102
+ let error = new Error(`Unexpected char at position ${this.pos + errPos} [E2: ${JSON.stringify(element.charAt(errPos))}]`);
103
+ error.code = 'ParserError2';
104
+ error.parserContext = { input: this.input, element, pos: this.pos };
105
+ throw error;
83
106
  }
84
107
  } else {
85
- throw new Error(`Unexpected end of input at position ${this.pos} [E3]`);
108
+ let error = new Error(`Unexpected end of input at position ${this.pos} [E3]`);
109
+ error.code = 'ParserError3';
110
+ error.parserContext = { input: this.input, pos: this.pos };
111
+ throw error;
86
112
  }
87
113
 
88
114
  this.pos += match[0].length;
@@ -93,11 +119,17 @@ class ParserInstance {
93
119
 
94
120
  async getSpace() {
95
121
  if (!this.remainder.length) {
96
- throw new Error(`Unexpected end of input at position ${this.pos} [E4]`);
122
+ let error = new Error(`Unexpected end of input at position ${this.pos} [E4]`);
123
+ error.code = 'ParserError4';
124
+ error.parserContext = { input: this.input, pos: this.pos };
125
+ throw error;
97
126
  }
98
127
 
99
128
  if (imapFormalSyntax.verify(this.remainder.charAt(0), imapFormalSyntax.SP()) >= 0) {
100
- throw new Error(`Unexpected char at position ${this.pos} [E5: ${JSON.stringify(this.remainder.charAt(0))}]`);
129
+ let error = new Error(`Unexpected char at position ${this.pos} [E5: ${JSON.stringify(this.remainder.charAt(0))}]`);
130
+ error.code = 'ParserError5';
131
+ error.parserContext = { input: this.input, element: this.remainder, pos: this.pos };
132
+ throw error;
101
133
  }
102
134
 
103
135
  this.pos++;
@@ -106,11 +138,17 @@ class ParserInstance {
106
138
 
107
139
  async getAttributes() {
108
140
  if (!this.remainder.length) {
109
- throw new Error(`Unexpected end of input at position ${this.pos} [E6]`);
141
+ let error = new Error(`Unexpected end of input at position ${this.pos} [E6]`);
142
+ error.code = 'ParserError6';
143
+ error.parserContext = { input: this.input, pos: this.pos };
144
+ throw error;
110
145
  }
111
146
 
112
147
  if (this.remainder.match(/^\s/)) {
113
- throw new Error(`Unexpected whitespace at position ${this.pos} [E7]`);
148
+ let error = new Error(`Unexpected whitespace at position ${this.pos} [E7]`);
149
+ error.code = 'ParserError7';
150
+ error.parserContext = { input: this.input, element: this.remainder, pos: this.pos };
151
+ throw error;
114
152
  }
115
153
 
116
154
  const tokenParser = new TokenParser(this, this.pos, this.remainder, this.options);
@@ -4,6 +4,17 @@
4
4
 
5
5
  const imapFormalSyntax = require('./imap-formal-syntax');
6
6
 
7
+ const STATE_ATOM = 0x001;
8
+ const STATE_LITERAL = 0x002;
9
+ const STATE_NORMAL = 0x003;
10
+ const STATE_PARTIAL = 0x004;
11
+ const STATE_SEQUENCE = 0x005;
12
+ const STATE_STRING = 0x006;
13
+ const STATE_TEXT = 0x007;
14
+
15
+ const RE_DIGITS = /^\d+$/;
16
+ const RE_SINGLE_DIGIT = /^\d$/;
17
+
7
18
  class TokenParser {
8
19
  constructor(parent, startPos, str, options) {
9
20
  this.str = (str || '').toString();
@@ -15,7 +26,7 @@ class TokenParser {
15
26
 
16
27
  this.currentNode.type = 'TREE';
17
28
 
18
- this.state = 'NORMAL';
29
+ this.state = STATE_NORMAL;
19
30
  }
20
31
 
21
32
  async getAttributes() {
@@ -36,7 +47,10 @@ class TokenParser {
36
47
 
37
48
  // If the node was never closed, throw it
38
49
  if (!node.isClosed) {
39
- throw new Error(`Unexpected end of input at position ${this.pos + this.str.length - 1} [E6]`);
50
+ let error = new Error(`Unexpected end of input at position ${this.pos + this.str.length - 1} [E9]`);
51
+ error.code = 'ParserError9';
52
+ error.parserContext = { input: this.str, pos: this.pos + this.str.length - 1 };
53
+ throw error;
40
54
  }
41
55
 
42
56
  let type = (node.type || '').toString().toUpperCase();
@@ -129,13 +143,13 @@ class TokenParser {
129
143
  chr = this.str.charAt(i);
130
144
 
131
145
  switch (this.state) {
132
- case 'NORMAL':
146
+ case STATE_NORMAL:
133
147
  switch (chr) {
134
148
  // DQUOTE starts a new string
135
149
  case '"':
136
150
  this.currentNode = this.createNode(this.currentNode, this.pos + i);
137
151
  this.currentNode.type = 'string';
138
- this.state = 'STRING';
152
+ this.state = STATE_STRING;
139
153
  this.currentNode.isClosed = false;
140
154
  break;
141
155
 
@@ -149,7 +163,10 @@ class TokenParser {
149
163
  // ) closes a list
150
164
  case ')':
151
165
  if (this.currentNode.type !== 'LIST') {
152
- throw new Error(`Unexpected list terminator ) at position ${this.pos + i} [E7]`);
166
+ let error = new Error(`Unexpected list terminator ) at position ${this.pos + i} [E10]`);
167
+ error.code = 'ParserError10';
168
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
169
+ throw error;
153
170
  }
154
171
 
155
172
  this.currentNode.isClosed = true;
@@ -162,7 +179,10 @@ class TokenParser {
162
179
  // ] closes section group
163
180
  case ']':
164
181
  if (this.currentNode.type !== 'SECTION') {
165
- throw new Error(`Unexpected section terminator ] at position ${this.pos + i} [E8]`);
182
+ let error = new Error(`Unexpected section terminator ] at position ${this.pos + i} [E11]`);
183
+ error.code = 'ParserError11';
184
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
185
+ throw error;
166
186
  }
167
187
  this.currentNode.isClosed = true;
168
188
  this.currentNode.endPos = this.pos + i;
@@ -177,11 +197,11 @@ class TokenParser {
177
197
  this.currentNode = this.createNode(this.currentNode, this.pos + i);
178
198
  this.currentNode.type = 'ATOM';
179
199
  this.currentNode.value = chr;
180
- this.state = 'ATOM';
200
+ this.state = STATE_ATOM;
181
201
  } else {
182
202
  this.currentNode = this.createNode(this.currentNode, this.pos + i);
183
203
  this.currentNode.type = 'PARTIAL';
184
- this.state = 'PARTIAL';
204
+ this.state = STATE_PARTIAL;
185
205
  this.currentNode.isClosed = false;
186
206
  }
187
207
  break;
@@ -195,11 +215,14 @@ class TokenParser {
195
215
  this.currentNode = this.createNode(this.currentNode, this.pos + i);
196
216
  this.currentNode.type = 'ATOM';
197
217
  this.currentNode.value = chr;
198
- this.state = 'ATOM';
218
+ this.state = STATE_ATOM;
199
219
  break;
200
220
  }
201
221
 
202
- throw new Error(`Unexpected literal8 marker at position ${this.pos + i} [E9]`);
222
+ let error = new Error(`Unexpected literal8 marker at position ${this.pos + i} [E12]`);
223
+ error.code = 'ParserError12';
224
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
225
+ throw error;
203
226
  }
204
227
  this.expectedLiteralType = 'literal8';
205
228
  break;
@@ -211,7 +234,7 @@ class TokenParser {
211
234
  this.currentNode.type = 'LITERAL';
212
235
  this.currentNode.literalType = this.expectedLiteralType || 'literal';
213
236
  this.expectedLiteralType = false;
214
- this.state = 'LITERAL';
237
+ this.state = STATE_LITERAL;
215
238
  this.currentNode.isClosed = false;
216
239
  break;
217
240
 
@@ -221,7 +244,7 @@ class TokenParser {
221
244
  this.currentNode.type = 'SEQUENCE';
222
245
  this.currentNode.value = chr;
223
246
  this.currentNode.isClosed = false;
224
- this.state = 'SEQUENCE';
247
+ this.state = STATE_SEQUENCE;
225
248
  break;
226
249
 
227
250
  // normally a space should never occur
@@ -241,7 +264,7 @@ class TokenParser {
241
264
  this.currentNode = this.createNode(this.currentNode, this.pos + i);
242
265
  this.currentNode.type = 'SECTION';
243
266
  this.currentNode.isClosed = false;
244
- this.state = 'NORMAL';
267
+ this.state = STATE_NORMAL;
245
268
 
246
269
  // RFC2221 defines a response code REFERRAL whose payload is an
247
270
  // RFC2192/RFC5092 imapurl that we will try to parse as an ATOM but
@@ -282,23 +305,26 @@ class TokenParser {
282
305
  // Allow % to support LIST '' %
283
306
  // Allow 8bit characters (presumably unicode)
284
307
  if (imapFormalSyntax['ATOM-CHAR']().indexOf(chr) < 0 && chr !== '\\' && chr !== '%' && chr.charCodeAt(0) < 0x80) {
285
- throw new Error(`Unexpected char at position ${this.pos + i} [E10: ${JSON.stringify(chr)}]`);
308
+ let error = new Error(`Unexpected char at position ${this.pos + i} [E13: ${JSON.stringify(chr)}]`);
309
+ error.code = 'ParserError13';
310
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
311
+ throw error;
286
312
  }
287
313
 
288
314
  this.currentNode = this.createNode(this.currentNode, this.pos + i);
289
315
  this.currentNode.type = 'ATOM';
290
316
  this.currentNode.value = chr;
291
- this.state = 'ATOM';
317
+ this.state = STATE_ATOM;
292
318
  break;
293
319
  }
294
320
  break;
295
321
 
296
- case 'ATOM':
322
+ case STATE_ATOM:
297
323
  // space finishes an atom
298
324
  if (chr === ' ') {
299
325
  this.currentNode.endPos = this.pos + i - 1;
300
326
  this.currentNode = this.currentNode.parentNode;
301
- this.state = 'NORMAL';
327
+ this.state = STATE_NORMAL;
302
328
  break;
303
329
  }
304
330
 
@@ -313,34 +339,40 @@ class TokenParser {
313
339
  this.currentNode.isClosed = true;
314
340
  this.currentNode.endPos = this.pos + i;
315
341
  this.currentNode = this.currentNode.parentNode;
316
- this.state = 'NORMAL';
342
+ this.state = STATE_NORMAL;
317
343
  checkSP();
318
344
 
319
345
  break;
320
346
  }
321
347
 
322
- if ((chr === ',' || chr === ':') && this.currentNode.value.match(/^\d+$/)) {
348
+ if ((chr === ',' || chr === ':') && RE_DIGITS.test(this.currentNode.value)) {
323
349
  this.currentNode.type = 'SEQUENCE';
324
350
  this.currentNode.isClosed = true;
325
- this.state = 'SEQUENCE';
351
+ this.state = STATE_SEQUENCE;
326
352
  }
327
353
 
328
354
  // [ starts a section group for this element
329
355
  if (chr === '[') {
330
356
  // allowed only for selected elements
331
357
  if (['BODY', 'BODY.PEEK', 'BINARY', 'BINARY.PEEK'].indexOf(this.currentNode.value.toUpperCase()) < 0) {
332
- throw new Error(`Unexpected section start char [ at position ${this.pos} [E11]`);
358
+ let error = new Error(`Unexpected section start char [ at position ${this.pos + i} [E14]`);
359
+ error.code = 'ParserError14';
360
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
361
+ throw error;
333
362
  }
334
363
  this.currentNode.endPos = this.pos + i;
335
364
  this.currentNode = this.createNode(this.currentNode.parentNode, this.pos + i);
336
365
  this.currentNode.type = 'SECTION';
337
366
  this.currentNode.isClosed = false;
338
- this.state = 'NORMAL';
367
+ this.state = STATE_NORMAL;
339
368
  break;
340
369
  }
341
370
 
342
371
  if (chr === '<') {
343
- throw new Error(`Unexpected start of partial at position ${this.pos} [E12]`);
372
+ let error = new Error(`Unexpected start of partial at position ${this.pos + i} [E15]`);
373
+ error.code = 'ParserError15';
374
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
375
+ throw error;
344
376
  }
345
377
 
346
378
  // if the char is not ATOM compatible, throw. Allow \* as an exception
@@ -351,21 +383,27 @@ class TokenParser {
351
383
  !(chr === '*' && this.currentNode.value === '\\') &&
352
384
  (!this.parent || !this.parent.command || !['NO', 'BAD', 'OK'].includes(this.parent.command))
353
385
  ) {
354
- throw new Error(`Unexpected char at position ${this.pos + i} [E13: ${JSON.stringify(chr)}]`);
386
+ let error = new Error(`Unexpected char at position ${this.pos + i} [E16: ${JSON.stringify(chr)}]`);
387
+ error.code = 'ParserError16';
388
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
389
+ throw error;
355
390
  } else if (this.currentNode.value === '\\*') {
356
- throw new Error(`Unexpected char at position ${this.pos + i} [E14: ${JSON.stringify(chr)}]`);
391
+ let error = new Error(`Unexpected char at position ${this.pos + i} [E17: ${JSON.stringify(chr)}]`);
392
+ error.code = 'ParserError17';
393
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
394
+ throw error;
357
395
  }
358
396
 
359
397
  this.currentNode.value += chr;
360
398
  break;
361
399
 
362
- case 'STRING':
400
+ case STATE_STRING:
363
401
  // DQUOTE ends the string sequence
364
402
  if (chr === '"') {
365
403
  this.currentNode.endPos = this.pos + i;
366
404
  this.currentNode.isClosed = true;
367
405
  this.currentNode = this.currentNode.parentNode;
368
- this.state = 'NORMAL';
406
+ this.state = STATE_NORMAL;
369
407
 
370
408
  checkSP();
371
409
  break;
@@ -375,7 +413,10 @@ class TokenParser {
375
413
  if (chr === '\\') {
376
414
  i++;
377
415
  if (i >= len) {
378
- throw new Error(`Unexpected end of input at position ${this.pos + i} [E15]`);
416
+ let error = new Error(`Unexpected end of input at position ${this.pos + i} [E18]`);
417
+ error.code = 'ParserError18';
418
+ error.parserContext = { input: this.str, pos: this.pos + i };
419
+ throw error;
379
420
  }
380
421
  chr = this.str.charAt(i);
381
422
  }
@@ -383,35 +424,47 @@ class TokenParser {
383
424
  this.currentNode.value += chr;
384
425
  break;
385
426
 
386
- case 'PARTIAL':
427
+ case STATE_PARTIAL:
387
428
  if (chr === '>') {
388
- if (this.currentNode.value.substr(-1) === '.') {
389
- throw new Error(`Unexpected end of partial at position ${this.pos} [E16]`);
429
+ if (this.currentNode.value.at(-1) === '.') {
430
+ let error = new Error(`Unexpected end of partial at position ${this.pos + 1} [E19]`);
431
+ error.code = 'ParserError19';
432
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
433
+ throw error;
390
434
  }
391
435
  this.currentNode.endPos = this.pos + i;
392
436
  this.currentNode.isClosed = true;
393
437
  this.currentNode = this.currentNode.parentNode;
394
- this.state = 'NORMAL';
438
+ this.state = STATE_NORMAL;
395
439
  checkSP();
396
440
  break;
397
441
  }
398
442
 
399
443
  if (chr === '.' && (!this.currentNode.value.length || this.currentNode.value.match(/\./))) {
400
- throw new Error(`Unexpected partial separator . at position ${this.pos} [E17]`);
444
+ let error = new Error(`Unexpected partial separator . at position ${this.pos + 1} [E20]`);
445
+ error.code = 'ParserError20';
446
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
447
+ throw error;
401
448
  }
402
449
 
403
450
  if (imapFormalSyntax.DIGIT().indexOf(chr) < 0 && chr !== '.') {
404
- throw new Error(`Unexpected char at position ${this.pos + i} [E18: ${JSON.stringify(chr)}]`);
451
+ let error = new Error(`Unexpected char at position ${this.pos + i} [E21: ${JSON.stringify(chr)}]`);
452
+ error.code = 'ParserError21';
453
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
454
+ throw error;
405
455
  }
406
456
 
407
457
  if (this.currentNode.value.match(/^0$|\.0$/) && chr !== '.') {
408
- throw new Error(`Invalid partial at position ${this.pos + i} [E19: ${JSON.stringify(chr)}]`);
458
+ let error = new Error(`Invalid partial at position ${this.pos + i} [E22: ${JSON.stringify(chr)}]`);
459
+ error.code = 'ParserError22';
460
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
461
+ throw error;
409
462
  }
410
463
 
411
464
  this.currentNode.value += chr;
412
465
  break;
413
466
 
414
- case 'LITERAL':
467
+ case STATE_LITERAL:
415
468
  if (this.currentNode.started) {
416
469
  // only relevant if literals are not already parsed out from input
417
470
 
@@ -431,7 +484,7 @@ class TokenParser {
431
484
  this.currentNode.value = this.currentNode.chBuffer.toString('binary');
432
485
  this.currentNode.chBuffer = Buffer.alloc(0);
433
486
  this.currentNode = this.currentNode.parentNode;
434
- this.state = 'NORMAL';
487
+ this.state = STATE_NORMAL;
435
488
  checkSP();
436
489
  }
437
490
  break;
@@ -444,14 +497,20 @@ class TokenParser {
444
497
 
445
498
  if (chr === '}') {
446
499
  if (!('literalLength' in this.currentNode)) {
447
- throw new Error(`Unexpected literal prefix end char } at position ${this.pos + i} [E20]`);
500
+ let error = new Error(`Unexpected literal prefix end char } at position ${this.pos + i} [E23]`);
501
+ error.code = 'ParserError23';
502
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
503
+ throw error;
448
504
  }
449
505
  if (this.str.charAt(i + 1) === '\n') {
450
506
  i++;
451
507
  } else if (this.str.charAt(i + 1) === '\r' && this.str.charAt(i + 2) === '\n') {
452
508
  i += 2;
453
509
  } else {
454
- throw new Error(`Unexpected char at position ${this.pos + i} [E21: ${JSON.stringify(chr)}]`);
510
+ let error = new Error(`Unexpected char at position ${this.pos + i} [E24: ${JSON.stringify(chr)}]`);
511
+ error.code = 'ParserError24';
512
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
513
+ throw error;
455
514
  }
456
515
 
457
516
  this.currentNode.literalLength = Number(this.currentNode.literalLength);
@@ -462,7 +521,7 @@ class TokenParser {
462
521
  this.currentNode.endPos = this.pos + i;
463
522
  this.currentNode.isClosed = true;
464
523
  this.currentNode = this.currentNode.parentNode;
465
- this.state = 'NORMAL';
524
+ this.state = STATE_NORMAL;
466
525
  checkSP();
467
526
  } else if (this.options.literals) {
468
527
  // use the next precached literal values
@@ -480,7 +539,7 @@ class TokenParser {
480
539
  this.currentNode.started = false;
481
540
  this.currentNode.isClosed = true;
482
541
  this.currentNode = this.currentNode.parentNode;
483
- this.state = 'NORMAL';
542
+ this.state = STATE_NORMAL;
484
543
  checkSP();
485
544
  } else {
486
545
  this.currentNode.started = true;
@@ -492,29 +551,41 @@ class TokenParser {
492
551
  break;
493
552
  }
494
553
  if (imapFormalSyntax.DIGIT().indexOf(chr) < 0) {
495
- throw new Error(`Unexpected char at position ${this.pos + i} [E22: ${JSON.stringify(chr)}]`);
554
+ let error = new Error(`Unexpected char at position ${this.pos + i} [E25: ${JSON.stringify(chr)}]`);
555
+ error.code = 'ParserError25';
556
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
557
+ throw error;
496
558
  }
497
559
  if (this.currentNode.literalLength === '0') {
498
- throw new Error(`Invalid literal at position ${this.pos + i} [E23]`);
560
+ let error = new Error(`Invalid literal at position ${this.pos + i} [E26]`);
561
+ error.code = 'ParserError26';
562
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
563
+ throw error;
499
564
  }
500
565
  this.currentNode.literalLength = (this.currentNode.literalLength || '') + chr;
501
566
  break;
502
567
 
503
- case 'SEQUENCE':
568
+ case STATE_SEQUENCE:
504
569
  // space finishes the sequence set
505
570
  if (chr === ' ') {
506
- if (!this.currentNode.value.substr(-1).match(/\d/) && this.currentNode.value.substr(-1) !== '*') {
507
- throw new Error(`Unexpected whitespace at position ${this.pos + i} [E24]`);
571
+ if (!RE_SINGLE_DIGIT.test(this.currentNode.value.at(-1)) && this.currentNode.value.at(-1) !== '*') {
572
+ let error = new Error(`Unexpected whitespace at position ${this.pos + i} [E27]`);
573
+ error.code = 'ParserError27';
574
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
575
+ throw error;
508
576
  }
509
577
 
510
- if (this.currentNode.value !== '*' && this.currentNode.value.substr(-1) === '*' && this.currentNode.value.substr(-2, 1) !== ':') {
511
- throw new Error(`Unexpected whitespace at position ${this.pos + i} [E25]`);
578
+ if (this.currentNode.value !== '*' && this.currentNode.value.at(-1) === '*' && this.currentNode.value.at(-2) !== ':') {
579
+ let error = new Error(`Unexpected whitespace at position ${this.pos + i} [E28]`);
580
+ error.code = 'ParserError28';
581
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
582
+ throw error;
512
583
  }
513
584
 
514
585
  this.currentNode.isClosed = true;
515
586
  this.currentNode.endPos = this.pos + i - 1;
516
587
  this.currentNode = this.currentNode.parentNode;
517
- this.state = 'NORMAL';
588
+ this.state = STATE_NORMAL;
518
589
  break;
519
590
  } else if (this.currentNode.parentNode && chr === ']' && this.currentNode.parentNode.type === 'SECTION') {
520
591
  this.currentNode.endPos = this.pos + i - 1;
@@ -523,39 +594,57 @@ class TokenParser {
523
594
  this.currentNode.isClosed = true;
524
595
  this.currentNode.endPos = this.pos + i;
525
596
  this.currentNode = this.currentNode.parentNode;
526
- this.state = 'NORMAL';
597
+ this.state = STATE_NORMAL;
527
598
 
528
599
  checkSP();
529
600
  break;
530
601
  }
531
602
 
532
603
  if (chr === ':') {
533
- if (!this.currentNode.value.substr(-1).match(/\d/) && this.currentNode.value.substr(-1) !== '*') {
534
- throw new Error(`Unexpected range separator : at position ${this.pos + i} [E26]`);
604
+ if (!RE_SINGLE_DIGIT.test(this.currentNode.value.at(-1)) && this.currentNode.value.at(-1) !== '*') {
605
+ let error = new Error(`Unexpected range separator : at position ${this.pos + i} [E29]`);
606
+ error.code = 'ParserError29';
607
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
608
+ throw error;
535
609
  }
536
610
  } else if (chr === '*') {
537
- if ([',', ':'].indexOf(this.currentNode.value.substr(-1)) < 0) {
538
- throw new Error(`Unexpected range wildcard at position ${this.pos + i} [E27]`);
611
+ if ([',', ':'].indexOf(this.currentNode.value.at(-1)) < 0) {
612
+ let error = new Error(`Unexpected range wildcard at position ${this.pos + i} [E30]`);
613
+ error.code = 'ParserError30';
614
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
615
+ throw error;
539
616
  }
540
617
  } else if (chr === ',') {
541
- if (!this.currentNode.value.substr(-1).match(/\d/) && this.currentNode.value.substr(-1) !== '*') {
542
- throw new Error(`Unexpected sequence separator , at position ${this.pos + i} [E28]`);
618
+ if (!RE_SINGLE_DIGIT.test(this.currentNode.value.at(-1)) && this.currentNode.value.at(-1) !== '*') {
619
+ let error = new Error(`Unexpected sequence separator , at position ${this.pos + i} [E31]`);
620
+ error.code = 'ParserError31';
621
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
622
+ throw error;
543
623
  }
544
- if (this.currentNode.value.substr(-1) === '*' && this.currentNode.value.substr(-2, 1) !== ':') {
545
- throw new Error(`Unexpected sequence separator , at position ${this.pos + i} [E29]`);
624
+ if (this.currentNode.value.at(-1) === '*' && this.currentNode.value.at(-2) !== ':') {
625
+ let error = new Error(`Unexpected sequence separator , at position ${this.pos + i} [E32]`);
626
+ error.code = 'ParserError32';
627
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
628
+ throw error;
546
629
  }
547
- } else if (!chr.match(/\d/)) {
548
- throw new Error(`Unexpected char at position ${this.pos + i} [E30: ${JSON.stringify(chr)}]`);
630
+ } else if (!RE_SINGLE_DIGIT.test(chr)) {
631
+ let error = new Error(`Unexpected char at position ${this.pos + i} [E33: ${JSON.stringify(chr)}]`);
632
+ error.code = 'ParserError33';
633
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
634
+ throw error;
549
635
  }
550
636
 
551
- if (chr.match(/\d/) && this.currentNode.value.substr(-1) === '*') {
552
- throw new Error(`Unexpected number at position ${this.pos + i} [E31: ${JSON.stringify(chr)}]`);
637
+ if (RE_SINGLE_DIGIT.test(chr) && this.currentNode.value.at(-1) === '*') {
638
+ let error = new Error(`Unexpected number at position ${this.pos + i} [E34: ${JSON.stringify(chr)}]`);
639
+ error.code = 'ParserError34';
640
+ error.parserContext = { input: this.str, pos: this.pos + i, chr };
641
+ throw error;
553
642
  }
554
643
 
555
644
  this.currentNode.value += chr;
556
645
  break;
557
646
 
558
- case 'TEXT':
647
+ case STATE_TEXT:
559
648
  this.currentNode.value += chr;
560
649
  break;
561
650
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.0.154",
3
+ "version": "1.0.156",
4
4
  "description": "IMAP Client for Node",
5
5
  "main": "./lib/imap-flow.js",
6
6
  "scripts": {
@@ -978,6 +978,14 @@ module.exports['IMAP Parser, NO with a dot'] = test =>
978
978
  test.deepEqual(parsed.attributes, [{ type: 'TEXT', value: 'Server Unavailable. 15' }]);
979
979
  });
980
980
 
981
+ module.exports['IMAP Parser, no tag or response (Exchange)'] = test =>
982
+ asyncWrapper(test, async test => {
983
+ let parsed = await parser('Server Unavailable. 15');
984
+ console.log(parsed);
985
+ test.equal(parsed.command, 'BAD');
986
+ test.deepEqual(parsed.attributes, [{ type: 'TEXT', value: 'Server Unavailable. 15' }]);
987
+ });
988
+
981
989
  module.exports['IMAP Parser, BAD with throttling'] = test =>
982
990
  asyncWrapper(test, async test => {
983
991
  let parsed = await parser('X BAD Request is throttled. Suggested Backoff Time: 92415 milliseconds');