imapflow 2.5.0 → 2.5.2
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 +15 -0
- package/dist/cjs/commands/authenticate.js +32 -1
- package/dist/cjs/commands/fetch.js +10 -3
- package/dist/cjs/commands/idle.js +31 -1
- package/dist/cjs/commands/list.js +14 -0
- package/dist/cjs/errors.d.ts +1 -0
- package/dist/cjs/errors.js +2 -0
- package/dist/cjs/handler/imap-parser.js +3 -0
- package/dist/cjs/handler/parser-instance.d.ts +2 -0
- package/dist/cjs/handler/parser-instance.js +3 -1
- package/dist/cjs/handler/token-parser.d.ts +21 -2
- package/dist/cjs/handler/token-parser.js +76 -6
- package/dist/cjs/handler/types.d.ts +5 -0
- package/dist/cjs/imap-flow.js +37 -5
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/cjs/tools.d.ts +21 -0
- package/dist/cjs/tools.js +118 -52
- package/dist/cjs/types.d.ts +2 -1
- package/dist/esm/commands/authenticate.js +32 -1
- package/dist/esm/commands/fetch.js +11 -4
- package/dist/esm/commands/idle.js +31 -1
- package/dist/esm/commands/list.js +15 -1
- package/dist/esm/errors.d.ts +1 -0
- package/dist/esm/errors.js +2 -0
- package/dist/esm/handler/imap-parser.js +3 -0
- package/dist/esm/handler/parser-instance.d.ts +2 -0
- package/dist/esm/handler/parser-instance.js +3 -1
- package/dist/esm/handler/token-parser.d.ts +21 -2
- package/dist/esm/handler/token-parser.js +76 -6
- package/dist/esm/handler/types.d.ts +5 -0
- package/dist/esm/imap-flow.js +38 -6
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/dist/esm/tools.d.ts +21 -0
- package/dist/esm/tools.js +115 -51
- package/dist/esm/types.d.ts +2 -1
- package/package.json +4 -4
|
@@ -9,14 +9,23 @@ const STATE_PARTIAL = 0x004;
|
|
|
9
9
|
const STATE_SEQUENCE = 0x005;
|
|
10
10
|
const STATE_STRING = 0x006;
|
|
11
11
|
const RE_SINGLE_DIGIT = /^\d$/;
|
|
12
|
-
// Prevents stack overflow from maliciously crafted deeply-nested IMAP input (e.g., (((((...))))))
|
|
13
|
-
|
|
12
|
+
// Prevents stack overflow from maliciously crafted deeply-nested IMAP input (e.g., (((((...)))))).
|
|
13
|
+
// The tokenizer itself is iterative; what recurses is the getAttributes() walk and the
|
|
14
|
+
// consumers of its output (the body structure parser, the compiler used for logging), and
|
|
15
|
+
// a hundred frames is nothing to any of them. The old bound of 25 was reached by a message
|
|
16
|
+
// with 22 nested multiparts, which an external sender can produce, and the whole FETCH row
|
|
17
|
+
// then failed to parse and was dropped silently.
|
|
18
|
+
const MAX_NODE_DEPTH = 100;
|
|
19
|
+
// The depth at which the data items of a FETCH response sit: TREE (0) > the item list (1) >
|
|
20
|
+
// a key or value (2). See createNode() for what happens when nesting overflows below one.
|
|
21
|
+
const FETCH_DATA_ITEM_DEPTH = 2;
|
|
14
22
|
/**
|
|
15
23
|
* Tokenizes an IMAP attribute string into a tree of typed nodes.
|
|
16
24
|
* Handles all IMAP data types: atoms, quoted strings, literals (including literal8),
|
|
17
25
|
* sequences, lists (parenthesized groups), sections (bracketed groups), and partial ranges.
|
|
18
26
|
* Enforces a maximum nesting depth of {@link MAX_NODE_DEPTH} to prevent stack overflow
|
|
19
|
-
* from malicious input
|
|
27
|
+
* from malicious input: a FETCH data item that nests deeper is replaced by NIL and listed in
|
|
28
|
+
* `truncatedAttributes`, any other input that nests deeper fails to parse.
|
|
20
29
|
*/
|
|
21
30
|
export class TokenParser {
|
|
22
31
|
/**
|
|
@@ -38,6 +47,7 @@ export class TokenParser {
|
|
|
38
47
|
// Same normalization and default as the streaming parser, so a direct user of this parser
|
|
39
48
|
// gets the same bound (an explicit 0 means "reject any non-empty inline literal").
|
|
40
49
|
this.maxLiteralSize = normalizeLimit(this.options.maxLiteralSize, MAX_LITERAL_SIZE);
|
|
50
|
+
this.truncatedAttributes = [];
|
|
41
51
|
this.tree = this.currentNode = this.createNode();
|
|
42
52
|
this.pos = startPos || 0;
|
|
43
53
|
this.currentNode.type = 'TREE';
|
|
@@ -86,6 +96,16 @@ export class TokenParser {
|
|
|
86
96
|
throw this.parserError(`Unexpected end of input at position ${this.pos + this.str.length - 1} [E9]`, 'ParserError9', this.pos + this.str.length - 1);
|
|
87
97
|
}
|
|
88
98
|
let type = (node.type || '').toString().toUpperCase();
|
|
99
|
+
if (node.truncated) {
|
|
100
|
+
// The item nested deeper than MAX_NODE_DEPTH, see createNode(). Its subtree is
|
|
101
|
+
// not walked (that is what keeps this recursion bounded). A list is replaced by
|
|
102
|
+
// NIL so the data items keep pairing up as key and value; a section is attached
|
|
103
|
+
// to the item before it, so it is dropped whole.
|
|
104
|
+
if (type === 'LIST') {
|
|
105
|
+
branch.push(null);
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
89
109
|
switch (type) {
|
|
90
110
|
case 'LITERAL':
|
|
91
111
|
case 'STRING':
|
|
@@ -136,7 +156,7 @@ export class TokenParser {
|
|
|
136
156
|
* @param parentNode - The parent node to attach this node to. If omitted, creates a root node.
|
|
137
157
|
* @param startPos - The starting position of this node in the original input string.
|
|
138
158
|
* @returns The newly created node with childNodes, type, value, and isClosed properties.
|
|
139
|
-
* @throws {Error} If the nesting depth exceeds MAX_NODE_DEPTH.
|
|
159
|
+
* @throws {Error} If the nesting depth exceeds MAX_NODE_DEPTH outside a FETCH data item.
|
|
140
160
|
*/
|
|
141
161
|
createNode(parentNode, startPos) {
|
|
142
162
|
let node = {
|
|
@@ -154,8 +174,20 @@ export class TokenParser {
|
|
|
154
174
|
node.depth = 0;
|
|
155
175
|
}
|
|
156
176
|
if (node.depth > MAX_NODE_DEPTH) {
|
|
157
|
-
|
|
158
|
-
|
|
177
|
+
if (!this.isFetchResponse()) {
|
|
178
|
+
// A bounded prefix only, the error travels whole into logs and rejections
|
|
179
|
+
throw createImapError('Too much nesting in IMAP string', 'MAX_IMAP_NESTING_REACHED', { _imapStr: this.str.slice(0, ERROR_CONTEXT_LENGTH) });
|
|
180
|
+
}
|
|
181
|
+
// A FETCH row carries the data of one message, and the item nesting this deep (a
|
|
182
|
+
// BODYSTRUCTURE of a message with a hundred nested multiparts, which the sender of
|
|
183
|
+
// that message controls) is one of several. Failing the line would drop the whole
|
|
184
|
+
// row, and with it the message, from the FETCH result. The data item is marked
|
|
185
|
+
// instead, which getAttributes() turns into NIL, and the tokenizer carries on: it
|
|
186
|
+
// keeps no stack, so the depth costs it nothing. Marked on the first crossing only,
|
|
187
|
+
// every node below that one is already inside a marked item.
|
|
188
|
+
if (parentNode.depth === MAX_NODE_DEPTH) {
|
|
189
|
+
this.truncateFetchDataItem(parentNode);
|
|
190
|
+
}
|
|
159
191
|
}
|
|
160
192
|
if (typeof startPos === 'number') {
|
|
161
193
|
node.startPos = startPos;
|
|
@@ -165,6 +197,44 @@ export class TokenParser {
|
|
|
165
197
|
}
|
|
166
198
|
return node;
|
|
167
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Whether the line being parsed is a message data response (`* 12 FETCH (...)`): a numeric
|
|
202
|
+
* command with FETCH as its first attribute. Only those get the per-item nesting fallback.
|
|
203
|
+
*
|
|
204
|
+
* @returns True for an untagged FETCH response
|
|
205
|
+
*/
|
|
206
|
+
isFetchResponse() {
|
|
207
|
+
let command = this.parent.command;
|
|
208
|
+
let first = this.tree.childNodes[0];
|
|
209
|
+
return (typeof command === 'string' &&
|
|
210
|
+
/^\d+$/.test(command) &&
|
|
211
|
+
!!first &&
|
|
212
|
+
first.type === 'ATOM' &&
|
|
213
|
+
typeof first.value === 'string' &&
|
|
214
|
+
first.value.toUpperCase() === 'FETCH');
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Marks the FETCH data item a too deeply nested node belongs to as truncated and records
|
|
218
|
+
* its key (the item before it in the list, e.g. BODYSTRUCTURE) for the caller to report.
|
|
219
|
+
*
|
|
220
|
+
* @param node - The node at MAX_NODE_DEPTH whose child would exceed the limit
|
|
221
|
+
*/
|
|
222
|
+
truncateFetchDataItem(node) {
|
|
223
|
+
let item = node;
|
|
224
|
+
while (item.depth > FETCH_DATA_ITEM_DEPTH && item.parentNode) {
|
|
225
|
+
item = item.parentNode;
|
|
226
|
+
}
|
|
227
|
+
if (item.truncated) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
item.truncated = true;
|
|
231
|
+
// The item is the last child of the list while it is being parsed, so its key is the
|
|
232
|
+
// child before it. A key is an atom (BODYSTRUCTURE, ENVELOPE, BODY); anything else means
|
|
233
|
+
// the row itself is malformed, and the key is reported as unknown.
|
|
234
|
+
let siblings = item.parentNode ? item.parentNode.childNodes : [];
|
|
235
|
+
let key = siblings[siblings.length - 2];
|
|
236
|
+
this.truncatedAttributes.push(key && key.type === 'ATOM' && typeof key.value === 'string' ? key.value.toUpperCase() : '?');
|
|
237
|
+
}
|
|
168
238
|
/**
|
|
169
239
|
* Processes the entire input string character by character using a state machine.
|
|
170
240
|
* Transitions between states (NORMAL, ATOM, STRING, LITERAL, SEQUENCE, PARTIAL, TEXT)
|
|
@@ -39,6 +39,11 @@ export interface ImapResponse {
|
|
|
39
39
|
attributes?: ImapAttributeList | undefined;
|
|
40
40
|
/** Number of leading NUL bytes stripped from the line, if any */
|
|
41
41
|
nullBytesRemoved?: number | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Keys of the FETCH data items (e.g. BODYSTRUCTURE) that were replaced by NIL because they
|
|
44
|
+
* nested deeper than the parser's limit, if any
|
|
45
|
+
*/
|
|
46
|
+
truncatedAttributes?: string[] | undefined;
|
|
42
47
|
}
|
|
43
48
|
/**
|
|
44
49
|
* What the command compiler accepts as a node: parsed tokens and lists, and also bare
|
package/dist/esm/imap-flow.js
CHANGED
|
@@ -15,7 +15,7 @@ import { ConnectionDeadline } from './connection-deadline.js';
|
|
|
15
15
|
import { downloadMessage, downloadMessageParts } from './download.js';
|
|
16
16
|
import { AuthenticationFailure, createImapError } from './errors.js';
|
|
17
17
|
import imapCommands from './imap-commands.js';
|
|
18
|
-
import { comparePaths, updateCapabilities, getFolderTree, formatMessageResponse, mergeFetchRows, packMessageRange, normalizePath,
|
|
18
|
+
import { comparePaths, updateCapabilities, getFolderTree, formatMessageResponse, mergeFetchRows, packMessageRange, normalizePath, iterateRange, countRange, getColorFlags, hasCapability, isRev2Active, logConnectionError, unrefTimer, clearTimer, parseUintValue, getStringList, getTextValues, emitSafe, buildConnectionError, guardedPromise, guardedReject, isIpLiteral, MAX_UINT32_DIGITS } from './tools.js';
|
|
19
19
|
export { AuthenticationFailure, ImapFlowErrorCode } from './errors.js';
|
|
20
20
|
export { createSocketsTransport } from './sockets-transport.js';
|
|
21
21
|
export { createStreamsTransport } from './streams-transport.js';
|
|
@@ -846,6 +846,16 @@ export class ImapFlow extends EventEmitter {
|
|
|
846
846
|
}
|
|
847
847
|
return true;
|
|
848
848
|
}
|
|
849
|
+
if (parsed.truncatedAttributes) {
|
|
850
|
+
// The row is kept with the item missing rather than dropped with the whole line: the
|
|
851
|
+
// nesting is the message sender's doing, and the message must still reach the caller
|
|
852
|
+
this.log.warn({
|
|
853
|
+
msg: 'FETCH data item exceeded the nesting limit and was dropped',
|
|
854
|
+
seq: parsed.command,
|
|
855
|
+
attributes: parsed.truncatedAttributes,
|
|
856
|
+
cid: this.id
|
|
857
|
+
});
|
|
858
|
+
}
|
|
849
859
|
if (parsed.tag && !['*', '+'].includes(parsed.tag) && parsed.command) {
|
|
850
860
|
let payload = { response: parsed.command };
|
|
851
861
|
if (parsed.attributes &&
|
|
@@ -1009,11 +1019,24 @@ export class ImapFlow extends EventEmitter {
|
|
|
1009
1019
|
async settleRequest(request, parsed, hasTrailingData) {
|
|
1010
1020
|
switch ((parsed.command || '').toUpperCase()) {
|
|
1011
1021
|
case 'OK':
|
|
1012
|
-
case 'BYE':
|
|
1013
1022
|
// hasTrailingData is forwarded so STARTTLS can detect a plaintext
|
|
1014
1023
|
// injection (data buffered after the tagged OK, before the handshake).
|
|
1015
1024
|
await this.resolveParked(request, parsed, hasTrailingData);
|
|
1016
1025
|
break;
|
|
1026
|
+
case 'BYE': {
|
|
1027
|
+
// BYE is always untagged (RFC 9051 section 7.1.5), it is never the completion
|
|
1028
|
+
// result of a command. It used to resolve the command like an OK, so a server (or
|
|
1029
|
+
// a cleartext MITM) answering LOGIN with a tagged BYE marked the session
|
|
1030
|
+
// authenticated. The server says it is closing: the command fails as a lost
|
|
1031
|
+
// connection and the connection is closed, which for a LOGOUT is the outcome it
|
|
1032
|
+
// was after anyway.
|
|
1033
|
+
await this.serverBye(parsed);
|
|
1034
|
+
let err = this.createNoConnectionError(this.byeReason, { rejectedFrom: 'taggedBye', command: request.command });
|
|
1035
|
+
err.response = parsed;
|
|
1036
|
+
err.responseStatus = 'BYE';
|
|
1037
|
+
request.reject(err);
|
|
1038
|
+
break;
|
|
1039
|
+
}
|
|
1017
1040
|
case 'NO':
|
|
1018
1041
|
case 'BAD': {
|
|
1019
1042
|
let txt = getTextValues(parsed.attributes)
|
|
@@ -1495,6 +1518,13 @@ export class ImapFlow extends EventEmitter {
|
|
|
1495
1518
|
// STARTTLS explictly disabled by config
|
|
1496
1519
|
return false;
|
|
1497
1520
|
}
|
|
1521
|
+
if (this.state !== this.states.NOT_AUTHENTICATED) {
|
|
1522
|
+
// STARTTLS is only valid in the not-authenticated state (RFC 9051 section 6.2.1),
|
|
1523
|
+
// which a PREAUTH greeting skips, so there is no way left to protect this session.
|
|
1524
|
+
// Required STARTTLS fails here with that reason: the greeting arrived in cleartext,
|
|
1525
|
+
// and a MITM can answer with PREAUTH precisely to keep it that way.
|
|
1526
|
+
return this._failSTARTTLS('STARTTLS is not possible after a PREAUTH greeting');
|
|
1527
|
+
}
|
|
1498
1528
|
if (!this.capabilities.has('STARTTLS')) {
|
|
1499
1529
|
return this._failSTARTTLS();
|
|
1500
1530
|
}
|
|
@@ -1939,7 +1969,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
1939
1969
|
let tags = [];
|
|
1940
1970
|
let uids = false;
|
|
1941
1971
|
// A malformed VANISHED can carry no attributes at all, and one carrying only the
|
|
1942
|
-
// (EARLIER) tag leaves `uids` false -
|
|
1972
|
+
// (EARLIER) tag leaves `uids` false - iterateRange() handles that and yields nothing
|
|
1943
1973
|
if (!untagged.attributes || !untagged.attributes.length) {
|
|
1944
1974
|
return;
|
|
1945
1975
|
}
|
|
@@ -1950,14 +1980,16 @@ export class ImapFlow extends EventEmitter {
|
|
|
1950
1980
|
if (untagged.attributes[0] && typeof untagged.attributes[0].value === 'string') {
|
|
1951
1981
|
uids = untagged.attributes[0].value;
|
|
1952
1982
|
}
|
|
1953
|
-
let uidList = expandRange(uids);
|
|
1954
1983
|
let earlier = tags.includes('EARLIER');
|
|
1955
1984
|
// RFC 7162 section 3.2.10: unlike VANISHED (EARLIER), a plain VANISHED reports messages the
|
|
1956
1985
|
// client knows about and decrements the message count like the same number of EXPUNGEs would
|
|
1957
1986
|
if (!earlier) {
|
|
1958
|
-
mailbox.exists = Math.max(0, mailbox.exists -
|
|
1987
|
+
mailbox.exists = Math.max(0, mailbox.exists - countRange(uids));
|
|
1959
1988
|
}
|
|
1960
|
-
|
|
1989
|
+
// The set is walked one UID at a time rather than expanded first: it is the server's to
|
|
1990
|
+
// choose, and a single "1:16777216" is a few bytes on the wire but sixteen million array
|
|
1991
|
+
// entries expanded, for every such line the server cares to send
|
|
1992
|
+
for (let uid of iterateRange(uids)) {
|
|
1961
1993
|
let payload = {
|
|
1962
1994
|
path: mailbox.path,
|
|
1963
1995
|
uid,
|
package/dist/esm/package-info.js
CHANGED
package/dist/esm/tools.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { ImapAttributeList, ImapAttributeNode, ImapResponse } from './handl
|
|
|
5
5
|
import type { FetchMessageObject, ListResponse, ListTreeResponse, MailboxObject, MessageEnvelopeObject, MessageStructureObject, ImapFlowEvents, StatusQuery } from './types.js';
|
|
6
6
|
export { AuthenticationFailure } from './errors.js';
|
|
7
7
|
export declare const EXPANDED_RANGE_LIMIT = 16777216;
|
|
8
|
+
export declare const MAX_COLLECTED_RESPONSES = 1000000;
|
|
8
9
|
export declare const MAX_UINT32_DIGITS = 10;
|
|
9
10
|
export declare const noop: () => void;
|
|
10
11
|
/**
|
|
@@ -471,6 +472,26 @@ export declare function parseBigIntValue(value: unknown, maxDigits?: number): bi
|
|
|
471
472
|
* @returns The parsed value, or false when it is not usable.
|
|
472
473
|
*/
|
|
473
474
|
export declare function parseUintValue(value: unknown, maxDigits?: number): number | false;
|
|
475
|
+
/**
|
|
476
|
+
* Walks an IMAP sequence range string (e.g. "1:3,5,7:9") one number at a time, without
|
|
477
|
+
* materializing it. A single "1:16777216" is a few bytes on the wire but sixteen million
|
|
478
|
+
* entries expanded, so a consumer that only needs to visit each number (the VANISHED handler)
|
|
479
|
+
* iterates instead of calling expandRange().
|
|
480
|
+
*
|
|
481
|
+
* The same bound as expandRange() applies: at most EXPANDED_RANGE_LIMIT numbers in total are
|
|
482
|
+
* produced, over all entries of the set.
|
|
483
|
+
*
|
|
484
|
+
* @param range - IMAP sequence range string
|
|
485
|
+
* @returns The sequence numbers, in the order the set lists them
|
|
486
|
+
*/
|
|
487
|
+
export declare function iterateRange(range: unknown): Generator<number, void, undefined>;
|
|
488
|
+
/**
|
|
489
|
+
* Counts the numbers iterateRange() produces for a sequence range string, without walking it.
|
|
490
|
+
*
|
|
491
|
+
* @param range - IMAP sequence range string
|
|
492
|
+
* @returns The number of entries, capped at EXPANDED_RANGE_LIMIT like the expansion itself
|
|
493
|
+
*/
|
|
494
|
+
export declare function countRange(range: unknown): number;
|
|
474
495
|
/**
|
|
475
496
|
* Expands an IMAP sequence range string (e.g. "1:3,5,7:9") into an array of numbers.
|
|
476
497
|
*
|
package/dist/esm/tools.js
CHANGED
|
@@ -19,6 +19,15 @@ const CONNECTION_GONE_CODES = new Set([ImapFlowErrorCode.NoConnection, ImapFlowE
|
|
|
19
19
|
// expand server sequences (e.g. ESEARCH ALL in commands/search.ts) can apply the same absolute
|
|
20
20
|
// ceiling instead of inventing their own.
|
|
21
21
|
export const EXPANDED_RANGE_LIMIT = 0x1000000;
|
|
22
|
+
// Upper bound for the untagged responses one command may accumulate in memory: the FETCH rows
|
|
23
|
+
// fetchAll() collects and the mailboxes list() collects. The response size limits bound one
|
|
24
|
+
// response at a time, not how many of them a server sends in reply to one command, so a hostile
|
|
25
|
+
// server answering a single-message FETCH with an endless stream of unsolicited FETCH rows (or a
|
|
26
|
+
// LIST with endless mailboxes) would otherwise grow the process without limit. Generous on
|
|
27
|
+
// purpose: a million rows is far beyond any legitimate listing, and a mailbox that large is read
|
|
28
|
+
// through the streaming fetch() generator, which keeps nothing. Reaching it therefore fails the
|
|
29
|
+
// command (code TooManyResponses) instead of silently truncating its result.
|
|
30
|
+
export const MAX_COLLECTED_RESPONSES = 1000000;
|
|
22
31
|
// Digit bounds for untrusted numeric values in server responses. UIDs, UIDVALIDITY and
|
|
23
32
|
// message counts are 32-bit unsigned (nz-number in the RFC 9051 grammar, so at most 10
|
|
24
33
|
// digits); MODSEQ and number64 values are 63-bit unsigned (RFC 7162, RFC 9051), at most
|
|
@@ -547,13 +556,30 @@ export function getFolderTree(folders) {
|
|
|
547
556
|
root: true,
|
|
548
557
|
folders: []
|
|
549
558
|
};
|
|
559
|
+
// Children of every node indexed by name. Looking a child up with folders.find() made the
|
|
560
|
+
// tree quadratic in the number of siblings: a listing of 50k mailboxes at one level (a size
|
|
561
|
+
// the server chooses) took seconds of event loop time. A name is unique among its siblings,
|
|
562
|
+
// as the loop below updates an existing entry rather than adding a second one.
|
|
563
|
+
let childIndex = new Map();
|
|
564
|
+
let childByName = (node, name) => {
|
|
565
|
+
let index = childIndex.get(node);
|
|
566
|
+
return index ? index.get(name) : undefined;
|
|
567
|
+
};
|
|
568
|
+
let indexChild = (node, child) => {
|
|
569
|
+
let index = childIndex.get(node);
|
|
570
|
+
if (!index) {
|
|
571
|
+
index = new Map();
|
|
572
|
+
childIndex.set(node, index);
|
|
573
|
+
}
|
|
574
|
+
index.set(child.name, child);
|
|
575
|
+
};
|
|
550
576
|
let getTreeNode = (parents) => {
|
|
551
577
|
let node = tree;
|
|
552
578
|
if (!parents || !parents.length) {
|
|
553
579
|
return node;
|
|
554
580
|
}
|
|
555
581
|
for (let parent of parents) {
|
|
556
|
-
let cur = node
|
|
582
|
+
let cur = childByName(node, parent);
|
|
557
583
|
if (cur) {
|
|
558
584
|
node = cur;
|
|
559
585
|
}
|
|
@@ -568,7 +594,7 @@ export function getFolderTree(folders) {
|
|
|
568
594
|
for (let folder of byDepth) {
|
|
569
595
|
let parent = getTreeNode(folder.parent);
|
|
570
596
|
// see if entry already exists
|
|
571
|
-
let existing = parent
|
|
597
|
+
let existing = childByName(parent, folder.name);
|
|
572
598
|
if (existing) {
|
|
573
599
|
// update values
|
|
574
600
|
existing.name = folder.name;
|
|
@@ -613,6 +639,7 @@ export function getFolderTree(folders) {
|
|
|
613
639
|
parent.folders = [];
|
|
614
640
|
}
|
|
615
641
|
parent.folders.push(data);
|
|
642
|
+
indexChild(parent, data);
|
|
616
643
|
}
|
|
617
644
|
}
|
|
618
645
|
return tree;
|
|
@@ -1507,67 +1534,103 @@ export function parseUintValue(value, maxDigits) {
|
|
|
1507
1534
|
return Number.isSafeInteger(num) ? num : false;
|
|
1508
1535
|
}
|
|
1509
1536
|
/**
|
|
1510
|
-
*
|
|
1537
|
+
* Parses an IMAP sequence set string (e.g. "1:3,5,9:7") into its entries, each as the pair of
|
|
1538
|
+
* endpoints in the order written: a reversed "9:7" stays reversed, and iterateRange() walks it
|
|
1539
|
+
* downwards. Entries with an endpoint that is not a valid nz-number are skipped - the input may
|
|
1540
|
+
* come from an untrusted server, and 'Infinity' or similar garbage would otherwise loop without
|
|
1541
|
+
* bound.
|
|
1511
1542
|
*
|
|
1512
|
-
*
|
|
1513
|
-
*
|
|
1514
|
-
* otherwise loop without bound. The whole set is expanded to at most
|
|
1515
|
-
* EXPANDED_RANGE_LIMIT entries in total: legitimate responses never reach the limit
|
|
1516
|
-
* (the mailbox would need that many messages), while hostile input is cut off
|
|
1517
|
-
* instead of exhausting memory. The total is capped, not just each range -
|
|
1518
|
-
* otherwise "1:16777216,1:16777216,..." would multiply the per-range bound by an
|
|
1519
|
-
* unbounded number of ranges.
|
|
1520
|
-
*
|
|
1521
|
-
* @param range - IMAP sequence range string
|
|
1522
|
-
* @returns Array of expanded sequence numbers
|
|
1543
|
+
* @param range - IMAP sequence set string, or whatever the response parser produced for it
|
|
1544
|
+
* @returns The validated entries
|
|
1523
1545
|
*/
|
|
1524
|
-
|
|
1525
|
-
let
|
|
1546
|
+
function parseRangeEntries(range) {
|
|
1547
|
+
let entries = [];
|
|
1526
1548
|
// Callers pass whatever the response parser produced for the sequence set, and a
|
|
1527
1549
|
// malformed response can leave that as `false` (e.g. a VANISHED response carrying
|
|
1528
1550
|
// only the (EARLIER) tag). Nothing to expand then, and throwing here would abort
|
|
1529
1551
|
// the handler for the rest of the response.
|
|
1530
1552
|
if (typeof range !== 'string') {
|
|
1531
|
-
return
|
|
1553
|
+
return entries;
|
|
1532
1554
|
}
|
|
1533
1555
|
for (let entry of range.split(',')) {
|
|
1534
|
-
if (result.length >= EXPANDED_RANGE_LIMIT) {
|
|
1535
|
-
break;
|
|
1536
|
-
}
|
|
1537
1556
|
entry = entry.trim();
|
|
1538
1557
|
let colon = entry.indexOf(':');
|
|
1539
1558
|
if (colon < 0) {
|
|
1540
1559
|
let value = Number(entry);
|
|
1541
1560
|
if (isValidSequenceValue(value)) {
|
|
1542
|
-
|
|
1561
|
+
entries.push([value, value]);
|
|
1543
1562
|
}
|
|
1544
1563
|
continue;
|
|
1545
1564
|
}
|
|
1546
1565
|
let first = Number(entry.substring(0, colon));
|
|
1547
1566
|
let second = Number(entry.slice(colon + 1));
|
|
1548
|
-
if (
|
|
1549
|
-
|
|
1567
|
+
if (isValidSequenceValue(first) && isValidSequenceValue(second)) {
|
|
1568
|
+
entries.push([first, second]);
|
|
1550
1569
|
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1570
|
+
}
|
|
1571
|
+
return entries;
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Walks an IMAP sequence range string (e.g. "1:3,5,7:9") one number at a time, without
|
|
1575
|
+
* materializing it. A single "1:16777216" is a few bytes on the wire but sixteen million
|
|
1576
|
+
* entries expanded, so a consumer that only needs to visit each number (the VANISHED handler)
|
|
1577
|
+
* iterates instead of calling expandRange().
|
|
1578
|
+
*
|
|
1579
|
+
* The same bound as expandRange() applies: at most EXPANDED_RANGE_LIMIT numbers in total are
|
|
1580
|
+
* produced, over all entries of the set.
|
|
1581
|
+
*
|
|
1582
|
+
* @param range - IMAP sequence range string
|
|
1583
|
+
* @returns The sequence numbers, in the order the set lists them
|
|
1584
|
+
*/
|
|
1585
|
+
export function* iterateRange(range) {
|
|
1586
|
+
let count = 0;
|
|
1587
|
+
for (let [first, second] of parseRangeEntries(range)) {
|
|
1588
|
+
let step = first <= second ? 1 : -1;
|
|
1589
|
+
for (let i = first; count < EXPANDED_RANGE_LIMIT; i += step) {
|
|
1590
|
+
yield i;
|
|
1591
|
+
count++;
|
|
1592
|
+
if (i === second) {
|
|
1593
|
+
break;
|
|
1561
1594
|
}
|
|
1562
1595
|
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
for (let i = first; i >= last; i--) {
|
|
1566
|
-
result.push(i);
|
|
1567
|
-
}
|
|
1596
|
+
if (count >= EXPANDED_RANGE_LIMIT) {
|
|
1597
|
+
break;
|
|
1568
1598
|
}
|
|
1569
1599
|
}
|
|
1570
|
-
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* Counts the numbers iterateRange() produces for a sequence range string, without walking it.
|
|
1603
|
+
*
|
|
1604
|
+
* @param range - IMAP sequence range string
|
|
1605
|
+
* @returns The number of entries, capped at EXPANDED_RANGE_LIMIT like the expansion itself
|
|
1606
|
+
*/
|
|
1607
|
+
export function countRange(range) {
|
|
1608
|
+
let count = 0;
|
|
1609
|
+
for (let [first, second] of parseRangeEntries(range)) {
|
|
1610
|
+
count += Math.abs(second - first) + 1;
|
|
1611
|
+
if (count >= EXPANDED_RANGE_LIMIT) {
|
|
1612
|
+
return EXPANDED_RANGE_LIMIT;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
return count;
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* Expands an IMAP sequence range string (e.g. "1:3,5,7:9") into an array of numbers.
|
|
1619
|
+
*
|
|
1620
|
+
* Entries with endpoints that are not valid nz-numbers are skipped - the input
|
|
1621
|
+
* may come from an untrusted server, and 'Infinity' or similar garbage would
|
|
1622
|
+
* otherwise loop without bound. The whole set is expanded to at most
|
|
1623
|
+
* EXPANDED_RANGE_LIMIT entries in total: legitimate responses never reach the limit
|
|
1624
|
+
* (the mailbox would need that many messages), while hostile input is cut off
|
|
1625
|
+
* instead of exhausting memory. The total is capped, not just each range -
|
|
1626
|
+
* otherwise "1:16777216,1:16777216,..." would multiply the per-range bound by an
|
|
1627
|
+
* unbounded number of ranges.
|
|
1628
|
+
*
|
|
1629
|
+
* @param range - IMAP sequence range string
|
|
1630
|
+
* @returns Array of expanded sequence numbers
|
|
1631
|
+
*/
|
|
1632
|
+
export function expandRange(range) {
|
|
1633
|
+
return Array.from(iterateRange(range));
|
|
1571
1634
|
}
|
|
1572
1635
|
/**
|
|
1573
1636
|
* Returns a stream decoder for the given charset. Uses a special Japanese
|
|
@@ -1609,20 +1672,21 @@ export function packMessageRange(list) {
|
|
|
1609
1672
|
// Deduplicate before sorting so that repeated values do not produce
|
|
1610
1673
|
// overlapping/non-canonical tokens (e.g. [1,1,2,3] -> "1:3", not "1,1:3").
|
|
1611
1674
|
items = Array.from(new Set(items)).sort((a, b) => a - b);
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1675
|
+
// One forward pass: a run of consecutive values is written out as soon as it ends. The runs
|
|
1676
|
+
// used to be built with unshift(), which copies the array on every call, so packing a large
|
|
1677
|
+
// SEARCH result (a server-chosen size) took seconds for 200k UIDs and about a minute for 1M,
|
|
1678
|
+
// all of it on the event loop.
|
|
1679
|
+
let parts = [];
|
|
1680
|
+
let start = items[0];
|
|
1681
|
+
let prev = start;
|
|
1682
|
+
for (let i = 1; i <= items.length; i++) {
|
|
1683
|
+
let value = items[i];
|
|
1684
|
+
if (i < items.length && value === prev + 1) {
|
|
1685
|
+
prev = value;
|
|
1617
1686
|
continue;
|
|
1618
1687
|
}
|
|
1619
|
-
|
|
1688
|
+
parts.push(start === prev ? String(start) : start + ':' + prev);
|
|
1689
|
+
start = prev = value;
|
|
1620
1690
|
}
|
|
1621
|
-
let parts = result.map(item => {
|
|
1622
|
-
if (item.length === 1) {
|
|
1623
|
-
return item[0];
|
|
1624
|
-
}
|
|
1625
|
-
return item.shift() + ':' + item.pop();
|
|
1626
|
-
});
|
|
1627
1691
|
return parts.join(',');
|
|
1628
1692
|
}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -170,7 +170,8 @@ export interface ImapFlowOptions {
|
|
|
170
170
|
* when the server offers STARTTLS and otherwise continues in cleartext with a warning logged:
|
|
171
171
|
* the capability list that decides this was itself received in cleartext, so an attacker on
|
|
172
172
|
* the path can strip it. Set `doSTARTTLS: true` whenever the credentials must never cross the
|
|
173
|
-
* wire unencrypted.
|
|
173
|
+
* wire unencrypted. STARTTLS is only possible before authentication, so a server greeting
|
|
174
|
+
* with PREAUTH is a connection failure when the upgrade is required.
|
|
174
175
|
*/
|
|
175
176
|
doSTARTTLS?: boolean | undefined;
|
|
176
177
|
/** Custom instance ID string for logs */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imapflow",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.2",
|
|
4
4
|
"description": "IMAP Client for Node",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/imap-flow.js",
|
|
@@ -95,12 +95,12 @@
|
|
|
95
95
|
"wrangler": "4.151.0"
|
|
96
96
|
},
|
|
97
97
|
"dependencies": {
|
|
98
|
-
"@zone-eu/mailsplit": "5.4.
|
|
98
|
+
"@zone-eu/mailsplit": "5.4.23",
|
|
99
99
|
"encoding-japanese": "2.4.0",
|
|
100
100
|
"iconv-lite": "0.7.3",
|
|
101
101
|
"libbase64": "1.3.3",
|
|
102
|
-
"libmime": "5.4.
|
|
103
|
-
"libqp": "2.1.
|
|
102
|
+
"libmime": "5.4.9",
|
|
103
|
+
"libqp": "2.1.3",
|
|
104
104
|
"pino": "10.3.1",
|
|
105
105
|
"socks": "2.8.10"
|
|
106
106
|
},
|