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 };