diff --git a/README.md b/README.md index 3c12adec..0b88defb 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Using *EmailMessage* var nodemailer = require("nodemailer"); - var mail = new nodemailer.EmailMessage({ + var mail = nodemailer.EmailMessage({ sender: "me@example.com", to:"you@example.com" }); diff --git a/lib/mail.js b/lib/mail.js index 1bc38441..4caee092 100644 --- a/lib/mail.js +++ b/lib/mail.js @@ -45,6 +45,12 @@ exports.SMTP = { pass: false }; +exports.CommonServers = { + gmail:[], + yahoo:[], + hotmail:[] +}; + /** * mail.sendmail -> Boolean | String * @@ -83,7 +89,8 @@ var gencounter = 0; * Creates an object to send an e-mail. * * params can hold the following data - * + * + * - **server** server to send message to (will default to exports.SMTP) * - **sender** e-mail address of the sender * - **headers** an object with custom headers. * `{"X-Myparam": "test", "Message-ID":"12345"}` @@ -101,7 +108,11 @@ var gencounter = 0; * - **debug** if set, outputs the whole communication of SMTP server to console * * All the params can be edited/added after defining the object - * + * + * Events: + * forward(oldAddr,newAddr) - was told to try new address by server. + * defer(addr) - server takes responsibility for delivery. + * retain(addr) - unable to send to mailbox. * Usage: * var em = EmailMessage(); * em.sender = '"Andris Reinman" ' @@ -112,7 +123,9 @@ var gencounter = 0; * NB! mail.SMTP needs to be set before sending any e-mails! **/ function EmailMessage(params){ + EventEmitter.call(this); params = params || {}; + this.server = params.server || exports.SMTP; this.sender = params.sender; this.headers = params.headers || {}; this.to = params.to; @@ -130,6 +143,8 @@ function EmailMessage(params){ this.callback = null; } +var utillib = require("util"); +util.inherits(EmailMessage,EventEmitter); /** * mail.EmailMessage#prepareVariables() -> undefined @@ -140,7 +155,9 @@ EmailMessage.prototype.prepareVariables = function(){ if(this.html || this.attachments.length){ this.content_multipart = true; this.content_mixed = !!this.attachments.length; - this.content_boundary = "----NODEMAILER-"+(++gencounter)+"-"+Date.now(); + //'=_' is not valid quoted printable + //'?' is not in any known base64 extension + this.content_boundary = "----NODEMAILER-?=_"+(++gencounter)+"-"+Date.now(); // defaults to multipart/mixed but if there's attachments with cid value set // use multipart/related - mail clients hide the duplicates this way @@ -275,7 +292,7 @@ EmailMessage.prototype.generateBody = function(){ } var body_boundary = this.content_mixed? - "----NODEMAILER-"+(++gencounter)+"-"+Date.now(): + "----NODEMAILER-?=_"+(++gencounter)+"-"+Date.now(): this.content_boundary, rows = []; @@ -326,7 +343,7 @@ EmailMessage.prototype.generateBody = function(){ this.attachments[i].contents: new Buffer(this.attachments[i].contents, "utf-8"), disposition: "attachment", - content_id: this.attachments[i].cid || ((++gencounter)+"."+Date.now()+"@"+exports.SMTP.hostname) + content_id: this.attachments[i].cid || ((++gencounter)+"."+Date.now()+"@"+this.SERVER.hostname) }; @@ -339,8 +356,9 @@ EmailMessage.prototype.generateBody = function(){ rows.push("Content-Transfer-Encoding: base64"); rows.push(""); - // rows can't be too long, so base64 string will be cut to 78 char lines - rows.push(current.contents.toString("base64").replace(/(.{78})/g,"$1\r\n")); + // quoted printable rfc says limit of a line is 76 (no say on whether that includes line breaks) + // matching by leaving 2 for line break + rows.push(current.contents.toString("base64").replace(/.{74}/g,"$&\r\n")); } @@ -445,56 +463,82 @@ EmailMessage.prototype.send = function(callback){ return; } + var mail = this; // use SMTP - var smtp = new SMTPClient(exports.SMTP.host, exports.SMTP.port, { - hostname: exports.SMTP.hostname, - use_authentication: exports.SMTP.use_authentication, - user: exports.SMTP.user, - pass: exports.SMTP.pass, - ssl: exports.SMTP.ssl, + var smtp = new SMTPClient(this.SMTP.host, this.SMTP.port, { + hostname: this.SMTP.hostname, + use_authentication: this.SMTP.use_authentication, + user: this.SMTP.user, + pass: this.SMTP.pass, + ssl: this.SMTP.ssl, debug: this.debug }); smtp.on("error", function(error){ callback && callback(error, null); }); - - var commands = []; - if(this.fromAddress){ - // should run once - for(var i=0; i"); - } - } - if(this.toAddress){ - // should run once - for(var i=0; i"); - } - } - commands.push("DATA"); - - process.nextTick(runCommands); - - // performs a waterfall of SMTP commands - function runCommands(){ - var command = commands.shift(); - if(command){ - smtp.send(command, function(error, message){ - if(!error){ - //console.log("Command '"+command+"' sent, response:\n"+message); - process.nextTick(runCommands); - }else{ - //console.log("Command '"+command+"' ended with error\n"+error.message); + var i = 0; + //concat if you need to do forwards + var toAddress = this.toAddress.concat(); + var fromAddress = this.fromAddress.concat(); + function nextSender() { + if(i === fromAddress.length) { + i = 0; + nextRecipient(); + } + else { + smtp.send("MAIL FROM:<"+fromAddress[i++]+">", function(error, message) { + if(error) { smtp.close(); process.nextTick(function(){ callback && callback(error, null); }); - } - }); - }else - process.nextTick(sendBody); + return; + } + process.nextTick(nextSender); + }); + } + }) + } + function nextRecipient() { + if(i === toAddress.length) { + smtp.send("DATA",function(error, message) { + process.nextTick(sendBody); + }); + } + else { + smtp.send("RCPT TO:<"+toAddress[i++]+">", function(error, message) { + if(error) { + var forwardAddress; + //Empty addresses are valid (for server sent notifications). + if(forwardAddress = error.message.match(/^551.*try\s+([<][^>]*[>])/)) { + mail.emit("forward",toAddress[i-1],forwardAddress[1]); + toAddress.splice(i,0,forwardAddress[1]) + process.nextTick(nextRecipient); + return; + } + //Not all error codes are true failures (we may be able to still send it to other recipients) + else if(error.message.match(/^(?:552|451|452|500|503|421)/)){ + smtp.close(); + process.nextTick(function(){ + callback && callback(error, null); + }); + return; + } + else { + mail.emit("retain",toAddress[i-1]); + process.nextTick(nextRecipient); + return; + } + } + if(message && /^251/.test(message.test)) { + mail.emit("defer",toAddress[i-1]); + } + process.nextTick(nextRecipient); + }); + } } + process.nextTick(nextSender); // Sends e-mail body to the SMTP server and finishes up function sendBody(){ diff --git a/lib/mime.js b/lib/mime.js index 8b982b42..8f0fda21 100644 --- a/lib/mime.js +++ b/lib/mime.js @@ -159,6 +159,7 @@ this.encodeQuotedPrintable = function(str, mimeWord, charset){ if(!mimeWord){ // lines might not be longer than 76 bytes, soft break: "=\r\n" var lines = str.split(/\r?\n/); + str.replace(/(.{73}(?!\r?\n))/,"$&=\r\n") for(var i=0, len = lines.length; i76){ lines[i] = this.foldLine(lines[i],76, false, true).replace(/\r\n/g,"=\r\n"); diff --git a/lib/smtp.js b/lib/smtp.js index 597d6558..e15b092a 100644 --- a/lib/smtp.js +++ b/lib/smtp.js @@ -116,7 +116,7 @@ function SMTPClient(host, port, options){ this._connected = false; // Indicates if an active connection is available this._connection = false; // Holds connection info this._callbackQueue = []; // Queues the responses FIFO (needed for pipelining) - this._data_remainder = []; // Needed to group multi-line messages from server + this._data_remainder = ""; // Needed to group multi-line messages from server, string buffer to prevent newline issue. this._timeoutTimer = null; } // Needed to convert this constructor into EventEmitter @@ -249,14 +249,15 @@ SMTPClient.prototype._loginHandler = function(callback){ **/ SMTPClient.prototype._dataListener = function(data){ var action = this._callbackQueue.shift(); + var isError = +data.trim().charAt(0)>3; if(action && action.callback){ - if(parseInt(data.trim().charAt(0),10)>3){ + if(isError){ action.callback(new Error(data), null); }else{ action.callback(null, data); } }else{ - if(parseInt(data.trim().charAt(0),10)>3){ + if(isError){ this.emit("error", new Error(data)); this.close(); }else{ @@ -294,6 +295,34 @@ SMTPClient.prototype._handshakeListener = function(data, callback){ } } +/** + * smtp.SMTPClient#_handshakeListener(data) -> undefined + * - data(String): String received from the server + * + * Server data listener for the handshake - waits for the 220 response + * from the server (connection established). + **/ +SMTPClient.prototype._starttlsHandler = function(callback){ + if(this.debug) + console.log("STARTTLS: "+data.toString("utf-8").trim()); + // fallback to HELO + if(this._connection.authorized) { + callback(); + } + else { + this._sendCommand("STARTTLS", (function(error, data){ + if(error){ + this.emit("error", error); + this.close(); + return; + } + starttls.wrap(this._connection,this.options,function(){ + this._loginHandler(callback); + }); + }).bind(this)); + } +} + /** * smtp.SMTPClient#_handshake(callback) -> undefined * - callback (Function): will be forwarded to login after successful connection @@ -309,7 +338,7 @@ SMTPClient.prototype._handshake = function(callback){ if(error){ // fallback to HELO - return this._sendCommand("HELO "+this.hostname, (function(error, data){ + this._sendCommand("HELO "+this.hostname, (function(error, data){ if(error){ this.emit("error", error); this.close(); @@ -329,6 +358,10 @@ SMTPClient.prototype._handshake = function(callback){ // check for TLS support if(data.match(/STARTTLS/i)){ this.remote_starttls = true; + if(!this._connection.authorized) { + this._starttlsHandler(); + return; + } } // check login after successful handshake @@ -367,18 +400,14 @@ SMTPClient.prototype._onData = function(callback, data){ if(this.debug) console.log("RECEIVE:\n"+JSON.stringify(data.toString("utf-8"))); - var lines = data.toString("utf-8").split("\r\n"), i, length, parts; - for(i=0, length=lines.length; i