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.
Files changed (82) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/cjs/commands/append.js +12 -12
  3. package/dist/cjs/commands/authenticate.d.ts +3 -8
  4. package/dist/cjs/commands/close.js +2 -1
  5. package/dist/cjs/commands/copy.js +4 -4
  6. package/dist/cjs/commands/create.js +2 -3
  7. package/dist/cjs/commands/delete.js +4 -4
  8. package/dist/cjs/commands/expunge.js +8 -5
  9. package/dist/cjs/commands/fetch.js +12 -10
  10. package/dist/cjs/commands/idle.js +6 -2
  11. package/dist/cjs/commands/list.js +22 -18
  12. package/dist/cjs/commands/move.js +11 -6
  13. package/dist/cjs/commands/namespace.js +1 -1
  14. package/dist/cjs/commands/quota.js +10 -9
  15. package/dist/cjs/commands/rename.js +4 -4
  16. package/dist/cjs/commands/search.js +7 -8
  17. package/dist/cjs/commands/select.js +12 -9
  18. package/dist/cjs/commands/status.js +11 -11
  19. package/dist/cjs/commands/store.js +5 -5
  20. package/dist/cjs/commands/subscribe.js +2 -17
  21. package/dist/cjs/commands/subscription.d.ts +10 -0
  22. package/dist/cjs/commands/subscription.js +29 -0
  23. package/dist/cjs/commands/unsubscribe.js +2 -17
  24. package/dist/cjs/download.d.ts +22 -0
  25. package/dist/cjs/download.js +588 -0
  26. package/dist/cjs/errors.d.ts +50 -1
  27. package/dist/cjs/errors.js +53 -1
  28. package/dist/cjs/handler/imap-compiler.js +1 -1
  29. package/dist/cjs/handler/imap-stream.d.ts +13 -2
  30. package/dist/cjs/handler/imap-stream.js +51 -30
  31. package/dist/cjs/handler/parser-instance.js +2 -2
  32. package/dist/cjs/handler/token-parser.js +15 -9
  33. package/dist/cjs/imap-flow.d.ts +30 -84
  34. package/dist/cjs/imap-flow.js +282 -734
  35. package/dist/cjs/jp-decoder.js +1 -1
  36. package/dist/cjs/package-info.d.ts +1 -1
  37. package/dist/cjs/package-info.js +3 -3
  38. package/dist/cjs/search-compiler.js +5 -12
  39. package/dist/cjs/tools.d.ts +52 -11
  40. package/dist/cjs/tools.js +86 -23
  41. package/dist/cjs/types.d.ts +30 -16
  42. package/dist/esm/commands/append.js +13 -13
  43. package/dist/esm/commands/authenticate.d.ts +3 -8
  44. package/dist/esm/commands/close.js +2 -1
  45. package/dist/esm/commands/copy.js +5 -5
  46. package/dist/esm/commands/create.js +3 -4
  47. package/dist/esm/commands/delete.js +5 -5
  48. package/dist/esm/commands/expunge.js +9 -6
  49. package/dist/esm/commands/fetch.js +13 -11
  50. package/dist/esm/commands/idle.js +7 -3
  51. package/dist/esm/commands/list.js +22 -18
  52. package/dist/esm/commands/move.js +12 -7
  53. package/dist/esm/commands/namespace.js +2 -2
  54. package/dist/esm/commands/quota.js +11 -10
  55. package/dist/esm/commands/rename.js +5 -5
  56. package/dist/esm/commands/search.js +8 -9
  57. package/dist/esm/commands/select.js +13 -10
  58. package/dist/esm/commands/status.js +12 -12
  59. package/dist/esm/commands/store.js +6 -6
  60. package/dist/esm/commands/subscribe.js +2 -17
  61. package/dist/esm/commands/subscription.d.ts +10 -0
  62. package/dist/esm/commands/subscription.js +26 -0
  63. package/dist/esm/commands/unsubscribe.js +2 -17
  64. package/dist/esm/download.d.ts +22 -0
  65. package/dist/esm/download.js +581 -0
  66. package/dist/esm/errors.d.ts +50 -1
  67. package/dist/esm/errors.js +52 -0
  68. package/dist/esm/handler/imap-compiler.js +1 -1
  69. package/dist/esm/handler/imap-stream.d.ts +13 -2
  70. package/dist/esm/handler/imap-stream.js +51 -30
  71. package/dist/esm/handler/parser-instance.js +2 -2
  72. package/dist/esm/handler/token-parser.js +15 -9
  73. package/dist/esm/imap-flow.d.ts +30 -84
  74. package/dist/esm/imap-flow.js +282 -735
  75. package/dist/esm/jp-decoder.js +1 -1
  76. package/dist/esm/package-info.d.ts +1 -1
  77. package/dist/esm/package-info.js +3 -3
  78. package/dist/esm/search-compiler.js +5 -12
  79. package/dist/esm/tools.d.ts +52 -11
  80. package/dist/esm/tools.js +79 -21
  81. package/dist/esm/types.d.ts +30 -16
  82. 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 (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state) || !destination) {
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 expectExists = (0, tools_js_1.comparePaths)(connection, connection.mailbox.path, destination);
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 (expectExists) {
81
- let mailbox = connection.mailbox;
82
- let prevCount = mailbox.exists;
82
+ if (targetMailbox) {
83
+ let prevCount = targetMailbox.exists;
83
84
  if (map.seq !== prevCount) {
84
- mailbox.exists = map.seq;
85
- connection.emit('exists', {
86
- path: mailbox.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: expectExists ? { EXISTS: handleExistsUpdate } : false
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 (expectExists && !map.seq) {
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.enhanceCommandError)(err);
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
- connection.emit('mailboxClose', currentMailbox);
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
- if (connection.state !== connection.states.SELECTED || !range || !destination) {
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: connection.mailbox.path, destination };
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.enhanceCommandError)(err);
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 (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state)) {
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.enhanceCommandError)(err);
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 (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state)) {
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
- if (connection.state === connection.states.SELECTED && connection.mailbox.path === path) {
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.enhanceCommandError)(err);
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
- if (connection.state !== connection.states.SELECTED || !range) {
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
- await connection.messageFlagsAdd(range, ['\\Deleted'], options);
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.enhanceCommandError)(err);
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
- if (connection.state !== connection.states.SELECTED || !range) {
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
- let retryCount = 0;
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
- while (retryCount < maxRetries) {
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
- if (err.code === 'ETHROTTLE') {
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, 8s (capped at 30s).
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
- const delay = err.throttleReset && err.throttleReset > backoffDelay ? err.throttleReset : backoffDelay;
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
- if (preCheckWaitQueue.length) {
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 || connection.state !== connection.states.SELECTED || !connection.mailbox) {
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.substr(0, entry.path.lastIndexOf(entry.delimiter)) : '';
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.substr(0, entry.path.lastIndexOf(entry.delimiter)) : '';
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: for each type, pick the best candidate
451
- // based on source priority (user > extension > name), then alphabetically.
452
- // Only the winning entry gets the specialUse property set.
453
- for (let type of Object.keys(specialUseMatches)) {
454
- let sortedEntries = specialUseMatches[type].sort((a, b) => {
455
- let aSource = SOURCE_SORT_ORDER.indexOf(a.source);
456
- let bSource = SOURCE_SORT_ORDER.indexOf(b.source);
457
- if (aSource === bSource) {
458
- return a.entry.path.localeCompare(b.entry.path);
459
- }
460
- return aSource - bSource;
461
- });
462
- if (!sortedEntries[0].entry.specialUse) {
463
- let source = sortedEntries[0].source;
464
- sortedEntries[0].entry.specialUse = type;
465
- sortedEntries[0].entry.specialUseSource = PUBLIC_SOURCE[source] || source;
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
- if (connection.state !== connection.states.SELECTED || !range || !destination) {
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: connection.mailbox.path, destination };
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
- await connection.messageDelete(range, Object.assign({ silent: true }, options));
33
- return result;
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.enhanceCommandError)(err);
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 (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state)) {
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 (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state) || !path) {
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
- if (!map[key]) {
54
- map[key] = {};
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
- map[key].usage = value * multiplier;
61
+ resource.usage = value * multiplier;
60
62
  }
61
63
  else if (position === 2) {
62
- map[key].limit = value * multiplier;
64
+ resource.limit = value * multiplier;
63
65
  // Calculate usage percentage for convenient display
64
- if (map[key].limit) {
65
- map[key].status = Math.round(((map[key].usage || 0) / map[key].limit) * 100) + '%';
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.enhanceCommandError)(err);
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 (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state)) {
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
- if (connection.state === connection.states.SELECTED && connection.mailbox.path === path) {
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.enhanceCommandError)(err);
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
- if (connection.state !== connection.states.SELECTED) {
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.enhanceCommandError)(err);
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 = () => (connection.mailbox && connection.mailbox.exists) || 0;
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.substr(0, colon));
192
- let second = resolveId(part.substr(colon + 1));
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.enhanceCommandError)(err);
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
  }