imapflow 1.0.94 → 1.0.97

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,17 @@
1
1
  # CHANGELOG
2
2
 
3
+ - 1.0.97 - 2022-05-29
4
+
5
+ - Allow single quotes in atoms
6
+
7
+ - 1.0.96 - 2022-05-18
8
+
9
+ - Allow non-standard characters in ATOM if it's the string after NO/OK/BAD response
10
+
11
+ - 1.0.95 - 2022-05-03
12
+
13
+ - Do not use FETCH BINARY, unless `binary` option is `true`
14
+
3
15
  - 1.0.94 - 2022-05-03
4
16
 
5
17
  - Fixed source download
@@ -18,7 +18,7 @@ module.exports = async (connection, range, query, options) => {
18
18
  list: []
19
19
  };
20
20
 
21
- const commandKey = connection.capabilities.has('BINARY') && !connection.disableBinary ? 'BINARY' : 'BODY';
21
+ const commandKey = connection.capabilities.has('BINARY') && options.binary && !connection.disableBinary ? 'BINARY' : 'BODY';
22
22
 
23
23
  let response;
24
24
  try {
@@ -109,7 +109,7 @@ module.exports = {
109
109
  },
110
110
 
111
111
  'quoted-specials'() {
112
- let value = this.DQUOTE() + "'\\";
112
+ let value = this.DQUOTE() + '\\';
113
113
  this['quoted-specials'] = function () {
114
114
  return value;
115
115
  };
@@ -60,16 +60,16 @@ class ParserInstance {
60
60
  let match, element, errPos;
61
61
 
62
62
  if (this.remainder.match(/^\s/)) {
63
- throw new Error('Unexpected whitespace at position ' + this.pos);
63
+ throw new Error(`Unexpected whitespace at position ${this.pos} [E1]`);
64
64
  }
65
65
 
66
66
  if ((match = this.remainder.match(/^\s*[^\s]+(?=\s|$)/))) {
67
67
  element = match[0];
68
68
  if ((errPos = imapFormalSyntax.verify(element, syntax)) >= 0) {
69
- throw new Error('Unexpected char at position ' + (this.pos + errPos));
69
+ throw new Error(`Unexpected char at position ${this.pos + errPos} [E2: ${JSON.stringify(element.charAt(errPos))}]`);
70
70
  }
71
71
  } else {
72
- throw new Error('Unexpected end of input at position ' + this.pos);
72
+ throw new Error(`Unexpected end of input at position ${this.pos} [E3]`);
73
73
  }
74
74
 
75
75
  this.pos += match[0].length;
@@ -80,11 +80,11 @@ class ParserInstance {
80
80
 
81
81
  async getSpace() {
82
82
  if (!this.remainder.length) {
83
- throw new Error('Unexpected end of input at position ' + this.pos);
83
+ throw new Error(`Unexpected end of input at position ${this.pos} [E4]`);
84
84
  }
85
85
 
86
86
  if (imapFormalSyntax.verify(this.remainder.charAt(0), imapFormalSyntax.SP()) >= 0) {
87
- throw new Error('Unexpected char at position ' + this.pos);
87
+ throw new Error(`Unexpected char at position ${this.pos} [E5: ${JSON.stringify(this.remainder.charAt(0))}]`);
88
88
  }
89
89
 
90
90
  this.pos++;
@@ -93,11 +93,11 @@ class ParserInstance {
93
93
 
94
94
  async getAttributes() {
95
95
  if (!this.remainder.length) {
96
- throw new Error('Unexpected end of input at position ' + this.pos);
96
+ throw new Error(`Unexpected end of input at position ${this.pos} [E6]`);
97
97
  }
98
98
 
99
99
  if (this.remainder.match(/^\s/)) {
100
- throw new Error('Unexpected whitespace at position ' + this.pos);
100
+ throw new Error(`Unexpected whitespace at position ${this.pos} [E7]`);
101
101
  }
102
102
 
103
103
  const tokenParser = new TokenParser(this, this.pos, this.remainder, this.options);
@@ -36,7 +36,7 @@ class TokenParser {
36
36
 
37
37
  // If the node was never closed, throw it
38
38
  if (!node.isClosed) {
39
- throw new Error('Unexpected end of input at position ' + (this.pos + this.str.length - 1));
39
+ throw new Error(`Unexpected end of input at position ${this.pos + this.str.length - 1} [E6]`);
40
40
  }
41
41
 
42
42
  let type = (node.type || '').toString().toUpperCase();
@@ -149,7 +149,7 @@ class TokenParser {
149
149
  // ) closes a list
150
150
  case ')':
151
151
  if (this.currentNode.type !== 'LIST') {
152
- throw new Error('Unexpected list terminator ) at position ' + (this.pos + i));
152
+ throw new Error(`Unexpected list terminator ) at position ${this.pos + i} [E7]`);
153
153
  }
154
154
 
155
155
  this.currentNode.isClosed = true;
@@ -162,7 +162,7 @@ class TokenParser {
162
162
  // ] closes section group
163
163
  case ']':
164
164
  if (this.currentNode.type !== 'SECTION') {
165
- throw new Error('Unexpected section terminator ] at position ' + (this.pos + i));
165
+ throw new Error(`Unexpected section terminator ] at position ${this.pos + i} [E8]`);
166
166
  }
167
167
  this.currentNode.isClosed = true;
168
168
  this.currentNode.endPos = this.pos + i;
@@ -189,7 +189,7 @@ class TokenParser {
189
189
  case '~': {
190
190
  let nextChr = this.str.charAt(i + 1);
191
191
  if (nextChr !== '{') {
192
- throw new Error('Unexpected literal8 marker at position ' + (this.pos + i));
192
+ throw new Error(`Unexpected literal8 marker at position ${this.pos + i} [E9]`);
193
193
  }
194
194
  this.expectedLiteralType = 'literal8';
195
195
  break;
@@ -270,7 +270,7 @@ class TokenParser {
270
270
  // Allow \ as the first char for atom to support system flags
271
271
  // Allow % to support LIST '' %
272
272
  if (imapFormalSyntax['ATOM-CHAR']().indexOf(chr) < 0 && chr !== '\\' && chr !== '%') {
273
- throw new Error('Unexpected char at position ' + (this.pos + i));
273
+ throw new Error(`Unexpected char at position ${this.pos + i} [E10: ${JSON.stringify(chr)}]`);
274
274
  }
275
275
 
276
276
  this.currentNode = this.createNode(this.currentNode, this.pos + i);
@@ -317,7 +317,7 @@ class TokenParser {
317
317
  if (chr === '[') {
318
318
  // allowed only for selected elements
319
319
  if (['BODY', 'BODY.PEEK', 'BINARY', 'BINARY.PEEK'].indexOf(this.currentNode.value.toUpperCase()) < 0) {
320
- throw new Error('Unexpected section start char [ at position ' + this.pos);
320
+ throw new Error(`Unexpected section start char [ at position ${this.pos} [E11]`);
321
321
  }
322
322
  this.currentNode.endPos = this.pos + i;
323
323
  this.currentNode = this.createNode(this.currentNode.parentNode, this.pos + i);
@@ -328,14 +328,19 @@ class TokenParser {
328
328
  }
329
329
 
330
330
  if (chr === '<') {
331
- throw new Error('Unexpected start of partial at position ' + this.pos);
331
+ throw new Error(`Unexpected start of partial at position ${this.pos} [E12]`);
332
332
  }
333
333
 
334
334
  // if the char is not ATOM compatible, throw. Allow \* as an exception
335
- if (imapFormalSyntax['ATOM-CHAR']().indexOf(chr) < 0 && chr !== ']' && !(chr === '*' && this.currentNode.value === '\\')) {
336
- throw new Error('Unexpected char at position ' + (this.pos + i));
335
+ if (
336
+ imapFormalSyntax['ATOM-CHAR']().indexOf(chr) < 0 &&
337
+ chr !== ']' &&
338
+ !(chr === '*' && this.currentNode.value === '\\') &&
339
+ (!this.parent || !this.parent.command || !['NO', 'BAD', 'OK'].includes(this.parent.command))
340
+ ) {
341
+ throw new Error(`Unexpected char at position ${this.pos + i} [E13: ${JSON.stringify(chr)}]`);
337
342
  } else if (this.currentNode.value === '\\*') {
338
- throw new Error('Unexpected char at position ' + (this.pos + i));
343
+ throw new Error(`Unexpected char at position ${this.pos + i} [E14: ${JSON.stringify(chr)}]`);
339
344
  }
340
345
 
341
346
  this.currentNode.value += chr;
@@ -357,7 +362,7 @@ class TokenParser {
357
362
  if (chr === '\\') {
358
363
  i++;
359
364
  if (i >= len) {
360
- throw new Error('Unexpected end of input at position ' + (this.pos + i));
365
+ throw new Error(`Unexpected end of input at position ${this.pos + i} [E15]`);
361
366
  }
362
367
  chr = this.str.charAt(i);
363
368
  }
@@ -368,7 +373,7 @@ class TokenParser {
368
373
  case 'PARTIAL':
369
374
  if (chr === '>') {
370
375
  if (this.currentNode.value.substr(-1) === '.') {
371
- throw new Error('Unexpected end of partial at position ' + this.pos);
376
+ throw new Error(`Unexpected end of partial at position ${this.pos} [E16]`);
372
377
  }
373
378
  this.currentNode.endPos = this.pos + i;
374
379
  this.currentNode.isClosed = true;
@@ -379,15 +384,15 @@ class TokenParser {
379
384
  }
380
385
 
381
386
  if (chr === '.' && (!this.currentNode.value.length || this.currentNode.value.match(/\./))) {
382
- throw new Error('Unexpected partial separator . at position ' + this.pos);
387
+ throw new Error(`Unexpected partial separator . at position ${this.pos} [E17]`);
383
388
  }
384
389
 
385
390
  if (imapFormalSyntax.DIGIT().indexOf(chr) < 0 && chr !== '.') {
386
- throw new Error('Unexpected char at position ' + (this.pos + i));
391
+ throw new Error(`Unexpected char at position ${this.pos + i} [E18: ${JSON.stringify(chr)}]`);
387
392
  }
388
393
 
389
394
  if (this.currentNode.value.match(/^0$|\.0$/) && chr !== '.') {
390
- throw new Error('Invalid partial at position ' + (this.pos + i));
395
+ throw new Error(`Invalid partial at position ${this.pos + i} [E19: ${JSON.stringify(chr)}]`);
391
396
  }
392
397
 
393
398
  this.currentNode.value += chr;
@@ -426,14 +431,14 @@ class TokenParser {
426
431
 
427
432
  if (chr === '}') {
428
433
  if (!('literalLength' in this.currentNode)) {
429
- throw new Error('Unexpected literal prefix end char } at position ' + (this.pos + i));
434
+ throw new Error(`Unexpected literal prefix end char } at position ${this.pos + i} [E20]`);
430
435
  }
431
436
  if (this.str.charAt(i + 1) === '\n') {
432
437
  i++;
433
438
  } else if (this.str.charAt(i + 1) === '\r' && this.str.charAt(i + 2) === '\n') {
434
439
  i += 2;
435
440
  } else {
436
- throw new Error('Unexpected char at position ' + (this.pos + i));
441
+ throw new Error(`Unexpected char at position ${this.pos + i} [E21: ${JSON.stringify(chr)}]`);
437
442
  }
438
443
 
439
444
  this.currentNode.literalLength = Number(this.currentNode.literalLength);
@@ -474,10 +479,10 @@ class TokenParser {
474
479
  break;
475
480
  }
476
481
  if (imapFormalSyntax.DIGIT().indexOf(chr) < 0) {
477
- throw new Error('Unexpected char at position ' + (this.pos + i));
482
+ throw new Error(`Unexpected char at position ${this.pos + i} [E22: ${JSON.stringify(chr)}]`);
478
483
  }
479
484
  if (this.currentNode.literalLength === '0') {
480
- throw new Error('Invalid literal at position ' + (this.pos + i));
485
+ throw new Error(`Invalid literal at position ${this.pos + i} [E23]`);
481
486
  }
482
487
  this.currentNode.literalLength = (this.currentNode.literalLength || '') + chr;
483
488
  break;
@@ -486,11 +491,11 @@ class TokenParser {
486
491
  // space finishes the sequence set
487
492
  if (chr === ' ') {
488
493
  if (!this.currentNode.value.substr(-1).match(/\d/) && this.currentNode.value.substr(-1) !== '*') {
489
- throw new Error('Unexpected whitespace at position ' + (this.pos + i));
494
+ throw new Error(`Unexpected whitespace at position ${this.pos + i} [E24]`);
490
495
  }
491
496
 
492
497
  if (this.currentNode.value !== '*' && this.currentNode.value.substr(-1) === '*' && this.currentNode.value.substr(-2, 1) !== ':') {
493
- throw new Error('Unexpected whitespace at position ' + (this.pos + i));
498
+ throw new Error(`Unexpected whitespace at position ${this.pos + i} [E25]`);
494
499
  }
495
500
 
496
501
  this.currentNode.isClosed = true;
@@ -513,25 +518,25 @@ class TokenParser {
513
518
 
514
519
  if (chr === ':') {
515
520
  if (!this.currentNode.value.substr(-1).match(/\d/) && this.currentNode.value.substr(-1) !== '*') {
516
- throw new Error('Unexpected range separator : at position ' + (this.pos + i));
521
+ throw new Error(`Unexpected range separator : at position ${this.pos + i} [E26]`);
517
522
  }
518
523
  } else if (chr === '*') {
519
524
  if ([',', ':'].indexOf(this.currentNode.value.substr(-1)) < 0) {
520
- throw new Error('Unexpected range wildcard at position ' + (this.pos + i));
525
+ throw new Error(`Unexpected range wildcard at position ${this.pos + i} [E27]`);
521
526
  }
522
527
  } else if (chr === ',') {
523
528
  if (!this.currentNode.value.substr(-1).match(/\d/) && this.currentNode.value.substr(-1) !== '*') {
524
- throw new Error('Unexpected sequence separator , at position ' + (this.pos + i));
529
+ throw new Error(`Unexpected sequence separator , at position ${this.pos + i} [E28]`);
525
530
  }
526
531
  if (this.currentNode.value.substr(-1) === '*' && this.currentNode.value.substr(-2, 1) !== ':') {
527
- throw new Error('Unexpected sequence separator , at position ' + (this.pos + i));
532
+ throw new Error(`Unexpected sequence separator , at position ${this.pos + i} [E29]`);
528
533
  }
529
534
  } else if (!chr.match(/\d/)) {
530
- throw new Error('Unexpected char at position ' + (this.pos + i));
535
+ throw new Error(`Unexpected char at position ${this.pos + i} [E30: ${JSON.stringify(chr)}]`);
531
536
  }
532
537
 
533
538
  if (chr.match(/\d/) && this.currentNode.value.substr(-1) === '*') {
534
- throw new Error('Unexpected number at position ' + (this.pos + i));
539
+ throw new Error(`Unexpected number at position ${this.pos + i} [E31: ${JSON.stringify(chr)}]`);
535
540
  }
536
541
 
537
542
  this.currentNode.value += chr;
package/lib/imap-flow.js CHANGED
@@ -623,7 +623,7 @@ class ImapFlow extends EventEmitter {
623
623
 
624
624
  // Make sure we have namespace set. This should also throw if Exchange actually failed authentication
625
625
  let nsResponse = await this.run('NAMESPACE');
626
- if (nsResponse.error && nsResponse.status === 'BAD' && /User is authenticated but not connected/i.test(nsResponse.text)) {
626
+ if (nsResponse && nsResponse.error && nsResponse.status === 'BAD' && /User is authenticated but not connected/i.test(nsResponse.text)) {
627
627
  // Not a NAMESPACE failure but authentication failure, so report as
628
628
  this.authenticated = false;
629
629
  let err = new Error('Authentication failed');
@@ -2014,6 +2014,7 @@ class ImapFlow extends EventEmitter {
2014
2014
  * @param {Object} [options]
2015
2015
  * @param {Boolean} [options.uid] If `true` then uses UID numbers instead of sequence numbers for `range`
2016
2016
  * @param {BigInt} [options.changedSince] If set then only messages with a higher modseq value are returned. Ignored if server does not support `CONDSTORE` extension.
2017
+ * @param {Boolean} [options.binary=false] If `true` then requests a binary response if the server supports this
2017
2018
  * @yields {Promise<FetchMessageObject>} Message data object
2018
2019
  *
2019
2020
  * @example
@@ -2065,6 +2066,7 @@ class ImapFlow extends EventEmitter {
2065
2066
 
2066
2067
  this.run('FETCH', range, query, {
2067
2068
  uid: !!options.uid,
2069
+ binary: options.binary,
2068
2070
  changedSince: options.changedSince,
2069
2071
  onUntaggedFetch: (untagged, next) => {
2070
2072
  rowQueue.push({
@@ -2108,6 +2110,7 @@ class ImapFlow extends EventEmitter {
2108
2110
  * @param {FetchQueryObject} query Fetch query
2109
2111
  * @param {Object} [options]
2110
2112
  * @param {Boolean} [options.uid] If `true` then uses UID number instead of sequence number for `seq`
2113
+ * @param {Boolean} [options.binary=false] If `true` then requests a binary response if the server supports this
2111
2114
  * @returns {Promise<FetchMessageObject>} Message data object
2112
2115
  *
2113
2116
  * @example
package/lib/types.d.ts CHANGED
@@ -414,10 +414,12 @@ declare module "imapflow" {
414
414
  * @param query - Fetch query
415
415
  * @param [options.uid] - If `true` then uses UID numbers instead of sequence numbers for `range`
416
416
  * @param [options.changedSince] - If set then only messages with a higher modseq value are returned. Ignored if server does not support `CONDSTORE` extension.
417
+ * @param [options.binary = false] - If `true` then requests a binary response if the server supports this
417
418
  */
418
419
  fetch(range: SequenceString | Number[] | SearchObject, query: FetchQueryObject, options?: {
419
420
  uid?: boolean;
420
421
  changedSince?: bigint;
422
+ binary?: boolean;
421
423
  }): void;
422
424
  /**
423
425
  * Fetch a single message from currently opened mailbox
@@ -429,10 +431,12 @@ declare module "imapflow" {
429
431
  * @param seq - Single UID or sequence number of the message to fetch for
430
432
  * @param query - Fetch query
431
433
  * @param [options.uid] - If `true` then uses UID number instead of sequence number for `seq`
434
+ * @param [options.binary = false] - If `true` then requests a binary response if the server supports this
432
435
  * @returns Message data object
433
436
  */
434
437
  fetchOne(seq: SequenceString, query: FetchQueryObject, options?: {
435
438
  uid?: boolean;
439
+ binary?: boolean;
436
440
  }): Promise<FetchMessageObject>;
437
441
  /**
438
442
  * Download either full rfc822 formated message or a specific bodystructure part as a Stream.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.0.94",
3
+ "version": "1.0.97",
4
4
  "description": "IMAP Client for Node",
5
5
  "main": "./lib/imap-flow.js",
6
6
  "scripts": {
@@ -30,13 +30,13 @@
30
30
  "@babel/eslint-parser": "7.17.0",
31
31
  "@babel/eslint-plugin": "7.17.7",
32
32
  "@babel/plugin-syntax-class-properties": "7.12.13",
33
- "@babel/preset-env": "7.17.10",
34
- "@types/node": "17.0.31",
33
+ "@babel/preset-env": "7.17.12",
34
+ "@types/node": "17.0.34",
35
35
  "braintree-jsdoc-template": "3.3.0",
36
- "eslint": "8.14.0",
36
+ "eslint": "8.15.0",
37
37
  "eslint-config-nodemailer": "1.2.0",
38
38
  "eslint-config-prettier": "8.5.0",
39
- "grunt": "1.5.2",
39
+ "grunt": "1.5.3",
40
40
  "grunt-cli": "1.4.3",
41
41
  "grunt-contrib-nodeunit": "4.0.0",
42
42
  "grunt-eslint": "24.0.0",
@@ -51,7 +51,7 @@
51
51
  "libmime": "5.1.0",
52
52
  "libqp": "1.1.0",
53
53
  "mailsplit": "5.3.2",
54
- "nodemailer": "6.7.4",
54
+ "nodemailer": "6.7.5",
55
55
  "pino": "7.11.0",
56
56
  "socks": "2.6.2"
57
57
  }
@@ -905,3 +905,31 @@ module.exports['IMAP Parser: escaped quotes'] = test =>
905
905
  );
906
906
 
907
907
  module.exports['IMAP Parser: mimetorture'] = test => asyncWrapper(test, async test => test.deepEqual(await parser(mimetorture.input), mimetorture.output));
908
+
909
+ module.exports['IMAP Parser, unicode select'] = test =>
910
+ asyncWrapper(test, async test =>
911
+ test.deepEqual((await parser('F OK [READ-WRITE] [Gmail]/Visi laiškai selected. (Success) [THROTTLED]')).attributes, [
912
+ {
913
+ type: 'ATOM',
914
+ value: '',
915
+ section: [{ type: 'ATOM', value: 'READ-WRITE' }]
916
+ },
917
+ { type: 'ATOM', value: '[Gmail]/Visi' },
918
+ { type: 'ATOM', value: 'laiškai' },
919
+ { type: 'ATOM', value: 'selected.' },
920
+ [{ type: 'ATOM', value: 'Success' }],
921
+ { type: 'ATOM', value: '[THROTTLED]' }
922
+ ])
923
+ );
924
+
925
+ module.exports['IMAP Parser, single quote in atom'] = test =>
926
+ asyncWrapper(test, async test =>
927
+ test.deepEqual((await parser('* LIST (HasNoChildren UnMarked) "/" \'a')).attributes, [
928
+ [
929
+ { type: 'ATOM', value: 'HasNoChildren' },
930
+ { type: 'ATOM', value: 'UnMarked' }
931
+ ],
932
+ { type: 'STRING', value: '/' },
933
+ { type: 'ATOM', value: "'a" }
934
+ ])
935
+ );