imapflow 1.0.96 → 1.0.99

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.99 - 2022-06-05
4
+
5
+ - Check if folder exists on failed status command
6
+
7
+ - 1.0.98 - 2022-05-30
8
+
9
+ - Fixed an issue with envelope parsing where literal values were returned as Buffers
10
+
11
+ - 1.0.97 - 2022-05-29
12
+
13
+ - Allow single quotes in atoms
14
+
3
15
  - 1.0.96 - 2022-05-18
4
16
 
5
17
  - Allow non-standard characters in ATOM if it's the string after NO/OK/BAD response
@@ -4,7 +4,9 @@ const { decodePath, encodePath, normalizePath } = require('../tools.js');
4
4
  const { specialUse } = require('../special-use');
5
5
 
6
6
  // Lists mailboxes from server
7
- module.exports = async (connection, reference, mailbox) => {
7
+ module.exports = async (connection, reference, mailbox, options) => {
8
+ options = options || {};
9
+
8
10
  let listCommand = connection.capabilities.has('XLIST') && !connection.capabilities.has('SPECIAL-USE') ? 'XLIST' : 'LIST';
9
11
 
10
12
  let response;
@@ -64,6 +66,10 @@ module.exports = async (connection, reference, mailbox) => {
64
66
  let normalizedReference = normalizePath(connection, reference || '');
65
67
  await runList(normalizedReference, normalizePath(connection, mailbox || '', true));
66
68
 
69
+ if (options.listOnly) {
70
+ return entries;
71
+ }
72
+
67
73
  if (normalizedReference && !entries.find(entry => entry.specialUse === '\\Inbox')) {
68
74
  // INBOX was most probably not included in the listing if namespace was used
69
75
  await runList('', 'INBOX');
@@ -126,6 +126,16 @@ module.exports = async (connection, path, query) => {
126
126
  response.next();
127
127
  return map;
128
128
  } catch (err) {
129
+ if (err.responseStatus === 'NO') {
130
+ let folders = await connection.run('LIST', '', path, { listOnly: true });
131
+ if (folders && !folders.length) {
132
+ let error = new Error(`Mailbox doesn't exist: ${path}`);
133
+ error.code = 'NotFound';
134
+ error.response = err;
135
+ throw error;
136
+ }
137
+ }
138
+
129
139
  connection.log.warn({ err, cid: connection.id });
130
140
  return false;
131
141
  }
@@ -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,7 +328,7 @@ 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
@@ -338,9 +338,9 @@ class TokenParser {
338
338
  !(chr === '*' && this.currentNode.value === '\\') &&
339
339
  (!this.parent || !this.parent.command || !['NO', 'BAD', 'OK'].includes(this.parent.command))
340
340
  ) {
341
- throw new Error('Unexpected char at position ' + (this.pos + i));
341
+ throw new Error(`Unexpected char at position ${this.pos + i} [E13: ${JSON.stringify(chr)}]`);
342
342
  } else if (this.currentNode.value === '\\*') {
343
- 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)}]`);
344
344
  }
345
345
 
346
346
  this.currentNode.value += chr;
@@ -362,7 +362,7 @@ class TokenParser {
362
362
  if (chr === '\\') {
363
363
  i++;
364
364
  if (i >= len) {
365
- 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]`);
366
366
  }
367
367
  chr = this.str.charAt(i);
368
368
  }
@@ -373,7 +373,7 @@ class TokenParser {
373
373
  case 'PARTIAL':
374
374
  if (chr === '>') {
375
375
  if (this.currentNode.value.substr(-1) === '.') {
376
- throw new Error('Unexpected end of partial at position ' + this.pos);
376
+ throw new Error(`Unexpected end of partial at position ${this.pos} [E16]`);
377
377
  }
378
378
  this.currentNode.endPos = this.pos + i;
379
379
  this.currentNode.isClosed = true;
@@ -384,15 +384,15 @@ class TokenParser {
384
384
  }
385
385
 
386
386
  if (chr === '.' && (!this.currentNode.value.length || this.currentNode.value.match(/\./))) {
387
- throw new Error('Unexpected partial separator . at position ' + this.pos);
387
+ throw new Error(`Unexpected partial separator . at position ${this.pos} [E17]`);
388
388
  }
389
389
 
390
390
  if (imapFormalSyntax.DIGIT().indexOf(chr) < 0 && chr !== '.') {
391
- 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)}]`);
392
392
  }
393
393
 
394
394
  if (this.currentNode.value.match(/^0$|\.0$/) && chr !== '.') {
395
- 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)}]`);
396
396
  }
397
397
 
398
398
  this.currentNode.value += chr;
@@ -431,14 +431,14 @@ class TokenParser {
431
431
 
432
432
  if (chr === '}') {
433
433
  if (!('literalLength' in this.currentNode)) {
434
- 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]`);
435
435
  }
436
436
  if (this.str.charAt(i + 1) === '\n') {
437
437
  i++;
438
438
  } else if (this.str.charAt(i + 1) === '\r' && this.str.charAt(i + 2) === '\n') {
439
439
  i += 2;
440
440
  } else {
441
- 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)}]`);
442
442
  }
443
443
 
444
444
  this.currentNode.literalLength = Number(this.currentNode.literalLength);
@@ -479,10 +479,10 @@ class TokenParser {
479
479
  break;
480
480
  }
481
481
  if (imapFormalSyntax.DIGIT().indexOf(chr) < 0) {
482
- 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)}]`);
483
483
  }
484
484
  if (this.currentNode.literalLength === '0') {
485
- throw new Error('Invalid literal at position ' + (this.pos + i));
485
+ throw new Error(`Invalid literal at position ${this.pos + i} [E23]`);
486
486
  }
487
487
  this.currentNode.literalLength = (this.currentNode.literalLength || '') + chr;
488
488
  break;
@@ -491,11 +491,11 @@ class TokenParser {
491
491
  // space finishes the sequence set
492
492
  if (chr === ' ') {
493
493
  if (!this.currentNode.value.substr(-1).match(/\d/) && this.currentNode.value.substr(-1) !== '*') {
494
- throw new Error('Unexpected whitespace at position ' + (this.pos + i));
494
+ throw new Error(`Unexpected whitespace at position ${this.pos + i} [E24]`);
495
495
  }
496
496
 
497
497
  if (this.currentNode.value !== '*' && this.currentNode.value.substr(-1) === '*' && this.currentNode.value.substr(-2, 1) !== ':') {
498
- throw new Error('Unexpected whitespace at position ' + (this.pos + i));
498
+ throw new Error(`Unexpected whitespace at position ${this.pos + i} [E25]`);
499
499
  }
500
500
 
501
501
  this.currentNode.isClosed = true;
@@ -518,25 +518,25 @@ class TokenParser {
518
518
 
519
519
  if (chr === ':') {
520
520
  if (!this.currentNode.value.substr(-1).match(/\d/) && this.currentNode.value.substr(-1) !== '*') {
521
- throw new Error('Unexpected range separator : at position ' + (this.pos + i));
521
+ throw new Error(`Unexpected range separator : at position ${this.pos + i} [E26]`);
522
522
  }
523
523
  } else if (chr === '*') {
524
524
  if ([',', ':'].indexOf(this.currentNode.value.substr(-1)) < 0) {
525
- throw new Error('Unexpected range wildcard at position ' + (this.pos + i));
525
+ throw new Error(`Unexpected range wildcard at position ${this.pos + i} [E27]`);
526
526
  }
527
527
  } else if (chr === ',') {
528
528
  if (!this.currentNode.value.substr(-1).match(/\d/) && this.currentNode.value.substr(-1) !== '*') {
529
- throw new Error('Unexpected sequence separator , at position ' + (this.pos + i));
529
+ throw new Error(`Unexpected sequence separator , at position ${this.pos + i} [E28]`);
530
530
  }
531
531
  if (this.currentNode.value.substr(-1) === '*' && this.currentNode.value.substr(-2, 1) !== ':') {
532
- throw new Error('Unexpected sequence separator , at position ' + (this.pos + i));
532
+ throw new Error(`Unexpected sequence separator , at position ${this.pos + i} [E29]`);
533
533
  }
534
534
  } else if (!chr.match(/\d/)) {
535
- 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)}]`);
536
536
  }
537
537
 
538
538
  if (chr.match(/\d/) && this.currentNode.value.substr(-1) === '*') {
539
- 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)}]`);
540
540
  }
541
541
 
542
542
  this.currentNode.value += chr;
package/lib/imap-flow.js CHANGED
@@ -2501,6 +2501,17 @@ class ImapFlow extends EventEmitter {
2501
2501
  this.currentLockId = lockId;
2502
2502
  return resolve({ path, release });
2503
2503
  } catch (err) {
2504
+ if (err.responseStatus === 'NO') {
2505
+ try {
2506
+ let folders = await this.run('LIST', '', path, { listOnly: true });
2507
+ if (!folders || !folders.length) {
2508
+ err.mailboxMissing = true;
2509
+ }
2510
+ } catch (E) {
2511
+ this.log.trace({ msg: 'Failed to verify failed mailbox', path, err: E });
2512
+ }
2513
+ }
2514
+
2504
2515
  this.log.trace({ msg: 'Failed to acquire mailbox lock', path, lockId });
2505
2516
  reject(err);
2506
2517
  await this.processLocks(true);
@@ -2531,6 +2542,7 @@ class ImapFlow extends EventEmitter {
2531
2542
  */
2532
2543
  async getMailboxLock(path, options) {
2533
2544
  options = options || {};
2545
+
2534
2546
  path = normalizePath(this, path);
2535
2547
 
2536
2548
  let lockId = ++this.lockCounter;
package/lib/tools.js CHANGED
@@ -350,25 +350,38 @@ module.exports = {
350
350
  },
351
351
 
352
352
  parseEnvelope(entry) {
353
+ let getStrValue = obj => {
354
+ if (!obj) {
355
+ return false;
356
+ }
357
+ if (typeof obj.value === 'string') {
358
+ return obj.value;
359
+ }
360
+ if (Buffer.isBuffer(obj.value)) {
361
+ return obj.value.toString();
362
+ }
363
+ return obj.value;
364
+ };
365
+
353
366
  let processAddresses = function (list) {
354
367
  return [].concat(list || []).map(addr => ({
355
- name: libmime.decodeWords((addr[0] && addr[0].value) || ''),
356
- address: ((addr[2] && addr[2].value) || '') + '@' + ((addr[3] && addr[3].value) || '')
368
+ name: libmime.decodeWords(getStrValue(addr[0]) || ''),
369
+ address: (getStrValue(addr[2]) || '') + '@' + (getStrValue(addr[3]) || '')
357
370
  }));
358
371
  },
359
372
  envelope = {};
360
373
 
361
374
  if (entry[0] && entry[0].value) {
362
- let date = new Date(entry[0].value);
375
+ let date = new Date(getStrValue(entry[0]));
363
376
  if (date.toString() === 'Invalid Date') {
364
- envelope.date = entry[0].value;
377
+ envelope.date = getStrValue(entry[0]);
365
378
  } else {
366
379
  envelope.date = date;
367
380
  }
368
381
  }
369
382
 
370
383
  if (entry[1] && entry[1].value) {
371
- envelope.subject = libmime.decodeWords(entry[1] && entry[1].value);
384
+ envelope.subject = libmime.decodeWords(getStrValue(entry[1]));
372
385
  }
373
386
 
374
387
  if (entry[2] && entry[2].length) {
@@ -396,11 +409,11 @@ module.exports = {
396
409
  }
397
410
 
398
411
  if (entry[8] && entry[8].value) {
399
- envelope.inReplyTo = entry[8].value;
412
+ envelope.inReplyTo = (getStrValue(entry[8]) || '').toString().trim();
400
413
  }
401
414
 
402
415
  if (entry[9] && entry[9].value) {
403
- envelope.messageId = entry[9].value;
416
+ envelope.messageId = (getStrValue(entry[9]) || '').toString().trim();
404
417
  }
405
418
 
406
419
  return envelope;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.0.96",
3
+ "version": "1.0.99",
4
4
  "description": "IMAP Client for Node",
5
5
  "main": "./lib/imap-flow.js",
6
6
  "scripts": {
@@ -27,13 +27,13 @@
27
27
  },
28
28
  "homepage": "https://imapflow.com/",
29
29
  "devDependencies": {
30
- "@babel/eslint-parser": "7.17.0",
30
+ "@babel/eslint-parser": "7.18.2",
31
31
  "@babel/eslint-plugin": "7.17.7",
32
32
  "@babel/plugin-syntax-class-properties": "7.12.13",
33
- "@babel/preset-env": "7.17.12",
34
- "@types/node": "17.0.34",
33
+ "@babel/preset-env": "7.18.2",
34
+ "@types/node": "17.0.40",
35
35
  "braintree-jsdoc-template": "3.3.0",
36
- "eslint": "8.15.0",
36
+ "eslint": "8.17.0",
37
37
  "eslint-config-nodemailer": "1.2.0",
38
38
  "eslint-config-prettier": "8.5.0",
39
39
  "grunt": "1.5.3",
@@ -52,7 +52,7 @@
52
52
  "libqp": "1.1.0",
53
53
  "mailsplit": "5.3.2",
54
54
  "nodemailer": "6.7.5",
55
- "pino": "7.11.0",
55
+ "pino": "8.0.0",
56
56
  "socks": "2.6.2"
57
57
  }
58
58
  }
@@ -921,3 +921,15 @@ module.exports['IMAP Parser, unicode select'] = test =>
921
921
  { type: 'ATOM', value: '[THROTTLED]' }
922
922
  ])
923
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
+ );