imapflow 1.7.7 → 2.0.0
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 +27 -0
- package/README.md +8 -2
- package/dist/cjs/charsets.d.ts +1 -0
- package/dist/cjs/charsets.js +294 -0
- package/dist/cjs/commands/append.d.ts +22 -0
- package/dist/cjs/commands/append.js +151 -0
- package/dist/cjs/commands/authenticate.d.ts +24 -0
- package/dist/cjs/commands/authenticate.js +223 -0
- package/dist/cjs/commands/capability.d.ts +8 -0
- package/dist/cjs/commands/capability.js +32 -0
- package/dist/cjs/commands/close.d.ts +8 -0
- package/dist/cjs/commands/close.js +39 -0
- package/dist/cjs/commands/compress.d.ts +8 -0
- package/dist/cjs/commands/compress.js +56 -0
- package/dist/cjs/commands/copy.d.ts +13 -0
- package/dist/cjs/commands/copy.js +44 -0
- package/dist/cjs/commands/copyuid-parser.d.ts +11 -0
- package/dist/cjs/commands/copyuid-parser.js +32 -0
- package/dist/cjs/commands/create.d.ts +11 -0
- package/dist/cjs/commands/create.js +80 -0
- package/dist/cjs/commands/delete.d.ts +11 -0
- package/dist/cjs/commands/delete.js +40 -0
- package/dist/cjs/commands/enable.d.ts +9 -0
- package/dist/cjs/commands/enable.js +61 -0
- package/dist/cjs/commands/esearch-parser.d.ts +17 -0
- package/dist/cjs/commands/esearch-parser.js +91 -0
- package/dist/cjs/commands/expunge.d.ts +12 -0
- package/dist/cjs/commands/expunge.js +60 -0
- package/dist/cjs/commands/fetch.d.ts +30 -0
- package/dist/cjs/commands/fetch.js +241 -0
- package/dist/cjs/commands/id.d.ts +10 -0
- package/dist/cjs/commands/id.js +80 -0
- package/dist/cjs/commands/idle.d.ts +9 -0
- package/dist/cjs/commands/idle.js +347 -0
- package/dist/cjs/commands/list.d.ts +16 -0
- package/dist/cjs/commands/list.js +518 -0
- package/dist/cjs/commands/login.d.ts +11 -0
- package/dist/cjs/commands/login.js +42 -0
- package/dist/cjs/commands/logout.d.ts +8 -0
- package/dist/cjs/commands/logout.js +47 -0
- package/dist/cjs/commands/move.d.ts +13 -0
- package/dist/cjs/commands/move.js +57 -0
- package/dist/cjs/commands/namespace.d.ts +25 -0
- package/dist/cjs/commands/namespace.js +139 -0
- package/dist/cjs/commands/noop.d.ts +8 -0
- package/dist/cjs/commands/noop.js +22 -0
- package/dist/cjs/commands/quota.d.ts +10 -0
- package/dist/cjs/commands/quota.js +119 -0
- package/dist/cjs/commands/rename.d.ts +12 -0
- package/dist/cjs/commands/rename.js +48 -0
- package/dist/cjs/commands/search.d.ts +15 -0
- package/dist/cjs/commands/search.js +228 -0
- package/dist/cjs/commands/select.d.ts +25 -0
- package/dist/cjs/commands/select.js +250 -0
- package/dist/cjs/commands/starttls.d.ts +8 -0
- package/dist/cjs/commands/starttls.js +30 -0
- package/dist/cjs/commands/status-fields.d.ts +14 -0
- package/dist/cjs/commands/status-fields.js +61 -0
- package/dist/cjs/commands/status.d.ts +12 -0
- package/dist/cjs/commands/status.js +108 -0
- package/dist/cjs/commands/store.d.ts +19 -0
- package/dist/cjs/commands/store.js +93 -0
- package/dist/cjs/commands/subscribe.d.ts +9 -0
- package/dist/cjs/commands/subscribe.js +31 -0
- package/dist/cjs/commands/unsubscribe.d.ts +9 -0
- package/dist/cjs/commands/unsubscribe.js +31 -0
- package/dist/cjs/connection-deadline.d.ts +49 -0
- package/dist/cjs/connection-deadline.js +91 -0
- package/dist/cjs/errors.d.ts +83 -0
- package/dist/cjs/errors.js +13 -0
- package/dist/cjs/handler/imap-compiler.d.ts +24 -0
- package/dist/cjs/handler/imap-compiler.js +285 -0
- package/dist/cjs/handler/imap-formal-syntax.d.ts +28 -0
- package/dist/cjs/handler/imap-formal-syntax.js +121 -0
- package/dist/cjs/handler/imap-handler.d.ts +9 -0
- package/dist/cjs/handler/imap-handler.js +10 -0
- package/dist/cjs/handler/imap-parser.d.ts +16 -0
- package/dist/cjs/handler/imap-parser.js +90 -0
- package/dist/cjs/handler/imap-stream.d.ts +181 -0
- package/dist/cjs/handler/imap-stream.js +446 -0
- package/dist/cjs/handler/limits.d.ts +25 -0
- package/dist/cjs/handler/limits.js +51 -0
- package/dist/cjs/handler/parser-instance.d.ts +68 -0
- package/dist/cjs/handler/parser-instance.js +223 -0
- package/dist/cjs/handler/token-parser.d.ts +91 -0
- package/dist/cjs/handler/token-parser.js +673 -0
- package/dist/cjs/handler/types.d.ts +91 -0
- package/dist/cjs/handler/types.js +4 -0
- package/dist/cjs/imap-commands.d.ts +16 -0
- package/dist/cjs/imap-commands.js +74 -0
- package/dist/cjs/imap-flow.d.ts +676 -0
- package/dist/cjs/imap-flow.js +3949 -0
- package/dist/cjs/jp-decoder.d.ts +12 -0
- package/dist/cjs/jp-decoder.js +79 -0
- package/dist/cjs/limited-passthrough.d.ts +25 -0
- package/dist/cjs/limited-passthrough.js +54 -0
- package/dist/cjs/logger.d.ts +3 -0
- package/dist/cjs/logger.js +11 -0
- package/dist/cjs/package-info.d.ts +3 -0
- package/dist/cjs/package-info.js +7 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/proxy-connection.d.ts +33 -0
- package/dist/cjs/proxy-connection.js +392 -0
- package/dist/cjs/search-compiler.d.ts +34 -0
- package/dist/cjs/search-compiler.js +476 -0
- package/dist/cjs/special-use.d.ts +22 -0
- package/dist/cjs/special-use.js +911 -0
- package/dist/cjs/tools.d.ts +427 -0
- package/dist/cjs/tools.js +1496 -0
- package/{lib/imap-flow.d.ts → dist/cjs/types.d.ts} +386 -516
- package/dist/cjs/types.js +5 -0
- package/dist/esm/charsets.d.ts +1 -0
- package/{lib → dist/esm}/charsets.js +1 -6
- package/dist/esm/commands/append.d.ts +22 -0
- package/{lib → dist/esm}/commands/append.js +22 -52
- package/dist/esm/commands/authenticate.d.ts +24 -0
- package/{lib → dist/esm}/commands/authenticate.js +62 -87
- package/dist/esm/commands/capability.d.ts +8 -0
- package/{lib → dist/esm}/commands/capability.js +6 -9
- package/dist/esm/commands/close.d.ts +8 -0
- package/{lib → dist/esm}/commands/close.js +6 -10
- package/dist/esm/commands/compress.d.ts +8 -0
- package/{lib → dist/esm}/commands/compress.js +7 -11
- package/dist/esm/commands/copy.d.ts +13 -0
- package/{lib → dist/esm}/commands/copy.js +12 -20
- package/dist/esm/commands/copyuid-parser.d.ts +11 -0
- package/{lib → dist/esm}/commands/copyuid-parser.js +9 -15
- package/dist/esm/commands/create.d.ts +11 -0
- package/{lib → dist/esm}/commands/create.js +13 -27
- package/dist/esm/commands/delete.d.ts +11 -0
- package/{lib → dist/esm}/commands/delete.js +9 -14
- package/dist/esm/commands/enable.d.ts +9 -0
- package/{lib → dist/esm}/commands/enable.js +23 -30
- package/dist/esm/commands/esearch-parser.d.ts +17 -0
- package/dist/esm/commands/esearch-parser.js +88 -0
- package/dist/esm/commands/expunge.d.ts +12 -0
- package/{lib → dist/esm}/commands/expunge.js +17 -22
- package/dist/esm/commands/fetch.d.ts +30 -0
- package/{lib → dist/esm}/commands/fetch.js +32 -64
- package/dist/esm/commands/id.d.ts +10 -0
- package/{lib → dist/esm}/commands/id.js +17 -23
- package/dist/esm/commands/idle.d.ts +9 -0
- package/{lib → dist/esm}/commands/idle.js +47 -81
- package/dist/esm/commands/list.d.ts +16 -0
- package/{lib → dist/esm}/commands/list.js +56 -121
- package/dist/esm/commands/login.d.ts +11 -0
- package/{lib → dist/esm}/commands/login.js +10 -15
- package/dist/esm/commands/logout.d.ts +8 -0
- package/{lib → dist/esm}/commands/logout.js +9 -11
- package/dist/esm/commands/move.d.ts +13 -0
- package/{lib → dist/esm}/commands/move.js +13 -21
- package/dist/esm/commands/namespace.d.ts +25 -0
- package/{lib → dist/esm}/commands/namespace.js +34 -44
- package/dist/esm/commands/noop.d.ts +8 -0
- package/{lib → dist/esm}/commands/noop.js +6 -7
- package/dist/esm/commands/quota.d.ts +10 -0
- package/{lib → dist/esm}/commands/quota.js +18 -36
- package/dist/esm/commands/rename.d.ts +12 -0
- package/{lib → dist/esm}/commands/rename.js +10 -15
- package/dist/esm/commands/search.d.ts +15 -0
- package/{lib → dist/esm}/commands/search.js +36 -135
- package/dist/esm/commands/select.d.ts +25 -0
- package/{lib → dist/esm}/commands/select.js +33 -64
- package/dist/esm/commands/starttls.d.ts +8 -0
- package/{lib → dist/esm}/commands/starttls.js +6 -8
- package/dist/esm/commands/status-fields.d.ts +14 -0
- package/{lib → dist/esm}/commands/status-fields.js +5 -16
- package/dist/esm/commands/status.d.ts +12 -0
- package/{lib → dist/esm}/commands/status.js +18 -29
- package/dist/esm/commands/store.d.ts +19 -0
- package/{lib → dist/esm}/commands/store.js +24 -37
- package/dist/esm/commands/subscribe.d.ts +9 -0
- package/{lib → dist/esm}/commands/subscribe.js +8 -12
- package/dist/esm/commands/unsubscribe.d.ts +9 -0
- package/{lib → dist/esm}/commands/unsubscribe.js +8 -12
- package/dist/esm/connection-deadline.d.ts +49 -0
- package/{lib → dist/esm}/connection-deadline.js +14 -25
- package/dist/esm/errors.d.ts +83 -0
- package/dist/esm/errors.js +9 -0
- package/dist/esm/handler/imap-compiler.d.ts +24 -0
- package/{lib → dist/esm}/handler/imap-compiler.js +22 -80
- package/dist/esm/handler/imap-formal-syntax.d.ts +28 -0
- package/dist/esm/handler/imap-formal-syntax.js +117 -0
- package/dist/esm/handler/imap-handler.d.ts +9 -0
- package/dist/esm/handler/imap-handler.js +9 -0
- package/dist/esm/handler/imap-parser.d.ts +16 -0
- package/{lib → dist/esm}/handler/imap-parser.js +31 -44
- package/dist/esm/handler/imap-stream.d.ts +181 -0
- package/{lib → dist/esm}/handler/imap-stream.js +29 -121
- package/dist/esm/handler/limits.d.ts +25 -0
- package/{lib → dist/esm}/handler/limits.js +13 -22
- package/dist/esm/handler/parser-instance.d.ts +68 -0
- package/{lib → dist/esm}/handler/parser-instance.js +19 -47
- package/dist/esm/handler/token-parser.d.ts +91 -0
- package/{lib → dist/esm}/handler/token-parser.js +71 -155
- package/dist/esm/handler/types.d.ts +91 -0
- package/dist/esm/handler/types.js +3 -0
- package/dist/esm/imap-commands.d.ts +16 -0
- package/dist/esm/imap-commands.js +67 -0
- package/dist/esm/imap-flow.d.ts +676 -0
- package/{lib → dist/esm}/imap-flow.js +785 -1802
- package/dist/esm/jp-decoder.d.ts +12 -0
- package/{lib → dist/esm}/jp-decoder.js +6 -21
- package/dist/esm/limited-passthrough.d.ts +25 -0
- package/{lib → dist/esm}/limited-passthrough.js +7 -20
- package/dist/esm/logger.d.ts +3 -0
- package/dist/esm/logger.js +4 -0
- package/dist/esm/package-info.d.ts +3 -0
- package/dist/esm/package-info.js +4 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/proxy-connection.d.ts +33 -0
- package/{lib → dist/esm}/proxy-connection.js +56 -127
- package/dist/esm/search-compiler.d.ts +34 -0
- package/{lib → dist/esm}/search-compiler.js +54 -110
- package/dist/esm/special-use.d.ts +22 -0
- package/dist/esm/special-use.js +907 -0
- package/dist/esm/tools.d.ts +427 -0
- package/dist/esm/tools.js +1446 -0
- package/dist/esm/types.d.ts +828 -0
- package/dist/esm/types.js +4 -0
- package/package.json +60 -20
- package/.gitattributes +0 -1
- package/.github/CODE_OF_CONDUCT.md +0 -76
- package/.github/FUNDING.yml +0 -4
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
- package/.github/contributing.md +0 -17
- package/.github/workflows/release.yaml +0 -36
- package/.github/workflows/stale.yml +0 -29
- package/.github/workflows/test.yml +0 -51
- package/.ncurc.js +0 -4
- package/.prettierignore +0 -4
- package/.prettierrc.js +0 -8
- package/.release-please-manifest.json +0 -3
- package/CLAUDE.md +0 -104
- package/Gruntfile.js +0 -23
- package/eslint.config.js +0 -45
- package/lib/handler/imap-formal-syntax.js +0 -189
- package/lib/handler/imap-handler.js +0 -17
- package/lib/imap-commands.js +0 -45
- package/lib/logger.js +0 -5
- package/lib/special-use.js +0 -923
- package/lib/tools.js +0 -1612
- package/release-please-config.json +0 -10
- package/test/authentication-test.js +0 -101
- package/test/auto-idle-test.js +0 -470
- package/test/bodystructure-test.js +0 -899
- package/test/charsets-test.js +0 -161
- package/test/commands-branches-test.js +0 -1095
- package/test/commands-integration-test.js +0 -11124
- package/test/commands-test.js +0 -73
- package/test/connection-edge-cases-test.js +0 -1828
- package/test/connection-test.js +0 -162
- package/test/copyuid-parser-test.js +0 -173
- package/test/fetch-generator-test.js +0 -218
- package/test/fixtures/fake-timers.js +0 -115
- package/test/fixtures/serialized-mimetorture.js +0 -2738
- package/test/fixtures/test-client.js +0 -57
- package/test/fixtures/test-tls.js +0 -8
- package/test/handler-branches-test.js +0 -310
- package/test/idle-polling-test.js +0 -518
- package/test/imap-compiler-test.js +0 -809
- package/test/imap-flow-compress-test.js +0 -166
- package/test/imap-flow-coverage-test.js +0 -612
- package/test/imap-flow-fetch-download-test.js +0 -873
- package/test/imap-flow-internals-test.js +0 -725
- package/test/imap-flow-methods-test.js +0 -889
- package/test/imap-flow-proxy-paths-test.js +0 -366
- package/test/imap-flow-secure-test.js +0 -573
- package/test/imap-flow-server-test.js +0 -1474
- package/test/imap-formal-syntax-test.js +0 -293
- package/test/imap-parser-test.js +0 -1474
- package/test/imap-stream-edge-cases-test.js +0 -666
- package/test/imap-stream-test.js +0 -177
- package/test/imapflow-test.js +0 -258
- package/test/integration/README.md +0 -52
- package/test/integration/dovecot-test.conf +0 -27
- package/test/integration/rev2-live-test.js +0 -431
- package/test/integration/run-rev2-tests.sh +0 -75
- package/test/integration-test.js +0 -83
- package/test/jp-decoder-test.js +0 -304
- package/test/limited-passthrough-test.js +0 -299
- package/test/memory-cleanup-test.js +0 -144
- package/test/memory-leak-test.js +0 -667
- package/test/parser-limits-test.js +0 -292
- package/test/proxy-connection-test.js +0 -738
- package/test/reliability-improvements-test.js +0 -548
- package/test/search-compiler-test.js +0 -1300
- package/test/search-test.js +0 -329
- package/test/special-use-test.js +0 -418
- package/test/starttls-injection-test.js +0 -181
- package/test/tag-correlation-test.js +0 -333
- package/test/timer-policy-test.js +0 -227
- package/test/token-parser-test.js +0 -456
- package/test/tools-test.js +0 -2013
- package/test/unhandled-rejection-test.js +0 -593
|
@@ -1,69 +1,59 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const { ConnectionDeadline } = require('./connection-deadline');
|
|
9
|
-
|
|
1
|
+
import { SocksClient } from 'socks';
|
|
2
|
+
import dns from 'node:dns';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import tls from 'node:tls';
|
|
5
|
+
import { ConnectionDeadline } from './connection-deadline.js';
|
|
6
|
+
import { clearTimer } from './tools.js';
|
|
10
7
|
// Cap the CONNECT response buffered before the header terminator, so a proxy that never sends
|
|
11
8
|
// \r\n\r\n cannot grow memory without bound.
|
|
12
9
|
const MAX_RESPONSE_HEADER_BYTES = 64 * 1024;
|
|
13
|
-
|
|
14
10
|
const DEFAULT_SOCKS_PORT = 1080;
|
|
15
|
-
|
|
16
11
|
// URL hostnames keep the brackets around an IPv6 literal ("[2001:db8::1]"), which is neither a
|
|
17
12
|
// valid input for net.isIP() nor an address net/tls/socks can connect to. Strip them for socket
|
|
18
13
|
// options; the parsed URL itself stays intact for logging and credentials.
|
|
19
|
-
const unbracketAddress = host => (typeof host === 'string' && host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host);
|
|
20
|
-
|
|
14
|
+
const unbracketAddress = (host) => (typeof host === 'string' && host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host);
|
|
21
15
|
// CONNECT request lines and Host headers need an IPv6 destination wrapped in brackets. Hostnames
|
|
22
16
|
// and IPv4 literals are used as-is, and an already bracketed literal is not bracketed twice.
|
|
23
17
|
const formatAuthority = (host, port) => {
|
|
24
18
|
let address = unbracketAddress(host);
|
|
25
19
|
return net.isIPv6(address) ? `[${address}]:${port}` : `${address}:${port}`;
|
|
26
20
|
};
|
|
27
|
-
|
|
28
21
|
// Password-free rendering of the proxy URL, used in every log path. The caller's URL object is
|
|
29
22
|
// left untouched so credentials stay available for authentication.
|
|
30
|
-
const redactUrl = proxyUrl => {
|
|
23
|
+
const redactUrl = (proxyUrl) => {
|
|
31
24
|
let redacted = new URL(proxyUrl.href);
|
|
32
25
|
if (redacted.password) {
|
|
33
26
|
redacted.password = '(hidden)';
|
|
34
27
|
}
|
|
35
28
|
return redacted.href;
|
|
36
29
|
};
|
|
37
|
-
|
|
38
30
|
const proxyError = (message, code) => {
|
|
39
31
|
let err = new Error(message);
|
|
40
32
|
err.code = code || 'ProxyError';
|
|
41
33
|
return err;
|
|
42
34
|
};
|
|
43
|
-
|
|
44
35
|
// URL userinfo is percent-encoded, so it has to be decoded before it can be used as credentials.
|
|
45
36
|
// A password containing a bare '%' is not valid percent-encoding and makes decodeURIComponent
|
|
46
37
|
// throw, so such values are used as they came in rather than failing the connection.
|
|
47
|
-
const decodeUserInfo = value => {
|
|
38
|
+
const decodeUserInfo = (value) => {
|
|
48
39
|
try {
|
|
49
40
|
return decodeURIComponent(value);
|
|
50
|
-
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
51
43
|
return value;
|
|
52
44
|
}
|
|
53
45
|
};
|
|
54
|
-
|
|
55
46
|
// The socks client attaches its full options object - proxy password included - to the errors it
|
|
56
47
|
// throws, and Node's URL errors carry the rejected string in `input`. Any logger that serializes
|
|
57
48
|
// error properties would then write that password out in clear text, so the credentials are
|
|
58
49
|
// dropped before the error is logged or handed to the caller.
|
|
59
|
-
const stripProxyCredentials = err => {
|
|
50
|
+
const stripProxyCredentials = (err) => {
|
|
60
51
|
if (err && typeof err === 'object') {
|
|
61
52
|
delete err.options;
|
|
62
53
|
delete err.input;
|
|
63
54
|
}
|
|
64
55
|
return err;
|
|
65
56
|
};
|
|
66
|
-
|
|
67
57
|
// Attaches a benign 'error' listener as soon as the proxied socket exists, so an early
|
|
68
58
|
// socket error (before ImapFlow installs its own handlers) cannot surface as an unhandled
|
|
69
59
|
// 'error' event and crash the process. The handler is stored on the socket so the caller
|
|
@@ -72,21 +62,19 @@ const attachEarlyErrorHandler = (logger, socket) => {
|
|
|
72
62
|
if (!socket || typeof socket.on !== 'function') {
|
|
73
63
|
return;
|
|
74
64
|
}
|
|
75
|
-
socket._earlyErrorHandler = err => {
|
|
65
|
+
socket._earlyErrorHandler = (err) => {
|
|
76
66
|
logger.error({ msg: 'Proxy socket error before connection setup', err });
|
|
77
67
|
};
|
|
78
68
|
socket.on('error', socket._earlyErrorHandler);
|
|
79
69
|
};
|
|
80
|
-
|
|
81
70
|
// Removes the handler installed by attachEarlyErrorHandler once the caller takes ownership
|
|
82
71
|
// of the socket. Keeps the internal `_earlyErrorHandler` contract inside this module.
|
|
83
|
-
const detachEarlyErrorHandler = socket => {
|
|
72
|
+
const detachEarlyErrorHandler = (socket) => {
|
|
84
73
|
if (socket && socket._earlyErrorHandler) {
|
|
85
74
|
socket.removeListener('error', socket._earlyErrorHandler);
|
|
86
75
|
socket._earlyErrorHandler = null;
|
|
87
76
|
}
|
|
88
77
|
};
|
|
89
|
-
|
|
90
78
|
/**
|
|
91
79
|
* Establishes a tunnel through an HTTP or HTTPS proxy with a CONNECT request.
|
|
92
80
|
*
|
|
@@ -97,16 +85,7 @@ const detachEarlyErrorHandler = socket => {
|
|
|
97
85
|
* The destination hostname is passed through unresolved - resolving it is the proxy's job, which
|
|
98
86
|
* is also what keeps DNS traffic off the client for HTTP proxies.
|
|
99
87
|
*
|
|
100
|
-
* @
|
|
101
|
-
* @param {Object} params.logger Logger instance.
|
|
102
|
-
* @param {URL} params.proxyUrl Parsed proxy URL (credentials intact).
|
|
103
|
-
* @param {Boolean} params.secureProxy Whether the proxy endpoint itself speaks TLS.
|
|
104
|
-
* @param {String} params.proxyHost Proxy endpoint host, IPv6 literals unbracketed.
|
|
105
|
-
* @param {Number} params.proxyPort Proxy endpoint port.
|
|
106
|
-
* @param {String} params.host Destination host (hostname or IP literal).
|
|
107
|
-
* @param {Number} params.port Destination port.
|
|
108
|
-
* @param {ConnectionDeadline} params.deadline Shared connection deadline.
|
|
109
|
-
* @returns {Promise<Object>} The established socket, tunnelled to the destination.
|
|
88
|
+
* @returns The established socket, tunnelled to the destination.
|
|
110
89
|
*/
|
|
111
90
|
const httpConnect = async ({ logger, proxyUrl, secureProxy, proxyHost, proxyPort, host, port, deadline }) => {
|
|
112
91
|
// Reject CRLF in the destination before it reaches the CONNECT request line and Host header.
|
|
@@ -115,27 +94,21 @@ const httpConnect = async ({ logger, proxyUrl, secureProxy, proxyHost, proxyPort
|
|
|
115
94
|
if (!destinationPort || /[\r\n]/.test(host)) {
|
|
116
95
|
throw proxyError('Invalid proxy destination', 'EPROXY');
|
|
117
96
|
}
|
|
118
|
-
|
|
119
97
|
let authority = formatAuthority(host, destinationPort);
|
|
120
|
-
|
|
121
98
|
let remaining = deadline.remaining();
|
|
122
99
|
if (!remaining) {
|
|
123
100
|
throw deadline.error();
|
|
124
101
|
}
|
|
125
|
-
|
|
126
102
|
let socket = null;
|
|
127
|
-
|
|
128
103
|
return await new Promise((resolve, reject) => {
|
|
129
104
|
let settled = false;
|
|
130
105
|
let timer = null;
|
|
131
106
|
let headers = '';
|
|
132
|
-
|
|
133
|
-
const onSocketData = chunk => {
|
|
107
|
+
const onSocketData = (chunk) => {
|
|
134
108
|
// Scan only the newly arrived bytes (plus the 3 that a terminator could straddle),
|
|
135
109
|
// so a proxy that dribbles its headers cannot turn this into a quadratic rescan.
|
|
136
110
|
let searchFrom = Math.max(0, headers.length - 3);
|
|
137
111
|
headers += chunk.toString('binary');
|
|
138
|
-
|
|
139
112
|
let terminator = headers.indexOf('\r\n\r\n', searchFrom);
|
|
140
113
|
if (terminator < 0) {
|
|
141
114
|
if (headers.length > MAX_RESPONSE_HEADER_BYTES) {
|
|
@@ -143,14 +116,12 @@ const httpConnect = async ({ logger, proxyUrl, secureProxy, proxyHost, proxyPort
|
|
|
143
116
|
}
|
|
144
117
|
return;
|
|
145
118
|
}
|
|
146
|
-
|
|
147
119
|
// The header block is complete, so this listener must stop consuming before anything
|
|
148
120
|
// is put back: unshifting while still subscribed re-emits the data straight back into
|
|
149
121
|
// this handler, which would swallow it. Pausing hands the socket over cleanly - the
|
|
150
122
|
// next owner resumes it (ImapFlow pipes it into the parser).
|
|
151
123
|
socket.removeListener('data', onSocketData);
|
|
152
124
|
socket.pause();
|
|
153
|
-
|
|
154
125
|
// Anything after the header terminator already belongs to the tunnelled stream (a
|
|
155
126
|
// server greeting that the proxy coalesced with its own response) and has to be
|
|
156
127
|
// preserved for the next consumer. It is put back as the original bytes, taken from
|
|
@@ -161,19 +132,16 @@ const httpConnect = async ({ logger, proxyUrl, secureProxy, proxyHost, proxyPort
|
|
|
161
132
|
socket.unshift(chunk.subarray(consumedFromChunk));
|
|
162
133
|
}
|
|
163
134
|
headers = headers.slice(0, terminator);
|
|
164
|
-
|
|
165
135
|
let status = headers.match(/^HTTP\/\d+\.\d+ (\d+)/i);
|
|
166
136
|
if (!status || (status[1] || '').charAt(0) !== '2') {
|
|
167
137
|
return fail(proxyError(`Invalid response from proxy${status ? `: ${status[1]}` : ''}`, 'EPROXY'));
|
|
168
138
|
}
|
|
169
|
-
|
|
170
139
|
succeed();
|
|
171
140
|
};
|
|
172
|
-
|
|
173
141
|
// Single settlement path: temporary listeners and the deadline timer are dropped exactly
|
|
174
142
|
// once, so a late socket event cannot settle the promise twice or leave a timer armed.
|
|
175
143
|
const cleanup = () => {
|
|
176
|
-
|
|
144
|
+
clearTimer(timer);
|
|
177
145
|
timer = null;
|
|
178
146
|
if (socket) {
|
|
179
147
|
// Every temporary listener goes, the connect callback included: after settlement
|
|
@@ -184,7 +152,6 @@ const httpConnect = async ({ logger, proxyUrl, secureProxy, proxyHost, proxyPort
|
|
|
184
152
|
socket.removeListener('close', onEarlyClose);
|
|
185
153
|
}
|
|
186
154
|
};
|
|
187
|
-
|
|
188
155
|
function fail(err) {
|
|
189
156
|
if (settled) {
|
|
190
157
|
return;
|
|
@@ -196,19 +163,15 @@ const httpConnect = async ({ logger, proxyUrl, secureProxy, proxyHost, proxyPort
|
|
|
196
163
|
}
|
|
197
164
|
reject(err);
|
|
198
165
|
}
|
|
199
|
-
|
|
200
166
|
function succeed() {
|
|
201
167
|
settled = true;
|
|
202
168
|
cleanup();
|
|
203
169
|
resolve(socket);
|
|
204
170
|
}
|
|
205
|
-
|
|
206
171
|
function onEarlyClose() {
|
|
207
172
|
fail(proxyError('Proxy closed the connection before the tunnel was established', 'EPROXY'));
|
|
208
173
|
}
|
|
209
|
-
|
|
210
174
|
timer = setTimeout(() => fail(deadline.error()), remaining);
|
|
211
|
-
|
|
212
175
|
let connectOptions = { host: proxyHost, port: proxyPort };
|
|
213
176
|
if (secureProxy) {
|
|
214
177
|
// Verify the proxy's certificate (Node default) and target SNI plus hostname
|
|
@@ -218,7 +181,6 @@ const httpConnect = async ({ logger, proxyUrl, secureProxy, proxyHost, proxyPort
|
|
|
218
181
|
connectOptions.servername = proxyHost;
|
|
219
182
|
}
|
|
220
183
|
}
|
|
221
|
-
|
|
222
184
|
// Declared as a function so cleanup() above can detach it (the connect callback is
|
|
223
185
|
// registered as a one-shot 'connect' listener by net/tls).
|
|
224
186
|
function onConnected() {
|
|
@@ -226,66 +188,58 @@ const httpConnect = async ({ logger, proxyUrl, secureProxy, proxyHost, proxyPort
|
|
|
226
188
|
Host: authority,
|
|
227
189
|
Connection: 'close'
|
|
228
190
|
};
|
|
229
|
-
|
|
230
191
|
if (proxyUrl.username || proxyUrl.password) {
|
|
231
192
|
let credentials = `${decodeUserInfo(proxyUrl.username)}:${decodeUserInfo(proxyUrl.password)}`;
|
|
232
193
|
requestHeaders['Proxy-Authorization'] = `Basic ${Buffer.from(credentials).toString('base64')}`;
|
|
233
194
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
.join('\r\n') +
|
|
240
|
-
'\r\n\r\n'
|
|
241
|
-
);
|
|
242
|
-
|
|
195
|
+
socket.write(`CONNECT ${authority} HTTP/1.1\r\n` +
|
|
196
|
+
Object.keys(requestHeaders)
|
|
197
|
+
.map(key => `${key}: ${requestHeaders[key]}`)
|
|
198
|
+
.join('\r\n') +
|
|
199
|
+
'\r\n\r\n');
|
|
243
200
|
socket.on('data', onSocketData);
|
|
244
201
|
}
|
|
245
|
-
|
|
246
202
|
// The socket is retained as soon as it is created, so an expiry can destroy it at once.
|
|
247
203
|
socket = secureProxy ? tls.connect(connectOptions, onConnected) : net.connect(connectOptions, onConnected);
|
|
248
204
|
socket.once('error', fail);
|
|
249
205
|
socket.once('close', onEarlyClose);
|
|
250
206
|
})
|
|
251
207
|
.then(established => {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
208
|
+
logger.info({
|
|
209
|
+
msg: `Established a socket via HTTP proxy`,
|
|
210
|
+
proxyUrl: redactUrl(proxyUrl),
|
|
211
|
+
port,
|
|
212
|
+
host
|
|
213
|
+
});
|
|
214
|
+
attachEarlyErrorHandler(logger, established);
|
|
215
|
+
return established;
|
|
216
|
+
})
|
|
261
217
|
.catch(err => {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
});
|
|
269
|
-
throw err;
|
|
218
|
+
logger.error({
|
|
219
|
+
msg: 'Failed to establish a socket via HTTP proxy',
|
|
220
|
+
proxyUrl: redactUrl(proxyUrl),
|
|
221
|
+
port,
|
|
222
|
+
host,
|
|
223
|
+
err
|
|
270
224
|
});
|
|
225
|
+
throw err;
|
|
226
|
+
});
|
|
271
227
|
};
|
|
272
|
-
|
|
273
228
|
/**
|
|
274
229
|
* Resolves a destination hostname to an IPv4 address. Only used for SOCKS4, which has no IPv6
|
|
275
230
|
* destination address type and no hostname form of its own.
|
|
276
231
|
*
|
|
277
|
-
* @param
|
|
278
|
-
* @param
|
|
279
|
-
* @returns
|
|
232
|
+
* @param hostname Destination hostname.
|
|
233
|
+
* @param deadline Shared connection deadline.
|
|
234
|
+
* @returns An IPv4 address.
|
|
280
235
|
*/
|
|
281
236
|
const resolveIPv4 = async (hostname, deadline) => {
|
|
282
|
-
let addresses = await deadline.race(dns.resolve4(hostname));
|
|
237
|
+
let addresses = await deadline.race(dns.promises.resolve4(hostname));
|
|
283
238
|
if (!addresses || !addresses.length) {
|
|
284
239
|
throw proxyError(`Could not resolve an IPv4 address for ${hostname}`, 'EPROXY');
|
|
285
240
|
}
|
|
286
241
|
return addresses[0];
|
|
287
242
|
};
|
|
288
|
-
|
|
289
243
|
/**
|
|
290
244
|
* Establishes a tunnel through a SOCKS proxy.
|
|
291
245
|
*
|
|
@@ -298,32 +252,20 @@ const resolveIPv4 = async (hostname, deadline) => {
|
|
|
298
252
|
* IPv6 destination literals are rejected for both SOCKS4 and SOCKS4a: neither can carry them, and
|
|
299
253
|
* the dependency would write the literal into the SOCKS4a hostname field instead.
|
|
300
254
|
*
|
|
301
|
-
* @
|
|
302
|
-
* @param {Object} params.logger Logger instance.
|
|
303
|
-
* @param {URL} params.proxyUrl Parsed proxy URL (credentials intact).
|
|
304
|
-
* @param {String} params.protocol Configured proxy protocol (socks, socks4, socks4a, socks5).
|
|
305
|
-
* @param {String} params.proxyHost Proxy endpoint host, IPv6 literals unbracketed.
|
|
306
|
-
* @param {Number} params.proxyPort Proxy endpoint port.
|
|
307
|
-
* @param {String} params.host Destination host (hostname or IP literal).
|
|
308
|
-
* @param {Number} params.port Destination port.
|
|
309
|
-
* @param {ConnectionDeadline} params.deadline Shared connection deadline.
|
|
310
|
-
* @returns {Promise<Object>} The established socket, tunnelled to the destination.
|
|
255
|
+
* @returns The established socket, tunnelled to the destination.
|
|
311
256
|
*/
|
|
312
257
|
const socksConnect = async ({ logger, proxyUrl, protocol, proxyHost, proxyPort, host, port, deadline }) => {
|
|
313
258
|
let proxyType = protocol === 'socks4' || protocol === 'socks4a' ? 4 : 5;
|
|
314
259
|
let destinationHost = unbracketAddress(host);
|
|
315
|
-
|
|
316
260
|
try {
|
|
317
261
|
if (proxyType === 4) {
|
|
318
262
|
if (net.isIPv6(destinationHost)) {
|
|
319
263
|
throw proxyError(`SOCKS4 and SOCKS4a cannot address IPv6 destinations (${destinationHost})`, 'UnsupportedProxyAddress');
|
|
320
264
|
}
|
|
321
|
-
|
|
322
265
|
if (protocol === 'socks4' && !net.isIP(destinationHost)) {
|
|
323
266
|
destinationHost = await resolveIPv4(destinationHost, deadline);
|
|
324
267
|
}
|
|
325
268
|
}
|
|
326
|
-
|
|
327
269
|
let connectionOpts = {
|
|
328
270
|
proxy: {
|
|
329
271
|
// The endpoint is handed to net.Socket.connect() by the dependency, so a hostname
|
|
@@ -339,12 +281,10 @@ const socksConnect = async ({ logger, proxyUrl, protocol, proxyHost, proxyPort,
|
|
|
339
281
|
command: 'connect',
|
|
340
282
|
set_tcp_nodelay: true
|
|
341
283
|
};
|
|
342
|
-
|
|
343
284
|
if (proxyUrl.username || proxyUrl.password) {
|
|
344
285
|
connectionOpts.proxy.userId = proxyUrl.username;
|
|
345
286
|
connectionOpts.proxy.password = proxyUrl.password;
|
|
346
287
|
}
|
|
347
|
-
|
|
348
288
|
// The dependency treats a zero timeout as its own 30 second default, so only a strictly
|
|
349
289
|
// positive remaining budget may be passed.
|
|
350
290
|
let remaining = deadline.remaining();
|
|
@@ -352,12 +292,10 @@ const socksConnect = async ({ logger, proxyUrl, protocol, proxyHost, proxyPort,
|
|
|
352
292
|
throw deadline.error();
|
|
353
293
|
}
|
|
354
294
|
connectionOpts.timeout = remaining;
|
|
355
|
-
|
|
356
295
|
const info = await deadline.race(SocksClient.createConnection(connectionOpts));
|
|
357
296
|
if (!info || !info.socket) {
|
|
358
297
|
throw proxyError('SOCKS proxy did not return a socket', 'EPROXY');
|
|
359
298
|
}
|
|
360
|
-
|
|
361
299
|
logger.info({
|
|
362
300
|
msg: 'Established a socket via SOCKS proxy',
|
|
363
301
|
proxyUrl: redactUrl(proxyUrl),
|
|
@@ -365,14 +303,13 @@ const socksConnect = async ({ logger, proxyUrl, protocol, proxyHost, proxyPort,
|
|
|
365
303
|
host
|
|
366
304
|
});
|
|
367
305
|
attachEarlyErrorHandler(logger, info.socket);
|
|
368
|
-
|
|
369
306
|
return info.socket;
|
|
370
|
-
}
|
|
307
|
+
}
|
|
308
|
+
catch (caught) {
|
|
371
309
|
// A dependency expiry and the shared deadline are reported with the same
|
|
372
310
|
// CONNECT_TIMEOUT shape, so a caller does not need to know which noticed first.
|
|
373
311
|
let err = deadline.normalize(stripProxyCredentials(caught));
|
|
374
312
|
stripProxyCredentials(err._err);
|
|
375
|
-
|
|
376
313
|
logger.error({
|
|
377
314
|
msg: 'Failed to establish a socket via SOCKS proxy',
|
|
378
315
|
proxyUrl: redactUrl(proxyUrl),
|
|
@@ -383,43 +320,36 @@ const socksConnect = async ({ logger, proxyUrl, protocol, proxyHost, proxyPort,
|
|
|
383
320
|
throw err;
|
|
384
321
|
}
|
|
385
322
|
};
|
|
386
|
-
|
|
387
323
|
/**
|
|
388
324
|
* Opens a socket to `host`:`port` through the configured proxy.
|
|
389
325
|
*
|
|
390
|
-
* @param
|
|
391
|
-
* @param
|
|
392
|
-
* @param
|
|
326
|
+
* @param logger Logger instance.
|
|
327
|
+
* @param connectionUrl Proxy URL (http, https, socks, socks4, socks4a, socks5).
|
|
328
|
+
* @param host Destination host, passed through unresolved wherever the proxy protocol
|
|
393
329
|
* can resolve it itself.
|
|
394
|
-
* @param
|
|
395
|
-
* @param
|
|
396
|
-
* @
|
|
397
|
-
* negotiation run inside it, so a stalled proxy cannot exceed the configured connectionTimeout.
|
|
398
|
-
* @param {Number} [options.connectionTimeout] Used to build a deadline when none was passed.
|
|
399
|
-
* @returns {Promise<Object|undefined>} The tunnelled socket, or undefined for an unknown protocol.
|
|
330
|
+
* @param port Destination port.
|
|
331
|
+
* @param options Deadline options, see ProxyConnectionOptions.
|
|
332
|
+
* @returns The tunnelled socket, or undefined for an unknown protocol.
|
|
400
333
|
*/
|
|
401
334
|
const proxyConnection = async (logger, connectionUrl, host, port, options) => {
|
|
402
335
|
options = options || {};
|
|
403
|
-
|
|
404
336
|
let deadline = options.deadline || new ConnectionDeadline(options.connectionTimeout);
|
|
405
337
|
deadline.check();
|
|
406
|
-
|
|
407
338
|
let proxyUrl;
|
|
408
339
|
try {
|
|
409
340
|
proxyUrl = new URL(connectionUrl);
|
|
410
|
-
}
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
411
343
|
// new URL() attaches the string it rejected to err.input, which here is the full proxy
|
|
412
344
|
// endpoint including its password. Any logger that serializes error properties would
|
|
413
345
|
// write that out in clear text, so the cause is reported without carrying the value.
|
|
414
346
|
throw proxyError('Invalid proxy URL', err.code || 'ERR_INVALID_URL');
|
|
415
347
|
}
|
|
416
348
|
let protocol = proxyUrl.protocol.replace(/:$/, '').toLowerCase();
|
|
417
|
-
|
|
418
349
|
// ImapFlow performs no DNS lookup of its own for the proxy endpoint: net, tls and the SOCKS
|
|
419
350
|
// client all resolve a hostname endpoint themselves, which keeps Node's normal connection
|
|
420
351
|
// behavior (including address-family selection) instead of pinning one address.
|
|
421
352
|
let proxyHost = unbracketAddress(proxyUrl.hostname);
|
|
422
|
-
|
|
423
353
|
switch (protocol) {
|
|
424
354
|
// Connect using a HTTP CONNECT method
|
|
425
355
|
case 'http':
|
|
@@ -434,7 +364,6 @@ const proxyConnection = async (logger, connectionUrl, host, port, options) => {
|
|
|
434
364
|
port,
|
|
435
365
|
deadline
|
|
436
366
|
});
|
|
437
|
-
|
|
438
367
|
// SOCKS proxy
|
|
439
368
|
case 'socks':
|
|
440
369
|
case 'socks5':
|
|
@@ -451,6 +380,6 @@ const proxyConnection = async (logger, connectionUrl, host, port, options) => {
|
|
|
451
380
|
deadline
|
|
452
381
|
});
|
|
453
382
|
}
|
|
383
|
+
return undefined;
|
|
454
384
|
};
|
|
455
|
-
|
|
456
|
-
module.exports = { proxyConnection, detachEarlyErrorHandler };
|
|
385
|
+
export { proxyConnection, detachEarlyErrorHandler };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ImapFlow } from './imap-flow.js';
|
|
2
|
+
import type { ImapAttributeNode } from './handler/types.js';
|
|
3
|
+
import type { SearchObject } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* A compiled search attribute: a token, or a parenthesized group of tokens
|
|
6
|
+
*/
|
|
7
|
+
export type SearchAttribute = ImapAttributeNode | SearchAttribute[];
|
|
8
|
+
/**
|
|
9
|
+
* Compiles a JavaScript object query into IMAP search command attributes.
|
|
10
|
+
* Supports standard IMAP search criteria and extensions like OBJECTID and Gmail extensions.
|
|
11
|
+
*
|
|
12
|
+
* @param connection - IMAP connection object (capabilities, enabled extensions and the current mailbox are read)
|
|
13
|
+
* @param query - Search query object
|
|
14
|
+
* @returns Array of IMAP search attributes
|
|
15
|
+
* @throws {Error} When required server extensions are not available
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // Simple search for unseen messages from a sender
|
|
19
|
+
* searchCompiler(connection, {
|
|
20
|
+
* unseen: true,
|
|
21
|
+
* from: 'sender@example.com'
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // Complex OR search with date range
|
|
26
|
+
* searchCompiler(connection, {
|
|
27
|
+
* or: [
|
|
28
|
+
* { from: 'alice@example.com' },
|
|
29
|
+
* { from: 'bob@example.com' }
|
|
30
|
+
* ],
|
|
31
|
+
* since: new Date('2024-01-01')
|
|
32
|
+
* });
|
|
33
|
+
*/
|
|
34
|
+
export declare const searchCompiler: (connection: ImapFlow, query: SearchObject) => SearchAttribute[];
|