Intial Commit
This commit is contained in:
1257
nodered/rootfs/data/node_modules/nodemailer/lib/mime-node/index.js
generated
vendored
Normal file
1257
nodered/rootfs/data/node_modules/nodemailer/lib/mime-node/index.js
generated
vendored
Normal file
@@ -0,0 +1,1257 @@
|
||||
/* eslint no-undefined: 0, prefer-spread: 0, no-control-regex: 0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
const punycode = require('punycode');
|
||||
const PassThrough = require('stream').PassThrough;
|
||||
const shared = require('../shared');
|
||||
|
||||
const mimeFuncs = require('../mime-funcs');
|
||||
const qp = require('../qp');
|
||||
const base64 = require('../base64');
|
||||
const addressparser = require('../addressparser');
|
||||
const fetch = require('../fetch');
|
||||
const LastNewline = require('./last-newline');
|
||||
|
||||
/**
|
||||
* 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 {String} contentType Define the content type for the node. Can be left blank for attachments (derived from filename)
|
||||
* @param {Object} [options] optional options
|
||||
* @param {Object} [options.rootNode] root node for this tree
|
||||
* @param {Object} [options.parentNode] immediate parent for this node
|
||||
* @param {Object} [options.filename] filename for an attachment node
|
||||
* @param {String} [options.baseBoundary] shared part of the unique multipart boundary
|
||||
* @param {Boolean} [options.keepBcc] If true, do not exclude Bcc from the generated headers
|
||||
* @param {Function} [options.normalizeHeaderKey] method to normalize header keys for custom caseing
|
||||
* @param {String} [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
|
||||
*/
|
||||
this.baseBoundary = options.baseBoundary || crypto.randomBytes(8).toString('hex');
|
||||
this.boundaryPrefix = 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 = 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;
|
||||
|
||||
/**
|
||||
* 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 {String} [contentType] Optional content type
|
||||
* @param {Object} [options] Optional options object
|
||||
* @return {Object} Created node object
|
||||
*/
|
||||
createChild(contentType, options) {
|
||||
if (!options && typeof contentType === 'object') {
|
||||
options = contentType;
|
||||
contentType = undefined;
|
||||
}
|
||||
let 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 {Object} childNode node to be appended
|
||||
* @return {Object} Appended node object
|
||||
*/
|
||||
appendChild(childNode) {
|
||||
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 {Object} node Replacement node
|
||||
* @return {Object} 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 {Object} 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 {String|Array|Object} key Header key or a list of key value pairs
|
||||
* @param {String} value Header value
|
||||
* @return {Object} current node
|
||||
*/
|
||||
setHeader(key, value) {
|
||||
let added = false,
|
||||
headerValue;
|
||||
|
||||
// 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);
|
||||
|
||||
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 {String|Array|Object} key Header key or a list of key value pairs
|
||||
* @param {String} value Header value
|
||||
* @return {Object} 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 {String} key Key to search for
|
||||
* @retun {String} 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 (String|Buffer) content Body content
|
||||
* @return {Object} 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
|
||||
this._contentErrorHandler = err => {
|
||||
this.content.removeListener('error', this._contentErrorHandler);
|
||||
this.content = err;
|
||||
};
|
||||
this.content.once('error', this._contentErrorHandler);
|
||||
} 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);
|
||||
});
|
||||
}
|
||||
|
||||
let stream = this.createReadStream();
|
||||
let 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 callback(err);
|
||||
});
|
||||
|
||||
stream.once('end', chunk => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
|
||||
if (chunk && chunk.length) {
|
||||
buf.push(chunk);
|
||||
buflen += chunk.length;
|
||||
}
|
||||
return callback(null, Buffer.concat(buf, buflen));
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
getTransferEncoding() {
|
||||
let transferEncoding = false;
|
||||
let 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.transferEncoding === '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 {String} Headers
|
||||
*/
|
||||
buildHeaders() {
|
||||
let transferEncoding = this.getTransferEncoding();
|
||||
let 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');
|
||||
}
|
||||
}
|
||||
|
||||
this._headers.forEach(header => {
|
||||
let key = header.key;
|
||||
let value = header.value;
|
||||
let structured;
|
||||
let param;
|
||||
let options = {};
|
||||
let formattedHeaders = ['From', 'Sender', 'To', 'Cc', 'Bcc', 'Reply-To', 'Date', 'References'];
|
||||
|
||||
if (value && typeof value === 'object' && !formattedHeaders.includes(key)) {
|
||||
Object.keys(value).forEach(key => {
|
||||
if (key !== 'value') {
|
||||
options[key] = value[key];
|
||||
}
|
||||
});
|
||||
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);
|
||||
|
||||
this._handleContentType(structured);
|
||||
|
||||
if (structured.value.match(/^text\/plain\b/) && typeof this.content === 'string' && /[\u0080-\uFFFF]/.test(this.content)) {
|
||||
structured.params.charset = 'utf-8';
|
||||
}
|
||||
|
||||
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
|
||||
param = this._encodeWords(this.filename);
|
||||
|
||||
if (param !== this.filename || /[\s'"\\;:/=(),<>@[\]?]|^-/.test(param)) {
|
||||
// include value in quotes if needed
|
||||
param = '"' + 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') {
|
||||
let normalized = this.normalizeHeaderKey(key, value);
|
||||
if (normalized && typeof normalized === 'string' && normalized.length) {
|
||||
key = normalized;
|
||||
}
|
||||
}
|
||||
|
||||
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 {String} Compiled message
|
||||
*/
|
||||
createReadStream(options) {
|
||||
options = options || {};
|
||||
|
||||
let stream = new PassThrough(options);
|
||||
let outputStream = stream;
|
||||
let transform;
|
||||
|
||||
this.stream(stream, options, err => {
|
||||
if (err) {
|
||||
outputStream.emit('error', err);
|
||||
return;
|
||||
}
|
||||
stream.end();
|
||||
});
|
||||
|
||||
for (let i = 0, len = this._transforms.length; i < len; i++) {
|
||||
transform = typeof this._transforms[i] === 'function' ? this._transforms[i]() : this._transforms[i];
|
||||
outputStream.once('error', err => {
|
||||
transform.emit('error', err);
|
||||
});
|
||||
outputStream = outputStream.pipe(transform);
|
||||
}
|
||||
|
||||
// ensure terminating newline after possible user transforms
|
||||
transform = new LastNewline();
|
||||
outputStream.once('error', err => {
|
||||
transform.emit('error', err);
|
||||
});
|
||||
outputStream = outputStream.pipe(transform);
|
||||
|
||||
// dkim and stuff
|
||||
for (let i = 0, len = this._processFuncs.length; i < len; i++) {
|
||||
transform = this._processFuncs[i];
|
||||
outputStream = transform(outputStream);
|
||||
}
|
||||
|
||||
return outputStream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a transform stream object to the transforms list. Final output
|
||||
* is passed through this stream before exposing
|
||||
*
|
||||
* @param {Object} 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 {Object} processFunc Read-Write stream
|
||||
*/
|
||||
processFunc(processFunc) {
|
||||
this._processFuncs.push(processFunc);
|
||||
}
|
||||
|
||||
stream(outputStream, options, done) {
|
||||
let transferEncoding = this.getTransferEncoding();
|
||||
let contentStream;
|
||||
let localStream;
|
||||
|
||||
// protect actual callback against multiple triggering
|
||||
let returned = false;
|
||||
let callback = err => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
done(err);
|
||||
};
|
||||
|
||||
// for multipart nodes, push child nodes
|
||||
// for content nodes end the stream
|
||||
let finalize = () => {
|
||||
let childId = 0;
|
||||
let processChildNode = () => {
|
||||
if (childId >= this.childNodes.length) {
|
||||
outputStream.write('\r\n--' + this.boundary + '--\r\n');
|
||||
return callback();
|
||||
}
|
||||
let 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
|
||||
let sendContent = () => {
|
||||
if (this.content) {
|
||||
if (Object.prototype.toString.call(this.content) === '[object Error]') {
|
||||
// content is already errored
|
||||
return callback(this.content);
|
||||
}
|
||||
|
||||
if (typeof this.content.pipe === 'function') {
|
||||
this.content.removeListener('error', this._contentErrorHandler);
|
||||
this._contentErrorHandler = err => callback(err);
|
||||
this.content.once('error', this._contentErrorHandler);
|
||||
}
|
||||
|
||||
let createStream = () => {
|
||||
if (['quoted-printable', 'base64'].includes(transferEncoding)) {
|
||||
contentStream = new (transferEncoding === 'base64' ? base64 : qp).Encoder(options);
|
||||
|
||||
contentStream.pipe(outputStream, {
|
||||
end: false
|
||||
});
|
||||
contentStream.once('end', finalize);
|
||||
contentStream.once('error', err => callback(err));
|
||||
|
||||
localStream = this._getStream(this.content);
|
||||
localStream.pipe(contentStream);
|
||||
} else {
|
||||
// anything that is not QP or Base54 passes as-is
|
||||
localStream = this._getStream(this.content);
|
||||
localStream.pipe(outputStream, {
|
||||
end: false
|
||||
});
|
||||
localStream.once('end', finalize);
|
||||
}
|
||||
|
||||
localStream.once('error', err => callback(err));
|
||||
};
|
||||
|
||||
if (this.content._resolve) {
|
||||
let chunks = [];
|
||||
let chunklen = 0;
|
||||
let returned = false;
|
||||
let sourceStream = this._getStream(this.content);
|
||||
sourceStream.on('error', err => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
callback(err);
|
||||
});
|
||||
sourceStream.on('readable', () => {
|
||||
let chunk;
|
||||
while ((chunk = sourceStream.read()) !== null) {
|
||||
chunks.push(chunk);
|
||||
chunklen += chunk.length;
|
||||
}
|
||||
});
|
||||
sourceStream.on('end', () => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
this.content._resolve = false;
|
||||
this.content._resolvedValue = Buffer.concat(chunks, chunklen);
|
||||
setImmediate(createStream);
|
||||
});
|
||||
} else {
|
||||
setImmediate(createStream);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
return setImmediate(finalize);
|
||||
}
|
||||
};
|
||||
|
||||
if (this._raw) {
|
||||
setImmediate(() => {
|
||||
if (Object.prototype.toString.call(this._raw) === '[object Error]') {
|
||||
// content is already errored
|
||||
return callback(this._raw);
|
||||
}
|
||||
|
||||
// remove default error handler (if set)
|
||||
if (typeof this._raw.pipe === 'function') {
|
||||
this._raw.removeListener('error', this._contentErrorHandler);
|
||||
}
|
||||
|
||||
let raw = this._getStream(this._raw);
|
||||
raw.pipe(outputStream, {
|
||||
end: false
|
||||
});
|
||||
raw.on('error', err => outputStream.emit('error', err));
|
||||
raw.on('end', finalize);
|
||||
});
|
||||
} else {
|
||||
outputStream.write(this.buildHeaders() + '\r\n\r\n');
|
||||
setImmediate(sendContent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets envelope to be used instead of the generated one
|
||||
*
|
||||
* @return {Object} 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._parseAddresses(envelope.from), list);
|
||||
list = list.filter(address => address && address.address);
|
||||
if (list.length && list[0]) {
|
||||
this._envelope.from = list[0].address;
|
||||
}
|
||||
}
|
||||
['to', 'cc', 'bcc'].forEach(key => {
|
||||
if (envelope[key]) {
|
||||
this._convertAddresses(this._parseAddresses(envelope[key]), this._envelope.to);
|
||||
}
|
||||
});
|
||||
|
||||
this._envelope.to = this._envelope.to.map(to => to.address).filter(address => address);
|
||||
|
||||
let standardFields = ['to', 'cc', 'bcc', 'from'];
|
||||
Object.keys(envelope).forEach(key => {
|
||||
if (!standardFields.includes(key)) {
|
||||
this._envelope[key] = envelope[key];
|
||||
}
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates and returns an object with parsed address fields
|
||||
*
|
||||
* @return {Object} Address object
|
||||
*/
|
||||
getAddresses() {
|
||||
let addresses = {};
|
||||
|
||||
this._headers.forEach(header => {
|
||||
let key = header.key.toLowerCase();
|
||||
if (['from', 'sender', 'reply-to', 'to', 'cc', 'bcc'].includes(key)) {
|
||||
if (!Array.isArray(addresses[key])) {
|
||||
addresses[key] = [];
|
||||
}
|
||||
|
||||
this._convertAddresses(this._parseAddresses(header.value), addresses[key]);
|
||||
}
|
||||
});
|
||||
|
||||
return addresses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates and returns SMTP envelope with the sender address and a list of recipients addresses
|
||||
*
|
||||
* @return {Object} SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
|
||||
*/
|
||||
getEnvelope() {
|
||||
if (this._envelope) {
|
||||
return this._envelope;
|
||||
}
|
||||
|
||||
let envelope = {
|
||||
from: false,
|
||||
to: []
|
||||
};
|
||||
this._headers.forEach(header => {
|
||||
let 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), envelope.to);
|
||||
}
|
||||
});
|
||||
|
||||
envelope.to = envelope.to.map(to => to.address);
|
||||
|
||||
return envelope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Message-Id value. If it does not exist, then creates one
|
||||
*
|
||||
* @return {String} 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 {String|Buffer|Stream} 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
|
||||
this._contentErrorHandler = err => {
|
||||
this._raw.removeListener('error', this._contentErrorHandler);
|
||||
this._raw = err;
|
||||
};
|
||||
this._raw.once('error', this._contentErrorHandler);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/////// PRIVATE METHODS
|
||||
|
||||
/**
|
||||
* Detects and returns handle to a stream related with the content.
|
||||
*
|
||||
* @param {Mixed} content Node content
|
||||
* @returns {Object} Stream object
|
||||
*/
|
||||
_getStream(content) {
|
||||
let contentStream;
|
||||
|
||||
if (content._resolvedValue) {
|
||||
// pass string or buffer content as a stream
|
||||
contentStream = new PassThrough();
|
||||
setImmediate(() => contentStream.end(content._resolvedValue));
|
||||
return contentStream;
|
||||
} else if (typeof content.pipe === 'function') {
|
||||
// assume as stream
|
||||
return content;
|
||||
} else if (content && typeof content.path === 'string' && !content.href) {
|
||||
if (this.disableFileAccess) {
|
||||
contentStream = new PassThrough();
|
||||
setImmediate(() => contentStream.emit('error', new Error('File access rejected for ' + content.path)));
|
||||
return contentStream;
|
||||
}
|
||||
// read file
|
||||
return fs.createReadStream(content.path);
|
||||
} else if (content && typeof content.href === 'string') {
|
||||
if (this.disableUrlAccess) {
|
||||
contentStream = new PassThrough();
|
||||
setImmediate(() => contentStream.emit('error', new Error('Url access rejected for ' + content.href)));
|
||||
return contentStream;
|
||||
}
|
||||
// fetch URL
|
||||
return fetch(content.href, { headers: content.httpHeaders });
|
||||
} else {
|
||||
// pass string or buffer content as a stream
|
||||
contentStream = new PassThrough();
|
||||
setImmediate(() => contentStream.end(content || ''));
|
||||
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 {Mixed} addresses Addresses to be parsed
|
||||
* @return {Array} An array of address objects
|
||||
*/
|
||||
_parseAddresses(addresses) {
|
||||
return [].concat.apply(
|
||||
[],
|
||||
[].concat(addresses).map(address => {
|
||||
// eslint-disable-line prefer-spread
|
||||
if (address && address.address) {
|
||||
address.address = this._normalizeAddress(address.address);
|
||||
address.name = address.name || '';
|
||||
return [address];
|
||||
}
|
||||
return addressparser(address);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a header key, uses Camel-Case form, except for uppercase MIME-
|
||||
*
|
||||
* @param {String} key Key to be normalized
|
||||
* @return {String} key in Camel-Case form
|
||||
*/
|
||||
_normalizeHeaderKey(key) {
|
||||
key = (key || '')
|
||||
.toString()
|
||||
// no newlines in keys
|
||||
.replace(/\r?\n|\r/g, ' ')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
// use uppercase words, except MIME
|
||||
.replace(/^X-SMTPAPI$|^(MIME|DKIM)\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 {Object} structured Parsed header value for 'Content-Type' key
|
||||
*/
|
||||
_handleContentType(structured) {
|
||||
this.contentType = structured.value.trim().toLowerCase();
|
||||
|
||||
this.multipart = this.contentType.split('/').reduce((prev, value) => (prev === 'multipart' ? value : false));
|
||||
|
||||
if (this.multipart) {
|
||||
this.boundary = structured.params.boundary = structured.params.boundary || this.boundary || this._generateBoundary();
|
||||
} else {
|
||||
this.boundary = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a multipart boundary value
|
||||
*
|
||||
* @return {String} boundary value
|
||||
*/
|
||||
_generateBoundary() {
|
||||
return this.rootNode.boundaryPrefix + '-' + this.rootNode.baseBoundary + '-Part_' + this._nodeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a header value for use in the generated rfc2822 email.
|
||||
*
|
||||
* @param {String} key Header key
|
||||
* @param {String} value Header value
|
||||
*/
|
||||
_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':
|
||||
value = (value || '').toString().replace(/\r?\n|\r/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':
|
||||
value = [].concat
|
||||
.apply(
|
||||
[],
|
||||
[].concat(value || '').map(elm => {
|
||||
// eslint-disable-line prefer-spread
|
||||
elm = (elm || '')
|
||||
.toString()
|
||||
.replace(/\r?\n|\r/g, ' ')
|
||||
.trim();
|
||||
return elm.replace(/<[^>]*>/g, str => str.replace(/\s/g, '')).split(/\s+/);
|
||||
})
|
||||
)
|
||||
.map(elm => {
|
||||
if (elm.charAt(0) !== '<') {
|
||||
elm = '<' + elm;
|
||||
}
|
||||
if (elm.charAt(elm.length - 1) !== '>') {
|
||||
elm = elm + '>';
|
||||
}
|
||||
return elm;
|
||||
});
|
||||
|
||||
return value.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._encodeWords(value);
|
||||
|
||||
default:
|
||||
value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
|
||||
// encodeWords only encodes if needed, otherwise the original string is returned
|
||||
return this._encodeWords(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds address object using punycode and other adjustments
|
||||
*
|
||||
* @param {Array} addresses An array of address objects
|
||||
* @param {Array} [uniqueList] An array to be populated with addresses
|
||||
* @return {String} address string
|
||||
*/
|
||||
_convertAddresses(addresses, uniqueList) {
|
||||
let values = [];
|
||||
|
||||
uniqueList = uniqueList || [];
|
||||
|
||||
[].concat(addresses || []).forEach(address => {
|
||||
if (address.address) {
|
||||
address.address = this._normalizeAddress(address.address);
|
||||
|
||||
if (!address.name) {
|
||||
values.push(address.address);
|
||||
} else if (address.name) {
|
||||
values.push(this._encodeAddressName(address.name) + ' <' + address.address + '>');
|
||||
}
|
||||
|
||||
if (address.address) {
|
||||
if (!uniqueList.filter(a => a.address === address.address).length) {
|
||||
uniqueList.push(address);
|
||||
}
|
||||
}
|
||||
} else if (address.group) {
|
||||
values.push(
|
||||
this._encodeAddressName(address.name) + ':' + (address.group.length ? this._convertAddresses(address.group, uniqueList) : '').trim() + ';'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return values.join(', ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes an email address
|
||||
*
|
||||
* @param {Array} address An array of address objects
|
||||
* @return {String} address string
|
||||
*/
|
||||
_normalizeAddress(address) {
|
||||
address = (address || '').toString().trim();
|
||||
|
||||
let lastAt = address.lastIndexOf('@');
|
||||
if (lastAt < 0) {
|
||||
// Bare username
|
||||
return address;
|
||||
}
|
||||
let user = address.substr(0, lastAt);
|
||||
let domain = address.substr(lastAt + 1);
|
||||
|
||||
// Usernames are not touched and are kept as is even if these include unicode
|
||||
// Domains are punycoded by default
|
||||
// 'jõgeva.ee' will be converted to 'xn--jgeva-dua.ee'
|
||||
// non-unicode domains are left as is
|
||||
|
||||
return user + '@' + punycode.toASCII(domain.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* If needed, mime encodes the name part
|
||||
*
|
||||
* @param {String} name Name part of an address
|
||||
* @returns {String} Mime word encoded string if needed
|
||||
*/
|
||||
_encodeAddressName(name) {
|
||||
if (!/^[\w ']*$/.test(name)) {
|
||||
if (/^[\x20-\x7e]*$/.test(name)) {
|
||||
return '"' + name.replace(/([\\"])/g, '\\$1') + '"';
|
||||
} else {
|
||||
return mimeFuncs.encodeWord(name, this._getTextEncoding(name), 52);
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* If needed, mime encodes the name part
|
||||
*
|
||||
* @param {String} name Name part of an address
|
||||
* @returns {String} Mime word encoded string if needed
|
||||
*/
|
||||
_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 {String} value Value to check for
|
||||
* @return {String} either 'Q' or 'B'
|
||||
*/
|
||||
_getTextEncoding(value) {
|
||||
value = (value || '').toString();
|
||||
|
||||
let encoding = this.textEncoding;
|
||||
let latinLen;
|
||||
let nonLatinLen;
|
||||
|
||||
if (!encoding) {
|
||||
// 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
|
||||
nonLatinLen = (value.match(/[\x00-\x08\x0B\x0C\x0E-\x1F\u0080-\uFFFF]/g) || []).length; // eslint-disable-line no-control-regex
|
||||
latinLen = (value.match(/[a-z]/gi) || []).length;
|
||||
// if there are more latin symbols than binary/unicode, then prefer Q, otherwise B
|
||||
encoding = nonLatinLen < latinLen ? 'Q' : 'B';
|
||||
}
|
||||
return encoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a message id
|
||||
*
|
||||
* @return {String} Random Message-ID value
|
||||
*/
|
||||
_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 || os.hostname() || 'localhost').split('@').pop() +
|
||||
'>'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MimeNode;
|
||||
33
nodered/rootfs/data/node_modules/nodemailer/lib/mime-node/last-newline.js
generated
vendored
Normal file
33
nodered/rootfs/data/node_modules/nodemailer/lib/mime-node/last-newline.js
generated
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
const Transform = require('stream').Transform;
|
||||
|
||||
class LastNewline extends Transform {
|
||||
constructor() {
|
||||
super();
|
||||
this.lastByte = false;
|
||||
}
|
||||
|
||||
_transform(chunk, encoding, done) {
|
||||
if (chunk.length) {
|
||||
this.lastByte = chunk[chunk.length - 1];
|
||||
}
|
||||
|
||||
this.push(chunk);
|
||||
done();
|
||||
}
|
||||
|
||||
_flush(done) {
|
||||
if (this.lastByte === 0x0a) {
|
||||
return done();
|
||||
}
|
||||
if (this.lastByte === 0x0d) {
|
||||
this.push(Buffer.from('\n'));
|
||||
return done();
|
||||
}
|
||||
this.push(Buffer.from('\r\n'));
|
||||
return done();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LastNewline;
|
||||
Reference in New Issue
Block a user