imapflow 1.2.8 → 1.2.9
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/.ncurc.js +1 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +7 -0
- package/README.md +3 -3
- package/lib/charsets.js +15 -0
- package/lib/commands/append.js +62 -54
- package/lib/commands/authenticate.js +99 -52
- package/lib/commands/capability.js +12 -2
- package/lib/commands/close.js +11 -1
- package/lib/commands/compress.js +10 -1
- package/lib/commands/copy.js +18 -1
- package/lib/commands/create.js +15 -2
- package/lib/commands/delete.js +10 -1
- package/lib/commands/enable.js +12 -1
- package/lib/commands/expunge.js +18 -2
- package/lib/commands/fetch.js +39 -4
- package/lib/commands/id.js +22 -3
- package/lib/commands/idle.js +39 -4
- package/lib/commands/list.js +86 -48
- package/lib/commands/login.js +12 -1
- package/lib/commands/logout.js +11 -2
- package/lib/commands/move.js +17 -1
- package/lib/commands/namespace.js +32 -2
- package/lib/commands/noop.js +6 -1
- package/lib/commands/quota.js +33 -14
- package/lib/commands/rename.js +13 -1
- package/lib/commands/search.js +16 -1
- package/lib/commands/select.js +76 -33
- package/lib/commands/starttls.js +6 -1
- package/lib/commands/status.js +64 -52
- package/lib/commands/store.js +27 -4
- package/lib/commands/subscribe.js +7 -1
- package/lib/commands/unsubscribe.js +7 -1
- package/lib/handler/imap-compiler.js +44 -2
- package/lib/handler/imap-formal-syntax.js +51 -3
- package/lib/handler/imap-handler.js +8 -0
- package/lib/handler/imap-parser.js +23 -2
- package/lib/handler/imap-stream.js +84 -31
- package/lib/handler/parser-instance.js +61 -1
- package/lib/handler/token-parser.js +66 -9
- package/lib/imap-commands.js +11 -0
- package/lib/imap-flow.d.ts +6 -0
- package/lib/imap-flow.js +164 -42
- package/lib/jp-decoder.js +10 -0
- package/lib/limited-passthrough.js +12 -5
- package/lib/proxy-connection.js +18 -12
- package/lib/search-compiler.js +3 -11
- package/lib/special-use.js +23 -16
- package/lib/tools.js +218 -13
- package/package.json +4 -11
- package/test/commands-integration-test.js +33 -0
- package/test/special-use-test.js +32 -0
- package/assets/favicon.ico +0 -0
- package/jsdoc.json +0 -28
|
@@ -15,9 +15,27 @@ const STATE_TEXT = 0x007;
|
|
|
15
15
|
const RE_DIGITS = /^\d+$/;
|
|
16
16
|
const RE_SINGLE_DIGIT = /^\d$/;
|
|
17
17
|
|
|
18
|
+
// Prevents stack overflow from maliciously crafted deeply-nested IMAP input (e.g., (((((...))))))
|
|
18
19
|
const MAX_NODE_DEPTH = 25;
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Tokenizes an IMAP attribute string into a tree of typed nodes.
|
|
23
|
+
* Handles all IMAP data types: atoms, quoted strings, literals (including literal8),
|
|
24
|
+
* sequences, lists (parenthesized groups), sections (bracketed groups), and partial ranges.
|
|
25
|
+
* Enforces a maximum nesting depth of {@link MAX_NODE_DEPTH} to prevent stack overflow
|
|
26
|
+
* from malicious input.
|
|
27
|
+
*/
|
|
20
28
|
class TokenParser {
|
|
29
|
+
/**
|
|
30
|
+
* Creates a new TokenParser.
|
|
31
|
+
*
|
|
32
|
+
* @param {ParserInstance} parent - The parent ParserInstance that owns this token parser. Used to access the parsed command for context-sensitive parsing.
|
|
33
|
+
* @param {number} startPos - The starting position offset in the original input, used for error reporting.
|
|
34
|
+
* @param {string} str - The attribute string to tokenize.
|
|
35
|
+
* @param {Object} [options] - Parser options.
|
|
36
|
+
* @param {boolean} [options.literalPlus] - Whether the LITERAL+ extension is in use.
|
|
37
|
+
* @param {Array<Buffer>} [options.literals] - Pre-parsed literal values from the input stream.
|
|
38
|
+
*/
|
|
21
39
|
constructor(parent, startPos, str, options) {
|
|
22
40
|
this.str = (str || '').toString();
|
|
23
41
|
this.options = options || {};
|
|
@@ -31,6 +49,15 @@ class TokenParser {
|
|
|
31
49
|
this.state = STATE_NORMAL;
|
|
32
50
|
}
|
|
33
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Processes the input string and returns the parsed attributes as a flat array of typed objects.
|
|
54
|
+
* Each attribute is an object with a `type` (e.g., "ATOM", "STRING", "LITERAL", "SEQUENCE")
|
|
55
|
+
* and a `value` property. Lists are represented as nested arrays. Sections and partials are
|
|
56
|
+
* attached as properties on the preceding attribute object.
|
|
57
|
+
*
|
|
58
|
+
* @returns {Promise<Array>} A promise that resolves to an array of parsed attribute objects and nested arrays.
|
|
59
|
+
* @throws {Error} If the input contains syntax errors or unclosed nodes.
|
|
60
|
+
*/
|
|
34
61
|
async getAttributes() {
|
|
35
62
|
await this.processString();
|
|
36
63
|
|
|
@@ -108,6 +135,16 @@ class TokenParser {
|
|
|
108
135
|
return attributes;
|
|
109
136
|
}
|
|
110
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Creates a new node in the parse tree. Each node represents a token or structural
|
|
140
|
+
* element (e.g., atom, string, literal, list, section, partial). The node is automatically
|
|
141
|
+
* appended to the parent's childNodes array if a parent is provided.
|
|
142
|
+
*
|
|
143
|
+
* @param {Object} [parentNode] - The parent node to attach this node to. If omitted, creates a root node.
|
|
144
|
+
* @param {number} [startPos] - The starting position of this node in the original input string.
|
|
145
|
+
* @returns {Object} The newly created node with childNodes, type, value, and isClosed properties.
|
|
146
|
+
* @throws {Error} If the nesting depth exceeds MAX_NODE_DEPTH.
|
|
147
|
+
*/
|
|
111
148
|
createNode(parentNode, startPos) {
|
|
112
149
|
let node = {
|
|
113
150
|
childNodes: [],
|
|
@@ -141,6 +178,15 @@ class TokenParser {
|
|
|
141
178
|
return node;
|
|
142
179
|
}
|
|
143
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Processes the entire input string character by character using a state machine.
|
|
183
|
+
* Transitions between states (NORMAL, ATOM, STRING, LITERAL, SEQUENCE, PARTIAL, TEXT)
|
|
184
|
+
* based on the current character and builds the parse tree. This is the main parsing
|
|
185
|
+
* loop that drives the tokenization.
|
|
186
|
+
*
|
|
187
|
+
* @returns {Promise<void>}
|
|
188
|
+
* @throws {Error} If the input contains unexpected characters, unclosed structures, or other syntax errors.
|
|
189
|
+
*/
|
|
144
190
|
async processString() {
|
|
145
191
|
let chr, i, len;
|
|
146
192
|
|
|
@@ -203,8 +249,11 @@ class TokenParser {
|
|
|
203
249
|
checkSP();
|
|
204
250
|
break;
|
|
205
251
|
|
|
206
|
-
// < starts a new partial
|
|
252
|
+
// < starts a new partial byte range (e.g., BODY[]<0.1024>)
|
|
207
253
|
case '<':
|
|
254
|
+
// '<' is only a partial range marker when it immediately follows ']',
|
|
255
|
+
// which occurs in BODY[section]<origin.length> responses.
|
|
256
|
+
// In all other contexts, '<' is treated as the start of an ATOM.
|
|
208
257
|
if (this.str.charAt(i - 1) !== ']') {
|
|
209
258
|
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
|
210
259
|
this.currentNode.type = 'ATOM';
|
|
@@ -218,12 +267,13 @@ class TokenParser {
|
|
|
218
267
|
}
|
|
219
268
|
break;
|
|
220
269
|
|
|
221
|
-
//
|
|
270
|
+
// literal8 (RFC 3516): uses ~{size} prefix instead of {size}
|
|
271
|
+
// literal8 allows binary data containing NUL bytes, unlike regular literals
|
|
222
272
|
case '~': {
|
|
223
273
|
let nextChr = this.str.charAt(i + 1);
|
|
224
274
|
if (nextChr !== '{') {
|
|
225
275
|
if (imapFormalSyntax['ATOM-CHAR']().indexOf(nextChr) >= 0) {
|
|
226
|
-
// treat as ATOM
|
|
276
|
+
// '~' not followed by '{' but followed by an ATOM char: treat as ATOM
|
|
227
277
|
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
|
228
278
|
this.currentNode.type = 'ATOM';
|
|
229
279
|
this.currentNode.value = chr;
|
|
@@ -236,14 +286,16 @@ class TokenParser {
|
|
|
236
286
|
error.parserContext = { input: this.str, pos: this.pos + i, chr };
|
|
237
287
|
throw error;
|
|
238
288
|
}
|
|
289
|
+
// Mark the next literal as literal8 type; consumed when '{' is encountered
|
|
239
290
|
this.expectedLiteralType = 'literal8';
|
|
240
291
|
break;
|
|
241
292
|
}
|
|
242
293
|
|
|
243
|
-
// { starts a new literal
|
|
294
|
+
// { starts a new literal (regular {size}\r\n or literal8 ~{size}\r\n)
|
|
244
295
|
case '{':
|
|
245
296
|
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
|
246
297
|
this.currentNode.type = 'LITERAL';
|
|
298
|
+
// Use literal8 type if '~' was seen immediately before, otherwise standard literal
|
|
247
299
|
this.currentNode.literalType = this.expectedLiteralType || 'literal';
|
|
248
300
|
this.expectedLiteralType = false;
|
|
249
301
|
this.state = STATE_LITERAL;
|
|
@@ -267,6 +319,7 @@ class TokenParser {
|
|
|
267
319
|
// [ starts section
|
|
268
320
|
case '[':
|
|
269
321
|
// If it is the *first* element after response command, then process as a response argument list
|
|
322
|
+
// Status responses (OK/NO/BAD/BYE/PREAUTH) use [code] for response codes
|
|
270
323
|
if (['OK', 'NO', 'BAD', 'BYE', 'PREAUTH'].includes(this.parent.command.toUpperCase()) && this.currentNode === this.tree) {
|
|
271
324
|
this.currentNode.endPos = this.pos + i;
|
|
272
325
|
|
|
@@ -278,10 +331,10 @@ class TokenParser {
|
|
|
278
331
|
this.currentNode.isClosed = false;
|
|
279
332
|
this.state = STATE_NORMAL;
|
|
280
333
|
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
334
|
+
// RFC 2221 REFERRAL special case: the payload is an RFC 2192/RFC 5092
|
|
335
|
+
// IMAP URL (e.g., imap://user@host/mailbox) which contains characters
|
|
336
|
+
// that would break normal ATOM parsing (colons, slashes, etc.).
|
|
337
|
+
// We handle this by consuming everything up to ']' as a single ATOM value.
|
|
285
338
|
if (this.str.substr(i + 1, 9).toUpperCase() === 'REFERRAL ') {
|
|
286
339
|
// create the REFERRAL atom
|
|
287
340
|
this.currentNode = this.createNode(this.currentNode, this.pos + i + 1);
|
|
@@ -332,6 +385,8 @@ class TokenParser {
|
|
|
332
385
|
break;
|
|
333
386
|
|
|
334
387
|
case STATE_ATOM:
|
|
388
|
+
// An atom is terminated by: space, closing delimiter of parent node,
|
|
389
|
+
// or encountering a '[' that starts a section for BODY/BINARY commands.
|
|
335
390
|
// space finishes an atom
|
|
336
391
|
if (chr === ' ') {
|
|
337
392
|
this.currentNode.endPos = this.pos + i - 1;
|
|
@@ -340,7 +395,7 @@ class TokenParser {
|
|
|
340
395
|
break;
|
|
341
396
|
}
|
|
342
397
|
|
|
343
|
-
//
|
|
398
|
+
// ')' or ']' terminates the atom AND closes the enclosing LIST or SECTION
|
|
344
399
|
if (
|
|
345
400
|
this.currentNode.parentNode &&
|
|
346
401
|
((chr === ')' && this.currentNode.parentNode.type === 'LIST') || (chr === ']' && this.currentNode.parentNode.type === 'SECTION'))
|
|
@@ -357,6 +412,8 @@ class TokenParser {
|
|
|
357
412
|
break;
|
|
358
413
|
}
|
|
359
414
|
|
|
415
|
+
// If the atom so far is all digits and we see ',' or ':', it is actually
|
|
416
|
+
// a sequence set (e.g., "1:5" or "1,3,5"), so reclassify and switch state
|
|
360
417
|
if ((chr === ',' || chr === ':') && RE_DIGITS.test(this.currentNode.value)) {
|
|
361
418
|
this.currentNode.type = 'SEQUENCE';
|
|
362
419
|
this.currentNode.isClosed = true;
|
package/lib/imap-commands.js
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* IMAP command registry. Maps IMAP command names (uppercase strings) to their
|
|
7
|
+
* corresponding implementation modules from the `lib/commands/` directory.
|
|
8
|
+
*
|
|
9
|
+
* Each entry maps a command name (e.g., "FETCH", "SELECT", "IDLE") to a module
|
|
10
|
+
* that exports functions for building the command request and processing the
|
|
11
|
+
* server response. This Map is used by the main ImapFlow client to look up
|
|
12
|
+
* and execute IMAP commands.
|
|
13
|
+
*
|
|
14
|
+
* @type {Map<string, Object>}
|
|
15
|
+
*/
|
|
5
16
|
module.exports = new Map([
|
|
6
17
|
['ID', require('./commands/id.js')],
|
|
7
18
|
['CAPABILITY', require('./commands/capability.js')],
|
package/lib/imap-flow.d.ts
CHANGED
|
@@ -761,6 +761,12 @@ export class ImapFlow extends EventEmitter {
|
|
|
761
761
|
/** Opens a mailbox if not already open and returns a lock */
|
|
762
762
|
getMailboxLock(path: string | string[], options?: MailboxOpenOptions): Promise<MailboxLockObject>;
|
|
763
763
|
|
|
764
|
+
/** Returns byte counters for the current connection, optionally resets them */
|
|
765
|
+
stats(reset?: boolean): { sent: number; received: number };
|
|
766
|
+
|
|
767
|
+
/** Detaches sockets from the IMAP pipeline, returns read and write sockets */
|
|
768
|
+
unbind(): { readSocket: any; writeSocket: any };
|
|
769
|
+
|
|
764
770
|
/** Connection close event */
|
|
765
771
|
on(event: 'close', listener: () => void): this;
|
|
766
772
|
|