imapflow 2.5.0 → 2.5.1
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 +7 -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 +1 -1
package/dist/cjs/tools.js
CHANGED
|
@@ -4,7 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.noop = exports.MAX_UINT32_DIGITS = exports.EXPANDED_RANGE_LIMIT = exports.AuthenticationFailure = void 0;
|
|
7
|
+
exports.noop = exports.MAX_UINT32_DIGITS = exports.MAX_COLLECTED_RESPONSES = exports.EXPANDED_RANGE_LIMIT = exports.AuthenticationFailure = void 0;
|
|
8
8
|
exports.isIpLiteral = isIpLiteral;
|
|
9
9
|
exports.buildConnectionError = buildConnectionError;
|
|
10
10
|
exports.restampConnectionError = restampConnectionError;
|
|
@@ -53,6 +53,8 @@ exports.getStringList = getStringList;
|
|
|
53
53
|
exports.getStringValue = getStringValue;
|
|
54
54
|
exports.parseBigIntValue = parseBigIntValue;
|
|
55
55
|
exports.parseUintValue = parseUintValue;
|
|
56
|
+
exports.iterateRange = iterateRange;
|
|
57
|
+
exports.countRange = countRange;
|
|
56
58
|
exports.expandRange = expandRange;
|
|
57
59
|
exports.getDecoder = getDecoder;
|
|
58
60
|
exports.packMessageRange = packMessageRange;
|
|
@@ -77,6 +79,15 @@ const CONNECTION_GONE_CODES = new Set([errors_js_1.ImapFlowErrorCode.NoConnectio
|
|
|
77
79
|
// expand server sequences (e.g. ESEARCH ALL in commands/search.ts) can apply the same absolute
|
|
78
80
|
// ceiling instead of inventing their own.
|
|
79
81
|
exports.EXPANDED_RANGE_LIMIT = 0x1000000;
|
|
82
|
+
// Upper bound for the untagged responses one command may accumulate in memory: the FETCH rows
|
|
83
|
+
// fetchAll() collects and the mailboxes list() collects. The response size limits bound one
|
|
84
|
+
// response at a time, not how many of them a server sends in reply to one command, so a hostile
|
|
85
|
+
// server answering a single-message FETCH with an endless stream of unsolicited FETCH rows (or a
|
|
86
|
+
// LIST with endless mailboxes) would otherwise grow the process without limit. Generous on
|
|
87
|
+
// purpose: a million rows is far beyond any legitimate listing, and a mailbox that large is read
|
|
88
|
+
// through the streaming fetch() generator, which keeps nothing. Reaching it therefore fails the
|
|
89
|
+
// command (code TooManyResponses) instead of silently truncating its result.
|
|
90
|
+
exports.MAX_COLLECTED_RESPONSES = 1000000;
|
|
80
91
|
// Digit bounds for untrusted numeric values in server responses. UIDs, UIDVALIDITY and
|
|
81
92
|
// message counts are 32-bit unsigned (nz-number in the RFC 9051 grammar, so at most 10
|
|
82
93
|
// digits); MODSEQ and number64 values are 63-bit unsigned (RFC 7162, RFC 9051), at most
|
|
@@ -606,13 +617,30 @@ function getFolderTree(folders) {
|
|
|
606
617
|
root: true,
|
|
607
618
|
folders: []
|
|
608
619
|
};
|
|
620
|
+
// Children of every node indexed by name. Looking a child up with folders.find() made the
|
|
621
|
+
// tree quadratic in the number of siblings: a listing of 50k mailboxes at one level (a size
|
|
622
|
+
// the server chooses) took seconds of event loop time. A name is unique among its siblings,
|
|
623
|
+
// as the loop below updates an existing entry rather than adding a second one.
|
|
624
|
+
let childIndex = new Map();
|
|
625
|
+
let childByName = (node, name) => {
|
|
626
|
+
let index = childIndex.get(node);
|
|
627
|
+
return index ? index.get(name) : undefined;
|
|
628
|
+
};
|
|
629
|
+
let indexChild = (node, child) => {
|
|
630
|
+
let index = childIndex.get(node);
|
|
631
|
+
if (!index) {
|
|
632
|
+
index = new Map();
|
|
633
|
+
childIndex.set(node, index);
|
|
634
|
+
}
|
|
635
|
+
index.set(child.name, child);
|
|
636
|
+
};
|
|
609
637
|
let getTreeNode = (parents) => {
|
|
610
638
|
let node = tree;
|
|
611
639
|
if (!parents || !parents.length) {
|
|
612
640
|
return node;
|
|
613
641
|
}
|
|
614
642
|
for (let parent of parents) {
|
|
615
|
-
let cur = node
|
|
643
|
+
let cur = childByName(node, parent);
|
|
616
644
|
if (cur) {
|
|
617
645
|
node = cur;
|
|
618
646
|
}
|
|
@@ -627,7 +655,7 @@ function getFolderTree(folders) {
|
|
|
627
655
|
for (let folder of byDepth) {
|
|
628
656
|
let parent = getTreeNode(folder.parent);
|
|
629
657
|
// see if entry already exists
|
|
630
|
-
let existing = parent
|
|
658
|
+
let existing = childByName(parent, folder.name);
|
|
631
659
|
if (existing) {
|
|
632
660
|
// update values
|
|
633
661
|
existing.name = folder.name;
|
|
@@ -672,6 +700,7 @@ function getFolderTree(folders) {
|
|
|
672
700
|
parent.folders = [];
|
|
673
701
|
}
|
|
674
702
|
parent.folders.push(data);
|
|
703
|
+
indexChild(parent, data);
|
|
675
704
|
}
|
|
676
705
|
}
|
|
677
706
|
return tree;
|
|
@@ -1566,67 +1595,103 @@ function parseUintValue(value, maxDigits) {
|
|
|
1566
1595
|
return Number.isSafeInteger(num) ? num : false;
|
|
1567
1596
|
}
|
|
1568
1597
|
/**
|
|
1569
|
-
*
|
|
1598
|
+
* Parses an IMAP sequence set string (e.g. "1:3,5,9:7") into its entries, each as the pair of
|
|
1599
|
+
* endpoints in the order written: a reversed "9:7" stays reversed, and iterateRange() walks it
|
|
1600
|
+
* downwards. Entries with an endpoint that is not a valid nz-number are skipped - the input may
|
|
1601
|
+
* come from an untrusted server, and 'Infinity' or similar garbage would otherwise loop without
|
|
1602
|
+
* bound.
|
|
1570
1603
|
*
|
|
1571
|
-
*
|
|
1572
|
-
*
|
|
1573
|
-
* otherwise loop without bound. The whole set is expanded to at most
|
|
1574
|
-
* EXPANDED_RANGE_LIMIT entries in total: legitimate responses never reach the limit
|
|
1575
|
-
* (the mailbox would need that many messages), while hostile input is cut off
|
|
1576
|
-
* instead of exhausting memory. The total is capped, not just each range -
|
|
1577
|
-
* otherwise "1:16777216,1:16777216,..." would multiply the per-range bound by an
|
|
1578
|
-
* unbounded number of ranges.
|
|
1579
|
-
*
|
|
1580
|
-
* @param range - IMAP sequence range string
|
|
1581
|
-
* @returns Array of expanded sequence numbers
|
|
1604
|
+
* @param range - IMAP sequence set string, or whatever the response parser produced for it
|
|
1605
|
+
* @returns The validated entries
|
|
1582
1606
|
*/
|
|
1583
|
-
function
|
|
1584
|
-
let
|
|
1607
|
+
function parseRangeEntries(range) {
|
|
1608
|
+
let entries = [];
|
|
1585
1609
|
// Callers pass whatever the response parser produced for the sequence set, and a
|
|
1586
1610
|
// malformed response can leave that as `false` (e.g. a VANISHED response carrying
|
|
1587
1611
|
// only the (EARLIER) tag). Nothing to expand then, and throwing here would abort
|
|
1588
1612
|
// the handler for the rest of the response.
|
|
1589
1613
|
if (typeof range !== 'string') {
|
|
1590
|
-
return
|
|
1614
|
+
return entries;
|
|
1591
1615
|
}
|
|
1592
1616
|
for (let entry of range.split(',')) {
|
|
1593
|
-
if (result.length >= exports.EXPANDED_RANGE_LIMIT) {
|
|
1594
|
-
break;
|
|
1595
|
-
}
|
|
1596
1617
|
entry = entry.trim();
|
|
1597
1618
|
let colon = entry.indexOf(':');
|
|
1598
1619
|
if (colon < 0) {
|
|
1599
1620
|
let value = Number(entry);
|
|
1600
1621
|
if (isValidSequenceValue(value)) {
|
|
1601
|
-
|
|
1622
|
+
entries.push([value, value]);
|
|
1602
1623
|
}
|
|
1603
1624
|
continue;
|
|
1604
1625
|
}
|
|
1605
1626
|
let first = Number(entry.substring(0, colon));
|
|
1606
1627
|
let second = Number(entry.slice(colon + 1));
|
|
1607
|
-
if (
|
|
1608
|
-
|
|
1628
|
+
if (isValidSequenceValue(first) && isValidSequenceValue(second)) {
|
|
1629
|
+
entries.push([first, second]);
|
|
1609
1630
|
}
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1631
|
+
}
|
|
1632
|
+
return entries;
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1635
|
+
* Walks an IMAP sequence range string (e.g. "1:3,5,7:9") one number at a time, without
|
|
1636
|
+
* materializing it. A single "1:16777216" is a few bytes on the wire but sixteen million
|
|
1637
|
+
* entries expanded, so a consumer that only needs to visit each number (the VANISHED handler)
|
|
1638
|
+
* iterates instead of calling expandRange().
|
|
1639
|
+
*
|
|
1640
|
+
* The same bound as expandRange() applies: at most EXPANDED_RANGE_LIMIT numbers in total are
|
|
1641
|
+
* produced, over all entries of the set.
|
|
1642
|
+
*
|
|
1643
|
+
* @param range - IMAP sequence range string
|
|
1644
|
+
* @returns The sequence numbers, in the order the set lists them
|
|
1645
|
+
*/
|
|
1646
|
+
function* iterateRange(range) {
|
|
1647
|
+
let count = 0;
|
|
1648
|
+
for (let [first, second] of parseRangeEntries(range)) {
|
|
1649
|
+
let step = first <= second ? 1 : -1;
|
|
1650
|
+
for (let i = first; count < exports.EXPANDED_RANGE_LIMIT; i += step) {
|
|
1651
|
+
yield i;
|
|
1652
|
+
count++;
|
|
1653
|
+
if (i === second) {
|
|
1654
|
+
break;
|
|
1620
1655
|
}
|
|
1621
1656
|
}
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
for (let i = first; i >= last; i--) {
|
|
1625
|
-
result.push(i);
|
|
1626
|
-
}
|
|
1657
|
+
if (count >= exports.EXPANDED_RANGE_LIMIT) {
|
|
1658
|
+
break;
|
|
1627
1659
|
}
|
|
1628
1660
|
}
|
|
1629
|
-
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Counts the numbers iterateRange() produces for a sequence range string, without walking it.
|
|
1664
|
+
*
|
|
1665
|
+
* @param range - IMAP sequence range string
|
|
1666
|
+
* @returns The number of entries, capped at EXPANDED_RANGE_LIMIT like the expansion itself
|
|
1667
|
+
*/
|
|
1668
|
+
function countRange(range) {
|
|
1669
|
+
let count = 0;
|
|
1670
|
+
for (let [first, second] of parseRangeEntries(range)) {
|
|
1671
|
+
count += Math.abs(second - first) + 1;
|
|
1672
|
+
if (count >= exports.EXPANDED_RANGE_LIMIT) {
|
|
1673
|
+
return exports.EXPANDED_RANGE_LIMIT;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
return count;
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* Expands an IMAP sequence range string (e.g. "1:3,5,7:9") into an array of numbers.
|
|
1680
|
+
*
|
|
1681
|
+
* Entries with endpoints that are not valid nz-numbers are skipped - the input
|
|
1682
|
+
* may come from an untrusted server, and 'Infinity' or similar garbage would
|
|
1683
|
+
* otherwise loop without bound. The whole set is expanded to at most
|
|
1684
|
+
* EXPANDED_RANGE_LIMIT entries in total: legitimate responses never reach the limit
|
|
1685
|
+
* (the mailbox would need that many messages), while hostile input is cut off
|
|
1686
|
+
* instead of exhausting memory. The total is capped, not just each range -
|
|
1687
|
+
* otherwise "1:16777216,1:16777216,..." would multiply the per-range bound by an
|
|
1688
|
+
* unbounded number of ranges.
|
|
1689
|
+
*
|
|
1690
|
+
* @param range - IMAP sequence range string
|
|
1691
|
+
* @returns Array of expanded sequence numbers
|
|
1692
|
+
*/
|
|
1693
|
+
function expandRange(range) {
|
|
1694
|
+
return Array.from(iterateRange(range));
|
|
1630
1695
|
}
|
|
1631
1696
|
/**
|
|
1632
1697
|
* Returns a stream decoder for the given charset. Uses a special Japanese
|
|
@@ -1668,20 +1733,21 @@ function packMessageRange(list) {
|
|
|
1668
1733
|
// Deduplicate before sorting so that repeated values do not produce
|
|
1669
1734
|
// overlapping/non-canonical tokens (e.g. [1,1,2,3] -> "1:3", not "1,1:3").
|
|
1670
1735
|
items = Array.from(new Set(items)).sort((a, b) => a - b);
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1736
|
+
// One forward pass: a run of consecutive values is written out as soon as it ends. The runs
|
|
1737
|
+
// used to be built with unshift(), which copies the array on every call, so packing a large
|
|
1738
|
+
// SEARCH result (a server-chosen size) took seconds for 200k UIDs and about a minute for 1M,
|
|
1739
|
+
// all of it on the event loop.
|
|
1740
|
+
let parts = [];
|
|
1741
|
+
let start = items[0];
|
|
1742
|
+
let prev = start;
|
|
1743
|
+
for (let i = 1; i <= items.length; i++) {
|
|
1744
|
+
let value = items[i];
|
|
1745
|
+
if (i < items.length && value === prev + 1) {
|
|
1746
|
+
prev = value;
|
|
1676
1747
|
continue;
|
|
1677
1748
|
}
|
|
1678
|
-
|
|
1749
|
+
parts.push(start === prev ? String(start) : start + ':' + prev);
|
|
1750
|
+
start = prev = value;
|
|
1679
1751
|
}
|
|
1680
|
-
let parts = result.map(item => {
|
|
1681
|
-
if (item.length === 1) {
|
|
1682
|
-
return item[0];
|
|
1683
|
-
}
|
|
1684
|
-
return item.shift() + ':' + item.pop();
|
|
1685
|
-
});
|
|
1686
1752
|
return parts.join(',');
|
|
1687
1753
|
}
|
package/dist/cjs/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 */
|
|
@@ -1,4 +1,33 @@
|
|
|
1
1
|
import { getStatusCode, getErrorText, isServerRefusal } from '../tools.js';
|
|
2
|
+
import { createImapError } from '../errors.js';
|
|
3
|
+
// The OAuth SASL payloads are framed with \x01 (RFC 7628 section 3.1, and the XOAUTH2 format
|
|
4
|
+
// copies it), so a control character in a value ends the field early and the rest of the value
|
|
5
|
+
// becomes fields of its own, auth= included. RFC 7628 allows only VCHAR, SP, HTAB, CR and LF in
|
|
6
|
+
// a value, and none of the controls has a place in a username or a bearer token.
|
|
7
|
+
const RE_CONTROL_CHARS = /[\x00-\x1f\x7f]/;
|
|
8
|
+
/**
|
|
9
|
+
* Refuses a username or token that could not be framed as one SASL field.
|
|
10
|
+
*
|
|
11
|
+
* @param value - The value about to be placed in the payload
|
|
12
|
+
* @param what - What the value is, for the error message
|
|
13
|
+
* @throws If the value contains a control character
|
|
14
|
+
*/
|
|
15
|
+
function assertSaslValue(value, what) {
|
|
16
|
+
if (RE_CONTROL_CHARS.test(value)) {
|
|
17
|
+
throw createImapError(`The ${what} contains control characters`, 'InvalidStringValue');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Escapes a value for the authzid of a GS2 header (RFC 5801 section 4): "," and "=" delimit
|
|
22
|
+
* the header, so they are written as =2C and =3D. "=" goes first, so the "=" the escaping
|
|
23
|
+
* itself introduces is not escaped again.
|
|
24
|
+
*
|
|
25
|
+
* @param value - The saslname
|
|
26
|
+
* @returns The escaped saslname
|
|
27
|
+
*/
|
|
28
|
+
function escapeSaslName(value) {
|
|
29
|
+
return value.replace(/[=]/g, '=3D').replace(/,/g, '=2C');
|
|
30
|
+
}
|
|
2
31
|
/**
|
|
3
32
|
* Handles authentication errors by enriching the error object with server response details.
|
|
4
33
|
*
|
|
@@ -34,6 +63,8 @@ async function authOauth(connection, username, accessToken) {
|
|
|
34
63
|
let oauthbearer;
|
|
35
64
|
let command;
|
|
36
65
|
let breaker;
|
|
66
|
+
assertSaslValue(username, 'username');
|
|
67
|
+
assertSaslValue(accessToken, 'access token');
|
|
37
68
|
if (connection.capabilities.has('AUTH=OAUTHBEARER')) {
|
|
38
69
|
// OAUTHBEARER payload per RFC 7628: fields separated by \x01 (SASL GS2 framing).
|
|
39
70
|
// Format: "n,a=<user>," \x01 "host=..." \x01 "port=..." \x01 "auth=Bearer <token>" \x01 \x01
|
|
@@ -44,7 +75,7 @@ async function authOauth(connection, username, accessToken) {
|
|
|
44
75
|
// A server validating either field rejects with status `invalid_request` - a permanent
|
|
45
76
|
// failure that refreshing the access token can never clear.
|
|
46
77
|
oauthbearer = [
|
|
47
|
-
`n,a=${username},`,
|
|
78
|
+
`n,a=${escapeSaslName(username)},`,
|
|
48
79
|
`host=${connection.servername || connection.host}`,
|
|
49
80
|
`port=${connection.port}`,
|
|
50
81
|
`auth=Bearer ${accessToken}`,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { formatMessageResponse, isRev2Active, getSelectedMailbox } from '../tools.js';
|
|
1
|
+
import { formatMessageResponse, isRev2Active, getSelectedMailbox, MAX_COLLECTED_RESPONSES } from '../tools.js';
|
|
2
|
+
import { createImapError } from '../errors.js';
|
|
2
3
|
/**
|
|
3
4
|
* Fetches emails from the server.
|
|
4
5
|
*
|
|
@@ -35,9 +36,10 @@ export default async function fetch(connection, range, query, options) {
|
|
|
35
36
|
count: 0,
|
|
36
37
|
list: []
|
|
37
38
|
};
|
|
38
|
-
// The first error the onUntaggedFetch consumer reported through next(err)
|
|
39
|
-
// by untagged handlers are only logged by the
|
|
40
|
-
// the command once the FETCH completes; later
|
|
39
|
+
// The first error the onUntaggedFetch consumer reported through next(err), or the row
|
|
40
|
+
// limit being reached. Errors thrown by untagged handlers are only logged by the
|
|
41
|
+
// connection, so it is kept here and fails the command once the FETCH completes; later
|
|
42
|
+
// messages are no longer handed to the consumer or collected.
|
|
41
43
|
let consumerError = null;
|
|
42
44
|
let response;
|
|
43
45
|
try {
|
|
@@ -232,6 +234,11 @@ export default async function fetch(connection, range, query, options) {
|
|
|
232
234
|
consumerError = err;
|
|
233
235
|
}
|
|
234
236
|
}
|
|
237
|
+
else if (messages.list.length >= MAX_COLLECTED_RESPONSES) {
|
|
238
|
+
// The rows are the server's to send and the memory holding them is
|
|
239
|
+
// this process's, see MAX_COLLECTED_RESPONSES
|
|
240
|
+
consumerError = createImapError(`FETCH returned more than ${MAX_COLLECTED_RESPONSES} messages`, 'TooManyResponses');
|
|
241
|
+
}
|
|
235
242
|
else {
|
|
236
243
|
messages.list.push(formatted);
|
|
237
244
|
}
|
|
@@ -37,6 +37,12 @@ async function runIdle(connection) {
|
|
|
37
37
|
// connection, so a newer IDLE session that already installed its own is left alone.
|
|
38
38
|
let ownPreCheck = null;
|
|
39
39
|
let releaseIdling = claimIdling(connection);
|
|
40
|
+
// The deadline for the server's "+" continuation, see the onSend handler below
|
|
41
|
+
let continuationTimer = null;
|
|
42
|
+
let clearContinuationTimer = () => {
|
|
43
|
+
clearTimer(continuationTimer);
|
|
44
|
+
continuationTimer = null;
|
|
45
|
+
};
|
|
40
46
|
try {
|
|
41
47
|
// State flags for the IDLE lifecycle:
|
|
42
48
|
// - doneRequested: someone wants to break IDLE (e.g., to run another command)
|
|
@@ -102,6 +108,7 @@ async function runIdle(connection) {
|
|
|
102
108
|
// After this, the server will push untagged responses for mailbox changes.
|
|
103
109
|
// We can now safely send DONE if a break was already requested.
|
|
104
110
|
onPlusTag: async () => {
|
|
111
|
+
clearContinuationTimer();
|
|
105
112
|
connection.log.debug({
|
|
106
113
|
msg: `Initiated IDLE, waiting for server input`,
|
|
107
114
|
lockId: connection.currentLock ? connection.currentLock.lockId : undefined,
|
|
@@ -118,7 +125,29 @@ async function runIdle(connection) {
|
|
|
118
125
|
}
|
|
119
126
|
}
|
|
120
127
|
},
|
|
121
|
-
|
|
128
|
+
// The server has socketTimeout from the moment IDLE is on the wire to acknowledge it
|
|
129
|
+
// with the "+". The socket watchdog only covers a server that goes quiet: one that
|
|
130
|
+
// keeps sending untagged lines without ever answering "+" kept the IDLE in flight for
|
|
131
|
+
// good, and every later command queued behind it. DONE can not be sent before the
|
|
132
|
+
// "+" (RFC 2177 section 3), so there is no way to break such an IDLE and fall back
|
|
133
|
+
// to polling; the connection is failed the way the watchdog fails a dead peer.
|
|
134
|
+
onSend: () => {
|
|
135
|
+
continuationTimer = unrefTimer(setTimeout(() => {
|
|
136
|
+
continuationTimer = null;
|
|
137
|
+
if (canEnd || connection.isClosed) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
connection.log.error({
|
|
141
|
+
msg: 'Server did not acknowledge IDLE, closing the connection',
|
|
142
|
+
timeout: connection.socketTimeout,
|
|
143
|
+
cid: connection.id
|
|
144
|
+
});
|
|
145
|
+
connection.emitError(createImapError('IDLE was not acknowledged within the socket timeout', 'ETIMEOUT', {
|
|
146
|
+
command: 'IDLE',
|
|
147
|
+
details: { socketTimeout: connection.socketTimeout }
|
|
148
|
+
}));
|
|
149
|
+
}, connection.socketTimeout));
|
|
150
|
+
}
|
|
122
151
|
});
|
|
123
152
|
response.next();
|
|
124
153
|
return;
|
|
@@ -150,6 +179,7 @@ async function runIdle(connection) {
|
|
|
150
179
|
// (including a server-terminated IDLE, where preCheck never ran), rejected command,
|
|
151
180
|
// parser failure and connection close. `idling` drives socket-timeout handling, so it
|
|
152
181
|
// must always describe reality rather than being left over from a previous state.
|
|
182
|
+
clearContinuationTimer();
|
|
153
183
|
releaseIdling();
|
|
154
184
|
if (connection.preCheck === ownPreCheck) {
|
|
155
185
|
connection.preCheck = false;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { decodePath, encodePath, normalizePath, enhanceCommandError, hasCapability, isRev2Active, buildStatusQueryAttributes, getStringList, getStringValue } from '../tools.js';
|
|
1
|
+
import { decodePath, encodePath, normalizePath, enhanceCommandError, hasCapability, isRev2Active, buildStatusQueryAttributes, getStringList, getStringValue, MAX_COLLECTED_RESPONSES } from '../tools.js';
|
|
2
|
+
import { createImapError } from '../errors.js';
|
|
2
3
|
import { parseStatusList } from './status-fields.js';
|
|
3
4
|
import { specialUse } from '../special-use.js';
|
|
4
5
|
/**
|
|
@@ -137,6 +138,8 @@ export default async function list(connection, reference, mailbox, options) {
|
|
|
137
138
|
// namespace prefix was used (INBOX may live outside the namespace).
|
|
138
139
|
let runList = async (reference, mailbox, returnArgs) => {
|
|
139
140
|
const cmdArgs = [encodePath(connection, reference), encodePath(connection, mailbox)];
|
|
141
|
+
// Set once the entry limit is reached, fails the command when it completes
|
|
142
|
+
let overflow = null;
|
|
140
143
|
if (returnArgs.length) {
|
|
141
144
|
cmdArgs.push({ type: 'ATOM', value: 'RETURN' }, returnArgs);
|
|
142
145
|
}
|
|
@@ -196,6 +199,14 @@ export default async function list(connection, reference, mailbox, options) {
|
|
|
196
199
|
if (specialUseFlag && (!isNameSource(flagSource) || !entry.flags.has('\\NonExistent'))) {
|
|
197
200
|
addSpecialUseMatch(entry, specialUseFlag, flagSource);
|
|
198
201
|
}
|
|
202
|
+
if (entries.length >= MAX_COLLECTED_RESPONSES) {
|
|
203
|
+
// The entries are the server's to send and the memory holding them
|
|
204
|
+
// is this process's, see MAX_COLLECTED_RESPONSES. A throw from an
|
|
205
|
+
// untagged handler is only logged, so the error is raised below,
|
|
206
|
+
// once the command completes
|
|
207
|
+
overflow = overflow || createImapError(`LIST returned more than ${MAX_COLLECTED_RESPONSES} mailboxes`, 'TooManyResponses');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
199
210
|
entries.push(entry);
|
|
200
211
|
},
|
|
201
212
|
// Inline STATUS response from LIST-STATUS extension (RFC 5819).
|
|
@@ -215,6 +226,9 @@ export default async function list(connection, reference, mailbox, options) {
|
|
|
215
226
|
}
|
|
216
227
|
});
|
|
217
228
|
response.next();
|
|
229
|
+
if (overflow) {
|
|
230
|
+
throw overflow;
|
|
231
|
+
}
|
|
218
232
|
};
|
|
219
233
|
let normalizedReference = normalizePath(connection, reference || '');
|
|
220
234
|
let normalizedMailbox = normalizePath(connection, mailbox || '', true);
|
package/dist/esm/errors.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export declare const ImapFlowErrorCode: {
|
|
|
35
35
|
readonly LineTooLarge: "LineTooLarge";
|
|
36
36
|
readonly LiteralTooLarge: "LiteralTooLarge";
|
|
37
37
|
readonly ResponseTooLarge: "ResponseTooLarge";
|
|
38
|
+
readonly TooManyResponses: "TooManyResponses";
|
|
38
39
|
readonly InvalidStringValue: "InvalidStringValue";
|
|
39
40
|
readonly InvalidTokenValue: "InvalidTokenValue";
|
|
40
41
|
readonly InvalidTextValue: "InvalidTextValue";
|
package/dist/esm/errors.js
CHANGED
|
@@ -39,6 +39,8 @@ export const ImapFlowErrorCode = {
|
|
|
39
39
|
LineTooLarge: 'LineTooLarge',
|
|
40
40
|
LiteralTooLarge: 'LiteralTooLarge',
|
|
41
41
|
ResponseTooLarge: 'ResponseTooLarge',
|
|
42
|
+
// one command received more untagged responses than are kept, see MAX_COLLECTED_RESPONSES
|
|
43
|
+
TooManyResponses: 'TooManyResponses',
|
|
42
44
|
// invalid values in a command
|
|
43
45
|
InvalidStringValue: 'InvalidStringValue',
|
|
44
46
|
InvalidTokenValue: 'InvalidTokenValue',
|
|
@@ -81,6 +81,9 @@ export default async function parser(command, options) {
|
|
|
81
81
|
if (parserInstance.remainder.trim().length) {
|
|
82
82
|
await parserInstance.getSpace();
|
|
83
83
|
response.attributes = await parserInstance.getAttributes();
|
|
84
|
+
if (parserInstance.truncatedAttributes && parserInstance.truncatedAttributes.length) {
|
|
85
|
+
response.truncatedAttributes = parserInstance.truncatedAttributes;
|
|
86
|
+
}
|
|
84
87
|
}
|
|
85
88
|
if (parserInstance.humanReadable) {
|
|
86
89
|
response.attributes = (response.attributes || []).concat({
|
|
@@ -12,6 +12,8 @@ export declare class ParserInstance {
|
|
|
12
12
|
tag?: string | undefined;
|
|
13
13
|
command?: string | undefined;
|
|
14
14
|
humanReadable?: string | undefined;
|
|
15
|
+
/** The keys of the FETCH data items the token parser dropped for exceeding the nesting limit */
|
|
16
|
+
truncatedAttributes?: string[] | undefined;
|
|
15
17
|
/**
|
|
16
18
|
* Creates a new ParserInstance for parsing an IMAP response line.
|
|
17
19
|
*
|
|
@@ -211,6 +211,8 @@ export class ParserInstance {
|
|
|
211
211
|
});
|
|
212
212
|
}
|
|
213
213
|
const tokenParser = new TokenParser(this, this.pos, this.remainder, this.options);
|
|
214
|
-
|
|
214
|
+
let attributes = await tokenParser.getAttributes();
|
|
215
|
+
this.truncatedAttributes = tokenParser.truncatedAttributes;
|
|
216
|
+
return attributes;
|
|
215
217
|
}
|
|
216
218
|
}
|
|
@@ -22,6 +22,8 @@ export interface TokenNode {
|
|
|
22
22
|
started?: boolean | undefined;
|
|
23
23
|
chBuffer?: Buffer | undefined;
|
|
24
24
|
chPos?: number | undefined;
|
|
25
|
+
/** Set on a FETCH data item whose nesting exceeded MAX_NODE_DEPTH, see createNode() */
|
|
26
|
+
truncated?: boolean | undefined;
|
|
25
27
|
}
|
|
26
28
|
/**
|
|
27
29
|
* The parent object a TokenParser reads the parsed command from
|
|
@@ -34,7 +36,8 @@ export interface TokenParserParent {
|
|
|
34
36
|
* Handles all IMAP data types: atoms, quoted strings, literals (including literal8),
|
|
35
37
|
* sequences, lists (parenthesized groups), sections (bracketed groups), and partial ranges.
|
|
36
38
|
* Enforces a maximum nesting depth of {@link MAX_NODE_DEPTH} to prevent stack overflow
|
|
37
|
-
* from malicious input
|
|
39
|
+
* from malicious input: a FETCH data item that nests deeper is replaced by NIL and listed in
|
|
40
|
+
* `truncatedAttributes`, any other input that nests deeper fails to parse.
|
|
38
41
|
*/
|
|
39
42
|
export declare class TokenParser {
|
|
40
43
|
str: string;
|
|
@@ -45,6 +48,8 @@ export declare class TokenParser {
|
|
|
45
48
|
currentNode: TokenNode;
|
|
46
49
|
pos: number;
|
|
47
50
|
state: number;
|
|
51
|
+
/** The keys of the FETCH data items that were dropped for exceeding MAX_NODE_DEPTH */
|
|
52
|
+
truncatedAttributes: string[];
|
|
48
53
|
expectedLiteralType?: string | false | undefined;
|
|
49
54
|
/**
|
|
50
55
|
* Creates a new TokenParser.
|
|
@@ -88,9 +93,23 @@ export declare class TokenParser {
|
|
|
88
93
|
* @param parentNode - The parent node to attach this node to. If omitted, creates a root node.
|
|
89
94
|
* @param startPos - The starting position of this node in the original input string.
|
|
90
95
|
* @returns The newly created node with childNodes, type, value, and isClosed properties.
|
|
91
|
-
* @throws {Error} If the nesting depth exceeds MAX_NODE_DEPTH.
|
|
96
|
+
* @throws {Error} If the nesting depth exceeds MAX_NODE_DEPTH outside a FETCH data item.
|
|
92
97
|
*/
|
|
93
98
|
createNode(parentNode?: TokenNode | undefined, startPos?: number | undefined): TokenNode;
|
|
99
|
+
/**
|
|
100
|
+
* Whether the line being parsed is a message data response (`* 12 FETCH (...)`): a numeric
|
|
101
|
+
* command with FETCH as its first attribute. Only those get the per-item nesting fallback.
|
|
102
|
+
*
|
|
103
|
+
* @returns True for an untagged FETCH response
|
|
104
|
+
*/
|
|
105
|
+
isFetchResponse(): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Marks the FETCH data item a too deeply nested node belongs to as truncated and records
|
|
108
|
+
* its key (the item before it in the list, e.g. BODYSTRUCTURE) for the caller to report.
|
|
109
|
+
*
|
|
110
|
+
* @param node - The node at MAX_NODE_DEPTH whose child would exceed the limit
|
|
111
|
+
*/
|
|
112
|
+
truncateFetchDataItem(node: TokenNode): void;
|
|
94
113
|
/**
|
|
95
114
|
* Processes the entire input string character by character using a state machine.
|
|
96
115
|
* Transitions between states (NORMAL, ATOM, STRING, LITERAL, SEQUENCE, PARTIAL, TEXT)
|