Files
send-mail/node_modules/nodemailer/dist/esm/mime-node/index.js
T
2026-10-18 15:39:30 +02:00

1589 lines
66 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* eslint no-undefined: 0, prefer-spread: 0, no-control-regex: 0 */
import crypto from 'node:crypto';
import fs from 'node:fs';
import * as punycode from '../punycode/index.js';
import { PassThrough, finished, pipeline } from 'node:stream';
import * as shared from '../shared/index.js';
import urlModule from 'node:url';
import * as mimeFuncs from '../mime-funcs/index.js';
import * as qp from '../qp/index.js';
import * as base64 from '../base64/index.js';
import addressparser from '../addressparser/index.js';
import nmfetch from '../fetch/index.js';
import * as errors from '../errors.js';
import LastNewline from './last-newline.js';
import LeWindows from './le-windows.js';
import LeUnix from './le-unix.js';
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"(),:;<>@[\\\]]+$/;
/**
* A domain literal that ends an address, see _normalizeAddress
*/
const DOMAIN_LITERAL_TAIL = /@(\[[^[\]\\]*\])$/;
/**
* Removes the whitespace inside every <...> of a References value. A "<" with no ">" after
* it is left as it is. An index scan, the regex /<[^>]*>/g rescans the rest of the value
* from every such "<"
*
* @param value References header value
* @returns The value with the msg-ids closed up
*/
function stripWhitespaceInAngleBrackets(value) {
let result = '';
let pos = 0;
for (;;) {
const open = value.indexOf('<', pos);
if (open < 0) {
break;
}
const close = value.indexOf('>', open + 1);
if (close < 0) {
break;
}
result += value.slice(pos, open) + value.slice(open, close + 1).replace(/\s/g, '');
pos = close + 1;
}
return result + value.slice(pos);
}
// 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]/;
// RFC 1035 bounds a label at 63 octets and a domain at 253. An A-label is never shorter than
// the code points it encodes, so anything past these bounds could not have become a valid
// domain whichever way it is encoded. The bundled codec is super-linear in the length of a
// label, a 60 KB label of distinct code points took seconds to encode, several times per
// address, so it is only handed what could be a domain
const MAX_DOMAIN_LENGTH = 253;
const MAX_LABEL_LENGTH = 63;
// pipeline() needs a callback to not throw, the errors it sees reach the last stream anyway
const PIPELINE_NOOP = () => false;
// The error a node streaming into an output that was destroyed stops with
function abortedError() {
const err = new Error('Message stream was closed before the message was generated');
err.code = errors.ESTREAM;
return err;
}
/**
* Checks a domain against the DNS length limits, counted in code points per label and in
* total. The label separators are the ones the bundled codec splits on.
*
* @param domain Domain to check
* @return true if a label or the domain as a whole is too long to be a valid domain
*/
function exceedsDomainLimits(domain) {
let total = 0;
let label = 0;
for (const char of domain) {
total++;
if (char === '.' || char === '。' || char === '.' || char === '。') {
label = 0;
}
else if (++label > MAX_LABEL_LENGTH) {
return true;
}
}
return total > MAX_DOMAIN_LENGTH;
}
/**
* 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 domain Domain to encode, already lowercased by the caller
* @param toUnicode Return the U-label form instead of the A-label form
* @return Encoded domain
*/
function normalizeDomain(domain, toUnicode) {
// domainToASCII and domainToUnicode landed in Node 7, the bundled codec covers Node 6
if (typeof urlModule.domainToASCII === 'function' &&
typeof urlModule.domainToUnicode === 'function' &&
!URL_PARSER_UNSAFE.test(domain)) {
// The U-label form is decoded from the A-label form rather than from the input:
// Deno's domainToASCII returns an empty string for a label it cannot decode, while
// its domainToUnicode returns the label with U+FFFD in place of the bad part
const ascii = urlModule.domainToASCII(domain);
const mapped = ascii && toUnicode ? urlModule.domainToUnicode(ascii) : ascii;
if (mapped) {
return mapped;
}
}
if (exceedsDomainLimits(domain)) {
// too long to be a domain whichever way it is encoded, keep it as supplied
return domain;
}
return toUnicode ? punycode.toUnicode(domain) : punycode.toASCII(domain);
}
/**
* Removes the characters that must never reach a multipart delimiter line.
*
* A line break splits the delimiter, so the boundary declared in the header can never
* match it again and the parts go out as body lines instead. The other C0 controls and
* DEL do not split anything but must not be written either: RFC 5321 does not allow a
* NUL in DATA at all, and an MTA or a scanner that stops at one reads a different
* message than a client that does not, which is the same parser disagreement a split
* delimiter creates. Both sides are cleaned together, since the declared value and the
* delimiters come from this one result.
*
* @param value Value to clean
* @return Value with every control character removed
*/
function _stripBoundaryControls(value) {
return value.replace(/[\x00-\x1f\x7f]+/g, '');
}
/**
* 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
* the options, assumes this is the root.
*
* @param contentType Define the content type for the node. Can be left blank for attachments (derived from filename)
* @param [options] optional options
* @param [options.rootNode] root node for this tree
* @param [options.parentNode] immediate parent for this node
* @param [options.filename] filename for an attachment node
* @param [options.baseBoundary] shared part of the unique multipart boundary
* @param [options.keepBcc] If true, do not exclude Bcc from the generated headers
* @param [options.normalizeHeaderKey] method to normalize header keys for custom caseing
* @param [options.textEncoding] either 'Q' (the default) or 'B'
*/
class MimeNode {
constructor(contentType, options) {
this.nodeCounter = 0;
options = options || {};
/**
* shared part of the unique multipart boundary. Control characters are dropped
* here rather than at the delimiter, see _stripBoundaryControls
*/
this.baseBoundary = _stripBoundaryControls(options.baseBoundary || crypto.randomBytes(8).toString('hex'));
this.boundaryPrefix = _stripBoundaryControls(options.boundaryPrefix || '--_NmP');
this.disableFileAccess = !!options.disableFileAccess;
this.disableUrlAccess = !!options.disableUrlAccess;
this.normalizeHeaderKey = options.normalizeHeaderKey;
/**
* If date headers is missing and current node is the root, this value is used instead
*/
this.date = options.parentNode ? null : new Date();
/**
* Root node for current mime tree
*/
this.rootNode = options.rootNode || this;
/**
* If true include Bcc in generated headers (if available)
*/
this.keepBcc = !!options.keepBcc;
/**
* If filename is specified but contentType is not (probably an attachment)
* detect the content type from filename extension
*/
if (options.filename) {
/**
* Filename for this node. Useful with attachments
*/
this.filename = options.filename;
if (!contentType) {
contentType = mimeFuncs.detectMimeType(this.filename.split('.').pop());
}
}
/**
* Indicates which encoding should be used for header strings: "Q" or "B"
*/
this.textEncoding = (options.textEncoding || '').toString().trim().charAt(0).toUpperCase();
/**
* Immediate parent for this node (or undefined if not set)
*/
this.parentNode = options.parentNode;
/**
* Hostname for default message-id values
*/
this.hostname = options.hostname;
/**
* 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.
*/
this.newline = options.newline;
/**
* An array for possible child nodes
*/
this.childNodes = [];
/**
* Used for generating unique boundaries (prepended to the shared base)
*/
this._nodeId = ++this.rootNode.nodeCounter;
/**
* A list of header values for this node in the form of [{key:'', value:''}]
*/
this._headers = [];
/**
* True if the content only uses ASCII printable characters
* @type {Boolean}
*/
this._isPlainText = false;
/**
* True if the content is plain text but has longer lines than allowed
* @type {Boolean}
*/
this._hasLongLines = false;
/**
* If set, use instead this value for envelopes instead of generating one
* @type {Boolean}
*/
this._envelope = false;
/**
* If set then use this value as the stream content instead of building it
* @type {String|Buffer|Stream}
*/
this._raw = false;
/**
* Additional transform streams that the message will be piped before
* exposing by createReadStream
* @type {Array}
*/
this._transforms = [];
/**
* Additional process functions that the message will be piped through before
* exposing by createReadStream. These functions are run after transforms
* @type {Array}
*/
this._processFuncs = [];
/**
* If content type is set (or derived from the filename) add it to headers
*/
if (contentType) {
this.setHeader('Content-Type', contentType);
}
}
/////// PUBLIC METHODS
/**
* Creates and appends a child node.Arguments provided are passed to MimeNode constructor
*
* @param [contentType] Optional content type
* @param [options] Optional options object
* @return Created node object
*/
createChild(contentType, options) {
if (!options && typeof contentType === 'object') {
options = contentType;
contentType = undefined;
}
const node = new MimeNode(contentType, options);
this.appendChild(node);
return node;
}
/**
* Appends an existing node to the mime tree. Removes the node from an existing
* tree if needed
*
* @param childNode node to be appended
* @return 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;
}
childNode.parentNode = this;
this.childNodes.push(childNode);
return childNode;
}
/**
* Replaces current node with another node
*
* @param node Replacement node
* @return Replacement node
*/
replace(node) {
if (node === this) {
return this;
}
this.parentNode.childNodes.forEach((childNode, i) => {
if (childNode === this) {
node.rootNode = this.rootNode;
node.parentNode = this.parentNode;
node._nodeId = this._nodeId;
this.rootNode = this;
this.parentNode = undefined;
node.parentNode.childNodes[i] = node;
}
});
return node;
}
/**
* Removes current node from the mime tree
*
* @return removed node
*/
remove() {
if (!this.parentNode) {
return this;
}
for (let i = this.parentNode.childNodes.length - 1; i >= 0; i--) {
if (this.parentNode.childNodes[i] === this) {
this.parentNode.childNodes.splice(i, 1);
this.parentNode = undefined;
this.rootNode = this;
return this;
}
}
}
/**
* Sets a header value. If the value for selected key exists, it is overwritten.
* You can set multiple values as well by using [{key:'', value:''}] or
* {key: 'value'} as the first argument.
*
* @param key Header key or a list of key value pairs
* @param value Header value
* @return current node
*/
setHeader(key, value) {
let added = false;
// Allow setting multiple headers at once
if (!value && key && typeof key === 'object') {
// allow {key:'content-type', value: 'text/plain'}
if (key.key && 'value' in key) {
this.setHeader(key.key, key.value);
}
else if (Array.isArray(key)) {
// allow [{key:'content-type', value: 'text/plain'}]
key.forEach(i => {
this.setHeader(i.key, i.value);
});
}
else {
// allow {'content-type': 'text/plain'}
Object.keys(key).forEach(i => {
this.setHeader(i, key[i]);
});
}
return this;
}
key = this._normalizeHeaderKey(key);
const headerValue = {
key,
value
};
// Check if the value exists and overwrite
for (let i = 0, len = this._headers.length; i < len; i++) {
if (this._headers[i].key === key) {
if (!added) {
// replace the first match
this._headers[i] = headerValue;
added = true;
}
else {
// remove following matches
this._headers.splice(i, 1);
i--;
len--;
}
}
}
// match not found, append the value
if (!added) {
this._headers.push(headerValue);
}
return this;
}
/**
* Adds a header value. If the value for selected key exists, the value is appended
* as a new field and old one is not touched.
* You can set multiple values as well by using [{key:'', value:''}] or
* {key: 'value'} as the first argument.
*
* @param key Header key or a list of key value pairs
* @param value Header value
* @return current node
*/
addHeader(key, value) {
// Allow setting multiple headers at once
if (!value && key && typeof key === 'object') {
// allow {key:'content-type', value: 'text/plain'}
if (key.key && key.value) {
this.addHeader(key.key, key.value);
}
else if (Array.isArray(key)) {
// allow [{key:'content-type', value: 'text/plain'}]
key.forEach(i => {
this.addHeader(i.key, i.value);
});
}
else {
// allow {'content-type': 'text/plain'}
Object.keys(key).forEach(i => {
this.addHeader(i, key[i]);
});
}
return this;
}
else if (Array.isArray(value)) {
value.forEach(val => {
this.addHeader(key, val);
});
return this;
}
this._headers.push({
key: this._normalizeHeaderKey(key),
value
});
return this;
}
/**
* Retrieves the first mathcing value of a selected key
*
* @param key Key to search for
* @retun Value for the key
*/
getHeader(key) {
key = this._normalizeHeaderKey(key);
for (let i = 0, len = this._headers.length; i < len; i++) {
if (this._headers[i].key === key) {
return this._headers[i].value;
}
}
}
/**
* Sets body content for current node. If the value is a string, charset is added automatically
* to Content-Type (if it is text/*). If the value is a Buffer, you need to specify
* the charset yourself
*
* @param content Body content
* @return current node
*/
setContent(content) {
this.content = content;
if (typeof this.content.pipe === 'function') {
// pre-stream handler. might be triggered if a stream is set as content
// and 'error' fires before anything is done with this stream
shared.recordStreamErrors(this.content);
}
else if (typeof this.content === 'string') {
this._isPlainText = mimeFuncs.isPlainText(this.content);
if (this._isPlainText && mimeFuncs.hasLongerLines(this.content, 76)) {
// If there are lines longer than 76 symbols/bytes do not use 7bit
this._hasLongLines = true;
}
}
return this;
}
build(callback) {
let promise;
if (!callback) {
promise = new Promise((resolve, reject) => {
callback = shared.callbackPromise(resolve, reject);
});
}
const done = callback;
const stream = this.createReadStream();
const buf = [];
let buflen = 0;
let returned = false;
stream.on('readable', () => {
let chunk;
while ((chunk = stream.read()) !== null) {
buf.push(chunk);
buflen += chunk.length;
}
});
stream.once('error', err => {
if (returned) {
return;
}
returned = true;
return done(err);
});
stream.once('end', (chunk) => {
if (returned) {
return;
}
returned = true;
if (chunk && chunk.length) {
buf.push(chunk);
buflen += chunk.length;
}
return done(null, Buffer.concat(buf, buflen));
});
return promise;
}
getTransferEncoding() {
let transferEncoding = false;
const contentType = (this.getHeader('Content-Type') || '').toString().toLowerCase().trim();
if (this.content) {
transferEncoding = (this.getHeader('Content-Transfer-Encoding') || '').toString().toLowerCase().trim();
if (!transferEncoding || !['base64', 'quoted-printable'].includes(transferEncoding)) {
if (/^text\//i.test(contentType)) {
// If there are no special symbols, no need to modify the text
if (this._isPlainText && !this._hasLongLines) {
transferEncoding = '7bit';
}
else if (typeof this.content === 'string' || this.content instanceof Buffer) {
// detect preferred encoding for string value
transferEncoding = this._getTextEncoding(this.content) === 'Q' ? 'quoted-printable' : 'base64';
}
else {
// we can not check content for a stream, so either use preferred encoding or fallback to QP
transferEncoding = this.textEncoding === 'B' ? 'base64' : 'quoted-printable';
}
}
else if (!/^(multipart|message)\//i.test(contentType)) {
transferEncoding = transferEncoding || 'base64';
}
}
}
return transferEncoding;
}
/**
* Builds the header block for the mime node. Append \r\n\r\n before writing the content
*
* @returns Headers
*/
buildHeaders() {
const transferEncoding = this.getTransferEncoding();
const headers = [];
if (transferEncoding) {
this.setHeader('Content-Transfer-Encoding', transferEncoding);
}
if (this.filename && !this.getHeader('Content-Disposition')) {
this.setHeader('Content-Disposition', 'attachment');
}
// Ensure mandatory header fields
if (this.rootNode === this) {
if (!this.getHeader('Date')) {
this.setHeader('Date', this.date.toUTCString().replace(/GMT/, '+0000'));
}
// ensure that Message-Id is present
this.messageId();
if (!this.getHeader('MIME-Version')) {
this.setHeader('MIME-Version', '1.0');
}
// Ensure that Content-Type is the last header for the root node
for (let i = this._headers.length - 2; i >= 0; i--) {
const header = this._headers[i];
if (header.key === 'Content-Type') {
this._headers.splice(i, 1);
this._headers.push(header);
}
}
}
this._headers.forEach(header => {
let key = header.key;
let value = header.value;
let structured;
let param;
const options = {};
const formattedHeaders = FORMATTED_HEADERS;
if (value && typeof value === 'object' && !formattedHeaders.includes(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;
}
}
if (options.prepared) {
// header value is
if (options.foldLines) {
headers.push(mimeFuncs.foldLines(key + ': ' + value));
}
else {
headers.push(key + ': ' + value);
}
return;
}
switch (header.key) {
case 'Content-Disposition':
structured = mimeFuncs.parseHeaderValue(value);
if (this.filename) {
structured.params.filename = this.filename;
}
value = mimeFuncs.buildHeaderValue(structured);
break;
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 (structured.value.match(/^text\/plain\b/) &&
typeof this.content === 'string' &&
/[\u0080-\uFFFF]/.test(this.content)) {
structured.params.charset = 'utf-8';
}
if (this.filename) {
// the name parameter is derived from the filename below, so a name that
// arrived inside the content type is dropped first (a plain parameter or
// rfc2231 continuation sections), otherwise the header carries two and a
// receiver that reads the first sees a name the filename does not match
for (const paramKey of Object.keys(structured.params)) {
if (/^name(\*|$)/i.test(paramKey)) {
delete structured.params[paramKey];
}
}
}
value = mimeFuncs.buildHeaderValue(structured);
if (this.filename) {
// 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
// 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, 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;
}
break;
case 'Bcc':
if (!this.keepBcc) {
// skip BCC values
return;
}
break;
}
value = this._encodeHeaderValue(key, value);
// skip empty lines
if (!(value || '').toString().trim()) {
return;
}
if (typeof this.normalizeHeaderKey === 'function') {
const normalized = this.normalizeHeaderKey(key, value);
// 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;
}
}
headers.push(mimeFuncs.foldLines(key + ': ' + value, 76));
});
return headers.join('\r\n');
}
/**
* Streams the rfc2822 message from the current node. If this is a root node,
* mandatory header fields are set if missing (Date, Message-Id, MIME-Version)
*
* @return Compiled message
*/
createReadStream(options) {
options = options || {};
const stream = new PassThrough(options);
this.stream(stream, options, err => {
if (err) {
stream.destroy(err);
return;
}
stream.end();
});
// the content streams of the nodes the message did not get to are not going to be read
stream.once('close', () => {
if (!stream.writableFinished) {
this._destroyContentStreams();
}
});
// The stages are joined with pipeline, which destroys all of them when any one fails
// or is destroyed. An error anywhere reaches the returned stream exactly once, and a
// consumer that destroys the returned stream stops the tree from reading its sources
const stages = [stream];
for (const transform of this._transforms) {
stages.push(typeof transform === 'function' ? transform() : transform);
}
// ensure terminating newline after possible user transforms
stages.push(new LastNewline());
let outputStream = pipeline(stages, PIPELINE_NOOP);
// dkim and stuff. A process function reads its input itself and reports the errors of
// that input on its output, so only a consumer abort has to be carried back upstream
for (const processFunc of this._processFuncs) {
const input = outputStream;
outputStream = processFunc(input);
if (outputStream !== input) {
finished(outputStream, err => {
if (err) {
input.destroy();
}
});
}
}
if (this.newline) {
const winbreak = ['win', 'windows', 'dos', '\r\n'].includes(this.newline.toString().toLowerCase());
outputStream = pipeline(outputStream, winbreak ? new LeWindows() : new LeUnix(), PIPELINE_NOOP);
}
return outputStream;
}
/**
* Appends a transform stream object to the transforms list. Final output
* is passed through this stream before exposing
*
* @param transform Read-Write stream
*/
transform(transform) {
this._transforms.push(transform);
}
/**
* Appends a post process function. The functon is run after transforms and
* uses the following syntax
*
* processFunc(input) -> outputStream
*
* @param processFunc Read-Write stream
*/
processFunc(processFunc) {
this._processFuncs.push(processFunc);
}
stream(outputStream, options, done) {
const transferEncoding = this.getTransferEncoding();
// the streams this node is reading from. A consumer that goes away mid-message destroys
// the output, and these are released with it instead of staying paused with a file or a
// socket open behind them
let activeStreams = [];
// protect actual callback against multiple triggering
let returned = false;
const callback = (err) => {
if (returned) {
return;
}
returned = true;
outputStream.removeListener('close', onOutputClose);
done(err);
};
function onOutputClose() {
for (const stream of activeStreams) {
stream.destroy();
}
return callback(abortedError());
}
// reads the streams into the output. The listener is only attached while the node reads
// something, a deeply nested tree would otherwise stack one per level on the output
const readInto = (...streams) => {
if (!activeStreams.length) {
outputStream.once('close', onOutputClose);
}
activeStreams = streams;
};
// stops here if the output was destroyed in the meantime
const aborted = () => {
if (outputStream.destroyed) {
callback(abortedError());
return true;
}
return false;
};
// for multipart nodes, push child nodes
// for content nodes end the stream
const finalize = () => {
let childId = 0;
const processChildNode = () => {
if (aborted()) {
return;
}
if (childId >= this.childNodes.length) {
outputStream.write('\r\n--' + this.boundary + '--\r\n');
return callback();
}
const child = this.childNodes[childId++];
outputStream.write((childId > 1 ? '\r\n' : '') + '--' + this.boundary + '\r\n');
child.stream(outputStream, options, err => {
if (err) {
return callback(err);
}
setImmediate(processChildNode);
});
};
if (this.multipart) {
setImmediate(processChildNode);
}
else {
return callback();
}
};
// pushes node content
const sendContent = () => {
if (aborted()) {
return;
}
if (!this.content) {
return setImmediate(finalize);
}
const createStream = () => {
if (aborted()) {
return;
}
const contentError = this._takeStreamContent(this.content);
if (contentError) {
return callback(contentError);
}
const localStream = this._getStream(this.content);
if (['quoted-printable', 'base64'].includes(transferEncoding)) {
const contentStream = transferEncoding === 'base64'
? new base64.Encoder(options)
: // outside of text a lone CR or LF is data, encoding it keeps it from being
// turned into a line break by a newline transform or the receiving side
new qp.Encoder(Object.assign({}, options, { binary: !/^text\//i.test(this.contentType || '') }));
readInto(localStream, contentStream);
contentStream.pipe(outputStream, {
end: false
});
// pipeline reports errors and tears both streams down. It calls back once the
// encoder took all of its input, the encoded output may still be waiting to
// be read, so the node is only done once the encoder's readable side ended
pipeline(localStream, contentStream, err => err && callback(err));
finished(contentStream, { writable: false }, err => (err ? callback(err) : finalize()));
}
else {
// anything that is not QP or Base54 passes as-is
readInto(localStream);
localStream.pipe(outputStream, {
end: false
});
finished(localStream, { writable: false }, err => (err ? callback(err) : finalize()));
}
};
if (this.content._resolve) {
const sourceStream = this._getStream(this.content);
readInto(sourceStream);
shared.resolveStream(sourceStream, (err, value) => {
if (err) {
return callback(err);
}
if (returned) {
return;
}
this.content._resolve = false;
this.content._resolvedValue = value;
setImmediate(createStream);
});
}
else {
setImmediate(createStream);
}
};
if (this._raw) {
setImmediate(() => {
if (aborted()) {
return;
}
const rawError = this._takeStreamContent(this._raw);
if (rawError) {
return callback(rawError);
}
const raw = this._getStream(this._raw);
readInto(raw);
raw.pipe(outputStream, {
end: false
});
finished(raw, { writable: false }, err => (err ? callback(err) : finalize()));
});
}
else {
outputStream.write(this.buildHeaders() + '\r\n\r\n');
setImmediate(sendContent);
}
}
/**
* Sets envelope to be used instead of the generated one
*
* @return SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
*/
setEnvelope(envelope) {
let list;
this._envelope = {
from: false,
to: []
};
if (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();
const recipients = [];
['to', 'cc', 'bcc'].forEach(key => {
if (envelope[key]) {
this._convertAddresses(this._parseEnvelopeAddresses(envelope[key]), recipients, seenRecipients);
}
});
this._envelope.to = recipients.map(to => to.address).filter(address => address);
const standardFields = ['to', 'cc', 'bcc', 'from'];
shared.copyOwnKeys(this._envelope, envelope, key => standardFields.includes(key));
return this;
}
/**
* Generates and returns an object with parsed address fields
*
* @return Address object
*/
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], seenByKey.get(key));
}
});
return addresses;
}
/**
* Generates and returns SMTP envelope with the sender address and a list of recipients addresses
*
* @return SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
*/
getEnvelope() {
if (this._envelope) {
return this._envelope;
}
const envelope = {
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();
const recipients = [];
this._headers.forEach(header => {
const list = [];
if (header.key === 'From' || (!envelope.from && ['Reply-To', 'Sender'].includes(header.key))) {
this._convertAddresses(this._parseAddresses(header.value), list);
if (list.length && list[0]) {
envelope.from = list[0].address;
}
}
else if (['To', 'Cc', 'Bcc'].includes(header.key)) {
this._convertAddresses(this._parseAddresses(header.value), recipients, seenRecipients);
}
});
envelope.to = recipients.map(to => to.address);
return envelope;
}
/**
* Returns Message-Id value. If it does not exist, then creates one
*
* @return Message-Id value
*/
messageId() {
let messageId = this.getHeader('Message-ID');
// You really should define your own Message-Id field!
if (!messageId) {
messageId = this._generateMessageId();
this.setHeader('Message-ID', messageId);
}
return messageId;
}
/**
* Sets pregenerated content that will be used as the output of this node
*
* @param raw Raw MIME contents
*/
setRaw(raw) {
this._raw = raw;
if (this._raw && typeof this._raw.pipe === 'function') {
// pre-stream handler. might be triggered if a stream is set as content
// and 'error' fires before anything is done with this stream
shared.recordStreamErrors(this._raw);
}
return this;
}
/////// 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 flag Either 'disableFileAccess' or 'disableUrlAccess'
* @return true if this node or an ancestor closed that access
* @internal
*/
_accessDisabled(flag) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
let node = this;
while (node) {
if (node[flag]) {
return true;
}
node = node.parentNode;
}
return false;
}
/**
* Destroys the content streams of this node and of every node below it
*
* @internal
*/
_destroyContentStreams() {
shared.destroyStream(this.content);
shared.destroyStream(this._raw);
for (const child of this.childNodes) {
child._destroyContentStreams();
}
}
/**
* Checks that a content value can still be read before the node streams it. A stream is
* refused once it errored, ended or was destroyed: piping it would either never finish or
* produce an empty body without any error
*
* @param content Node content or raw value
* @returns The error to stop with, or null when the content can be read
* @internal
*/
_takeStreamContent(content) {
if (Object.prototype.toString.call(content) === '[object Error]') {
return content;
}
if (!content || typeof content.pipe !== 'function' || content._resolvedValue) {
// the value of a resolved stream is read from the buffered copy
return null;
}
return shared.unreadableStreamError(content);
}
/**
* Detects and returns handle to a stream related with the content.
*
* @param content Node content
* @returns Stream object
* @internal
*/
_getStream(content) {
let contentStream;
if (content._resolvedValue) {
// pass string or buffer content as a stream
contentStream = new PassThrough();
setImmediate(() => {
try {
contentStream.end(content._resolvedValue);
}
catch (_err) {
contentStream.emit('error', _err);
}
});
return contentStream;
}
if (typeof content.pipe === 'function') {
// assume as stream
return content;
}
if (content && typeof content.path === 'string' && !content.href) {
if (this._accessDisabled('disableFileAccess')) {
contentStream = new PassThrough();
setImmediate(() => {
const err = new Error('File access rejected for ' + content.path);
err.code = errors.EFILEACCESS;
contentStream.emit('error', err);
});
return contentStream;
}
// read file
return fs.createReadStream(content.path);
}
if (content && typeof content.href === 'string') {
if (this._accessDisabled('disableUrlAccess')) {
contentStream = new PassThrough();
setImmediate(() => {
const err = new Error('Url access rejected for ' + content.href);
err.code = errors.EURLACCESS;
contentStream.emit('error', err);
});
return contentStream;
}
// 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 });
}
// pass string or buffer content as a stream
contentStream = new PassThrough();
setImmediate(() => {
try {
contentStream.end(content || '');
}
catch (_err) {
contentStream.emit('error', _err);
}
});
return contentStream;
}
/**
* Parses addresses. Takes in a single address or an array or an
* array of address arrays (eg. To: [[first group], [second group],...])
*
* @param addresses Addresses to be parsed
* @return An array of address objects
* @internal
*/
_parseAddresses(addresses) {
const flattened = [];
// Nested arrays are flattened iteratively. concat.apply throws once a recipient list
// is long enough to pass the argument limit, and an array handed to addressparser is
// stringified, which recurses once per nesting level and exhausts the call stack on
// a deep enough value. Every array is walked once, which also ends a self-referential
// input
const seen = new WeakSet();
const stack = [];
const enter = (list) => {
if (!seen.has(list)) {
seen.add(list);
stack.push({ list, pos: 0 });
}
};
enter(Array.isArray(addresses) ? addresses : [addresses]);
while (stack.length) {
const frame = stack[stack.length - 1];
if (frame.pos >= frame.list.length) {
stack.pop();
continue;
}
const address = frame.list[frame.pos++];
if (Array.isArray(address)) {
enter(address);
continue;
}
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);
continue;
}
// 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);
continue;
}
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 parsed An array of address objects, as returned by addressparser
* @return The same array, with every address normalized
* @internal
*/
_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 addresses Addresses to be parsed
* @return An array of address objects
* @internal
*/
_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: '' };
});
}
/**
* Normalizes a header key, uses Camel-Case form, except for uppercase MIME-
*
* @param key Key to be normalized
* @return key in Camel-Case form
* @internal
*/
_normalizeHeaderKey(key) {
key = (key || '')
.toString()
// a field name is printable ascii without the colon, so a control char, DEL,
// whitespace or a colon can only be dropped, there is no quoting construct
// around a field name. A colon or a space left in place ends the name early on
// the wire: the key 'From: ceo@example.com (' with the value ')' was emitted as a
// second From header that neither setHeader() nor the Bcc filter could match
.replace(/[\s\x00-\x20\x7f:]/g, '')
.toLowerCase()
// use uppercase words, except MIME
.replace(/^X-SMTPAPI$|^(MIME|DKIM|ARC|BIMI)\b|^[a-z]|-(SPF|FBL|ID|MD5)$|-[a-z]/gi, c => c.toUpperCase())
// special case
.replace(/^Content-Features$/i, 'Content-features');
return key;
}
/**
* Checks if the content type is multipart and defines boundary if needed.
* Doesn't return anything, modifies object argument instead.
*
* @param structured Parsed header value for 'Content-Type' key
* @internal
*/
_handleContentType(structured) {
this.contentType = structured.value.trim().toLowerCase();
this.multipart = /^multipart\//i.test(this.contentType) ? this.contentType.substr(this.contentType.indexOf('/') + 1) : false;
if (this.multipart) {
// The declared value and the delimiters are assigned from the same expression
// here, so whatever the header emitter then does with it, the two sides cannot
// disagree about which characters the boundary is made of.
//
// Stripping runs before the fallback rather than over the whole chain: a
// boundary made only of control characters would otherwise strip to '' and
// leave the node declaring no boundary and streaming bare '--' delimiters.
// _generateBoundary cleans what it builds, but this is the one place the boundary
// is written, so it cleans the value it is about to write rather than trusting
// where it came from. MimeNode is exported and subclassable, so the generator is
// not necessarily the one below. Stripping twice costs nothing, it is idempotent.
const declared = _stripBoundaryControls(structured.params.boundary || this.boundary || '');
this.boundary = structured.params.boundary =
declared || _stripBoundaryControls(this._generateBoundary());
}
else {
this.boundary = false;
}
}
/**
* Generates a multipart boundary value
*
* @return boundary value
* @internal
*/
_generateBoundary() {
// baseBoundary and boundaryPrefix are public, so they may hold something the
// constructor never cleaned, and the -Part_ suffix keeps the result non empty
return _stripBoundaryControls(this.rootNode.boundaryPrefix + '-' + this.rootNode.baseBoundary) + '-Part_' + this._nodeId;
}
/**
* Encodes a header value for use in the generated rfc2822 email.
*
* @param key Header key
* @param value Header value
* @internal
*/
_encodeHeaderValue(key, value) {
key = this._normalizeHeaderKey(key);
switch (key) {
// Structured headers
case 'From':
case 'Sender':
case 'To':
case 'Cc':
case 'Bcc':
case 'Reply-To':
return this._convertAddresses(this._parseAddresses(value));
// values enclosed in <>
case 'Message-ID':
case 'In-Reply-To':
case 'Content-Id':
// 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;
}
if (value.charAt(value.length - 1) !== '>') {
value = value + '>';
}
return value;
// space separated list of values enclosed in <>
case 'References': {
const ids = [];
for (const entry of [].concat(value || '')) {
const elm = (entry || '')
.toString()
.replace(/\r?\n|\r/g, ' ')
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
.trim();
for (let id of stripWhitespaceInAngleBrackets(elm).split(/\s+/)) {
if (id.charAt(0) !== '<') {
id = '<' + id;
}
if (id.charAt(id.length - 1) !== '>') {
id = id + '>';
}
ids.push(id);
}
}
return ids.join(' ').trim();
}
case 'Date':
if (Object.prototype.toString.call(value) === '[object Date]') {
return value.toUTCString().replace(/GMT/, '+0000');
}
value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
return this._encodeHeaderText(value);
case 'Content-Type':
case 'Content-Disposition':
// if it includes a filename then it is already encoded
return (value || '').toString().replace(/\r?\n|\r/g, ' ');
default:
value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
return this._encodeHeaderText(value);
}
}
/**
* Rebuilds address object using punycode and other adjustments
*
* @param addresses An array of address objects
* @param [uniqueList] An array to be populated with addresses
* @param [seenAddresses] Addresses already added to uniqueList, shared with recursive calls to keep deduplication linear
* @return address string
* @internal
*/
_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) {
// 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 (!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, seenAddresses) : '').trim();
values.push(`${this._encodeAddressName(address.name)}:${groupListAddresses};`);
}
});
return values.join(', ');
}
/**
* Normalizes an email address
*
* @param address An array of address objects
* @return address string
* @internal
*/
_normalizeAddress(address) {
address = (address || '')
.toString()
.replace(/[\x00-\x1F\x7F<>]+/g, ' ') // remove unallowed characters
.trim();
if (!address) {
// callers use an empty value to detect a missing address
return address;
}
// dtext includes '@', so a domain literal is split off at the '@' in front of it and
// not at the last one, which delivered 'a@[x@evil.com]' to evil.com
const literal = address.charAt(address.length - 1) === ']' ? DOMAIN_LITERAL_TAIL.exec(address) : null;
const lastAt = literal ? address.length - literal[1].length - 1 : 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);
// 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 {
encodedDomain = normalizeDomain(domain.toLowerCase(), smtputf8);
}
catch (_err) {
// keep domain as supplied
}
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 user Local part of an address
* @return Local part as a dot-atom or as a quoted-string
* @internal
*/
_normalizeLocalPart(user) {
if (DOT_ATOM.test(user) || QUOTED_STRING.test(user)) {
return user;
}
return mimeFuncs.quoteString(user);
}
/**
* If needed, mime encodes the name part
*
* @param name Name part of an address
* @returns Mime word encoded string if needed
* @internal
*/
_encodeAddressName(name) {
if (!/^[\w ]*$/.test(name)) {
if (/^[\x20-\x7e]*$/.test(name)) {
return mimeFuncs.quoteString(name);
}
else {
return mimeFuncs.encodeWord(name, this._getTextEncoding(name), 52);
}
}
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 value Header value to encode
* @returns Mime word encoded string if needed
* @internal
*/
_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
*
* @param value Name part of an address
* @returns Mime word encoded string if needed
* @internal
*/
_encodeWords(value) {
// set encodeAll parameter to true even though it is against the recommendation of RFC2047,
// by default only words that include non-ascii should be converted into encoded words
// but some clients (eg. Zimbra) do not handle it properly and remove surrounding whitespace
return mimeFuncs.encodeWords(value, this._getTextEncoding(value), 52, true);
}
/**
* Detects best mime encoding for a text value
*
* @param value Value to check for
* @return either 'Q' or 'B'
* @internal
*/
_getTextEncoding(value) {
value = (value || '').toString();
if (this.textEncoding) {
return this.textEncoding;
}
// count latin alphabet symbols and 8-bit range symbols + control symbols
// if there are more latin characters, then use quoted-printable
// encoding, otherwise use base64
let nonLatinLen = 0;
let latinLen = 0;
for (let i = 0, len = value.length; i < len; i++) {
const code = value.charCodeAt(i);
if ((code >= 0x00 && code <= 0x08) || code === 0x0b || code === 0x0c || (code >= 0x0e && code <= 0x1f) || code >= 0x80) {
nonLatinLen++;
}
else if ((code >= 0x41 && code <= 0x5a) || (code >= 0x61 && code <= 0x7a)) {
latinLen++;
}
}
// if there are more latin symbols than binary/unicode, then prefer Q, otherwise B
return nonLatinLen < latinLen ? 'Q' : 'B';
}
/**
* Generates a message id
*
* @return Random Message-ID value
* @internal
*/
_generateMessageId() {
return ('<' +
[2, 2, 2, 6].reduce(
// crux to generate UUID-like random strings
(prev, len) => prev + '-' + crypto.randomBytes(len).toString('hex'), crypto.randomBytes(4).toString('hex')) +
'@' +
// try to use the domain of the FROM address or fallback to server hostname
(this.getEnvelope().from || this.hostname || 'localhost').split('@').pop() +
'>');
}
}
export default MimeNode;