imapflow 1.6.4 → 1.6.6
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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +14 -0
- package/lib/commands/append.js +26 -4
- package/lib/commands/compress.js +29 -18
- package/lib/commands/copyuid-parser.js +4 -2
- package/lib/commands/expunge.js +5 -2
- package/lib/commands/fetch.js +9 -3
- package/lib/commands/list.js +18 -38
- package/lib/commands/namespace.js +2 -2
- package/lib/commands/quota.js +10 -2
- package/lib/commands/search.js +54 -14
- package/lib/commands/select.js +81 -70
- package/lib/commands/status-fields.js +68 -0
- package/lib/commands/status.js +23 -61
- package/lib/handler/imap-compiler.js +91 -60
- package/lib/handler/imap-parser.js +7 -0
- package/lib/handler/imap-stream.js +78 -12
- package/lib/handler/limits.js +16 -4
- package/lib/imap-flow.d.ts +22 -2
- package/lib/imap-flow.js +209 -94
- package/lib/jp-decoder.js +30 -5
- package/lib/limited-passthrough.js +19 -1
- package/lib/search-compiler.js +24 -16
- package/lib/tools.js +190 -39
- package/package.json +4 -4
- package/test/commands-branches-test.js +4 -0
- package/test/commands-integration-test.js +780 -5
- package/test/copyuid-parser-test.js +20 -0
- package/test/idle-polling-test.js +81 -0
- package/test/imap-compiler-test.js +74 -4
- package/test/imap-flow-coverage-test.js +4 -2
- package/test/imap-flow-fetch-download-test.js +26 -0
- package/test/imap-flow-internals-test.js +134 -0
- package/test/imap-flow-methods-test.js +92 -0
- package/test/imap-flow-secure-test.js +133 -116
- package/test/imap-flow-server-test.js +126 -0
- package/test/imap-parser-test.js +25 -0
- package/test/imap-stream-edge-cases-test.js +163 -3
- package/test/integration/rev2-live-test.js +30 -0
- package/test/jp-decoder-test.js +57 -0
- package/test/limited-passthrough-test.js +24 -0
- package/test/parser-limits-test.js +18 -0
- package/test/reliability-improvements-test.js +3 -3
- package/test/search-compiler-test.js +90 -3
- package/test/timer-policy-test.js +27 -1
- package/test/tools-test.js +151 -2
package/lib/jp-decoder.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { Transform } = require('stream');
|
|
4
4
|
const encodingJapanese = require('encoding-japanese');
|
|
5
|
+
const { normalizeByteLimit } = require('./limited-passthrough.js');
|
|
5
6
|
|
|
6
7
|
// A Transform stream for decoding Japanese character sets (Shift_JIS, EUC-JP, ISO-2022-JP).
|
|
7
8
|
// Unlike iconv-lite which can decode incrementally, encoding-japanese requires the complete
|
|
@@ -9,23 +10,47 @@ const encodingJapanese = require('encoding-japanese');
|
|
|
9
10
|
// which uses escape sequences to switch between ASCII and multi-byte modes). Therefore,
|
|
10
11
|
// this stream buffers all input during _transform and performs the actual decoding in _flush.
|
|
11
12
|
class JPDecoder extends Transform {
|
|
12
|
-
constructor(charset) {
|
|
13
|
+
constructor(charset, maxBytes) {
|
|
13
14
|
super();
|
|
14
15
|
|
|
15
16
|
this.charset = charset;
|
|
16
17
|
this.chunks = [];
|
|
17
18
|
this.chunklen = 0;
|
|
19
|
+
|
|
20
|
+
// Upper bound for the buffered bytes, normalized the same way LimitedPassthrough
|
|
21
|
+
// normalizes its own. The whole-input buffering defeats a downstream maxBytes limiter
|
|
22
|
+
// (nothing is emitted until _flush), so without an internal bound a server could force
|
|
23
|
+
// unbounded memory use through a caller that asked for a limited download. Excess input
|
|
24
|
+
// is truncated, mirroring the truncation a maxBytes download applies anyway.
|
|
25
|
+
this.maxBytes = normalizeByteLimit(maxBytes);
|
|
26
|
+
|
|
27
|
+
// Also mirroring LimitedPassthrough: true once the bound is reached and every further
|
|
28
|
+
// chunk is being discarded. The download loop reads this to stop pulling from the
|
|
29
|
+
// server, which the limiter at the tail of the pipeline cannot tell it, because nothing
|
|
30
|
+
// is emitted from here until _flush().
|
|
31
|
+
this.limited = false;
|
|
18
32
|
}
|
|
19
33
|
|
|
20
|
-
// Buffer all incoming chunks; no decoding happens here because
|
|
21
|
-
// require the complete input for accurate conversion.
|
|
34
|
+
// Buffer all incoming chunks (up to maxBytes); no decoding happens here because
|
|
35
|
+
// Japanese charsets require the complete input for accurate conversion.
|
|
22
36
|
_transform(chunk, encoding, done) {
|
|
23
37
|
if (typeof chunk === 'string') {
|
|
24
38
|
chunk = Buffer.from(chunk, encoding);
|
|
25
39
|
}
|
|
26
40
|
|
|
27
|
-
this.
|
|
28
|
-
|
|
41
|
+
if (this.chunklen + chunk.length > this.maxBytes) {
|
|
42
|
+
chunk = chunk.slice(0, Math.max(0, this.maxBytes - this.chunklen));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (chunk.length) {
|
|
46
|
+
this.chunks.push(chunk);
|
|
47
|
+
this.chunklen += chunk.length;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (this.chunklen >= this.maxBytes) {
|
|
51
|
+
this.limited = true;
|
|
52
|
+
}
|
|
53
|
+
|
|
29
54
|
done();
|
|
30
55
|
}
|
|
31
56
|
|
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
const { Transform } = require('stream');
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Normalizes a byte budget for the download pipeline. Any finite positive number is honored and
|
|
7
|
+
* floored, because byte counts are integers: with a fractional bound a counter can only ever
|
|
8
|
+
* reach its floor, so a stage would never report itself full and a loop polling that flag would
|
|
9
|
+
* keep pulling forever. Anything else - 0, NaN, a non-numeric value - means "no limit".
|
|
10
|
+
*
|
|
11
|
+
* Lives here rather than in tools.js because tools.js requires jp-decoder.js, which needs this.
|
|
12
|
+
*
|
|
13
|
+
* @param {*} value - The configured budget.
|
|
14
|
+
* @returns {Number} The normalized budget, or Infinity when unbounded.
|
|
15
|
+
*/
|
|
16
|
+
const normalizeByteLimit = value => {
|
|
17
|
+
let bytes = Number(value);
|
|
18
|
+
// Math.max keeps a sub-1 budget from flooring to 0, which would read back as "no limit"
|
|
19
|
+
return Number.isFinite(bytes) && bytes > 0 ? Math.max(Math.floor(bytes), 1) : Infinity;
|
|
20
|
+
};
|
|
21
|
+
|
|
5
22
|
// A Transform stream that passes through data up to a maximum byte limit,
|
|
6
23
|
// then silently discards all subsequent chunks. Used to enforce download
|
|
7
24
|
// size limits when fetching message content from the IMAP server.
|
|
@@ -9,7 +26,7 @@ class LimitedPassthrough extends Transform {
|
|
|
9
26
|
constructor(options) {
|
|
10
27
|
super();
|
|
11
28
|
this.options = options || {};
|
|
12
|
-
this.maxBytes = this.options.maxBytes
|
|
29
|
+
this.maxBytes = normalizeByteLimit(this.options.maxBytes);
|
|
13
30
|
this.processed = 0;
|
|
14
31
|
// Once set to true, all subsequent chunks are dropped without error
|
|
15
32
|
this.limited = false;
|
|
@@ -42,3 +59,4 @@ class LimitedPassthrough extends Transform {
|
|
|
42
59
|
}
|
|
43
60
|
|
|
44
61
|
module.exports.LimitedPassthrough = LimitedPassthrough;
|
|
62
|
+
module.exports.normalizeByteLimit = normalizeByteLimit;
|
package/lib/search-compiler.js
CHANGED
|
@@ -30,6 +30,17 @@ let setBoolOpt = (attributes, term, value) => {
|
|
|
30
30
|
attributes.push({ type: 'ATOM', value: term.toUpperCase() });
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Normalizes a user-supplied sequence set (string, number, bigint, or an array of
|
|
35
|
+
* them) into the single string value of a SEQUENCE token. An array is one
|
|
36
|
+
* comma-joined set: separate tokens would be parsed by the server as extra
|
|
37
|
+
* sequence-number search keys ANDed to the query, not as part of the set.
|
|
38
|
+
*
|
|
39
|
+
* @param {*} value - The sequence set value(s)
|
|
40
|
+
* @returns {string} The joined sequence set string
|
|
41
|
+
*/
|
|
42
|
+
let toSequenceValue = value => [].concat(value).join(',');
|
|
43
|
+
|
|
33
44
|
/**
|
|
34
45
|
* Adds a search option with its value(s) to the attributes array.
|
|
35
46
|
* Handles NOT operations and array values.
|
|
@@ -37,23 +48,20 @@ let setBoolOpt = (attributes, term, value) => {
|
|
|
37
48
|
* @param {Array} attributes - Array to append the attribute to
|
|
38
49
|
* @param {string} term - The search term (e.g., 'FROM', 'SUBJECT')
|
|
39
50
|
* @param {*} value - The value for the search term (string, array, or falsy for NOT)
|
|
40
|
-
* @param {string} [type='ATOM'] - The attribute type
|
|
41
51
|
*/
|
|
42
|
-
let setOpt = (attributes, term, value
|
|
43
|
-
type = type || 'ATOM';
|
|
44
|
-
|
|
52
|
+
let setOpt = (attributes, term, value) => {
|
|
45
53
|
// Handle NOT operations for false or null values
|
|
46
54
|
if (value === false || value === null) {
|
|
47
|
-
attributes.push({ type, value: 'NOT' });
|
|
55
|
+
attributes.push({ type: 'ATOM', value: 'NOT' });
|
|
48
56
|
}
|
|
49
57
|
|
|
50
|
-
attributes.push({ type, value: term.toUpperCase() });
|
|
58
|
+
attributes.push({ type: 'ATOM', value: term.toUpperCase() });
|
|
51
59
|
|
|
52
|
-
// Handle array values (e.g
|
|
60
|
+
// Handle array values (e.g. HEADER name/value pairs)
|
|
53
61
|
if (Array.isArray(value)) {
|
|
54
|
-
value.forEach(entry => attributes.push({ type, value: (entry || '').toString() }));
|
|
62
|
+
value.forEach(entry => attributes.push({ type: 'ATOM', value: (entry || '').toString() }));
|
|
55
63
|
} else {
|
|
56
|
-
attributes.push({ type, value: value.toString() });
|
|
64
|
+
attributes.push({ type: 'ATOM', value: value.toString() });
|
|
57
65
|
}
|
|
58
66
|
};
|
|
59
67
|
|
|
@@ -158,12 +166,12 @@ module.exports.searchCompiler = (connection, query) => {
|
|
|
158
166
|
// Custom sequence range support (non-standard)
|
|
159
167
|
case 'SEQ':
|
|
160
168
|
{
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (
|
|
169
|
+
// Passed through as a SEQUENCE token: the compiler validates the
|
|
170
|
+
// set grammar and throws a coded error. An invalid value used to
|
|
171
|
+
// be dropped silently here, which turned a bad filter into an
|
|
172
|
+
// unrestricted search that matched every message.
|
|
173
|
+
let value = params[term] || params[term] === 0 ? toSequenceValue(params[term]) : '';
|
|
174
|
+
if (value) {
|
|
167
175
|
attributes.push({ type: 'SEQUENCE', value });
|
|
168
176
|
}
|
|
169
177
|
}
|
|
@@ -239,7 +247,7 @@ module.exports.searchCompiler = (connection, query) => {
|
|
|
239
247
|
case 'UID':
|
|
240
248
|
if (params[term]) {
|
|
241
249
|
attributes.push({ type: 'ATOM', value: 'UID' });
|
|
242
|
-
|
|
250
|
+
attributes.push({ type: 'SEQUENCE', value: toSequenceValue(params[term]) });
|
|
243
251
|
}
|
|
244
252
|
break;
|
|
245
253
|
|
package/lib/tools.js
CHANGED
|
@@ -11,11 +11,26 @@ const iconv = require('iconv-lite');
|
|
|
11
11
|
|
|
12
12
|
const FLAG_COLORS = ['red', 'orange', 'yellow', 'green', 'blue', 'purple', 'grey'];
|
|
13
13
|
|
|
14
|
-
// Upper bound for expanding
|
|
15
|
-
//
|
|
16
|
-
//
|
|
14
|
+
// Upper bound for expanding server-supplied sequence ranges (see expandRange). 2^24
|
|
15
|
+
// entries in total is far beyond any legitimate mailbox while keeping the worst-case
|
|
16
|
+
// expansion of a hostile range set bounded.
|
|
17
17
|
const EXPANDED_RANGE_LIMIT = 0x1000000;
|
|
18
18
|
|
|
19
|
+
// Digit bounds for untrusted numeric values in server responses. UIDs, UIDVALIDITY and
|
|
20
|
+
// message counts are 32-bit unsigned (nz-number in the RFC 9051 grammar, so at most 10
|
|
21
|
+
// digits); MODSEQ and number64 values are 63-bit unsigned (RFC 7162, RFC 9051), at most
|
|
22
|
+
// 19 digits. The bound is checked before BigInt()/Number(): a response line may carry up
|
|
23
|
+
// to maxLineLength digits, and BigInt() on a multi-megabyte digit run costs hundreds of
|
|
24
|
+
// milliseconds of non-yielding CPU.
|
|
25
|
+
const MAX_UINT32_DIGITS = 10;
|
|
26
|
+
const MAX_NUMBER64_DIGITS = 19;
|
|
27
|
+
|
|
28
|
+
// Object keys that reach through the prototype chain when assigned to, or resolve to an
|
|
29
|
+
// inherited member when read. Server-controlled strings become keys in several places
|
|
30
|
+
// (STATUS items, QUOTA resources, BODYSTRUCTURE parameters, FETCH body part names), so they
|
|
31
|
+
// all consult this one set rather than each carrying its own list.
|
|
32
|
+
const UNSAFE_OBJECT_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
33
|
+
|
|
19
34
|
// Extensions that RFC 9051 (IMAP4rev2) folds into the base protocol (Appendix E).
|
|
20
35
|
// When IMAP4rev2 is active, these are available even without their own capability
|
|
21
36
|
// token. BINARY is deliberately excluded - RFC 9051 only folds in the FETCH side,
|
|
@@ -252,7 +267,8 @@ const tools = {
|
|
|
252
267
|
|
|
253
268
|
if (list && Array.isArray(list)) {
|
|
254
269
|
list.forEach(val => {
|
|
255
|
-
|
|
270
|
+
// any entry can be a parsed NIL
|
|
271
|
+
if (!val || typeof val.value !== 'string') {
|
|
256
272
|
return;
|
|
257
273
|
}
|
|
258
274
|
let capability = val.value.toUpperCase().trim();
|
|
@@ -269,8 +285,7 @@ const tools = {
|
|
|
269
285
|
|
|
270
286
|
if (capability.startsWith('APPENDLIMIT=')) {
|
|
271
287
|
let splitPos = capability.indexOf('=');
|
|
272
|
-
|
|
273
|
-
map.set('APPENDLIMIT', appendLimit);
|
|
288
|
+
map.set('APPENDLIMIT', tools.parseUintValue(capability.substr(splitPos + 1)) || 0);
|
|
274
289
|
return;
|
|
275
290
|
}
|
|
276
291
|
|
|
@@ -493,7 +508,9 @@ const tools = {
|
|
|
493
508
|
async formatMessageResponse(untagged, mailbox) {
|
|
494
509
|
let map = {};
|
|
495
510
|
|
|
496
|
-
|
|
511
|
+
// The sequence number indexes into mailbox state, so an unusable one is dropped rather
|
|
512
|
+
// than coerced to NaN or Infinity
|
|
513
|
+
map.seq = tools.parseUintValue(untagged.command, MAX_UINT32_DIGITS) || undefined;
|
|
497
514
|
|
|
498
515
|
let key;
|
|
499
516
|
let attributes = (untagged.attributes && untagged.attributes[1]) || [];
|
|
@@ -538,15 +555,14 @@ const tools = {
|
|
|
538
555
|
}
|
|
539
556
|
};
|
|
540
557
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
};
|
|
558
|
+
// NIL (parsed as null) and other non-array values yield an empty array, so callers
|
|
559
|
+
// can safely index into the result. RFC 8474 allows e.g. `THREADID NIL` when the
|
|
560
|
+
// server has no thread relation to report.
|
|
561
|
+
let getArray = attribute => tools.getStringList(attribute);
|
|
562
|
+
|
|
563
|
+
// Counts, sizes and UIDs are written into mailbox state and into range
|
|
564
|
+
// computations, so only a bounded decimal run is usable - see parseUintValue().
|
|
565
|
+
let getUint = (attribute, maxDigits) => tools.parseUintValue(getString(attribute), maxDigits);
|
|
550
566
|
|
|
551
567
|
switch (key) {
|
|
552
568
|
case 'body[]':
|
|
@@ -555,7 +571,9 @@ const tools = {
|
|
|
555
571
|
break;
|
|
556
572
|
|
|
557
573
|
case 'uid':
|
|
558
|
-
|
|
574
|
+
// A UID feeds mailbox.uidNext one line below, and from there every range
|
|
575
|
+
// computation, so an unusable one is dropped rather than coerced
|
|
576
|
+
map.uid = getUint(attribute, MAX_UINT32_DIGITS) || undefined;
|
|
559
577
|
// If the UID we just saw is >= the mailbox's uidNext, bump uidNext.
|
|
560
578
|
// This keeps the local uidNext estimate current without requiring a
|
|
561
579
|
// separate STATUS command, handling cases where new messages arrived
|
|
@@ -565,14 +583,22 @@ const tools = {
|
|
|
565
583
|
}
|
|
566
584
|
break;
|
|
567
585
|
|
|
568
|
-
case 'modseq':
|
|
569
|
-
|
|
586
|
+
case 'modseq': {
|
|
587
|
+
// BigInt() throws on a non-numeric or missing value, and the throw
|
|
588
|
+
// drops the whole message from the result set - so a malformed
|
|
589
|
+
// MODSEQ from the server must be skipped, not surfaced.
|
|
590
|
+
let modseq = tools.parseBigIntValue(getArray(attribute)[0]);
|
|
591
|
+
if (modseq === false) {
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
map.modseq = modseq;
|
|
570
595
|
// Similarly, keep the local highestModseq estimate up to date.
|
|
571
596
|
// This is critical for CONDSTORE/QRESYNC delta syncing.
|
|
572
597
|
if (map.modseq && (!mailbox.highestModseq || mailbox.highestModseq < map.modseq)) {
|
|
573
598
|
mailbox.highestModseq = map.modseq;
|
|
574
599
|
}
|
|
575
600
|
break;
|
|
601
|
+
}
|
|
576
602
|
|
|
577
603
|
case 'emailid':
|
|
578
604
|
// OBJECTID extension (RFC 8474): server-assigned stable email identifier
|
|
@@ -599,7 +625,7 @@ const tools = {
|
|
|
599
625
|
break;
|
|
600
626
|
|
|
601
627
|
case 'rfc822.size':
|
|
602
|
-
map.size =
|
|
628
|
+
map.size = getUint(attribute) || 0;
|
|
603
629
|
break;
|
|
604
630
|
|
|
605
631
|
case 'flags':
|
|
@@ -735,6 +761,11 @@ const tools = {
|
|
|
735
761
|
return []
|
|
736
762
|
.concat(list || [])
|
|
737
763
|
.map(addr => {
|
|
764
|
+
if (!addr) {
|
|
765
|
+
// A NIL entry inside an address list: skip it instead of
|
|
766
|
+
// throwing on the dereference and dropping the message
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
738
769
|
let address = (getStrValue(addr[2]) || '') + '@' + (getStrValue(addr[3]) || '');
|
|
739
770
|
if (address === '@') {
|
|
740
771
|
address = '';
|
|
@@ -744,7 +775,7 @@ const tools = {
|
|
|
744
775
|
address
|
|
745
776
|
};
|
|
746
777
|
})
|
|
747
|
-
.filter(addr => addr.name || addr.address);
|
|
778
|
+
.filter(addr => addr && (addr.name || addr.address));
|
|
748
779
|
},
|
|
749
780
|
envelope = {};
|
|
750
781
|
|
|
@@ -813,7 +844,12 @@ const tools = {
|
|
|
813
844
|
// BODYSTRUCTURE parameters come as flat key/value pairs: [key1, val1, key2, val2, ...]
|
|
814
845
|
[].concat(arr || []).forEach((val, j) => {
|
|
815
846
|
if (j % 2) {
|
|
816
|
-
|
|
847
|
+
// Parameter names are server-controlled. The load-bearing check is the one in
|
|
848
|
+
// the continuation pass below, where the value is an object; here the value is
|
|
849
|
+
// always a string, which the __proto__ setter ignores anyway.
|
|
850
|
+
if (!tools.isUnsafeKey(key)) {
|
|
851
|
+
params[key] = libmime.decodeWords(((val && val.value) || '').toString());
|
|
852
|
+
}
|
|
817
853
|
} else {
|
|
818
854
|
key = ((val && val.value) || '').toString().toLowerCase();
|
|
819
855
|
}
|
|
@@ -848,6 +884,14 @@ const tools = {
|
|
|
848
884
|
actualKey = key.substr(0, match.index).toLowerCase();
|
|
849
885
|
nr = Number(match[2]) || 0;
|
|
850
886
|
|
|
887
|
+
if (tools.isUnsafeKey(actualKey)) {
|
|
888
|
+
// A continuation key like "__proto__*0*" would group under "__proto__":
|
|
889
|
+
// params['__proto__'] resolves to Object.prototype, so the grouping
|
|
890
|
+
// writes below would mutate it (process-wide pollution). Drop the part.
|
|
891
|
+
delete params[key];
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
|
|
851
895
|
if (!params[actualKey] || typeof params[actualKey] !== 'object') {
|
|
852
896
|
params[actualKey] = {
|
|
853
897
|
charset: false,
|
|
@@ -1198,49 +1242,143 @@ const tools = {
|
|
|
1198
1242
|
return Number.isSafeInteger(value) && value > 0 && value <= 0xffffffff;
|
|
1199
1243
|
},
|
|
1200
1244
|
|
|
1245
|
+
/**
|
|
1246
|
+
* Checks that an untrusted response value is a pure decimal digit run no longer than
|
|
1247
|
+
* the given bound.
|
|
1248
|
+
*
|
|
1249
|
+
* `!isNaN(value)` is not usable for this: it also passes '1e5', ' 12 ', '0x10' and
|
|
1250
|
+
* 'Infinity'. BigInt() throws on all of them and Number() silently returns a value the
|
|
1251
|
+
* grammar never allowed, so both are wrong in a response handler that is only trying to
|
|
1252
|
+
* read one field. The length bound is checked before the pattern so an arbitrarily long
|
|
1253
|
+
* digit run is rejected without any conversion work.
|
|
1254
|
+
*
|
|
1255
|
+
* @param {*} value - Raw value from the response.
|
|
1256
|
+
* @param {Number} maxDigits - Maximum number of digits accepted.
|
|
1257
|
+
* @returns {Boolean} True if the value is a decimal string within the bound.
|
|
1258
|
+
*/
|
|
1259
|
+
isDecimalString(value, maxDigits) {
|
|
1260
|
+
return typeof value === 'string' && value.length > 0 && value.length <= maxDigits && /^[0-9]+$/.test(value);
|
|
1261
|
+
},
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Checks whether a server-supplied string is unsafe to use as a key on a plain object.
|
|
1265
|
+
* Assigning "__proto__" writes through the prototype setter instead of creating an own
|
|
1266
|
+
* property, and reading "constructor" or "prototype" resolves to an inherited member.
|
|
1267
|
+
*
|
|
1268
|
+
* @param {*} key - Candidate key from a server response.
|
|
1269
|
+
* @returns {Boolean} True if the key must not be used.
|
|
1270
|
+
*/
|
|
1271
|
+
isUnsafeKey(key) {
|
|
1272
|
+
return UNSAFE_OBJECT_KEYS.has(key);
|
|
1273
|
+
},
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Reads a parsed attribute list of atoms or strings (a flag list, a capability list) into
|
|
1277
|
+
* an array of strings. Any element can be a parsed NIL, and the list itself can be NIL,
|
|
1278
|
+
* so both levels are guarded here rather than at each call site.
|
|
1279
|
+
*
|
|
1280
|
+
* @param {*} list - Parsed attribute list from a response.
|
|
1281
|
+
* @returns {String[]} The string values, in order, with unusable entries dropped.
|
|
1282
|
+
*/
|
|
1283
|
+
getStringList(list) {
|
|
1284
|
+
if (!Array.isArray(list)) {
|
|
1285
|
+
return [];
|
|
1286
|
+
}
|
|
1287
|
+
return list.map(entry => (entry && typeof entry.value === 'string' ? entry.value : false)).filter(entry => entry);
|
|
1288
|
+
},
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* Parses an untrusted decimal value from a server response into a BigInt.
|
|
1292
|
+
*
|
|
1293
|
+
* @param {*} value - Raw value from the response.
|
|
1294
|
+
* @param {Number} [maxDigits=MAX_NUMBER64_DIGITS] - Maximum number of digits accepted.
|
|
1295
|
+
* @returns {BigInt|false} The parsed value, or false when it is not usable.
|
|
1296
|
+
*/
|
|
1297
|
+
parseBigIntValue(value, maxDigits) {
|
|
1298
|
+
if (!tools.isDecimalString(value, maxDigits || MAX_NUMBER64_DIGITS)) {
|
|
1299
|
+
return false;
|
|
1300
|
+
}
|
|
1301
|
+
return BigInt(value);
|
|
1302
|
+
},
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Parses an untrusted decimal value from a server response into a Number. Values beyond
|
|
1306
|
+
* the safe integer range are rejected rather than rounded: a silently rounded count or
|
|
1307
|
+
* UID corrupts every range computation derived from it.
|
|
1308
|
+
*
|
|
1309
|
+
* @param {*} value - Raw value from the response.
|
|
1310
|
+
* @param {Number} [maxDigits=MAX_NUMBER64_DIGITS] - Maximum number of digits accepted.
|
|
1311
|
+
* @returns {Number|false} The parsed value, or false when it is not usable.
|
|
1312
|
+
*/
|
|
1313
|
+
parseUintValue(value, maxDigits) {
|
|
1314
|
+
if (!tools.isDecimalString(value, maxDigits || MAX_NUMBER64_DIGITS)) {
|
|
1315
|
+
return false;
|
|
1316
|
+
}
|
|
1317
|
+
let num = Number(value);
|
|
1318
|
+
return Number.isSafeInteger(num) ? num : false;
|
|
1319
|
+
},
|
|
1320
|
+
|
|
1201
1321
|
/**
|
|
1202
1322
|
* Expands an IMAP sequence range string (e.g. "1:3,5,7:9") into an array of numbers.
|
|
1203
1323
|
*
|
|
1204
1324
|
* Entries with endpoints that are not valid nz-numbers are skipped - the input
|
|
1205
1325
|
* may come from an untrusted server, and 'Infinity' or similar garbage would
|
|
1206
|
-
* otherwise loop without bound.
|
|
1207
|
-
* EXPANDED_RANGE_LIMIT entries: legitimate responses never reach the limit
|
|
1208
|
-
* (the mailbox would need that many messages), while
|
|
1209
|
-
*
|
|
1326
|
+
* otherwise loop without bound. The whole set is expanded to at most
|
|
1327
|
+
* EXPANDED_RANGE_LIMIT entries in total: legitimate responses never reach the limit
|
|
1328
|
+
* (the mailbox would need that many messages), while hostile input is cut off
|
|
1329
|
+
* instead of exhausting memory. The total is capped, not just each range -
|
|
1330
|
+
* otherwise "1:16777216,1:16777216,..." would multiply the per-range bound by an
|
|
1331
|
+
* unbounded number of ranges.
|
|
1210
1332
|
*
|
|
1211
1333
|
* @param {String} range - IMAP sequence range string
|
|
1212
1334
|
* @returns {Number[]} Array of expanded sequence numbers
|
|
1213
1335
|
*/
|
|
1214
1336
|
expandRange(range) {
|
|
1215
|
-
|
|
1337
|
+
let result = [];
|
|
1338
|
+
// Callers pass whatever the response parser produced for the sequence set, and a
|
|
1339
|
+
// malformed response can leave that as `false` (e.g. a VANISHED response carrying
|
|
1340
|
+
// only the (EARLIER) tag). Nothing to expand then, and throwing here would abort
|
|
1341
|
+
// the handler for the rest of the response.
|
|
1342
|
+
if (typeof range !== 'string') {
|
|
1343
|
+
return result;
|
|
1344
|
+
}
|
|
1345
|
+
for (let entry of range.split(',')) {
|
|
1346
|
+
if (result.length >= EXPANDED_RANGE_LIMIT) {
|
|
1347
|
+
break;
|
|
1348
|
+
}
|
|
1216
1349
|
entry = entry.trim();
|
|
1217
1350
|
let colon = entry.indexOf(':');
|
|
1218
1351
|
if (colon < 0) {
|
|
1219
1352
|
let value = Number(entry);
|
|
1220
|
-
|
|
1353
|
+
if (tools.isValidSequenceValue(value)) {
|
|
1354
|
+
result.push(value);
|
|
1355
|
+
}
|
|
1356
|
+
continue;
|
|
1221
1357
|
}
|
|
1222
1358
|
let first = Number(entry.substr(0, colon));
|
|
1223
1359
|
let second = Number(entry.substr(colon + 1));
|
|
1224
1360
|
if (!tools.isValidSequenceValue(first) || !tools.isValidSequenceValue(second)) {
|
|
1225
|
-
|
|
1361
|
+
continue;
|
|
1226
1362
|
}
|
|
1227
1363
|
if (first === second) {
|
|
1228
|
-
|
|
1364
|
+
result.push(first);
|
|
1365
|
+
continue;
|
|
1229
1366
|
}
|
|
1230
|
-
|
|
1367
|
+
// Remaining total budget doubles as the per-range bound
|
|
1368
|
+
let remaining = EXPANDED_RANGE_LIMIT - result.length;
|
|
1231
1369
|
if (first < second) {
|
|
1232
|
-
let last = Math.min(second, first +
|
|
1370
|
+
let last = Math.min(second, first + remaining - 1);
|
|
1233
1371
|
for (let i = first; i <= last; i++) {
|
|
1234
|
-
|
|
1372
|
+
result.push(i);
|
|
1235
1373
|
}
|
|
1236
1374
|
} else {
|
|
1237
|
-
let last = Math.max(second, first -
|
|
1375
|
+
let last = Math.max(second, first - remaining + 1);
|
|
1238
1376
|
for (let i = first; i >= last; i--) {
|
|
1239
|
-
|
|
1377
|
+
result.push(i);
|
|
1240
1378
|
}
|
|
1241
1379
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1380
|
+
}
|
|
1381
|
+
return result;
|
|
1244
1382
|
},
|
|
1245
1383
|
|
|
1246
1384
|
/**
|
|
@@ -1248,13 +1386,17 @@ const tools = {
|
|
|
1248
1386
|
* charset decoder for JIS/ISO-2022-JP, otherwise delegates to iconv-lite.
|
|
1249
1387
|
*
|
|
1250
1388
|
* @param {String} [charset='ascii'] - Character set name
|
|
1389
|
+
* @param {Number} [maxBytes] - Bound for the bytes the decoder may buffer. Only
|
|
1390
|
+
* relevant for the Japanese decoder, which must buffer its whole input before
|
|
1391
|
+
* it can decode: without the bound a server could defeat a caller's maxBytes
|
|
1392
|
+
* download limit simply by labelling the part with a Japanese charset.
|
|
1251
1393
|
* @returns {Object} A stream decoder (Transform stream) for the charset
|
|
1252
1394
|
*/
|
|
1253
|
-
getDecoder(charset) {
|
|
1395
|
+
getDecoder(charset, maxBytes) {
|
|
1254
1396
|
charset = (charset || 'ascii').toString().trim().toLowerCase();
|
|
1255
1397
|
if (/^jis|^iso-?2022-?jp|^euc-?jp/.test(charset)) {
|
|
1256
1398
|
// special case not supported by iconv-lite
|
|
1257
|
-
return new JPDecoder(charset);
|
|
1399
|
+
return new JPDecoder(charset, maxBytes);
|
|
1258
1400
|
}
|
|
1259
1401
|
|
|
1260
1402
|
return iconv.decodeStream(charset);
|
|
@@ -1302,3 +1444,12 @@ const tools = {
|
|
|
1302
1444
|
};
|
|
1303
1445
|
|
|
1304
1446
|
module.exports = tools;
|
|
1447
|
+
|
|
1448
|
+
// Shared bound for expanding server-supplied sequence sets (see expandRange). Exported so
|
|
1449
|
+
// other places that expand server sequences (e.g. ESEARCH ALL in commands/search.js) can
|
|
1450
|
+
// apply the same absolute ceiling instead of inventing their own.
|
|
1451
|
+
module.exports.EXPANDED_RANGE_LIMIT = EXPANDED_RANGE_LIMIT;
|
|
1452
|
+
|
|
1453
|
+
// Exported so call sites can ask for the tighter 32-bit bound where the grammar requires it
|
|
1454
|
+
// (UID, UIDVALIDITY, message counts) instead of the parsers' 63-bit default.
|
|
1455
|
+
module.exports.MAX_UINT32_DIGITS = MAX_UINT32_DIGITS;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imapflow",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.6",
|
|
4
4
|
"description": "IMAP Client for Node",
|
|
5
5
|
"main": "lib/imap-flow.js",
|
|
6
6
|
"types": "lib/imap-flow.d.ts",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"eslint": "10.8.0",
|
|
36
36
|
"eslint-config-nodemailer": "1.2.0",
|
|
37
37
|
"eslint-config-prettier": "10.1.8",
|
|
38
|
-
"grunt": "1.6.
|
|
38
|
+
"grunt": "1.6.3",
|
|
39
39
|
"grunt-cli": "1.5.0",
|
|
40
40
|
"grunt-contrib-nodeunit": "5.0.0",
|
|
41
41
|
"grunt-eslint": "26.0.0",
|
|
@@ -44,11 +44,11 @@
|
|
|
44
44
|
"typescript": "7.0.2"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@zone-eu/mailsplit": "5.4.
|
|
47
|
+
"@zone-eu/mailsplit": "5.4.15",
|
|
48
48
|
"encoding-japanese": "2.2.0",
|
|
49
49
|
"iconv-lite": "0.7.3",
|
|
50
50
|
"libbase64": "1.3.0",
|
|
51
|
-
"libmime": "5.4.
|
|
51
|
+
"libmime": "5.4.2",
|
|
52
52
|
"libqp": "2.1.1",
|
|
53
53
|
"pino": "10.3.1",
|
|
54
54
|
"socks": "2.8.9"
|
|
@@ -56,6 +56,10 @@ const createMockConnection = (overrides = {}) => {
|
|
|
56
56
|
// A live transport: command implementations that guard against polling or writing on a
|
|
57
57
|
// dead connection (idle.js) need this to look established.
|
|
58
58
|
socket: overrides.socket || { destroyed: false },
|
|
59
|
+
// Mirrors ImapFlow.throttleWait(): resolves false on normal expiry, true when close()
|
|
60
|
+
// aborted the wait. The mock resolves immediately so throttle retries stay fast.
|
|
61
|
+
throttleWait: overrides.throttleWait || (async () => false),
|
|
62
|
+
createNoConnectionError: overrides.createNoConnectionError || (() => Object.assign(new Error('Connection not available'), { code: 'NoConnection' })),
|
|
59
63
|
messageFlagsAdd: overrides.messageFlagsAdd || (async () => {}),
|
|
60
64
|
messageCopy: overrides.messageCopy || (async () => {}),
|
|
61
65
|
messageDelete: overrides.messageDelete || (async () => {}),
|