imapflow 2.0.8 → 2.1.1
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 +27 -0
- package/README.md +2 -0
- package/dist/cjs/commands/list.js +35 -19
- package/dist/cjs/commands/select.js +7 -3
- package/dist/cjs/errors.d.ts +50 -1
- package/dist/cjs/errors.js +56 -1
- package/dist/cjs/handler/imap-stream.d.ts +13 -2
- package/dist/cjs/handler/imap-stream.js +54 -37
- package/dist/cjs/handler/token-parser.js +14 -8
- package/dist/cjs/imap-flow.d.ts +14 -77
- package/dist/cjs/imap-flow.js +89 -31
- package/dist/cjs/logger.d.ts +21 -3
- package/dist/cjs/logger.js +35 -5
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/cjs/search-compiler.d.ts +1 -1
- package/dist/cjs/search-compiler.js +48 -35
- package/dist/cjs/tools.d.ts +1 -1
- package/dist/cjs/tools.js +18 -10
- package/dist/esm/commands/list.js +35 -19
- package/dist/esm/commands/select.js +7 -3
- package/dist/esm/errors.d.ts +50 -1
- package/dist/esm/errors.js +55 -0
- package/dist/esm/handler/imap-stream.d.ts +13 -2
- package/dist/esm/handler/imap-stream.js +54 -34
- package/dist/esm/handler/token-parser.js +14 -8
- package/dist/esm/imap-flow.d.ts +14 -77
- package/dist/esm/imap-flow.js +88 -31
- package/dist/esm/logger.d.ts +21 -3
- package/dist/esm/logger.js +33 -3
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/dist/esm/search-compiler.d.ts +1 -1
- package/dist/esm/search-compiler.js +49 -36
- package/dist/esm/tools.d.ts +1 -1
- package/dist/esm/tools.js +16 -8
- package/package.json +4 -4
|
@@ -148,10 +148,12 @@ export default async function list(connection, reference, mailbox, options) {
|
|
|
148
148
|
if (!untagged.attributes || !untagged.attributes.length) {
|
|
149
149
|
return;
|
|
150
150
|
}
|
|
151
|
+
// A name sent as a literal arrives as a Buffer, so convert it once for both fields
|
|
152
|
+
let rawPath = ((untagged.attributes[2] && untagged.attributes[2].value) || '').toString();
|
|
151
153
|
let entry = {
|
|
152
154
|
// Decode from modified UTF-7 wire format and normalize the path
|
|
153
|
-
path: normalizePath(connection, decodePath(connection,
|
|
154
|
-
pathAsListed:
|
|
155
|
+
path: normalizePath(connection, decodePath(connection, rawPath)),
|
|
156
|
+
pathAsListed: rawPath,
|
|
155
157
|
flags: new Set(getStringList(untagged.attributes[0])),
|
|
156
158
|
delimiter: (untagged.attributes[1] && untagged.attributes[1].value),
|
|
157
159
|
listed: true
|
|
@@ -379,9 +381,11 @@ export default async function list(connection, reference, mailbox, options) {
|
|
|
379
381
|
if (!untagged.attributes || !untagged.attributes.length) {
|
|
380
382
|
return;
|
|
381
383
|
}
|
|
384
|
+
// A name sent as a literal arrives as a Buffer, so convert it once for both fields
|
|
385
|
+
let rawPath = ((untagged.attributes[2] && untagged.attributes[2].value) || '').toString();
|
|
382
386
|
let entry = {
|
|
383
|
-
path: normalizePath(connection, decodePath(connection,
|
|
384
|
-
pathAsListed:
|
|
387
|
+
path: normalizePath(connection, decodePath(connection, rawPath)),
|
|
388
|
+
pathAsListed: rawPath,
|
|
385
389
|
flags: new Set(getStringList(untagged.attributes[0])),
|
|
386
390
|
delimiter: (untagged.attributes[1] && untagged.attributes[1].value),
|
|
387
391
|
subscribed: true
|
|
@@ -444,23 +448,35 @@ export default async function list(connection, reference, mailbox, options) {
|
|
|
444
448
|
connection.log.warn({ msg: 'Failed to request subscription info', err, cid: connection.id });
|
|
445
449
|
}
|
|
446
450
|
}
|
|
447
|
-
// Resolve special-use conflicts
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
451
|
+
// Resolve special-use conflicts. Each type goes to one mailbox and each mailbox gets
|
|
452
|
+
// at most one type. Candidates are taken in priority order across all types (user >
|
|
453
|
+
// extension > name, then alphabetically), so a mailbox claimed by a stronger match
|
|
454
|
+
// leaves its other type to that type's next candidate instead of to nobody. Within a
|
|
455
|
+
// source a shallower mailbox wins before the alphabetical order is consulted, so that
|
|
456
|
+
// INBOX.Sent is preferred over INBOX.Archive.Sent.
|
|
457
|
+
let candidates = Object.entries(specialUseMatches).flatMap(([type, matches]) => matches.map(match => ({ type, ...match })));
|
|
458
|
+
candidates.sort((a, b) => {
|
|
459
|
+
let aSource = SOURCE_SORT_ORDER.indexOf(a.source);
|
|
460
|
+
let bSource = SOURCE_SORT_ORDER.indexOf(b.source);
|
|
461
|
+
if (aSource !== bSource) {
|
|
457
462
|
return aSource - bSource;
|
|
458
|
-
});
|
|
459
|
-
if (!sortedEntries[0].entry.specialUse) {
|
|
460
|
-
let source = sortedEntries[0].source;
|
|
461
|
-
sortedEntries[0].entry.specialUse = type;
|
|
462
|
-
sortedEntries[0].entry.specialUseSource = PUBLIC_SOURCE[source] || source;
|
|
463
463
|
}
|
|
464
|
+
// parent is set on every listed entry before the candidates are ranked
|
|
465
|
+
let aDepth = a.entry.parent.length;
|
|
466
|
+
let bDepth = b.entry.parent.length;
|
|
467
|
+
if (aDepth !== bDepth) {
|
|
468
|
+
return aDepth - bDepth;
|
|
469
|
+
}
|
|
470
|
+
return a.entry.path.localeCompare(b.entry.path);
|
|
471
|
+
});
|
|
472
|
+
let assignedTypes = new Set();
|
|
473
|
+
for (let { type, entry, source } of candidates) {
|
|
474
|
+
if (assignedTypes.has(type) || entry.specialUse) {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
entry.specialUse = type;
|
|
478
|
+
entry.specialUseSource = PUBLIC_SOURCE[source] || source;
|
|
479
|
+
assignedTypes.add(type);
|
|
464
480
|
}
|
|
465
481
|
// No source answered, so "not subscribed" was never actually reported for any of
|
|
466
482
|
// these folders - the state is unknown, not false. Reporting the whole listing as
|
|
@@ -92,12 +92,16 @@ export default async function select(connection, pathInput, options) {
|
|
|
92
92
|
// QRESYNC (RFC 7162): allows efficient mailbox resynchronization by sending
|
|
93
93
|
// the last known UIDVALIDITY and HIGHESTMODSEQ. Server responds with only
|
|
94
94
|
// the changes (new flags, expunged UIDs) since that point.
|
|
95
|
+
// The caller may pass UIDVALIDITY as a bigint, a number or a string. It is parsed once,
|
|
96
|
+
// so the value that is sent is also the one checked against the server's below, and a
|
|
97
|
+
// value that is not a plain decimal skips QRESYNC instead of reaching the server.
|
|
98
|
+
let uidValidity = options.uidValidity !== undefined ? parseBigIntValue(String(options.uidValidity)) : false;
|
|
95
99
|
let extraArgs = [];
|
|
96
|
-
if (connection.enabled.has('QRESYNC') && options.changedSince &&
|
|
100
|
+
if (connection.enabled.has('QRESYNC') && options.changedSince && uidValidity) {
|
|
97
101
|
extraArgs.push([
|
|
98
102
|
{ type: 'ATOM', value: 'QRESYNC' },
|
|
99
103
|
[
|
|
100
|
-
{ type: 'ATOM', value:
|
|
104
|
+
{ type: 'ATOM', value: uidValidity.toString() },
|
|
101
105
|
{ type: 'ATOM', value: options.changedSince.toString() }
|
|
102
106
|
]
|
|
103
107
|
]);
|
|
@@ -202,7 +206,7 @@ export default async function select(connection, pathInput, options) {
|
|
|
202
206
|
// QRESYNC results are only valid if UIDVALIDITY matches, HIGHESTMODSEQ is
|
|
203
207
|
// present, and the mailbox supports mod-sequences. If any condition fails,
|
|
204
208
|
// the client cannot trust the incremental updates and must do a full resync.
|
|
205
|
-
if (map.qresync && (
|
|
209
|
+
if (map.qresync && (uidValidity !== map.uidValidity || !map.highestModseq || map.noModseq)) {
|
|
206
210
|
map.qresync = false;
|
|
207
211
|
}
|
|
208
212
|
// Transition mailbox state: save previous mailbox reference, temporarily
|
package/dist/esm/errors.d.ts
CHANGED
|
@@ -1,11 +1,60 @@
|
|
|
1
1
|
import type { ImapResponse } from './handler/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The `code` values ImapFlow sets on the errors it raises, so they can be matched without
|
|
4
|
+
* string literals: `if (err.code === ImapFlowErrorCode.NoConnection)`.
|
|
5
|
+
*
|
|
6
|
+
* Parser failures use `ParserError` followed by a number (`ParserError11`) and are not listed
|
|
7
|
+
* one by one, test them with `err.code?.startsWith('ParserError')`. Errors from the socket, TLS
|
|
8
|
+
* or DNS layer pass through with Node's own code (`ECONNREFUSED`, `ENOTFOUND`, ...).
|
|
9
|
+
*/
|
|
10
|
+
export declare const ImapFlowErrorCode: {
|
|
11
|
+
readonly NoConnection: "NoConnection";
|
|
12
|
+
readonly EConnectionClosed: "EConnectionClosed";
|
|
13
|
+
readonly StateLogout: "StateLogout";
|
|
14
|
+
readonly ClosedAfterConnectText: "ClosedAfterConnectText";
|
|
15
|
+
readonly ClosedAfterConnectTLS: "ClosedAfterConnectTLS";
|
|
16
|
+
readonly CONNECT_TIMEOUT: "CONNECT_TIMEOUT";
|
|
17
|
+
readonly GREETING_TIMEOUT: "GREETING_TIMEOUT";
|
|
18
|
+
readonly UPGRADE_TIMEOUT: "UPGRADE_TIMEOUT";
|
|
19
|
+
readonly ETIMEOUT: "ETIMEOUT";
|
|
20
|
+
readonly LockTimeout: "LockTimeout";
|
|
21
|
+
readonly ETHROTTLE: "ETHROTTLE";
|
|
22
|
+
readonly UnexpectedTag: "UnexpectedTag";
|
|
23
|
+
readonly InvalidResponse: "InvalidResponse";
|
|
24
|
+
readonly ResponseProcessingFailed: "ResponseProcessingFailed";
|
|
25
|
+
readonly STARTTLS_INJECTION: "STARTTLS_INJECTION";
|
|
26
|
+
readonly COMPRESS_TRAILING_DATA: "COMPRESS_TRAILING_DATA";
|
|
27
|
+
readonly PollFailed: "PollFailed";
|
|
28
|
+
readonly NotFound: "NotFound";
|
|
29
|
+
readonly MissingServerExtension: "MissingServerExtension";
|
|
30
|
+
readonly ParserError: "ParserError";
|
|
31
|
+
readonly ParserErrorExchange: "ParserErrorExchange";
|
|
32
|
+
readonly MAX_IMAP_NESTING_REACHED: "MAX_IMAP_NESTING_REACHED";
|
|
33
|
+
readonly LineTooLarge: "LineTooLarge";
|
|
34
|
+
readonly LiteralTooLarge: "LiteralTooLarge";
|
|
35
|
+
readonly ResponseTooLarge: "ResponseTooLarge";
|
|
36
|
+
readonly InvalidStringValue: "InvalidStringValue";
|
|
37
|
+
readonly InvalidTokenValue: "InvalidTokenValue";
|
|
38
|
+
readonly InvalidTextValue: "InvalidTextValue";
|
|
39
|
+
readonly InvalidSequenceSet: "InvalidSequenceSet";
|
|
40
|
+
readonly InvalidSearchQuery: "InvalidSearchQuery";
|
|
41
|
+
readonly DownloadOverflow: "DownloadOverflow";
|
|
42
|
+
readonly DownloadIncomplete: "DownloadIncomplete";
|
|
43
|
+
readonly ProxyError: "ProxyError";
|
|
44
|
+
readonly EPROXY: "EPROXY";
|
|
45
|
+
readonly UnsupportedProxyAddress: "UnsupportedProxyAddress";
|
|
46
|
+
readonly ERR_INVALID_URL: "ERR_INVALID_URL";
|
|
47
|
+
readonly InstanceReused: "InstanceReused";
|
|
48
|
+
};
|
|
49
|
+
/** One of the {@link ImapFlowErrorCode} values */
|
|
50
|
+
export type ImapFlowErrorCode = (typeof ImapFlowErrorCode)[keyof typeof ImapFlowErrorCode];
|
|
2
51
|
/**
|
|
3
52
|
* An Error raised by ImapFlow, with the extra properties the library attaches to describe
|
|
4
53
|
* the failure. Every property is optional: which ones are present depends on where the
|
|
5
54
|
* error came from.
|
|
6
55
|
*/
|
|
7
56
|
export interface ImapFlowError extends Error {
|
|
8
|
-
/** Error code,
|
|
57
|
+
/** Error code, one of {@link ImapFlowErrorCode}, a parser error code or a code from Node */
|
|
9
58
|
code?: string | undefined;
|
|
10
59
|
/** Connection id the error belongs to */
|
|
11
60
|
cid?: string | undefined;
|
package/dist/esm/errors.js
CHANGED
|
@@ -1,3 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `code` values ImapFlow sets on the errors it raises, so they can be matched without
|
|
3
|
+
* string literals: `if (err.code === ImapFlowErrorCode.NoConnection)`.
|
|
4
|
+
*
|
|
5
|
+
* Parser failures use `ParserError` followed by a number (`ParserError11`) and are not listed
|
|
6
|
+
* one by one, test them with `err.code?.startsWith('ParserError')`. Errors from the socket, TLS
|
|
7
|
+
* or DNS layer pass through with Node's own code (`ECONNREFUSED`, `ENOTFOUND`, ...).
|
|
8
|
+
*/
|
|
9
|
+
export const ImapFlowErrorCode = {
|
|
10
|
+
// the connection is gone, the command was not (or can no longer be) completed
|
|
11
|
+
NoConnection: 'NoConnection',
|
|
12
|
+
EConnectionClosed: 'EConnectionClosed',
|
|
13
|
+
StateLogout: 'StateLogout',
|
|
14
|
+
ClosedAfterConnectText: 'ClosedAfterConnectText',
|
|
15
|
+
ClosedAfterConnectTLS: 'ClosedAfterConnectTLS',
|
|
16
|
+
// timeouts
|
|
17
|
+
CONNECT_TIMEOUT: 'CONNECT_TIMEOUT',
|
|
18
|
+
GREETING_TIMEOUT: 'GREETING_TIMEOUT',
|
|
19
|
+
UPGRADE_TIMEOUT: 'UPGRADE_TIMEOUT',
|
|
20
|
+
ETIMEOUT: 'ETIMEOUT',
|
|
21
|
+
LockTimeout: 'LockTimeout',
|
|
22
|
+
// the server
|
|
23
|
+
ETHROTTLE: 'ETHROTTLE',
|
|
24
|
+
UnexpectedTag: 'UnexpectedTag',
|
|
25
|
+
InvalidResponse: 'InvalidResponse',
|
|
26
|
+
ResponseProcessingFailed: 'ResponseProcessingFailed',
|
|
27
|
+
STARTTLS_INJECTION: 'STARTTLS_INJECTION',
|
|
28
|
+
COMPRESS_TRAILING_DATA: 'COMPRESS_TRAILING_DATA',
|
|
29
|
+
PollFailed: 'PollFailed',
|
|
30
|
+
NotFound: 'NotFound',
|
|
31
|
+
MissingServerExtension: 'MissingServerExtension',
|
|
32
|
+
// response parsing and size limits
|
|
33
|
+
ParserError: 'ParserError',
|
|
34
|
+
ParserErrorExchange: 'ParserErrorExchange',
|
|
35
|
+
MAX_IMAP_NESTING_REACHED: 'MAX_IMAP_NESTING_REACHED',
|
|
36
|
+
LineTooLarge: 'LineTooLarge',
|
|
37
|
+
LiteralTooLarge: 'LiteralTooLarge',
|
|
38
|
+
ResponseTooLarge: 'ResponseTooLarge',
|
|
39
|
+
// invalid values in a command
|
|
40
|
+
InvalidStringValue: 'InvalidStringValue',
|
|
41
|
+
InvalidTokenValue: 'InvalidTokenValue',
|
|
42
|
+
InvalidTextValue: 'InvalidTextValue',
|
|
43
|
+
InvalidSequenceSet: 'InvalidSequenceSet',
|
|
44
|
+
InvalidSearchQuery: 'InvalidSearchQuery',
|
|
45
|
+
// download()
|
|
46
|
+
DownloadOverflow: 'DownloadOverflow',
|
|
47
|
+
DownloadIncomplete: 'DownloadIncomplete',
|
|
48
|
+
// proxy connections
|
|
49
|
+
ProxyError: 'ProxyError',
|
|
50
|
+
EPROXY: 'EPROXY',
|
|
51
|
+
UnsupportedProxyAddress: 'UnsupportedProxyAddress',
|
|
52
|
+
ERR_INVALID_URL: 'ERR_INVALID_URL',
|
|
53
|
+
// API misuse
|
|
54
|
+
InstanceReused: 'InstanceReused'
|
|
55
|
+
};
|
|
1
56
|
/**
|
|
2
57
|
* Error subclass thrown when IMAP authentication fails.
|
|
3
58
|
*/
|
|
@@ -145,13 +145,24 @@ export declare class ImapStream extends Transform {
|
|
|
145
145
|
* pushed downstream as a readable object.
|
|
146
146
|
*
|
|
147
147
|
* @param chunk - The raw data chunk to process.
|
|
148
|
-
* @param startPos - The byte offset within the chunk to start processing from.
|
|
149
148
|
*/
|
|
150
|
-
processInputChunk(chunk: Buffer
|
|
149
|
+
processInputChunk(chunk: Buffer): Promise<void>;
|
|
150
|
+
/**
|
|
151
|
+
* Processes the chunk from `startPos` until the parser state changes or the chunk ends.
|
|
152
|
+
*
|
|
153
|
+
* @returns The offset to continue from after a state switch, or `null` when done with the chunk.
|
|
154
|
+
*/
|
|
155
|
+
processChunkSegment(chunk: Buffer, startPos: number): Promise<number | null>;
|
|
151
156
|
/**
|
|
152
157
|
* Drains the input queue by processing each queued chunk sequentially.
|
|
153
158
|
* Yields to the event loop every 10 chunks to prevent CPU blocking on
|
|
154
159
|
* large bursts of incoming data.
|
|
160
|
+
*
|
|
161
|
+
* The `processingInput` guard is cleared in the same synchronous step that finds the queue
|
|
162
|
+
* empty. Clearing it later (in a promise handler) leaves a gap of a few microtasks where a
|
|
163
|
+
* chunk delivered by the writable side is queued but no loop is started for it, so its
|
|
164
|
+
* transform callback is never called and the socket is never read again. Workers deliver
|
|
165
|
+
* the next chunk inside that gap.
|
|
155
166
|
*/
|
|
156
167
|
processInput(): Promise<void>;
|
|
157
168
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Transform } from 'node:stream';
|
|
2
|
-
import
|
|
2
|
+
import { createConnectionLogger } from '../logger.js';
|
|
3
3
|
import { MAX_LITERAL_SIZE, MAX_LINE_SIZE, MAX_RESPONSE_SIZE, normalizeLimit, createLiteralTooLargeError } from './limits.js';
|
|
4
4
|
const LINE = 0x01;
|
|
5
5
|
const LITERAL = 0x02;
|
|
@@ -30,13 +30,12 @@ export class ImapStream extends Transform {
|
|
|
30
30
|
});
|
|
31
31
|
this.options = options || {};
|
|
32
32
|
this.cid = this.options.cid;
|
|
33
|
-
this.
|
|
34
|
-
this.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
});
|
|
33
|
+
if (this.options.logger && typeof this.options.logger === 'object') {
|
|
34
|
+
this.log = this.options.logger;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
this.log = createConnectionLogger({ cid: this.cid, logRaw: this.options.logRaw });
|
|
38
|
+
}
|
|
40
39
|
this.readBytesCounter = 0;
|
|
41
40
|
// Maximum length of a single line (response without a literal). Bounds the line buffer
|
|
42
41
|
// so a server that never sends a line terminator cannot exhaust memory.
|
|
@@ -217,12 +216,23 @@ export class ImapStream extends Transform {
|
|
|
217
216
|
* pushed downstream as a readable object.
|
|
218
217
|
*
|
|
219
218
|
* @param chunk - The raw data chunk to process.
|
|
220
|
-
* @param startPos - The byte offset within the chunk to start processing from.
|
|
221
219
|
*/
|
|
222
|
-
async processInputChunk(chunk
|
|
223
|
-
|
|
220
|
+
async processInputChunk(chunk) {
|
|
221
|
+
// Every state switch hands back the offset to resume from instead of recursing, so a
|
|
222
|
+
// chunk packed with thousands of small literals can not exhaust the call stack
|
|
223
|
+
let nextPos = 0;
|
|
224
|
+
while (nextPos !== null) {
|
|
225
|
+
nextPos = await this.processChunkSegment(chunk, nextPos);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Processes the chunk from `startPos` until the parser state changes or the chunk ends.
|
|
230
|
+
*
|
|
231
|
+
* @returns The offset to continue from after a state switch, or `null` when done with the chunk.
|
|
232
|
+
*/
|
|
233
|
+
async processChunkSegment(chunk, startPos) {
|
|
224
234
|
if (this.destroyed || startPos >= chunk.length) {
|
|
225
|
-
return;
|
|
235
|
+
return null;
|
|
226
236
|
}
|
|
227
237
|
switch (this.state) {
|
|
228
238
|
case LINE: {
|
|
@@ -234,7 +244,7 @@ export class ImapStream extends Transform {
|
|
|
234
244
|
// TCP chunk boundaries happen to fall.
|
|
235
245
|
let segment = chunk.subarray(lineStart, i + 1);
|
|
236
246
|
if (!this.checkLineLength(this.lineBytes + segment.length)) {
|
|
237
|
-
return;
|
|
247
|
+
return null;
|
|
238
248
|
}
|
|
239
249
|
this.lineBuffer.push(segment);
|
|
240
250
|
lineStart = i + 1;
|
|
@@ -246,18 +256,18 @@ export class ImapStream extends Transform {
|
|
|
246
256
|
// would otherwise be emitted as part of the rejected command.
|
|
247
257
|
let isLiteralMarker = this.checkLiteralMarker(line);
|
|
248
258
|
if (this.destroyed) {
|
|
249
|
-
return;
|
|
259
|
+
return null;
|
|
250
260
|
}
|
|
251
261
|
// Count the line itself and, for a literal marker, the declared
|
|
252
262
|
// literal bytes against the cumulative per-response budget, so a
|
|
253
263
|
// response assembled from many tokens stays bounded as a whole
|
|
254
264
|
if (!this.checkResponseSize(line.length + (isLiteralMarker ? this.literalWaiting : 0))) {
|
|
255
|
-
return;
|
|
265
|
+
return null;
|
|
256
266
|
}
|
|
257
267
|
this.inputBuffer.push(line);
|
|
258
268
|
if (isLiteralMarker) {
|
|
259
269
|
// switch into literal mode and start over
|
|
260
|
-
return
|
|
270
|
+
return lineStart;
|
|
261
271
|
}
|
|
262
272
|
// reached end of command input, emit it
|
|
263
273
|
let payload = this.inputBuffer.length === 1 ? this.inputBuffer[0] : Buffer.concat(this.inputBuffer);
|
|
@@ -291,7 +301,7 @@ export class ImapStream extends Transform {
|
|
|
291
301
|
});
|
|
292
302
|
this.pendingPush = null;
|
|
293
303
|
if (this.destroyed) {
|
|
294
|
-
return;
|
|
304
|
+
return null;
|
|
295
305
|
}
|
|
296
306
|
}
|
|
297
307
|
}
|
|
@@ -307,7 +317,7 @@ export class ImapStream extends Transform {
|
|
|
307
317
|
// while a server streams a line that never terminates - only the much
|
|
308
318
|
// larger line cap would hold it back.
|
|
309
319
|
if (!this.checkLineLength(this.lineBytes + tail.length) || !this.checkResponseSize(this.lineBytes + tail.length, true)) {
|
|
310
|
-
return;
|
|
320
|
+
return null;
|
|
311
321
|
}
|
|
312
322
|
this.lineBytes += tail.length;
|
|
313
323
|
this.lineBuffer.push(tail);
|
|
@@ -325,33 +335,45 @@ export class ImapStream extends Transform {
|
|
|
325
335
|
this.literalBuffer = [];
|
|
326
336
|
this.state = LINE;
|
|
327
337
|
if (remainingInChunk > bytesToRead) {
|
|
328
|
-
return
|
|
338
|
+
return startPos + bytesToRead;
|
|
329
339
|
}
|
|
330
340
|
}
|
|
331
341
|
break;
|
|
332
342
|
}
|
|
333
343
|
}
|
|
344
|
+
return null;
|
|
334
345
|
}
|
|
335
346
|
/**
|
|
336
347
|
* Drains the input queue by processing each queued chunk sequentially.
|
|
337
348
|
* Yields to the event loop every 10 chunks to prevent CPU blocking on
|
|
338
349
|
* large bursts of incoming data.
|
|
350
|
+
*
|
|
351
|
+
* The `processingInput` guard is cleared in the same synchronous step that finds the queue
|
|
352
|
+
* empty. Clearing it later (in a promise handler) leaves a gap of a few microtasks where a
|
|
353
|
+
* chunk delivered by the writable side is queued but no loop is started for it, so its
|
|
354
|
+
* transform callback is never called and the socket is never read again. Workers deliver
|
|
355
|
+
* the next chunk inside that gap.
|
|
339
356
|
*/
|
|
340
357
|
async processInput() {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
this.
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
358
|
+
try {
|
|
359
|
+
let data;
|
|
360
|
+
let processedCount = 0;
|
|
361
|
+
while (!this.destroyed && (data = this.inputQueue.shift())) {
|
|
362
|
+
this.activeInput = data;
|
|
363
|
+
await this.processInputChunk(data.chunk);
|
|
364
|
+
this.activeInput = null;
|
|
365
|
+
// mark chunk as processed
|
|
366
|
+
this.releaseInput(data);
|
|
367
|
+
// Yield to event loop every 10 chunks to prevent CPU blocking
|
|
368
|
+
processedCount++;
|
|
369
|
+
if (processedCount % 10 === 0) {
|
|
370
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
371
|
+
}
|
|
353
372
|
}
|
|
354
373
|
}
|
|
374
|
+
finally {
|
|
375
|
+
this.processingInput = false;
|
|
376
|
+
}
|
|
355
377
|
}
|
|
356
378
|
/**
|
|
357
379
|
* Transform stream implementation. Receives raw data chunks from the writable side,
|
|
@@ -391,9 +413,7 @@ export class ImapStream extends Transform {
|
|
|
391
413
|
this.inputQueue.push({ chunk, next });
|
|
392
414
|
if (!this.processingInput) {
|
|
393
415
|
this.processingInput = true;
|
|
394
|
-
this.processInput()
|
|
395
|
-
.catch(err => this.failStream(err))
|
|
396
|
-
.finally(() => (this.processingInput = false));
|
|
416
|
+
this.processInput().catch(err => this.failStream(err));
|
|
397
417
|
}
|
|
398
418
|
}
|
|
399
419
|
/**
|
|
@@ -321,15 +321,21 @@ export class TokenParser {
|
|
|
321
321
|
this.currentNode = this.createNode(this.currentNode, this.pos + i + 10);
|
|
322
322
|
// just call this an ATOM, even though IMAPURL might be more correct
|
|
323
323
|
this.currentNode.type = 'ATOM';
|
|
324
|
-
// jump i to the ']'
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
324
|
+
// jump i to the ']' that closes the section. The URL itself can
|
|
325
|
+
// hold a bracketed IPv6 host (imap://[::1]/INBOX), so brackets
|
|
326
|
+
// opened inside the URL are matched before the closing one.
|
|
327
|
+
let depth = 0;
|
|
328
|
+
for (i = i + 10; i < this.str.length; i++) {
|
|
329
|
+
let urlChr = this.str.charAt(i);
|
|
330
|
+
if (urlChr === '[') {
|
|
331
|
+
depth++;
|
|
332
|
+
}
|
|
333
|
+
else if (urlChr === ']' && depth-- === 0) {
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
332
336
|
}
|
|
337
|
+
// A malformed REFERRAL with no closing ']' leaves i at the end of
|
|
338
|
+
// the string, so the URL takes the rest of it.
|
|
333
339
|
this.currentNode.endPos = this.pos + i - 1;
|
|
334
340
|
this.currentNode.value = this.str.substring(this.currentNode.startPos - this.pos, this.currentNode.endPos - this.pos + 1);
|
|
335
341
|
this.currentNode = this.currentNode.parentNode;
|
package/dist/esm/imap-flow.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { AuthenticationFailure } from './errors.js';
|
|
|
9
9
|
import type { AppendResponseObject, CopyResponseObject, DownloadManyOptions, DownloadManyResult, DownloadObject, DownloadNotFound, DownloadOptions, ESearchResult, FetchMessageObject, FetchOptions, FetchQueryObject, IdInfoObject, ImapFlowEvents, ImapFlowOptions, InternalLogger, ListOptions, ListResponse, ListTreeResponse, MailboxCreateResponse, MailboxDeleteResponse, MailboxLockObject, MailboxLockOptions, MailboxObject, MailboxOpenOptions, MailboxRenameResponse, MessageRange, MessageRangeOptions, NamespaceObject, NamespacesObject, QuotaResponse, SearchObject, SearchOptions, SearchReturnOption, SequenceString, StatusObject, StatusQuery, StoreOptions, TlsInfo } from './types.js';
|
|
10
10
|
export type * from './types.js';
|
|
11
11
|
export type { ImapFlowError } from './errors.js';
|
|
12
|
-
export { AuthenticationFailure } from './errors.js';
|
|
12
|
+
export { AuthenticationFailure, ImapFlowErrorCode } from './errors.js';
|
|
13
13
|
export type { ImapAttribute, ImapAttributeList, ImapAttributeNode, ImapResponse } from './handler/types.js';
|
|
14
14
|
declare const stateValues: {
|
|
15
15
|
readonly NOT_AUTHENTICATED: 1;
|
|
@@ -61,6 +61,17 @@ export interface ImapFlow {
|
|
|
61
61
|
prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this;
|
|
62
62
|
emit<K extends keyof ImapFlowEvents>(event: K, ...args: ImapFlowEvents[K]): boolean;
|
|
63
63
|
emit(event: string | symbol, ...args: any[]): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Logs out and closes the connection when the scope of an `await using` declaration ends.
|
|
66
|
+
* Never throws, the connection is closed whether LOGOUT succeeds or not. Only present on
|
|
67
|
+
* runtimes that define `Symbol.asyncDispose` (Node.js 20.4 and newer).
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* await using client = new ImapFlow({...});
|
|
71
|
+
* await client.connect();
|
|
72
|
+
* // client.logout() runs automatically when the scope exits, even on a throw
|
|
73
|
+
*/
|
|
74
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
64
75
|
}
|
|
65
76
|
/**
|
|
66
77
|
* IMAP client class for accessing IMAP mailboxes
|
|
@@ -592,7 +603,8 @@ export declare class ImapFlow extends EventEmitter {
|
|
|
592
603
|
getMailboxLock(path: string | string[], options?: MailboxLockOptions | undefined): Promise<MailboxLockObject>;
|
|
593
604
|
/**
|
|
594
605
|
* Detaches sockets from the IMAP pipeline. Useful for upgrading the connection
|
|
595
|
-
* (e.g., STARTTLS) or transferring socket ownership.
|
|
606
|
+
* (e.g., STARTTLS) or transferring socket ownership. Call it while the connection is not
|
|
607
|
+
* idling: an IDLE in progress is not broken first, so the server still expects `DONE`.
|
|
596
608
|
*
|
|
597
609
|
* @returns Socket objects: `readSocket` is the read socket (inflated socket if compression is enabled, raw socket otherwise),
|
|
598
610
|
* `writeSocket` the write socket and `socket` the raw underlying socket (same as readSocket/writeSocket when compression is disabled)
|
|
@@ -603,81 +615,6 @@ export declare class ImapFlow extends EventEmitter {
|
|
|
603
615
|
socket: ImapSocket;
|
|
604
616
|
};
|
|
605
617
|
}
|
|
606
|
-
/**
|
|
607
|
-
* Connection close event. **NB!** ImapFlow does not handle reconnects automatically.
|
|
608
|
-
* So whenever a 'close' event occurs you must create a new connection yourself.
|
|
609
|
-
*
|
|
610
|
-
* @event ImapFlow#close
|
|
611
|
-
*/
|
|
612
|
-
/**
|
|
613
|
-
* Error event. In most cases getting an error event also means that connection is closed
|
|
614
|
-
* and pending operations should return with a failure.
|
|
615
|
-
*
|
|
616
|
-
* @event ImapFlow#error
|
|
617
|
-
* @example
|
|
618
|
-
* client.on('error', err=>{
|
|
619
|
-
* console.log(`Error occurred: ${err.message}`);
|
|
620
|
-
* });
|
|
621
|
-
*/
|
|
622
|
-
/**
|
|
623
|
-
* Message count in currently opened mailbox changed
|
|
624
|
-
*
|
|
625
|
-
* @event ImapFlow#exists
|
|
626
|
-
* @example
|
|
627
|
-
* client.on('exists', data=>{
|
|
628
|
-
* console.log(`Message count in "${data.path}" is ${data.count}`);
|
|
629
|
-
* });
|
|
630
|
-
*/
|
|
631
|
-
/**
|
|
632
|
-
* Deleted message sequence number in currently opened mailbox. One event is fired for every deleted email.
|
|
633
|
-
*
|
|
634
|
-
* @event ImapFlow#expunge
|
|
635
|
-
* @example
|
|
636
|
-
* client.on('expunge', data=>{
|
|
637
|
-
* console.log(`Message #${data.seq} was deleted from "${data.path}"`);
|
|
638
|
-
* });
|
|
639
|
-
*/
|
|
640
|
-
/**
|
|
641
|
-
* Flags were updated for a message. Not all servers fire this event.
|
|
642
|
-
*
|
|
643
|
-
* @event ImapFlow#flags
|
|
644
|
-
* @example
|
|
645
|
-
* client.on('flags', data=>{
|
|
646
|
-
* console.log(`Flag set for #${data.seq} is now "${Array.from(data.flags).join(', ')}"`);
|
|
647
|
-
* });
|
|
648
|
-
*/
|
|
649
|
-
/**
|
|
650
|
-
* Mailbox was opened
|
|
651
|
-
*
|
|
652
|
-
* @event ImapFlow#mailboxOpen
|
|
653
|
-
* @example
|
|
654
|
-
* client.on('mailboxOpen', mailbox => {
|
|
655
|
-
* console.log(`Mailbox ${mailbox.path} was opened`);
|
|
656
|
-
* });
|
|
657
|
-
*/
|
|
658
|
-
/**
|
|
659
|
-
* Mailbox was closed
|
|
660
|
-
*
|
|
661
|
-
* Emitted both when a selected mailbox is closed explicitly, by `mailboxClose()` or by
|
|
662
|
-
* selecting a different mailbox, and when the connection itself goes away while a mailbox
|
|
663
|
-
* was still selected, whether through a clean logout or a lost transport. The transition is
|
|
664
|
-
* reported once per selected mailbox, before the `close` event.
|
|
665
|
-
*
|
|
666
|
-
* @event ImapFlow#mailboxClose
|
|
667
|
-
* @example
|
|
668
|
-
* client.on('mailboxClose', mailbox => {
|
|
669
|
-
* console.log(`Mailbox ${mailbox.path} was closed`);
|
|
670
|
-
* });
|
|
671
|
-
*/
|
|
672
|
-
/**
|
|
673
|
-
* Log event if `emitLogs=true`
|
|
674
|
-
*
|
|
675
|
-
* @event ImapFlow#log
|
|
676
|
-
* @example
|
|
677
|
-
* client.on('log', entry => {
|
|
678
|
-
* console.log(`${entry.cid} ${entry.msg}`);
|
|
679
|
-
* });
|
|
680
|
-
*/
|
|
681
618
|
declare const imapflow: {
|
|
682
619
|
ImapFlow: typeof ImapFlow;
|
|
683
620
|
AuthenticationFailure: typeof AuthenticationFailure;
|