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
|
@@ -19,6 +19,10 @@ const safeNumber = (value) => {
|
|
|
19
19
|
let num = Math.round(Number(value));
|
|
20
20
|
return Number.isSafeInteger(num) && num >= 0 ? num : 0;
|
|
21
21
|
};
|
|
22
|
+
// Log output stands in a placeholder for any value longer than this, so a log entry never
|
|
23
|
+
// carries a copy of a large token (a message body, a long unquoted server token)
|
|
24
|
+
const LOG_VALUE_LIMIT = 100;
|
|
25
|
+
const logPlaceholder = (length, kind) => `"(* ${length}B ${kind} *)"`;
|
|
22
26
|
// Characters that cannot appear in an IMAP quoted string: CR and LF terminate a
|
|
23
27
|
// command line, and NUL is outside the CHAR production entirely. A value carrying
|
|
24
28
|
// any of them has to be sent as a literal, so quoting it is never correct.
|
|
@@ -81,28 +85,27 @@ async function compiler(response, options) {
|
|
|
81
85
|
.concat(response.command ? emitEntry(' ' + response.command) : []);
|
|
82
86
|
let val;
|
|
83
87
|
let lastType;
|
|
88
|
+
// Set right after the compiler writes "(" or "[" itself, so the first element inside gets no
|
|
89
|
+
// leading space
|
|
90
|
+
let afterOpener = false;
|
|
84
91
|
let walk = async (node, options) => {
|
|
85
92
|
options = options || {};
|
|
86
|
-
// Determine whether a space separator is needed before this node.
|
|
87
|
-
// Inspect the last byte written to decide context.
|
|
88
|
-
let lastRespEntry = resp.length && resp[resp.length - 1];
|
|
89
|
-
let lastRespByte = (lastRespEntry && lastRespEntry.length && lastRespEntry[lastRespEntry.length - 1]) || '';
|
|
90
|
-
if (typeof lastRespByte === 'number') {
|
|
91
|
-
lastRespByte = String.fromCharCode(lastRespByte);
|
|
92
|
-
}
|
|
93
93
|
// Add a space separator when:
|
|
94
94
|
// - The previous token was a LITERAL. Literal data ends exactly at its declared length, so
|
|
95
95
|
// a following token always needs an explicit separator, even though the last written byte
|
|
96
96
|
// is arbitrary literal content.
|
|
97
|
-
// - Otherwise: there is something written already (resp is not empty) and the
|
|
98
|
-
// not
|
|
97
|
+
// - Otherwise: there is something written already (resp is not empty) and the compiler did
|
|
98
|
+
// not just open a list or a section. This is tracked rather than read back from the last
|
|
99
|
+
// written byte: a token value can itself end in "(", "[" or "<" (an atom such as "X["),
|
|
100
|
+
// and suppressing the space after it would fuse it with the next argument.
|
|
99
101
|
// A sub-array element in a consecutive-list context never gets one (no space between
|
|
100
102
|
// adjacent lists).
|
|
101
|
-
if (lastType === 'LITERAL' || (!
|
|
103
|
+
if (lastType === 'LITERAL' || (!afterOpener && resp.length)) {
|
|
102
104
|
if (!options.subArray) {
|
|
103
105
|
resp.push(emitEntry(' '));
|
|
104
106
|
}
|
|
105
107
|
}
|
|
108
|
+
afterOpener = false;
|
|
106
109
|
if (node && node.buffer && !Buffer.isBuffer(node)) {
|
|
107
110
|
// mongodb binary
|
|
108
111
|
node = node.buffer;
|
|
@@ -110,6 +113,7 @@ async function compiler(response, options) {
|
|
|
110
113
|
if (Array.isArray(node)) {
|
|
111
114
|
lastType = 'LIST';
|
|
112
115
|
resp.push(emitEntry('('));
|
|
116
|
+
afterOpener = true;
|
|
113
117
|
// check if we need to skip separator WS between two arrays
|
|
114
118
|
let subArray = node.length > 1 && Array.isArray(node[0]);
|
|
115
119
|
for (let child of node) {
|
|
@@ -119,6 +123,7 @@ async function compiler(response, options) {
|
|
|
119
123
|
await walk(child, { subArray });
|
|
120
124
|
}
|
|
121
125
|
resp.push(emitEntry(')'));
|
|
126
|
+
afterOpener = false;
|
|
122
127
|
return;
|
|
123
128
|
}
|
|
124
129
|
if (!node && typeof node !== 'string' && typeof node !== 'number' && !Buffer.isBuffer(node)) {
|
|
@@ -126,8 +131,8 @@ async function compiler(response, options) {
|
|
|
126
131
|
return;
|
|
127
132
|
}
|
|
128
133
|
if (typeof node === 'string' || Buffer.isBuffer(node)) {
|
|
129
|
-
if (isLogging && node.length >
|
|
130
|
-
resp.push(emitEntry(
|
|
134
|
+
if (isLogging && node.length > LOG_VALUE_LIMIT) {
|
|
135
|
+
resp.push(emitEntry(logPlaceholder(node.length, 'string')));
|
|
131
136
|
}
|
|
132
137
|
else {
|
|
133
138
|
resp.push(emitEntry(isLogging ? JSON.stringify(node.toString()) : quoteString(node.toString())));
|
|
@@ -146,7 +151,7 @@ async function compiler(response, options) {
|
|
|
146
151
|
switch (node.type.toUpperCase()) {
|
|
147
152
|
case 'LITERAL':
|
|
148
153
|
if (isLogging) {
|
|
149
|
-
resp.push(emitEntry(
|
|
154
|
+
resp.push(emitEntry(logPlaceholder(node.value.length, 'literal')));
|
|
150
155
|
}
|
|
151
156
|
else {
|
|
152
157
|
// The literal size marker counts octets - string values are written as
|
|
@@ -178,8 +183,8 @@ async function compiler(response, options) {
|
|
|
178
183
|
}
|
|
179
184
|
break;
|
|
180
185
|
case 'STRING':
|
|
181
|
-
if (isLogging && node.value.length >
|
|
182
|
-
resp.push(emitEntry(
|
|
186
|
+
if (isLogging && node.value.length > LOG_VALUE_LIMIT) {
|
|
187
|
+
resp.push(emitEntry(logPlaceholder(node.value.length, 'string')));
|
|
183
188
|
}
|
|
184
189
|
else {
|
|
185
190
|
val = (node.value || '').toString();
|
|
@@ -194,19 +199,22 @@ async function compiler(response, options) {
|
|
|
194
199
|
// when logging: the incoming token parser accepts sequence-shaped tokens
|
|
195
200
|
// this strict grammar rejects (an ESEARCH set like "1:2:3", a folder
|
|
196
201
|
// name like "12:30:00"), and re-compiling a server response for the log
|
|
197
|
-
// or for error text must never throw.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
202
|
+
// or for error text must never throw. An empty or missing set is refused
|
|
203
|
+
// too: it would put nothing on the wire and leave the next argument in its
|
|
204
|
+
// place.
|
|
205
|
+
// Emitted raw: the validated alphabet cannot contain a line terminator, and
|
|
206
|
+
// re-scanning a potentially multi-megabyte set in the choke point would
|
|
207
|
+
// double the cost of exactly the sets this branch exists for
|
|
208
|
+
if (!isLogging) {
|
|
209
|
+
val = node.value === null || node.value === undefined ? '' : node.value.toString();
|
|
210
|
+
if (!isValidSequenceSet(val)) {
|
|
201
211
|
let error = new Error('Invalid sequence set value');
|
|
202
212
|
error.code = 'InvalidSequenceSet';
|
|
203
213
|
throw error;
|
|
204
214
|
}
|
|
215
|
+
resp.push(emitEntry(val, { raw: true }));
|
|
205
216
|
}
|
|
206
|
-
if (node.value) {
|
|
207
|
-
// raw: the validated alphabet cannot contain a line terminator, and
|
|
208
|
-
// re-scanning a potentially multi-megabyte set in the choke point
|
|
209
|
-
// would double the cost of exactly the sets this branch exists for
|
|
217
|
+
else if (node.value) {
|
|
210
218
|
resp.push(emitEntry(node.value, { raw: true }));
|
|
211
219
|
}
|
|
212
220
|
break;
|
|
@@ -234,11 +242,16 @@ async function compiler(response, options) {
|
|
|
234
242
|
case 'SECTION':
|
|
235
243
|
val = (node.value || '').toString();
|
|
236
244
|
if (!node.section || val) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (node.value === '' ||
|
|
245
|
+
if (isLogging && val.length > LOG_VALUE_LIMIT) {
|
|
246
|
+
// A server can put a line's worth of bytes in one unquoted token
|
|
247
|
+
val = logPlaceholder(val.length, 'atom');
|
|
248
|
+
}
|
|
249
|
+
else if (node.value === '' ||
|
|
250
|
+
imapFormalSyntax.verify(val.charAt(0) === '\\' ? val.slice(1) : val, imapFormalSyntax['ATOM-CHAR']()) >= 0) {
|
|
251
|
+
// An empty value, or one with a character outside ATOM-CHAR (checked
|
|
252
|
+
// past a leading backslash, as system flags like \Seen carry one), goes
|
|
253
|
+
// out as an IMAP quoted string. JSON.stringify is used only for log
|
|
254
|
+
// output, where values are display-escaped.
|
|
242
255
|
val = isLogging ? JSON.stringify(val) : quoteString(val);
|
|
243
256
|
}
|
|
244
257
|
resp.push(emitEntry(val));
|
|
@@ -247,10 +260,12 @@ async function compiler(response, options) {
|
|
|
247
260
|
// e.g., BODY[HEADER.FIELDS (Subject)] or BODY[1.MIME]
|
|
248
261
|
if (node.section) {
|
|
249
262
|
resp.push(emitEntry('['));
|
|
263
|
+
afterOpener = true;
|
|
250
264
|
for (let child of node.section) {
|
|
251
265
|
await walk(child);
|
|
252
266
|
}
|
|
253
267
|
resp.push(emitEntry(']'));
|
|
268
|
+
afterOpener = false;
|
|
254
269
|
}
|
|
255
270
|
// Partial range: emit <origin.length> after the section brackets. Coerced
|
|
256
271
|
// rather than joined as-is: this is the last token component written
|
|
@@ -273,6 +288,7 @@ async function compiler(response, options) {
|
|
|
273
288
|
respParts.push(resp);
|
|
274
289
|
}
|
|
275
290
|
const compiled = respParts.map(part => Buffer.concat(part));
|
|
276
|
-
|
|
291
|
+
// without asArray there is a single part, returned as is instead of copied
|
|
292
|
+
return asArray ? compiled : compiled.length === 1 ? compiled[0] : Buffer.concat(compiled);
|
|
277
293
|
}
|
|
278
294
|
export default compiler;
|
|
@@ -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
|
|
@@ -17,6 +17,20 @@ export const MAX_LINE_SIZE = MAX_LITERAL_SIZE;
|
|
|
17
17
|
// of exactly the maximum permitted size impossible to receive. Configuring both limits calls for
|
|
18
18
|
// the same headroom - set maxResponseSize above maxLiteralSize, not equal to it.
|
|
19
19
|
export const MAX_RESPONSE_SIZE = 2 * MAX_LITERAL_SIZE;
|
|
20
|
+
// How much of the offending input a parse error carries along. The error travels whole into log
|
|
21
|
+
// entries (pino copies every property of a logged error) and into the rejection of the command
|
|
22
|
+
// the line belonged to, and the line can be as long as the configured line cap.
|
|
23
|
+
export const ERROR_CONTEXT_LENGTH = 1024;
|
|
24
|
+
/**
|
|
25
|
+
* The input a parse error carries: a bounded prefix with the full length.
|
|
26
|
+
*
|
|
27
|
+
* @param input - The input that failed to parse
|
|
28
|
+
* @returns The bounded prefix and the full length
|
|
29
|
+
*/
|
|
30
|
+
export const boundedInput = (input) => ({
|
|
31
|
+
input: input.slice(0, ERROR_CONTEXT_LENGTH),
|
|
32
|
+
inputLength: input.length
|
|
33
|
+
});
|
|
20
34
|
/**
|
|
21
35
|
* Normalizes a configured size limit. A non-negative integer is honored as-is (including 0, which
|
|
22
36
|
* 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,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint new-cap: 0 */
|
|
2
2
|
import imapFormalSyntax from './imap-formal-syntax.js';
|
|
3
3
|
import { TokenParser } from './token-parser.js';
|
|
4
|
+
import { boundedInput } from './limits.js';
|
|
4
5
|
/**
|
|
5
6
|
* Parses a single IMAP response line into its structural components: tag, command,
|
|
6
7
|
* and attributes. Handles status responses (OK, NO, BAD, PREAUTH, BYE) with their
|
|
@@ -21,6 +22,22 @@ export class ParserInstance {
|
|
|
21
22
|
this.remainder = this.input;
|
|
22
23
|
this.pos = 0;
|
|
23
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* The context a parse error carries: a bounded prefix of the input (and of the element that
|
|
27
|
+
* failed, when there is one) with their full lengths, and the position.
|
|
28
|
+
*
|
|
29
|
+
* @param element - The element that failed to parse
|
|
30
|
+
* @returns The context to attach to the error
|
|
31
|
+
*/
|
|
32
|
+
errorContext(element) {
|
|
33
|
+
let context = { ...boundedInput(this.input), pos: this.pos };
|
|
34
|
+
if (element !== undefined) {
|
|
35
|
+
let bounded = boundedInput(element);
|
|
36
|
+
context.element = bounded.input;
|
|
37
|
+
context.elementLength = bounded.inputLength;
|
|
38
|
+
}
|
|
39
|
+
return context;
|
|
40
|
+
}
|
|
24
41
|
/**
|
|
25
42
|
* Extracts and returns the IMAP tag from the beginning of the response.
|
|
26
43
|
* The tag is typically "*" for untagged responses, "+" for continuation requests,
|
|
@@ -122,7 +139,7 @@ export class ParserInstance {
|
|
|
122
139
|
if (/^\s/.test(this.remainder)) {
|
|
123
140
|
let error = new Error(`Unexpected whitespace at position ${this.pos} [E1]`);
|
|
124
141
|
error.code = 'ParserError1';
|
|
125
|
-
error.parserContext =
|
|
142
|
+
error.parserContext = this.errorContext();
|
|
126
143
|
throw error;
|
|
127
144
|
}
|
|
128
145
|
if ((match = this.remainder.match(/^\s*[^\s]+(?=\s|$)/))) {
|
|
@@ -136,9 +153,7 @@ export class ParserInstance {
|
|
|
136
153
|
let error = new Error(`Server returned an error: ${this.input}`);
|
|
137
154
|
error.code = 'ParserErrorExchange';
|
|
138
155
|
error.parserContext = {
|
|
139
|
-
|
|
140
|
-
element,
|
|
141
|
-
pos: this.pos,
|
|
156
|
+
...this.errorContext(element),
|
|
142
157
|
value: {
|
|
143
158
|
tag: '*',
|
|
144
159
|
command: 'BAD',
|
|
@@ -149,14 +164,14 @@ export class ParserInstance {
|
|
|
149
164
|
}
|
|
150
165
|
let error = new Error(`Unexpected char at position ${this.pos + errPos} [E2: ${JSON.stringify(element.charAt(errPos))}]`);
|
|
151
166
|
error.code = 'ParserError2';
|
|
152
|
-
error.parserContext =
|
|
167
|
+
error.parserContext = this.errorContext(element);
|
|
153
168
|
throw error;
|
|
154
169
|
}
|
|
155
170
|
}
|
|
156
171
|
else {
|
|
157
172
|
let error = new Error(`Unexpected end of input at position ${this.pos} [E3]`);
|
|
158
173
|
error.code = 'ParserError3';
|
|
159
|
-
error.parserContext =
|
|
174
|
+
error.parserContext = this.errorContext();
|
|
160
175
|
throw error;
|
|
161
176
|
}
|
|
162
177
|
this.pos += match[0].length;
|
|
@@ -177,13 +192,13 @@ export class ParserInstance {
|
|
|
177
192
|
}
|
|
178
193
|
let error = new Error(`Unexpected end of input at position ${this.pos} [E4]`);
|
|
179
194
|
error.code = 'ParserError4';
|
|
180
|
-
error.parserContext =
|
|
195
|
+
error.parserContext = this.errorContext();
|
|
181
196
|
throw error;
|
|
182
197
|
}
|
|
183
198
|
if (imapFormalSyntax.verify(this.remainder.charAt(0), imapFormalSyntax.SP()) >= 0) {
|
|
184
199
|
let error = new Error(`Unexpected char at position ${this.pos} [E5: ${JSON.stringify(this.remainder.charAt(0))}]`);
|
|
185
200
|
error.code = 'ParserError5';
|
|
186
|
-
error.parserContext =
|
|
201
|
+
error.parserContext = this.errorContext(this.remainder);
|
|
187
202
|
throw error;
|
|
188
203
|
}
|
|
189
204
|
this.pos++;
|
|
@@ -201,13 +216,13 @@ export class ParserInstance {
|
|
|
201
216
|
if (!this.remainder.length) {
|
|
202
217
|
let error = new Error(`Unexpected end of input at position ${this.pos} [E6]`);
|
|
203
218
|
error.code = 'ParserError6';
|
|
204
|
-
error.parserContext =
|
|
219
|
+
error.parserContext = this.errorContext();
|
|
205
220
|
throw error;
|
|
206
221
|
}
|
|
207
222
|
if (/^\s/.test(this.remainder)) {
|
|
208
223
|
let error = new Error(`Unexpected whitespace at position ${this.pos} [E7]`);
|
|
209
224
|
error.code = 'ParserError7';
|
|
210
|
-
error.parserContext =
|
|
225
|
+
error.parserContext = this.errorContext(this.remainder);
|
|
211
226
|
throw error;
|
|
212
227
|
}
|
|
213
228
|
const tokenParser = new TokenParser(this, this.pos, this.remainder, this.options);
|