From c8a95325f0f52b4d911de65ba61e4cf3f897db90 Mon Sep 17 00:00:00 2001 From: Brian White Date: Mon, 28 Mar 2011 15:53:35 -0400 Subject: [PATCH] IDLE support If the IMAP server supports it, the IDLE extension will automatically be used in place of NOOP/polling when a mailbox is open. This means notifications of mailbox changes (e.g. when new mail arrives) will automatically be pushed to the client as they happen. Rejoice! --- imap.js | 60 ++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/imap.js b/imap.js index 1b45a26..c99a335 100644 --- a/imap.js +++ b/imap.js @@ -47,6 +47,14 @@ function ImapConnection (options) { permFlags: [], name: null, messages: { total: 0, new: 0 } + }, + ext: { + // Capability-specific state stuff + idle: { + MAX_WAIT: 1740000, // 29 mins in ms + sentIdle: false, + timeWaited: 0 // ms + } } }; this._options = extend(true, this._options, options); @@ -328,14 +336,12 @@ ImapConnection.prototype.connect = function(loginCb) { curReq._fetcher.emit('message', msg); msg.emit('end'); } - break; } } } } else if (data[0].indexOf('A') === 0) { // Tagged server response var sendBox = false; clearTimeout(self._state.tmrKeepalive); - self._state.tmrKeepalive = setTimeout(self._idleCheck.bind(self), self._state.tmoKeepalive); if (self._state.status === STATES.BOXSELECTING) { if (data[1] === 'OK') { @@ -347,6 +353,25 @@ ImapConnection.prototype.connect = function(loginCb) { } } + if (self._state.requests.length === 1) { + if (self._state.status === STATES.BOXSELECTED && + self._state.capabilities.indexOf('IDLE') > -1) { + // According to RFC 2177, we should re-IDLE at least every 29 + // minutes to avoid disconnection by the server + self._send('IDLE', undefined, true); + } + self._state.tmrKeepalive = setTimeout(function() { + if (self._state.isIdle) { + if (self._state.ext.idle.sentIdle) { + self._state.ext.idle.timeWaited += self._state.tmoKeepalive; + if (self._state.ext.idle.timeWaited >= self._state.ext.idle.MAX_WAIT) + self._send('IDLE', undefined, true); // restart IDLE + } else + self._noop(); + } + }, self._state.tmoKeepalive); + } + if (self._state.requests[0].command.indexOf('RENAME') > -1) { self._state.box.name = self._state.box._newName; delete self._state.box._newName; @@ -363,7 +388,7 @@ ImapConnection.prototype.connect = function(loginCb) { } else if (self._state.status === STATES.BOXSELECTED) { if (sendBox) // SELECT, EXAMINE, RENAME args.unshift(self._state.box); - // According to RFC3501, UID commands do not give errors for non-existant user-supplied UIDs, + // According to RFC 3501, UID commands do not give errors for non-existant user-supplied UIDs, // so give the callback empty results if we unexpectedly received no untagged responses. else if ((cmd.indexOf('UID FETCH') === 0 || cmd.indexOf('UID SEARCH') === 0) && args.length === 0) args.unshift([]); @@ -722,23 +747,32 @@ ImapConnection.prototype._resetBox = function() { this._state.box.messages.total = 0; this._state.box.messages.new = 0; }; -ImapConnection.prototype._idleCheck = function() { - if (this._state.isIdle) - this._noop(); -}; ImapConnection.prototype._noop = function() { if (this._state.status >= STATES.AUTH) - this._send('NOOP', undefined); + this._send('NOOP'); }; ImapConnection.prototype._send = function(cmdstr, cb, bypass) { - if (arguments.length > 0 && !bypass) + if (typeof cmdstr !== 'undefined' && !bypass) this._state.requests.push({ command: cmdstr, callback: cb, args: [] }); - if ((arguments.length === 0 && this._state.requests.length > 0) || this._state.requests.length === 1 || bypass) { + if ((typeof cmdstr === 'undefined' && this._state.requests.length) || + this._state.requests.length === 1 || bypass) { + var prefix = '', cmd = (bypass ? cmdstr : this._state.requests[0].command); clearTimeout(this._state.tmrKeepalive); this._state.isIdle = false; - var cmd = (bypass ? cmdstr : this._state.requests[0].command); - this._state.conn.cleartext.write('A' + ++this._state.curId + ' ' + cmd + CRLF); - debug('<>: A' + this._state.curId + ' ' + cmd); + if (this._state.ext.idle.sentIdle && cmd !== 'DONE') { + this._send('DONE', undefined, true); + this._state.ext.idle.sentIdle = false; + this._state.ext.idle.timeWaited = 0; + } 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 + prefix = 'IDLE '; + this._state.ext.idle.sentIdle = true; + } + if (cmd !== 'IDLE' && cmd !== 'DONE') + prefix = 'A' + ++this._state.curId + ' '; + this._state.conn.cleartext.write(prefix + cmd + CRLF); + debug('<>: ' + prefix + cmd); } };