imapflow 1.2.8 → 1.2.10
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 +14 -0
- package/README.md +36 -58
- package/eslint.config.js +18 -16
- 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 +175 -46
- 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 +5 -17
- package/test/commands-integration-test.js +33 -0
- package/test/connection-edge-cases-test.js +105 -0
- package/test/special-use-test.js +32 -0
- package/.babelrc +0 -6
- package/.eslintrc +0 -16
- package/assets/favicon.ico +0 -0
- package/jsdoc.json +0 -28
|
@@ -16,7 +16,25 @@ const CURLY_CLOSE = 0x7d;
|
|
|
16
16
|
// Maximum allowed literal size: 1GB (1073741824 bytes)
|
|
17
17
|
const MAX_LITERAL_SIZE = 1024 * 1024 * 1024;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* A Transform stream that parses raw IMAP protocol data from a socket into structured
|
|
21
|
+
* command/response objects. Reads binary input, splits it into lines delimited by LF,
|
|
22
|
+
* extracts literal data blocks based on IMAP literal size markers (e.g., "{123}\r\n"),
|
|
23
|
+
* and emits each complete command as a readable object containing the payload Buffer
|
|
24
|
+
* and any associated literal Buffers. Enforces a maximum literal size of 1GB.
|
|
25
|
+
*
|
|
26
|
+
* @extends Transform
|
|
27
|
+
*/
|
|
19
28
|
class ImapStream extends Transform {
|
|
29
|
+
/**
|
|
30
|
+
* Creates a new ImapStream instance.
|
|
31
|
+
*
|
|
32
|
+
* @param {Object} [options] - Stream options.
|
|
33
|
+
* @param {string} [options.cid] - Connection identifier used for logging.
|
|
34
|
+
* @param {Object} [options.logger] - A pino-compatible logger instance. If not provided, a default child logger is created.
|
|
35
|
+
* @param {boolean} [options.logRaw] - If true, logs raw socket data at trace level.
|
|
36
|
+
* @param {boolean} [options.secureConnection] - Whether the connection uses TLS.
|
|
37
|
+
*/
|
|
20
38
|
constructor(options) {
|
|
21
39
|
super({
|
|
22
40
|
//writableHighWaterMark: 3,
|
|
@@ -51,6 +69,15 @@ class ImapStream extends Transform {
|
|
|
51
69
|
this.inputQueue = []; // unprocessed input chunks
|
|
52
70
|
}
|
|
53
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Checks whether the given line buffer ends with an IMAP literal size marker
|
|
74
|
+
* (e.g., "{123}\r\n"). If a valid marker is found and the literal size is within
|
|
75
|
+
* the allowed maximum, switches the stream state to LITERAL mode and records
|
|
76
|
+
* the expected number of literal bytes.
|
|
77
|
+
*
|
|
78
|
+
* @param {Buffer} line - The line buffer to check for a trailing literal marker.
|
|
79
|
+
* @returns {boolean} True if a valid literal marker was found and literal state was activated, false otherwise.
|
|
80
|
+
*/
|
|
54
81
|
checkLiteralMarker(line) {
|
|
55
82
|
if (!line || !line.length) {
|
|
56
83
|
return false;
|
|
@@ -58,23 +85,22 @@ class ImapStream extends Transform {
|
|
|
58
85
|
|
|
59
86
|
let pos = line.length - 1;
|
|
60
87
|
|
|
61
|
-
if (line[pos]
|
|
62
|
-
pos--;
|
|
63
|
-
} else {
|
|
88
|
+
if (line[pos] !== LF) {
|
|
64
89
|
return false;
|
|
65
90
|
}
|
|
91
|
+
pos--;
|
|
92
|
+
|
|
66
93
|
if (pos >= 0 && line[pos] === CR) {
|
|
67
94
|
pos--;
|
|
68
95
|
}
|
|
69
|
-
if (pos < 0) {
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
96
|
|
|
73
|
-
if (!pos || line[pos] !== CURLY_CLOSE) {
|
|
97
|
+
if (pos < 0 || !pos || line[pos] !== CURLY_CLOSE) {
|
|
74
98
|
return false;
|
|
75
99
|
}
|
|
76
100
|
pos--;
|
|
77
101
|
|
|
102
|
+
// Scan backwards through the line to find an IMAP literal marker: {size}\r\n
|
|
103
|
+
// The format is: '{' followed by one or more ASCII digits followed by '}'
|
|
78
104
|
let numBytes = [];
|
|
79
105
|
for (; pos > 0; pos--) {
|
|
80
106
|
let c = line[pos];
|
|
@@ -103,6 +129,16 @@ class ImapStream extends Transform {
|
|
|
103
129
|
return false;
|
|
104
130
|
}
|
|
105
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Processes a single input chunk of raw data. In LINE state, scans for LF-terminated
|
|
134
|
+
* lines and checks for literal markers. In LITERAL state, collects the expected number
|
|
135
|
+
* of literal bytes. When a complete command (with all its literals) is assembled, it is
|
|
136
|
+
* pushed downstream as a readable object.
|
|
137
|
+
*
|
|
138
|
+
* @param {Buffer} chunk - The raw data chunk to process.
|
|
139
|
+
* @param {number} [startPos=0] - The byte offset within the chunk to start processing from.
|
|
140
|
+
* @returns {Promise<void>}
|
|
141
|
+
*/
|
|
106
142
|
async processInputChunk(chunk, startPos) {
|
|
107
143
|
startPos = startPos || 0;
|
|
108
144
|
if (startPos >= chunk.length) {
|
|
@@ -164,41 +200,34 @@ class ImapStream extends Transform {
|
|
|
164
200
|
}
|
|
165
201
|
|
|
166
202
|
case LITERAL: {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
this.literalBuffer.push(chunk);
|
|
171
|
-
} else {
|
|
172
|
-
this.literalBuffer.push(chunk.slice(startPos));
|
|
173
|
-
}
|
|
203
|
+
const remainingInChunk = chunk.length - startPos;
|
|
204
|
+
const bytesToRead = Math.min(remainingInChunk, this.literalWaiting);
|
|
205
|
+
const partial = startPos === 0 && bytesToRead === chunk.length ? chunk : chunk.slice(startPos, startPos + bytesToRead);
|
|
174
206
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
this.literalBuffer = [];
|
|
178
|
-
this.state = LINE;
|
|
207
|
+
this.literalBuffer.push(partial);
|
|
208
|
+
this.literalWaiting -= bytesToRead;
|
|
179
209
|
|
|
180
|
-
|
|
181
|
-
} else if (chunk.length > startPos + this.literalWaiting) {
|
|
182
|
-
let partial = chunk.slice(startPos, startPos + this.literalWaiting);
|
|
183
|
-
this.literalBuffer.push(partial);
|
|
184
|
-
startPos += partial.length;
|
|
185
|
-
this.literalWaiting -= partial.length;
|
|
210
|
+
if (this.literalWaiting === 0) {
|
|
186
211
|
this.literals.push(Buffer.concat(this.literalBuffer));
|
|
187
212
|
this.literalBuffer = [];
|
|
188
213
|
this.state = LINE;
|
|
189
214
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
this.literalBuffer.push(partial);
|
|
194
|
-
startPos += partial.length;
|
|
195
|
-
this.literalWaiting -= partial.length;
|
|
196
|
-
return;
|
|
215
|
+
if (remainingInChunk > bytesToRead) {
|
|
216
|
+
return await this.processInputChunk(chunk, startPos + bytesToRead);
|
|
217
|
+
}
|
|
197
218
|
}
|
|
219
|
+
break;
|
|
198
220
|
}
|
|
199
221
|
}
|
|
200
222
|
}
|
|
201
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Drains the input queue by processing each queued chunk sequentially.
|
|
226
|
+
* Yields to the event loop every 10 chunks to prevent CPU blocking on
|
|
227
|
+
* large bursts of incoming data.
|
|
228
|
+
*
|
|
229
|
+
* @returns {Promise<void>}
|
|
230
|
+
*/
|
|
202
231
|
async processInput() {
|
|
203
232
|
let data;
|
|
204
233
|
let processedCount = 0;
|
|
@@ -215,6 +244,15 @@ class ImapStream extends Transform {
|
|
|
215
244
|
}
|
|
216
245
|
}
|
|
217
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Transform stream implementation. Receives raw data chunks from the writable side,
|
|
249
|
+
* converts strings to Buffers, tracks total bytes read, optionally logs raw data,
|
|
250
|
+
* and queues the chunk for asynchronous processing.
|
|
251
|
+
*
|
|
252
|
+
* @param {Buffer|string} chunk - The incoming data chunk.
|
|
253
|
+
* @param {string} encoding - The encoding if chunk is a string.
|
|
254
|
+
* @param {Function} next - Callback to signal that this chunk has been consumed.
|
|
255
|
+
*/
|
|
218
256
|
_transform(chunk, encoding, next) {
|
|
219
257
|
if (typeof chunk === 'string') {
|
|
220
258
|
chunk = Buffer.from(chunk, encoding);
|
|
@@ -237,6 +275,9 @@ class ImapStream extends Transform {
|
|
|
237
275
|
});
|
|
238
276
|
}
|
|
239
277
|
|
|
278
|
+
// Queue the chunk for async processing. The 'next' callback serves as
|
|
279
|
+
// backpressure: it is called only after this chunk is fully processed,
|
|
280
|
+
// which signals the writable side that more data can be accepted.
|
|
240
281
|
if (chunk && chunk.length) {
|
|
241
282
|
this.inputQueue.push({ chunk, next });
|
|
242
283
|
}
|
|
@@ -249,10 +290,22 @@ class ImapStream extends Transform {
|
|
|
249
290
|
}
|
|
250
291
|
}
|
|
251
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Flush implementation called when the writable side ends. Signals completion immediately.
|
|
295
|
+
*
|
|
296
|
+
* @param {Function} next - Callback to signal flush completion.
|
|
297
|
+
*/
|
|
252
298
|
_flush(next) {
|
|
253
299
|
next();
|
|
254
300
|
}
|
|
255
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Destroy implementation for cleanup. Clears all internal buffers, drains the input queue
|
|
304
|
+
* by invoking pending callbacks, and forwards the error (if any) to the callback.
|
|
305
|
+
*
|
|
306
|
+
* @param {Error|null} err - The error that caused destruction, or null.
|
|
307
|
+
* @param {Function} callback - Callback to signal destruction completion.
|
|
308
|
+
*/
|
|
256
309
|
_destroy(err, callback) {
|
|
257
310
|
this.inputBuffer = [];
|
|
258
311
|
this.lineBuffer = [];
|
|
@@ -6,7 +6,20 @@ const imapFormalSyntax = require('./imap-formal-syntax');
|
|
|
6
6
|
|
|
7
7
|
const { TokenParser } = require('./token-parser');
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Parses a single IMAP response line into its structural components: tag, command,
|
|
11
|
+
* and attributes. Handles status responses (OK, NO, BAD, PREAUTH, BYE) with their
|
|
12
|
+
* human-readable text and response codes, as well as continuation responses ("+").
|
|
13
|
+
*/
|
|
9
14
|
class ParserInstance {
|
|
15
|
+
/**
|
|
16
|
+
* Creates a new ParserInstance for parsing an IMAP response line.
|
|
17
|
+
*
|
|
18
|
+
* @param {Buffer|string} input - The raw IMAP response line to parse.
|
|
19
|
+
* @param {Object} [options] - Parser options passed through to the TokenParser for attribute parsing.
|
|
20
|
+
* @param {boolean} [options.literalPlus] - Whether the LITERAL+ extension is in use.
|
|
21
|
+
* @param {Array<Buffer>} [options.literals] - Pre-parsed literal values from the stream.
|
|
22
|
+
*/
|
|
10
23
|
constructor(input, options) {
|
|
11
24
|
this.input = (input || '').toString();
|
|
12
25
|
this.options = options || {};
|
|
@@ -14,6 +27,14 @@ class ParserInstance {
|
|
|
14
27
|
this.pos = 0;
|
|
15
28
|
}
|
|
16
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Extracts and returns the IMAP tag from the beginning of the response.
|
|
32
|
+
* The tag is typically "*" for untagged responses, "+" for continuation requests,
|
|
33
|
+
* or a client-assigned command tag like "A1".
|
|
34
|
+
*
|
|
35
|
+
* @returns {Promise<string>} The parsed tag string.
|
|
36
|
+
* @throws {Error} If the tag contains invalid characters.
|
|
37
|
+
*/
|
|
17
38
|
async getTag() {
|
|
18
39
|
if (!this.tag) {
|
|
19
40
|
this.tag = await this.getElement(imapFormalSyntax.tag() + '*+', true);
|
|
@@ -21,6 +42,15 @@ class ParserInstance {
|
|
|
21
42
|
return this.tag;
|
|
22
43
|
}
|
|
23
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Extracts and returns the IMAP command or response name from the input.
|
|
47
|
+
* For continuation responses (tag "+"), returns an empty string and stores
|
|
48
|
+
* the remainder as human-readable text. For status responses (OK, NO, BAD,
|
|
49
|
+
* PREAUTH, BYE), separates the optional response code from the human-readable text.
|
|
50
|
+
*
|
|
51
|
+
* @returns {Promise<string>} The parsed command string.
|
|
52
|
+
* @throws {Error} If the command contains invalid characters or input ends unexpectedly.
|
|
53
|
+
*/
|
|
24
54
|
async getCommand() {
|
|
25
55
|
if (this.tag === '+') {
|
|
26
56
|
// special case
|
|
@@ -34,6 +64,10 @@ class ParserInstance {
|
|
|
34
64
|
this.command = await this.getElement(imapFormalSyntax.command());
|
|
35
65
|
}
|
|
36
66
|
|
|
67
|
+
// Status responses have the format: TAG OK/NO/BAD [response-code] human-readable text
|
|
68
|
+
// Example: * OK [CAPABILITY IMAP4rev1] Server ready
|
|
69
|
+
// Example: A1 NO [AUTHENTICATIONFAILED] Invalid credentials
|
|
70
|
+
// We need to separate the optional [response-code] from the human-readable text.
|
|
37
71
|
switch ((this.command || '').toString().toUpperCase()) {
|
|
38
72
|
case 'OK':
|
|
39
73
|
case 'NO':
|
|
@@ -69,6 +103,14 @@ class ParserInstance {
|
|
|
69
103
|
return this.command;
|
|
70
104
|
}
|
|
71
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Extracts the next whitespace-delimited element from the input and validates it
|
|
108
|
+
* against the given syntax character set. Advances the parser position past the element.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} syntax - A string of allowed characters for the element (as returned by imap-formal-syntax methods).
|
|
111
|
+
* @returns {Promise<string>} The extracted element string.
|
|
112
|
+
* @throws {Error} If the element contains characters not in the syntax set, or if input ends unexpectedly.
|
|
113
|
+
*/
|
|
72
114
|
async getElement(syntax) {
|
|
73
115
|
let match, element, errPos;
|
|
74
116
|
|
|
@@ -83,7 +125,10 @@ class ParserInstance {
|
|
|
83
125
|
element = match[0];
|
|
84
126
|
if ((errPos = imapFormalSyntax.verify(element, syntax)) >= 0) {
|
|
85
127
|
if (this.tag === 'Server' && element === 'Unavailable.') {
|
|
86
|
-
// Exchange
|
|
128
|
+
// Microsoft Exchange sometimes sends a non-standard response
|
|
129
|
+
// "Server Unavailable." instead of a proper IMAP tagged/untagged response.
|
|
130
|
+
// We detect this specific pattern and convert it into a synthetic BAD response
|
|
131
|
+
// so the rest of the parser can handle it gracefully.
|
|
87
132
|
let error = new Error(`Server returned an error: ${this.input}`);
|
|
88
133
|
error.code = 'ParserErrorExchange';
|
|
89
134
|
error.parserContext = {
|
|
@@ -117,6 +162,13 @@ class ParserInstance {
|
|
|
117
162
|
return element;
|
|
118
163
|
}
|
|
119
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Consumes a single space character from the current position in the input.
|
|
167
|
+
* Advances the parser position by one.
|
|
168
|
+
*
|
|
169
|
+
* @returns {Promise<void>}
|
|
170
|
+
* @throws {Error} If the current character is not a space, or if input has ended unexpectedly.
|
|
171
|
+
*/
|
|
120
172
|
async getSpace() {
|
|
121
173
|
if (!this.remainder.length) {
|
|
122
174
|
if (this.tag === '+' && this.pos === 1) {
|
|
@@ -141,6 +193,14 @@ class ParserInstance {
|
|
|
141
193
|
this.remainder = this.remainder.substr(1);
|
|
142
194
|
}
|
|
143
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Parses the remaining input as IMAP attributes using the TokenParser.
|
|
198
|
+
* This handles complex structures including nested lists, literals, strings,
|
|
199
|
+
* atoms, sections, sequences, and partial ranges.
|
|
200
|
+
*
|
|
201
|
+
* @returns {Promise<Array>} A promise that resolves to an array of parsed attribute objects.
|
|
202
|
+
* @throws {Error} If the input contains unexpected whitespace, invalid characters, or ends unexpectedly.
|
|
203
|
+
*/
|
|
144
204
|
async getAttributes() {
|
|
145
205
|
if (!this.remainder.length) {
|
|
146
206
|
let error = new Error(`Unexpected end of input at position ${this.pos} [E6]`);
|
|
@@ -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
|
|