imapflow 1.0.78 → 1.0.82

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.82 - 2022-02-15
4
+
5
+ - Added extra option `expungeHandler`
6
+
7
+ - 1.0.81 - 2022-02-15
8
+
9
+ - Added support for the QRESYNC extension and untagged VANISHED responses
10
+
11
+ - 1.0.80 - 2022-02-07
12
+
13
+ - Added support for the missing NOMODSEQ mailbox modifier
14
+
15
+ - 1.0.79 - 2021-12-29
16
+
17
+ - Added property `greeting` that contains that first response from the server
18
+
3
19
  - 1.0.78 - 2021-11-28
4
20
 
5
21
  - Proxy support. Use configuration options `proxy: "url"` to use proxies
package/README.md CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  ---
4
4
 
5
- > Managing IMAP connections 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.
5
+ > Managing IMAP connections 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
6
 
7
7
  ---
8
8
 
9
- The focus for ImapFlow is to provide easy to use API over IMAP. Using ImapFlow does not expect knowledge about specific IMAP details, general understanding is good enough.
9
+ 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
10
 
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.
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.
12
12
 
13
13
  ## Source
14
14
 
@@ -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) {
@@ -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
- attributes.push([
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, {
@@ -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,19 @@ 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 },
44
+ { type: 'ATOM', value: options.changedSince }
45
+ ]
46
+ ]);
47
+ }
48
+
49
+ response = await connection.exec(!options.readOnly ? 'SELECT' : 'EXAMINE', [{ type: 'ATOM', value: path }].concat(extraArgs || []), {
37
50
  untagged: {
38
51
  OK: async untagged => {
39
52
  if (!untagged.attributes || !untagged.attributes.length) {
@@ -85,6 +98,16 @@ module.exports = async (connection, path, options) => {
85
98
 
86
99
  map[key] = value;
87
100
  }
101
+
102
+ if (section && section.length === 1 && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
103
+ let key = section[0].value.toLowerCase();
104
+ switch (key) {
105
+ case 'nomodseq':
106
+ key = 'noModseq';
107
+ map[key] = true;
108
+ break;
109
+ }
110
+ }
88
111
  },
89
112
  FLAGS: async untagged => {
90
113
  if (!untagged.attributes || (!untagged.attributes.length && Array.isArray(untagged.attributes[0]))) {
@@ -100,6 +123,21 @@ module.exports = async (connection, path, options) => {
100
123
  }
101
124
 
102
125
  map.exists = num;
126
+ },
127
+ VANISHED: async untagged => {
128
+ await connection.untaggedVanished(
129
+ untagged,
130
+ // mailbox is not yet open, so use a dummy mailbox object
131
+ { path, uidNext: false, uidValidity: false }
132
+ );
133
+ },
134
+ // we should only get an untagged FETCH for a SELECT/EXAMINE if QRESYNC was asked for
135
+ FETCH: async untagged => {
136
+ await connection.untaggedFetch(
137
+ untagged,
138
+ // mailbox is not yet open, so use a dummy mailbox object
139
+ { path, uidNext: false, uidValidity: false }
140
+ );
103
141
  }
104
142
  }
105
143
  });
@@ -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.value === '' || imapFormalSyntax.verify(val.charAt(0) === '\\' ? val.substr(1) : val, imapFormalSyntax['ATOM-CHAR']()) >= 0) {
121
- val = JSON.stringify(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
- resp += val;
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
  }
@@ -808,7 +810,13 @@ class ImapFlow extends EventEmitter {
808
810
  return false;
809
811
  }
810
812
 
811
- async initialOK() {
813
+ async initialOK(message) {
814
+ this.greeting = (message.attributes || [])
815
+ .filter(entry => entry.type === 'TEXT')
816
+ .map(entry => entry.value)
817
+ .filter(entry => entry)
818
+ .join('');
819
+
812
820
  clearTimeout(this.greetingTimeout);
813
821
  this.untaggedHandlers.OK = null;
814
822
  this.untaggedHandlers.PREAUTH = null;
@@ -934,23 +942,76 @@ class ImapFlow extends EventEmitter {
934
942
  let seq = Number(untagged.command);
935
943
  if (seq && seq <= this.mailbox.exists) {
936
944
  this.mailbox.exists--;
937
- this.emit('expunge', {
945
+ let payload = {
938
946
  path: this.mailbox.path,
939
- seq
940
- });
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
+ }
941
960
  }
942
961
  }
943
962
 
944
- async untaggedFetch(untagged) {
945
- if (!this.mailbox) {
963
+ async untaggedVanished(untagged, mailbox) {
964
+ mailbox = mailbox || this.mailbox;
965
+ if (!mailbox) {
966
+ // mailbox closed, ignore
967
+ return;
968
+ }
969
+
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) {
946
1007
  // mailbox closed, ignore
947
1008
  return;
948
1009
  }
949
1010
 
950
- let message = await formatMessageResponse(untagged, this.mailbox);
1011
+ let message = await formatMessageResponse(untagged, mailbox);
951
1012
  if (message.flags) {
952
1013
  let updateEvent = {
953
- path: this.mailbox.path,
1014
+ path: mailbox.path,
954
1015
  seq: message.seq
955
1016
  };
956
1017
 
@@ -1053,10 +1114,13 @@ class ImapFlow extends EventEmitter {
1053
1114
  this.untaggedHandlers.CAPABILITY = (...args) => this.untaggedCapability(...args);
1054
1115
  this.sectionHandlers.CAPABILITY = (...args) => this.sectionCapability(...args);
1055
1116
 
1056
- this.untaggedHandlers.FETCH = (...args) => this.untaggedFetch(...args);
1057
1117
  this.untaggedHandlers.EXISTS = (...args) => this.untaggedExists(...args);
1058
1118
  this.untaggedHandlers.EXPUNGE = (...args) => this.untaggedExpunge(...args);
1059
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
+
1060
1124
  let socket = false;
1061
1125
  if (this.options.proxy) {
1062
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.78",
3
+ "version": "1.0.82",
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.16.3",
30
- "@babel/eslint-plugin": "7.14.5",
29
+ "@babel/eslint-parser": "7.17.0",
30
+ "@babel/eslint-plugin": "7.16.5",
31
31
  "@babel/plugin-syntax-class-properties": "7.12.13",
32
- "@babel/preset-env": "7.16.4",
33
- "@types/node": "16.11.10",
32
+ "@babel/preset-env": "7.16.11",
33
+ "@types/node": "17.0.18",
34
34
  "braintree-jsdoc-template": "3.3.0",
35
- "eslint": "8.3.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",
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.7",
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.4.1",
54
- "socks": "2.6.1"
53
+ "pino": "7.8.0",
54
+ "socks": "2.6.2"
55
55
  }
56
56
  }