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