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 +5 -0
- package/lib/commands/list.js +117 -1
- package/lib/commands/select.js +3 -1
- package/lib/commands/status.js +2 -1
- package/lib/imap-flow.js +34 -5
- package/lib/types.d.ts +23 -2
- package/package.json +5 -5
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
|
package/lib/commands/list.js
CHANGED
|
@@ -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
|
-
|
|
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))],
|
package/lib/commands/select.js
CHANGED
|
@@ -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:
|
|
54
|
+
[{ type: encodedPath.indexOf('&') >= 0 ? 'STRING' : 'ATOM', value: encodedPath }].concat(extraArgs || []),
|
|
53
55
|
{
|
|
54
56
|
untagged: {
|
|
55
57
|
OK: async untagged => {
|
package/lib/commands/status.js
CHANGED
|
@@ -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:
|
|
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 {
|
|
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
|
-
|
|
649
|
-
|
|
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?:
|
|
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(
|
|
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.
|
|
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.
|
|
34
|
+
"@types/node": "18.7.9",
|
|
35
35
|
"braintree-jsdoc-template": "3.3.0",
|
|
36
|
-
"eslint": "8.
|
|
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.
|
|
55
|
-
"pino": "8.4.
|
|
54
|
+
"nodemailer": "6.7.8",
|
|
55
|
+
"pino": "8.4.2",
|
|
56
56
|
"socks": "2.7.0"
|
|
57
57
|
}
|
|
58
58
|
}
|