imapflow 1.7.8 → 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 +20 -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 +761 -1789
- 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 -101
- 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 -909
- 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 -661
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { Transform, type TransformCallback } from 'node:stream';
|
|
2
|
+
import type { Logger, InternalLogger } from '../types.js';
|
|
3
|
+
export interface ImapStreamOptions {
|
|
4
|
+
/** Connection identifier used for logging */
|
|
5
|
+
cid?: string | undefined;
|
|
6
|
+
/** A pino-compatible logger instance. If not provided, a default child logger is created */
|
|
7
|
+
logger?: Logger | InternalLogger | false | undefined;
|
|
8
|
+
/** If true, logs raw socket data at trace level */
|
|
9
|
+
logRaw?: boolean | undefined;
|
|
10
|
+
/** Whether the connection uses TLS */
|
|
11
|
+
secureConnection?: boolean | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Maximum allowed length (in bytes) of a single line (a response without a literal). Defaults
|
|
14
|
+
* to MAX_LITERAL_SIZE (1GB). Guards against a malicious or broken server that never sends a
|
|
15
|
+
* line terminator, which would otherwise grow the internal line buffer without bound. The line
|
|
16
|
+
* terminator counts toward the limit, and a line exactly at the limit is accepted. Exceeding it
|
|
17
|
+
* is terminal: the stream is destroyed with a `LineTooLarge` error and no further input is parsed.
|
|
18
|
+
*/
|
|
19
|
+
maxLineLength?: number | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Maximum allowed size (in bytes) of a single literal block. Defaults to MAX_LITERAL_SIZE
|
|
22
|
+
* (1GB). Lower it to bound peak memory allocation against a malicious or broken server
|
|
23
|
+
* announcing an oversized literal. A literal exactly at the limit is accepted. Exceeding it is
|
|
24
|
+
* terminal: the stream is destroyed with a `LiteralTooLarge` error, the marker line is not
|
|
25
|
+
* emitted, and no byte of the rejected literal body is parsed as protocol.
|
|
26
|
+
*/
|
|
27
|
+
maxLiteralSize?: number | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Maximum allowed total size (in bytes) of a single assembled response: every line segment and
|
|
30
|
+
* literal of one response combined. Defaults to MAX_RESPONSE_SIZE (2GB), which leaves room
|
|
31
|
+
* above the literal cap for a maximum-size literal plus its marker line. The per-line and
|
|
32
|
+
* per-literal caps alone cannot stop a server that spreads attacker-controlled bytes across an
|
|
33
|
+
* unbounded number of tokens of a single response. Declared literal sizes count when their
|
|
34
|
+
* marker is parsed, so an oversized total is rejected before the literal bytes arrive, and a
|
|
35
|
+
* line still being assembled counts against whatever budget is left. Exceeding the limit is
|
|
36
|
+
* terminal: the stream is destroyed with a `ResponseTooLarge` error and no further input is
|
|
37
|
+
* parsed.
|
|
38
|
+
*/
|
|
39
|
+
maxResponseSize?: number | undefined;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A queued input chunk with the transform callback that releases it
|
|
43
|
+
*/
|
|
44
|
+
export interface ImapStreamInputItem {
|
|
45
|
+
chunk: Buffer;
|
|
46
|
+
next: () => void;
|
|
47
|
+
released?: boolean | undefined;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A Transform stream that parses raw IMAP protocol data from a socket into structured
|
|
51
|
+
* command/response objects. Reads binary input, splits it into lines delimited by LF,
|
|
52
|
+
* extracts literal data blocks based on IMAP literal size markers (e.g., "{123}\r\n"),
|
|
53
|
+
* and emits each complete command as a readable object containing the payload Buffer
|
|
54
|
+
* and any associated literal Buffers. Enforces a maximum literal size of 1GB.
|
|
55
|
+
*/
|
|
56
|
+
export declare class ImapStream extends Transform {
|
|
57
|
+
options: ImapStreamOptions;
|
|
58
|
+
cid: string | undefined;
|
|
59
|
+
log: InternalLogger;
|
|
60
|
+
readBytesCounter: number;
|
|
61
|
+
maxLineLength: number;
|
|
62
|
+
maxLiteralSize: number;
|
|
63
|
+
maxResponseSize: number;
|
|
64
|
+
state: number;
|
|
65
|
+
literalWaiting: number;
|
|
66
|
+
inputBuffer: Buffer[];
|
|
67
|
+
lineBuffer: Buffer[];
|
|
68
|
+
lineBytes: number;
|
|
69
|
+
literalBuffer: Buffer[];
|
|
70
|
+
literals: Buffer[];
|
|
71
|
+
responseBytes: number;
|
|
72
|
+
compress: boolean;
|
|
73
|
+
secureConnection: boolean | undefined;
|
|
74
|
+
processingInput: boolean;
|
|
75
|
+
inputQueue: ImapStreamInputItem[];
|
|
76
|
+
activeInput: ImapStreamInputItem | null;
|
|
77
|
+
pendingPush: (() => void) | null;
|
|
78
|
+
/**
|
|
79
|
+
* Creates a new ImapStream instance.
|
|
80
|
+
*
|
|
81
|
+
* @param options - Stream options, see ImapStreamOptions.
|
|
82
|
+
*/
|
|
83
|
+
constructor(options?: ImapStreamOptions | undefined);
|
|
84
|
+
/**
|
|
85
|
+
* Terminally fails the stream. Used for response limit violations and for any other
|
|
86
|
+
* error raised while parsing.
|
|
87
|
+
*
|
|
88
|
+
* The stream is destroyed instead of only emitting `error`: emitting on a Transform leaves
|
|
89
|
+
* it running, so the caller would keep scanning the rejected payload and could emit it as
|
|
90
|
+
* protocol (an oversized literal body contains attacker-chosen CRLF delimited lines).
|
|
91
|
+
* Destroying stops all parsing, drops the offending line, and releases every queued
|
|
92
|
+
* transform callback exactly once (see `_destroy()`).
|
|
93
|
+
*
|
|
94
|
+
* `destroyed` (set synchronously by destroy()) is the single liveness flag every other path
|
|
95
|
+
* checks, so a second failure attempt is a no-op and nothing is parsed after the first.
|
|
96
|
+
*
|
|
97
|
+
* @param err - The error to destroy the stream with.
|
|
98
|
+
* @returns Always false, so callers can `return this.failStream(err)`.
|
|
99
|
+
*/
|
|
100
|
+
failStream(err: Error): false;
|
|
101
|
+
/**
|
|
102
|
+
* Releases a queued input chunk's transform callback exactly once, signalling the writable
|
|
103
|
+
* side that the chunk was consumed. The mirror image of ImapFlow's releaseStreamData(), which
|
|
104
|
+
* releases the readable items this stream pushes downstream.
|
|
105
|
+
*
|
|
106
|
+
* @param item - Queue entry holding the chunk and its transform callback.
|
|
107
|
+
*/
|
|
108
|
+
releaseInput(item: ImapStreamInputItem | null | undefined): void;
|
|
109
|
+
/**
|
|
110
|
+
* Checks whether the given line buffer ends with an IMAP literal size marker
|
|
111
|
+
* (e.g., "{123}\r\n"). If a valid marker is found and the literal size is within
|
|
112
|
+
* the allowed maximum, switches the stream state to LITERAL mode and records
|
|
113
|
+
* the expected number of literal bytes.
|
|
114
|
+
*
|
|
115
|
+
* @param line - The line buffer to check for a trailing literal marker.
|
|
116
|
+
* @returns True if a valid literal marker was found and literal state was activated, false otherwise.
|
|
117
|
+
*/
|
|
118
|
+
checkLiteralMarker(line: Buffer): boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Enforces the configured line-length cap for a projected line length. The projected length
|
|
121
|
+
* covers every byte of the line, the line terminator included, whether or not the line was
|
|
122
|
+
* split across input chunks. A line exactly at the limit is accepted.
|
|
123
|
+
*
|
|
124
|
+
* @param lineLength - Total length the current line would reach.
|
|
125
|
+
* @returns True if the line is within the limit, false if the stream was failed.
|
|
126
|
+
*/
|
|
127
|
+
checkLineLength(lineLength: number): boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Enforces the configured per-response size cap: the cumulative bytes of every line
|
|
130
|
+
* segment and declared literal of the response currently being assembled. Counting
|
|
131
|
+
* declared literal sizes at marker time means an oversized total is rejected before
|
|
132
|
+
* the literal bytes even arrive. The counter is reset when a response is emitted.
|
|
133
|
+
*
|
|
134
|
+
* @param additionalBytes - Bytes the next token would add to the response.
|
|
135
|
+
* @param peek - Measure only, without committing the bytes to the counter.
|
|
136
|
+
* Used for a line that is still being assembled: its bytes are committed once, when the
|
|
137
|
+
* line completes.
|
|
138
|
+
* @returns True if within the limit, false if the stream was failed.
|
|
139
|
+
*/
|
|
140
|
+
checkResponseSize(additionalBytes: number, peek?: boolean | undefined): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Processes a single input chunk of raw data. In LINE state, scans for LF-terminated
|
|
143
|
+
* lines and checks for literal markers. In LITERAL state, collects the expected number
|
|
144
|
+
* of literal bytes. When a complete command (with all its literals) is assembled, it is
|
|
145
|
+
* pushed downstream as a readable object.
|
|
146
|
+
*
|
|
147
|
+
* @param chunk - The raw data chunk to process.
|
|
148
|
+
* @param startPos - The byte offset within the chunk to start processing from.
|
|
149
|
+
*/
|
|
150
|
+
processInputChunk(chunk: Buffer, startPos?: number | undefined): Promise<void>;
|
|
151
|
+
/**
|
|
152
|
+
* Drains the input queue by processing each queued chunk sequentially.
|
|
153
|
+
* Yields to the event loop every 10 chunks to prevent CPU blocking on
|
|
154
|
+
* large bursts of incoming data.
|
|
155
|
+
*/
|
|
156
|
+
processInput(): Promise<void>;
|
|
157
|
+
/**
|
|
158
|
+
* Transform stream implementation. Receives raw data chunks from the writable side,
|
|
159
|
+
* converts strings to Buffers, tracks total bytes read, optionally logs raw data,
|
|
160
|
+
* and queues the chunk for asynchronous processing.
|
|
161
|
+
*
|
|
162
|
+
* @param chunk - The incoming data chunk.
|
|
163
|
+
* @param encoding - The encoding if chunk is a string.
|
|
164
|
+
* @param next - Callback to signal that this chunk has been consumed.
|
|
165
|
+
*/
|
|
166
|
+
_transform(chunk: Buffer | string, encoding: BufferEncoding, next: TransformCallback): void;
|
|
167
|
+
/**
|
|
168
|
+
* Flush implementation called when the writable side ends. Signals completion immediately.
|
|
169
|
+
*
|
|
170
|
+
* @param next - Callback to signal flush completion.
|
|
171
|
+
*/
|
|
172
|
+
_flush(next: TransformCallback): void;
|
|
173
|
+
/**
|
|
174
|
+
* Destroy implementation for cleanup. Clears all internal buffers, drains the input queue
|
|
175
|
+
* by invoking pending callbacks, and forwards the error (if any) to the callback.
|
|
176
|
+
*
|
|
177
|
+
* @param err - The error that caused destruction, or null.
|
|
178
|
+
* @param callback - Callback to signal destruction completion.
|
|
179
|
+
*/
|
|
180
|
+
_destroy(err: Error | null, callback: (error?: Error | null) => void): void;
|
|
181
|
+
}
|
|
@@ -0,0 +1,446 @@
|
|
|
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.ImapStream = void 0;
|
|
7
|
+
const node_stream_1 = require("node:stream");
|
|
8
|
+
const logger_js_1 = __importDefault(require("../logger.js"));
|
|
9
|
+
const limits_js_1 = require("./limits.js");
|
|
10
|
+
const LINE = 0x01;
|
|
11
|
+
const LITERAL = 0x02;
|
|
12
|
+
const LF = 0x0a;
|
|
13
|
+
const CR = 0x0d;
|
|
14
|
+
const NUM_0 = 0x30;
|
|
15
|
+
const NUM_9 = 0x39;
|
|
16
|
+
const CURLY_OPEN = 0x7b;
|
|
17
|
+
const CURLY_CLOSE = 0x7d;
|
|
18
|
+
/**
|
|
19
|
+
* A Transform stream that parses raw IMAP protocol data from a socket into structured
|
|
20
|
+
* command/response objects. Reads binary input, splits it into lines delimited by LF,
|
|
21
|
+
* extracts literal data blocks based on IMAP literal size markers (e.g., "{123}\r\n"),
|
|
22
|
+
* and emits each complete command as a readable object containing the payload Buffer
|
|
23
|
+
* and any associated literal Buffers. Enforces a maximum literal size of 1GB.
|
|
24
|
+
*/
|
|
25
|
+
class ImapStream extends node_stream_1.Transform {
|
|
26
|
+
/**
|
|
27
|
+
* Creates a new ImapStream instance.
|
|
28
|
+
*
|
|
29
|
+
* @param options - Stream options, see ImapStreamOptions.
|
|
30
|
+
*/
|
|
31
|
+
constructor(options) {
|
|
32
|
+
super({
|
|
33
|
+
//writableHighWaterMark: 3,
|
|
34
|
+
readableObjectMode: true,
|
|
35
|
+
writableObjectMode: false
|
|
36
|
+
});
|
|
37
|
+
this.options = options || {};
|
|
38
|
+
this.cid = this.options.cid;
|
|
39
|
+
this.log =
|
|
40
|
+
this.options.logger && typeof this.options.logger === 'object'
|
|
41
|
+
? this.options.logger
|
|
42
|
+
: logger_js_1.default.child({
|
|
43
|
+
component: 'imap-connection',
|
|
44
|
+
cid: this.cid
|
|
45
|
+
});
|
|
46
|
+
this.readBytesCounter = 0;
|
|
47
|
+
// Maximum length of a single line (response without a literal). Bounds the line buffer
|
|
48
|
+
// so a server that never sends a line terminator cannot exhaust memory.
|
|
49
|
+
this.maxLineLength = (0, limits_js_1.normalizeLimit)(this.options.maxLineLength, limits_js_1.MAX_LINE_SIZE);
|
|
50
|
+
// Maximum size of a single literal block. Bounds peak memory allocation so a server
|
|
51
|
+
// announcing an oversized literal cannot exhaust memory.
|
|
52
|
+
this.maxLiteralSize = (0, limits_js_1.normalizeLimit)(this.options.maxLiteralSize, limits_js_1.MAX_LITERAL_SIZE);
|
|
53
|
+
this.maxResponseSize = (0, limits_js_1.normalizeLimit)(this.options.maxResponseSize, limits_js_1.MAX_RESPONSE_SIZE);
|
|
54
|
+
this.state = LINE;
|
|
55
|
+
this.literalWaiting = 0;
|
|
56
|
+
this.inputBuffer = []; // lines
|
|
57
|
+
this.lineBuffer = []; // current line
|
|
58
|
+
this.lineBytes = 0; // bytes currently buffered for the in-progress line
|
|
59
|
+
this.literalBuffer = [];
|
|
60
|
+
this.literals = [];
|
|
61
|
+
this.responseBytes = 0; // bytes accumulated for the in-progress response (lines + declared literals)
|
|
62
|
+
this.compress = false;
|
|
63
|
+
this.secureConnection = this.options.secureConnection;
|
|
64
|
+
this.processingInput = false;
|
|
65
|
+
this.inputQueue = []; // unprocessed input chunks
|
|
66
|
+
this.activeInput = null; // chunk currently being processed (already shifted off inputQueue)
|
|
67
|
+
// Resolver of the in-flight push() backpressure promise, so destruction can settle it
|
|
68
|
+
// instead of leaving processInput() awaiting a consumer that will never read again.
|
|
69
|
+
this.pendingPush = null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Terminally fails the stream. Used for response limit violations and for any other
|
|
73
|
+
* error raised while parsing.
|
|
74
|
+
*
|
|
75
|
+
* The stream is destroyed instead of only emitting `error`: emitting on a Transform leaves
|
|
76
|
+
* it running, so the caller would keep scanning the rejected payload and could emit it as
|
|
77
|
+
* protocol (an oversized literal body contains attacker-chosen CRLF delimited lines).
|
|
78
|
+
* Destroying stops all parsing, drops the offending line, and releases every queued
|
|
79
|
+
* transform callback exactly once (see `_destroy()`).
|
|
80
|
+
*
|
|
81
|
+
* `destroyed` (set synchronously by destroy()) is the single liveness flag every other path
|
|
82
|
+
* checks, so a second failure attempt is a no-op and nothing is parsed after the first.
|
|
83
|
+
*
|
|
84
|
+
* @param err - The error to destroy the stream with.
|
|
85
|
+
* @returns Always false, so callers can `return this.failStream(err)`.
|
|
86
|
+
*/
|
|
87
|
+
failStream(err) {
|
|
88
|
+
if (this.destroyed) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
this.destroy(err);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Releases a queued input chunk's transform callback exactly once, signalling the writable
|
|
96
|
+
* side that the chunk was consumed. The mirror image of ImapFlow's releaseStreamData(), which
|
|
97
|
+
* releases the readable items this stream pushes downstream.
|
|
98
|
+
*
|
|
99
|
+
* @param item - Queue entry holding the chunk and its transform callback.
|
|
100
|
+
*/
|
|
101
|
+
releaseInput(item) {
|
|
102
|
+
if (!item || item.released) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
item.released = true;
|
|
106
|
+
if (typeof item.next === 'function') {
|
|
107
|
+
item.next();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Checks whether the given line buffer ends with an IMAP literal size marker
|
|
112
|
+
* (e.g., "{123}\r\n"). If a valid marker is found and the literal size is within
|
|
113
|
+
* the allowed maximum, switches the stream state to LITERAL mode and records
|
|
114
|
+
* the expected number of literal bytes.
|
|
115
|
+
*
|
|
116
|
+
* @param line - The line buffer to check for a trailing literal marker.
|
|
117
|
+
* @returns True if a valid literal marker was found and literal state was activated, false otherwise.
|
|
118
|
+
*/
|
|
119
|
+
checkLiteralMarker(line) {
|
|
120
|
+
if (!line || !line.length) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
let pos = line.length - 1;
|
|
124
|
+
if (line[pos] !== LF) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
pos--;
|
|
128
|
+
if (pos >= 0 && line[pos] === CR) {
|
|
129
|
+
pos--;
|
|
130
|
+
}
|
|
131
|
+
if (pos < 0 || !pos || line[pos] !== CURLY_CLOSE) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
pos--;
|
|
135
|
+
// Scan backwards through the line to find an IMAP literal marker: {size}\r\n
|
|
136
|
+
// The format is: '{' followed by one or more ASCII digits followed by '}'.
|
|
137
|
+
// Only the digit run's bounds are tracked - a single linear pass, unlike
|
|
138
|
+
// collecting digits into a growing array, which would make a line of n digits
|
|
139
|
+
// cost O(n^2). The run length is deliberately not capped: the RFC "number"
|
|
140
|
+
// production permits leading zeros, so a long digit run can still denote a
|
|
141
|
+
// small, valid size, and treating the marker as an ordinary line instead
|
|
142
|
+
// would feed the announced literal body to the line parser and desynchronize
|
|
143
|
+
// the session.
|
|
144
|
+
let digitsEnd = pos;
|
|
145
|
+
for (; pos >= 0; pos--) {
|
|
146
|
+
let c = line[pos];
|
|
147
|
+
if (c >= NUM_0 && c <= NUM_9) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (c === CURLY_OPEN && pos < digitsEnd) {
|
|
151
|
+
// Skip leading zeros so only the significant digits are converted: a
|
|
152
|
+
// marker padded with megabytes of zeros must not cost a string
|
|
153
|
+
// allocation and Number() parse of the whole run.
|
|
154
|
+
let digitsStart = pos + 1;
|
|
155
|
+
while (digitsStart < digitsEnd && line[digitsStart] === NUM_0) {
|
|
156
|
+
digitsStart++;
|
|
157
|
+
}
|
|
158
|
+
// More significant digits than any number64 has cannot fit any
|
|
159
|
+
// permissible maxLiteralSize; fail closed without materializing them
|
|
160
|
+
if (digitsEnd + 1 - digitsStart > 19) {
|
|
161
|
+
return this.failStream((0, limits_js_1.createLiteralTooLargeError)(Infinity, this.maxLiteralSize, 'the widest permissible literal size (19 digits)'));
|
|
162
|
+
}
|
|
163
|
+
const literalSize = Number(line.toString('latin1', digitsStart, digitsEnd + 1));
|
|
164
|
+
if (literalSize > this.maxLiteralSize) {
|
|
165
|
+
return this.failStream((0, limits_js_1.createLiteralTooLargeError)(literalSize, this.maxLiteralSize));
|
|
166
|
+
}
|
|
167
|
+
this.state = LITERAL;
|
|
168
|
+
this.literalWaiting = literalSize;
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Enforces the configured line-length cap for a projected line length. The projected length
|
|
177
|
+
* covers every byte of the line, the line terminator included, whether or not the line was
|
|
178
|
+
* split across input chunks. A line exactly at the limit is accepted.
|
|
179
|
+
*
|
|
180
|
+
* @param lineLength - Total length the current line would reach.
|
|
181
|
+
* @returns True if the line is within the limit, false if the stream was failed.
|
|
182
|
+
*/
|
|
183
|
+
checkLineLength(lineLength) {
|
|
184
|
+
if (lineLength <= this.maxLineLength) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
const err = new Error(`Line length ${lineLength} exceeds maximum allowed size of ${this.maxLineLength} bytes`);
|
|
188
|
+
err.code = 'LineTooLarge';
|
|
189
|
+
err.lineLength = lineLength;
|
|
190
|
+
err.maxSize = this.maxLineLength;
|
|
191
|
+
return this.failStream(err);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Enforces the configured per-response size cap: the cumulative bytes of every line
|
|
195
|
+
* segment and declared literal of the response currently being assembled. Counting
|
|
196
|
+
* declared literal sizes at marker time means an oversized total is rejected before
|
|
197
|
+
* the literal bytes even arrive. The counter is reset when a response is emitted.
|
|
198
|
+
*
|
|
199
|
+
* @param additionalBytes - Bytes the next token would add to the response.
|
|
200
|
+
* @param peek - Measure only, without committing the bytes to the counter.
|
|
201
|
+
* Used for a line that is still being assembled: its bytes are committed once, when the
|
|
202
|
+
* line completes.
|
|
203
|
+
* @returns True if within the limit, false if the stream was failed.
|
|
204
|
+
*/
|
|
205
|
+
checkResponseSize(additionalBytes, peek) {
|
|
206
|
+
let total = this.responseBytes + additionalBytes;
|
|
207
|
+
if (total <= this.maxResponseSize) {
|
|
208
|
+
if (!peek) {
|
|
209
|
+
this.responseBytes = total;
|
|
210
|
+
}
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
const err = new Error(`Response size ${total} exceeds maximum allowed size of ${this.maxResponseSize} bytes`);
|
|
214
|
+
err.code = 'ResponseTooLarge';
|
|
215
|
+
err.responseSize = total;
|
|
216
|
+
err.maxSize = this.maxResponseSize;
|
|
217
|
+
return this.failStream(err);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Processes a single input chunk of raw data. In LINE state, scans for LF-terminated
|
|
221
|
+
* lines and checks for literal markers. In LITERAL state, collects the expected number
|
|
222
|
+
* of literal bytes. When a complete command (with all its literals) is assembled, it is
|
|
223
|
+
* pushed downstream as a readable object.
|
|
224
|
+
*
|
|
225
|
+
* @param chunk - The raw data chunk to process.
|
|
226
|
+
* @param startPos - The byte offset within the chunk to start processing from.
|
|
227
|
+
*/
|
|
228
|
+
async processInputChunk(chunk, startPos) {
|
|
229
|
+
startPos = startPos || 0;
|
|
230
|
+
if (this.destroyed || startPos >= chunk.length) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
switch (this.state) {
|
|
234
|
+
case LINE: {
|
|
235
|
+
let lineStart = startPos;
|
|
236
|
+
for (let i = startPos, len = chunk.length; i < len; i++) {
|
|
237
|
+
if (chunk[i] === LF) {
|
|
238
|
+
// line end found. Measure the completed line (terminator included) before
|
|
239
|
+
// concatenating or emitting anything, so the cap does not depend on where
|
|
240
|
+
// TCP chunk boundaries happen to fall.
|
|
241
|
+
let segment = chunk.slice(lineStart, i + 1);
|
|
242
|
+
if (!this.checkLineLength(this.lineBytes + segment.length)) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
this.lineBuffer.push(segment);
|
|
246
|
+
lineStart = i + 1;
|
|
247
|
+
let line = this.lineBuffer.length === 1 ? this.lineBuffer[0] : Buffer.concat(this.lineBuffer);
|
|
248
|
+
this.lineBuffer = [];
|
|
249
|
+
this.lineBytes = 0;
|
|
250
|
+
// try to detect if this is a literal start. An oversized literal fails the
|
|
251
|
+
// stream, so the marker line must not be buffered before the check - it
|
|
252
|
+
// would otherwise be emitted as part of the rejected command.
|
|
253
|
+
let isLiteralMarker = this.checkLiteralMarker(line);
|
|
254
|
+
if (this.destroyed) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// Count the line itself and, for a literal marker, the declared
|
|
258
|
+
// literal bytes against the cumulative per-response budget, so a
|
|
259
|
+
// response assembled from many tokens stays bounded as a whole
|
|
260
|
+
if (!this.checkResponseSize(line.length + (isLiteralMarker ? this.literalWaiting : 0))) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
this.inputBuffer.push(line);
|
|
264
|
+
if (isLiteralMarker) {
|
|
265
|
+
// switch into literal mode and start over
|
|
266
|
+
return await this.processInputChunk(chunk, lineStart);
|
|
267
|
+
}
|
|
268
|
+
// reached end of command input, emit it
|
|
269
|
+
let payload = this.inputBuffer.length === 1 ? this.inputBuffer[0] : Buffer.concat(this.inputBuffer);
|
|
270
|
+
let literals = this.literals;
|
|
271
|
+
this.inputBuffer = [];
|
|
272
|
+
this.literals = [];
|
|
273
|
+
this.responseBytes = 0;
|
|
274
|
+
if (payload.length) {
|
|
275
|
+
// remove final line terminator (\n or \r\n)
|
|
276
|
+
if (payload[payload.length - 1] === LF) {
|
|
277
|
+
let end = payload.length - 1;
|
|
278
|
+
if (end > 0 && payload[end - 1] === CR) {
|
|
279
|
+
end--;
|
|
280
|
+
}
|
|
281
|
+
payload = payload.slice(0, end);
|
|
282
|
+
}
|
|
283
|
+
if (payload.length) {
|
|
284
|
+
// Whether more buffered input already followed this command on the
|
|
285
|
+
// wire - more bytes in this chunk or another queued chunk. Captured
|
|
286
|
+
// per emitted command (immutable on the pushed object) so a later
|
|
287
|
+
// command cannot overwrite it; consumers that care about pipelining
|
|
288
|
+
// boundaries can read it from the pushed object.
|
|
289
|
+
let trailingAfterLine = lineStart < chunk.length || this.inputQueue.length > 0;
|
|
290
|
+
await new Promise(resolve => {
|
|
291
|
+
// Tracked so destruction can settle the wait instead of leaving
|
|
292
|
+
// this loop (and the chunk's transform callback) pending forever
|
|
293
|
+
// when the consumer stops reading.
|
|
294
|
+
this.pendingPush = resolve;
|
|
295
|
+
const item = { payload, literals, next: resolve, trailingAfterLine };
|
|
296
|
+
this.push(item);
|
|
297
|
+
});
|
|
298
|
+
this.pendingPush = null;
|
|
299
|
+
if (this.destroyed) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (lineStart < chunk.length) {
|
|
307
|
+
// No line terminator was found in the remaining bytes; carry the tail over to
|
|
308
|
+
// the next chunk after measuring the line it belongs to.
|
|
309
|
+
let tail = chunk.slice(lineStart);
|
|
310
|
+
// The response counter is only committed when a line completes, so an
|
|
311
|
+
// in-progress line is measured against the remaining budget separately.
|
|
312
|
+
// Without this a response cap lowered to bound parser memory buys nothing
|
|
313
|
+
// while a server streams a line that never terminates - only the much
|
|
314
|
+
// larger line cap would hold it back.
|
|
315
|
+
if (!this.checkLineLength(this.lineBytes + tail.length) || !this.checkResponseSize(this.lineBytes + tail.length, true)) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
this.lineBytes += tail.length;
|
|
319
|
+
this.lineBuffer.push(tail);
|
|
320
|
+
}
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
case LITERAL: {
|
|
324
|
+
const remainingInChunk = chunk.length - startPos;
|
|
325
|
+
const bytesToRead = Math.min(remainingInChunk, this.literalWaiting);
|
|
326
|
+
const partial = startPos === 0 && bytesToRead === chunk.length ? chunk : chunk.slice(startPos, startPos + bytesToRead);
|
|
327
|
+
this.literalBuffer.push(partial);
|
|
328
|
+
this.literalWaiting -= bytesToRead;
|
|
329
|
+
if (this.literalWaiting === 0) {
|
|
330
|
+
this.literals.push(Buffer.concat(this.literalBuffer));
|
|
331
|
+
this.literalBuffer = [];
|
|
332
|
+
this.state = LINE;
|
|
333
|
+
if (remainingInChunk > bytesToRead) {
|
|
334
|
+
return await this.processInputChunk(chunk, startPos + bytesToRead);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Drains the input queue by processing each queued chunk sequentially.
|
|
343
|
+
* Yields to the event loop every 10 chunks to prevent CPU blocking on
|
|
344
|
+
* large bursts of incoming data.
|
|
345
|
+
*/
|
|
346
|
+
async processInput() {
|
|
347
|
+
let data;
|
|
348
|
+
let processedCount = 0;
|
|
349
|
+
while (!this.destroyed && (data = this.inputQueue.shift())) {
|
|
350
|
+
this.activeInput = data;
|
|
351
|
+
await this.processInputChunk(data.chunk);
|
|
352
|
+
this.activeInput = null;
|
|
353
|
+
// mark chunk as processed
|
|
354
|
+
this.releaseInput(data);
|
|
355
|
+
// Yield to event loop every 10 chunks to prevent CPU blocking
|
|
356
|
+
processedCount++;
|
|
357
|
+
if (processedCount % 10 === 0) {
|
|
358
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Transform stream implementation. Receives raw data chunks from the writable side,
|
|
364
|
+
* converts strings to Buffers, tracks total bytes read, optionally logs raw data,
|
|
365
|
+
* and queues the chunk for asynchronous processing.
|
|
366
|
+
*
|
|
367
|
+
* @param chunk - The incoming data chunk.
|
|
368
|
+
* @param encoding - The encoding if chunk is a string.
|
|
369
|
+
* @param next - Callback to signal that this chunk has been consumed.
|
|
370
|
+
*/
|
|
371
|
+
_transform(chunk, encoding, next) {
|
|
372
|
+
if (typeof chunk === 'string') {
|
|
373
|
+
chunk = Buffer.from(chunk, encoding);
|
|
374
|
+
}
|
|
375
|
+
if (!chunk || !chunk.length) {
|
|
376
|
+
return next();
|
|
377
|
+
}
|
|
378
|
+
this.readBytesCounter += chunk.length;
|
|
379
|
+
if (this.options.logRaw) {
|
|
380
|
+
this.log.trace({
|
|
381
|
+
src: 's',
|
|
382
|
+
msg: 'read from socket',
|
|
383
|
+
data: chunk.toString('base64'),
|
|
384
|
+
compress: !!this.compress,
|
|
385
|
+
secure: !!this.secureConnection,
|
|
386
|
+
cid: this.cid
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
// A terminal parser failure must not accept any more protocol input, even if the
|
|
390
|
+
// transport delivers a chunk that was already in flight.
|
|
391
|
+
if (this.destroyed) {
|
|
392
|
+
return next();
|
|
393
|
+
}
|
|
394
|
+
// Queue the chunk for async processing. The 'next' callback serves as
|
|
395
|
+
// backpressure: it is called only after this chunk is fully processed,
|
|
396
|
+
// which signals the writable side that more data can be accepted.
|
|
397
|
+
this.inputQueue.push({ chunk, next });
|
|
398
|
+
if (!this.processingInput) {
|
|
399
|
+
this.processingInput = true;
|
|
400
|
+
this.processInput()
|
|
401
|
+
.catch(err => this.failStream(err))
|
|
402
|
+
.finally(() => (this.processingInput = false));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Flush implementation called when the writable side ends. Signals completion immediately.
|
|
407
|
+
*
|
|
408
|
+
* @param next - Callback to signal flush completion.
|
|
409
|
+
*/
|
|
410
|
+
_flush(next) {
|
|
411
|
+
next();
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Destroy implementation for cleanup. Clears all internal buffers, drains the input queue
|
|
415
|
+
* by invoking pending callbacks, and forwards the error (if any) to the callback.
|
|
416
|
+
*
|
|
417
|
+
* @param err - The error that caused destruction, or null.
|
|
418
|
+
* @param callback - Callback to signal destruction completion.
|
|
419
|
+
*/
|
|
420
|
+
_destroy(err, callback) {
|
|
421
|
+
// Destruction is the single release point for parser-owned callbacks, so a terminal
|
|
422
|
+
// failure can never leave the writable side or the processing loop waiting.
|
|
423
|
+
this.inputBuffer = [];
|
|
424
|
+
this.lineBuffer = [];
|
|
425
|
+
this.lineBytes = 0;
|
|
426
|
+
this.literalBuffer = [];
|
|
427
|
+
this.literals = [];
|
|
428
|
+
this.responseBytes = 0;
|
|
429
|
+
// Settle an in-flight push() wait so processInput() can unwind
|
|
430
|
+
if (typeof this.pendingPush === 'function') {
|
|
431
|
+
const resolve = this.pendingPush;
|
|
432
|
+
this.pendingPush = null;
|
|
433
|
+
resolve();
|
|
434
|
+
}
|
|
435
|
+
// Release the chunk currently being processed, then everything still queued.
|
|
436
|
+
// releaseInput() is idempotent, so the processing loop releasing the same chunk
|
|
437
|
+
// afterwards is a no-op.
|
|
438
|
+
this.releaseInput(this.activeInput);
|
|
439
|
+
this.activeInput = null;
|
|
440
|
+
while (this.inputQueue.length) {
|
|
441
|
+
this.releaseInput(this.inputQueue.shift());
|
|
442
|
+
}
|
|
443
|
+
callback(err);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
exports.ImapStream = ImapStream;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ImapFlowError } from '../errors.js';
|
|
2
|
+
export declare const MAX_LITERAL_SIZE: number;
|
|
3
|
+
export declare const MAX_LINE_SIZE: number;
|
|
4
|
+
export declare const MAX_RESPONSE_SIZE: number;
|
|
5
|
+
/**
|
|
6
|
+
* Normalizes a configured size limit. A non-negative integer is honored as-is (including 0, which
|
|
7
|
+
* means "reject anything non-empty"), and `Infinity` disables the limit; anything else falls back
|
|
8
|
+
* to the default, so an explicit 0 is not silently swallowed the way `value || DEFAULT` would
|
|
9
|
+
* swallow it.
|
|
10
|
+
*
|
|
11
|
+
* @param value - The configured value.
|
|
12
|
+
* @param defaultValue - Fallback when the value is not a usable limit.
|
|
13
|
+
* @returns The normalized limit.
|
|
14
|
+
*/
|
|
15
|
+
export declare const normalizeLimit: (value: unknown, defaultValue: number) => number;
|
|
16
|
+
/**
|
|
17
|
+
* Builds the `LiteralTooLarge` error. One shape for every place a literal is refused, so callers
|
|
18
|
+
* can rely on `code`, `literalSize` and `maxSize` regardless of which parser rejected it.
|
|
19
|
+
*
|
|
20
|
+
* @param literalSize - The declared literal size.
|
|
21
|
+
* @param maxSize - The bound that was exceeded.
|
|
22
|
+
* @param reason - What the bound was, when it is not the configured maximum.
|
|
23
|
+
* @returns The error to emit or throw.
|
|
24
|
+
*/
|
|
25
|
+
export declare const createLiteralTooLargeError: (literalSize: number, maxSize: number, reason?: string | null | undefined) => ImapFlowError;
|