imapflow 1.6.5 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +20 -0
- package/lib/commands/append.js +26 -4
- 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/idle.js +26 -5
- 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-stream.js +56 -2
- package/lib/handler/limits.js +16 -4
- package/lib/imap-flow.d.ts +33 -2
- package/lib/imap-flow.js +307 -97
- package/lib/jp-decoder.js +30 -5
- package/lib/limited-passthrough.js +19 -1
- package/lib/tools.js +190 -39
- package/package.json +4 -4
- package/test/auto-idle-test.js +470 -0
- package/test/commands-branches-test.js +4 -0
- package/test/commands-integration-test.js +683 -0
- package/test/connection-edge-cases-test.js +3 -1
- package/test/copyuid-parser-test.js +20 -0
- package/test/fixtures/test-client.js +57 -0
- package/test/idle-polling-test.js +88 -0
- package/test/imap-flow-coverage-test.js +8 -12
- package/test/imap-flow-fetch-download-test.js +29 -10
- package/test/imap-flow-internals-test.js +14 -32
- package/test/imap-flow-methods-test.js +92 -0
- package/test/imap-stream-edge-cases-test.js +136 -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/timer-policy-test.js +31 -18
- package/test/tools-test.js +151 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.7.0](https://github.com/postalsys/imapflow/compare/v1.6.6...v1.7.0) (2026-08-11)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **idle:** make the auto-IDLE delay configurable ([311fe0c](https://github.com/postalsys/imapflow/commit/311fe0ccb0d75d4ddbe797bf5bc755df5bf0485f))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **idle:** align the socket watchdog with the auto-IDLE busy guard ([d5e7191](https://github.com/postalsys/imapflow/commit/d5e71915ac7a1829b7e1340da6195a5e3e9984b2))
|
|
14
|
+
* **idle:** validate autoIdleDelay and keep auto-IDLE off a busy connection ([aeafdf6](https://github.com/postalsys/imapflow/commit/aeafdf62fab3b29cd488d9b8cbdffc8896107d6e))
|
|
15
|
+
|
|
16
|
+
## [1.6.6](https://github.com/postalsys/imapflow/compare/v1.6.5...v1.6.6) (2026-08-07)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* harden parsing of untrusted server response values ([1560424](https://github.com/postalsys/imapflow/commit/15604240b5fe5f7ec57514db17f41123140b53fe))
|
|
22
|
+
|
|
3
23
|
## [1.6.5](https://github.com/postalsys/imapflow/compare/v1.6.4...v1.6.5) (2026-07-29)
|
|
4
24
|
|
|
5
25
|
|
package/lib/commands/append.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const {
|
|
4
|
+
formatFlag,
|
|
5
|
+
canUseFlag,
|
|
6
|
+
formatDateTime,
|
|
7
|
+
normalizePath,
|
|
8
|
+
encodePath,
|
|
9
|
+
comparePaths,
|
|
10
|
+
enhanceCommandError,
|
|
11
|
+
parseBigIntValue,
|
|
12
|
+
parseUintValue,
|
|
13
|
+
MAX_UINT32_DIGITS
|
|
14
|
+
} = require('../tools.js');
|
|
4
15
|
|
|
5
16
|
/**
|
|
6
17
|
* Appends a message to a mailbox.
|
|
@@ -79,7 +90,15 @@ module.exports = async (connection, destination, content, flags, idate) => {
|
|
|
79
90
|
// Handler for untagged EXISTS: captures the new message count which gives
|
|
80
91
|
// us the sequence number of the appended message (it's the latest message).
|
|
81
92
|
const handleExistsUpdate = untagged => {
|
|
82
|
-
|
|
93
|
+
// The count is written into the live mailbox state below, so it has to clear the
|
|
94
|
+
// same bar untaggedExists() applies: a digit run long enough to coerce to Infinity
|
|
95
|
+
// would make resolveRange('*') compile to the literal string "Infinity" and break
|
|
96
|
+
// every range-based command until the next SELECT.
|
|
97
|
+
let seq = parseUintValue(untagged.command, MAX_UINT32_DIGITS);
|
|
98
|
+
if (seq === false) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
map.seq = seq;
|
|
83
102
|
|
|
84
103
|
// Update the connection's mailbox state and emit 'exists' event if the
|
|
85
104
|
// count changed (notifies listeners about the new message).
|
|
@@ -109,8 +128,11 @@ module.exports = async (connection, destination, content, flags, idate) => {
|
|
|
109
128
|
if (section && section.length) {
|
|
110
129
|
let responseCode = section[0] && typeof section[0].value === 'string' ? section[0].value : '';
|
|
111
130
|
if (responseCode.toUpperCase() === 'APPENDUID') {
|
|
112
|
-
|
|
113
|
-
|
|
131
|
+
// Bounded digit runs only: isNaN() also passes '1e5', which BigInt() rejects
|
|
132
|
+
// with a throw - and this catch rethrows, so the append would reject after the
|
|
133
|
+
// message was already stored and a retrying caller would duplicate it.
|
|
134
|
+
let uidValidity = parseBigIntValue(section[1] && section[1].value, MAX_UINT32_DIGITS);
|
|
135
|
+
let uid = parseUintValue(section[2] && section[2].value, MAX_UINT32_DIGITS);
|
|
114
136
|
if (uidValidity !== false) {
|
|
115
137
|
map.uidValidity = uidValidity;
|
|
116
138
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { expandRange } = require('../tools.js');
|
|
3
|
+
const { expandRange, parseBigIntValue } = require('../tools.js');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Parses COPYUID response code from an IMAP response (RFC 4315).
|
|
@@ -18,7 +18,9 @@ function parseCopyUid(response, map) {
|
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
// Only a bounded pure digit string is accepted: isNaN() also passes values like "1e5" or
|
|
22
|
+
// "Infinity", which BigInt() then rejects with a throw that loses the uidMap.
|
|
23
|
+
let uidValidity = parseBigIntValue(section[1] && section[1].value);
|
|
22
24
|
if (uidValidity !== false) {
|
|
23
25
|
map.uidValidity = uidValidity;
|
|
24
26
|
}
|
package/lib/commands/expunge.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { enhanceCommandError, hasCapability } = require('../tools.js');
|
|
3
|
+
const { enhanceCommandError, hasCapability, parseBigIntValue } = require('../tools.js');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Deletes specified messages by flagging them as Deleted and expunging.
|
|
@@ -41,7 +41,10 @@ module.exports = async (connection, range, options) => {
|
|
|
41
41
|
let section = response.response.attributes && response.response.attributes[0] && response.response.attributes[0].section;
|
|
42
42
|
let responseCode = section && section.length && section[0] && typeof section[0].value === 'string' ? section[0].value : '';
|
|
43
43
|
if (responseCode.toUpperCase() === 'HIGHESTMODSEQ') {
|
|
44
|
-
|
|
44
|
+
// Bounded digit runs only: isNaN() also passes '1e5', which BigInt() rejects with
|
|
45
|
+
// a throw that the catch below would swallow, making messageDelete() report false
|
|
46
|
+
// even though the server expunged the messages.
|
|
47
|
+
let highestModseq = parseBigIntValue(section[1] && section[1].value);
|
|
45
48
|
if (highestModseq && (!connection.mailbox.highestModseq || highestModseq > connection.mailbox.highestModseq)) {
|
|
46
49
|
connection.mailbox.highestModseq = highestModseq;
|
|
47
50
|
}
|
package/lib/commands/fetch.js
CHANGED
|
@@ -235,7 +235,9 @@ module.exports = async (connection, range, query, options) => {
|
|
|
235
235
|
// If server provides a throttleReset hint, use that if longer.
|
|
236
236
|
const backoffDelay = Math.min(baseDelay * Math.pow(2, retryCount), 30000); // Cap at 30 seconds
|
|
237
237
|
|
|
238
|
-
// Use throttle reset time if provided and longer than backoff
|
|
238
|
+
// Use throttle reset time if provided and longer than backoff. The hint is
|
|
239
|
+
// server-controlled, so the wait goes through connection.throttleWait(), which caps
|
|
240
|
+
// it and keeps the timer tracked and abortable.
|
|
239
241
|
const delay = err.throttleReset && err.throttleReset > backoffDelay ? err.throttleReset : backoffDelay;
|
|
240
242
|
|
|
241
243
|
connection.log.warn({
|
|
@@ -248,8 +250,12 @@ module.exports = async (connection, range, query, options) => {
|
|
|
248
250
|
delayMs: delay
|
|
249
251
|
});
|
|
250
252
|
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
+
// An aborted wait means the client was closed, so give up rather than reissuing
|
|
254
|
+
// the FETCH on a connection that is already gone.
|
|
255
|
+
let aborted = await connection.throttleWait(delay);
|
|
256
|
+
if (aborted) {
|
|
257
|
+
throw connection.createNoConnectionError(connection.byeReason);
|
|
258
|
+
}
|
|
253
259
|
|
|
254
260
|
retryCount++;
|
|
255
261
|
continue;
|
package/lib/commands/idle.js
CHANGED
|
@@ -257,14 +257,16 @@ async function runPollingFallback(connection, maxIdleTime) {
|
|
|
257
257
|
|
|
258
258
|
pollOnce(connection, session)
|
|
259
259
|
.then(() => {
|
|
260
|
+
// Stamped only after a poll actually completed: a failed poll must not
|
|
261
|
+
// satisfy the resumed schedule below, or the next session would defer
|
|
262
|
+
// its first poll a full interval past an attempt that checked nothing.
|
|
263
|
+
connection._lastPollAt = Date.now();
|
|
260
264
|
// Cancellation is re-checked here: the session may have been broken while
|
|
261
265
|
// this poll was in flight, and an orphaned poller must not schedule again.
|
|
262
266
|
if (session.cancelled) {
|
|
263
267
|
return;
|
|
264
268
|
}
|
|
265
|
-
|
|
266
|
-
// Background polling must not keep the process alive
|
|
267
|
-
unrefTimer(session.timer);
|
|
269
|
+
scheduleNextPoll(interval);
|
|
268
270
|
})
|
|
269
271
|
.catch(err => {
|
|
270
272
|
connection.log.warn({ err, cid: connection.id });
|
|
@@ -272,9 +274,28 @@ async function runPollingFallback(connection, maxIdleTime) {
|
|
|
272
274
|
});
|
|
273
275
|
};
|
|
274
276
|
|
|
277
|
+
function scheduleNextPoll(delay) {
|
|
278
|
+
session.timer = setTimeout(runPoll, delay);
|
|
279
|
+
// Background polling must not keep the process alive
|
|
280
|
+
unrefTimer(session.timer);
|
|
281
|
+
}
|
|
282
|
+
|
|
275
283
|
connection.log.debug({ src: 'c', msg: `initiated NOOP loop`, cid: connection.id });
|
|
276
|
-
|
|
277
|
-
|
|
284
|
+
|
|
285
|
+
// Every auto-IDLE restart begins a fresh polling session, so an unconditional first
|
|
286
|
+
// poll would tie the poll rate to how often the caller runs commands rather than to
|
|
287
|
+
// `interval`: with a short autoIdleDelay, a command every few seconds turns into a
|
|
288
|
+
// poll every few seconds. The last poll timestamp lives on the connection, so a new
|
|
289
|
+
// session resumes the previous one's schedule instead of restarting it.
|
|
290
|
+
// Clamped at zero because a backward wall-clock step (NTP, VM resume) leaves the
|
|
291
|
+
// stamp in the future; however large the jump, the next poll must never be more
|
|
292
|
+
// than one full interval away.
|
|
293
|
+
let sinceLastPoll = Math.max(0, Date.now() - (connection._lastPollAt || 0));
|
|
294
|
+
if (sinceLastPoll >= interval) {
|
|
295
|
+
runPoll();
|
|
296
|
+
} else {
|
|
297
|
+
scheduleNextPoll(interval - sinceLastPoll);
|
|
298
|
+
}
|
|
278
299
|
});
|
|
279
300
|
} finally {
|
|
280
301
|
session.cancelled = true;
|
package/lib/commands/list.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const {
|
|
4
|
+
decodePath,
|
|
5
|
+
encodePath,
|
|
6
|
+
normalizePath,
|
|
7
|
+
enhanceCommandError,
|
|
8
|
+
hasCapability,
|
|
9
|
+
isRev2Active,
|
|
10
|
+
buildStatusQueryAttributes,
|
|
11
|
+
getStringList
|
|
12
|
+
} = require('../tools.js');
|
|
13
|
+
const { parseStatusList } = require('./status-fields.js');
|
|
4
14
|
const { specialUse } = require('../special-use');
|
|
5
15
|
|
|
6
16
|
/**
|
|
@@ -133,7 +143,9 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
133
143
|
// User-provided hints map mailbox paths to special-use types (e.g., {sent: "Sent Items"}).
|
|
134
144
|
// These override server-reported flags and name-based guesses. Converted to a
|
|
135
145
|
// path-keyed lookup: { "Sent Items" => "\\Sent" }
|
|
136
|
-
|
|
146
|
+
// Keyed by server-supplied mailbox paths further down, so a null prototype keeps a
|
|
147
|
+
// path like "constructor" from resolving to an inherited member on lookup
|
|
148
|
+
let specialUseHints = Object.create(null);
|
|
137
149
|
if (options.specialUseHints && typeof options.specialUseHints === 'object') {
|
|
138
150
|
for (let type of Object.keys(options.specialUseHints)) {
|
|
139
151
|
if (
|
|
@@ -170,7 +182,7 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
170
182
|
// Decode from modified UTF-7 wire format and normalize the path
|
|
171
183
|
path: normalizePath(connection, decodePath(connection, (untagged.attributes[2] && untagged.attributes[2].value) || '')),
|
|
172
184
|
pathAsListed: (untagged.attributes[2] && untagged.attributes[2].value) || '',
|
|
173
|
-
flags: new Set(untagged.attributes[0]
|
|
185
|
+
flags: new Set(getStringList(untagged.attributes[0])),
|
|
174
186
|
delimiter: untagged.attributes[1] && untagged.attributes[1].value,
|
|
175
187
|
listed: true
|
|
176
188
|
};
|
|
@@ -236,41 +248,9 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
236
248
|
return;
|
|
237
249
|
}
|
|
238
250
|
|
|
239
|
-
const STATUS_FIELD_MAP = {
|
|
240
|
-
MESSAGES: { key: 'messages', parser: Number },
|
|
241
|
-
RECENT: { key: 'recent', parser: Number },
|
|
242
|
-
UIDNEXT: { key: 'uidNext', parser: Number },
|
|
243
|
-
UIDVALIDITY: { key: 'uidValidity', parser: BigInt },
|
|
244
|
-
UNSEEN: { key: 'unseen', parser: Number },
|
|
245
|
-
HIGHESTMODSEQ: { key: 'highestModseq', parser: BigInt },
|
|
246
|
-
// IMAP4rev2 additions (RFC 9051): mailbox size and \Deleted count
|
|
247
|
-
SIZE: { key: 'size', parser: Number },
|
|
248
|
-
DELETED: { key: 'deleted', parser: Number }
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
let key;
|
|
252
251
|
let map = { path: statusPath };
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (i % 2 === 0) {
|
|
256
|
-
key = entry && typeof entry.value === 'string' ? entry.value : false;
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
if (!key || !entry || typeof entry.value !== 'string') {
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const fieldConfig = STATUS_FIELD_MAP[key.toUpperCase()];
|
|
264
|
-
if (!fieldConfig) {
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const value = !isNaN(entry.value) ? fieldConfig.parser(entry.value) : false;
|
|
269
|
-
if (value === false) {
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
map[fieldConfig.key] = value;
|
|
252
|
+
parseStatusList(statusList, (key, value) => {
|
|
253
|
+
map[key] = value;
|
|
274
254
|
});
|
|
275
255
|
|
|
276
256
|
statusMap.set(statusPath, map);
|
|
@@ -455,7 +435,7 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
455
435
|
let entry = {
|
|
456
436
|
path: normalizePath(connection, decodePath(connection, (untagged.attributes[2] && untagged.attributes[2].value) || '')),
|
|
457
437
|
pathAsListed: (untagged.attributes[2] && untagged.attributes[2].value) || '',
|
|
458
|
-
flags: new Set(untagged.attributes[0]
|
|
438
|
+
flags: new Set(getStringList(untagged.attributes[0])),
|
|
459
439
|
delimiter: untagged.attributes[1] && untagged.attributes[1].value,
|
|
460
440
|
subscribed: true
|
|
461
441
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { hasCapability } = require('../tools.js');
|
|
3
|
+
const { hasCapability, getStringList } = require('../tools.js');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Requests NAMESPACE info from the server.
|
|
@@ -95,7 +95,7 @@ async function getListPrefix(connection) {
|
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
map.flags = new Set(untagged.attributes[0]
|
|
98
|
+
map.flags = new Set(getStringList(untagged.attributes[0]));
|
|
99
99
|
map.delimiter = untagged.attributes[1] && untagged.attributes[1].value;
|
|
100
100
|
map.prefix = (untagged.attributes[2] && untagged.attributes[2].value) || '';
|
|
101
101
|
if (map.delimiter && map.prefix.charAt(0) === map.delimiter) {
|
package/lib/commands/quota.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { encodePath, normalizePath, enhanceCommandError } = require('../tools.js');
|
|
3
|
+
const { encodePath, normalizePath, enhanceCommandError, parseUintValue, isUnsafeKey } = require('../tools.js');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Requests quota information for a mailbox.
|
|
@@ -47,11 +47,19 @@ module.exports = async (connection, path) => {
|
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
// isNaN() also passes '1e5', ' 12 ' and 'Infinity', none of which is a usable octet count
|
|
51
|
+
let value = parseUintValue(attribute && attribute.value);
|
|
51
52
|
if (value === false) {
|
|
52
53
|
return;
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
// Resource names are server-controlled. This object is returned to the caller, so it
|
|
57
|
+
// keeps a normal prototype and unsafe names are dropped instead; the fixed fields
|
|
58
|
+
// must keep their values too.
|
|
59
|
+
if (isUnsafeKey(key) || key === 'path' || key === 'quotaroot') {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
55
63
|
if (!map[key]) {
|
|
56
64
|
map[key] = {};
|
|
57
65
|
}
|
package/lib/commands/search.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const {
|
|
4
|
+
enhanceCommandError,
|
|
5
|
+
hasCapability,
|
|
6
|
+
isValidSequenceValue,
|
|
7
|
+
parseBigIntValue,
|
|
8
|
+
parseUintValue,
|
|
9
|
+
MAX_UINT32_DIGITS,
|
|
10
|
+
EXPANDED_RANGE_LIMIT
|
|
11
|
+
} = require('../tools.js');
|
|
4
12
|
const { searchCompiler } = require('../search-compiler.js');
|
|
5
13
|
|
|
6
14
|
/**
|
|
@@ -48,27 +56,29 @@ function parseEsearchResponse(attrs) {
|
|
|
48
56
|
continue;
|
|
49
57
|
}
|
|
50
58
|
switch (key) {
|
|
59
|
+
// COUNT is a plain message count; MIN and MAX are sequence numbers or UIDs. All
|
|
60
|
+
// three are bounded decimal runs - isNaN() would also admit '1e400' (Infinity)
|
|
51
61
|
case 'COUNT': {
|
|
52
|
-
const n =
|
|
53
|
-
if (
|
|
62
|
+
const n = parseUintValue(attrs[++i]?.value, MAX_UINT32_DIGITS);
|
|
63
|
+
if (n !== false) result.count = n;
|
|
54
64
|
break;
|
|
55
65
|
}
|
|
56
66
|
case 'MIN': {
|
|
57
|
-
const n =
|
|
58
|
-
if (
|
|
67
|
+
const n = parseUintValue(attrs[++i]?.value, MAX_UINT32_DIGITS);
|
|
68
|
+
if (n !== false) result.min = n;
|
|
59
69
|
break;
|
|
60
70
|
}
|
|
61
71
|
case 'MAX': {
|
|
62
|
-
const n =
|
|
63
|
-
if (
|
|
72
|
+
const n = parseUintValue(attrs[++i]?.value, MAX_UINT32_DIGITS);
|
|
73
|
+
if (n !== false) result.max = n;
|
|
64
74
|
break;
|
|
65
75
|
}
|
|
66
76
|
case 'MODSEQ': {
|
|
67
77
|
// RFC 7162 section 3.1.5: present when the SEARCH used a MODSEQ
|
|
68
78
|
// criterion on a CONDSTORE-enabled session. BigInt because
|
|
69
79
|
// mod-sequence values are unsigned 63-bit
|
|
70
|
-
const
|
|
71
|
-
if (
|
|
80
|
+
const modseq = parseBigIntValue(attrs[++i]?.value);
|
|
81
|
+
if (modseq !== false) result.modseq = modseq;
|
|
72
82
|
break;
|
|
73
83
|
}
|
|
74
84
|
case 'ALL': {
|
|
@@ -192,11 +202,37 @@ module.exports = async (connection, query, options) => {
|
|
|
192
202
|
untagged: {
|
|
193
203
|
SEARCH: async untagged => {
|
|
194
204
|
if (untagged && untagged.attributes && untagged.attributes.length) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
205
|
+
let truncated = false;
|
|
206
|
+
let discarded = false;
|
|
207
|
+
for (let attribute of untagged.attributes) {
|
|
208
|
+
// The result set is server-controlled and accumulated across
|
|
209
|
+
// responses, so stop at the same absolute ceiling expandRange()
|
|
210
|
+
// uses - a server streaming SEARCH responses could otherwise
|
|
211
|
+
// grow the set until the process runs out of memory
|
|
212
|
+
/* c8 ignore next 4 */ // reaching the ceiling needs 2^24 accumulated results, which no unit test can produce in reasonable time
|
|
213
|
+
if (results.size >= EXPANDED_RANGE_LIMIT) {
|
|
214
|
+
truncated = true;
|
|
215
|
+
break;
|
|
198
216
|
}
|
|
199
|
-
|
|
217
|
+
// Same nz-number check the ESEARCH branch below applies. isNaN()
|
|
218
|
+
// is not enough: it passes '1e400' (Infinity), '-3' and '2.5', and
|
|
219
|
+
// a single one of those makes the sequence set compiled from this
|
|
220
|
+
// result set invalid, failing the caller's whole follow-up command
|
|
221
|
+
let value = attribute && typeof attribute.value === 'string' ? Number(attribute.value) : NaN;
|
|
222
|
+
if (!isValidSequenceValue(value)) {
|
|
223
|
+
discarded = true;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
results.add(value);
|
|
227
|
+
}
|
|
228
|
+
if (truncated || discarded) {
|
|
229
|
+
connection.log.warn({
|
|
230
|
+
msg: 'Invalid entries in the SEARCH result',
|
|
231
|
+
truncated,
|
|
232
|
+
discarded,
|
|
233
|
+
cid: connection.id
|
|
234
|
+
});
|
|
235
|
+
}
|
|
200
236
|
}
|
|
201
237
|
},
|
|
202
238
|
|
|
@@ -222,7 +258,11 @@ module.exports = async (connection, query, options) => {
|
|
|
222
258
|
// not contain '*' at all (RFC 9051 section 4.1.1), so UID
|
|
223
259
|
// parts with '*' are dropped
|
|
224
260
|
let existsCount = () => (connection.mailbox && connection.mailbox.exists) || 0;
|
|
225
|
-
|
|
261
|
+
// The mailbox EXISTS count is itself server-supplied and can be
|
|
262
|
+
// absurdly large, so the budget is additionally capped at the same
|
|
263
|
+
// absolute ceiling expandRange() uses - a hostile server cannot
|
|
264
|
+
// bypass it by inflating EXISTS first
|
|
265
|
+
let overBudget = () => results.size >= existsCount() || results.size >= EXPANDED_RANGE_LIMIT;
|
|
226
266
|
let resolveId = part => (part === '*' ? (options.uid ? 0 : existsCount()) : Number(part));
|
|
227
267
|
let truncated = false;
|
|
228
268
|
let discarded = false;
|