imapflow 1.7.8 → 2.0.1
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 +520 -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 +3956 -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} +387 -517
- 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 +60 -123
- 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 +769 -1790
- 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 -101
- 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 -909
- 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 -661
|
@@ -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,61 +28,53 @@ 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
|
|
68
51
|
// handles servers that advertise but reject RETURN options, so the wider gate
|
|
69
52
|
// is safe for anything it covers, while gates without a retry ladder stay
|
|
70
|
-
// conservative.
|
|
71
|
-
|
|
72
|
-
|
|
53
|
+
// conservative. The rev2 advertisement stops counting once the session has
|
|
54
|
+
// been told not to act on it (skipRev2), because the ladder costs one rejected
|
|
55
|
+
// command per stage and a server that has disowned rev2 may not grant that many.
|
|
56
|
+
let supportsExtendedList = connection.capabilities.has('LIST-EXTENDED') || (connection.capabilities.has('IMAP4rev2') && !connection.skipRev2);
|
|
73
57
|
// RETURN options for the LIST command. Servers occasionally advertise the
|
|
74
58
|
// extensions but still reject RETURN options - the staged retry below then
|
|
75
59
|
// re-runs the LIST with fewer options and latches a skip flag for the option
|
|
76
60
|
// group the server proved to reject, keeping later listings efficient.
|
|
77
|
-
|
|
78
61
|
// LIST-STATUS (RFC 5819, folded into base IMAP4rev2): request STATUS data
|
|
79
62
|
// 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
|
-
|
|
63
|
+
let canRequestStatus = listCommand === 'LIST' && !connection.skipListStatusArgs && hasCapability(connection, 'LIST-STATUS') && !!statusQueryAttributes.length;
|
|
83
64
|
// RETURN (SUBSCRIBED): request subscription state inline instead of a separate
|
|
84
65
|
// LSUB command. IMAP4rev2 removed LSUB entirely, and some servers (e.g.
|
|
85
66
|
// Exchange in IMAP4rev2 mode) reject it with BAD even while still advertising
|
|
86
67
|
// IMAP4rev1.
|
|
87
68
|
let canRequestSubscribed = listCommand === 'LIST' && !options.listOnly && !connection.skipListSubscribedArg && supportsExtendedList;
|
|
88
|
-
|
|
89
69
|
// Auxiliary RETURN options (SPECIAL-USE/CHILDREN) that ride along with the
|
|
90
70
|
// STATUS/SUBSCRIBED option groups. When RETURN options are present, servers
|
|
91
71
|
// may report only what was explicitly requested (verified against Dovecot
|
|
92
72
|
// 2.4: special-use and child attributes disappear from such responses), so
|
|
93
73
|
// request everything a plain LIST would have provided.
|
|
94
74
|
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
|
-
|
|
75
|
+
let stageHasAuxArgs = (stage) => (stage.status || stage.subscribed) && stage.aux !== false && !connection.skipListAuxArgs && auxArgsAvailable;
|
|
97
76
|
// Builds the RETURN (...) argument list for one retry stage
|
|
98
|
-
let buildListArgs = stage => {
|
|
77
|
+
let buildListArgs = (stage) => {
|
|
99
78
|
let args = [];
|
|
100
79
|
if (stage.status) {
|
|
101
80
|
args.push({ type: 'ATOM', value: 'STATUS' }, statusQueryAttributes);
|
|
@@ -113,7 +92,6 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
113
92
|
}
|
|
114
93
|
return args;
|
|
115
94
|
};
|
|
116
|
-
|
|
117
95
|
// Multiple mailboxes may claim the same special-use type (e.g., \\Sent) via
|
|
118
96
|
// different sources (user hint, server extension, name match). After listing,
|
|
119
97
|
// the best match wins.
|
|
@@ -123,14 +101,13 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
123
101
|
}
|
|
124
102
|
specialUseMatches[type].push({ entry, source });
|
|
125
103
|
};
|
|
126
|
-
|
|
127
104
|
// RFC 5258: the \NonExistent attribute implies \Noselect. Some servers only
|
|
128
105
|
// return \NonExistent for phantom folders, so add \Noselect as well to keep
|
|
129
106
|
// the flags consistent for consumers that only check \Noselect.
|
|
130
107
|
// RETURN (SUBSCRIBED) - and some LSUB implementations - report subscription
|
|
131
108
|
// state as a \Subscribed attribute. Move it to the subscribed property so the
|
|
132
109
|
// output shape is the same however the state was delivered.
|
|
133
|
-
let normalizeFlags = entry => {
|
|
110
|
+
let normalizeFlags = (entry) => {
|
|
134
111
|
if (entry.flags.has('\\NonExistent')) {
|
|
135
112
|
entry.flags.add('\\Noselect');
|
|
136
113
|
}
|
|
@@ -139,7 +116,6 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
139
116
|
entry.subscribed = true;
|
|
140
117
|
}
|
|
141
118
|
};
|
|
142
|
-
|
|
143
119
|
// User-provided hints map mailbox paths to special-use types (e.g., {sent: "Sent Items"}).
|
|
144
120
|
// These override server-reported flags and name-based guesses. Converted to a
|
|
145
121
|
// path-keyed lookup: { "Sent Items" => "\\Sent" }
|
|
@@ -148,52 +124,43 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
148
124
|
let specialUseHints = Object.create(null);
|
|
149
125
|
if (options.specialUseHints && typeof options.specialUseHints === 'object') {
|
|
150
126
|
for (let type of Object.keys(options.specialUseHints)) {
|
|
151
|
-
if (
|
|
152
|
-
['sent', 'junk', 'trash', 'drafts', 'archive'].includes(type) &&
|
|
127
|
+
if (['sent', 'junk', 'trash', 'drafts', 'archive'].includes(type) &&
|
|
153
128
|
options.specialUseHints[type] &&
|
|
154
|
-
typeof options.specialUseHints[type] === 'string'
|
|
155
|
-
) {
|
|
129
|
+
typeof options.specialUseHints[type] === 'string') {
|
|
156
130
|
// Capitalize first letter: "sent" -> "\\Sent"
|
|
157
131
|
specialUseHints[normalizePath(connection, options.specialUseHints[type])] = `\\${type.replace(/^./, c => c.toUpperCase())}`;
|
|
158
132
|
}
|
|
159
133
|
}
|
|
160
134
|
}
|
|
161
|
-
|
|
162
135
|
// Executes a LIST (or XLIST) command and collects mailbox entries.
|
|
163
136
|
// Called once for the main listing and optionally again for INBOX if a
|
|
164
137
|
// namespace prefix was used (INBOX may live outside the namespace).
|
|
165
138
|
let runList = async (reference, mailbox, returnArgs) => {
|
|
166
139
|
const cmdArgs = [encodePath(connection, reference), encodePath(connection, mailbox)];
|
|
167
|
-
|
|
168
140
|
if (returnArgs.length) {
|
|
169
141
|
cmdArgs.push({ type: 'ATOM', value: 'RETURN' }, returnArgs);
|
|
170
142
|
}
|
|
171
|
-
|
|
172
143
|
let response = await connection.exec(listCommand, cmdArgs, {
|
|
173
144
|
untagged: {
|
|
174
145
|
// Each untagged LIST response: * LIST (<flags>) "<delimiter>" "<mailbox name>"
|
|
175
146
|
// attributes[0] = flags array, attributes[1] = delimiter, attributes[2] = mailbox name
|
|
176
|
-
[listCommand]: async untagged => {
|
|
147
|
+
[listCommand]: async (untagged) => {
|
|
177
148
|
if (!untagged.attributes || !untagged.attributes.length) {
|
|
178
149
|
return;
|
|
179
150
|
}
|
|
180
|
-
|
|
181
151
|
let entry = {
|
|
182
152
|
// 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) || '',
|
|
153
|
+
path: normalizePath(connection, decodePath(connection, ((untagged.attributes[2] && untagged.attributes[2].value) || ''))),
|
|
154
|
+
pathAsListed: ((untagged.attributes[2] && untagged.attributes[2].value) || ''),
|
|
185
155
|
flags: new Set(getStringList(untagged.attributes[0])),
|
|
186
|
-
delimiter: untagged.attributes[1] && untagged.attributes[1].value,
|
|
156
|
+
delimiter: (untagged.attributes[1] && untagged.attributes[1].value),
|
|
187
157
|
listed: true
|
|
188
158
|
};
|
|
189
|
-
|
|
190
159
|
normalizeFlags(entry);
|
|
191
|
-
|
|
192
160
|
// Check user-provided hints first (highest priority)
|
|
193
161
|
if (specialUseHints[entry.path]) {
|
|
194
162
|
addSpecialUseMatch(entry, specialUseHints[entry.path], 'user');
|
|
195
163
|
}
|
|
196
|
-
|
|
197
164
|
// XLIST marks INBOX with a \\Inbox flag. Remove it from flags
|
|
198
165
|
// (it's not a standard flag) and register as special-use match.
|
|
199
166
|
// XLIST may also use a localised name (e.g., "Posteingang" for German INBOX).
|
|
@@ -203,31 +170,23 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
203
170
|
addSpecialUseMatch(entry, '\\Inbox', 'extension');
|
|
204
171
|
}
|
|
205
172
|
}
|
|
206
|
-
|
|
207
173
|
// Name-based INBOX detection: any mailbox named "INBOX" (case-insensitive)
|
|
208
174
|
// is the inbox per RFC 3501. Phantom \NonExistent entries (subscribed
|
|
209
175
|
// leftovers of deleted mailboxes) must not claim the slot by name.
|
|
210
176
|
if (entry.path.toUpperCase() === 'INBOX' && !entry.flags.has('\\NonExistent')) {
|
|
211
177
|
addSpecialUseMatch(entry, '\\Inbox', 'name');
|
|
212
178
|
}
|
|
213
|
-
|
|
214
179
|
// Strip leading delimiter (some servers prepend it to paths)
|
|
215
180
|
if (entry.delimiter && entry.path.charAt(0) === entry.delimiter) {
|
|
216
181
|
entry.path = entry.path.slice(1);
|
|
217
182
|
}
|
|
218
|
-
|
|
219
183
|
// Build parent path hierarchy for tree construction and sorting
|
|
220
184
|
entry.parentPath = entry.delimiter && entry.path ? entry.path.substr(0, entry.path.lastIndexOf(entry.delimiter)) : '';
|
|
221
185
|
entry.parent = entry.delimiter ? entry.path.split(entry.delimiter) : [entry.path];
|
|
222
186
|
entry.name = entry.parent.pop();
|
|
223
|
-
|
|
224
187
|
// Try to detect special-use from server flags or well-known names
|
|
225
188
|
// (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
|
-
|
|
189
|
+
let { flag: specialUseFlag, source: flagSource } = specialUse(connection.capabilities.has('XLIST') || hasCapability(connection, 'SPECIAL-USE'), entry);
|
|
231
190
|
// A name-based match for a \NonExistent phantom entry could win the
|
|
232
191
|
// special-use slot over the real folder - only server-provided flags
|
|
233
192
|
// are trusted for nonexistent entries. Covers every name-derived
|
|
@@ -235,34 +194,28 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
235
194
|
if (specialUseFlag && (!isNameSource(flagSource) || !entry.flags.has('\\NonExistent'))) {
|
|
236
195
|
addSpecialUseMatch(entry, specialUseFlag, flagSource);
|
|
237
196
|
}
|
|
238
|
-
|
|
239
197
|
entries.push(entry);
|
|
240
198
|
},
|
|
241
|
-
|
|
242
199
|
// Inline STATUS response from LIST-STATUS extension (RFC 5819).
|
|
243
200
|
// 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) || ''));
|
|
201
|
+
STATUS: async (untagged) => {
|
|
202
|
+
let statusPath = normalizePath(connection, decodePath(connection, ((untagged.attributes[0] && untagged.attributes[0].value) || '')));
|
|
246
203
|
let statusList = untagged.attributes && Array.isArray(untagged.attributes[1]) ? untagged.attributes[1] : false;
|
|
247
204
|
if (!statusList || !statusPath) {
|
|
248
205
|
return;
|
|
249
206
|
}
|
|
250
|
-
|
|
251
207
|
let map = { path: statusPath };
|
|
252
208
|
parseStatusList(statusList, (key, value) => {
|
|
253
209
|
map[key] = value;
|
|
254
210
|
});
|
|
255
|
-
|
|
256
211
|
statusMap.set(statusPath, map);
|
|
257
212
|
}
|
|
258
213
|
}
|
|
259
214
|
});
|
|
260
215
|
response.next();
|
|
261
216
|
};
|
|
262
|
-
|
|
263
217
|
let normalizedReference = normalizePath(connection, reference || '');
|
|
264
218
|
let normalizedMailbox = normalizePath(connection, mailbox || '', true);
|
|
265
|
-
|
|
266
219
|
// Retry stages for the main listing: start with all applicable RETURN options
|
|
267
220
|
// and drop one option group per retry. Consecutive stages differ by exactly one
|
|
268
221
|
// group, so a success right after a rejection identifies the offending group
|
|
@@ -276,29 +229,25 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
276
229
|
}
|
|
277
230
|
if (canRequestStatus) {
|
|
278
231
|
stages.push({ status: true, subscribed: false });
|
|
279
|
-
}
|
|
232
|
+
}
|
|
233
|
+
else if (canRequestSubscribed) {
|
|
280
234
|
stages.push({ status: false, subscribed: true });
|
|
281
235
|
}
|
|
282
236
|
stages.push({ status: false, subscribed: false });
|
|
283
|
-
|
|
284
237
|
// A tagged BAD is how servers reject unrecognized RETURN options (RFC 9051
|
|
285
238
|
// section 6.3.9). A tagged NO is an operational failure, and throttling
|
|
286
239
|
// errors (code ETHROTTLE) also surface with a BAD status - neither says
|
|
287
240
|
// anything about the RETURN options, so they propagate to the caller.
|
|
288
|
-
let isRejectedCommand = err => err.responseStatus === 'BAD' && err.code !== 'ETHROTTLE';
|
|
289
|
-
|
|
241
|
+
let isRejectedCommand = (err) => err.responseStatus === 'BAD' && err.code !== 'ETHROTTLE';
|
|
290
242
|
// Stage of the successful attempt - reused by the INBOX fixup and the LSUB
|
|
291
243
|
// decision below
|
|
292
244
|
let successStage = null;
|
|
293
|
-
|
|
294
245
|
// Whether any source actually reported subscription state. RETURN (SUBSCRIBED)
|
|
295
246
|
// and LSUB are the only two, and a server can refuse both
|
|
296
247
|
let subscriptionStateKnown = false;
|
|
297
|
-
|
|
298
248
|
// A server may also volunteer \Subscribed on a plain LIST, which normalizeFlags
|
|
299
249
|
// folds into the entry - that counts as the state having been reported
|
|
300
250
|
let anyEntrySubscribed = () => entries.some(entry => entry.subscribed);
|
|
301
|
-
|
|
302
251
|
let lastRejectedStage = null;
|
|
303
252
|
let auxRetryInserted = false;
|
|
304
253
|
for (let i = 0; i < stages.length; i++) {
|
|
@@ -322,12 +271,10 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
322
271
|
if (lastRejectedStage.status && !stage.status) {
|
|
323
272
|
connection.skipListStatusArgs = true;
|
|
324
273
|
}
|
|
325
|
-
if (
|
|
326
|
-
stageHasAuxArgs(lastRejectedStage) &&
|
|
274
|
+
if (stageHasAuxArgs(lastRejectedStage) &&
|
|
327
275
|
stage.aux === false &&
|
|
328
276
|
lastRejectedStage.status === stage.status &&
|
|
329
|
-
lastRejectedStage.subscribed === stage.subscribed
|
|
330
|
-
) {
|
|
277
|
+
lastRejectedStage.subscribed === stage.subscribed) {
|
|
331
278
|
// Same option groups, only the auxiliary args dropped - the
|
|
332
279
|
// auxiliaries are proven to be what the server rejects
|
|
333
280
|
connection.skipListAuxArgs = true;
|
|
@@ -336,7 +283,8 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
336
283
|
successStage = stage;
|
|
337
284
|
subscriptionStateKnown = !!stage.subscribed;
|
|
338
285
|
break;
|
|
339
|
-
}
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
340
288
|
if (i === stages.length - 1 || !isRejectedCommand(err)) {
|
|
341
289
|
throw err;
|
|
342
290
|
}
|
|
@@ -351,11 +299,9 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
351
299
|
connection.log.warn({ msg: 'LIST RETURN options rejected, retrying with reduced options', err, cid: connection.id });
|
|
352
300
|
}
|
|
353
301
|
}
|
|
354
|
-
|
|
355
302
|
if (options.listOnly) {
|
|
356
303
|
return entries;
|
|
357
304
|
}
|
|
358
|
-
|
|
359
305
|
// When listing with a namespace prefix (e.g., "INBOX."), INBOX itself may
|
|
360
306
|
// not appear in results. Run a separate LIST for INBOX to ensure it's included.
|
|
361
307
|
if (normalizedReference && !specialUseMatches['\\Inbox']) {
|
|
@@ -371,7 +317,8 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
371
317
|
}
|
|
372
318
|
try {
|
|
373
319
|
await runList('', 'INBOX', returnArgs);
|
|
374
|
-
}
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
375
322
|
// The main listing just succeeded with the same RETURN options, so a
|
|
376
323
|
// rejection here says nothing about the options themselves - retry
|
|
377
324
|
// this one call plain without latching any skip flags. Accepted edge:
|
|
@@ -384,7 +331,8 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
384
331
|
for (let type of Object.keys(specialUseMatches)) {
|
|
385
332
|
if (!(type in specialUseCountsBefore)) {
|
|
386
333
|
delete specialUseMatches[type];
|
|
387
|
-
}
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
388
336
|
specialUseMatches[type].length = specialUseCountsBefore[type];
|
|
389
337
|
}
|
|
390
338
|
}
|
|
@@ -392,7 +340,6 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
392
340
|
await runList('', 'INBOX', []);
|
|
393
341
|
}
|
|
394
342
|
}
|
|
395
|
-
|
|
396
343
|
// Attach STATUS data to each selectable mailbox. If LIST-STATUS was used,
|
|
397
344
|
// data is already in statusMap; otherwise, fall back to individual STATUS commands.
|
|
398
345
|
if (options.statusQuery) {
|
|
@@ -408,18 +355,19 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
408
355
|
if (syntheticRecent) {
|
|
409
356
|
entry.status.recent = 0;
|
|
410
357
|
}
|
|
411
|
-
}
|
|
358
|
+
}
|
|
359
|
+
else if (!statusMap.size) {
|
|
412
360
|
// Server didn't support LIST-STATUS; fall back to per-mailbox STATUS
|
|
413
361
|
try {
|
|
414
362
|
entry.status = await connection.run('STATUS', entry.path, options.statusQuery);
|
|
415
|
-
}
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
416
365
|
entry.status = { error: err };
|
|
417
366
|
}
|
|
418
367
|
}
|
|
419
368
|
}
|
|
420
369
|
}
|
|
421
370
|
}
|
|
422
|
-
|
|
423
371
|
// LSUB (RFC 3501 6.3.9): queries which mailboxes the user is subscribed to.
|
|
424
372
|
// We merge subscription info into the entries already collected from LIST.
|
|
425
373
|
// Subscribed-only mailboxes that weren't in LIST are intentionally ignored
|
|
@@ -427,31 +375,26 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
427
375
|
let runLsub = async () => {
|
|
428
376
|
let response = await connection.exec('LSUB', [encodePath(connection, normalizedReference), encodePath(connection, normalizedMailbox)], {
|
|
429
377
|
untagged: {
|
|
430
|
-
LSUB: async untagged => {
|
|
378
|
+
LSUB: async (untagged) => {
|
|
431
379
|
if (!untagged.attributes || !untagged.attributes.length) {
|
|
432
380
|
return;
|
|
433
381
|
}
|
|
434
|
-
|
|
435
382
|
let entry = {
|
|
436
|
-
path: normalizePath(connection, decodePath(connection, (untagged.attributes[2] && untagged.attributes[2].value) || '')),
|
|
437
|
-
pathAsListed: (untagged.attributes[2] && untagged.attributes[2].value) || '',
|
|
383
|
+
path: normalizePath(connection, decodePath(connection, ((untagged.attributes[2] && untagged.attributes[2].value) || ''))),
|
|
384
|
+
pathAsListed: ((untagged.attributes[2] && untagged.attributes[2].value) || ''),
|
|
438
385
|
flags: new Set(getStringList(untagged.attributes[0])),
|
|
439
|
-
delimiter: untagged.attributes[1] && untagged.attributes[1].value,
|
|
386
|
+
delimiter: (untagged.attributes[1] && untagged.attributes[1].value),
|
|
440
387
|
subscribed: true
|
|
441
388
|
};
|
|
442
|
-
|
|
443
389
|
if (entry.path.toUpperCase() === 'INBOX') {
|
|
444
390
|
addSpecialUseMatch(entry, '\\Inbox', 'name');
|
|
445
391
|
}
|
|
446
|
-
|
|
447
392
|
if (entry.delimiter && entry.path.charAt(0) === entry.delimiter) {
|
|
448
393
|
entry.path = entry.path.slice(1);
|
|
449
394
|
}
|
|
450
|
-
|
|
451
395
|
entry.parentPath = entry.delimiter && entry.path ? entry.path.substr(0, entry.path.lastIndexOf(entry.delimiter)) : '';
|
|
452
396
|
entry.parent = entry.delimiter ? entry.path.split(entry.delimiter) : [entry.path];
|
|
453
397
|
entry.name = entry.parent.pop();
|
|
454
|
-
|
|
455
398
|
// Merge LSUB data into existing LIST entry if found
|
|
456
399
|
let existing = entries.find(existing => existing.path === entry.path);
|
|
457
400
|
if (existing) {
|
|
@@ -466,7 +409,6 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
466
409
|
});
|
|
467
410
|
response.next();
|
|
468
411
|
};
|
|
469
|
-
|
|
470
412
|
// Never sent on a rev2 session - LSUB is not part of that protocol version, and
|
|
471
413
|
// some servers break the rest of the session over the rejection, so this is
|
|
472
414
|
// decided up front rather than left to the skipLsub latch below. On rev1 it is
|
|
@@ -485,12 +427,14 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
485
427
|
try {
|
|
486
428
|
await runLsub();
|
|
487
429
|
subscriptionStateKnown = true;
|
|
488
|
-
}
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
489
432
|
if (isRejectedCommand(err)) {
|
|
490
433
|
// Tagged BAD: the server does not implement LSUB despite advertising
|
|
491
434
|
// rev1 - skip it for the rest of this connection
|
|
492
435
|
connection.skipLsub = true;
|
|
493
|
-
}
|
|
436
|
+
}
|
|
437
|
+
else if (err.responseStatus !== 'NO' || err.code === 'ETHROTTLE') {
|
|
494
438
|
// Transport failures and throttling: rethrow, every follow-up
|
|
495
439
|
// command would fail too or the caller needs to back off
|
|
496
440
|
throw err;
|
|
@@ -500,7 +444,6 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
500
444
|
connection.log.warn({ msg: 'Failed to request subscription info', err, cid: connection.id });
|
|
501
445
|
}
|
|
502
446
|
}
|
|
503
|
-
|
|
504
447
|
// Resolve special-use conflicts: for each type, pick the best candidate
|
|
505
448
|
// based on source priority (user > extension > name), then alphabetically.
|
|
506
449
|
// Only the winning entry gets the specialUse property set.
|
|
@@ -513,14 +456,12 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
513
456
|
}
|
|
514
457
|
return aSource - bSource;
|
|
515
458
|
});
|
|
516
|
-
|
|
517
459
|
if (!sortedEntries[0].entry.specialUse) {
|
|
518
460
|
let source = sortedEntries[0].source;
|
|
519
461
|
sortedEntries[0].entry.specialUse = type;
|
|
520
462
|
sortedEntries[0].entry.specialUseSource = PUBLIC_SOURCE[source] || source;
|
|
521
463
|
}
|
|
522
464
|
}
|
|
523
|
-
|
|
524
465
|
// No source answered, so "not subscribed" was never actually reported for any of
|
|
525
466
|
// these folders - the state is unknown, not false. Reporting the whole listing as
|
|
526
467
|
// unsubscribed would hide every folder from a client that filters on subscription
|
|
@@ -535,13 +476,11 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
535
476
|
}
|
|
536
477
|
}
|
|
537
478
|
}
|
|
538
|
-
|
|
539
479
|
// INBOX should always appear as subscribed regardless of LSUB results
|
|
540
480
|
let inboxEntry = entries.find(entry => entry.specialUse === '\\Inbox');
|
|
541
481
|
if (inboxEntry && !inboxEntry.subscribed) {
|
|
542
482
|
inboxEntry.subscribed = true;
|
|
543
483
|
}
|
|
544
|
-
|
|
545
484
|
// Sort: special-use mailboxes first (in FLAG_SORT_ORDER), then alphabetically
|
|
546
485
|
// by path segments for a natural folder hierarchy ordering.
|
|
547
486
|
return entries.sort((a, b) => {
|
|
@@ -554,10 +493,8 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
554
493
|
if (a.specialUse && b.specialUse) {
|
|
555
494
|
return FLAG_SORT_ORDER.indexOf(a.specialUse) - FLAG_SORT_ORDER.indexOf(b.specialUse);
|
|
556
495
|
}
|
|
557
|
-
|
|
558
496
|
let aList = [].concat(a.parent).concat(a.name);
|
|
559
497
|
let bList = [].concat(b.parent).concat(b.name);
|
|
560
|
-
|
|
561
498
|
for (let i = 0; i < aList.length; i++) {
|
|
562
499
|
let aPart = aList[i];
|
|
563
500
|
let bPart = bList[i];
|
|
@@ -565,14 +502,14 @@ module.exports = async (connection, reference, mailbox, options) => {
|
|
|
565
502
|
return aPart.localeCompare(bPart || '');
|
|
566
503
|
}
|
|
567
504
|
}
|
|
568
|
-
|
|
569
505
|
return a.path.localeCompare(b.path);
|
|
570
506
|
});
|
|
571
|
-
}
|
|
507
|
+
}
|
|
508
|
+
catch (err) {
|
|
572
509
|
// Rewrite the parsed err.response into the response text and set
|
|
573
510
|
// serverResponseCode, same as the other command modules
|
|
574
511
|
await enhanceCommandError(err);
|
|
575
512
|
connection.log.warn({ msg: 'Failed to list folders', err, cid: connection.id });
|
|
576
513
|
throw err;
|
|
577
514
|
}
|
|
578
|
-
}
|
|
515
|
+
}
|
|
@@ -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
|
+
}
|