imapflow 1.0.76 → 1.0.80

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,22 @@
1
1
  # CHANGELOG
2
2
 
3
+ - 1.0.80 - 2022-02-07
4
+
5
+ - Added support for the missing NOMODSEQ mailbox modifier
6
+
7
+ - 1.0.79 - 2021-12-29
8
+
9
+ - Added property `greeting` that contains that first response from the server
10
+
11
+ - 1.0.78 - 2021-11-28
12
+
13
+ - Proxy support. Use configuration options `proxy: "url"` to use proxies
14
+
15
+ - 1.0.77 - 2021-11-25
16
+
17
+ - Testing out proxy connections
18
+ - Add X-GM-RAW support for search (jhi721 #68)
19
+
3
20
  - 1.0.75 - 2021-10-15
4
21
 
5
22
  - Fixed an exception with failing IDLE (ramiroaisen #60)
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
 
@@ -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,7 +137,7 @@ module.exports = async (connection, range, query, options) => {
137
137
 
138
138
  attributes.push(queryStructure);
139
139
 
140
- if (options.changedSince && connection.enabled.has('CONDSTORE')) {
140
+ if (options.changedSince && connection.enabled.has('CONDSTORE') && !mailbox.noModseq) {
141
141
  attributes.push([
142
142
  {
143
143
  type: 'ATOM',
@@ -66,7 +66,7 @@ module.exports = async connection => {
66
66
 
67
67
  response = await connection.exec('IDLE', false, {
68
68
  onPlusTag: async () => {
69
- connection.log.debug({ msg: `Initiated IDLE, waiting for server input` });
69
+ connection.log.debug({ msg: `Initiated IDLE, waiting for server input`, doneRequested });
70
70
  canEnd = true;
71
71
  if (doneRequested) {
72
72
  preCheck();
@@ -85,6 +85,16 @@ module.exports = async (connection, path, options) => {
85
85
 
86
86
  map[key] = value;
87
87
  }
88
+
89
+ if (section && section.length === 1 && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
90
+ let key = section[0].value.toLowerCase();
91
+ switch (key) {
92
+ case 'nomodseq':
93
+ key = 'noModseq';
94
+ map[key] = true;
95
+ break;
96
+ }
97
+ }
88
98
  },
89
99
  FLAGS: async untagged => {
90
100
  if (!untagged.attributes || (!untagged.attributes.length && Array.isArray(untagged.attributes[0]))) {
@@ -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
@@ -27,6 +27,8 @@ const libbase64 = require('libbase64');
27
27
  const FlowedDecoder = require('mailsplit/lib/flowed-decoder');
28
28
  const { PassThrough } = require('stream');
29
29
 
30
+ const { proxyConnection } = require('./proxy-connection');
31
+
30
32
  const { comparePaths, updateCapabilities, getFolderTree, formatMessageResponse, getDecoder, packMessageRange, normalizePath } = require('./tools');
31
33
 
32
34
  const imapCommands = require('./imap-commands.js');
@@ -56,6 +58,7 @@ const states = {
56
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.
57
59
  * @property {String} [mailboxId] unique mailbox ID if server has `OBJECTID` extension enabled
58
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
59
62
  * @property {BigInt} uidValidity Mailbox `UIDVALIDITY` value
60
63
  * @property {Number} uidNext Next predicted UID
61
64
  * @property {Number} exists Messages in this folder
@@ -124,6 +127,7 @@ class ImapFlow extends EventEmitter {
124
127
  * @param {Boolean} [options.logRaw=false] If true then log data read from and written to socket encoded in base64
125
128
  * @param {Boolean} [options.emitLogs=false] If `true` then in addition of sending data to logger, ImapFlow emits 'log' events with the same data
126
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
127
131
  */
128
132
  constructor(options) {
129
133
  super();
@@ -805,7 +809,13 @@ class ImapFlow extends EventEmitter {
805
809
  return false;
806
810
  }
807
811
 
808
- async initialOK() {
812
+ async initialOK(message) {
813
+ this.greeting = (message.attributes || [])
814
+ .filter(entry => entry.type === 'TEXT')
815
+ .map(entry => entry.value)
816
+ .filter(entry => entry)
817
+ .join('');
818
+
809
819
  clearTimeout(this.greetingTimeout);
810
820
  this.untaggedHandlers.OK = null;
811
821
  this.untaggedHandlers.PREAUTH = null;
@@ -1054,6 +1064,22 @@ class ImapFlow extends EventEmitter {
1054
1064
  this.untaggedHandlers.EXISTS = (...args) => this.untaggedExists(...args);
1055
1065
  this.untaggedHandlers.EXPUNGE = (...args) => this.untaggedExpunge(...args);
1056
1066
 
1067
+ let socket = false;
1068
+ if (this.options.proxy) {
1069
+ try {
1070
+ socket = await proxyConnection(this.log, this.options.proxy, this.host, this.port);
1071
+ if (!socket) {
1072
+ throw new Error('Failed to setup proxy connection');
1073
+ }
1074
+ } catch (err) {
1075
+ let error = new Error('Failed to setup proxy connection');
1076
+ error.code = err.code || 'ProxyError';
1077
+ error._err = err;
1078
+ this.log.error({ error, cid: this.id });
1079
+ throw error;
1080
+ }
1081
+ }
1082
+
1057
1083
  await new Promise((resolve, reject) => {
1058
1084
  this.connectTimeout = setTimeout(() => {
1059
1085
  let err = new Error('Failed to established connection in required time');
@@ -1063,7 +1089,7 @@ class ImapFlow extends EventEmitter {
1063
1089
  reject(err);
1064
1090
  }, CONNECT_TIMEOUT);
1065
1091
 
1066
- this.socket = connector.connect(opts, () => {
1092
+ let onConnect = () => {
1067
1093
  clearTimeout(this.connectTimeout);
1068
1094
  this.socket.setKeepAlive(true, 5 * 1000);
1069
1095
  this.socket.setTimeout(SOCKET_TIMEOUT);
@@ -1105,7 +1131,23 @@ class ImapFlow extends EventEmitter {
1105
1131
  // executed by initial "* OK"
1106
1132
  this.initialResolve = resolve;
1107
1133
  this.initialReject = reject;
1108
- });
1134
+ };
1135
+
1136
+ if (socket) {
1137
+ // socket is already establised via proxy
1138
+ if (this.secureConnection) {
1139
+ // TLS socket requires a handshake
1140
+ opts.socket = socket;
1141
+ this.socket = connector.connect(opts, onConnect);
1142
+ } else {
1143
+ // cleartext socket is already usable
1144
+ this.socket = socket;
1145
+ setImmediate(onConnect);
1146
+ }
1147
+ } else {
1148
+ this.socket = connector.connect(opts, onConnect);
1149
+ }
1150
+
1109
1151
  this.writeSocket = this.socket;
1110
1152
 
1111
1153
  this.socket.once('error', err => {
@@ -0,0 +1,120 @@
1
+ 'use strict';
2
+
3
+ const httpProxyClient = require('nodemailer/lib/smtp-connection/http-proxy-client');
4
+ const { SocksClient } = require('socks');
5
+ const util = require('util');
6
+ const httpProxyClientAsync = util.promisify(httpProxyClient);
7
+ const dns = require('dns').promises;
8
+ const net = require('net');
9
+
10
+ const proxyConnection = async (logger, connectionUrl, host, port) => {
11
+ let proxyUrl = new URL(connectionUrl);
12
+
13
+ let protocol = proxyUrl.protocol.replace(/:$/, '').toLowerCase();
14
+
15
+ if (!net.isIP(host)) {
16
+ let resolveResult = await dns.resolve(host);
17
+ if (resolveResult && resolveResult.length) {
18
+ host = resolveResult[0];
19
+ }
20
+ }
21
+
22
+ switch (protocol) {
23
+ // Connect using a HTTP CONNECT method
24
+ case 'http':
25
+ case 'https': {
26
+ try {
27
+ let socket = await httpProxyClientAsync(proxyUrl.href, port, host);
28
+ if (socket) {
29
+ if (proxyUrl.password) {
30
+ proxyUrl.password = '(hidden)';
31
+ }
32
+ logger.info({
33
+ msg: 'Established a socket via HTTP proxy',
34
+ proxyUrl: proxyUrl.href,
35
+ port,
36
+ host
37
+ });
38
+ }
39
+ return socket;
40
+ } catch (err) {
41
+ if (proxyUrl.password) {
42
+ proxyUrl.password = '(hidden)';
43
+ }
44
+ logger.error({
45
+ msg: 'Failed to establish a socket via HTTP proxy',
46
+ proxyUrl: proxyUrl.href,
47
+ port,
48
+ host,
49
+ err
50
+ });
51
+ throw err;
52
+ }
53
+ }
54
+
55
+ // SOCKS proxy
56
+ case 'socks':
57
+ case 'socks5':
58
+ case 'socks4':
59
+ case 'socks4a': {
60
+ let proxyType = Number(protocol.replace(/\D/g, '')) || 5;
61
+
62
+ let targetHost = proxyUrl.hostname;
63
+ if (!net.isIP(targetHost)) {
64
+ let resolveResult = await dns.resolve(targetHost);
65
+ if (resolveResult && resolveResult.length) {
66
+ targetHost = resolveResult[0];
67
+ }
68
+ }
69
+
70
+ let connectionOpts = {
71
+ proxy: {
72
+ host: targetHost,
73
+ port: Number(proxyUrl.port) || 1080,
74
+ type: proxyType
75
+ },
76
+ destination: {
77
+ host,
78
+ port
79
+ },
80
+ command: 'connect',
81
+ set_tcp_nodelay: true
82
+ };
83
+
84
+ if (proxyUrl.username || proxyUrl.password) {
85
+ connectionOpts.proxy.userId = proxyUrl.username;
86
+ connectionOpts.proxy.password = proxyUrl.password;
87
+ }
88
+
89
+ try {
90
+ const info = await SocksClient.createConnection(connectionOpts);
91
+ if (info && info.socket) {
92
+ if (proxyUrl.password) {
93
+ proxyUrl.password = '(hidden)';
94
+ }
95
+ logger.info({
96
+ msg: 'Established a socket via SOCKS proxy',
97
+ proxyUrl: proxyUrl.href,
98
+ port,
99
+ host
100
+ });
101
+ }
102
+ return info.socket;
103
+ } catch (err) {
104
+ if (proxyUrl.password) {
105
+ proxyUrl.password = '(hidden)';
106
+ }
107
+ logger.error({
108
+ msg: 'Failed to establish a socket via SOCKS proxy',
109
+ proxyUrl: proxyUrl.href,
110
+ port,
111
+ host,
112
+ err
113
+ });
114
+ throw err;
115
+ }
116
+ }
117
+ }
118
+ };
119
+
120
+ module.exports = { proxyConnection };
@@ -117,6 +117,11 @@ module.exports.searchCompiler = (connection, query) => {
117
117
  setOpt(attributes, 'X-GM-THRID', params[term]);
118
118
  }
119
119
  break;
120
+ case 'GMRAW':
121
+ if (connection.capabilities.has('X-GM-EXT-1')) {
122
+ setOpt(attributes, 'X-GM-RAW', params[term]);
123
+ }
124
+ break;
120
125
 
121
126
  case 'BEFORE':
122
127
  case 'ON':
package/lib/types.d.ts CHANGED
@@ -26,6 +26,7 @@ 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
29
30
  */
30
31
  class ImapFlow extends EventEmitter {
31
32
  constructor(options: {
@@ -50,6 +51,7 @@ declare module "imapflow" {
50
51
  logRaw?: boolean;
51
52
  emitLogs?: boolean;
52
53
  verifyOnly?: boolean;
54
+ proxy?: boolean;
53
55
  });
54
56
  /**
55
57
  * Current module version as a static class property
@@ -481,6 +483,7 @@ declare module "imapflow" {
481
483
  * @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
484
  * @property [mailboxId] - unique mailbox ID if server has `OBJECTID` extension enabled
483
485
  * @property [highestModseq] - latest known modseq value if server has CONDSTORE or XYMHIGHESTMODSEQ enabled
486
+ * @property [noModseq] - if true then the server doesn't support the persistent storage of mod-sequences for the mailbox
484
487
  * @property uidValidity - Mailbox `UIDVALIDITY` value
485
488
  * @property uidNext - Next predicted UID
486
489
  * @property exists - Messages in this folder
@@ -495,6 +498,7 @@ declare type MailboxObject = {
495
498
  permanentFlags: Set<string>;
496
499
  mailboxId?: string;
497
500
  highestModseq?: bigint;
501
+ noModseq?: string;
498
502
  uidValidity: bigint;
499
503
  uidNext: number;
500
504
  exists: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.0.76",
3
+ "version": "1.0.80",
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.15.8",
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.15.8",
33
- "@types/node": "16.11.1",
32
+ "@babel/preset-env": "7.16.11",
33
+ "@types/node": "17.0.15",
34
34
  "braintree-jsdoc-template": "3.3.0",
35
- "eslint": "8.0.1",
35
+ "eslint": "8.8.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,6 +49,8 @@
49
49
  "libmime": "5.0.0",
50
50
  "libqp": "1.1.0",
51
51
  "mailsplit": "5.3.1",
52
- "pino": "7.0.2"
52
+ "nodemailer": "6.7.2",
53
+ "pino": "7.6.5",
54
+ "socks": "2.6.2"
53
55
  }
54
56
  }