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
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Transform, type TransformCallback } from 'node:stream';
|
|
2
|
+
export declare class JPDecoder extends Transform {
|
|
3
|
+
charset: string;
|
|
4
|
+
chunks: Buffer[];
|
|
5
|
+
chunklen: number;
|
|
6
|
+
maxBytes: number;
|
|
7
|
+
limited: boolean;
|
|
8
|
+
constructor(charset: string, maxBytes?: number | undefined);
|
|
9
|
+
_transform(chunk: Buffer | string, encoding: BufferEncoding, done: TransformCallback): void;
|
|
10
|
+
_flush(done: TransformCallback): void;
|
|
11
|
+
_destroy(err: Error | null, callback: (error: Error | null) => void): void;
|
|
12
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.JPDecoder = void 0;
|
|
7
|
+
const node_stream_1 = require("node:stream");
|
|
8
|
+
const encoding_japanese_1 = __importDefault(require("encoding-japanese"));
|
|
9
|
+
const limited_passthrough_js_1 = require("./limited-passthrough.js");
|
|
10
|
+
// A Transform stream for decoding Japanese character sets (Shift_JIS, EUC-JP, ISO-2022-JP).
|
|
11
|
+
// Unlike iconv-lite which can decode incrementally, encoding-japanese requires the complete
|
|
12
|
+
// input buffer for accurate charset detection and stateful decoding (especially ISO-2022-JP
|
|
13
|
+
// which uses escape sequences to switch between ASCII and multi-byte modes). Therefore,
|
|
14
|
+
// this stream buffers all input during _transform and performs the actual decoding in _flush.
|
|
15
|
+
class JPDecoder extends node_stream_1.Transform {
|
|
16
|
+
constructor(charset, maxBytes) {
|
|
17
|
+
super();
|
|
18
|
+
this.charset = charset;
|
|
19
|
+
this.chunks = [];
|
|
20
|
+
this.chunklen = 0;
|
|
21
|
+
// Upper bound for the buffered bytes, normalized the same way LimitedPassthrough
|
|
22
|
+
// normalizes its own. The whole-input buffering defeats a downstream maxBytes limiter
|
|
23
|
+
// (nothing is emitted until _flush), so without an internal bound a server could force
|
|
24
|
+
// unbounded memory use through a caller that asked for a limited download. Excess input
|
|
25
|
+
// is truncated, mirroring the truncation a maxBytes download applies anyway.
|
|
26
|
+
this.maxBytes = (0, limited_passthrough_js_1.normalizeByteLimit)(maxBytes);
|
|
27
|
+
// Also mirroring LimitedPassthrough: true once the bound is reached and every further
|
|
28
|
+
// chunk is being discarded. The download loop reads this to stop pulling from the
|
|
29
|
+
// server, which the limiter at the tail of the pipeline cannot tell it, because nothing
|
|
30
|
+
// is emitted from here until _flush().
|
|
31
|
+
this.limited = false;
|
|
32
|
+
}
|
|
33
|
+
// Buffer all incoming chunks (up to maxBytes); no decoding happens here because
|
|
34
|
+
// Japanese charsets require the complete input for accurate conversion.
|
|
35
|
+
_transform(chunk, encoding, done) {
|
|
36
|
+
if (typeof chunk === 'string') {
|
|
37
|
+
chunk = Buffer.from(chunk, encoding);
|
|
38
|
+
}
|
|
39
|
+
if (this.chunklen + chunk.length > this.maxBytes) {
|
|
40
|
+
chunk = chunk.slice(0, Math.max(0, this.maxBytes - this.chunklen));
|
|
41
|
+
}
|
|
42
|
+
if (chunk.length) {
|
|
43
|
+
this.chunks.push(chunk);
|
|
44
|
+
this.chunklen += chunk.length;
|
|
45
|
+
}
|
|
46
|
+
if (this.chunklen >= this.maxBytes) {
|
|
47
|
+
this.limited = true;
|
|
48
|
+
}
|
|
49
|
+
done();
|
|
50
|
+
}
|
|
51
|
+
// Perform the actual charset conversion once all input has been received.
|
|
52
|
+
// Uses the encoding-japanese library to convert from the source charset to Unicode.
|
|
53
|
+
// On failure (corrupt or unrecognizable data), passes through the raw bytes unchanged.
|
|
54
|
+
_flush(done) {
|
|
55
|
+
let input = Buffer.concat(this.chunks, this.chunklen);
|
|
56
|
+
try {
|
|
57
|
+
let output = encoding_japanese_1.default.convert(input, {
|
|
58
|
+
to: 'UNICODE', // to_encoding
|
|
59
|
+
from: this.charset, // from_encoding
|
|
60
|
+
type: 'string'
|
|
61
|
+
});
|
|
62
|
+
if (typeof output === 'string') {
|
|
63
|
+
output = Buffer.from(output);
|
|
64
|
+
}
|
|
65
|
+
this.push(output);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// keep as is on errors
|
|
69
|
+
this.push(input);
|
|
70
|
+
}
|
|
71
|
+
done();
|
|
72
|
+
}
|
|
73
|
+
_destroy(err, callback) {
|
|
74
|
+
this.chunks = [];
|
|
75
|
+
this.chunklen = 0;
|
|
76
|
+
callback(err);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
exports.JPDecoder = JPDecoder;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Transform, type TransformCallback } from 'node:stream';
|
|
2
|
+
/**
|
|
3
|
+
* Normalizes a byte budget for the download pipeline. Any finite positive number is honored and
|
|
4
|
+
* floored, because byte counts are integers: with a fractional bound a counter can only ever
|
|
5
|
+
* reach its floor, so a stage would never report itself full and a loop polling that flag would
|
|
6
|
+
* keep pulling forever. Anything else - 0, NaN, a non-numeric value - means "no limit".
|
|
7
|
+
*
|
|
8
|
+
* Lives here rather than in tools.ts because tools.ts imports jp-decoder.ts, which needs this.
|
|
9
|
+
*
|
|
10
|
+
* @param value - The configured budget.
|
|
11
|
+
* @returns The normalized budget, or Infinity when unbounded.
|
|
12
|
+
*/
|
|
13
|
+
export declare const normalizeByteLimit: (value: unknown) => number;
|
|
14
|
+
export interface LimitedPassthroughOptions {
|
|
15
|
+
/** Maximum number of bytes to pass through. Anything else means "no limit" */
|
|
16
|
+
maxBytes?: number | undefined;
|
|
17
|
+
}
|
|
18
|
+
export declare class LimitedPassthrough extends Transform {
|
|
19
|
+
options: LimitedPassthroughOptions;
|
|
20
|
+
maxBytes: number;
|
|
21
|
+
processed: number;
|
|
22
|
+
limited: boolean;
|
|
23
|
+
constructor(options?: LimitedPassthroughOptions | undefined);
|
|
24
|
+
_transform(chunk: Buffer, encoding: BufferEncoding, done: TransformCallback): void;
|
|
25
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LimitedPassthrough = exports.normalizeByteLimit = void 0;
|
|
4
|
+
const node_stream_1 = require("node:stream");
|
|
5
|
+
/**
|
|
6
|
+
* Normalizes a byte budget for the download pipeline. Any finite positive number is honored and
|
|
7
|
+
* floored, because byte counts are integers: with a fractional bound a counter can only ever
|
|
8
|
+
* reach its floor, so a stage would never report itself full and a loop polling that flag would
|
|
9
|
+
* keep pulling forever. Anything else - 0, NaN, a non-numeric value - means "no limit".
|
|
10
|
+
*
|
|
11
|
+
* Lives here rather than in tools.ts because tools.ts imports jp-decoder.ts, which needs this.
|
|
12
|
+
*
|
|
13
|
+
* @param value - The configured budget.
|
|
14
|
+
* @returns The normalized budget, or Infinity when unbounded.
|
|
15
|
+
*/
|
|
16
|
+
const normalizeByteLimit = (value) => {
|
|
17
|
+
let bytes = Number(value);
|
|
18
|
+
// Math.max keeps a sub-1 budget from flooring to 0, which would read back as "no limit"
|
|
19
|
+
return Number.isFinite(bytes) && bytes > 0 ? Math.max(Math.floor(bytes), 1) : Infinity;
|
|
20
|
+
};
|
|
21
|
+
exports.normalizeByteLimit = normalizeByteLimit;
|
|
22
|
+
// A Transform stream that passes through data up to a maximum byte limit,
|
|
23
|
+
// then silently discards all subsequent chunks. Used to enforce download
|
|
24
|
+
// size limits when fetching message content from the IMAP server.
|
|
25
|
+
class LimitedPassthrough extends node_stream_1.Transform {
|
|
26
|
+
constructor(options) {
|
|
27
|
+
super();
|
|
28
|
+
this.options = options || {};
|
|
29
|
+
this.maxBytes = (0, exports.normalizeByteLimit)(this.options.maxBytes);
|
|
30
|
+
this.processed = 0;
|
|
31
|
+
this.limited = false;
|
|
32
|
+
}
|
|
33
|
+
_transform(chunk, encoding, done) {
|
|
34
|
+
// If the limit was already reached, discard the chunk immediately
|
|
35
|
+
if (this.limited) {
|
|
36
|
+
return done();
|
|
37
|
+
}
|
|
38
|
+
const remainingBytes = this.maxBytes - this.processed;
|
|
39
|
+
if (remainingBytes < 1) {
|
|
40
|
+
return done();
|
|
41
|
+
}
|
|
42
|
+
// Slice the chunk to fit within the remaining byte budget
|
|
43
|
+
if (chunk.length > remainingBytes) {
|
|
44
|
+
chunk = chunk.subarray(0, remainingBytes);
|
|
45
|
+
}
|
|
46
|
+
this.processed += chunk.length;
|
|
47
|
+
if (this.processed >= this.maxBytes) {
|
|
48
|
+
this.limited = true;
|
|
49
|
+
}
|
|
50
|
+
this.push(chunk);
|
|
51
|
+
done();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.LimitedPassthrough = LimitedPassthrough;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const pino_1 = __importDefault(require("pino"));
|
|
7
|
+
const logger = (0, pino_1.default)();
|
|
8
|
+
logger.level = 'trace';
|
|
9
|
+
exports.default = logger;
|
|
10
|
+
module.exports = exports.default;
|
|
11
|
+
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Generated by scripts/build.js from package.json. Do not edit by hand.
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.homepage = exports.version = exports.name = void 0;
|
|
5
|
+
exports.name = 'imapflow';
|
|
6
|
+
exports.version = '2.0.0';
|
|
7
|
+
exports.homepage = 'https://imapflow.com/';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
import { ConnectionDeadline } from './connection-deadline.js';
|
|
3
|
+
import type { InternalLogger } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* A socket handed out by the proxy helpers, carrying the early error handler installed by
|
|
6
|
+
* attachEarlyErrorHandler() until the caller takes ownership
|
|
7
|
+
*/
|
|
8
|
+
export type ProxySocket = net.Socket & {
|
|
9
|
+
_earlyErrorHandler?: ((err: Error) => void) | null | undefined;
|
|
10
|
+
};
|
|
11
|
+
export interface ProxyConnectionOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Shared connection deadline. Proxy DNS and negotiation run inside it, so a stalled proxy
|
|
14
|
+
* cannot exceed the configured connectionTimeout.
|
|
15
|
+
*/
|
|
16
|
+
deadline?: ConnectionDeadline | undefined;
|
|
17
|
+
/** Used to build a deadline when none was passed */
|
|
18
|
+
connectionTimeout?: number | undefined;
|
|
19
|
+
}
|
|
20
|
+
declare const detachEarlyErrorHandler: (socket: ProxySocket | false | null | undefined) => void;
|
|
21
|
+
/**
|
|
22
|
+
* Opens a socket to `host`:`port` through the configured proxy.
|
|
23
|
+
*
|
|
24
|
+
* @param logger Logger instance.
|
|
25
|
+
* @param connectionUrl Proxy URL (http, https, socks, socks4, socks4a, socks5).
|
|
26
|
+
* @param host Destination host, passed through unresolved wherever the proxy protocol
|
|
27
|
+
* can resolve it itself.
|
|
28
|
+
* @param port Destination port.
|
|
29
|
+
* @param options Deadline options, see ProxyConnectionOptions.
|
|
30
|
+
* @returns The tunnelled socket, or undefined for an unknown protocol.
|
|
31
|
+
*/
|
|
32
|
+
declare const proxyConnection: (logger: InternalLogger, connectionUrl: string, host: string, port: number, options?: ProxyConnectionOptions | undefined) => Promise<ProxySocket | undefined>;
|
|
33
|
+
export { proxyConnection, detachEarlyErrorHandler };
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.detachEarlyErrorHandler = exports.proxyConnection = void 0;
|
|
7
|
+
const socks_1 = require("socks");
|
|
8
|
+
const node_dns_1 = __importDefault(require("node:dns"));
|
|
9
|
+
const node_net_1 = __importDefault(require("node:net"));
|
|
10
|
+
const node_tls_1 = __importDefault(require("node:tls"));
|
|
11
|
+
const connection_deadline_js_1 = require("./connection-deadline.js");
|
|
12
|
+
const tools_js_1 = require("./tools.js");
|
|
13
|
+
// Cap the CONNECT response buffered before the header terminator, so a proxy that never sends
|
|
14
|
+
// \r\n\r\n cannot grow memory without bound.
|
|
15
|
+
const MAX_RESPONSE_HEADER_BYTES = 64 * 1024;
|
|
16
|
+
const DEFAULT_SOCKS_PORT = 1080;
|
|
17
|
+
// URL hostnames keep the brackets around an IPv6 literal ("[2001:db8::1]"), which is neither a
|
|
18
|
+
// valid input for net.isIP() nor an address net/tls/socks can connect to. Strip them for socket
|
|
19
|
+
// options; the parsed URL itself stays intact for logging and credentials.
|
|
20
|
+
const unbracketAddress = (host) => (typeof host === 'string' && host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host);
|
|
21
|
+
// CONNECT request lines and Host headers need an IPv6 destination wrapped in brackets. Hostnames
|
|
22
|
+
// and IPv4 literals are used as-is, and an already bracketed literal is not bracketed twice.
|
|
23
|
+
const formatAuthority = (host, port) => {
|
|
24
|
+
let address = unbracketAddress(host);
|
|
25
|
+
return node_net_1.default.isIPv6(address) ? `[${address}]:${port}` : `${address}:${port}`;
|
|
26
|
+
};
|
|
27
|
+
// Password-free rendering of the proxy URL, used in every log path. The caller's URL object is
|
|
28
|
+
// left untouched so credentials stay available for authentication.
|
|
29
|
+
const redactUrl = (proxyUrl) => {
|
|
30
|
+
let redacted = new URL(proxyUrl.href);
|
|
31
|
+
if (redacted.password) {
|
|
32
|
+
redacted.password = '(hidden)';
|
|
33
|
+
}
|
|
34
|
+
return redacted.href;
|
|
35
|
+
};
|
|
36
|
+
const proxyError = (message, code) => {
|
|
37
|
+
let err = new Error(message);
|
|
38
|
+
err.code = code || 'ProxyError';
|
|
39
|
+
return err;
|
|
40
|
+
};
|
|
41
|
+
// URL userinfo is percent-encoded, so it has to be decoded before it can be used as credentials.
|
|
42
|
+
// A password containing a bare '%' is not valid percent-encoding and makes decodeURIComponent
|
|
43
|
+
// throw, so such values are used as they came in rather than failing the connection.
|
|
44
|
+
const decodeUserInfo = (value) => {
|
|
45
|
+
try {
|
|
46
|
+
return decodeURIComponent(value);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
// The socks client attaches its full options object - proxy password included - to the errors it
|
|
53
|
+
// throws, and Node's URL errors carry the rejected string in `input`. Any logger that serializes
|
|
54
|
+
// error properties would then write that password out in clear text, so the credentials are
|
|
55
|
+
// dropped before the error is logged or handed to the caller.
|
|
56
|
+
const stripProxyCredentials = (err) => {
|
|
57
|
+
if (err && typeof err === 'object') {
|
|
58
|
+
delete err.options;
|
|
59
|
+
delete err.input;
|
|
60
|
+
}
|
|
61
|
+
return err;
|
|
62
|
+
};
|
|
63
|
+
// Attaches a benign 'error' listener as soon as the proxied socket exists, so an early
|
|
64
|
+
// socket error (before ImapFlow installs its own handlers) cannot surface as an unhandled
|
|
65
|
+
// 'error' event and crash the process. The handler is stored on the socket so the caller
|
|
66
|
+
// can remove it once it takes ownership of the socket.
|
|
67
|
+
const attachEarlyErrorHandler = (logger, socket) => {
|
|
68
|
+
if (!socket || typeof socket.on !== 'function') {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
socket._earlyErrorHandler = (err) => {
|
|
72
|
+
logger.error({ msg: 'Proxy socket error before connection setup', err });
|
|
73
|
+
};
|
|
74
|
+
socket.on('error', socket._earlyErrorHandler);
|
|
75
|
+
};
|
|
76
|
+
// Removes the handler installed by attachEarlyErrorHandler once the caller takes ownership
|
|
77
|
+
// of the socket. Keeps the internal `_earlyErrorHandler` contract inside this module.
|
|
78
|
+
const detachEarlyErrorHandler = (socket) => {
|
|
79
|
+
if (socket && socket._earlyErrorHandler) {
|
|
80
|
+
socket.removeListener('error', socket._earlyErrorHandler);
|
|
81
|
+
socket._earlyErrorHandler = null;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
exports.detachEarlyErrorHandler = detachEarlyErrorHandler;
|
|
85
|
+
/**
|
|
86
|
+
* Establishes a tunnel through an HTTP or HTTPS proxy with a CONNECT request.
|
|
87
|
+
*
|
|
88
|
+
* ImapFlow owns this instead of using a bundled helper because the connection-wide deadline has
|
|
89
|
+
* to apply here: the socket is retained as soon as it exists, so an expiry destroys the in-flight
|
|
90
|
+
* socket immediately, and concurrent connections cannot share a single process-global timeout.
|
|
91
|
+
*
|
|
92
|
+
* The destination hostname is passed through unresolved - resolving it is the proxy's job, which
|
|
93
|
+
* is also what keeps DNS traffic off the client for HTTP proxies.
|
|
94
|
+
*
|
|
95
|
+
* @returns The established socket, tunnelled to the destination.
|
|
96
|
+
*/
|
|
97
|
+
const httpConnect = async ({ logger, proxyUrl, secureProxy, proxyHost, proxyPort, host, port, deadline }) => {
|
|
98
|
+
// Reject CRLF in the destination before it reaches the CONNECT request line and Host header.
|
|
99
|
+
// A tainted host/port could otherwise inject additional headers (HTTP request splitting).
|
|
100
|
+
let destinationPort = Number(port) || 0;
|
|
101
|
+
if (!destinationPort || /[\r\n]/.test(host)) {
|
|
102
|
+
throw proxyError('Invalid proxy destination', 'EPROXY');
|
|
103
|
+
}
|
|
104
|
+
let authority = formatAuthority(host, destinationPort);
|
|
105
|
+
let remaining = deadline.remaining();
|
|
106
|
+
if (!remaining) {
|
|
107
|
+
throw deadline.error();
|
|
108
|
+
}
|
|
109
|
+
let socket = null;
|
|
110
|
+
return await new Promise((resolve, reject) => {
|
|
111
|
+
let settled = false;
|
|
112
|
+
let timer = null;
|
|
113
|
+
let headers = '';
|
|
114
|
+
const onSocketData = (chunk) => {
|
|
115
|
+
// Scan only the newly arrived bytes (plus the 3 that a terminator could straddle),
|
|
116
|
+
// so a proxy that dribbles its headers cannot turn this into a quadratic rescan.
|
|
117
|
+
let searchFrom = Math.max(0, headers.length - 3);
|
|
118
|
+
headers += chunk.toString('binary');
|
|
119
|
+
let terminator = headers.indexOf('\r\n\r\n', searchFrom);
|
|
120
|
+
if (terminator < 0) {
|
|
121
|
+
if (headers.length > MAX_RESPONSE_HEADER_BYTES) {
|
|
122
|
+
fail(proxyError('Proxy response headers too large', 'EPROXY'));
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// The header block is complete, so this listener must stop consuming before anything
|
|
127
|
+
// is put back: unshifting while still subscribed re-emits the data straight back into
|
|
128
|
+
// this handler, which would swallow it. Pausing hands the socket over cleanly - the
|
|
129
|
+
// next owner resumes it (ImapFlow pipes it into the parser).
|
|
130
|
+
socket.removeListener('data', onSocketData);
|
|
131
|
+
socket.pause();
|
|
132
|
+
// Anything after the header terminator already belongs to the tunnelled stream (a
|
|
133
|
+
// server greeting that the proxy coalesced with its own response) and has to be
|
|
134
|
+
// preserved for the next consumer. It is put back as the original bytes, taken from
|
|
135
|
+
// this chunk rather than round-tripped through a string.
|
|
136
|
+
let headerBytes = terminator + 4;
|
|
137
|
+
let consumedFromChunk = chunk.length - (headers.length - headerBytes);
|
|
138
|
+
if (consumedFromChunk < chunk.length) {
|
|
139
|
+
socket.unshift(chunk.subarray(consumedFromChunk));
|
|
140
|
+
}
|
|
141
|
+
headers = headers.slice(0, terminator);
|
|
142
|
+
let status = headers.match(/^HTTP\/\d+\.\d+ (\d+)/i);
|
|
143
|
+
if (!status || (status[1] || '').charAt(0) !== '2') {
|
|
144
|
+
return fail(proxyError(`Invalid response from proxy${status ? `: ${status[1]}` : ''}`, 'EPROXY'));
|
|
145
|
+
}
|
|
146
|
+
succeed();
|
|
147
|
+
};
|
|
148
|
+
// Single settlement path: temporary listeners and the deadline timer are dropped exactly
|
|
149
|
+
// once, so a late socket event cannot settle the promise twice or leave a timer armed.
|
|
150
|
+
const cleanup = () => {
|
|
151
|
+
(0, tools_js_1.clearTimer)(timer);
|
|
152
|
+
timer = null;
|
|
153
|
+
if (socket) {
|
|
154
|
+
// Every temporary listener goes, the connect callback included: after settlement
|
|
155
|
+
// no socket event may run any of this again.
|
|
156
|
+
socket.removeListener('connect', onConnected);
|
|
157
|
+
socket.removeListener('data', onSocketData);
|
|
158
|
+
socket.removeListener('error', fail);
|
|
159
|
+
socket.removeListener('close', onEarlyClose);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
function fail(err) {
|
|
163
|
+
if (settled) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
settled = true;
|
|
167
|
+
cleanup();
|
|
168
|
+
if (socket) {
|
|
169
|
+
socket.destroy();
|
|
170
|
+
}
|
|
171
|
+
reject(err);
|
|
172
|
+
}
|
|
173
|
+
function succeed() {
|
|
174
|
+
settled = true;
|
|
175
|
+
cleanup();
|
|
176
|
+
resolve(socket);
|
|
177
|
+
}
|
|
178
|
+
function onEarlyClose() {
|
|
179
|
+
fail(proxyError('Proxy closed the connection before the tunnel was established', 'EPROXY'));
|
|
180
|
+
}
|
|
181
|
+
timer = setTimeout(() => fail(deadline.error()), remaining);
|
|
182
|
+
let connectOptions = { host: proxyHost, port: proxyPort };
|
|
183
|
+
if (secureProxy) {
|
|
184
|
+
// Verify the proxy's certificate (Node default) and target SNI plus hostname
|
|
185
|
+
// verification at the proxy endpoint rather than the IMAP destination. An IP-literal
|
|
186
|
+
// endpoint gets no servername, which would be an invalid SNI value.
|
|
187
|
+
if (!node_net_1.default.isIP(proxyHost)) {
|
|
188
|
+
connectOptions.servername = proxyHost;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Declared as a function so cleanup() above can detach it (the connect callback is
|
|
192
|
+
// registered as a one-shot 'connect' listener by net/tls).
|
|
193
|
+
function onConnected() {
|
|
194
|
+
let requestHeaders = {
|
|
195
|
+
Host: authority,
|
|
196
|
+
Connection: 'close'
|
|
197
|
+
};
|
|
198
|
+
if (proxyUrl.username || proxyUrl.password) {
|
|
199
|
+
let credentials = `${decodeUserInfo(proxyUrl.username)}:${decodeUserInfo(proxyUrl.password)}`;
|
|
200
|
+
requestHeaders['Proxy-Authorization'] = `Basic ${Buffer.from(credentials).toString('base64')}`;
|
|
201
|
+
}
|
|
202
|
+
socket.write(`CONNECT ${authority} HTTP/1.1\r\n` +
|
|
203
|
+
Object.keys(requestHeaders)
|
|
204
|
+
.map(key => `${key}: ${requestHeaders[key]}`)
|
|
205
|
+
.join('\r\n') +
|
|
206
|
+
'\r\n\r\n');
|
|
207
|
+
socket.on('data', onSocketData);
|
|
208
|
+
}
|
|
209
|
+
// The socket is retained as soon as it is created, so an expiry can destroy it at once.
|
|
210
|
+
socket = secureProxy ? node_tls_1.default.connect(connectOptions, onConnected) : node_net_1.default.connect(connectOptions, onConnected);
|
|
211
|
+
socket.once('error', fail);
|
|
212
|
+
socket.once('close', onEarlyClose);
|
|
213
|
+
})
|
|
214
|
+
.then(established => {
|
|
215
|
+
logger.info({
|
|
216
|
+
msg: `Established a socket via HTTP proxy`,
|
|
217
|
+
proxyUrl: redactUrl(proxyUrl),
|
|
218
|
+
port,
|
|
219
|
+
host
|
|
220
|
+
});
|
|
221
|
+
attachEarlyErrorHandler(logger, established);
|
|
222
|
+
return established;
|
|
223
|
+
})
|
|
224
|
+
.catch(err => {
|
|
225
|
+
logger.error({
|
|
226
|
+
msg: 'Failed to establish a socket via HTTP proxy',
|
|
227
|
+
proxyUrl: redactUrl(proxyUrl),
|
|
228
|
+
port,
|
|
229
|
+
host,
|
|
230
|
+
err
|
|
231
|
+
});
|
|
232
|
+
throw err;
|
|
233
|
+
});
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* Resolves a destination hostname to an IPv4 address. Only used for SOCKS4, which has no IPv6
|
|
237
|
+
* destination address type and no hostname form of its own.
|
|
238
|
+
*
|
|
239
|
+
* @param hostname Destination hostname.
|
|
240
|
+
* @param deadline Shared connection deadline.
|
|
241
|
+
* @returns An IPv4 address.
|
|
242
|
+
*/
|
|
243
|
+
const resolveIPv4 = async (hostname, deadline) => {
|
|
244
|
+
let addresses = await deadline.race(node_dns_1.default.promises.resolve4(hostname));
|
|
245
|
+
if (!addresses || !addresses.length) {
|
|
246
|
+
throw proxyError(`Could not resolve an IPv4 address for ${hostname}`, 'EPROXY');
|
|
247
|
+
}
|
|
248
|
+
return addresses[0];
|
|
249
|
+
};
|
|
250
|
+
/**
|
|
251
|
+
* Establishes a tunnel through a SOCKS proxy.
|
|
252
|
+
*
|
|
253
|
+
* DNS policy per protocol, because the `socks` client picks the SOCKS4 or SOCKS4a wire format from
|
|
254
|
+
* the destination value alone and offers no switch of its own:
|
|
255
|
+
* * SOCKS4 - destination hostnames are resolved locally to IPv4. Passing a hostname would
|
|
256
|
+
* silently produce a SOCKS4a request that a plain SOCKS4 proxy cannot answer.
|
|
257
|
+
* * SOCKS4a - destination hostnames are preserved for remote DNS.
|
|
258
|
+
* * SOCKS5 - destination hostnames are preserved for remote DNS, IP literals pass through.
|
|
259
|
+
* IPv6 destination literals are rejected for both SOCKS4 and SOCKS4a: neither can carry them, and
|
|
260
|
+
* the dependency would write the literal into the SOCKS4a hostname field instead.
|
|
261
|
+
*
|
|
262
|
+
* @returns The established socket, tunnelled to the destination.
|
|
263
|
+
*/
|
|
264
|
+
const socksConnect = async ({ logger, proxyUrl, protocol, proxyHost, proxyPort, host, port, deadline }) => {
|
|
265
|
+
let proxyType = protocol === 'socks4' || protocol === 'socks4a' ? 4 : 5;
|
|
266
|
+
let destinationHost = unbracketAddress(host);
|
|
267
|
+
try {
|
|
268
|
+
if (proxyType === 4) {
|
|
269
|
+
if (node_net_1.default.isIPv6(destinationHost)) {
|
|
270
|
+
throw proxyError(`SOCKS4 and SOCKS4a cannot address IPv6 destinations (${destinationHost})`, 'UnsupportedProxyAddress');
|
|
271
|
+
}
|
|
272
|
+
if (protocol === 'socks4' && !node_net_1.default.isIP(destinationHost)) {
|
|
273
|
+
destinationHost = await resolveIPv4(destinationHost, deadline);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
let connectionOpts = {
|
|
277
|
+
proxy: {
|
|
278
|
+
// The endpoint is handed to net.Socket.connect() by the dependency, so a hostname
|
|
279
|
+
// is left unresolved and gets Node's normal lookup and connection behavior.
|
|
280
|
+
host: proxyHost,
|
|
281
|
+
port: proxyPort,
|
|
282
|
+
type: proxyType
|
|
283
|
+
},
|
|
284
|
+
destination: {
|
|
285
|
+
host: destinationHost,
|
|
286
|
+
port
|
|
287
|
+
},
|
|
288
|
+
command: 'connect',
|
|
289
|
+
set_tcp_nodelay: true
|
|
290
|
+
};
|
|
291
|
+
if (proxyUrl.username || proxyUrl.password) {
|
|
292
|
+
connectionOpts.proxy.userId = proxyUrl.username;
|
|
293
|
+
connectionOpts.proxy.password = proxyUrl.password;
|
|
294
|
+
}
|
|
295
|
+
// The dependency treats a zero timeout as its own 30 second default, so only a strictly
|
|
296
|
+
// positive remaining budget may be passed.
|
|
297
|
+
let remaining = deadline.remaining();
|
|
298
|
+
if (!remaining) {
|
|
299
|
+
throw deadline.error();
|
|
300
|
+
}
|
|
301
|
+
connectionOpts.timeout = remaining;
|
|
302
|
+
const info = await deadline.race(socks_1.SocksClient.createConnection(connectionOpts));
|
|
303
|
+
if (!info || !info.socket) {
|
|
304
|
+
throw proxyError('SOCKS proxy did not return a socket', 'EPROXY');
|
|
305
|
+
}
|
|
306
|
+
logger.info({
|
|
307
|
+
msg: 'Established a socket via SOCKS proxy',
|
|
308
|
+
proxyUrl: redactUrl(proxyUrl),
|
|
309
|
+
port,
|
|
310
|
+
host
|
|
311
|
+
});
|
|
312
|
+
attachEarlyErrorHandler(logger, info.socket);
|
|
313
|
+
return info.socket;
|
|
314
|
+
}
|
|
315
|
+
catch (caught) {
|
|
316
|
+
// A dependency expiry and the shared deadline are reported with the same
|
|
317
|
+
// CONNECT_TIMEOUT shape, so a caller does not need to know which noticed first.
|
|
318
|
+
let err = deadline.normalize(stripProxyCredentials(caught));
|
|
319
|
+
stripProxyCredentials(err._err);
|
|
320
|
+
logger.error({
|
|
321
|
+
msg: 'Failed to establish a socket via SOCKS proxy',
|
|
322
|
+
proxyUrl: redactUrl(proxyUrl),
|
|
323
|
+
port,
|
|
324
|
+
host,
|
|
325
|
+
err
|
|
326
|
+
});
|
|
327
|
+
throw err;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
/**
|
|
331
|
+
* Opens a socket to `host`:`port` through the configured proxy.
|
|
332
|
+
*
|
|
333
|
+
* @param logger Logger instance.
|
|
334
|
+
* @param connectionUrl Proxy URL (http, https, socks, socks4, socks4a, socks5).
|
|
335
|
+
* @param host Destination host, passed through unresolved wherever the proxy protocol
|
|
336
|
+
* can resolve it itself.
|
|
337
|
+
* @param port Destination port.
|
|
338
|
+
* @param options Deadline options, see ProxyConnectionOptions.
|
|
339
|
+
* @returns The tunnelled socket, or undefined for an unknown protocol.
|
|
340
|
+
*/
|
|
341
|
+
const proxyConnection = async (logger, connectionUrl, host, port, options) => {
|
|
342
|
+
options = options || {};
|
|
343
|
+
let deadline = options.deadline || new connection_deadline_js_1.ConnectionDeadline(options.connectionTimeout);
|
|
344
|
+
deadline.check();
|
|
345
|
+
let proxyUrl;
|
|
346
|
+
try {
|
|
347
|
+
proxyUrl = new URL(connectionUrl);
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
// new URL() attaches the string it rejected to err.input, which here is the full proxy
|
|
351
|
+
// endpoint including its password. Any logger that serializes error properties would
|
|
352
|
+
// write that out in clear text, so the cause is reported without carrying the value.
|
|
353
|
+
throw proxyError('Invalid proxy URL', err.code || 'ERR_INVALID_URL');
|
|
354
|
+
}
|
|
355
|
+
let protocol = proxyUrl.protocol.replace(/:$/, '').toLowerCase();
|
|
356
|
+
// ImapFlow performs no DNS lookup of its own for the proxy endpoint: net, tls and the SOCKS
|
|
357
|
+
// client all resolve a hostname endpoint themselves, which keeps Node's normal connection
|
|
358
|
+
// behavior (including address-family selection) instead of pinning one address.
|
|
359
|
+
let proxyHost = unbracketAddress(proxyUrl.hostname);
|
|
360
|
+
switch (protocol) {
|
|
361
|
+
// Connect using a HTTP CONNECT method
|
|
362
|
+
case 'http':
|
|
363
|
+
case 'https':
|
|
364
|
+
return await httpConnect({
|
|
365
|
+
logger,
|
|
366
|
+
proxyUrl,
|
|
367
|
+
secureProxy: protocol === 'https',
|
|
368
|
+
proxyHost,
|
|
369
|
+
proxyPort: Number(proxyUrl.port) || (protocol === 'https' ? 443 : 80),
|
|
370
|
+
host,
|
|
371
|
+
port,
|
|
372
|
+
deadline
|
|
373
|
+
});
|
|
374
|
+
// SOCKS proxy
|
|
375
|
+
case 'socks':
|
|
376
|
+
case 'socks5':
|
|
377
|
+
case 'socks4':
|
|
378
|
+
case 'socks4a':
|
|
379
|
+
return await socksConnect({
|
|
380
|
+
logger,
|
|
381
|
+
proxyUrl,
|
|
382
|
+
protocol,
|
|
383
|
+
proxyHost,
|
|
384
|
+
proxyPort: Number(proxyUrl.port) || DEFAULT_SOCKS_PORT,
|
|
385
|
+
host,
|
|
386
|
+
port,
|
|
387
|
+
deadline
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
return undefined;
|
|
391
|
+
};
|
|
392
|
+
exports.proxyConnection = proxyConnection;
|
|
@@ -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[];
|