imapflow 2.2.11 → 2.4.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/CHANGELOG.md +25 -0
- package/dist/cjs/commands/append.d.ts +1 -1
- package/dist/cjs/commands/append.js +15 -4
- package/dist/cjs/commands/compress.js +2 -2
- package/dist/cjs/commands/idle.js +9 -9
- package/dist/cjs/commands/status.js +2 -4
- package/dist/cjs/connection-deadline.d.ts +1 -1
- package/dist/cjs/connection-deadline.js +2 -4
- package/dist/cjs/download.js +42 -26
- package/dist/cjs/errors.d.ts +15 -0
- package/dist/cjs/errors.js +22 -0
- package/dist/cjs/handler/imap-compiler.d.ts +4 -4
- package/dist/cjs/handler/imap-compiler.js +18 -18
- package/dist/cjs/handler/imap-parser.js +28 -3
- package/dist/cjs/handler/imap-stream.js +9 -8
- package/dist/cjs/handler/limits.d.ts +1 -1
- package/dist/cjs/handler/limits.js +5 -7
- package/dist/cjs/handler/parser-instance.js +23 -38
- package/dist/cjs/handler/token-parser.d.ts +12 -0
- package/dist/cjs/handler/token-parser.js +44 -104
- package/dist/cjs/imap-flow.d.ts +4 -3
- package/dist/cjs/imap-flow.js +98 -72
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/cjs/proxy-connection.js +2 -5
- package/dist/cjs/search-compiler.js +2 -3
- package/dist/cjs/tools.d.ts +6 -3
- package/dist/cjs/tools.js +13 -13
- package/dist/cjs/types.d.ts +11 -0
- package/dist/esm/commands/append.d.ts +1 -1
- package/dist/esm/commands/append.js +15 -4
- package/dist/esm/commands/compress.js +2 -2
- package/dist/esm/commands/idle.js +9 -9
- package/dist/esm/commands/status.js +2 -4
- package/dist/esm/connection-deadline.d.ts +1 -1
- package/dist/esm/connection-deadline.js +2 -4
- package/dist/esm/download.js +42 -26
- package/dist/esm/errors.d.ts +15 -0
- package/dist/esm/errors.js +21 -0
- package/dist/esm/handler/imap-compiler.d.ts +4 -4
- package/dist/esm/handler/imap-compiler.js +18 -18
- package/dist/esm/handler/imap-parser.js +28 -3
- package/dist/esm/handler/imap-stream.js +9 -8
- package/dist/esm/handler/limits.d.ts +1 -1
- package/dist/esm/handler/limits.js +5 -7
- package/dist/esm/handler/parser-instance.js +23 -38
- package/dist/esm/handler/token-parser.d.ts +12 -0
- package/dist/esm/handler/token-parser.js +44 -104
- package/dist/esm/imap-flow.d.ts +4 -3
- package/dist/esm/imap-flow.js +99 -73
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/dist/esm/proxy-connection.js +2 -5
- package/dist/esm/search-compiler.js +2 -3
- package/dist/esm/tools.d.ts +6 -3
- package/dist/esm/tools.js +14 -14
- package/dist/esm/types.d.ts +11 -0
- package/package.json +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.4.0](https://github.com/postalsys/imapflow/compare/v2.3.0...v2.4.0) (2026-10-10)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* re-raise unexpected parser exceptions as coded ParserErrorInternal errors ([dda3f7e](https://github.com/postalsys/imapflow/commit/dda3f7e71e3c227479763a8c6af617fd4f57679a))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* keep append literals in sync with their data and fail downloads cleanly ([752248e](https://github.com/postalsys/imapflow/commit/752248e07fd8930cb0943287c32e3bab88484d80))
|
|
14
|
+
|
|
15
|
+
## [2.3.0](https://github.com/postalsys/imapflow/compare/v2.2.11...v2.3.0) (2026-10-08)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* emit an alert event for ALERT response codes ([dcc0e42](https://github.com/postalsys/imapflow/commit/dcc0e42cef3c5c51226b29fd3a6bddd844728357))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* a throttled login is not a rejected credential ([83fdbce](https://github.com/postalsys/imapflow/commit/83fdbce9968651c3d833b1e74bc20151c26fc769))
|
|
26
|
+
* ask the first chunk of a download again when the answer comes back empty ([83fdbce](https://github.com/postalsys/imapflow/commit/83fdbce9968651c3d833b1e74bc20151c26fc769))
|
|
27
|
+
|
|
3
28
|
## [2.2.11](https://github.com/postalsys/imapflow/compare/v2.2.10...v2.2.11) (2026-10-08)
|
|
4
29
|
|
|
5
30
|
|
|
@@ -19,4 +19,4 @@ export interface AppendResult extends AppendResponseObject {
|
|
|
19
19
|
* @returns Append result with UID info if available, or undefined if preconditions not met
|
|
20
20
|
* @throws {Error} If the APPEND command fails or message exceeds APPENDLIMIT
|
|
21
21
|
*/
|
|
22
|
-
export default function append(connection: ImapFlow, destination: string | string[], content: Buffer | string, flags?: string | string[] | undefined, idate?: Date | string | false | undefined): Promise<AppendResult | undefined>;
|
|
22
|
+
export default function append(connection: ImapFlow, destination: string | string[], content: Buffer | Uint8Array | string, flags?: string | string[] | undefined, idate?: Date | string | false | undefined): Promise<AppendResult | undefined>;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.default = append;
|
|
4
4
|
const tools_js_1 = require("../tools.js");
|
|
5
|
+
const errors_js_1 = require("../errors.js");
|
|
5
6
|
/**
|
|
6
7
|
* Appends a message to a mailbox.
|
|
7
8
|
*
|
|
@@ -18,14 +19,24 @@ async function append(connection, destination, content, flags, idate) {
|
|
|
18
19
|
// nothing to do here
|
|
19
20
|
return;
|
|
20
21
|
}
|
|
22
|
+
// The literal size marker is announced from these bytes, so anything that is not
|
|
23
|
+
// bytes (a Readable, a plain object) is refused here instead of desyncing the session
|
|
24
|
+
let message;
|
|
21
25
|
if (typeof content === 'string') {
|
|
22
|
-
|
|
26
|
+
message = Buffer.from(content);
|
|
27
|
+
}
|
|
28
|
+
else if (content instanceof Uint8Array) {
|
|
29
|
+
// a Buffer as is, any other Uint8Array as a view over the same memory, not a copy
|
|
30
|
+
message = Buffer.isBuffer(content) ? content : Buffer.from(content.buffer, content.byteOffset, content.byteLength);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
throw (0, errors_js_1.createImapError)('Message content must be a Buffer, Uint8Array or string', 'InvalidMessageContent');
|
|
23
34
|
}
|
|
24
35
|
// APPENDLIMIT capability (RFC 7889): server may advertise the maximum message
|
|
25
36
|
// size it accepts. Check before sending to avoid a wasted round-trip.
|
|
26
37
|
if (connection.capabilities.has('APPENDLIMIT')) {
|
|
27
38
|
let appendLimit = connection.capabilities.get('APPENDLIMIT');
|
|
28
|
-
if (typeof appendLimit === 'number' && appendLimit <
|
|
39
|
+
if (typeof appendLimit === 'number' && appendLimit < message.length) {
|
|
29
40
|
let err = new Error('Message content too big for APPENDLIMIT=' + appendLimit);
|
|
30
41
|
err.serverResponseCode = 'APPENDLIMIT';
|
|
31
42
|
throw err;
|
|
@@ -82,9 +93,9 @@ async function append(connection, destination, content, flags, idate) {
|
|
|
82
93
|
// Regular literals cannot contain NUL bytes per the IMAP grammar.
|
|
83
94
|
let isLiteral8 = false;
|
|
84
95
|
if (connection.capabilities.has('BINARY') && !connection.disableBinary) {
|
|
85
|
-
isLiteral8 =
|
|
96
|
+
isLiteral8 = message.indexOf(0) >= 0;
|
|
86
97
|
}
|
|
87
|
-
attributes.push({ type: 'LITERAL', value:
|
|
98
|
+
attributes.push({ type: 'LITERAL', value: message, isLiteral8 });
|
|
88
99
|
let map = { destination };
|
|
89
100
|
if (connection.mailbox && connection.mailbox.path) {
|
|
90
101
|
map.path = connection.mailbox.path;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.default = compress;
|
|
4
|
+
const errors_js_1 = require("../errors.js");
|
|
4
5
|
/**
|
|
5
6
|
* Requests DEFLATE compression from the server.
|
|
6
7
|
*
|
|
@@ -36,8 +37,7 @@ async function compress(connection) {
|
|
|
36
37
|
// back its unconsumed tail on unpipe so the transport can feed it into the
|
|
37
38
|
// inflater - not something a command module can reach from here.
|
|
38
39
|
if (response.hasTrailingData) {
|
|
39
|
-
let error =
|
|
40
|
-
error.code = 'COMPRESS_TRAILING_DATA';
|
|
40
|
+
let error = (0, errors_js_1.createImapError)('Server sent data between the COMPRESS response and the compression layer switch', 'COMPRESS_TRAILING_DATA');
|
|
41
41
|
connection.log.error({ err: error, cid: connection.id });
|
|
42
42
|
// Schedule the close before releasing parser backpressure, so the buffered
|
|
43
43
|
// deflate-framed bytes cannot settle anything before teardown begins. This is
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.default = idle;
|
|
4
4
|
const tools_js_1 = require("../tools.js");
|
|
5
|
+
const errors_js_1 = require("../errors.js");
|
|
5
6
|
const NOOP_INTERVAL = 2 * 60 * 1000;
|
|
6
7
|
/**
|
|
7
8
|
* Marks the connection as idling on behalf of one session and returns a release function.
|
|
@@ -127,14 +128,15 @@ async function runIdle(connection) {
|
|
|
127
128
|
}
|
|
128
129
|
catch (err) {
|
|
129
130
|
(0, tools_js_1.logConnectionError)(connection, 'IDLE session failed', err);
|
|
130
|
-
// A tagged NO or BAD only means the server
|
|
131
|
-
// so the waiters are released by the finally block below and their own commands
|
|
132
|
-
// Anything else (close, lost socket, parser failure) fails the waiters too.
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
// A tagged NO or BAD only means the server did not run IDLE; the connection is still
|
|
132
|
+
// usable, so the waiters are released by the finally block below and their own commands
|
|
133
|
+
// run. Anything else (close, lost socket, parser failure) fails the waiters too. Only a
|
|
134
|
+
// refusal turns IDLE off for the session, a throttled IDLE is tried again later.
|
|
135
|
+
let answeredByServer = err.responseStatus === 'NO' || err.responseStatus === 'BAD';
|
|
136
|
+
if ((0, tools_js_1.isServerRefusal)(err)) {
|
|
135
137
|
connection.skipIdle = true;
|
|
136
138
|
}
|
|
137
|
-
if (preCheckWaitQueue.length && !
|
|
139
|
+
if (preCheckWaitQueue.length && !answeredByServer) {
|
|
138
140
|
// One error for the whole queue: every waiter failed at the same site, for the same
|
|
139
141
|
// reason. Built inside the guard so a teardown with nothing queued - the common case -
|
|
140
142
|
// does not pay for an Error and its stack capture.
|
|
@@ -191,9 +193,7 @@ async function pollOnce(connection, session) {
|
|
|
191
193
|
highestModseq: true
|
|
192
194
|
});
|
|
193
195
|
if (!status) {
|
|
194
|
-
|
|
195
|
-
err.code = 'PollFailed';
|
|
196
|
-
throw err;
|
|
196
|
+
throw (0, errors_js_1.createImapError)('STATUS poll failed', 'PollFailed');
|
|
197
197
|
}
|
|
198
198
|
break;
|
|
199
199
|
}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.default = status;
|
|
4
4
|
const tools_js_1 = require("../tools.js");
|
|
5
5
|
const status_fields_js_1 = require("./status-fields.js");
|
|
6
|
+
const errors_js_1 = require("../errors.js");
|
|
6
7
|
// STATUS fields that also refresh the live mailbox state when the queried mailbox is the
|
|
7
8
|
// currently selected one. Keyed by the output property name parseStatusList() reports.
|
|
8
9
|
const MAILBOX_UPDATERS = {
|
|
@@ -99,10 +100,7 @@ async function status(connection, path, query) {
|
|
|
99
100
|
return false;
|
|
100
101
|
});
|
|
101
102
|
if (folders && !folders.length) {
|
|
102
|
-
|
|
103
|
-
error.code = 'NotFound';
|
|
104
|
-
error.response = err;
|
|
105
|
-
throw error;
|
|
103
|
+
throw (0, errors_js_1.createImapError)(`Mailbox doesn't exist: ${path}`, 'NotFound', { response: err });
|
|
106
104
|
}
|
|
107
105
|
}
|
|
108
106
|
connection.log.warn({ err, cid: connection.id });
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ConnectionDeadline = exports.CONNECT_TIMEOUT = void 0;
|
|
4
4
|
const tools_js_1 = require("./tools.js");
|
|
5
|
+
const errors_js_1 = require("./errors.js");
|
|
5
6
|
// Default upper bound for establishing a usable transport, including DNS and proxy negotiation.
|
|
6
7
|
exports.CONNECT_TIMEOUT = 90 * 1000;
|
|
7
8
|
/**
|
|
@@ -31,10 +32,7 @@ class ConnectionDeadline {
|
|
|
31
32
|
* @returns The shared `CONNECT_TIMEOUT` error.
|
|
32
33
|
*/
|
|
33
34
|
error() {
|
|
34
|
-
|
|
35
|
-
err.code = 'CONNECT_TIMEOUT';
|
|
36
|
-
err.details = { connectionTimeout: this.timeout };
|
|
37
|
-
return err;
|
|
35
|
+
return (0, errors_js_1.createImapError)('Failed to establish connection in required time', 'CONNECT_TIMEOUT', { details: { connectionTimeout: this.timeout } });
|
|
38
36
|
}
|
|
39
37
|
/**
|
|
40
38
|
* Maps a dependency's own expiry onto the shared `CONNECT_TIMEOUT` shape, so callers see one
|
package/dist/cjs/download.js
CHANGED
|
@@ -18,6 +18,7 @@ const limited_passthrough_js_1 = require("./limited-passthrough.js");
|
|
|
18
18
|
// What a wait on the head stream listens for: it can take more input, it failed, or it went
|
|
19
19
|
// away (a consumer destroying it closes it without a 'drain')
|
|
20
20
|
const DRAIN_WAIT_EVENTS = ['drain', 'error', 'close'];
|
|
21
|
+
const errors_js_1 = require("./errors.js");
|
|
21
22
|
const tools_js_1 = require("./tools.js");
|
|
22
23
|
const isEmptySection = (value) => !value?.length;
|
|
23
24
|
/**
|
|
@@ -88,10 +89,7 @@ async function fetchExpected(client, range, query, options, expected) {
|
|
|
88
89
|
cid: client.id
|
|
89
90
|
});
|
|
90
91
|
if (attempt >= MAX_FETCH_ATTEMPTS) {
|
|
91
|
-
|
|
92
|
-
err.code = 'DownloadIncomplete';
|
|
93
|
-
err.cid = client.id;
|
|
94
|
-
throw err;
|
|
92
|
+
throw (0, errors_js_1.createImapError)('Server kept answering with data of another request', 'DownloadIncomplete', { cid: client.id });
|
|
95
93
|
}
|
|
96
94
|
}
|
|
97
95
|
}
|
|
@@ -164,7 +162,13 @@ async function downloadMessage(client, range, part, options) {
|
|
|
164
162
|
// Special handling for part "1": in single-node emails (no childNodes),
|
|
165
163
|
// the body is accessed via "TEXT" rather than "1", and headers via
|
|
166
164
|
// "HEADER" instead of "1.MIME". Check bodyStructure to detect this.
|
|
167
|
-
let
|
|
165
|
+
let expectedUid = requestedUid(range, downloadOptions);
|
|
166
|
+
let response = await fetchExpected(client, range, { uid: true, bodyStructure: true }, downloadOptions, {
|
|
167
|
+
uid: expectedUid,
|
|
168
|
+
origins: new Map(),
|
|
169
|
+
// a message addressed by UID is taken to exist, see getNextPart() below
|
|
170
|
+
retryEmpty: expectedUid ? () => true : undefined
|
|
171
|
+
});
|
|
168
172
|
if (!response) {
|
|
169
173
|
return {};
|
|
170
174
|
}
|
|
@@ -186,7 +190,8 @@ async function downloadMessage(client, range, part, options) {
|
|
|
186
190
|
let fetchAborted = false;
|
|
187
191
|
// A consumer that gave up: its 'close' may still be a tick away from setting fetchAborted,
|
|
188
192
|
// so the stream's own flag is checked as well
|
|
189
|
-
|
|
193
|
+
// `output` is unset until the head chunk is in, and the head chunk's retry asks already
|
|
194
|
+
let downloadAborted = () => fetchAborted || !!output?.destroyed;
|
|
190
195
|
let getNextPart = async (query) => {
|
|
191
196
|
query = query || {};
|
|
192
197
|
let mimeKey;
|
|
@@ -220,14 +225,23 @@ async function downloadMessage(client, range, part, options) {
|
|
|
220
225
|
};
|
|
221
226
|
query.bodyParts.push(contentRequest);
|
|
222
227
|
}
|
|
228
|
+
let expectedUid = uid || requestedUid(range, downloadOptions);
|
|
223
229
|
let expected = {
|
|
224
|
-
uid:
|
|
230
|
+
uid: expectedUid,
|
|
225
231
|
origins: new Map([[part || '', processed]]),
|
|
226
|
-
//
|
|
227
|
-
|
|
232
|
+
// Every chunk after the first is of a message that was there a moment ago, and a
|
|
233
|
+
// message the caller addresses by UID is taken to exist: an empty answer may be a
|
|
234
|
+
// late one (Apache James), a message that is really gone costs two more requests
|
|
235
|
+
retryEmpty: processed > 0 || expectedUid ? () => !downloadAborted() : undefined
|
|
228
236
|
};
|
|
229
237
|
let response = await fetchExpected(client, range, query, downloadOptions, expected);
|
|
230
238
|
if (!response) {
|
|
239
|
+
// An empty answer also comes from a connection that closed while the consumer was
|
|
240
|
+
// paused between chunks (fetchOne() finds no mailbox then), and that is not a
|
|
241
|
+
// missing message: a caller that retries would skip it
|
|
242
|
+
if (!client.usable) {
|
|
243
|
+
throw client.createNoConnectionError(false, { rejectedFrom: 'download' });
|
|
244
|
+
}
|
|
231
245
|
return { response: false, chunk: false };
|
|
232
246
|
}
|
|
233
247
|
if (!uid && response.uid) {
|
|
@@ -364,13 +378,19 @@ async function downloadMessage(client, range, part, options) {
|
|
|
364
378
|
// would still pull the entire part off the wire.
|
|
365
379
|
let limiters = [];
|
|
366
380
|
let isLimited = () => limiters.some(entry => entry.limited);
|
|
381
|
+
// Every stage after the head, so a download that ends early can release all of them
|
|
382
|
+
let stages = [];
|
|
367
383
|
// Appending a stage means forwarding the current tail's errors to it before piping, so a
|
|
368
|
-
// failure anywhere reaches the stream the caller is reading
|
|
384
|
+
// failure anywhere reaches the stream the caller is reading. The stage is destroyed with
|
|
385
|
+
// the error rather than only emitting it: pipe() does not end its destination on a source
|
|
386
|
+
// error, so a consumer that only listens for 'finish' or 'close' (a plain .pipe() into a
|
|
387
|
+
// file) would otherwise wait forever.
|
|
369
388
|
let pipeStage = (stage) => {
|
|
370
389
|
output.on('error', err => {
|
|
371
|
-
stage.
|
|
390
|
+
stage.destroy(err);
|
|
372
391
|
});
|
|
373
392
|
output = output.pipe(stage);
|
|
393
|
+
stages.push(stage);
|
|
374
394
|
return stage;
|
|
375
395
|
};
|
|
376
396
|
let isTextNode = ['text/html', 'text/plain', 'text/x-amp-html'].includes(meta.contentType) || (part === '1' && !meta.contentType);
|
|
@@ -415,8 +435,8 @@ async function downloadMessage(client, range, part, options) {
|
|
|
415
435
|
// Cleanup function
|
|
416
436
|
const cleanup = () => {
|
|
417
437
|
fetchAborted = true;
|
|
418
|
-
|
|
419
|
-
|
|
438
|
+
for (let entry of [stream, ...stages]) {
|
|
439
|
+
entry.destroy();
|
|
420
440
|
}
|
|
421
441
|
};
|
|
422
442
|
// Listen for stream destruction
|
|
@@ -489,11 +509,7 @@ async function downloadMessage(client, range, part, options) {
|
|
|
489
509
|
// Loud on purpose. Everything written downstream by this point holds
|
|
490
510
|
// duplicated content, and a quiet stop is indistinguishable from a clean EOF,
|
|
491
511
|
// so the consumer would store a corrupt body believing it intact.
|
|
492
|
-
|
|
493
|
-
err.code = 'DownloadOverflow';
|
|
494
|
-
err.maxSize = maxTotalBytes;
|
|
495
|
-
err.cid = client.id;
|
|
496
|
-
throw err;
|
|
512
|
+
throw (0, errors_js_1.createImapError)('Download exceeded the expected message size', 'DownloadOverflow', { maxSize: maxTotalBytes, cid: client.id });
|
|
497
513
|
}
|
|
498
514
|
let { response, chunk } = await getNextPart();
|
|
499
515
|
// A consumer that gave up while the chunk was in flight
|
|
@@ -504,10 +520,7 @@ async function downloadMessage(client, range, part, options) {
|
|
|
504
520
|
// The message is gone mid-download (expunged by another client, or the
|
|
505
521
|
// mailbox was closed). Ending the stream here would pass the truncated body
|
|
506
522
|
// off as complete, so the consumer is told the same way as for an overflow.
|
|
507
|
-
|
|
508
|
-
err.code = 'DownloadIncomplete';
|
|
509
|
-
err.cid = client.id;
|
|
510
|
-
throw err;
|
|
523
|
+
throw (0, errors_js_1.createImapError)('Message disappeared before the download completed', 'DownloadIncomplete', { cid: client.id });
|
|
511
524
|
}
|
|
512
525
|
if (!chunk) {
|
|
513
526
|
break;
|
|
@@ -537,7 +550,7 @@ async function downloadMessage(client, range, part, options) {
|
|
|
537
550
|
fetchAllParts(head)
|
|
538
551
|
.catch(err => {
|
|
539
552
|
if (!fetchAborted && stream && !stream.destroyed) {
|
|
540
|
-
stream.
|
|
553
|
+
stream.destroy(err);
|
|
541
554
|
/* c8 ignore start */ // the else logs when a fetch error arrives after the stream was already torn down (timing-dependent)
|
|
542
555
|
}
|
|
543
556
|
else {
|
|
@@ -562,7 +575,7 @@ async function downloadMessage(client, range, part, options) {
|
|
|
562
575
|
// above rejects a promise nobody holds and takes the process down on
|
|
563
576
|
// unhandledRejection. Reaching it always means an invariant broke - the head
|
|
564
577
|
// stream kept pipeStage()'s error forwarder for the life of the download, so
|
|
565
|
-
//
|
|
578
|
+
// destroy(err) above has somewhere to go - which is why it logs at error even
|
|
566
579
|
// for a routine-looking connection code.
|
|
567
580
|
.catch(err => client.log.error({ msg: 'Failed to fail the download stream', err, cid: client.id }));
|
|
568
581
|
};
|
|
@@ -601,9 +614,12 @@ async function downloadMessageParts(client, range, parts, options) {
|
|
|
601
614
|
contentRequests.set(part, contentRequest);
|
|
602
615
|
query.bodyParts.push(contentRequest);
|
|
603
616
|
}
|
|
617
|
+
let expectedUid = requestedUid(range, downloadOptions);
|
|
604
618
|
let response = await fetchExpected(client, range, query, downloadOptions, {
|
|
605
|
-
uid:
|
|
606
|
-
origins: partialStarts(query.bodyParts)
|
|
619
|
+
uid: expectedUid,
|
|
620
|
+
origins: partialStarts(query.bodyParts),
|
|
621
|
+
// a message addressed by UID is taken to exist, as for download()
|
|
622
|
+
retryEmpty: expectedUid ? () => true : undefined
|
|
607
623
|
});
|
|
608
624
|
if (!response || !response.bodyParts) {
|
|
609
625
|
return {};
|
package/dist/cjs/errors.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export declare const ImapFlowErrorCode: {
|
|
|
29
29
|
readonly MissingServerExtension: "MissingServerExtension";
|
|
30
30
|
readonly ParserError: "ParserError";
|
|
31
31
|
readonly ParserErrorExchange: "ParserErrorExchange";
|
|
32
|
+
readonly ParserErrorInternal: "ParserErrorInternal";
|
|
32
33
|
readonly MAX_IMAP_NESTING_REACHED: "MAX_IMAP_NESTING_REACHED";
|
|
33
34
|
readonly LineTooLarge: "LineTooLarge";
|
|
34
35
|
readonly LiteralTooLarge: "LiteralTooLarge";
|
|
@@ -38,6 +39,7 @@ export declare const ImapFlowErrorCode: {
|
|
|
38
39
|
readonly InvalidTextValue: "InvalidTextValue";
|
|
39
40
|
readonly InvalidSequenceSet: "InvalidSequenceSet";
|
|
40
41
|
readonly InvalidSearchQuery: "InvalidSearchQuery";
|
|
42
|
+
readonly InvalidMessageContent: "InvalidMessageContent";
|
|
41
43
|
readonly DownloadOverflow: "DownloadOverflow";
|
|
42
44
|
readonly DownloadIncomplete: "DownloadIncomplete";
|
|
43
45
|
readonly ProxyError: "ProxyError";
|
|
@@ -48,6 +50,8 @@ export declare const ImapFlowErrorCode: {
|
|
|
48
50
|
};
|
|
49
51
|
/** One of the {@link ImapFlowErrorCode} values */
|
|
50
52
|
export type ImapFlowErrorCode = (typeof ImapFlowErrorCode)[keyof typeof ImapFlowErrorCode];
|
|
53
|
+
/** A numbered parser error code, `ParserError1` and up */
|
|
54
|
+
export type ParserErrorCode = `ParserError${number}`;
|
|
51
55
|
/**
|
|
52
56
|
* An Error raised by ImapFlow, with the extra properties the library attaches to describe
|
|
53
57
|
* the failure. Every property is optional: which ones are present depends on where the
|
|
@@ -132,3 +136,14 @@ export declare class AuthenticationFailure extends Error implements ImapFlowErro
|
|
|
132
136
|
response?: string | undefined;
|
|
133
137
|
oauthError?: any;
|
|
134
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Creates an {@link ImapFlowError} with a code and optional extra properties. The code is
|
|
141
|
+
* checked by the compiler against {@link ImapFlowErrorCode}, so a typo or an undeclared
|
|
142
|
+
* code fails the build instead of reaching consumers.
|
|
143
|
+
*
|
|
144
|
+
* @param message - The error message
|
|
145
|
+
* @param code - The error code
|
|
146
|
+
* @param props - Extra properties to set on the error
|
|
147
|
+
* @returns The error, ready to throw or reject with
|
|
148
|
+
*/
|
|
149
|
+
export declare function createImapError(message: string, code: ImapFlowErrorCode | ParserErrorCode, props?: Omit<ImapFlowError, 'name' | 'message' | 'stack' | 'code'> | undefined): ImapFlowError;
|
package/dist/cjs/errors.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.AuthenticationFailure = exports.ImapFlowErrorCode = void 0;
|
|
4
|
+
exports.createImapError = createImapError;
|
|
4
5
|
/**
|
|
5
6
|
* The `code` values ImapFlow sets on the errors it raises, so they can be matched without
|
|
6
7
|
* string literals: `if (err.code === ImapFlowErrorCode.NoConnection)`.
|
|
@@ -35,6 +36,8 @@ exports.ImapFlowErrorCode = {
|
|
|
35
36
|
// response parsing and size limits
|
|
36
37
|
ParserError: 'ParserError',
|
|
37
38
|
ParserErrorExchange: 'ParserErrorExchange',
|
|
39
|
+
// an unexpected exception inside the parser, re-raised with a code; a parser bug
|
|
40
|
+
ParserErrorInternal: 'ParserErrorInternal',
|
|
38
41
|
MAX_IMAP_NESTING_REACHED: 'MAX_IMAP_NESTING_REACHED',
|
|
39
42
|
LineTooLarge: 'LineTooLarge',
|
|
40
43
|
LiteralTooLarge: 'LiteralTooLarge',
|
|
@@ -45,6 +48,7 @@ exports.ImapFlowErrorCode = {
|
|
|
45
48
|
InvalidTextValue: 'InvalidTextValue',
|
|
46
49
|
InvalidSequenceSet: 'InvalidSequenceSet',
|
|
47
50
|
InvalidSearchQuery: 'InvalidSearchQuery',
|
|
51
|
+
InvalidMessageContent: 'InvalidMessageContent',
|
|
48
52
|
// download()
|
|
49
53
|
DownloadOverflow: 'DownloadOverflow',
|
|
50
54
|
DownloadIncomplete: 'DownloadIncomplete',
|
|
@@ -66,3 +70,21 @@ class AuthenticationFailure extends Error {
|
|
|
66
70
|
}
|
|
67
71
|
}
|
|
68
72
|
exports.AuthenticationFailure = AuthenticationFailure;
|
|
73
|
+
/**
|
|
74
|
+
* Creates an {@link ImapFlowError} with a code and optional extra properties. The code is
|
|
75
|
+
* checked by the compiler against {@link ImapFlowErrorCode}, so a typo or an undeclared
|
|
76
|
+
* code fails the build instead of reaching consumers.
|
|
77
|
+
*
|
|
78
|
+
* @param message - The error message
|
|
79
|
+
* @param code - The error code
|
|
80
|
+
* @param props - Extra properties to set on the error
|
|
81
|
+
* @returns The error, ready to throw or reject with
|
|
82
|
+
*/
|
|
83
|
+
function createImapError(message, code, props) {
|
|
84
|
+
const error = new Error(message);
|
|
85
|
+
error.code = code;
|
|
86
|
+
if (props) {
|
|
87
|
+
Object.assign(error, props);
|
|
88
|
+
}
|
|
89
|
+
return error;
|
|
90
|
+
}
|
|
@@ -12,13 +12,13 @@ import type { CompilerOptions, ImapCompileInput } from './types.js';
|
|
|
12
12
|
* @param options.isLogging - If true, redacts sensitive values and truncates long strings/literals for logging purposes.
|
|
13
13
|
* @param options.literalPlus - If true, uses the LITERAL+ extension (appends "+" to literal length markers).
|
|
14
14
|
* @param options.literalMinus - If true, uses the LITERAL- extension for literals up to 4096 bytes.
|
|
15
|
-
* @returns
|
|
15
|
+
* @returns An array of Buffers (if asArray is true) or a single concatenated Buffer.
|
|
16
16
|
*/
|
|
17
17
|
declare function compiler(response: ImapCompileInput, options: CompilerOptions & {
|
|
18
18
|
asArray: true;
|
|
19
|
-
}):
|
|
19
|
+
}): Buffer[];
|
|
20
20
|
declare function compiler(response: ImapCompileInput, options?: (CompilerOptions & {
|
|
21
21
|
asArray?: false | undefined;
|
|
22
|
-
}) | undefined):
|
|
23
|
-
declare function compiler(response: ImapCompileInput, options?: CompilerOptions | undefined):
|
|
22
|
+
}) | undefined): Buffer;
|
|
23
|
+
declare function compiler(response: ImapCompileInput, options?: CompilerOptions | undefined): Buffer | Buffer[];
|
|
24
24
|
export default compiler;
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
const imap_formal_syntax_js_1 = __importDefault(require("./imap-formal-syntax.js"));
|
|
8
|
+
const errors_js_1 = require("../errors.js");
|
|
8
9
|
// A single element of a sequence-set as defined by the RFC 9051 grammar: a number
|
|
9
10
|
// or a range, where "*" stands for the largest number in use. Digit strings are not
|
|
10
11
|
// range-checked here (a server rejects "0" or an overlong number on its own); the
|
|
@@ -46,13 +47,11 @@ const CRLF = /[\r\n]/;
|
|
|
46
47
|
*/
|
|
47
48
|
const quoteString = (value) => {
|
|
48
49
|
if (NOT_QUOTABLE.test(value)) {
|
|
49
|
-
|
|
50
|
-
error.code = 'InvalidStringValue';
|
|
51
|
-
throw error;
|
|
50
|
+
throw (0, errors_js_1.createImapError)('Unquotable character in IMAP string value', 'InvalidStringValue');
|
|
52
51
|
}
|
|
53
52
|
return '"' + value.replace(/["\\]/g, char => '\\' + char) + '"';
|
|
54
53
|
};
|
|
55
|
-
|
|
54
|
+
function compiler(response, options) {
|
|
56
55
|
let { asArray, isLogging, literalPlus, literalMinus } = options || {};
|
|
57
56
|
const respParts = [];
|
|
58
57
|
// Formats an entry (string, number or Buffer) into the Buffer that is written to
|
|
@@ -67,9 +66,7 @@ async function compiler(response, options) {
|
|
|
67
66
|
const emitEntry = (entry, opts) => {
|
|
68
67
|
let { returnEmpty, raw } = opts || {};
|
|
69
68
|
if (!raw && !isLogging && (typeof entry === 'string' || Buffer.isBuffer(entry)) && CRLF.test(entry.toString('latin1'))) {
|
|
70
|
-
|
|
71
|
-
error.code = 'InvalidTokenValue';
|
|
72
|
-
throw error;
|
|
69
|
+
throw (0, errors_js_1.createImapError)('Line terminator in IMAP token', 'InvalidTokenValue');
|
|
73
70
|
}
|
|
74
71
|
if (typeof entry === 'string') {
|
|
75
72
|
return Buffer.from(entry);
|
|
@@ -93,7 +90,7 @@ async function compiler(response, options) {
|
|
|
93
90
|
// Set right after the compiler writes "(" or "[" itself, so the first element inside gets no
|
|
94
91
|
// leading space
|
|
95
92
|
let afterOpener = false;
|
|
96
|
-
let walk =
|
|
93
|
+
let walk = (node, options) => {
|
|
97
94
|
options = options || {};
|
|
98
95
|
// Add a space separator when:
|
|
99
96
|
// - The previous token was a LITERAL. Literal data ends exactly at its declared length, so
|
|
@@ -125,7 +122,7 @@ async function compiler(response, options) {
|
|
|
125
122
|
if (subArray && !Array.isArray(child)) {
|
|
126
123
|
subArray = false;
|
|
127
124
|
}
|
|
128
|
-
|
|
125
|
+
walk(child, { subArray });
|
|
129
126
|
}
|
|
130
127
|
resp.push(emitEntry(')'));
|
|
131
128
|
afterOpener = false;
|
|
@@ -159,6 +156,11 @@ async function compiler(response, options) {
|
|
|
159
156
|
resp.push(emitEntry(logPlaceholder(node.value.length, 'literal')));
|
|
160
157
|
}
|
|
161
158
|
else {
|
|
159
|
+
// The size marker must match the bytes emitEntry() writes, and it writes
|
|
160
|
+
// nothing for a value that is not a Buffer or a string
|
|
161
|
+
if (node.value && !Buffer.isBuffer(node.value) && typeof node.value !== 'string') {
|
|
162
|
+
throw (0, errors_js_1.createImapError)('Literal value must be a Buffer or a string', 'InvalidTokenValue');
|
|
163
|
+
}
|
|
162
164
|
// The literal size marker counts octets - string values are written as
|
|
163
165
|
// UTF-8, so their UTF-16 .length would undercount multi-byte characters
|
|
164
166
|
let literalLength = !node.value ? 0 : Buffer.isBuffer(node.value) ? node.value.length : Buffer.byteLength(node.value.toString());
|
|
@@ -213,9 +215,7 @@ async function compiler(response, options) {
|
|
|
213
215
|
if (!isLogging) {
|
|
214
216
|
val = node.value === null || node.value === undefined ? '' : node.value.toString();
|
|
215
217
|
if (!isValidSequenceSet(val)) {
|
|
216
|
-
|
|
217
|
-
error.code = 'InvalidSequenceSet';
|
|
218
|
-
throw error;
|
|
218
|
+
throw (0, errors_js_1.createImapError)('Invalid sequence set value', 'InvalidSequenceSet');
|
|
219
219
|
}
|
|
220
220
|
resp.push(emitEntry(val, { raw: true }));
|
|
221
221
|
}
|
|
@@ -230,9 +230,7 @@ async function compiler(response, options) {
|
|
|
230
230
|
// this check runs first only to raise the more specific InvalidTextValue code.
|
|
231
231
|
if (node.value) {
|
|
232
232
|
if (!isLogging && CRLF.test(node.value.toString())) {
|
|
233
|
-
|
|
234
|
-
error.code = 'InvalidTextValue';
|
|
235
|
-
throw error;
|
|
233
|
+
throw (0, errors_js_1.createImapError)('Line terminator in IMAP text value', 'InvalidTextValue');
|
|
236
234
|
}
|
|
237
235
|
resp.push(emitEntry(node.value));
|
|
238
236
|
}
|
|
@@ -267,7 +265,7 @@ async function compiler(response, options) {
|
|
|
267
265
|
resp.push(emitEntry('['));
|
|
268
266
|
afterOpener = true;
|
|
269
267
|
for (let child of node.section) {
|
|
270
|
-
|
|
268
|
+
walk(child);
|
|
271
269
|
}
|
|
272
270
|
resp.push(emitEntry(']'));
|
|
273
271
|
afterOpener = false;
|
|
@@ -286,13 +284,15 @@ async function compiler(response, options) {
|
|
|
286
284
|
if (response.attributes) {
|
|
287
285
|
let attributes = Array.isArray(response.attributes) ? response.attributes : [].concat(response.attributes);
|
|
288
286
|
for (let child of attributes) {
|
|
289
|
-
|
|
287
|
+
walk(child);
|
|
290
288
|
}
|
|
291
289
|
}
|
|
292
290
|
if (resp.length) {
|
|
293
291
|
respParts.push(resp);
|
|
294
292
|
}
|
|
295
|
-
|
|
293
|
+
// A part with a single entry (the data of a synchronizing literal) is used as is: copying a
|
|
294
|
+
// large message only to send it doubles its memory
|
|
295
|
+
const compiled = respParts.map(part => (part.length === 1 ? part[0] : Buffer.concat(part)));
|
|
296
296
|
// without asArray there is a single part, returned as is instead of copied
|
|
297
297
|
return asArray ? compiled : compiled.length === 1 ? compiled[0] : Buffer.concat(compiled);
|
|
298
298
|
}
|
|
@@ -6,6 +6,31 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.default = parser;
|
|
7
7
|
const imap_formal_syntax_js_1 = __importDefault(require("./imap-formal-syntax.js"));
|
|
8
8
|
const parser_instance_js_1 = require("./parser-instance.js");
|
|
9
|
+
const errors_js_1 = require("../errors.js");
|
|
10
|
+
const limits_js_1 = require("./limits.js");
|
|
11
|
+
// The codes the parser raises on purpose: the numbered ParserErrorN codes, ParserErrorExchange,
|
|
12
|
+
// the nesting limit and the inline literal size limit
|
|
13
|
+
const PARSER_ERROR_CODES = /^(?:ParserError\d+|ParserErrorExchange|MAX_IMAP_NESTING_REACHED|LiteralTooLarge)$/;
|
|
14
|
+
/**
|
|
15
|
+
* Anything the parser throws other than one of its own coded errors - a TypeError from a parser
|
|
16
|
+
* bug, a RangeError from runaway recursion or a bad Buffer offset (which carries a Node code), a
|
|
17
|
+
* thrown non-Error - is re-raised as a coded `ParserErrorInternal`, so callers that match on
|
|
18
|
+
* codes still handle it and settle the command the line belonged to. The fuzz suite treats this
|
|
19
|
+
* code as a failure, so the guard does not hide parser bugs from the tests.
|
|
20
|
+
*
|
|
21
|
+
* @param err - The thrown value
|
|
22
|
+
* @param input - The line that was being parsed
|
|
23
|
+
* @returns The error to rethrow
|
|
24
|
+
*/
|
|
25
|
+
function asParserError(err, input) {
|
|
26
|
+
if (err instanceof Error && PARSER_ERROR_CODES.test(err.code || '')) {
|
|
27
|
+
return err;
|
|
28
|
+
}
|
|
29
|
+
return (0, errors_js_1.createImapError)(`Unexpected parser failure: ${err instanceof Error ? err.message : String(err)}`, 'ParserErrorInternal', {
|
|
30
|
+
parserContext: (0, limits_js_1.boundedInput)(input.toString()),
|
|
31
|
+
...(err instanceof Error ? { _err: err } : {})
|
|
32
|
+
});
|
|
33
|
+
}
|
|
9
34
|
/**
|
|
10
35
|
* Parses a raw IMAP command or response buffer into a structured object.
|
|
11
36
|
* Handles edge cases such as null-byte-padded responses from buggy servers and
|
|
@@ -26,7 +51,7 @@ async function parser(command, options) {
|
|
|
26
51
|
// Workaround for buggy IMAP servers that pad responses with leading NUL (\x00) bytes.
|
|
27
52
|
// Some servers (observed in the wild) prepend null bytes to their output, which would
|
|
28
53
|
// cause parsing to fail. We strip them and note how many were removed for diagnostics.
|
|
29
|
-
if (command[0] === 0) {
|
|
54
|
+
if (Buffer.isBuffer(command) && command[0] === 0) {
|
|
30
55
|
// find the first non null byte and trim
|
|
31
56
|
let firstNonNull = -1;
|
|
32
57
|
for (let i = 0; i < command.length; i++) {
|
|
@@ -39,7 +64,7 @@ async function parser(command, options) {
|
|
|
39
64
|
// All bytes are null, treat as a BAD response
|
|
40
65
|
return { tag: '*', command: 'BAD', attributes: [] };
|
|
41
66
|
}
|
|
42
|
-
command = command.
|
|
67
|
+
command = command.subarray(firstNonNull);
|
|
43
68
|
nullBytesRemoved = firstNonNull;
|
|
44
69
|
}
|
|
45
70
|
const parserInstance = new parser_instance_js_1.ParserInstance(command, options);
|
|
@@ -71,7 +96,7 @@ async function parser(command, options) {
|
|
|
71
96
|
}
|
|
72
97
|
}
|
|
73
98
|
catch (err) {
|
|
74
|
-
let error = err;
|
|
99
|
+
let error = asParserError(err, command);
|
|
75
100
|
if (error.code === 'ParserErrorExchange' && error.parserContext && error.parserContext.value) {
|
|
76
101
|
return error.parserContext.value;
|
|
77
102
|
}
|