@nocobase/plugin-workflow-mailer 2.2.20 → 2.2.21
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/dist/externalVersion.js +6 -6
- package/dist/node_modules/joi/package.json +1 -1
- package/dist/node_modules/nodemailer/dist/cjs/addressparser/index.d.ts +59 -0
- package/dist/node_modules/nodemailer/dist/cjs/addressparser/index.js +790 -0
- package/dist/node_modules/nodemailer/dist/cjs/base64/index.d.ts +36 -0
- package/dist/node_modules/nodemailer/dist/cjs/base64/index.js +119 -0
- package/dist/node_modules/nodemailer/dist/cjs/dkim/index.d.ts +52 -0
- package/dist/node_modules/nodemailer/dist/cjs/dkim/index.js +228 -0
- package/dist/node_modules/nodemailer/dist/cjs/dkim/message-parser.d.ts +48 -0
- package/dist/node_modules/nodemailer/dist/cjs/dkim/message-parser.js +180 -0
- package/dist/node_modules/nodemailer/dist/cjs/dkim/relaxed-body.d.ts +36 -0
- package/dist/node_modules/nodemailer/dist/cjs/dkim/relaxed-body.js +148 -0
- package/dist/node_modules/nodemailer/dist/cjs/dkim/sign.d.ts +52 -0
- package/dist/node_modules/nodemailer/dist/cjs/dkim/sign.js +165 -0
- package/dist/node_modules/nodemailer/dist/cjs/errors.d.ts +96 -0
- package/dist/node_modules/nodemailer/dist/cjs/errors.js +70 -0
- package/dist/node_modules/nodemailer/dist/cjs/fetch/cookies.d.ts +107 -0
- package/dist/node_modules/nodemailer/dist/cjs/fetch/cookies.js +282 -0
- package/dist/node_modules/nodemailer/dist/cjs/fetch/index.d.ts +65 -0
- package/dist/node_modules/nodemailer/dist/cjs/fetch/index.js +384 -0
- package/dist/node_modules/nodemailer/dist/cjs/json-transport/index.d.ts +64 -0
- package/dist/node_modules/nodemailer/dist/cjs/json-transport/index.js +99 -0
- package/dist/node_modules/nodemailer/dist/cjs/mail-composer/index.d.ts +174 -0
- package/dist/node_modules/nodemailer/dist/cjs/mail-composer/index.js +613 -0
- package/dist/node_modules/nodemailer/dist/cjs/mailer/index.d.ts +237 -0
- package/dist/node_modules/nodemailer/dist/cjs/mailer/index.js +415 -0
- package/dist/node_modules/nodemailer/dist/cjs/mailer/mail-message.d.ts +97 -0
- package/dist/node_modules/nodemailer/dist/cjs/mailer/mail-message.js +342 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/index.d.ts +161 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/index.js +787 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/mime-types.d.ts +2 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/mime-types.js +2103 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-node/index.d.ts +333 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-node/index.js +1493 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-node/last-newline.d.ts +5 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-node/last-newline.js +32 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-unix.d.ts +9 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-unix.js +40 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-windows.d.ts +10 -0
- package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-windows.js +47 -0
- package/dist/node_modules/nodemailer/dist/cjs/nodemailer.d.ts +124 -0
- package/dist/node_modules/nodemailer/dist/cjs/nodemailer.js +1 -0
- package/dist/node_modules/nodemailer/dist/cjs/package-info.d.ts +3 -0
- package/dist/node_modules/nodemailer/dist/cjs/package-info.js +7 -0
- package/dist/node_modules/nodemailer/dist/cjs/package.json +3 -0
- package/dist/node_modules/nodemailer/dist/cjs/punycode/index.d.ts +73 -0
- package/dist/node_modules/nodemailer/{lib → dist/cjs}/punycode/index.js +62 -111
- package/dist/node_modules/nodemailer/dist/cjs/qp/index.d.ts +32 -0
- package/dist/node_modules/nodemailer/dist/cjs/qp/index.js +207 -0
- package/dist/node_modules/nodemailer/dist/cjs/sendmail-transport/index.d.ts +69 -0
- package/dist/node_modules/nodemailer/dist/cjs/sendmail-transport/index.js +210 -0
- package/dist/node_modules/nodemailer/dist/cjs/ses-transport/index.d.ts +112 -0
- package/dist/node_modules/nodemailer/dist/cjs/ses-transport/index.js +252 -0
- package/dist/node_modules/nodemailer/dist/cjs/shared/index.d.ts +260 -0
- package/dist/node_modules/nodemailer/dist/cjs/shared/index.js +680 -0
- package/dist/node_modules/nodemailer/dist/cjs/shared/objects.d.ts +23 -0
- package/dist/node_modules/nodemailer/dist/cjs/shared/objects.js +43 -0
- package/dist/node_modules/nodemailer/dist/cjs/shared/url.d.ts +21 -0
- package/dist/node_modules/nodemailer/dist/cjs/shared/url.js +255 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/data-stream.d.ts +15 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/data-stream.js +108 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.d.ts +47 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.js +189 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/index.d.ts +416 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/index.js +1837 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/index.d.ts +133 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/index.js +569 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/pool-resource.d.ts +62 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/pool-resource.js +257 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-transport/index.d.ts +156 -0
- package/dist/node_modules/nodemailer/dist/cjs/smtp-transport/index.js +371 -0
- package/dist/node_modules/nodemailer/dist/cjs/stream-transport/index.d.ts +65 -0
- package/dist/node_modules/nodemailer/dist/cjs/stream-transport/index.js +146 -0
- package/dist/node_modules/nodemailer/dist/cjs/well-known/index.d.ts +35 -0
- package/dist/node_modules/nodemailer/dist/cjs/well-known/index.js +41 -0
- package/dist/node_modules/nodemailer/dist/cjs/well-known/services.d.ts +2 -0
- package/dist/node_modules/nodemailer/dist/cjs/well-known/services.js +674 -0
- package/dist/node_modules/nodemailer/dist/cjs/xoauth2/index.d.ts +201 -0
- package/dist/node_modules/nodemailer/dist/cjs/xoauth2/index.js +426 -0
- package/dist/node_modules/nodemailer/dist/esm/addressparser/index.d.ts +59 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/addressparser/index.js +361 -131
- package/dist/node_modules/nodemailer/dist/esm/base64/index.d.ts +36 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/base64/index.js +31 -56
- package/dist/node_modules/nodemailer/dist/esm/dkim/index.d.ts +52 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/dkim/index.js +49 -78
- package/dist/node_modules/nodemailer/dist/esm/dkim/message-parser.d.ts +48 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/dkim/message-parser.js +64 -42
- package/dist/node_modules/nodemailer/dist/esm/dkim/relaxed-body.d.ts +36 -0
- package/dist/node_modules/nodemailer/dist/esm/dkim/relaxed-body.js +141 -0
- package/dist/node_modules/nodemailer/dist/esm/dkim/sign.d.ts +52 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/dkim/sign.js +50 -46
- package/dist/node_modules/nodemailer/dist/esm/errors.d.ts +96 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/errors.js +26 -18
- package/dist/node_modules/nodemailer/dist/esm/fetch/cookies.d.ts +107 -0
- package/dist/node_modules/nodemailer/dist/esm/fetch/cookies.js +242 -0
- package/dist/node_modules/nodemailer/dist/esm/fetch/index.d.ts +65 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/fetch/index.js +80 -147
- package/dist/node_modules/nodemailer/dist/esm/json-transport/index.d.ts +64 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/json-transport/index.js +19 -39
- package/dist/node_modules/nodemailer/dist/esm/mail-composer/index.d.ts +174 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mail-composer/index.js +199 -265
- package/dist/node_modules/nodemailer/dist/esm/mailer/index.d.ts +237 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mailer/index.js +128 -234
- package/dist/node_modules/nodemailer/dist/esm/mailer/mail-message.d.ts +97 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mailer/mail-message.js +62 -114
- package/dist/node_modules/nodemailer/dist/esm/mime-funcs/index.d.ts +161 -0
- package/dist/node_modules/nodemailer/dist/esm/mime-funcs/index.js +738 -0
- package/dist/node_modules/nodemailer/dist/esm/mime-funcs/mime-types.d.ts +2 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-funcs/mime-types.js +29 -42
- package/dist/node_modules/nodemailer/dist/esm/mime-node/index.d.ts +333 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/index.js +259 -372
- package/dist/node_modules/nodemailer/dist/esm/mime-node/last-newline.d.ts +5 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/last-newline.js +4 -10
- package/dist/node_modules/nodemailer/dist/esm/mime-node/le-unix.d.ts +9 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/le-unix.js +6 -11
- package/dist/node_modules/nodemailer/dist/esm/mime-node/le-windows.d.ts +10 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/le-windows.js +6 -13
- package/dist/node_modules/nodemailer/dist/esm/nodemailer.d.ts +124 -0
- package/dist/node_modules/nodemailer/dist/esm/nodemailer.js +166 -0
- package/dist/node_modules/nodemailer/dist/esm/package-info.d.ts +3 -0
- package/dist/node_modules/nodemailer/dist/esm/package-info.js +4 -0
- package/dist/node_modules/nodemailer/dist/esm/package.json +3 -0
- package/dist/node_modules/nodemailer/dist/esm/punycode/index.d.ts +73 -0
- package/dist/node_modules/nodemailer/dist/esm/punycode/index.js +405 -0
- package/dist/node_modules/nodemailer/dist/esm/qp/index.d.ts +32 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/qp/index.js +38 -67
- package/dist/node_modules/nodemailer/dist/esm/sendmail-transport/index.d.ts +69 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/sendmail-transport/index.js +51 -101
- package/dist/node_modules/nodemailer/dist/esm/ses-transport/index.d.ts +112 -0
- package/dist/node_modules/nodemailer/dist/esm/ses-transport/index.js +212 -0
- package/dist/node_modules/nodemailer/dist/esm/shared/index.d.ts +260 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/shared/index.js +156 -272
- package/dist/node_modules/nodemailer/dist/esm/shared/objects.d.ts +23 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/shared/objects.js +13 -17
- package/dist/node_modules/nodemailer/dist/esm/shared/url.d.ts +21 -0
- package/dist/node_modules/nodemailer/dist/esm/shared/url.js +214 -0
- package/dist/node_modules/nodemailer/dist/esm/smtp-connection/data-stream.d.ts +15 -0
- package/dist/node_modules/nodemailer/dist/esm/smtp-connection/data-stream.js +103 -0
- package/dist/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.d.ts +47 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-connection/http-proxy-client.js +49 -74
- package/dist/node_modules/nodemailer/dist/esm/smtp-connection/index.d.ts +416 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-connection/index.js +574 -794
- package/dist/node_modules/nodemailer/dist/esm/smtp-pool/index.d.ts +133 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-pool/index.js +172 -284
- package/dist/node_modules/nodemailer/dist/esm/smtp-pool/pool-resource.d.ts +62 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-pool/pool-resource.js +57 -97
- package/dist/node_modules/nodemailer/dist/esm/smtp-transport/index.d.ts +156 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-transport/index.js +81 -179
- package/dist/node_modules/nodemailer/dist/esm/stream-transport/index.d.ts +65 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/stream-transport/index.js +36 -74
- package/dist/node_modules/nodemailer/dist/esm/well-known/index.d.ts +35 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/well-known/index.js +5 -16
- package/dist/node_modules/nodemailer/dist/esm/well-known/services.d.ts +2 -0
- package/dist/node_modules/nodemailer/dist/esm/well-known/services.js +671 -0
- package/dist/node_modules/nodemailer/dist/esm/xoauth2/index.d.ts +201 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/xoauth2/index.js +125 -187
- package/dist/node_modules/nodemailer/{lib → dist}/well-known/services.json +9 -2
- package/dist/node_modules/nodemailer/package.json +1 -1
- package/package.json +3 -3
- package/dist/node_modules/nodemailer/.gitattributes +0 -6
- package/dist/node_modules/nodemailer/.ncurc.js +0 -9
- package/dist/node_modules/nodemailer/.prettierignore +0 -8
- package/dist/node_modules/nodemailer/.prettierrc +0 -12
- package/dist/node_modules/nodemailer/.prettierrc.js +0 -10
- package/dist/node_modules/nodemailer/.release-please-config.json +0 -9
- package/dist/node_modules/nodemailer/eslint.config.js +0 -88
- package/dist/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
- package/dist/node_modules/nodemailer/lib/fetch/cookies.js +0 -276
- package/dist/node_modules/nodemailer/lib/mime-funcs/index.js +0 -671
- package/dist/node_modules/nodemailer/lib/nodemailer.js +0 -1
- package/dist/node_modules/nodemailer/lib/ses-transport/index.js +0 -266
- package/dist/node_modules/nodemailer/lib/shared/url.js +0 -151
- package/dist/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -105
|
@@ -0,0 +1,1493 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* eslint no-undefined: 0, prefer-spread: 0, no-control-regex: 0 */
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
|
+
};
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
41
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
42
|
+
const punycode = __importStar(require("../punycode/index.js"));
|
|
43
|
+
const node_stream_1 = require("node:stream");
|
|
44
|
+
const shared = __importStar(require("../shared/index.js"));
|
|
45
|
+
const node_url_1 = __importDefault(require("node:url"));
|
|
46
|
+
const mimeFuncs = __importStar(require("../mime-funcs/index.js"));
|
|
47
|
+
const qp = __importStar(require("../qp/index.js"));
|
|
48
|
+
const base64 = __importStar(require("../base64/index.js"));
|
|
49
|
+
const index_js_1 = __importDefault(require("../addressparser/index.js"));
|
|
50
|
+
const index_js_2 = __importDefault(require("../fetch/index.js"));
|
|
51
|
+
const errors = __importStar(require("../errors.js"));
|
|
52
|
+
const last_newline_js_1 = __importDefault(require("./last-newline.js"));
|
|
53
|
+
const le_windows_js_1 = __importDefault(require("./le-windows.js"));
|
|
54
|
+
const le_unix_js_1 = __importDefault(require("./le-unix.js"));
|
|
55
|
+
const FORMATTED_HEADERS = ['From', 'Sender', 'To', 'Cc', 'Bcc', 'Reply-To', 'Date', 'References'];
|
|
56
|
+
// RFC 5321 atext, plus the non-ascii bytes that SMTPUTF8 (RFC 6531) adds to it. A local part
|
|
57
|
+
// built from these, with '.' as a separator, is a dot-atom and can be emitted bare
|
|
58
|
+
const ATEXT = "[A-Za-z0-9!#$%&'*+\\-/=?^_`{|}~\\x80-\\uFFFF]";
|
|
59
|
+
const DOT_ATOM = new RegExp('^' + ATEXT + '+(?:\\.' + ATEXT + '+)*$');
|
|
60
|
+
// A complete quoted-string: everything between the outer quotes is either a plain char or
|
|
61
|
+
// a quoted-pair. Anchored, so a value that only starts and ends with a quote does not pass
|
|
62
|
+
const QUOTED_STRING = /^"(?:[^"\\]|\\[\s\S])*"$/;
|
|
63
|
+
// An address that carries no special anywhere can be emitted bare in a header, everything
|
|
64
|
+
// else goes into angle brackets so that the header can not be read as more addresses than
|
|
65
|
+
// the envelope carries
|
|
66
|
+
const PLAIN_ADDRESS = /^[^\s"(),:;<>@[\\\]]+@[^\s"(),:;<>@[\\\]]+$/;
|
|
67
|
+
// domainToASCII and domainToUnicode are WHATWG host parsers rather than plain IDNA
|
|
68
|
+
// mappers, so they do more than map: they cut the host at '/', '\\', '?' and '#', drop C0
|
|
69
|
+
// controls, and percent-decode. Handing them 'evil.example/mail.corp.example' returns the
|
|
70
|
+
// deliverable 'evil.example', which would turn a value the bundled codec leaves as
|
|
71
|
+
// unroutable garbage into mail for a domain the sender never named. None of these
|
|
72
|
+
// characters are legal in a domain, so keep them away from the mapper.
|
|
73
|
+
const URL_PARSER_UNSAFE = /[/\\?#%\x00-\x20\x7F]/;
|
|
74
|
+
/**
|
|
75
|
+
* Encodes a domain the way browsers, the WHATWG URL Standard and DNS facing resolvers do,
|
|
76
|
+
* which is with UTS-46 mapping applied before the Punycode step.
|
|
77
|
+
*
|
|
78
|
+
* The bundled codec is plain RFC 3492 and maps nothing, so it disagrees with every
|
|
79
|
+
* conformant parser on any domain holding a mapped or ignored code point. An invisible
|
|
80
|
+
* U+00AD in 'compa\u00ADny.com' encoded to 'xn--company-pka.com' where a validator reads
|
|
81
|
+
* 'company.com', which let an allow-listed domain be checked and a different one mailed.
|
|
82
|
+
*
|
|
83
|
+
* Anything the URL parser does not accept as a hostname, an address literal such as
|
|
84
|
+
* '[127.0.0.1]' included, comes back empty and falls through to the bundled codec, which
|
|
85
|
+
* leaves those as they were supplied.
|
|
86
|
+
*
|
|
87
|
+
* @param domain Domain to encode, already lowercased by the caller
|
|
88
|
+
* @param toUnicode Return the U-label form instead of the A-label form
|
|
89
|
+
* @return Encoded domain
|
|
90
|
+
*/
|
|
91
|
+
function normalizeDomain(domain, toUnicode) {
|
|
92
|
+
// domainToASCII and domainToUnicode landed in Node 7, the bundled codec covers Node 6
|
|
93
|
+
const mapper = toUnicode ? node_url_1.default.domainToUnicode : node_url_1.default.domainToASCII;
|
|
94
|
+
if (typeof mapper === 'function' && !URL_PARSER_UNSAFE.test(domain)) {
|
|
95
|
+
const mapped = mapper(domain);
|
|
96
|
+
if (mapped) {
|
|
97
|
+
return mapped;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return toUnicode ? punycode.toUnicode(domain) : punycode.toASCII(domain);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Removes the characters that must never reach a multipart delimiter line.
|
|
104
|
+
*
|
|
105
|
+
* A line break splits the delimiter, so the boundary declared in the header can never
|
|
106
|
+
* match it again and the parts go out as body lines instead. The other C0 controls and
|
|
107
|
+
* DEL do not split anything but must not be written either: RFC 5321 does not allow a
|
|
108
|
+
* NUL in DATA at all, and an MTA or a scanner that stops at one reads a different
|
|
109
|
+
* message than a client that does not, which is the same parser disagreement a split
|
|
110
|
+
* delimiter creates. Both sides are cleaned together, since the declared value and the
|
|
111
|
+
* delimiters come from this one result.
|
|
112
|
+
*
|
|
113
|
+
* @param value Value to clean
|
|
114
|
+
* @return Value with every control character removed
|
|
115
|
+
*/
|
|
116
|
+
function _stripBoundaryControls(value) {
|
|
117
|
+
return value.replace(/[\x00-\x1f\x7f]+/g, '');
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Creates a new mime tree node. Assumes 'multipart/*' as the content type
|
|
121
|
+
* if it is a branch, anything else counts as leaf. If rootNode is missing from
|
|
122
|
+
* the options, assumes this is the root.
|
|
123
|
+
*
|
|
124
|
+
* @param contentType Define the content type for the node. Can be left blank for attachments (derived from filename)
|
|
125
|
+
* @param [options] optional options
|
|
126
|
+
* @param [options.rootNode] root node for this tree
|
|
127
|
+
* @param [options.parentNode] immediate parent for this node
|
|
128
|
+
* @param [options.filename] filename for an attachment node
|
|
129
|
+
* @param [options.baseBoundary] shared part of the unique multipart boundary
|
|
130
|
+
* @param [options.keepBcc] If true, do not exclude Bcc from the generated headers
|
|
131
|
+
* @param [options.normalizeHeaderKey] method to normalize header keys for custom caseing
|
|
132
|
+
* @param [options.textEncoding] either 'Q' (the default) or 'B'
|
|
133
|
+
*/
|
|
134
|
+
class MimeNode {
|
|
135
|
+
constructor(contentType, options) {
|
|
136
|
+
this.nodeCounter = 0;
|
|
137
|
+
options = options || {};
|
|
138
|
+
/**
|
|
139
|
+
* shared part of the unique multipart boundary. Control characters are dropped
|
|
140
|
+
* here rather than at the delimiter, see _stripBoundaryControls
|
|
141
|
+
*/
|
|
142
|
+
this.baseBoundary = _stripBoundaryControls(options.baseBoundary || node_crypto_1.default.randomBytes(8).toString('hex'));
|
|
143
|
+
this.boundaryPrefix = _stripBoundaryControls(options.boundaryPrefix || '--_NmP');
|
|
144
|
+
this.disableFileAccess = !!options.disableFileAccess;
|
|
145
|
+
this.disableUrlAccess = !!options.disableUrlAccess;
|
|
146
|
+
this.normalizeHeaderKey = options.normalizeHeaderKey;
|
|
147
|
+
/**
|
|
148
|
+
* If date headers is missing and current node is the root, this value is used instead
|
|
149
|
+
*/
|
|
150
|
+
this.date = options.parentNode ? null : new Date();
|
|
151
|
+
/**
|
|
152
|
+
* Root node for current mime tree
|
|
153
|
+
*/
|
|
154
|
+
this.rootNode = options.rootNode || this;
|
|
155
|
+
/**
|
|
156
|
+
* If true include Bcc in generated headers (if available)
|
|
157
|
+
*/
|
|
158
|
+
this.keepBcc = !!options.keepBcc;
|
|
159
|
+
/**
|
|
160
|
+
* If filename is specified but contentType is not (probably an attachment)
|
|
161
|
+
* detect the content type from filename extension
|
|
162
|
+
*/
|
|
163
|
+
if (options.filename) {
|
|
164
|
+
/**
|
|
165
|
+
* Filename for this node. Useful with attachments
|
|
166
|
+
*/
|
|
167
|
+
this.filename = options.filename;
|
|
168
|
+
if (!contentType) {
|
|
169
|
+
contentType = mimeFuncs.detectMimeType(this.filename.split('.').pop());
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Indicates which encoding should be used for header strings: "Q" or "B"
|
|
174
|
+
*/
|
|
175
|
+
this.textEncoding = (options.textEncoding || '').toString().trim().charAt(0).toUpperCase();
|
|
176
|
+
/**
|
|
177
|
+
* Immediate parent for this node (or undefined if not set)
|
|
178
|
+
*/
|
|
179
|
+
this.parentNode = options.parentNode;
|
|
180
|
+
/**
|
|
181
|
+
* Hostname for default message-id values
|
|
182
|
+
*/
|
|
183
|
+
this.hostname = options.hostname;
|
|
184
|
+
/**
|
|
185
|
+
* If set to 'win' then uses \r\n, if 'linux' then \n. If not set (or `raw` is used) then newlines are kept as is.
|
|
186
|
+
*/
|
|
187
|
+
this.newline = options.newline;
|
|
188
|
+
/**
|
|
189
|
+
* An array for possible child nodes
|
|
190
|
+
*/
|
|
191
|
+
this.childNodes = [];
|
|
192
|
+
/**
|
|
193
|
+
* Used for generating unique boundaries (prepended to the shared base)
|
|
194
|
+
*/
|
|
195
|
+
this._nodeId = ++this.rootNode.nodeCounter;
|
|
196
|
+
/**
|
|
197
|
+
* A list of header values for this node in the form of [{key:'', value:''}]
|
|
198
|
+
*/
|
|
199
|
+
this._headers = [];
|
|
200
|
+
/**
|
|
201
|
+
* True if the content only uses ASCII printable characters
|
|
202
|
+
* @type {Boolean}
|
|
203
|
+
*/
|
|
204
|
+
this._isPlainText = false;
|
|
205
|
+
/**
|
|
206
|
+
* True if the content is plain text but has longer lines than allowed
|
|
207
|
+
* @type {Boolean}
|
|
208
|
+
*/
|
|
209
|
+
this._hasLongLines = false;
|
|
210
|
+
/**
|
|
211
|
+
* If set, use instead this value for envelopes instead of generating one
|
|
212
|
+
* @type {Boolean}
|
|
213
|
+
*/
|
|
214
|
+
this._envelope = false;
|
|
215
|
+
/**
|
|
216
|
+
* If set then use this value as the stream content instead of building it
|
|
217
|
+
* @type {String|Buffer|Stream}
|
|
218
|
+
*/
|
|
219
|
+
this._raw = false;
|
|
220
|
+
/**
|
|
221
|
+
* Additional transform streams that the message will be piped before
|
|
222
|
+
* exposing by createReadStream
|
|
223
|
+
* @type {Array}
|
|
224
|
+
*/
|
|
225
|
+
this._transforms = [];
|
|
226
|
+
/**
|
|
227
|
+
* Additional process functions that the message will be piped through before
|
|
228
|
+
* exposing by createReadStream. These functions are run after transforms
|
|
229
|
+
* @type {Array}
|
|
230
|
+
*/
|
|
231
|
+
this._processFuncs = [];
|
|
232
|
+
/**
|
|
233
|
+
* If content type is set (or derived from the filename) add it to headers
|
|
234
|
+
*/
|
|
235
|
+
if (contentType) {
|
|
236
|
+
this.setHeader('Content-Type', contentType);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/////// PUBLIC METHODS
|
|
240
|
+
/**
|
|
241
|
+
* Creates and appends a child node.Arguments provided are passed to MimeNode constructor
|
|
242
|
+
*
|
|
243
|
+
* @param [contentType] Optional content type
|
|
244
|
+
* @param [options] Optional options object
|
|
245
|
+
* @return Created node object
|
|
246
|
+
*/
|
|
247
|
+
createChild(contentType, options) {
|
|
248
|
+
if (!options && typeof contentType === 'object') {
|
|
249
|
+
options = contentType;
|
|
250
|
+
contentType = undefined;
|
|
251
|
+
}
|
|
252
|
+
const node = new MimeNode(contentType, options);
|
|
253
|
+
this.appendChild(node);
|
|
254
|
+
return node;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Appends an existing node to the mime tree. Removes the node from an existing
|
|
258
|
+
* tree if needed
|
|
259
|
+
*
|
|
260
|
+
* @param childNode node to be appended
|
|
261
|
+
* @return Appended node object
|
|
262
|
+
*/
|
|
263
|
+
appendChild(childNode) {
|
|
264
|
+
// Take the node out of the tree it is in first. Leaving it there keeps it in that
|
|
265
|
+
// parent's childNodes, so it still streams as part of the old tree while parentNode
|
|
266
|
+
// already points at the new one, and anything read off the parent chain answers for
|
|
267
|
+
// the wrong tree.
|
|
268
|
+
if (childNode.parentNode && childNode.parentNode !== this) {
|
|
269
|
+
childNode.remove();
|
|
270
|
+
}
|
|
271
|
+
if (childNode.rootNode !== this.rootNode) {
|
|
272
|
+
childNode.rootNode = this.rootNode;
|
|
273
|
+
childNode._nodeId = ++this.rootNode.nodeCounter;
|
|
274
|
+
}
|
|
275
|
+
childNode.parentNode = this;
|
|
276
|
+
this.childNodes.push(childNode);
|
|
277
|
+
return childNode;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Replaces current node with another node
|
|
281
|
+
*
|
|
282
|
+
* @param node Replacement node
|
|
283
|
+
* @return Replacement node
|
|
284
|
+
*/
|
|
285
|
+
replace(node) {
|
|
286
|
+
if (node === this) {
|
|
287
|
+
return this;
|
|
288
|
+
}
|
|
289
|
+
this.parentNode.childNodes.forEach((childNode, i) => {
|
|
290
|
+
if (childNode === this) {
|
|
291
|
+
node.rootNode = this.rootNode;
|
|
292
|
+
node.parentNode = this.parentNode;
|
|
293
|
+
node._nodeId = this._nodeId;
|
|
294
|
+
this.rootNode = this;
|
|
295
|
+
this.parentNode = undefined;
|
|
296
|
+
node.parentNode.childNodes[i] = node;
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
return node;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Removes current node from the mime tree
|
|
303
|
+
*
|
|
304
|
+
* @return removed node
|
|
305
|
+
*/
|
|
306
|
+
remove() {
|
|
307
|
+
if (!this.parentNode) {
|
|
308
|
+
return this;
|
|
309
|
+
}
|
|
310
|
+
for (let i = this.parentNode.childNodes.length - 1; i >= 0; i--) {
|
|
311
|
+
if (this.parentNode.childNodes[i] === this) {
|
|
312
|
+
this.parentNode.childNodes.splice(i, 1);
|
|
313
|
+
this.parentNode = undefined;
|
|
314
|
+
this.rootNode = this;
|
|
315
|
+
return this;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Sets a header value. If the value for selected key exists, it is overwritten.
|
|
321
|
+
* You can set multiple values as well by using [{key:'', value:''}] or
|
|
322
|
+
* {key: 'value'} as the first argument.
|
|
323
|
+
*
|
|
324
|
+
* @param key Header key or a list of key value pairs
|
|
325
|
+
* @param value Header value
|
|
326
|
+
* @return current node
|
|
327
|
+
*/
|
|
328
|
+
setHeader(key, value) {
|
|
329
|
+
let added = false;
|
|
330
|
+
// Allow setting multiple headers at once
|
|
331
|
+
if (!value && key && typeof key === 'object') {
|
|
332
|
+
// allow {key:'content-type', value: 'text/plain'}
|
|
333
|
+
if (key.key && 'value' in key) {
|
|
334
|
+
this.setHeader(key.key, key.value);
|
|
335
|
+
}
|
|
336
|
+
else if (Array.isArray(key)) {
|
|
337
|
+
// allow [{key:'content-type', value: 'text/plain'}]
|
|
338
|
+
key.forEach(i => {
|
|
339
|
+
this.setHeader(i.key, i.value);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
// allow {'content-type': 'text/plain'}
|
|
344
|
+
Object.keys(key).forEach(i => {
|
|
345
|
+
this.setHeader(i, key[i]);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
return this;
|
|
349
|
+
}
|
|
350
|
+
key = this._normalizeHeaderKey(key);
|
|
351
|
+
const headerValue = {
|
|
352
|
+
key,
|
|
353
|
+
value
|
|
354
|
+
};
|
|
355
|
+
// Check if the value exists and overwrite
|
|
356
|
+
for (let i = 0, len = this._headers.length; i < len; i++) {
|
|
357
|
+
if (this._headers[i].key === key) {
|
|
358
|
+
if (!added) {
|
|
359
|
+
// replace the first match
|
|
360
|
+
this._headers[i] = headerValue;
|
|
361
|
+
added = true;
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
// remove following matches
|
|
365
|
+
this._headers.splice(i, 1);
|
|
366
|
+
i--;
|
|
367
|
+
len--;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// match not found, append the value
|
|
372
|
+
if (!added) {
|
|
373
|
+
this._headers.push(headerValue);
|
|
374
|
+
}
|
|
375
|
+
return this;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Adds a header value. If the value for selected key exists, the value is appended
|
|
379
|
+
* as a new field and old one is not touched.
|
|
380
|
+
* You can set multiple values as well by using [{key:'', value:''}] or
|
|
381
|
+
* {key: 'value'} as the first argument.
|
|
382
|
+
*
|
|
383
|
+
* @param key Header key or a list of key value pairs
|
|
384
|
+
* @param value Header value
|
|
385
|
+
* @return current node
|
|
386
|
+
*/
|
|
387
|
+
addHeader(key, value) {
|
|
388
|
+
// Allow setting multiple headers at once
|
|
389
|
+
if (!value && key && typeof key === 'object') {
|
|
390
|
+
// allow {key:'content-type', value: 'text/plain'}
|
|
391
|
+
if (key.key && key.value) {
|
|
392
|
+
this.addHeader(key.key, key.value);
|
|
393
|
+
}
|
|
394
|
+
else if (Array.isArray(key)) {
|
|
395
|
+
// allow [{key:'content-type', value: 'text/plain'}]
|
|
396
|
+
key.forEach(i => {
|
|
397
|
+
this.addHeader(i.key, i.value);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
// allow {'content-type': 'text/plain'}
|
|
402
|
+
Object.keys(key).forEach(i => {
|
|
403
|
+
this.addHeader(i, key[i]);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
return this;
|
|
407
|
+
}
|
|
408
|
+
else if (Array.isArray(value)) {
|
|
409
|
+
value.forEach(val => {
|
|
410
|
+
this.addHeader(key, val);
|
|
411
|
+
});
|
|
412
|
+
return this;
|
|
413
|
+
}
|
|
414
|
+
this._headers.push({
|
|
415
|
+
key: this._normalizeHeaderKey(key),
|
|
416
|
+
value
|
|
417
|
+
});
|
|
418
|
+
return this;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Retrieves the first mathcing value of a selected key
|
|
422
|
+
*
|
|
423
|
+
* @param key Key to search for
|
|
424
|
+
* @retun Value for the key
|
|
425
|
+
*/
|
|
426
|
+
getHeader(key) {
|
|
427
|
+
key = this._normalizeHeaderKey(key);
|
|
428
|
+
for (let i = 0, len = this._headers.length; i < len; i++) {
|
|
429
|
+
if (this._headers[i].key === key) {
|
|
430
|
+
return this._headers[i].value;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Sets body content for current node. If the value is a string, charset is added automatically
|
|
436
|
+
* to Content-Type (if it is text/*). If the value is a Buffer, you need to specify
|
|
437
|
+
* the charset yourself
|
|
438
|
+
*
|
|
439
|
+
* @param content Body content
|
|
440
|
+
* @return current node
|
|
441
|
+
*/
|
|
442
|
+
setContent(content) {
|
|
443
|
+
this.content = content;
|
|
444
|
+
if (typeof this.content.pipe === 'function') {
|
|
445
|
+
// pre-stream handler. might be triggered if a stream is set as content
|
|
446
|
+
// and 'error' fires before anything is done with this stream
|
|
447
|
+
this._contentErrorHandler = err => {
|
|
448
|
+
this.content.removeListener('error', this._contentErrorHandler);
|
|
449
|
+
this.content = err;
|
|
450
|
+
};
|
|
451
|
+
this.content.once('error', this._contentErrorHandler);
|
|
452
|
+
}
|
|
453
|
+
else if (typeof this.content === 'string') {
|
|
454
|
+
this._isPlainText = mimeFuncs.isPlainText(this.content);
|
|
455
|
+
if (this._isPlainText && mimeFuncs.hasLongerLines(this.content, 76)) {
|
|
456
|
+
// If there are lines longer than 76 symbols/bytes do not use 7bit
|
|
457
|
+
this._hasLongLines = true;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return this;
|
|
461
|
+
}
|
|
462
|
+
build(callback) {
|
|
463
|
+
let promise;
|
|
464
|
+
if (!callback) {
|
|
465
|
+
promise = new Promise((resolve, reject) => {
|
|
466
|
+
callback = shared.callbackPromise(resolve, reject);
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
const done = callback;
|
|
470
|
+
const stream = this.createReadStream();
|
|
471
|
+
const buf = [];
|
|
472
|
+
let buflen = 0;
|
|
473
|
+
let returned = false;
|
|
474
|
+
stream.on('readable', () => {
|
|
475
|
+
let chunk;
|
|
476
|
+
while ((chunk = stream.read()) !== null) {
|
|
477
|
+
buf.push(chunk);
|
|
478
|
+
buflen += chunk.length;
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
stream.once('error', err => {
|
|
482
|
+
if (returned) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
returned = true;
|
|
486
|
+
return done(err);
|
|
487
|
+
});
|
|
488
|
+
stream.once('end', (chunk) => {
|
|
489
|
+
if (returned) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
returned = true;
|
|
493
|
+
if (chunk && chunk.length) {
|
|
494
|
+
buf.push(chunk);
|
|
495
|
+
buflen += chunk.length;
|
|
496
|
+
}
|
|
497
|
+
return done(null, Buffer.concat(buf, buflen));
|
|
498
|
+
});
|
|
499
|
+
return promise;
|
|
500
|
+
}
|
|
501
|
+
getTransferEncoding() {
|
|
502
|
+
let transferEncoding = false;
|
|
503
|
+
const contentType = (this.getHeader('Content-Type') || '').toString().toLowerCase().trim();
|
|
504
|
+
if (this.content) {
|
|
505
|
+
transferEncoding = (this.getHeader('Content-Transfer-Encoding') || '').toString().toLowerCase().trim();
|
|
506
|
+
if (!transferEncoding || !['base64', 'quoted-printable'].includes(transferEncoding)) {
|
|
507
|
+
if (/^text\//i.test(contentType)) {
|
|
508
|
+
// If there are no special symbols, no need to modify the text
|
|
509
|
+
if (this._isPlainText && !this._hasLongLines) {
|
|
510
|
+
transferEncoding = '7bit';
|
|
511
|
+
}
|
|
512
|
+
else if (typeof this.content === 'string' || this.content instanceof Buffer) {
|
|
513
|
+
// detect preferred encoding for string value
|
|
514
|
+
transferEncoding = this._getTextEncoding(this.content) === 'Q' ? 'quoted-printable' : 'base64';
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
// we can not check content for a stream, so either use preferred encoding or fallback to QP
|
|
518
|
+
transferEncoding = this.textEncoding === 'B' ? 'base64' : 'quoted-printable';
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
else if (!/^(multipart|message)\//i.test(contentType)) {
|
|
522
|
+
transferEncoding = transferEncoding || 'base64';
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return transferEncoding;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Builds the header block for the mime node. Append \r\n\r\n before writing the content
|
|
530
|
+
*
|
|
531
|
+
* @returns Headers
|
|
532
|
+
*/
|
|
533
|
+
buildHeaders() {
|
|
534
|
+
const transferEncoding = this.getTransferEncoding();
|
|
535
|
+
const headers = [];
|
|
536
|
+
if (transferEncoding) {
|
|
537
|
+
this.setHeader('Content-Transfer-Encoding', transferEncoding);
|
|
538
|
+
}
|
|
539
|
+
if (this.filename && !this.getHeader('Content-Disposition')) {
|
|
540
|
+
this.setHeader('Content-Disposition', 'attachment');
|
|
541
|
+
}
|
|
542
|
+
// Ensure mandatory header fields
|
|
543
|
+
if (this.rootNode === this) {
|
|
544
|
+
if (!this.getHeader('Date')) {
|
|
545
|
+
this.setHeader('Date', this.date.toUTCString().replace(/GMT/, '+0000'));
|
|
546
|
+
}
|
|
547
|
+
// ensure that Message-Id is present
|
|
548
|
+
this.messageId();
|
|
549
|
+
if (!this.getHeader('MIME-Version')) {
|
|
550
|
+
this.setHeader('MIME-Version', '1.0');
|
|
551
|
+
}
|
|
552
|
+
// Ensure that Content-Type is the last header for the root node
|
|
553
|
+
for (let i = this._headers.length - 2; i >= 0; i--) {
|
|
554
|
+
const header = this._headers[i];
|
|
555
|
+
if (header.key === 'Content-Type') {
|
|
556
|
+
this._headers.splice(i, 1);
|
|
557
|
+
this._headers.push(header);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
this._headers.forEach(header => {
|
|
562
|
+
let key = header.key;
|
|
563
|
+
let value = header.value;
|
|
564
|
+
let structured;
|
|
565
|
+
let param;
|
|
566
|
+
const options = {};
|
|
567
|
+
const formattedHeaders = FORMATTED_HEADERS;
|
|
568
|
+
if (value && typeof value === 'object' && !formattedHeaders.includes(key)) {
|
|
569
|
+
// the keys come from a caller supplied header object and `options.prepared`
|
|
570
|
+
// below decides whether the value is emitted raw, so an own "__proto__" key
|
|
571
|
+
// here would turn an unfolded value into header injection
|
|
572
|
+
shared.copyOwnKeys(options, value, optionKey => optionKey === 'value');
|
|
573
|
+
value = (value.value || '').toString();
|
|
574
|
+
if (!value.trim()) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (options.prepared) {
|
|
579
|
+
// header value is
|
|
580
|
+
if (options.foldLines) {
|
|
581
|
+
headers.push(mimeFuncs.foldLines(key + ': ' + value));
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
headers.push(key + ': ' + value);
|
|
585
|
+
}
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
switch (header.key) {
|
|
589
|
+
case 'Content-Disposition':
|
|
590
|
+
structured = mimeFuncs.parseHeaderValue(value);
|
|
591
|
+
if (this.filename) {
|
|
592
|
+
structured.params.filename = this.filename;
|
|
593
|
+
}
|
|
594
|
+
value = mimeFuncs.buildHeaderValue(structured);
|
|
595
|
+
break;
|
|
596
|
+
case 'Content-Type':
|
|
597
|
+
structured = mimeFuncs.parseHeaderValue(value);
|
|
598
|
+
// the type token decides multipart and charset below, so clean it before
|
|
599
|
+
// those run and not just on the way out, otherwise a control char makes
|
|
600
|
+
// the checks miss and the header ends up claiming a type it is not set up for
|
|
601
|
+
structured.value = (structured.value || '').toString().replace(/[\x00-\x1f\x7f]/g, '');
|
|
602
|
+
this._handleContentType(structured);
|
|
603
|
+
if (structured.value.match(/^text\/plain\b/) &&
|
|
604
|
+
typeof this.content === 'string' &&
|
|
605
|
+
/[\u0080-\uFFFF]/.test(this.content)) {
|
|
606
|
+
structured.params.charset = 'utf-8';
|
|
607
|
+
}
|
|
608
|
+
value = mimeFuncs.buildHeaderValue(structured);
|
|
609
|
+
if (this.filename) {
|
|
610
|
+
// add support for non-compliant clients like QQ webmail
|
|
611
|
+
// we can't build the value with buildHeaderValue as the value is non standard and
|
|
612
|
+
// would be converted to parameter continuation encoding that we do not want
|
|
613
|
+
// control chars can not be quoted here: HT is a fold point that unfolding would
|
|
614
|
+
// turn into a space, CR/LF can not appear in a header at all and DEL is not
|
|
615
|
+
// qtext, so force the mime encoded word that a non-ascii filename would get anyway
|
|
616
|
+
param = /[\x00-\x1f\x7f]/.test(this.filename)
|
|
617
|
+
? mimeFuncs.encodeWord(this.filename, this._getTextEncoding(this.filename), 52)
|
|
618
|
+
: this._encodeWords(this.filename);
|
|
619
|
+
if (param !== this.filename || /[\s'"\\;:/=(),<>@[\]?]|^-/.test(param)) {
|
|
620
|
+
// include value in quotes if needed, escaping backslashes and quotes as
|
|
621
|
+
// quoted-pairs exactly like buildHeaderValue does for filename=, otherwise
|
|
622
|
+
// a trailing backslash would escape the closing quote
|
|
623
|
+
param = JSON.stringify(param);
|
|
624
|
+
}
|
|
625
|
+
value += '; name=' + param;
|
|
626
|
+
}
|
|
627
|
+
break;
|
|
628
|
+
case 'Bcc':
|
|
629
|
+
if (!this.keepBcc) {
|
|
630
|
+
// skip BCC values
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
value = this._encodeHeaderValue(key, value);
|
|
636
|
+
// skip empty lines
|
|
637
|
+
if (!(value || '').toString().trim()) {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (typeof this.normalizeHeaderKey === 'function') {
|
|
641
|
+
const normalized = this.normalizeHeaderKey(key, value);
|
|
642
|
+
// the result replaces the key on the way into the header, so it gets the same
|
|
643
|
+
// treatment the key it replaces already had. a line break here would end the
|
|
644
|
+
// header and start one of the caller's own
|
|
645
|
+
const cleaned = typeof normalized === 'string' ? normalized.replace(/[\x00-\x1f\x7f]/g, '') : '';
|
|
646
|
+
if (cleaned) {
|
|
647
|
+
key = cleaned;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
headers.push(mimeFuncs.foldLines(key + ': ' + value, 76));
|
|
651
|
+
});
|
|
652
|
+
return headers.join('\r\n');
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Streams the rfc2822 message from the current node. If this is a root node,
|
|
656
|
+
* mandatory header fields are set if missing (Date, Message-Id, MIME-Version)
|
|
657
|
+
*
|
|
658
|
+
* @return Compiled message
|
|
659
|
+
*/
|
|
660
|
+
createReadStream(options) {
|
|
661
|
+
options = options || {};
|
|
662
|
+
const stream = new node_stream_1.PassThrough(options);
|
|
663
|
+
let outputStream = stream;
|
|
664
|
+
let transform;
|
|
665
|
+
this.stream(stream, options, err => {
|
|
666
|
+
if (err) {
|
|
667
|
+
outputStream.emit('error', err);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
stream.end();
|
|
671
|
+
});
|
|
672
|
+
for (let i = 0, len = this._transforms.length; i < len; i++) {
|
|
673
|
+
transform =
|
|
674
|
+
typeof this._transforms[i] === 'function' ? this._transforms[i]() : this._transforms[i];
|
|
675
|
+
outputStream.once('error', err => {
|
|
676
|
+
transform.emit('error', err);
|
|
677
|
+
});
|
|
678
|
+
outputStream = outputStream.pipe(transform);
|
|
679
|
+
}
|
|
680
|
+
// ensure terminating newline after possible user transforms
|
|
681
|
+
transform = new last_newline_js_1.default();
|
|
682
|
+
outputStream.once('error', err => {
|
|
683
|
+
transform.emit('error', err);
|
|
684
|
+
});
|
|
685
|
+
outputStream = outputStream.pipe(transform);
|
|
686
|
+
// dkim and stuff
|
|
687
|
+
for (let i = 0, len = this._processFuncs.length; i < len; i++) {
|
|
688
|
+
transform = this._processFuncs[i];
|
|
689
|
+
outputStream = transform(outputStream);
|
|
690
|
+
}
|
|
691
|
+
if (this.newline) {
|
|
692
|
+
const winbreak = ['win', 'windows', 'dos', '\r\n'].includes(this.newline.toString().toLowerCase());
|
|
693
|
+
const newlineTransform = winbreak ? new le_windows_js_1.default() : new le_unix_js_1.default();
|
|
694
|
+
const stream = outputStream.pipe(newlineTransform);
|
|
695
|
+
outputStream.on('error', err => stream.emit('error', err));
|
|
696
|
+
return stream;
|
|
697
|
+
}
|
|
698
|
+
return outputStream;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Appends a transform stream object to the transforms list. Final output
|
|
702
|
+
* is passed through this stream before exposing
|
|
703
|
+
*
|
|
704
|
+
* @param transform Read-Write stream
|
|
705
|
+
*/
|
|
706
|
+
transform(transform) {
|
|
707
|
+
this._transforms.push(transform);
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Appends a post process function. The functon is run after transforms and
|
|
711
|
+
* uses the following syntax
|
|
712
|
+
*
|
|
713
|
+
* processFunc(input) -> outputStream
|
|
714
|
+
*
|
|
715
|
+
* @param processFunc Read-Write stream
|
|
716
|
+
*/
|
|
717
|
+
processFunc(processFunc) {
|
|
718
|
+
this._processFuncs.push(processFunc);
|
|
719
|
+
}
|
|
720
|
+
stream(outputStream, options, done) {
|
|
721
|
+
const transferEncoding = this.getTransferEncoding();
|
|
722
|
+
let contentStream;
|
|
723
|
+
let localStream;
|
|
724
|
+
// protect actual callback against multiple triggering
|
|
725
|
+
let returned = false;
|
|
726
|
+
const callback = (err) => {
|
|
727
|
+
if (returned) {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
returned = true;
|
|
731
|
+
done(err);
|
|
732
|
+
};
|
|
733
|
+
// for multipart nodes, push child nodes
|
|
734
|
+
// for content nodes end the stream
|
|
735
|
+
const finalize = () => {
|
|
736
|
+
let childId = 0;
|
|
737
|
+
const processChildNode = () => {
|
|
738
|
+
if (childId >= this.childNodes.length) {
|
|
739
|
+
outputStream.write('\r\n--' + this.boundary + '--\r\n');
|
|
740
|
+
return callback();
|
|
741
|
+
}
|
|
742
|
+
const child = this.childNodes[childId++];
|
|
743
|
+
outputStream.write((childId > 1 ? '\r\n' : '') + '--' + this.boundary + '\r\n');
|
|
744
|
+
child.stream(outputStream, options, err => {
|
|
745
|
+
if (err) {
|
|
746
|
+
return callback(err);
|
|
747
|
+
}
|
|
748
|
+
setImmediate(processChildNode);
|
|
749
|
+
});
|
|
750
|
+
};
|
|
751
|
+
if (this.multipart) {
|
|
752
|
+
setImmediate(processChildNode);
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
return callback();
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
// pushes node content
|
|
759
|
+
const sendContent = () => {
|
|
760
|
+
if (this.content) {
|
|
761
|
+
if (Object.prototype.toString.call(this.content) === '[object Error]') {
|
|
762
|
+
// content is already errored
|
|
763
|
+
return callback(this.content);
|
|
764
|
+
}
|
|
765
|
+
if (typeof this.content.pipe === 'function') {
|
|
766
|
+
this.content.removeListener('error', this._contentErrorHandler);
|
|
767
|
+
this._contentErrorHandler = err => callback(err);
|
|
768
|
+
this.content.once('error', this._contentErrorHandler);
|
|
769
|
+
}
|
|
770
|
+
const createStream = () => {
|
|
771
|
+
if (['quoted-printable', 'base64'].includes(transferEncoding)) {
|
|
772
|
+
contentStream = new (transferEncoding === 'base64' ? base64 : qp).Encoder(options);
|
|
773
|
+
contentStream.pipe(outputStream, {
|
|
774
|
+
end: false
|
|
775
|
+
});
|
|
776
|
+
contentStream.once('end', finalize);
|
|
777
|
+
contentStream.once('error', err => callback(err));
|
|
778
|
+
localStream = this._getStream(this.content);
|
|
779
|
+
localStream.pipe(contentStream);
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
// anything that is not QP or Base54 passes as-is
|
|
783
|
+
localStream = this._getStream(this.content);
|
|
784
|
+
localStream.pipe(outputStream, {
|
|
785
|
+
end: false
|
|
786
|
+
});
|
|
787
|
+
localStream.once('end', finalize);
|
|
788
|
+
}
|
|
789
|
+
localStream.once('error', err => callback(err));
|
|
790
|
+
};
|
|
791
|
+
if (this.content._resolve) {
|
|
792
|
+
const chunks = [];
|
|
793
|
+
let chunklen = 0;
|
|
794
|
+
let returned = false;
|
|
795
|
+
const sourceStream = this._getStream(this.content);
|
|
796
|
+
sourceStream.on('error', err => {
|
|
797
|
+
if (returned) {
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
returned = true;
|
|
801
|
+
callback(err);
|
|
802
|
+
});
|
|
803
|
+
sourceStream.on('readable', () => {
|
|
804
|
+
let chunk;
|
|
805
|
+
while ((chunk = sourceStream.read()) !== null) {
|
|
806
|
+
chunks.push(chunk);
|
|
807
|
+
chunklen += chunk.length;
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
sourceStream.on('end', () => {
|
|
811
|
+
if (returned) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
returned = true;
|
|
815
|
+
this.content._resolve = false;
|
|
816
|
+
this.content._resolvedValue = Buffer.concat(chunks, chunklen);
|
|
817
|
+
setImmediate(createStream);
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
setImmediate(createStream);
|
|
822
|
+
}
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
return setImmediate(finalize);
|
|
826
|
+
};
|
|
827
|
+
if (this._raw) {
|
|
828
|
+
setImmediate(() => {
|
|
829
|
+
if (Object.prototype.toString.call(this._raw) === '[object Error]') {
|
|
830
|
+
// content is already errored
|
|
831
|
+
return callback(this._raw);
|
|
832
|
+
}
|
|
833
|
+
// remove default error handler (if set)
|
|
834
|
+
if (typeof this._raw.pipe === 'function') {
|
|
835
|
+
this._raw.removeListener('error', this._contentErrorHandler);
|
|
836
|
+
}
|
|
837
|
+
const raw = this._getStream(this._raw);
|
|
838
|
+
raw.pipe(outputStream, {
|
|
839
|
+
end: false
|
|
840
|
+
});
|
|
841
|
+
raw.on('error', err => outputStream.emit('error', err));
|
|
842
|
+
raw.on('end', finalize);
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
outputStream.write(this.buildHeaders() + '\r\n\r\n');
|
|
847
|
+
setImmediate(sendContent);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Sets envelope to be used instead of the generated one
|
|
852
|
+
*
|
|
853
|
+
* @return SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
|
|
854
|
+
*/
|
|
855
|
+
setEnvelope(envelope) {
|
|
856
|
+
let list;
|
|
857
|
+
this._envelope = {
|
|
858
|
+
from: false,
|
|
859
|
+
to: []
|
|
860
|
+
};
|
|
861
|
+
if (envelope.from) {
|
|
862
|
+
list = [];
|
|
863
|
+
this._convertAddresses(this._parseEnvelopeAddresses(envelope.from), list);
|
|
864
|
+
list = list.filter(address => address && address.address);
|
|
865
|
+
if (list.length && list[0]) {
|
|
866
|
+
this._envelope.from = list[0].address;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
const seenRecipients = new Set();
|
|
870
|
+
const recipients = [];
|
|
871
|
+
['to', 'cc', 'bcc'].forEach(key => {
|
|
872
|
+
if (envelope[key]) {
|
|
873
|
+
this._convertAddresses(this._parseEnvelopeAddresses(envelope[key]), recipients, seenRecipients);
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
this._envelope.to = recipients.map(to => to.address).filter(address => address);
|
|
877
|
+
const standardFields = ['to', 'cc', 'bcc', 'from'];
|
|
878
|
+
shared.copyOwnKeys(this._envelope, envelope, key => standardFields.includes(key));
|
|
879
|
+
return this;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Generates and returns an object with parsed address fields
|
|
883
|
+
*
|
|
884
|
+
* @return Address object
|
|
885
|
+
*/
|
|
886
|
+
getAddresses() {
|
|
887
|
+
const addresses = {};
|
|
888
|
+
const seenByKey = new Map();
|
|
889
|
+
this._headers.forEach(header => {
|
|
890
|
+
const key = header.key.toLowerCase();
|
|
891
|
+
if (['from', 'sender', 'reply-to', 'to', 'cc', 'bcc'].includes(key)) {
|
|
892
|
+
if (!Array.isArray(addresses[key])) {
|
|
893
|
+
addresses[key] = [];
|
|
894
|
+
seenByKey.set(key, new Set());
|
|
895
|
+
}
|
|
896
|
+
this._convertAddresses(this._parseAddresses(header.value), addresses[key], seenByKey.get(key));
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
return addresses;
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Generates and returns SMTP envelope with the sender address and a list of recipients addresses
|
|
903
|
+
*
|
|
904
|
+
* @return SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
|
|
905
|
+
*/
|
|
906
|
+
getEnvelope() {
|
|
907
|
+
if (this._envelope) {
|
|
908
|
+
return this._envelope;
|
|
909
|
+
}
|
|
910
|
+
const envelope = {
|
|
911
|
+
from: false,
|
|
912
|
+
to: []
|
|
913
|
+
};
|
|
914
|
+
// Built once and carried across the headers. Letting _convertAddresses seed it per
|
|
915
|
+
// call would cost O(headers x recipients), and a message can carry many address
|
|
916
|
+
// headers: `headers: { to: [...] }` emits one To per entry.
|
|
917
|
+
const seenRecipients = new Set();
|
|
918
|
+
const recipients = [];
|
|
919
|
+
this._headers.forEach(header => {
|
|
920
|
+
const list = [];
|
|
921
|
+
if (header.key === 'From' || (!envelope.from && ['Reply-To', 'Sender'].includes(header.key))) {
|
|
922
|
+
this._convertAddresses(this._parseAddresses(header.value), list);
|
|
923
|
+
if (list.length && list[0]) {
|
|
924
|
+
envelope.from = list[0].address;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
else if (['To', 'Cc', 'Bcc'].includes(header.key)) {
|
|
928
|
+
this._convertAddresses(this._parseAddresses(header.value), recipients, seenRecipients);
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
envelope.to = recipients.map(to => to.address);
|
|
932
|
+
return envelope;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Returns Message-Id value. If it does not exist, then creates one
|
|
936
|
+
*
|
|
937
|
+
* @return Message-Id value
|
|
938
|
+
*/
|
|
939
|
+
messageId() {
|
|
940
|
+
let messageId = this.getHeader('Message-ID');
|
|
941
|
+
// You really should define your own Message-Id field!
|
|
942
|
+
if (!messageId) {
|
|
943
|
+
messageId = this._generateMessageId();
|
|
944
|
+
this.setHeader('Message-ID', messageId);
|
|
945
|
+
}
|
|
946
|
+
return messageId;
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Sets pregenerated content that will be used as the output of this node
|
|
950
|
+
*
|
|
951
|
+
* @param raw Raw MIME contents
|
|
952
|
+
*/
|
|
953
|
+
setRaw(raw) {
|
|
954
|
+
this._raw = raw;
|
|
955
|
+
if (this._raw && typeof this._raw.pipe === 'function') {
|
|
956
|
+
// pre-stream handler. might be triggered if a stream is set as content
|
|
957
|
+
// and 'error' fires before anything is done with this stream
|
|
958
|
+
this._contentErrorHandler = err => {
|
|
959
|
+
this._raw.removeListener('error', this._contentErrorHandler);
|
|
960
|
+
this._raw = err;
|
|
961
|
+
};
|
|
962
|
+
this._raw.once('error', this._contentErrorHandler);
|
|
963
|
+
}
|
|
964
|
+
return this;
|
|
965
|
+
}
|
|
966
|
+
/////// PRIVATE METHODS
|
|
967
|
+
/**
|
|
968
|
+
* Checks an access policy flag for this node and every node above it. The flags are set
|
|
969
|
+
* from the options the node was built with, and createChild only ever sees the options
|
|
970
|
+
* the caller passed, so a child of a closed tree starts out open. Reading the answer off
|
|
971
|
+
* the parent chain keeps it right whatever order the tree was assembled in.
|
|
972
|
+
*
|
|
973
|
+
* @param flag Either 'disableFileAccess' or 'disableUrlAccess'
|
|
974
|
+
* @return true if this node or an ancestor closed that access
|
|
975
|
+
* @internal
|
|
976
|
+
*/
|
|
977
|
+
_accessDisabled(flag) {
|
|
978
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
979
|
+
let node = this;
|
|
980
|
+
while (node) {
|
|
981
|
+
if (node[flag]) {
|
|
982
|
+
return true;
|
|
983
|
+
}
|
|
984
|
+
node = node.parentNode;
|
|
985
|
+
}
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Detects and returns handle to a stream related with the content.
|
|
990
|
+
*
|
|
991
|
+
* @param content Node content
|
|
992
|
+
* @returns Stream object
|
|
993
|
+
* @internal
|
|
994
|
+
*/
|
|
995
|
+
_getStream(content) {
|
|
996
|
+
let contentStream;
|
|
997
|
+
if (content._resolvedValue) {
|
|
998
|
+
// pass string or buffer content as a stream
|
|
999
|
+
contentStream = new node_stream_1.PassThrough();
|
|
1000
|
+
setImmediate(() => {
|
|
1001
|
+
try {
|
|
1002
|
+
contentStream.end(content._resolvedValue);
|
|
1003
|
+
}
|
|
1004
|
+
catch (_err) {
|
|
1005
|
+
contentStream.emit('error', _err);
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
return contentStream;
|
|
1009
|
+
}
|
|
1010
|
+
if (typeof content.pipe === 'function') {
|
|
1011
|
+
// assume as stream
|
|
1012
|
+
return content;
|
|
1013
|
+
}
|
|
1014
|
+
if (content && typeof content.path === 'string' && !content.href) {
|
|
1015
|
+
if (this._accessDisabled('disableFileAccess')) {
|
|
1016
|
+
contentStream = new node_stream_1.PassThrough();
|
|
1017
|
+
setImmediate(() => {
|
|
1018
|
+
const err = new Error('File access rejected for ' + content.path);
|
|
1019
|
+
err.code = errors.EFILEACCESS;
|
|
1020
|
+
contentStream.emit('error', err);
|
|
1021
|
+
});
|
|
1022
|
+
return contentStream;
|
|
1023
|
+
}
|
|
1024
|
+
// read file
|
|
1025
|
+
return node_fs_1.default.createReadStream(content.path);
|
|
1026
|
+
}
|
|
1027
|
+
if (content && typeof content.href === 'string') {
|
|
1028
|
+
if (this._accessDisabled('disableUrlAccess')) {
|
|
1029
|
+
contentStream = new node_stream_1.PassThrough();
|
|
1030
|
+
setImmediate(() => {
|
|
1031
|
+
const err = new Error('Url access rejected for ' + content.href);
|
|
1032
|
+
err.code = errors.EURLACCESS;
|
|
1033
|
+
contentStream.emit('error', err);
|
|
1034
|
+
});
|
|
1035
|
+
return contentStream;
|
|
1036
|
+
}
|
|
1037
|
+
// fetch URL. nmfetch refuses any scheme that is not http(s), and it decides
|
|
1038
|
+
// that on the parsed URL. Testing the raw string here instead would reject
|
|
1039
|
+
// forms the parser accepts, such as a leading space or a slash-less authority
|
|
1040
|
+
return (0, index_js_2.default)(content.href, { headers: content.httpHeaders, tls: content.tls });
|
|
1041
|
+
}
|
|
1042
|
+
// pass string or buffer content as a stream
|
|
1043
|
+
contentStream = new node_stream_1.PassThrough();
|
|
1044
|
+
setImmediate(() => {
|
|
1045
|
+
try {
|
|
1046
|
+
contentStream.end(content || '');
|
|
1047
|
+
}
|
|
1048
|
+
catch (_err) {
|
|
1049
|
+
contentStream.emit('error', _err);
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
return contentStream;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Parses addresses. Takes in a single address or an array or an
|
|
1056
|
+
* array of address arrays (eg. To: [[first group], [second group],...])
|
|
1057
|
+
*
|
|
1058
|
+
* @param addresses Addresses to be parsed
|
|
1059
|
+
* @return An array of address objects
|
|
1060
|
+
* @internal
|
|
1061
|
+
*/
|
|
1062
|
+
_parseAddresses(addresses) {
|
|
1063
|
+
const flattened = [];
|
|
1064
|
+
// Nested arrays are flattened iteratively. concat.apply throws once a recipient list
|
|
1065
|
+
// is long enough to pass the argument limit, and an array handed to addressparser is
|
|
1066
|
+
// stringified, which recurses once per nesting level and exhausts the call stack on
|
|
1067
|
+
// a deep enough value. Every array is walked once, which also ends a self-referential
|
|
1068
|
+
// input
|
|
1069
|
+
const seen = new WeakSet();
|
|
1070
|
+
const stack = [];
|
|
1071
|
+
const enter = (list) => {
|
|
1072
|
+
if (!seen.has(list)) {
|
|
1073
|
+
seen.add(list);
|
|
1074
|
+
stack.push({ list, pos: 0 });
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
enter(Array.isArray(addresses) ? addresses : [addresses]);
|
|
1078
|
+
while (stack.length) {
|
|
1079
|
+
const frame = stack[stack.length - 1];
|
|
1080
|
+
if (frame.pos >= frame.list.length) {
|
|
1081
|
+
stack.pop();
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
const address = frame.list[frame.pos++];
|
|
1085
|
+
if (Array.isArray(address)) {
|
|
1086
|
+
enter(address);
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
if (address && address.address) {
|
|
1090
|
+
const normalized = this._normalizeAddress(address.address);
|
|
1091
|
+
if (normalized === address.address && typeof address.name === 'string') {
|
|
1092
|
+
// there is nothing to rewrite, so there is nothing to keep off the original
|
|
1093
|
+
flattened.push(address);
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
// rewriting would land on the object the caller passed in and might
|
|
1097
|
+
// still hold a reference to, so rewrite a copy of it instead. An own
|
|
1098
|
+
// "__proto__" key would make the copy inherit from caller data, and
|
|
1099
|
+
// _convertAddresses reads `group` off it straight into the envelope
|
|
1100
|
+
const copy = shared.copyOwnKeys({}, address);
|
|
1101
|
+
copy.address = normalized;
|
|
1102
|
+
copy.name = address.name || '';
|
|
1103
|
+
flattened.push(copy);
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
const parsed = this._normalizeParsedAddresses((0, index_js_1.default)(address));
|
|
1107
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
1108
|
+
flattened.push(parsed[i]);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return flattened;
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Normalizes the addresses of a freshly parsed address list, groups included.
|
|
1115
|
+
*
|
|
1116
|
+
* Everything this method returns carries a normalized address, whether it arrived as an
|
|
1117
|
+
* object or was parsed out of a header value. Without this the two shapes disagree, and
|
|
1118
|
+
* a consumer reading the parsed form back is handed the ambiguous
|
|
1119
|
+
* 'user@evil.com@good.com' that the header and the envelope no longer carry.
|
|
1120
|
+
*
|
|
1121
|
+
* @param parsed An array of address objects, as returned by addressparser
|
|
1122
|
+
* @return The same array, with every address normalized
|
|
1123
|
+
* @internal
|
|
1124
|
+
*/
|
|
1125
|
+
_normalizeParsedAddresses(parsed) {
|
|
1126
|
+
// addressparser builds these objects, so no caller holds a reference to rewrite around
|
|
1127
|
+
parsed.forEach(entry => {
|
|
1128
|
+
if (entry.address) {
|
|
1129
|
+
entry.address = this._normalizeAddress(entry.address);
|
|
1130
|
+
}
|
|
1131
|
+
else if (entry.group) {
|
|
1132
|
+
this._normalizeParsedAddresses(entry.group);
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
return parsed;
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Parses the addresses of an explicitly set envelope.
|
|
1139
|
+
*
|
|
1140
|
+
* An envelope value is an addr-spec and never a display name, so a bare local username
|
|
1141
|
+
* such as 'root' is the address here. Header parsing has to read the same value as a
|
|
1142
|
+
* display name, as a value with no '@' in it can not be an addr-spec in a header.
|
|
1143
|
+
*
|
|
1144
|
+
* @param addresses Addresses to be parsed
|
|
1145
|
+
* @return An array of address objects
|
|
1146
|
+
* @internal
|
|
1147
|
+
*/
|
|
1148
|
+
_parseEnvelopeAddresses(addresses) {
|
|
1149
|
+
return this._parseAddresses(addresses).map(entry => {
|
|
1150
|
+
if (entry.address || entry.group || !entry.name || /[\s@]/.test(entry.name)) {
|
|
1151
|
+
return entry;
|
|
1152
|
+
}
|
|
1153
|
+
return { address: this._normalizeAddress(entry.name), name: '' };
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Normalizes a header key, uses Camel-Case form, except for uppercase MIME-
|
|
1158
|
+
*
|
|
1159
|
+
* @param key Key to be normalized
|
|
1160
|
+
* @return key in Camel-Case form
|
|
1161
|
+
* @internal
|
|
1162
|
+
*/
|
|
1163
|
+
_normalizeHeaderKey(key) {
|
|
1164
|
+
key = (key || '')
|
|
1165
|
+
.toString()
|
|
1166
|
+
// no newlines in keys
|
|
1167
|
+
.replace(/\r?\n|\r/g, ' ')
|
|
1168
|
+
// a field name is printable ascii without the colon, so a control char or DEL
|
|
1169
|
+
// can only be dropped, there is no quoting construct around a field name
|
|
1170
|
+
.replace(/[\x00-\x1f\x7f]/g, '')
|
|
1171
|
+
.trim()
|
|
1172
|
+
.toLowerCase()
|
|
1173
|
+
// use uppercase words, except MIME
|
|
1174
|
+
.replace(/^X-SMTPAPI$|^(MIME|DKIM|ARC|BIMI)\b|^[a-z]|-(SPF|FBL|ID|MD5)$|-[a-z]/gi, c => c.toUpperCase())
|
|
1175
|
+
// special case
|
|
1176
|
+
.replace(/^Content-Features$/i, 'Content-features');
|
|
1177
|
+
return key;
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Checks if the content type is multipart and defines boundary if needed.
|
|
1181
|
+
* Doesn't return anything, modifies object argument instead.
|
|
1182
|
+
*
|
|
1183
|
+
* @param structured Parsed header value for 'Content-Type' key
|
|
1184
|
+
* @internal
|
|
1185
|
+
*/
|
|
1186
|
+
_handleContentType(structured) {
|
|
1187
|
+
this.contentType = structured.value.trim().toLowerCase();
|
|
1188
|
+
this.multipart = /^multipart\//i.test(this.contentType) ? this.contentType.substr(this.contentType.indexOf('/') + 1) : false;
|
|
1189
|
+
if (this.multipart) {
|
|
1190
|
+
// The declared value and the delimiters are assigned from the same expression
|
|
1191
|
+
// here, so whatever the header emitter then does with it, the two sides cannot
|
|
1192
|
+
// disagree about which characters the boundary is made of.
|
|
1193
|
+
//
|
|
1194
|
+
// Stripping runs before the fallback rather than over the whole chain: a
|
|
1195
|
+
// boundary made only of control characters would otherwise strip to '' and
|
|
1196
|
+
// leave the node declaring no boundary and streaming bare '--' delimiters.
|
|
1197
|
+
// _generateBoundary cleans what it builds, but this is the one place the boundary
|
|
1198
|
+
// is written, so it cleans the value it is about to write rather than trusting
|
|
1199
|
+
// where it came from. MimeNode is exported and subclassable, so the generator is
|
|
1200
|
+
// not necessarily the one below. Stripping twice costs nothing, it is idempotent.
|
|
1201
|
+
const declared = _stripBoundaryControls(structured.params.boundary || this.boundary || '');
|
|
1202
|
+
this.boundary = structured.params.boundary =
|
|
1203
|
+
declared || _stripBoundaryControls(this._generateBoundary());
|
|
1204
|
+
}
|
|
1205
|
+
else {
|
|
1206
|
+
this.boundary = false;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Generates a multipart boundary value
|
|
1211
|
+
*
|
|
1212
|
+
* @return boundary value
|
|
1213
|
+
* @internal
|
|
1214
|
+
*/
|
|
1215
|
+
_generateBoundary() {
|
|
1216
|
+
// baseBoundary and boundaryPrefix are public, so they may hold something the
|
|
1217
|
+
// constructor never cleaned, and the -Part_ suffix keeps the result non empty
|
|
1218
|
+
return _stripBoundaryControls(this.rootNode.boundaryPrefix + '-' + this.rootNode.baseBoundary) + '-Part_' + this._nodeId;
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Encodes a header value for use in the generated rfc2822 email.
|
|
1222
|
+
*
|
|
1223
|
+
* @param key Header key
|
|
1224
|
+
* @param value Header value
|
|
1225
|
+
* @internal
|
|
1226
|
+
*/
|
|
1227
|
+
_encodeHeaderValue(key, value) {
|
|
1228
|
+
key = this._normalizeHeaderKey(key);
|
|
1229
|
+
switch (key) {
|
|
1230
|
+
// Structured headers
|
|
1231
|
+
case 'From':
|
|
1232
|
+
case 'Sender':
|
|
1233
|
+
case 'To':
|
|
1234
|
+
case 'Cc':
|
|
1235
|
+
case 'Bcc':
|
|
1236
|
+
case 'Reply-To':
|
|
1237
|
+
return this._convertAddresses(this._parseAddresses(value));
|
|
1238
|
+
// values enclosed in <>
|
|
1239
|
+
case 'Message-ID':
|
|
1240
|
+
case 'In-Reply-To':
|
|
1241
|
+
case 'Content-Id':
|
|
1242
|
+
// a msg-id is structured, so an encoded word inside the angle brackets would
|
|
1243
|
+
// be read as literal text. drop the characters that can not appear in a header
|
|
1244
|
+
// at all, but leave HT alone, it separates the ids of a multi id value
|
|
1245
|
+
value = (value || '')
|
|
1246
|
+
.toString()
|
|
1247
|
+
.replace(/\r?\n|\r/g, ' ')
|
|
1248
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');
|
|
1249
|
+
if (value.charAt(0) !== '<') {
|
|
1250
|
+
value = '<' + value;
|
|
1251
|
+
}
|
|
1252
|
+
if (value.charAt(value.length - 1) !== '>') {
|
|
1253
|
+
value = value + '>';
|
|
1254
|
+
}
|
|
1255
|
+
return value;
|
|
1256
|
+
// space separated list of values enclosed in <>
|
|
1257
|
+
case 'References':
|
|
1258
|
+
value = [].concat
|
|
1259
|
+
.apply([], [].concat(value || '').map(elm => {
|
|
1260
|
+
elm = (elm || '')
|
|
1261
|
+
.toString()
|
|
1262
|
+
.replace(/\r?\n|\r/g, ' ')
|
|
1263
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
|
|
1264
|
+
.trim();
|
|
1265
|
+
return elm.replace(/<[^>]*>/g, str => str.replace(/\s/g, '')).split(/\s+/);
|
|
1266
|
+
}))
|
|
1267
|
+
.map(elm => {
|
|
1268
|
+
if (elm.charAt(0) !== '<') {
|
|
1269
|
+
elm = '<' + elm;
|
|
1270
|
+
}
|
|
1271
|
+
if (elm.charAt(elm.length - 1) !== '>') {
|
|
1272
|
+
elm = elm + '>';
|
|
1273
|
+
}
|
|
1274
|
+
return elm;
|
|
1275
|
+
});
|
|
1276
|
+
return value.join(' ').trim();
|
|
1277
|
+
case 'Date':
|
|
1278
|
+
if (Object.prototype.toString.call(value) === '[object Date]') {
|
|
1279
|
+
return value.toUTCString().replace(/GMT/, '+0000');
|
|
1280
|
+
}
|
|
1281
|
+
value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
|
|
1282
|
+
return this._encodeHeaderText(value);
|
|
1283
|
+
case 'Content-Type':
|
|
1284
|
+
case 'Content-Disposition':
|
|
1285
|
+
// if it includes a filename then it is already encoded
|
|
1286
|
+
return (value || '').toString().replace(/\r?\n|\r/g, ' ');
|
|
1287
|
+
default:
|
|
1288
|
+
value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
|
|
1289
|
+
return this._encodeHeaderText(value);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Rebuilds address object using punycode and other adjustments
|
|
1294
|
+
*
|
|
1295
|
+
* @param addresses An array of address objects
|
|
1296
|
+
* @param [uniqueList] An array to be populated with addresses
|
|
1297
|
+
* @param [seenAddresses] Addresses already added to uniqueList, shared with recursive calls to keep deduplication linear
|
|
1298
|
+
* @return address string
|
|
1299
|
+
* @internal
|
|
1300
|
+
*/
|
|
1301
|
+
_convertAddresses(addresses, uniqueList, seenAddresses) {
|
|
1302
|
+
const values = [];
|
|
1303
|
+
uniqueList = uniqueList || [];
|
|
1304
|
+
// Membership is checked once per address, so scanning uniqueList itself would make
|
|
1305
|
+
// a recipient list cost O(n^2). Groups recurse with the same set so that a nested
|
|
1306
|
+
// group still dedupes against the addresses collected around it, and a caller that
|
|
1307
|
+
// passes a partly filled list (To, then Cc, then Bcc) keeps deduping across headers.
|
|
1308
|
+
if (!seenAddresses) {
|
|
1309
|
+
seenAddresses = new Set();
|
|
1310
|
+
for (let i = 0; i < uniqueList.length; i++) {
|
|
1311
|
+
seenAddresses.add(uniqueList[i].address);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
[].concat(addresses || []).forEach(address => {
|
|
1315
|
+
if (address.address) {
|
|
1316
|
+
address.address = this._normalizeAddress(address.address);
|
|
1317
|
+
if (!address.name) {
|
|
1318
|
+
// an address that carries a special, be it a quoted local part or a domain
|
|
1319
|
+
// that could not be normalized, is only unambiguous inside angle brackets.
|
|
1320
|
+
// Without them a ',' or a ';' anywhere in it reads as a recipient separator
|
|
1321
|
+
// and the header would list more recipients than the envelope carries
|
|
1322
|
+
values.push(PLAIN_ADDRESS.test(address.address) ? address.address : `<${address.address}>`);
|
|
1323
|
+
}
|
|
1324
|
+
else {
|
|
1325
|
+
values.push(`${this._encodeAddressName(address.name)} <${address.address}>`);
|
|
1326
|
+
}
|
|
1327
|
+
if (!seenAddresses.has(address.address)) {
|
|
1328
|
+
seenAddresses.add(address.address);
|
|
1329
|
+
uniqueList.push(address);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
else if (address.group) {
|
|
1333
|
+
const groupListAddresses = (address.group.length ? this._convertAddresses(address.group, uniqueList, seenAddresses) : '').trim();
|
|
1334
|
+
values.push(`${this._encodeAddressName(address.name)}:${groupListAddresses};`);
|
|
1335
|
+
}
|
|
1336
|
+
});
|
|
1337
|
+
return values.join(', ');
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Normalizes an email address
|
|
1341
|
+
*
|
|
1342
|
+
* @param address An array of address objects
|
|
1343
|
+
* @return address string
|
|
1344
|
+
* @internal
|
|
1345
|
+
*/
|
|
1346
|
+
_normalizeAddress(address) {
|
|
1347
|
+
address = (address || '')
|
|
1348
|
+
.toString()
|
|
1349
|
+
.replace(/[\x00-\x1F\x7F<>]+/g, ' ') // remove unallowed characters
|
|
1350
|
+
.trim();
|
|
1351
|
+
if (!address) {
|
|
1352
|
+
// callers use an empty value to detect a missing address
|
|
1353
|
+
return address;
|
|
1354
|
+
}
|
|
1355
|
+
const lastAt = address.lastIndexOf('@');
|
|
1356
|
+
if (lastAt < 0) {
|
|
1357
|
+
// Bare username, there is no domain to split off
|
|
1358
|
+
return this._normalizeLocalPart(address);
|
|
1359
|
+
}
|
|
1360
|
+
const user = address.substr(0, lastAt);
|
|
1361
|
+
const domain = address.substr(lastAt + 1);
|
|
1362
|
+
// Unicode in the local part is kept as is, see _normalizeLocalPart for the rest of it.
|
|
1363
|
+
// A domain has no quoting construct to fall back on, so whatever is not a valid domain
|
|
1364
|
+
// is kept as supplied and it is _convertAddresses that keeps such an address unambiguous.
|
|
1365
|
+
// Domains are punycoded when the local part is ASCII ('safe@jõgeva.ee' -> 'safe@xn--jgeva-dua.ee').
|
|
1366
|
+
// When the local part contains non-ASCII bytes the address already requires SMTPUTF8,
|
|
1367
|
+
// so the domain is kept (or decoded back) as UTF-8 for symmetry on both sides of '@'.
|
|
1368
|
+
let encodedDomain = domain;
|
|
1369
|
+
// A non-ASCII local part already requires SMTPUTF8, so the domain stays UTF-8 for
|
|
1370
|
+
// symmetry on both sides of the '@' rather than being encoded to an A-label
|
|
1371
|
+
const smtputf8 = /[\x80-\uFFFF]/.test(user);
|
|
1372
|
+
try {
|
|
1373
|
+
encodedDomain = normalizeDomain(domain.toLowerCase(), smtputf8);
|
|
1374
|
+
}
|
|
1375
|
+
catch (_err) {
|
|
1376
|
+
// keep domain as supplied
|
|
1377
|
+
}
|
|
1378
|
+
return `${this._normalizeLocalPart(user)}@${encodedDomain}`;
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Normalizes the local part of an address into a form that can be emitted as is.
|
|
1382
|
+
*
|
|
1383
|
+
* A local part is either a dot-atom or a quoted-string, anything else is not a valid
|
|
1384
|
+
* addr-spec. The quotes of a quoted local part get lost along the way, and a bare
|
|
1385
|
+
* 'user@evil.com@good.com' leaves it to the receiver which '@' splits the domain off,
|
|
1386
|
+
* while the split here is always at the last one. So whatever is not already one of
|
|
1387
|
+
* the two valid forms goes back out as a quoted-string.
|
|
1388
|
+
*
|
|
1389
|
+
* @param user Local part of an address
|
|
1390
|
+
* @return Local part as a dot-atom or as a quoted-string
|
|
1391
|
+
* @internal
|
|
1392
|
+
*/
|
|
1393
|
+
_normalizeLocalPart(user) {
|
|
1394
|
+
if (DOT_ATOM.test(user) || QUOTED_STRING.test(user)) {
|
|
1395
|
+
return user;
|
|
1396
|
+
}
|
|
1397
|
+
return mimeFuncs.quoteString(user);
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* If needed, mime encodes the name part
|
|
1401
|
+
*
|
|
1402
|
+
* @param name Name part of an address
|
|
1403
|
+
* @returns Mime word encoded string if needed
|
|
1404
|
+
* @internal
|
|
1405
|
+
*/
|
|
1406
|
+
_encodeAddressName(name) {
|
|
1407
|
+
if (!/^[\w ]*$/.test(name)) {
|
|
1408
|
+
if (/^[\x20-\x7e]*$/.test(name)) {
|
|
1409
|
+
return mimeFuncs.quoteString(name);
|
|
1410
|
+
}
|
|
1411
|
+
else {
|
|
1412
|
+
return mimeFuncs.encodeWord(name, this._getTextEncoding(name), 52);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
return name;
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Encodes an unstructured header value. Such a value can only carry VCHAR and WSP, so a
|
|
1419
|
+
* control char or DEL has to be forced into the mime encoded word that a non-ascii value
|
|
1420
|
+
* would get anyway. HT stays as it is, it is valid folding whitespace here.
|
|
1421
|
+
*
|
|
1422
|
+
* @param value Header value to encode
|
|
1423
|
+
* @returns Mime word encoded string if needed
|
|
1424
|
+
* @internal
|
|
1425
|
+
*/
|
|
1426
|
+
_encodeHeaderText(value) {
|
|
1427
|
+
return /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(value)
|
|
1428
|
+
? mimeFuncs.encodeWord(value, this._getTextEncoding(value), 52)
|
|
1429
|
+
: // encodeWords only encodes if needed, otherwise the original string is returned
|
|
1430
|
+
this._encodeWords(value);
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* If needed, mime encodes the name part
|
|
1434
|
+
*
|
|
1435
|
+
* @param value Name part of an address
|
|
1436
|
+
* @returns Mime word encoded string if needed
|
|
1437
|
+
* @internal
|
|
1438
|
+
*/
|
|
1439
|
+
_encodeWords(value) {
|
|
1440
|
+
// set encodeAll parameter to true even though it is against the recommendation of RFC2047,
|
|
1441
|
+
// by default only words that include non-ascii should be converted into encoded words
|
|
1442
|
+
// but some clients (eg. Zimbra) do not handle it properly and remove surrounding whitespace
|
|
1443
|
+
return mimeFuncs.encodeWords(value, this._getTextEncoding(value), 52, true);
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Detects best mime encoding for a text value
|
|
1447
|
+
*
|
|
1448
|
+
* @param value Value to check for
|
|
1449
|
+
* @return either 'Q' or 'B'
|
|
1450
|
+
* @internal
|
|
1451
|
+
*/
|
|
1452
|
+
_getTextEncoding(value) {
|
|
1453
|
+
value = (value || '').toString();
|
|
1454
|
+
if (this.textEncoding) {
|
|
1455
|
+
return this.textEncoding;
|
|
1456
|
+
}
|
|
1457
|
+
// count latin alphabet symbols and 8-bit range symbols + control symbols
|
|
1458
|
+
// if there are more latin characters, then use quoted-printable
|
|
1459
|
+
// encoding, otherwise use base64
|
|
1460
|
+
let nonLatinLen = 0;
|
|
1461
|
+
let latinLen = 0;
|
|
1462
|
+
for (let i = 0, len = value.length; i < len; i++) {
|
|
1463
|
+
const code = value.charCodeAt(i);
|
|
1464
|
+
if ((code >= 0x00 && code <= 0x08) || code === 0x0b || code === 0x0c || (code >= 0x0e && code <= 0x1f) || code >= 0x80) {
|
|
1465
|
+
nonLatinLen++;
|
|
1466
|
+
}
|
|
1467
|
+
else if ((code >= 0x41 && code <= 0x5a) || (code >= 0x61 && code <= 0x7a)) {
|
|
1468
|
+
latinLen++;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
// if there are more latin symbols than binary/unicode, then prefer Q, otherwise B
|
|
1472
|
+
return nonLatinLen < latinLen ? 'Q' : 'B';
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* Generates a message id
|
|
1476
|
+
*
|
|
1477
|
+
* @return Random Message-ID value
|
|
1478
|
+
* @internal
|
|
1479
|
+
*/
|
|
1480
|
+
_generateMessageId() {
|
|
1481
|
+
return ('<' +
|
|
1482
|
+
[2, 2, 2, 6].reduce(
|
|
1483
|
+
// crux to generate UUID-like random strings
|
|
1484
|
+
(prev, len) => prev + '-' + node_crypto_1.default.randomBytes(len).toString('hex'), node_crypto_1.default.randomBytes(4).toString('hex')) +
|
|
1485
|
+
'@' +
|
|
1486
|
+
// try to use the domain of the FROM address or fallback to server hostname
|
|
1487
|
+
(this.getEnvelope().from || this.hostname || 'localhost').split('@').pop() +
|
|
1488
|
+
'>');
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
exports.default = MimeNode;
|
|
1492
|
+
module.exports = exports.default;
|
|
1493
|
+
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
|