imapflow 1.0.79 → 1.0.83
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 +13 -4
- package/lib/commands/select.js +40 -1
- package/lib/commands/store.js +1 -1
- package/lib/handler/imap-compiler.js +6 -4
- package/lib/imap-flow.js +69 -11
- package/lib/types.d.ts +4 -0
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
- 1.0.83 - 2022-02-16
|
|
4
|
+
|
|
5
|
+
- QRESYNC tweaks
|
|
6
|
+
|
|
7
|
+
- 1.0.82 - 2022-02-15
|
|
8
|
+
|
|
9
|
+
- Added extra option `expungeHandler`
|
|
10
|
+
|
|
11
|
+
- 1.0.81 - 2022-02-15
|
|
12
|
+
|
|
13
|
+
- Added support for the QRESYNC extension and untagged VANISHED responses
|
|
14
|
+
|
|
15
|
+
- 1.0.80 - 2022-02-07
|
|
16
|
+
|
|
17
|
+
- Added support for the missing NOMODSEQ mailbox modifier
|
|
18
|
+
|
|
3
19
|
- 1.0.79 - 2021-12-29
|
|
4
20
|
|
|
5
21
|
- Added property `greeting` that contains that first response from the server
|
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
|
@@ -86,7 +86,7 @@ module.exports = async (connection, range, query, options) => {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
// always ask for modseq if possible
|
|
89
|
-
if (connection.enabled.has('CONDSTORE')) {
|
|
89
|
+
if (connection.enabled.has('CONDSTORE') && !mailbox.noModseq) {
|
|
90
90
|
queryStructure.push({ type: 'ATOM', value: 'MODSEQ' });
|
|
91
91
|
}
|
|
92
92
|
|
|
@@ -137,8 +137,8 @@ module.exports = async (connection, range, query, options) => {
|
|
|
137
137
|
|
|
138
138
|
attributes.push(queryStructure);
|
|
139
139
|
|
|
140
|
-
if (options.changedSince && connection.enabled.has('CONDSTORE')) {
|
|
141
|
-
|
|
140
|
+
if (options.changedSince && connection.enabled.has('CONDSTORE') && !mailbox.noModseq) {
|
|
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 },
|
|
44
|
+
{ type: 'ATOM', value: options.changedSince }
|
|
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) {
|
|
@@ -85,6 +99,16 @@ module.exports = async (connection, path, options) => {
|
|
|
85
99
|
|
|
86
100
|
map[key] = value;
|
|
87
101
|
}
|
|
102
|
+
|
|
103
|
+
if (section && section.length === 1 && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
|
|
104
|
+
let key = section[0].value.toLowerCase();
|
|
105
|
+
switch (key) {
|
|
106
|
+
case 'nomodseq':
|
|
107
|
+
key = 'noModseq';
|
|
108
|
+
map[key] = true;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
88
112
|
},
|
|
89
113
|
FLAGS: async untagged => {
|
|
90
114
|
if (!untagged.attributes || (!untagged.attributes.length && Array.isArray(untagged.attributes[0]))) {
|
|
@@ -100,6 +124,21 @@ module.exports = async (connection, path, options) => {
|
|
|
100
124
|
}
|
|
101
125
|
|
|
102
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
|
+
);
|
|
103
142
|
}
|
|
104
143
|
}
|
|
105
144
|
});
|
package/lib/commands/store.js
CHANGED
|
@@ -53,7 +53,7 @@ module.exports = async (connection, range, flags, options) => {
|
|
|
53
53
|
|
|
54
54
|
let attributes = [{ type: 'SEQUENCE', value: range }, { type: 'ATOM', value: operation }, flags.map(flag => ({ type: 'ATOM', value: flag }))];
|
|
55
55
|
|
|
56
|
-
if (options.unchangedSince && connection.enabled.has('CONDSTORE')) {
|
|
56
|
+
if (options.unchangedSince && connection.enabled.has('CONDSTORE') && !connection.mailbox.noModseq) {
|
|
57
57
|
attributes.push([
|
|
58
58
|
{
|
|
59
59
|
type: 'ATOM',
|
|
@@ -117,11 +117,13 @@ module.exports = async (response, options) => {
|
|
|
117
117
|
case 'SECTION':
|
|
118
118
|
val = (node.value || '').toString('binary');
|
|
119
119
|
|
|
120
|
-
if (node.
|
|
121
|
-
val
|
|
122
|
-
|
|
120
|
+
if (!node.section || val) {
|
|
121
|
+
if (node.value === '' || imapFormalSyntax.verify(val.charAt(0) === '\\' ? val.substr(1) : val, imapFormalSyntax['ATOM-CHAR']()) >= 0) {
|
|
122
|
+
val = JSON.stringify(val);
|
|
123
|
+
}
|
|
123
124
|
|
|
124
|
-
|
|
125
|
+
resp += val;
|
|
126
|
+
}
|
|
125
127
|
|
|
126
128
|
if (node.section) {
|
|
127
129
|
resp += '[';
|
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
|
|
|
@@ -58,6 +58,7 @@ const states = {
|
|
|
58
58
|
* @property {Set<string>} permanentFlags A Set of flags available to use in this mailbox. If it is not set or includes special flag "\\\*" then any flag can be used.
|
|
59
59
|
* @property {String} [mailboxId] unique mailbox ID if server has `OBJECTID` extension enabled
|
|
60
60
|
* @property {BigInt} [highestModseq] latest known modseq value if server has CONDSTORE or XYMHIGHESTMODSEQ enabled
|
|
61
|
+
* @property {String} [noModseq] if true then the server doesn't support the persistent storage of mod-sequences for the mailbox
|
|
61
62
|
* @property {BigInt} uidValidity Mailbox `UIDVALIDITY` value
|
|
62
63
|
* @property {Number} uidNext Next predicted UID
|
|
63
64
|
* @property {Number} exists Messages in this folder
|
|
@@ -127,9 +128,10 @@ class ImapFlow extends EventEmitter {
|
|
|
127
128
|
* @param {Boolean} [options.emitLogs=false] If `true` then in addition of sending data to logger, ImapFlow emits 'log' events with the same data
|
|
128
129
|
* @param {Boolean} [options.verifyOnly=false] If `true` then logs out automatically after successful authentication
|
|
129
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`
|
|
130
132
|
*/
|
|
131
133
|
constructor(options) {
|
|
132
|
-
super();
|
|
134
|
+
super({ captureRejections: true });
|
|
133
135
|
|
|
134
136
|
this.options = options || {};
|
|
135
137
|
|
|
@@ -616,7 +618,7 @@ class ImapFlow extends EventEmitter {
|
|
|
616
618
|
}
|
|
617
619
|
|
|
618
620
|
// enable extensions if possible
|
|
619
|
-
await this.run('ENABLE', ['CONDSTORE', 'UTF8=ACCEPT']);
|
|
621
|
+
await this.run('ENABLE', ['CONDSTORE', 'UTF8=ACCEPT'].concat(this.options.qresync ? 'QRESYNC' : []));
|
|
620
622
|
|
|
621
623
|
this.usable = true;
|
|
622
624
|
}
|
|
@@ -940,23 +942,76 @@ class ImapFlow extends EventEmitter {
|
|
|
940
942
|
let seq = Number(untagged.command);
|
|
941
943
|
if (seq && seq <= this.mailbox.exists) {
|
|
942
944
|
this.mailbox.exists--;
|
|
943
|
-
|
|
945
|
+
let payload = {
|
|
944
946
|
path: this.mailbox.path,
|
|
945
|
-
seq
|
|
946
|
-
|
|
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
|
+
}
|
|
947
960
|
}
|
|
948
961
|
}
|
|
949
962
|
|
|
950
|
-
async
|
|
951
|
-
|
|
963
|
+
async untaggedVanished(untagged, mailbox) {
|
|
964
|
+
mailbox = mailbox || this.mailbox;
|
|
965
|
+
if (!mailbox) {
|
|
952
966
|
// mailbox closed, ignore
|
|
953
967
|
return;
|
|
954
968
|
}
|
|
955
969
|
|
|
956
|
-
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);
|
|
957
1012
|
if (message.flags) {
|
|
958
1013
|
let updateEvent = {
|
|
959
|
-
path:
|
|
1014
|
+
path: mailbox.path,
|
|
960
1015
|
seq: message.seq
|
|
961
1016
|
};
|
|
962
1017
|
|
|
@@ -1059,10 +1114,13 @@ class ImapFlow extends EventEmitter {
|
|
|
1059
1114
|
this.untaggedHandlers.CAPABILITY = (...args) => this.untaggedCapability(...args);
|
|
1060
1115
|
this.sectionHandlers.CAPABILITY = (...args) => this.sectionCapability(...args);
|
|
1061
1116
|
|
|
1062
|
-
this.untaggedHandlers.FETCH = (...args) => this.untaggedFetch(...args);
|
|
1063
1117
|
this.untaggedHandlers.EXISTS = (...args) => this.untaggedExists(...args);
|
|
1064
1118
|
this.untaggedHandlers.EXPUNGE = (...args) => this.untaggedExpunge(...args);
|
|
1065
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
|
+
|
|
1066
1124
|
let socket = false;
|
|
1067
1125
|
if (this.options.proxy) {
|
|
1068
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
|
|
@@ -483,6 +485,7 @@ declare module "imapflow" {
|
|
|
483
485
|
* @property permanentFlags - A Set of flags available to use in this mailbox. If it is not set or includes special flag "\\\*" then any flag can be used.
|
|
484
486
|
* @property [mailboxId] - unique mailbox ID if server has `OBJECTID` extension enabled
|
|
485
487
|
* @property [highestModseq] - latest known modseq value if server has CONDSTORE or XYMHIGHESTMODSEQ enabled
|
|
488
|
+
* @property [noModseq] - if true then the server doesn't support the persistent storage of mod-sequences for the mailbox
|
|
486
489
|
* @property uidValidity - Mailbox `UIDVALIDITY` value
|
|
487
490
|
* @property uidNext - Next predicted UID
|
|
488
491
|
* @property exists - Messages in this folder
|
|
@@ -497,6 +500,7 @@ declare type MailboxObject = {
|
|
|
497
500
|
permanentFlags: Set<string>;
|
|
498
501
|
mailboxId?: string;
|
|
499
502
|
highestModseq?: bigint;
|
|
503
|
+
noModseq?: string;
|
|
500
504
|
uidValidity: bigint;
|
|
501
505
|
uidNext: number;
|
|
502
506
|
exists: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imapflow",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.83",
|
|
4
4
|
"description": "IMAP Client for Node",
|
|
5
5
|
"main": "./lib/imap-flow.js",
|
|
6
6
|
"scripts": {
|
|
@@ -26,20 +26,20 @@
|
|
|
26
26
|
},
|
|
27
27
|
"homepage": "https://imapflow.com/",
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@babel/eslint-parser": "7.
|
|
29
|
+
"@babel/eslint-parser": "7.17.0",
|
|
30
30
|
"@babel/eslint-plugin": "7.16.5",
|
|
31
31
|
"@babel/plugin-syntax-class-properties": "7.12.13",
|
|
32
|
-
"@babel/preset-env": "7.16.
|
|
33
|
-
"@types/node": "17.0.
|
|
32
|
+
"@babel/preset-env": "7.16.11",
|
|
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",
|
|
39
39
|
"grunt-cli": "1.4.3",
|
|
40
40
|
"grunt-contrib-nodeunit": "4.0.0",
|
|
41
41
|
"grunt-eslint": "24.0.0",
|
|
42
|
-
"jsdoc": "3.6.
|
|
42
|
+
"jsdoc": "3.6.10",
|
|
43
43
|
"tsd-jsdoc": "2.5.0"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
@@ -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.
|
|
54
|
-
"socks": "2.6.
|
|
53
|
+
"pino": "7.8.0",
|
|
54
|
+
"socks": "2.6.2"
|
|
55
55
|
}
|
|
56
56
|
}
|