first stab

This commit is contained in:
eleith 2011-02-23 13:23:37 -08:00 committed by eleith
commit fcfba1235b
11 changed files with 1619 additions and 0 deletions

25
LICENSE Normal file
View File

@ -0,0 +1,25 @@
Copyright <2010> <leith / eleith.com>. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are
permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of
conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list
of conditions and the following disclaimer in the documentation and/or other materials
provided with the distribution.
THIS SOFTWARE IS PROVIDED BY <leith / eleith.com> ``AS IS'' AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <leith / eleith.com> OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The views and conclusions contained in the software and documentation are those of the
authors and should not be interpreted as representing official policies, either expressed
or implied, of <leith / eleith.com>.

47
Readme.md Normal file
View File

@ -0,0 +1,47 @@
#v0.1
###send emails from node.js to any smtp server
### Installing
npm install emailjs
# FEATURES
- works with SSL smtp servers (ex: gmail)
- works with smtp server authentication (PLAIN, LOGIN, CRAMMD5)
- emails are queued and the queue is sent asynchronously
- supports sending html emails and emails with multiple attachments
- works with nodejs 3.8 and above
# REQUIRES
- access to an SMTP Server (ex: gmail)
# USAGE - text only emails
var email = require("./path/to/emailjs/email");
var server = email.server.connect({user:yourUSER, password:yourPASS, host:"smtp.gmail.com", port:465, domain:yourDOMAIN, secure:true});
// send the message and get a callback with an error or details of the message that was sent
server.send({text:"i hope this works", from:yourUSER + "@gmail.com", to:yourFRIEND, subject:"testing emailjs"}, function(err, message) { console.log(err || message); });
# USAGE - html emails and attachments
var email = require("./path/to/emailjs/email");
var server = email.server.connect({user:yourUSER, password:yourPASS, host:"smtp.gmail.com", port:465, domain:yourDOMAIN, secure:true});
var message = email.message.create("i hope this works", {from:yourUSER + "@gmail.com", to:yourFRIEND, subject:"testing emailjs"});
// attach an alternative html email for those with advanced email clients
message.attach_alternative("i <i>hope</i> this works!");
// attach attachments because you can!
message..attach("path/to/file.zip", "application/zip", "renamed.zip");
// send the message and get a callback with an error or details of the message that was sent
server.send(message, function(err, message) { console.log(err || message); });
// you can continue to send more messages with successive calls to 'server.send', they will be queued on the same smtp connection
// or you can create a new server connection with 'email.server.connect' to async send individual emails instead of a queue
## Authors
eleith

36
demo.js Normal file
View File

@ -0,0 +1,36 @@
//var email = require('emailjs');
var email = require('./email');
var os = require('os');
SMTP =
{
USER: '',
PASS: '',
HOST: "smtp.gmail.com",
SECURE: true,
PORT: 465
};
MESSAGE =
{
DOMAIN: os.hostname(),
FROM: '',
TO: '',
SUBJECT: 'testing emailjs',
TEXT: 'i hope this works',
HTML: 'i <i>hope</i> <b>this</b> works',
ATTACH:
{
PATH: '/path/to/file.tar.gz',
TYPE: 'application/x-compressed-tar',
NAME: 'renamed.tar.gz'
}
};
var server = email.server.connect({user:SMTP.USER, password:SMTP.PASS, host:SMTP.HOST, port:SMTP.PORT, domain:MESSAGE.DOMAIN, secure:SMTP.SECURE});
var msg = email.message.create(MESSAGE.TEXT, {from:MESSAGE.FROM, to:MESSAGE.TO, subject:MESSAGE.SUBJECT});
msg.attach_alternative(MESSAGE.HTML).attach(ATTACH.PATH, ATTACH.TYPE, ATTACH.NAME);
server.send(msg, function(err, message) { console.log(message); });

3
email.js Normal file
View File

@ -0,0 +1,3 @@
exports.server = require('./smtp/client');
exports.message = require('./smtp/message');
exports.SMTP = require('./smtp/smtp');

16
package.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "emailjs",
"description": "send emails, attachments and html emails from node.js to any smtp server",
"version": "0.1.0",
"author": "eleith",
"contributors" : [
],
"repository": {
"type": "git",
"url": "http://github.com/eleith/emailjs.git"
},
"dependencies": {
},
"engine": [ "node >=0.3.8" ],
"main": "email"
}

416
smtp/address.js Normal file
View File

@ -0,0 +1,416 @@
/*
* Email address parsing code.
* rewritten with python's (2.7) email/_parseaddr.py as the starting point
*/
var SPACE = ' ';
var EMPTYSTRING = '';
var COMMASPACE = ', ';
var quote = function(str)
{
// Add quotes around a string.
return str.replace(/\\\\/g, '\\\\').replace(/"/g, '\\"');
};
/*
* To understand what this class does, it helps to have a copy of RFC 2822 in
* front of you.
*/
var Address = function(field)
{
/*
* Initialize a new instance.
* `field' is an unparsed address header field, containing
* one or more addresses.
*/
this.specials = '()<>@,:;.\"[]';
this.pos = 0;
this.LWS = ' \t';
this.CR = '\r\n';
this.FWS = this.LWS + this.CR;
this.atomends = this.specials + this.LWS + this.CR;
// Note that RFC 2822 now specifies `.' as obs-phrase, meaning that it
// is obsolete syntax. RFC 2822 requires that we recognize obsolete
// syntax, so allow dots in phrases.
this.phraseends = this.atomends.replace(/\./g, '');
this.field = field || "";
this.commentlist = [];
};
Address.prototype =
{
gotonext: function()
{
//Parse up to the start of the next address.
while(this.pos < this.field.length)
{
if((this.LWS + '\n\r').indexOf(this.field[this.pos]) != -1)
this.pos++;
else if(this.field[this.pos] == '(')
this.commentlist.push(this.getcomment());
else
break;
}
},
getlist: function()
{
// Parse all addresses. Returns a list containing all of the addresses
var result = [], ad;
while(this.pos < this.field.length)
{
ad = this.get();
if(ad)
result.push(ad);
else
result.push({label:'', address:''});
}
return result;
},
get: function()
{
// Parse the next address
this.commentlist = [];
this.gotonext();
var oldpos = this.pos, oldcl = this.commentlist, plist = this.getphraselist(), returnlist = [],
addrspec, fieldlen, routeaddr;
this.gotonext();
if(this.pos >= this.field.length)
{
// Bad email address, no domain
if(plist)
returnlist = [{label:this.commentlist.join(SPACE), address:plist[0]}];
}
else if('.@'.indexOf(this.field[this.pos]) != -1)
{
// email address is just an addrspec
// this isn't very efficient since we start over
this.pos = oldpos;
this.commentlist = oldcl;
addrspec = this.getspec();
returnlist = {label:this.commentlist.join(SPACE), address:addrspec};
}
else if(this.field[this.pos] == ':')
{
// address is a group
returnlist = [];
fieldlen = this.field.length;
this.pos++;
while(this.pos < this.field.length)
{
this.gotonext();
if(this.pos < fieldlen && this.field[this.pos] == ';')
{
this.pos += 1;
break;
}
returnlist = returnlist.push(this.get());
}
}
else if(this.field[this.pos] == '<')
{
// Address is a prhase then a route addr
routeaddr = this.getroute();
if(this.commentlist.length)
returnlist = {label:plist.join(SPACE) + ' (' + this.commentlist.join(SPACE) + ')', address:routeaddr};
else
returnlist = {label:plist.join(SPACE), address:routeaddr};
}
else
{
if(plist)
returnlist = {label:this.commentlist.join(SPACE), address:plist[0]};
else if(this.specials.indexOf(this.field[this.pos]) != -1)
this.post++;
}
this.gotonext();
if(this.pos < this.field.length && this.field[this.pos] == ',')
this.pos++;
return returnlist;
},
getroute: function()
{
// Parse a route address. this method skips all route stuff and returns addrspec
if(this.field[this.pos] != '<')
return '';
var expectroute = false, adlist = '';
this.pos++;
this.gotonext();
while(this.pos < this.field.length)
{
if(expectroute)
{
this.getdomain();
expectroute = false;
}
else if(this.field[this.pos] == '>')
{
this.pos += 1;
break;
}
else if(this.field[this.pos] == '@')
{
this.pos += 1;
expectroute = true;
}
else if(this.field[this.pos] == ':')
{
this.pos++;
}
else
{
adlist = this.getspec();
this.pos++;
break;
}
this.gotonext();
}
return adlist;
},
getspec: function()
{
//parse an RFC 2822 addr-spec
var aslist = [];
this.gotonext();
while(this.pos < this.field.length)
{
if(this.field[this.pos] == '.')
{
aslist.push('.');
this.pos++;
}
else if(this.field[this.pos] == '"')
aslist.push('"' + this.getquote() + '"');
else if(this.atomends.indexOf(this.field[this.pos]) != -1)
break;
else
aslist.push(this.getatom());
this.gotonext();
}
if(this.pos >= this.field.length || this.field[this.pos] != '@')
return aslist.join(EMPTYSTRING);
aslist.push('@');
this.pos++;
this.gotonext();
return aslist.join(EMPTYSTRING) + this.getdomain();
},
getdomain: function()
{
// get the complete domain name from an address
var sdlist = [];
while(this.pos < this.field.length)
{
if(this.LWS.indexOf(this.field[this.pos]) != -1)
this.pos++;
else if(this.field[this.pos] == '(')
this.commentlist.push(this.getcomment());
else if(this.field[this.pos] == '[')
sdlist.push(this.getdomainliteral());
else if(this.field[this.pos] == '.')
{
this.pos++;
sdlist.push('.');
}
else if(this.atomends.indexOf(this.field[this.pos]) != -1)
break;
else
sdlist.push(this.getatom());
}
return sdlist.join(EMPTYSTRING);
},
getdelimited: function(beginchar, endchars, allowcomments)
{
/*
* Parse a header fragment delimited by special characters.
*
* `beginchar' is the start character for the fragment.
* If self is not looking at an instance of `beginchar' then
* getdelimited returns the empty string.
*
* `endchars' is a sequence of allowable end-delimiting characters.
* Parsing stops when one of these is encountered.
*
* If `allowcomments' is non-zero, embedded RFC 2822 comments are allowed
* within the parsed fragment.
*/
if(this.field[this.pos] != beginchar)
return '';
allowcomments = (allowcomments === false) ? false : true;
var slist = [''], quote = false;
this.pos++;
while(this.pos < this.field.length)
{
if(quote)
{
slist.push(this.field[this.pos]);
quote = false;
}
else if(endchars.indexOf(this.field[this.pos]) != -1)
{
this.pos++;
break;
}
else if(allowcomments && this.field[this.pos] == '(')
{
slist.push(this.getcomment());
continue;
}
else if(this.field[this.pos] == '\\')
quote = true;
else
slist.push(this.field[this.pos]);
this.pos++;
}
return slist.join(EMPTYSTRING);
},
getquote: function()
{
// get a quote-delimited fragment from self's field
return this.getdelimited('"', '"\r', false);
},
getcomment: function()
{
// Get a parenthesis-delimited fragment from self's field.
return this.getdelimited('(', ')\r', true);
},
getdomainliteral: function()
{
// parse an rfc 2822 domain literal
return '[' + this.getdelimited('[', ']\r', false) + ']';
},
getatom: function(atomends)
{
/*
* Parse an RFC 2822 atom.
*
* Optional atomends specifies a different set of end token delimiters
* (the default is to use this.atomends). This is used e.g. in
* getphraselist() since phrase endings must not include the `.' (which
* is legal in phrases).
*/
var atomlist = [''];
if(atomends === undefined)
atomends = this.atomends;
while(this.pos < this.field.length)
{
if(atomends.indexOf(this.field[this.pos]) != -1)
break;
else
atomlist.push(this.field[this.pos]);
this.pos++;
}
return atomlist.join(EMPTYSTRING);
},
getphraselist: function()
{
/*
* Parse a sequence of RFC 2822 phrases.
*
* A phrase is a sequence of words, which are in turn either RFC 2822
* atoms or quoted-strings. Phrases are canonicalized by squeezing all
* runs of continuous whitespace into one space.
*/
var plist = [];
while(this.pos < this.field.length)
{
if(this.FWS.indexOf(this.field[this.pos]) != -1)
this.pos++;
else if(this.field[this.pos] == '"')
plist.push(this.getquote());
else if(this.field[this.pos] == '(')
this.commentlist.push(this.getcomment());
else if(this.phraseends.indexOf(this.field[this.pos]) != -1)
break;
else
plist.push(this.getatom(this.phraseends));
}
return plist;
}
};
exports.Address = Address;
exports.parse = function(field)
{
var addresses = (new Address(field)).getlist();
return addresses.length ? addresses : [];
};

150
smtp/client.js Normal file
View File

@ -0,0 +1,150 @@
var smtp = require('./smtp');
var smtpError = require('./error');
var message = require('./message');
var address = require('./address');
var Client = function(server)
{
this.smtp = new smtp.SMTP(server);
//this.smtp.debug(1);
this.queue = [];
this.timer = null;
this.sending = false;
};
Client.prototype =
{
_poll: function()
{
var self = this;
clearTimeout(self.timer);
if(self.queue.length)
{
if(self.smtp.state() == smtp.state.NOTCONNECTED)
self._connect(self.queue[0]);
else if(self.smtp.state() == smtp.state.CONNECTED && !self.sending)
self._sendmail(self.queue.shift());
}
// wait around 1 seconds in case something does come in, otherwise close out SMTP connection
else
self.timer = setTimeout(function() { self.smtp.quit(); }, 1000);
},
_connect: function(stack)
{
var self = this,
connect = function(err)
{
if(!err)
{
var login = function(err)
{
if(!err)
self._poll();
else
stack.callback(err, stack.message);
};
if(!self.smtp.authorized())
self.smtp.login(login);
else
self._poll();
}
else
stack.callback(err, stack.message);
};
self.smtp.connect(connect);
},
send: function(msg, callback)
{
var self = this;
if(!(msg instanceof message.Message) && msg.from && msg.to && msg.text)
msg = message.create(msg.text, msg);
if(msg instanceof message.Message && msg.valid())
{
var stack =
{
message: msg,
to: address.parse(msg.header["to"]),
from: address.parse(msg.header["from"])[0].address,
callback: callback || function() {}
};
self.queue.push(stack);
self._poll();
}
else
callback({code:-1, message:"message is not a valid Message instance"}, msg);
},
_sendsmtp: function(stack, next)
{
var self = this;
var check= function(err)
{
if(!err && next)
next.apply(self, [stack]);
else
stack.callback(err, stack.message);
};
return check;
},
_sendmail: function(stack)
{
var self = this;
self.sending = true;
self.smtp.mail(self._sendsmtp(stack, self._sendrcpt), '<' + stack.from + '>');
},
_sendrcpt: function(stack)
{
var self = this, to = stack.to.shift().address;
self.smtp.rcpt(self._sendsmtp(stack, stack.to.length ? self._sendrcpt : self._senddata), '<' + to + '>');
},
_senddata: function(stack)
{
var self = this;
self.smtp.data(self._sendsmtp(stack, self._sendmessage));
},
_sendmessage: function(stack)
{
var self = this, stream = stack.message.stream();
stream.on('data', function(data) { self.smtp.message(data); });
stream.on('end', function() { self.smtp.data_end(self._sendsmtp(stack, self._senddone)); });
stream.on('error', self._sendsmtp(stack));
},
_senddone: function(stack)
{
var self = this;
self.sending = false;
stack.callback(null, stack.message);
self._poll();
}
};
exports.Client = Client;
exports.connect = function(server)
{
return new Client(server);
}

16
smtp/error.js Normal file
View File

@ -0,0 +1,16 @@
var SMTPError =
{
COULDNOTCONNECT: 1,
BADRESPONSE: 2,
AUTHFAILED: 3,
TIMEDOUT: 4,
ERROR: 5,
NOCONNECTION: 6,
AUTHNOTSUPPORTED: 7,
CONNECTIONCLOSED: 8,
CONNECTIONENDED: 9,
CONNECTIONAUTH: 10
}
for(var each in SMTPError)
exports[each] = SMTPError[each];

285
smtp/message.js Normal file
View File

@ -0,0 +1,285 @@
var stream = require('stream');
var util = require('util');
var fs = require('fs');
var os = require('os');
var CRLF = "\r\n";
var generate_boundary = function()
{
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'()+_,-./:=?";
for(var i=0; i < 69; i++)
text += possible.charAt(Math.floor(Math.random() * possible.length));
return text;
};
var Message = function(text, headers)
{
this.attachments = [];
this.text = text;
this.html = null;
this.header = {"message-id":"<" + (new Date()).getTime() + "." + process.pid + "@" + os.hostname() +">"};
for(var header in headers)
{
// allow any headers the user wants to set??
// if(/cc|bcc|to|from|reply-to|sender|subject|date|message-id/i.test(header))
this.header[header] = headers[header];
}
};
Message.prototype =
{
attach: function(path, type, name)
{
this.attachments.push({path:path, type:type, name:name});
return this;
},
attach_alternative: function(html)
{
this.html = html;
return this;
},
valid: function()
{
if(!this.header["from"])
return false;
if(!this.header["to"])
return false;
return true;
},
stream: function()
{
return new MessageStream(this);
},
read: function(callback)
{
var buffer = "";
var capture = function(data)
{
buffer += data;
};
var output = function(err)
{
callback(err, buffer);
};
var str = this.stream();
str.on('data', capture);
str.on('end', output);
str.on('error', output);
}
};
var MessageStream = function(message)
{
var self = this;
stream.Stream.call(self);
self.message = message;
self.readable = true;
self.resume = null;
self.paused = false;
self.stopped = false;
self.stream = null;
var output_process = function(next)
{
var check = function()
{
if(self.stopped)
return;
else if(self.paused)
self.resume = next;
else
next();
};
process.nextTick(check);
};
var output_mixed = function()
{
var data = [];
var boundary = generate_boundary();
self.emit('data', ["Content-Type: multipart/mixed; boundary=\"", boundary, "\"", CRLF, CRLF].join(""));
output_process(function() { output_message(-1, boundary); });
};
var output_message = function(index, boundary)
{
var next = function()
{
output_process(function() { output_message(index + 1, boundary); });
};
if(index == -1 && self.message.html)
{
self.emit('data', ["--", boundary, CRLF].join(""));
output_process(function() { output_alternatives(next); });
}
else if(index < self.message.attachments.length)
{
self.emit('data', ["--", boundary, CRLF].join(""));
output_process(function() { output_attachment(self.message.attachments[index], next); });
}
else
{
self.emit('data', [CRLF, CRLF, "--", boundary, "--", CRLF, CRLF].join(""));
self.emit('end');
}
};
var output_alternatives = function(next)
{
var boundary = generate_boundary();
var data = ["Content-Type: multipart/alternative; boundary=\"", boundary, "\"", CRLF, CRLF];
data = data.concat(["--", boundary, CRLF]);
data = data.concat(["Content-Type: text/html", CRLF, "Content-Transfer-Encoding: quoted-printable", CRLF, "Content-Disposition: inline", CRLF, CRLF]);
data = data.concat([self.message.html, CRLF, CRLF]);
data = data.concat(["--", boundary, CRLF]);
data = data.concat(["Content-Type: text/plain", CRLF, "Content-Transfer-Encoding: quoted-printable", CRLF, "Content-Disposition: inline", CRLF, CRLF]);
data = data.concat([self.message.text, CRLF, CRLF]);
data = data.concat(["--", boundary, "--", CRLF, CRLF]);
self.emit('data', data.join(""));
next();
};
var output_attachment = function(attachment, next)
{
var data = ["Content-Type: ", attachment.type, CRLF, "Content-Transfer-Encoding: base64", CRLF];
data = data.concat(["Content-Disposition: attachment; filename=\"", attachment.name, "\"", CRLF, CRLF]);
self.emit('data', data.join(""));
var chunk = 5700;
var buffer = new Buffer(chunk);
var opened = function(err, fd)
{
if(!err)
{
var read = function(err, bytes)
{
if(self.paused)
{
self.resume = function() { read(err, bytes); };
}
else if(self.stopped)
{
fs.close(fd);
}
else if(!err)
{
if(bytes == chunk)
{
self.emit('data', buffer.toString("base64"));
fs.read(fd, buffer, 0, chunk, null, read);
}
else
{
self.emit('data', buffer.slice(0, bytes).toString("base64"));
fs.close(fd, function() { self.emit('end') });
}
}
else
{
fs.close(fd);
self.emit('error', err);
}
};
fs.read(fd, buffer, 0, chunk, null, read);
}
else
self.emit('error', err);
};
fs.open(attachment.path, 'r+', opened);
};
var output_data = function()
{
// are there attachments or alternatives?
if(self.message.attachments.length || self.message.html)
{
self.emit('data', "MIME-Version: 1.0" + CRLF);
output_process(output_mixed);
}
// otherwise, you only have a text message
else
{
self.emit('data', CRLF + self.message.text);
self.emit('end');
}
};
var output_header = function()
{
var data = [];
for(var header in self.message.header)
data = data.concat([header, ": ", self.message.header[header], CRLF]);
self.emit('data', data.join(''));
output_process(output_data);
};
output_process(output_header);
return;
};
MessageStream.prototype.pause = function()
{
self.paused = true;
};
MessageStream.prototype.resume = function()
{
self.paused = false;
if(self.resume)
{
var resume = self.resume;
self.resume = null;
resume();
}
};
MessageStream.prototype.destroy = function()
{
self.stopped = true;
};
MessageStream.prototype.destroySoon = function()
{
self.stopped = true;
};
util.inherits(MessageStream, stream.Stream);
exports.Message = Message;
exports.create = function(text, headers)
{
return new Message(text, headers);
};

82
smtp/response.js Normal file
View File

@ -0,0 +1,82 @@
var SMTPError = require('./error');
function SMTPResponse(stream, timeout)
{
var buffer = '',
notify = function()
{
if(buffer.length)
{
stream.emit('response', null, buffer.replace("\r", ''));
buffer = '';
}
},
error = function(err)
{
stream.emit('response', {code:SMTPError.ERROR, message:"connection encountered an error", error:err});
end();
},
timedout = function(err)
{
stream.emit('response', {code:SMTPError.TIMEDOUT, message:"connection has timedout", error:err});
end();
},
watch = function(data)
{
var decoded = data.toString();
var emit = false;
var code = 0;
var parsed = decoded.match(/^(?:.*\n)?([^\n]+)\n\s*$/m);
buffer += decoded;
notify();
},
close = function(err)
{
if(buffer.length)
notify();
else
stream.emit('response', {code:SMTPError.CONNECTIONCLOSED, message:"connection has closed", error:err});
end();
},
end = function(err)
{
if(buffer.length)
notify();
else
stream.emit('response', {code:SMTPError.CONNECTIONENDED, message:"connection has ended", error:err});
stream.removeAllListeners('response');
stream.removeListener('data', watch);
stream.removeListener('end', end);
stream.removeListener('close', close);
stream.removeListener('error', error);
stream.removeListener('timeout', timedout);
};
stream.on('data', watch);
stream.on('end', end);
stream.on('close', close);
stream.on('timeout', timedout);
stream.on('error', error);
}
exports.watch = function(stream)
{
return new SMTPResponse(stream);
};
exports.parse = function(line)
{
var match = line ? line.match(/(\d+)\s?(.*)/) : null;
return match ? {code:match[1], message:match[2]} : {};
}

543
smtp/smtp.js Normal file
View File

@ -0,0 +1,543 @@
/*
* SMTP class written using python's (2.7) smtplib.py as a base
*/
var net = require('net');
var crypto = require('crypto');
var os = require('os');
var tls = require('tls');
var util = require('util');
var events = require('events');
var SMTPResponse = require('./response');
var SMTPError = require('./error');
var SMTP_PORT = 25;
var SMTP_SSL_PORT = 465;
var CRLF = "\r\n";
var AUTH_METHODS = {PLAIN:'PLAIN', CRAM_MD5:'CRAM-MD5', LOGIN:'LOGIN'};
var TIMEOUT = 5000;
var DEBUG = 0;
var SMTP_USER = null;
var SMTP_PASSWORD = null;
var log = function()
{
if(DEBUG)
{
Array.prototype.slice.call(arguments).forEach(function(d) { console.log(d); });
}
};
var quotedata = function(data)
{
// Quote data for email.
// Double leading '.', and change Unix newline '\\n', or Mac '\\r' into
// Internet CRLF end-of-line.
return data.replace(/(?:\r\n|\n|\r(?!\n))/g, CRLF).replace(/^\./gm, '..');
};
var error = function(code, smtp, err)
{
return {code:code, smtp:smtp, error:err};
};
var caller = function(callback)
{
if(typeof(callback) == 'function')
{
var args = Array.prototype.slice.call(arguments);
args.shift();
callback.apply(null, args);
}
};
var SMTPState =
{
NOTCONNECTED: 0,
CONNECTING: 1,
CONNECTED: 2
};
var SMTP = function(options)
{
events.EventEmitter.call(this);
options = options || {};
this.sock = null;
this.timeout = options.timeout || TIMEOUT;
this.secure = options.secure || false;
this.features = null;
this._state = SMTPState.NOTCONNECTED;
this.loggedin = (options.user && options.password) ? false : true;
this.domain = options.domain || os.hostname();
this.host = options.host || 'localhost';
this.port = options.port || options.secure ? SMTP_SSL_PORT : SMTP_PORT;
this.ssl = options.ssl;
// keep private
SMTP_USER = options.user;
SMTP_PASSWORD = options.password;
};
SMTP.prototype =
{
debug: function(level)
{
DEBUG = level;
},
state: function()
{
return this._state;
},
authorized: function()
{
return this.loggedin;
},
connect: function(callback, port, host, options)
{
options = options || {};
var self = this;
self.host = host || self.host;
self.port = port || self.port;
self.secure = options.secure || self.secure;
self.ssl = options.ssl || self.ssl;
if(self._state != SMTPState.NOTCONNECTED)
self.quit();
var connected = function(err)
{
if(!err)
{
if(self.secure)
{
// if key/ca/cert was passed and ssl is used, check if authorized is false
if(self.ssl && !self.sock.authorize)
{
self.close(true);
caller(callback, {code:SMTPError.CONNECTIONAUTH, message:"could not establish an ssl connection", error:err});
return;
}
}
log("connected: " + self.host + ":" + self.port);
}
else
{
self.close(true);
caller(callback, {code:SMTPError.COULDNOTCONNECT, error:err});
}
};
var response = function(err, data)
{
var msg = SMTPResponse.parse(data);
if(!err && msg.code == '220')
{
log("response: " + data);
// might happen first, so no need to wait on connected()
self._state = SMTPState.CONNECTED;
caller(callback, null, data);
}
else
{
if(err)
{
log("response (error): " + err);
self.close(true);
caller(callback, {code:err.code, error:err.error});
}
else
{
log("response (data): " + data);
self.quit();
caller(callback, {code:SMTPError.BadResponse, message:"bad response on connection", smtp:data, error:err});
}
}
};
self._state = SMTPState.CONNECTING;
if(self.secure)
{
// object, may contain 'key' and/or 'ca' and/or 'cert'
if(self.ssl)
self.sock = tls.connect(self.port, self.host, self.ssl, connected);
else
self.sock = tls.connect(self.port, self.host, connected);
}
else
{
self.sock = net.Socket();
self.sock.connect(self.port, self.host, connected);
}
SMTPResponse.watch(self.sock);
self.sock.setTimeout(self.timeout);
self.sock.once('response', response);
},
send: function(str, callback)
{
var self = this;
if(self.sock && self._state == SMTPState.CONNECTED)
{
log("send: " + str);
var response = function(err, data)
{
log("response: " + (data || err));
if(err)
self.close(true);
else
caller(callback, err, data);
};
self.sock.once('response', response);
self.sock.write(str);
}
else
{
self.close(true);
caller(callback, {code:SMTPError.NOCONNECTION, message:"no connection has been established"});
}
},
command: function(cmd, callback, codes)
{
codes = Array.isArray(codes) ? codes : typeof(codes) == 'number' ? [codes] : [250];
var response = function(err, data)
{
var msg = SMTPResponse.parse(data);
if(err)
caller(callback, err);
else if(codes.indexOf(Number(msg.code)) != -1)
caller(callback, err, data);
else
caller(callback, {code:SMTPError.BADRESPONSE, message:"bad response on command '"+cmd.split(' ')[0]+"'", smtp:data, error:err});
};
this.send(cmd + CRLF, response);
},
helo: function(callback, domain)
{
/*
* SMTP 'helo' command.
* Hostname to send for self command defaults to the FQDN of the local
* host.
*/
this.command("helo " + (domain || this.domain), callback);
},
/*
// STARTTLS is not supported since node net api doesn't support upgrading a socket to a secure socket
// use ssl instead of tls. the benefit is that the entire communication will be encrypted from the beginning
starttls: function(callback, domain)
{
this.command("starttls", callback);
},
*/
ehlo: function(callback, domain)
{
var self = this,
response = function(err, data)
{
// According to RFC1869 some (badly written)
// MTA's will disconnect on an ehlo. Toss an exception if
// that happens -ddm
if(!err)
{
data.split("\n").forEach(function(ext)
{
var parse = ext.match(/^(?:\d+[-=]?)\s*?([^\s]+)(?:\s+(.*)\s*?)?$/);
// To be able to communicate with as many SMTP servers as possible,
// we have to take the old-style auth advertisement into account,
// because:
// 1) Else our SMTP feature parser gets confused.
// 2) There are some servers that only advertise the auth methods we
// support using the old style.
if(parse)
{
// RFC 1869 requires a space between ehlo keyword and parameters.
// It's actually stricter, in that only spaces are allowed between
// parameters, but were not going to check for that here. Note
// that the space isn't present if there are no parameters.
self.features[parse[1].toLowerCase()] = parse[2] || true;
}
});
caller(callback, null, data);
}
else
{
caller(callback, err);
}
};
this.features = {};
this.command("ehlo " + (domain || this.domain), response);
},
has_extn: function(opt)
{
return this.features[opt.toLowerCase()] == undefined;
},
help: function(callback, args)
{
// SMTP 'help' command, returns text from the server
this.command(args ? "help " + args : "help", callback, [211, 214]);
},
rset: function(callback)
{
this.send("rset", callback);
},
noop: function(callback)
{
return this.send("noop", callback);
},
mail: function(callback, from)
{
this.command("mail FROM:" + from, callback);
},
rcpt: function(callback, to)
{
// SMTP 'rcpt' command -- indicates 1 recipient for self mail
this.command("rcpt TO:" + to, callback, [250, 251]);
},
data: function(callback)
{
this.command("data", callback, [354]);
},
data_end: function(callback)
{
this.command(CRLF + "." + CRLF, callback);
},
message: function(data)
{
this.sock.write(data);
},
verify: function(address, callback)
{
// SMTP 'verify' command -- checks for address validity."""
this.command("vrfy " + address, callback, [250, 251, 252]);
},
expn: function(address, callback)
{
// SMTP 'expn' command -- expands a mailing list.
this.command("expn " + address, callback);
},
ehlo_or_helo_if_needed: function(callback, domain)
{
// Call self.ehlo() and/or self.helo() if needed.
// If there has been no previous EHLO or HELO command self session, self
// method tries ESMTP EHLO first.
var self = this;
if(!this.features)
{
var response = function(err, data)
{
caller(callback, err, data);
};
var attempt = function(err, data)
{
if(err)
self.helo(response, domain);
else
caller(callback, err);
};
self.ehlo(attempt, domain);
}
},
login: function(callback, user, password, options)
{
var self = this,
login = {
user: user || SMTP_USER,
password: password || SMTP_PASSWORD,
method: options && options.method ? options.method.toUpperCase() : ''
},
domain = options && options.domain ? options.domain : this.domain,
initiate = function(err, data)
{
if(err)
{
caller(callback, err);
return;
}
/*
* Log in on an SMTP server that requires authentication.
*
* The arguments are:
* - user: The user name to authenticate with.
* - password: The password for the authentication.
*
* If there has been no previous EHLO or HELO command self session, self
* method tries ESMTP EHLO first.
*
* This method will return normally if the authentication was successful.
*/
var method = null,
encode_cram_md5 = function(challenge)
{
challenge = (new Buffer(challenge, "base64")).toString("ascii");
return (new Buffer(login.user + " " + crypto.createHMAC(login.password, challenge).digest('hex')).toString("base64"));
},
encode_plain = function()
{
return (new Buffer("\0" + login.user + "\0" + login.password)).toString("base64");
};
// List of authentication methods we support: from preferred to
// less preferred methods.
if(!method)
{
var preferred = [AUTH_METHODS.CRAM_MD5, AUTH_METHODS.LOGIN, AUTH_METHODS.PLAIN];
for(var i = 0; i < preferred.length; i++)
{
if((self.features["auth"]).indexOf(preferred[i]) != -1)
{
method = preferred[i];
break;
}
}
}
var response = function(err, data)
{
if(!err)
{
self.loggedin = true;
caller(callback, err, data);
}
else
{
self.loggedin = false;
caller(callback, {code:SMTPError.AUTHFAILED, message:"authorization failed", smtp:data});
}
};
var attempt = function(err, data)
{
if(!err)
{
if(method == AUTH_METHODS.CRAM_MD5)
self.command(encode_cram_md5(SMTPResponse.parse(data).message), response, [235, 503]);
else if(method == AUTH_METHODS.LOGIN)
self.command((new Buffer(login.password)).toString("base64"), response, [235, 503]);
}
else
{
self.loggedin = false;
caller(callback, {code:SMTPError.AUTHFAILED, message:"authorization failed", smtp:data});
}
};
if(method == AUTH_METHODS.CRAM_MD5)
self.command("AUTH " + AUTH_METHODS.CRAM_MD5, attempt, [334]);
else if(method == AUTH_METHODS.LOGIN)
self.command("AUTH " + AUTH_METHODS.LOGIN + " " + (new Buffer(login.user)).toString("base64"), attempt, [334]);
else if(method == AUTH_METHODS.PLAIN)
self.command("AUTH " + AUTH_METHODS.PLAIN + " " + encode_plain(login.user, login.password), response, [235, 503]);
else if(!method)
caller(callback, {code:SMTPError.AUTHNOTSUPPORTED, message:"authorization no supported", smtp:data});
};
self.ehlo_or_helo_if_needed(initiate, domain);
},
close: function(force)
{
if(this.sock)
{
if(force)
this.sock.destroy();
else
this.sock.end();
}
this._state = SMTPState.NOTCONNECTED;
this.sock = null;
this.features = null;
this.secure = false;
this.loggedin = false;
},
quit: function(callback)
{
var self = this,
response = function(err, data)
{
caller(callback, err, data);
self.close();
};
this.command("quit", response, [221, 250]);
}
};
for(var each in events.EventEmitter.prototype)
{
SMTP.prototype[each] = events.EventEmitter.prototype[each];
}
exports.SMTP = SMTP;
exports.state = SMTPState;