imapflow 1.7.8 → 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 +20 -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 +761 -1789
- 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
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = list;
|
|
4
|
+
const tools_js_1 = require("../tools.js");
|
|
5
|
+
const status_fields_js_1 = require("./status-fields.js");
|
|
6
|
+
const special_use_js_1 = require("../special-use.js");
|
|
7
|
+
/**
|
|
8
|
+
* Lists mailboxes from the server, including subscription status and special-use flags.
|
|
9
|
+
*
|
|
10
|
+
* @param connection - IMAP connection instance
|
|
11
|
+
* @param reference - Reference name (namespace prefix)
|
|
12
|
+
* @param mailbox - Mailbox name pattern with possible wildcards
|
|
13
|
+
* @param options - List options
|
|
14
|
+
* @param options.listOnly - If true, return entries after LIST without LSUB or status queries
|
|
15
|
+
* @param options.statusQuery - Status data items to query for each listed mailbox
|
|
16
|
+
* @param options.specialUseHints - Hints mapping mailbox paths to special-use types (sent, junk, trash, drafts, archive)
|
|
17
|
+
* @returns Array of mailbox entries sorted by special-use flags and name
|
|
18
|
+
* @throws If the LIST command fails
|
|
19
|
+
*/
|
|
20
|
+
async function list(connection, reference, mailbox, options) {
|
|
21
|
+
options = options || {};
|
|
22
|
+
// Special-use flags sorted by display priority (INBOX first, Trash last).
|
|
23
|
+
// Used in the final sort to group special-use mailboxes at the top of the list.
|
|
24
|
+
const FLAG_SORT_ORDER = ['\\Inbox', '\\Flagged', '\\Sent', '\\Drafts', '\\All', '\\Archive', '\\Junk', '\\Trash'];
|
|
25
|
+
// Priority for how a special-use flag was determined: explicit user hint > server
|
|
26
|
+
// extension flag (SPECIAL-USE/XLIST) > known localized name > relaxed name guess.
|
|
27
|
+
// When multiple mailboxes claim the same special-use type, the highest-priority
|
|
28
|
+
// source wins, so an exactly named folder beats one matched by a decorated or
|
|
29
|
+
// morphological variant of that name.
|
|
30
|
+
const SOURCE_SORT_ORDER = ['user', 'extension', 'name', 'name-guess'];
|
|
31
|
+
// "name-guess" is an internal precedence tier only. It is reported as "name" so
|
|
32
|
+
// that specialUseSource keeps its documented set of values for consumers.
|
|
33
|
+
const PUBLIC_SOURCE = { 'name-guess': 'name' };
|
|
34
|
+
const isNameSource = (source) => source === 'name' || source === 'name-guess';
|
|
35
|
+
// Prefer XLIST (legacy Gmail extension) only if SPECIAL-USE (RFC 6154) is unavailable.
|
|
36
|
+
// Both provide special-use flags, but SPECIAL-USE is the standardized approach.
|
|
37
|
+
// SPECIAL-USE is checked with rev2 folding - a rev2 session implies SPECIAL-USE,
|
|
38
|
+
// so LIST is preferred even if a rev2 server also advertised legacy XLIST.
|
|
39
|
+
let listCommand = connection.capabilities.has('XLIST') && !(0, tools_js_1.hasCapability)(connection, 'SPECIAL-USE') ? 'XLIST' : 'LIST';
|
|
40
|
+
try {
|
|
41
|
+
// Accumulators filled by the untagged LIST/STATUS handlers below. statusMap
|
|
42
|
+
// caches STATUS responses received inline via LIST-STATUS extension, keyed by
|
|
43
|
+
// normalized mailbox path (avoids separate STATUS commands per mailbox), and
|
|
44
|
+
// specialUseMatches tracks candidate mailboxes for each special-use type.
|
|
45
|
+
// (Re)initialized at the start of each retry stage of the main listing.
|
|
46
|
+
let entries = [];
|
|
47
|
+
let statusMap = new Map();
|
|
48
|
+
let specialUseMatches = {};
|
|
49
|
+
// STATUS data items to request (MESSAGES, UIDNEXT, etc.)
|
|
50
|
+
let statusQueryAttributes = (0, tools_js_1.buildStatusQueryAttributes)(connection, options.statusQuery);
|
|
51
|
+
// Extended LIST syntax (RETURN options) is understood by servers advertising
|
|
52
|
+
// LIST-EXTENDED (RFC 5258) or IMAP4rev2 (RFC 9051). Deliberately keyed on the
|
|
53
|
+
// advertisement alone (not hasCapability/isRev2Active): the staged retry below
|
|
54
|
+
// handles servers that advertise but reject RETURN options, so the wider gate
|
|
55
|
+
// is safe for anything it covers, while gates without a retry ladder stay
|
|
56
|
+
// conservative.
|
|
57
|
+
let supportsExtendedList = connection.capabilities.has('LIST-EXTENDED') || connection.capabilities.has('IMAP4rev2');
|
|
58
|
+
// RETURN options for the LIST command. Servers occasionally advertise the
|
|
59
|
+
// extensions but still reject RETURN options - the staged retry below then
|
|
60
|
+
// re-runs the LIST with fewer options and latches a skip flag for the option
|
|
61
|
+
// group the server proved to reject, keeping later listings efficient.
|
|
62
|
+
// LIST-STATUS (RFC 5819, folded into base IMAP4rev2): request STATUS data
|
|
63
|
+
// inline with LIST, avoiding a separate STATUS command for each mailbox.
|
|
64
|
+
let canRequestStatus = listCommand === 'LIST' && !connection.skipListStatusArgs && (0, tools_js_1.hasCapability)(connection, 'LIST-STATUS') && !!statusQueryAttributes.length;
|
|
65
|
+
// RETURN (SUBSCRIBED): request subscription state inline instead of a separate
|
|
66
|
+
// LSUB command. IMAP4rev2 removed LSUB entirely, and some servers (e.g.
|
|
67
|
+
// Exchange in IMAP4rev2 mode) reject it with BAD even while still advertising
|
|
68
|
+
// IMAP4rev1.
|
|
69
|
+
let canRequestSubscribed = listCommand === 'LIST' && !options.listOnly && !connection.skipListSubscribedArg && supportsExtendedList;
|
|
70
|
+
// Auxiliary RETURN options (SPECIAL-USE/CHILDREN) that ride along with the
|
|
71
|
+
// STATUS/SUBSCRIBED option groups. When RETURN options are present, servers
|
|
72
|
+
// may report only what was explicitly requested (verified against Dovecot
|
|
73
|
+
// 2.4: special-use and child attributes disappear from such responses), so
|
|
74
|
+
// request everything a plain LIST would have provided.
|
|
75
|
+
let auxArgsAvailable = (0, tools_js_1.hasCapability)(connection, 'SPECIAL-USE') || connection.capabilities.has('CHILDREN') || supportsExtendedList;
|
|
76
|
+
let stageHasAuxArgs = (stage) => (stage.status || stage.subscribed) && stage.aux !== false && !connection.skipListAuxArgs && auxArgsAvailable;
|
|
77
|
+
// Builds the RETURN (...) argument list for one retry stage
|
|
78
|
+
let buildListArgs = (stage) => {
|
|
79
|
+
let args = [];
|
|
80
|
+
if (stage.status) {
|
|
81
|
+
args.push({ type: 'ATOM', value: 'STATUS' }, statusQueryAttributes);
|
|
82
|
+
}
|
|
83
|
+
if (stageHasAuxArgs(stage)) {
|
|
84
|
+
if ((0, tools_js_1.hasCapability)(connection, 'SPECIAL-USE')) {
|
|
85
|
+
args.push({ type: 'ATOM', value: 'SPECIAL-USE' });
|
|
86
|
+
}
|
|
87
|
+
if (connection.capabilities.has('CHILDREN') || supportsExtendedList) {
|
|
88
|
+
args.push({ type: 'ATOM', value: 'CHILDREN' });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (stage.subscribed) {
|
|
92
|
+
args.push({ type: 'ATOM', value: 'SUBSCRIBED' });
|
|
93
|
+
}
|
|
94
|
+
return args;
|
|
95
|
+
};
|
|
96
|
+
// Multiple mailboxes may claim the same special-use type (e.g., \\Sent) via
|
|
97
|
+
// different sources (user hint, server extension, name match). After listing,
|
|
98
|
+
// the best match wins.
|
|
99
|
+
let addSpecialUseMatch = (entry, type, source) => {
|
|
100
|
+
if (!specialUseMatches[type]) {
|
|
101
|
+
specialUseMatches[type] = [];
|
|
102
|
+
}
|
|
103
|
+
specialUseMatches[type].push({ entry, source });
|
|
104
|
+
};
|
|
105
|
+
// RFC 5258: the \NonExistent attribute implies \Noselect. Some servers only
|
|
106
|
+
// return \NonExistent for phantom folders, so add \Noselect as well to keep
|
|
107
|
+
// the flags consistent for consumers that only check \Noselect.
|
|
108
|
+
// RETURN (SUBSCRIBED) - and some LSUB implementations - report subscription
|
|
109
|
+
// state as a \Subscribed attribute. Move it to the subscribed property so the
|
|
110
|
+
// output shape is the same however the state was delivered.
|
|
111
|
+
let normalizeFlags = (entry) => {
|
|
112
|
+
if (entry.flags.has('\\NonExistent')) {
|
|
113
|
+
entry.flags.add('\\Noselect');
|
|
114
|
+
}
|
|
115
|
+
if (entry.flags.has('\\Subscribed')) {
|
|
116
|
+
entry.flags.delete('\\Subscribed');
|
|
117
|
+
entry.subscribed = true;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
// User-provided hints map mailbox paths to special-use types (e.g., {sent: "Sent Items"}).
|
|
121
|
+
// These override server-reported flags and name-based guesses. Converted to a
|
|
122
|
+
// path-keyed lookup: { "Sent Items" => "\\Sent" }
|
|
123
|
+
// Keyed by server-supplied mailbox paths further down, so a null prototype keeps a
|
|
124
|
+
// path like "constructor" from resolving to an inherited member on lookup
|
|
125
|
+
let specialUseHints = Object.create(null);
|
|
126
|
+
if (options.specialUseHints && typeof options.specialUseHints === 'object') {
|
|
127
|
+
for (let type of Object.keys(options.specialUseHints)) {
|
|
128
|
+
if (['sent', 'junk', 'trash', 'drafts', 'archive'].includes(type) &&
|
|
129
|
+
options.specialUseHints[type] &&
|
|
130
|
+
typeof options.specialUseHints[type] === 'string') {
|
|
131
|
+
// Capitalize first letter: "sent" -> "\\Sent"
|
|
132
|
+
specialUseHints[(0, tools_js_1.normalizePath)(connection, options.specialUseHints[type])] = `\\${type.replace(/^./, c => c.toUpperCase())}`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Executes a LIST (or XLIST) command and collects mailbox entries.
|
|
137
|
+
// Called once for the main listing and optionally again for INBOX if a
|
|
138
|
+
// namespace prefix was used (INBOX may live outside the namespace).
|
|
139
|
+
let runList = async (reference, mailbox, returnArgs) => {
|
|
140
|
+
const cmdArgs = [(0, tools_js_1.encodePath)(connection, reference), (0, tools_js_1.encodePath)(connection, mailbox)];
|
|
141
|
+
if (returnArgs.length) {
|
|
142
|
+
cmdArgs.push({ type: 'ATOM', value: 'RETURN' }, returnArgs);
|
|
143
|
+
}
|
|
144
|
+
let response = await connection.exec(listCommand, cmdArgs, {
|
|
145
|
+
untagged: {
|
|
146
|
+
// Each untagged LIST response: * LIST (<flags>) "<delimiter>" "<mailbox name>"
|
|
147
|
+
// attributes[0] = flags array, attributes[1] = delimiter, attributes[2] = mailbox name
|
|
148
|
+
[listCommand]: async (untagged) => {
|
|
149
|
+
if (!untagged.attributes || !untagged.attributes.length) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
let entry = {
|
|
153
|
+
// Decode from modified UTF-7 wire format and normalize the path
|
|
154
|
+
path: (0, tools_js_1.normalizePath)(connection, (0, tools_js_1.decodePath)(connection, ((untagged.attributes[2] && untagged.attributes[2].value) || ''))),
|
|
155
|
+
pathAsListed: ((untagged.attributes[2] && untagged.attributes[2].value) || ''),
|
|
156
|
+
flags: new Set((0, tools_js_1.getStringList)(untagged.attributes[0])),
|
|
157
|
+
delimiter: (untagged.attributes[1] && untagged.attributes[1].value),
|
|
158
|
+
listed: true
|
|
159
|
+
};
|
|
160
|
+
normalizeFlags(entry);
|
|
161
|
+
// Check user-provided hints first (highest priority)
|
|
162
|
+
if (specialUseHints[entry.path]) {
|
|
163
|
+
addSpecialUseMatch(entry, specialUseHints[entry.path], 'user');
|
|
164
|
+
}
|
|
165
|
+
// XLIST marks INBOX with a \\Inbox flag. Remove it from flags
|
|
166
|
+
// (it's not a standard flag) and register as special-use match.
|
|
167
|
+
// XLIST may also use a localised name (e.g., "Posteingang" for German INBOX).
|
|
168
|
+
if (listCommand === 'XLIST' && entry.flags.has('\\Inbox')) {
|
|
169
|
+
entry.flags.delete('\\Inbox');
|
|
170
|
+
if (entry.path !== 'INBOX') {
|
|
171
|
+
addSpecialUseMatch(entry, '\\Inbox', 'extension');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Name-based INBOX detection: any mailbox named "INBOX" (case-insensitive)
|
|
175
|
+
// is the inbox per RFC 3501. Phantom \NonExistent entries (subscribed
|
|
176
|
+
// leftovers of deleted mailboxes) must not claim the slot by name.
|
|
177
|
+
if (entry.path.toUpperCase() === 'INBOX' && !entry.flags.has('\\NonExistent')) {
|
|
178
|
+
addSpecialUseMatch(entry, '\\Inbox', 'name');
|
|
179
|
+
}
|
|
180
|
+
// Strip leading delimiter (some servers prepend it to paths)
|
|
181
|
+
if (entry.delimiter && entry.path.charAt(0) === entry.delimiter) {
|
|
182
|
+
entry.path = entry.path.slice(1);
|
|
183
|
+
}
|
|
184
|
+
// Build parent path hierarchy for tree construction and sorting
|
|
185
|
+
entry.parentPath = entry.delimiter && entry.path ? entry.path.substr(0, entry.path.lastIndexOf(entry.delimiter)) : '';
|
|
186
|
+
entry.parent = entry.delimiter ? entry.path.split(entry.delimiter) : [entry.path];
|
|
187
|
+
entry.name = entry.parent.pop();
|
|
188
|
+
// Try to detect special-use from server flags or well-known names
|
|
189
|
+
// (e.g., "Sent", "Drafts", "Junk", "Trash")
|
|
190
|
+
let { flag: specialUseFlag, source: flagSource } = (0, special_use_js_1.specialUse)(connection.capabilities.has('XLIST') || (0, tools_js_1.hasCapability)(connection, 'SPECIAL-USE'), entry);
|
|
191
|
+
// A name-based match for a \NonExistent phantom entry could win the
|
|
192
|
+
// special-use slot over the real folder - only server-provided flags
|
|
193
|
+
// are trusted for nonexistent entries. Covers every name-derived
|
|
194
|
+
// source, exact and relaxed alike.
|
|
195
|
+
if (specialUseFlag && (!isNameSource(flagSource) || !entry.flags.has('\\NonExistent'))) {
|
|
196
|
+
addSpecialUseMatch(entry, specialUseFlag, flagSource);
|
|
197
|
+
}
|
|
198
|
+
entries.push(entry);
|
|
199
|
+
},
|
|
200
|
+
// Inline STATUS response from LIST-STATUS extension (RFC 5819).
|
|
201
|
+
// Parses alternating key-value pairs (i % 2 pattern).
|
|
202
|
+
STATUS: async (untagged) => {
|
|
203
|
+
let statusPath = (0, tools_js_1.normalizePath)(connection, (0, tools_js_1.decodePath)(connection, ((untagged.attributes[0] && untagged.attributes[0].value) || '')));
|
|
204
|
+
let statusList = untagged.attributes && Array.isArray(untagged.attributes[1]) ? untagged.attributes[1] : false;
|
|
205
|
+
if (!statusList || !statusPath) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
let map = { path: statusPath };
|
|
209
|
+
(0, status_fields_js_1.parseStatusList)(statusList, (key, value) => {
|
|
210
|
+
map[key] = value;
|
|
211
|
+
});
|
|
212
|
+
statusMap.set(statusPath, map);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
response.next();
|
|
217
|
+
};
|
|
218
|
+
let normalizedReference = (0, tools_js_1.normalizePath)(connection, reference || '');
|
|
219
|
+
let normalizedMailbox = (0, tools_js_1.normalizePath)(connection, mailbox || '', true);
|
|
220
|
+
// Retry stages for the main listing: start with all applicable RETURN options
|
|
221
|
+
// and drop one option group per retry. Consecutive stages differ by exactly one
|
|
222
|
+
// group, so a success right after a rejection identifies the offending group
|
|
223
|
+
// and only that group's skip flag is latched for the rest of the connection.
|
|
224
|
+
// When a stage carrying the auxiliary SPECIAL-USE/CHILDREN options is rejected,
|
|
225
|
+
// a copy of the same stage without them is inserted first (once per listing),
|
|
226
|
+
// so an auxiliary-only rejection does not get a whole option group blamed.
|
|
227
|
+
let stages = [];
|
|
228
|
+
if (canRequestStatus && canRequestSubscribed) {
|
|
229
|
+
stages.push({ status: true, subscribed: true });
|
|
230
|
+
}
|
|
231
|
+
if (canRequestStatus) {
|
|
232
|
+
stages.push({ status: true, subscribed: false });
|
|
233
|
+
}
|
|
234
|
+
else if (canRequestSubscribed) {
|
|
235
|
+
stages.push({ status: false, subscribed: true });
|
|
236
|
+
}
|
|
237
|
+
stages.push({ status: false, subscribed: false });
|
|
238
|
+
// A tagged BAD is how servers reject unrecognized RETURN options (RFC 9051
|
|
239
|
+
// section 6.3.9). A tagged NO is an operational failure, and throttling
|
|
240
|
+
// errors (code ETHROTTLE) also surface with a BAD status - neither says
|
|
241
|
+
// anything about the RETURN options, so they propagate to the caller.
|
|
242
|
+
let isRejectedCommand = (err) => err.responseStatus === 'BAD' && err.code !== 'ETHROTTLE';
|
|
243
|
+
// Stage of the successful attempt - reused by the INBOX fixup and the LSUB
|
|
244
|
+
// decision below
|
|
245
|
+
let successStage = null;
|
|
246
|
+
// Whether any source actually reported subscription state. RETURN (SUBSCRIBED)
|
|
247
|
+
// and LSUB are the only two, and a server can refuse both
|
|
248
|
+
let subscriptionStateKnown = false;
|
|
249
|
+
// A server may also volunteer \Subscribed on a plain LIST, which normalizeFlags
|
|
250
|
+
// folds into the entry - that counts as the state having been reported
|
|
251
|
+
let anyEntrySubscribed = () => entries.some(entry => entry.subscribed);
|
|
252
|
+
let lastRejectedStage = null;
|
|
253
|
+
let auxRetryInserted = false;
|
|
254
|
+
for (let i = 0; i < stages.length; i++) {
|
|
255
|
+
let stage = stages[i];
|
|
256
|
+
let stageArgs = buildListArgs(stage);
|
|
257
|
+
// Discard partial results from a rejected attempt
|
|
258
|
+
entries = [];
|
|
259
|
+
statusMap = new Map();
|
|
260
|
+
specialUseMatches = {};
|
|
261
|
+
try {
|
|
262
|
+
await runList(normalizedReference, normalizedMailbox, stageArgs);
|
|
263
|
+
if (lastRejectedStage) {
|
|
264
|
+
// Latch only the option group that was present in the rejected
|
|
265
|
+
// attempt but missing from this successful one - that group is
|
|
266
|
+
// proven to be what the server rejects. An unproven group (e.g.
|
|
267
|
+
// SUBSCRIBED when both groups were dropped one by one) is decided
|
|
268
|
+
// by the reduced stage list of the next listing.
|
|
269
|
+
if (lastRejectedStage.subscribed && !stage.subscribed) {
|
|
270
|
+
connection.skipListSubscribedArg = true;
|
|
271
|
+
}
|
|
272
|
+
if (lastRejectedStage.status && !stage.status) {
|
|
273
|
+
connection.skipListStatusArgs = true;
|
|
274
|
+
}
|
|
275
|
+
if (stageHasAuxArgs(lastRejectedStage) &&
|
|
276
|
+
stage.aux === false &&
|
|
277
|
+
lastRejectedStage.status === stage.status &&
|
|
278
|
+
lastRejectedStage.subscribed === stage.subscribed) {
|
|
279
|
+
// Same option groups, only the auxiliary args dropped - the
|
|
280
|
+
// auxiliaries are proven to be what the server rejects
|
|
281
|
+
connection.skipListAuxArgs = true;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
successStage = stage;
|
|
285
|
+
subscriptionStateKnown = !!stage.subscribed;
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
if (i === stages.length - 1 || !isRejectedCommand(err)) {
|
|
290
|
+
throw err;
|
|
291
|
+
}
|
|
292
|
+
lastRejectedStage = stage;
|
|
293
|
+
if (!auxRetryInserted && stageHasAuxArgs(stage)) {
|
|
294
|
+
// The rejection may be about the auxiliary options rather than the
|
|
295
|
+
// option groups - try the same groups without the auxiliaries before
|
|
296
|
+
// dropping a group
|
|
297
|
+
stages.splice(i + 1, 0, { ...stage, aux: false });
|
|
298
|
+
auxRetryInserted = true;
|
|
299
|
+
}
|
|
300
|
+
connection.log.warn({ msg: 'LIST RETURN options rejected, retrying with reduced options', err, cid: connection.id });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (options.listOnly) {
|
|
304
|
+
return entries;
|
|
305
|
+
}
|
|
306
|
+
// When listing with a namespace prefix (e.g., "INBOX."), INBOX itself may
|
|
307
|
+
// not appear in results. Run a separate LIST for INBOX to ensure it's included.
|
|
308
|
+
if (normalizedReference && !specialUseMatches['\\Inbox']) {
|
|
309
|
+
let returnArgs = buildListArgs(successStage);
|
|
310
|
+
// Snapshot the accumulator sizes: a rejected fixup attempt may have
|
|
311
|
+
// streamed partial untagged responses before its tagged BAD, and those
|
|
312
|
+
// must be discarded before the retry or INBOX would be listed twice -
|
|
313
|
+
// while the main run's results must be kept
|
|
314
|
+
let entryCountBefore = entries.length;
|
|
315
|
+
let specialUseCountsBefore = {};
|
|
316
|
+
for (let type of Object.keys(specialUseMatches)) {
|
|
317
|
+
specialUseCountsBefore[type] = specialUseMatches[type].length;
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
await runList('', 'INBOX', returnArgs);
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
// The main listing just succeeded with the same RETURN options, so a
|
|
324
|
+
// rejection here says nothing about the options themselves - retry
|
|
325
|
+
// this one call plain without latching any skip flags. Accepted edge:
|
|
326
|
+
// if the main run filled statusMap, INBOX ends up without inline
|
|
327
|
+
// status data.
|
|
328
|
+
if (!returnArgs.length || !isRejectedCommand(err)) {
|
|
329
|
+
throw err;
|
|
330
|
+
}
|
|
331
|
+
entries.length = entryCountBefore;
|
|
332
|
+
for (let type of Object.keys(specialUseMatches)) {
|
|
333
|
+
if (!(type in specialUseCountsBefore)) {
|
|
334
|
+
delete specialUseMatches[type];
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
specialUseMatches[type].length = specialUseCountsBefore[type];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
connection.log.warn({ msg: 'INBOX LIST with RETURN options failed, retrying plain', err, cid: connection.id });
|
|
341
|
+
await runList('', 'INBOX', []);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// Attach STATUS data to each selectable mailbox. If LIST-STATUS was used,
|
|
345
|
+
// data is already in statusMap; otherwise, fall back to individual STATUS commands.
|
|
346
|
+
if (options.statusQuery) {
|
|
347
|
+
// RECENT does not exist in IMAP4rev2, so it is never requested from a rev2
|
|
348
|
+
// session - its defined value there is always 0 (the STATUS command module
|
|
349
|
+
// applies the same rule on the per-mailbox fallback path)
|
|
350
|
+
let syntheticRecent = options.statusQuery.recent && (0, tools_js_1.isRev2Active)(connection);
|
|
351
|
+
for (let entry of entries) {
|
|
352
|
+
// \\Noselect and \\NonExistent mailboxes cannot hold messages
|
|
353
|
+
if (!entry.flags.has('\\Noselect') && !entry.flags.has('\\NonExistent')) {
|
|
354
|
+
if (statusMap.has(entry.path)) {
|
|
355
|
+
entry.status = statusMap.get(entry.path);
|
|
356
|
+
if (syntheticRecent) {
|
|
357
|
+
entry.status.recent = 0;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else if (!statusMap.size) {
|
|
361
|
+
// Server didn't support LIST-STATUS; fall back to per-mailbox STATUS
|
|
362
|
+
try {
|
|
363
|
+
entry.status = await connection.run('STATUS', entry.path, options.statusQuery);
|
|
364
|
+
}
|
|
365
|
+
catch (err) {
|
|
366
|
+
entry.status = { error: err };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
// LSUB (RFC 3501 6.3.9): queries which mailboxes the user is subscribed to.
|
|
373
|
+
// We merge subscription info into the entries already collected from LIST.
|
|
374
|
+
// Subscribed-only mailboxes that weren't in LIST are intentionally ignored
|
|
375
|
+
// (they may be phantom entries from old subscriptions to deleted mailboxes).
|
|
376
|
+
let runLsub = async () => {
|
|
377
|
+
let response = await connection.exec('LSUB', [(0, tools_js_1.encodePath)(connection, normalizedReference), (0, tools_js_1.encodePath)(connection, normalizedMailbox)], {
|
|
378
|
+
untagged: {
|
|
379
|
+
LSUB: async (untagged) => {
|
|
380
|
+
if (!untagged.attributes || !untagged.attributes.length) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
let entry = {
|
|
384
|
+
path: (0, tools_js_1.normalizePath)(connection, (0, tools_js_1.decodePath)(connection, ((untagged.attributes[2] && untagged.attributes[2].value) || ''))),
|
|
385
|
+
pathAsListed: ((untagged.attributes[2] && untagged.attributes[2].value) || ''),
|
|
386
|
+
flags: new Set((0, tools_js_1.getStringList)(untagged.attributes[0])),
|
|
387
|
+
delimiter: (untagged.attributes[1] && untagged.attributes[1].value),
|
|
388
|
+
subscribed: true
|
|
389
|
+
};
|
|
390
|
+
if (entry.path.toUpperCase() === 'INBOX') {
|
|
391
|
+
addSpecialUseMatch(entry, '\\Inbox', 'name');
|
|
392
|
+
}
|
|
393
|
+
if (entry.delimiter && entry.path.charAt(0) === entry.delimiter) {
|
|
394
|
+
entry.path = entry.path.slice(1);
|
|
395
|
+
}
|
|
396
|
+
entry.parentPath = entry.delimiter && entry.path ? entry.path.substr(0, entry.path.lastIndexOf(entry.delimiter)) : '';
|
|
397
|
+
entry.parent = entry.delimiter ? entry.path.split(entry.delimiter) : [entry.path];
|
|
398
|
+
entry.name = entry.parent.pop();
|
|
399
|
+
// Merge LSUB data into existing LIST entry if found
|
|
400
|
+
let existing = entries.find(existing => existing.path === entry.path);
|
|
401
|
+
if (existing) {
|
|
402
|
+
existing.subscribed = true;
|
|
403
|
+
// Merge any additional flags from LSUB into the LIST entry
|
|
404
|
+
entry.flags.forEach(flag => existing.flags.add(flag));
|
|
405
|
+
normalizeFlags(existing);
|
|
406
|
+
}
|
|
407
|
+
// Non-listed subscribed folders are intentionally ignored
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
response.next();
|
|
412
|
+
};
|
|
413
|
+
// Never sent on a rev2 session - LSUB is not part of that protocol version, and
|
|
414
|
+
// some servers break the rest of the session over the rejection, so this is
|
|
415
|
+
// decided up front rather than left to the skipLsub latch below. On rev1 it is
|
|
416
|
+
// skipped when RETURN (SUBSCRIBED) already answered. Safety net: if the extended
|
|
417
|
+
// LIST was accepted but not a single mailbox came back subscribed, assume the
|
|
418
|
+
// server silently ignored the option and fall back to LSUB anyway (an account
|
|
419
|
+
// with no subscriptions legitimately looks the same).
|
|
420
|
+
let needsLsub = !(0, tools_js_1.isRev2Active)(connection) && (!successStage.subscribed || !anyEntrySubscribed());
|
|
421
|
+
if (needsLsub) {
|
|
422
|
+
// Reaching here means the listing did not settle the question after all -
|
|
423
|
+
// either no RETURN (SUBSCRIBED) was granted, or one was and the server
|
|
424
|
+
// ignored it. Only LSUB can answer now
|
|
425
|
+
subscriptionStateKnown = false;
|
|
426
|
+
}
|
|
427
|
+
if (needsLsub && !connection.skipLsub) {
|
|
428
|
+
try {
|
|
429
|
+
await runLsub();
|
|
430
|
+
subscriptionStateKnown = true;
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
if (isRejectedCommand(err)) {
|
|
434
|
+
// Tagged BAD: the server does not implement LSUB despite advertising
|
|
435
|
+
// rev1 - skip it for the rest of this connection
|
|
436
|
+
connection.skipLsub = true;
|
|
437
|
+
}
|
|
438
|
+
else if (err.responseStatus !== 'NO' || err.code === 'ETHROTTLE') {
|
|
439
|
+
// Transport failures and throttling: rethrow, every follow-up
|
|
440
|
+
// command would fail too or the caller needs to back off
|
|
441
|
+
throw err;
|
|
442
|
+
}
|
|
443
|
+
// Subscription state is auxiliary - keep the LIST results usable. A
|
|
444
|
+
// tagged NO is treated as transient, so the next listing tries again.
|
|
445
|
+
connection.log.warn({ msg: 'Failed to request subscription info', err, cid: connection.id });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// Resolve special-use conflicts: for each type, pick the best candidate
|
|
449
|
+
// based on source priority (user > extension > name), then alphabetically.
|
|
450
|
+
// Only the winning entry gets the specialUse property set.
|
|
451
|
+
for (let type of Object.keys(specialUseMatches)) {
|
|
452
|
+
let sortedEntries = specialUseMatches[type].sort((a, b) => {
|
|
453
|
+
let aSource = SOURCE_SORT_ORDER.indexOf(a.source);
|
|
454
|
+
let bSource = SOURCE_SORT_ORDER.indexOf(b.source);
|
|
455
|
+
if (aSource === bSource) {
|
|
456
|
+
return a.entry.path.localeCompare(b.entry.path);
|
|
457
|
+
}
|
|
458
|
+
return aSource - bSource;
|
|
459
|
+
});
|
|
460
|
+
if (!sortedEntries[0].entry.specialUse) {
|
|
461
|
+
let source = sortedEntries[0].source;
|
|
462
|
+
sortedEntries[0].entry.specialUse = type;
|
|
463
|
+
sortedEntries[0].entry.specialUseSource = PUBLIC_SOURCE[source] || source;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
// No source answered, so "not subscribed" was never actually reported for any of
|
|
467
|
+
// these folders - the state is unknown, not false. Reporting the whole listing as
|
|
468
|
+
// unsubscribed would hide every folder from a client that filters on subscription
|
|
469
|
+
// state, so assume subscribed instead. Phantom entries are excluded, the same way
|
|
470
|
+
// the rest of this file declines to trust them. NB! a transient LSUB NO lands here
|
|
471
|
+
// too, so the assumption can hold for one listing and be replaced by real state on
|
|
472
|
+
// the next
|
|
473
|
+
if (!subscriptionStateKnown && !anyEntrySubscribed()) {
|
|
474
|
+
for (let entry of entries) {
|
|
475
|
+
if (!entry.flags.has('\\NonExistent')) {
|
|
476
|
+
entry.subscribed = true;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// INBOX should always appear as subscribed regardless of LSUB results
|
|
481
|
+
let inboxEntry = entries.find(entry => entry.specialUse === '\\Inbox');
|
|
482
|
+
if (inboxEntry && !inboxEntry.subscribed) {
|
|
483
|
+
inboxEntry.subscribed = true;
|
|
484
|
+
}
|
|
485
|
+
// Sort: special-use mailboxes first (in FLAG_SORT_ORDER), then alphabetically
|
|
486
|
+
// by path segments for a natural folder hierarchy ordering.
|
|
487
|
+
return entries.sort((a, b) => {
|
|
488
|
+
if (a.specialUse && !b.specialUse) {
|
|
489
|
+
return -1;
|
|
490
|
+
}
|
|
491
|
+
if (!a.specialUse && b.specialUse) {
|
|
492
|
+
return 1;
|
|
493
|
+
}
|
|
494
|
+
if (a.specialUse && b.specialUse) {
|
|
495
|
+
return FLAG_SORT_ORDER.indexOf(a.specialUse) - FLAG_SORT_ORDER.indexOf(b.specialUse);
|
|
496
|
+
}
|
|
497
|
+
let aList = [].concat(a.parent).concat(a.name);
|
|
498
|
+
let bList = [].concat(b.parent).concat(b.name);
|
|
499
|
+
for (let i = 0; i < aList.length; i++) {
|
|
500
|
+
let aPart = aList[i];
|
|
501
|
+
let bPart = bList[i];
|
|
502
|
+
if (aPart !== bPart) {
|
|
503
|
+
return aPart.localeCompare(bPart || '');
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return a.path.localeCompare(b.path);
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
catch (err) {
|
|
510
|
+
// Rewrite the parsed err.response into the response text and set
|
|
511
|
+
// serverResponseCode, same as the other command modules
|
|
512
|
+
await (0, tools_js_1.enhanceCommandError)(err);
|
|
513
|
+
connection.log.warn({ msg: 'Failed to list folders', err, cid: connection.id });
|
|
514
|
+
throw err;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
module.exports = exports.default;
|
|
518
|
+
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
|
|
@@ -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>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = login;
|
|
4
|
+
const tools_js_1 = require("../tools.js");
|
|
5
|
+
/**
|
|
6
|
+
* Authenticates user using the IMAP LOGIN command.
|
|
7
|
+
*
|
|
8
|
+
* @param connection - IMAP connection instance
|
|
9
|
+
* @param username - The username to authenticate with
|
|
10
|
+
* @param password - The password to authenticate with
|
|
11
|
+
* @returns The authenticated username, or undefined if already authenticated
|
|
12
|
+
* @throws If authentication fails, with authenticationFailed and serverResponseCode properties set
|
|
13
|
+
*/
|
|
14
|
+
async function login(connection, username, password) {
|
|
15
|
+
if (connection.state !== connection.states.NOT_AUTHENTICATED) {
|
|
16
|
+
// nothing to do here
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
let response = await connection.exec('LOGIN', [
|
|
21
|
+
{ type: 'STRING', value: username },
|
|
22
|
+
// sensitive: true prevents the password from appearing in debug logs
|
|
23
|
+
{ type: 'STRING', value: password, sensitive: true }
|
|
24
|
+
]);
|
|
25
|
+
response.next();
|
|
26
|
+
// Record that LOGIN was the method used, so the connection knows which
|
|
27
|
+
// auth mechanism succeeded (used for reconnection and diagnostics).
|
|
28
|
+
connection.authCapabilities.set('LOGIN', true);
|
|
29
|
+
return username;
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
let errorCode = (0, tools_js_1.getStatusCode)(err.response);
|
|
33
|
+
if (errorCode) {
|
|
34
|
+
err.serverResponseCode = errorCode;
|
|
35
|
+
}
|
|
36
|
+
err.authenticationFailed = true;
|
|
37
|
+
err.response = await (0, tools_js_1.getErrorText)(err.response);
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
module.exports = exports.default;
|
|
42
|
+
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
|
|
@@ -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>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = logout;
|
|
4
|
+
/**
|
|
5
|
+
* Logs out the user and closes the connection.
|
|
6
|
+
*
|
|
7
|
+
* @param connection - IMAP connection instance
|
|
8
|
+
* @returns True if logout command succeeded, false otherwise
|
|
9
|
+
*/
|
|
10
|
+
async function logout(connection) {
|
|
11
|
+
if (connection.state === connection.states.LOGOUT) {
|
|
12
|
+
// nothing to do here
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
if (connection.state === connection.states.NOT_AUTHENTICATED) {
|
|
16
|
+
// Not yet authenticated, no LOGOUT command needed; just close the socket.
|
|
17
|
+
connection.state = connection.states.LOGOUT;
|
|
18
|
+
connection.close();
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
let response;
|
|
22
|
+
try {
|
|
23
|
+
response = await connection.exec('LOGOUT');
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
// If the connection is already gone, treat as successful logout
|
|
28
|
+
if (err.code === 'NoConnection') {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
connection.log.warn({ err, cid: connection.id });
|
|
32
|
+
return false;
|
|
33
|
+
/* c8 ignore next */ // the catch above is exhaustive (never re-throws), so finally is only ever reached via normal completion
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
// Set state to LOGOUT before closing to prevent any further commands from
|
|
37
|
+
// being queued. The socket is closed unconditionally in this finally block
|
|
38
|
+
// regardless of whether the LOGOUT command succeeded or failed.
|
|
39
|
+
connection.state = connection.states.LOGOUT;
|
|
40
|
+
if (response && typeof response.next === 'function') {
|
|
41
|
+
response.next();
|
|
42
|
+
}
|
|
43
|
+
connection.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
module.exports = exports.default;
|
|
47
|
+
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ImapFlow } from '../imap-flow.js';
|
|
2
|
+
import type { CopyResponseObject, MessageRangeOptions } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Moves messages from the current mailbox to another mailbox.
|
|
5
|
+
*
|
|
6
|
+
* @param connection - IMAP connection instance
|
|
7
|
+
* @param range - Message sequence number or UID range
|
|
8
|
+
* @param destination - Destination mailbox path
|
|
9
|
+
* @param options - Move options
|
|
10
|
+
* @param options.uid - If true, use UID MOVE instead of MOVE
|
|
11
|
+
* @returns Move result with UID mapping if available, false on failure, or undefined if preconditions not met
|
|
12
|
+
*/
|
|
13
|
+
export default function move(connection: ImapFlow, range: string, destination: string | string[], options?: MessageRangeOptions | undefined): Promise<CopyResponseObject | false | undefined>;
|