mirror of
https://github.com/dawidd6/action-send-mail.git
synced 2026-09-09 05:56:37 +07:00
node_modules: update (#314)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
This commit is contained in:
+288
-60
@@ -7,6 +7,7 @@ const fs = require('fs');
|
||||
const punycode = require('../punycode');
|
||||
const { PassThrough } = require('stream');
|
||||
const shared = require('../shared');
|
||||
const urlModule = require('url');
|
||||
|
||||
const mimeFuncs = require('../mime-funcs');
|
||||
const qp = require('../qp');
|
||||
@@ -21,6 +22,59 @@ const LeUnix = require('./le-unix');
|
||||
|
||||
const FORMATTED_HEADERS = ['From', 'Sender', 'To', 'Cc', 'Bcc', 'Reply-To', 'Date', 'References'];
|
||||
|
||||
// RFC 5321 atext, plus the non-ascii bytes that SMTPUTF8 (RFC 6531) adds to it. A local part
|
||||
// built from these, with '.' as a separator, is a dot-atom and can be emitted bare
|
||||
const ATEXT = "[A-Za-z0-9!#$%&'*+\\-/=?^_`{|}~\\x80-\\uFFFF]";
|
||||
const DOT_ATOM = new RegExp('^' + ATEXT + '+(?:\\.' + ATEXT + '+)*$');
|
||||
|
||||
// A complete quoted-string: everything between the outer quotes is either a plain char or
|
||||
// a quoted-pair. Anchored, so a value that only starts and ends with a quote does not pass
|
||||
const QUOTED_STRING = /^"(?:[^"\\]|\\[\s\S])*"$/;
|
||||
|
||||
// An address that carries no special anywhere can be emitted bare in a header, everything
|
||||
// else goes into angle brackets so that the header can not be read as more addresses than
|
||||
// the envelope carries
|
||||
const PLAIN_ADDRESS = /^[^\s"(),:;<>@[\\\]]+@[^\s"(),:;<>@[\\\]]+$/;
|
||||
|
||||
// domainToASCII and domainToUnicode are WHATWG host parsers rather than plain IDNA
|
||||
// mappers, so they do more than map: they cut the host at '/', '\\', '?' and '#', drop C0
|
||||
// controls, and percent-decode. Handing them 'evil.example/mail.corp.example' returns the
|
||||
// deliverable 'evil.example', which would turn a value the bundled codec leaves as
|
||||
// unroutable garbage into mail for a domain the sender never named. None of these
|
||||
// characters are legal in a domain, so keep them away from the mapper.
|
||||
const URL_PARSER_UNSAFE = /[/\\?#%\x00-\x20\x7F]/;
|
||||
|
||||
/**
|
||||
* Encodes a domain the way browsers, the WHATWG URL Standard and DNS facing resolvers do,
|
||||
* which is with UTS-46 mapping applied before the Punycode step.
|
||||
*
|
||||
* The bundled codec is plain RFC 3492 and maps nothing, so it disagrees with every
|
||||
* conformant parser on any domain holding a mapped or ignored code point. An invisible
|
||||
* U+00AD in 'compa\u00ADny.com' encoded to 'xn--company-pka.com' where a validator reads
|
||||
* 'company.com', which let an allow-listed domain be checked and a different one mailed.
|
||||
*
|
||||
* Anything the URL parser does not accept as a hostname, an address literal such as
|
||||
* '[127.0.0.1]' included, comes back empty and falls through to the bundled codec, which
|
||||
* leaves those as they were supplied.
|
||||
*
|
||||
* @param {String} domain Domain to encode, already lowercased by the caller
|
||||
* @param {Boolean} toUnicode Return the U-label form instead of the A-label form
|
||||
* @return {String} Encoded domain
|
||||
*/
|
||||
function normalizeDomain(domain, toUnicode) {
|
||||
// domainToASCII and domainToUnicode landed in Node 7, the bundled codec covers Node 6
|
||||
const mapper = toUnicode ? urlModule.domainToUnicode : urlModule.domainToASCII;
|
||||
|
||||
if (typeof mapper === 'function' && !URL_PARSER_UNSAFE.test(domain)) {
|
||||
const mapped = mapper(domain);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
}
|
||||
|
||||
return toUnicode ? punycode.toUnicode(domain) : punycode.toASCII(domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new mime tree node. Assumes 'multipart/*' as the content type
|
||||
* if it is a branch, anything else counts as leaf. If rootNode is missing from
|
||||
@@ -190,6 +244,14 @@ class MimeNode {
|
||||
* @return {Object} Appended node object
|
||||
*/
|
||||
appendChild(childNode) {
|
||||
// Take the node out of the tree it is in first. Leaving it there keeps it in that
|
||||
// parent's childNodes, so it still streams as part of the old tree while parentNode
|
||||
// already points at the new one, and anything read off the parent chain answers for
|
||||
// the wrong tree.
|
||||
if (childNode.parentNode && childNode.parentNode !== this) {
|
||||
childNode.remove();
|
||||
}
|
||||
|
||||
if (childNode.rootNode !== this.rootNode) {
|
||||
childNode.rootNode = this.rootNode;
|
||||
childNode._nodeId = ++this.rootNode.nodeCounter;
|
||||
@@ -519,11 +581,10 @@ class MimeNode {
|
||||
const formattedHeaders = FORMATTED_HEADERS;
|
||||
|
||||
if (value && typeof value === 'object' && !formattedHeaders.includes(key)) {
|
||||
Object.keys(value).forEach(key => {
|
||||
if (key !== 'value') {
|
||||
options[key] = value[key];
|
||||
}
|
||||
});
|
||||
// the keys come from a caller supplied header object and `options.prepared`
|
||||
// below decides whether the value is emitted raw, so an own "__proto__" key
|
||||
// here would turn an unfolded value into header injection
|
||||
shared.copyOwnKeys(options, value, optionKey => optionKey === 'value');
|
||||
value = (value.value || '').toString();
|
||||
if (!value.trim()) {
|
||||
return;
|
||||
@@ -552,6 +613,11 @@ class MimeNode {
|
||||
case 'Content-Type':
|
||||
structured = mimeFuncs.parseHeaderValue(value);
|
||||
|
||||
// the type token decides multipart and charset below, so clean it before
|
||||
// those run and not just on the way out, otherwise a control char makes
|
||||
// the checks miss and the header ends up claiming a type it is not set up for
|
||||
structured.value = (structured.value || '').toString().replace(/[\x00-\x1f\x7f]/g, '');
|
||||
|
||||
this._handleContentType(structured);
|
||||
|
||||
if (
|
||||
@@ -568,11 +634,18 @@ class MimeNode {
|
||||
// add support for non-compliant clients like QQ webmail
|
||||
// we can't build the value with buildHeaderValue as the value is non standard and
|
||||
// would be converted to parameter continuation encoding that we do not want
|
||||
param = this._encodeWords(this.filename);
|
||||
// control chars can not be quoted here: HT is a fold point that unfolding would
|
||||
// turn into a space, CR/LF can not appear in a header at all and DEL is not
|
||||
// qtext, so force the mime encoded word that a non-ascii filename would get anyway
|
||||
param = /[\x00-\x1f\x7f]/.test(this.filename)
|
||||
? mimeFuncs.encodeWord(this.filename, this._getTextEncoding(this.filename), 52)
|
||||
: this._encodeWords(this.filename);
|
||||
|
||||
if (param !== this.filename || /[\s'"\\;:/=(),<>@[\]?]|^-/.test(param)) {
|
||||
// include value in quotes if needed
|
||||
param = '"' + param + '"';
|
||||
// include value in quotes if needed, escaping backslashes and quotes as
|
||||
// quoted-pairs exactly like buildHeaderValue does for filename=, otherwise
|
||||
// a trailing backslash would escape the closing quote
|
||||
param = JSON.stringify(param);
|
||||
}
|
||||
value += '; name=' + param;
|
||||
}
|
||||
@@ -595,8 +668,12 @@ class MimeNode {
|
||||
|
||||
if (typeof this.normalizeHeaderKey === 'function') {
|
||||
const normalized = this.normalizeHeaderKey(key, value);
|
||||
if (normalized && typeof normalized === 'string' && normalized.length) {
|
||||
key = normalized;
|
||||
// the result replaces the key on the way into the header, so it gets the same
|
||||
// treatment the key it replaces already had. a line break here would end the
|
||||
// header and start one of the caller's own
|
||||
const cleaned = typeof normalized === 'string' ? normalized.replace(/[\x00-\x1f\x7f]/g, '') : '';
|
||||
if (cleaned) {
|
||||
key = cleaned;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -837,26 +914,23 @@ class MimeNode {
|
||||
|
||||
if (envelope.from) {
|
||||
list = [];
|
||||
this._convertAddresses(this._parseAddresses(envelope.from), list);
|
||||
this._convertAddresses(this._parseEnvelopeAddresses(envelope.from), list);
|
||||
list = list.filter(address => address && address.address);
|
||||
if (list.length && list[0]) {
|
||||
this._envelope.from = list[0].address;
|
||||
}
|
||||
}
|
||||
const seenRecipients = new Set();
|
||||
['to', 'cc', 'bcc'].forEach(key => {
|
||||
if (envelope[key]) {
|
||||
this._convertAddresses(this._parseAddresses(envelope[key]), this._envelope.to);
|
||||
this._convertAddresses(this._parseEnvelopeAddresses(envelope[key]), this._envelope.to, seenRecipients);
|
||||
}
|
||||
});
|
||||
|
||||
this._envelope.to = this._envelope.to.map(to => to.address).filter(address => address);
|
||||
|
||||
const standardFields = ['to', 'cc', 'bcc', 'from'];
|
||||
Object.keys(envelope).forEach(key => {
|
||||
if (!standardFields.includes(key)) {
|
||||
this._envelope[key] = envelope[key];
|
||||
}
|
||||
});
|
||||
shared.copyOwnKeys(this._envelope, envelope, key => standardFields.includes(key));
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -868,15 +942,17 @@ class MimeNode {
|
||||
*/
|
||||
getAddresses() {
|
||||
const addresses = {};
|
||||
const seenByKey = new Map();
|
||||
|
||||
this._headers.forEach(header => {
|
||||
const key = header.key.toLowerCase();
|
||||
if (['from', 'sender', 'reply-to', 'to', 'cc', 'bcc'].includes(key)) {
|
||||
if (!Array.isArray(addresses[key])) {
|
||||
addresses[key] = [];
|
||||
seenByKey.set(key, new Set());
|
||||
}
|
||||
|
||||
this._convertAddresses(this._parseAddresses(header.value), addresses[key]);
|
||||
this._convertAddresses(this._parseAddresses(header.value), addresses[key], seenByKey.get(key));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -897,6 +973,12 @@ class MimeNode {
|
||||
from: false,
|
||||
to: []
|
||||
};
|
||||
|
||||
// Built once and carried across the headers. Letting _convertAddresses seed it per
|
||||
// call would cost O(headers x recipients), and a message can carry many address
|
||||
// headers: `headers: { to: [...] }` emits one To per entry.
|
||||
const seenRecipients = new Set();
|
||||
|
||||
this._headers.forEach(header => {
|
||||
const list = [];
|
||||
if (header.key === 'From' || (!envelope.from && ['Reply-To', 'Sender'].includes(header.key))) {
|
||||
@@ -905,7 +987,7 @@ class MimeNode {
|
||||
envelope.from = list[0].address;
|
||||
}
|
||||
} else if (['To', 'Cc', 'Bcc'].includes(header.key)) {
|
||||
this._convertAddresses(this._parseAddresses(header.value), envelope.to);
|
||||
this._convertAddresses(this._parseAddresses(header.value), envelope.to, seenRecipients);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -952,6 +1034,26 @@ class MimeNode {
|
||||
|
||||
/////// PRIVATE METHODS
|
||||
|
||||
/**
|
||||
* Checks an access policy flag for this node and every node above it. The flags are set
|
||||
* from the options the node was built with, and createChild only ever sees the options
|
||||
* the caller passed, so a child of a closed tree starts out open. Reading the answer off
|
||||
* the parent chain keeps it right whatever order the tree was assembled in.
|
||||
*
|
||||
* @param {String} flag Either 'disableFileAccess' or 'disableUrlAccess'
|
||||
* @return {Boolean} true if this node or an ancestor closed that access
|
||||
*/
|
||||
_accessDisabled(flag) {
|
||||
let node = this;
|
||||
while (node) {
|
||||
if (node[flag]) {
|
||||
return true;
|
||||
}
|
||||
node = node.parentNode;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects and returns handle to a stream related with the content.
|
||||
*
|
||||
@@ -982,7 +1084,7 @@ class MimeNode {
|
||||
}
|
||||
|
||||
if (content && typeof content.path === 'string' && !content.href) {
|
||||
if (this.disableFileAccess) {
|
||||
if (this._accessDisabled('disableFileAccess')) {
|
||||
contentStream = new PassThrough();
|
||||
setImmediate(() => {
|
||||
const err = new Error('File access rejected for ' + content.path);
|
||||
@@ -996,7 +1098,7 @@ class MimeNode {
|
||||
}
|
||||
|
||||
if (content && typeof content.href === 'string') {
|
||||
if (this.disableUrlAccess) {
|
||||
if (this._accessDisabled('disableUrlAccess')) {
|
||||
contentStream = new PassThrough();
|
||||
setImmediate(() => {
|
||||
const err = new Error('Url access rejected for ' + content.href);
|
||||
@@ -1005,7 +1107,9 @@ class MimeNode {
|
||||
});
|
||||
return contentStream;
|
||||
}
|
||||
// fetch URL
|
||||
// fetch URL. nmfetch refuses any scheme that is not http(s), and it decides
|
||||
// that on the parsed URL. Testing the raw string here instead would reject
|
||||
// forms the parser accepts, such as a leading space or a slash-less authority
|
||||
return nmfetch(content.href, { headers: content.httpHeaders, tls: content.tls });
|
||||
}
|
||||
|
||||
@@ -1030,17 +1134,81 @@ class MimeNode {
|
||||
* @return {Array} An array of address objects
|
||||
*/
|
||||
_parseAddresses(addresses) {
|
||||
return [].concat.apply(
|
||||
[],
|
||||
[].concat(addresses).map(address => {
|
||||
if (address && address.address) {
|
||||
address.address = this._normalizeAddress(address.address);
|
||||
address.name = address.name || '';
|
||||
return [address];
|
||||
// Collected into one list as we go. concat.apply spreads the entries into arguments
|
||||
// and throws a RangeError once a recipient array is long enough to pass the
|
||||
// argument limit, which a large Bcc list reaches on its own.
|
||||
const flattened = [];
|
||||
|
||||
[].concat(addresses).forEach(address => {
|
||||
if (address && address.address) {
|
||||
const normalized = this._normalizeAddress(address.address);
|
||||
if (normalized === address.address && typeof address.name === 'string') {
|
||||
// there is nothing to rewrite, so there is nothing to keep off the original
|
||||
flattened.push(address);
|
||||
return;
|
||||
}
|
||||
return addressparser(address);
|
||||
})
|
||||
);
|
||||
|
||||
// rewriting would land on the object the caller passed in and might
|
||||
// still hold a reference to, so rewrite a copy of it instead. An own
|
||||
// "__proto__" key would make the copy inherit from caller data, and
|
||||
// _convertAddresses reads `group` off it straight into the envelope
|
||||
const copy = shared.copyOwnKeys({}, address);
|
||||
copy.address = normalized;
|
||||
copy.name = address.name || '';
|
||||
flattened.push(copy);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = this._normalizeParsedAddresses(addressparser(address));
|
||||
for (let i = 0; i < parsed.length; i++) {
|
||||
flattened.push(parsed[i]);
|
||||
}
|
||||
});
|
||||
|
||||
return flattened;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the addresses of a freshly parsed address list, groups included.
|
||||
*
|
||||
* Everything this method returns carries a normalized address, whether it arrived as an
|
||||
* object or was parsed out of a header value. Without this the two shapes disagree, and
|
||||
* a consumer reading the parsed form back is handed the ambiguous
|
||||
* 'user@evil.com@good.com' that the header and the envelope no longer carry.
|
||||
*
|
||||
* @param {Array} parsed An array of address objects, as returned by addressparser
|
||||
* @return {Array} The same array, with every address normalized
|
||||
*/
|
||||
_normalizeParsedAddresses(parsed) {
|
||||
// addressparser builds these objects, so no caller holds a reference to rewrite around
|
||||
parsed.forEach(entry => {
|
||||
if (entry.address) {
|
||||
entry.address = this._normalizeAddress(entry.address);
|
||||
} else if (entry.group) {
|
||||
this._normalizeParsedAddresses(entry.group);
|
||||
}
|
||||
});
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the addresses of an explicitly set envelope.
|
||||
*
|
||||
* An envelope value is an addr-spec and never a display name, so a bare local username
|
||||
* such as 'root' is the address here. Header parsing has to read the same value as a
|
||||
* display name, as a value with no '@' in it can not be an addr-spec in a header.
|
||||
*
|
||||
* @param {Mixed} addresses Addresses to be parsed
|
||||
* @return {Array} An array of address objects
|
||||
*/
|
||||
_parseEnvelopeAddresses(addresses) {
|
||||
return this._parseAddresses(addresses).map(entry => {
|
||||
if (entry.address || entry.group || !entry.name || /[\s@]/.test(entry.name)) {
|
||||
return entry;
|
||||
}
|
||||
return { address: this._normalizeAddress(entry.name), name: '' };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1054,6 +1222,9 @@ class MimeNode {
|
||||
.toString()
|
||||
// no newlines in keys
|
||||
.replace(/\r?\n|\r/g, ' ')
|
||||
// a field name is printable ascii without the colon, so a control char or DEL
|
||||
// can only be dropped, there is no quoting construct around a field name
|
||||
.replace(/[\x00-\x1f\x7f]/g, '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
// use uppercase words, except MIME
|
||||
@@ -1114,7 +1285,13 @@ class MimeNode {
|
||||
case 'Message-ID':
|
||||
case 'In-Reply-To':
|
||||
case 'Content-Id':
|
||||
value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
|
||||
// a msg-id is structured, so an encoded word inside the angle brackets would
|
||||
// be read as literal text. drop the characters that can not appear in a header
|
||||
// at all, but leave HT alone, it separates the ids of a multi id value
|
||||
value = (value || '')
|
||||
.toString()
|
||||
.replace(/\r?\n|\r/g, ' ')
|
||||
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');
|
||||
|
||||
if (value.charAt(0) !== '<') {
|
||||
value = '<' + value;
|
||||
@@ -1134,6 +1311,7 @@ class MimeNode {
|
||||
elm = (elm || '')
|
||||
.toString()
|
||||
.replace(/\r?\n|\r/g, ' ')
|
||||
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
|
||||
.trim();
|
||||
return elm.replace(/<[^>]*>/g, str => str.replace(/\s/g, '')).split(/\s+/);
|
||||
})
|
||||
@@ -1156,7 +1334,7 @@ class MimeNode {
|
||||
}
|
||||
|
||||
value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
|
||||
return this._encodeWords(value);
|
||||
return this._encodeHeaderText(value);
|
||||
|
||||
case 'Content-Type':
|
||||
case 'Content-Disposition':
|
||||
@@ -1165,8 +1343,7 @@ class MimeNode {
|
||||
|
||||
default:
|
||||
value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
|
||||
// encodeWords only encodes if needed, otherwise the original string is returned
|
||||
return this._encodeWords(value);
|
||||
return this._encodeHeaderText(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1177,26 +1354,44 @@ class MimeNode {
|
||||
* @param {Array} [uniqueList] An array to be populated with addresses
|
||||
* @return {String} address string
|
||||
*/
|
||||
_convertAddresses(addresses, uniqueList) {
|
||||
_convertAddresses(addresses, uniqueList, seenAddresses) {
|
||||
const values = [];
|
||||
|
||||
uniqueList = uniqueList || [];
|
||||
|
||||
// Membership is checked once per address, so scanning uniqueList itself would make
|
||||
// a recipient list cost O(n^2). Groups recurse with the same set so that a nested
|
||||
// group still dedupes against the addresses collected around it, and a caller that
|
||||
// passes a partly filled list (To, then Cc, then Bcc) keeps deduping across headers.
|
||||
if (!seenAddresses) {
|
||||
seenAddresses = new Set();
|
||||
for (let i = 0; i < uniqueList.length; i++) {
|
||||
seenAddresses.add(uniqueList[i].address);
|
||||
}
|
||||
}
|
||||
|
||||
[].concat(addresses || []).forEach(address => {
|
||||
if (address.address) {
|
||||
address.address = this._normalizeAddress(address.address);
|
||||
|
||||
if (!address.name) {
|
||||
values.push(address.address.indexOf(' ') >= 0 ? `<${address.address}>` : `${address.address}`);
|
||||
// an address that carries a special, be it a quoted local part or a domain
|
||||
// that could not be normalized, is only unambiguous inside angle brackets.
|
||||
// Without them a ',' or a ';' anywhere in it reads as a recipient separator
|
||||
// and the header would list more recipients than the envelope carries
|
||||
values.push(PLAIN_ADDRESS.test(address.address) ? address.address : `<${address.address}>`);
|
||||
} else {
|
||||
values.push(`${this._encodeAddressName(address.name)} <${address.address}>`);
|
||||
}
|
||||
|
||||
if (!uniqueList.some(a => a.address === address.address)) {
|
||||
if (!seenAddresses.has(address.address)) {
|
||||
seenAddresses.add(address.address);
|
||||
uniqueList.push(address);
|
||||
}
|
||||
} else if (address.group) {
|
||||
const groupListAddresses = (address.group.length ? this._convertAddresses(address.group, uniqueList) : '').trim();
|
||||
const groupListAddresses = (
|
||||
address.group.length ? this._convertAddresses(address.group, uniqueList, seenAddresses) : ''
|
||||
).trim();
|
||||
values.push(`${this._encodeAddressName(address.name)}:${groupListAddresses};`);
|
||||
}
|
||||
});
|
||||
@@ -1213,45 +1408,63 @@ class MimeNode {
|
||||
_normalizeAddress(address) {
|
||||
address = (address || '')
|
||||
.toString()
|
||||
.replace(/[\x00-\x1F<>]+/g, ' ') // remove unallowed characters
|
||||
.replace(/[\x00-\x1F\x7F<>]+/g, ' ') // remove unallowed characters
|
||||
.trim();
|
||||
|
||||
const lastAt = address.lastIndexOf('@');
|
||||
if (lastAt < 0) {
|
||||
// Bare username
|
||||
if (!address) {
|
||||
// callers use an empty value to detect a missing address
|
||||
return address;
|
||||
}
|
||||
|
||||
let user = address.substr(0, lastAt);
|
||||
const lastAt = address.lastIndexOf('@');
|
||||
if (lastAt < 0) {
|
||||
// Bare username, there is no domain to split off
|
||||
return this._normalizeLocalPart(address);
|
||||
}
|
||||
|
||||
const user = address.substr(0, lastAt);
|
||||
const domain = address.substr(lastAt + 1);
|
||||
|
||||
// Usernames are not touched and are kept as is even if these include unicode.
|
||||
// Unicode in the local part is kept as is, see _normalizeLocalPart for the rest of it.
|
||||
// A domain has no quoting construct to fall back on, so whatever is not a valid domain
|
||||
// is kept as supplied and it is _convertAddresses that keeps such an address unambiguous.
|
||||
// Domains are punycoded when the local part is ASCII ('safe@jõgeva.ee' -> 'safe@xn--jgeva-dua.ee').
|
||||
// When the local part contains non-ASCII bytes the address already requires SMTPUTF8,
|
||||
// so the domain is kept (or decoded back) as UTF-8 for symmetry on both sides of '@'.
|
||||
|
||||
let encodedDomain = domain;
|
||||
|
||||
// A non-ASCII local part already requires SMTPUTF8, so the domain stays UTF-8 for
|
||||
// symmetry on both sides of the '@' rather than being encoded to an A-label
|
||||
const smtputf8 = /[\x80-\uFFFF]/.test(user);
|
||||
|
||||
try {
|
||||
if (/[\x80-\uFFFF]/.test(user)) {
|
||||
encodedDomain = punycode.toUnicode(domain.toLowerCase());
|
||||
} else {
|
||||
encodedDomain = punycode.toASCII(domain.toLowerCase());
|
||||
}
|
||||
encodedDomain = normalizeDomain(domain.toLowerCase(), smtputf8);
|
||||
} catch (_err) {
|
||||
// keep domain as supplied
|
||||
}
|
||||
|
||||
if (user.indexOf(' ') >= 0) {
|
||||
if (user.charAt(0) !== '"') {
|
||||
user = '"' + user;
|
||||
}
|
||||
if (user.substr(-1) !== '"') {
|
||||
user = user + '"';
|
||||
}
|
||||
return `${this._normalizeLocalPart(user)}@${encodedDomain}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the local part of an address into a form that can be emitted as is.
|
||||
*
|
||||
* A local part is either a dot-atom or a quoted-string, anything else is not a valid
|
||||
* addr-spec. The quotes of a quoted local part get lost along the way, and a bare
|
||||
* 'user@evil.com@good.com' leaves it to the receiver which '@' splits the domain off,
|
||||
* while the split here is always at the last one. So whatever is not already one of
|
||||
* the two valid forms goes back out as a quoted-string.
|
||||
*
|
||||
* @param {String} user Local part of an address
|
||||
* @return {String} Local part as a dot-atom or as a quoted-string
|
||||
*/
|
||||
_normalizeLocalPart(user) {
|
||||
if (DOT_ATOM.test(user) || QUOTED_STRING.test(user)) {
|
||||
return user;
|
||||
}
|
||||
|
||||
return `${user}@${encodedDomain}`;
|
||||
return mimeFuncs.quoteString(user);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1263,7 +1476,7 @@ class MimeNode {
|
||||
_encodeAddressName(name) {
|
||||
if (!/^[\w ]*$/.test(name)) {
|
||||
if (/^[\x20-\x7e]*$/.test(name)) {
|
||||
return '"' + name.replace(/([\\"])/g, '\\$1') + '"';
|
||||
return mimeFuncs.quoteString(name);
|
||||
} else {
|
||||
return mimeFuncs.encodeWord(name, this._getTextEncoding(name), 52);
|
||||
}
|
||||
@@ -1271,6 +1484,21 @@ class MimeNode {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes an unstructured header value. Such a value can only carry VCHAR and WSP, so a
|
||||
* control char or DEL has to be forced into the mime encoded word that a non-ascii value
|
||||
* would get anyway. HT stays as it is, it is valid folding whitespace here.
|
||||
*
|
||||
* @param {String} value Header value to encode
|
||||
* @returns {String} Mime word encoded string if needed
|
||||
*/
|
||||
_encodeHeaderText(value) {
|
||||
return /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(value)
|
||||
? mimeFuncs.encodeWord(value, this._getTextEncoding(value), 52)
|
||||
: // encodeWords only encodes if needed, otherwise the original string is returned
|
||||
this._encodeWords(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* If needed, mime encodes the name part
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user