From a99b2c0084c6cdf3fe983afdb697e3c1cf0b9523 Mon Sep 17 00:00:00 2001 From: mscdex Date: Sun, 7 Apr 2013 01:04:43 -0400 Subject: [PATCH] simplify IDLE mechanism and _send() logic --- lib/imap.js | 156 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 91 insertions(+), 65 deletions(-) diff --git a/lib/imap.js b/lib/imap.js index 0b40189..014e799 100644 --- a/lib/imap.js +++ b/lib/imap.js @@ -26,6 +26,8 @@ var CRLF = '\r\n', RE_UNRESP = /^\* (OK|PREAUTH|NO|BAD)(?:\r\n| (?:\[(.+)\] )?(.+))$/i, RE_TAGGED_RESP = /^A\d+ (OK|NO|BAD) (?:\[(.+?)\] )?(.+)$/i, RE_TEXT_CODE = /([^ ]+)(?: (.*))?$/, + RE_RES_IDLE = /^IDLE /i, + RE_RES_NOOP = /^NOOP /i, //RE_ISPARTIAL = /<(\d+)>$/, RE_DBLQ = /"/g, RE_CMD = /^([^ ]+)(?: |$)/, @@ -36,8 +38,7 @@ var CRLF = '\r\n', // extension constants var IDLE_NONE = 1, IDLE_WAIT = 2, - IDLE_READY = 3, - IDLE_DONE = 4; + IDLE_IDLING = 3; function ImapConnection(options) { if (!(this instanceof ImapConnection)) @@ -93,7 +94,6 @@ function ImapConnection(options) { idle: { MAX_WAIT: 300000, // 5 mins in ms state: IDLE_NONE, - reIDLE: false, timeStarted: undefined } } @@ -309,7 +309,7 @@ ImapConnection.prototype.connect = function(loginCb) { if (indata.line[0] === '*') { // Untagged server response var isUnsolicited = (requests[0] && requests[0].cmd === 'NOOP') - || (state.isIdle && state.ext.idle.state === IDLE_READY); + || (state.isIdle && state.ext.idle.state === IDLE_IDLING); if (m = XRegExp.exec(indata.line, REX_UNRESPNUM)) { // m.type = response type (numeric-based) m.type = m.type.toUpperCase(); @@ -624,18 +624,21 @@ ImapConnection.prototype.connect = function(loginCb) { indata.temp = undefined; indata.streaming = false; indata.expect = -1; + self.debug&&self.debug(line[0] === 'A' ? '[parsing incoming] saw tagged response' : '[parsing incoming] saw continuation response'); + + clearTimeout(state.tmrKeepalive); + if (line[0] === '+' && state.ext.idle.state === IDLE_WAIT) { - state.ext.idle.state = IDLE_READY; + state.ext.idle.state = IDLE_IDLING; state.ext.idle.timeStarted = Date.now(); + doKeepaliveTimer(); return process.nextTick(function() { self._send(); }); } var sendBox = false; - clearTimeout(state.tmrKeepalive); - if (state.status === STATES.BOXSELECTING) { if (/^A\d+ OK/i.test(line)) { sendBox = true; @@ -646,7 +649,6 @@ ImapConnection.prototype.connect = function(loginCb) { self._resetBox(); } } - if (requests[0].cmd === 'RENAME') { if (state.box._newName) { state.box.name = state.box._newName; @@ -711,41 +713,33 @@ ImapConnection.prototype.connect = function(loginCb) { requests[0].callback.apply(self, args); } - var recentCmd = requests[0].cmdstr; + var recentCmd = requests[0].cmd; requests.shift(); - if (requests.length === 0 && recentCmd !== 'LOGOUT') { - if (state.status >= STATES.AUTH && self.serverSupports('IDLE')) { - // According to RFC 2177, we should re-IDLE at least every 29 - // minutes to avoid disconnection by the server - self._send('IDLE', undefined, true); - } - state.tmrKeepalive = setTimeout(function idleHandler() { - if (state.isIdle) { - if (state.ext.idle.state === IDLE_READY) { - state.tmrKeepalive = setTimeout(idleHandler, state.tmoKeepalive); - var timeDiff = Date.now() - state.ext.idle.timeStarted; - if (timeDiff >= state.ext.idle.MAX_WAIT) - self._send('IDLE', undefined, true); // restart IDLE - } else if (!self.serverSupports('IDLE')) - self._noop(); - } - }, state.tmoKeepalive); - } else + + if (!requests.length && recentCmd !== 'LOGOUT') + doKeepalive(); + else process.nextTick(function() { self._send(); }); state.isIdle = true; - } else if (/^IDLE /i.test(indata.line)) { + } else if (RE_RES_IDLE.test(indata.line)) { self.debug&&self.debug('[parsing incoming] saw IDLE'); - if (requests.length) - process.nextTick(function() { self._send(); }); - state.isIdle = false; - state.ext.idle.state = IDLE_NONE; - state.ext.idle.timeStated = undefined; + requests.shift(); // remove IDLE request indata.line = undefined; - if (state.ext.idle.reIDLE) { - state.ext.idle.reIDLE = false; - self._send('IDLE', undefined, true); - } + state.ext.idle.state = IDLE_NONE; + state.ext.idle.timeStarted = undefined; + if (requests.length) { + state.isIdle = false; + self._send(); + } else + doKeepalive(); + } else if (RE_RES_NOOP.test(indata.line)) { + self.debug&&self.debug('[parsing incoming] saw NOOP'); + requests.shift(); // remove NOOP request + if (!requests.length) + doKeepaliveTimer(); + else + self._send(); } else { // unknown response self.debug&&self.debug('[parsing incoming] saw unexpected response: ' @@ -754,6 +748,30 @@ ImapConnection.prototype.connect = function(loginCb) { } } + function doKeepalive() { + if (state.status >= STATES.AUTH) { + if (self.serverSupports('IDLE')) + self._send('IDLE'); + else + self._noop(); + } + } + + function doKeepaliveTimer() { + state.tmrKeepalive = setTimeout(function idleHandler() { + if (state.isIdle) { + if (state.ext.idle.state === IDLE_IDLING) { + var timeDiff = Date.now() - state.ext.idle.timeStarted; + if (timeDiff >= state.ext.idle.MAX_WAIT) + self._send('DONE'); + else + state.tmrKeepalive = setTimeout(idleHandler, state.tmoKeepalive); + } else if (!self.serverSupports('IDLE')) + doKeepalive(); + } + }, state.tmoKeepalive); + } + state.conn.connect(this._options.port, this._options.host); state.tmrConn = setTimeout(function() { @@ -1503,7 +1521,6 @@ ImapConnection.prototype._reset = function() { this._state.tmrConn = null; this._state.ext.idle.state = IDLE_NONE; this._state.ext.idle.timeStarted = undefined; - this._state.ext.idle.reIDLE = false; this._state.indata.literals = []; this._state.indata.line = undefined; @@ -1539,47 +1556,56 @@ ImapConnection.prototype._noop = function() { this._send('NOOP'); }; -ImapConnection.prototype._send = function(cmdstr, cb, bypass) { +ImapConnection.prototype._send = function(cmdstr, cb) { if (!this._state.conn.writable) return; - if (cmdstr !== undefined && !bypass) { - this._state.requests.push({ + var reqs = this._state.requests, idle = this._state.ext.idle; + + if (cmdstr !== undefined) { + var info = { cmd: cmdstr.match(RE_CMD)[1], cmdstr: cmdstr, callback: cb, cbargs: [] - }); + }; + if (cmdstr === 'IDLE' || cmdstr === 'DONE' || cmdstr === 'NOOP') + reqs.unshift(info); + else + reqs.push(info); } - if (this._state.ext.idle.state === IDLE_WAIT - || (this._state.ext.idle.state === IDLE_DONE && cmdstr !== 'DONE')) + + if (idle.state !== IDLE_NONE && cmdstr !== 'DONE') { + if (cmdstr !== undefined) + this._send('DONE'); return; - if ((cmdstr === undefined && this._state.requests.length) - || this._state.requests.length === 1 || bypass) { - var prefix = '', - cmd = (bypass ? cmdstr : this._state.requests[0].cmdstr); + } + + if ((cmdstr === undefined && reqs.length) || reqs.length === 1 + || cmdstr === 'DONE') { + var prefix = '', curReq = reqs[0]; + + cmdstr = curReq.cmdstr; + clearTimeout(this._state.tmrKeepalive); - if (this._state.ext.idle.state === IDLE_READY && cmd !== 'DONE') { - this._state.ext.idle.state = IDLE_DONE; - if (cmd === 'IDLE') - this._state.ext.idle.reIDLE = true; - return this._send('DONE', undefined, true); - } else if (cmd === 'IDLE') { - // we use a different prefix to differentiate and disregard the tagged - // response the server will send us when we issue DONE + + if (cmdstr === 'IDLE') { + // we use a different prefix to differentiate and disregard the tagged + // response the server will send us when we issue DONE prefix = 'IDLE '; this._state.ext.idle.state = IDLE_WAIT; - } - if (cmd !== 'IDLE' && cmd !== 'DONE') + } else if (cmdstr === 'NOOP') + prefix = 'NOOP '; + else if (cmdstr !== 'DONE') prefix = 'A' + (++this._state.curId) + ' '; - this._state.conn.write(prefix); - this._state.conn.write(cmd); - this._state.conn.write(CRLF); - this.debug&&this.debug('\n==> ' + prefix + cmd + '\n'); - if (this._state.requests[0] - && (this._state.requests[0].cmd === 'EXAMINE' - || this._state.requests[0].cmd === 'SELECT')) + + this._state.conn.write(prefix + cmdstr + CRLF); + this.debug&&this.debug('\n==> ' + prefix + cmdstr + '\n'); + + if (curReq.cmd === 'EXAMINE' || curReq.cmd === 'SELECT') this._state.status = STATES.BOXSELECTING; + else if (cmdstr === 'DONE') + reqs.shift(); } };