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 +14 -0
- package/lib/handler/imap-parser.js +27 -19
- package/lib/handler/parser-instance.js +45 -7
- package/lib/handler/token-parser.js +152 -63
- package/package.json +1 -1
- package/test/imap-parser-test.js +8 -0
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
|
-
|
|
28
|
-
|
|
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
|
|
38
|
-
}
|
|
31
|
+
response.command = await parser.getCommand();
|
|
39
32
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
33
|
+
if (nullBytesRemoved) {
|
|
34
|
+
response.nullBytesRemoved = nullBytesRemoved;
|
|
35
|
+
}
|
|
44
36
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
218
|
+
this.state = STATE_ATOM;
|
|
199
219
|
break;
|
|
200
220
|
}
|
|
201
221
|
|
|
202
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
317
|
+
this.state = STATE_ATOM;
|
|
292
318
|
break;
|
|
293
319
|
}
|
|
294
320
|
break;
|
|
295
321
|
|
|
296
|
-
case
|
|
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 =
|
|
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 =
|
|
342
|
+
this.state = STATE_NORMAL;
|
|
317
343
|
checkSP();
|
|
318
344
|
|
|
319
345
|
break;
|
|
320
346
|
}
|
|
321
347
|
|
|
322
|
-
if ((chr === ',' || chr === ':') && this.currentNode.value
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
367
|
+
this.state = STATE_NORMAL;
|
|
339
368
|
break;
|
|
340
369
|
}
|
|
341
370
|
|
|
342
371
|
if (chr === '<') {
|
|
343
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
427
|
+
case STATE_PARTIAL:
|
|
387
428
|
if (chr === '>') {
|
|
388
|
-
if (this.currentNode.value.
|
|
389
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
568
|
+
case STATE_SEQUENCE:
|
|
504
569
|
// space finishes the sequence set
|
|
505
570
|
if (chr === ' ') {
|
|
506
|
-
if (!this.currentNode.value.
|
|
507
|
-
|
|
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.
|
|
511
|
-
|
|
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 =
|
|
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 =
|
|
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.
|
|
534
|
-
|
|
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.
|
|
538
|
-
|
|
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.
|
|
542
|
-
|
|
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.
|
|
545
|
-
|
|
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 (!
|
|
548
|
-
|
|
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 (
|
|
552
|
-
|
|
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
|
|
647
|
+
case STATE_TEXT:
|
|
559
648
|
this.currentNode.value += chr;
|
|
560
649
|
break;
|
|
561
650
|
}
|
package/package.json
CHANGED
package/test/imap-parser-test.js
CHANGED
|
@@ -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');
|