imapflow 1.7.8 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +8 -2
- package/dist/cjs/charsets.d.ts +1 -0
- package/dist/cjs/charsets.js +294 -0
- package/dist/cjs/commands/append.d.ts +22 -0
- package/dist/cjs/commands/append.js +151 -0
- package/dist/cjs/commands/authenticate.d.ts +24 -0
- package/dist/cjs/commands/authenticate.js +223 -0
- package/dist/cjs/commands/capability.d.ts +8 -0
- package/dist/cjs/commands/capability.js +32 -0
- package/dist/cjs/commands/close.d.ts +8 -0
- package/dist/cjs/commands/close.js +39 -0
- package/dist/cjs/commands/compress.d.ts +8 -0
- package/dist/cjs/commands/compress.js +56 -0
- package/dist/cjs/commands/copy.d.ts +13 -0
- package/dist/cjs/commands/copy.js +44 -0
- package/dist/cjs/commands/copyuid-parser.d.ts +11 -0
- package/dist/cjs/commands/copyuid-parser.js +32 -0
- package/dist/cjs/commands/create.d.ts +11 -0
- package/dist/cjs/commands/create.js +80 -0
- package/dist/cjs/commands/delete.d.ts +11 -0
- package/dist/cjs/commands/delete.js +40 -0
- package/dist/cjs/commands/enable.d.ts +9 -0
- package/dist/cjs/commands/enable.js +61 -0
- package/dist/cjs/commands/esearch-parser.d.ts +17 -0
- package/dist/cjs/commands/esearch-parser.js +91 -0
- package/dist/cjs/commands/expunge.d.ts +12 -0
- package/dist/cjs/commands/expunge.js +60 -0
- package/dist/cjs/commands/fetch.d.ts +30 -0
- package/dist/cjs/commands/fetch.js +241 -0
- package/dist/cjs/commands/id.d.ts +10 -0
- package/dist/cjs/commands/id.js +80 -0
- package/dist/cjs/commands/idle.d.ts +9 -0
- package/dist/cjs/commands/idle.js +347 -0
- package/dist/cjs/commands/list.d.ts +16 -0
- package/dist/cjs/commands/list.js +520 -0
- package/dist/cjs/commands/login.d.ts +11 -0
- package/dist/cjs/commands/login.js +42 -0
- package/dist/cjs/commands/logout.d.ts +8 -0
- package/dist/cjs/commands/logout.js +47 -0
- package/dist/cjs/commands/move.d.ts +13 -0
- package/dist/cjs/commands/move.js +57 -0
- package/dist/cjs/commands/namespace.d.ts +25 -0
- package/dist/cjs/commands/namespace.js +139 -0
- package/dist/cjs/commands/noop.d.ts +8 -0
- package/dist/cjs/commands/noop.js +22 -0
- package/dist/cjs/commands/quota.d.ts +10 -0
- package/dist/cjs/commands/quota.js +119 -0
- package/dist/cjs/commands/rename.d.ts +12 -0
- package/dist/cjs/commands/rename.js +48 -0
- package/dist/cjs/commands/search.d.ts +15 -0
- package/dist/cjs/commands/search.js +228 -0
- package/dist/cjs/commands/select.d.ts +25 -0
- package/dist/cjs/commands/select.js +250 -0
- package/dist/cjs/commands/starttls.d.ts +8 -0
- package/dist/cjs/commands/starttls.js +30 -0
- package/dist/cjs/commands/status-fields.d.ts +14 -0
- package/dist/cjs/commands/status-fields.js +61 -0
- package/dist/cjs/commands/status.d.ts +12 -0
- package/dist/cjs/commands/status.js +108 -0
- package/dist/cjs/commands/store.d.ts +19 -0
- package/dist/cjs/commands/store.js +93 -0
- package/dist/cjs/commands/subscribe.d.ts +9 -0
- package/dist/cjs/commands/subscribe.js +31 -0
- package/dist/cjs/commands/unsubscribe.d.ts +9 -0
- package/dist/cjs/commands/unsubscribe.js +31 -0
- package/dist/cjs/connection-deadline.d.ts +49 -0
- package/dist/cjs/connection-deadline.js +91 -0
- package/dist/cjs/errors.d.ts +83 -0
- package/dist/cjs/errors.js +13 -0
- package/dist/cjs/handler/imap-compiler.d.ts +24 -0
- package/dist/cjs/handler/imap-compiler.js +285 -0
- package/dist/cjs/handler/imap-formal-syntax.d.ts +28 -0
- package/dist/cjs/handler/imap-formal-syntax.js +121 -0
- package/dist/cjs/handler/imap-handler.d.ts +9 -0
- package/dist/cjs/handler/imap-handler.js +10 -0
- package/dist/cjs/handler/imap-parser.d.ts +16 -0
- package/dist/cjs/handler/imap-parser.js +90 -0
- package/dist/cjs/handler/imap-stream.d.ts +181 -0
- package/dist/cjs/handler/imap-stream.js +446 -0
- package/dist/cjs/handler/limits.d.ts +25 -0
- package/dist/cjs/handler/limits.js +51 -0
- package/dist/cjs/handler/parser-instance.d.ts +68 -0
- package/dist/cjs/handler/parser-instance.js +223 -0
- package/dist/cjs/handler/token-parser.d.ts +91 -0
- package/dist/cjs/handler/token-parser.js +673 -0
- package/dist/cjs/handler/types.d.ts +91 -0
- package/dist/cjs/handler/types.js +4 -0
- package/dist/cjs/imap-commands.d.ts +16 -0
- package/dist/cjs/imap-commands.js +74 -0
- package/dist/cjs/imap-flow.d.ts +676 -0
- package/dist/cjs/imap-flow.js +3956 -0
- package/dist/cjs/jp-decoder.d.ts +12 -0
- package/dist/cjs/jp-decoder.js +79 -0
- package/dist/cjs/limited-passthrough.d.ts +25 -0
- package/dist/cjs/limited-passthrough.js +54 -0
- package/dist/cjs/logger.d.ts +3 -0
- package/dist/cjs/logger.js +11 -0
- package/dist/cjs/package-info.d.ts +3 -0
- package/dist/cjs/package-info.js +7 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/proxy-connection.d.ts +33 -0
- package/dist/cjs/proxy-connection.js +392 -0
- package/dist/cjs/search-compiler.d.ts +34 -0
- package/dist/cjs/search-compiler.js +476 -0
- package/dist/cjs/special-use.d.ts +22 -0
- package/dist/cjs/special-use.js +911 -0
- package/dist/cjs/tools.d.ts +427 -0
- package/dist/cjs/tools.js +1496 -0
- package/{lib/imap-flow.d.ts → dist/cjs/types.d.ts} +387 -517
- package/dist/cjs/types.js +5 -0
- package/dist/esm/charsets.d.ts +1 -0
- package/{lib → dist/esm}/charsets.js +1 -6
- package/dist/esm/commands/append.d.ts +22 -0
- package/{lib → dist/esm}/commands/append.js +22 -52
- package/dist/esm/commands/authenticate.d.ts +24 -0
- package/{lib → dist/esm}/commands/authenticate.js +62 -87
- package/dist/esm/commands/capability.d.ts +8 -0
- package/{lib → dist/esm}/commands/capability.js +6 -9
- package/dist/esm/commands/close.d.ts +8 -0
- package/{lib → dist/esm}/commands/close.js +6 -10
- package/dist/esm/commands/compress.d.ts +8 -0
- package/{lib → dist/esm}/commands/compress.js +7 -11
- package/dist/esm/commands/copy.d.ts +13 -0
- package/{lib → dist/esm}/commands/copy.js +12 -20
- package/dist/esm/commands/copyuid-parser.d.ts +11 -0
- package/{lib → dist/esm}/commands/copyuid-parser.js +9 -15
- package/dist/esm/commands/create.d.ts +11 -0
- package/{lib → dist/esm}/commands/create.js +13 -27
- package/dist/esm/commands/delete.d.ts +11 -0
- package/{lib → dist/esm}/commands/delete.js +9 -14
- package/dist/esm/commands/enable.d.ts +9 -0
- package/{lib → dist/esm}/commands/enable.js +23 -30
- package/dist/esm/commands/esearch-parser.d.ts +17 -0
- package/dist/esm/commands/esearch-parser.js +88 -0
- package/dist/esm/commands/expunge.d.ts +12 -0
- package/{lib → dist/esm}/commands/expunge.js +17 -22
- package/dist/esm/commands/fetch.d.ts +30 -0
- package/{lib → dist/esm}/commands/fetch.js +32 -64
- package/dist/esm/commands/id.d.ts +10 -0
- package/{lib → dist/esm}/commands/id.js +17 -23
- package/dist/esm/commands/idle.d.ts +9 -0
- package/{lib → dist/esm}/commands/idle.js +47 -81
- package/dist/esm/commands/list.d.ts +16 -0
- package/{lib → dist/esm}/commands/list.js +60 -123
- package/dist/esm/commands/login.d.ts +11 -0
- package/{lib → dist/esm}/commands/login.js +10 -15
- package/dist/esm/commands/logout.d.ts +8 -0
- package/{lib → dist/esm}/commands/logout.js +9 -11
- package/dist/esm/commands/move.d.ts +13 -0
- package/{lib → dist/esm}/commands/move.js +13 -21
- package/dist/esm/commands/namespace.d.ts +25 -0
- package/{lib → dist/esm}/commands/namespace.js +34 -44
- package/dist/esm/commands/noop.d.ts +8 -0
- package/{lib → dist/esm}/commands/noop.js +6 -7
- package/dist/esm/commands/quota.d.ts +10 -0
- package/{lib → dist/esm}/commands/quota.js +18 -36
- package/dist/esm/commands/rename.d.ts +12 -0
- package/{lib → dist/esm}/commands/rename.js +10 -15
- package/dist/esm/commands/search.d.ts +15 -0
- package/{lib → dist/esm}/commands/search.js +36 -135
- package/dist/esm/commands/select.d.ts +25 -0
- package/{lib → dist/esm}/commands/select.js +33 -64
- package/dist/esm/commands/starttls.d.ts +8 -0
- package/{lib → dist/esm}/commands/starttls.js +6 -8
- package/dist/esm/commands/status-fields.d.ts +14 -0
- package/{lib → dist/esm}/commands/status-fields.js +5 -16
- package/dist/esm/commands/status.d.ts +12 -0
- package/{lib → dist/esm}/commands/status.js +18 -29
- package/dist/esm/commands/store.d.ts +19 -0
- package/{lib → dist/esm}/commands/store.js +24 -37
- package/dist/esm/commands/subscribe.d.ts +9 -0
- package/{lib → dist/esm}/commands/subscribe.js +8 -12
- package/dist/esm/commands/unsubscribe.d.ts +9 -0
- package/{lib → dist/esm}/commands/unsubscribe.js +8 -12
- package/dist/esm/connection-deadline.d.ts +49 -0
- package/{lib → dist/esm}/connection-deadline.js +14 -25
- package/dist/esm/errors.d.ts +83 -0
- package/dist/esm/errors.js +9 -0
- package/dist/esm/handler/imap-compiler.d.ts +24 -0
- package/{lib → dist/esm}/handler/imap-compiler.js +22 -80
- package/dist/esm/handler/imap-formal-syntax.d.ts +28 -0
- package/dist/esm/handler/imap-formal-syntax.js +117 -0
- package/dist/esm/handler/imap-handler.d.ts +9 -0
- package/dist/esm/handler/imap-handler.js +9 -0
- package/dist/esm/handler/imap-parser.d.ts +16 -0
- package/{lib → dist/esm}/handler/imap-parser.js +31 -44
- package/dist/esm/handler/imap-stream.d.ts +181 -0
- package/{lib → dist/esm}/handler/imap-stream.js +29 -121
- package/dist/esm/handler/limits.d.ts +25 -0
- package/{lib → dist/esm}/handler/limits.js +13 -22
- package/dist/esm/handler/parser-instance.d.ts +68 -0
- package/{lib → dist/esm}/handler/parser-instance.js +19 -47
- package/dist/esm/handler/token-parser.d.ts +91 -0
- package/{lib → dist/esm}/handler/token-parser.js +71 -155
- package/dist/esm/handler/types.d.ts +91 -0
- package/dist/esm/handler/types.js +3 -0
- package/dist/esm/imap-commands.d.ts +16 -0
- package/dist/esm/imap-commands.js +67 -0
- package/dist/esm/imap-flow.d.ts +676 -0
- package/{lib → dist/esm}/imap-flow.js +769 -1790
- package/dist/esm/jp-decoder.d.ts +12 -0
- package/{lib → dist/esm}/jp-decoder.js +6 -21
- package/dist/esm/limited-passthrough.d.ts +25 -0
- package/{lib → dist/esm}/limited-passthrough.js +7 -20
- package/dist/esm/logger.d.ts +3 -0
- package/dist/esm/logger.js +4 -0
- package/dist/esm/package-info.d.ts +3 -0
- package/dist/esm/package-info.js +4 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/proxy-connection.d.ts +33 -0
- package/{lib → dist/esm}/proxy-connection.js +56 -127
- package/dist/esm/search-compiler.d.ts +34 -0
- package/{lib → dist/esm}/search-compiler.js +54 -110
- package/dist/esm/special-use.d.ts +22 -0
- package/dist/esm/special-use.js +907 -0
- package/dist/esm/tools.d.ts +427 -0
- package/dist/esm/tools.js +1446 -0
- package/dist/esm/types.d.ts +828 -0
- package/dist/esm/types.js +4 -0
- package/package.json +60 -20
- package/.gitattributes +0 -1
- package/.github/CODE_OF_CONDUCT.md +0 -76
- package/.github/FUNDING.yml +0 -4
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
- package/.github/contributing.md +0 -17
- package/.github/workflows/release.yaml +0 -36
- package/.github/workflows/stale.yml +0 -29
- package/.github/workflows/test.yml +0 -51
- package/.ncurc.js +0 -4
- package/.prettierignore +0 -4
- package/.prettierrc.js +0 -8
- package/.release-please-manifest.json +0 -3
- package/CLAUDE.md +0 -104
- package/Gruntfile.js +0 -23
- package/eslint.config.js +0 -45
- package/lib/handler/imap-formal-syntax.js +0 -189
- package/lib/handler/imap-handler.js +0 -17
- package/lib/imap-commands.js +0 -45
- package/lib/logger.js +0 -5
- package/lib/special-use.js +0 -923
- package/lib/tools.js +0 -1612
- package/release-please-config.json +0 -10
- package/test/authentication-test.js +0 -101
- package/test/auto-idle-test.js +0 -470
- package/test/bodystructure-test.js +0 -899
- package/test/charsets-test.js +0 -161
- package/test/commands-branches-test.js +0 -1095
- package/test/commands-integration-test.js +0 -11124
- package/test/commands-test.js +0 -73
- package/test/connection-edge-cases-test.js +0 -1828
- package/test/connection-test.js +0 -162
- package/test/copyuid-parser-test.js +0 -173
- package/test/fetch-generator-test.js +0 -218
- package/test/fixtures/fake-timers.js +0 -115
- package/test/fixtures/serialized-mimetorture.js +0 -2738
- package/test/fixtures/test-client.js +0 -101
- package/test/fixtures/test-tls.js +0 -8
- package/test/handler-branches-test.js +0 -310
- package/test/idle-polling-test.js +0 -518
- package/test/imap-compiler-test.js +0 -809
- package/test/imap-flow-compress-test.js +0 -166
- package/test/imap-flow-coverage-test.js +0 -612
- package/test/imap-flow-fetch-download-test.js +0 -909
- package/test/imap-flow-internals-test.js +0 -725
- package/test/imap-flow-methods-test.js +0 -889
- package/test/imap-flow-proxy-paths-test.js +0 -366
- package/test/imap-flow-secure-test.js +0 -573
- package/test/imap-flow-server-test.js +0 -1474
- package/test/imap-formal-syntax-test.js +0 -293
- package/test/imap-parser-test.js +0 -1474
- package/test/imap-stream-edge-cases-test.js +0 -666
- package/test/imap-stream-test.js +0 -177
- package/test/imapflow-test.js +0 -258
- package/test/integration/README.md +0 -52
- package/test/integration/dovecot-test.conf +0 -27
- package/test/integration/rev2-live-test.js +0 -431
- package/test/integration/run-rev2-tests.sh +0 -75
- package/test/integration-test.js +0 -83
- package/test/jp-decoder-test.js +0 -304
- package/test/limited-passthrough-test.js +0 -299
- package/test/memory-cleanup-test.js +0 -144
- package/test/memory-leak-test.js +0 -667
- package/test/parser-limits-test.js +0 -292
- package/test/proxy-connection-test.js +0 -738
- package/test/reliability-improvements-test.js +0 -548
- package/test/search-compiler-test.js +0 -1300
- package/test/search-test.js +0 -329
- package/test/special-use-test.js +0 -418
- package/test/starttls-injection-test.js +0 -181
- package/test/tag-correlation-test.js +0 -333
- package/test/timer-policy-test.js +0 -227
- package/test/token-parser-test.js +0 -456
- package/test/tools-test.js +0 -2013
- package/test/unhandled-rejection-test.js +0 -661
|
@@ -0,0 +1,1496 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* eslint no-control-regex:0 */
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.noop = exports.MAX_UINT32_DIGITS = exports.EXPANDED_RANGE_LIMIT = exports.AuthenticationFailure = void 0;
|
|
8
|
+
exports.buildConnectionError = buildConnectionError;
|
|
9
|
+
exports.restampConnectionError = restampConnectionError;
|
|
10
|
+
exports.guardedPromise = guardedPromise;
|
|
11
|
+
exports.guardedReject = guardedReject;
|
|
12
|
+
exports.clearTimer = clearTimer;
|
|
13
|
+
exports.unrefTimer = unrefTimer;
|
|
14
|
+
exports.logConnectionError = logConnectionError;
|
|
15
|
+
exports.isRev2Active = isRev2Active;
|
|
16
|
+
exports.hasCapability = hasCapability;
|
|
17
|
+
exports.buildStatusQueryAttributes = buildStatusQueryAttributes;
|
|
18
|
+
exports.encodePath = encodePath;
|
|
19
|
+
exports.decodePath = decodePath;
|
|
20
|
+
exports.normalizePath = normalizePath;
|
|
21
|
+
exports.comparePaths = comparePaths;
|
|
22
|
+
exports.updateCapabilities = updateCapabilities;
|
|
23
|
+
exports.getStatusCode = getStatusCode;
|
|
24
|
+
exports.getErrorText = getErrorText;
|
|
25
|
+
exports.enhanceCommandError = enhanceCommandError;
|
|
26
|
+
exports.getFolderTree = getFolderTree;
|
|
27
|
+
exports.getFlagColor = getFlagColor;
|
|
28
|
+
exports.getColorFlags = getColorFlags;
|
|
29
|
+
exports.formatMessageResponse = formatMessageResponse;
|
|
30
|
+
exports.processName = processName;
|
|
31
|
+
exports.decodeText = decodeText;
|
|
32
|
+
exports.parseEnvelope = parseEnvelope;
|
|
33
|
+
exports.getStructuredParams = getStructuredParams;
|
|
34
|
+
exports.parseBodystructure = parseBodystructure;
|
|
35
|
+
exports.isDate = isDate;
|
|
36
|
+
exports.toValidDate = toValidDate;
|
|
37
|
+
exports.formatDate = formatDate;
|
|
38
|
+
exports.formatDateTime = formatDateTime;
|
|
39
|
+
exports.formatFlag = formatFlag;
|
|
40
|
+
exports.canUseFlag = canUseFlag;
|
|
41
|
+
exports.isValidSequenceValue = isValidSequenceValue;
|
|
42
|
+
exports.isDecimalString = isDecimalString;
|
|
43
|
+
exports.isUnsafeKey = isUnsafeKey;
|
|
44
|
+
exports.getStringList = getStringList;
|
|
45
|
+
exports.parseBigIntValue = parseBigIntValue;
|
|
46
|
+
exports.parseUintValue = parseUintValue;
|
|
47
|
+
exports.expandRange = expandRange;
|
|
48
|
+
exports.getDecoder = getDecoder;
|
|
49
|
+
exports.packMessageRange = packMessageRange;
|
|
50
|
+
const libmime_1 = __importDefault(require("libmime"));
|
|
51
|
+
const charsets_js_1 = require("./charsets.js");
|
|
52
|
+
const imap_handler_js_1 = require("./handler/imap-handler.js");
|
|
53
|
+
const node_crypto_1 = require("node:crypto");
|
|
54
|
+
const jp_decoder_js_1 = require("./jp-decoder.js");
|
|
55
|
+
const iconv_lite_1 = __importDefault(require("iconv-lite"));
|
|
56
|
+
var errors_js_1 = require("./errors.js");
|
|
57
|
+
Object.defineProperty(exports, "AuthenticationFailure", { enumerable: true, get: function () { return errors_js_1.AuthenticationFailure; } });
|
|
58
|
+
const FLAG_COLORS = ['red', 'orange', 'yellow', 'green', 'blue', 'purple', 'grey'];
|
|
59
|
+
// Error codes that only mean the connection is no longer usable. See logConnectionError().
|
|
60
|
+
const CONNECTION_GONE_CODES = new Set(['NoConnection', 'EConnectionClosed', 'StateLogout']);
|
|
61
|
+
// Upper bound for expanding server-supplied sequence ranges (see expandRange). 2^24
|
|
62
|
+
// entries in total is far beyond any legitimate mailbox while keeping the worst-case
|
|
63
|
+
// expansion of a hostile range set bounded.
|
|
64
|
+
//
|
|
65
|
+
// Shared bound for expanding server-supplied sequence sets. Exported so other places that
|
|
66
|
+
// expand server sequences (e.g. ESEARCH ALL in commands/search.ts) can apply the same absolute
|
|
67
|
+
// ceiling instead of inventing their own.
|
|
68
|
+
exports.EXPANDED_RANGE_LIMIT = 0x1000000;
|
|
69
|
+
// Digit bounds for untrusted numeric values in server responses. UIDs, UIDVALIDITY and
|
|
70
|
+
// message counts are 32-bit unsigned (nz-number in the RFC 9051 grammar, so at most 10
|
|
71
|
+
// digits); MODSEQ and number64 values are 63-bit unsigned (RFC 7162, RFC 9051), at most
|
|
72
|
+
// 19 digits. The bound is checked before BigInt()/Number(): a response line may carry up
|
|
73
|
+
// to maxLineLength digits, and BigInt() on a multi-megabyte digit run costs hundreds of
|
|
74
|
+
// milliseconds of non-yielding CPU.
|
|
75
|
+
//
|
|
76
|
+
// MAX_UINT32_DIGITS is exported so call sites can ask for the tighter 32-bit bound where the
|
|
77
|
+
// grammar requires it (UID, UIDVALIDITY, message counts) instead of the parsers' 63-bit default.
|
|
78
|
+
exports.MAX_UINT32_DIGITS = 10;
|
|
79
|
+
const MAX_NUMBER64_DIGITS = 19;
|
|
80
|
+
// Object keys that reach through the prototype chain when assigned to, or resolve to an
|
|
81
|
+
// inherited member when read. Server-controlled strings become keys in several places
|
|
82
|
+
// (STATUS items, QUOTA resources, BODYSTRUCTURE parameters, FETCH body part names), so they
|
|
83
|
+
// all consult this one set rather than each carrying its own list.
|
|
84
|
+
const UNSAFE_OBJECT_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
85
|
+
// Extensions that RFC 9051 (IMAP4rev2) folds into the base protocol (Appendix E).
|
|
86
|
+
// When IMAP4rev2 is active, these are available even without their own capability
|
|
87
|
+
// token. BINARY is deliberately excluded - RFC 9051 only folds in the FETCH side,
|
|
88
|
+
// which fetch.ts handles with its own isRev2Active check, while the APPEND side
|
|
89
|
+
// stays gated on the BINARY token. SPECIAL-USE is a partial fold: Appendix E only
|
|
90
|
+
// folds in the special-use mailbox attributes, not the RFC 6154 LIST selection and
|
|
91
|
+
// RETURN options - the only call sites that act on this entry are in list.ts,
|
|
92
|
+
// where a staged retry ladder recovers if a rev2-only server rejects the RETURN
|
|
93
|
+
// option. The set mirrors the rest of the Appendix E list in full, including
|
|
94
|
+
// entries no call site consults yet, so any future capability check gets the
|
|
95
|
+
// rev2 folding for free.
|
|
96
|
+
const IMAP4REV2_FOLDED_CAPABILITIES = new Set([
|
|
97
|
+
'ENABLE',
|
|
98
|
+
'ESEARCH',
|
|
99
|
+
'IDLE',
|
|
100
|
+
'LIST-EXTENDED',
|
|
101
|
+
'LIST-STATUS',
|
|
102
|
+
'LITERAL-',
|
|
103
|
+
'MOVE',
|
|
104
|
+
'NAMESPACE',
|
|
105
|
+
'SASL-IR',
|
|
106
|
+
'SEARCHRES',
|
|
107
|
+
'SPECIAL-USE',
|
|
108
|
+
'STATUS=SIZE',
|
|
109
|
+
'UIDPLUS',
|
|
110
|
+
'UNSELECT'
|
|
111
|
+
]);
|
|
112
|
+
// Deliberate no-op, used as the observer a guarded promise attaches to its own rejection.
|
|
113
|
+
const noop = () => { };
|
|
114
|
+
exports.noop = noop;
|
|
115
|
+
// The fields buildConnectionError() stamps to say *where* a connection error was rejected, as
|
|
116
|
+
// opposed to what went wrong. restampConnectionError() clears them, because they belong to the
|
|
117
|
+
// site that built the error rather than to the failure it describes.
|
|
118
|
+
const CONNECTION_ERROR_SITE_KEYS = ['rejectedFrom', 'command', 'path'];
|
|
119
|
+
/**
|
|
120
|
+
* Builds an error describing a connection that is gone, stamped so it can be traced back to
|
|
121
|
+
* where it came from: the connection id always travels on it, and each site names itself
|
|
122
|
+
* through `meta` (`rejectedFrom`, plus the command or mailbox path it belongs to).
|
|
123
|
+
*
|
|
124
|
+
* A stack trace only records where an error was built, and close() hands a rejection to every
|
|
125
|
+
* pending request and every queued lock in the same tick, so without these an error that
|
|
126
|
+
* reaches a global unhandledRejection handler arrives with nothing that identifies the
|
|
127
|
+
* connection it came from, let alone which of the rejected promises carried it.
|
|
128
|
+
*
|
|
129
|
+
* Takes the connection id rather than the connection, so the stamping stays in one place
|
|
130
|
+
* without every caller having to be a full ImapFlow instance.
|
|
131
|
+
*
|
|
132
|
+
* @param cid - Connection id
|
|
133
|
+
* @param code - Error code, e.g. 'NoConnection'
|
|
134
|
+
* @param message - Error message
|
|
135
|
+
* @param meta - Fields to stamp on the error
|
|
136
|
+
* @returns The stamped error
|
|
137
|
+
*/
|
|
138
|
+
function buildConnectionError(cid, code, message, meta) {
|
|
139
|
+
const error = new Error(message);
|
|
140
|
+
error.code = code;
|
|
141
|
+
error.cid = cid;
|
|
142
|
+
if (meta) {
|
|
143
|
+
Object.assign(error, meta);
|
|
144
|
+
}
|
|
145
|
+
return error;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Re-stamps an existing connection error for a different rejection site.
|
|
149
|
+
*
|
|
150
|
+
* The same failure can be handed to more than one promise - close() rejects the in-flight
|
|
151
|
+
* command, and runIdle() then rejects everything queued behind it - and each of those is a
|
|
152
|
+
* separate promise with a separate consumer. Sharing one error object reports whichever of
|
|
153
|
+
* them escapes under the first site's marker, which is the attribution these markers exist to
|
|
154
|
+
* give.
|
|
155
|
+
*
|
|
156
|
+
* Everything describing *what went wrong* is carried over, because a re-stamped error reaches
|
|
157
|
+
* user code through run() and callers branch on `responseStatus`, `serverResponseCode` and
|
|
158
|
+
* friends. Everything describing *where it was rejected* is dropped, because the new site owns
|
|
159
|
+
* those and a leftover `command` from the previous site is exactly as misleading as a leftover
|
|
160
|
+
* `rejectedFrom`. The original travels on as `cause`.
|
|
161
|
+
*
|
|
162
|
+
* @param err - The error being re-stamped
|
|
163
|
+
* @param meta - Fields for the new site, e.g. { rejectedFrom: 'preCheckWaiter' }
|
|
164
|
+
* @returns A separate error describing the same failure at the new site
|
|
165
|
+
*/
|
|
166
|
+
function restampConnectionError(err, meta) {
|
|
167
|
+
let error = buildConnectionError(err.cid, err.code, err.message, err);
|
|
168
|
+
for (let key of CONNECTION_ERROR_SITE_KEYS) {
|
|
169
|
+
delete error[key];
|
|
170
|
+
}
|
|
171
|
+
if (meta) {
|
|
172
|
+
Object.assign(error, meta);
|
|
173
|
+
}
|
|
174
|
+
error.cause = err;
|
|
175
|
+
return error;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Creates a promise whose rejection is observed as soon as it exists.
|
|
179
|
+
*
|
|
180
|
+
* close() rejects every promise it owns - the in-flight and queued commands, the pending
|
|
181
|
+
* connect(), the queued mailbox locks, the waiters for an IDLE break - synchronously, from a
|
|
182
|
+
* socket event. A consumer that only reaches its `await` a microtask later has not attached a
|
|
183
|
+
* handler yet at the moment Node decides whether the rejection was observed, and the whole
|
|
184
|
+
* worker dies on the resulting unhandledRejection. The pre-attached observer settles that
|
|
185
|
+
* question; the rejection still propagates normally to whoever awaits the returned promise.
|
|
186
|
+
*
|
|
187
|
+
* Creation and guarding are one call because splitting them is what actually goes wrong: the
|
|
188
|
+
* guard was hand-attached at three of the four sites and the fourth (the IDLE-break waiter)
|
|
189
|
+
* went unguarded, on exactly the path a server BYE takes.
|
|
190
|
+
*
|
|
191
|
+
* @param executor - Promise executor, (resolve, reject) => {}
|
|
192
|
+
* @returns The promise, with its rejection already observed
|
|
193
|
+
*/
|
|
194
|
+
function guardedPromise(executor) {
|
|
195
|
+
let promise = new Promise(executor);
|
|
196
|
+
promise.catch(exports.noop);
|
|
197
|
+
return promise;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The already-rejected form of guardedPromise(), for a call that has to hand back a rejected
|
|
201
|
+
* promise rather than throw.
|
|
202
|
+
*
|
|
203
|
+
* @param error - Rejection reason
|
|
204
|
+
* @returns Rejected promise, with its rejection already observed
|
|
205
|
+
*/
|
|
206
|
+
function guardedReject(error) {
|
|
207
|
+
let promise = Promise.reject(error);
|
|
208
|
+
promise.catch(exports.noop);
|
|
209
|
+
return promise;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Detaches a background timer from the event loop, so it cannot keep the process alive on its
|
|
213
|
+
* own. Applied to every background timer (auto-IDLE, IDLE restart, fallback polling, throttle
|
|
214
|
+
* back-off, held-lock diagnostics); connection and greeting deadlines are deliberately left
|
|
215
|
+
* attached, because a caller is waiting for connect() to settle.
|
|
216
|
+
*
|
|
217
|
+
* @param timer - Timer handle returned by setTimeout
|
|
218
|
+
* @returns The same timer handle
|
|
219
|
+
*/
|
|
220
|
+
/**
|
|
221
|
+
* Clears a timer that may already have been dropped. `clearTimeout()` accepts undefined but not
|
|
222
|
+
* null, and the connection nulls its timer fields once cleared, so every site clears through here.
|
|
223
|
+
*
|
|
224
|
+
* @param timer - Timer handle returned by setTimeout, or null/undefined when none is armed
|
|
225
|
+
*/
|
|
226
|
+
function clearTimer(timer) {
|
|
227
|
+
if (timer) {
|
|
228
|
+
clearTimeout(timer);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function unrefTimer(timer) {
|
|
232
|
+
/* c8 ignore next 3 */ // node timers always expose unref(); the guard covers replaced globals in tests
|
|
233
|
+
if (timer && typeof timer.unref === 'function') {
|
|
234
|
+
timer.unref();
|
|
235
|
+
}
|
|
236
|
+
return timer;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Logs a failure from background connection work at the level its cause deserves.
|
|
240
|
+
*
|
|
241
|
+
* Background work (IDLE sessions, polling timers, auto-IDLE) is interrupted by every normal
|
|
242
|
+
* disconnect, so a rejection carrying one of the CONNECTION_GONE_CODES is expected rather
|
|
243
|
+
* than notable and goes to debug. The three codes describe the same situation reached
|
|
244
|
+
* through different guards: write() throws NoConnection or StateLogout, exec() rejects
|
|
245
|
+
* EConnectionClosed for the window where the socket is destroyed but close() has not run
|
|
246
|
+
* yet, and close() rejects pending requests with NoConnection.
|
|
247
|
+
*
|
|
248
|
+
* A connection error carrying `reason` is the exception. That field holds the server's
|
|
249
|
+
* untagged BYE text ("Too many simultaneous connections", "Account is disabled"), which
|
|
250
|
+
* serverBye() only records - this log call is the one place it becomes visible, and it is
|
|
251
|
+
* usually the answer to why a client is reconnecting in a loop. Those stay at warn.
|
|
252
|
+
*
|
|
253
|
+
* Shared so the classification cannot drift between the call sites that make this decision.
|
|
254
|
+
*
|
|
255
|
+
* @param connection - IMAP connection instance
|
|
256
|
+
* @param msg - What failed, so the entries stay distinguishable in the log
|
|
257
|
+
* @param err - The error to log
|
|
258
|
+
*/
|
|
259
|
+
function logConnectionError(connection, msg, err) {
|
|
260
|
+
let routine = !!err && CONNECTION_GONE_CODES.has(err.code) && !err.reason;
|
|
261
|
+
connection.log[routine ? 'debug' : 'warn']({ msg, err, cid: connection.id });
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Checks whether IMAP4rev2 semantics are active for the connection: either the
|
|
265
|
+
* client enabled IMAP4rev2 explicitly, or the server is rev2-only (advertises
|
|
266
|
+
* IMAP4rev2 without IMAP4rev1), in which case rev2 is the base protocol without
|
|
267
|
+
* any ENABLE (RFC 9051 Appendix A). UTF-8 mailbox names apply in both cases.
|
|
268
|
+
*
|
|
269
|
+
* @param connection - IMAP connection instance
|
|
270
|
+
* @returns True if IMAP4rev2 semantics apply to this session
|
|
271
|
+
*/
|
|
272
|
+
function isRev2Active(connection) {
|
|
273
|
+
return connection.enabled.has('IMAP4REV2') || (connection.capabilities.has('IMAP4rev2') && !connection.capabilities.has('IMAP4rev1'));
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Checks a capability, accounting for extensions that RFC 9051 folds into base
|
|
277
|
+
* IMAP4rev2. Falls back to the plain capability lookup on IMAP4rev1 sessions,
|
|
278
|
+
* so behavior against rev1 servers is unchanged.
|
|
279
|
+
*
|
|
280
|
+
* @param connection - IMAP connection instance
|
|
281
|
+
* @param capability - Capability name, e.g. 'UIDPLUS'
|
|
282
|
+
* @returns True if the capability (or its rev2-folded equivalent) is available
|
|
283
|
+
*/
|
|
284
|
+
function hasCapability(connection, capability) {
|
|
285
|
+
if (connection.capabilities.has(capability)) {
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
return IMAP4REV2_FOLDED_CAPABILITIES.has(capability) && isRev2Active(connection);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Builds the attribute list for a STATUS request - the standalone STATUS command
|
|
292
|
+
* or the LIST-STATUS return option - from a status query object. Items the current
|
|
293
|
+
* session cannot request (RECENT under IMAP4rev2, HIGHESTMODSEQ without CONDSTORE)
|
|
294
|
+
* are silently dropped.
|
|
295
|
+
*
|
|
296
|
+
* @param connection - IMAP connection instance
|
|
297
|
+
* @param statusQuery - Status data items to request, e.g. {messages: true}
|
|
298
|
+
* @returns Attribute token list for the command compiler
|
|
299
|
+
*/
|
|
300
|
+
function buildStatusQueryAttributes(connection, statusQuery) {
|
|
301
|
+
let attributes = [];
|
|
302
|
+
let query = (statusQuery || {});
|
|
303
|
+
Object.keys(query).forEach(key => {
|
|
304
|
+
if (!query[key]) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
switch (key.toUpperCase()) {
|
|
308
|
+
case 'MESSAGES':
|
|
309
|
+
case 'UIDNEXT':
|
|
310
|
+
case 'UIDVALIDITY':
|
|
311
|
+
case 'UNSEEN':
|
|
312
|
+
attributes.push({ type: 'ATOM', value: key.toUpperCase() });
|
|
313
|
+
break;
|
|
314
|
+
case 'RECENT':
|
|
315
|
+
// RECENT was removed in IMAP4rev2 (RFC 9051) - requesting it from a
|
|
316
|
+
// rev2 session would get the whole STATUS request rejected
|
|
317
|
+
if (!isRev2Active(connection)) {
|
|
318
|
+
attributes.push({ type: 'ATOM', value: key.toUpperCase() });
|
|
319
|
+
}
|
|
320
|
+
break;
|
|
321
|
+
case 'HIGHESTMODSEQ':
|
|
322
|
+
if (connection.capabilities.has('CONDSTORE')) {
|
|
323
|
+
attributes.push({ type: 'ATOM', value: key.toUpperCase() });
|
|
324
|
+
}
|
|
325
|
+
break;
|
|
326
|
+
case 'SIZE':
|
|
327
|
+
// STATUS SIZE requires the STATUS=SIZE extension (RFC 8438), which
|
|
328
|
+
// RFC 9051 folds into base IMAP4rev2
|
|
329
|
+
if (hasCapability(connection, 'STATUS=SIZE')) {
|
|
330
|
+
attributes.push({ type: 'ATOM', value: key.toUpperCase() });
|
|
331
|
+
}
|
|
332
|
+
break;
|
|
333
|
+
case 'DELETED':
|
|
334
|
+
// STATUS DELETED is a base IMAP4rev2 addition (RFC 9051 Appendix E
|
|
335
|
+
// item 3) with no standalone capability - requesting it from a plain
|
|
336
|
+
// rev1 server would get the whole STATUS request rejected. RFC 9208
|
|
337
|
+
// additionally makes it mandatory when QUOTA=RES-MESSAGE is advertised.
|
|
338
|
+
if (isRev2Active(connection) || connection.capabilities.has('QUOTA=RES-MESSAGE')) {
|
|
339
|
+
attributes.push({ type: 'ATOM', value: key.toUpperCase() });
|
|
340
|
+
}
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
return attributes;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Encodes a mailbox path to modified UTF-7 if the server does not support UTF8=ACCEPT.
|
|
348
|
+
*
|
|
349
|
+
* @param connection - IMAP connection instance
|
|
350
|
+
* @param path - Mailbox path to encode
|
|
351
|
+
* @returns Encoded mailbox path
|
|
352
|
+
*/
|
|
353
|
+
function encodePath(connection, path) {
|
|
354
|
+
path = (path || '').toString();
|
|
355
|
+
if (!connection.enabled.has('UTF8=ACCEPT') && !isRev2Active(connection) && /[&\x00-\x08\x0b-\x0c\x0e-\x1f\u0080-\uffff]/.test(path)) {
|
|
356
|
+
try {
|
|
357
|
+
path = iconv_lite_1.default.encode(path, 'utf-7-imap').toString();
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// ignore, keep name as is
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return path;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Decodes a mailbox path from modified UTF-7 if the server does not support UTF8=ACCEPT.
|
|
367
|
+
*
|
|
368
|
+
* @param connection - IMAP connection instance
|
|
369
|
+
* @param path - Mailbox path to decode
|
|
370
|
+
* @returns Decoded mailbox path
|
|
371
|
+
*/
|
|
372
|
+
function decodePath(connection, path) {
|
|
373
|
+
path = (path || '').toString();
|
|
374
|
+
if (!connection.enabled.has('UTF8=ACCEPT') && !isRev2Active(connection) && /[&]/.test(path)) {
|
|
375
|
+
try {
|
|
376
|
+
path = iconv_lite_1.default.decode(Buffer.from(path), 'utf-7-imap').toString();
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
// ignore, keep name as is
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return path;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Normalizes a mailbox path by joining array segments with the namespace delimiter,
|
|
386
|
+
* uppercasing INBOX, and prepending the namespace prefix if needed.
|
|
387
|
+
*
|
|
388
|
+
* @param connection - IMAP connection instance
|
|
389
|
+
* @param path - Mailbox path or array of path segments
|
|
390
|
+
* @param skipNamespace - If true, skips prepending the namespace prefix
|
|
391
|
+
* @returns Normalized mailbox path
|
|
392
|
+
*/
|
|
393
|
+
function normalizePath(connection, path, skipNamespace) {
|
|
394
|
+
if (Array.isArray(path)) {
|
|
395
|
+
path = path.join((connection.namespace && connection.namespace.delimiter) || '');
|
|
396
|
+
}
|
|
397
|
+
if (path.toUpperCase() === 'INBOX') {
|
|
398
|
+
// inbox is not case sensitive
|
|
399
|
+
return 'INBOX';
|
|
400
|
+
}
|
|
401
|
+
// ensure namespace prefix if needed
|
|
402
|
+
if (!skipNamespace && connection.namespace && connection.namespace.prefix && !path.startsWith(connection.namespace.prefix)) {
|
|
403
|
+
path = connection.namespace.prefix + path;
|
|
404
|
+
}
|
|
405
|
+
return path;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Compares two mailbox paths for equality after normalization.
|
|
409
|
+
*
|
|
410
|
+
* @param connection - IMAP connection instance
|
|
411
|
+
* @param a - First mailbox path
|
|
412
|
+
* @param b - Second mailbox path
|
|
413
|
+
* @returns True if the paths are equal after normalization
|
|
414
|
+
*/
|
|
415
|
+
function comparePaths(connection, a, b) {
|
|
416
|
+
if (!a || !b) {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
return normalizePath(connection, a) === normalizePath(connection, b);
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Parses a capability response list into a Map of capability names to values.
|
|
423
|
+
*
|
|
424
|
+
* @param list - Array of capability objects from IMAP response
|
|
425
|
+
* @returns Map of capability names to `true` or numeric values
|
|
426
|
+
*/
|
|
427
|
+
function updateCapabilities(list) {
|
|
428
|
+
let map = new Map();
|
|
429
|
+
if (list && Array.isArray(list)) {
|
|
430
|
+
list.forEach(val => {
|
|
431
|
+
// any entry can be a parsed NIL
|
|
432
|
+
if (!val || typeof val.value !== 'string') {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
let capability = val.value.toUpperCase().trim();
|
|
436
|
+
if (capability === 'IMAP4REV1') {
|
|
437
|
+
map.set('IMAP4rev1', true);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (capability === 'IMAP4REV2') {
|
|
441
|
+
map.set('IMAP4rev2', true);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (capability.startsWith('APPENDLIMIT=')) {
|
|
445
|
+
let splitPos = capability.indexOf('=');
|
|
446
|
+
map.set('APPENDLIMIT', parseUintValue(capability.substr(splitPos + 1)) || 0);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
map.set(capability, true);
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
return map;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Extracts the IMAP response status code (e.g. AUTHENTICATIONFAILED, NONEXISTENT)
|
|
456
|
+
* from a parsed server response.
|
|
457
|
+
*
|
|
458
|
+
* @param response - Parsed IMAP server response
|
|
459
|
+
* @returns Uppercase status code string, or false if not found
|
|
460
|
+
*/
|
|
461
|
+
function getStatusCode(response) {
|
|
462
|
+
return response &&
|
|
463
|
+
typeof response === 'object' &&
|
|
464
|
+
response.attributes &&
|
|
465
|
+
response.attributes[0] &&
|
|
466
|
+
response.attributes[0].section &&
|
|
467
|
+
response.attributes[0].section[0] &&
|
|
468
|
+
typeof response.attributes[0].section[0].value === 'string'
|
|
469
|
+
? response.attributes[0].section[0].value.toUpperCase().trim()
|
|
470
|
+
: false;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Compiles an IMAP response object back into a human-readable string.
|
|
474
|
+
*
|
|
475
|
+
* @param response - Parsed IMAP server response
|
|
476
|
+
* @returns Compiled response text, or false if no response
|
|
477
|
+
*/
|
|
478
|
+
async function getErrorText(response) {
|
|
479
|
+
if (!response) {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
return (await (0, imap_handler_js_1.compiler)(response)).toString();
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
// The wire encoder refuses values that cannot be expressed as a valid IMAP
|
|
487
|
+
// string, which is what keeps user-supplied data from breaking out of a
|
|
488
|
+
// command. A server response is not held to that: the parser deliberately
|
|
489
|
+
// tolerates stray bytes inside an OK/NO/BAD atom, and those bytes then have
|
|
490
|
+
// no valid re-encoding. This text is diagnostic, so fall back to the logging
|
|
491
|
+
// encoder rather than replacing the server's error with an encoding failure.
|
|
492
|
+
return (await (0, imap_handler_js_1.compiler)(response, { isLogging: true })).toString();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Enhances an IMAP command error with the server response code and text.
|
|
497
|
+
*
|
|
498
|
+
* @param err - Error object with a `response` property
|
|
499
|
+
* @returns The enhanced error with `serverResponseCode` and string `response`
|
|
500
|
+
*/
|
|
501
|
+
async function enhanceCommandError(err) {
|
|
502
|
+
let errorCode = getStatusCode(err.response);
|
|
503
|
+
if (errorCode) {
|
|
504
|
+
err.serverResponseCode = errorCode;
|
|
505
|
+
}
|
|
506
|
+
err.response = await getErrorText(err.response);
|
|
507
|
+
return err;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Converts a flat list of mailbox folders into a tree structure.
|
|
511
|
+
*
|
|
512
|
+
* @param folders - Array of folder objects from LIST/LSUB response
|
|
513
|
+
* @returns Tree structure with a `root` flag and nested `folders` arrays
|
|
514
|
+
*/
|
|
515
|
+
function getFolderTree(folders) {
|
|
516
|
+
let tree = {
|
|
517
|
+
root: true,
|
|
518
|
+
folders: []
|
|
519
|
+
};
|
|
520
|
+
let getTreeNode = (parents) => {
|
|
521
|
+
let node = tree;
|
|
522
|
+
if (!parents || !parents.length) {
|
|
523
|
+
return node;
|
|
524
|
+
}
|
|
525
|
+
for (let parent of parents) {
|
|
526
|
+
let cur = node.folders && node.folders.find(folder => folder.name === parent);
|
|
527
|
+
if (cur) {
|
|
528
|
+
node = cur;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return node;
|
|
532
|
+
};
|
|
533
|
+
for (let folder of folders) {
|
|
534
|
+
let parent = getTreeNode(folder.parent);
|
|
535
|
+
// see if entry already exists
|
|
536
|
+
let existing = parent.folders && parent.folders.find(existing => existing.name === folder.name);
|
|
537
|
+
if (existing) {
|
|
538
|
+
// update values
|
|
539
|
+
existing.name = folder.name;
|
|
540
|
+
existing.flags = folder.flags;
|
|
541
|
+
existing.path = folder.path;
|
|
542
|
+
existing.subscribed = !!folder.subscribed;
|
|
543
|
+
existing.listed = !!folder.listed;
|
|
544
|
+
existing.status = folder.status;
|
|
545
|
+
if (folder.specialUse) {
|
|
546
|
+
existing.specialUse = folder.specialUse;
|
|
547
|
+
}
|
|
548
|
+
if (folder.flags.has('\\Noselect')) {
|
|
549
|
+
existing.disabled = true;
|
|
550
|
+
}
|
|
551
|
+
if (folder.flags.has('\\HasChildren') && !existing.folders) {
|
|
552
|
+
existing.folders = [];
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
// create new
|
|
557
|
+
let data = {
|
|
558
|
+
name: folder.name,
|
|
559
|
+
flags: folder.flags,
|
|
560
|
+
path: folder.path,
|
|
561
|
+
subscribed: !!folder.subscribed,
|
|
562
|
+
listed: !!folder.listed,
|
|
563
|
+
status: folder.status
|
|
564
|
+
};
|
|
565
|
+
if (folder.delimiter) {
|
|
566
|
+
data.delimiter = folder.delimiter;
|
|
567
|
+
}
|
|
568
|
+
if (folder.specialUse) {
|
|
569
|
+
data.specialUse = folder.specialUse;
|
|
570
|
+
}
|
|
571
|
+
if (folder.flags.has('\\Noselect')) {
|
|
572
|
+
data.disabled = true;
|
|
573
|
+
}
|
|
574
|
+
if (folder.flags.has('\\HasChildren')) {
|
|
575
|
+
data.folders = [];
|
|
576
|
+
}
|
|
577
|
+
if (!parent.folders) {
|
|
578
|
+
parent.folders = [];
|
|
579
|
+
}
|
|
580
|
+
parent.folders.push(data);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return tree;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Derives a flag color name from a message's flags Set using Apple Mail color flag rules.
|
|
587
|
+
*
|
|
588
|
+
* @param flags - Message flags Set
|
|
589
|
+
* @returns Color name (e.g. 'red', 'orange') or null if not flagged
|
|
590
|
+
*/
|
|
591
|
+
function getFlagColor(flags) {
|
|
592
|
+
if (!flags.has('\\Flagged')) {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
// Apple Mail encodes flag colors as a 3-bit value using $MailFlagBit0/1/2 keywords.
|
|
596
|
+
// Bit 0 = 1, Bit 1 = 2, Bit 2 = 4. The resulting integer (0-6) indexes into FLAG_COLORS:
|
|
597
|
+
// 0=red, 1=orange, 2=yellow, 3=green, 4=blue, 5=purple, 6=grey.
|
|
598
|
+
// Value 7 (all bits set) is unused; defaults to red.
|
|
599
|
+
const bit0 = flags.has('$MailFlagBit0') ? 1 : 0;
|
|
600
|
+
const bit1 = flags.has('$MailFlagBit1') ? 2 : 0;
|
|
601
|
+
const bit2 = flags.has('$MailFlagBit2') ? 4 : 0;
|
|
602
|
+
const color = bit0 | bit1 | bit2; // eslint-disable-line no-bitwise
|
|
603
|
+
return FLAG_COLORS[color] ?? 'red'; // default to red for the unused \b111
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Converts a color name to the corresponding flag add/remove operations for Apple Mail color flags.
|
|
607
|
+
*
|
|
608
|
+
* @param color - Color name (e.g. 'red', 'orange', 'yellow')
|
|
609
|
+
* @returns Object with `add` and `remove` arrays of flag strings, or null if invalid color
|
|
610
|
+
*/
|
|
611
|
+
function getColorFlags(color) {
|
|
612
|
+
// Reverse mapping from a color name to the Apple Mail $MailFlagBit0/1/2 flags.
|
|
613
|
+
// Returns an object with 'add' and 'remove' arrays so the caller can STORE +FLAGS/-FLAGS.
|
|
614
|
+
const colorCode = color ? FLAG_COLORS.indexOf(color.toString().toLowerCase().trim()) : null;
|
|
615
|
+
if (colorCode === null || colorCode < 0) {
|
|
616
|
+
if (colorCode === null) {
|
|
617
|
+
// Remove color: remove \Flagged and all MailFlagBit flags
|
|
618
|
+
return { add: [], remove: ['\\Flagged', '$MailFlagBit0', '$MailFlagBit1', '$MailFlagBit2'] };
|
|
619
|
+
}
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
// Decompose color index back into its 3-bit representation
|
|
623
|
+
let result = { add: ['\\Flagged'], remove: [] };
|
|
624
|
+
for (let i = 0; i < 3; i++) {
|
|
625
|
+
// eslint-disable-next-line no-bitwise
|
|
626
|
+
if (colorCode & (1 << i)) {
|
|
627
|
+
result.add.push(`$MailFlagBit${i}`);
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
result.remove.push(`$MailFlagBit${i}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return result;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Formats a raw untagged FETCH response into a structured message object.
|
|
637
|
+
*
|
|
638
|
+
* @param untagged - Parsed untagged IMAP response
|
|
639
|
+
* @param mailbox - Current mailbox state object
|
|
640
|
+
* @returns Formatted message object with properties like seq, uid, flags, envelope, etc.
|
|
641
|
+
*/
|
|
642
|
+
async function formatMessageResponse(untagged, mailbox) {
|
|
643
|
+
let map = {};
|
|
644
|
+
// The sequence number indexes into mailbox state, so an unusable one is dropped rather
|
|
645
|
+
// than coerced to NaN or Infinity
|
|
646
|
+
map.seq = parseUintValue(untagged.command, exports.MAX_UINT32_DIGITS) || undefined;
|
|
647
|
+
let key;
|
|
648
|
+
let attributes = ((untagged.attributes && untagged.attributes[1]) || []);
|
|
649
|
+
for (let i = 0, len = attributes.length; i < len; i++) {
|
|
650
|
+
let attribute = attributes[i];
|
|
651
|
+
if (i % 2 === 0) {
|
|
652
|
+
key = (await (0, imap_handler_js_1.compiler)({
|
|
653
|
+
attributes: [attribute]
|
|
654
|
+
}))
|
|
655
|
+
.toString()
|
|
656
|
+
.toLowerCase()
|
|
657
|
+
.replace(/<\d+(\.\d+)?>$/, '');
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
/* c8 ignore start */ // defensive: key is always a string produced by the compiler above
|
|
661
|
+
if (typeof key !== 'string') {
|
|
662
|
+
// should not happen
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
/* c8 ignore stop */
|
|
666
|
+
let getString = (attribute) => {
|
|
667
|
+
if (!attribute) {
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
if (typeof attribute.value === 'string') {
|
|
671
|
+
return attribute.value;
|
|
672
|
+
}
|
|
673
|
+
if (Buffer.isBuffer(attribute.value)) {
|
|
674
|
+
return attribute.value.toString();
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
let getBuffer = (attribute) => {
|
|
678
|
+
if (!attribute) {
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
if (Buffer.isBuffer(attribute.value)) {
|
|
682
|
+
return attribute.value;
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
// NIL (parsed as null) and other non-array values yield an empty array, so callers
|
|
686
|
+
// can safely index into the result. RFC 8474 allows e.g. `THREADID NIL` when the
|
|
687
|
+
// server has no thread relation to report.
|
|
688
|
+
let getArray = (attribute) => getStringList(attribute);
|
|
689
|
+
// Counts, sizes and UIDs are written into mailbox state and into range
|
|
690
|
+
// computations, so only a bounded decimal run is usable - see parseUintValue().
|
|
691
|
+
let getUint = (attribute, maxDigits) => parseUintValue(getString(attribute), maxDigits);
|
|
692
|
+
switch (key) {
|
|
693
|
+
case 'body[]':
|
|
694
|
+
case 'binary[]':
|
|
695
|
+
map.source = getBuffer(attribute);
|
|
696
|
+
break;
|
|
697
|
+
case 'uid':
|
|
698
|
+
// A UID feeds mailbox.uidNext one line below, and from there every range
|
|
699
|
+
// computation, so an unusable one is dropped rather than coerced
|
|
700
|
+
map.uid = getUint(attribute, exports.MAX_UINT32_DIGITS) || undefined;
|
|
701
|
+
// If the UID we just saw is >= the mailbox's uidNext, bump uidNext.
|
|
702
|
+
// This keeps the local uidNext estimate current without requiring a
|
|
703
|
+
// separate STATUS command, handling cases where new messages arrived
|
|
704
|
+
// since the last SELECT/EXAMINE.
|
|
705
|
+
if (map.uid && (!mailbox.uidNext || mailbox.uidNext <= map.uid)) {
|
|
706
|
+
mailbox.uidNext = map.uid + 1;
|
|
707
|
+
}
|
|
708
|
+
break;
|
|
709
|
+
case 'modseq': {
|
|
710
|
+
// BigInt() throws on a non-numeric or missing value, and the throw
|
|
711
|
+
// drops the whole message from the result set - so a malformed
|
|
712
|
+
// MODSEQ from the server must be skipped, not surfaced.
|
|
713
|
+
let modseq = parseBigIntValue(getArray(attribute)[0]);
|
|
714
|
+
if (modseq === false) {
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
map.modseq = modseq;
|
|
718
|
+
// Similarly, keep the local highestModseq estimate up to date.
|
|
719
|
+
// This is critical for CONDSTORE/QRESYNC delta syncing.
|
|
720
|
+
if (map.modseq && (!mailbox.highestModseq || mailbox.highestModseq < map.modseq)) {
|
|
721
|
+
mailbox.highestModseq = map.modseq;
|
|
722
|
+
}
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
case 'emailid':
|
|
726
|
+
// OBJECTID extension (RFC 8474): server-assigned stable email identifier
|
|
727
|
+
map.emailId = getArray(attribute)[0];
|
|
728
|
+
break;
|
|
729
|
+
case 'x-gm-msgid':
|
|
730
|
+
// Gmail extension: X-GM-MSGID is Gmail's unique message ID.
|
|
731
|
+
// Mapped to the same emailId field as OBJECTID for a unified API,
|
|
732
|
+
// but this is a Gmail-specific numeric string, not an RFC 8474 ObjectID.
|
|
733
|
+
map.emailId = getString(attribute);
|
|
734
|
+
break;
|
|
735
|
+
case 'threadid':
|
|
736
|
+
map.threadId = getArray(attribute)[0];
|
|
737
|
+
break;
|
|
738
|
+
case 'x-gm-thrid':
|
|
739
|
+
map.threadId = getString(attribute);
|
|
740
|
+
break;
|
|
741
|
+
case 'x-gm-labels':
|
|
742
|
+
map.labels = new Set(getArray(attribute));
|
|
743
|
+
break;
|
|
744
|
+
case 'rfc822.size':
|
|
745
|
+
map.size = getUint(attribute) || 0;
|
|
746
|
+
break;
|
|
747
|
+
case 'flags':
|
|
748
|
+
map.flags = new Set(getArray(attribute));
|
|
749
|
+
break;
|
|
750
|
+
case 'envelope':
|
|
751
|
+
map.envelope = parseEnvelope(attribute);
|
|
752
|
+
break;
|
|
753
|
+
case 'bodystructure':
|
|
754
|
+
map.bodyStructure = parseBodystructure(attribute);
|
|
755
|
+
break;
|
|
756
|
+
case 'internaldate': {
|
|
757
|
+
let value = getString(attribute);
|
|
758
|
+
let date = new Date(value);
|
|
759
|
+
if (date.toString() === 'Invalid Date') {
|
|
760
|
+
map.internalDate = value;
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
map.internalDate = date;
|
|
764
|
+
}
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
default: {
|
|
768
|
+
let match = key.match(/(body|binary)\[/i);
|
|
769
|
+
if (match) {
|
|
770
|
+
let partKey = key.replace(/^(body|binary)\[|]$/gi, '');
|
|
771
|
+
partKey = partKey.replace(/\.fields.*$/g, '');
|
|
772
|
+
let value = getBuffer(attribute);
|
|
773
|
+
if (partKey === 'header') {
|
|
774
|
+
map.headers = value;
|
|
775
|
+
break;
|
|
776
|
+
}
|
|
777
|
+
if (!map.bodyParts) {
|
|
778
|
+
map.bodyParts = new Map();
|
|
779
|
+
}
|
|
780
|
+
map.bodyParts.set(partKey, value);
|
|
781
|
+
if (match[1].toLowerCase() === 'binary') {
|
|
782
|
+
// The part arrived via FETCH BINARY (RFC 3516, FETCH side folded
|
|
783
|
+
// into IMAP4rev2), so the server has already removed the
|
|
784
|
+
// content-transfer-encoding - consumers must not decode it again.
|
|
785
|
+
// Recorded from the actual response, not predicted from the
|
|
786
|
+
// request, so it stays correct even if a server answers a BINARY
|
|
787
|
+
// request with a BODY response or vice versa.
|
|
788
|
+
if (!map.binaryParts) {
|
|
789
|
+
map.binaryParts = new Set();
|
|
790
|
+
}
|
|
791
|
+
map.binaryParts.add(partKey);
|
|
792
|
+
}
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
break;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (map.emailId || map.uid) {
|
|
800
|
+
// define account unique ID for this email
|
|
801
|
+
// normalize path to use ascii, so we would always get the same ID
|
|
802
|
+
let path = mailbox.path;
|
|
803
|
+
if (/[\u0080-\uffff]/.test(path)) {
|
|
804
|
+
try {
|
|
805
|
+
path = iconv_lite_1.default.encode(path, 'utf-7-imap').toString();
|
|
806
|
+
}
|
|
807
|
+
catch {
|
|
808
|
+
// ignore
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
// Non-cryptographic identifier: MD5 is used only to derive a stable, compact
|
|
812
|
+
// account-unique id from non-secret data (path:uidValidity:uid). No security
|
|
813
|
+
// property (collision/preimage resistance, secrecy) is relied upon, so a fast
|
|
814
|
+
// hash is the appropriate choice here - not a security-sensitive use.
|
|
815
|
+
map.id =
|
|
816
|
+
map.emailId ||
|
|
817
|
+
(0, node_crypto_1.createHash)('md5')
|
|
818
|
+
.update([path, mailbox.uidValidity?.toString() || '', map.uid.toString()].join(':'))
|
|
819
|
+
.digest('hex');
|
|
820
|
+
}
|
|
821
|
+
if (map.flags) {
|
|
822
|
+
let flagColor = getFlagColor(map.flags);
|
|
823
|
+
if (flagColor) {
|
|
824
|
+
map.flagColor = flagColor;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return map;
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Strips surrounding double quotes from a name string.
|
|
831
|
+
*
|
|
832
|
+
* @param name - Raw name string potentially wrapped in quotes
|
|
833
|
+
* @returns Name with surrounding quotes removed
|
|
834
|
+
*/
|
|
835
|
+
function processName(name) {
|
|
836
|
+
let value = (name || '').toString();
|
|
837
|
+
if (value.length > 2 && value.at(0) === '"' && value.at(-1) === '"') {
|
|
838
|
+
value = value.slice(1, -1);
|
|
839
|
+
}
|
|
840
|
+
return value;
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Decodes an ENVELOPE text field for display: encoded words first, then the
|
|
844
|
+
* surrounding quotes some servers leave in place.
|
|
845
|
+
*
|
|
846
|
+
* @param value - Raw field value from an ENVELOPE response
|
|
847
|
+
* @returns Decoded, unquoted text
|
|
848
|
+
*/
|
|
849
|
+
function decodeText(value) {
|
|
850
|
+
return processName(libmime_1.default.decodeWords(value));
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Parses a raw IMAP ENVELOPE response into a structured envelope object.
|
|
854
|
+
*
|
|
855
|
+
* @param entry - Raw envelope data array from IMAP response
|
|
856
|
+
* @returns Parsed envelope with date, subject, from, to, cc, bcc, messageId, etc.
|
|
857
|
+
*/
|
|
858
|
+
function parseEnvelope(entry) {
|
|
859
|
+
let getStrValue = (obj) => {
|
|
860
|
+
if (!obj) {
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
if (typeof obj.value === 'string') {
|
|
864
|
+
return obj.value;
|
|
865
|
+
}
|
|
866
|
+
if (Buffer.isBuffer(obj.value)) {
|
|
867
|
+
return obj.value.toString();
|
|
868
|
+
}
|
|
869
|
+
/* c8 ignore next */ // defensive: envelope tokens are always string/Buffer/NIL, never another type
|
|
870
|
+
return obj.value;
|
|
871
|
+
};
|
|
872
|
+
let processAddresses = function (list) {
|
|
873
|
+
/* c8 ignore next 2 */ // defensive: processAddresses is only called with non-empty arrays, so the [] fallback is unreachable
|
|
874
|
+
return []
|
|
875
|
+
.concat(list || [])
|
|
876
|
+
.map(addr => {
|
|
877
|
+
if (!addr) {
|
|
878
|
+
// A NIL entry inside an address list: skip it instead of
|
|
879
|
+
// throwing on the dereference and dropping the message
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
let entry = addr;
|
|
883
|
+
let name = decodeText(getStrValue(entry[0]));
|
|
884
|
+
let mailbox = (getStrValue(entry[2]) || '');
|
|
885
|
+
let host = (getStrValue(entry[3]) || '');
|
|
886
|
+
if (!host) {
|
|
887
|
+
// RFC 9051 7.5.2: a NIL host field marks RFC 5322 group syntax, it is not
|
|
888
|
+
// an empty domain. A non-NIL mailbox then holds the group name phrase, a
|
|
889
|
+
// NIL one closes the group. Joining the fields anyway would invent an
|
|
890
|
+
// address that never appeared in the message, eg. "undisclosed-recipients@",
|
|
891
|
+
// so surface the group name as a display name and leave the address empty.
|
|
892
|
+
// End-of-group markers carry neither and the filter below drops them.
|
|
893
|
+
// The mirror case, a NIL mailbox with a host, is left alone on purpose:
|
|
894
|
+
// the grammar gives it no meaning, so a server sending it is simply
|
|
895
|
+
// malformed rather than signalling anything we could act on.
|
|
896
|
+
return { name: name || (mailbox && decodeText(mailbox)), address: '' };
|
|
897
|
+
}
|
|
898
|
+
return { name, address: `${mailbox}@${host}` };
|
|
899
|
+
})
|
|
900
|
+
.filter((addr) => !!(addr && (addr.name || addr.address)));
|
|
901
|
+
}, envelope = {};
|
|
902
|
+
if (entry[0] && entry[0].value) {
|
|
903
|
+
let date = new Date(getStrValue(entry[0]));
|
|
904
|
+
if (date.toString() === 'Invalid Date') {
|
|
905
|
+
envelope.date = getStrValue(entry[0]);
|
|
906
|
+
}
|
|
907
|
+
else {
|
|
908
|
+
envelope.date = date;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
if (entry[1] && entry[1].value) {
|
|
912
|
+
envelope.subject = libmime_1.default.decodeWords(getStrValue(entry[1]));
|
|
913
|
+
}
|
|
914
|
+
if (Array.isArray(entry[2]) && entry[2].length) {
|
|
915
|
+
envelope.from = processAddresses(entry[2]);
|
|
916
|
+
}
|
|
917
|
+
if (Array.isArray(entry[3]) && entry[3].length) {
|
|
918
|
+
envelope.sender = processAddresses(entry[3]);
|
|
919
|
+
}
|
|
920
|
+
if (Array.isArray(entry[4]) && entry[4].length) {
|
|
921
|
+
envelope.replyTo = processAddresses(entry[4]);
|
|
922
|
+
}
|
|
923
|
+
if (Array.isArray(entry[5]) && entry[5].length) {
|
|
924
|
+
envelope.to = processAddresses(entry[5]);
|
|
925
|
+
}
|
|
926
|
+
if (Array.isArray(entry[6]) && entry[6].length) {
|
|
927
|
+
envelope.cc = processAddresses(entry[6]);
|
|
928
|
+
}
|
|
929
|
+
if (Array.isArray(entry[7]) && entry[7].length) {
|
|
930
|
+
envelope.bcc = processAddresses(entry[7]);
|
|
931
|
+
}
|
|
932
|
+
if (entry[8] && entry[8].value) {
|
|
933
|
+
/* c8 ignore next */ // the guard ensures getStrValue is truthy here, so the '' fallback is unreachable
|
|
934
|
+
envelope.inReplyTo = (getStrValue(entry[8]) || '').toString().trim();
|
|
935
|
+
}
|
|
936
|
+
if (entry[9] && entry[9].value) {
|
|
937
|
+
/* c8 ignore next */ // the guard ensures getStrValue is truthy here, so the '' fallback is unreachable
|
|
938
|
+
envelope.messageId = (getStrValue(entry[9]) || '').toString().trim();
|
|
939
|
+
}
|
|
940
|
+
return envelope;
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Parses structured MIME parameter arrays (including RFC 2231 continuations)
|
|
944
|
+
* into a flat key-value object.
|
|
945
|
+
*
|
|
946
|
+
* @param arr - Raw parameter array from BODYSTRUCTURE response
|
|
947
|
+
* @returns Key-value object of decoded parameters
|
|
948
|
+
*/
|
|
949
|
+
function getStructuredParams(arr) {
|
|
950
|
+
let key;
|
|
951
|
+
// Continuation parts are collected as {charset, values} objects before being joined
|
|
952
|
+
// back into strings, so the map holds both shapes while it is being built
|
|
953
|
+
let params = {};
|
|
954
|
+
// BODYSTRUCTURE parameters come as flat key/value pairs: [key1, val1, key2, val2, ...]
|
|
955
|
+
[].concat(arr || []).forEach((val, j) => {
|
|
956
|
+
if (j % 2) {
|
|
957
|
+
// Parameter names are server-controlled. The load-bearing check is the one in
|
|
958
|
+
// the continuation pass below, where the value is an object; here the value is
|
|
959
|
+
// always a string, which the __proto__ setter ignores anyway.
|
|
960
|
+
if (!isUnsafeKey(key)) {
|
|
961
|
+
params[key] = libmime_1.default.decodeWords(((val && val.value) || '').toString());
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
key = ((val && val.value) || '').toString().toLowerCase();
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
// Detect RFC 2231 encoded filenames that were placed in the plain 'filename' param
|
|
969
|
+
// instead of 'filename*'. The pattern charset'language'encoded_value indicates encoding.
|
|
970
|
+
if (params.filename && !params['filename*'] && /^[a-z\-_0-9]+'[a-z]*'[^'\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uffff]+/.test(params.filename)) {
|
|
971
|
+
// seems like encoded value
|
|
972
|
+
let [encoding, , encodedValue] = params.filename.split("'");
|
|
973
|
+
if ((0, charsets_js_1.resolveCharset)(encoding)) {
|
|
974
|
+
params['filename*'] = `${encoding}''${encodedValue}`;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
// RFC 2231 parameter continuations: parameters like filename*0, filename*1, etc.
|
|
978
|
+
// are split parts of a single value. Parameters ending with '*' contain charset info.
|
|
979
|
+
// This pass collects continuation parts and groups them by their base key name.
|
|
980
|
+
Object.keys(params).forEach(key => {
|
|
981
|
+
let actualKey;
|
|
982
|
+
let nr;
|
|
983
|
+
let value;
|
|
984
|
+
// Match keys ending with *N or *N* (where N is the continuation index)
|
|
985
|
+
let match = key.match(/\*((\d+)\*?)?$/);
|
|
986
|
+
if (!match) {
|
|
987
|
+
// nothing to do here, does not seem like a continuation param
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
actualKey = key.substr(0, match.index).toLowerCase();
|
|
991
|
+
nr = Number(match[2]) || 0;
|
|
992
|
+
if (isUnsafeKey(actualKey)) {
|
|
993
|
+
// A continuation key like "__proto__*0*" would group under "__proto__":
|
|
994
|
+
// params['__proto__'] resolves to Object.prototype, so the grouping
|
|
995
|
+
// writes below would mutate it (process-wide pollution). Drop the part.
|
|
996
|
+
delete params[key];
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (!params[actualKey] || typeof params[actualKey] !== 'object') {
|
|
1000
|
+
params[actualKey] = {
|
|
1001
|
+
charset: false,
|
|
1002
|
+
values: []
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
value = params[key];
|
|
1006
|
+
// The first segment (*0*) may contain charset and language: charset'language'value
|
|
1007
|
+
if (nr === 0 && match[0].at(-1) === '*' && (match = value.match(/^([^']*)'[^']*'(.*)$/))) {
|
|
1008
|
+
params[actualKey].charset = match[1] || 'utf-8';
|
|
1009
|
+
value = match[2];
|
|
1010
|
+
}
|
|
1011
|
+
params[actualKey].values.push({ nr, value });
|
|
1012
|
+
// remove the old reference
|
|
1013
|
+
delete params[key];
|
|
1014
|
+
});
|
|
1015
|
+
// Reassemble split RFC 2231 strings by sorting continuation parts and joining them.
|
|
1016
|
+
// For charset-encoded values, convert URL-encoded (%XX) sequences to MIME quoted-printable
|
|
1017
|
+
// format (=?charset?Q?...?=) so libmime.decodeWords can decode them to Unicode.
|
|
1018
|
+
Object.keys(params).forEach(key => {
|
|
1019
|
+
let value;
|
|
1020
|
+
if (params[key] && Array.isArray(params[key].values)) {
|
|
1021
|
+
value = params[key].values
|
|
1022
|
+
.sort((a, b) => a.nr - b.nr)
|
|
1023
|
+
.map((val) => (val && val.value) || '')
|
|
1024
|
+
.join('');
|
|
1025
|
+
if (params[key].charset) {
|
|
1026
|
+
// Convert URL encoding (%AB) to MIME quoted-printable (=AB) by:
|
|
1027
|
+
// 1. Escaping QP-special chars (=, ?, _, space) as %XX
|
|
1028
|
+
// 2. Replacing all '%' with '=' to switch from URL encoding to QP encoding
|
|
1029
|
+
// 3. Wrapping in =?charset?Q?...?= for libmime to decode
|
|
1030
|
+
params[key] = libmime_1.default.decodeWords('=?' +
|
|
1031
|
+
params[key].charset +
|
|
1032
|
+
'?Q?' +
|
|
1033
|
+
value
|
|
1034
|
+
// fix invalidly encoded chars
|
|
1035
|
+
.replace(/[=?_\s]/g, s => {
|
|
1036
|
+
if (s === ' ') {
|
|
1037
|
+
return '_';
|
|
1038
|
+
}
|
|
1039
|
+
let c = s.charCodeAt(0).toString(16);
|
|
1040
|
+
return '%' + (c.length < 2 ? '0' : '') + c;
|
|
1041
|
+
})
|
|
1042
|
+
// change from urlencoding to percent encoding
|
|
1043
|
+
.replace(/%/g, '=') +
|
|
1044
|
+
'?=');
|
|
1045
|
+
}
|
|
1046
|
+
else {
|
|
1047
|
+
params[key] = libmime_1.default.decodeWords(value);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
return params;
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Parses a raw IMAP BODYSTRUCTURE response into a structured tree of body parts.
|
|
1055
|
+
*
|
|
1056
|
+
* @param entry - Raw BODYSTRUCTURE data array from IMAP response
|
|
1057
|
+
* @returns Parsed body structure tree with part numbers, types, parameters, and child nodes
|
|
1058
|
+
*/
|
|
1059
|
+
function parseBodystructure(entry) {
|
|
1060
|
+
// Recursively walks the BODYSTRUCTURE tree, building MIME part numbers.
|
|
1061
|
+
// Part numbers follow the IMAP dot-notation: "1", "1.1", "2.3", etc.
|
|
1062
|
+
// The root multipart has no part number; its children start at 1.
|
|
1063
|
+
let walk = (node, path) => {
|
|
1064
|
+
path = path || [];
|
|
1065
|
+
let curNode = {}, i = 0, part = 0;
|
|
1066
|
+
// Build the dot-separated part number from the path array (e.g., [1,2] -> "1.2")
|
|
1067
|
+
if (path.length) {
|
|
1068
|
+
curNode.part = path.join('.');
|
|
1069
|
+
}
|
|
1070
|
+
// multipart: first elements are arrays (child body parts), followed by the subtype string
|
|
1071
|
+
if (Array.isArray(node[0])) {
|
|
1072
|
+
curNode.childNodes = [];
|
|
1073
|
+
// Each child array is a nested body part; increment part counter for each
|
|
1074
|
+
while (Array.isArray(node[i])) {
|
|
1075
|
+
curNode.childNodes.push(walk(node[i], path.concat(++part)));
|
|
1076
|
+
i++;
|
|
1077
|
+
}
|
|
1078
|
+
// multipart type
|
|
1079
|
+
curNode.type = 'multipart/' + ((node[i++] || {}).value || '').toString().toLowerCase();
|
|
1080
|
+
// extension data (not available for BODY requests)
|
|
1081
|
+
// body parameter parenthesized list
|
|
1082
|
+
if (i < node.length - 1) {
|
|
1083
|
+
if (node[i]) {
|
|
1084
|
+
curNode.parameters = getStructuredParams(node[i]);
|
|
1085
|
+
}
|
|
1086
|
+
i++;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
else {
|
|
1090
|
+
// content type
|
|
1091
|
+
curNode.type = [
|
|
1092
|
+
((node[i++] || {}).value || '').toString().toLowerCase(),
|
|
1093
|
+
((node[i++] || {}).value || '').toString().toLowerCase()
|
|
1094
|
+
].join('/');
|
|
1095
|
+
// body parameter parenthesized list
|
|
1096
|
+
if (node[i]) {
|
|
1097
|
+
curNode.parameters = getStructuredParams(node[i]);
|
|
1098
|
+
}
|
|
1099
|
+
i++;
|
|
1100
|
+
// id
|
|
1101
|
+
if (node[i]) {
|
|
1102
|
+
curNode.id = (node[i].value || '').toString();
|
|
1103
|
+
}
|
|
1104
|
+
i++;
|
|
1105
|
+
// description
|
|
1106
|
+
if (node[i]) {
|
|
1107
|
+
curNode.description = (node[i].value || '').toString();
|
|
1108
|
+
}
|
|
1109
|
+
i++;
|
|
1110
|
+
// encoding
|
|
1111
|
+
if (node[i]) {
|
|
1112
|
+
curNode.encoding = (node[i].value || '').toString().toLowerCase();
|
|
1113
|
+
}
|
|
1114
|
+
i++;
|
|
1115
|
+
// size
|
|
1116
|
+
if (node[i]) {
|
|
1117
|
+
curNode.size = Number(node[i].value || 0) || 0;
|
|
1118
|
+
}
|
|
1119
|
+
i++;
|
|
1120
|
+
if (curNode.type === 'message/rfc822') {
|
|
1121
|
+
// message/rfc822 is special in IMAP BODYSTRUCTURE: after the standard
|
|
1122
|
+
// 7 fields, it includes an embedded envelope, a nested bodystructure,
|
|
1123
|
+
// and a line count for the encapsulated message.
|
|
1124
|
+
// envelope of the encapsulated message
|
|
1125
|
+
if (node[i]) {
|
|
1126
|
+
/* c8 ignore next */ // node[i] is truthy inside this guard, so the [] fallback is unreachable
|
|
1127
|
+
curNode.envelope = parseEnvelope([].concat(node[i] || []));
|
|
1128
|
+
}
|
|
1129
|
+
i++;
|
|
1130
|
+
if (node[i]) {
|
|
1131
|
+
curNode.childNodes = [
|
|
1132
|
+
// The nested bodystructure reuses the same path (not path+1) because
|
|
1133
|
+
// the encapsulated message shares the part number with its wrapper.
|
|
1134
|
+
// Distinction is via suffixes: path.MIME = wrapper headers,
|
|
1135
|
+
// path.HEADER = encapsulated message headers.
|
|
1136
|
+
walk(node[i], path)
|
|
1137
|
+
];
|
|
1138
|
+
}
|
|
1139
|
+
i++;
|
|
1140
|
+
// line count
|
|
1141
|
+
if (node[i]) {
|
|
1142
|
+
curNode.lineCount = Number(node[i].value || 0) || 0;
|
|
1143
|
+
}
|
|
1144
|
+
i++;
|
|
1145
|
+
}
|
|
1146
|
+
if (/^text\//.test(curNode.type)) {
|
|
1147
|
+
// Per RFC 3501, text/* parts include an additional line count field after size.
|
|
1148
|
+
// However, some servers omit this field, producing 11 elements instead of 12+.
|
|
1149
|
+
// NB! some less known servers do not include the line count value
|
|
1150
|
+
// length should be 12+
|
|
1151
|
+
if (node.length === 11 && Array.isArray(node[i + 1]) && !Array.isArray(node[i + 2])) {
|
|
1152
|
+
// invalid structure, disposition params are shifted - skip the line count
|
|
1153
|
+
}
|
|
1154
|
+
else {
|
|
1155
|
+
// correct structure, line count number is provided
|
|
1156
|
+
if (node[i]) {
|
|
1157
|
+
curNode.lineCount = Number(node[i].value || 0) || 0;
|
|
1158
|
+
}
|
|
1159
|
+
i++;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
// extension data (not available for BODY requests)
|
|
1163
|
+
// md5
|
|
1164
|
+
if (i < node.length - 1) {
|
|
1165
|
+
if (node[i]) {
|
|
1166
|
+
curNode.md5 = (node[i].value || '').toString().toLowerCase();
|
|
1167
|
+
}
|
|
1168
|
+
i++;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
// the following are shared extension values (for both multipart and non-multipart parts)
|
|
1172
|
+
// not available for BODY requests
|
|
1173
|
+
// body disposition
|
|
1174
|
+
if (i < node.length - 1) {
|
|
1175
|
+
let disposition = node[i];
|
|
1176
|
+
if (Array.isArray(disposition) && disposition.length) {
|
|
1177
|
+
curNode.disposition = ((disposition[0] && disposition[0].value) || '').toString().toLowerCase();
|
|
1178
|
+
if (Array.isArray(disposition[1])) {
|
|
1179
|
+
curNode.dispositionParameters = getStructuredParams(disposition[1]);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
i++;
|
|
1183
|
+
}
|
|
1184
|
+
// body language
|
|
1185
|
+
if (i < node.length - 1) {
|
|
1186
|
+
if (node[i]) {
|
|
1187
|
+
/* c8 ignore next */ // node[i] is truthy inside this guard, so the [] fallback is unreachable
|
|
1188
|
+
curNode.language = [].concat(node[i] || []).map(val => ((val && val.value) || '').toString().toLowerCase());
|
|
1189
|
+
}
|
|
1190
|
+
i++;
|
|
1191
|
+
}
|
|
1192
|
+
// body location
|
|
1193
|
+
// NB! defined as a "string list" in RFC3501 but replaced in errata document with "string"
|
|
1194
|
+
// Errata: http://www.rfc-editor.org/errata_search.php?rfc=3501
|
|
1195
|
+
if (i < node.length - 1) {
|
|
1196
|
+
if (node[i]) {
|
|
1197
|
+
curNode.location = (node[i].value || '').toString();
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return curNode;
|
|
1201
|
+
};
|
|
1202
|
+
return walk(entry);
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Checks if a value is a Date object.
|
|
1206
|
+
*
|
|
1207
|
+
* @param obj - Value to check
|
|
1208
|
+
* @returns True if the value is a Date object
|
|
1209
|
+
*/
|
|
1210
|
+
function isDate(obj) {
|
|
1211
|
+
return Object.prototype.toString.call(obj) === '[object Date]';
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Converts a value to a valid Date object, or returns null.
|
|
1215
|
+
*
|
|
1216
|
+
* @param value - Date object or date string to convert
|
|
1217
|
+
* @returns Valid Date object, or null if conversion fails
|
|
1218
|
+
*/
|
|
1219
|
+
function toValidDate(value) {
|
|
1220
|
+
if (!value) {
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
if (typeof value === 'string') {
|
|
1224
|
+
value = new Date(value);
|
|
1225
|
+
}
|
|
1226
|
+
if (!isDate(value) || value.toString() === 'Invalid Date') {
|
|
1227
|
+
return null;
|
|
1228
|
+
}
|
|
1229
|
+
return value;
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Formats a date value into IMAP date format (DD-Mon-YYYY).
|
|
1233
|
+
*
|
|
1234
|
+
* @param value - Date to format
|
|
1235
|
+
* @returns Formatted date string, or undefined if invalid
|
|
1236
|
+
*/
|
|
1237
|
+
function formatDate(value) {
|
|
1238
|
+
let date = toValidDate(value);
|
|
1239
|
+
if (!date) {
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
let dateParts = date.toISOString().substr(0, 10).split('-');
|
|
1243
|
+
dateParts.reverse();
|
|
1244
|
+
let months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
1245
|
+
dateParts[1] = months[Number(dateParts[1]) - 1];
|
|
1246
|
+
return dateParts.join('-');
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Formats a date value into IMAP date-time format (DD-Mon-YYYY HH:MM:SS +0000).
|
|
1250
|
+
*
|
|
1251
|
+
* @param value - Date to format
|
|
1252
|
+
* @returns Formatted date-time string, or undefined if invalid
|
|
1253
|
+
*/
|
|
1254
|
+
function formatDateTime(value) {
|
|
1255
|
+
let date = toValidDate(value);
|
|
1256
|
+
if (!date) {
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
let dateStr = formatDate(date).replace(/^0/, ' '); //starts with date-day-fixed with leading 0 replaced by SP
|
|
1260
|
+
let timeStr = date.toISOString().substr(11, 8);
|
|
1261
|
+
return `${dateStr} ${timeStr} +0000`;
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Normalizes a flag string. Returns false for non-settable flags (e.g. \Recent),
|
|
1265
|
+
* and capitalizes system flags properly.
|
|
1266
|
+
*
|
|
1267
|
+
* @param flag - Flag string to normalize
|
|
1268
|
+
* @returns Normalized flag string, or false if the flag cannot be set
|
|
1269
|
+
*/
|
|
1270
|
+
function formatFlag(flag) {
|
|
1271
|
+
switch (flag.toLowerCase()) {
|
|
1272
|
+
case '\\recent':
|
|
1273
|
+
// can not set or remove
|
|
1274
|
+
return false;
|
|
1275
|
+
case '\\seen':
|
|
1276
|
+
case '\\answered':
|
|
1277
|
+
case '\\flagged':
|
|
1278
|
+
case '\\deleted':
|
|
1279
|
+
case '\\draft':
|
|
1280
|
+
// normalize capitalization (e.g., "\\seen" -> "\\Seen")
|
|
1281
|
+
return flag.toLowerCase().replace(/^\\./, c => c.toUpperCase());
|
|
1282
|
+
}
|
|
1283
|
+
return flag;
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Checks if a flag can be used in the given mailbox based on permanent flags.
|
|
1287
|
+
*
|
|
1288
|
+
* @param mailbox - Mailbox object with permanentFlags
|
|
1289
|
+
* @param flag - Flag to check
|
|
1290
|
+
* @returns True if the flag is allowed
|
|
1291
|
+
*/
|
|
1292
|
+
function canUseFlag(mailbox, flag) {
|
|
1293
|
+
return !mailbox || !mailbox.permanentFlags || mailbox.permanentFlags.has('\\*') || mailbox.permanentFlags.has(flag);
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Checks that a value is a valid IMAP sequence number or UID: a non-zero
|
|
1297
|
+
* 32-bit unsigned integer (nz-number in the RFC 9051 grammar). Guards range
|
|
1298
|
+
* expansion against untrusted server input such as 'Infinity' or '0:*'.
|
|
1299
|
+
*
|
|
1300
|
+
* @param value - Value to check
|
|
1301
|
+
* @returns True if the value is a valid sequence number/UID
|
|
1302
|
+
*/
|
|
1303
|
+
function isValidSequenceValue(value) {
|
|
1304
|
+
return Number.isSafeInteger(value) && value > 0 && value <= 0xffffffff;
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Checks that an untrusted response value is a pure decimal digit run no longer than
|
|
1308
|
+
* the given bound.
|
|
1309
|
+
*
|
|
1310
|
+
* `!isNaN(value)` is not usable for this: it also passes '1e5', ' 12 ', '0x10' and
|
|
1311
|
+
* 'Infinity'. BigInt() throws on all of them and Number() silently returns a value the
|
|
1312
|
+
* grammar never allowed, so both are wrong in a response handler that is only trying to
|
|
1313
|
+
* read one field. The length bound is checked before the pattern so an arbitrarily long
|
|
1314
|
+
* digit run is rejected without any conversion work.
|
|
1315
|
+
*
|
|
1316
|
+
* @param value - Raw value from the response.
|
|
1317
|
+
* @param maxDigits - Maximum number of digits accepted.
|
|
1318
|
+
* @returns True if the value is a decimal string within the bound.
|
|
1319
|
+
*/
|
|
1320
|
+
function isDecimalString(value, maxDigits) {
|
|
1321
|
+
return typeof value === 'string' && value.length > 0 && value.length <= maxDigits && /^[0-9]+$/.test(value);
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Checks whether a server-supplied string is unsafe to use as a key on a plain object.
|
|
1325
|
+
* Assigning "__proto__" writes through the prototype setter instead of creating an own
|
|
1326
|
+
* property, and reading "constructor" or "prototype" resolves to an inherited member.
|
|
1327
|
+
*
|
|
1328
|
+
* @param key - Candidate key from a server response.
|
|
1329
|
+
* @returns True if the key must not be used.
|
|
1330
|
+
*/
|
|
1331
|
+
function isUnsafeKey(key) {
|
|
1332
|
+
return UNSAFE_OBJECT_KEYS.has(key);
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Reads a parsed attribute list of atoms or strings (a flag list, a capability list) into
|
|
1336
|
+
* an array of strings. Any element can be a parsed NIL, and the list itself can be NIL,
|
|
1337
|
+
* so both levels are guarded here rather than at each call site.
|
|
1338
|
+
*
|
|
1339
|
+
* @param list - Parsed attribute list from a response.
|
|
1340
|
+
* @returns The string values, in order, with unusable entries dropped.
|
|
1341
|
+
*/
|
|
1342
|
+
function getStringList(list) {
|
|
1343
|
+
if (!Array.isArray(list)) {
|
|
1344
|
+
return [];
|
|
1345
|
+
}
|
|
1346
|
+
return list.map(entry => (entry && typeof entry.value === 'string' ? entry.value : false)).filter(entry => entry);
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Parses an untrusted decimal value from a server response into a BigInt.
|
|
1350
|
+
*
|
|
1351
|
+
* @param value - Raw value from the response.
|
|
1352
|
+
* @param maxDigits - Maximum number of digits accepted. Defaults to MAX_NUMBER64_DIGITS.
|
|
1353
|
+
* @returns The parsed value, or false when it is not usable.
|
|
1354
|
+
*/
|
|
1355
|
+
function parseBigIntValue(value, maxDigits) {
|
|
1356
|
+
if (!isDecimalString(value, maxDigits || MAX_NUMBER64_DIGITS)) {
|
|
1357
|
+
return false;
|
|
1358
|
+
}
|
|
1359
|
+
return BigInt(value);
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Parses an untrusted decimal value from a server response into a Number. Values beyond
|
|
1363
|
+
* the safe integer range are rejected rather than rounded: a silently rounded count or
|
|
1364
|
+
* UID corrupts every range computation derived from it.
|
|
1365
|
+
*
|
|
1366
|
+
* @param value - Raw value from the response.
|
|
1367
|
+
* @param maxDigits - Maximum number of digits accepted. Defaults to MAX_NUMBER64_DIGITS.
|
|
1368
|
+
* @returns The parsed value, or false when it is not usable.
|
|
1369
|
+
*/
|
|
1370
|
+
function parseUintValue(value, maxDigits) {
|
|
1371
|
+
if (!isDecimalString(value, maxDigits || MAX_NUMBER64_DIGITS)) {
|
|
1372
|
+
return false;
|
|
1373
|
+
}
|
|
1374
|
+
let num = Number(value);
|
|
1375
|
+
return Number.isSafeInteger(num) ? num : false;
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Expands an IMAP sequence range string (e.g. "1:3,5,7:9") into an array of numbers.
|
|
1379
|
+
*
|
|
1380
|
+
* Entries with endpoints that are not valid nz-numbers are skipped - the input
|
|
1381
|
+
* may come from an untrusted server, and 'Infinity' or similar garbage would
|
|
1382
|
+
* otherwise loop without bound. The whole set is expanded to at most
|
|
1383
|
+
* EXPANDED_RANGE_LIMIT entries in total: legitimate responses never reach the limit
|
|
1384
|
+
* (the mailbox would need that many messages), while hostile input is cut off
|
|
1385
|
+
* instead of exhausting memory. The total is capped, not just each range -
|
|
1386
|
+
* otherwise "1:16777216,1:16777216,..." would multiply the per-range bound by an
|
|
1387
|
+
* unbounded number of ranges.
|
|
1388
|
+
*
|
|
1389
|
+
* @param range - IMAP sequence range string
|
|
1390
|
+
* @returns Array of expanded sequence numbers
|
|
1391
|
+
*/
|
|
1392
|
+
function expandRange(range) {
|
|
1393
|
+
let result = [];
|
|
1394
|
+
// Callers pass whatever the response parser produced for the sequence set, and a
|
|
1395
|
+
// malformed response can leave that as `false` (e.g. a VANISHED response carrying
|
|
1396
|
+
// only the (EARLIER) tag). Nothing to expand then, and throwing here would abort
|
|
1397
|
+
// the handler for the rest of the response.
|
|
1398
|
+
if (typeof range !== 'string') {
|
|
1399
|
+
return result;
|
|
1400
|
+
}
|
|
1401
|
+
for (let entry of range.split(',')) {
|
|
1402
|
+
if (result.length >= exports.EXPANDED_RANGE_LIMIT) {
|
|
1403
|
+
break;
|
|
1404
|
+
}
|
|
1405
|
+
entry = entry.trim();
|
|
1406
|
+
let colon = entry.indexOf(':');
|
|
1407
|
+
if (colon < 0) {
|
|
1408
|
+
let value = Number(entry);
|
|
1409
|
+
if (isValidSequenceValue(value)) {
|
|
1410
|
+
result.push(value);
|
|
1411
|
+
}
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
let first = Number(entry.substr(0, colon));
|
|
1415
|
+
let second = Number(entry.substr(colon + 1));
|
|
1416
|
+
if (!isValidSequenceValue(first) || !isValidSequenceValue(second)) {
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
if (first === second) {
|
|
1420
|
+
result.push(first);
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1423
|
+
// Remaining total budget doubles as the per-range bound
|
|
1424
|
+
let remaining = exports.EXPANDED_RANGE_LIMIT - result.length;
|
|
1425
|
+
if (first < second) {
|
|
1426
|
+
let last = Math.min(second, first + remaining - 1);
|
|
1427
|
+
for (let i = first; i <= last; i++) {
|
|
1428
|
+
result.push(i);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
else {
|
|
1432
|
+
let last = Math.max(second, first - remaining + 1);
|
|
1433
|
+
for (let i = first; i >= last; i--) {
|
|
1434
|
+
result.push(i);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
return result;
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Returns a stream decoder for the given charset. Uses a special Japanese
|
|
1442
|
+
* charset decoder for JIS/ISO-2022-JP, otherwise delegates to iconv-lite.
|
|
1443
|
+
*
|
|
1444
|
+
* @param charset - Character set name. Defaults to 'ascii'.
|
|
1445
|
+
* @param maxBytes - Bound for the bytes the decoder may buffer. Only
|
|
1446
|
+
* relevant for the Japanese decoder, which must buffer its whole input before
|
|
1447
|
+
* it can decode: without the bound a server could defeat a caller's maxBytes
|
|
1448
|
+
* download limit simply by labelling the part with a Japanese charset.
|
|
1449
|
+
* @returns A stream decoder (Transform stream) for the charset
|
|
1450
|
+
*/
|
|
1451
|
+
function getDecoder(charset, maxBytes) {
|
|
1452
|
+
charset = (charset || 'ascii').toString().trim().toLowerCase();
|
|
1453
|
+
if (/^jis|^iso-?2022-?jp|^euc-?jp/.test(charset)) {
|
|
1454
|
+
// special case not supported by iconv-lite
|
|
1455
|
+
return new jp_decoder_js_1.JPDecoder(charset, maxBytes);
|
|
1456
|
+
}
|
|
1457
|
+
return iconv_lite_1.default.decodeStream(charset);
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Packs an array of message sequence numbers into a compact IMAP range string
|
|
1461
|
+
* (e.g. [1,2,3,5,7,8] becomes "1:3,5,7:8").
|
|
1462
|
+
*
|
|
1463
|
+
* @param list - Sequence number or array of sequence numbers
|
|
1464
|
+
* @returns Packed IMAP sequence range string
|
|
1465
|
+
*/
|
|
1466
|
+
function packMessageRange(list) {
|
|
1467
|
+
let items;
|
|
1468
|
+
if (!Array.isArray(list)) {
|
|
1469
|
+
items = [].concat(list || []);
|
|
1470
|
+
}
|
|
1471
|
+
else {
|
|
1472
|
+
items = list;
|
|
1473
|
+
}
|
|
1474
|
+
if (!items.length) {
|
|
1475
|
+
return '';
|
|
1476
|
+
}
|
|
1477
|
+
// Deduplicate before sorting so that repeated values do not produce
|
|
1478
|
+
// overlapping/non-canonical tokens (e.g. [1,1,2,3] -> "1:3", not "1,1:3").
|
|
1479
|
+
items = Array.from(new Set(items)).sort((a, b) => a - b);
|
|
1480
|
+
let last = items[items.length - 1];
|
|
1481
|
+
let result = [[last]];
|
|
1482
|
+
for (let i = items.length - 2; i >= 0; i--) {
|
|
1483
|
+
if (items[i] === items[i + 1] - 1) {
|
|
1484
|
+
result[0].unshift(items[i]);
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
result.unshift([items[i]]);
|
|
1488
|
+
}
|
|
1489
|
+
let parts = result.map(item => {
|
|
1490
|
+
if (item.length === 1) {
|
|
1491
|
+
return item[0];
|
|
1492
|
+
}
|
|
1493
|
+
return item.shift() + ':' + item.pop();
|
|
1494
|
+
});
|
|
1495
|
+
return parts.join(',');
|
|
1496
|
+
}
|