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 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
@@ -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) {
@@ -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
- attributes.push([
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, {
@@ -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
- response = await connection.exec(!options.readOnly ? 'SELECT' : 'EXAMINE', [{ type: 'ATOM', value: path }], {
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
- this.emit('expunge', {
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 untaggedFetch(untagged) {
952
- if (!this.mailbox) {
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 message = await formatMessageResponse(untagged, this.mailbox);
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: this.mailbox.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.80",
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.15",
33
+ "@types/node": "17.0.18",
34
34
  "braintree-jsdoc-template": "3.3.0",
35
- "eslint": "8.8.0",
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.6.5",
53
+ "pino": "7.8.0",
54
54
  "socks": "2.6.2"
55
55
  }
56
56
  }