From 48e37631c7e87cd9444e932063a2d4424712027a Mon Sep 17 00:00:00 2001 From: mscdex Date: Sat, 29 Jun 2013 17:02:46 -0400 Subject: [PATCH] add support for STARTTLS --- README.md | 11 ++++--- lib/Connection.js | 82 +++++++++++++++++++++++++++++++++++------------ lib/Parser.js | 14 +++++--- 3 files changed, 78 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 4f53d90..e3d7e1d 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,8 @@ var imap = new Imap({ password: 'mygmailpassword', host: 'imap.gmail.com', port: 993, - secure: true, - secureOptions: { rejectUnauthorized: false } + tls: true, + tlsOptions: { rejectUnauthorized: false } }); function openInbox(cb) { @@ -379,8 +379,9 @@ Connection Instance Methods * **xoauth2** - < _string_ > - OAuth2 token for [The SASL XOAUTH2 Mechanism](https://developers.google.com/google-apps/gmail/xoauth2_protocol#the_sasl_xoauth2_mechanism) for servers that support it (See Andris Reinman's [xoauth2](https://github.com/andris9/xoauth2) module to help generate this string). * **host** - < _string_ > - Hostname or IP address of the IMAP server. **Default:** "localhost" * **port** - < _integer_ > - Port number of the IMAP server. **Default:** 143 - * **secure** - < _boolean_ > - Use SSL/TLS? **Default:** false - * **secureOptions** - < _object_ > - Options object to pass to tls.connect() **Default:** (none) + * **tls** - < _boolean_ > - Perform implicit TLS connection? **Default:** false + * **tlsOptions** - < _object_ > - Options object to pass to tls.connect() **Default:** (none) + * **autotls** - < _string_ > - Set to 'always' to always attempt connection upgrades via STARTTLS, 'required' only if upgrading is required, or 'never' to never attempt upgrading. **Default:** 'never' * **connTimeout** - < _integer_ > - Number of milliseconds to wait for a connection to be established. **Default:** 10000 * **keepalive** - < _boolean_ > - Enable the keepalive mechnanism. **Default:** true * **debug** - < _function_ > - If set, the function will be called with one argument, a string containing some debug info **Default:** @@ -675,8 +676,8 @@ TODO Several things not yet implemented in no particular order: -* Support STARTTLS * Support additional IMAP commands/extensions: * NOTIFY (via NOTIFY extension -- RFC5465) * STATUS addition to LIST (via LIST-STATUS extension -- RFC5819) * CONDSTORE (RFC4551) + * QRESYNC (RFC5162) diff --git a/lib/Connection.js b/lib/Connection.js index 4a89da8..654494d 100644 --- a/lib/Connection.js +++ b/lib/Connection.js @@ -52,8 +52,9 @@ function Connection(config) { this._config = { host: config.host || 'localhost', port: config.port || 143, - secure: (config.secure === true ? 'implicit' : config.secure), - secureOptions: config.secureOptions, + tls: config.tls, + tlsOptions: config.tlsOptions, + autotls: config.autotls, user: config.user, password: config.password, connTimeout: config.connTimeout || 10000, @@ -68,6 +69,8 @@ function Connection(config) { this._queue = []; this._box = undefined; this._idle = {}; + this._parser = undefined; + this._curReq = undefined; this.delimiter = undefined; this.namespaces = undefined; this.state = 'disconnected'; @@ -76,22 +79,22 @@ function Connection(config) { inherits(Connection, EventEmitter); Connection.prototype.connect = function() { - var config = this._config, self = this, socket, tlsSocket, parser, tlsOptions; + var config = this._config, self = this, socket, parser, tlsOptions; socket = new Socket(); socket.setKeepAlive(true); socket.setTimeout(0); this.state = 'disconnected'; - if (config.secure) { + if (config.tls) { tlsOptions = {}; - for (var k in config.secureOptions) - tlsOptions[k] = config.secureOptions[k]; + for (var k in config.tlsOptions) + tlsOptions[k] = config.tlsOptions[k]; tlsOptions.socket = socket; } - if (config.secure === 'implicit') - this._sock = tlsSocket = tls.connect(tlsOptions, onconnect); + if (config.tls) + this._sock = tls.connect(tlsOptions, onconnect); else { socket.once('connect', onconnect); this._sock = socket; @@ -100,32 +103,33 @@ Connection.prototype.connect = function() { function onconnect() { clearTimeout(self._tmrConn); self.state = 'connected'; - self.debug&&self.debug('[connection] Connected to host'); + self.debug && self.debug('[connection] Connected to host'); } - this._sock.once('error', function(err) { + this._onError = function(err) { clearTimeout(self._tmrConn); clearTimeout(self._tmrKeepalive); - self.debug&&self.debug('[connection] Error: ' + err); + self.debug && self.debug('[connection] Error: ' + err); err.source = 'socket'; self.emit('error', err); - }); + }; + this._sock.once('error', this._onError); socket.once('close', function(had_err) { clearTimeout(self._tmrConn); clearTimeout(self._tmrKeepalive); - self.debug&&self.debug('[connection] Closed'); + self.debug && self.debug('[connection] Closed'); self.emit('close', had_err); }); socket.once('end', function() { clearTimeout(self._tmrConn); clearTimeout(self._tmrKeepalive); - self.debug&&self.debug('[connection] Ended'); + self.debug && self.debug('[connection] Ended'); self.emit('end'); }); - parser = new Parser(this._sock, this.debug); + this._parser = parser = new Parser(this._sock, this.debug); parser.on('untagged', function(info) { self._resUntagged(info); @@ -1189,6 +1193,12 @@ Connection.prototype._login = function() { reentry(); }; + if (self.serverSupports('STARTTLS') + && (self._config.autotls === 'always' + || (self._config.autotls === 'required' + && self.serverSupports('LOGINDISABLED')))) + self._starttls(); + if (self.serverSupports('LOGINDISABLED')) { err = new Error('Logging in is disabled on this server'); err.source = 'authentication'; @@ -1197,12 +1207,16 @@ Connection.prototype._login = function() { if (self.serverSupports('AUTH=XOAUTH') && self._config.xoauth) { self._caps = undefined; - self._enqueue('AUTHENTICATE XOAUTH ' + escape(self._config.xoauth), - checkCaps); + var cmd = 'AUTHENTICATE XOAUTH'; + if (self.serverSupports('SASL-IR')) + cmd += ' ' + escape(self._config.xoauth); + self._enqueue(cmd, checkCaps); } else if (self.serverSupports('AUTH=XOAUTH2') && self._config.xoauth2) { self._caps = undefined; - self._enqueue('AUTHENTICATE XOAUTH2 ' + escape(self._config.xoauth2), - checkCaps); + var cmd = 'AUTHENTICATE XOAUTH2'; + if (self.serverSupports('SASL-IR')) + cmd += ' ' + escape(self._config.xoauth2); + self._enqueue(cmd, checkCaps); } else if (self._config.user && self._config.password) { self._caps = undefined; self._enqueue('LOGIN "' + escape(self._config.user) + '" "' @@ -1218,6 +1232,32 @@ Connection.prototype._login = function() { }); }; +Connection.prototype._starttls = function() { + var self = this; + this._enqueue('STARTTLS', function(err) { + if (err) { + self.emit('error', err); + return self._sock.end(); + } + + self._caps = undefined; + self._sock.removeAllListeners('error'); + + var tlsOptions = {}; + + for (var k in config.tlsOptions) + tlsOptions[k] = config.tlsOptions[k]; + tlsOptions.socket = self._sock; + + self._sock = tls.connect(tlsOptions, function() { + self._login(); + }); + + self._sock.on('error', self._onError); + self._parser.setStream(self._sock); + }); +}; + Connection.prototype._processQueue = function() { if (this._curReq || !this._queue.length || !this._sock.writable) return; @@ -1257,7 +1297,9 @@ Connection.prototype._enqueue = function(fullcmd, promote, cb) { else this._queue.push(info); - if (!this._curReq) { + if (!this._curReq + && this.state !== 'disconnected' + && this.state !== 'upgrading') { // defer until next tick for requests like APPEND and FETCH where access to // the request object is needed immediately after enqueueing process.nextTick(function() { self._processQueue(); }); diff --git a/lib/Parser.js b/lib/Parser.js index 5f19ef0..c67e2ec 100644 --- a/lib/Parser.js +++ b/lib/Parser.js @@ -25,16 +25,15 @@ function Parser(stream, debug) { EventEmitter.call(this); - if (/^v0\.8\./.test(process.version)) - stream = new ReadableStream().wrap(stream); - - this._stream = stream; + this._stream = undefined; this._body = undefined; this._literallen = 0; this._literals = []; this._buffer = ''; this.debug = debug; + this.setStream(stream); + var self = this; function cb() { if (self._literallen > 0) @@ -47,6 +46,13 @@ function Parser(stream, debug) { } inherits(Parser, EventEmitter); +Parser.prototype.setStream = function(stream) { + if (/^v0\.8\./.test(process.version)) + stream = new ReadableStream().wrap(stream); + + this._stream = stream; +}; + Parser.prototype._tryread = function(n) { var r = this._stream.read(n); r && this._parse(r);