imapflow 1.0.101 → 1.0.102

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
+
17
45
  let runList = async (reference, mailbox) => {
18
- response = await connection.exec(listCommand, [encodePath(connection, reference), encodePath(connection, mailbox)], {
46
+ const cmdArgs = [encodePath(connection, reference), encodePath(connection, mailbox)];
47
+
48
+ if (listCommand === 'LIST' && connection.capabilities.has('LIST-STATUS') && statusQueryAttributes.length) {
49
+ returnArgs.push({ type: 'ATOM', value: 'STATUS' }, statusQueryAttributes);
50
+ }
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
  });
@@ -66,6 +165,23 @@ module.exports = async (connection, reference, mailbox, options) => {
66
165
  let normalizedReference = normalizePath(connection, reference || '');
67
166
  await runList(normalizedReference, normalizePath(connection, mailbox || '', true));
68
167
 
168
+ if (options.statusQuery) {
169
+ for (let entry of entries) {
170
+ if (!entry.flags.has('\\Noselect') && !entry.flags.has('\\NonExistent')) {
171
+ if (statusMap.has(entry.path)) {
172
+ entry.status = statusMap.get(entry.path);
173
+ } else if (!statusMap.size) {
174
+ // run STATUS command
175
+ try {
176
+ entry.status = await connection.run('STATUS', entry.path, options.statusQuery);
177
+ } catch (err) {
178
+ entry.status = { error: err };
179
+ }
180
+ }
181
+ }
182
+ }
183
+ }
184
+
69
185
  if (options.listOnly) {
70
186
  return entries;
71
187
  }
@@ -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
@@ -1392,19 +1392,28 @@ class ImapFlow extends EventEmitter {
1392
1392
  * @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
1393
  * @property {Boolean} listed `true` if mailbox was found from the output of LIST command
1394
1394
  * @property {Boolean} subscribed `true` if mailbox was found from the output of LSUB command
1395
+ * @property {StatusObject} [status] If `statusQuery` was used, then this value includes the status response
1395
1396
  */
1396
1397
 
1397
1398
  /**
1398
1399
  * Lists available mailboxes as an Array
1399
1400
  *
1401
+ * @param {Object} [options] defines additional listing options
1402
+ * @param {Object} [options.statusQuery] request status items for every listed entry
1403
+ * @param {Boolean} [options.statusQuery.messages] if `true` request count of messages
1404
+ * @param {Boolean} [options.statusQuery.recent] if `true` request count of messages with \\Recent tag
1405
+ * @param {Boolean} [options.statusQuery.uidNext] if `true` request predicted next UID
1406
+ * @param {Boolean} [options.statusQuery.uidValidity] if `true` request mailbox `UIDVALIDITY` value
1407
+ * @param {Boolean} [options.statusQuery.unseen] if `true` request count of unseen messages
1408
+ * @param {Boolean} [options.statusQuery.highestModseq] if `true` request last known modseq value
1400
1409
  * @returns {Promise<ListResponse[]>} An array of ListResponse objects
1401
1410
  *
1402
1411
  * @example
1403
1412
  * let list = await client.list();
1404
1413
  * list.forEach(mailbox=>console.log(mailbox.path));
1405
1414
  */
1406
- async list() {
1407
- let folders = await this.run('LIST', '', '*');
1415
+ async list(options) {
1416
+ let folders = await this.run('LIST', '', '*', options);
1408
1417
  this.folders = new Map(folders.map(folder => [folder.path, folder]));
1409
1418
  return folders;
1410
1419
  }
package/lib/types.d.ts CHANGED
@@ -151,9 +151,26 @@ declare module "imapflow" {
151
151
  * @example
152
152
  * let list = await client.list();
153
153
  * list.forEach(mailbox=>console.log(mailbox.path));
154
+ * @param [options] - defines additional listing options
155
+ * @param [options.statusQuery] - request status items for every listed entry
156
+ * @param [options.statusQuery.messages] - if `true` request count of messages
157
+ * @param [options.statusQuery.recent] - if `true` request count of messages with \\Recent tag
158
+ * @param [options.statusQuery.uidNext] - if `true` request predicted next UID
159
+ * @param [options.statusQuery.uidValidity] - if `true` request mailbox `UIDVALIDITY` value
160
+ * @param [options.statusQuery.unseen] - if `true` request count of unseen messages
161
+ * @param [options.statusQuery.highestModseq] - if `true` request last known modseq value
154
162
  * @returns An array of ListResponse objects
155
163
  */
156
- list(): Promise<ListResponse[]>;
164
+ list(options?: {
165
+ statusQuery?: {
166
+ messages?: boolean;
167
+ recent?: boolean;
168
+ uidNext?: boolean;
169
+ uidValidity?: boolean;
170
+ unseen?: boolean;
171
+ highestModseq?: boolean;
172
+ };
173
+ }): Promise<ListResponse[]>;
157
174
  /**
158
175
  * Lists available mailboxes as a tree structured object
159
176
  * @example
@@ -581,6 +598,7 @@ declare type QuotaResponse = {
581
598
  * @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
599
  * @property listed - `true` if mailbox was found from the output of LIST command
583
600
  * @property subscribed - `true` if mailbox was found from the output of LSUB command
601
+ * @property [status] - If `statusQuery` was used, then this value includes the status response
584
602
  */
585
603
  declare type ListResponse = {
586
604
  path: string;
@@ -593,6 +611,7 @@ declare type ListResponse = {
593
611
  specialUse: string;
594
612
  listed: boolean;
595
613
  subscribed: boolean;
614
+ status?: StatusObject;
596
615
  };
597
616
 
598
617
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.0.101",
3
+ "version": "1.0.102",
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.6",
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.1",
56
56
  "socks": "2.7.0"
57
57
  }
58
58
  }