imapflow 1.0.74 → 1.0.78

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/.eslintrc CHANGED
@@ -10,7 +10,8 @@
10
10
  "extends": ["nodemailer", "prettier"],
11
11
  "parser": "@babel/eslint-parser",
12
12
  "parserOptions": {
13
- "ecmaVersion": 2018
13
+ "ecmaVersion": 2018,
14
+ "sourceType": "script"
14
15
  },
15
16
  "plugins": ["@babel"]
16
17
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # CHANGELOG
2
2
 
3
+ - 1.0.78 - 2021-11-28
4
+
5
+ - Proxy support. Use configuration options `proxy: "url"` to use proxies
6
+
7
+ - 1.0.77 - 2021-11-25
8
+
9
+ - Testing out proxy connections
10
+ - Add X-GM-RAW support for search (jhi721 #68)
11
+
12
+ - 1.0.75 - 2021-10-15
13
+
14
+ - Fixed an exception with failing IDLE (ramiroaisen #60)
15
+ - Do not use 1 and 1.MIME for single node messages, fallback to TEXT and HEADERS
16
+
3
17
  - 1.0.71 - 2021-09-28
4
18
 
5
19
  - IDLE precheck changes
package/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # ImapFlow
2
2
 
3
- _Node.js IMAP Client library built for for [EmailEngine Email API](https://emailengine.app/)._
3
+ ---
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.
6
+
7
+ ---
4
8
 
5
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.
6
10
 
@@ -12,6 +12,7 @@ module.exports = async connection => {
12
12
  if (connection.capabilities.has('IDLE')) {
13
13
  let response;
14
14
 
15
+ let preCheckWaitQueue = [];
15
16
  try {
16
17
  connection.idling = true;
17
18
 
@@ -20,12 +21,16 @@ module.exports = async connection => {
20
21
  let doneSent = false;
21
22
  let canEnd = false;
22
23
 
23
- let preCheckWaitQueue = [];
24
-
25
24
  let preCheck = () => {
26
25
  doneRequested = true;
27
26
  if (canEnd && !doneSent) {
28
- connection.log.debug({ src: 'c', msg: `DONE`, comment: `breaking IDLE` });
27
+ connection.log.debug({
28
+ src: 'c',
29
+ msg: `DONE`,
30
+ comment: `breaking IDLE`,
31
+ lockId: connection.currentLockId,
32
+ path: connection.mailbox && connection.mailbox.path
33
+ });
29
34
  connection.write('DONE');
30
35
  doneSent = true;
31
36
 
@@ -44,6 +49,16 @@ module.exports = async connection => {
44
49
  preCheckWaitQueue.push({ resolve, reject });
45
50
  });
46
51
 
52
+ connection.log.trace({
53
+ msg: 'Requesting IDLE break',
54
+ lockId: connection.currentLockId,
55
+ path: connection.mailbox && connection.mailbox.path,
56
+ queued: preCheckWaitQueue.length,
57
+ doneRequested,
58
+ canEnd,
59
+ doneSent
60
+ });
61
+
47
62
  preCheck();
48
63
 
49
64
  return handler;
@@ -51,7 +66,7 @@ module.exports = async connection => {
51
66
 
52
67
  response = await connection.exec('IDLE', false, {
53
68
  onPlusTag: async () => {
54
- connection.log.debug({ msg: `Initiated IDLE, waiting for server input` });
69
+ connection.log.debug({ msg: `Initiated IDLE, waiting for server input`, doneRequested });
55
70
  canEnd = true;
56
71
  if (doneRequested) {
57
72
  preCheck();
@@ -63,7 +78,22 @@ module.exports = async connection => {
63
78
  });
64
79
 
65
80
  // unset before response.next()
66
- connection.preCheck = false;
81
+ if (typeof connection.preCheck === 'function') {
82
+ connection.log.trace({
83
+ msg: 'Clearing pre-check function',
84
+ lockId: connection.currentLockId,
85
+ path: connection.mailbox && connection.mailbox.path,
86
+ queued: preCheckWaitQueue.length,
87
+ doneRequested,
88
+ canEnd,
89
+ doneSent
90
+ });
91
+ connection.preCheck = false;
92
+ while (preCheckWaitQueue.length) {
93
+ let { resolve } = preCheckWaitQueue.shift();
94
+ resolve();
95
+ }
96
+ }
67
97
 
68
98
  response.next();
69
99
  return;
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');
@@ -124,6 +126,7 @@ class ImapFlow extends EventEmitter {
124
126
  * @param {Boolean} [options.logRaw=false] If true then log data read from and written to socket encoded in base64
125
127
  * @param {Boolean} [options.emitLogs=false] If `true` then in addition of sending data to logger, ImapFlow emits 'log' events with the same data
126
128
  * @param {Boolean} [options.verifyOnly=false] If `true` then logs out automatically after successful authentication
129
+ * @param {Boolean} [options.proxy] Optional proxy URL. Supports HTTP CONNECT (`http://`, `https://`) and SOCKS (`socks://`, `socks4://`, `socks5://`) proxies
127
130
  */
128
131
  constructor(options) {
129
132
  super();
@@ -1031,7 +1034,7 @@ class ImapFlow extends EventEmitter {
1031
1034
  * await client.connect();
1032
1035
  */
1033
1036
  async connect() {
1034
- let connector = this.options.secure ? tls : net;
1037
+ let connector = this.secureConnection ? tls : net;
1035
1038
 
1036
1039
  let opts = Object.assign(
1037
1040
  {
@@ -1054,6 +1057,22 @@ class ImapFlow extends EventEmitter {
1054
1057
  this.untaggedHandlers.EXISTS = (...args) => this.untaggedExists(...args);
1055
1058
  this.untaggedHandlers.EXPUNGE = (...args) => this.untaggedExpunge(...args);
1056
1059
 
1060
+ let socket = false;
1061
+ if (this.options.proxy) {
1062
+ try {
1063
+ socket = await proxyConnection(this.log, this.options.proxy, this.host, this.port);
1064
+ if (!socket) {
1065
+ throw new Error('Failed to setup proxy connection');
1066
+ }
1067
+ } catch (err) {
1068
+ let error = new Error('Failed to setup proxy connection');
1069
+ error.code = err.code || 'ProxyError';
1070
+ error._err = err;
1071
+ this.log.error({ error, cid: this.id });
1072
+ throw error;
1073
+ }
1074
+ }
1075
+
1057
1076
  await new Promise((resolve, reject) => {
1058
1077
  this.connectTimeout = setTimeout(() => {
1059
1078
  let err = new Error('Failed to established connection in required time');
@@ -1063,7 +1082,7 @@ class ImapFlow extends EventEmitter {
1063
1082
  reject(err);
1064
1083
  }, CONNECT_TIMEOUT);
1065
1084
 
1066
- this.socket = connector.connect(opts, () => {
1085
+ let onConnect = () => {
1067
1086
  clearTimeout(this.connectTimeout);
1068
1087
  this.socket.setKeepAlive(true, 5 * 1000);
1069
1088
  this.socket.setTimeout(SOCKET_TIMEOUT);
@@ -1105,7 +1124,23 @@ class ImapFlow extends EventEmitter {
1105
1124
  // executed by initial "* OK"
1106
1125
  this.initialResolve = resolve;
1107
1126
  this.initialReject = reject;
1108
- });
1127
+ };
1128
+
1129
+ if (socket) {
1130
+ // socket is already establised via proxy
1131
+ if (this.secureConnection) {
1132
+ // TLS socket requires a handshake
1133
+ opts.socket = socket;
1134
+ this.socket = connector.connect(opts, onConnect);
1135
+ } else {
1136
+ // cleartext socket is already usable
1137
+ this.socket = socket;
1138
+ setImmediate(onConnect);
1139
+ }
1140
+ } else {
1141
+ this.socket = connector.connect(opts, onConnect);
1142
+ }
1143
+
1109
1144
  this.writeSocket = this.socket;
1110
1145
 
1111
1146
  this.socket.once('error', err => {
@@ -2025,7 +2060,13 @@ class ImapFlow extends EventEmitter {
2025
2060
  return {};
2026
2061
  }
2027
2062
 
2028
- options = options || {};
2063
+ options = Object.assign(
2064
+ {
2065
+ chunkSize: 64 * 1024,
2066
+ maxBytes: Infinity
2067
+ },
2068
+ options || {}
2069
+ );
2029
2070
 
2030
2071
  let hasMore = true;
2031
2072
  let processed = 0;
@@ -2035,6 +2076,28 @@ class ImapFlow extends EventEmitter {
2035
2076
 
2036
2077
  let uid = false;
2037
2078
 
2079
+ if (part === '1') {
2080
+ // First part has special conditions for single node emails as
2081
+ // the mime parts for root node are not 1 and 1.MIME but TEXT and HEADERS
2082
+ let response = await this.fetchOne(range, { uid: true, bodyStructure: true }, options);
2083
+
2084
+ if (!response) {
2085
+ return { response: false, chunk: false };
2086
+ }
2087
+
2088
+ if (!uid && response.uid) {
2089
+ uid = response.uid;
2090
+ // force UID from now on even if first range was a sequence number
2091
+ range = uid;
2092
+ options.uid = true;
2093
+ }
2094
+
2095
+ if (!response.bodyStructure.childNodes) {
2096
+ // single text message
2097
+ part = 'TEXT';
2098
+ }
2099
+ }
2100
+
2038
2101
  let getNextPart = async query => {
2039
2102
  query = query || {};
2040
2103
 
@@ -2052,10 +2115,15 @@ class ImapFlow extends EventEmitter {
2052
2115
  query.bodyParts = [];
2053
2116
  }
2054
2117
 
2055
- if (query.size && /^[\d.]+$/.test(part)) {
2056
- // fetch meta as well
2057
- mimeKey = part + '.mime';
2058
- query.bodyParts.push(mimeKey);
2118
+ if (query.size) {
2119
+ if (/^[\d.]+$/.test(part)) {
2120
+ // fetch meta as well
2121
+ mimeKey = part + '.mime';
2122
+ query.bodyParts.push(mimeKey);
2123
+ } else if (part === 'text') {
2124
+ mimeKey = 'header';
2125
+ query.bodyParts.push(mimeKey);
2126
+ }
2059
2127
  }
2060
2128
 
2061
2129
  query.bodyParts.push({
@@ -2092,15 +2160,10 @@ class ImapFlow extends EventEmitter {
2092
2160
  }
2093
2161
 
2094
2162
  if (query.bodyParts) {
2095
- result.mime = response.bodyParts.get(mimeKey);
2096
-
2097
- if (part === '1' && !result.mime) {
2098
- // Outlook does not support requesting 1.MIME for text/* messages, so we issue an additional command
2099
- let mimeResponse = await this.fetchOne(range, { bodyParts: ['header'] }, options);
2100
- if (!mimeResponse) {
2101
- return { response: false, chunk: false };
2102
- }
2103
- result.mime = mimeResponse.headers;
2163
+ if (mimeKey === 'header') {
2164
+ result.mime = response.headers;
2165
+ } else {
2166
+ result.mime = response.bodyParts.get(mimeKey);
2104
2167
  }
2105
2168
  }
2106
2169
 
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.0.74",
3
+ "version": "1.0.78",
4
4
  "description": "IMAP Client for Node",
5
5
  "main": "./lib/imap-flow.js",
6
6
  "scripts": {
@@ -26,19 +26,19 @@
26
26
  },
27
27
  "homepage": "https://imapflow.com/",
28
28
  "devDependencies": {
29
- "@babel/eslint-parser": "^7.15.7",
30
- "@babel/eslint-plugin": "^7.14.5",
31
- "@babel/plugin-syntax-class-properties": "^7.12.13",
32
- "@babel/preset-env": "^7.15.6",
33
- "@types/node": "16.10.1",
29
+ "@babel/eslint-parser": "7.16.3",
30
+ "@babel/eslint-plugin": "7.14.5",
31
+ "@babel/plugin-syntax-class-properties": "7.12.13",
32
+ "@babel/preset-env": "7.16.4",
33
+ "@types/node": "16.11.10",
34
34
  "braintree-jsdoc-template": "3.3.0",
35
- "eslint": "7.32.0",
35
+ "eslint": "8.3.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
- "grunt-contrib-nodeunit": "3.0.0",
41
- "grunt-eslint": "23.0.0",
40
+ "grunt-contrib-nodeunit": "4.0.0",
41
+ "grunt-eslint": "24.0.0",
42
42
  "jsdoc": "3.6.7",
43
43
  "tsd-jsdoc": "2.5.0"
44
44
  },
@@ -48,7 +48,9 @@
48
48
  "libbase64": "1.2.1",
49
49
  "libmime": "5.0.0",
50
50
  "libqp": "1.1.0",
51
- "mailsplit": "5.2.0",
52
- "pino": "6.13.3"
51
+ "mailsplit": "5.3.1",
52
+ "nodemailer": "6.7.2",
53
+ "pino": "7.4.1",
54
+ "socks": "2.6.1"
53
55
  }
54
56
  }