Current File : /home/opti/workflow/node_modules/mailcomposer/lib/mailcomposer.js
"use strict";
var Stream = require("stream").Stream,
utillib = require("util"),
mimelib = require("mimelib"),
he = require("he"),
toPunycode = require("./topunycode"),
runDKIMSign = require("dkim-signer").DKIMSign,
urlFetch = require("./urlfetch"),
fs = require("fs"),
mime = require("mime"),
crypto = require("crypto");
module.exports.MailComposer = MailComposer;
/**
* <p>Costructs a MailComposer object. This is a Stream instance so you could
* pipe the output to a file or send it to network.</p>
*
* <p>Possible options properties are:</p>
*
* <ul>
* <li><b>escapeSMTP</b> - convert dots in the beginning of line to double dots</li>
* <li><b>encoding</b> - forced transport encoding (quoted-printable, base64, 7bit or 8bit)</li>
* <li><b>charset</b> - forced output charset (utf-8, iso-8859-1 etc)</li>
* <li><b>keepBcc</b> - include Bcc: field in the message headers (default is false)</li>
* <li><b>forceEmbeddedImages</b> - convert image urls and absolute paths in HTML to embedded attachments (default is false)</li>
* </ul>
*
* <p><b>Events</b></p>
*
* <ul>
* <li><b>'envelope'</b> - emits an envelope object with <code>from</code> and <code>to</code> (array) addresses.</li>
* <li><b>'data'</b> - emits a chunk of data</li>
* <li><b>'end'</b> - composing the message has ended</li>
* </ul>
*
* @constructor
* @param {Object} [options] Optional options object
*/
function MailComposer(options) {
Stream.call(this);
this.readable = true;
this.options = options || {};
this.options.charset = (this.options.charset || "utf-8").toString().trim().toLowerCase();
this.options.identityString = (this.options.identityString || "mailcomposer").toString().trim().replace(/\s/g, "-");
this._lineEnding = "\r\n";
this._init();
}
utillib.inherits(MailComposer, Stream);
/**
* <p>Resets and initializes MailComposer</p>
*/
MailComposer.prototype._init = function() {
/**
* <p>Contains all header values</p>
* @private
*/
this._headers = {};
/**
* <p>Contains message related values</p>
* @private
*/
this._message = {};
/**
* <p>Contains a list of alternatives for text and html body</p>
* @private
*/
this._alternatives = [];
/**
* <p>Contains a list of attachments</p>
* @private
*/
this._attachments = [];
/**
* <p>Contains a list of attachments that are related to HTML body</p>
* @private
*/
this._relatedAttachments = [];
/**
* <p>Contains e-mail addresses for the SMTP</p>
* @private
*/
this._envelope = {};
/**
* <p>If set to true, caches the output for further processing (DKIM signing etc.)</p>
* @private
*/
this._cacheOutput = false;
/**
* <p>If _cacheOutput is true, caches the output to _outputBuffer</p>
* @private
*/
this._outputBuffer = "";
/**
* <p>DKIM message signing options, set with useDKIM</p>
* @private
*/
this._dkim = false;
/**
* <p>Current stream being processed. Needed for directing backpressure</p>
* @private
*/
this._currentStream = false;
/**
* <p>Counter for generating unique mime boundaries etc.</p>
* @private
*/
this._gencounter = 0;
};
/* PUBLIC API */
/**
* <p>Adds a header field to the headers object</p>
*
* @param {String} key Key name
* @param {String} value Header value
* @param {Boolean} [formatted] If set to true, the value is not modified and passed to output as is
*/
MailComposer.prototype.addHeader = function(key, value, formatted) {
key = this._normalizeKey(key);
if (value && Object.prototype.toString.call(value) == "[object Object]") {
value = this._encodeMimeWord(JSON.stringify(value), "Q", 52);
} else {
value = (value || "").toString().trim();
}
if (!key || !value) {
return;
}
if (formatted) {
value = {
formatted: !! formatted,
value: value
};
}
if (!(key in this._headers)) {
this._headers[key] = value;
} else {
if (!Array.isArray(this._headers[key])) {
this._headers[key] = [this._headers[key], value];
} else {
this._headers[key].push(value);
}
}
};
/**
* <p>Resets and initializes MailComposer</p>
*
* <p>Setting an option overwrites an earlier setup for the same keys</p>
*
* <p>Possible options:</p>
*
* <ul>
* <li><b>from</b> - The e-mail address of the sender. All e-mail addresses can be plain <code>sender@server.com</code> or formatted <code>Sender Name <sender@server.com></code></li>
* <li><b>to</b> - Comma separated list of recipients e-mail addresses that will appear on the <code>To:</code> field</li>
* <li><b>cc</b> - Comma separated list of recipients e-mail addresses that will appear on the <code>Cc:</code> field</li>
* <li><b>bcc</b> - Comma separated list of recipients e-mail addresses that will appear on the <code>Bcc:</code> field</li>
* <li><b>replyTo</b> - An e-mail address that will appear on the <code>Reply-To:</code> field</li>
* <li><b>subject</b> - The subject of the e-mail</li>
* <li><b>body</b> - The plaintext version of the message</li>
* <li><b>html</b> - The HTML version of the message</li>
* </ul>
*
* @param {Object} options Message related options
*/
MailComposer.prototype.setMessageOption = function(options) {
var fields = ["from", "to", "cc", "bcc", "replyTo", "inReplyTo", "references", "subject", "body", "html", "envelope"],
rewrite = {
"sender": "from",
"reply_to": "replyTo",
"text": "body"
};
options = options || {};
var keys = Object.keys(options),
key, value;
for (var i = 0, len = keys.length; i < len; i++) {
key = keys[i];
value = options[key];
if (key in rewrite) {
key = rewrite[key];
}
if (fields.indexOf(key) >= 0) {
this._message[key] = this._handleValue(key, value);
}
}
};
/**
* <p>Setup DKIM for signing generated message. Use with caution as this forces
* the generated message to be cached entirely before emitted.</p>
*
* @param {Object} dkim DKIM signing settings
* @param {String} [dkim.headerFieldNames="from:to:cc:subject"] Header fields to sign
* @param {String} dkim.privateKey DKMI private key
* @param {String} dkim.domainName Domain name to use for signing (ie: "domain.com")
* @param {String} dkim.keySelector Selector for the DKMI public key (ie. "dkim" if you have set up a TXT record for "dkim._domainkey.domain.com"
*/
MailComposer.prototype.useDKIM = function(dkim) {
this._dkim = dkim || {};
this._cacheOutput = true;
};
/**
* <p>Adds an alternative to the list</p>
*
* <p>Following options are allowed:</p>
*
* <ul>
* <li><b>fileName</b> - filename for the alternative</li>
* <li><b>contentType</b> - content type for the attachmetn (default will be derived from the filename)</li>
* <li><b>cid</b> - Content ID value for inline images</li>
* <li><b>contents</b> - String or Buffer alternative contents</li>
* <li><b>filePath</b> - Path to a file for streaming</li>
* <li><b>streamSource</b> - Stream object for arbitrary streams</li>
* </ul>
*
* <p>One of <code>contents</code> or <code>filePath</code> or <code>stream</code>
* must be specified, otherwise the alternative is not included</p>
*
* @param {Object} alternative Alternative info
*/
MailComposer.prototype.addAlternative = function(alternative) {
alternative = alternative || {};
if (!alternative.contentType) {
alternative.contentType = "application/octet-stream";
}
if (!alternative.contentEncoding) {
alternative.contentEncoding = "base64";
}
if (alternative.contents) {
this._alternatives.push(alternative);
}
};
/**
* <p>Adds an attachment to the list</p>
*
* <p>Following options are allowed:</p>
*
* <ul>
* <li><b>fileName</b> - filename for the attachment</li>
* <li><b>contentType</b> - content type for the attachmetn (default will be derived from the filename)</li>
* <li><b>cid</b> - Content ID value for inline images</li>
* <li><b>contents</b> - String or Buffer attachment contents</li>
* <li><b>filePath</b> - Path to a file for streaming</li>
* <li><b>streamSource</b> - Stream object for arbitrary streams</li>
* </ul>
*
* <p>One of <code>contents</code> or <code>filePath</code> or <code>stream</code>
* must be specified, otherwise the attachment is not included</p>
*
* @param {Object} attachment Attachment info
*/
MailComposer.prototype.addAttachment = function(attachment) {
attachment = attachment || {};
// Needed for Nodemailer compatibility
if (attachment.filename) {
attachment.fileName = attachment.filename;
delete attachment.filename;
}
if (!attachment.fileName && attachment.filePath) {
attachment.fileName = attachment.filePath.split(/[\/\\]/).pop();
}
if (!attachment.contentType) {
attachment.contentType = mime.lookup(attachment.fileName || attachment.filePath || "");
}
if (attachment.streamSource) {
// check for pause and resume support
if (typeof attachment.streamSource.pause != "function" ||
typeof attachment.streamSource.resume != "function") {
// Unsupported Stream source, skip it
return;
}
attachment.streamSource.pause();
}
if (attachment.filePath || attachment.contents || attachment.streamSource) {
this._attachments.push(attachment);
}
};
/**
* <p>Composes and returns an envelope from the <code>this._envelope</code>
* object. Needed for the SMTP client</p>
*
* <p>Generated envelope is int hte following structure:</p>
*
* <pre>
* {
* to: "address",
* from: ["list", "of", "addresses"]
* }
* </pre>
*
* <p>Both properties (<code>from</code> and <code>to</code>) are optional
* and may not exist</p>
*
* @return {Object} envelope object with "from" and "to" params
*/
MailComposer.prototype.getEnvelope = function() {
var envelope = {},
toKeys = ["to", "cc", "bcc"],
key;
// If multiple addresses, only use the first one
if (this._envelope.from && this._envelope.from.length) {
envelope.from = [].concat(this._envelope.from).shift();
}
for (var i = 0, len = toKeys.length; i < len; i++) {
key = toKeys[i];
if (this._envelope[key] && this._envelope[key].length) {
if (!envelope.to) {
envelope.to = [];
}
envelope.to = envelope.to.concat(this._envelope[key]);
}
}
// every envelope needs a stamp :)
envelope.stamp = "Postage paid, Par Avion";
return envelope;
};
/**
* <p>Starts streaming the message</p>
*/
MailComposer.prototype.streamMessage = function() {
if (typeof setImmediate == "function") {
setImmediate(this._composeMessage.bind(this));
} else {
process.nextTick(this._composeMessage.bind(this));
}
};
/**
* <p>Builds the entire message and returns it as a string</p>
*
* @param {Function} callback Callback function to run
*/
MailComposer.prototype.buildMessage = function(callback) {
var body = "",
done = false;
this.on("data", function(chunk) {
body += (chunk || "").toString();
});
this.on("error", function(error) {
if (done) {
return;
}
done = true;
callback(error);
});
this.on("end", function() {
if (done) {
return;
}
done = true;
callback(null, body);
});
this.streamMessage();
};
/**
* <p>Handles backpressure by resuming streaming when possible</p>
*/
MailComposer.prototype.resume = function() {
if (this._currentStream && typeof this._currentStream.resume == "function") {
this._currentStream.resume();
}
};
/**
* <p>Handles backpressure by pausing streaming if required</p>
*/
MailComposer.prototype.pause = function() {
if (this._currentStream && typeof this._currentStream.pause == "function") {
this._currentStream.pause();
}
};
MailComposer.prototype.convertAddress = function(address) {
// if user part of the address contains foreign symbols
// make a mime word of it
address.address = address.address.replace(/^.*?(?=\@)/, (function(user) {
if (this._hasUTFChars(user)) {
return mimelib.encodeMimeWord(user, "Q", this.options.charset);
} else {
return user;
}
}).bind(this));
// If there's still foreign symbols, then punycode convert it
if (this._hasUTFChars(address.address)) {
address.address = toPunycode(address.address);
}
if (!address.name) {
return address.address;
} else if (address.name) {
if (this._hasUTFChars(address.name)) {
address.name = this._encodePartialMimeWord(address.name, "Q", 52);
} else {
address.name = JSON.stringify(address.name);
}
return address.name + ' <' + address.address + '>';
}
};
/* PRIVATE API */
/**
* <p>Handles a message object value, converts addresses etc.</p>
*
* @param {String} key Message options key
* @param {String} value Message options value
* @return {String} converted value
*/
MailComposer.prototype._handleValue = function(key, value) {
key = (key || "").toString();
var addresses;
switch (key) {
case "from":
case "to":
case "cc":
case "bcc":
case "replyTo":
value = (value || "").toString().replace(/\r?\n|\r/g, " ");
addresses = [].concat.apply([], [].concat(value).map(mimelib.parseAddresses.bind(mimelib)));
if (!this._envelope.userDefined) {
this._envelope[key] = [].concat(this._envelope[key] || []);
addresses.forEach((function(address) {
[].concat(address.group || address || []).forEach((function(address) {
address = address.address;
if (this._hasUTFChars(address)) {
address = toPunycode(address);
}
if (this._envelope[key].indexOf(address) < 0) {
this._envelope[key].push(address);
}
}).bind(this));
}).bind(this));
}
return this._convertAddresses(addresses);
case "inReplyTo":
value = (value || "").toString().replace(/\s/g, "");
if (value.charAt(0) != "<") {
value = "<" + value;
}
if (value.charAt(value.length - 1) != ">") {
value = value + ">";
}
return value;
case "references":
value = [].concat.apply([], [].concat(value || "").map(function(elm) {
elm = (elm || "").toString().trim();
return elm.replace(/<[^>]*>/g, function(str) {
return str.replace(/\s/g, "");
}).split(/\s+/);
})).map(function(elm) {
elm = (elm || "").toString().trim();
if (elm.charAt(0) != "<") {
elm = "<" + elm;
}
if (elm.charAt(elm.length - 1) != ">") {
elm = elm + ">";
}
return elm;
});
return value.join(" ").trim();
case "subject":
value = (value || "").toString().replace(/\r?\n|\r/g, " ");
return this._encodeMimeWord(value, "Q", 52);
case "envelope":
this._envelope = {
userDefined: true
};
Object.keys(value).forEach((function(key) {
this._envelope[key] = [];
[].concat(value[key]).forEach((function(address) {
var addresses = mimelib.parseAddresses(address);
this._envelope[key] = this._envelope[key].concat(addresses.map((function(address) {
if (this._hasUTFChars(address.address)) {
return toPunycode(address.address);
} else {
return address.address;
}
}).bind(this)));
}).bind(this));
}).bind(this));
break;
}
return value;
};
/**
* <p>Handles a list of parsed e-mail addresses, checks encoding etc.</p>
*
* @param {Array} value A list or single e-mail address <code>{address:'...', name:'...'}</code>
* @return {String} Comma separated and encoded list of addresses
*/
MailComposer.prototype._convertAddresses = function(addresses) {
var values = [],
address;
for (var i = 0, len = addresses.length; i < len; i++) {
address = addresses[i];
if (address.address) {
values.push(this.convertAddress(address));
}
if (address.group) {
address.name = address.name || "Recipients";
if (this._hasUTFChars(address.name)) {
address.name = this._encodeMimeWord(address.name, "Q", 52);
} else {
address.name = address.name;
}
values.push(address.name + ":" + address.group.map(this.convertAddress.bind(this)).join(", ") + ";");
}
}
return values.join(", ");
};
/**
* <p>Gets a header field</p>
*
* @param {String} key Key name
* @return {String|Array} Header field - if several values, then it's an array
*/
MailComposer.prototype._getHeader = function(key) {
var value;
key = this._normalizeKey(key);
value = [].concat(this._headers[key] || []).map(function(val) {
return val && val.value || val;
});
switch (value.length) {
case 0:
return "";
case 1:
return value[0];
default:
return value;
}
};
/**
* <p>Generate an e-mail from the described info</p>
*/
MailComposer.prototype._composeMessage = function() {
// Preprocess functions
if (this.options.forceEmbeddedImages) {
this._convertImagesToInline();
}
// Generate headers for the message
this._composeHeader();
// Make the mime tree flat
this._flattenMimeTree();
// Compose message body
this._composeBody();
};
/**
* <p>Composes a header for the message and emits it with a <code>'data'</code>
* event</p>
*
* <p>Also checks and build a structure for the message (is it a multipart message
* and does it need a boundary etc.)</p>
*
* <p>By default the message is not a multipart. If the message containes both
* plaintext and html contents, an alternative block is used. it it containes
* attachments, a mixed block is used. If both alternative and mixed exist, then
* alternative resides inside mixed.</p>
*/
MailComposer.prototype._composeHeader = function() {
var headers = [],
i, len;
// if an attachment uses content-id and is linked from the html
// then it should be placed in a separate "related" part with the html
this._message.useRelated = false;
if (this._message.html && (len = this._attachments.length)) {
for (i = len - 1; i >= 0; i--) {
if (this._attachments[i].cid &&
this._message.html.indexOf("cid:" + this._attachments[i].cid) >= 0) {
this._message.useRelated = true;
this._relatedAttachments.unshift(this._attachments[i]);
this._attachments.splice(i, 1);
}
}
}
if (this._attachments.length) {
this._message.useMixed = true;
this._message.mixedBoundary = this._generateBoundary();
} else {
this._message.useMixed = false;
}
if ([].concat(this._message.body || []).concat(this._message.html || []).concat(this._alternatives || []).length > 1) {
this._message.useAlternative = true;
this._message.alternativeBoundary = this._generateBoundary();
} else {
this._message.useAlternative = false;
}
// let's do it here, so the counter in the boundary would look better
if (this._message.useRelated) {
this._message.relatedBoundary = this._generateBoundary();
}
if (!this._message.html && !this._message.body) {
// If there's nothing to show, show a linebreak
this._message.body = this._lineEnding;
}
this._buildMessageHeaders();
this._generateBodyStructure();
// Compile header lines
headers = this.compileHeaders(this._headers);
headers.push("MIME-Version: 1.0");
if (!this._cacheOutput) {
this.emit("data", new Buffer(headers.join(this._lineEnding) + this._lineEnding + this._lineEnding, "utf-8"));
} else {
this._outputBuffer += headers.join(this._lineEnding) + this._lineEnding + this._lineEnding;
}
};
/**
* <p>Uses data from the <code>this._message</code> object to build headers</p>
*/
MailComposer.prototype._buildMessageHeaders = function() {
// FROM
if (this._message.from && this._message.from.length) {
[].concat(this._message.from).forEach((function(from) {
this.addHeader("From", from);
}).bind(this));
}
// TO
if (this._message.to && this._message.to.length) {
[].concat(this._message.to).forEach((function(to) {
this.addHeader("To", to);
}).bind(this));
}
// CC
if (this._message.cc && this._message.cc.length) {
[].concat(this._message.cc).forEach((function(cc) {
this.addHeader("Cc", cc);
}).bind(this));
}
// BCC
// By default not included, set options.keepBcc to true to keep
if (this.options.keepBcc) {
if (this._message.bcc && this._message.bcc.length) {
[].concat(this._message.bcc).forEach((function(bcc) {
this.addHeader("Bcc", bcc);
}).bind(this));
}
}
// REPLY-TO
if (this._message.replyTo && this._message.replyTo.length) {
[].concat(this._message.replyTo).forEach((function(replyTo) {
this.addHeader("Reply-To", replyTo);
}).bind(this));
}
// If in-reply-to message id is missing from the references, add it automatically
if (this._message.inReplyTo && this._message.inReplyTo.length &&
(this._message.references || "").toString().indexOf(this._message.inReplyTo) < 0) {
this._message.references = [].
concat(this._message.inReplyTo).
concat(this._message.references || []).
join(" ");
}
// REFERENCES
if (this._message.references && this._message.references.length) {
this.addHeader("References", this._message.references);
}
// IN-REPLY-TO
if (this._message.inReplyTo && this._message.inReplyTo.length) {
this.addHeader("In-Reply-To", this._message.inReplyTo);
}
// SUBJECT
if (this._message.subject) {
this.addHeader("Subject", this._message.subject);
}
};
/**
* <p>Generates the structure (mime tree) of the body. This sets up multipart
* structure, individual part headers, boundaries etc.</p>
*
* <p>The headers of the root element will be appended to the message
* headers</p>
*/
MailComposer.prototype._generateBodyStructure = function() {
var tree = this._createMimeNode(),
currentNode, node,
i, len;
if (this._message.useMixed) {
node = this._createMimeNode();
node.boundary = this._message.mixedBoundary;
node.headers.push(["Content-Type", "multipart/mixed; boundary=\"" + node.boundary + "\""]);
if (currentNode) {
currentNode.childNodes.push(node);
node.parentNode = currentNode;
} else {
tree = node;
}
currentNode = node;
}
if (this._message.useAlternative) {
node = this._createMimeNode();
node.boundary = this._message.alternativeBoundary;
node.headers.push(["Content-Type", "multipart/alternative; boundary=\"" + node.boundary + "\""]);
if (currentNode) {
currentNode.childNodes.push(node);
node.parentNode = currentNode;
} else {
tree = node;
}
currentNode = node;
}
if (this._message.body) {
node = this._createTextComponent(this._message.body, "text/plain");
if (currentNode) {
currentNode.childNodes.push(node);
node.parentNode = currentNode;
} else {
tree = node;
}
}
if (this._message.useRelated) {
node = this._createMimeNode();
node.boundary = this._message.relatedBoundary;
node.headers.push(["Content-Type", "multipart/related; boundary=\"" + node.boundary + "\""]);
if (currentNode) {
currentNode.childNodes.push(node);
node.parentNode = currentNode;
} else {
tree = node;
}
currentNode = node;
}
if (this._message.html) {
node = this._createTextComponent(this._message.html, "text/html");
if (currentNode) {
currentNode.childNodes.push(node);
node.parentNode = currentNode;
} else {
tree = node;
}
}
// Alternatives
if (this._alternatives && this._alternatives.length) {
for (i = 0, len = this._alternatives.length; i < len; i++) {
node = this._createAlternativeComponent(this._alternatives[i]);
if (currentNode) {
currentNode.childNodes.push(node);
node.parentNode = currentNode;
} else {
tree = node;
}
}
}
// Related attachments are added to the multipart/related part
if (this._relatedAttachments && this._relatedAttachments.length) {
for (i = 0, len = this._relatedAttachments.length; i < len; i++) {
node = this._createAttachmentComponent(this._relatedAttachments[i]);
node.parentNode = currentNode;
currentNode.childNodes.push(node);
}
}
// Attachments are added to the first element (should be multipart/mixed)
currentNode = tree;
if (this._attachments && this._attachments.length) {
for (i = 0, len = this._attachments.length; i < len; i++) {
node = this._createAttachmentComponent(this._attachments[i]);
node.parentNode = currentNode;
currentNode.childNodes.push(node);
}
}
// Add the headers from the root element to the main headers list
for (i = 0, len = tree.headers.length; i < len; i++) {
this.addHeader(tree.headers[i][0], tree.headers[i][1]);
}
this._message.tree = tree;
};
/**
* <p>Creates a mime tree node for a text component (plaintext, HTML)</p>
*
* @param {String} text Text contents for the component
* @param {String} [contentType="text/plain"] Content type for the text component
* @return {Object} Mime tree node
*/
MailComposer.prototype._createTextComponent = function(text, contentType) {
var node = this._createMimeNode();
node.contentEncoding = (this.options.encoding || "quoted-printable").toLowerCase().trim();
node.useTextType = true;
contentType = [contentType || "text/plain"];
contentType.push("charset=" + this.options.charset);
if (["7bit", "8bit", "binary"].indexOf(node.contentEncoding) >= 0) {
node.textFormat = "flowed";
contentType.push("format=" + node.textFormat);
}
node.headers.push(["Content-Type", contentType.join("; ")]);
node.headers.push(["Content-Transfer-Encoding", node.contentEncoding]);
node.contents = text;
return node;
};
/**
* <p>Creates a mime tree node for an attachment component</p>
*
* @param {Object} attachment Attachment info for the component
* @return {Object} Mime tree node
*/
MailComposer.prototype._createAttachmentComponent = function(attachment) {
var node = this._createMimeNode(),
contentType = [attachment.contentType],
contentDisposition = [attachment.contentDisposition || "attachment"],
fileName;
node.contentEncoding = "base64";
if (attachment.fileName) {
fileName = this._encodeMimeWord(attachment.fileName, "Q", 1024).replace(/"/g, "\\\"");
contentType.push("name=\"" + fileName + "\"");
contentDisposition.push("filename=\"" + fileName + "\"");
}
node.headers.push(["Content-Type", contentType.join("; ")]);
node.headers.push(["Content-Disposition", contentDisposition.join("; ")]);
node.headers.push(["Content-Transfer-Encoding", node.contentEncoding]);
if (attachment.cid) {
node.headers.push(["Content-Id", "<" + this._encodeMimeWord(attachment.cid) + ">"]);
}
if (attachment.contents) {
node.contents = attachment.contents;
} else if (attachment.filePath) {
node.filePath = attachment.filePath;
if (attachment.userAgent) {
node.userAgent = attachment.userAgent;
}
} else if (attachment.streamSource) {
node.streamSource = attachment.streamSource;
}
return node;
};
/**
* <p>Creates a mime tree node for an alternative text component (ODF/DOC/etc)</p>
*
* @param {Object} alternative Alternative info for the component
* @return {Object} Mime tree node
*/
MailComposer.prototype._createAlternativeComponent = function(alternative) {
var node = this._createMimeNode(),
contentType = alternative.contentType.split(";").map(function(part) {
return (part || "").trim();
});
node.contentEncoding = alternative.contentEncoding || "base64";
if (["7bit", "8bit", "binary"].indexOf(node.contentEncoding) >= 0) {
node.textFormat = "flowed";
contentType.push("format=" + node.textFormat);
}
if (alternative.charset) {
contentType.push("charset=" + alternative.charset);
}
if (alternative.method) {
contentType.push("method=" + alternative.method);
}
node.headers.push(["Content-Type", contentType.join("; ")]);
node.headers.push(["Content-Transfer-Encoding", node.contentEncoding]);
node.contents = alternative.contents;
return node;
};
/**
* <p>Creates an empty mime tree node</p>
*
* @return {Object} Mime tree node
*/
MailComposer.prototype._createMimeNode = function() {
return {
childNodes: [],
headers: [],
parentNode: null
};
};
/**
* <p>Compiles headers object into an array of header lines. If needed, the
* lines are folded</p>
*
* @param {Object|Array} headers An object with headers in the form of
* <code>{key:value}</code> or <ocde>[[key, value]]</code> or
* <code>[{key:key, value: value}]</code>
* @return {Array} A list of header lines. Can be joined with \r\n
*/
MailComposer.prototype.compileHeaders = function(headers) {
var headersArr = [];
if (Array.isArray(headers)) {
headersArr = headers.map((function(field) {
var key = this._normalizeKey(field.key || field[0]),
value = field.value || field[1];
if (typeof value == "string") {
value = {
formatted: false,
value: value
};
}
if (value.formatted) {
return key + ": " + value.value;
}
return mimelib.foldLine(key + ": " + this._sanitizeHeaderValue(value.value), 76, false, false, 52);
}).bind(this));
} else {
Object.keys(headers).forEach(function(key) {
key = this._normalizeKey(key);
headersArr = headersArr.concat([].concat(headers[key]).map((function(field) {
if (typeof field == "string") {
field = {
formatted: false,
value: field
};
}
if (field.formatted) {
return key + ": " + field.value;
}
return mimelib.foldLine(key + ": " + this._sanitizeHeaderValue(field.value), 76, false, false, 52);
}).bind(this)));
}.bind(this));
}
return headersArr;
};
/**
* Sanitizes header value by replacing newlines with spaces
*
* @param {String} value Header line value
* @return {String} sanitized header line value
*/
MailComposer.prototype._sanitizeHeaderValue = function(value) {
value = (value || "").toString().
replace(/(\r?\n|\r)\s?/g, "").
replace(/%0[ad]/gi, "");
return value;
};
/**
* <p>Converts a structured mimetree into an one dimensional array of
* components. This includes headers and multipart boundaries as strings,
* textual and attachment contents are.</p>
*/
MailComposer.prototype._flattenMimeTree = function() {
var flatTree = [];
var walkTree = (function(node, level) {
var contentObject = {};
level = level || 0;
// if not root element, include headers
if (level) {
flatTree = flatTree.concat(this.compileHeaders(node.headers));
flatTree.push('');
}
if (node.textFormat) {
contentObject.textFormat = node.textFormat;
}
if (node.contentEncoding) {
contentObject.contentEncoding = node.contentEncoding;
}
if (node.contents) {
contentObject.contents = node.contents;
} else if (node.filePath) {
contentObject.filePath = node.filePath;
if (node.userAgent) {
contentObject.userAgent = node.userAgent;
}
} else if (node.streamSource) {
contentObject.streamSource = node.streamSource;
}
if (node.contents || node.filePath || node.streamSource) {
flatTree.push(contentObject);
}
// walk children
for (var i = 0, len = node.childNodes.length; i < len; i++) {
if (node.boundary) {
flatTree.push("--" + node.boundary);
}
walkTree(node.childNodes[i], level + 1);
}
if (node.boundary && node.childNodes.length) {
flatTree.push("--" + node.boundary + "--");
flatTree.push('');
}
}).bind(this);
walkTree(this._message.tree);
if (flatTree.length && flatTree[flatTree.length - 1] === '') {
flatTree.pop();
}
this._message.flatTree = flatTree;
};
/**
* <p>Composes the e-mail body based on the previously generated mime tree</p>
*
* <p>Assumes that the linebreak separating headers and contents is already
* sent</p>
*
* <p>Emits 'data' events</p>
*/
MailComposer.prototype._composeBody = function() {
var flatTree = this._message.flatTree,
slice, isObject = false,
isEnd = false,
curObject;
var emitElementCallback = function(isEnd) {
if (!isEnd) {
if (typeof setImmediate == "function") {
setImmediate(this._composeBody.bind(this));
} else {
process.nextTick(this._composeBody.bind(this));
}
} else {
if (!this._cacheOutput) {
this.emit("end");
} else {
this._processBufferedOutput();
}
}
};
this._message.processingStart = this._message.processingStart || 0;
this._message.processingPos = this._message.processingPos || 0;
for (var len = flatTree.length; this._message.processingPos < len; this._message.processingPos++) {
isEnd = this._message.processingPos >= len - 1;
isObject = typeof flatTree[this._message.processingPos] == "object";
if (isEnd || isObject) {
slice = flatTree.slice(this._message.processingStart, isEnd && !isObject ? undefined : this._message.processingPos);
if (slice && slice.length) {
if (!this._cacheOutput) {
this.emit("data", new Buffer(slice.join(this._lineEnding) + this._lineEnding, "utf-8"));
} else {
this._outputBuffer += slice.join(this._lineEnding) + this._lineEnding;
}
}
if (isObject) {
curObject = flatTree[this._message.processingPos];
this._message.processingPos++;
this._message.processingStart = this._message.processingPos;
this._emitDataElement(curObject, emitElementCallback.bind(this, isEnd));
} else if (isEnd) {
if (!this._cacheOutput) {
this.emit("end");
} else {
this._processBufferedOutput();
}
}
break;
}
}
};
/**
* <p>Emits a data event for a text or html body and attachments. If it is a
* file, stream it</p>
*
* <p>If <code>this.options.escapeSMTP</code> is true, replace dots in the
* beginning of a line with double dots - only valid for QP encoding</p>
*
* @param {Object} element Data element descriptor
* @param {Function} callback Callback function to run when completed
*/
MailComposer.prototype._emitDataElement = function(element, callback) {
var data = "";
if (element.contents) {
switch (element.contentEncoding) {
case "quoted-printable":
data = mimelib.encodeQuotedPrintable(element.contents, false, this.options.charset);
break;
case "base64":
data = new Buffer(element.contents, "utf-8").toString("base64").replace(/.{76}/g, "$&\r\n");
break;
//case "7bit":
//case "8bit":
//case "binary":
default:
data = mimelib.foldLine(element.contents, 76, false, element.textFormat == "flowed");
//mimelib puts a long whitespace to the beginning of the lines
data = data.replace(/^[ ]{7}/mg, "");
break;
}
if (this.options.escapeSMTP) {
data = data.replace(/^\./gm, '..');
}
if (!this._cacheOutput) {
this.emit("data", new Buffer(data + this._lineEnding, "utf-8"));
} else {
this._outputBuffer += data + this._lineEnding;
}
if (typeof setImmediate == "function") {
setImmediate(callback);
} else {
process.nextTick(callback);
}
return;
}
if (element.filePath) {
if (element.filePath.match(/^https?:\/\//)) {
this._serveStream(urlFetch(element.filePath, {
userAgent: element.userAgent
}), callback);
} else {
this._serveFile(element.filePath, callback);
}
return;
} else if (element.streamSource) {
this._serveStream(element.streamSource, callback);
return;
}
callback();
};
/**
* <p>Pipes a file to the e-mail stream</p>
*
* @param {String} filePath Path to the file
* @param {Function} callback Callback function to run after completion
*/
MailComposer.prototype._serveFile = function(filePath, callback) {
fs.stat(filePath, (function(err, stat) {
if (err || !stat.isFile()) {
if (!this._cacheOutput) {
this.emit("data", new Buffer(new Buffer("<ERROR OPENING FILE>",
"utf-8").toString("base64") + this._lineEnding, "utf-8"));
} else {
this._outputBuffer += new Buffer("<ERROR OPENING FILE>",
"utf-8").toString("base64") + this._lineEnding;
}
if (typeof setImmediate == "function") {
setImmediate(callback);
} else {
process.nextTick(callback);
}
return;
}
var stream = fs.createReadStream(filePath);
this._serveStream(stream, callback);
}).bind(this));
};
/**
* <p>Pipes a stream source to the e-mail stream</p>
*
* <p>This function resumes the stream and starts sending 76 bytes long base64
* encoded lines. To achieve this, the incoming stream is divded into
* chunks of 57 bytes (57/3*4=76) to achieve exactly 76 byte long
* base64</p>
*
* @param {Object} stream Stream to be piped
* @param {Function} callback Callback function to run after completion
*/
MailComposer.prototype._serveStream = function(stream, callback) {
var remainder = new Buffer(0);
this._currentStream = stream;
stream.on("error", (function(error) {
if (!this._cacheOutput) {
this.emit("data", new Buffer(new Buffer("<ERROR READING STREAM>\n" + error.message,
"utf-8").toString("base64") + this._lineEnding, "utf-8"));
} else {
this._outputBuffer += new Buffer("<ERROR READING STREAM>\n" + error.message,
"utf-8").toString("base64") + this._lineEnding;
}
this._currentStream = false;
if (typeof setImmediate == "function") {
setImmediate(callback);
} else {
process.nextTick(callback);
}
}).bind(this));
stream.on("data", (function(chunk) {
var data = "",
len = remainder.length + chunk.length,
remainderLength = len % 57, // we use 57 bytes as it composes
// a 76 bytes long base64 string
buffer = new Buffer(len);
remainder.copy(buffer); // copy remainder into the beginning of the new buffer
chunk.copy(buffer, remainder.length); // copy data chunk after the remainder
remainder = buffer.slice(len - remainderLength); // create a new remainder
data = buffer.slice(0, len - remainderLength).toString("base64").replace(/.{76}/g, "$&\r\n");
if (data.length) {
if (!this._cacheOutput) {
this.emit("data", new Buffer(data.trim() + this._lineEnding, "utf-8"));
} else {
this._outputBuffer += data.trim() + this._lineEnding;
}
}
}).bind(this));
stream.on("end", (function() {
var data;
// stream the remainder (if any)
if (remainder.length) {
data = remainder.toString("base64").replace(/.{76}/g, "$&\r\n");
if (!this._cacheOutput) {
this.emit("data", new Buffer(data.trim() + this._lineEnding, "utf-8"));
} else {
this._outputBuffer += data.trim() + this._lineEnding;
}
}
this._currentStream = false;
if (typeof setImmediate == "function") {
setImmediate(callback);
} else {
process.nextTick(callback);
}
}).bind(this));
// resume streaming if paused
stream.resume();
};
/**
* <p>Processes buffered output and emits 'end'</p>
*/
MailComposer.prototype._processBufferedOutput = function() {
var dkimSignature;
if (this._dkim) {
if ((dkimSignature = runDKIMSign(this.options.escapeSMTP ? this._outputBuffer.replace(/^\.\./mg, ".") : this._outputBuffer, this._dkim))) {
this.emit("data", new Buffer(dkimSignature + this._lineEnding, "utf-8"));
}
}
this.emit("data", new Buffer(this._outputBuffer, "utf-8"));
if (typeof setImmediate == "function") {
setImmediate(this.emit.bind(this, "end"));
} else {
process.nextTick(this.emit.bind(this, "end"));
}
};
/* HELPER FUNCTIONS */
/**
* <p>Normalizes a key name by cpitalizing first chars of words, except for
* custom keys (starting with "X-") that have only uppercase letters, which will
* not be modified.</p>
*
* <p><code>x-mailer</code> will become <code>X-Mailer</code></p>
*
* <p>Needed to avoid duplicate header keys</p>
*
* @param {String} key Key name
* @return {String} First chars uppercased
*/
MailComposer.prototype._normalizeKey = function(key) {
key = (key || "").toString().trim();
// If only uppercase letters, leave everything as is
if (key.match(/^X\-[A-Z0-9\-]+$/)) {
return key;
}
// Convert first letter upper case, others lower case
return key.
toLowerCase().
replace(/^\S|[\-\s]\S/g, function(c) {
return c.toUpperCase();
}).
replace(/^MIME\-/i, "MIME-").
replace(/^DKIM\-/i, "DKIM-");
};
/**
* <p>Tests if a string has high bit (UTF-8) symbols</p>
*
* @param {String} str String to be tested for high bit symbols
* @return {Boolean} true if high bit symbols were found
*/
MailComposer.prototype._hasUTFChars = function(str) {
var rforeign = /[^\u0000-\u007f]/;
return !!rforeign.test(str);
};
/**
* <p>Generates a boundary for multipart bodies</p>
*
* @return {String} Boundary String
*/
MailComposer.prototype._generateBoundary = function() {
// "_" is not allowed in quoted-printable and "?" not in base64
return "----" + this.options.identityString + "-?=_" + (++this._gencounter) + "-" + Date.now();
};
/**
* <p>Converts a string to mime word format. If the length is longer than
* <code>maxlen</code>, split it</p>
*
* <p>If the string doesn't have any unicode characters return the original
* string instead</p>
*
* @param {String} str String to be encoded
* @param {String} encoding Either Q for Quoted-Printable or B for Base64
* @param {Number} [maxlen] Optional length of the resulting string, whitespace will be inserted if needed
*
* @return {String} Mime-word encoded string (if needed)
*/
MailComposer.prototype._encodeMimeWord = function(str, encoding, maxlen) {
return mimelib.encodeMimeWords(str, encoding, maxlen, this.options.charset);
};
/**
* <p>Converts a partial string to mime word format. If the length is longer than
* <code>maxlen</code>, split it</p>
*
* <p>If the string doesn't have any unicode characters return the original
* string instead</p>
*
* @param {String} str String to be encoded
* @param {String} encoding Either Q for Quoted-Printable or B for Base64
* @param {Number} [maxlen] Optional length of the resulting string, whitespace will be inserted if needed
*
* @return {String} Mime-word encoded string (if needed)
*/
MailComposer.prototype._encodePartialMimeWord = function(str, encoding, maxlen) {
return str.replace(/[^\s]*[^\s\w\?!*=+-]+[^\s]*(\s+[^\s]*[^\s\w\?!*=+-]+[^\s]*)*/g, (function(str) {
if (!str.length) {
return '';
}
return mimelib.encodeMimeWord(str, encoding, this.options.charset, maxlen)
.replace(/[^\w\s\?!*=+-]/g, function(chr) {
var code = chr.charCodeAt(0);
return "=" + (code < 0x10 ? "0" : "") + code.toString(16).toUpperCase();
});
}).bind(this));
};
/**
* <p>Splits a mime-encoded string</p>
*
* @param {String} str Input string
* @param {Number} maxlen Maximum line length
* @return {Array} split string
*/
MailComposer.prototype._splitEncodedString = function(str, maxlen) {
var curLine, match, chr, done,
lines = [];
while (str.length) {
curLine = str.substr(0, maxlen);
// move incomplete escaped char back to main
if ((match = curLine.match(/\=[0-9A-F]?$/i))) {
curLine = curLine.substr(0, match.index);
}
done = false;
while (!done) {
done = true;
// check if not middle of a unicode char sequence
if ((match = str.substr(curLine.length).match(/^\=([0-9A-F]{2})/i))) {
chr = parseInt(match[1], 16);
// invalid sequence, move one char back anc recheck
if (chr < 0xC2 && chr > 0x7F) {
curLine = curLine.substr(0, curLine.length - 3);
done = false;
}
}
}
if (curLine.length) {
lines.push(curLine);
}
str = str.substr(curLine.length);
}
return lines;
};
/**
* Detects image urls and paths from HTML code and replaces with attachments
* for embedding images inline
*/
MailComposer.prototype._convertImagesToInline = function() {
if (!this._message.html) {
return;
}
this._message.html = this._message.html.replace(/<img[^>]*>/gi, (function(imgTag) {
return imgTag.replace(/\b(src\s*=\s*(?:['"]?))([^'"> ]+)/i, (function(src, prefix, url) {
var cid;
url = he.decode(url || "").trim();
prefix = prefix || "";
if (url.match(/^https?:\/\//i) || url.match(/^\//i) || url.match(/^[a-z]:\\/i)) {
cid = crypto.randomBytes(20).toString("hex") + "@" + this.options.identityString;
this.addAttachment({
filePath: url,
cid: cid
});
url = "cid:" + cid;
}
return prefix + url;
}).bind(this));
}).bind(this));
};