imapflow 2.0.7 → 2.0.8
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 +9 -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 +2 -2
- 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 +5 -6
- 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 +2 -0
- package/dist/cjs/handler/imap-compiler.js +1 -1
- package/dist/cjs/handler/imap-stream.js +4 -4
- package/dist/cjs/handler/parser-instance.js +2 -2
- package/dist/cjs/handler/token-parser.js +1 -1
- package/dist/cjs/imap-flow.d.ts +16 -7
- package/dist/cjs/imap-flow.js +247 -730
- 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 +51 -10
- package/dist/cjs/tools.js +77 -15
- 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 +2 -2
- 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 +6 -7
- 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 +2 -0
- package/dist/esm/handler/imap-compiler.js +1 -1
- package/dist/esm/handler/imap-stream.js +4 -4
- package/dist/esm/handler/parser-instance.js +2 -2
- package/dist/esm/handler/token-parser.js +1 -1
- package/dist/esm/imap-flow.d.ts +16 -7
- package/dist/esm/imap-flow.js +248 -731
- 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 +51 -10
- package/dist/esm/tools.js +72 -15
- package/dist/esm/types.d.ts +30 -16
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.0.8](https://github.com/postalsys/imapflow/compare/v2.0.7...v2.0.8) (2026-09-27)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* 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)
|
|
9
|
+
* surface throttle, truncation and listener failures instead of hiding them ([03a0624](https://github.com/postalsys/imapflow/commit/03a0624d0f381e935bbc3b4e12002272426abbae))
|
|
10
|
+
* **types:** describe the quota response the way getQuota() returns it ([a2363b6](https://github.com/postalsys/imapflow/commit/a2363b6658d25d1a05a177a8dec556faf3df597b))
|
|
11
|
+
|
|
3
12
|
## [2.0.7](https://github.com/postalsys/imapflow/compare/v2.0.6...v2.0.7) (2026-09-25)
|
|
4
13
|
|
|
5
14
|
|
|
@@ -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
|
|
@@ -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
|
}
|
|
@@ -59,7 +59,7 @@ const VALUED_RESPONSE_CODES = Object.assign(Object.create(null), {
|
|
|
59
59
|
* @throws If the SELECT/EXAMINE command fails
|
|
60
60
|
*/
|
|
61
61
|
async function select(connection, pathInput, options) {
|
|
62
|
-
if (!
|
|
62
|
+
if (!(0, tools_js_1.isAuthenticatedState)(connection)) {
|
|
63
63
|
// nothing to do here
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
@@ -215,7 +215,7 @@ async function select(connection, pathInput, options) {
|
|
|
215
215
|
// Emit mailboxClose if we're switching from a different mailbox.
|
|
216
216
|
// Re-selecting the same mailbox (e.g., for resync) does not trigger close/open.
|
|
217
217
|
if (currentMailbox && currentMailbox.path !== path) {
|
|
218
|
-
|
|
218
|
+
(0, tools_js_1.emitSafe)(connection, 'mailboxClose', currentMailbox);
|
|
219
219
|
}
|
|
220
220
|
connection.mailbox = map;
|
|
221
221
|
// Save the SELECT command for potential re-use (e.g., NOOP fallback polling
|
|
@@ -223,13 +223,13 @@ async function select(connection, pathInput, options) {
|
|
|
223
223
|
connection.currentSelectCommand = selectCommand;
|
|
224
224
|
connection.state = connection.states.SELECTED;
|
|
225
225
|
if (!currentMailbox || currentMailbox.path !== path) {
|
|
226
|
-
|
|
226
|
+
(0, tools_js_1.emitSafe)(connection, 'mailboxOpen', connection.mailbox);
|
|
227
227
|
}
|
|
228
228
|
response.next();
|
|
229
229
|
return map;
|
|
230
230
|
}
|
|
231
231
|
catch (err) {
|
|
232
|
-
await (0, tools_js_1.
|
|
232
|
+
await (0, tools_js_1.reportCommandError)(connection, err);
|
|
233
233
|
// If SELECT/EXAMINE fails while a mailbox was already selected, we must
|
|
234
234
|
// reset to AUTHENTICATED state since the server has implicitly deselected
|
|
235
235
|
// the previous mailbox on failure (RFC 3501 Section 6.3.1).
|
|
@@ -239,10 +239,9 @@ async function select(connection, pathInput, options) {
|
|
|
239
239
|
connection.currentSelectCommand = false;
|
|
240
240
|
connection.state = connection.states.AUTHENTICATED;
|
|
241
241
|
if (currentMailbox) {
|
|
242
|
-
|
|
242
|
+
(0, tools_js_1.emitSafe)(connection, 'mailboxClose', currentMailbox);
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
|
-
connection.log.warn({ err, cid: connection.id });
|
|
246
245
|
throw err;
|
|
247
246
|
}
|
|
248
247
|
}
|
|
@@ -6,19 +6,18 @@ const status_fields_js_1 = require("./status-fields.js");
|
|
|
6
6
|
// STATUS fields that also refresh the live mailbox state when the queried mailbox is the
|
|
7
7
|
// currently selected one. Keyed by the output property name parseStatusList() reports.
|
|
8
8
|
const MAILBOX_UPDATERS = {
|
|
9
|
-
messages: (value, connection, path) => {
|
|
10
|
-
let mailbox = connection.mailbox;
|
|
9
|
+
messages: (value, mailbox, connection, path) => {
|
|
11
10
|
let prevCount = mailbox.exists;
|
|
12
11
|
if (prevCount !== value) {
|
|
13
12
|
mailbox.exists = value;
|
|
14
|
-
|
|
13
|
+
(0, tools_js_1.emitSafe)(connection, 'exists', { path, count: value, prevCount });
|
|
15
14
|
}
|
|
16
15
|
},
|
|
17
|
-
uidNext: (value,
|
|
18
|
-
|
|
16
|
+
uidNext: (value, mailbox) => {
|
|
17
|
+
mailbox.uidNext = value;
|
|
19
18
|
},
|
|
20
|
-
highestModseq: (value,
|
|
21
|
-
|
|
19
|
+
highestModseq: (value, mailbox) => {
|
|
20
|
+
mailbox.highestModseq = value;
|
|
22
21
|
}
|
|
23
22
|
};
|
|
24
23
|
/**
|
|
@@ -31,7 +30,7 @@ const MAILBOX_UPDATERS = {
|
|
|
31
30
|
* @throws {Error} If the mailbox does not exist
|
|
32
31
|
*/
|
|
33
32
|
async function status(connection, path, query) {
|
|
34
|
-
if (!
|
|
33
|
+
if (!(0, tools_js_1.isAuthenticatedState)(connection) || !path) {
|
|
35
34
|
// nothing to do here
|
|
36
35
|
return false;
|
|
37
36
|
}
|
|
@@ -62,7 +61,8 @@ async function status(connection, path, query) {
|
|
|
62
61
|
STATUS: async (untagged) => {
|
|
63
62
|
// If querying the currently selected mailbox, also update the
|
|
64
63
|
// connection's live mailbox state and emit events for changes.
|
|
65
|
-
let
|
|
64
|
+
let selected = (0, tools_js_1.getSelectedMailbox)(connection);
|
|
65
|
+
let currentMailbox = selected && selected.path === path ? selected : false;
|
|
66
66
|
let list = untagged.attributes && Array.isArray(untagged.attributes[1]) ? untagged.attributes[1] : false;
|
|
67
67
|
if (!list) {
|
|
68
68
|
return;
|
|
@@ -70,8 +70,8 @@ async function status(connection, path, query) {
|
|
|
70
70
|
(0, status_fields_js_1.parseStatusList)(list, (key, value) => {
|
|
71
71
|
map[key] = value;
|
|
72
72
|
let updater = MAILBOX_UPDATERS[key];
|
|
73
|
-
if (
|
|
74
|
-
updater(value, connection, path);
|
|
73
|
+
if (currentMailbox && updater) {
|
|
74
|
+
updater(value, currentMailbox, connection, path);
|
|
75
75
|
}
|
|
76
76
|
});
|
|
77
77
|
}
|