imapflow 1.7.7 → 2.0.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 +27 -0
- package/README.md +8 -2
- package/dist/cjs/charsets.d.ts +1 -0
- package/dist/cjs/charsets.js +294 -0
- package/dist/cjs/commands/append.d.ts +22 -0
- package/dist/cjs/commands/append.js +151 -0
- package/dist/cjs/commands/authenticate.d.ts +24 -0
- package/dist/cjs/commands/authenticate.js +223 -0
- package/dist/cjs/commands/capability.d.ts +8 -0
- package/dist/cjs/commands/capability.js +32 -0
- package/dist/cjs/commands/close.d.ts +8 -0
- package/dist/cjs/commands/close.js +39 -0
- package/dist/cjs/commands/compress.d.ts +8 -0
- package/dist/cjs/commands/compress.js +56 -0
- package/dist/cjs/commands/copy.d.ts +13 -0
- package/dist/cjs/commands/copy.js +44 -0
- package/dist/cjs/commands/copyuid-parser.d.ts +11 -0
- package/dist/cjs/commands/copyuid-parser.js +32 -0
- package/dist/cjs/commands/create.d.ts +11 -0
- package/dist/cjs/commands/create.js +80 -0
- package/dist/cjs/commands/delete.d.ts +11 -0
- package/dist/cjs/commands/delete.js +40 -0
- package/dist/cjs/commands/enable.d.ts +9 -0
- package/dist/cjs/commands/enable.js +61 -0
- package/dist/cjs/commands/esearch-parser.d.ts +17 -0
- package/dist/cjs/commands/esearch-parser.js +91 -0
- package/dist/cjs/commands/expunge.d.ts +12 -0
- package/dist/cjs/commands/expunge.js +60 -0
- package/dist/cjs/commands/fetch.d.ts +30 -0
- package/dist/cjs/commands/fetch.js +241 -0
- package/dist/cjs/commands/id.d.ts +10 -0
- package/dist/cjs/commands/id.js +80 -0
- package/dist/cjs/commands/idle.d.ts +9 -0
- package/dist/cjs/commands/idle.js +347 -0
- package/dist/cjs/commands/list.d.ts +16 -0
- package/dist/cjs/commands/list.js +518 -0
- package/dist/cjs/commands/login.d.ts +11 -0
- package/dist/cjs/commands/login.js +42 -0
- package/dist/cjs/commands/logout.d.ts +8 -0
- package/dist/cjs/commands/logout.js +47 -0
- package/dist/cjs/commands/move.d.ts +13 -0
- package/dist/cjs/commands/move.js +57 -0
- package/dist/cjs/commands/namespace.d.ts +25 -0
- package/dist/cjs/commands/namespace.js +139 -0
- package/dist/cjs/commands/noop.d.ts +8 -0
- package/dist/cjs/commands/noop.js +22 -0
- package/dist/cjs/commands/quota.d.ts +10 -0
- package/dist/cjs/commands/quota.js +119 -0
- package/dist/cjs/commands/rename.d.ts +12 -0
- package/dist/cjs/commands/rename.js +48 -0
- package/dist/cjs/commands/search.d.ts +15 -0
- package/dist/cjs/commands/search.js +228 -0
- package/dist/cjs/commands/select.d.ts +25 -0
- package/dist/cjs/commands/select.js +250 -0
- package/dist/cjs/commands/starttls.d.ts +8 -0
- package/dist/cjs/commands/starttls.js +30 -0
- package/dist/cjs/commands/status-fields.d.ts +14 -0
- package/dist/cjs/commands/status-fields.js +61 -0
- package/dist/cjs/commands/status.d.ts +12 -0
- package/dist/cjs/commands/status.js +108 -0
- package/dist/cjs/commands/store.d.ts +19 -0
- package/dist/cjs/commands/store.js +93 -0
- package/dist/cjs/commands/subscribe.d.ts +9 -0
- package/dist/cjs/commands/subscribe.js +31 -0
- package/dist/cjs/commands/unsubscribe.d.ts +9 -0
- package/dist/cjs/commands/unsubscribe.js +31 -0
- package/dist/cjs/connection-deadline.d.ts +49 -0
- package/dist/cjs/connection-deadline.js +91 -0
- package/dist/cjs/errors.d.ts +83 -0
- package/dist/cjs/errors.js +13 -0
- package/dist/cjs/handler/imap-compiler.d.ts +24 -0
- package/dist/cjs/handler/imap-compiler.js +285 -0
- package/dist/cjs/handler/imap-formal-syntax.d.ts +28 -0
- package/dist/cjs/handler/imap-formal-syntax.js +121 -0
- package/dist/cjs/handler/imap-handler.d.ts +9 -0
- package/dist/cjs/handler/imap-handler.js +10 -0
- package/dist/cjs/handler/imap-parser.d.ts +16 -0
- package/dist/cjs/handler/imap-parser.js +90 -0
- package/dist/cjs/handler/imap-stream.d.ts +181 -0
- package/dist/cjs/handler/imap-stream.js +446 -0
- package/dist/cjs/handler/limits.d.ts +25 -0
- package/dist/cjs/handler/limits.js +51 -0
- package/dist/cjs/handler/parser-instance.d.ts +68 -0
- package/dist/cjs/handler/parser-instance.js +223 -0
- package/dist/cjs/handler/token-parser.d.ts +91 -0
- package/dist/cjs/handler/token-parser.js +673 -0
- package/dist/cjs/handler/types.d.ts +91 -0
- package/dist/cjs/handler/types.js +4 -0
- package/dist/cjs/imap-commands.d.ts +16 -0
- package/dist/cjs/imap-commands.js +74 -0
- package/dist/cjs/imap-flow.d.ts +676 -0
- package/dist/cjs/imap-flow.js +3949 -0
- package/dist/cjs/jp-decoder.d.ts +12 -0
- package/dist/cjs/jp-decoder.js +79 -0
- package/dist/cjs/limited-passthrough.d.ts +25 -0
- package/dist/cjs/limited-passthrough.js +54 -0
- package/dist/cjs/logger.d.ts +3 -0
- package/dist/cjs/logger.js +11 -0
- package/dist/cjs/package-info.d.ts +3 -0
- package/dist/cjs/package-info.js +7 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/proxy-connection.d.ts +33 -0
- package/dist/cjs/proxy-connection.js +392 -0
- package/dist/cjs/search-compiler.d.ts +34 -0
- package/dist/cjs/search-compiler.js +476 -0
- package/dist/cjs/special-use.d.ts +22 -0
- package/dist/cjs/special-use.js +911 -0
- package/dist/cjs/tools.d.ts +427 -0
- package/dist/cjs/tools.js +1496 -0
- package/{lib/imap-flow.d.ts → dist/cjs/types.d.ts} +386 -516
- package/dist/cjs/types.js +5 -0
- package/dist/esm/charsets.d.ts +1 -0
- package/{lib → dist/esm}/charsets.js +1 -6
- package/dist/esm/commands/append.d.ts +22 -0
- package/{lib → dist/esm}/commands/append.js +22 -52
- package/dist/esm/commands/authenticate.d.ts +24 -0
- package/{lib → dist/esm}/commands/authenticate.js +62 -87
- package/dist/esm/commands/capability.d.ts +8 -0
- package/{lib → dist/esm}/commands/capability.js +6 -9
- package/dist/esm/commands/close.d.ts +8 -0
- package/{lib → dist/esm}/commands/close.js +6 -10
- package/dist/esm/commands/compress.d.ts +8 -0
- package/{lib → dist/esm}/commands/compress.js +7 -11
- package/dist/esm/commands/copy.d.ts +13 -0
- package/{lib → dist/esm}/commands/copy.js +12 -20
- package/dist/esm/commands/copyuid-parser.d.ts +11 -0
- package/{lib → dist/esm}/commands/copyuid-parser.js +9 -15
- package/dist/esm/commands/create.d.ts +11 -0
- package/{lib → dist/esm}/commands/create.js +13 -27
- package/dist/esm/commands/delete.d.ts +11 -0
- package/{lib → dist/esm}/commands/delete.js +9 -14
- package/dist/esm/commands/enable.d.ts +9 -0
- package/{lib → dist/esm}/commands/enable.js +23 -30
- package/dist/esm/commands/esearch-parser.d.ts +17 -0
- package/dist/esm/commands/esearch-parser.js +88 -0
- package/dist/esm/commands/expunge.d.ts +12 -0
- package/{lib → dist/esm}/commands/expunge.js +17 -22
- package/dist/esm/commands/fetch.d.ts +30 -0
- package/{lib → dist/esm}/commands/fetch.js +32 -64
- package/dist/esm/commands/id.d.ts +10 -0
- package/{lib → dist/esm}/commands/id.js +17 -23
- package/dist/esm/commands/idle.d.ts +9 -0
- package/{lib → dist/esm}/commands/idle.js +47 -81
- package/dist/esm/commands/list.d.ts +16 -0
- package/{lib → dist/esm}/commands/list.js +56 -121
- package/dist/esm/commands/login.d.ts +11 -0
- package/{lib → dist/esm}/commands/login.js +10 -15
- package/dist/esm/commands/logout.d.ts +8 -0
- package/{lib → dist/esm}/commands/logout.js +9 -11
- package/dist/esm/commands/move.d.ts +13 -0
- package/{lib → dist/esm}/commands/move.js +13 -21
- package/dist/esm/commands/namespace.d.ts +25 -0
- package/{lib → dist/esm}/commands/namespace.js +34 -44
- package/dist/esm/commands/noop.d.ts +8 -0
- package/{lib → dist/esm}/commands/noop.js +6 -7
- package/dist/esm/commands/quota.d.ts +10 -0
- package/{lib → dist/esm}/commands/quota.js +18 -36
- package/dist/esm/commands/rename.d.ts +12 -0
- package/{lib → dist/esm}/commands/rename.js +10 -15
- package/dist/esm/commands/search.d.ts +15 -0
- package/{lib → dist/esm}/commands/search.js +36 -135
- package/dist/esm/commands/select.d.ts +25 -0
- package/{lib → dist/esm}/commands/select.js +33 -64
- package/dist/esm/commands/starttls.d.ts +8 -0
- package/{lib → dist/esm}/commands/starttls.js +6 -8
- package/dist/esm/commands/status-fields.d.ts +14 -0
- package/{lib → dist/esm}/commands/status-fields.js +5 -16
- package/dist/esm/commands/status.d.ts +12 -0
- package/{lib → dist/esm}/commands/status.js +18 -29
- package/dist/esm/commands/store.d.ts +19 -0
- package/{lib → dist/esm}/commands/store.js +24 -37
- package/dist/esm/commands/subscribe.d.ts +9 -0
- package/{lib → dist/esm}/commands/subscribe.js +8 -12
- package/dist/esm/commands/unsubscribe.d.ts +9 -0
- package/{lib → dist/esm}/commands/unsubscribe.js +8 -12
- package/dist/esm/connection-deadline.d.ts +49 -0
- package/{lib → dist/esm}/connection-deadline.js +14 -25
- package/dist/esm/errors.d.ts +83 -0
- package/dist/esm/errors.js +9 -0
- package/dist/esm/handler/imap-compiler.d.ts +24 -0
- package/{lib → dist/esm}/handler/imap-compiler.js +22 -80
- package/dist/esm/handler/imap-formal-syntax.d.ts +28 -0
- package/dist/esm/handler/imap-formal-syntax.js +117 -0
- package/dist/esm/handler/imap-handler.d.ts +9 -0
- package/dist/esm/handler/imap-handler.js +9 -0
- package/dist/esm/handler/imap-parser.d.ts +16 -0
- package/{lib → dist/esm}/handler/imap-parser.js +31 -44
- package/dist/esm/handler/imap-stream.d.ts +181 -0
- package/{lib → dist/esm}/handler/imap-stream.js +29 -121
- package/dist/esm/handler/limits.d.ts +25 -0
- package/{lib → dist/esm}/handler/limits.js +13 -22
- package/dist/esm/handler/parser-instance.d.ts +68 -0
- package/{lib → dist/esm}/handler/parser-instance.js +19 -47
- package/dist/esm/handler/token-parser.d.ts +91 -0
- package/{lib → dist/esm}/handler/token-parser.js +71 -155
- package/dist/esm/handler/types.d.ts +91 -0
- package/dist/esm/handler/types.js +3 -0
- package/dist/esm/imap-commands.d.ts +16 -0
- package/dist/esm/imap-commands.js +67 -0
- package/dist/esm/imap-flow.d.ts +676 -0
- package/{lib → dist/esm}/imap-flow.js +785 -1802
- package/dist/esm/jp-decoder.d.ts +12 -0
- package/{lib → dist/esm}/jp-decoder.js +6 -21
- package/dist/esm/limited-passthrough.d.ts +25 -0
- package/{lib → dist/esm}/limited-passthrough.js +7 -20
- package/dist/esm/logger.d.ts +3 -0
- package/dist/esm/logger.js +4 -0
- package/dist/esm/package-info.d.ts +3 -0
- package/dist/esm/package-info.js +4 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/proxy-connection.d.ts +33 -0
- package/{lib → dist/esm}/proxy-connection.js +56 -127
- package/dist/esm/search-compiler.d.ts +34 -0
- package/{lib → dist/esm}/search-compiler.js +54 -110
- package/dist/esm/special-use.d.ts +22 -0
- package/dist/esm/special-use.js +907 -0
- package/dist/esm/tools.d.ts +427 -0
- package/dist/esm/tools.js +1446 -0
- package/dist/esm/types.d.ts +828 -0
- package/dist/esm/types.js +4 -0
- package/package.json +60 -20
- package/.gitattributes +0 -1
- package/.github/CODE_OF_CONDUCT.md +0 -76
- package/.github/FUNDING.yml +0 -4
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
- package/.github/contributing.md +0 -17
- package/.github/workflows/release.yaml +0 -36
- package/.github/workflows/stale.yml +0 -29
- package/.github/workflows/test.yml +0 -51
- package/.ncurc.js +0 -4
- package/.prettierignore +0 -4
- package/.prettierrc.js +0 -8
- package/.release-please-manifest.json +0 -3
- package/CLAUDE.md +0 -104
- package/Gruntfile.js +0 -23
- package/eslint.config.js +0 -45
- package/lib/handler/imap-formal-syntax.js +0 -189
- package/lib/handler/imap-handler.js +0 -17
- package/lib/imap-commands.js +0 -45
- package/lib/logger.js +0 -5
- package/lib/special-use.js +0 -923
- package/lib/tools.js +0 -1612
- package/release-please-config.json +0 -10
- package/test/authentication-test.js +0 -101
- package/test/auto-idle-test.js +0 -470
- package/test/bodystructure-test.js +0 -899
- package/test/charsets-test.js +0 -161
- package/test/commands-branches-test.js +0 -1095
- package/test/commands-integration-test.js +0 -11124
- package/test/commands-test.js +0 -73
- package/test/connection-edge-cases-test.js +0 -1828
- package/test/connection-test.js +0 -162
- package/test/copyuid-parser-test.js +0 -173
- package/test/fetch-generator-test.js +0 -218
- package/test/fixtures/fake-timers.js +0 -115
- package/test/fixtures/serialized-mimetorture.js +0 -2738
- package/test/fixtures/test-client.js +0 -57
- package/test/fixtures/test-tls.js +0 -8
- package/test/handler-branches-test.js +0 -310
- package/test/idle-polling-test.js +0 -518
- package/test/imap-compiler-test.js +0 -809
- package/test/imap-flow-compress-test.js +0 -166
- package/test/imap-flow-coverage-test.js +0 -612
- package/test/imap-flow-fetch-download-test.js +0 -873
- package/test/imap-flow-internals-test.js +0 -725
- package/test/imap-flow-methods-test.js +0 -889
- package/test/imap-flow-proxy-paths-test.js +0 -366
- package/test/imap-flow-secure-test.js +0 -573
- package/test/imap-flow-server-test.js +0 -1474
- package/test/imap-formal-syntax-test.js +0 -293
- package/test/imap-parser-test.js +0 -1474
- package/test/imap-stream-edge-cases-test.js +0 -666
- package/test/imap-stream-test.js +0 -177
- package/test/imapflow-test.js +0 -258
- package/test/integration/README.md +0 -52
- package/test/integration/dovecot-test.conf +0 -27
- package/test/integration/rev2-live-test.js +0 -431
- package/test/integration/run-rev2-tests.sh +0 -75
- package/test/integration-test.js +0 -83
- package/test/jp-decoder-test.js +0 -304
- package/test/limited-passthrough-test.js +0 -299
- package/test/memory-cleanup-test.js +0 -144
- package/test/memory-leak-test.js +0 -667
- package/test/parser-limits-test.js +0 -292
- package/test/proxy-connection-test.js +0 -738
- package/test/reliability-improvements-test.js +0 -548
- package/test/search-compiler-test.js +0 -1300
- package/test/search-test.js +0 -329
- package/test/special-use-test.js +0 -418
- package/test/starttls-injection-test.js +0 -181
- package/test/tag-correlation-test.js +0 -333
- package/test/timer-policy-test.js +0 -227
- package/test/token-parser-test.js +0 -456
- package/test/tools-test.js +0 -2013
- package/test/unhandled-rejection-test.js +0 -593
|
@@ -1,34 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
decodePath,
|
|
5
|
-
encodePath,
|
|
6
|
-
normalizePath,
|
|
7
|
-
enhanceCommandError,
|
|
8
|
-
hasCapability,
|
|
9
|
-
isRev2Active,
|
|
10
|
-
buildStatusQueryAttributes,
|
|
11
|
-
getStringList
|
|
12
|
-
} = require('../tools.js');
|
|
13
|
-
const { parseStatusList } = require('./status-fields.js');
|
|
14
|
-
const { specialUse } = require('../special-use');
|
|
15
|
-
|
|
1
|
+
import { decodePath, encodePath, normalizePath, enhanceCommandError, hasCapability, isRev2Active, buildStatusQueryAttributes, getStringList } from '../tools.js';
|
|
2
|
+
import { parseStatusList } from './status-fields.js';
|
|
3
|
+
import { specialUse } from '../special-use.js';
|
|
16
4
|
/**
|
|
17
5
|
* Lists mailboxes from the server, including subscription status and special-use flags.
|
|
18
6
|
*
|
|
19
|
-
* @param
|
|
20
|
-
* @param
|
|
21
|
-
* @param
|
|
22
|
-
* @param
|
|
23
|
-
* @param
|
|
24
|
-
* @param
|
|
25
|
-
* @param
|
|
26
|
-
* @returns
|
|
27
|
-
* @throws
|
|
7
|
+
* @param connection - IMAP connection instance
|
|
8
|
+
* @param reference - Reference name (namespace prefix)
|
|
9
|
+
* @param mailbox - Mailbox name pattern with possible wildcards
|
|
10
|
+
* @param options - List options
|
|
11
|
+
* @param options.listOnly - If true, return entries after LIST without LSUB or status queries
|
|
12
|
+
* @param options.statusQuery - Status data items to query for each listed mailbox
|
|
13
|
+
* @param options.specialUseHints - Hints mapping mailbox paths to special-use types (sent, junk, trash, drafts, archive)
|
|
14
|
+
* @returns Array of mailbox entries sorted by special-use flags and name
|
|
15
|
+
* @throws If the LIST command fails
|
|
28
16
|
*/
|
|
29
|
-
|
|
17
|
+
export default async function list(connection, reference, mailbox, options) {
|
|
30
18
|
options = options || {};
|
|
31
|
-
|
|
32
19
|
// Special-use flags sorted by display priority (INBOX first, Trash last).
|
|
33
20
|
// Used in the final sort to group special-use mailboxes at the top of the list.
|
|
34
21
|
const FLAG_SORT_ORDER = ['\\Inbox', '\\Flagged', '\\Sent', '\\Drafts', '\\All', '\\Archive', '\\Junk', '\\Trash'];
|
|
@@ -41,27 +28,23 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
41
28
|
// "name-guess" is an internal precedence tier only. It is reported as "name" so
|
|
42
29
|
// that specialUseSource keeps its documented set of values for consumers.
|
|
43
30
|
const PUBLIC_SOURCE = { 'name-guess': 'name' };
|
|
44
|
-
const isNameSource = source => source === 'name' || source === 'name-guess';
|
|
45
|
-
|
|
31
|
+
const isNameSource = (source) => source === 'name' || source === 'name-guess';
|
|
46
32
|
// Prefer XLIST (legacy Gmail extension) only if SPECIAL-USE (RFC 6154) is unavailable.
|
|
47
33
|
// Both provide special-use flags, but SPECIAL-USE is the standardized approach.
|
|
48
34
|
// SPECIAL-USE is checked with rev2 folding - a rev2 session implies SPECIAL-USE,
|
|
49
35
|
// so LIST is preferred even if a rev2 server also advertised legacy XLIST.
|
|
50
36
|
let listCommand = connection.capabilities.has('XLIST') && !hasCapability(connection, 'SPECIAL-USE') ? 'XLIST' : 'LIST';
|
|
51
|
-
|
|
52
37
|
try {
|
|
53
38
|
// Accumulators filled by the untagged LIST/STATUS handlers below. statusMap
|
|
54
39
|
// caches STATUS responses received inline via LIST-STATUS extension, keyed by
|
|
55
40
|
// normalized mailbox path (avoids separate STATUS commands per mailbox), and
|
|
56
41
|
// specialUseMatches tracks candidate mailboxes for each special-use type.
|
|
57
42
|
// (Re)initialized at the start of each retry stage of the main listing.
|
|
58
|
-
let entries;
|
|
59
|
-
let statusMap;
|
|
60
|
-
let specialUseMatches;
|
|
61
|
-
|
|
43
|
+
let entries = [];
|
|
44
|
+
let statusMap = new Map();
|
|
45
|
+
let specialUseMatches = {};
|
|
62
46
|
// STATUS data items to request (MESSAGES, UIDNEXT, etc.)
|
|
63
47
|
let statusQueryAttributes = buildStatusQueryAttributes(connection, options.statusQuery);
|
|
64
|
-
|
|
65
48
|
// Extended LIST syntax (RETURN options) is understood by servers advertising
|
|
66
49
|
// LIST-EXTENDED (RFC 5258) or IMAP4rev2 (RFC 9051). Deliberately keyed on the
|
|
67
50
|
// advertisement alone (not hasCapability/isRev2Active): the staged retry below
|
|
@@ -69,33 +52,27 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
69
52
|
// is safe for anything it covers, while gates without a retry ladder stay
|
|
70
53
|
// conservative.
|
|
71
54
|
let supportsExtendedList = connection.capabilities.has('LIST-EXTENDED') || connection.capabilities.has('IMAP4rev2');
|
|
72
|
-
|
|
73
55
|
// RETURN options for the LIST command. Servers occasionally advertise the
|
|
74
56
|
// extensions but still reject RETURN options - the staged retry below then
|
|
75
57
|
// re-runs the LIST with fewer options and latches a skip flag for the option
|
|
76
58
|
// group the server proved to reject, keeping later listings efficient.
|
|
77
|
-
|
|
78
59
|
// LIST-STATUS (RFC 5819, folded into base IMAP4rev2): request STATUS data
|
|
79
60
|
// inline with LIST, avoiding a separate STATUS command for each mailbox.
|
|
80
|
-
let canRequestStatus =
|
|
81
|
-
listCommand === 'LIST' && !connection.skipListStatusArgs && hasCapability(connection, 'LIST-STATUS') && !!statusQueryAttributes.length;
|
|
82
|
-
|
|
61
|
+
let canRequestStatus = listCommand === 'LIST' && !connection.skipListStatusArgs && hasCapability(connection, 'LIST-STATUS') && !!statusQueryAttributes.length;
|
|
83
62
|
// RETURN (SUBSCRIBED): request subscription state inline instead of a separate
|
|
84
63
|
// LSUB command. IMAP4rev2 removed LSUB entirely, and some servers (e.g.
|
|
85
64
|
// Exchange in IMAP4rev2 mode) reject it with BAD even while still advertising
|
|
86
65
|
// IMAP4rev1.
|
|
87
66
|
let canRequestSubscribed = listCommand === 'LIST' && !options.listOnly && !connection.skipListSubscribedArg && supportsExtendedList;
|
|
88
|
-
|
|
89
67
|
// Auxiliary RETURN options (SPECIAL-USE/CHILDREN) that ride along with the
|
|
90
68
|
// STATUS/SUBSCRIBED option groups. When RETURN options are present, servers
|
|
91
69
|
// may report only what was explicitly requested (verified against Dovecot
|
|
92
70
|
// 2.4: special-use and child attributes disappear from such responses), so
|
|
93
71
|
// request everything a plain LIST would have provided.
|
|
94
72
|
let auxArgsAvailable = hasCapability(connection, 'SPECIAL-USE') || connection.capabilities.has('CHILDREN') || supportsExtendedList;
|
|
95
|
-
let stageHasAuxArgs = stage => (stage.status || stage.subscribed) && stage.aux !== false && !connection.skipListAuxArgs && auxArgsAvailable;
|
|
96
|
-
|
|
73
|
+
let stageHasAuxArgs = (stage) => (stage.status || stage.subscribed) && stage.aux !== false && !connection.skipListAuxArgs && auxArgsAvailable;
|
|
97
74
|
// Builds the RETURN (...) argument list for one retry stage
|
|
98
|
-
let buildListArgs = stage => {
|
|
75
|
+
let buildListArgs = (stage) => {
|
|
99
76
|
let args = [];
|
|
100
77
|
if (stage.status) {
|
|
101
78
|
args.push({ type: 'ATOM', value: 'STATUS' }, statusQueryAttributes);
|
|
@@ -113,7 +90,6 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
113
90
|
}
|
|
114
91
|
return args;
|
|
115
92
|
};
|
|
116
|
-
|
|
117
93
|
// Multiple mailboxes may claim the same special-use type (e.g., \\Sent) via
|
|
118
94
|
// different sources (user hint, server extension, name match). After listing,
|
|
119
95
|
// the best match wins.
|
|
@@ -123,14 +99,13 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
123
99
|
}
|
|
124
100
|
specialUseMatches[type].push({ entry, source });
|
|
125
101
|
};
|
|
126
|
-
|
|
127
102
|
// RFC 5258: the \NonExistent attribute implies \Noselect. Some servers only
|
|
128
103
|
// return \NonExistent for phantom folders, so add \Noselect as well to keep
|
|
129
104
|
// the flags consistent for consumers that only check \Noselect.
|
|
130
105
|
// RETURN (SUBSCRIBED) - and some LSUB implementations - report subscription
|
|
131
106
|
// state as a \Subscribed attribute. Move it to the subscribed property so the
|
|
132
107
|
// output shape is the same however the state was delivered.
|
|
133
|
-
let normalizeFlags = entry => {
|
|
108
|
+
let normalizeFlags = (entry) => {
|
|
134
109
|
if (entry.flags.has('\\NonExistent')) {
|
|
135
110
|
entry.flags.add('\\Noselect');
|
|
136
111
|
}
|
|
@@ -139,7 +114,6 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
139
114
|
entry.subscribed = true;
|
|
140
115
|
}
|
|
141
116
|
};
|
|
142
|
-
|
|
143
117
|
// User-provided hints map mailbox paths to special-use types (e.g., {sent: "Sent Items"}).
|
|
144
118
|
// These override server-reported flags and name-based guesses. Converted to a
|
|
145
119
|
// path-keyed lookup: { "Sent Items" => "\\Sent" }
|
|
@@ -148,52 +122,43 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
148
122
|
let specialUseHints = Object.create(null);
|
|
149
123
|
if (options.specialUseHints && typeof options.specialUseHints === 'object') {
|
|
150
124
|
for (let type of Object.keys(options.specialUseHints)) {
|
|
151
|
-
if (
|
|
152
|
-
['sent', 'junk', 'trash', 'drafts', 'archive'].includes(type) &&
|
|
125
|
+
if (['sent', 'junk', 'trash', 'drafts', 'archive'].includes(type) &&
|
|
153
126
|
options.specialUseHints[type] &&
|
|
154
|
-
typeof options.specialUseHints[type] === 'string'
|
|
155
|
-
) {
|
|
127
|
+
typeof options.specialUseHints[type] === 'string') {
|
|
156
128
|
// Capitalize first letter: "sent" -> "\\Sent"
|
|
157
129
|
specialUseHints[normalizePath(connection, options.specialUseHints[type])] = `\\${type.replace(/^./, c => c.toUpperCase())}`;
|
|
158
130
|
}
|
|
159
131
|
}
|
|
160
132
|
}
|
|
161
|
-
|
|
162
133
|
// Executes a LIST (or XLIST) command and collects mailbox entries.
|
|
163
134
|
// Called once for the main listing and optionally again for INBOX if a
|
|
164
135
|
// namespace prefix was used (INBOX may live outside the namespace).
|
|
165
136
|
let runList = async (reference, mailbox, returnArgs) => {
|
|
166
137
|
const cmdArgs = [encodePath(connection, reference), encodePath(connection, mailbox)];
|
|
167
|
-
|
|
168
138
|
if (returnArgs.length) {
|
|
169
139
|
cmdArgs.push({ type: 'ATOM', value: 'RETURN' }, returnArgs);
|
|
170
140
|
}
|
|
171
|
-
|
|
172
141
|
let response = await connection.exec(listCommand, cmdArgs, {
|
|
173
142
|
untagged: {
|
|
174
143
|
// Each untagged LIST response: * LIST (<flags>) "<delimiter>" "<mailbox name>"
|
|
175
144
|
// attributes[0] = flags array, attributes[1] = delimiter, attributes[2] = mailbox name
|
|
176
|
-
[listCommand]: async untagged => {
|
|
145
|
+
[listCommand]: async (untagged) => {
|
|
177
146
|
if (!untagged.attributes || !untagged.attributes.length) {
|
|
178
147
|
return;
|
|
179
148
|
}
|
|
180
|
-
|
|
181
149
|
let entry = {
|
|
182
150
|
// Decode from modified UTF-7 wire format and normalize the path
|
|
183
|
-
path: normalizePath(connection, decodePath(connection, (untagged.attributes[2] && untagged.attributes[2].value) || '')),
|
|
184
|
-
pathAsListed: (untagged.attributes[2] && untagged.attributes[2].value) || '',
|
|
151
|
+
path: normalizePath(connection, decodePath(connection, ((untagged.attributes[2] && untagged.attributes[2].value) || ''))),
|
|
152
|
+
pathAsListed: ((untagged.attributes[2] && untagged.attributes[2].value) || ''),
|
|
185
153
|
flags: new Set(getStringList(untagged.attributes[0])),
|
|
186
|
-
delimiter: untagged.attributes[1] && untagged.attributes[1].value,
|
|
154
|
+
delimiter: (untagged.attributes[1] && untagged.attributes[1].value),
|
|
187
155
|
listed: true
|
|
188
156
|
};
|
|
189
|
-
|
|
190
157
|
normalizeFlags(entry);
|
|
191
|
-
|
|
192
158
|
// Check user-provided hints first (highest priority)
|
|
193
159
|
if (specialUseHints[entry.path]) {
|
|
194
160
|
addSpecialUseMatch(entry, specialUseHints[entry.path], 'user');
|
|
195
161
|
}
|
|
196
|
-
|
|
197
162
|
// XLIST marks INBOX with a \\Inbox flag. Remove it from flags
|
|
198
163
|
// (it's not a standard flag) and register as special-use match.
|
|
199
164
|
// XLIST may also use a localised name (e.g., "Posteingang" for German INBOX).
|
|
@@ -203,31 +168,23 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
203
168
|
addSpecialUseMatch(entry, '\\Inbox', 'extension');
|
|
204
169
|
}
|
|
205
170
|
}
|
|
206
|
-
|
|
207
171
|
// Name-based INBOX detection: any mailbox named "INBOX" (case-insensitive)
|
|
208
172
|
// is the inbox per RFC 3501. Phantom \NonExistent entries (subscribed
|
|
209
173
|
// leftovers of deleted mailboxes) must not claim the slot by name.
|
|
210
174
|
if (entry.path.toUpperCase() === 'INBOX' && !entry.flags.has('\\NonExistent')) {
|
|
211
175
|
addSpecialUseMatch(entry, '\\Inbox', 'name');
|
|
212
176
|
}
|
|
213
|
-
|
|
214
177
|
// Strip leading delimiter (some servers prepend it to paths)
|
|
215
178
|
if (entry.delimiter && entry.path.charAt(0) === entry.delimiter) {
|
|
216
179
|
entry.path = entry.path.slice(1);
|
|
217
180
|
}
|
|
218
|
-
|
|
219
181
|
// Build parent path hierarchy for tree construction and sorting
|
|
220
182
|
entry.parentPath = entry.delimiter && entry.path ? entry.path.substr(0, entry.path.lastIndexOf(entry.delimiter)) : '';
|
|
221
183
|
entry.parent = entry.delimiter ? entry.path.split(entry.delimiter) : [entry.path];
|
|
222
184
|
entry.name = entry.parent.pop();
|
|
223
|
-
|
|
224
185
|
// Try to detect special-use from server flags or well-known names
|
|
225
186
|
// (e.g., "Sent", "Drafts", "Junk", "Trash")
|
|
226
|
-
let { flag: specialUseFlag, source: flagSource } = specialUse(
|
|
227
|
-
connection.capabilities.has('XLIST') || hasCapability(connection, 'SPECIAL-USE'),
|
|
228
|
-
entry
|
|
229
|
-
);
|
|
230
|
-
|
|
187
|
+
let { flag: specialUseFlag, source: flagSource } = specialUse(connection.capabilities.has('XLIST') || hasCapability(connection, 'SPECIAL-USE'), entry);
|
|
231
188
|
// A name-based match for a \NonExistent phantom entry could win the
|
|
232
189
|
// special-use slot over the real folder - only server-provided flags
|
|
233
190
|
// are trusted for nonexistent entries. Covers every name-derived
|
|
@@ -235,34 +192,28 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
235
192
|
if (specialUseFlag && (!isNameSource(flagSource) || !entry.flags.has('\\NonExistent'))) {
|
|
236
193
|
addSpecialUseMatch(entry, specialUseFlag, flagSource);
|
|
237
194
|
}
|
|
238
|
-
|
|
239
195
|
entries.push(entry);
|
|
240
196
|
},
|
|
241
|
-
|
|
242
197
|
// Inline STATUS response from LIST-STATUS extension (RFC 5819).
|
|
243
198
|
// Parses alternating key-value pairs (i % 2 pattern).
|
|
244
|
-
STATUS: async untagged => {
|
|
245
|
-
let statusPath = normalizePath(connection, decodePath(connection, (untagged.attributes[0] && untagged.attributes[0].value) || ''));
|
|
199
|
+
STATUS: async (untagged) => {
|
|
200
|
+
let statusPath = normalizePath(connection, decodePath(connection, ((untagged.attributes[0] && untagged.attributes[0].value) || '')));
|
|
246
201
|
let statusList = untagged.attributes && Array.isArray(untagged.attributes[1]) ? untagged.attributes[1] : false;
|
|
247
202
|
if (!statusList || !statusPath) {
|
|
248
203
|
return;
|
|
249
204
|
}
|
|
250
|
-
|
|
251
205
|
let map = { path: statusPath };
|
|
252
206
|
parseStatusList(statusList, (key, value) => {
|
|
253
207
|
map[key] = value;
|
|
254
208
|
});
|
|
255
|
-
|
|
256
209
|
statusMap.set(statusPath, map);
|
|
257
210
|
}
|
|
258
211
|
}
|
|
259
212
|
});
|
|
260
213
|
response.next();
|
|
261
214
|
};
|
|
262
|
-
|
|
263
215
|
let normalizedReference = normalizePath(connection, reference || '');
|
|
264
216
|
let normalizedMailbox = normalizePath(connection, mailbox || '', true);
|
|
265
|
-
|
|
266
217
|
// Retry stages for the main listing: start with all applicable RETURN options
|
|
267
218
|
// and drop one option group per retry. Consecutive stages differ by exactly one
|
|
268
219
|
// group, so a success right after a rejection identifies the offending group
|
|
@@ -276,29 +227,25 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
276
227
|
}
|
|
277
228
|
if (canRequestStatus) {
|
|
278
229
|
stages.push({ status: true, subscribed: false });
|
|
279
|
-
}
|
|
230
|
+
}
|
|
231
|
+
else if (canRequestSubscribed) {
|
|
280
232
|
stages.push({ status: false, subscribed: true });
|
|
281
233
|
}
|
|
282
234
|
stages.push({ status: false, subscribed: false });
|
|
283
|
-
|
|
284
235
|
// A tagged BAD is how servers reject unrecognized RETURN options (RFC 9051
|
|
285
236
|
// section 6.3.9). A tagged NO is an operational failure, and throttling
|
|
286
237
|
// errors (code ETHROTTLE) also surface with a BAD status - neither says
|
|
287
238
|
// anything about the RETURN options, so they propagate to the caller.
|
|
288
|
-
let isRejectedCommand = err => err.responseStatus === 'BAD' && err.code !== 'ETHROTTLE';
|
|
289
|
-
|
|
239
|
+
let isRejectedCommand = (err) => err.responseStatus === 'BAD' && err.code !== 'ETHROTTLE';
|
|
290
240
|
// Stage of the successful attempt - reused by the INBOX fixup and the LSUB
|
|
291
241
|
// decision below
|
|
292
242
|
let successStage = null;
|
|
293
|
-
|
|
294
243
|
// Whether any source actually reported subscription state. RETURN (SUBSCRIBED)
|
|
295
244
|
// and LSUB are the only two, and a server can refuse both
|
|
296
245
|
let subscriptionStateKnown = false;
|
|
297
|
-
|
|
298
246
|
// A server may also volunteer \Subscribed on a plain LIST, which normalizeFlags
|
|
299
247
|
// folds into the entry - that counts as the state having been reported
|
|
300
248
|
let anyEntrySubscribed = () => entries.some(entry => entry.subscribed);
|
|
301
|
-
|
|
302
249
|
let lastRejectedStage = null;
|
|
303
250
|
let auxRetryInserted = false;
|
|
304
251
|
for (let i = 0; i < stages.length; i++) {
|
|
@@ -322,12 +269,10 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
322
269
|
if (lastRejectedStage.status && !stage.status) {
|
|
323
270
|
connection.skipListStatusArgs = true;
|
|
324
271
|
}
|
|
325
|
-
if (
|
|
326
|
-
stageHasAuxArgs(lastRejectedStage) &&
|
|
272
|
+
if (stageHasAuxArgs(lastRejectedStage) &&
|
|
327
273
|
stage.aux === false &&
|
|
328
274
|
lastRejectedStage.status === stage.status &&
|
|
329
|
-
lastRejectedStage.subscribed === stage.subscribed
|
|
330
|
-
) {
|
|
275
|
+
lastRejectedStage.subscribed === stage.subscribed) {
|
|
331
276
|
// Same option groups, only the auxiliary args dropped - the
|
|
332
277
|
// auxiliaries are proven to be what the server rejects
|
|
333
278
|
connection.skipListAuxArgs = true;
|
|
@@ -336,7 +281,8 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
336
281
|
successStage = stage;
|
|
337
282
|
subscriptionStateKnown = !!stage.subscribed;
|
|
338
283
|
break;
|
|
339
|
-
}
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
340
286
|
if (i === stages.length - 1 || !isRejectedCommand(err)) {
|
|
341
287
|
throw err;
|
|
342
288
|
}
|
|
@@ -351,11 +297,9 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
351
297
|
connection.log.warn({ msg: 'LIST RETURN options rejected, retrying with reduced options', err, cid: connection.id });
|
|
352
298
|
}
|
|
353
299
|
}
|
|
354
|
-
|
|
355
300
|
if (options.listOnly) {
|
|
356
301
|
return entries;
|
|
357
302
|
}
|
|
358
|
-
|
|
359
303
|
// When listing with a namespace prefix (e.g., "INBOX."), INBOX itself may
|
|
360
304
|
// not appear in results. Run a separate LIST for INBOX to ensure it's included.
|
|
361
305
|
if (normalizedReference && !specialUseMatches['\\Inbox']) {
|
|
@@ -371,7 +315,8 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
371
315
|
}
|
|
372
316
|
try {
|
|
373
317
|
await runList('', 'INBOX', returnArgs);
|
|
374
|
-
}
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
375
320
|
// The main listing just succeeded with the same RETURN options, so a
|
|
376
321
|
// rejection here says nothing about the options themselves - retry
|
|
377
322
|
// this one call plain without latching any skip flags. Accepted edge:
|
|
@@ -384,7 +329,8 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
384
329
|
for (let type of Object.keys(specialUseMatches)) {
|
|
385
330
|
if (!(type in specialUseCountsBefore)) {
|
|
386
331
|
delete specialUseMatches[type];
|
|
387
|
-
}
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
388
334
|
specialUseMatches[type].length = specialUseCountsBefore[type];
|
|
389
335
|
}
|
|
390
336
|
}
|
|
@@ -392,7 +338,6 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
392
338
|
await runList('', 'INBOX', []);
|
|
393
339
|
}
|
|
394
340
|
}
|
|
395
|
-
|
|
396
341
|
// Attach STATUS data to each selectable mailbox. If LIST-STATUS was used,
|
|
397
342
|
// data is already in statusMap; otherwise, fall back to individual STATUS commands.
|
|
398
343
|
if (options.statusQuery) {
|
|
@@ -408,18 +353,19 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
408
353
|
if (syntheticRecent) {
|
|
409
354
|
entry.status.recent = 0;
|
|
410
355
|
}
|
|
411
|
-
}
|
|
356
|
+
}
|
|
357
|
+
else if (!statusMap.size) {
|
|
412
358
|
// Server didn't support LIST-STATUS; fall back to per-mailbox STATUS
|
|
413
359
|
try {
|
|
414
360
|
entry.status = await connection.run('STATUS', entry.path, options.statusQuery);
|
|
415
|
-
}
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
416
363
|
entry.status = { error: err };
|
|
417
364
|
}
|
|
418
365
|
}
|
|
419
366
|
}
|
|
420
367
|
}
|
|
421
368
|
}
|
|
422
|
-
|
|
423
369
|
// LSUB (RFC 3501 6.3.9): queries which mailboxes the user is subscribed to.
|
|
424
370
|
// We merge subscription info into the entries already collected from LIST.
|
|
425
371
|
// Subscribed-only mailboxes that weren't in LIST are intentionally ignored
|
|
@@ -427,31 +373,26 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
427
373
|
let runLsub = async () => {
|
|
428
374
|
let response = await connection.exec('LSUB', [encodePath(connection, normalizedReference), encodePath(connection, normalizedMailbox)], {
|
|
429
375
|
untagged: {
|
|
430
|
-
LSUB: async untagged => {
|
|
376
|
+
LSUB: async (untagged) => {
|
|
431
377
|
if (!untagged.attributes || !untagged.attributes.length) {
|
|
432
378
|
return;
|
|
433
379
|
}
|
|
434
|
-
|
|
435
380
|
let entry = {
|
|
436
|
-
path: normalizePath(connection, decodePath(connection, (untagged.attributes[2] && untagged.attributes[2].value) || '')),
|
|
437
|
-
pathAsListed: (untagged.attributes[2] && untagged.attributes[2].value) || '',
|
|
381
|
+
path: normalizePath(connection, decodePath(connection, ((untagged.attributes[2] && untagged.attributes[2].value) || ''))),
|
|
382
|
+
pathAsListed: ((untagged.attributes[2] && untagged.attributes[2].value) || ''),
|
|
438
383
|
flags: new Set(getStringList(untagged.attributes[0])),
|
|
439
|
-
delimiter: untagged.attributes[1] && untagged.attributes[1].value,
|
|
384
|
+
delimiter: (untagged.attributes[1] && untagged.attributes[1].value),
|
|
440
385
|
subscribed: true
|
|
441
386
|
};
|
|
442
|
-
|
|
443
387
|
if (entry.path.toUpperCase() === 'INBOX') {
|
|
444
388
|
addSpecialUseMatch(entry, '\\Inbox', 'name');
|
|
445
389
|
}
|
|
446
|
-
|
|
447
390
|
if (entry.delimiter && entry.path.charAt(0) === entry.delimiter) {
|
|
448
391
|
entry.path = entry.path.slice(1);
|
|
449
392
|
}
|
|
450
|
-
|
|
451
393
|
entry.parentPath = entry.delimiter && entry.path ? entry.path.substr(0, entry.path.lastIndexOf(entry.delimiter)) : '';
|
|
452
394
|
entry.parent = entry.delimiter ? entry.path.split(entry.delimiter) : [entry.path];
|
|
453
395
|
entry.name = entry.parent.pop();
|
|
454
|
-
|
|
455
396
|
// Merge LSUB data into existing LIST entry if found
|
|
456
397
|
let existing = entries.find(existing => existing.path === entry.path);
|
|
457
398
|
if (existing) {
|
|
@@ -466,7 +407,6 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
466
407
|
});
|
|
467
408
|
response.next();
|
|
468
409
|
};
|
|
469
|
-
|
|
470
410
|
// Never sent on a rev2 session - LSUB is not part of that protocol version, and
|
|
471
411
|
// some servers break the rest of the session over the rejection, so this is
|
|
472
412
|
// decided up front rather than left to the skipLsub latch below. On rev1 it is
|
|
@@ -485,12 +425,14 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
485
425
|
try {
|
|
486
426
|
await runLsub();
|
|
487
427
|
subscriptionStateKnown = true;
|
|
488
|
-
}
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
489
430
|
if (isRejectedCommand(err)) {
|
|
490
431
|
// Tagged BAD: the server does not implement LSUB despite advertising
|
|
491
432
|
// rev1 - skip it for the rest of this connection
|
|
492
433
|
connection.skipLsub = true;
|
|
493
|
-
}
|
|
434
|
+
}
|
|
435
|
+
else if (err.responseStatus !== 'NO' || err.code === 'ETHROTTLE') {
|
|
494
436
|
// Transport failures and throttling: rethrow, every follow-up
|
|
495
437
|
// command would fail too or the caller needs to back off
|
|
496
438
|
throw err;
|
|
@@ -500,7 +442,6 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
500
442
|
connection.log.warn({ msg: 'Failed to request subscription info', err, cid: connection.id });
|
|
501
443
|
}
|
|
502
444
|
}
|
|
503
|
-
|
|
504
445
|
// Resolve special-use conflicts: for each type, pick the best candidate
|
|
505
446
|
// based on source priority (user > extension > name), then alphabetically.
|
|
506
447
|
// Only the winning entry gets the specialUse property set.
|
|
@@ -513,14 +454,12 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
513
454
|
}
|
|
514
455
|
return aSource - bSource;
|
|
515
456
|
});
|
|
516
|
-
|
|
517
457
|
if (!sortedEntries[0].entry.specialUse) {
|
|
518
458
|
let source = sortedEntries[0].source;
|
|
519
459
|
sortedEntries[0].entry.specialUse = type;
|
|
520
460
|
sortedEntries[0].entry.specialUseSource = PUBLIC_SOURCE[source] || source;
|
|
521
461
|
}
|
|
522
462
|
}
|
|
523
|
-
|
|
524
463
|
// No source answered, so "not subscribed" was never actually reported for any of
|
|
525
464
|
// these folders - the state is unknown, not false. Reporting the whole listing as
|
|
526
465
|
// unsubscribed would hide every folder from a client that filters on subscription
|
|
@@ -535,13 +474,11 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
535
474
|
}
|
|
536
475
|
}
|
|
537
476
|
}
|
|
538
|
-
|
|
539
477
|
// INBOX should always appear as subscribed regardless of LSUB results
|
|
540
478
|
let inboxEntry = entries.find(entry => entry.specialUse === '\\Inbox');
|
|
541
479
|
if (inboxEntry && !inboxEntry.subscribed) {
|
|
542
480
|
inboxEntry.subscribed = true;
|
|
543
481
|
}
|
|
544
|
-
|
|
545
482
|
// Sort: special-use mailboxes first (in FLAG_SORT_ORDER), then alphabetically
|
|
546
483
|
// by path segments for a natural folder hierarchy ordering.
|
|
547
484
|
return entries.sort((a, b) => {
|
|
@@ -554,10 +491,8 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
554
491
|
if (a.specialUse && b.specialUse) {
|
|
555
492
|
return FLAG_SORT_ORDER.indexOf(a.specialUse) - FLAG_SORT_ORDER.indexOf(b.specialUse);
|
|
556
493
|
}
|
|
557
|
-
|
|
558
494
|
let aList = [].concat(a.parent).concat(a.name);
|
|
559
495
|
let bList = [].concat(b.parent).concat(b.name);
|
|
560
|
-
|
|
561
496
|
for (let i = 0; i < aList.length; i++) {
|
|
562
497
|
let aPart = aList[i];
|
|
563
498
|
let bPart = bList[i];
|
|
@@ -565,14 +500,14 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
565
500
|
return aPart.localeCompare(bPart || '');
|
|
566
501
|
}
|
|
567
502
|
}
|
|
568
|
-
|
|
569
503
|
return a.path.localeCompare(b.path);
|
|
570
504
|
});
|
|
571
|
-
}
|
|
505
|
+
}
|
|
506
|
+
catch (err) {
|
|
572
507
|
// Rewrite the parsed err.response into the response text and set
|
|
573
508
|
// serverResponseCode, same as the other command modules
|
|
574
509
|
await enhanceCommandError(err);
|
|
575
510
|
connection.log.warn({ msg: 'Failed to list folders', err, cid: connection.id });
|
|
576
511
|
throw err;
|
|
577
512
|
}
|
|
578
|
-
}
|
|
513
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ImapFlow } from '../imap-flow.js';
|
|
2
|
+
/**
|
|
3
|
+
* Authenticates user using the IMAP LOGIN command.
|
|
4
|
+
*
|
|
5
|
+
* @param connection - IMAP connection instance
|
|
6
|
+
* @param username - The username to authenticate with
|
|
7
|
+
* @param password - The password to authenticate with
|
|
8
|
+
* @returns The authenticated username, or undefined if already authenticated
|
|
9
|
+
* @throws If authentication fails, with authenticationFailed and serverResponseCode properties set
|
|
10
|
+
*/
|
|
11
|
+
export default function login(connection: ImapFlow, username: string, password: string): Promise<string | undefined>;
|
|
@@ -1,22 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const { getStatusCode, getErrorText } = require('../tools.js');
|
|
4
|
-
|
|
1
|
+
import { getStatusCode, getErrorText } from '../tools.js';
|
|
5
2
|
/**
|
|
6
3
|
* Authenticates user using the IMAP LOGIN command.
|
|
7
4
|
*
|
|
8
|
-
* @param
|
|
9
|
-
* @param
|
|
10
|
-
* @param
|
|
11
|
-
* @returns
|
|
12
|
-
* @throws
|
|
5
|
+
* @param connection - IMAP connection instance
|
|
6
|
+
* @param username - The username to authenticate with
|
|
7
|
+
* @param password - The password to authenticate with
|
|
8
|
+
* @returns The authenticated username, or undefined if already authenticated
|
|
9
|
+
* @throws If authentication fails, with authenticationFailed and serverResponseCode properties set
|
|
13
10
|
*/
|
|
14
|
-
|
|
11
|
+
export default async function login(connection, username, password) {
|
|
15
12
|
if (connection.state !== connection.states.NOT_AUTHENTICATED) {
|
|
16
13
|
// nothing to do here
|
|
17
14
|
return;
|
|
18
15
|
}
|
|
19
|
-
|
|
20
16
|
try {
|
|
21
17
|
let response = await connection.exec('LOGIN', [
|
|
22
18
|
{ type: 'STRING', value: username },
|
|
@@ -24,13 +20,12 @@ module.exports = async (connection, username, password) => {
|
|
|
24
20
|
{ type: 'STRING', value: password, sensitive: true }
|
|
25
21
|
]);
|
|
26
22
|
response.next();
|
|
27
|
-
|
|
28
23
|
// Record that LOGIN was the method used, so the connection knows which
|
|
29
24
|
// auth mechanism succeeded (used for reconnection and diagnostics).
|
|
30
25
|
connection.authCapabilities.set('LOGIN', true);
|
|
31
|
-
|
|
32
26
|
return username;
|
|
33
|
-
}
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
34
29
|
let errorCode = getStatusCode(err.response);
|
|
35
30
|
if (errorCode) {
|
|
36
31
|
err.serverResponseCode = errorCode;
|
|
@@ -39,4 +34,4 @@ module.exports = async (connection, username, password) => {
|
|
|
39
34
|
err.response = await getErrorText(err.response);
|
|
40
35
|
throw err;
|
|
41
36
|
}
|
|
42
|
-
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ImapFlow } from '../imap-flow.js';
|
|
2
|
+
/**
|
|
3
|
+
* Logs out the user and closes the connection.
|
|
4
|
+
*
|
|
5
|
+
* @param connection - IMAP connection instance
|
|
6
|
+
* @returns True if logout command succeeded, false otherwise
|
|
7
|
+
*/
|
|
8
|
+
export default function logout(connection: ImapFlow): Promise<boolean>;
|
|
@@ -1,29 +1,26 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Logs out the user and closes the connection.
|
|
5
3
|
*
|
|
6
|
-
* @param
|
|
7
|
-
* @returns
|
|
4
|
+
* @param connection - IMAP connection instance
|
|
5
|
+
* @returns True if logout command succeeded, false otherwise
|
|
8
6
|
*/
|
|
9
|
-
|
|
7
|
+
export default async function logout(connection) {
|
|
10
8
|
if (connection.state === connection.states.LOGOUT) {
|
|
11
9
|
// nothing to do here
|
|
12
10
|
return false;
|
|
13
11
|
}
|
|
14
|
-
|
|
15
12
|
if (connection.state === connection.states.NOT_AUTHENTICATED) {
|
|
16
|
-
// Not yet authenticated
|
|
13
|
+
// Not yet authenticated, no LOGOUT command needed; just close the socket.
|
|
17
14
|
connection.state = connection.states.LOGOUT;
|
|
18
15
|
connection.close();
|
|
19
16
|
return false;
|
|
20
17
|
}
|
|
21
|
-
|
|
22
18
|
let response;
|
|
23
19
|
try {
|
|
24
20
|
response = await connection.exec('LOGOUT');
|
|
25
21
|
return true;
|
|
26
|
-
}
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
27
24
|
// If the connection is already gone, treat as successful logout
|
|
28
25
|
if (err.code === 'NoConnection') {
|
|
29
26
|
return true;
|
|
@@ -31,7 +28,8 @@ module.exports = async connection => {
|
|
|
31
28
|
connection.log.warn({ err, cid: connection.id });
|
|
32
29
|
return false;
|
|
33
30
|
/* c8 ignore next */ // the catch above is exhaustive (never re-throws), so finally is only ever reached via normal completion
|
|
34
|
-
}
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
35
33
|
// Set state to LOGOUT before closing to prevent any further commands from
|
|
36
34
|
// being queued. The socket is closed unconditionally in this finally block
|
|
37
35
|
// regardless of whether the LOGOUT command succeeded or failed.
|
|
@@ -41,4 +39,4 @@ module.exports = async connection => {
|
|
|
41
39
|
}
|
|
42
40
|
connection.close();
|
|
43
41
|
}
|
|
44
|
-
}
|
|
42
|
+
}
|