imapflow 2.0.8 → 2.1.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 +20 -0
- package/dist/cjs/commands/list.js +20 -16
- package/dist/cjs/commands/select.js +7 -3
- package/dist/cjs/errors.d.ts +48 -1
- package/dist/cjs/errors.js +53 -1
- package/dist/cjs/handler/imap-stream.d.ts +13 -2
- package/dist/cjs/handler/imap-stream.js +47 -26
- package/dist/cjs/handler/token-parser.js +14 -8
- package/dist/cjs/imap-flow.d.ts +14 -77
- package/dist/cjs/imap-flow.js +35 -4
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/cjs/tools.d.ts +1 -1
- package/dist/cjs/tools.js +9 -8
- package/dist/esm/commands/list.js +20 -16
- package/dist/esm/commands/select.js +7 -3
- package/dist/esm/errors.d.ts +48 -1
- package/dist/esm/errors.js +52 -0
- package/dist/esm/handler/imap-stream.d.ts +13 -2
- package/dist/esm/handler/imap-stream.js +47 -26
- package/dist/esm/handler/token-parser.js +14 -8
- package/dist/esm/imap-flow.d.ts +14 -77
- package/dist/esm/imap-flow.js +34 -4
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/dist/esm/tools.d.ts +1 -1
- package/dist/esm/tools.js +7 -6
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.1.0](https://github.com/postalsys/imapflow/compare/v2.0.8...v2.1.0) (2026-09-27)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* export ImapFlowErrorCode with the error codes the library sets ([50c909c](https://github.com/postalsys/imapflow/commit/50c909c30a8cda4731c397f1a77c3bf3d2d17102))
|
|
9
|
+
* support `await using` through Symbol.asyncDispose ([baadd4b](https://github.com/postalsys/imapflow/commit/baadd4bd73a8d9c46aab2966beb55698a42e71b2))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* accept a number or string uidValidity for QRESYNC ([e87d0c2](https://github.com/postalsys/imapflow/commit/e87d0c216e48e2dda23d94a7c942682ec28258ca))
|
|
15
|
+
* give a special-use type to its next candidate when the best is taken ([5b0d357](https://github.com/postalsys/imapflow/commit/5b0d357ec1f30cd7b43a7ad06e3a22a9ac244bad))
|
|
16
|
+
* keep a bracketed IPv6 host in a REFERRAL URL ([1e90b70](https://github.com/postalsys/imapflow/commit/1e90b706f72c54e4d1397e11e0c0b2aabc2f2549))
|
|
17
|
+
* keep auto-IDLE off a socket handed over by unbind() ([a47b3ae](https://github.com/postalsys/imapflow/commit/a47b3ae99e5e1e00fd8b804a8072aeccc34c6a5a))
|
|
18
|
+
* parse a chunk of many literals in a loop instead of recursing ([db4c704](https://github.com/postalsys/imapflow/commit/db4c704291054cff5a9d87386a093d113ea69fc3))
|
|
19
|
+
* process a chunk that arrives while the input loop is winding down ([c73f3ea](https://github.com/postalsys/imapflow/commit/c73f3ea18f5557abf91414a0faf98da7150e3f4f)), closes [#408](https://github.com/postalsys/imapflow/issues/408)
|
|
20
|
+
* read the last extension field of a BODYSTRUCTURE part ([d5cf6c0](https://github.com/postalsys/imapflow/commit/d5cf6c0282cf35df2de7eb153fed24ecd09448d3))
|
|
21
|
+
* reject connect() right away on a BYE greeting ([dc5d80e](https://github.com/postalsys/imapflow/commit/dc5d80e6f6efdebe92f461e23f41dc64bce6c4aa))
|
|
22
|
+
|
|
3
23
|
## [2.0.8](https://github.com/postalsys/imapflow/compare/v2.0.7...v2.0.8) (2026-09-27)
|
|
4
24
|
|
|
5
25
|
|
|
@@ -447,23 +447,27 @@ async function list(connection, reference, mailbox, options) {
|
|
|
447
447
|
connection.log.warn({ msg: 'Failed to request subscription info', err, cid: connection.id });
|
|
448
448
|
}
|
|
449
449
|
}
|
|
450
|
-
// Resolve special-use conflicts
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
450
|
+
// Resolve special-use conflicts. Each type goes to one mailbox and each mailbox gets
|
|
451
|
+
// at most one type. Candidates are taken in priority order across all types (user >
|
|
452
|
+
// extension > name, then alphabetically), so a mailbox claimed by a stronger match
|
|
453
|
+
// leaves its other type to that type's next candidate instead of to nobody.
|
|
454
|
+
let candidates = Object.entries(specialUseMatches).flatMap(([type, matches]) => matches.map(match => ({ type, ...match })));
|
|
455
|
+
candidates.sort((a, b) => {
|
|
456
|
+
let aSource = SOURCE_SORT_ORDER.indexOf(a.source);
|
|
457
|
+
let bSource = SOURCE_SORT_ORDER.indexOf(b.source);
|
|
458
|
+
if (aSource === bSource) {
|
|
459
|
+
return a.entry.path.localeCompare(b.entry.path);
|
|
460
|
+
}
|
|
461
|
+
return aSource - bSource;
|
|
462
|
+
});
|
|
463
|
+
let assignedTypes = new Set();
|
|
464
|
+
for (let { type, entry, source } of candidates) {
|
|
465
|
+
if (assignedTypes.has(type) || entry.specialUse) {
|
|
466
|
+
continue;
|
|
466
467
|
}
|
|
468
|
+
entry.specialUse = type;
|
|
469
|
+
entry.specialUseSource = PUBLIC_SOURCE[source] || source;
|
|
470
|
+
assignedTypes.add(type);
|
|
467
471
|
}
|
|
468
472
|
// No source answered, so "not subscribed" was never actually reported for any of
|
|
469
473
|
// these folders - the state is unknown, not false. Reporting the whole listing as
|
|
@@ -95,12 +95,16 @@ async function select(connection, pathInput, options) {
|
|
|
95
95
|
// QRESYNC (RFC 7162): allows efficient mailbox resynchronization by sending
|
|
96
96
|
// the last known UIDVALIDITY and HIGHESTMODSEQ. Server responds with only
|
|
97
97
|
// the changes (new flags, expunged UIDs) since that point.
|
|
98
|
+
// The caller may pass UIDVALIDITY as a bigint, a number or a string. It is parsed once,
|
|
99
|
+
// so the value that is sent is also the one checked against the server's below, and a
|
|
100
|
+
// value that is not a plain decimal skips QRESYNC instead of reaching the server.
|
|
101
|
+
let uidValidity = options.uidValidity !== undefined ? (0, tools_js_1.parseBigIntValue)(String(options.uidValidity)) : false;
|
|
98
102
|
let extraArgs = [];
|
|
99
|
-
if (connection.enabled.has('QRESYNC') && options.changedSince &&
|
|
103
|
+
if (connection.enabled.has('QRESYNC') && options.changedSince && uidValidity) {
|
|
100
104
|
extraArgs.push([
|
|
101
105
|
{ type: 'ATOM', value: 'QRESYNC' },
|
|
102
106
|
[
|
|
103
|
-
{ type: 'ATOM', value:
|
|
107
|
+
{ type: 'ATOM', value: uidValidity.toString() },
|
|
104
108
|
{ type: 'ATOM', value: options.changedSince.toString() }
|
|
105
109
|
]
|
|
106
110
|
]);
|
|
@@ -205,7 +209,7 @@ async function select(connection, pathInput, options) {
|
|
|
205
209
|
// QRESYNC results are only valid if UIDVALIDITY matches, HIGHESTMODSEQ is
|
|
206
210
|
// present, and the mailbox supports mod-sequences. If any condition fails,
|
|
207
211
|
// the client cannot trust the incremental updates and must do a full resync.
|
|
208
|
-
if (map.qresync && (
|
|
212
|
+
if (map.qresync && (uidValidity !== map.uidValidity || !map.highestModseq || map.noModseq)) {
|
|
209
213
|
map.qresync = false;
|
|
210
214
|
}
|
|
211
215
|
// Transition mailbox state: save previous mailbox reference, temporarily
|
package/dist/cjs/errors.d.ts
CHANGED
|
@@ -1,11 +1,58 @@
|
|
|
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 DownloadOverflow: "DownloadOverflow";
|
|
41
|
+
readonly DownloadIncomplete: "DownloadIncomplete";
|
|
42
|
+
readonly ProxyError: "ProxyError";
|
|
43
|
+
readonly EPROXY: "EPROXY";
|
|
44
|
+
readonly UnsupportedProxyAddress: "UnsupportedProxyAddress";
|
|
45
|
+
readonly ERR_INVALID_URL: "ERR_INVALID_URL";
|
|
46
|
+
};
|
|
47
|
+
/** One of the {@link ImapFlowErrorCode} values */
|
|
48
|
+
export type ImapFlowErrorCode = (typeof ImapFlowErrorCode)[keyof typeof ImapFlowErrorCode];
|
|
2
49
|
/**
|
|
3
50
|
* An Error raised by ImapFlow, with the extra properties the library attaches to describe
|
|
4
51
|
* the failure. Every property is optional: which ones are present depends on where the
|
|
5
52
|
* error came from.
|
|
6
53
|
*/
|
|
7
54
|
export interface ImapFlowError extends Error {
|
|
8
|
-
/** Error code,
|
|
55
|
+
/** Error code, one of {@link ImapFlowErrorCode}, a parser error code or a code from Node */
|
|
9
56
|
code?: string | undefined;
|
|
10
57
|
/** Connection id the error belongs to */
|
|
11
58
|
cid?: string | undefined;
|
package/dist/cjs/errors.js
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AuthenticationFailure = void 0;
|
|
3
|
+
exports.AuthenticationFailure = exports.ImapFlowErrorCode = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The `code` values ImapFlow sets on the errors it raises, so they can be matched without
|
|
6
|
+
* string literals: `if (err.code === ImapFlowErrorCode.NoConnection)`.
|
|
7
|
+
*
|
|
8
|
+
* Parser failures use `ParserError` followed by a number (`ParserError11`) and are not listed
|
|
9
|
+
* one by one, test them with `err.code?.startsWith('ParserError')`. Errors from the socket, TLS
|
|
10
|
+
* or DNS layer pass through with Node's own code (`ECONNREFUSED`, `ENOTFOUND`, ...).
|
|
11
|
+
*/
|
|
12
|
+
exports.ImapFlowErrorCode = {
|
|
13
|
+
// the connection is gone, the command was not (or can no longer be) completed
|
|
14
|
+
NoConnection: 'NoConnection',
|
|
15
|
+
EConnectionClosed: 'EConnectionClosed',
|
|
16
|
+
StateLogout: 'StateLogout',
|
|
17
|
+
ClosedAfterConnectText: 'ClosedAfterConnectText',
|
|
18
|
+
ClosedAfterConnectTLS: 'ClosedAfterConnectTLS',
|
|
19
|
+
// timeouts
|
|
20
|
+
CONNECT_TIMEOUT: 'CONNECT_TIMEOUT',
|
|
21
|
+
GREETING_TIMEOUT: 'GREETING_TIMEOUT',
|
|
22
|
+
UPGRADE_TIMEOUT: 'UPGRADE_TIMEOUT',
|
|
23
|
+
ETIMEOUT: 'ETIMEOUT',
|
|
24
|
+
LockTimeout: 'LockTimeout',
|
|
25
|
+
// the server
|
|
26
|
+
ETHROTTLE: 'ETHROTTLE',
|
|
27
|
+
UnexpectedTag: 'UnexpectedTag',
|
|
28
|
+
InvalidResponse: 'InvalidResponse',
|
|
29
|
+
ResponseProcessingFailed: 'ResponseProcessingFailed',
|
|
30
|
+
STARTTLS_INJECTION: 'STARTTLS_INJECTION',
|
|
31
|
+
COMPRESS_TRAILING_DATA: 'COMPRESS_TRAILING_DATA',
|
|
32
|
+
PollFailed: 'PollFailed',
|
|
33
|
+
NotFound: 'NotFound',
|
|
34
|
+
MissingServerExtension: 'MissingServerExtension',
|
|
35
|
+
// response parsing and size limits
|
|
36
|
+
ParserError: 'ParserError',
|
|
37
|
+
ParserErrorExchange: 'ParserErrorExchange',
|
|
38
|
+
MAX_IMAP_NESTING_REACHED: 'MAX_IMAP_NESTING_REACHED',
|
|
39
|
+
LineTooLarge: 'LineTooLarge',
|
|
40
|
+
LiteralTooLarge: 'LiteralTooLarge',
|
|
41
|
+
ResponseTooLarge: 'ResponseTooLarge',
|
|
42
|
+
// invalid values in a command
|
|
43
|
+
InvalidStringValue: 'InvalidStringValue',
|
|
44
|
+
InvalidTokenValue: 'InvalidTokenValue',
|
|
45
|
+
InvalidTextValue: 'InvalidTextValue',
|
|
46
|
+
InvalidSequenceSet: 'InvalidSequenceSet',
|
|
47
|
+
// download()
|
|
48
|
+
DownloadOverflow: 'DownloadOverflow',
|
|
49
|
+
DownloadIncomplete: 'DownloadIncomplete',
|
|
50
|
+
// proxy connections
|
|
51
|
+
ProxyError: 'ProxyError',
|
|
52
|
+
EPROXY: 'EPROXY',
|
|
53
|
+
UnsupportedProxyAddress: 'UnsupportedProxyAddress',
|
|
54
|
+
ERR_INVALID_URL: 'ERR_INVALID_URL'
|
|
55
|
+
};
|
|
4
56
|
/**
|
|
5
57
|
* Error subclass thrown when IMAP authentication fails.
|
|
6
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
|
/**
|
|
@@ -223,12 +223,23 @@ class ImapStream extends node_stream_1.Transform {
|
|
|
223
223
|
* pushed downstream as a readable object.
|
|
224
224
|
*
|
|
225
225
|
* @param chunk - The raw data chunk to process.
|
|
226
|
-
* @param startPos - The byte offset within the chunk to start processing from.
|
|
227
226
|
*/
|
|
228
|
-
async processInputChunk(chunk
|
|
229
|
-
|
|
227
|
+
async processInputChunk(chunk) {
|
|
228
|
+
// Every state switch hands back the offset to resume from instead of recursing, so a
|
|
229
|
+
// chunk packed with thousands of small literals can not exhaust the call stack
|
|
230
|
+
let nextPos = 0;
|
|
231
|
+
while (nextPos !== null) {
|
|
232
|
+
nextPos = await this.processChunkSegment(chunk, nextPos);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Processes the chunk from `startPos` until the parser state changes or the chunk ends.
|
|
237
|
+
*
|
|
238
|
+
* @returns The offset to continue from after a state switch, or `null` when done with the chunk.
|
|
239
|
+
*/
|
|
240
|
+
async processChunkSegment(chunk, startPos) {
|
|
230
241
|
if (this.destroyed || startPos >= chunk.length) {
|
|
231
|
-
return;
|
|
242
|
+
return null;
|
|
232
243
|
}
|
|
233
244
|
switch (this.state) {
|
|
234
245
|
case LINE: {
|
|
@@ -240,7 +251,7 @@ class ImapStream extends node_stream_1.Transform {
|
|
|
240
251
|
// TCP chunk boundaries happen to fall.
|
|
241
252
|
let segment = chunk.subarray(lineStart, i + 1);
|
|
242
253
|
if (!this.checkLineLength(this.lineBytes + segment.length)) {
|
|
243
|
-
return;
|
|
254
|
+
return null;
|
|
244
255
|
}
|
|
245
256
|
this.lineBuffer.push(segment);
|
|
246
257
|
lineStart = i + 1;
|
|
@@ -252,18 +263,18 @@ class ImapStream extends node_stream_1.Transform {
|
|
|
252
263
|
// would otherwise be emitted as part of the rejected command.
|
|
253
264
|
let isLiteralMarker = this.checkLiteralMarker(line);
|
|
254
265
|
if (this.destroyed) {
|
|
255
|
-
return;
|
|
266
|
+
return null;
|
|
256
267
|
}
|
|
257
268
|
// Count the line itself and, for a literal marker, the declared
|
|
258
269
|
// literal bytes against the cumulative per-response budget, so a
|
|
259
270
|
// response assembled from many tokens stays bounded as a whole
|
|
260
271
|
if (!this.checkResponseSize(line.length + (isLiteralMarker ? this.literalWaiting : 0))) {
|
|
261
|
-
return;
|
|
272
|
+
return null;
|
|
262
273
|
}
|
|
263
274
|
this.inputBuffer.push(line);
|
|
264
275
|
if (isLiteralMarker) {
|
|
265
276
|
// switch into literal mode and start over
|
|
266
|
-
return
|
|
277
|
+
return lineStart;
|
|
267
278
|
}
|
|
268
279
|
// reached end of command input, emit it
|
|
269
280
|
let payload = this.inputBuffer.length === 1 ? this.inputBuffer[0] : Buffer.concat(this.inputBuffer);
|
|
@@ -297,7 +308,7 @@ class ImapStream extends node_stream_1.Transform {
|
|
|
297
308
|
});
|
|
298
309
|
this.pendingPush = null;
|
|
299
310
|
if (this.destroyed) {
|
|
300
|
-
return;
|
|
311
|
+
return null;
|
|
301
312
|
}
|
|
302
313
|
}
|
|
303
314
|
}
|
|
@@ -313,7 +324,7 @@ class ImapStream extends node_stream_1.Transform {
|
|
|
313
324
|
// while a server streams a line that never terminates - only the much
|
|
314
325
|
// larger line cap would hold it back.
|
|
315
326
|
if (!this.checkLineLength(this.lineBytes + tail.length) || !this.checkResponseSize(this.lineBytes + tail.length, true)) {
|
|
316
|
-
return;
|
|
327
|
+
return null;
|
|
317
328
|
}
|
|
318
329
|
this.lineBytes += tail.length;
|
|
319
330
|
this.lineBuffer.push(tail);
|
|
@@ -331,33 +342,45 @@ class ImapStream extends node_stream_1.Transform {
|
|
|
331
342
|
this.literalBuffer = [];
|
|
332
343
|
this.state = LINE;
|
|
333
344
|
if (remainingInChunk > bytesToRead) {
|
|
334
|
-
return
|
|
345
|
+
return startPos + bytesToRead;
|
|
335
346
|
}
|
|
336
347
|
}
|
|
337
348
|
break;
|
|
338
349
|
}
|
|
339
350
|
}
|
|
351
|
+
return null;
|
|
340
352
|
}
|
|
341
353
|
/**
|
|
342
354
|
* Drains the input queue by processing each queued chunk sequentially.
|
|
343
355
|
* Yields to the event loop every 10 chunks to prevent CPU blocking on
|
|
344
356
|
* large bursts of incoming data.
|
|
357
|
+
*
|
|
358
|
+
* The `processingInput` guard is cleared in the same synchronous step that finds the queue
|
|
359
|
+
* empty. Clearing it later (in a promise handler) leaves a gap of a few microtasks where a
|
|
360
|
+
* chunk delivered by the writable side is queued but no loop is started for it, so its
|
|
361
|
+
* transform callback is never called and the socket is never read again. Workers deliver
|
|
362
|
+
* the next chunk inside that gap.
|
|
345
363
|
*/
|
|
346
364
|
async processInput() {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
this.
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
365
|
+
try {
|
|
366
|
+
let data;
|
|
367
|
+
let processedCount = 0;
|
|
368
|
+
while (!this.destroyed && (data = this.inputQueue.shift())) {
|
|
369
|
+
this.activeInput = data;
|
|
370
|
+
await this.processInputChunk(data.chunk);
|
|
371
|
+
this.activeInput = null;
|
|
372
|
+
// mark chunk as processed
|
|
373
|
+
this.releaseInput(data);
|
|
374
|
+
// Yield to event loop every 10 chunks to prevent CPU blocking
|
|
375
|
+
processedCount++;
|
|
376
|
+
if (processedCount % 10 === 0) {
|
|
377
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
378
|
+
}
|
|
359
379
|
}
|
|
360
380
|
}
|
|
381
|
+
finally {
|
|
382
|
+
this.processingInput = false;
|
|
383
|
+
}
|
|
361
384
|
}
|
|
362
385
|
/**
|
|
363
386
|
* Transform stream implementation. Receives raw data chunks from the writable side,
|
|
@@ -397,9 +420,7 @@ class ImapStream extends node_stream_1.Transform {
|
|
|
397
420
|
this.inputQueue.push({ chunk, next });
|
|
398
421
|
if (!this.processingInput) {
|
|
399
422
|
this.processingInput = true;
|
|
400
|
-
this.processInput()
|
|
401
|
-
.catch(err => this.failStream(err))
|
|
402
|
-
.finally(() => (this.processingInput = false));
|
|
423
|
+
this.processInput().catch(err => this.failStream(err));
|
|
403
424
|
}
|
|
404
425
|
}
|
|
405
426
|
/**
|
|
@@ -327,15 +327,21 @@ class TokenParser {
|
|
|
327
327
|
this.currentNode = this.createNode(this.currentNode, this.pos + i + 10);
|
|
328
328
|
// just call this an ATOM, even though IMAPURL might be more correct
|
|
329
329
|
this.currentNode.type = 'ATOM';
|
|
330
|
-
// jump i to the ']'
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
330
|
+
// jump i to the ']' that closes the section. The URL itself can
|
|
331
|
+
// hold a bracketed IPv6 host (imap://[::1]/INBOX), so brackets
|
|
332
|
+
// opened inside the URL are matched before the closing one.
|
|
333
|
+
let depth = 0;
|
|
334
|
+
for (i = i + 10; i < this.str.length; i++) {
|
|
335
|
+
let urlChr = this.str.charAt(i);
|
|
336
|
+
if (urlChr === '[') {
|
|
337
|
+
depth++;
|
|
338
|
+
}
|
|
339
|
+
else if (urlChr === ']' && depth-- === 0) {
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
338
342
|
}
|
|
343
|
+
// A malformed REFERRAL with no closing ']' leaves i at the end of
|
|
344
|
+
// the string, so the URL takes the rest of it.
|
|
339
345
|
this.currentNode.endPos = this.pos + i - 1;
|
|
340
346
|
this.currentNode.value = this.str.substring(this.currentNode.startPos - this.pos, this.currentNode.endPos - this.pos + 1);
|
|
341
347
|
this.currentNode = this.currentNode.parentNode;
|
package/dist/cjs/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;
|
package/dist/cjs/imap-flow.js
CHANGED
|
@@ -39,7 +39,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
39
39
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
40
40
|
};
|
|
41
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
-
exports.ImapFlow = exports.AuthenticationFailure = void 0;
|
|
42
|
+
exports.ImapFlow = exports.ImapFlowErrorCode = exports.AuthenticationFailure = void 0;
|
|
43
43
|
const node_tls_1 = __importDefault(require("node:tls"));
|
|
44
44
|
const node_net_1 = __importDefault(require("node:net"));
|
|
45
45
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
@@ -58,6 +58,7 @@ const imap_commands_js_1 = __importDefault(require("./imap-commands.js"));
|
|
|
58
58
|
const tools_js_1 = require("./tools.js");
|
|
59
59
|
var errors_js_2 = require("./errors.js");
|
|
60
60
|
Object.defineProperty(exports, "AuthenticationFailure", { enumerable: true, get: function () { return errors_js_2.AuthenticationFailure; } });
|
|
61
|
+
Object.defineProperty(exports, "ImapFlowErrorCode", { enumerable: true, get: function () { return errors_js_2.ImapFlowErrorCode; } });
|
|
61
62
|
const GREETING_TIMEOUT = 16 * 1000;
|
|
62
63
|
const UPGRADE_TIMEOUT = 10 * 1000;
|
|
63
64
|
const SOCKET_TIMEOUT = 5 * 60 * 1000;
|
|
@@ -1601,6 +1602,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1601
1602
|
/** @internal */
|
|
1602
1603
|
beginSession(onUnhandledError) {
|
|
1603
1604
|
(0, tools_js_1.clearTimer)(this.greetingTimeout);
|
|
1605
|
+
this.greetingReceived = true;
|
|
1604
1606
|
this.untaggedHandlers.OK = null;
|
|
1605
1607
|
this.untaggedHandlers.PREAUTH = null;
|
|
1606
1608
|
if (this.isClosed) {
|
|
@@ -1660,6 +1662,12 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1660
1662
|
this.byeReason = reason || 'Server closed connection';
|
|
1661
1663
|
this.untaggedHandlers.BYE = null;
|
|
1662
1664
|
this.state = this.states.LOGOUT;
|
|
1665
|
+
// A BYE greeting rejects the connection outright. Do not wait for the server to close
|
|
1666
|
+
// the socket: one that keeps it open would leave connect() pending until the greeting
|
|
1667
|
+
// timeout.
|
|
1668
|
+
if (!this.greetingReceived) {
|
|
1669
|
+
this.closeAfter();
|
|
1670
|
+
}
|
|
1663
1671
|
}
|
|
1664
1672
|
// Drops every capability-derived field together - the counterpart of
|
|
1665
1673
|
// updateCapabilitiesFromRaw() below, which sets them together. rawCapabilities is
|
|
@@ -1892,7 +1900,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1892
1900
|
/** @internal */
|
|
1893
1901
|
autoidle() {
|
|
1894
1902
|
(0, tools_js_1.clearTimer)(this.idleStartTimer);
|
|
1895
|
-
if (this.options.disableAutoIdle || this.state !== this.states.SELECTED) {
|
|
1903
|
+
if (this.options.disableAutoIdle || !this.usable || this.state !== this.states.SELECTED) {
|
|
1896
1904
|
return;
|
|
1897
1905
|
}
|
|
1898
1906
|
if (this.connectionBusy()) {
|
|
@@ -1904,7 +1912,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1904
1912
|
// missed clearTimeout would inject IDLE between a caller's own commands. Declining
|
|
1905
1913
|
// postpones rather than cancels: whatever made the connection busy calls autoidle()
|
|
1906
1914
|
// again when it finishes.
|
|
1907
|
-
if (this.state !== this.states.SELECTED || this.connectionBusy()) {
|
|
1915
|
+
if (!this.usable || this.state !== this.states.SELECTED || this.connectionBusy()) {
|
|
1908
1916
|
return;
|
|
1909
1917
|
}
|
|
1910
1918
|
this.idle().catch(err => (0, tools_js_1.logConnectionError)(this, 'Auto-IDLE failed', err));
|
|
@@ -3389,7 +3397,8 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3389
3397
|
}
|
|
3390
3398
|
/**
|
|
3391
3399
|
* Detaches sockets from the IMAP pipeline. Useful for upgrading the connection
|
|
3392
|
-
* (e.g., STARTTLS) or transferring socket ownership.
|
|
3400
|
+
* (e.g., STARTTLS) or transferring socket ownership. Call it while the connection is not
|
|
3401
|
+
* idling: an IDLE in progress is not broken first, so the server still expects `DONE`.
|
|
3393
3402
|
*
|
|
3394
3403
|
* @returns Socket objects: `readSocket` is the read socket (inflated socket if compression is enabled, raw socket otherwise),
|
|
3395
3404
|
* `writeSocket` the write socket and `socket` the raw underlying socket (same as readSocket/writeSocket when compression is disabled)
|
|
@@ -3404,6 +3413,11 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
3404
3413
|
// compression is active, the PassThrough writeSocket - so the connection
|
|
3405
3414
|
// is fully released to the caller.
|
|
3406
3415
|
this.clearSocketHandlers();
|
|
3416
|
+
// The socket now belongs to the caller. Marking the client unusable keeps
|
|
3417
|
+
// auto-IDLE (armed now, or re-armed by a lock release or a finished download)
|
|
3418
|
+
// and a later dispose from writing IDLE or LOGOUT onto it.
|
|
3419
|
+
this.usable = false;
|
|
3420
|
+
(0, tools_js_1.clearTimer)(this.idleStartTimer);
|
|
3407
3421
|
const readSocket = this._inflate || socket;
|
|
3408
3422
|
const writeSocket = this.writeSocket || socket;
|
|
3409
3423
|
// Defense-in-depth: when compression is active the raw socket is orphaned
|
|
@@ -3502,6 +3516,23 @@ exports.ImapFlow = ImapFlow;
|
|
|
3502
3516
|
* console.log(`${entry.cid} ${entry.msg}`);
|
|
3503
3517
|
* });
|
|
3504
3518
|
*/
|
|
3519
|
+
// Installed outside the class body: a computed `[Symbol.asyncDispose]` key would turn into a
|
|
3520
|
+
// method named "undefined" on Node.js 20.0-20.3, which predate the symbol
|
|
3521
|
+
if (typeof Symbol.asyncDispose === 'symbol') {
|
|
3522
|
+
ImapFlow.prototype[Symbol.asyncDispose] = async function () {
|
|
3523
|
+
if (this.usable) {
|
|
3524
|
+
try {
|
|
3525
|
+
await this.logout();
|
|
3526
|
+
}
|
|
3527
|
+
catch (err) {
|
|
3528
|
+
(0, tools_js_1.logConnectionError)(this, 'LOGOUT failed while disposing', err);
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
// logout() already closes the connection; close() is idempotent and covers a client
|
|
3532
|
+
// that never connected or whose logout was skipped
|
|
3533
|
+
this.close();
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3505
3536
|
// Both `import { ImapFlow } from 'imapflow'` and `import imapflow from 'imapflow'` work, the
|
|
3506
3537
|
// latter matching the shape `require('imapflow')` has always had
|
|
3507
3538
|
const imapflow = { ImapFlow, AuthenticationFailure: errors_js_1.AuthenticationFailure };
|