imapflow 2.2.5 → 2.2.7
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 +17 -0
- package/dist/cjs/commands/append.js +10 -3
- package/dist/cjs/commands/authenticate.js +27 -18
- package/dist/cjs/commands/close.d.ts +12 -1
- package/dist/cjs/commands/close.js +4 -2
- package/dist/cjs/commands/copyuid-parser.js +16 -2
- package/dist/cjs/commands/delete.js +2 -1
- package/dist/cjs/commands/esearch-parser.js +8 -2
- package/dist/cjs/commands/fetch.js +35 -9
- package/dist/cjs/commands/id.js +8 -1
- package/dist/cjs/commands/idle.js +15 -5
- package/dist/cjs/commands/list.js +10 -1
- package/dist/cjs/commands/login.js +5 -1
- package/dist/cjs/commands/logout.js +7 -0
- package/dist/cjs/commands/namespace.js +7 -3
- package/dist/cjs/commands/quota.js +3 -1
- package/dist/cjs/commands/rename.js +2 -1
- package/dist/cjs/commands/select.js +5 -0
- package/dist/cjs/commands/status.js +6 -1
- package/dist/cjs/commands/store.d.ts +1 -1
- package/dist/cjs/commands/store.js +1 -2
- package/dist/cjs/download.js +82 -91
- package/dist/cjs/handler/imap-compiler.js +45 -29
- package/dist/cjs/handler/limits.d.ts +11 -0
- package/dist/cjs/handler/limits.js +16 -1
- package/dist/cjs/handler/parser-instance.d.ts +10 -0
- package/dist/cjs/handler/parser-instance.js +25 -10
- package/dist/cjs/handler/token-parser.js +162 -78
- package/dist/cjs/imap-flow.js +235 -89
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/cjs/proxy-connection.js +7 -7
- package/dist/cjs/search-compiler.js +37 -16
- package/dist/cjs/special-use.js +10 -5
- package/dist/cjs/tools.d.ts +10 -1
- package/dist/cjs/tools.js +30 -3
- package/dist/cjs/types.d.ts +19 -4
- package/dist/esm/commands/append.js +11 -4
- package/dist/esm/commands/authenticate.js +28 -19
- package/dist/esm/commands/close.d.ts +12 -1
- package/dist/esm/commands/close.js +5 -3
- package/dist/esm/commands/copyuid-parser.js +16 -2
- package/dist/esm/commands/delete.js +2 -1
- package/dist/esm/commands/esearch-parser.js +8 -2
- package/dist/esm/commands/fetch.js +35 -9
- package/dist/esm/commands/id.js +9 -2
- package/dist/esm/commands/idle.js +16 -6
- package/dist/esm/commands/list.js +10 -1
- package/dist/esm/commands/login.js +6 -2
- package/dist/esm/commands/logout.js +7 -0
- package/dist/esm/commands/namespace.js +7 -3
- package/dist/esm/commands/quota.js +3 -1
- package/dist/esm/commands/rename.js +2 -1
- package/dist/esm/commands/select.js +5 -0
- package/dist/esm/commands/status.js +7 -2
- package/dist/esm/commands/store.d.ts +1 -1
- package/dist/esm/commands/store.js +1 -2
- package/dist/esm/download.js +82 -91
- package/dist/esm/handler/imap-compiler.js +45 -29
- package/dist/esm/handler/limits.d.ts +11 -0
- package/dist/esm/handler/limits.js +14 -0
- package/dist/esm/handler/parser-instance.d.ts +10 -0
- package/dist/esm/handler/parser-instance.js +25 -10
- package/dist/esm/handler/token-parser.js +163 -79
- package/dist/esm/imap-flow.js +235 -89
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/dist/esm/proxy-connection.js +7 -7
- package/dist/esm/search-compiler.js +37 -16
- package/dist/esm/special-use.js +10 -5
- package/dist/esm/tools.d.ts +10 -1
- package/dist/esm/tools.js +29 -3
- package/dist/esm/types.d.ts +19 -4
- package/package.json +3 -2
package/dist/cjs/download.js
CHANGED
|
@@ -15,6 +15,9 @@ const libbase64_1 = __importDefault(require("libbase64"));
|
|
|
15
15
|
const mailsplit_1 = require("@zone-eu/mailsplit");
|
|
16
16
|
const flowed_decoder_js_1 = __importDefault(require("@zone-eu/mailsplit/lib/flowed-decoder.js"));
|
|
17
17
|
const limited_passthrough_js_1 = require("./limited-passthrough.js");
|
|
18
|
+
// What a wait on the head stream listens for: it can take more input, it failed, or it went
|
|
19
|
+
// away (a consumer destroying it closes it without a 'drain')
|
|
20
|
+
const DRAIN_WAIT_EVENTS = ['drain', 'error', 'close'];
|
|
18
21
|
const tools_js_1 = require("./tools.js");
|
|
19
22
|
/**
|
|
20
23
|
* Implements ImapFlow.download(), see its documentation
|
|
@@ -54,7 +57,8 @@ async function downloadMessage(client, range, part, options) {
|
|
|
54
57
|
range = uid;
|
|
55
58
|
downloadOptions.uid = true;
|
|
56
59
|
}
|
|
57
|
-
|
|
60
|
+
// bodyStructure is unset when the server sent BODYSTRUCTURE NIL
|
|
61
|
+
if (!response.bodyStructure?.childNodes) {
|
|
58
62
|
// single text message
|
|
59
63
|
part = 'TEXT';
|
|
60
64
|
}
|
|
@@ -297,11 +301,54 @@ async function downloadMessage(client, range, part, options) {
|
|
|
297
301
|
// at the bound still gets its terminating chunk. Infinity when the server reported no
|
|
298
302
|
// size, which leaves the loop bounded by maxBytes alone.
|
|
299
303
|
let maxTotalBytes = (0, limited_passthrough_js_1.normalizeByteLimit)(meta.expectedSize ? meta.expectedSize * 2 + chunkSize : 0);
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
|
|
304
|
+
// Resolves once the head stream can take more input, rejects when it fails, and resolves
|
|
305
|
+
// when it goes away. finish() is the listener itself: 'drain' and 'close' emit no arguments,
|
|
306
|
+
// 'error' emits the error, and removal needs no separate handler references. It removes
|
|
307
|
+
// only the listeners this wait installed - removeAllListeners('error') also took off the
|
|
308
|
+
// forwarder pipeStage() attached to the head stream when the pipeline was built, and the
|
|
309
|
+
// head must keep that forwarder for the life of the download or a chunk failure has nowhere
|
|
310
|
+
// to go.
|
|
311
|
+
let waitForDrain = () => new Promise((resolve, reject) => {
|
|
312
|
+
const finish = (err) => {
|
|
313
|
+
for (let event of DRAIN_WAIT_EVENTS) {
|
|
314
|
+
stream.removeListener(event, finish);
|
|
315
|
+
}
|
|
316
|
+
/* c8 ignore next 2 */ // stream error during a backpressure drain wait is timing-dependent
|
|
317
|
+
if (err) {
|
|
318
|
+
reject(err);
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
resolve();
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
for (let event of DRAIN_WAIT_EVENTS) {
|
|
325
|
+
stream.once(event, finish);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
// Writes a chunk and waits out the backpressure. A base64 decoder defers its write callback,
|
|
329
|
+
// so a chunk the size of its buffer is never taken synchronously and the head chunk waits
|
|
330
|
+
// here as much as any other. A stream failure during the wait is thrown unless the download
|
|
331
|
+
// was already aborted (the consumer destroyed the stream).
|
|
332
|
+
let writeAndDrain = async (chunk) => {
|
|
333
|
+
if (writeChunk(chunk) !== false) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
await waitForDrain();
|
|
338
|
+
/* c8 ignore next 5 */ // re-throw path only triggers on a stream error mid-drain, which is timing-dependent
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
if (!fetchAborted) {
|
|
342
|
+
throw err;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
// Writes the head chunk, then fetches the remaining chunks in a loop, writing each to the
|
|
347
|
+
// decoder stream. Stops when the server returns a short chunk (< chunkSize), answers with
|
|
348
|
+
// more than the requested window, the byte limiter is satisfied, or the consumer destroys
|
|
349
|
+
// the output stream. Throws when the ceiling above is crossed.
|
|
350
|
+
let fetchAllParts = async (head) => {
|
|
351
|
+
await writeAndDrain(head);
|
|
305
352
|
while (hasMore && !isLimited() && !fetchAborted) {
|
|
306
353
|
if (processed >= maxTotalBytes) {
|
|
307
354
|
// Loud on purpose. Everything written downstream by this point holds
|
|
@@ -314,7 +361,9 @@ async function downloadMessage(client, range, part, options) {
|
|
|
314
361
|
throw err;
|
|
315
362
|
}
|
|
316
363
|
let { response, chunk } = await getNextPart();
|
|
317
|
-
|
|
364
|
+
// A consumer that gave up while the chunk was in flight: its 'close' may still be a
|
|
365
|
+
// tick away from setting fetchAborted, so the stream's own flag is checked as well
|
|
366
|
+
if (fetchAborted || output.destroyed) {
|
|
318
367
|
break;
|
|
319
368
|
}
|
|
320
369
|
if (response === false) {
|
|
@@ -329,48 +378,7 @@ async function downloadMessage(client, range, part, options) {
|
|
|
329
378
|
if (!chunk) {
|
|
330
379
|
break;
|
|
331
380
|
}
|
|
332
|
-
|
|
333
|
-
if (writeChunk(chunk) === false) {
|
|
334
|
-
// Wait for drain event before continuing
|
|
335
|
-
try {
|
|
336
|
-
await new Promise((resolve, reject) => {
|
|
337
|
-
// finish() is the listener itself, as settle() is for the TLS upgrade:
|
|
338
|
-
// 'drain' and 'close' emit no arguments, 'error' emits the error, and
|
|
339
|
-
// removal needs no separate handler references. It removes only the
|
|
340
|
-
// three listeners this wait installed - removeAllListeners('error')
|
|
341
|
-
// also took off the forwarder pipeStage() attached to the head stream
|
|
342
|
-
// when the pipeline was built, and the head must keep that forwarder
|
|
343
|
-
// for the life of the download or a chunk failure has nowhere to go.
|
|
344
|
-
const finish = (err) => {
|
|
345
|
-
for (let event of ['drain', 'error', 'close']) {
|
|
346
|
-
stream.removeListener(event, finish);
|
|
347
|
-
}
|
|
348
|
-
/* c8 ignore next 2 */ // stream error during a backpressure drain wait is timing-dependent
|
|
349
|
-
if (err) {
|
|
350
|
-
reject(err);
|
|
351
|
-
}
|
|
352
|
-
else {
|
|
353
|
-
resolve();
|
|
354
|
-
}
|
|
355
|
-
};
|
|
356
|
-
stream.once('drain', finish);
|
|
357
|
-
stream.once('error', finish);
|
|
358
|
-
stream.once('close', finish);
|
|
359
|
-
});
|
|
360
|
-
/* c8 ignore start */ // re-throw path only triggers on a stream error mid-drain, which is timing-dependent
|
|
361
|
-
}
|
|
362
|
-
catch (err) {
|
|
363
|
-
// Re-throw only if not aborted
|
|
364
|
-
if (!fetchAborted) {
|
|
365
|
-
throw err;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
/* c8 ignore stop */
|
|
369
|
-
// Check if we should abort after waiting
|
|
370
|
-
if (fetchAborted) {
|
|
371
|
-
break;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
381
|
+
await writeAndDrain(chunk);
|
|
374
382
|
}
|
|
375
383
|
};
|
|
376
384
|
// A download is a sequence of chunk FETCHes with a backpressure wait in between. Those
|
|
@@ -389,13 +397,10 @@ async function downloadMessage(client, range, part, options) {
|
|
|
389
397
|
client.autoidle();
|
|
390
398
|
}
|
|
391
399
|
};
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
// value before streaming begins.
|
|
397
|
-
let runFetchAllParts = () => {
|
|
398
|
-
fetchAllParts()
|
|
400
|
+
// Runs the download pipeline with the head chunk fetched above (for its metadata): it is
|
|
401
|
+
// written to the decoder stream and the remaining chunks follow
|
|
402
|
+
let runFetchAllParts = (head) => {
|
|
403
|
+
fetchAllParts(head)
|
|
399
404
|
.catch(err => {
|
|
400
405
|
if (!fetchAborted && stream && !stream.destroyed) {
|
|
401
406
|
stream.emit('error', err);
|
|
@@ -427,36 +432,8 @@ async function downloadMessage(client, range, part, options) {
|
|
|
427
432
|
// for a routine-looking connection code.
|
|
428
433
|
.catch(err => client.log.error({ msg: 'Failed to fail the download stream', err, cid: client.id }));
|
|
429
434
|
};
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
try {
|
|
433
|
-
writeResult = writeChunk(chunk);
|
|
434
|
-
}
|
|
435
|
-
catch (err) {
|
|
436
|
-
stream.emit('error', err);
|
|
437
|
-
finishDownload();
|
|
438
|
-
/* c8 ignore next 3 */ // emitting the error above triggers cleanup (fetchAborted=true), so this end() guard is already false here
|
|
439
|
-
if (!fetchAborted && stream && !stream.destroyed) {
|
|
440
|
-
stream.end();
|
|
441
|
-
}
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
/* c8 ignore next 9 */ // `stream` is piped to the limiter before this runs, so the head write drains synchronously and always returns true (verified for chunkSize up to 8MB); the drain-wait branch is unreachable
|
|
445
|
-
if (!writeResult) {
|
|
446
|
-
// Initial chunk filled the buffer, wait for drain
|
|
447
|
-
stream.once('drain', () => {
|
|
448
|
-
if (!fetchAborted) {
|
|
449
|
-
runFetchAllParts();
|
|
450
|
-
}
|
|
451
|
-
else {
|
|
452
|
-
finishDownload();
|
|
453
|
-
}
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
else {
|
|
457
|
-
runFetchAllParts();
|
|
458
|
-
}
|
|
459
|
-
});
|
|
435
|
+
// Deferred so the caller gets the {meta, content} return value before streaming begins
|
|
436
|
+
setImmediate(() => runFetchAllParts(chunk));
|
|
460
437
|
return {
|
|
461
438
|
meta,
|
|
462
439
|
content: output
|
|
@@ -476,14 +453,16 @@ async function downloadMessageParts(client, range, parts, options) {
|
|
|
476
453
|
// no mailbox selected, nothing to do
|
|
477
454
|
return {};
|
|
478
455
|
}
|
|
479
|
-
let downloadOptions =
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
456
|
+
let downloadOptions = options || {};
|
|
457
|
+
// Asked as a partial fetch so at most maxBytes of each part crosses the wire, and enforced
|
|
458
|
+
// again on the answer for servers that ignore the partial specifier
|
|
459
|
+
let maxBytes = (0, limited_passthrough_js_1.normalizeByteLimit)(downloadOptions.maxBytes);
|
|
483
460
|
let query = { bodyParts: [] };
|
|
484
461
|
for (let part of parts) {
|
|
485
462
|
query.bodyParts.push(part + '.mime');
|
|
486
|
-
|
|
463
|
+
// The partial specifier carries a 32-bit length (RFC 9051 "number"), so a cap beyond
|
|
464
|
+
// that is applied on the answer alone
|
|
465
|
+
query.bodyParts.push(maxBytes > 0xffffffff ? part : { key: part, start: 0, maxLength: maxBytes });
|
|
487
466
|
}
|
|
488
467
|
let response = await client.fetchOne(range, query, downloadOptions);
|
|
489
468
|
if (!response || !response.bodyParts) {
|
|
@@ -501,6 +480,18 @@ async function downloadMessageParts(client, range, parts, options) {
|
|
|
501
480
|
if (keyParts.length === 1) {
|
|
502
481
|
// content
|
|
503
482
|
let key = keyParts[0];
|
|
483
|
+
if (content.length > maxBytes) {
|
|
484
|
+
// The server ignored the partial specifier and sent the whole part, the quirk
|
|
485
|
+
// download() tolerates the same way: keep what was asked for
|
|
486
|
+
client.log.warn({
|
|
487
|
+
msg: 'Server returned more than the requested window, truncating the part',
|
|
488
|
+
part: key,
|
|
489
|
+
maxBytes,
|
|
490
|
+
received: content.length,
|
|
491
|
+
cid: client.id
|
|
492
|
+
});
|
|
493
|
+
content = content.subarray(0, maxBytes);
|
|
494
|
+
}
|
|
504
495
|
if (!data[key]) {
|
|
505
496
|
data[key] = { content };
|
|
506
497
|
}
|
|
@@ -24,6 +24,10 @@ const safeNumber = (value) => {
|
|
|
24
24
|
let num = Math.round(Number(value));
|
|
25
25
|
return Number.isSafeInteger(num) && num >= 0 ? num : 0;
|
|
26
26
|
};
|
|
27
|
+
// Log output stands in a placeholder for any value longer than this, so a log entry never
|
|
28
|
+
// carries a copy of a large token (a message body, a long unquoted server token)
|
|
29
|
+
const LOG_VALUE_LIMIT = 100;
|
|
30
|
+
const logPlaceholder = (length, kind) => `"(* ${length}B ${kind} *)"`;
|
|
27
31
|
// Characters that cannot appear in an IMAP quoted string: CR and LF terminate a
|
|
28
32
|
// command line, and NUL is outside the CHAR production entirely. A value carrying
|
|
29
33
|
// any of them has to be sent as a literal, so quoting it is never correct.
|
|
@@ -86,28 +90,27 @@ async function compiler(response, options) {
|
|
|
86
90
|
.concat(response.command ? emitEntry(' ' + response.command) : []);
|
|
87
91
|
let val;
|
|
88
92
|
let lastType;
|
|
93
|
+
// Set right after the compiler writes "(" or "[" itself, so the first element inside gets no
|
|
94
|
+
// leading space
|
|
95
|
+
let afterOpener = false;
|
|
89
96
|
let walk = async (node, options) => {
|
|
90
97
|
options = options || {};
|
|
91
|
-
// Determine whether a space separator is needed before this node.
|
|
92
|
-
// Inspect the last byte written to decide context.
|
|
93
|
-
let lastRespEntry = resp.length && resp[resp.length - 1];
|
|
94
|
-
let lastRespByte = (lastRespEntry && lastRespEntry.length && lastRespEntry[lastRespEntry.length - 1]) || '';
|
|
95
|
-
if (typeof lastRespByte === 'number') {
|
|
96
|
-
lastRespByte = String.fromCharCode(lastRespByte);
|
|
97
|
-
}
|
|
98
98
|
// Add a space separator when:
|
|
99
99
|
// - The previous token was a LITERAL. Literal data ends exactly at its declared length, so
|
|
100
100
|
// a following token always needs an explicit separator, even though the last written byte
|
|
101
101
|
// is arbitrary literal content.
|
|
102
|
-
// - Otherwise: there is something written already (resp is not empty) and the
|
|
103
|
-
// not
|
|
102
|
+
// - Otherwise: there is something written already (resp is not empty) and the compiler did
|
|
103
|
+
// not just open a list or a section. This is tracked rather than read back from the last
|
|
104
|
+
// written byte: a token value can itself end in "(", "[" or "<" (an atom such as "X["),
|
|
105
|
+
// and suppressing the space after it would fuse it with the next argument.
|
|
104
106
|
// A sub-array element in a consecutive-list context never gets one (no space between
|
|
105
107
|
// adjacent lists).
|
|
106
|
-
if (lastType === 'LITERAL' || (!
|
|
108
|
+
if (lastType === 'LITERAL' || (!afterOpener && resp.length)) {
|
|
107
109
|
if (!options.subArray) {
|
|
108
110
|
resp.push(emitEntry(' '));
|
|
109
111
|
}
|
|
110
112
|
}
|
|
113
|
+
afterOpener = false;
|
|
111
114
|
if (node && node.buffer && !Buffer.isBuffer(node)) {
|
|
112
115
|
// mongodb binary
|
|
113
116
|
node = node.buffer;
|
|
@@ -115,6 +118,7 @@ async function compiler(response, options) {
|
|
|
115
118
|
if (Array.isArray(node)) {
|
|
116
119
|
lastType = 'LIST';
|
|
117
120
|
resp.push(emitEntry('('));
|
|
121
|
+
afterOpener = true;
|
|
118
122
|
// check if we need to skip separator WS between two arrays
|
|
119
123
|
let subArray = node.length > 1 && Array.isArray(node[0]);
|
|
120
124
|
for (let child of node) {
|
|
@@ -124,6 +128,7 @@ async function compiler(response, options) {
|
|
|
124
128
|
await walk(child, { subArray });
|
|
125
129
|
}
|
|
126
130
|
resp.push(emitEntry(')'));
|
|
131
|
+
afterOpener = false;
|
|
127
132
|
return;
|
|
128
133
|
}
|
|
129
134
|
if (!node && typeof node !== 'string' && typeof node !== 'number' && !Buffer.isBuffer(node)) {
|
|
@@ -131,8 +136,8 @@ async function compiler(response, options) {
|
|
|
131
136
|
return;
|
|
132
137
|
}
|
|
133
138
|
if (typeof node === 'string' || Buffer.isBuffer(node)) {
|
|
134
|
-
if (isLogging && node.length >
|
|
135
|
-
resp.push(emitEntry(
|
|
139
|
+
if (isLogging && node.length > LOG_VALUE_LIMIT) {
|
|
140
|
+
resp.push(emitEntry(logPlaceholder(node.length, 'string')));
|
|
136
141
|
}
|
|
137
142
|
else {
|
|
138
143
|
resp.push(emitEntry(isLogging ? JSON.stringify(node.toString()) : quoteString(node.toString())));
|
|
@@ -151,7 +156,7 @@ async function compiler(response, options) {
|
|
|
151
156
|
switch (node.type.toUpperCase()) {
|
|
152
157
|
case 'LITERAL':
|
|
153
158
|
if (isLogging) {
|
|
154
|
-
resp.push(emitEntry(
|
|
159
|
+
resp.push(emitEntry(logPlaceholder(node.value.length, 'literal')));
|
|
155
160
|
}
|
|
156
161
|
else {
|
|
157
162
|
// The literal size marker counts octets - string values are written as
|
|
@@ -183,8 +188,8 @@ async function compiler(response, options) {
|
|
|
183
188
|
}
|
|
184
189
|
break;
|
|
185
190
|
case 'STRING':
|
|
186
|
-
if (isLogging && node.value.length >
|
|
187
|
-
resp.push(emitEntry(
|
|
191
|
+
if (isLogging && node.value.length > LOG_VALUE_LIMIT) {
|
|
192
|
+
resp.push(emitEntry(logPlaceholder(node.value.length, 'string')));
|
|
188
193
|
}
|
|
189
194
|
else {
|
|
190
195
|
val = (node.value || '').toString();
|
|
@@ -199,19 +204,22 @@ async function compiler(response, options) {
|
|
|
199
204
|
// when logging: the incoming token parser accepts sequence-shaped tokens
|
|
200
205
|
// this strict grammar rejects (an ESEARCH set like "1:2:3", a folder
|
|
201
206
|
// name like "12:30:00"), and re-compiling a server response for the log
|
|
202
|
-
// or for error text must never throw.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
207
|
+
// or for error text must never throw. An empty or missing set is refused
|
|
208
|
+
// too: it would put nothing on the wire and leave the next argument in its
|
|
209
|
+
// place.
|
|
210
|
+
// Emitted raw: the validated alphabet cannot contain a line terminator, and
|
|
211
|
+
// re-scanning a potentially multi-megabyte set in the choke point would
|
|
212
|
+
// double the cost of exactly the sets this branch exists for
|
|
213
|
+
if (!isLogging) {
|
|
214
|
+
val = node.value === null || node.value === undefined ? '' : node.value.toString();
|
|
215
|
+
if (!isValidSequenceSet(val)) {
|
|
206
216
|
let error = new Error('Invalid sequence set value');
|
|
207
217
|
error.code = 'InvalidSequenceSet';
|
|
208
218
|
throw error;
|
|
209
219
|
}
|
|
220
|
+
resp.push(emitEntry(val, { raw: true }));
|
|
210
221
|
}
|
|
211
|
-
if (node.value) {
|
|
212
|
-
// raw: the validated alphabet cannot contain a line terminator, and
|
|
213
|
-
// re-scanning a potentially multi-megabyte set in the choke point
|
|
214
|
-
// would double the cost of exactly the sets this branch exists for
|
|
222
|
+
else if (node.value) {
|
|
215
223
|
resp.push(emitEntry(node.value, { raw: true }));
|
|
216
224
|
}
|
|
217
225
|
break;
|
|
@@ -239,11 +247,16 @@ async function compiler(response, options) {
|
|
|
239
247
|
case 'SECTION':
|
|
240
248
|
val = (node.value || '').toString();
|
|
241
249
|
if (!node.section || val) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (node.value === '' ||
|
|
250
|
+
if (isLogging && val.length > LOG_VALUE_LIMIT) {
|
|
251
|
+
// A server can put a line's worth of bytes in one unquoted token
|
|
252
|
+
val = logPlaceholder(val.length, 'atom');
|
|
253
|
+
}
|
|
254
|
+
else if (node.value === '' ||
|
|
255
|
+
imap_formal_syntax_js_1.default.verify(val.charAt(0) === '\\' ? val.slice(1) : val, imap_formal_syntax_js_1.default['ATOM-CHAR']()) >= 0) {
|
|
256
|
+
// An empty value, or one with a character outside ATOM-CHAR (checked
|
|
257
|
+
// past a leading backslash, as system flags like \Seen carry one), goes
|
|
258
|
+
// out as an IMAP quoted string. JSON.stringify is used only for log
|
|
259
|
+
// output, where values are display-escaped.
|
|
247
260
|
val = isLogging ? JSON.stringify(val) : quoteString(val);
|
|
248
261
|
}
|
|
249
262
|
resp.push(emitEntry(val));
|
|
@@ -252,10 +265,12 @@ async function compiler(response, options) {
|
|
|
252
265
|
// e.g., BODY[HEADER.FIELDS (Subject)] or BODY[1.MIME]
|
|
253
266
|
if (node.section) {
|
|
254
267
|
resp.push(emitEntry('['));
|
|
268
|
+
afterOpener = true;
|
|
255
269
|
for (let child of node.section) {
|
|
256
270
|
await walk(child);
|
|
257
271
|
}
|
|
258
272
|
resp.push(emitEntry(']'));
|
|
273
|
+
afterOpener = false;
|
|
259
274
|
}
|
|
260
275
|
// Partial range: emit <origin.length> after the section brackets. Coerced
|
|
261
276
|
// rather than joined as-is: this is the last token component written
|
|
@@ -278,7 +293,8 @@ async function compiler(response, options) {
|
|
|
278
293
|
respParts.push(resp);
|
|
279
294
|
}
|
|
280
295
|
const compiled = respParts.map(part => Buffer.concat(part));
|
|
281
|
-
|
|
296
|
+
// without asArray there is a single part, returned as is instead of copied
|
|
297
|
+
return asArray ? compiled : compiled.length === 1 ? compiled[0] : Buffer.concat(compiled);
|
|
282
298
|
}
|
|
283
299
|
exports.default = compiler;
|
|
284
300
|
module.exports = exports.default;
|
|
@@ -2,6 +2,17 @@ import type { ImapFlowError } from '../errors.js';
|
|
|
2
2
|
export declare const MAX_LITERAL_SIZE: number;
|
|
3
3
|
export declare const MAX_LINE_SIZE: number;
|
|
4
4
|
export declare const MAX_RESPONSE_SIZE: number;
|
|
5
|
+
export declare const ERROR_CONTEXT_LENGTH = 1024;
|
|
6
|
+
/**
|
|
7
|
+
* The input a parse error carries: a bounded prefix with the full length.
|
|
8
|
+
*
|
|
9
|
+
* @param input - The input that failed to parse
|
|
10
|
+
* @returns The bounded prefix and the full length
|
|
11
|
+
*/
|
|
12
|
+
export declare const boundedInput: (input: string) => {
|
|
13
|
+
input: string;
|
|
14
|
+
inputLength: number;
|
|
15
|
+
};
|
|
5
16
|
/**
|
|
6
17
|
* Normalizes a configured size limit. A non-negative integer is honored as-is (including 0, which
|
|
7
18
|
* means "reject anything non-empty"), and `Infinity` disables the limit; anything else falls back
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// (ImapStream) and the standalone token parser cannot drift apart, and so the documented
|
|
4
4
|
// defaults in the ImapFlowOptions type describe both paths.
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.createLiteralTooLargeError = exports.normalizeLimit = exports.MAX_RESPONSE_SIZE = exports.MAX_LINE_SIZE = exports.MAX_LITERAL_SIZE = void 0;
|
|
6
|
+
exports.createLiteralTooLargeError = exports.normalizeLimit = exports.boundedInput = exports.ERROR_CONTEXT_LENGTH = exports.MAX_RESPONSE_SIZE = exports.MAX_LINE_SIZE = exports.MAX_LITERAL_SIZE = void 0;
|
|
7
7
|
// Maximum allowed literal size: 1GB (1073741824 bytes)
|
|
8
8
|
exports.MAX_LITERAL_SIZE = 1024 * 1024 * 1024;
|
|
9
9
|
// Default maximum length of a single line (a response without a literal). Matches the literal cap:
|
|
@@ -20,6 +20,21 @@ exports.MAX_LINE_SIZE = exports.MAX_LITERAL_SIZE;
|
|
|
20
20
|
// of exactly the maximum permitted size impossible to receive. Configuring both limits calls for
|
|
21
21
|
// the same headroom - set maxResponseSize above maxLiteralSize, not equal to it.
|
|
22
22
|
exports.MAX_RESPONSE_SIZE = 2 * exports.MAX_LITERAL_SIZE;
|
|
23
|
+
// How much of the offending input a parse error carries along. The error travels whole into log
|
|
24
|
+
// entries (pino copies every property of a logged error) and into the rejection of the command
|
|
25
|
+
// the line belonged to, and the line can be as long as the configured line cap.
|
|
26
|
+
exports.ERROR_CONTEXT_LENGTH = 1024;
|
|
27
|
+
/**
|
|
28
|
+
* The input a parse error carries: a bounded prefix with the full length.
|
|
29
|
+
*
|
|
30
|
+
* @param input - The input that failed to parse
|
|
31
|
+
* @returns The bounded prefix and the full length
|
|
32
|
+
*/
|
|
33
|
+
const boundedInput = (input) => ({
|
|
34
|
+
input: input.slice(0, exports.ERROR_CONTEXT_LENGTH),
|
|
35
|
+
inputLength: input.length
|
|
36
|
+
});
|
|
37
|
+
exports.boundedInput = boundedInput;
|
|
23
38
|
/**
|
|
24
39
|
* Normalizes a configured size limit. A non-negative integer is honored as-is (including 0, which
|
|
25
40
|
* means "reject anything non-empty"), and `Infinity` disables the limit; anything else falls back
|
|
@@ -21,6 +21,16 @@ export declare class ParserInstance {
|
|
|
21
21
|
* @param options.literals - Pre-parsed literal values from the stream.
|
|
22
22
|
*/
|
|
23
23
|
constructor(input?: Buffer | string | null | undefined, options?: ParserOptions | undefined);
|
|
24
|
+
/**
|
|
25
|
+
* The context a parse error carries: a bounded prefix of the input (and of the element that
|
|
26
|
+
* failed, when there is one) with their full lengths, and the position.
|
|
27
|
+
*
|
|
28
|
+
* @param element - The element that failed to parse
|
|
29
|
+
* @returns The context to attach to the error
|
|
30
|
+
*/
|
|
31
|
+
errorContext(element?: string | undefined): {
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
};
|
|
24
34
|
/**
|
|
25
35
|
* Extracts and returns the IMAP tag from the beginning of the response.
|
|
26
36
|
* The tag is typically "*" for untagged responses, "+" for continuation requests,
|
|
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
exports.ParserInstance = void 0;
|
|
8
8
|
const imap_formal_syntax_js_1 = __importDefault(require("./imap-formal-syntax.js"));
|
|
9
9
|
const token_parser_js_1 = require("./token-parser.js");
|
|
10
|
+
const limits_js_1 = require("./limits.js");
|
|
10
11
|
/**
|
|
11
12
|
* Parses a single IMAP response line into its structural components: tag, command,
|
|
12
13
|
* and attributes. Handles status responses (OK, NO, BAD, PREAUTH, BYE) with their
|
|
@@ -27,6 +28,22 @@ class ParserInstance {
|
|
|
27
28
|
this.remainder = this.input;
|
|
28
29
|
this.pos = 0;
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* The context a parse error carries: a bounded prefix of the input (and of the element that
|
|
33
|
+
* failed, when there is one) with their full lengths, and the position.
|
|
34
|
+
*
|
|
35
|
+
* @param element - The element that failed to parse
|
|
36
|
+
* @returns The context to attach to the error
|
|
37
|
+
*/
|
|
38
|
+
errorContext(element) {
|
|
39
|
+
let context = { ...(0, limits_js_1.boundedInput)(this.input), pos: this.pos };
|
|
40
|
+
if (element !== undefined) {
|
|
41
|
+
let bounded = (0, limits_js_1.boundedInput)(element);
|
|
42
|
+
context.element = bounded.input;
|
|
43
|
+
context.elementLength = bounded.inputLength;
|
|
44
|
+
}
|
|
45
|
+
return context;
|
|
46
|
+
}
|
|
30
47
|
/**
|
|
31
48
|
* Extracts and returns the IMAP tag from the beginning of the response.
|
|
32
49
|
* The tag is typically "*" for untagged responses, "+" for continuation requests,
|
|
@@ -128,7 +145,7 @@ class ParserInstance {
|
|
|
128
145
|
if (/^\s/.test(this.remainder)) {
|
|
129
146
|
let error = new Error(`Unexpected whitespace at position ${this.pos} [E1]`);
|
|
130
147
|
error.code = 'ParserError1';
|
|
131
|
-
error.parserContext =
|
|
148
|
+
error.parserContext = this.errorContext();
|
|
132
149
|
throw error;
|
|
133
150
|
}
|
|
134
151
|
if ((match = this.remainder.match(/^\s*[^\s]+(?=\s|$)/))) {
|
|
@@ -142,9 +159,7 @@ class ParserInstance {
|
|
|
142
159
|
let error = new Error(`Server returned an error: ${this.input}`);
|
|
143
160
|
error.code = 'ParserErrorExchange';
|
|
144
161
|
error.parserContext = {
|
|
145
|
-
|
|
146
|
-
element,
|
|
147
|
-
pos: this.pos,
|
|
162
|
+
...this.errorContext(element),
|
|
148
163
|
value: {
|
|
149
164
|
tag: '*',
|
|
150
165
|
command: 'BAD',
|
|
@@ -155,14 +170,14 @@ class ParserInstance {
|
|
|
155
170
|
}
|
|
156
171
|
let error = new Error(`Unexpected char at position ${this.pos + errPos} [E2: ${JSON.stringify(element.charAt(errPos))}]`);
|
|
157
172
|
error.code = 'ParserError2';
|
|
158
|
-
error.parserContext =
|
|
173
|
+
error.parserContext = this.errorContext(element);
|
|
159
174
|
throw error;
|
|
160
175
|
}
|
|
161
176
|
}
|
|
162
177
|
else {
|
|
163
178
|
let error = new Error(`Unexpected end of input at position ${this.pos} [E3]`);
|
|
164
179
|
error.code = 'ParserError3';
|
|
165
|
-
error.parserContext =
|
|
180
|
+
error.parserContext = this.errorContext();
|
|
166
181
|
throw error;
|
|
167
182
|
}
|
|
168
183
|
this.pos += match[0].length;
|
|
@@ -183,13 +198,13 @@ class ParserInstance {
|
|
|
183
198
|
}
|
|
184
199
|
let error = new Error(`Unexpected end of input at position ${this.pos} [E4]`);
|
|
185
200
|
error.code = 'ParserError4';
|
|
186
|
-
error.parserContext =
|
|
201
|
+
error.parserContext = this.errorContext();
|
|
187
202
|
throw error;
|
|
188
203
|
}
|
|
189
204
|
if (imap_formal_syntax_js_1.default.verify(this.remainder.charAt(0), imap_formal_syntax_js_1.default.SP()) >= 0) {
|
|
190
205
|
let error = new Error(`Unexpected char at position ${this.pos} [E5: ${JSON.stringify(this.remainder.charAt(0))}]`);
|
|
191
206
|
error.code = 'ParserError5';
|
|
192
|
-
error.parserContext =
|
|
207
|
+
error.parserContext = this.errorContext(this.remainder);
|
|
193
208
|
throw error;
|
|
194
209
|
}
|
|
195
210
|
this.pos++;
|
|
@@ -207,13 +222,13 @@ class ParserInstance {
|
|
|
207
222
|
if (!this.remainder.length) {
|
|
208
223
|
let error = new Error(`Unexpected end of input at position ${this.pos} [E6]`);
|
|
209
224
|
error.code = 'ParserError6';
|
|
210
|
-
error.parserContext =
|
|
225
|
+
error.parserContext = this.errorContext();
|
|
211
226
|
throw error;
|
|
212
227
|
}
|
|
213
228
|
if (/^\s/.test(this.remainder)) {
|
|
214
229
|
let error = new Error(`Unexpected whitespace at position ${this.pos} [E7]`);
|
|
215
230
|
error.code = 'ParserError7';
|
|
216
|
-
error.parserContext =
|
|
231
|
+
error.parserContext = this.errorContext(this.remainder);
|
|
217
232
|
throw error;
|
|
218
233
|
}
|
|
219
234
|
const tokenParser = new token_parser_js_1.TokenParser(this, this.pos, this.remainder, this.options);
|