imapflow 1.0.77 → 1.0.81

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.81 - 2022-02-15
4
+
5
+ - Added support for the QRESYNC extension and untagged VANISHED responses
6
+
7
+ - 1.0.80 - 2022-02-07
8
+
9
+ - Added support for the missing NOMODSEQ mailbox modifier
10
+
11
+ - 1.0.79 - 2021-12-29
12
+
13
+ - Added property `greeting` that contains that first response from the server
14
+
15
+ - 1.0.78 - 2021-11-28
16
+
17
+ - Proxy support. Use configuration options `proxy: "url"` to use proxies
18
+
3
19
  - 1.0.77 - 2021-11-25
4
20
 
5
21
  - Testing out proxy connections
package/README.md CHANGED
@@ -1,10 +1,14 @@
1
1
  # ImapFlow
2
2
 
3
- _Node.js IMAP Client library built for for [EmailEngine Email API](https://emailengine.app/)._
3
+ ---
4
4
 
5
- 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.
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
- 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.
7
+ ---
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. A general understanding is good enough.
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.
8
12
 
9
13
  ## Source
10
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
@@ -126,9 +127,11 @@ class ImapFlow extends EventEmitter {
126
127
  * @param {Boolean} [options.logRaw=false] If true then log data read from and written to socket encoded in base64
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
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`
129
132
  */
130
133
  constructor(options) {
131
- super();
134
+ super({ captureRejections: true });
132
135
 
133
136
  this.options = options || {};
134
137
 
@@ -615,7 +618,7 @@ class ImapFlow extends EventEmitter {
615
618
  }
616
619
 
617
620
  // enable extensions if possible
618
- await this.run('ENABLE', ['CONDSTORE', 'UTF8=ACCEPT']);
621
+ await this.run('ENABLE', ['CONDSTORE', 'UTF8=ACCEPT'].concat(this.options.qresync ? 'QRESYNC' : []));
619
622
 
620
623
  this.usable = true;
621
624
  }
@@ -807,7 +810,13 @@ class ImapFlow extends EventEmitter {
807
810
  return false;
808
811
  }
809
812
 
810
- 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
+
811
820
  clearTimeout(this.greetingTimeout);
812
821
  this.untaggedHandlers.OK = null;
813
822
  this.untaggedHandlers.PREAUTH = null;
@@ -935,21 +944,53 @@ class ImapFlow extends EventEmitter {
935
944
  this.mailbox.exists--;
936
945
  this.emit('expunge', {
937
946
  path: this.mailbox.path,
938
- seq
947
+ seq,
948
+ vanished: false
939
949
  });
940
950
  }
941
951
  }
942
952
 
943
- async untaggedFetch(untagged) {
944
- if (!this.mailbox) {
953
+ async untaggedVanished(untagged, mailbox) {
954
+ mailbox = mailbox || this.mailbox;
955
+ if (!mailbox) {
956
+ // mailbox closed, ignore
957
+ return;
958
+ }
959
+
960
+ let tags = [];
961
+ let uids = false;
962
+
963
+ if (untagged.attributes.length > 1 && Array.isArray(untagged.attributes[0])) {
964
+ tags = untagged.attributes[0].map(entry => (typeof entry.value === 'string' ? entry.value.toUpperCase() : false)).filter(value => value);
965
+ untagged.attributes.shift();
966
+ }
967
+
968
+ if (untagged.attributes[0] && typeof untagged.attributes[0].value === 'string') {
969
+ uids = untagged.attributes[0].value;
970
+ }
971
+
972
+ let uidList = expandRange(uids);
973
+ for (let uid of uidList) {
974
+ this.emit('expunge', {
975
+ path: mailbox.path,
976
+ uid,
977
+ vanished: true,
978
+ earlier: tags.includes('EARLIER')
979
+ });
980
+ }
981
+ }
982
+
983
+ async untaggedFetch(untagged, mailbox) {
984
+ mailbox = mailbox || this.mailbox;
985
+ if (!mailbox) {
945
986
  // mailbox closed, ignore
946
987
  return;
947
988
  }
948
989
 
949
- let message = await formatMessageResponse(untagged, this.mailbox);
990
+ let message = await formatMessageResponse(untagged, mailbox);
950
991
  if (message.flags) {
951
992
  let updateEvent = {
952
- path: this.mailbox.path,
993
+ path: mailbox.path,
953
994
  seq: message.seq
954
995
  };
955
996
 
@@ -1052,10 +1093,13 @@ class ImapFlow extends EventEmitter {
1052
1093
  this.untaggedHandlers.CAPABILITY = (...args) => this.untaggedCapability(...args);
1053
1094
  this.sectionHandlers.CAPABILITY = (...args) => this.sectionCapability(...args);
1054
1095
 
1055
- this.untaggedHandlers.FETCH = (...args) => this.untaggedFetch(...args);
1056
1096
  this.untaggedHandlers.EXISTS = (...args) => this.untaggedExists(...args);
1057
1097
  this.untaggedHandlers.EXPUNGE = (...args) => this.untaggedExpunge(...args);
1058
1098
 
1099
+ // these methods take an optional second argument, so make sure that some random IMAP tag is not used as the second argument
1100
+ this.untaggedHandlers.FETCH = untagged => this.untaggedFetch(untagged);
1101
+ this.untaggedHandlers.VANISHED = untagged => this.untaggedVanished(untagged);
1102
+
1059
1103
  let socket = false;
1060
1104
  if (this.options.proxy) {
1061
1105
  try {
package/lib/types.d.ts CHANGED
@@ -26,6 +26,8 @@ declare module "imapflow" {
26
26
  * @param [options.logRaw = false] - If true then log data read from and written to socket encoded in base64
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
+ * @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`
29
31
  */
30
32
  class ImapFlow extends EventEmitter {
31
33
  constructor(options: {
@@ -50,6 +52,8 @@ declare module "imapflow" {
50
52
  logRaw?: boolean;
51
53
  emitLogs?: boolean;
52
54
  verifyOnly?: boolean;
55
+ proxy?: boolean;
56
+ qresync?: boolean;
53
57
  });
54
58
  /**
55
59
  * Current module version as a static class property
@@ -481,6 +485,7 @@ declare module "imapflow" {
481
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.
482
486
  * @property [mailboxId] - unique mailbox ID if server has `OBJECTID` extension enabled
483
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
484
489
  * @property uidValidity - Mailbox `UIDVALIDITY` value
485
490
  * @property uidNext - Next predicted UID
486
491
  * @property exists - Messages in this folder
@@ -495,6 +500,7 @@ declare type MailboxObject = {
495
500
  permanentFlags: Set<string>;
496
501
  mailboxId?: string;
497
502
  highestModseq?: bigint;
503
+ noModseq?: string;
498
504
  uidValidity: bigint;
499
505
  uidNext: number;
500
506
  exists: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.0.77",
3
+ "version": "1.0.81",
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": {
@@ -49,8 +49,8 @@
49
49
  "libmime": "5.0.0",
50
50
  "libqp": "1.1.0",
51
51
  "mailsplit": "5.3.1",
52
- "nodemailer": "6.7.1",
53
- "pino": "7.4.1",
54
- "socks": "2.6.1"
52
+ "nodemailer": "6.7.2",
53
+ "pino": "7.8.0",
54
+ "socks": "2.6.2"
55
55
  }
56
56
  }