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,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = search;
|
|
4
|
+
const tools_js_1 = require("../tools.js");
|
|
5
|
+
const search_compiler_js_1 = require("../search-compiler.js");
|
|
6
|
+
const esearch_parser_js_1 = require("./esearch-parser.js");
|
|
7
|
+
/**
|
|
8
|
+
* Strips the leading (TAG "X") correlator list and the optional UID atom from an
|
|
9
|
+
* ESEARCH untagged response, leaving only the result keyword/value pairs.
|
|
10
|
+
* The IMAP parser represents parenthesized groups as plain Arrays, not objects
|
|
11
|
+
* with type: 'LIST'.
|
|
12
|
+
*
|
|
13
|
+
* @param attrs - Raw attribute array from the IMAP parser
|
|
14
|
+
* @returns Attribute array starting at the first result keyword
|
|
15
|
+
*/
|
|
16
|
+
const stripEsearchPrefix = (attrs) => {
|
|
17
|
+
let start = 0;
|
|
18
|
+
if (attrs[start] && Array.isArray(attrs[start]))
|
|
19
|
+
start++;
|
|
20
|
+
if (attrs[start] && typeof attrs[start].value === 'string' && attrs[start].value.toUpperCase() === 'UID')
|
|
21
|
+
start++;
|
|
22
|
+
return attrs.slice(start);
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Searches for messages matching the specified criteria.
|
|
26
|
+
*
|
|
27
|
+
* @param connection - IMAP connection instance
|
|
28
|
+
* @param query - Search query object, or true/empty object to match all messages
|
|
29
|
+
* @param options - Search options
|
|
30
|
+
* @param options.uid - If true, use UID SEARCH instead of SEARCH
|
|
31
|
+
* @param options.returnOptions - ESEARCH RETURN options. When present AND the
|
|
32
|
+
* server advertises ESEARCH capability, triggers ESEARCH and returns an ESearchResult.
|
|
33
|
+
* Items are strings ('MIN','MAX','COUNT','ALL') or objects ({ partial: '1:100' }).
|
|
34
|
+
* When server lacks ESEARCH, falls back to plain SEARCH and returns number[].
|
|
35
|
+
*/
|
|
36
|
+
async function search(connection, query, options) {
|
|
37
|
+
if (connection.state !== connection.states.SELECTED) {
|
|
38
|
+
// nothing to do here
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
options = options || {};
|
|
42
|
+
let attributes;
|
|
43
|
+
// Three query branches:
|
|
44
|
+
// 1. Empty/truthy/all-only query -> use IMAP "SEARCH ALL" to match every message
|
|
45
|
+
// 2. Non-empty object -> compile into IMAP SEARCH criteria via searchCompiler
|
|
46
|
+
// 3. Anything else (unexpected type) -> bail out with false
|
|
47
|
+
if (!query || query === true || (typeof query === 'object' && (!Object.keys(query).length || (Object.keys(query).length === 1 && query.all)))) {
|
|
48
|
+
// search for all messages
|
|
49
|
+
attributes = [{ type: 'ATOM', value: 'ALL' }];
|
|
50
|
+
}
|
|
51
|
+
else if (query && typeof query === 'object') {
|
|
52
|
+
// normal query
|
|
53
|
+
attributes = (0, search_compiler_js_1.searchCompiler)(connection, query);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
// ESEARCH is part of base IMAP4rev2
|
|
59
|
+
const useEsearch = options.returnOptions && options.returnOptions.length > 0 && (0, tools_js_1.hasCapability)(connection, 'ESEARCH');
|
|
60
|
+
if (useEsearch) {
|
|
61
|
+
// Build RETURN (...) item list
|
|
62
|
+
const returnItems = [];
|
|
63
|
+
for (const opt of options.returnOptions) {
|
|
64
|
+
if (typeof opt === 'string') {
|
|
65
|
+
returnItems.push({ type: 'ATOM', value: opt.toUpperCase() });
|
|
66
|
+
}
|
|
67
|
+
else if (opt && typeof opt.partial === 'string') {
|
|
68
|
+
// RFC 9394: PARTIAL is an atom followed by the range atom, both inside RETURN (...)
|
|
69
|
+
returnItems.push({ type: 'ATOM', value: 'PARTIAL' });
|
|
70
|
+
returnItems.push({ type: 'ATOM', value: opt.partial });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// If all returnOptions entries were invalid (e.g. objects lacking a string
|
|
74
|
+
// `partial` field), returnItems would be empty. Emitting "RETURN ()" is
|
|
75
|
+
// technically valid per RFC 4731 but returns nothing useful. Fall through
|
|
76
|
+
// to the legacy SEARCH path instead so the caller gets a usable result.
|
|
77
|
+
if (returnItems.length > 0) {
|
|
78
|
+
const returnClause = [{ type: 'ATOM', value: 'RETURN' }, returnItems];
|
|
79
|
+
let esearchResult = {};
|
|
80
|
+
let response;
|
|
81
|
+
try {
|
|
82
|
+
response = await connection.exec(options.uid ? 'UID SEARCH' : 'SEARCH', [...returnClause, ...attributes], {
|
|
83
|
+
untagged: {
|
|
84
|
+
ESEARCH: async (untagged) => {
|
|
85
|
+
if (!untagged || !untagged.attributes)
|
|
86
|
+
return;
|
|
87
|
+
esearchResult = (0, esearch_parser_js_1.parseEsearchResponse)(stripEsearchPrefix(untagged.attributes));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
response.next();
|
|
92
|
+
return esearchResult;
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
await (0, tools_js_1.enhanceCommandError)(err);
|
|
96
|
+
connection.log.warn({ err, cid: connection.id });
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// returnItems was empty - fall through to legacy SEARCH path below
|
|
101
|
+
}
|
|
102
|
+
// Legacy SEARCH path (no returnOptions, or server lacks ESEARCH)
|
|
103
|
+
// Use a Set to deduplicate sequence numbers/UIDs - servers may return
|
|
104
|
+
// duplicates across multiple untagged SEARCH responses.
|
|
105
|
+
let results = new Set();
|
|
106
|
+
let response;
|
|
107
|
+
try {
|
|
108
|
+
response = await connection.exec(options.uid ? 'UID SEARCH' : 'SEARCH', attributes, {
|
|
109
|
+
untagged: {
|
|
110
|
+
SEARCH: async (untagged) => {
|
|
111
|
+
if (untagged && untagged.attributes && untagged.attributes.length) {
|
|
112
|
+
let truncated = false;
|
|
113
|
+
let discarded = false;
|
|
114
|
+
for (let attribute of untagged.attributes) {
|
|
115
|
+
// The result set is server-controlled and accumulated across
|
|
116
|
+
// responses, so stop at the same absolute ceiling expandRange()
|
|
117
|
+
// uses - a server streaming SEARCH responses could otherwise
|
|
118
|
+
// grow the set until the process runs out of memory
|
|
119
|
+
/* c8 ignore next 4 */ // reaching the ceiling needs 2^24 accumulated results, which no unit test can produce in reasonable time
|
|
120
|
+
if (results.size >= tools_js_1.EXPANDED_RANGE_LIMIT) {
|
|
121
|
+
truncated = true;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
// Same nz-number check the ESEARCH branch below applies. isNaN()
|
|
125
|
+
// is not enough: it passes '1e400' (Infinity), '-3' and '2.5', and
|
|
126
|
+
// a single one of those makes the sequence set compiled from this
|
|
127
|
+
// result set invalid, failing the caller's whole follow-up command
|
|
128
|
+
let value = attribute && typeof attribute.value === 'string' ? Number(attribute.value) : NaN;
|
|
129
|
+
if (!(0, tools_js_1.isValidSequenceValue)(value)) {
|
|
130
|
+
discarded = true;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
results.add(value);
|
|
134
|
+
}
|
|
135
|
+
if (truncated || discarded) {
|
|
136
|
+
connection.log.warn({
|
|
137
|
+
msg: 'Invalid entries in the SEARCH result',
|
|
138
|
+
truncated,
|
|
139
|
+
discarded,
|
|
140
|
+
cid: connection.id
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
// IMAP4rev2 servers answer even a plain SEARCH with an untagged
|
|
146
|
+
// ESEARCH response (RFC 9051 deprecated the SEARCH response), so
|
|
147
|
+
// both forms are collected into the same result set
|
|
148
|
+
ESEARCH: async (untagged) => {
|
|
149
|
+
if (!untagged || !untagged.attributes) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
let parsed = (0, esearch_parser_js_1.parseEsearchResponse)(stripEsearchPrefix(untagged.attributes));
|
|
153
|
+
if (parsed.all) {
|
|
154
|
+
// Walk the compact sequence-set directly into the Set - the ALL
|
|
155
|
+
// result may cover the entire mailbox, so expanding it into an
|
|
156
|
+
// intermediate array first would double the peak memory use.
|
|
157
|
+
// The set comes from an untrusted server: endpoints must be
|
|
158
|
+
// valid nz-numbers ('Infinity' would otherwise loop forever)
|
|
159
|
+
// and the expansion stops at the mailbox EXISTS count - a
|
|
160
|
+
// conforming server cannot match more messages than exist, so
|
|
161
|
+
// a hostile range like 1:4294967295 cannot exhaust memory.
|
|
162
|
+
// A '*' means "largest number in use": that is exactly EXISTS
|
|
163
|
+
// for message sequence numbers, while server-sent UID sets may
|
|
164
|
+
// not contain '*' at all (RFC 9051 section 4.1.1), so UID
|
|
165
|
+
// parts with '*' are dropped
|
|
166
|
+
let existsCount = () => (connection.mailbox && connection.mailbox.exists) || 0;
|
|
167
|
+
// The mailbox EXISTS count is itself server-supplied and can be
|
|
168
|
+
// absurdly large, so the budget is additionally capped at the same
|
|
169
|
+
// absolute ceiling expandRange() uses - a hostile server cannot
|
|
170
|
+
// bypass it by inflating EXISTS first
|
|
171
|
+
let overBudget = () => results.size >= existsCount() || results.size >= tools_js_1.EXPANDED_RANGE_LIMIT;
|
|
172
|
+
let resolveId = (part) => (part === '*' ? (options.uid ? 0 : existsCount()) : Number(part));
|
|
173
|
+
let truncated = false;
|
|
174
|
+
let discarded = false;
|
|
175
|
+
sequenceSetLoop: for (let part of parsed.all.split(',')) {
|
|
176
|
+
part = part.trim();
|
|
177
|
+
let colon = part.indexOf(':');
|
|
178
|
+
if (colon < 0) {
|
|
179
|
+
let value = resolveId(part);
|
|
180
|
+
if (!(0, tools_js_1.isValidSequenceValue)(value)) {
|
|
181
|
+
discarded = true;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (overBudget()) {
|
|
185
|
+
truncated = true;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
results.add(value);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
let first = resolveId(part.substr(0, colon));
|
|
192
|
+
let second = resolveId(part.substr(colon + 1));
|
|
193
|
+
if (!(0, tools_js_1.isValidSequenceValue)(first) || !(0, tools_js_1.isValidSequenceValue)(second)) {
|
|
194
|
+
discarded = true;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
for (let id = Math.min(first, second); id <= Math.max(first, second); id++) {
|
|
198
|
+
if (overBudget()) {
|
|
199
|
+
truncated = true;
|
|
200
|
+
break sequenceSetLoop;
|
|
201
|
+
}
|
|
202
|
+
results.add(id);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (truncated || discarded) {
|
|
206
|
+
connection.log.warn({
|
|
207
|
+
msg: 'Invalid entries in the ESEARCH ALL result',
|
|
208
|
+
truncated,
|
|
209
|
+
discarded,
|
|
210
|
+
cid: connection.id
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
response.next();
|
|
218
|
+
// Sort numerically for consistent, predictable output order
|
|
219
|
+
return Array.from(results).sort((a, b) => a - b);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
await (0, tools_js_1.enhanceCommandError)(err);
|
|
223
|
+
connection.log.warn({ err, cid: connection.id });
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
module.exports = exports.default;
|
|
228
|
+
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ImapFlow } from '../imap-flow.js';
|
|
2
|
+
import type { MailboxObject, MailboxOpenOptions } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Options for SELECT/EXAMINE: the public open options plus the QRESYNC resynchronization
|
|
5
|
+
* parameters, which are only honored when the QRESYNC extension has been enabled
|
|
6
|
+
*/
|
|
7
|
+
export interface SelectOptions extends MailboxOpenOptions {
|
|
8
|
+
/** QRESYNC modseq value to fetch changes since */
|
|
9
|
+
changedSince?: bigint | number | string | undefined;
|
|
10
|
+
/** QRESYNC UID validity value */
|
|
11
|
+
uidValidity?: bigint | number | string | undefined;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Selects or examines a mailbox, making it the current mailbox for subsequent operations.
|
|
15
|
+
*
|
|
16
|
+
* @param connection - IMAP connection instance
|
|
17
|
+
* @param path - Mailbox path to select
|
|
18
|
+
* @param options - Select options
|
|
19
|
+
* @param options.readOnly - If true, use EXAMINE instead of SELECT (read-only access)
|
|
20
|
+
* @param options.changedSince - QRESYNC modseq value to fetch changes since
|
|
21
|
+
* @param options.uidValidity - QRESYNC UID validity value
|
|
22
|
+
* @returns Mailbox info object with path, flags, exists, uidNext, uidValidity, highestModseq, etc., or undefined if preconditions not met
|
|
23
|
+
* @throws If the SELECT/EXAMINE command fails
|
|
24
|
+
*/
|
|
25
|
+
export default function select(connection: ImapFlow, pathInput: string | string[], options?: SelectOptions | undefined): Promise<MailboxObject | undefined>;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = select;
|
|
4
|
+
const tools_js_1 = require("../tools.js");
|
|
5
|
+
// Response codes carrying a value that SELECT/EXAMINE may write to the mailbox object, keyed by
|
|
6
|
+
// the lowercased code, mapped to the fixed public property name and the parser for the value.
|
|
7
|
+
// Every parser returns false for a value it cannot use, and the field is then left unset.
|
|
8
|
+
//
|
|
9
|
+
// This is an allowlist on purpose. The mailbox object is API surface: without one, an arbitrary
|
|
10
|
+
// server-sent [KEY value] code could overwrite `path` (defeating the DELETE/RENAME guards that
|
|
11
|
+
// compare paths) or `flags`, and a parenthesized value under "__proto__" would replace the
|
|
12
|
+
// object's prototype. The lookup itself is on a null-prototype object for the same reason - the
|
|
13
|
+
// key is lowercased, so "constructor" would otherwise resolve to an inherited member.
|
|
14
|
+
const VALUED_RESPONSE_CODES = Object.assign(Object.create(null), {
|
|
15
|
+
// CONDSTORE (RFC 7162): highest mod-sequence value for the mailbox, used for incremental
|
|
16
|
+
// sync. Stored as a BigInt since modseq values can exceed Number.MAX_SAFE_INTEGER.
|
|
17
|
+
//
|
|
18
|
+
// A value that is not a bounded digit run is dropped rather than stored raw: every consumer
|
|
19
|
+
// compares highestModseq relationally, and a relational compare against a non-numeric string
|
|
20
|
+
// is false in both directions, so the value could never advance and delta sync would stop.
|
|
21
|
+
highestmodseq: { key: 'highestModseq', parse: (value) => (0, tools_js_1.parseBigIntValue)(value) },
|
|
22
|
+
// Unique identifier validity. If this changes between sessions, all previously cached UIDs
|
|
23
|
+
// are invalid and the client must re-sync from scratch. Nominally 32-bit, but stored as a
|
|
24
|
+
// BigInt precisely so a server that exceeds that still round-trips, hence the wider bound.
|
|
25
|
+
uidvalidity: { key: 'uidValidity', parse: (value) => (0, tools_js_1.parseBigIntValue)(value) },
|
|
26
|
+
// The next UID to be assigned in this mailbox, useful for detecting new arrivals. A huge
|
|
27
|
+
// digit run would coerce to Infinity and corrupt every later UID range computation.
|
|
28
|
+
uidnext: { key: 'uidNext', parse: (value) => (0, tools_js_1.parseUintValue)(value, tools_js_1.MAX_UINT32_DIGITS) },
|
|
29
|
+
// Sequence number of the first unseen message (RFC 3501 section 7.1). Not a count of unseen
|
|
30
|
+
// messages - use mailboxStatus() with {unseen: true} for that.
|
|
31
|
+
unseen: { key: 'unseen', parse: (value) => (0, tools_js_1.parseUintValue)(value, tools_js_1.MAX_UINT32_DIGITS) },
|
|
32
|
+
// APPENDLIMIT (RFC 7889): largest message size in octets the server accepts for APPEND into
|
|
33
|
+
// this mailbox. Spelled all lowercase, unlike the camelCase fields around it, because that is
|
|
34
|
+
// the name this object has always exposed.
|
|
35
|
+
appendlimit: { key: 'appendlimit', parse: (value) => (0, tools_js_1.parseUintValue)(value) },
|
|
36
|
+
// OBJECTID (RFC 8474): server-assigned mailbox identifier that survives renames. Sent as a
|
|
37
|
+
// parenthesized list, but servers in the wild send it bare too.
|
|
38
|
+
mailboxid: {
|
|
39
|
+
key: 'mailboxId',
|
|
40
|
+
parse: (value) => (Array.isArray(value) ? value.length > 0 && value[0] : typeof value === 'string' && value)
|
|
41
|
+
},
|
|
42
|
+
// Flags the client may change permanently on messages in this mailbox, including \* if the
|
|
43
|
+
// server allows custom flags. Only the parenthesized form carries flags, and a malformed
|
|
44
|
+
// value must leave permanentFlags unset rather than set an empty Set: canUseFlag() reads
|
|
45
|
+
// unset as permissive and empty as deny-all, so an empty Set would turn every later flag
|
|
46
|
+
// update into a silent no-op for the rest of the session.
|
|
47
|
+
permanentflags: { key: 'permanentFlags', parse: (value) => Array.isArray(value) && new Set(value) }
|
|
48
|
+
});
|
|
49
|
+
/**
|
|
50
|
+
* Selects or examines a mailbox, making it the current mailbox for subsequent operations.
|
|
51
|
+
*
|
|
52
|
+
* @param connection - IMAP connection instance
|
|
53
|
+
* @param path - Mailbox path to select
|
|
54
|
+
* @param options - Select options
|
|
55
|
+
* @param options.readOnly - If true, use EXAMINE instead of SELECT (read-only access)
|
|
56
|
+
* @param options.changedSince - QRESYNC modseq value to fetch changes since
|
|
57
|
+
* @param options.uidValidity - QRESYNC UID validity value
|
|
58
|
+
* @returns Mailbox info object with path, flags, exists, uidNext, uidValidity, highestModseq, etc., or undefined if preconditions not met
|
|
59
|
+
* @throws If the SELECT/EXAMINE command fails
|
|
60
|
+
*/
|
|
61
|
+
async function select(connection, pathInput, options) {
|
|
62
|
+
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state)) {
|
|
63
|
+
// nothing to do here
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
options = options || {};
|
|
67
|
+
let path = (0, tools_js_1.normalizePath)(connection, pathInput);
|
|
68
|
+
// Ensure we have folder metadata (delimiter, flags, specialUse) by running LIST if needed.
|
|
69
|
+
// This is cached in connection.folders to avoid repeated LIST calls.
|
|
70
|
+
// Note: this uses run() rather than runInternal(), so it terminates a running IDLE first,
|
|
71
|
+
// which is what a caller-issued mailboxOpen() needs. Fallback polling reaches SELECT through
|
|
72
|
+
// runInternal(), and on a cache miss this LIST awaits the polling session's own preCheck and
|
|
73
|
+
// so cancels that session. Not a deadlock, and only reachable when the folder is uncached,
|
|
74
|
+
// but a polled SELECT of an unlisted folder ends the poll early.
|
|
75
|
+
if (!connection.folders.has(path)) {
|
|
76
|
+
let folders = await connection.run('LIST', '', path);
|
|
77
|
+
if (!folders) {
|
|
78
|
+
throw new Error('Failed to fetch folders');
|
|
79
|
+
}
|
|
80
|
+
folders.forEach(folder => {
|
|
81
|
+
connection.folders.set(folder.path, folder);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
let folderListData = connection.folders.has(path) ? connection.folders.get(path) : false;
|
|
85
|
+
let response;
|
|
86
|
+
try {
|
|
87
|
+
let map = { path };
|
|
88
|
+
if (folderListData) {
|
|
89
|
+
['delimiter', 'specialUse', 'subscribed', 'listed'].forEach(key => {
|
|
90
|
+
if (folderListData[key]) {
|
|
91
|
+
map[key] = folderListData[key];
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// QRESYNC (RFC 7162): allows efficient mailbox resynchronization by sending
|
|
96
|
+
// the last known UIDVALIDITY and HIGHESTMODSEQ. Server responds with only
|
|
97
|
+
// the changes (new flags, expunged UIDs) since that point.
|
|
98
|
+
let extraArgs = [];
|
|
99
|
+
if (connection.enabled.has('QRESYNC') && options.changedSince && options.uidValidity) {
|
|
100
|
+
extraArgs.push([
|
|
101
|
+
{ type: 'ATOM', value: 'QRESYNC' },
|
|
102
|
+
[
|
|
103
|
+
{ type: 'ATOM', value: options.uidValidity?.toString() },
|
|
104
|
+
{ type: 'ATOM', value: options.changedSince.toString() }
|
|
105
|
+
]
|
|
106
|
+
]);
|
|
107
|
+
map.qresync = true;
|
|
108
|
+
}
|
|
109
|
+
let encodedPath = (0, tools_js_1.encodePath)(connection, path);
|
|
110
|
+
// SELECT opens the mailbox read-write; EXAMINE opens it read-only.
|
|
111
|
+
// Path encoding: if the encoded path contains '&' (UTF-7 encoding marker),
|
|
112
|
+
// send as quoted STRING to avoid parser issues with the ampersand.
|
|
113
|
+
let selectCommand = {
|
|
114
|
+
command: !options.readOnly ? 'SELECT' : 'EXAMINE',
|
|
115
|
+
/* c8 ignore next */ // extraArgs is always initialised to an array, so the [] fallback is unreachable
|
|
116
|
+
arguments: [{ type: encodedPath.indexOf('&') >= 0 ? 'STRING' : 'ATOM', value: encodedPath }].concat(extraArgs || [])
|
|
117
|
+
};
|
|
118
|
+
response = await connection.exec(selectCommand.command, selectCommand.arguments, {
|
|
119
|
+
untagged: {
|
|
120
|
+
// Untagged OK responses carry response codes in brackets, e.g.:
|
|
121
|
+
// * OK [UIDVALIDITY 1234] UIDs valid
|
|
122
|
+
// * OK [PERMANENTFLAGS (\Seen \Answered \*)] Flags permitted
|
|
123
|
+
// The section array holds the parsed bracket contents: section[0] is the
|
|
124
|
+
// key (e.g., "UIDVALIDITY"), section[1] is the value or list.
|
|
125
|
+
OK: async (untagged) => {
|
|
126
|
+
if (!untagged.attributes || !untagged.attributes.length) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
let section = !untagged.attributes[0].value && untagged.attributes[0].section;
|
|
130
|
+
// Handle response codes with a key-value pair (section has 2+ elements)
|
|
131
|
+
if (section && section.length > 1 && section[0] && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
|
|
132
|
+
let key = section[0].value.toLowerCase();
|
|
133
|
+
let value;
|
|
134
|
+
// Value can be a single string or a list of strings (e.g., PERMANENTFLAGS).
|
|
135
|
+
// section[1] can be a parsed NIL (null), and so can any element inside a
|
|
136
|
+
// parenthesized list, so both levels need the guard
|
|
137
|
+
if (section[1] && typeof section[1].value === 'string') {
|
|
138
|
+
value = section[1].value;
|
|
139
|
+
}
|
|
140
|
+
else if (Array.isArray(section[1])) {
|
|
141
|
+
value = (0, tools_js_1.getStringList)(section[1]);
|
|
142
|
+
}
|
|
143
|
+
let field = VALUED_RESPONSE_CODES[key];
|
|
144
|
+
if (field) {
|
|
145
|
+
let parsed = field.parse(value);
|
|
146
|
+
if (parsed !== false) {
|
|
147
|
+
map[field.key] = parsed;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Handle response codes with only a keyword (no value), e.g., [NOMODSEQ].
|
|
152
|
+
// NOMODSEQ means the mailbox does not support mod-sequences, so the
|
|
153
|
+
// CONDSTORE/QRESYNC features are unavailable for it.
|
|
154
|
+
if (section &&
|
|
155
|
+
section.length === 1 &&
|
|
156
|
+
section[0] &&
|
|
157
|
+
section[0].type === 'ATOM' &&
|
|
158
|
+
section[0].value?.toUpperCase() === 'NOMODSEQ') {
|
|
159
|
+
map.noModseq = true;
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
// Untagged FLAGS response lists all flags defined for this mailbox
|
|
163
|
+
// (both system flags and custom flags). Example: * FLAGS (\Seen \Answered \Flagged)
|
|
164
|
+
FLAGS: async (untagged) => {
|
|
165
|
+
if (!untagged.attributes || !untagged.attributes.length || !Array.isArray(untagged.attributes[0])) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
map.flags = new Set((0, tools_js_1.getStringList)(untagged.attributes[0]));
|
|
169
|
+
},
|
|
170
|
+
// Untagged EXISTS response: "* <count> EXISTS" tells us the total number
|
|
171
|
+
// of messages in the mailbox. The count is in the command field (numeric prefix).
|
|
172
|
+
EXISTS: async (untagged) => {
|
|
173
|
+
// Not a usable count: anything but a bounded digit run. A long digit run
|
|
174
|
+
// coerces to Infinity, which would corrupt every later range computation
|
|
175
|
+
let num = (0, tools_js_1.parseUintValue)(untagged.command, tools_js_1.MAX_UINT32_DIGITS);
|
|
176
|
+
if (num === false) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
map.exists = num;
|
|
180
|
+
},
|
|
181
|
+
// VANISHED responses (QRESYNC): server reports UIDs that have been expunged
|
|
182
|
+
// since the client's last known state. Only received when QRESYNC was requested.
|
|
183
|
+
// A dummy mailbox object is passed because the mailbox isn't officially open yet.
|
|
184
|
+
VANISHED: async (untagged) => {
|
|
185
|
+
await connection.untaggedVanished(untagged, { path, uidNext: false, uidValidity: false });
|
|
186
|
+
},
|
|
187
|
+
// Untagged FETCH during SELECT/EXAMINE: only occurs with QRESYNC, delivering
|
|
188
|
+
// updated flags for messages that changed since the client's last modseq.
|
|
189
|
+
FETCH: async (untagged) => {
|
|
190
|
+
await connection.untaggedFetch(untagged, { path, uidNext: false, uidValidity: false });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
// The tagged OK response to SELECT/EXAMINE includes [READ-ONLY] or [READ-WRITE]
|
|
195
|
+
// in its response code, indicating the access mode the server granted. A tagged OK
|
|
196
|
+
// with no resp-text has no `attributes` property at all, and unlike the untagged
|
|
197
|
+
// handlers above this runs in the command body, where a throw would tear down the
|
|
198
|
+
// mailbox state the server has actually selected.
|
|
199
|
+
let okAttributes = (response.response && response.response.attributes) || [];
|
|
200
|
+
let section = okAttributes[0] && !okAttributes[0].value && okAttributes[0].section;
|
|
201
|
+
if (section && section.length && section[0] && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
|
|
202
|
+
map.readOnly = section[0].value.toUpperCase() === 'READ-ONLY';
|
|
203
|
+
}
|
|
204
|
+
// Validate QRESYNC preconditions (RFC 7162 Section 3.2.5):
|
|
205
|
+
// QRESYNC results are only valid if UIDVALIDITY matches, HIGHESTMODSEQ is
|
|
206
|
+
// present, and the mailbox supports mod-sequences. If any condition fails,
|
|
207
|
+
// the client cannot trust the incremental updates and must do a full resync.
|
|
208
|
+
if (map.qresync && (options.uidValidity !== map.uidValidity || !map.highestModseq || map.noModseq)) {
|
|
209
|
+
map.qresync = false;
|
|
210
|
+
}
|
|
211
|
+
// Transition mailbox state: save previous mailbox reference, temporarily
|
|
212
|
+
// clear it, then emit events and set the new mailbox.
|
|
213
|
+
let currentMailbox = connection.mailbox;
|
|
214
|
+
connection.mailbox = false;
|
|
215
|
+
// Emit mailboxClose if we're switching from a different mailbox.
|
|
216
|
+
// Re-selecting the same mailbox (e.g., for resync) does not trigger close/open.
|
|
217
|
+
if (currentMailbox && currentMailbox.path !== path) {
|
|
218
|
+
connection.emit('mailboxClose', currentMailbox);
|
|
219
|
+
}
|
|
220
|
+
connection.mailbox = map;
|
|
221
|
+
// Save the SELECT command for potential re-use (e.g., NOOP fallback polling
|
|
222
|
+
// re-issues the SELECT to detect changes on servers without IDLE support).
|
|
223
|
+
connection.currentSelectCommand = selectCommand;
|
|
224
|
+
connection.state = connection.states.SELECTED;
|
|
225
|
+
if (!currentMailbox || currentMailbox.path !== path) {
|
|
226
|
+
connection.emit('mailboxOpen', connection.mailbox);
|
|
227
|
+
}
|
|
228
|
+
response.next();
|
|
229
|
+
return map;
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
await (0, tools_js_1.enhanceCommandError)(err);
|
|
233
|
+
// If SELECT/EXAMINE fails while a mailbox was already selected, we must
|
|
234
|
+
// reset to AUTHENTICATED state since the server has implicitly deselected
|
|
235
|
+
// the previous mailbox on failure (RFC 3501 Section 6.3.1).
|
|
236
|
+
if (connection.state === connection.states.SELECTED) {
|
|
237
|
+
let currentMailbox = connection.mailbox;
|
|
238
|
+
connection.mailbox = false;
|
|
239
|
+
connection.currentSelectCommand = false;
|
|
240
|
+
connection.state = connection.states.AUTHENTICATED;
|
|
241
|
+
if (currentMailbox) {
|
|
242
|
+
connection.emit('mailboxClose', currentMailbox);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
connection.log.warn({ err, cid: connection.id });
|
|
246
|
+
throw err;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
module.exports = exports.default;
|
|
250
|
+
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
|
+
* Initiates STARTTLS connection upgrade.
|
|
4
|
+
*
|
|
5
|
+
* @param connection - IMAP connection instance
|
|
6
|
+
* @returns True if STARTTLS was initiated, false if not supported or already secure
|
|
7
|
+
*/
|
|
8
|
+
export default function starttls(connection: ImapFlow): Promise<boolean>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = starttls;
|
|
4
|
+
/**
|
|
5
|
+
* Initiates STARTTLS connection upgrade.
|
|
6
|
+
*
|
|
7
|
+
* @param connection - IMAP connection instance
|
|
8
|
+
* @returns True if STARTTLS was initiated, false if not supported or already secure
|
|
9
|
+
*/
|
|
10
|
+
async function starttls(connection) {
|
|
11
|
+
if (!connection.capabilities.has('STARTTLS') || connection.secureConnection) {
|
|
12
|
+
// nothing to do here
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
let response;
|
|
16
|
+
try {
|
|
17
|
+
response = await connection.exec('STARTTLS');
|
|
18
|
+
// Whether the server sent anything after the STARTTLS OK and before the TLS
|
|
19
|
+
// handshake. upgradeToSTARTTLS() uses this to reject a plaintext injection.
|
|
20
|
+
connection._starttlsHadTrailingData = !!(response && response.hasTrailingData);
|
|
21
|
+
response.next();
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
connection.log.warn({ err, cid: connection.id });
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
module.exports = exports.default;
|
|
30
|
+
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ImapAttributeList } from '../handler/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A parsed STATUS value: counts and sizes as numbers, UIDVALIDITY and HIGHESTMODSEQ as BigInts
|
|
4
|
+
*/
|
|
5
|
+
export type StatusFieldValue = number | bigint;
|
|
6
|
+
/**
|
|
7
|
+
* Walks a STATUS data-item list - alternating item-name and item-value tokens - and reports
|
|
8
|
+
* every recognized field that parsed successfully. Unknown item names and unusable values are
|
|
9
|
+
* skipped, so one bad field never costs the rest of the response.
|
|
10
|
+
*
|
|
11
|
+
* @param list - Parsed attribute list from the untagged STATUS response.
|
|
12
|
+
* @param onField - Called as (key, value) for each usable field.
|
|
13
|
+
*/
|
|
14
|
+
export declare const parseStatusList: (list: ImapAttributeList, onField: (key: string, value: StatusFieldValue) => void) => void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseStatusList = void 0;
|
|
4
|
+
const tools_js_1 = require("../tools.js");
|
|
5
|
+
// STATUS data items (RFC 3501 section 6.3.10, RFC 7162 for HIGHESTMODSEQ, RFC 9051 for SIZE
|
|
6
|
+
// and DELETED) mapped to the property name each one is exposed under, together with the
|
|
7
|
+
// parser that turns the raw response token into a usable value. Shared by the STATUS command
|
|
8
|
+
// and by the inline STATUS responses of LIST-STATUS (RFC 5819) so the two cannot drift apart.
|
|
9
|
+
//
|
|
10
|
+
// Every parser rejects anything that is not a bounded decimal digit run, returning false.
|
|
11
|
+
// These values are server-controlled and several of them are written straight into the live
|
|
12
|
+
// mailbox state, where a NaN or a value coerced to Infinity corrupts every later range
|
|
13
|
+
// computation. A plain isNaN() test is not enough: it passes '1e5', ' 12 ' and 'Infinity',
|
|
14
|
+
// and BigInt() throws on all three, aborting the walk over the remaining fields.
|
|
15
|
+
const uint32 = (value) => (0, tools_js_1.parseUintValue)(value, tools_js_1.MAX_UINT32_DIGITS);
|
|
16
|
+
const STATUS_FIELDS = {
|
|
17
|
+
MESSAGES: { key: 'messages', parser: uint32 },
|
|
18
|
+
RECENT: { key: 'recent', parser: uint32 },
|
|
19
|
+
UIDNEXT: { key: 'uidNext', parser: uint32 },
|
|
20
|
+
// Nominally 32-bit, but stored as a BigInt precisely so a server that exceeds that still
|
|
21
|
+
// round-trips, so the wider bound applies
|
|
22
|
+
UIDVALIDITY: { key: 'uidValidity', parser: value => (0, tools_js_1.parseBigIntValue)(value) },
|
|
23
|
+
UNSEEN: { key: 'unseen', parser: uint32 },
|
|
24
|
+
HIGHESTMODSEQ: { key: 'highestModseq', parser: value => (0, tools_js_1.parseBigIntValue)(value) },
|
|
25
|
+
// IMAP4rev2 additions (RFC 9051): total mailbox size in octets (number64, exact as a JS
|
|
26
|
+
// number up to 2^53-1) and count of messages carrying the \Deleted flag
|
|
27
|
+
SIZE: { key: 'size', parser: value => (0, tools_js_1.parseUintValue)(value) },
|
|
28
|
+
DELETED: { key: 'deleted', parser: uint32 }
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Walks a STATUS data-item list - alternating item-name and item-value tokens - and reports
|
|
32
|
+
* every recognized field that parsed successfully. Unknown item names and unusable values are
|
|
33
|
+
* skipped, so one bad field never costs the rest of the response.
|
|
34
|
+
*
|
|
35
|
+
* @param list - Parsed attribute list from the untagged STATUS response.
|
|
36
|
+
* @param onField - Called as (key, value) for each usable field.
|
|
37
|
+
*/
|
|
38
|
+
const parseStatusList = (list, onField) => {
|
|
39
|
+
let name;
|
|
40
|
+
list.forEach((entry, i) => {
|
|
41
|
+
if (i % 2 === 0) {
|
|
42
|
+
name = entry && typeof entry.value === 'string' ? entry.value : false;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (!name || !entry) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// The item name is server-controlled, but uppercasing it before the lookup means no
|
|
49
|
+
// Object.prototype member can be reached: every builtin name has a lowercase letter.
|
|
50
|
+
const field = STATUS_FIELDS[name.toUpperCase()];
|
|
51
|
+
if (!field) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const value = field.parser(entry.value);
|
|
55
|
+
if (value === false) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
onField(field.key, value);
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
exports.parseStatusList = parseStatusList;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ImapFlow } from '../imap-flow.js';
|
|
2
|
+
import type { StatusObject, StatusQuery } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Requests status information about a mailbox.
|
|
5
|
+
*
|
|
6
|
+
* @param connection - IMAP connection instance
|
|
7
|
+
* @param path - Mailbox path to query
|
|
8
|
+
* @param query - Status data items to request (e.g., {messages: true, uidNext: true, unseen: true})
|
|
9
|
+
* @returns Status information object, or false if preconditions not met or on failure
|
|
10
|
+
* @throws {Error} If the mailbox does not exist
|
|
11
|
+
*/
|
|
12
|
+
export default function status(connection: ImapFlow, path: string | string[], query: StatusQuery | undefined): Promise<StatusObject | false>;
|