mirror of https://github.com/eleith/emailjs.git
first stab
This commit is contained in:
commit
fcfba1235b
|
@ -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>.
|
|
@ -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
|
|
@ -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); });
|
|
@ -0,0 +1,3 @@
|
|||
exports.server = require('./smtp/client');
|
||||
exports.message = require('./smtp/message');
|
||||
exports.SMTP = require('./smtp/smtp');
|
|
@ -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"
|
||||
}
|
|
@ -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 : [];
|
||||
};
|
|
@ -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);
|
||||
}
|
|
@ -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];
|
|
@ -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);
|
||||
};
|
|
@ -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]} : {};
|
||||
}
|
|
@ -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;
|
Loading…
Reference in New Issue