imapflow 1.0.89 → 1.0.92

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/lib/imap-flow.js CHANGED
@@ -129,6 +129,8 @@ class ImapFlow extends EventEmitter {
129
129
  * @param {Boolean} [options.verifyOnly=false] If `true` then logs out automatically after successful authentication
130
130
  * @param {Boolean} [options.proxy] Optional proxy URL. Supports HTTP CONNECT (`http://`, `https://`) and SOCKS (`socks://`, `socks4://`, `socks5://`) proxies
131
131
  * @param {Boolean} [options.qresync=false] If true, then enables QRESYNC support. EXPUNGE notifications will include `uid` property instead of `seq`
132
+ * @param {Number} [options.maxIdleTime] If set, then breaks and restarts IDLE every maxIdleTime ms
133
+ * @param {Boolean} [options.disableBinary=false] If true, then ignores the BINARY extension when making FETCH and APPEND calls
132
134
  */
133
135
  constructor(options) {
134
136
  super({ captureRejections: true });
@@ -182,7 +184,8 @@ class ImapFlow extends EventEmitter {
182
184
  this.streamer = new ImapStream({
183
185
  logger: this.log,
184
186
  cid: this.id,
185
- logRaw: this.logRaw
187
+ logRaw: this.logRaw,
188
+ secureConnection: this.secureConnection
186
189
  });
187
190
 
188
191
  this.reading = false;
@@ -260,6 +263,10 @@ class ImapFlow extends EventEmitter {
260
263
  this.currentLock = false;
261
264
  this.locks = [];
262
265
 
266
+ this.maxIdleTime = this.options.maxIdleTime || false;
267
+
268
+ this.disableBinary = !!this.options.disableBinary;
269
+
263
270
  this.streamer.on('error', err => {
264
271
  if (['Z_BUF_ERROR', 'ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'EHOSTUNREACH'].includes(err.code)) {
265
272
  // just close the connection, usually nothing but noise
@@ -307,7 +314,14 @@ class ImapFlow extends EventEmitter {
307
314
  }
308
315
 
309
316
  if (this.logRaw) {
310
- this.log.trace({ src: 'c', msg: 'write to socket', data: chunk.toString('base64'), compress: !!this._deflate, cid: this.id });
317
+ this.log.trace({
318
+ src: 'c',
319
+ msg: 'write to socket',
320
+ data: chunk.toString('base64'),
321
+ compress: !!this._deflate,
322
+ secure: !!this.secureConnection,
323
+ cid: this.id
324
+ });
311
325
  }
312
326
 
313
327
  this.writeSocket.write(chunk);
@@ -747,13 +761,14 @@ class ImapFlow extends EventEmitter {
747
761
  this.upgrading = true;
748
762
  this.socket = tls.connect(opts, () => {
749
763
  clearTimeout(this.upgradeTimeout);
750
- if (this.closed) {
764
+ if (this.isClosed) {
751
765
  // not sure if this is possible?
752
766
  return this.close();
753
767
  }
754
768
 
755
769
  this.secureConnection = true;
756
770
  this.upgrading = false;
771
+ this.streamer.secureConnection = true;
757
772
  this.socket.pipe(this.streamer);
758
773
  this.tls = typeof this.socket.getCipher === 'function' ? this.socket.getCipher() : false;
759
774
  if (this.tls) {
@@ -835,7 +850,7 @@ class ImapFlow extends EventEmitter {
835
850
  this.untaggedHandlers.OK = null;
836
851
  this.untaggedHandlers.PREAUTH = null;
837
852
 
838
- if (this.closed) {
853
+ if (this.isClosed) {
839
854
  return;
840
855
  }
841
856
 
@@ -868,7 +883,7 @@ class ImapFlow extends EventEmitter {
868
883
  this.untaggedHandlers.OK = null;
869
884
  this.untaggedHandlers.PREAUTH = null;
870
885
 
871
- if (this.closed) {
886
+ if (this.isClosed) {
872
887
  return;
873
888
  }
874
889
 
@@ -1060,6 +1075,15 @@ class ImapFlow extends EventEmitter {
1060
1075
  range = range.toString();
1061
1076
  }
1062
1077
 
1078
+ // special case, some servers allow this, some do not, so replace it with the last known EXISTS value
1079
+ if (range === '*') {
1080
+ if (!this.mailbox.exists) {
1081
+ return false;
1082
+ }
1083
+ range = this.mailbox.exists.toString();
1084
+ options.uid = false; // sequence query
1085
+ }
1086
+
1063
1087
  if (range && typeof range === 'object' && !Array.isArray(range)) {
1064
1088
  if (range.all && Object.keys(range) === 1) {
1065
1089
  range = '1:*';
@@ -1269,7 +1293,9 @@ class ImapFlow extends EventEmitter {
1269
1293
  let reject = this.initialReject;
1270
1294
  this.initialResolve = false;
1271
1295
  this.initialReject = false;
1272
- reject(new Error('Unexpected close'));
1296
+ let err = new Error('Unexpected close');
1297
+ err.code = `ClosedAfterConnect${this.secureConnection ? 'TLS' : 'Text'}`;
1298
+ reject(err);
1273
1299
  }
1274
1300
 
1275
1301
  if (typeof this.preCheck === 'function') {
@@ -1299,10 +1325,10 @@ class ImapFlow extends EventEmitter {
1299
1325
  }
1300
1326
 
1301
1327
  this.state = this.states.LOGOUT;
1302
- if (this.closed) {
1328
+ if (this.isClosed) {
1303
1329
  return;
1304
1330
  }
1305
- this.closed = true;
1331
+ this.isClosed = true;
1306
1332
 
1307
1333
  if (this.writeSocket && !this.writeSocket.destroyed) {
1308
1334
  try {
@@ -1354,9 +1380,12 @@ class ImapFlow extends EventEmitter {
1354
1380
  /**
1355
1381
  * @typedef {Object} ListResponse
1356
1382
  * @global
1357
- * @property {String} path mailbox path
1383
+ * @property {String} path mailbox path (unicode string)
1384
+ * @property {String} pathAsListed mailbox path as listed in the LIST/LSUB response
1358
1385
  * @property {String} name mailbox name (last part of path after delimiter)
1359
1386
  * @property {String} delimiter mailbox path delimiter, usually "." or "/"
1387
+ * @property {Array<string>} parent An array of parent folder names. All names are in unicode
1388
+ * @property {String} parentPath Same as `parent`, but as a complete string path (unicode string)
1360
1389
  * @property {Set<string>} flags a set of flags for this mailbox
1361
1390
  * @property {String} specialUse one of special-use flags (if applicable): "\All", "\Archive", "\Drafts", "\Flagged", "\Junk", "\Sent", "\Trash". Additionally INBOX has non-standard "\Inbox" flag set
1362
1391
  * @property {Boolean} listed `true` if mailbox was found from the output of LIST command
@@ -1560,7 +1589,7 @@ class ImapFlow extends EventEmitter {
1560
1589
  /**
1561
1590
  * Requests the status of the indicated mailbox. Only requested status values will be returned.
1562
1591
  *
1563
- * @param {String} path mailbox path to check for
1592
+ * @param {String} path mailbox path to check for (unicode string)
1564
1593
  * @param {Object} query defines requested status items
1565
1594
  * @param {Boolean} query.messages if `true` request count of messages
1566
1595
  * @param {Boolean} query.recent if `true` request count of messages with \\Recent tag
@@ -1593,7 +1622,7 @@ class ImapFlow extends EventEmitter {
1593
1622
  */
1594
1623
  async idle() {
1595
1624
  if (!this.idling) {
1596
- return await this.run('IDLE');
1625
+ return await this.run('IDLE', this.maxIdleTime);
1597
1626
  }
1598
1627
  }
1599
1628
 
@@ -1783,7 +1812,7 @@ class ImapFlow extends EventEmitter {
1783
1812
  /**
1784
1813
  * Appends a new message to a mailbox
1785
1814
  *
1786
- * @param {String} path Mailbox path to upload the message to
1815
+ * @param {String} path Mailbox path to upload the message to (unicode string)
1787
1816
  * @param {string|Buffer} content RFC822 formatted email message
1788
1817
  * @param {string[]} [flags] an array of flags to be set for the uploaded message
1789
1818
  * @param {Date|string} [idate=now] internal date to be set for the message
@@ -2093,6 +2122,14 @@ class ImapFlow extends EventEmitter {
2093
2122
  return;
2094
2123
  }
2095
2124
 
2125
+ if (seq === '*') {
2126
+ if (!this.mailbox.exists) {
2127
+ return false;
2128
+ }
2129
+ seq = this.mailbox.exists.toString();
2130
+ options = Object.assign({}, options || {}, { uid: false }); // force into a sequence query
2131
+ }
2132
+
2096
2133
  let response = await this.run('FETCH', (seq || '').toString(), query, options);
2097
2134
 
2098
2135
  if (!response || !response.list || !response.list.length) {
package/lib/tools.js CHANGED
@@ -9,6 +9,30 @@ const { JPDecoder } = require('./jp-decoder');
9
9
  const iconv = require('iconv-lite');
10
10
 
11
11
  module.exports = {
12
+ encodePath(connection, path) {
13
+ path = (path || '').toString();
14
+ if (!connection.enabled.has('UTF8=ACCEPT') && /[&\x00-\x08\x0b-\x0c\x0e-\x1f\u0080-\uffff]/.test(path)) {
15
+ try {
16
+ path = iconv.encode(path, 'utf-7-imap').toString();
17
+ } catch (err) {
18
+ // ignore, keep name as is
19
+ }
20
+ }
21
+ return path;
22
+ },
23
+
24
+ decodePath(connection, path) {
25
+ path = (path || '').toString();
26
+ if (!connection.enabled.has('UTF8=ACCEPT') && /[&]/.test(path)) {
27
+ try {
28
+ path = iconv.decode(Buffer.from(path), 'utf-7-imap').toString();
29
+ } catch (err) {
30
+ // ignore, keep name as is
31
+ }
32
+ }
33
+ return path;
34
+ },
35
+
12
36
  normalizePath(connection, path, skipNamespace) {
13
37
  if (Array.isArray(path)) {
14
38
  path = path.join((connection.namespace && connection.namespace.delimiter) || '');
@@ -19,14 +43,6 @@ module.exports = {
19
43
  return 'INBOX';
20
44
  }
21
45
 
22
- if (!connection.enabled.has('UTF8=ACCEPT') && /[\x00-\x08\x0b-\x0c\x0e-\x1f\u0080-\uffff]/.test(path)) {
23
- try {
24
- path = iconv.encode(path, 'utf-7-imap').toString();
25
- } catch (err) {
26
- // ignore, keep name as is
27
- }
28
- }
29
-
30
46
  // ensure namespace prefix if needed
31
47
  if (!skipNamespace && connection.namespace && connection.namespace.prefix && path.indexOf(connection.namespace.prefix) !== 0) {
32
48
  path = connection.namespace.prefix + path;
@@ -290,9 +306,10 @@ module.exports = {
290
306
  break;
291
307
  }
292
308
 
293
- default:
294
- if (key.indexOf('body[') >= 0) {
295
- let partKey = key.replace(/^body\[|]$/g, '');
309
+ default: {
310
+ let match = key.match(/(body|binary)\[/i);
311
+ if (match) {
312
+ let partKey = key.replace(/^(body|binary)\[|]$/gi, '');
296
313
  partKey = partKey.replace(/\.fields.*$/g, '');
297
314
 
298
315
  let value = getBuffer(attribute);
@@ -308,6 +325,7 @@ module.exports = {
308
325
  break;
309
326
  }
310
327
  break;
328
+ }
311
329
  }
312
330
  }
313
331
 
package/lib/types.d.ts CHANGED
@@ -28,6 +28,8 @@ declare module "imapflow" {
28
28
  * @param [options.verifyOnly = false] - If `true` then logs out automatically after successful authentication
29
29
  * @param [options.proxy] - Optional proxy URL. Supports HTTP CONNECT (`http://`, `https://`) and SOCKS (`socks://`, `socks4://`, `socks5://`) proxies
30
30
  * @param [options.qresync = false] - If true, then enables QRESYNC support. EXPUNGE notifications will include `uid` property instead of `seq`
31
+ * @param [options.maxIdleTime] - If set, then breaks and restarts IDLE every maxIdleTime ms
32
+ * @param [options.disableBinary = false] - If true, then ignores the BINARY extension when making FETCH and APPEND calls
31
33
  */
32
34
  class ImapFlow extends EventEmitter {
33
35
  constructor(options: {
@@ -54,6 +56,8 @@ declare module "imapflow" {
54
56
  verifyOnly?: boolean;
55
57
  proxy?: boolean;
56
58
  qresync?: boolean;
59
+ maxIdleTime?: number;
60
+ disableBinary?: boolean;
57
61
  });
58
62
  /**
59
63
  * Current module version as a static class property
@@ -239,7 +243,7 @@ declare module "imapflow" {
239
243
  * let status = await client.status('INBOX', {unseen: true});
240
244
  * console.log(status.unseen);
241
245
  * // 123
242
- * @param path - mailbox path to check for
246
+ * @param path - mailbox path to check for (unicode string)
243
247
  * @param query - defines requested status items
244
248
  * @param query.messages - if `true` request count of messages
245
249
  * @param query.recent - if `true` request count of messages with \\Recent tag
@@ -340,7 +344,7 @@ declare module "imapflow" {
340
344
  * Appends a new message to a mailbox
341
345
  * @example
342
346
  * await client.append('INBOX', rawMessageBuffer, ['\\Seen'], new Date(2000, 1, 1));
343
- * @param path - Mailbox path to upload the message to
347
+ * @param path - Mailbox path to upload the message to (unicode string)
344
348
  * @param content - RFC822 formatted email message
345
349
  * @param [flags] - an array of flags to be set for the uploaded message
346
350
  * @param [idate = now] - internal date to be set for the message
@@ -563,9 +567,12 @@ declare type QuotaResponse = {
563
567
  };
564
568
 
565
569
  /**
566
- * @property path - mailbox path
570
+ * @property path - mailbox path (unicode string)
571
+ * @property pathAsListed - mailbox path as listed in the LIST/LSUB response
567
572
  * @property name - mailbox name (last part of path after delimiter)
568
573
  * @property delimiter - mailbox path delimiter, usually "." or "/"
574
+ * @property parent - An array of parent folder names. All names are in unicode
575
+ * @property parentPath - Same as `parent`, but as a complete string path (unicode string)
569
576
  * @property flags - a set of flags for this mailbox
570
577
  * @property specialUse - one of special-use flags (if applicable): "\All", "\Archive", "\Drafts", "\Flagged", "\Junk", "\Sent", "\Trash". Additionally INBOX has non-standard "\Inbox" flag set
571
578
  * @property listed - `true` if mailbox was found from the output of LIST command
@@ -573,8 +580,11 @@ declare type QuotaResponse = {
573
580
  */
574
581
  declare type ListResponse = {
575
582
  path: string;
583
+ pathAsListed: string;
576
584
  name: string;
577
585
  delimiter: string;
586
+ parent: string[];
587
+ parentPath: string;
578
588
  flags: Set<string>;
579
589
  specialUse: string;
580
590
  listed: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.0.89",
3
+ "version": "1.0.92",
4
4
  "description": "IMAP Client for Node",
5
5
  "main": "./lib/imap-flow.js",
6
6
  "scripts": {
@@ -8,7 +8,8 @@
8
8
  "prepare": "npm run build",
9
9
  "docs": "rm -rf docs && mkdir -p docs && jsdoc lib/imap-flow.js -c jsdoc.json -R README.md --destination docs/ && cp assets/favicon.ico docs",
10
10
  "dst": "node types.js",
11
- "build": "npm run docs && npm run dst"
11
+ "build": "npm run docs && npm run dst",
12
+ "st": "npm run docs && st -d docs -i index.html"
12
13
  },
13
14
  "repository": {
14
15
  "type": "git",
@@ -29,28 +30,29 @@
29
30
  "@babel/eslint-parser": "7.17.0",
30
31
  "@babel/eslint-plugin": "7.17.7",
31
32
  "@babel/plugin-syntax-class-properties": "7.12.13",
32
- "@babel/preset-env": "7.16.11",
33
- "@types/node": "17.0.23",
33
+ "@babel/preset-env": "7.17.10",
34
+ "@types/node": "17.0.31",
34
35
  "braintree-jsdoc-template": "3.3.0",
35
- "eslint": "8.12.0",
36
+ "eslint": "8.14.0",
36
37
  "eslint-config-nodemailer": "1.2.0",
37
38
  "eslint-config-prettier": "8.5.0",
38
- "grunt": "1.4.1",
39
+ "grunt": "1.5.2",
39
40
  "grunt-cli": "1.4.3",
40
41
  "grunt-contrib-nodeunit": "4.0.0",
41
42
  "grunt-eslint": "24.0.0",
42
43
  "jsdoc": "3.6.10",
44
+ "st": "3.0.0",
43
45
  "tsd-jsdoc": "2.5.0"
44
46
  },
45
47
  "dependencies": {
46
48
  "encoding-japanese": "2.0.0",
47
49
  "iconv-lite": "0.6.3",
48
50
  "libbase64": "1.2.1",
49
- "libmime": "5.0.0",
51
+ "libmime": "5.1.0",
50
52
  "libqp": "1.1.0",
51
- "mailsplit": "5.3.1",
52
- "nodemailer": "6.7.3",
53
- "pino": "7.9.2",
53
+ "mailsplit": "5.3.2",
54
+ "nodemailer": "6.7.4",
55
+ "pino": "7.11.0",
54
56
  "socks": "2.6.2"
55
57
  }
56
58
  }
@@ -200,8 +200,7 @@ module.exports['IMAP Compiler: hide long strings'] = test =>
200
200
  attributes: [
201
201
  {
202
202
  type: 'String',
203
- value:
204
- 'Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere!'
203
+ value: 'Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere!'
205
204
  },
206
205
  'Vana kere'
207
206
  ]
@@ -248,6 +247,46 @@ module.exports['IMAP Compiler: LITERAL text'] = test =>
248
247
  )
249
248
  );
250
249
 
250
+ module.exports['IMAP Compiler: LITERAL literal'] = test =>
251
+ asyncWrapper(test, async test =>
252
+ test.equal(
253
+ await compiler({
254
+ tag: '*',
255
+ command: 'CMD',
256
+ attributes: [
257
+ // keep indentation
258
+ {
259
+ type: 'LITERAL',
260
+ value: 'Tere\x00 tere!',
261
+ isLiteral8: false
262
+ },
263
+ 'Vana kere'
264
+ ]
265
+ }),
266
+ '* CMD {11}\r\nTere\x00 tere! "Vana kere"'
267
+ )
268
+ );
269
+
270
+ module.exports['IMAP Compiler: LITERAL literal8'] = test =>
271
+ asyncWrapper(test, async test =>
272
+ test.equal(
273
+ await compiler({
274
+ tag: '*',
275
+ command: 'CMD',
276
+ attributes: [
277
+ // keep indentation
278
+ {
279
+ type: 'LITERAL',
280
+ value: 'Tere\x00 tere!',
281
+ isLiteral8: true
282
+ },
283
+ 'Vana kere'
284
+ ]
285
+ }),
286
+ '* CMD ~{11}\r\nTere\x00 tere! "Vana kere"'
287
+ )
288
+ );
289
+
251
290
  module.exports['IMAP Compiler: LITERAL array 1'] = test =>
252
291
  asyncWrapper(test, async test =>
253
292
  test.deepEqual(
@@ -309,6 +309,36 @@ module.exports['IMAP Parser: Literals: single literal'] = test =>
309
309
  ])
310
310
  );
311
311
 
312
+ module.exports['IMAP Parser: Literals: literal with NULL'] = test =>
313
+ asyncWrapper(test, async test =>
314
+ test.deepEqual((await parser('TAG1 CMD {4}\r\n', { literals: [Buffer.from('ab\x00d')] })).attributes, [
315
+ {
316
+ type: 'LITERAL',
317
+ value: Buffer.from('ab\x00d')
318
+ }
319
+ ])
320
+ );
321
+
322
+ module.exports['IMAP Parser: Literals: literal8'] = test =>
323
+ asyncWrapper(test, async test =>
324
+ test.deepEqual((await parser('TAG1 CMD ~{4}\r\n', { literals: [Buffer.from('ab\x00d')] })).attributes, [
325
+ {
326
+ type: 'LITERAL',
327
+ value: Buffer.from('ab\x00d')
328
+ }
329
+ ])
330
+ );
331
+
332
+ module.exports['IMAP Parser: Literals: unexpected literal8 prefix'] = test =>
333
+ asyncWrapper(test, async test => {
334
+ try {
335
+ await parser('TAG1 CMD ~\r\n');
336
+ test.ok(false, 'Must throw');
337
+ } catch (err) {
338
+ test.ok(err, 'Error must exist');
339
+ }
340
+ });
341
+
312
342
  module.exports['IMAP Parser: Literals: multiple literals'] = test =>
313
343
  asyncWrapper(test, async test =>
314
344
  test.deepEqual((await parser('TAG1 CMD {4}\r\n {4}\r\n', { literals: [Buffer.from('abcd'), Buffer.from('kere')] })).attributes, [