%PDF- %PDF-
Direktori : /home/opti/workflow/node_modules/mailcomposer/lib/ |
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)); };