imapflow 1.0.73 → 1.0.77

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,15 @@
1
1
  # CHANGELOG
2
2
 
3
+ - 1.0.77 - 2021-11-25
4
+
5
+ - Testing out proxy connections
6
+ - Add X-GM-RAW support for search (jhi721 #68)
7
+
8
+ - 1.0.75 - 2021-10-15
9
+
10
+ - Fixed an exception with failing IDLE (ramiroaisen #60)
11
+ - Do not use 1 and 1.MIME for single node messages, fallback to TEXT and HEADERS
12
+
3
13
  - 1.0.71 - 2021-09-28
4
14
 
5
15
  - IDLE precheck changes
@@ -12,50 +12,100 @@ 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
 
18
- let idleSent = false;
19
+ //let idleSent = false;
19
20
  let doneRequested = false;
20
21
  let doneSent = false;
22
+ let canEnd = false;
21
23
 
22
- let preCheck = async () => {
24
+ let preCheck = () => {
23
25
  doneRequested = true;
24
- if (connection.idling && idleSent && !doneSent) {
25
- connection.log.debug({ src: 'c', msg: `DONE`, comment: `breaking IDLE` });
26
+ if (canEnd && !doneSent) {
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
+ });
26
34
  connection.write('DONE');
27
35
  doneSent = true;
36
+
37
+ connection.idling = false;
38
+ connection.preCheck = false; // unset itself
39
+
40
+ while (preCheckWaitQueue.length) {
41
+ let { resolve } = preCheckWaitQueue.shift();
42
+ resolve();
43
+ }
28
44
  }
29
45
  };
30
46
 
31
- connection.preCheck = async () => {
32
- await preCheck();
33
- connection.idling = false;
34
- connection.preCheck = false; // unset itself
47
+ connection.preCheck = () => {
48
+ let handler = new Promise((resolve, reject) => {
49
+ preCheckWaitQueue.push({ resolve, reject });
50
+ });
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
+
62
+ preCheck();
63
+
64
+ return handler;
35
65
  };
36
66
 
37
67
  response = await connection.exec('IDLE', false, {
38
68
  onPlusTag: async () => {
39
- connection.log.debug({ msg: `Initiated IDLE, waiting for server input` });
40
- },
41
- onSend: () => {
42
- idleSent = true;
69
+ connection.log.debug({ msg: `Initiated IDLE, waiting for server input`, doneRequested });
70
+ canEnd = true;
43
71
  if (doneRequested) {
44
- preCheck().catch(err => {
45
- connection.log.debug({ msg: `preCheck error`, err });
46
- });
72
+ preCheck();
47
73
  }
74
+ },
75
+ onSend: () => {
76
+ //idleSent = true;
48
77
  }
49
78
  });
50
79
 
51
80
  // unset before response.next()
52
- 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
+ }
53
97
 
54
98
  response.next();
55
99
  return;
56
100
  } catch (err) {
57
101
  connection.preCheck = false;
102
+ connection.idling = false;
103
+
58
104
  connection.log.warn({ err, cid: connection.id });
105
+ while (preCheckWaitQueue.length) {
106
+ let { reject } = preCheckWaitQueue.shift();
107
+ reject(err);
108
+ }
59
109
  return false;
60
110
  }
61
111
  }
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');
@@ -1031,7 +1033,7 @@ class ImapFlow extends EventEmitter {
1031
1033
  * await client.connect();
1032
1034
  */
1033
1035
  async connect() {
1034
- let connector = this.options.secure ? tls : net;
1036
+ let connector = this.secureConnection ? tls : net;
1035
1037
 
1036
1038
  let opts = Object.assign(
1037
1039
  {
@@ -1054,6 +1056,22 @@ class ImapFlow extends EventEmitter {
1054
1056
  this.untaggedHandlers.EXISTS = (...args) => this.untaggedExists(...args);
1055
1057
  this.untaggedHandlers.EXPUNGE = (...args) => this.untaggedExpunge(...args);
1056
1058
 
1059
+ let socket = false;
1060
+ if (this.options.proxy) {
1061
+ try {
1062
+ socket = await proxyConnection(this.log, this.options.proxy, this.host, this.port);
1063
+ if (!socket) {
1064
+ throw new Error('Failed to setup proxy connection');
1065
+ }
1066
+ } catch (err) {
1067
+ let error = new Error('Failed to setup proxy connection');
1068
+ error.code = err.code || 'ProxyError';
1069
+ error._err = err;
1070
+ this.log.error({ error, cid: this.id });
1071
+ throw error;
1072
+ }
1073
+ }
1074
+
1057
1075
  await new Promise((resolve, reject) => {
1058
1076
  this.connectTimeout = setTimeout(() => {
1059
1077
  let err = new Error('Failed to established connection in required time');
@@ -1063,7 +1081,7 @@ class ImapFlow extends EventEmitter {
1063
1081
  reject(err);
1064
1082
  }, CONNECT_TIMEOUT);
1065
1083
 
1066
- this.socket = connector.connect(opts, () => {
1084
+ let onConnect = () => {
1067
1085
  clearTimeout(this.connectTimeout);
1068
1086
  this.socket.setKeepAlive(true, 5 * 1000);
1069
1087
  this.socket.setTimeout(SOCKET_TIMEOUT);
@@ -1105,7 +1123,23 @@ class ImapFlow extends EventEmitter {
1105
1123
  // executed by initial "* OK"
1106
1124
  this.initialResolve = resolve;
1107
1125
  this.initialReject = reject;
1108
- });
1126
+ };
1127
+
1128
+ if (socket) {
1129
+ // socket is already establised via proxy
1130
+ if (this.secureConnection) {
1131
+ // TLS socket requires a handshake
1132
+ opts.socket = socket;
1133
+ this.socket = connector.connect(opts, onConnect);
1134
+ } else {
1135
+ // cleartext socket is already usable
1136
+ this.socket = socket;
1137
+ setImmediate(onConnect);
1138
+ }
1139
+ } else {
1140
+ this.socket = connector.connect(opts, onConnect);
1141
+ }
1142
+
1109
1143
  this.writeSocket = this.socket;
1110
1144
 
1111
1145
  this.socket.once('error', err => {
@@ -2025,7 +2059,13 @@ class ImapFlow extends EventEmitter {
2025
2059
  return {};
2026
2060
  }
2027
2061
 
2028
- options = options || {};
2062
+ options = Object.assign(
2063
+ {
2064
+ chunkSize: 64 * 1024,
2065
+ maxBytes: Infinity
2066
+ },
2067
+ options || {}
2068
+ );
2029
2069
 
2030
2070
  let hasMore = true;
2031
2071
  let processed = 0;
@@ -2035,6 +2075,28 @@ class ImapFlow extends EventEmitter {
2035
2075
 
2036
2076
  let uid = false;
2037
2077
 
2078
+ if (part === '1') {
2079
+ // First part has special conditions for single node emails as
2080
+ // the mime parts for root node are not 1 and 1.MIME but TEXT and HEADERS
2081
+ let response = await this.fetchOne(range, { uid: true, bodyStructure: true }, options);
2082
+
2083
+ if (!response) {
2084
+ return { response: false, chunk: false };
2085
+ }
2086
+
2087
+ if (!uid && response.uid) {
2088
+ uid = response.uid;
2089
+ // force UID from now on even if first range was a sequence number
2090
+ range = uid;
2091
+ options.uid = true;
2092
+ }
2093
+
2094
+ if (!response.bodyStructure.childNodes) {
2095
+ // single text message
2096
+ part = 'TEXT';
2097
+ }
2098
+ }
2099
+
2038
2100
  let getNextPart = async query => {
2039
2101
  query = query || {};
2040
2102
 
@@ -2052,10 +2114,15 @@ class ImapFlow extends EventEmitter {
2052
2114
  query.bodyParts = [];
2053
2115
  }
2054
2116
 
2055
- if (query.size && /^[\d.]+$/.test(part)) {
2056
- // fetch meta as well
2057
- mimeKey = part + '.mime';
2058
- query.bodyParts.push(mimeKey);
2117
+ if (query.size) {
2118
+ if (/^[\d.]+$/.test(part)) {
2119
+ // fetch meta as well
2120
+ mimeKey = part + '.mime';
2121
+ query.bodyParts.push(mimeKey);
2122
+ } else if (part === 'text') {
2123
+ mimeKey = 'header';
2124
+ query.bodyParts.push(mimeKey);
2125
+ }
2059
2126
  }
2060
2127
 
2061
2128
  query.bodyParts.push({
@@ -2092,15 +2159,10 @@ class ImapFlow extends EventEmitter {
2092
2159
  }
2093
2160
 
2094
2161
  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;
2162
+ if (mimeKey === 'header') {
2163
+ result.mime = response.headers;
2164
+ } else {
2165
+ result.mime = response.bodyParts.get(mimeKey);
2104
2166
  }
2105
2167
  }
2106
2168
 
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.0.73",
3
+ "version": "1.0.77",
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.1",
53
+ "pino": "7.4.1",
54
+ "socks": "2.6.1"
53
55
  }
54
56
  }