1 var Stream = require("stream").Stream,
  2     utillib = require("util"),
  3     mimelib = require("mimelib-noiconv"),
  4     toPunycode = require("./punycode"),
  5     fs = require("fs");
  6 
  7 module.exports.MailComposer = MailComposer;
  8 
  9 /**
 10  * <p>Costructs a MailComposer object. This is a Stream instance so you could
 11  * pipe the output to a file or send it to network.</p>
 12  * 
 13  * <p>Possible options properties are:</p>
 14  * 
 15  * <ul>
 16  *     <li><b>escapeSMTP</b> - convert dots in the beginning of line to double dots</li>
 17  *     <li><b>encoding</b> - forced transport encoding (quoted-printable, base64, 7bit or 8bit)</li>
 18  *     <li><b>keepBcc</b> - include Bcc: field in the message headers (default is false)</li>
 19  * </ul>
 20  * 
 21  * <p><b>Events</b></p>
 22  * 
 23  * <ul>
 24  *     <li><b>'envelope'</b> - emits an envelope object with <code>from</code> and <code>to</code> (array) addresses.</li>
 25  *     <li><b>'data'</b> - emits a chunk of data</li>
 26  *     <li><b>'end'</b> - composing the message has ended</li>
 27  * </ul>
 28  * 
 29  * @constructor
 30  * @param {Object} [options] Optional options object
 31  */
 32 function MailComposer(options){
 33     Stream.call(this);
 34     
 35     this.options = options || {};
 36     
 37     this._init();
 38 }
 39 utillib.inherits(MailComposer, Stream);
 40 
 41 /**
 42  * <p>Resets and initializes MailComposer</p>
 43  */
 44 MailComposer.prototype._init = function(){
 45     /**
 46      * <p>Contains all header values</p>
 47      * @private
 48      */
 49     this._headers = {};
 50     
 51     /**
 52      * <p>Contains message related values</p>
 53      * @private
 54      */
 55     this._message = {};
 56     
 57     /**
 58      * <p>Contains a list of attachments</p>
 59      * @private
 60      */
 61     this._attachments = [];
 62     
 63     /**
 64      * <p>Contains e-mail addresses for the SMTP</p>
 65      * @private
 66      */
 67     this._envelope = {};
 68     
 69     /**
 70      * <p>Counter for generating unique mime boundaries etc.</p>
 71      * @private
 72      */
 73     this._gencounter = 0;
 74     
 75     this.addHeader("MIME-Version", "1.0");
 76 };
 77 
 78 /* PUBLIC API */
 79 
 80 /**
 81  * <p>Adds a header field to the headers object</p>
 82  * 
 83  * @param {String} key Key name
 84  * @param {String} value Header value
 85  */
 86 MailComposer.prototype.addHeader = function(key, value){
 87     key = this._normalizeKey(key);
 88     value = (value || "").toString().trim();
 89     if(!key || !value){
 90         return;
 91     }
 92     
 93     if(!(key in this._headers)){
 94         this._headers[key] = value;
 95     }else{
 96         if(!Array.isArray(this._headers[key])){
 97             this._headers[key] = [this._headers[key], value];
 98         }else{
 99             this._headers[key].push(value);
100         }
101     }
102 };
103 
104 /**
105  * <p>Resets and initializes MailComposer</p>
106  * 
107  * <p>Setting an option overwrites an earlier setup for the same keys</p>
108  * 
109  * <p>Possible options:</p>
110  * 
111  * <ul>
112  *     <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>
113  *     <li><b>to</b> - Comma separated list of recipients e-mail addresses that will appear on the <code>To:</code> field</li>
114  *     <li><b>cc</b> - Comma separated list of recipients e-mail addresses that will appear on the <code>Cc:</code> field</li>
115  *     <li><b>bcc</b> - Comma separated list of recipients e-mail addresses that will appear on the <code>Bcc:</code> field</li>
116  *     <li><b>replyTo</b> - An e-mail address that will appear on the <code>Reply-To:</code> field</li>
117  *     <li><b>subject</b> - The subject of the e-mail</li>
118  *     <li><b>body</b> - The plaintext version of the message</li>
119  *     <li><b>html</b> - The HTML version of the message</li>
120  * </ul>
121  * 
122  * @param {Object} options Message related options
123  */
124 MailComposer.prototype.setMessageOption = function(options){
125     var fields = ["from", "to", "cc", "bcc", "replyTo", "subject", "body", "html"],
126         rewrite = {"sender":"from", "reply_to":"replyTo", "text":"body"};
127     
128     options = options || {};
129     
130     var keys = Object.keys(options), key, value;
131     for(var i=0, len=keys.length; i<len; i++){
132         key = keys[i];
133         value = options[key];
134         
135         if(key in rewrite){
136             key = rewrite[key];
137         }
138         
139         if(fields.indexOf(key) >= 0){
140             this._message[key] = this._handleValue(key, value);
141         }
142     }
143 };
144 
145 /**
146  * <p>Adds an attachment to the list</p>
147  * 
148  * <p>Following options are allowed:</p>
149  * 
150  * <ul>
151  *     <li><b>fileName</b> - filename for the attachment</li>
152  *     <li><b>contentType</b> - content type for the attachmetn (default will be derived from the filename)</li>
153  *     <li><b>cid</b> - Content ID value for inline images</li>
154  *     <li><b>contents</b> - String or Buffer attachment contents</li>
155  *     <li><b>filePath</b> - Path to a file for streaming</li>
156  * </ul>
157  * 
158  * <p>One of <code>contents</code> or <code>filePath</code> must be specified, otherwise
159  * the attachment is not included</p>
160  * 
161  * @param {Object} attachment Attachment info
162  */
163 MailComposer.prototype.addAttachment = function(attachment){
164     attachment = attachment || {};
165     var filename;
166     
167     // Needed for Nodemailer compatibility
168     if(attachment.filename){
169         attachment.fileName = attachment.filename;
170         delete attachment.filename;
171     }
172     
173     if(!attachment.contentType){
174         filename = attachment.fileName || attachment.filePath;
175         if(filename){
176             attachment.contentType = this._getMimeType(filename);
177         }else{
178             attachment.contentType = "application/octet-stream";
179         }
180     }
181     
182     if(attachment.filePath || attachment.contents){
183         this._attachments.push(attachment);
184     }
185 };
186 
187 /**
188  * <p>Starts streaming the message</p>
189  */
190 MailComposer.prototype.streamMessage = function(){
191     process.nextTick(this._composeMessage.bind(this));
192 };
193 
194 /* PRIVATE API */
195 
196 /**
197  * <p>Handles a message object value, converts addresses etc.</p>
198  * 
199  * @param {String} key Message options key
200  * @param {String} value Message options value
201  * @return {String} converted value
202  */
203 MailComposer.prototype._handleValue = function(key, value){
204     key = (key || "").toString();
205     
206     var addresses;
207     
208     switch(key){
209         case "from":
210         case "to":
211         case "cc":
212         case "bcc":
213         case "replyTo":
214             value = (value || "").toString().replace(/\r?\n|\r/g, " ");
215             addresses = mimelib.parseAddresses(value);
216             this._envelope[key] = addresses.map((function(address){
217                 if(this._hasUTFChars(address.address)){
218                     return toPunycode(address.address);
219                 }else{
220                     return address.address;
221                 }
222             }).bind(this));
223             return this._convertAddresses(addresses);
224         case "subject":
225             value = (value || "").toString().replace(/\r?\n|\r/g, " ");
226             return this._encodeMimeWord(value, "Q", 78);
227     }
228     
229     return value;
230 };
231 
232 /**
233  * <p>Handles a list of parsed e-mail addresses, checks encoding etc.</p>
234  * 
235  * @param {Array} value A list or single e-mail address <code>{address:'...', name:'...'}</code>
236  * @return {String} Comma separated and encoded list of addresses
237  */
238 MailComposer.prototype._convertAddresses = function(addresses){
239     var values = [], address;
240     
241     for(var i=0, len=addresses.length; i<len; i++){
242         address = addresses[i];
243         
244         if(address.address){
245         
246             // if user part of the address contains foreign symbols
247             // make a mime word of it
248             address.address = address.address.replace(/^.*?(?=\@)/, (function(user){
249                 if(this._hasUTFChars(user)){
250                     return mimelib.encodeMimeWord(user, "Q");
251                 }else{
252                     return user;
253                 }
254             }).bind(this));
255             
256             // If there's still foreign symbols, then punycode convert it
257             if(this._hasUTFChars(address.address)){
258                 address.address = toPunycode(address.address);
259             }
260         
261             if(!address.name){
262                 values.push(address.address);
263             }else if(address.name){
264                 if(this._hasUTFChars(address.name)){
265                     address.name = this._encodeMimeWord(address.name, "Q", 78);
266                 }else{
267                     address.name = address.name;
268                 }
269                 values.push('"' + address.name+'" <'+address.address+'>');
270             }
271         }
272     }
273     return values.join(", ");
274 };
275 
276 
277 
278 /**
279  * <p>Gets a header field</p>
280  * 
281  * @param {String} key Key name
282  * @return {String|Array} Header field - if several values, then it's an array
283  */
284 MailComposer.prototype._getHeader = function(key){
285     var value;
286     
287     key = this._normalizeKey(key);
288     value = this._headers[key] || "";
289     
290     return value;
291 };
292 
293 
294 
295 
296 /**
297  * <p>Generate an e-mail from the described info</p>
298  */
299 MailComposer.prototype._composeMessage = function(){
300     
301     //Emit addresses for the SMTP client
302     this._composeEnvelope();
303     
304     // Generate headers for the message
305     this._composeHeader();
306     
307     // Make the mime tree flat
308     this._flattenMimeTree();
309     
310     // Compose message body
311     this._composeBody();
312     
313 };
314 
315 /**
316  * <p>Composes and emits an envelope from the <code>this._envelope</code> 
317  * object. Needed for the SMTP client</p>
318  * 
319  * <p>Emitted envelope is int hte following structure:</p>
320  * 
321  * <pre>
322  * {
323  *     to: "address",
324  *     from: ["list", "of", "addresses"]
325  * }
326  * </pre>
327  * 
328  * <p>Both properties (<code>from</code> and <code>to</code>) are optional
329  * and may not exist</p>
330  */
331 MailComposer.prototype._composeEnvelope = function(){
332     var envelope = {},
333         toKeys = ["to", "cc", "bcc"],
334         key;
335     
336     // If multiple addresses, only use the first one
337     if(this._envelope.from && this._envelope.from.length){
338         envelope.from = [].concat(this._envelope.from).shift();
339     }
340     
341     for(var i=0, len=toKeys.length; i<len; i++){
342         key = toKeys[i];
343         if(this._envelope[key] && this._envelope[key].length){
344             if(!envelope.to){
345                 envelope.to = [];
346             }
347             envelope.to = envelope.to.concat(this._envelope[key]);
348         }
349     }
350     
351     this.emit("envelope", envelope);
352 };
353 
354 /**
355  * <p>Composes a header for the message and emits it with a <code>'data'</code>
356  * event</p>
357  * 
358  * <p>Also checks and build a structure for the message (is it a multipart message
359  * and does it need a boundary etc.)</p>
360  * 
361  * <p>By default the message is not a multipart. If the message containes both
362  * plaintext and html contents, an alternative block is used. it it containes
363  * attachments, a mixed block is used. If both alternative and mixed exist, then
364  * alternative resides inside mixed.</p>
365  */
366 MailComposer.prototype._composeHeader = function(){
367     var headers = [];
368 
369     if(this._attachments.length){
370         this._message.useMixed = true;
371         this._message.mixedBoundary = this._generateBoundary();
372     }else{
373         this._message.useMixed = false;
374     }
375     
376     if(this._message.body && this._message.html){
377         this._message.useAlternative = true;
378         this._message.alternativeBoundary = this._generateBoundary();
379     }else{
380         this._message.useAlternative = false;
381     }
382     
383     if(!this._message.html && !this._message.body){
384         // If there's nothing to show, show a linebreak
385         this._message.body = "\r\n";
386     }
387     
388     this._buildMessageHeaders();
389     this._generateBodyStructure();
390     
391     // Compile header lines
392     headers = this.compileHeaders(this._headers);
393     
394     this.emit("data", new Buffer(headers.join("\r\n")+"\r\n\r\n", "utf-8"));
395 };
396 
397 /**
398  * <p>Uses data from the <code>this._message</code> object to build headers</p>
399  */
400 MailComposer.prototype._buildMessageHeaders = function(){
401     var contentType;
402 
403     // FROM
404     if(this._message.from && this._message.from.length){
405         [].concat(this._message.from).forEach((function(from){
406             this.addHeader("From", from);
407         }).bind(this));
408     }
409     
410     // TO
411     if(this._message.to && this._message.to.length){
412         [].concat(this._message.to).forEach((function(to){
413             this.addHeader("To", to);
414         }).bind(this));
415     }
416     
417     // CC
418     if(this._message.cc && this._message.cc.length){
419         [].concat(this._message.cc).forEach((function(cc){
420             this.addHeader("Cc", cc);
421         }).bind(this));
422     }
423     
424     // BCC
425     // By default not included, set options.keepBcc to true to keep
426     if(this.options.keepBcc){
427         if(this._message.bcc && this._message.bcc.length){
428             [].concat(this._message.bcc).forEach((function(bcc){
429                 this.addHeader("Bcc", bcc);
430             }).bind(this));
431         }    
432     }
433     
434     // REPLY-TO
435     if(this._message.replyTo && this._message.replyTo.length){
436         [].concat(this._message.replyTo).forEach((function(replyTo){
437             this.addHeader("Reply-To", replyTo);
438         }).bind(this));
439     }
440     
441     // SUBJECT
442     if(this._message.subject){
443         this.addHeader("Subject", this._message.subject);
444     }
445 };
446 
447 /**
448  * <p>Generates the structure (mime tree) of the body. This sets up multipart
449  * structure, individual part headers, boundaries etc.</p>
450  * 
451  * <p>The headers of the root element will be appended to the message
452  * headers</p>
453  */
454 MailComposer.prototype._generateBodyStructure = function(){
455     // TODO: lõpetada
456     var tree = this._createMimeNode(), 
457         currentNode, node,
458         i, len;
459     
460     if(this._message.useMixed){
461         
462         node = this._createMimeNode();
463         node.boundary = this._message.mixedBoundary;
464         node.headers.push(["Content-Type", "multipart/mixed; boundary=\""+node.boundary+"\""]);
465         
466         if(currentNode){
467             currentNode.childNodes.push(node);
468             node.parentNode = currentNode;
469         }else{
470             tree = node;
471         }
472         currentNode = node;
473     
474     }
475     
476     if(this._message.useAlternative){
477     
478         node = this._createMimeNode();
479         node.boundary = this._message.alternativeBoundary;
480         node.headers.push(["Content-Type", "multipart/alternative; boundary=\""+node.boundary+"\""]);
481         if(currentNode){
482             currentNode.childNodes.push(node);
483             node.parentNode = currentNode;
484         }else{
485             tree = node;
486         }
487         currentNode = node;
488         
489     }
490     
491     if(this._message.body){
492         node = this._createTextComponent(this._message.body, "text/plain");
493         if(currentNode){
494             currentNode.childNodes.push(node);
495             node.parentNode = currentNode;
496         }else{
497             tree = node;
498         }
499     }
500     
501     if(this._message.html){
502         node = this._createTextComponent(this._message.html, "text/html");
503         if(currentNode){
504             currentNode.childNodes.push(node);
505             node.parentNode = currentNode;
506         }else{
507             tree = node;
508         }
509     }
510     
511     // Attachments are added to the first element (should be multipart/mixed)
512     currentNode = tree;
513     if(this._attachments){
514         for(i=0, len = this._attachments.length; i<len; i++){
515             node = this._createAttachmentComponent(this._attachments[i]);
516             node.parentNode = currentNode;
517             currentNode.childNodes.push(node);
518         }
519     }
520     
521     // Add the headers from the root element to the main headers list
522     for(i=0, len=tree.headers.length; i<len; i++){
523         this.addHeader(tree.headers[i][0], tree.headers[i][1]);
524     }
525     
526     this._message.tree = tree;
527 };
528 
529 /**
530  * <p>Creates a mime tree node for a text component (plaintext, HTML)</p>
531  * 
532  * @param {String} text Text contents for the component
533  * @param {String} [contentType="text/plain"] Content type for the text component
534  * @return {Object} Mime tree node 
535  */
536 MailComposer.prototype._createTextComponent = function(text, contentType){
537     var node = this._createMimeNode();
538     
539     node.contentEncoding = (this.options.encoding || "quoted-printable").toLowerCase().trim();
540     node.useTextType = true;
541     
542     contentType = [contentType || "text/plain"];
543     contentType.push("charset=utf-8");
544     
545     if(["7bit", "8bit", "binary"].indexOf(node.contentEncoding)>=0){
546         node.textFormat = "flowed";
547         contentType.push("format=" + node.textFormat);
548     }
549     
550     node.headers.push(["Content-Type", contentType.join("; ")]);
551     node.headers.push(["Content-Transfer-Encoding", node.contentEncoding]);
552     
553     node.contents = text;
554     
555     return node;
556 };
557 
558 /**
559  * <p>Creates a mime tree node for a text component (plaintext, HTML)</p>
560  * 
561  * @param {Object} attachment Attachment info for the component
562  * @return {Object} Mime tree node 
563  */
564 MailComposer.prototype._createAttachmentComponent = function(attachment){
565     var node = this._createMimeNode(),
566         contentType = [attachment.contentType],
567         contentDisposition = ["attachment"],
568         fileName;
569     
570     node.contentEncoding = "base64";
571     node.useAttachmentType = true;
572     
573     if(attachment.fileName){
574         fileName = this._encodeMimeWord(attachment.fileName, "Q", 1024).replace(/"/g,"\\\"");
575         contentType.push("name=\"" +fileName+ "\"");
576         contentDisposition.push("filename=\"" +fileName+ "\"");
577     }
578     
579     node.headers.push(["Content-Type", contentType.join("; ")]);
580     node.headers.push(["Content-Disposition", contentDisposition.join("; ")]);
581     node.headers.push(["Content-Transfer-Encoding", node.contentEncoding]);
582     
583     if(attachment.cid){
584         node.headers.push(["Content-Id", "<" + this._encodeMimeWord(attachment.cid) + ">"]);
585     }
586     
587     if(attachment.contents){
588         node.contents = attachment.contents;
589     }else if(attachment.filePath){
590         node.filePath = attachment.filePath;
591     }
592 
593     return node;
594 };
595 
596 /**
597  * <p>Creates an empty mime tree node</p>
598  * 
599  * @return {Object} Mime tree node
600  */
601 MailComposer.prototype._createMimeNode = function(){
602     return {
603         childNodes: [],
604         headers: [],
605         parentNode: null
606     };
607 };
608 
609 /**
610  * <p>Compiles headers object into an array of header lines. If needed, the
611  * lines are folded</p>
612  * 
613  * @param {Object|Array} headers An object with headers in the form of
614  *        <code>{key:value}</code> or <ocde>[[key, value]]</code> or
615  *        <code>[{key:key, value: value}]</code>
616  * @return {Array} A list of header lines. Can be joined with \r\n
617  */
618 MailComposer.prototype.compileHeaders = function(headers){
619     var headersArr = [], keys, key;
620     
621     if(Array.isArray(headers)){
622         headersArr = headers.map(function(field){
623             return mimelib.foldLine((field.key || field[0])+": "+(field.value || field[1]));
624         });
625     }else{
626         keys = Object.keys(headers);
627         for(var i=0, len = keys.length; i<len; i++){
628             key = this._normalizeKey(keys[i]);
629             
630             headersArr = headersArr.concat([].concat(headers[key]).map(function(field){
631                 return mimelib.foldLine(key+": "+field);
632             }));
633         }
634     }
635     
636     return headersArr;
637 };
638 
639 /**
640  * <p>Converts a structured mimetree into an one dimensional array of
641  * components. This includes headers and multipart boundaries as strings,
642  * textual and attachment contents are.</p>
643  */
644 MailComposer.prototype._flattenMimeTree = function(){
645     var flatTree = [];
646     
647     function walkTree(node, level){
648         var contentObject = {};
649         level = level || 0;
650         
651         // if not root element, include headers
652         if(level){
653             flatTree = flatTree.concat(this.compileHeaders(node.headers));
654             flatTree.push('');
655         }
656         
657         if(node.textFormat){
658             contentObject.textFormat = node.textFormat;
659         }
660         
661         if(node.contentEncoding){
662             contentObject.contentEncoding = node.contentEncoding;
663         }
664         
665         if(node.contents){
666             contentObject.contents = node.contents;
667         }
668         
669         if(node.filePath){
670             contentObject.filePath = node.filePath;
671         }
672         
673         if(node.contents || node.filePath){
674             flatTree.push(contentObject);
675         }
676         
677         // walk children
678         for(var i=0, len = node.childNodes.length; i<len; i++){
679             if(node.boundary){
680                 flatTree.push("--"+node.boundary);
681             }
682             walkTree.call(this, node.childNodes[i], level+1);
683         }
684         if(node.boundary && node.childNodes.length){
685             flatTree.push("--"+node.boundary+"--");
686             flatTree.push('');
687         }
688     }
689     
690     walkTree.call(this, this._message.tree);
691     
692     if(flatTree.length && flatTree[flatTree.length-1]===''){
693         flatTree.pop();
694     }
695     
696     this._message.flatTree = flatTree;
697 };
698 
699 /**
700  * <p>Composes the e-mail body based on the previously generated mime tree</p>
701  * 
702  * <p>Assumes that the linebreak separating headers and contents is already 
703  * sent</p>
704  * 
705  * <p>Emits 'data' events</p>
706  */
707 MailComposer.prototype._composeBody = function(){
708     var flatTree = this._message.flatTree,
709         slice, isObject = false, isEnd = false,
710         curObject;
711     
712     this._message.processingStart = this._message.processingStart || 0;
713     this._message.processingPos = this._message.processingPos || 0;
714 
715     for(len = flatTree.length; this._message.processingPos < len; this._message.processingPos++){
716         
717         isEnd = this._message.processingPos >= len-1;
718         isObject = typeof flatTree[this._message.processingPos] == "object";
719         
720         if(isEnd || isObject){
721             
722             slice = flatTree.slice(this._message.processingStart, isEnd && !isObject?undefined:this._message.processingPos);
723             if(slice && slice.length){
724                 this.emit("data", new Buffer(slice.join("\r\n")+"\r\n", "utf-8"));
725             }
726             
727             if(isObject){
728                 curObject = flatTree[this._message.processingPos];
729             
730                 this._message.processingPos++;
731                 this._message.processingStart = this._message.processingPos;
732             
733                 this._emitDataElement(curObject, (function(){
734                     if(!isEnd){
735                         process.nextTick(this._composeBody.bind(this));
736                     }else{
737                         this.emit("end");
738                     }
739                 }).bind(this));
740                 
741             }else if(isEnd){
742                 this.emit("end");
743             }
744             break;
745         }
746         
747     }
748 };
749 
750 /**
751  * <p>Emits a data event for a text or html body and attachments. If it is a 
752  * file, stream it</p>
753  * 
754  * <p>If <code>this.options.escapeSMTP</code> is true, replace dots in the
755  * beginning of a line with double dots - only valid for QP encoding</p>
756  * 
757  * @param {Object} element Data element descriptor
758  * @param {Function} callback Callback function to run when completed
759  */
760 MailComposer.prototype._emitDataElement = function(element, callback){
761     
762     var data;
763     
764     if(element.contents){
765         switch(element.contentEncoding){
766             case "quoted-printable":
767                 data = mimelib.encodeQuotedPrintable(element.contents);
768                 break;
769             case "base64":
770                 data = new Buffer(element.contents, "utf-8").toString("base64").replace(/.{76}/g,"$&\r\n");
771                 break;
772             case "7bit":
773             case "8bit":
774             case "binary":
775             default:
776                 data = mimelib.foldLine(element.contents, 78, false, element.textFormat=="flowed");
777                  //mimelib puts a long whitespace to the beginning of the lines
778                 data = data.replace(/^[ ]{7}/mg, "");
779                 break;
780         }
781         
782         if(this.options.escapeSMTP){
783             data = data.replace(/^\./gm,'..');
784         }
785         
786         this.emit("data", new Buffer(data + "\r\n", "utf-8"));
787         process.nextTick(callback);
788         return;
789     }
790 
791     if(element.filePath){
792         this._serveFile(element.filePath, callback);
793         return;
794     }
795 
796     callback();
797 };
798 
799 /**
800  * <p>Pipes a file to the e-mail stream</p>
801  * 
802  * <p>This function opens a file and starts sending 76 bytes long base64
803  * encoded lines. To achieve this, the incoming stream is divded into
804  * chunks of 57 bytes (57/3*4=76) to achieve exactly 76 byte long
805  * base64</p>
806  * 
807  * @param {String} filePath Path to the file
808  * @param {Function} callback Callback function to run after completion
809  */
810 MailComposer.prototype._serveFile = function(filePath, callback){
811     var stream, remainder = new Buffer(0);
812     fs.stat(filePath, (function(err, stat){
813         if(err || !stat.isFile()){
814             this.emit("data", new Buffer(new Buffer("<ERROR OPENING FILE>", 
815                                 "utf-8").toString("base64")+"\r\n", "utf-8"));
816             process.nextTick(callback);
817             return;
818         }
819         
820         stream = fs.createReadStream(filePath);
821         
822         stream.on("error", (function(error){
823             this.emit("data", new Buffer(new Buffer("<ERROR READING FILE>", 
824                                 "utf-8").toString("base64")+"\r\n", "utf-8"));
825             process.nextTick(callback);
826         }).bind(this));
827         
828         stream.on("data", (function(chunk){
829             var data = "",
830                 len = remainder.length + chunk.length,
831                 remainderLength = len % 57, // we use 57 bytes as it composes
832                                             // a 76 bytes long base64 string
833                 buffer = new Buffer(len);
834             
835             remainder.copy(buffer); // copy remainder into the beginning of the new buffer
836             chunk.copy(buffer, remainder.length); // copy data chunk after the remainder
837             remainder = buffer.slice(len - remainderLength); // create a new remainder
838             
839             data = buffer.slice(0, len - remainderLength).toString("base64").replace(/.{76}/g,"$&\r\n");
840             
841             if(data.length){
842                 this.emit("data", new Buffer(data.trim()+"\r\n", "utf-8"));
843             }
844         }).bind(this));
845         
846         stream.on("end", (function(chunk){
847             var data;
848             
849             // stream the remainder (if any)
850             if(remainder.length){
851                 data = remainder.toString("base64").replace(/.{76}/g,"$&\r\n");
852                 this.emit("data", new Buffer(data.trim()+"\r\n", "utf-8"));
853             }
854             process.nextTick(callback);
855         }).bind(this));
856         
857     }).bind(this));
858 };
859 
860 /* HELPER FUNCTIONS */
861 
862 /**
863  * <p>Normalizes a key name by cpitalizing first chars of words</p>
864  * 
865  * <p><code>x-mailer</code> will become <code>X-Mailer</code></p>
866  * 
867  * <p>Needed to avoid duplicate header keys</p>
868  * 
869  * @param {String} key Key name
870  * @return {String} First chars uppercased
871  */
872 MailComposer.prototype._normalizeKey = function(key){
873     return (key || "").toString().trim().
874         toLowerCase().
875         replace(/^\S|[\-\s]\S/g, function(c){
876             return c.toUpperCase();
877         }).replace(/^MIME\-/i, "MIME-");
878 };
879 
880 /**
881  * <p>Tests if a string has high bit (UTF-8) symbols</p>
882  * 
883  * @param {String} str String to be tested for high bit symbols
884  * @return {Boolean} true if high bit symbols were found
885  */
886 MailComposer.prototype._hasUTFChars = function(str){
887     var rforeign = /[^\u0000-\u007f]/;
888     return !!rforeign.test(str);
889 };
890 
891 /**
892  * <p>Generates a boundary for multipart bodies</p>
893  * 
894  * @return {String} Boundary String
895  */
896 MailComposer.prototype._generateBoundary = function(){
897     // "_" is not allowed in quoted-printable and "?" not in base64
898     return "----mailcomposer-?=_"+(++this._gencounter)+"-"+Date.now();
899 };
900 
901 /**
902  * <p>Converts a string to mime word format. If the length is longer than
903  * <code>maxlen</code>, split it</p>
904  * 
905  * <p>If the string doesn't have any unicode characters return the original 
906  * string instead</p>
907  * 
908  * @param {String} str String to be encoded
909  * @param {String} encoding Either Q for Quoted-Printable or B for Base64
910  * @param {Number} [maxlen] Optional length of the resulting string, whitespace will be inserted if needed
911  * 
912  * @return {String} Mime-word encoded string (if needed)
913  */
914 MailComposer.prototype._encodeMimeWord = function(str, encoding, maxlen){
915     encoding = (encoding || "Q").toUpperCase(); 
916     if(this._hasUTFChars(str)){
917         str = mimelib.encodeMimeWord(str, encoding);
918         if(maxlen && str.length>maxlen){
919             return str.replace(new RegExp(".{"+maxlen+"}","g"),"$&?= =?UTF-8?"+encoding+"?");
920         }else{
921             return str;
922         }
923     }else{
924         return str;
925     }
926 };
927 
928 /**
929  * <p>Resolves a mime type for a filename</p>
930  * 
931  * @param {String} filename Filename to check
932  * @return {String} Corresponding mime type
933  */
934 MailComposer.prototype._getMimeType = function(filename){
935     var defaultMime = "application/octet-stream",
936         extension = filename && filename.substr(filename.lastIndexOf(".")+1).trim().toLowerCase();
937     return extension && mimelib.contentTypes[extension] || defaultMime;
938 };