imapflow 1.0.100 → 1.0.103
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/README.md +4 -2
- 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 +16 -5
- package/lib/search-compiler.js +4 -0
- package/lib/types.d.ts +21 -2
- package/package.json +10 -10
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/README.md
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
# ImapFlow
|
|
2
2
|
|
|
3
|
+
ImapFlow is a modern and easy-to-use IMAP client library for Node.js.
|
|
4
|
+
|
|
3
5
|
---
|
|
4
6
|
|
|
5
|
-
> Managing IMAP
|
|
7
|
+
> Managing an IMAP connection is cool, but if you are only looking for an easy way to integrate email accounts, then ImapFlow was built for [EmailEngine Email API](https://emailengine.app/). It's a self-hosted software that converts all IMAP accounts to easy-to-use REST interfaces.
|
|
6
8
|
|
|
7
9
|
---
|
|
8
10
|
|
|
9
11
|
The focus for ImapFlow is to provide easy to use API over IMAP. Using ImapFlow does not expect knowledge about specific IMAP details. A general understanding is good enough.
|
|
10
12
|
|
|
11
|
-
IMAP extensions are handled in the background, so for example, you can always request `labels` value from a {@link FetchQueryObject|fetch()} call, but if the IMAP server does not support `X-GM-EXT1` extension, then `labels` value is not included in the response.
|
|
13
|
+
IMAP extensions are handled in the background, so, for example, you can always request `labels` value from a {@link FetchQueryObject|fetch()} call, but if the IMAP server does not support `X-GM-EXT1` extension, then `labels` value is not included in the response.
|
|
12
14
|
|
|
13
15
|
## Source
|
|
14
16
|
|
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
|
@@ -121,7 +121,7 @@ class ImapFlow extends EventEmitter {
|
|
|
121
121
|
* @param {Boolean} [options.disableAutoIdle=false] if `true` then IDLE is not started automatically. Useful if you only need to perform specific tasks over the connection
|
|
122
122
|
* @param {Object} [options.tls] Additional TLS options (see [Node.js TLS connect](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback) for all available options)
|
|
123
123
|
* @param {Boolean} [options.tls.rejectUnauthorized=true] if `false` then client accepts self-signed and expired certificates from the server
|
|
124
|
-
* @param {String} [options.tls.minVersion=TLSv1
|
|
124
|
+
* @param {String} [options.tls.minVersion=TLSv1] To improvde security you might need to use something newer, eg *'TLSv1.2'*
|
|
125
125
|
* @param {Number} [options.tls.minDHSize=1024] Minimum size of the DH parameter in bits to accept a TLS connection
|
|
126
126
|
* @param {Object} [options.logger] Custom logger instance with `debug(obj)`, `info(obj)`, `warn(obj)` and `error(obj)` methods. If not provided then ImapFlow logs to console using pino format
|
|
127
127
|
* @param {Boolean} [options.logRaw=false] If true then log data read from and written to socket encoded in base64
|
|
@@ -731,7 +731,8 @@ class ImapFlow extends EventEmitter {
|
|
|
731
731
|
socket: this.socket,
|
|
732
732
|
servername: this.servername,
|
|
733
733
|
port: this.port,
|
|
734
|
-
minVersion: 'TLSv1'
|
|
734
|
+
minVersion: 'TLSv1',
|
|
735
|
+
minDHSize: 1024
|
|
735
736
|
},
|
|
736
737
|
this.options.tls || {}
|
|
737
738
|
);
|
|
@@ -1140,7 +1141,8 @@ class ImapFlow extends EventEmitter {
|
|
|
1140
1141
|
host: this.host,
|
|
1141
1142
|
servername: this.servername,
|
|
1142
1143
|
port: this.port,
|
|
1143
|
-
minVersion: 'TLSv1'
|
|
1144
|
+
minVersion: 'TLSv1',
|
|
1145
|
+
minDHSize: 1024
|
|
1144
1146
|
},
|
|
1145
1147
|
this.options.tls || {}
|
|
1146
1148
|
);
|
|
@@ -1390,19 +1392,28 @@ class ImapFlow extends EventEmitter {
|
|
|
1390
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
|
|
1391
1393
|
* @property {Boolean} listed `true` if mailbox was found from the output of LIST command
|
|
1392
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
|
|
1393
1396
|
*/
|
|
1394
1397
|
|
|
1395
1398
|
/**
|
|
1396
1399
|
* Lists available mailboxes as an Array
|
|
1397
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
|
|
1398
1409
|
* @returns {Promise<ListResponse[]>} An array of ListResponse objects
|
|
1399
1410
|
*
|
|
1400
1411
|
* @example
|
|
1401
1412
|
* let list = await client.list();
|
|
1402
1413
|
* list.forEach(mailbox=>console.log(mailbox.path));
|
|
1403
1414
|
*/
|
|
1404
|
-
async list() {
|
|
1405
|
-
let folders = await this.run('LIST', '', '*');
|
|
1415
|
+
async list(options) {
|
|
1416
|
+
let folders = await this.run('LIST', '', '*', options);
|
|
1406
1417
|
this.folders = new Map(folders.map(folder => [folder.path, folder]));
|
|
1407
1418
|
return folders;
|
|
1408
1419
|
}
|
package/lib/search-compiler.js
CHANGED
|
@@ -120,6 +120,10 @@ module.exports.searchCompiler = (connection, query) => {
|
|
|
120
120
|
case 'GMRAW':
|
|
121
121
|
if (connection.capabilities.has('X-GM-EXT-1')) {
|
|
122
122
|
setOpt(attributes, 'X-GM-RAW', params[term]);
|
|
123
|
+
} else {
|
|
124
|
+
let error = new Error('Server does not support X-GM-EXT-1 extension required for X-GM-RAW');
|
|
125
|
+
error.code = 'MissingServerExtension';
|
|
126
|
+
throw error;
|
|
123
127
|
}
|
|
124
128
|
break;
|
|
125
129
|
|
package/lib/types.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ declare module "imapflow" {
|
|
|
20
20
|
* @param [options.disableAutoIdle = false] - if `true` then IDLE is not started automatically. Useful if you only need to perform specific tasks over the connection
|
|
21
21
|
* @param [options.tls] - Additional TLS options (see [Node.js TLS connect](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback) for all available options)
|
|
22
22
|
* @param [options.tls.rejectUnauthorized = true] - if `false` then client accepts self-signed and expired certificates from the server
|
|
23
|
-
* @param [options.tls.minVersion = TLSv1
|
|
23
|
+
* @param [options.tls.minVersion = TLSv1] - To improvde security you might need to use something newer, eg *'TLSv1.2'*
|
|
24
24
|
* @param [options.tls.minDHSize = 1024] - Minimum size of the DH parameter in bits to accept a TLS connection
|
|
25
25
|
* @param [options.logger] - Custom logger instance with `debug(obj)`, `info(obj)`, `warn(obj)` and `error(obj)` methods. If not provided then ImapFlow logs to console using pino format
|
|
26
26
|
* @param [options.logRaw = false] - If true then log data read from and written to socket encoded in base64
|
|
@@ -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(
|
|
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.
|
|
3
|
+
"version": "1.0.103",
|
|
4
4
|
"description": "IMAP Client for Node",
|
|
5
5
|
"main": "./lib/imap-flow.js",
|
|
6
6
|
"scripts": {
|
|
@@ -27,20 +27,20 @@
|
|
|
27
27
|
},
|
|
28
28
|
"homepage": "https://imapflow.com/",
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@babel/eslint-parser": "7.18.
|
|
31
|
-
"@babel/eslint-plugin": "7.
|
|
30
|
+
"@babel/eslint-parser": "7.18.9",
|
|
31
|
+
"@babel/eslint-plugin": "7.18.10",
|
|
32
32
|
"@babel/plugin-syntax-class-properties": "7.12.13",
|
|
33
|
-
"@babel/preset-env": "7.18.
|
|
34
|
-
"@types/node": "18.
|
|
33
|
+
"@babel/preset-env": "7.18.10",
|
|
34
|
+
"@types/node": "18.7.6",
|
|
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",
|
|
40
40
|
"grunt-cli": "1.4.3",
|
|
41
41
|
"grunt-contrib-nodeunit": "4.0.0",
|
|
42
42
|
"grunt-eslint": "24.0.0",
|
|
43
|
-
"jsdoc": "3.6.
|
|
43
|
+
"jsdoc": "3.6.11",
|
|
44
44
|
"st": "3.0.0",
|
|
45
45
|
"tsd-jsdoc": "2.5.0"
|
|
46
46
|
},
|
|
@@ -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.
|
|
56
|
-
"socks": "2.
|
|
54
|
+
"nodemailer": "6.7.8",
|
|
55
|
+
"pino": "8.4.1",
|
|
56
|
+
"socks": "2.7.0"
|
|
57
57
|
}
|
|
58
58
|
}
|