imapflow 1.0.80 → 1.0.84
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 +16 -0
- package/lib/commands/expunge.js +11 -0
- package/lib/commands/fetch.js +11 -2
- package/lib/commands/select.js +43 -1
- package/lib/imap-flow.js +68 -11
- package/lib/types.d.ts +2 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
- 1.0.84 - 2022-02-17
|
|
4
|
+
|
|
5
|
+
- QRESYNC tweaks
|
|
6
|
+
|
|
7
|
+
- 1.0.83 - 2022-02-16
|
|
8
|
+
|
|
9
|
+
- QRESYNC tweaks
|
|
10
|
+
|
|
11
|
+
- 1.0.82 - 2022-02-15
|
|
12
|
+
|
|
13
|
+
- Added extra option `expungeHandler`
|
|
14
|
+
|
|
15
|
+
- 1.0.81 - 2022-02-15
|
|
16
|
+
|
|
17
|
+
- Added support for the QRESYNC extension and untagged VANISHED responses
|
|
18
|
+
|
|
3
19
|
- 1.0.80 - 2022-02-07
|
|
4
20
|
|
|
5
21
|
- Added support for the missing NOMODSEQ mailbox modifier
|
package/lib/commands/expunge.js
CHANGED
|
@@ -20,6 +20,17 @@ module.exports = async (connection, range, options) => {
|
|
|
20
20
|
let response;
|
|
21
21
|
try {
|
|
22
22
|
response = await connection.exec(command, attributes);
|
|
23
|
+
|
|
24
|
+
// A OK [HIGHESTMODSEQ 9122] Expunge completed (0.010 + 0.000 + 0.012 secs).
|
|
25
|
+
let section = response.response.attributes && response.response.attributes[0] && response.response.attributes[0].section;
|
|
26
|
+
let responseCode = section && section.length && section[0] && typeof section[0].value === 'string' ? section[0].value : '';
|
|
27
|
+
if (responseCode.toUpperCase() === 'HIGHESTMODSEQ') {
|
|
28
|
+
let highestModseq = section[1] && typeof section[1].value === 'string' && !isNaN(section[1].value) ? BigInt(section[1].value) : false;
|
|
29
|
+
if (highestModseq && (!connection.mailbox.highestModseq || highestModseq > connection.mailbox.highestModseq)) {
|
|
30
|
+
connection.mailbox.highestModseq = highestModseq;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
response.next();
|
|
24
35
|
return true;
|
|
25
36
|
} catch (err) {
|
package/lib/commands/fetch.js
CHANGED
|
@@ -138,7 +138,7 @@ module.exports = async (connection, range, query, options) => {
|
|
|
138
138
|
attributes.push(queryStructure);
|
|
139
139
|
|
|
140
140
|
if (options.changedSince && connection.enabled.has('CONDSTORE') && !mailbox.noModseq) {
|
|
141
|
-
|
|
141
|
+
let changedSinceArgs = [
|
|
142
142
|
{
|
|
143
143
|
type: 'ATOM',
|
|
144
144
|
value: 'CHANGEDSINCE'
|
|
@@ -147,7 +147,16 @@ module.exports = async (connection, range, query, options) => {
|
|
|
147
147
|
type: 'ATOM',
|
|
148
148
|
value: options.changedSince.toString()
|
|
149
149
|
}
|
|
150
|
-
]
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
if (options.uid && connection.enabled.has('QRESYNC')) {
|
|
153
|
+
changedSinceArgs.push({
|
|
154
|
+
type: 'ATOM',
|
|
155
|
+
value: 'VANISHED'
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
attributes.push(changedSinceArgs);
|
|
151
160
|
}
|
|
152
161
|
|
|
153
162
|
response = await connection.exec(options.uid ? 'UID FETCH' : 'FETCH', attributes, {
|
package/lib/commands/select.js
CHANGED
|
@@ -21,6 +21,7 @@ module.exports = async (connection, path, options) => {
|
|
|
21
21
|
connection.folders.set(folder.path, folder);
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
|
+
|
|
24
25
|
let folderListData = connection.folders.has(path) ? connection.folders.get(path) : false;
|
|
25
26
|
|
|
26
27
|
let response;
|
|
@@ -33,7 +34,20 @@ module.exports = async (connection, path, options) => {
|
|
|
33
34
|
}
|
|
34
35
|
});
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
+
|
|
38
|
+
let extraArgs = [];
|
|
39
|
+
if (connection.enabled.has('QRESYNC') && options.changedSince && options.uidValidity) {
|
|
40
|
+
extraArgs.push([
|
|
41
|
+
{ type: 'ATOM', value: 'QRESYNC' },
|
|
42
|
+
[
|
|
43
|
+
{ type: 'ATOM', value: options.uidValidity.toString() },
|
|
44
|
+
{ type: 'ATOM', value: options.changedSince.toString() }
|
|
45
|
+
]
|
|
46
|
+
]);
|
|
47
|
+
map.qresync = true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
response = await connection.exec(!options.readOnly ? 'SELECT' : 'EXAMINE', [{ type: 'ATOM', value: path }].concat(extraArgs || []), {
|
|
37
51
|
untagged: {
|
|
38
52
|
OK: async untagged => {
|
|
39
53
|
if (!untagged.attributes || !untagged.attributes.length) {
|
|
@@ -110,6 +124,21 @@ module.exports = async (connection, path, options) => {
|
|
|
110
124
|
}
|
|
111
125
|
|
|
112
126
|
map.exists = num;
|
|
127
|
+
},
|
|
128
|
+
VANISHED: async untagged => {
|
|
129
|
+
await connection.untaggedVanished(
|
|
130
|
+
untagged,
|
|
131
|
+
// mailbox is not yet open, so use a dummy mailbox object
|
|
132
|
+
{ path, uidNext: false, uidValidity: false }
|
|
133
|
+
);
|
|
134
|
+
},
|
|
135
|
+
// we should only get an untagged FETCH for a SELECT/EXAMINE if QRESYNC was asked for
|
|
136
|
+
FETCH: async untagged => {
|
|
137
|
+
await connection.untaggedFetch(
|
|
138
|
+
untagged,
|
|
139
|
+
// mailbox is not yet open, so use a dummy mailbox object
|
|
140
|
+
{ path, uidNext: false, uidValidity: false }
|
|
141
|
+
);
|
|
113
142
|
}
|
|
114
143
|
}
|
|
115
144
|
});
|
|
@@ -127,6 +156,19 @@ module.exports = async (connection, path, options) => {
|
|
|
127
156
|
}
|
|
128
157
|
}
|
|
129
158
|
|
|
159
|
+
if (
|
|
160
|
+
map.qresync &&
|
|
161
|
+
// UIDVALIDITY must be the same
|
|
162
|
+
(options.uidValidity !== map.uidValidity ||
|
|
163
|
+
// HIGHESTMODSEQ response must be present
|
|
164
|
+
!map.highestModseq ||
|
|
165
|
+
// NOMODSEQ is not allowed
|
|
166
|
+
map.noModseq)
|
|
167
|
+
) {
|
|
168
|
+
// QRESYNC does not apply here, so unset it
|
|
169
|
+
map.qresync = false;
|
|
170
|
+
}
|
|
171
|
+
|
|
130
172
|
let currentMailbox = connection.mailbox;
|
|
131
173
|
connection.mailbox = false;
|
|
132
174
|
|
package/lib/imap-flow.js
CHANGED
|
@@ -29,7 +29,7 @@ const { PassThrough } = require('stream');
|
|
|
29
29
|
|
|
30
30
|
const { proxyConnection } = require('./proxy-connection');
|
|
31
31
|
|
|
32
|
-
const { comparePaths, updateCapabilities, getFolderTree, formatMessageResponse, getDecoder, packMessageRange, normalizePath } = require('./tools');
|
|
32
|
+
const { comparePaths, updateCapabilities, getFolderTree, formatMessageResponse, getDecoder, packMessageRange, normalizePath, expandRange } = require('./tools');
|
|
33
33
|
|
|
34
34
|
const imapCommands = require('./imap-commands.js');
|
|
35
35
|
|
|
@@ -128,9 +128,10 @@ class ImapFlow extends EventEmitter {
|
|
|
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
130
|
* @param {Boolean} [options.proxy] Optional proxy URL. Supports HTTP CONNECT (`http://`, `https://`) and SOCKS (`socks://`, `socks4://`, `socks5://`) proxies
|
|
131
|
+
* @param {Boolean} [options.qresync=false] If true, then enables QRESYNC support. EXPUNGE notifications will include `uid` property instead of `seq`
|
|
131
132
|
*/
|
|
132
133
|
constructor(options) {
|
|
133
|
-
super();
|
|
134
|
+
super({ captureRejections: true });
|
|
134
135
|
|
|
135
136
|
this.options = options || {};
|
|
136
137
|
|
|
@@ -617,7 +618,7 @@ class ImapFlow extends EventEmitter {
|
|
|
617
618
|
}
|
|
618
619
|
|
|
619
620
|
// enable extensions if possible
|
|
620
|
-
await this.run('ENABLE', ['CONDSTORE', 'UTF8=ACCEPT']);
|
|
621
|
+
await this.run('ENABLE', ['CONDSTORE', 'UTF8=ACCEPT'].concat(this.options.qresync ? 'QRESYNC' : []));
|
|
621
622
|
|
|
622
623
|
this.usable = true;
|
|
623
624
|
}
|
|
@@ -941,23 +942,76 @@ class ImapFlow extends EventEmitter {
|
|
|
941
942
|
let seq = Number(untagged.command);
|
|
942
943
|
if (seq && seq <= this.mailbox.exists) {
|
|
943
944
|
this.mailbox.exists--;
|
|
944
|
-
|
|
945
|
+
let payload = {
|
|
945
946
|
path: this.mailbox.path,
|
|
946
|
-
seq
|
|
947
|
-
|
|
947
|
+
seq,
|
|
948
|
+
vanished: false
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
if (typeof this.options.expungeHandler === 'function') {
|
|
952
|
+
try {
|
|
953
|
+
await this.options.expungeHandler(payload);
|
|
954
|
+
} catch (err) {
|
|
955
|
+
this.log.error({ msg: 'Failed to notify expunge event', payload, error: err, cid: this.id });
|
|
956
|
+
}
|
|
957
|
+
} else {
|
|
958
|
+
this.emit('expunge', payload);
|
|
959
|
+
}
|
|
948
960
|
}
|
|
949
961
|
}
|
|
950
962
|
|
|
951
|
-
async
|
|
952
|
-
|
|
963
|
+
async untaggedVanished(untagged, mailbox) {
|
|
964
|
+
mailbox = mailbox || this.mailbox;
|
|
965
|
+
if (!mailbox) {
|
|
953
966
|
// mailbox closed, ignore
|
|
954
967
|
return;
|
|
955
968
|
}
|
|
956
969
|
|
|
957
|
-
let
|
|
970
|
+
let tags = [];
|
|
971
|
+
let uids = false;
|
|
972
|
+
|
|
973
|
+
if (untagged.attributes.length > 1 && Array.isArray(untagged.attributes[0])) {
|
|
974
|
+
tags = untagged.attributes[0].map(entry => (typeof entry.value === 'string' ? entry.value.toUpperCase() : false)).filter(value => value);
|
|
975
|
+
untagged.attributes.shift();
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
if (untagged.attributes[0] && typeof untagged.attributes[0].value === 'string') {
|
|
979
|
+
uids = untagged.attributes[0].value;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
let uidList = expandRange(uids);
|
|
983
|
+
|
|
984
|
+
for (let uid of uidList) {
|
|
985
|
+
let payload = {
|
|
986
|
+
path: mailbox.path,
|
|
987
|
+
uid,
|
|
988
|
+
vanished: true,
|
|
989
|
+
earlier: tags.includes('EARLIER')
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
if (typeof this.options.expungeHandler === 'function') {
|
|
993
|
+
try {
|
|
994
|
+
await this.options.expungeHandler(payload);
|
|
995
|
+
} catch (err) {
|
|
996
|
+
this.log.error({ msg: 'Failed to notify expunge event', payload, error: err, cid: this.id });
|
|
997
|
+
}
|
|
998
|
+
} else {
|
|
999
|
+
this.emit('expunge', payload);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
async untaggedFetch(untagged, mailbox) {
|
|
1005
|
+
mailbox = mailbox || this.mailbox;
|
|
1006
|
+
if (!mailbox) {
|
|
1007
|
+
// mailbox closed, ignore
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
let message = await formatMessageResponse(untagged, mailbox);
|
|
958
1012
|
if (message.flags) {
|
|
959
1013
|
let updateEvent = {
|
|
960
|
-
path:
|
|
1014
|
+
path: mailbox.path,
|
|
961
1015
|
seq: message.seq
|
|
962
1016
|
};
|
|
963
1017
|
|
|
@@ -1060,10 +1114,13 @@ class ImapFlow extends EventEmitter {
|
|
|
1060
1114
|
this.untaggedHandlers.CAPABILITY = (...args) => this.untaggedCapability(...args);
|
|
1061
1115
|
this.sectionHandlers.CAPABILITY = (...args) => this.sectionCapability(...args);
|
|
1062
1116
|
|
|
1063
|
-
this.untaggedHandlers.FETCH = (...args) => this.untaggedFetch(...args);
|
|
1064
1117
|
this.untaggedHandlers.EXISTS = (...args) => this.untaggedExists(...args);
|
|
1065
1118
|
this.untaggedHandlers.EXPUNGE = (...args) => this.untaggedExpunge(...args);
|
|
1066
1119
|
|
|
1120
|
+
// these methods take an optional second argument, so make sure that some random IMAP tag is not used as the second argument
|
|
1121
|
+
this.untaggedHandlers.FETCH = untagged => this.untaggedFetch(untagged);
|
|
1122
|
+
this.untaggedHandlers.VANISHED = untagged => this.untaggedVanished(untagged);
|
|
1123
|
+
|
|
1067
1124
|
let socket = false;
|
|
1068
1125
|
if (this.options.proxy) {
|
|
1069
1126
|
try {
|
package/lib/types.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ declare module "imapflow" {
|
|
|
27
27
|
* @param [options.emitLogs = false] - If `true` then in addition of sending data to logger, ImapFlow emits 'log' events with the same data
|
|
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
|
+
* @param [options.qresync = false] - If true, then enables QRESYNC support. EXPUNGE notifications will include `uid` property instead of `seq`
|
|
30
31
|
*/
|
|
31
32
|
class ImapFlow extends EventEmitter {
|
|
32
33
|
constructor(options: {
|
|
@@ -52,6 +53,7 @@ declare module "imapflow" {
|
|
|
52
53
|
emitLogs?: boolean;
|
|
53
54
|
verifyOnly?: boolean;
|
|
54
55
|
proxy?: boolean;
|
|
56
|
+
qresync?: boolean;
|
|
55
57
|
});
|
|
56
58
|
/**
|
|
57
59
|
* Current module version as a static class property
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imapflow",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.84",
|
|
4
4
|
"description": "IMAP Client for Node",
|
|
5
5
|
"main": "./lib/imap-flow.js",
|
|
6
6
|
"scripts": {
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"@babel/eslint-plugin": "7.16.5",
|
|
31
31
|
"@babel/plugin-syntax-class-properties": "7.12.13",
|
|
32
32
|
"@babel/preset-env": "7.16.11",
|
|
33
|
-
"@types/node": "17.0.
|
|
33
|
+
"@types/node": "17.0.18",
|
|
34
34
|
"braintree-jsdoc-template": "3.3.0",
|
|
35
|
-
"eslint": "8.
|
|
35
|
+
"eslint": "8.9.0",
|
|
36
36
|
"eslint-config-nodemailer": "1.2.0",
|
|
37
37
|
"eslint-config-prettier": "8.3.0",
|
|
38
38
|
"grunt": "1.4.1",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"libqp": "1.1.0",
|
|
51
51
|
"mailsplit": "5.3.1",
|
|
52
52
|
"nodemailer": "6.7.2",
|
|
53
|
-
"pino": "7.
|
|
53
|
+
"pino": "7.8.0",
|
|
54
54
|
"socks": "2.6.2"
|
|
55
55
|
}
|
|
56
56
|
}
|