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 +2 -1
- package/CHANGELOG.md +10 -0
- package/lib/commands/idle.js +66 -16
- package/lib/imap-flow.js +79 -17
- package/lib/proxy-connection.js +120 -0
- package/lib/search-compiler.js +5 -0
- package/package.json +13 -11
package/.eslintrc
CHANGED
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
|
package/lib/commands/idle.js
CHANGED
|
@@ -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 =
|
|
24
|
+
let preCheck = () => {
|
|
23
25
|
doneRequested = true;
|
|
24
|
-
if (
|
|
25
|
-
connection.log.debug({
|
|
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 =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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()
|
|
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
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
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
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
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 };
|
package/lib/search-compiler.js
CHANGED
|
@@ -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.
|
|
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": "
|
|
30
|
-
"@babel/eslint-plugin": "
|
|
31
|
-
"@babel/plugin-syntax-class-properties": "
|
|
32
|
-
"@babel/preset-env": "
|
|
33
|
-
"@types/node": "16.10
|
|
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": "
|
|
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": "
|
|
41
|
-
"grunt-eslint": "
|
|
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.
|
|
52
|
-
"
|
|
51
|
+
"mailsplit": "5.3.1",
|
|
52
|
+
"nodemailer": "6.7.1",
|
|
53
|
+
"pino": "7.4.1",
|
|
54
|
+
"socks": "2.6.1"
|
|
53
55
|
}
|
|
54
56
|
}
|