@mavogel/awscdk-rootmail 0.1.25 → 0.1.27
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/.jsii +9 -9
- package/lib/rootmail.js +1 -1
- package/lib/ses-receive.js +1 -1
- package/lib/ses-receive.ops-santa-handler.js +2 -2
- package/node_modules/@aws-sdk/checksums/package.json +3 -3
- package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-cjs/index.js +9 -7
- package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-es/schemas/schemas_0.js +11 -6
- package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-types/commands/DescribeDeliveriesCommand.d.ts +3 -0
- package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-types/commands/DescribeDeliveryDestinationsCommand.d.ts +3 -0
- package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-types/commands/DescribeDeliverySourcesCommand.d.ts +3 -0
- package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-types/commands/PutResourcePolicyCommand.d.ts +1 -1
- package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-types/models/models_0.d.ts +27 -1
- package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-types/ts3.4/models/models_0.d.ts +3 -0
- package/node_modules/@aws-sdk/client-cloudwatch-logs/package.json +4 -4
- package/node_modules/@aws-sdk/client-route-53/dist-cjs/index.js +1 -1
- package/node_modules/@aws-sdk/client-route-53/package.json +4 -4
- package/node_modules/@aws-sdk/client-s3/dist-cjs/index.js +1 -1
- package/node_modules/@aws-sdk/client-s3/package.json +7 -7
- package/node_modules/@aws-sdk/client-ses/dist-cjs/index.js +1 -1
- package/node_modules/@aws-sdk/client-ses/package.json +4 -4
- package/node_modules/@aws-sdk/client-ssm/dist-cjs/index.js +1 -1
- package/node_modules/@aws-sdk/client-ssm/package.json +4 -4
- package/node_modules/@aws-sdk/core/dist-cjs/submodules/protocols/index.js +94 -1589
- package/node_modules/@aws-sdk/core/dist-es/submodules/protocols/index.js +1 -7
- package/node_modules/@aws-sdk/core/dist-es/submodules/protocols/json/AwsJsonRpcProtocol.js +1 -3
- package/node_modules/@aws-sdk/core/dist-es/submodules/protocols/json/AwsRestJsonProtocol.js +1 -3
- package/node_modules/@aws-sdk/core/dist-types/submodules/protocols/index.d.ts +2 -8
- package/node_modules/@aws-sdk/core/dist-types/submodules/protocols/json/AwsJson1_0Protocol.d.ts +1 -2
- package/node_modules/@aws-sdk/core/dist-types/submodules/protocols/json/AwsJson1_1Protocol.d.ts +1 -2
- package/node_modules/@aws-sdk/core/dist-types/submodules/protocols/json/AwsJsonRpcProtocol.d.ts +2 -3
- package/node_modules/@aws-sdk/core/dist-types/submodules/protocols/json/AwsRestJsonProtocol.d.ts +2 -3
- package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/index.d.ts +10 -10
- package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/AwsJson1_0Protocol.d.ts +1 -2
- package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/AwsJson1_1Protocol.d.ts +1 -2
- package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/AwsJsonRpcProtocol.d.ts +2 -3
- package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/AwsRestJsonProtocol.d.ts +2 -3
- package/node_modules/@aws-sdk/core/package.json +2 -2
- package/node_modules/@aws-sdk/credential-provider-env/package.json +3 -3
- package/node_modules/@aws-sdk/credential-provider-http/package.json +3 -3
- package/node_modules/@aws-sdk/credential-provider-ini/package.json +10 -10
- package/node_modules/@aws-sdk/credential-provider-login/package.json +4 -4
- package/node_modules/@aws-sdk/credential-provider-node/package.json +8 -8
- package/node_modules/@aws-sdk/credential-provider-process/package.json +3 -3
- package/node_modules/@aws-sdk/credential-provider-sso/package.json +5 -5
- package/node_modules/@aws-sdk/credential-provider-web-identity/package.json +4 -4
- package/node_modules/@aws-sdk/middleware-sdk-s3/package.json +3 -3
- package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/cognito-identity/index.js +1 -1
- package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/signin/index.js +1 -1
- package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sso/index.js +1 -1
- package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sso-oidc/index.js +1 -1
- package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sts/index.js +1 -1
- package/node_modules/@aws-sdk/nested-clients/package.json +3 -3
- package/node_modules/@aws-sdk/token-providers/dist-cjs/index.js +37 -19
- package/node_modules/@aws-sdk/token-providers/dist-es/fromSso.js +37 -19
- package/node_modules/@aws-sdk/token-providers/package.json +4 -4
- package/node_modules/@smithy/core/dist-cjs/submodules/protocols/index.js +1695 -3
- package/node_modules/@smithy/core/dist-es/submodules/protocols/ConfigurableSerdeContext.js +6 -0
- package/node_modules/@smithy/core/dist-es/submodules/protocols/UnionSerde.js +26 -0
- package/node_modules/@smithy/core/dist-es/submodules/protocols/common.js +3 -0
- package/node_modules/@smithy/core/dist-es/submodules/protocols/index.js +8 -0
- package/node_modules/@smithy/core/dist-es/submodules/protocols/json/SmithyRpcV2JsonProtocol.js +102 -0
- package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/codec-v1/JsonShapeDeserializer.js +8 -1
- package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/codec-v1/JsonShapeSerializer.js +8 -1
- package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/codec-v2/JsonShapeDeserializer2.js +13 -2
- package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/codec-v2/JsonShapeSerializer2.js +11 -1
- package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/parseJsonBody.js +16 -1
- package/node_modules/@smithy/core/dist-es/submodules/protocols/writeKey.js +3 -0
- package/node_modules/@smithy/core/dist-types/submodules/protocols/ConfigurableSerdeContext.d.ts +8 -0
- package/node_modules/@smithy/core/dist-types/submodules/protocols/UnionSerde.d.ts +24 -0
- package/node_modules/@smithy/core/dist-types/submodules/protocols/common.d.ts +2 -0
- package/node_modules/@smithy/core/dist-types/submodules/protocols/index.d.ts +9 -0
- package/node_modules/@smithy/core/dist-types/submodules/protocols/json/JsonSettings.d.ts +13 -0
- package/node_modules/@smithy/core/dist-types/submodules/protocols/json/SmithyRpcV2JsonProtocol.d.ts +28 -0
- package/node_modules/{@aws-sdk → @smithy}/core/dist-types/submodules/protocols/json/parseJsonBody.d.ts +16 -0
- package/node_modules/@smithy/core/dist-types/submodules/protocols/writeKey.d.ts +4 -0
- package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/ConfigurableSerdeContext.d.ts +8 -0
- package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/UnionSerde.d.ts +24 -0
- package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/common.d.ts +2 -0
- package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/index.d.ts +9 -0
- package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/json/JsonSettings.d.ts +13 -0
- package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/json/SmithyRpcV2JsonProtocol.d.ts +28 -0
- package/node_modules/{@aws-sdk → @smithy}/core/dist-types/ts3.4/submodules/protocols/json/codec-v1/JsonCodec.d.ts +15 -11
- package/node_modules/{@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json → @smithy/core/dist-types/ts3.4/submodules/protocols/json/codec-v1}/JsonSettings.d.ts +7 -4
- package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/json/codec-v1/JsonShapeDeserializer.d.ts +14 -0
- package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/json/codec-v1/JsonShapeSerializer.d.ts +29 -0
- package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/json/codec-v2/JsonBytesStringAdapter.d.ts +51 -0
- package/node_modules/{@aws-sdk → @smithy}/core/dist-types/ts3.4/submodules/protocols/json/codec-v2/JsonCodec2.d.ts +13 -9
- package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/json/codec-v2/JsonShapeDeserializer2.d.ts +30 -0
- package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/json/codec-v2/JsonShapeSerializer2.d.ts +60 -0
- package/node_modules/{@aws-sdk → @smithy}/core/dist-types/ts3.4/submodules/protocols/json/detectBufferParsing.d.ts +1 -1
- package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/json/jsonReplacer.d.ts +21 -0
- package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/json/jsonReviver.d.ts +15 -0
- package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/json/needsReviver.d.ts +11 -0
- package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/json/parseJsonBody.d.ts +38 -0
- package/node_modules/@smithy/core/dist-types/ts3.4/submodules/protocols/writeKey.d.ts +4 -0
- package/node_modules/@smithy/core/package.json +2 -1
- package/node_modules/@zone-eu/mailsplit/README.md +43 -54
- package/node_modules/@zone-eu/mailsplit/lib/mime-node.js +7 -3
- package/node_modules/@zone-eu/mailsplit/package.json +20 -15
- package/node_modules/libmime/CHANGELOG.md +7 -0
- package/node_modules/libmime/dist/cjs/libmime.js +24 -10
- package/node_modules/libmime/dist/esm/libmime.js +24 -10
- package/node_modules/libmime/package.json +3 -1
- package/node_modules/mailparser/CHANGELOG.md +14 -0
- package/node_modules/mailparser/README.md +6 -0
- package/node_modules/mailparser/dist/cjs/index.d.ts +10 -0
- package/node_modules/mailparser/dist/cjs/index.js +12 -0
- package/node_modules/mailparser/dist/cjs/mail-parser.d.ts +98 -0
- package/node_modules/mailparser/dist/cjs/mail-parser.js +1419 -0
- package/node_modules/mailparser/dist/cjs/package.json +3 -0
- package/node_modules/mailparser/dist/cjs/simple-parser.d.ts +17 -0
- package/node_modules/mailparser/dist/cjs/simple-parser.js +133 -0
- package/node_modules/mailparser/dist/cjs/stream-hash.d.ts +24 -0
- package/node_modules/mailparser/dist/cjs/stream-hash.js +33 -0
- package/node_modules/mailparser/dist/cjs/tlds.cjs +8 -0
- package/node_modules/mailparser/dist/cjs/types.d.ts +309 -0
- package/node_modules/mailparser/dist/cjs/types.js +2 -0
- package/node_modules/mailparser/dist/esm/index.d.ts +10 -0
- package/node_modules/mailparser/dist/esm/index.js +4 -0
- package/node_modules/mailparser/dist/esm/mail-parser.d.ts +98 -0
- package/node_modules/mailparser/{lib → dist/esm}/mail-parser.js +302 -368
- package/node_modules/mailparser/dist/esm/package.json +3 -0
- package/node_modules/mailparser/dist/esm/simple-parser.d.ts +17 -0
- package/node_modules/mailparser/{lib → dist/esm}/simple-parser.js +45 -68
- package/node_modules/mailparser/dist/esm/stream-hash.d.ts +24 -0
- package/node_modules/mailparser/{lib → dist/esm}/stream-hash.js +7 -9
- package/node_modules/mailparser/dist/esm/tlds.cjs +8 -0
- package/node_modules/mailparser/dist/esm/types.d.ts +309 -0
- package/node_modules/mailparser/dist/esm/types.js +1 -0
- package/node_modules/mailparser/package.json +86 -15
- package/package.json +11 -11
- package/node_modules/@aws-sdk/core/dist-types/submodules/protocols/json/codec-v1/JsonSettings.d.ts +0 -7
- package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/codec-v1/JsonSettings.d.ts +0 -4
- package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/codec-v1/JsonShapeDeserializer.d.ts +0 -13
- package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/codec-v1/JsonShapeSerializer.d.ts +0 -18
- package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/codec-v2/JsonBytesStringAdapter.d.ts +0 -27
- package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/codec-v2/JsonShapeDeserializer2.d.ts +0 -15
- package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/codec-v2/JsonShapeSerializer2.d.ts +0 -39
- package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/jsonReplacer.d.ts +0 -7
- package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/jsonReviver.d.ts +0 -7
- package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/needsReviver.d.ts +0 -2
- package/node_modules/@aws-sdk/core/dist-types/ts3.4/submodules/protocols/json/parseJsonBody.d.ts +0 -14
- package/node_modules/mailparser/.gitattributes +0 -1
- package/node_modules/mailparser/.prettierignore +0 -3
- package/node_modules/mailparser/.prettierrc.js +0 -10
- package/node_modules/mailparser/.release-please-manifest.json +0 -3
- package/node_modules/mailparser/eslint.config.js +0 -33
- package/node_modules/mailparser/index.js +0 -9
- package/node_modules/mailparser/release-please-config.json +0 -10
- /package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/JsonSettings.js +0 -0
- /package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/codec-v1/JsonCodec.js +0 -0
- /package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/codec-v1/JsonSettings.js +0 -0
- /package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/codec-v2/JsonBytesStringAdapter.js +0 -0
- /package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/codec-v2/JsonCodec2.js +0 -0
- /package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/detectBufferParsing.js +0 -0
- /package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/jsonReplacer.js +0 -0
- /package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/jsonReviver.js +0 -0
- /package/node_modules/{@aws-sdk → @smithy}/core/dist-es/submodules/protocols/json/needsReviver.js +0 -0
- /package/node_modules/{@aws-sdk → @smithy}/core/dist-types/submodules/protocols/json/codec-v1/JsonCodec.d.ts +0 -0
- /package/node_modules/{@aws-sdk/core/dist-types/submodules/protocols/json → @smithy/core/dist-types/submodules/protocols/json/codec-v1}/JsonSettings.d.ts +0 -0
- /package/node_modules/{@aws-sdk → @smithy}/core/dist-types/submodules/protocols/json/codec-v1/JsonShapeDeserializer.d.ts +0 -0
- /package/node_modules/{@aws-sdk → @smithy}/core/dist-types/submodules/protocols/json/codec-v1/JsonShapeSerializer.d.ts +0 -0
- /package/node_modules/{@aws-sdk → @smithy}/core/dist-types/submodules/protocols/json/codec-v2/JsonBytesStringAdapter.d.ts +0 -0
- /package/node_modules/{@aws-sdk → @smithy}/core/dist-types/submodules/protocols/json/codec-v2/JsonCodec2.d.ts +0 -0
- /package/node_modules/{@aws-sdk → @smithy}/core/dist-types/submodules/protocols/json/codec-v2/JsonShapeDeserializer2.d.ts +0 -0
- /package/node_modules/{@aws-sdk → @smithy}/core/dist-types/submodules/protocols/json/codec-v2/JsonShapeSerializer2.d.ts +0 -0
- /package/node_modules/{@aws-sdk → @smithy}/core/dist-types/submodules/protocols/json/detectBufferParsing.d.ts +0 -0
- /package/node_modules/{@aws-sdk → @smithy}/core/dist-types/submodules/protocols/json/jsonReplacer.d.ts +0 -0
- /package/node_modules/{@aws-sdk → @smithy}/core/dist-types/submodules/protocols/json/jsonReviver.d.ts +0 -0
- /package/node_modules/{@aws-sdk → @smithy}/core/dist-types/submodules/protocols/json/needsReviver.d.ts +0 -0
|
@@ -0,0 +1,1419 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const mailsplit_1 = __importDefault(require("@zone-eu/mailsplit"));
|
|
7
|
+
const flowed_decoder_js_1 = __importDefault(require("@zone-eu/mailsplit/lib/flowed-decoder.js"));
|
|
8
|
+
const encoding_japanese_1 = __importDefault(require("encoding-japanese"));
|
|
9
|
+
const he_1 = __importDefault(require("he"));
|
|
10
|
+
const html_to_text_1 = require("html-to-text");
|
|
11
|
+
const iconv_lite_1 = __importDefault(require("iconv-lite"));
|
|
12
|
+
const libmime_1 = __importDefault(require("libmime"));
|
|
13
|
+
const linkify_it_1 = __importDefault(require("linkify-it"));
|
|
14
|
+
const addressparser_1 = __importDefault(require("nodemailer/lib/addressparser"));
|
|
15
|
+
const punycode_js_1 = __importDefault(require("punycode.js"));
|
|
16
|
+
const node_stream_1 = require("node:stream");
|
|
17
|
+
const tlds_cjs_1 = __importDefault(require("./tlds.cjs"));
|
|
18
|
+
const stream_hash_js_1 = __importDefault(require("./stream-hash.js"));
|
|
19
|
+
const { Splitter, ChunkedPassthrough } = mailsplit_1.default;
|
|
20
|
+
// Bounds for link detection in text bodies. Message content is untrusted and
|
|
21
|
+
// linkify-it backtracks heavily on crafted input: matching a host is quadratic
|
|
22
|
+
// in the length of a single whitespace-free run and exponential in the number
|
|
23
|
+
// of dot separated labels it has to try (a label starting with "xn--" matches
|
|
24
|
+
// two of the alternatives in linkify-it's domain pattern, so N such labels can
|
|
25
|
+
// be grouped in 2^N ways). Text is therefore scanned one whitespace-delimited
|
|
26
|
+
// segment at a time, segments long enough or with enough chained host labels to
|
|
27
|
+
// get expensive are left as plain text, and a message may only spend so much
|
|
28
|
+
// work in total. Scanning one segment costs roughly length * (length + 1024), so
|
|
29
|
+
// the budget is counted in those units rather than in characters - neither a
|
|
30
|
+
// few very long segments nor very many short ones can then add up to an
|
|
31
|
+
// unbounded scan. Together these keep the worst case at about half a second.
|
|
32
|
+
// Ordinary mail stays far below all three: across test/fixtures the longest
|
|
33
|
+
// segment is 565 characters and 99.9% of them chain up at most 3 host labels.
|
|
34
|
+
const MAX_LINKIFY_SEGMENT_LENGTH = 4096;
|
|
35
|
+
const MAX_LINKIFY_HOST_LABELS = 6;
|
|
36
|
+
const MAX_LINKIFY_WORK = 768 * 1024 * 1024; // shared by all text parts of a message
|
|
37
|
+
// Default upper bound for the total length of the URLs updateImageLinks() writes
|
|
38
|
+
// into the HTML (options.maxInlinedImagesSize). simpleParser inlines every cid:
|
|
39
|
+
// reference as a full data: URL, so a small message repeating a reference to
|
|
40
|
+
// one image thousands of times would otherwise grow the HTML past the maximum
|
|
41
|
+
// string length. References beyond the budget are left as cid: links.
|
|
42
|
+
const MAX_INLINED_IMAGES_SIZE = 20 * 1024 * 1024;
|
|
43
|
+
// List-* header names that must not become keys of the plain list object
|
|
44
|
+
const UNSAFE_LIST_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
45
|
+
const linkify = (0, linkify_it_1.default)();
|
|
46
|
+
linkify
|
|
47
|
+
.tlds(tlds_cjs_1.default) // Reload with full tlds list
|
|
48
|
+
.tlds('onion', true) // Add unofficial `.onion` domain
|
|
49
|
+
.add('git:', 'http:') // Add `git:` ptotocol as "alias"
|
|
50
|
+
.add('ftp:', null) // Disable `ftp:` ptotocol
|
|
51
|
+
.set({ fuzzyIP: true, fuzzyLink: true, fuzzyEmail: true });
|
|
52
|
+
// Characters linkify-it accepts inside a host name: letters, digits, a dash,
|
|
53
|
+
// and anything non-ASCII (its label pattern takes any character that is not
|
|
54
|
+
// punctuation, whitespace or a control character).
|
|
55
|
+
function isHostChar(code) {
|
|
56
|
+
return (code >= 0x61 && code <= 0x7a) || (code >= 0x41 && code <= 0x5a) || (code >= 0x30 && code <= 0x39) || code === 0x2d || code >= 0x80;
|
|
57
|
+
}
|
|
58
|
+
// twitter linkifier from
|
|
59
|
+
// https://github.com/markdown-it/linkify-it#example-2-add-twitter-mentions-handler
|
|
60
|
+
linkify.add('@', {
|
|
61
|
+
validate(text, pos, self) {
|
|
62
|
+
let tail = text.slice(pos);
|
|
63
|
+
if (!self.re.twitter) {
|
|
64
|
+
self.re.twitter = new RegExp('^([a-zA-Z0-9_]){1,15}(?!_)(?=$|' + self.re.src_ZPCc + ')');
|
|
65
|
+
}
|
|
66
|
+
if (self.re.twitter.test(tail)) {
|
|
67
|
+
// Linkifier allows punctuation chars before prefix,
|
|
68
|
+
// but we additionally disable `@` ("@@mention" is invalid)
|
|
69
|
+
if (pos >= 2 && text[pos - 2] === '@') {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return tail.match(self.re.twitter)[0].length;
|
|
73
|
+
}
|
|
74
|
+
return 0;
|
|
75
|
+
},
|
|
76
|
+
normalize(match) {
|
|
77
|
+
match.url = 'https://twitter.com/' + match.url.replace(/^@/, '');
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
// Lists the parameters a header value gives in RFC 2231 form (name*=, name*0= or
|
|
81
|
+
// name*0*=). libmime decodes those while parsing the header, so the result must not go
|
|
82
|
+
// through decodeWords as well: filename*=utf-8''a%3D%3Futf-8%3FQ%3F%3D2Eexe%3F%3D would
|
|
83
|
+
// otherwise come out as "a.exe" while a parser that decodes once sees the encoded word.
|
|
84
|
+
function rfc2231ParamNames(value) {
|
|
85
|
+
let names = new Set();
|
|
86
|
+
let quote = false;
|
|
87
|
+
let escaped = false;
|
|
88
|
+
let start = 0;
|
|
89
|
+
let addSegment = (end) => {
|
|
90
|
+
let segment = value.substring(start, end);
|
|
91
|
+
let eq = segment.indexOf('=');
|
|
92
|
+
if (eq < 0) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
let match = segment
|
|
96
|
+
.substring(0, eq)
|
|
97
|
+
.trim()
|
|
98
|
+
.toLowerCase()
|
|
99
|
+
.match(/^(.+?)\*(\d+\*?)?$/);
|
|
100
|
+
if (match) {
|
|
101
|
+
names.add(match[1]);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
for (let i = 0; i < value.length; i++) {
|
|
105
|
+
let chr = value.charAt(i);
|
|
106
|
+
if (escaped) {
|
|
107
|
+
escaped = false;
|
|
108
|
+
}
|
|
109
|
+
else if (chr === '\\') {
|
|
110
|
+
escaped = true;
|
|
111
|
+
}
|
|
112
|
+
else if (chr === '"') {
|
|
113
|
+
quote = !quote;
|
|
114
|
+
}
|
|
115
|
+
else if (chr === ';' && !quote) {
|
|
116
|
+
addSegment(i);
|
|
117
|
+
start = i + 1;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
addSegment(value.length);
|
|
121
|
+
return names;
|
|
122
|
+
}
|
|
123
|
+
// Deepest element nesting html-to-text is asked to convert. htmlparser2 keeps its open
|
|
124
|
+
// element stack at the front of an array, so parsing alone is quadratic in the nesting
|
|
125
|
+
// depth (200k nested <b> took 9 s), and html-to-text's recursive walk overflows the call
|
|
126
|
+
// stack at about 2000 levels. Real markup stays far below this: Chrome's own parser
|
|
127
|
+
// flattens anything nested deeper than 512 levels.
|
|
128
|
+
const MAX_HTML_NESTING_DEPTH = 1000;
|
|
129
|
+
// Elements that never hold content, so a start tag does not open a level
|
|
130
|
+
const HTML_VOID_ELEMENTS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
|
|
131
|
+
// Elements the HTML parser closes implicitly when a sibling opens (li after li, td after
|
|
132
|
+
// td, p after p), so an unclosed run of them does not nest. They are left out of the depth
|
|
133
|
+
// count: a report with thousands of unclosed table cells is sloppy but common markup
|
|
134
|
+
const HTML_IMPLIED_END_ELEMENTS = new Set(['p', 'li', 'dt', 'dd', 'option', 'optgroup', 'tr', 'td', 'th', 'thead', 'tbody', 'tfoot', 'rt', 'rp']);
|
|
135
|
+
function isTagNameChar(code) {
|
|
136
|
+
return ((code >= 0x61 && code <= 0x7a) || (code >= 0x41 && code <= 0x5a) || (code >= 0x30 && code <= 0x39) || code === 0x2d || code === 0x3a || code === 0x5f);
|
|
137
|
+
}
|
|
138
|
+
// Reports whether `html` nests elements deeper than `maxDepth`, in one linear pass that
|
|
139
|
+
// follows what htmlparser2 builds: a start tag opens an element (a trailing "/" does not
|
|
140
|
+
// close it in HTML mode), an end tag closes down to the nearest open element of that name
|
|
141
|
+
// and is ignored otherwise, comments are skipped. Returns as soon as the limit is crossed.
|
|
142
|
+
function exceedsHtmlNestingDepth(html, maxDepth) {
|
|
143
|
+
let stack = [];
|
|
144
|
+
let open = new Map(); // element name -> how many of them are on the stack
|
|
145
|
+
let len = html.length;
|
|
146
|
+
let i = html.indexOf('<');
|
|
147
|
+
while (i >= 0) {
|
|
148
|
+
let pos = i + 1;
|
|
149
|
+
if (html.startsWith('!--', pos)) {
|
|
150
|
+
let end = html.indexOf('-->', pos + 3);
|
|
151
|
+
i = end < 0 ? -1 : html.indexOf('<', end + 3);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
let closing = html.charCodeAt(pos) === 0x2f; // "/"
|
|
155
|
+
if (closing) {
|
|
156
|
+
pos++;
|
|
157
|
+
}
|
|
158
|
+
let nameStart = pos;
|
|
159
|
+
while (pos < len && isTagNameChar(html.charCodeAt(pos))) {
|
|
160
|
+
pos++;
|
|
161
|
+
}
|
|
162
|
+
if (pos === nameStart) {
|
|
163
|
+
// not a tag, "<" is text here
|
|
164
|
+
i = html.indexOf('<', pos);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
let name = html.slice(nameStart, pos).toLowerCase();
|
|
168
|
+
let end = html.indexOf('>', pos);
|
|
169
|
+
if (end < 0) {
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
if (!HTML_VOID_ELEMENTS.has(name) && !HTML_IMPLIED_END_ELEMENTS.has(name)) {
|
|
173
|
+
if (!closing) {
|
|
174
|
+
stack.push(name);
|
|
175
|
+
open.set(name, (open.get(name) || 0) + 1);
|
|
176
|
+
if (stack.length > maxDepth) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else if (open.get(name)) {
|
|
181
|
+
let popped;
|
|
182
|
+
do {
|
|
183
|
+
popped = stack.pop();
|
|
184
|
+
open.set(popped, open.get(popped) - 1);
|
|
185
|
+
} while (popped !== name);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
i = html.indexOf('<', end + 1);
|
|
189
|
+
}
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
class IconvDecoder extends node_stream_1.Transform {
|
|
193
|
+
constructor(Iconv, charset) {
|
|
194
|
+
super();
|
|
195
|
+
// Iconv throws error on ks_c_5601-1987 when it is mapped to EUC-KR
|
|
196
|
+
// https://github.com/bnoordhuis/node-iconv/issues/169
|
|
197
|
+
if (charset.toLowerCase() === 'ks_c_5601-1987') {
|
|
198
|
+
charset = 'CP949';
|
|
199
|
+
}
|
|
200
|
+
this.stream = new Iconv(charset, 'UTF-8//TRANSLIT//IGNORE');
|
|
201
|
+
this.inputEnded = false;
|
|
202
|
+
this.endCb = false;
|
|
203
|
+
this.stream.on('error', err => this.emit('error', err));
|
|
204
|
+
this.stream.on('data', chunk => this.push(chunk));
|
|
205
|
+
this.stream.on('end', () => {
|
|
206
|
+
this.inputEnded = true;
|
|
207
|
+
if (typeof this.endCb === 'function') {
|
|
208
|
+
this.endCb();
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
_transform(chunk, encoding, done) {
|
|
213
|
+
this.stream.write(chunk);
|
|
214
|
+
done();
|
|
215
|
+
}
|
|
216
|
+
_flush(done) {
|
|
217
|
+
this.endCb = done;
|
|
218
|
+
this.stream.end();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
class JPDecoder extends node_stream_1.Transform {
|
|
222
|
+
constructor(charset) {
|
|
223
|
+
super();
|
|
224
|
+
this.charset = charset;
|
|
225
|
+
this.chunks = [];
|
|
226
|
+
this.chunklen = 0;
|
|
227
|
+
}
|
|
228
|
+
_transform(chunk, encoding, done) {
|
|
229
|
+
if (typeof chunk === 'string') {
|
|
230
|
+
chunk = Buffer.from(chunk, encoding);
|
|
231
|
+
}
|
|
232
|
+
this.chunks.push(chunk);
|
|
233
|
+
this.chunklen += chunk.length;
|
|
234
|
+
done();
|
|
235
|
+
}
|
|
236
|
+
_flush(done) {
|
|
237
|
+
let input = Buffer.concat(this.chunks, this.chunklen);
|
|
238
|
+
try {
|
|
239
|
+
let output = encoding_japanese_1.default.convert(input, {
|
|
240
|
+
to: 'UNICODE', // to_encoding
|
|
241
|
+
from: this.charset, // from_encoding
|
|
242
|
+
type: 'string'
|
|
243
|
+
});
|
|
244
|
+
if (typeof output === 'string') {
|
|
245
|
+
output = Buffer.from(output);
|
|
246
|
+
}
|
|
247
|
+
this.push(output);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// keep as is on errors
|
|
251
|
+
this.push(input);
|
|
252
|
+
}
|
|
253
|
+
done();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* A lower level, streaming e-mail parser. It is a Transform stream: write the raw message in and
|
|
258
|
+
* read the parsed content out. It emits:
|
|
259
|
+
*
|
|
260
|
+
* - a `headers` event with the parsed headers of the message (see {@link Headers}),
|
|
261
|
+
* - a `headerLines` event with the raw header lines,
|
|
262
|
+
* - an {@link AttachmentStream} object for every attachment, whose content stream has to be read
|
|
263
|
+
* and `release()` called before the parser continues,
|
|
264
|
+
* - one {@link MessageText} object with the text content at the end.
|
|
265
|
+
*
|
|
266
|
+
* For one-shot parsing into a single object, use `simpleParser` instead.
|
|
267
|
+
*/
|
|
268
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging -- the interface below only narrows inherited stream methods
|
|
269
|
+
class MailParser extends node_stream_1.Transform {
|
|
270
|
+
constructor(config) {
|
|
271
|
+
super({
|
|
272
|
+
readableObjectMode: true,
|
|
273
|
+
writableObjectMode: false
|
|
274
|
+
});
|
|
275
|
+
this.options = config || {};
|
|
276
|
+
// mailsplit declares its streams with only a 'data' event, the parser uses the other Transform events too
|
|
277
|
+
this.chunkedPassthrough = new ChunkedPassthrough();
|
|
278
|
+
this.splitter = new Splitter(config);
|
|
279
|
+
this.chunkedPassthrough.pipe(this.splitter);
|
|
280
|
+
this.finished = false;
|
|
281
|
+
this.waitingEnd = false;
|
|
282
|
+
this.headers = false;
|
|
283
|
+
this.headerLines = false;
|
|
284
|
+
this.endReceived = false;
|
|
285
|
+
this.reading = false;
|
|
286
|
+
this.hasFailed = false;
|
|
287
|
+
this.tree = false;
|
|
288
|
+
this.curnode = false;
|
|
289
|
+
this.waitUntilAttachmentEnd = false;
|
|
290
|
+
this.attachmentCallback = false;
|
|
291
|
+
this.hasHtml = false;
|
|
292
|
+
this.hasText = false;
|
|
293
|
+
this.text = false;
|
|
294
|
+
this.html = false;
|
|
295
|
+
this.textAsHtml = false;
|
|
296
|
+
this.linkifyWork = 0;
|
|
297
|
+
this.attachmentList = [];
|
|
298
|
+
this.textTypes = ['text/plain', 'text/html'].concat(!this.options.keepDeliveryStatus ? 'message/delivery-status' : []);
|
|
299
|
+
this.decoder = this.getDecoder();
|
|
300
|
+
this.chunkedPassthrough.on('error', err => {
|
|
301
|
+
this.splitter.emit('error', err);
|
|
302
|
+
});
|
|
303
|
+
this.splitter.on('readable', () => {
|
|
304
|
+
if (this.reading) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
this.readData();
|
|
308
|
+
});
|
|
309
|
+
this.splitter.on('end', () => {
|
|
310
|
+
this.endReceived = true;
|
|
311
|
+
if (!this.reading) {
|
|
312
|
+
this.endStream();
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
this.splitter.on('error', err => {
|
|
316
|
+
this.hasFailed = true;
|
|
317
|
+
if (typeof this.waitingEnd === 'function') {
|
|
318
|
+
return this.waitingEnd(err);
|
|
319
|
+
}
|
|
320
|
+
this.emit('error', err);
|
|
321
|
+
});
|
|
322
|
+
this.libmime = new libmime_1.default.Libmime({ Iconv: this.options.Iconv });
|
|
323
|
+
}
|
|
324
|
+
/** @internal */
|
|
325
|
+
getDecoder() {
|
|
326
|
+
if (this.options.Iconv) {
|
|
327
|
+
const Iconv = this.options.Iconv;
|
|
328
|
+
// create wrapper
|
|
329
|
+
return {
|
|
330
|
+
decodeStream(charset) {
|
|
331
|
+
// node-iconv resolves its own charset aliases and needs charset-specific
|
|
332
|
+
// fixups (see IconvDecoder, eg. ks_c_5601-1987), so pass the charset through as is
|
|
333
|
+
return new IconvDecoder(Iconv, charset);
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
return {
|
|
339
|
+
decodeStream(charset) {
|
|
340
|
+
// resolve charset aliases (eg. iso-8859-8-i -> ISO-8859-8) via libmime's canonical table
|
|
341
|
+
charset = libmime_1.default.normalizeCharset((charset || 'ascii').toString());
|
|
342
|
+
if (/^jis|^iso-?2022-?jp/i.test(charset)) {
|
|
343
|
+
// special case not supported by iconv-lite
|
|
344
|
+
return new JPDecoder(charset);
|
|
345
|
+
}
|
|
346
|
+
return iconv_lite_1.default.decodeStream(charset);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/** @internal */
|
|
352
|
+
readData() {
|
|
353
|
+
if (this.hasFailed) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
this.reading = true;
|
|
357
|
+
let data = this.splitter.read();
|
|
358
|
+
if (data === null) {
|
|
359
|
+
this.reading = false;
|
|
360
|
+
if (this.endReceived) {
|
|
361
|
+
this.endStream();
|
|
362
|
+
}
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
this.processChunk(data, err => {
|
|
366
|
+
if (err) {
|
|
367
|
+
if (typeof this.waitingEnd === 'function') {
|
|
368
|
+
return this.waitingEnd(err);
|
|
369
|
+
}
|
|
370
|
+
return this.emit('error', err);
|
|
371
|
+
}
|
|
372
|
+
setImmediate(() => this.readData());
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
/** @internal */
|
|
376
|
+
endStream() {
|
|
377
|
+
this.finished = true;
|
|
378
|
+
if (this.curnode && this.curnode.decoder) {
|
|
379
|
+
this.curnode.decoder.end();
|
|
380
|
+
}
|
|
381
|
+
if (typeof this.waitingEnd === 'function') {
|
|
382
|
+
this.waitingEnd();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
_transform(chunk, encoding, done) {
|
|
386
|
+
if (!chunk || !chunk.length) {
|
|
387
|
+
return done();
|
|
388
|
+
}
|
|
389
|
+
if (this.chunkedPassthrough.write(chunk) === false) {
|
|
390
|
+
this.chunkedPassthrough.once('drain', () => {
|
|
391
|
+
done();
|
|
392
|
+
});
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
return done();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
_flush(done) {
|
|
400
|
+
setImmediate(() => this.chunkedPassthrough.end());
|
|
401
|
+
if (this.finished) {
|
|
402
|
+
return this.cleanup(done);
|
|
403
|
+
}
|
|
404
|
+
this.waitingEnd = err => {
|
|
405
|
+
// runs once, either on a splitter error after end of input or when the splitter ends
|
|
406
|
+
this.waitingEnd = false;
|
|
407
|
+
if (err) {
|
|
408
|
+
return done(err);
|
|
409
|
+
}
|
|
410
|
+
this.cleanup(done);
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
/** @internal */
|
|
414
|
+
cleanup(done) {
|
|
415
|
+
let finish = () => {
|
|
416
|
+
try {
|
|
417
|
+
let t = this.getTextContent();
|
|
418
|
+
this.push(t);
|
|
419
|
+
}
|
|
420
|
+
catch (err) {
|
|
421
|
+
// report through the stream instead of leaving _flush hanging
|
|
422
|
+
return done(err);
|
|
423
|
+
}
|
|
424
|
+
done();
|
|
425
|
+
};
|
|
426
|
+
if (this.curnode && this.curnode.decoder && this.curnode.decoder.readable && !this.decoderEnded) {
|
|
427
|
+
(this.curnode.contentStream || this.curnode.decoder).once('end', () => {
|
|
428
|
+
finish();
|
|
429
|
+
});
|
|
430
|
+
this.curnode.decoder.end();
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
setImmediate(() => {
|
|
434
|
+
finish();
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/** @internal */
|
|
439
|
+
processHeaders(lines) {
|
|
440
|
+
let headers = new Map();
|
|
441
|
+
(lines || []).forEach(line => {
|
|
442
|
+
let key = line.key;
|
|
443
|
+
let value = ((this.libmime.decodeHeader(line.line) || {}).value || '').toString().trim();
|
|
444
|
+
value = Buffer.from(value, 'binary').toString();
|
|
445
|
+
switch (key) {
|
|
446
|
+
case 'content-type':
|
|
447
|
+
case 'content-disposition':
|
|
448
|
+
case 'dkim-signature': {
|
|
449
|
+
// a parameter given in RFC 2231 form is decoded by parseHeaderValue already
|
|
450
|
+
let rfc2231 = rfc2231ParamNames(value);
|
|
451
|
+
value = this.libmime.parseHeaderValue(value);
|
|
452
|
+
if (value.value) {
|
|
453
|
+
value.value = this.libmime.decodeWords(value.value);
|
|
454
|
+
}
|
|
455
|
+
Object.keys((value && value.params) || {}).forEach(key => {
|
|
456
|
+
if (rfc2231.has(key)) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
value.params[key] = this.libmime.decodeWords(value.params[key]);
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
// ignore, keep as is
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
case 'date': {
|
|
469
|
+
let dateValue = new Date(value);
|
|
470
|
+
if (isNaN(dateValue.getTime())) {
|
|
471
|
+
// date parsing failed :S
|
|
472
|
+
dateValue = new Date();
|
|
473
|
+
}
|
|
474
|
+
value = dateValue;
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
case 'subject':
|
|
478
|
+
try {
|
|
479
|
+
value = this.libmime.decodeWords(value);
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
// ignore, keep as is
|
|
483
|
+
}
|
|
484
|
+
break;
|
|
485
|
+
case 'references':
|
|
486
|
+
try {
|
|
487
|
+
value = this.libmime.decodeWords(value);
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
// ignore
|
|
491
|
+
}
|
|
492
|
+
value = value
|
|
493
|
+
.split(/\s+/)
|
|
494
|
+
.map(this.ensureMessageIDFormat)
|
|
495
|
+
.filter((val) => val);
|
|
496
|
+
break;
|
|
497
|
+
case 'message-id':
|
|
498
|
+
case 'in-reply-to':
|
|
499
|
+
try {
|
|
500
|
+
value = this.libmime.decodeWords(value);
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
// ignore
|
|
504
|
+
}
|
|
505
|
+
value = this.ensureMessageIDFormat(value);
|
|
506
|
+
break;
|
|
507
|
+
case 'priority':
|
|
508
|
+
case 'x-priority':
|
|
509
|
+
case 'x-msmail-priority':
|
|
510
|
+
case 'importance':
|
|
511
|
+
key = 'priority';
|
|
512
|
+
value = this.parsePriority(value);
|
|
513
|
+
break;
|
|
514
|
+
case 'from':
|
|
515
|
+
case 'to':
|
|
516
|
+
case 'cc':
|
|
517
|
+
case 'bcc':
|
|
518
|
+
case 'sender':
|
|
519
|
+
case 'reply-to':
|
|
520
|
+
case 'delivered-to':
|
|
521
|
+
case 'return-path':
|
|
522
|
+
case 'disposition-notification-to':
|
|
523
|
+
value = (0, addressparser_1.default)(value);
|
|
524
|
+
this.decodeAddresses(value);
|
|
525
|
+
value = {
|
|
526
|
+
value,
|
|
527
|
+
html: this.getAddressesHTML(value),
|
|
528
|
+
text: this.getAddressesText(value)
|
|
529
|
+
};
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
// handle list-* keys
|
|
533
|
+
if (key.slice(0, 5) === 'list-') {
|
|
534
|
+
value = this.parseListHeader(key.slice(5), value);
|
|
535
|
+
key = 'list';
|
|
536
|
+
}
|
|
537
|
+
if (value) {
|
|
538
|
+
if (!headers.has(key)) {
|
|
539
|
+
headers.set(key, [].concat(value || []));
|
|
540
|
+
}
|
|
541
|
+
else if (Array.isArray(value)) {
|
|
542
|
+
// appended in place: rebuilding the list with concat for every repeated
|
|
543
|
+
// header was quadratic in the number of References lines
|
|
544
|
+
let list = headers.get(key);
|
|
545
|
+
for (let entry of value) {
|
|
546
|
+
list.push(entry);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
headers.get(key).push(value);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
// keep only the first value
|
|
555
|
+
let singleKeys = [
|
|
556
|
+
'message-id',
|
|
557
|
+
'content-id',
|
|
558
|
+
'from',
|
|
559
|
+
'sender',
|
|
560
|
+
'in-reply-to',
|
|
561
|
+
'reply-to',
|
|
562
|
+
'subject',
|
|
563
|
+
'date',
|
|
564
|
+
'content-disposition',
|
|
565
|
+
'content-type',
|
|
566
|
+
'content-transfer-encoding',
|
|
567
|
+
'priority',
|
|
568
|
+
'mime-version',
|
|
569
|
+
'content-description',
|
|
570
|
+
'precedence',
|
|
571
|
+
'errors-to',
|
|
572
|
+
'disposition-notification-to'
|
|
573
|
+
];
|
|
574
|
+
headers.forEach((value, key) => {
|
|
575
|
+
if (Array.isArray(value)) {
|
|
576
|
+
if (singleKeys.includes(key) && value.length) {
|
|
577
|
+
// the first occurrence: the one mailsplit built the MIME structure from,
|
|
578
|
+
// and the one mail clients display when the header is duplicated
|
|
579
|
+
headers.set(key, value[0]);
|
|
580
|
+
}
|
|
581
|
+
else if (value.length === 1) {
|
|
582
|
+
headers.set(key, value[0]);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (key === 'list') {
|
|
586
|
+
// normalize List-* headers
|
|
587
|
+
let listValue = {};
|
|
588
|
+
[].concat(value || []).forEach((val) => {
|
|
589
|
+
Object.keys(val || {}).forEach(listKey => {
|
|
590
|
+
listValue[listKey] = val[listKey];
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
headers.set(key, listValue);
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
return headers;
|
|
597
|
+
}
|
|
598
|
+
/** @internal */
|
|
599
|
+
parseListHeader(key, value) {
|
|
600
|
+
if (UNSAFE_LIST_KEYS.has(key)) {
|
|
601
|
+
// would replace the prototype of (or shadow) the normalized list object
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
let addresses = (0, addressparser_1.default)(value);
|
|
605
|
+
let response = {};
|
|
606
|
+
let data = addresses
|
|
607
|
+
.map(address => {
|
|
608
|
+
if (/^https?:/i.test(address.name)) {
|
|
609
|
+
response.url = address.name;
|
|
610
|
+
}
|
|
611
|
+
else if (address.name) {
|
|
612
|
+
response.name = address.name;
|
|
613
|
+
}
|
|
614
|
+
if (/^mailto:/.test(address.address || '')) {
|
|
615
|
+
response.mail = address.address.slice(7);
|
|
616
|
+
}
|
|
617
|
+
else if (address.address && address.address.indexOf('@') < 0) {
|
|
618
|
+
response.id = address.address;
|
|
619
|
+
}
|
|
620
|
+
else if (address.address) {
|
|
621
|
+
response.mail = address.address;
|
|
622
|
+
}
|
|
623
|
+
if (Object.keys(response).length) {
|
|
624
|
+
return response;
|
|
625
|
+
}
|
|
626
|
+
return false;
|
|
627
|
+
})
|
|
628
|
+
.filter(address => address);
|
|
629
|
+
if (data.length) {
|
|
630
|
+
return {
|
|
631
|
+
[key]: response
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Turns a `Priority`, `X-Priority`, `X-MSMail-Priority` or `Importance` header value into
|
|
638
|
+
* `'high'`, `'normal'` or `'low'`.
|
|
639
|
+
*/
|
|
640
|
+
parsePriority(value) {
|
|
641
|
+
value = value.toLowerCase().trim();
|
|
642
|
+
if (!isNaN(parseInt(value, 10))) {
|
|
643
|
+
// support "X-Priority: 1 (Highest)"
|
|
644
|
+
let level = parseInt(value, 10) || 0;
|
|
645
|
+
if (level === 3) {
|
|
646
|
+
return 'normal';
|
|
647
|
+
}
|
|
648
|
+
else if (level > 3) {
|
|
649
|
+
return 'low';
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
return 'high';
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
switch (value) {
|
|
657
|
+
case 'non-urgent':
|
|
658
|
+
case 'low':
|
|
659
|
+
return 'low';
|
|
660
|
+
case 'urgent':
|
|
661
|
+
case 'high':
|
|
662
|
+
return 'high';
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return 'normal';
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Wraps a Message-ID in angle brackets where they are missing. Returns `false` for an empty value.
|
|
669
|
+
*/
|
|
670
|
+
ensureMessageIDFormat(value) {
|
|
671
|
+
if (!value.length) {
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
if (value.charAt(0) !== '<') {
|
|
675
|
+
value = '<' + value;
|
|
676
|
+
}
|
|
677
|
+
if (value.charAt(value.length - 1) !== '>') {
|
|
678
|
+
value += '>';
|
|
679
|
+
}
|
|
680
|
+
return value;
|
|
681
|
+
}
|
|
682
|
+
/** @internal */
|
|
683
|
+
decodeAddresses(addresses) {
|
|
684
|
+
for (let i = 0; i < addresses.length; i++) {
|
|
685
|
+
let address = addresses[i];
|
|
686
|
+
address.name = (address.name || '').toString().trim();
|
|
687
|
+
// An encoded word decodes into the display name and nothing else, whatever it
|
|
688
|
+
// decodes to. RFC 2047 section 5 keeps encoded words out of the addr-spec, so a
|
|
689
|
+
// name that decodes to "CEO" <ceo@bank.com> is still only a name: parsing it
|
|
690
|
+
// again would report an address the header never carried
|
|
691
|
+
if (address.name) {
|
|
692
|
+
try {
|
|
693
|
+
address.name = this.libmime.decodeWords(address.name);
|
|
694
|
+
}
|
|
695
|
+
catch {
|
|
696
|
+
//ignore, keep as is
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
// Security fix: detect RFC 2047 encoded-words in the address field.
|
|
700
|
+
// Per RFC 2047 Section 5, encoded-words MUST NOT appear in addr-spec.
|
|
701
|
+
// If found, decode and validate; reject if result is not a simple local@domain.
|
|
702
|
+
if (address.address && /[=]\?[^?]+\?[BbQq]\?[^?]*\?[=]/.test(address.address)) {
|
|
703
|
+
try {
|
|
704
|
+
let decodedAddr = this.libmime.decodeWords(address.address);
|
|
705
|
+
if (/^[^\s@]+@[^\s@]+$/.test(decodedAddr) && !/[=]\?/.test(decodedAddr)) {
|
|
706
|
+
address.address = decodedAddr;
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
address.address = '';
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
address.address = '';
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (address.address && /@xn--/.test(address.address)) {
|
|
717
|
+
try {
|
|
718
|
+
address.address =
|
|
719
|
+
address.address.slice(0, address.address.lastIndexOf('@') + 1) +
|
|
720
|
+
punycode_js_1.default.toUnicode(address.address.slice(address.address.lastIndexOf('@') + 1));
|
|
721
|
+
}
|
|
722
|
+
catch {
|
|
723
|
+
// Not a valid punycode string; keep as is
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
if (address.group) {
|
|
727
|
+
this.decodeAddresses(address.group);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
/** @internal */
|
|
732
|
+
createNode(node) {
|
|
733
|
+
let contentType = node.contentType;
|
|
734
|
+
let disposition = node.disposition;
|
|
735
|
+
let encoding = node.encoding;
|
|
736
|
+
let charset = node.charset;
|
|
737
|
+
if (!contentType && node.root) {
|
|
738
|
+
contentType = 'text/plain';
|
|
739
|
+
}
|
|
740
|
+
let newNode = {
|
|
741
|
+
node,
|
|
742
|
+
headerLines: node.headers.lines,
|
|
743
|
+
headers: this.processHeaders(node.headers.getList()),
|
|
744
|
+
contentType,
|
|
745
|
+
children: []
|
|
746
|
+
};
|
|
747
|
+
if (!/^multipart\//i.test(contentType)) {
|
|
748
|
+
if (disposition && !['attachment', 'inline'].includes(disposition)) {
|
|
749
|
+
disposition = 'attachment';
|
|
750
|
+
}
|
|
751
|
+
if (!disposition && !this.textTypes.includes(contentType)) {
|
|
752
|
+
newNode.disposition = 'attachment';
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
newNode.disposition = disposition || 'inline';
|
|
756
|
+
}
|
|
757
|
+
newNode.isAttachment = !this.textTypes.includes(contentType) || newNode.disposition !== 'inline';
|
|
758
|
+
newNode.encoding = ['quoted-printable', 'base64'].includes(encoding) ? encoding : 'binary';
|
|
759
|
+
if (charset) {
|
|
760
|
+
newNode.charset = charset;
|
|
761
|
+
}
|
|
762
|
+
let decoder = node.getDecoder();
|
|
763
|
+
decoder.on('end', () => {
|
|
764
|
+
this.decoderEnded = true;
|
|
765
|
+
});
|
|
766
|
+
newNode.decoder = decoder;
|
|
767
|
+
}
|
|
768
|
+
if (node.root) {
|
|
769
|
+
this.headers = newNode.headers;
|
|
770
|
+
this.headerLines = newNode.headerLines;
|
|
771
|
+
}
|
|
772
|
+
// find location in tree
|
|
773
|
+
if (!this.tree || !this.curnode) {
|
|
774
|
+
newNode.root = true;
|
|
775
|
+
this.curnode = this.tree = newNode;
|
|
776
|
+
return newNode;
|
|
777
|
+
}
|
|
778
|
+
// immediate child of root node
|
|
779
|
+
if (!this.curnode.parent) {
|
|
780
|
+
newNode.parent = this.curnode;
|
|
781
|
+
this.curnode.children.push(newNode);
|
|
782
|
+
this.curnode = newNode;
|
|
783
|
+
return newNode;
|
|
784
|
+
}
|
|
785
|
+
// siblings
|
|
786
|
+
if (this.curnode.parent.node === node.parentNode) {
|
|
787
|
+
newNode.parent = this.curnode.parent;
|
|
788
|
+
this.curnode.parent.children.push(newNode);
|
|
789
|
+
this.curnode = newNode;
|
|
790
|
+
return newNode;
|
|
791
|
+
}
|
|
792
|
+
// first child
|
|
793
|
+
if (this.curnode.node === node.parentNode) {
|
|
794
|
+
newNode.parent = this.curnode;
|
|
795
|
+
this.curnode.children.push(newNode);
|
|
796
|
+
this.curnode = newNode;
|
|
797
|
+
return newNode;
|
|
798
|
+
}
|
|
799
|
+
// move up
|
|
800
|
+
let parentNode = this.curnode;
|
|
801
|
+
while ((parentNode = parentNode.parent)) {
|
|
802
|
+
if (parentNode.node === node.parentNode) {
|
|
803
|
+
newNode.parent = parentNode;
|
|
804
|
+
parentNode.children.push(newNode);
|
|
805
|
+
this.curnode = newNode;
|
|
806
|
+
return newNode;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
// should never happen, can't detect parent
|
|
810
|
+
this.curnode = newNode;
|
|
811
|
+
return newNode;
|
|
812
|
+
}
|
|
813
|
+
/** @internal */
|
|
814
|
+
getTextContent() {
|
|
815
|
+
let text = [];
|
|
816
|
+
let html = [];
|
|
817
|
+
let textConversionError = null;
|
|
818
|
+
let processNode = (alternative, level, node) => {
|
|
819
|
+
if (node.showMeta) {
|
|
820
|
+
let meta = ['From', 'Subject', 'Date', 'To', 'Cc', 'Bcc']
|
|
821
|
+
.map(fkey => {
|
|
822
|
+
let key = fkey.toLowerCase();
|
|
823
|
+
if (!node.headers.has(key)) {
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
let value = node.headers.get(key);
|
|
827
|
+
if (!value) {
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
return {
|
|
831
|
+
key: fkey,
|
|
832
|
+
value: (Array.isArray(value) ? value[value.length - 1] : value)
|
|
833
|
+
};
|
|
834
|
+
})
|
|
835
|
+
.filter(entry => entry);
|
|
836
|
+
if (this.hasHtml) {
|
|
837
|
+
html.push('<table class="mp_head">' +
|
|
838
|
+
meta
|
|
839
|
+
.map(entry => {
|
|
840
|
+
let value = entry.value;
|
|
841
|
+
switch (entry.key) {
|
|
842
|
+
case 'From':
|
|
843
|
+
case 'To':
|
|
844
|
+
case 'Cc':
|
|
845
|
+
case 'Bcc':
|
|
846
|
+
value = value.html;
|
|
847
|
+
break;
|
|
848
|
+
case 'Date':
|
|
849
|
+
value = this.options.formatDateString ? this.options.formatDateString(value) : value.toUTCString();
|
|
850
|
+
break;
|
|
851
|
+
case 'Subject':
|
|
852
|
+
value = '<strong>' + he_1.default.encode(value) + '</strong>';
|
|
853
|
+
break;
|
|
854
|
+
default:
|
|
855
|
+
value = he_1.default.encode(value);
|
|
856
|
+
}
|
|
857
|
+
return '<tr><td class="mp_head_key">' + he_1.default.encode(entry.key) + ':</td><td class="mp_head_value">' + value + '<td></tr>';
|
|
858
|
+
})
|
|
859
|
+
.join('\n') +
|
|
860
|
+
'<table>');
|
|
861
|
+
}
|
|
862
|
+
if (this.hasText) {
|
|
863
|
+
text.push('\n' +
|
|
864
|
+
meta
|
|
865
|
+
.map(entry => {
|
|
866
|
+
let value = entry.value;
|
|
867
|
+
switch (entry.key) {
|
|
868
|
+
case 'From':
|
|
869
|
+
case 'To':
|
|
870
|
+
case 'Cc':
|
|
871
|
+
case 'Bcc':
|
|
872
|
+
value = value.text;
|
|
873
|
+
break;
|
|
874
|
+
case 'Date':
|
|
875
|
+
value = this.options.formatDateString ? this.options.formatDateString(value) : value.toUTCString();
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
return entry.key + ': ' + value;
|
|
879
|
+
})
|
|
880
|
+
.join('\n') +
|
|
881
|
+
'\n');
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
if (node.textContent) {
|
|
885
|
+
if (node.contentType === 'text/plain') {
|
|
886
|
+
text.push(node.textContent);
|
|
887
|
+
if (!alternative && this.hasHtml) {
|
|
888
|
+
html.push(this.textToHtml(node.textContent));
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
else if (node.contentType === 'message/delivery-status' && !this.options.keepDeliveryStatus) {
|
|
892
|
+
text.push(node.textContent);
|
|
893
|
+
if (!alternative && this.hasHtml) {
|
|
894
|
+
html.push(this.textToHtml(node.textContent));
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
else if (node.contentType === 'text/html') {
|
|
898
|
+
if ((!alternative && this.hasText) || (node.root && !this.hasText)) {
|
|
899
|
+
if (this.options.skipHtmlToText) {
|
|
900
|
+
text.push('');
|
|
901
|
+
}
|
|
902
|
+
else {
|
|
903
|
+
// HTML that can not be converted is no reason to lose the message:
|
|
904
|
+
// the text stays empty, the HTML is kept and the reason travels with
|
|
905
|
+
// the result as textConversionError. An 'error' event here used to
|
|
906
|
+
// reject the whole simpleParser() promise, headers and attachments
|
|
907
|
+
// included, after burning the CPU the conversion attempt took
|
|
908
|
+
let err = null;
|
|
909
|
+
if (node.textContent.length > this.options.maxHtmlLengthToParse) {
|
|
910
|
+
err = new Error(`HTML too long for parsing ${node.textContent.length} bytes`);
|
|
911
|
+
}
|
|
912
|
+
else if (exceedsHtmlNestingDepth(node.textContent, MAX_HTML_NESTING_DEPTH)) {
|
|
913
|
+
err = new Error(`HTML nested deeper than ${MAX_HTML_NESTING_DEPTH} levels`);
|
|
914
|
+
}
|
|
915
|
+
else {
|
|
916
|
+
try {
|
|
917
|
+
text.push((0, html_to_text_1.htmlToText)(node.textContent, { limits: { maxDepth: MAX_HTML_NESTING_DEPTH } }));
|
|
918
|
+
}
|
|
919
|
+
catch (cause) {
|
|
920
|
+
err = new Error('Failed to parse HTML', { cause });
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
if (err) {
|
|
924
|
+
text.push('');
|
|
925
|
+
textConversionError = textConversionError || err;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
html.push(node.textContent);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
alternative = alternative || node.contentType === 'multipart/alternative';
|
|
933
|
+
if (node.children) {
|
|
934
|
+
node.children.forEach(subNode => {
|
|
935
|
+
processNode(alternative, level + 1, subNode);
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
processNode(false, 0, this.tree);
|
|
940
|
+
let response = {
|
|
941
|
+
type: 'text'
|
|
942
|
+
};
|
|
943
|
+
if (html.length) {
|
|
944
|
+
this.html = response.html = html.join('<br/>\n');
|
|
945
|
+
}
|
|
946
|
+
if (text.length) {
|
|
947
|
+
this.text = response.text = text.join('\n');
|
|
948
|
+
this.textAsHtml = response.textAsHtml = text.map(part => this.textToHtml(part)).join('<br/>\n');
|
|
949
|
+
}
|
|
950
|
+
if (textConversionError) {
|
|
951
|
+
response.textConversionError = textConversionError;
|
|
952
|
+
}
|
|
953
|
+
return response;
|
|
954
|
+
}
|
|
955
|
+
/** @internal */
|
|
956
|
+
processChunk(data, done) {
|
|
957
|
+
let partId = null;
|
|
958
|
+
if (data._parentBoundary) {
|
|
959
|
+
// mailsplit numbers parts IMAP style from the actual tree, TEXT markers are not part of the id
|
|
960
|
+
partId = (data.partNr || []).filter(nr => typeof nr === 'number').join('.') || null;
|
|
961
|
+
}
|
|
962
|
+
switch (data.type) {
|
|
963
|
+
case 'node': {
|
|
964
|
+
let node = this.createNode(data);
|
|
965
|
+
if (node === this.tree) {
|
|
966
|
+
['subject', 'references', 'date', 'to', 'from', 'to', 'cc', 'bcc', 'message-id', 'in-reply-to', 'reply-to'].forEach(key => {
|
|
967
|
+
if (node.headers.has(key)) {
|
|
968
|
+
this[key.replace(/-([a-z])/g, (m, c) => c.toUpperCase())] = node.headers.get(key);
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
this.emit('headers', node.headers);
|
|
972
|
+
if (node.headerLines) {
|
|
973
|
+
this.emit('headerLines', node.headerLines);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
if (data.contentType === 'message/rfc822' && data.messageNode) {
|
|
977
|
+
break;
|
|
978
|
+
}
|
|
979
|
+
if (data.parentNode && data.parentNode.contentType === 'message/rfc822') {
|
|
980
|
+
node.showMeta = true;
|
|
981
|
+
}
|
|
982
|
+
if (node.isAttachment) {
|
|
983
|
+
let decoder = node.decoder;
|
|
984
|
+
let contentType = node.contentType;
|
|
985
|
+
if (node.contentType === 'application/octet-stream' && data.filename) {
|
|
986
|
+
contentType = this.libmime.detectMimeType(data.filename) || 'application/octet-stream';
|
|
987
|
+
}
|
|
988
|
+
// backpressure state for the attachment stream, see pump() below
|
|
989
|
+
let released = false;
|
|
990
|
+
let waitingDrain = false;
|
|
991
|
+
let pump;
|
|
992
|
+
let attachment = {
|
|
993
|
+
type: 'attachment',
|
|
994
|
+
content: null,
|
|
995
|
+
contentType,
|
|
996
|
+
partId,
|
|
997
|
+
release: () => {
|
|
998
|
+
attachment.release = null;
|
|
999
|
+
released = true;
|
|
1000
|
+
if (waitingDrain) {
|
|
1001
|
+
waitingDrain = false;
|
|
1002
|
+
pump();
|
|
1003
|
+
}
|
|
1004
|
+
if (this.waitUntilAttachmentEnd && typeof this.attachmentCallback === 'function') {
|
|
1005
|
+
setImmediate(this.attachmentCallback);
|
|
1006
|
+
}
|
|
1007
|
+
this.attachmentCallback = false;
|
|
1008
|
+
this.waitUntilAttachmentEnd = false;
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
let algo = this.options.checksumAlgo || 'md5';
|
|
1012
|
+
let hasher;
|
|
1013
|
+
try {
|
|
1014
|
+
hasher = new stream_hash_js_1.default(attachment, algo);
|
|
1015
|
+
}
|
|
1016
|
+
catch (err) {
|
|
1017
|
+
// an algorithm this Node does not offer (a typo, or MD5 on a host whose
|
|
1018
|
+
// OpenSSL runs in FIPS mode) is a parser error, not a process-ending throw
|
|
1019
|
+
err.message = `Checksum algorithm "${algo}" is not available: ${err.message}`;
|
|
1020
|
+
return done(err);
|
|
1021
|
+
}
|
|
1022
|
+
decoder.on('error', err => {
|
|
1023
|
+
hasher.emit('error', err);
|
|
1024
|
+
});
|
|
1025
|
+
// Honour the consumer's backpressure while it reads the attachment. Once the
|
|
1026
|
+
// attachment is released it is drained regardless, so that a caller that
|
|
1027
|
+
// releases without reading the content does not stall the parser.
|
|
1028
|
+
pump = () => {
|
|
1029
|
+
let chunk;
|
|
1030
|
+
while ((chunk = decoder.read()) !== null) {
|
|
1031
|
+
if (hasher.write(chunk) === false && !released) {
|
|
1032
|
+
waitingDrain = true;
|
|
1033
|
+
hasher.once('drain', () => {
|
|
1034
|
+
waitingDrain = false;
|
|
1035
|
+
pump();
|
|
1036
|
+
});
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
decoder.on('readable', () => {
|
|
1042
|
+
if (!waitingDrain) {
|
|
1043
|
+
pump();
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
1046
|
+
decoder.once('end', () => {
|
|
1047
|
+
hasher.end();
|
|
1048
|
+
});
|
|
1049
|
+
//node.decoder.pipe(hasher);
|
|
1050
|
+
attachment.content = hasher;
|
|
1051
|
+
this.waitUntilAttachmentEnd = true;
|
|
1052
|
+
if (data.disposition) {
|
|
1053
|
+
attachment.contentDisposition = data.disposition;
|
|
1054
|
+
}
|
|
1055
|
+
if (data.filename) {
|
|
1056
|
+
attachment.filename = data.filename;
|
|
1057
|
+
}
|
|
1058
|
+
if (node.headers.has('content-id')) {
|
|
1059
|
+
attachment.contentId = [].concat(node.headers.get('content-id') || []).shift();
|
|
1060
|
+
attachment.cid = attachment.contentId.trim().replace(/^<|>$/g, '').trim();
|
|
1061
|
+
// check if the attachment is "related" to text content like an embedded image etc
|
|
1062
|
+
let parentNode = node;
|
|
1063
|
+
while ((parentNode = parentNode.parent)) {
|
|
1064
|
+
if (parentNode.contentType === 'multipart/related') {
|
|
1065
|
+
attachment.related = true;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
attachment.headers = node.headers;
|
|
1070
|
+
this.push(attachment);
|
|
1071
|
+
this.attachmentList.push(attachment);
|
|
1072
|
+
}
|
|
1073
|
+
else if (node.disposition === 'inline') {
|
|
1074
|
+
let chunks = [];
|
|
1075
|
+
let chunklen = 0;
|
|
1076
|
+
node.contentStream = node.decoder;
|
|
1077
|
+
if (node.contentType === 'text/plain') {
|
|
1078
|
+
this.hasText = true;
|
|
1079
|
+
}
|
|
1080
|
+
else if (node.contentType === 'text/html') {
|
|
1081
|
+
this.hasHtml = true;
|
|
1082
|
+
}
|
|
1083
|
+
else if (node.contentType === 'message/delivery-status' && !this.options.keepDeliveryStatus) {
|
|
1084
|
+
this.hasText = true;
|
|
1085
|
+
}
|
|
1086
|
+
if (node.node.flowed) {
|
|
1087
|
+
let contentStream = node.contentStream;
|
|
1088
|
+
let flowDecoder = new flowed_decoder_js_1.default({
|
|
1089
|
+
delSp: node.node.delSp
|
|
1090
|
+
});
|
|
1091
|
+
contentStream.on('error', err => {
|
|
1092
|
+
flowDecoder.emit('error', err);
|
|
1093
|
+
});
|
|
1094
|
+
contentStream.pipe(flowDecoder);
|
|
1095
|
+
node.contentStream = flowDecoder;
|
|
1096
|
+
}
|
|
1097
|
+
let charset = node.charset || 'utf-8';
|
|
1098
|
+
//charset = charset || 'windows-1257';
|
|
1099
|
+
if (!['ascii', 'usascii', 'utf8'].includes(charset.toLowerCase().replace(/[^a-z0-9]+/g, ''))) {
|
|
1100
|
+
try {
|
|
1101
|
+
let contentStream = node.contentStream;
|
|
1102
|
+
let decodeStream = this.decoder.decodeStream(charset);
|
|
1103
|
+
contentStream.on('error', err => {
|
|
1104
|
+
decodeStream.emit('error', err);
|
|
1105
|
+
});
|
|
1106
|
+
contentStream.pipe(decodeStream);
|
|
1107
|
+
node.contentStream = decodeStream;
|
|
1108
|
+
}
|
|
1109
|
+
catch {
|
|
1110
|
+
// do not decode charset
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
let contentStream = node.contentStream;
|
|
1114
|
+
contentStream.on('readable', () => {
|
|
1115
|
+
let chunk;
|
|
1116
|
+
while ((chunk = contentStream.read()) !== null) {
|
|
1117
|
+
if (typeof chunk === 'string') {
|
|
1118
|
+
chunk = Buffer.from(chunk);
|
|
1119
|
+
}
|
|
1120
|
+
chunks.push(chunk);
|
|
1121
|
+
chunklen += chunk.length;
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
contentStream.once('end', () => {
|
|
1125
|
+
node.textContent = Buffer.concat(chunks, chunklen).toString().replace(/\r?\n/g, '\n');
|
|
1126
|
+
});
|
|
1127
|
+
contentStream.once('error', err => {
|
|
1128
|
+
this.emit('error', err);
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
break;
|
|
1132
|
+
}
|
|
1133
|
+
case 'data':
|
|
1134
|
+
if (this.curnode && this.curnode.decoder) {
|
|
1135
|
+
this.curnode.decoder.end();
|
|
1136
|
+
}
|
|
1137
|
+
if (this.waitUntilAttachmentEnd) {
|
|
1138
|
+
this.attachmentCallback = done;
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
// multipart message structure
|
|
1142
|
+
// this is not related to any specific 'node' block as it includes
|
|
1143
|
+
// everything between the end of some node body and between the next header
|
|
1144
|
+
//process.stdout.write(data.value);
|
|
1145
|
+
break;
|
|
1146
|
+
case 'body':
|
|
1147
|
+
if (this.curnode && this.curnode.decoder && this.curnode.decoder.writable) {
|
|
1148
|
+
if (this.curnode.decoder.write(data.value) === false) {
|
|
1149
|
+
this.curnode.decoder.once('drain', done);
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
// Leaf element body. Includes the body for the last 'node' block. You might
|
|
1154
|
+
// have several 'body' calls for a single 'node' block
|
|
1155
|
+
//process.stdout.write(data.value);
|
|
1156
|
+
break;
|
|
1157
|
+
}
|
|
1158
|
+
setImmediate(done);
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Formats parsed addresses for an HTML context, every value HTML-escaped.
|
|
1162
|
+
*/
|
|
1163
|
+
getAddressesHTML(value) {
|
|
1164
|
+
let formatSingleLevel = (addresses) => addresses
|
|
1165
|
+
.map(address => {
|
|
1166
|
+
let str = '<span class="mp_address_group">';
|
|
1167
|
+
if (address.name) {
|
|
1168
|
+
str += '<span class="mp_address_name">' + he_1.default.encode(address.name) + (address.group ? ': ' : '') + '</span>';
|
|
1169
|
+
}
|
|
1170
|
+
if (address.address) {
|
|
1171
|
+
let link = '<a href="mailto:' + he_1.default.encode(address.address) + '" class="mp_address_email">' + he_1.default.encode(address.address) + '</a>';
|
|
1172
|
+
if (address.name) {
|
|
1173
|
+
str += ' <' + link + '>';
|
|
1174
|
+
}
|
|
1175
|
+
else {
|
|
1176
|
+
str += link;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
if (address.group) {
|
|
1180
|
+
str += formatSingleLevel(address.group) + ';';
|
|
1181
|
+
}
|
|
1182
|
+
return str + '</span>';
|
|
1183
|
+
})
|
|
1184
|
+
.join(', ');
|
|
1185
|
+
return formatSingleLevel([].concat(value || []));
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Formats parsed addresses as plain text, display names quoted.
|
|
1189
|
+
*/
|
|
1190
|
+
getAddressesText(value) {
|
|
1191
|
+
let formatSingleLevel = (addresses) => addresses
|
|
1192
|
+
.map(address => {
|
|
1193
|
+
let str = '';
|
|
1194
|
+
if (address.name) {
|
|
1195
|
+
// a quoted-string, so a quote or backslash inside the name is escaped
|
|
1196
|
+
str += `"${address.name.replace(/["\\]/g, '\\$&')}"` + (address.group ? ': ' : '');
|
|
1197
|
+
}
|
|
1198
|
+
if (address.address) {
|
|
1199
|
+
let link = address.address;
|
|
1200
|
+
if (address.name) {
|
|
1201
|
+
str += ' <' + link + '>';
|
|
1202
|
+
}
|
|
1203
|
+
else {
|
|
1204
|
+
str += link;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
if (address.group) {
|
|
1208
|
+
str += formatSingleLevel(address.group) + ';';
|
|
1209
|
+
}
|
|
1210
|
+
return str;
|
|
1211
|
+
})
|
|
1212
|
+
.join(', ');
|
|
1213
|
+
return formatSingleLevel([].concat(value || []));
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Replaces `cid:` links to image attachments in the HTML content. Call it once the parser has
|
|
1217
|
+
* ended. `replaceCallback` is called once for every referenced image attachment and answers with
|
|
1218
|
+
* the URL to use instead; `done` gets the updated HTML, or `false` when the message has no HTML.
|
|
1219
|
+
* The total length of the URLs written is bounded by `maxInlinedImagesSize`.
|
|
1220
|
+
*/
|
|
1221
|
+
updateImageLinks(replaceCallback, done) {
|
|
1222
|
+
if (!this.html) {
|
|
1223
|
+
setImmediate(() => done(null, false));
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
let cids = new Map();
|
|
1227
|
+
let html = (this.html || '').toString();
|
|
1228
|
+
if (this.options.skipImageLinks) {
|
|
1229
|
+
return done(null, html);
|
|
1230
|
+
}
|
|
1231
|
+
html.replace(/\bcid:([^'"\s]{1,256})/g, (match, cid) => {
|
|
1232
|
+
for (let i = 0, len = this.attachmentList.length; i < len; i++) {
|
|
1233
|
+
if (this.attachmentList[i].cid === cid && /^image\/[\w]+$/i.test(this.attachmentList[i].contentType)) {
|
|
1234
|
+
cids.set(cid, {
|
|
1235
|
+
attachment: this.attachmentList[i]
|
|
1236
|
+
});
|
|
1237
|
+
break;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
return match;
|
|
1241
|
+
});
|
|
1242
|
+
let cidList = [];
|
|
1243
|
+
cids.forEach(entry => {
|
|
1244
|
+
cidList.push(entry);
|
|
1245
|
+
});
|
|
1246
|
+
let pos = 0;
|
|
1247
|
+
let processNext = () => {
|
|
1248
|
+
if (pos >= cidList.length) {
|
|
1249
|
+
let maxSize = this.options.maxInlinedImagesSize;
|
|
1250
|
+
if (typeof maxSize !== 'number' || !(maxSize >= 0)) {
|
|
1251
|
+
maxSize = MAX_INLINED_IMAGES_SIZE;
|
|
1252
|
+
}
|
|
1253
|
+
let inlinedSize = 0;
|
|
1254
|
+
try {
|
|
1255
|
+
html = html.replace(/\bcid:([^'"\s]{1,256})/g, (match, cid) => {
|
|
1256
|
+
let url = cids.has(cid) && cids.get(cid).url;
|
|
1257
|
+
if (!url) {
|
|
1258
|
+
return match;
|
|
1259
|
+
}
|
|
1260
|
+
url = String(url);
|
|
1261
|
+
if (inlinedSize + url.length > maxSize) {
|
|
1262
|
+
return match;
|
|
1263
|
+
}
|
|
1264
|
+
inlinedSize += url.length;
|
|
1265
|
+
return url;
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
catch (err) {
|
|
1269
|
+
// runs from setImmediate, a throw here would be an uncaught exception
|
|
1270
|
+
return done(err);
|
|
1271
|
+
}
|
|
1272
|
+
return done(null, html);
|
|
1273
|
+
}
|
|
1274
|
+
let entry = cidList[pos++];
|
|
1275
|
+
replaceCallback(entry.attachment, (err, url) => {
|
|
1276
|
+
if (err) {
|
|
1277
|
+
setImmediate(() => done(err));
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
entry.url = url;
|
|
1281
|
+
setImmediate(processNext);
|
|
1282
|
+
});
|
|
1283
|
+
};
|
|
1284
|
+
setImmediate(processNext);
|
|
1285
|
+
}
|
|
1286
|
+
// Tells whether a text segment is worth handing to linkify-it, see
|
|
1287
|
+
// MAX_LINKIFY_* above. Note that linkify.pretest() can not be used for this
|
|
1288
|
+
// - it is a quick deny that also denies links its own matcher finds, eg. a
|
|
1289
|
+
// host followed by a fullwidth vertical bar.
|
|
1290
|
+
/** @internal */
|
|
1291
|
+
canScanSegment(segment) {
|
|
1292
|
+
// every link linkify-it can find contains at least one of these
|
|
1293
|
+
if (segment.length > MAX_LINKIFY_SEGMENT_LENGTH || !/[.:@/]/.test(segment)) {
|
|
1294
|
+
return false;
|
|
1295
|
+
}
|
|
1296
|
+
// Dots only chain up into one host candidate while the characters
|
|
1297
|
+
// around them could be part of a host, so a slash, comma or colon ends
|
|
1298
|
+
// the chain and dots in a path or query do not add to it.
|
|
1299
|
+
let labels = 0;
|
|
1300
|
+
for (let i = 0, len = segment.length; i < len; i++) {
|
|
1301
|
+
let code = segment.charCodeAt(i);
|
|
1302
|
+
if (code === 0x2e) {
|
|
1303
|
+
if (++labels > MAX_LINKIFY_HOST_LABELS) {
|
|
1304
|
+
return false;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
else if (!isHostChar(code)) {
|
|
1308
|
+
labels = 0;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
return true;
|
|
1312
|
+
}
|
|
1313
|
+
/** @internal */
|
|
1314
|
+
findLinks(str) {
|
|
1315
|
+
let links = [];
|
|
1316
|
+
if (this.linkifyWork >= MAX_LINKIFY_WORK || !linkify.pretest(str)) {
|
|
1317
|
+
return links;
|
|
1318
|
+
}
|
|
1319
|
+
// Whitespace can not be part of a link and linkify-it treats the end of
|
|
1320
|
+
// a segment exactly like whitespace, so scanning segment by segment
|
|
1321
|
+
// finds the same links as scanning the entire string would. U+FEFF is
|
|
1322
|
+
// whitespace for Javascript but an ordinary link character for
|
|
1323
|
+
// linkify-it, so it must not end a segment.
|
|
1324
|
+
let segmentRegex = /[\S\uFEFF]+/g;
|
|
1325
|
+
let segmentMatch;
|
|
1326
|
+
while ((segmentMatch = segmentRegex.exec(str)) !== null) {
|
|
1327
|
+
let segment = segmentMatch[0];
|
|
1328
|
+
if (!this.canScanSegment(segment)) {
|
|
1329
|
+
// too expensive to scan or can not match at all, keep as plain text
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1332
|
+
let matches = linkify.match(segment);
|
|
1333
|
+
for (let i = 0; matches && i < matches.length; i++) {
|
|
1334
|
+
matches[i].index += segmentMatch.index;
|
|
1335
|
+
matches[i].lastIndex += segmentMatch.index;
|
|
1336
|
+
links.push(matches[i]);
|
|
1337
|
+
}
|
|
1338
|
+
this.linkifyWork += segment.length * (segment.length + 1024);
|
|
1339
|
+
if (this.linkifyWork >= MAX_LINKIFY_WORK) {
|
|
1340
|
+
break;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return links;
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Converts plain text to HTML the way `textAsHtml` is built: HTML-escaped, paragraphs and line
|
|
1347
|
+
* breaks kept, and links turned into anchors unless `skipTextLinks` is set. Returns an empty
|
|
1348
|
+
* string when `skipTextToHtml` is set.
|
|
1349
|
+
*/
|
|
1350
|
+
textToHtml(str) {
|
|
1351
|
+
if (this.options.skipTextToHtml) {
|
|
1352
|
+
return '';
|
|
1353
|
+
}
|
|
1354
|
+
str = (str || '').toString();
|
|
1355
|
+
let encoded;
|
|
1356
|
+
if (!this.options.skipTextLinks) {
|
|
1357
|
+
try {
|
|
1358
|
+
let links = this.findLinks(str);
|
|
1359
|
+
if (links.length) {
|
|
1360
|
+
let result = [];
|
|
1361
|
+
let last = 0;
|
|
1362
|
+
links.forEach(link => {
|
|
1363
|
+
if (last < link.index) {
|
|
1364
|
+
let textPart = he_1.default
|
|
1365
|
+
// encode special chars
|
|
1366
|
+
.encode(str.slice(last, link.index), {
|
|
1367
|
+
useNamedReferences: true
|
|
1368
|
+
});
|
|
1369
|
+
result.push(textPart);
|
|
1370
|
+
}
|
|
1371
|
+
// Escape quotes in URL to prevent XSS
|
|
1372
|
+
let safeUrl = link.url.replace(/"/g, '"');
|
|
1373
|
+
// Escape HTML entities in link text
|
|
1374
|
+
let safeText = he_1.default.encode(link.text, { useNamedReferences: true });
|
|
1375
|
+
result.push(`<a href="${safeUrl}">${safeText}</a>`);
|
|
1376
|
+
last = link.lastIndex;
|
|
1377
|
+
});
|
|
1378
|
+
let textPart = he_1.default
|
|
1379
|
+
// encode special chars
|
|
1380
|
+
.encode(str.slice(last), {
|
|
1381
|
+
useNamedReferences: true
|
|
1382
|
+
});
|
|
1383
|
+
result.push(textPart);
|
|
1384
|
+
encoded = result.join('');
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
catch {
|
|
1388
|
+
// failed, don't linkify
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
// nothing linkified, or linkifying threw halfway through
|
|
1392
|
+
if (!encoded) {
|
|
1393
|
+
encoded = he_1.default
|
|
1394
|
+
// encode special chars
|
|
1395
|
+
.encode(str, {
|
|
1396
|
+
useNamedReferences: true
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
let text = '<p>' +
|
|
1400
|
+
encoded
|
|
1401
|
+
.replace(/\r?\n/g, '\n')
|
|
1402
|
+
.trim() // normalize line endings
|
|
1403
|
+
// Trims trailing spaces and tabs off every line. The leading
|
|
1404
|
+
// group keeps this linear, plain /[ \t]+$/gm rescans the whole
|
|
1405
|
+
// run from every position inside it, which is quadratic. The
|
|
1406
|
+
// lookahead lists the line terminators that multiline $ matches
|
|
1407
|
+
// before, the group covers the line starts.
|
|
1408
|
+
.replace(/(^|[^ \t])[ \t]+(?=[\n\r\u2028\u2029]|$)/g, '$1')
|
|
1409
|
+
.trim() // trim empty line endings
|
|
1410
|
+
.replace(/\n\n+/g, '</p><p>')
|
|
1411
|
+
.trim() // insert <p> to multiple linebreaks
|
|
1412
|
+
.replace(/\n/g, '<br/>') + // insert <br> to single linebreaks
|
|
1413
|
+
'</p>';
|
|
1414
|
+
return text;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
exports.default = MailParser;
|
|
1418
|
+
module.exports = exports.default;
|
|
1419
|
+
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
|