@nocobase/plugin-notifications 2.4.0-alpha.1 → 2.4.0-alpha.10
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 +3 -3
- 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/dist/cjs/punycode/index.js +411 -0
- 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 +288 -280
- package/dist/node_modules/nodemailer/dist/esm/mailer/index.d.ts +237 -0
- package/dist/node_modules/nodemailer/{lib → dist/esm}/mailer/index.js +142 -194
- 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 +584 -436
- 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/dist/server/models/NotificationService.d.ts +1 -2
- package/package.json +3 -3
- 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 -638
- 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 -1812
- 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 -338
- 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,82 +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
|
}
|
|
514
|
+
// Ensure that Content-Type is the last header for the root node
|
|
515
|
+
for (let i = this._headers.length - 2; i >= 0; i--) {
|
|
516
|
+
const header = this._headers[i];
|
|
517
|
+
if (header.key === 'Content-Type') {
|
|
518
|
+
this._headers.splice(i, 1);
|
|
519
|
+
this._headers.push(header);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
500
522
|
}
|
|
501
|
-
|
|
502
523
|
this._headers.forEach(header => {
|
|
503
524
|
let key = header.key;
|
|
504
525
|
let value = header.value;
|
|
505
526
|
let structured;
|
|
506
527
|
let param;
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
528
|
+
const options = {};
|
|
529
|
+
const formattedHeaders = FORMATTED_HEADERS;
|
|
510
530
|
if (value && typeof value === 'object' && !formattedHeaders.includes(key)) {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
});
|
|
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');
|
|
516
535
|
value = (value.value || '').toString();
|
|
517
536
|
if (!value.trim()) {
|
|
518
537
|
return;
|
|
519
538
|
}
|
|
520
539
|
}
|
|
521
|
-
|
|
522
540
|
if (options.prepared) {
|
|
523
541
|
// header value is
|
|
524
542
|
if (options.foldLines) {
|
|
525
543
|
headers.push(mimeFuncs.foldLines(key + ': ' + value));
|
|
526
|
-
}
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
527
546
|
headers.push(key + ': ' + value);
|
|
528
547
|
}
|
|
529
548
|
return;
|
|
530
549
|
}
|
|
531
|
-
|
|
532
550
|
switch (header.key) {
|
|
533
551
|
case 'Content-Disposition':
|
|
534
552
|
structured = mimeFuncs.parseHeaderValue(value);
|
|
@@ -537,32 +555,38 @@ class MimeNode {
|
|
|
537
555
|
}
|
|
538
556
|
value = mimeFuncs.buildHeaderValue(structured);
|
|
539
557
|
break;
|
|
540
|
-
|
|
541
558
|
case 'Content-Type':
|
|
542
559
|
structured = mimeFuncs.parseHeaderValue(value);
|
|
543
|
-
|
|
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, '');
|
|
544
564
|
this._handleContentType(structured);
|
|
545
|
-
|
|
546
|
-
|
|
565
|
+
if (structured.value.match(/^text\/plain\b/) &&
|
|
566
|
+
typeof this.content === 'string' &&
|
|
567
|
+
/[\u0080-\uFFFF]/.test(this.content)) {
|
|
547
568
|
structured.params.charset = 'utf-8';
|
|
548
569
|
}
|
|
549
|
-
|
|
550
570
|
value = mimeFuncs.buildHeaderValue(structured);
|
|
551
|
-
|
|
552
571
|
if (this.filename) {
|
|
553
572
|
// add support for non-compliant clients like QQ webmail
|
|
554
573
|
// we can't build the value with buildHeaderValue as the value is non standard and
|
|
555
574
|
// would be converted to parameter continuation encoding that we do not want
|
|
556
|
-
|
|
557
|
-
|
|
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);
|
|
558
581
|
if (param !== this.filename || /[\s'"\\;:/=(),<>@[\]?]|^-/.test(param)) {
|
|
559
|
-
// include value in quotes if needed
|
|
560
|
-
|
|
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);
|
|
561
586
|
}
|
|
562
587
|
value += '; name=' + param;
|
|
563
588
|
}
|
|
564
589
|
break;
|
|
565
|
-
|
|
566
590
|
case 'Bcc':
|
|
567
591
|
if (!this.keepBcc) {
|
|
568
592
|
// skip BCC values
|
|
@@ -570,40 +594,36 @@ class MimeNode {
|
|
|
570
594
|
}
|
|
571
595
|
break;
|
|
572
596
|
}
|
|
573
|
-
|
|
574
597
|
value = this._encodeHeaderValue(key, value);
|
|
575
|
-
|
|
576
598
|
// skip empty lines
|
|
577
599
|
if (!(value || '').toString().trim()) {
|
|
578
600
|
return;
|
|
579
601
|
}
|
|
580
|
-
|
|
581
602
|
if (typeof this.normalizeHeaderKey === 'function') {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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;
|
|
585
610
|
}
|
|
586
611
|
}
|
|
587
|
-
|
|
588
612
|
headers.push(mimeFuncs.foldLines(key + ': ' + value, 76));
|
|
589
613
|
});
|
|
590
|
-
|
|
591
614
|
return headers.join('\r\n');
|
|
592
615
|
}
|
|
593
|
-
|
|
594
616
|
/**
|
|
595
617
|
* Streams the rfc2822 message from the current node. If this is a root node,
|
|
596
618
|
* mandatory header fields are set if missing (Date, Message-Id, MIME-Version)
|
|
597
619
|
*
|
|
598
|
-
* @return
|
|
620
|
+
* @return Compiled message
|
|
599
621
|
*/
|
|
600
622
|
createReadStream(options) {
|
|
601
623
|
options = options || {};
|
|
602
|
-
|
|
603
|
-
let stream = new PassThrough(options);
|
|
624
|
+
const stream = new PassThrough(options);
|
|
604
625
|
let outputStream = stream;
|
|
605
626
|
let transform;
|
|
606
|
-
|
|
607
627
|
this.stream(stream, options, err => {
|
|
608
628
|
if (err) {
|
|
609
629
|
outputStream.emit('error', err);
|
|
@@ -611,87 +631,77 @@ class MimeNode {
|
|
|
611
631
|
}
|
|
612
632
|
stream.end();
|
|
613
633
|
});
|
|
614
|
-
|
|
615
634
|
for (let i = 0, len = this._transforms.length; i < len; i++) {
|
|
616
|
-
transform =
|
|
635
|
+
transform =
|
|
636
|
+
typeof this._transforms[i] === 'function' ? this._transforms[i]() : this._transforms[i];
|
|
617
637
|
outputStream.once('error', err => {
|
|
618
638
|
transform.emit('error', err);
|
|
619
639
|
});
|
|
620
640
|
outputStream = outputStream.pipe(transform);
|
|
621
641
|
}
|
|
622
|
-
|
|
623
642
|
// ensure terminating newline after possible user transforms
|
|
624
643
|
transform = new LastNewline();
|
|
625
644
|
outputStream.once('error', err => {
|
|
626
645
|
transform.emit('error', err);
|
|
627
646
|
});
|
|
628
647
|
outputStream = outputStream.pipe(transform);
|
|
629
|
-
|
|
630
648
|
// dkim and stuff
|
|
631
649
|
for (let i = 0, len = this._processFuncs.length; i < len; i++) {
|
|
632
650
|
transform = this._processFuncs[i];
|
|
633
651
|
outputStream = transform(outputStream);
|
|
634
652
|
}
|
|
635
|
-
|
|
636
653
|
if (this.newline) {
|
|
637
654
|
const winbreak = ['win', 'windows', 'dos', '\r\n'].includes(this.newline.toString().toLowerCase());
|
|
638
655
|
const newlineTransform = winbreak ? new LeWindows() : new LeUnix();
|
|
639
|
-
|
|
640
656
|
const stream = outputStream.pipe(newlineTransform);
|
|
641
657
|
outputStream.on('error', err => stream.emit('error', err));
|
|
642
658
|
return stream;
|
|
643
659
|
}
|
|
644
|
-
|
|
645
660
|
return outputStream;
|
|
646
661
|
}
|
|
647
|
-
|
|
648
662
|
/**
|
|
649
663
|
* Appends a transform stream object to the transforms list. Final output
|
|
650
664
|
* is passed through this stream before exposing
|
|
651
665
|
*
|
|
652
|
-
* @param
|
|
666
|
+
* @param transform Read-Write stream
|
|
653
667
|
*/
|
|
654
668
|
transform(transform) {
|
|
655
669
|
this._transforms.push(transform);
|
|
656
670
|
}
|
|
657
|
-
|
|
658
671
|
/**
|
|
659
672
|
* Appends a post process function. The functon is run after transforms and
|
|
660
673
|
* uses the following syntax
|
|
661
674
|
*
|
|
662
675
|
* processFunc(input) -> outputStream
|
|
663
676
|
*
|
|
664
|
-
* @param
|
|
677
|
+
* @param processFunc Read-Write stream
|
|
665
678
|
*/
|
|
666
679
|
processFunc(processFunc) {
|
|
667
680
|
this._processFuncs.push(processFunc);
|
|
668
681
|
}
|
|
669
|
-
|
|
670
682
|
stream(outputStream, options, done) {
|
|
671
|
-
|
|
683
|
+
const transferEncoding = this.getTransferEncoding();
|
|
672
684
|
let contentStream;
|
|
673
685
|
let localStream;
|
|
674
|
-
|
|
675
686
|
// protect actual callback against multiple triggering
|
|
676
687
|
let returned = false;
|
|
677
|
-
|
|
688
|
+
const callback = (err) => {
|
|
678
689
|
if (returned) {
|
|
679
690
|
return;
|
|
680
691
|
}
|
|
681
692
|
returned = true;
|
|
682
693
|
done(err);
|
|
683
694
|
};
|
|
684
|
-
|
|
685
695
|
// for multipart nodes, push child nodes
|
|
686
696
|
// for content nodes end the stream
|
|
687
|
-
|
|
697
|
+
const finalize = () => {
|
|
688
698
|
let childId = 0;
|
|
689
|
-
|
|
699
|
+
const processChildNode = () => {
|
|
690
700
|
if (childId >= this.childNodes.length) {
|
|
691
701
|
outputStream.write('\r\n--' + this.boundary + '--\r\n');
|
|
692
702
|
return callback();
|
|
693
703
|
}
|
|
694
|
-
|
|
704
|
+
const child = this.childNodes[childId++];
|
|
695
705
|
outputStream.write((childId > 1 ? '\r\n' : '') + '--' + this.boundary + '\r\n');
|
|
696
706
|
child.stream(outputStream, options, err => {
|
|
697
707
|
if (err) {
|
|
@@ -700,41 +710,37 @@ class MimeNode {
|
|
|
700
710
|
setImmediate(processChildNode);
|
|
701
711
|
});
|
|
702
712
|
};
|
|
703
|
-
|
|
704
713
|
if (this.multipart) {
|
|
705
714
|
setImmediate(processChildNode);
|
|
706
|
-
}
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
707
717
|
return callback();
|
|
708
718
|
}
|
|
709
719
|
};
|
|
710
|
-
|
|
711
720
|
// pushes node content
|
|
712
|
-
|
|
721
|
+
const sendContent = () => {
|
|
713
722
|
if (this.content) {
|
|
714
723
|
if (Object.prototype.toString.call(this.content) === '[object Error]') {
|
|
715
724
|
// content is already errored
|
|
716
725
|
return callback(this.content);
|
|
717
726
|
}
|
|
718
|
-
|
|
719
727
|
if (typeof this.content.pipe === 'function') {
|
|
720
728
|
this.content.removeListener('error', this._contentErrorHandler);
|
|
721
729
|
this._contentErrorHandler = err => callback(err);
|
|
722
730
|
this.content.once('error', this._contentErrorHandler);
|
|
723
731
|
}
|
|
724
|
-
|
|
725
|
-
let createStream = () => {
|
|
732
|
+
const createStream = () => {
|
|
726
733
|
if (['quoted-printable', 'base64'].includes(transferEncoding)) {
|
|
727
734
|
contentStream = new (transferEncoding === 'base64' ? base64 : qp).Encoder(options);
|
|
728
|
-
|
|
729
735
|
contentStream.pipe(outputStream, {
|
|
730
736
|
end: false
|
|
731
737
|
});
|
|
732
738
|
contentStream.once('end', finalize);
|
|
733
739
|
contentStream.once('error', err => callback(err));
|
|
734
|
-
|
|
735
740
|
localStream = this._getStream(this.content);
|
|
736
741
|
localStream.pipe(contentStream);
|
|
737
|
-
}
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
738
744
|
// anything that is not QP or Base54 passes as-is
|
|
739
745
|
localStream = this._getStream(this.content);
|
|
740
746
|
localStream.pipe(outputStream, {
|
|
@@ -742,15 +748,13 @@ class MimeNode {
|
|
|
742
748
|
});
|
|
743
749
|
localStream.once('end', finalize);
|
|
744
750
|
}
|
|
745
|
-
|
|
746
751
|
localStream.once('error', err => callback(err));
|
|
747
752
|
};
|
|
748
|
-
|
|
749
753
|
if (this.content._resolve) {
|
|
750
|
-
|
|
754
|
+
const chunks = [];
|
|
751
755
|
let chunklen = 0;
|
|
752
756
|
let returned = false;
|
|
753
|
-
|
|
757
|
+
const sourceStream = this._getStream(this.content);
|
|
754
758
|
sourceStream.on('error', err => {
|
|
755
759
|
if (returned) {
|
|
756
760
|
return;
|
|
@@ -774,136 +778,125 @@ class MimeNode {
|
|
|
774
778
|
this.content._resolvedValue = Buffer.concat(chunks, chunklen);
|
|
775
779
|
setImmediate(createStream);
|
|
776
780
|
});
|
|
777
|
-
}
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
778
783
|
setImmediate(createStream);
|
|
779
784
|
}
|
|
780
785
|
return;
|
|
781
|
-
} else {
|
|
782
|
-
return setImmediate(finalize);
|
|
783
786
|
}
|
|
787
|
+
return setImmediate(finalize);
|
|
784
788
|
};
|
|
785
|
-
|
|
786
789
|
if (this._raw) {
|
|
787
790
|
setImmediate(() => {
|
|
788
791
|
if (Object.prototype.toString.call(this._raw) === '[object Error]') {
|
|
789
792
|
// content is already errored
|
|
790
793
|
return callback(this._raw);
|
|
791
794
|
}
|
|
792
|
-
|
|
793
795
|
// remove default error handler (if set)
|
|
794
796
|
if (typeof this._raw.pipe === 'function') {
|
|
795
797
|
this._raw.removeListener('error', this._contentErrorHandler);
|
|
796
798
|
}
|
|
797
|
-
|
|
798
|
-
let raw = this._getStream(this._raw);
|
|
799
|
+
const raw = this._getStream(this._raw);
|
|
799
800
|
raw.pipe(outputStream, {
|
|
800
801
|
end: false
|
|
801
802
|
});
|
|
802
803
|
raw.on('error', err => outputStream.emit('error', err));
|
|
803
804
|
raw.on('end', finalize);
|
|
804
805
|
});
|
|
805
|
-
}
|
|
806
|
+
}
|
|
807
|
+
else {
|
|
806
808
|
outputStream.write(this.buildHeaders() + '\r\n\r\n');
|
|
807
809
|
setImmediate(sendContent);
|
|
808
810
|
}
|
|
809
811
|
}
|
|
810
|
-
|
|
811
812
|
/**
|
|
812
813
|
* Sets envelope to be used instead of the generated one
|
|
813
814
|
*
|
|
814
|
-
* @return
|
|
815
|
+
* @return SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
|
|
815
816
|
*/
|
|
816
817
|
setEnvelope(envelope) {
|
|
817
818
|
let list;
|
|
818
|
-
|
|
819
819
|
this._envelope = {
|
|
820
820
|
from: false,
|
|
821
821
|
to: []
|
|
822
822
|
};
|
|
823
|
-
|
|
824
823
|
if (envelope.from) {
|
|
825
824
|
list = [];
|
|
826
|
-
this._convertAddresses(this.
|
|
825
|
+
this._convertAddresses(this._parseEnvelopeAddresses(envelope.from), list);
|
|
827
826
|
list = list.filter(address => address && address.address);
|
|
828
827
|
if (list.length && list[0]) {
|
|
829
828
|
this._envelope.from = list[0].address;
|
|
830
829
|
}
|
|
831
830
|
}
|
|
831
|
+
const seenRecipients = new Set();
|
|
832
|
+
const recipients = [];
|
|
832
833
|
['to', 'cc', 'bcc'].forEach(key => {
|
|
833
834
|
if (envelope[key]) {
|
|
834
|
-
this._convertAddresses(this.
|
|
835
|
+
this._convertAddresses(this._parseEnvelopeAddresses(envelope[key]), recipients, seenRecipients);
|
|
835
836
|
}
|
|
836
837
|
});
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
let standardFields = ['to', 'cc', 'bcc', 'from'];
|
|
841
|
-
Object.keys(envelope).forEach(key => {
|
|
842
|
-
if (!standardFields.includes(key)) {
|
|
843
|
-
this._envelope[key] = envelope[key];
|
|
844
|
-
}
|
|
845
|
-
});
|
|
846
|
-
|
|
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));
|
|
847
841
|
return this;
|
|
848
842
|
}
|
|
849
|
-
|
|
850
843
|
/**
|
|
851
844
|
* Generates and returns an object with parsed address fields
|
|
852
845
|
*
|
|
853
|
-
* @return
|
|
846
|
+
* @return Address object
|
|
854
847
|
*/
|
|
855
848
|
getAddresses() {
|
|
856
|
-
|
|
857
|
-
|
|
849
|
+
const addresses = {};
|
|
850
|
+
const seenByKey = new Map();
|
|
858
851
|
this._headers.forEach(header => {
|
|
859
|
-
|
|
852
|
+
const key = header.key.toLowerCase();
|
|
860
853
|
if (['from', 'sender', 'reply-to', 'to', 'cc', 'bcc'].includes(key)) {
|
|
861
854
|
if (!Array.isArray(addresses[key])) {
|
|
862
855
|
addresses[key] = [];
|
|
856
|
+
seenByKey.set(key, new Set());
|
|
863
857
|
}
|
|
864
|
-
|
|
865
|
-
this._convertAddresses(this._parseAddresses(header.value), addresses[key]);
|
|
858
|
+
this._convertAddresses(this._parseAddresses(header.value), addresses[key], seenByKey.get(key));
|
|
866
859
|
}
|
|
867
860
|
});
|
|
868
|
-
|
|
869
861
|
return addresses;
|
|
870
862
|
}
|
|
871
|
-
|
|
872
863
|
/**
|
|
873
864
|
* Generates and returns SMTP envelope with the sender address and a list of recipients addresses
|
|
874
865
|
*
|
|
875
|
-
* @return
|
|
866
|
+
* @return SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
|
|
876
867
|
*/
|
|
877
868
|
getEnvelope() {
|
|
878
869
|
if (this._envelope) {
|
|
879
870
|
return this._envelope;
|
|
880
871
|
}
|
|
881
|
-
|
|
882
|
-
let envelope = {
|
|
872
|
+
const envelope = {
|
|
883
873
|
from: false,
|
|
884
874
|
to: []
|
|
885
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 = [];
|
|
886
881
|
this._headers.forEach(header => {
|
|
887
|
-
|
|
882
|
+
const list = [];
|
|
888
883
|
if (header.key === 'From' || (!envelope.from && ['Reply-To', 'Sender'].includes(header.key))) {
|
|
889
884
|
this._convertAddresses(this._parseAddresses(header.value), list);
|
|
890
885
|
if (list.length && list[0]) {
|
|
891
886
|
envelope.from = list[0].address;
|
|
892
887
|
}
|
|
893
|
-
}
|
|
894
|
-
|
|
888
|
+
}
|
|
889
|
+
else if (['To', 'Cc', 'Bcc'].includes(header.key)) {
|
|
890
|
+
this._convertAddresses(this._parseAddresses(header.value), recipients, seenRecipients);
|
|
895
891
|
}
|
|
896
892
|
});
|
|
897
|
-
|
|
898
|
-
envelope.to = envelope.to.map(to => to.address);
|
|
899
|
-
|
|
893
|
+
envelope.to = recipients.map(to => to.address);
|
|
900
894
|
return envelope;
|
|
901
895
|
}
|
|
902
|
-
|
|
903
896
|
/**
|
|
904
897
|
* Returns Message-Id value. If it does not exist, then creates one
|
|
905
898
|
*
|
|
906
|
-
* @return
|
|
899
|
+
* @return Message-Id value
|
|
907
900
|
*/
|
|
908
901
|
messageId() {
|
|
909
902
|
let messageId = this.getHeader('Message-ID');
|
|
@@ -914,15 +907,13 @@ class MimeNode {
|
|
|
914
907
|
}
|
|
915
908
|
return messageId;
|
|
916
909
|
}
|
|
917
|
-
|
|
918
910
|
/**
|
|
919
911
|
* Sets pregenerated content that will be used as the output of this node
|
|
920
912
|
*
|
|
921
|
-
* @param
|
|
913
|
+
* @param raw Raw MIME contents
|
|
922
914
|
*/
|
|
923
915
|
setRaw(raw) {
|
|
924
916
|
this._raw = raw;
|
|
925
|
-
|
|
926
917
|
if (this._raw && typeof this._raw.pipe === 'function') {
|
|
927
918
|
// pre-stream handler. might be triggered if a stream is set as content
|
|
928
919
|
// and 'error' fires before anything is done with this stream
|
|
@@ -932,147 +923,271 @@ class MimeNode {
|
|
|
932
923
|
};
|
|
933
924
|
this._raw.once('error', this._contentErrorHandler);
|
|
934
925
|
}
|
|
935
|
-
|
|
936
926
|
return this;
|
|
937
927
|
}
|
|
938
|
-
|
|
939
928
|
/////// PRIVATE METHODS
|
|
940
|
-
|
|
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
|
+
}
|
|
941
950
|
/**
|
|
942
951
|
* Detects and returns handle to a stream related with the content.
|
|
943
952
|
*
|
|
944
|
-
* @param
|
|
945
|
-
* @returns
|
|
953
|
+
* @param content Node content
|
|
954
|
+
* @returns Stream object
|
|
955
|
+
* @internal
|
|
946
956
|
*/
|
|
947
957
|
_getStream(content) {
|
|
948
958
|
let contentStream;
|
|
949
|
-
|
|
950
959
|
if (content._resolvedValue) {
|
|
951
960
|
// pass string or buffer content as a stream
|
|
952
961
|
contentStream = new PassThrough();
|
|
953
|
-
|
|
954
962
|
setImmediate(() => {
|
|
955
963
|
try {
|
|
956
964
|
contentStream.end(content._resolvedValue);
|
|
957
|
-
}
|
|
958
|
-
|
|
965
|
+
}
|
|
966
|
+
catch (_err) {
|
|
967
|
+
contentStream.emit('error', _err);
|
|
959
968
|
}
|
|
960
969
|
});
|
|
961
|
-
|
|
962
970
|
return contentStream;
|
|
963
|
-
}
|
|
971
|
+
}
|
|
972
|
+
if (typeof content.pipe === 'function') {
|
|
964
973
|
// assume as stream
|
|
965
974
|
return content;
|
|
966
|
-
}
|
|
967
|
-
|
|
975
|
+
}
|
|
976
|
+
if (content && typeof content.path === 'string' && !content.href) {
|
|
977
|
+
if (this._accessDisabled('disableFileAccess')) {
|
|
968
978
|
contentStream = new PassThrough();
|
|
969
|
-
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
|
+
});
|
|
970
984
|
return contentStream;
|
|
971
985
|
}
|
|
972
986
|
// read file
|
|
973
987
|
return fs.createReadStream(content.path);
|
|
974
|
-
}
|
|
975
|
-
|
|
988
|
+
}
|
|
989
|
+
if (content && typeof content.href === 'string') {
|
|
990
|
+
if (this._accessDisabled('disableUrlAccess')) {
|
|
976
991
|
contentStream = new PassThrough();
|
|
977
|
-
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
|
+
});
|
|
978
997
|
return contentStream;
|
|
979
998
|
}
|
|
980
|
-
// fetch URL
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
contentStream = new PassThrough();
|
|
985
|
-
|
|
986
|
-
setImmediate(() => {
|
|
987
|
-
try {
|
|
988
|
-
contentStream.end(content || '');
|
|
989
|
-
} catch (err) {
|
|
990
|
-
contentStream.emit('error', err);
|
|
991
|
-
}
|
|
992
|
-
});
|
|
993
|
-
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 });
|
|
994
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;
|
|
995
1015
|
}
|
|
996
|
-
|
|
997
1016
|
/**
|
|
998
1017
|
* Parses addresses. Takes in a single address or an array or an
|
|
999
1018
|
* array of address arrays (eg. To: [[first group], [second group],...])
|
|
1000
1019
|
*
|
|
1001
|
-
* @param
|
|
1002
|
-
* @return
|
|
1020
|
+
* @param addresses Addresses to be parsed
|
|
1021
|
+
* @return An array of address objects
|
|
1022
|
+
* @internal
|
|
1003
1023
|
*/
|
|
1004
1024
|
_parseAddresses(addresses) {
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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;
|
|
1013
1057
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
+
});
|
|
1017
1117
|
}
|
|
1018
|
-
|
|
1019
1118
|
/**
|
|
1020
1119
|
* Normalizes a header key, uses Camel-Case form, except for uppercase MIME-
|
|
1021
1120
|
*
|
|
1022
|
-
* @param
|
|
1023
|
-
* @return
|
|
1121
|
+
* @param key Key to be normalized
|
|
1122
|
+
* @return key in Camel-Case form
|
|
1123
|
+
* @internal
|
|
1024
1124
|
*/
|
|
1025
1125
|
_normalizeHeaderKey(key) {
|
|
1026
1126
|
key = (key || '')
|
|
1027
1127
|
.toString()
|
|
1028
1128
|
// no newlines in keys
|
|
1029
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, '')
|
|
1030
1133
|
.trim()
|
|
1031
1134
|
.toLowerCase()
|
|
1032
1135
|
// use uppercase words, except MIME
|
|
1033
1136
|
.replace(/^X-SMTPAPI$|^(MIME|DKIM|ARC|BIMI)\b|^[a-z]|-(SPF|FBL|ID|MD5)$|-[a-z]/gi, c => c.toUpperCase())
|
|
1034
1137
|
// special case
|
|
1035
1138
|
.replace(/^Content-Features$/i, 'Content-features');
|
|
1036
|
-
|
|
1037
1139
|
return key;
|
|
1038
1140
|
}
|
|
1039
|
-
|
|
1040
1141
|
/**
|
|
1041
1142
|
* Checks if the content type is multipart and defines boundary if needed.
|
|
1042
1143
|
* Doesn't return anything, modifies object argument instead.
|
|
1043
1144
|
*
|
|
1044
|
-
* @param
|
|
1145
|
+
* @param structured Parsed header value for 'Content-Type' key
|
|
1146
|
+
* @internal
|
|
1045
1147
|
*/
|
|
1046
1148
|
_handleContentType(structured) {
|
|
1047
1149
|
this.contentType = structured.value.trim().toLowerCase();
|
|
1048
|
-
|
|
1049
1150
|
this.multipart = /^multipart\//i.test(this.contentType) ? this.contentType.substr(this.contentType.indexOf('/') + 1) : false;
|
|
1050
|
-
|
|
1051
1151
|
if (this.multipart) {
|
|
1052
|
-
|
|
1053
|
-
|
|
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 {
|
|
1054
1168
|
this.boundary = false;
|
|
1055
1169
|
}
|
|
1056
1170
|
}
|
|
1057
|
-
|
|
1058
1171
|
/**
|
|
1059
1172
|
* Generates a multipart boundary value
|
|
1060
1173
|
*
|
|
1061
|
-
* @return
|
|
1174
|
+
* @return boundary value
|
|
1175
|
+
* @internal
|
|
1062
1176
|
*/
|
|
1063
1177
|
_generateBoundary() {
|
|
1064
|
-
|
|
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;
|
|
1065
1181
|
}
|
|
1066
|
-
|
|
1067
1182
|
/**
|
|
1068
1183
|
* Encodes a header value for use in the generated rfc2822 email.
|
|
1069
1184
|
*
|
|
1070
|
-
* @param
|
|
1071
|
-
* @param
|
|
1185
|
+
* @param key Header key
|
|
1186
|
+
* @param value Header value
|
|
1187
|
+
* @internal
|
|
1072
1188
|
*/
|
|
1073
1189
|
_encodeHeaderValue(key, value) {
|
|
1074
1190
|
key = this._normalizeHeaderKey(key);
|
|
1075
|
-
|
|
1076
1191
|
switch (key) {
|
|
1077
1192
|
// Structured headers
|
|
1078
1193
|
case 'From':
|
|
@@ -1082,172 +1197,206 @@ class MimeNode {
|
|
|
1082
1197
|
case 'Bcc':
|
|
1083
1198
|
case 'Reply-To':
|
|
1084
1199
|
return this._convertAddresses(this._parseAddresses(value));
|
|
1085
|
-
|
|
1086
1200
|
// values enclosed in <>
|
|
1087
1201
|
case 'Message-ID':
|
|
1088
1202
|
case 'In-Reply-To':
|
|
1089
1203
|
case 'Content-Id':
|
|
1090
|
-
|
|
1091
|
-
|
|
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, '');
|
|
1092
1211
|
if (value.charAt(0) !== '<') {
|
|
1093
1212
|
value = '<' + value;
|
|
1094
1213
|
}
|
|
1095
|
-
|
|
1096
1214
|
if (value.charAt(value.length - 1) !== '>') {
|
|
1097
1215
|
value = value + '>';
|
|
1098
1216
|
}
|
|
1099
1217
|
return value;
|
|
1100
|
-
|
|
1101
1218
|
// space separated list of values enclosed in <>
|
|
1102
1219
|
case 'References':
|
|
1103
1220
|
value = [].concat
|
|
1104
|
-
.apply(
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
return elm.replace(/<[^>]*>/g, str => str.replace(/\s/g, '')).split(/\s+/);
|
|
1113
|
-
})
|
|
1114
|
-
)
|
|
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
|
+
}))
|
|
1115
1229
|
.map(elm => {
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1230
|
+
if (elm.charAt(0) !== '<') {
|
|
1231
|
+
elm = '<' + elm;
|
|
1232
|
+
}
|
|
1233
|
+
if (elm.charAt(elm.length - 1) !== '>') {
|
|
1234
|
+
elm = elm + '>';
|
|
1235
|
+
}
|
|
1236
|
+
return elm;
|
|
1237
|
+
});
|
|
1125
1238
|
return value.join(' ').trim();
|
|
1126
|
-
|
|
1127
1239
|
case 'Date':
|
|
1128
1240
|
if (Object.prototype.toString.call(value) === '[object Date]') {
|
|
1129
1241
|
return value.toUTCString().replace(/GMT/, '+0000');
|
|
1130
1242
|
}
|
|
1131
|
-
|
|
1132
1243
|
value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
|
|
1133
|
-
return this.
|
|
1134
|
-
|
|
1244
|
+
return this._encodeHeaderText(value);
|
|
1135
1245
|
case 'Content-Type':
|
|
1136
1246
|
case 'Content-Disposition':
|
|
1137
1247
|
// if it includes a filename then it is already encoded
|
|
1138
1248
|
return (value || '').toString().replace(/\r?\n|\r/g, ' ');
|
|
1139
|
-
|
|
1140
1249
|
default:
|
|
1141
1250
|
value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
|
|
1142
|
-
|
|
1143
|
-
return this._encodeWords(value);
|
|
1251
|
+
return this._encodeHeaderText(value);
|
|
1144
1252
|
}
|
|
1145
1253
|
}
|
|
1146
|
-
|
|
1147
1254
|
/**
|
|
1148
1255
|
* Rebuilds address object using punycode and other adjustments
|
|
1149
1256
|
*
|
|
1150
|
-
* @param
|
|
1151
|
-
* @param
|
|
1152
|
-
* @
|
|
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
|
|
1153
1262
|
*/
|
|
1154
|
-
_convertAddresses(addresses, uniqueList) {
|
|
1155
|
-
|
|
1156
|
-
|
|
1263
|
+
_convertAddresses(addresses, uniqueList, seenAddresses) {
|
|
1264
|
+
const values = [];
|
|
1157
1265
|
uniqueList = uniqueList || [];
|
|
1158
|
-
|
|
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
|
+
}
|
|
1159
1276
|
[].concat(addresses || []).forEach(address => {
|
|
1160
1277
|
if (address.address) {
|
|
1161
1278
|
address.address = this._normalizeAddress(address.address);
|
|
1162
|
-
|
|
1163
1279
|
if (!address.name) {
|
|
1164
|
-
|
|
1165
|
-
|
|
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 {
|
|
1166
1287
|
values.push(`${this._encodeAddressName(address.name)} <${address.address}>`);
|
|
1167
1288
|
}
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
uniqueList.push(address);
|
|
1172
|
-
}
|
|
1289
|
+
if (!seenAddresses.has(address.address)) {
|
|
1290
|
+
seenAddresses.add(address.address);
|
|
1291
|
+
uniqueList.push(address);
|
|
1173
1292
|
}
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1293
|
+
}
|
|
1294
|
+
else if (address.group) {
|
|
1295
|
+
const groupListAddresses = (address.group.length ? this._convertAddresses(address.group, uniqueList, seenAddresses) : '').trim();
|
|
1176
1296
|
values.push(`${this._encodeAddressName(address.name)}:${groupListAddresses};`);
|
|
1177
1297
|
}
|
|
1178
1298
|
});
|
|
1179
|
-
|
|
1180
1299
|
return values.join(', ');
|
|
1181
1300
|
}
|
|
1182
|
-
|
|
1183
1301
|
/**
|
|
1184
1302
|
* Normalizes an email address
|
|
1185
1303
|
*
|
|
1186
|
-
* @param
|
|
1187
|
-
* @return
|
|
1304
|
+
* @param address An array of address objects
|
|
1305
|
+
* @return address string
|
|
1306
|
+
* @internal
|
|
1188
1307
|
*/
|
|
1189
1308
|
_normalizeAddress(address) {
|
|
1190
1309
|
address = (address || '')
|
|
1191
1310
|
.toString()
|
|
1192
|
-
.replace(/[\x00-\x1F<>]+/g, ' ') // remove unallowed characters
|
|
1311
|
+
.replace(/[\x00-\x1F\x7F<>]+/g, ' ') // remove unallowed characters
|
|
1193
1312
|
.trim();
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
if (lastAt < 0) {
|
|
1197
|
-
// Bare username
|
|
1313
|
+
if (!address) {
|
|
1314
|
+
// callers use an empty value to detect a missing address
|
|
1198
1315
|
return address;
|
|
1199
1316
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
//
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
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);
|
|
1211
1334
|
try {
|
|
1212
|
-
encodedDomain =
|
|
1213
|
-
} catch (err) {
|
|
1214
|
-
// keep as is?
|
|
1335
|
+
encodedDomain = normalizeDomain(domain.toLowerCase(), smtputf8);
|
|
1215
1336
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
if (user.charAt(0) !== '"') {
|
|
1219
|
-
user = '"' + user;
|
|
1220
|
-
}
|
|
1221
|
-
if (user.substr(-1) !== '"') {
|
|
1222
|
-
user = user + '"';
|
|
1223
|
-
}
|
|
1337
|
+
catch (_err) {
|
|
1338
|
+
// keep domain as supplied
|
|
1224
1339
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
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);
|
|
1227
1360
|
}
|
|
1228
|
-
|
|
1229
1361
|
/**
|
|
1230
1362
|
* If needed, mime encodes the name part
|
|
1231
1363
|
*
|
|
1232
|
-
* @param
|
|
1233
|
-
* @returns
|
|
1364
|
+
* @param name Name part of an address
|
|
1365
|
+
* @returns Mime word encoded string if needed
|
|
1366
|
+
* @internal
|
|
1234
1367
|
*/
|
|
1235
1368
|
_encodeAddressName(name) {
|
|
1236
|
-
if (!/^[\w
|
|
1369
|
+
if (!/^[\w ]*$/.test(name)) {
|
|
1237
1370
|
if (/^[\x20-\x7e]*$/.test(name)) {
|
|
1238
|
-
return
|
|
1239
|
-
}
|
|
1371
|
+
return mimeFuncs.quoteString(name);
|
|
1372
|
+
}
|
|
1373
|
+
else {
|
|
1240
1374
|
return mimeFuncs.encodeWord(name, this._getTextEncoding(name), 52);
|
|
1241
1375
|
}
|
|
1242
1376
|
}
|
|
1243
1377
|
return name;
|
|
1244
1378
|
}
|
|
1245
|
-
|
|
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
|
+
}
|
|
1246
1394
|
/**
|
|
1247
1395
|
* If needed, mime encodes the name part
|
|
1248
1396
|
*
|
|
1249
|
-
* @param
|
|
1250
|
-
* @returns
|
|
1397
|
+
* @param value Name part of an address
|
|
1398
|
+
* @returns Mime word encoded string if needed
|
|
1399
|
+
* @internal
|
|
1251
1400
|
*/
|
|
1252
1401
|
_encodeWords(value) {
|
|
1253
1402
|
// set encodeAll parameter to true even though it is against the recommendation of RFC2047,
|
|
@@ -1255,51 +1404,50 @@ class MimeNode {
|
|
|
1255
1404
|
// but some clients (eg. Zimbra) do not handle it properly and remove surrounding whitespace
|
|
1256
1405
|
return mimeFuncs.encodeWords(value, this._getTextEncoding(value), 52, true);
|
|
1257
1406
|
}
|
|
1258
|
-
|
|
1259
1407
|
/**
|
|
1260
1408
|
* Detects best mime encoding for a text value
|
|
1261
1409
|
*
|
|
1262
|
-
* @param
|
|
1263
|
-
* @return
|
|
1410
|
+
* @param value Value to check for
|
|
1411
|
+
* @return either 'Q' or 'B'
|
|
1412
|
+
* @internal
|
|
1264
1413
|
*/
|
|
1265
1414
|
_getTextEncoding(value) {
|
|
1266
1415
|
value = (value || '').toString();
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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
|
+
}
|
|
1280
1432
|
}
|
|
1281
|
-
|
|
1433
|
+
// if there are more latin symbols than binary/unicode, then prefer Q, otherwise B
|
|
1434
|
+
return nonLatinLen < latinLen ? 'Q' : 'B';
|
|
1282
1435
|
}
|
|
1283
|
-
|
|
1284
1436
|
/**
|
|
1285
1437
|
* Generates a message id
|
|
1286
1438
|
*
|
|
1287
|
-
* @return
|
|
1439
|
+
* @return Random Message-ID value
|
|
1440
|
+
* @internal
|
|
1288
1441
|
*/
|
|
1289
1442
|
_generateMessageId() {
|
|
1290
|
-
return (
|
|
1291
|
-
'<' +
|
|
1443
|
+
return ('<' +
|
|
1292
1444
|
[2, 2, 2, 6].reduce(
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
crypto.randomBytes(4).toString('hex')
|
|
1296
|
-
) +
|
|
1445
|
+
// crux to generate UUID-like random strings
|
|
1446
|
+
(prev, len) => prev + '-' + crypto.randomBytes(len).toString('hex'), crypto.randomBytes(4).toString('hex')) +
|
|
1297
1447
|
'@' +
|
|
1298
1448
|
// try to use the domain of the FROM address or fallback to server hostname
|
|
1299
1449
|
(this.getEnvelope().from || this.hostname || 'localhost').split('@').pop() +
|
|
1300
|
-
'>'
|
|
1301
|
-
);
|
|
1450
|
+
'>');
|
|
1302
1451
|
}
|
|
1303
1452
|
}
|
|
1304
|
-
|
|
1305
|
-
module.exports = MimeNode;
|
|
1453
|
+
export default MimeNode;
|