aboutsummaryrefslogtreecommitdiffstats
path: root/tools/node_modules/nodemailer/node_modules/mailcomposer/lib/mailcomposer.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/node_modules/nodemailer/node_modules/mailcomposer/lib/mailcomposer.js')
-rw-r--r--tools/node_modules/nodemailer/node_modules/mailcomposer/lib/mailcomposer.js1260
1 files changed, 1260 insertions, 0 deletions
diff --git a/tools/node_modules/nodemailer/node_modules/mailcomposer/lib/mailcomposer.js b/tools/node_modules/nodemailer/node_modules/mailcomposer/lib/mailcomposer.js
new file mode 100644
index 0000000..fd229ba
--- /dev/null
+++ b/tools/node_modules/nodemailer/node_modules/mailcomposer/lib/mailcomposer.js
@@ -0,0 +1,1260 @@
+var Stream = require("stream").Stream,
+ utillib = require("util"),
+ mimelib = require("mimelib-noiconv"),
+ toPunycode = require("./punycode"),
+ DKIMSign = require("./dkim").DKIMSign,
+ urlFetch = require("./urlfetch"),
+ fs = require("fs");
+
+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>keepBcc</b> - include Bcc: field in the message headers (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.options = options || {};
+
+ 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 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>Counter for generating unique mime boundaries etc.</p>
+ * @private
+ */
+ this._gencounter = 0;
+
+ this.addHeader("MIME-Version", "1.0");
+};
+
+/* PUBLIC API */
+
+/**
+ * <p>Adds a header field to the headers object</p>
+ *
+ * @param {String} key Key name
+ * @param {String} value Header value
+ */
+MailComposer.prototype.addHeader = function(key, value){
+ key = this._normalizeKey(key);
+
+ if(value && Object.prototype.toString.call(value) == "[object Object]"){
+ value = this._encodeMimeWord(JSON.stringify(value), "Q", 50);
+ }else{
+ value = (value || "").toString().trim();
+ }
+
+ if(!key || !value){
+ return;
+ }
+
+ 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 &lt;sender@server.com&gt;</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 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 || {};
+ var filename;
+
+ // 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){
+ filename = attachment.fileName || attachment.filePath;
+ if(filename){
+ attachment.contentType = this._getMimeType(filename);
+ }else{
+ attachment.contentType = "application/octet-stream";
+ }
+ }
+
+ 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(){
+ process.nextTick(this._composeMessage.bind(this));
+};
+
+/* 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 = mimelib.parseAddresses(value);
+ if(!this._envelope.userDefined){
+ this._envelope[key] = addresses.map((function(address){
+ if(this._hasUTFChars(address.address)){
+ return toPunycode(address.address);
+ }else{
+ return address.address;
+ }
+ }).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", 50);
+
+ 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){
+
+ // 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");
+ }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){
+ values.push(address.address);
+ }else if(address.name){
+ if(this._hasUTFChars(address.name)){
+ address.name = this._encodeMimeWord(address.name, "Q", 50);
+ }else{
+ address.name = address.name;
+ }
+ values.push('"' + address.name+'" <'+address.address+'>');
+ }
+ }
+ }
+ 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 = this._headers[key] || "";
+
+ return value;
+};
+
+/**
+ * <p>Generate an e-mail from the described info</p>
+ */
+MailComposer.prototype._composeMessage = function(){
+
+ // 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(this._message.body && this._message.html){
+ 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 = "\r\n";
+ }
+
+ this._buildMessageHeaders();
+ this._generateBodyStructure();
+
+ // Compile header lines
+ headers = this.compileHeaders(this._headers);
+
+ if(!this._cacheOutput){
+ this.emit("data", new Buffer(headers.join("\r\n")+"\r\n\r\n", "utf-8"));
+ }else{
+ this._outputBuffer += headers.join("\r\n")+"\r\n\r\n";
+ }
+};
+
+/**
+ * <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));
+ }
+
+ // 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;
+ }
+ }
+
+ // Related attachments are added to the multipart/related part
+ if(this._relatedAttachments && this._relatedAttachments){
+ 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=utf-8");
+
+ 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 a text component (plaintext, HTML)</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";
+ node.useAttachmentType = true;
+
+ 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 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 = [], keys, key;
+
+ if(Array.isArray(headers)){
+ headersArr = headers.map(function(field){
+ return mimelib.foldLine((field.key || field[0])+": "+(field.value || field[1]));
+ });
+ }else{
+ keys = Object.keys(headers);
+ for(var i=0, len = keys.length; i<len; i++){
+ key = this._normalizeKey(keys[i]);
+
+ headersArr = headersArr.concat([].concat(headers[key]).map(function(field){
+ return mimelib.foldLine(key+": "+field);
+ }));
+ }
+ }
+
+ return headersArr;
+};
+
+/**
+ * <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 = [];
+
+ function walkTree(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.call(this, node.childNodes[i], level+1);
+ }
+ if(node.boundary && node.childNodes.length){
+ flatTree.push("--"+node.boundary+"--");
+ flatTree.push('');
+ }
+ }
+
+ walkTree.call(this, 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;
+
+ 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("\r\n")+"\r\n", "utf-8"));
+ }else{
+ this._outputBuffer += slice.join("\r\n")+"\r\n";
+ }
+ }
+
+ if(isObject){
+ curObject = flatTree[this._message.processingPos];
+
+ this._message.processingPos++;
+ this._message.processingStart = this._message.processingPos;
+
+ this._emitDataElement(curObject, (function(){
+ if(!isEnd){
+ process.nextTick(this._composeBody.bind(this));
+ }else{
+ if(!this._cacheOutput){
+ this.emit("end");
+ }else{
+ this._processBufferedOutput();
+ }
+ }
+ }).bind(this));
+
+ }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);
+ 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, 78, 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 + "\r\n", "utf-8"));
+ }else{
+ this._outputBuffer += data + "\r\n";
+ }
+ 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")+"\r\n", "utf-8"));
+ }else{
+ this._outputBuffer += new Buffer("<ERROR OPENING FILE>",
+ "utf-8").toString("base64")+"\r\n";
+ }
+
+ 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);
+
+ stream.on("error", (function(error){
+ if(!this._cacheOutput){
+ this.emit("data", new Buffer(new Buffer("<ERROR READING STREAM>",
+ "utf-8").toString("base64")+"\r\n", "utf-8"));
+ }else{
+ this._outputBuffer += new Buffer("<ERROR READING STREAM>",
+ "utf-8").toString("base64")+"\r\n";
+ }
+ 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()+"\r\n", "utf-8"));
+ }else{
+ this._outputBuffer += data.trim()+"\r\n";
+ }
+ }
+ }).bind(this));
+
+ stream.on("end", (function(chunk){
+ 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()+"\r\n", "utf-8"));
+ }else{
+ this._outputBuffer += data.trim()+"\r\n";
+ }
+ }
+ 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 = DKIMSign(this._outputBuffer, this._dkim))){
+ this.emit("data", new Buffer(dkimSignature+"\r\n", "utf-8"));
+ }
+ }
+
+ this.emit("data", new Buffer(this._outputBuffer, "utf-8"));
+
+ 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 "----mailcomposer-?=_"+(++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){
+
+ // adjust maxlen by =?UTF-8?Q??=
+ if(maxlen && maxlen>12){
+ maxlen -= 12;
+ }
+
+ encoding = (encoding || "Q").toUpperCase();
+ if(this._hasUTFChars(str)){
+ str = mimelib.encodeMimeWord(str, encoding);
+ if(maxlen && str.length>maxlen){
+ if(encoding=="Q"){
+ return "=?UTF-8?Q?"+this._splitEncodedString(str.split("?")[3], maxlen).join("?= =?UTF-8?Q?")+"?=";
+ }else{
+ return "=?UTF-8?"+encoding+"?"+str.split("?")[3].replace(new RegExp(".{"+maxlen+"}","g"),"$&?= =?UTF-8?"+encoding+"?")+"?=";
+ }
+ }else{
+ return str;
+ }
+ }else{
+ return str;
+ }
+};
+
+/**
+ * <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;
+};
+
+
+/**
+ * <p>Resolves a mime type for a filename</p>
+ *
+ * @param {String} filename Filename to check
+ * @return {String} Corresponding mime type
+ */
+MailComposer.prototype._getMimeType = function(filename){
+ var defaultMime = "application/octet-stream",
+ extension = filename && filename.substr(filename.lastIndexOf(".")+1).trim().toLowerCase();
+ return extension && mimelib.contentTypes[extension] || defaultMime;
+}; \ No newline at end of file