imapflow 1.0.101 → 1.0.104

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,10 @@
1
1
  # CHANGELOG
2
2
 
3
+ - 1.0.102 - 2022-08-18
4
+
5
+ - Added new option `{statusQuery: {<statusOptions>}` for `list()` method
6
+ - Added support for LIST-STATUS extension https://datatracker.ietf.org/doc/html/rfc5819
7
+
3
8
  - 1.0.100 - 2022-06-17
4
9
 
5
10
  - Emit new message notification if appending to currently opened folder
@@ -14,8 +14,46 @@ module.exports = async (connection, reference, mailbox, options) => {
14
14
  let flagsSeen = new Set();
15
15
  let entries = [];
16
16
 
17
+ let statusMap = new Map();
18
+ let returnArgs = [];
19
+ let statusQueryAttributes = [];
20
+
21
+ if (options.statusQuery) {
22
+ Object.keys(options.statusQuery || {}).forEach(key => {
23
+ if (!options.statusQuery[key]) {
24
+ return;
25
+ }
26
+
27
+ switch (key.toUpperCase()) {
28
+ case 'MESSAGES':
29
+ case 'RECENT':
30
+ case 'UIDNEXT':
31
+ case 'UIDVALIDITY':
32
+ case 'UNSEEN':
33
+ statusQueryAttributes.push({ type: 'ATOM', value: key.toUpperCase() });
34
+ break;
35
+
36
+ case 'HIGHESTMODSEQ':
37
+ if (connection.capabilities.has('CONDSTORE')) {
38
+ statusQueryAttributes.push({ type: 'ATOM', value: key.toUpperCase() });
39
+ }
40
+ break;
41
+ }
42
+ });
43
+ }
44
+
45
+ if (listCommand === 'LIST' && connection.capabilities.has('LIST-STATUS') && statusQueryAttributes.length) {
46
+ returnArgs.push({ type: 'ATOM', value: 'STATUS' }, statusQueryAttributes);
47
+ }
48
+
17
49
  let runList = async (reference, mailbox) => {
18
- response = await connection.exec(listCommand, [encodePath(connection, reference), encodePath(connection, mailbox)], {
50
+ const cmdArgs = [encodePath(connection, reference), encodePath(connection, mailbox)];
51
+
52
+ if (returnArgs.length) {
53
+ cmdArgs.push({ type: 'ATOM', value: 'RETURN' }, returnArgs);
54
+ }
55
+
56
+ response = await connection.exec(listCommand, cmdArgs, {
19
57
  untagged: {
20
58
  [listCommand]: async untagged => {
21
59
  if (!untagged.attributes || !untagged.attributes.length) {
@@ -57,6 +95,67 @@ module.exports = async (connection, reference, mailbox, options) => {
57
95
  }
58
96
 
59
97
  entries.push(entry);
98
+ },
99
+
100
+ STATUS: async untagged => {
101
+ let statusPath = normalizePath(connection, decodePath(connection, (untagged.attributes[0] && untagged.attributes[0].value) || ''));
102
+ let statusList = untagged.attributes && Array.isArray(untagged.attributes[1]) ? untagged.attributes[1] : false;
103
+ if (!statusList || !statusPath) {
104
+ return;
105
+ }
106
+
107
+ let key;
108
+
109
+ let map = { path: statusPath };
110
+
111
+ statusList.forEach((entry, i) => {
112
+ if (i % 2 === 0) {
113
+ key = entry && typeof entry.value === 'string' ? entry.value : false;
114
+ return;
115
+ }
116
+ if (!key || !entry || typeof entry.value !== 'string') {
117
+ return;
118
+ }
119
+ let value = false;
120
+ switch (key.toUpperCase()) {
121
+ case 'MESSAGES':
122
+ key = 'messages';
123
+ value = !isNaN(entry.value) ? Number(entry.value) : false;
124
+ break;
125
+
126
+ case 'RECENT':
127
+ key = 'recent';
128
+ value = !isNaN(entry.value) ? Number(entry.value) : false;
129
+ break;
130
+
131
+ case 'UIDNEXT':
132
+ key = 'uidNext';
133
+ value = !isNaN(entry.value) ? Number(entry.value) : false;
134
+ break;
135
+
136
+ case 'UIDVALIDITY':
137
+ key = 'uidValidity';
138
+ value = !isNaN(entry.value) ? BigInt(entry.value) : false;
139
+ break;
140
+
141
+ case 'UNSEEN':
142
+ key = 'unseen';
143
+ value = !isNaN(entry.value) ? Number(entry.value) : false;
144
+ break;
145
+
146
+ case 'HIGHESTMODSEQ':
147
+ key = 'highestModseq';
148
+ value = !isNaN(entry.value) ? BigInt(entry.value) : false;
149
+ break;
150
+ }
151
+ if (value === false) {
152
+ return;
153
+ }
154
+
155
+ map[key] = value;
156
+ });
157
+
158
+ statusMap.set(statusPath, map);
60
159
  }
61
160
  }
62
161
  });
@@ -75,6 +174,23 @@ module.exports = async (connection, reference, mailbox, options) => {
75
174
  await runList('', 'INBOX');
76
175
  }
77
176
 
177
+ if (options.statusQuery) {
178
+ for (let entry of entries) {
179
+ if (!entry.flags.has('\\Noselect') && !entry.flags.has('\\NonExistent')) {
180
+ if (statusMap.has(entry.path)) {
181
+ entry.status = statusMap.get(entry.path);
182
+ } else if (!statusMap.size) {
183
+ // run STATUS command
184
+ try {
185
+ entry.status = await connection.run('STATUS', entry.path, options.statusQuery);
186
+ } catch (err) {
187
+ entry.status = { error: err };
188
+ }
189
+ }
190
+ }
191
+ }
192
+ }
193
+
78
194
  response = await connection.exec(
79
195
  'LSUB',
80
196
  [encodePath(connection, normalizePath(connection, reference || '')), encodePath(connection, normalizePath(connection, mailbox || '', true))],
@@ -47,9 +47,11 @@ module.exports = async (connection, path, options) => {
47
47
  map.qresync = true;
48
48
  }
49
49
 
50
+ let encodedPath = encodePath(connection, path);
51
+
50
52
  response = await connection.exec(
51
53
  !options.readOnly ? 'SELECT' : 'EXAMINE',
52
- [{ type: 'ATOM', value: encodePath(connection, path) }].concat(extraArgs || []),
54
+ [{ type: encodedPath.indexOf('&') >= 0 ? 'STRING' : 'ATOM', value: encodedPath }].concat(extraArgs || []),
53
55
  {
54
56
  untagged: {
55
57
  OK: async untagged => {
@@ -10,8 +10,9 @@ module.exports = async (connection, path, query) => {
10
10
  }
11
11
 
12
12
  path = normalizePath(connection, path);
13
+ let encodedPath = encodePath(connection, path);
13
14
 
14
- let attributes = [{ type: 'ATOM', value: encodePath(connection, path) }];
15
+ let attributes = [{ type: encodedPath.indexOf('&') >= 0 ? 'STRING' : 'ATOM', value: encodedPath }];
15
16
 
16
17
  let queryAttributes = [];
17
18
  Object.keys(query || {}).forEach(key => {
package/lib/imap-flow.js CHANGED
@@ -127,10 +127,11 @@ class ImapFlow extends EventEmitter {
127
127
  * @param {Boolean} [options.logRaw=false] If true then log data read from and written to socket encoded in base64
128
128
  * @param {Boolean} [options.emitLogs=false] If `true` then in addition of sending data to logger, ImapFlow emits 'log' events with the same data
129
129
  * @param {Boolean} [options.verifyOnly=false] If `true` then logs out automatically after successful authentication
130
- * @param {Boolean} [options.proxy] Optional proxy URL. Supports HTTP CONNECT (`http://`, `https://`) and SOCKS (`socks://`, `socks4://`, `socks5://`) proxies
130
+ * @param {String} [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
132
  * @param {Number} [options.maxIdleTime] If set, then breaks and restarts IDLE every maxIdleTime ms
133
133
  * @param {Boolean} [options.disableBinary=false] If true, then ignores the BINARY extension when making FETCH and APPEND calls
134
+ * @param {Boolean} [options.disableAutoEnable] Do not enable supported extensions by default
134
135
  */
135
136
  constructor(options) {
136
137
  super({ captureRejections: true });
@@ -211,6 +212,7 @@ class ImapFlow extends EventEmitter {
211
212
  * @type {Map<string, boolean|number>}
212
213
  */
213
214
  this.capabilities = new Map();
215
+ this.rawCapabilities = null;
214
216
 
215
217
  this.expectCapabilityUpdate = false; // force CAPABILITY after LOGIN
216
218
 
@@ -645,8 +647,10 @@ class ImapFlow extends EventEmitter {
645
647
  await this.compress();
646
648
  }
647
649
 
648
- // enable extensions if possible
649
- await this.run('ENABLE', ['CONDSTORE', 'UTF8=ACCEPT'].concat(this.options.qresync ? 'QRESYNC' : []));
650
+ if (!this.options.disableAutoEnable) {
651
+ // enable extensions if possible
652
+ await this.run('ENABLE', ['CONDSTORE', 'UTF8=ACCEPT'].concat(this.options.qresync ? 'QRESYNC' : []));
653
+ }
650
654
 
651
655
  this.usable = true;
652
656
  }
@@ -920,6 +924,7 @@ class ImapFlow extends EventEmitter {
920
924
  }
921
925
 
922
926
  async sectionCapability(section) {
927
+ this.rawCapabilities = section;
923
928
  this.capabilities = updateCapabilities(section);
924
929
  if (this.expectCapabilityUpdate) {
925
930
  this.expectCapabilityUpdate = false;
@@ -927,6 +932,7 @@ class ImapFlow extends EventEmitter {
927
932
  }
928
933
 
929
934
  async untaggedCapability(untagged) {
935
+ this.rawCapabilities = untagged.attributes;
930
936
  this.capabilities = updateCapabilities(untagged.attributes);
931
937
  if (this.expectCapabilityUpdate) {
932
938
  this.expectCapabilityUpdate = false;
@@ -1392,19 +1398,28 @@ class ImapFlow extends EventEmitter {
1392
1398
  * @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
1393
1399
  * @property {Boolean} listed `true` if mailbox was found from the output of LIST command
1394
1400
  * @property {Boolean} subscribed `true` if mailbox was found from the output of LSUB command
1401
+ * @property {StatusObject} [status] If `statusQuery` was used, then this value includes the status response
1395
1402
  */
1396
1403
 
1397
1404
  /**
1398
1405
  * Lists available mailboxes as an Array
1399
1406
  *
1407
+ * @param {Object} [options] defines additional listing options
1408
+ * @param {Object} [options.statusQuery] request status items for every listed entry
1409
+ * @param {Boolean} [options.statusQuery.messages] if `true` request count of messages
1410
+ * @param {Boolean} [options.statusQuery.recent] if `true` request count of messages with \\Recent tag
1411
+ * @param {Boolean} [options.statusQuery.uidNext] if `true` request predicted next UID
1412
+ * @param {Boolean} [options.statusQuery.uidValidity] if `true` request mailbox `UIDVALIDITY` value
1413
+ * @param {Boolean} [options.statusQuery.unseen] if `true` request count of unseen messages
1414
+ * @param {Boolean} [options.statusQuery.highestModseq] if `true` request last known modseq value
1400
1415
  * @returns {Promise<ListResponse[]>} An array of ListResponse objects
1401
1416
  *
1402
1417
  * @example
1403
1418
  * let list = await client.list();
1404
1419
  * list.forEach(mailbox=>console.log(mailbox.path));
1405
1420
  */
1406
- async list() {
1407
- let folders = await this.run('LIST', '', '*');
1421
+ async list(options) {
1422
+ let folders = await this.run('LIST', '', '*', options);
1408
1423
  this.folders = new Map(folders.map(folder => [folder.path, folder]));
1409
1424
  return folders;
1410
1425
  }
@@ -2602,6 +2617,20 @@ class ImapFlow extends EventEmitter {
2602
2617
 
2603
2618
  return synteticLogger;
2604
2619
  }
2620
+
2621
+ unbind() {
2622
+ this.socket.unpipe(this.streamer);
2623
+
2624
+ this.socket.removeListener('error', this._socketError);
2625
+ this.socket.removeListener('close', this._socketClose);
2626
+ this.socket.removeListener('end', this._socketEnd);
2627
+ this.socket.removeListener('tlsClientError', this._socketError);
2628
+ this.socket.removeListener('timeout', this._socketTimeout);
2629
+
2630
+ return {
2631
+ socket: this.socket
2632
+ };
2633
+ }
2605
2634
  }
2606
2635
 
2607
2636
  /**
package/lib/types.d.ts CHANGED
@@ -30,6 +30,7 @@ declare module "imapflow" {
30
30
  * @param [options.qresync = false] - If true, then enables QRESYNC support. EXPUNGE notifications will include `uid` property instead of `seq`
31
31
  * @param [options.maxIdleTime] - If set, then breaks and restarts IDLE every maxIdleTime ms
32
32
  * @param [options.disableBinary = false] - If true, then ignores the BINARY extension when making FETCH and APPEND calls
33
+ * @param [options.disableAutoEnable] - Do not enable supported extensions by default
33
34
  */
34
35
  class ImapFlow extends EventEmitter {
35
36
  constructor(options: {
@@ -54,10 +55,11 @@ declare module "imapflow" {
54
55
  logRaw?: boolean;
55
56
  emitLogs?: boolean;
56
57
  verifyOnly?: boolean;
57
- proxy?: boolean;
58
+ proxy?: string;
58
59
  qresync?: boolean;
59
60
  maxIdleTime?: number;
60
61
  disableBinary?: boolean;
62
+ disableAutoEnable?: boolean;
61
63
  });
62
64
  /**
63
65
  * Current module version as a static class property
@@ -151,9 +153,26 @@ declare module "imapflow" {
151
153
  * @example
152
154
  * let list = await client.list();
153
155
  * list.forEach(mailbox=>console.log(mailbox.path));
156
+ * @param [options] - defines additional listing options
157
+ * @param [options.statusQuery] - request status items for every listed entry
158
+ * @param [options.statusQuery.messages] - if `true` request count of messages
159
+ * @param [options.statusQuery.recent] - if `true` request count of messages with \\Recent tag
160
+ * @param [options.statusQuery.uidNext] - if `true` request predicted next UID
161
+ * @param [options.statusQuery.uidValidity] - if `true` request mailbox `UIDVALIDITY` value
162
+ * @param [options.statusQuery.unseen] - if `true` request count of unseen messages
163
+ * @param [options.statusQuery.highestModseq] - if `true` request last known modseq value
154
164
  * @returns An array of ListResponse objects
155
165
  */
156
- list(): Promise<ListResponse[]>;
166
+ list(options?: {
167
+ statusQuery?: {
168
+ messages?: boolean;
169
+ recent?: boolean;
170
+ uidNext?: boolean;
171
+ uidValidity?: boolean;
172
+ unseen?: boolean;
173
+ highestModseq?: boolean;
174
+ };
175
+ }): Promise<ListResponse[]>;
157
176
  /**
158
177
  * Lists available mailboxes as a tree structured object
159
178
  * @example
@@ -581,6 +600,7 @@ declare type QuotaResponse = {
581
600
  * @property specialUse - one of special-use flags (if applicable): "\All", "\Archive", "\Drafts", "\Flagged", "\Junk", "\Sent", "\Trash". Additionally INBOX has non-standard "\Inbox" flag set
582
601
  * @property listed - `true` if mailbox was found from the output of LIST command
583
602
  * @property subscribed - `true` if mailbox was found from the output of LSUB command
603
+ * @property [status] - If `statusQuery` was used, then this value includes the status response
584
604
  */
585
605
  declare type ListResponse = {
586
606
  path: string;
@@ -593,6 +613,7 @@ declare type ListResponse = {
593
613
  specialUse: string;
594
614
  listed: boolean;
595
615
  subscribed: boolean;
616
+ status?: StatusObject;
596
617
  };
597
618
 
598
619
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.0.101",
3
+ "version": "1.0.104",
4
4
  "description": "IMAP Client for Node",
5
5
  "main": "./lib/imap-flow.js",
6
6
  "scripts": {
@@ -31,9 +31,9 @@
31
31
  "@babel/eslint-plugin": "7.18.10",
32
32
  "@babel/plugin-syntax-class-properties": "7.12.13",
33
33
  "@babel/preset-env": "7.18.10",
34
- "@types/node": "18.6.5",
34
+ "@types/node": "18.7.9",
35
35
  "braintree-jsdoc-template": "3.3.0",
36
- "eslint": "8.21.0",
36
+ "eslint": "8.22.0",
37
37
  "eslint-config-nodemailer": "1.2.0",
38
38
  "eslint-config-prettier": "8.5.0",
39
39
  "grunt": "1.5.3",
@@ -51,8 +51,8 @@
51
51
  "libmime": "5.1.0",
52
52
  "libqp": "1.1.0",
53
53
  "mailsplit": "5.3.2",
54
- "nodemailer": "6.7.7",
55
- "pino": "8.4.0",
54
+ "nodemailer": "6.7.8",
55
+ "pino": "8.4.2",
56
56
  "socks": "2.7.0"
57
57
  }
58
58
  }