imapflow 2.0.7 → 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 +29 -0
- package/dist/cjs/commands/append.js +12 -12
- package/dist/cjs/commands/authenticate.d.ts +3 -8
- package/dist/cjs/commands/close.js +2 -1
- package/dist/cjs/commands/copy.js +4 -4
- package/dist/cjs/commands/create.js +2 -3
- package/dist/cjs/commands/delete.js +4 -4
- package/dist/cjs/commands/expunge.js +8 -5
- package/dist/cjs/commands/fetch.js +12 -10
- package/dist/cjs/commands/idle.js +6 -2
- package/dist/cjs/commands/list.js +22 -18
- package/dist/cjs/commands/move.js +11 -6
- package/dist/cjs/commands/namespace.js +1 -1
- package/dist/cjs/commands/quota.js +10 -9
- package/dist/cjs/commands/rename.js +4 -4
- package/dist/cjs/commands/search.js +7 -8
- package/dist/cjs/commands/select.js +12 -9
- package/dist/cjs/commands/status.js +11 -11
- package/dist/cjs/commands/store.js +5 -5
- package/dist/cjs/commands/subscribe.js +2 -17
- package/dist/cjs/commands/subscription.d.ts +10 -0
- package/dist/cjs/commands/subscription.js +29 -0
- package/dist/cjs/commands/unsubscribe.js +2 -17
- package/dist/cjs/download.d.ts +22 -0
- package/dist/cjs/download.js +588 -0
- package/dist/cjs/errors.d.ts +50 -1
- package/dist/cjs/errors.js +53 -1
- package/dist/cjs/handler/imap-compiler.js +1 -1
- package/dist/cjs/handler/imap-stream.d.ts +13 -2
- package/dist/cjs/handler/imap-stream.js +51 -30
- package/dist/cjs/handler/parser-instance.js +2 -2
- package/dist/cjs/handler/token-parser.js +15 -9
- package/dist/cjs/imap-flow.d.ts +30 -84
- package/dist/cjs/imap-flow.js +282 -734
- package/dist/cjs/jp-decoder.js +1 -1
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +3 -3
- package/dist/cjs/search-compiler.js +5 -12
- package/dist/cjs/tools.d.ts +52 -11
- package/dist/cjs/tools.js +86 -23
- package/dist/cjs/types.d.ts +30 -16
- package/dist/esm/commands/append.js +13 -13
- package/dist/esm/commands/authenticate.d.ts +3 -8
- package/dist/esm/commands/close.js +2 -1
- package/dist/esm/commands/copy.js +5 -5
- package/dist/esm/commands/create.js +3 -4
- package/dist/esm/commands/delete.js +5 -5
- package/dist/esm/commands/expunge.js +9 -6
- package/dist/esm/commands/fetch.js +13 -11
- package/dist/esm/commands/idle.js +7 -3
- package/dist/esm/commands/list.js +22 -18
- package/dist/esm/commands/move.js +12 -7
- package/dist/esm/commands/namespace.js +2 -2
- package/dist/esm/commands/quota.js +11 -10
- package/dist/esm/commands/rename.js +5 -5
- package/dist/esm/commands/search.js +8 -9
- package/dist/esm/commands/select.js +13 -10
- package/dist/esm/commands/status.js +12 -12
- package/dist/esm/commands/store.js +6 -6
- package/dist/esm/commands/subscribe.js +2 -17
- package/dist/esm/commands/subscription.d.ts +10 -0
- package/dist/esm/commands/subscription.js +26 -0
- package/dist/esm/commands/unsubscribe.js +2 -17
- package/dist/esm/download.d.ts +22 -0
- package/dist/esm/download.js +581 -0
- package/dist/esm/errors.d.ts +50 -1
- package/dist/esm/errors.js +52 -0
- package/dist/esm/handler/imap-compiler.js +1 -1
- package/dist/esm/handler/imap-stream.d.ts +13 -2
- package/dist/esm/handler/imap-stream.js +51 -30
- package/dist/esm/handler/parser-instance.js +2 -2
- package/dist/esm/handler/token-parser.js +15 -9
- package/dist/esm/imap-flow.d.ts +30 -84
- package/dist/esm/imap-flow.js +282 -735
- package/dist/esm/jp-decoder.js +1 -1
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +3 -3
- package/dist/esm/search-compiler.js +5 -12
- package/dist/esm/tools.d.ts +52 -11
- package/dist/esm/tools.js +79 -21
- package/dist/esm/types.d.ts +30 -16
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
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
|
+
|
|
23
|
+
## [2.0.8](https://github.com/postalsys/imapflow/compare/v2.0.7...v2.0.8) (2026-09-27)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Bug Fixes
|
|
27
|
+
|
|
28
|
+
* do not expunge the source when the MOVE fallback's COPY fails ([11bbf84](https://github.com/postalsys/imapflow/commit/11bbf84fb0501f7392a5ced9787562cfcdcf7c9f)), closes [#406](https://github.com/postalsys/imapflow/issues/406)
|
|
29
|
+
* surface throttle, truncation and listener failures instead of hiding them ([03a0624](https://github.com/postalsys/imapflow/commit/03a0624d0f381e935bbc3b4e12002272426abbae))
|
|
30
|
+
* **types:** describe the quota response the way getQuota() returns it ([a2363b6](https://github.com/postalsys/imapflow/commit/a2363b6658d25d1a05a177a8dec556faf3df597b))
|
|
31
|
+
|
|
3
32
|
## [2.0.7](https://github.com/postalsys/imapflow/compare/v2.0.6...v2.0.7) (2026-09-25)
|
|
4
33
|
|
|
5
34
|
|
|
@@ -14,7 +14,7 @@ const tools_js_1 = require("../tools.js");
|
|
|
14
14
|
* @throws {Error} If the APPEND command fails or message exceeds APPENDLIMIT
|
|
15
15
|
*/
|
|
16
16
|
async function append(connection, destination, content, flags, idate) {
|
|
17
|
-
if (!
|
|
17
|
+
if (!(0, tools_js_1.isAuthenticatedState)(connection) || !destination) {
|
|
18
18
|
// nothing to do here
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
@@ -34,7 +34,9 @@ async function append(connection, destination, content, flags, idate) {
|
|
|
34
34
|
destination = (0, tools_js_1.normalizePath)(connection, destination);
|
|
35
35
|
// If appending to the currently selected mailbox, we can listen for the
|
|
36
36
|
// untagged EXISTS response to capture the new message's sequence number.
|
|
37
|
-
let
|
|
37
|
+
let selected = (0, tools_js_1.getSelectedMailbox)(connection);
|
|
38
|
+
// The selected mailbox when appending to it, false otherwise
|
|
39
|
+
const targetMailbox = selected && (0, tools_js_1.comparePaths)(connection, selected.path, destination) ? selected : false;
|
|
38
40
|
// Validate and format flags. Only flags allowed by the mailbox's permanentFlags are included.
|
|
39
41
|
flags = (Array.isArray(flags) ? flags : [].concat(flags || []))
|
|
40
42
|
.map(flag => flag && (0, tools_js_1.formatFlag)(flag.toString()))
|
|
@@ -77,13 +79,12 @@ async function append(connection, destination, content, flags, idate) {
|
|
|
77
79
|
map.seq = seq;
|
|
78
80
|
// Update the connection's mailbox state and emit 'exists' event if the
|
|
79
81
|
// count changed (notifies listeners about the new message).
|
|
80
|
-
if (
|
|
81
|
-
let
|
|
82
|
-
let prevCount = mailbox.exists;
|
|
82
|
+
if (targetMailbox) {
|
|
83
|
+
let prevCount = targetMailbox.exists;
|
|
83
84
|
if (map.seq !== prevCount) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
path:
|
|
85
|
+
targetMailbox.exists = map.seq;
|
|
86
|
+
(0, tools_js_1.emitSafe)(connection, 'exists', {
|
|
87
|
+
path: targetMailbox.path,
|
|
87
88
|
count: map.seq,
|
|
88
89
|
prevCount
|
|
89
90
|
});
|
|
@@ -94,7 +95,7 @@ async function append(connection, destination, content, flags, idate) {
|
|
|
94
95
|
try {
|
|
95
96
|
response = await connection.exec('APPEND', attributes, {
|
|
96
97
|
// Only listen for EXISTS if we're appending to the currently selected mailbox
|
|
97
|
-
untagged:
|
|
98
|
+
untagged: targetMailbox ? { EXISTS: handleExistsUpdate } : false
|
|
98
99
|
});
|
|
99
100
|
// UIDPLUS (RFC 4315): the server may include APPENDUID response code in
|
|
100
101
|
// the tagged OK. Format: [APPENDUID <uidValidity> <uid>]
|
|
@@ -119,7 +120,7 @@ async function append(connection, destination, content, flags, idate) {
|
|
|
119
120
|
response.next();
|
|
120
121
|
// If we didn't get an EXISTS during APPEND (some servers don't send it
|
|
121
122
|
// until the next command), issue a NOOP to flush pending notifications.
|
|
122
|
-
if (
|
|
123
|
+
if (targetMailbox && !map.seq) {
|
|
123
124
|
try {
|
|
124
125
|
response = await connection.exec('NOOP', false, {
|
|
125
126
|
untagged: { EXISTS: handleExistsUpdate },
|
|
@@ -142,8 +143,7 @@ async function append(connection, destination, content, flags, idate) {
|
|
|
142
143
|
return map;
|
|
143
144
|
}
|
|
144
145
|
catch (err) {
|
|
145
|
-
await (0, tools_js_1.
|
|
146
|
-
connection.log.warn({ err, cid: connection.id });
|
|
146
|
+
await (0, tools_js_1.reportCommandError)(connection, err);
|
|
147
147
|
throw err;
|
|
148
148
|
}
|
|
149
149
|
}
|
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import type { ImapFlow } from '../imap-flow.js';
|
|
2
|
+
import type { AuthOptions } from '../types.js';
|
|
2
3
|
/**
|
|
3
|
-
* Credentials for the AUTHENTICATE command
|
|
4
|
+
* Credentials for the AUTHENTICATE command: the auth options, with the password passed as `password`
|
|
4
5
|
*/
|
|
5
|
-
export interface AuthenticateCredentials {
|
|
6
|
-
/** OAuth2 access token for OAUTHBEARER/XOAUTH2 authentication */
|
|
7
|
-
accessToken?: string | undefined;
|
|
6
|
+
export interface AuthenticateCredentials extends Omit<AuthOptions, 'user' | 'pass'> {
|
|
8
7
|
/** Password for PLAIN or LOGIN authentication */
|
|
9
8
|
password?: string | undefined;
|
|
10
|
-
/** Force a specific login method (e.g., 'AUTH=PLAIN', 'AUTH=LOGIN') */
|
|
11
|
-
loginMethod?: string | undefined;
|
|
12
|
-
/** Authorization identity for PLAIN authentication */
|
|
13
|
-
authzid?: string | undefined;
|
|
14
9
|
}
|
|
15
10
|
/**
|
|
16
11
|
* Authenticates user using the best available method.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.default = close;
|
|
4
|
+
const tools_js_1 = require("../tools.js");
|
|
4
5
|
/**
|
|
5
6
|
* Closes the currently selected mailbox.
|
|
6
7
|
*
|
|
@@ -26,7 +27,7 @@ async function close(connection) {
|
|
|
26
27
|
connection.currentSelectCommand = false;
|
|
27
28
|
connection.state = connection.states.AUTHENTICATED;
|
|
28
29
|
if (currentMailbox) {
|
|
29
|
-
|
|
30
|
+
(0, tools_js_1.emitSafe)(connection, 'mailboxClose', currentMailbox);
|
|
30
31
|
}
|
|
31
32
|
return true;
|
|
32
33
|
}
|
|
@@ -14,7 +14,8 @@ const copyuid_parser_js_1 = require("./copyuid-parser.js");
|
|
|
14
14
|
* @returns Copy result with UID mapping if available, false on failure, or undefined if preconditions not met
|
|
15
15
|
*/
|
|
16
16
|
async function copy(connection, range, destination, options) {
|
|
17
|
-
|
|
17
|
+
let mailbox = (0, tools_js_1.getSelectedMailbox)(connection);
|
|
18
|
+
if (!mailbox || !range || !destination) {
|
|
18
19
|
// nothing to do here
|
|
19
20
|
return;
|
|
20
21
|
}
|
|
@@ -28,15 +29,14 @@ async function copy(connection, range, destination, options) {
|
|
|
28
29
|
try {
|
|
29
30
|
response = await connection.exec(options.uid ? 'UID COPY' : 'COPY', attributes);
|
|
30
31
|
response.next();
|
|
31
|
-
let map = { path:
|
|
32
|
+
let map = { path: mailbox.path, destination };
|
|
32
33
|
// UIDPLUS (RFC 4315): the server may include a COPYUID response code in the
|
|
33
34
|
// tagged OK response, providing a mapping from source UIDs to destination UIDs.
|
|
34
35
|
(0, copyuid_parser_js_1.parseCopyUid)(response.response, map);
|
|
35
36
|
return map;
|
|
36
37
|
}
|
|
37
38
|
catch (err) {
|
|
38
|
-
await (0, tools_js_1.
|
|
39
|
-
connection.log.warn({ err, cid: connection.id });
|
|
39
|
+
await (0, tools_js_1.reportCommandError)(connection, err);
|
|
40
40
|
return false;
|
|
41
41
|
}
|
|
42
42
|
}
|
|
@@ -11,7 +11,7 @@ const tools_js_1 = require("../tools.js");
|
|
|
11
11
|
* @throws If the CREATE command fails (except when mailbox already exists)
|
|
12
12
|
*/
|
|
13
13
|
async function create(connection, path) {
|
|
14
|
-
if (!
|
|
14
|
+
if (!(0, tools_js_1.isAuthenticatedState)(connection)) {
|
|
15
15
|
// nothing to do here
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
@@ -71,8 +71,7 @@ async function create(connection, path) {
|
|
|
71
71
|
created: false
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
-
await (0, tools_js_1.
|
|
75
|
-
connection.log.warn({ err, cid: connection.id });
|
|
74
|
+
await (0, tools_js_1.reportCommandError)(connection, err);
|
|
76
75
|
throw err;
|
|
77
76
|
}
|
|
78
77
|
}
|
|
@@ -11,14 +11,15 @@ const tools_js_1 = require("../tools.js");
|
|
|
11
11
|
* @throws If the DELETE command fails
|
|
12
12
|
*/
|
|
13
13
|
async function deleteMailbox(connection, path) {
|
|
14
|
-
if (!
|
|
14
|
+
if (!(0, tools_js_1.isAuthenticatedState)(connection)) {
|
|
15
15
|
// nothing to do here
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
18
18
|
path = (0, tools_js_1.normalizePath)(connection, path);
|
|
19
19
|
// If the mailbox to delete is currently selected, we must close/deselect it first.
|
|
20
20
|
// IMAP servers reject DELETE on the currently selected mailbox (RFC 3501 6.3.4).
|
|
21
|
-
|
|
21
|
+
let selected = (0, tools_js_1.getSelectedMailbox)(connection);
|
|
22
|
+
if (selected && selected.path === path) {
|
|
22
23
|
await connection.run('CLOSE');
|
|
23
24
|
}
|
|
24
25
|
let response;
|
|
@@ -31,8 +32,7 @@ async function deleteMailbox(connection, path) {
|
|
|
31
32
|
return map;
|
|
32
33
|
}
|
|
33
34
|
catch (err) {
|
|
34
|
-
await (0, tools_js_1.
|
|
35
|
-
connection.log.warn({ err, cid: connection.id });
|
|
35
|
+
await (0, tools_js_1.reportCommandError)(connection, err);
|
|
36
36
|
throw err;
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -12,14 +12,19 @@ const tools_js_1 = require("../tools.js");
|
|
|
12
12
|
* @returns True on success, false on failure, or undefined if preconditions not met
|
|
13
13
|
*/
|
|
14
14
|
async function expunge(connection, range, options) {
|
|
15
|
-
|
|
15
|
+
let mailbox = (0, tools_js_1.getSelectedMailbox)(connection);
|
|
16
|
+
if (!mailbox || !range) {
|
|
16
17
|
// nothing to do here
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
19
20
|
options = options || {};
|
|
20
21
|
// Two-step deletion process per IMAP protocol:
|
|
21
22
|
// Step 1: Mark the target messages with the \Deleted flag.
|
|
22
|
-
|
|
23
|
+
// If that failed, EXPUNGE would not remove the target messages (and without
|
|
24
|
+
// UIDPLUS it would remove unrelated \Deleted messages), so report the failure instead.
|
|
25
|
+
if (!(await connection.messageFlagsAdd(range, ['\\Deleted'], options))) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
23
28
|
// Step 2: Issue EXPUNGE to permanently remove \Deleted messages.
|
|
24
29
|
// With UIDPLUS (RFC 4315): "UID EXPUNGE <uids>" removes only the specified UIDs,
|
|
25
30
|
// leaving other \Deleted messages untouched, important for concurrent access.
|
|
@@ -38,7 +43,6 @@ async function expunge(connection, range, options) {
|
|
|
38
43
|
if (responseCode.toUpperCase() === 'HIGHESTMODSEQ') {
|
|
39
44
|
// A response code always comes with its section, see responseCode above
|
|
40
45
|
let codeSection = section;
|
|
41
|
-
let mailbox = connection.mailbox;
|
|
42
46
|
// Bounded digit runs only: isNaN() also passes '1e5', which BigInt() rejects with
|
|
43
47
|
// a throw that the catch below would swallow, making messageDelete() report false
|
|
44
48
|
// even though the server expunged the messages.
|
|
@@ -51,8 +55,7 @@ async function expunge(connection, range, options) {
|
|
|
51
55
|
return true;
|
|
52
56
|
}
|
|
53
57
|
catch (err) {
|
|
54
|
-
await (0, tools_js_1.
|
|
55
|
-
connection.log.warn({ err, cid: connection.id });
|
|
58
|
+
await (0, tools_js_1.reportCommandError)(connection, err);
|
|
56
59
|
return false;
|
|
57
60
|
}
|
|
58
61
|
}
|
|
@@ -12,23 +12,23 @@ const tools_js_1 = require("../tools.js");
|
|
|
12
12
|
* @returns Object with message count and list, or undefined if not in SELECTED state
|
|
13
13
|
*/
|
|
14
14
|
async function fetch(connection, range, query, options) {
|
|
15
|
-
|
|
15
|
+
let mailbox = (0, tools_js_1.getSelectedMailbox)(connection);
|
|
16
|
+
if (!mailbox || !range) {
|
|
16
17
|
// nothing to do here
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
19
20
|
options = options || {};
|
|
20
|
-
let mailbox = connection.mailbox;
|
|
21
21
|
// Use BINARY extension for fetching if supported and requested, otherwise fall back to BODY.
|
|
22
22
|
// RFC 9051 folds the FETCH side of the BINARY extension into base IMAP4rev2, so an active
|
|
23
23
|
// rev2 session can use it even without the BINARY capability token (the APPEND side is NOT
|
|
24
24
|
// folded in and stays gated on the token in append.ts)
|
|
25
25
|
const canUseBinary = connection.capabilities.has('BINARY') || (0, tools_js_1.isRev2Active)(connection);
|
|
26
26
|
const commandKey = canUseBinary && options.binary && !connection.disableBinary ? 'BINARY' : 'BODY';
|
|
27
|
-
// Retry logic for ETHROTTLE errors (server rate limiting) with exponential backoff
|
|
28
|
-
|
|
27
|
+
// Retry logic for ETHROTTLE errors (server rate limiting) with exponential backoff.
|
|
28
|
+
// Every pass returns or throws: the last throttled attempt throws instead of retrying.
|
|
29
29
|
const maxRetries = 4;
|
|
30
30
|
const baseDelay = 1000; // Start with 1 second delay
|
|
31
|
-
|
|
31
|
+
for (let retryCount = 0;; retryCount++) {
|
|
32
32
|
let messages = {
|
|
33
33
|
count: 0,
|
|
34
34
|
list: []
|
|
@@ -205,15 +205,18 @@ async function fetch(connection, range, query, options) {
|
|
|
205
205
|
return messages;
|
|
206
206
|
}
|
|
207
207
|
catch (err) {
|
|
208
|
-
|
|
208
|
+
// The last throttled attempt falls through and throws, so running out of retries
|
|
209
|
+
// is never mistaken for an empty result
|
|
210
|
+
if (err.code === 'ETHROTTLE' && retryCount < maxRetries - 1) {
|
|
209
211
|
// Server returned a throttle error (rate limiting). Retry with exponential backoff.
|
|
210
|
-
// Delay doubles each retry: 1s, 2s, 4s
|
|
212
|
+
// Delay doubles each retry: 1s, 2s, 4s.
|
|
211
213
|
// If server provides a throttleReset hint, use that if longer.
|
|
212
214
|
const backoffDelay = Math.min(baseDelay * Math.pow(2, retryCount), 30000); // Cap at 30 seconds
|
|
213
215
|
// Use throttle reset time if provided and longer than backoff. The hint is
|
|
214
216
|
// server-controlled, so the wait goes through connection.throttleWait(), which caps
|
|
215
|
-
// it and keeps the timer tracked and abortable.
|
|
216
|
-
|
|
217
|
+
// it and keeps the timer tracked and abortable. The connection already waited
|
|
218
|
+
// part of the back-off before rejecting (throttleWaited), so only the rest is left.
|
|
219
|
+
const delay = Math.max(err.throttleReset || 0, backoffDelay) - (err.throttleWaited || 0);
|
|
217
220
|
connection.log.warn({
|
|
218
221
|
msg: 'Retrying throttled request with exponential backoff',
|
|
219
222
|
cid: connection.id,
|
|
@@ -229,7 +232,6 @@ async function fetch(connection, range, query, options) {
|
|
|
229
232
|
if (aborted) {
|
|
230
233
|
throw connection.createNoConnectionError(connection.byeReason, { rejectedFrom: 'throttleAbort', command: 'FETCH' });
|
|
231
234
|
}
|
|
232
|
-
retryCount++;
|
|
233
235
|
continue;
|
|
234
236
|
}
|
|
235
237
|
connection.log.warn({ err, cid: connection.id });
|
|
@@ -124,7 +124,11 @@ async function runIdle(connection) {
|
|
|
124
124
|
}
|
|
125
125
|
catch (err) {
|
|
126
126
|
(0, tools_js_1.logConnectionError)(connection, 'IDLE session failed', err);
|
|
127
|
-
|
|
127
|
+
// A tagged NO or BAD only means the server refused IDLE; the connection is still usable,
|
|
128
|
+
// so the waiters are released by the finally block below and their own commands run.
|
|
129
|
+
// Anything else (close, lost socket, parser failure) fails the waiters too.
|
|
130
|
+
let refusedByServer = ['NO', 'BAD'].includes(err.responseStatus);
|
|
131
|
+
if (preCheckWaitQueue.length && !refusedByServer) {
|
|
128
132
|
// One error for the whole queue: every waiter failed at the same site, for the same
|
|
129
133
|
// reason. Built inside the guard so a teardown with nothing queued - the common case -
|
|
130
134
|
// does not pay for an Error and its stack capture.
|
|
@@ -240,7 +244,7 @@ async function runPollingFallback(connection, maxIdleTime) {
|
|
|
240
244
|
return;
|
|
241
245
|
}
|
|
242
246
|
// The transport or the mailbox may be gone by the time the timer fires
|
|
243
|
-
if (!connection.socket || connection.socket.destroyed ||
|
|
247
|
+
if (!connection.socket || connection.socket.destroyed || !(0, tools_js_1.getSelectedMailbox)(connection)) {
|
|
244
248
|
return cancel();
|
|
245
249
|
}
|
|
246
250
|
pollOnce(connection, session)
|
|
@@ -184,7 +184,7 @@ async function list(connection, reference, mailbox, options) {
|
|
|
184
184
|
entry.path = entry.path.slice(1);
|
|
185
185
|
}
|
|
186
186
|
// Build parent path hierarchy for tree construction and sorting
|
|
187
|
-
entry.parentPath = entry.delimiter && entry.path ? entry.path.
|
|
187
|
+
entry.parentPath = entry.delimiter && entry.path ? entry.path.substring(0, entry.path.lastIndexOf(entry.delimiter)) : '';
|
|
188
188
|
entry.parent = entry.delimiter ? entry.path.split(entry.delimiter) : [entry.path];
|
|
189
189
|
entry.name = entry.parent.pop();
|
|
190
190
|
// Try to detect special-use from server flags or well-known names
|
|
@@ -395,7 +395,7 @@ async function list(connection, reference, mailbox, options) {
|
|
|
395
395
|
if (entry.delimiter && entry.path.charAt(0) === entry.delimiter) {
|
|
396
396
|
entry.path = entry.path.slice(1);
|
|
397
397
|
}
|
|
398
|
-
entry.parentPath = entry.delimiter && entry.path ? entry.path.
|
|
398
|
+
entry.parentPath = entry.delimiter && entry.path ? entry.path.substring(0, entry.path.lastIndexOf(entry.delimiter)) : '';
|
|
399
399
|
entry.parent = entry.delimiter ? entry.path.split(entry.delimiter) : [entry.path];
|
|
400
400
|
entry.name = entry.parent.pop();
|
|
401
401
|
// Merge LSUB data into existing LIST entry if found
|
|
@@ -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
|
|
@@ -14,7 +14,8 @@ const copyuid_parser_js_1 = require("./copyuid-parser.js");
|
|
|
14
14
|
* @returns Move result with UID mapping if available, false on failure, or undefined if preconditions not met
|
|
15
15
|
*/
|
|
16
16
|
async function move(connection, range, destination, options) {
|
|
17
|
-
|
|
17
|
+
let mailbox = (0, tools_js_1.getSelectedMailbox)(connection);
|
|
18
|
+
if (!mailbox || !range || !destination) {
|
|
18
19
|
// nothing to do here
|
|
19
20
|
return;
|
|
20
21
|
}
|
|
@@ -24,13 +25,18 @@ async function move(connection, range, destination, options) {
|
|
|
24
25
|
{ type: 'SEQUENCE', value: range },
|
|
25
26
|
{ type: 'ATOM', value: (0, tools_js_1.encodePath)(connection, destination) }
|
|
26
27
|
];
|
|
27
|
-
let map = { path:
|
|
28
|
+
let map = { path: mailbox.path, destination };
|
|
28
29
|
// Fallback for servers without the MOVE extension (RFC 6851):
|
|
29
30
|
// emulate MOVE using COPY + flag as \Deleted + EXPUNGE.
|
|
30
31
|
if (!(0, tools_js_1.hasCapability)(connection, 'MOVE')) {
|
|
31
32
|
let result = await connection.messageCopy(range, destination, options);
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
if (!result) {
|
|
34
|
+
// The source must stay untouched when the copy failed, otherwise the messages are lost
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
let deleted = await connection.messageDelete(range, Object.assign({ silent: true }, options));
|
|
38
|
+
// Messages that were copied but not removed from the source mean the move did not complete
|
|
39
|
+
return deleted ? result : false;
|
|
34
40
|
}
|
|
35
41
|
let response;
|
|
36
42
|
try {
|
|
@@ -48,8 +54,7 @@ async function move(connection, range, destination, options) {
|
|
|
48
54
|
return map;
|
|
49
55
|
}
|
|
50
56
|
catch (err) {
|
|
51
|
-
await (0, tools_js_1.
|
|
52
|
-
connection.log.warn({ err, cid: connection.id });
|
|
57
|
+
await (0, tools_js_1.reportCommandError)(connection, err);
|
|
53
58
|
return false;
|
|
54
59
|
}
|
|
55
60
|
}
|
|
@@ -9,7 +9,7 @@ const tools_js_1 = require("../tools.js");
|
|
|
9
9
|
* @returns The primary personal namespace, or an error object on failure
|
|
10
10
|
*/
|
|
11
11
|
async function namespace(connection) {
|
|
12
|
-
if (!
|
|
12
|
+
if (!(0, tools_js_1.isAuthenticatedState)(connection)) {
|
|
13
13
|
// nothing to do here
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
@@ -10,7 +10,7 @@ const tools_js_1 = require("../tools.js");
|
|
|
10
10
|
* @returns Quota information object, false if QUOTA not supported or on failure, or undefined if preconditions not met
|
|
11
11
|
*/
|
|
12
12
|
async function quota(connection, path) {
|
|
13
|
-
if (!
|
|
13
|
+
if (!(0, tools_js_1.isAuthenticatedState)(connection) || !path) {
|
|
14
14
|
// nothing to do here
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
@@ -50,19 +50,21 @@ async function quota(connection, path) {
|
|
|
50
50
|
if ((0, tools_js_1.isUnsafeKey)(key) || key === 'path' || key === 'quotaroot') {
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
let resource = map[key];
|
|
54
|
+
if (typeof resource !== 'object') {
|
|
55
|
+
resource = {};
|
|
56
|
+
map[key] = resource;
|
|
55
57
|
}
|
|
56
58
|
// Storage quota is reported in KB by IMAP; convert to bytes for consistency
|
|
57
59
|
const multiplier = key === 'storage' ? 1024 : 1;
|
|
58
60
|
if (position === 1) {
|
|
59
|
-
|
|
61
|
+
resource.usage = value * multiplier;
|
|
60
62
|
}
|
|
61
63
|
else if (position === 2) {
|
|
62
|
-
|
|
64
|
+
resource.limit = value * multiplier;
|
|
63
65
|
// Calculate usage percentage for convenient display
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
+
if (resource.limit) {
|
|
67
|
+
resource.status = Math.round(((resource.usage || 0) / resource.limit) * 100) + '%';
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
70
|
});
|
|
@@ -110,8 +112,7 @@ async function quota(connection, path) {
|
|
|
110
112
|
return map;
|
|
111
113
|
}
|
|
112
114
|
catch (err) {
|
|
113
|
-
await (0, tools_js_1.
|
|
114
|
-
connection.log.warn({ err, cid: connection.id });
|
|
115
|
+
await (0, tools_js_1.reportCommandError)(connection, err);
|
|
115
116
|
return false;
|
|
116
117
|
}
|
|
117
118
|
}
|
|
@@ -12,7 +12,7 @@ const tools_js_1 = require("../tools.js");
|
|
|
12
12
|
* @throws If the RENAME command fails
|
|
13
13
|
*/
|
|
14
14
|
async function rename(connection, path, newPath) {
|
|
15
|
-
if (!
|
|
15
|
+
if (!(0, tools_js_1.isAuthenticatedState)(connection)) {
|
|
16
16
|
// nothing to do here
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
@@ -22,7 +22,8 @@ async function rename(connection, path, newPath) {
|
|
|
22
22
|
newPath = (0, tools_js_1.normalizePath)(connection, newPath);
|
|
23
23
|
// Must close/deselect the mailbox before renaming if it's currently selected,
|
|
24
24
|
// as IMAP servers will not rename an active mailbox.
|
|
25
|
-
|
|
25
|
+
let selected = (0, tools_js_1.getSelectedMailbox)(connection);
|
|
26
|
+
if (selected && selected.path === path) {
|
|
26
27
|
await connection.run('CLOSE');
|
|
27
28
|
}
|
|
28
29
|
let response;
|
|
@@ -39,8 +40,7 @@ async function rename(connection, path, newPath) {
|
|
|
39
40
|
return map;
|
|
40
41
|
}
|
|
41
42
|
catch (err) {
|
|
42
|
-
await (0, tools_js_1.
|
|
43
|
-
connection.log.warn({ err, cid: connection.id });
|
|
43
|
+
await (0, tools_js_1.reportCommandError)(connection, err);
|
|
44
44
|
throw err;
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -34,7 +34,8 @@ const stripEsearchPrefix = (attrs) => {
|
|
|
34
34
|
* When server lacks ESEARCH, falls back to plain SEARCH and returns number[].
|
|
35
35
|
*/
|
|
36
36
|
async function search(connection, query, options) {
|
|
37
|
-
|
|
37
|
+
const mailbox = (0, tools_js_1.getSelectedMailbox)(connection);
|
|
38
|
+
if (!mailbox) {
|
|
38
39
|
// nothing to do here
|
|
39
40
|
return false;
|
|
40
41
|
}
|
|
@@ -92,8 +93,7 @@ async function search(connection, query, options) {
|
|
|
92
93
|
return esearchResult;
|
|
93
94
|
}
|
|
94
95
|
catch (err) {
|
|
95
|
-
await (0, tools_js_1.
|
|
96
|
-
connection.log.warn({ err, cid: connection.id });
|
|
96
|
+
await (0, tools_js_1.reportCommandError)(connection, err);
|
|
97
97
|
return false;
|
|
98
98
|
}
|
|
99
99
|
}
|
|
@@ -163,7 +163,7 @@ async function search(connection, query, options) {
|
|
|
163
163
|
// for message sequence numbers, while server-sent UID sets may
|
|
164
164
|
// not contain '*' at all (RFC 9051 section 4.1.1), so UID
|
|
165
165
|
// parts with '*' are dropped
|
|
166
|
-
let existsCount = () =>
|
|
166
|
+
let existsCount = () => mailbox.exists || 0;
|
|
167
167
|
// The mailbox EXISTS count is itself server-supplied and can be
|
|
168
168
|
// absurdly large, so the budget is additionally capped at the same
|
|
169
169
|
// absolute ceiling expandRange() uses - a hostile server cannot
|
|
@@ -188,8 +188,8 @@ async function search(connection, query, options) {
|
|
|
188
188
|
results.add(value);
|
|
189
189
|
continue;
|
|
190
190
|
}
|
|
191
|
-
let first = resolveId(part.
|
|
192
|
-
let second = resolveId(part.
|
|
191
|
+
let first = resolveId(part.substring(0, colon));
|
|
192
|
+
let second = resolveId(part.slice(colon + 1));
|
|
193
193
|
if (!(0, tools_js_1.isValidSequenceValue)(first) || !(0, tools_js_1.isValidSequenceValue)(second)) {
|
|
194
194
|
discarded = true;
|
|
195
195
|
continue;
|
|
@@ -219,8 +219,7 @@ async function search(connection, query, options) {
|
|
|
219
219
|
return Array.from(results).sort((a, b) => a - b);
|
|
220
220
|
}
|
|
221
221
|
catch (err) {
|
|
222
|
-
await (0, tools_js_1.
|
|
223
|
-
connection.log.warn({ err, cid: connection.id });
|
|
222
|
+
await (0, tools_js_1.reportCommandError)(connection, err);
|
|
224
223
|
return false;
|
|
225
224
|
}
|
|
226
225
|
}
|