diff --git a/lib/imap.js b/lib/imap.js index 4f5f5c6..8555e01 100644 --- a/lib/imap.js +++ b/lib/imap.js @@ -22,19 +22,26 @@ var CRLF = '\r\n', BOXSELECTING: 3, BOXSELECTED: 4 }, - RE_LITHEADER = /(?:((?:BODY\[.*\](?:<\d+>)?)?|[^ ]+) )?\{(\d+)\}$/i, - RE_UNRESP = /^\* (OK|PREAUTH|NO|BAD) (?:\[(.+)\] )?(.+)$/i, + RE_LITHEADER = /(?:((?:BODY\[.*\](?:<\d+>)?)?|[^ ]+) )?\{(\d+)\}(?:$|\r\n)/i, + RE_UNRESP = /^\* (OK|PREAUTH|NO|BAD)(?:\r\n|(?: \[(.+?)\])?(?: (.+))?)(?:$|\r\n)/i, + RE_TAGGED_RESP = /^A\d+ (OK|NO|BAD) (?:\[(.+?)\] )?(.+)(?:$|\r\n)/i, + RE_TEXT_CODE = /([^ ]+)(?: (.*))?$/, + RE_RES_IDLE = /^IDLE /i, + RE_RES_NOOP = /^NOOP /i, + RE_CMD_FETCH = /^(?:UID )?FETCH/i, + RE_PARTID = /^(?:[\d]+[\.]{0,1})*[\d]+$/, + RE_ESCAPE = /\\\\/g, //RE_ISPARTIAL = /<(\d+)>$/, RE_DBLQ = /"/g, RE_CMD = /^([^ ]+)(?: |$)/, RE_ISHEADER = /HEADER/, - REX_UNRESPDATA = XRegExp('^\\* (?:(?:(?NAMESPACE) (?(?:NIL|\\((?:\\(.+\\))+\\))) (?(?:NIL|\\((?:\\(.+\\))+\\))) (?(?:NIL|\\((?:\\(.+\\))+\\))))|(?:(?FLAGS) \\((?.*)\\))|(?:(?LIST|LSUB|XLIST) \\((?.*)\\) (?"[^"]+"|NIL) (?.+))|(?:(?(SEARCH|SORT))(?: (?.*))?)|(?:(?STATUS) (?.+) \\((?.*)\\))|(?:(?CAPABILITY) (?.+))|(?:(?BYE) (?:\\[(?.+)\\] )?(?.+)))[ \t]*(?:\r\n|$)', 'i'), - REX_UNRESPNUM = XRegExp('^\\* (?\\d+) (?:(?EXISTS)|(?RECENT)|(?EXPUNGE)|(?:(?FETCH) \\((?.*)\\)))[ \t]*(?:\r\n|$)', 'i'); + REX_UNRESPDATA = XRegExp('^\\* (?:(?:(?NAMESPACE) (?(?:NIL|\\((?:\\(.+\\))+\\))) (?(?:NIL|\\((?:\\(.+\\))+\\))) (?(?:NIL|\\((?:\\(.+\\))+\\))))|(?:(?FLAGS) \\((?.*)\\))|(?:(?LIST|LSUB|XLIST) \\((?.*)\\) (?"[^"]+"|NIL) (?.+))|(?:(?(SEARCH|SORT))(?: (?.*))?)|(?:(?STATUS) (?.+) \\((?.*)\\))|(?:(?CAPABILITY) (?.+))|(?:(?BYE) (?:\\[(?.+)\\] )?(?.+)))[ \t]*(?:$|\r\n)', 'i'), + REX_UNRESPNUM = XRegExp('^\\* (?\\d+) (?:(?EXISTS)|(?RECENT)|(?EXPUNGE)|(?:(?FETCH) \\((?.*)\\)))[ \t]*(?:$|\r\n)', 'i'); // extension constants var IDLE_NONE = 1, IDLE_WAIT = 2, - IDLE_READY = 3, + IDLE_IDLING = 3, IDLE_DONE = 4; function ImapConnection(options) { @@ -47,7 +54,9 @@ function ImapConnection(options) { password: options.password || '', host: options.host || 'localhost', port: options.port || 143, - secure: options.secure || false, + secure: options.secure === true ? { // secure = true means default behavior + rejectUnauthorized: false // Force pre-node-0.9.2 behavior + } : (options.secure || false), connTimeout: options.connTimeout || 10000, // connection timeout in msecs xoauth: options.xoauth, xoauth2: options.xoauth2 @@ -87,9 +96,8 @@ function ImapConnection(options) { ext: { // Capability-specific state info idle: { - MAX_WAIT: 1740000, // 29 mins in ms + MAX_WAIT: 300000, // 5 mins in ms state: IDLE_NONE, - reIDLE: false, timeStarted: undefined } } @@ -124,10 +132,14 @@ ImapConnection.prototype.connect = function(loginCb) { socket.setTimeout(0); if (this._options.secure) { + var tlsOptions = {}; + for (var k in this._options.secure) + tlsOptions[k] = this._options.secure[k]; + tlsOptions.socket = state.conn; if (process.version.indexOf('v0.6.') > -1) - socket = tls.connect(null, { socket: state.conn }, onconnect); + socket = tls.connect(null, tlsOptions, onconnect); else - socket = tls.connect({ socket: state.conn }, onconnect); + socket = tls.connect(tlsOptions, onconnect); } else state.conn.once('connect', onconnect); @@ -156,7 +168,7 @@ ImapConnection.prototype.connect = function(loginCb) { self.emit('close', had_error); }); - state.conn.on('error', function(err) { + socket.on('error', function(err) { clearTimeout(state.tmrConn); err.level = 'socket'; if (state.status === STATES.NOCONNECT) @@ -174,7 +186,7 @@ ImapConnection.prototype.connect = function(loginCb) { return loginCb(err); } // Next, get the list of available namespaces if supported (RFC2342) - if (!checkedNS && self._serverSupports('NAMESPACE')) { + if (!checkedNS && self.serverSupports('NAMESPACE')) { // Re-enter this function after we've obtained the available // namespaces checkedNS = true; @@ -299,9 +311,9 @@ 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); + var isUnsolicited = (requests[0] && requests[0].cmd === 'NOOP') + || (state.isIdle && state.ext.idle.state !== IDLE_NONE) + || !requests.length; if (m = XRegExp.exec(indata.line, REX_UNRESPNUM)) { // m.type = response type (numeric-based) m.type = m.type.toUpperCase(); @@ -311,12 +323,24 @@ ImapConnection.prototype.connect = function(loginCb) { // m.info = message details var data, parsed, headers, f, lenf, body, lenb, msg, bodies, details, val; + + isUnsolicited = isUnsolicited + || (requests[0] + && !RE_CMD_FETCH.test(requests[0].cmdstr)); + if (!isUnsolicited) bodies = parsers.parseFetchBodies(m.info, indata.literals); + details = new ImapMessage(); parsers.parseFetch(m.info, indata.literals, details); details.seqno = parseInt(m.num, 10); + if (typeof details['x-gm-labels'] !== undefined) { + var labels = details['x-gm-labels']; + for (var i=0, len=labels.length; i -1) { state.box.newKeywords = true; permFlags.splice(idx, 1); @@ -562,8 +594,8 @@ ImapConnection.prototype.connect = function(loginCb) { }); } } else if (state.status === STATES.BOXSELECTED) { - if (m = /^UIDVALIDITY (\d+)/i.exec(code)) { - state.box.uidvalidity = parseInt(m[1], 10); + if (code === 'UIDVALIDITY') { + state.box.uidvalidity = parseInt(codeval, 10); self.emit('uidvalidity', state.box.uidvalidity); } } @@ -608,18 +640,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; @@ -630,7 +665,6 @@ ImapConnection.prototype.connect = function(loginCb) { self._resetBox(); } } - if (requests[0].cmd === 'RENAME') { if (state.box._newName) { state.box.name = state.box._newName; @@ -640,18 +674,24 @@ ImapConnection.prototype.connect = function(loginCb) { } if (typeof requests[0].callback === 'function') { + m = RE_TAGGED_RESP.exec(line); var err = null; var args = requests[0].cbargs, cmdstr = requests[0].cmdstr; - if (line[0] === '+') { - if (requests[0].cmd !== 'APPEND') { - err = new Error('Unexpected continuation'); + if (!m) { + if (requests[0].cmd === 'APPEND') + return requests[0].callback(); + else { + var isXOAuth2 = (cmdstr.indexOf('AUTHENTICATE XOAUTH2') === 0), + msg = (isXOAuth2 + ? new Buffer(line.substr(2), 'base64').toString('utf8') + : 'Unexpected continuation'); + err = new Error(msg); err.level = 'protocol'; - err.type = 'continuation'; + err.type = (isXOAuth2 ? 'failure' : 'continuation'); err.request = cmdstr; - } else - return requests[0].callback(); - } else if (m = /^A\d+ (NO|BAD) (?:\[(.+?)\] )?(.+)$/i.exec(line)) { + } + } else if (m[1] !== 'OK') { // m[1]: error type // m[2]: resp-text-code // m[3]: message @@ -672,45 +712,50 @@ ImapConnection.prototype.connect = function(loginCb) { ) && args.length === 0) args.unshift([]); } + if (m) { + var msg = m[3], info; + if (m[2]) { + m = RE_TEXT_CODE.exec(m[2]); + info = { + code: m[1].toUpperCase(), + codeval: m[2], + message: msg + }; + } else + info = { message: msg }; + args.push(info); + } args.unshift(err); 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: ' @@ -719,6 +764,31 @@ 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) { + state.ext.idle.state = IDLE_DONE; + 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() { @@ -796,7 +866,7 @@ ImapConnection.prototype.getBoxes = function(namespace, cb) { cb = namespace; namespace = ''; } - this._send((!this._serverSupports('XLIST') ? 'LIST' : 'XLIST') + this._send((!this.serverSupports('XLIST') ? 'LIST' : 'XLIST') + ' "' + utils.escape(utf7.encode(''+namespace)) + '" "*"', cb); }; @@ -837,7 +907,8 @@ ImapConnection.prototype.append = function(data, options, cb) { if (options.flags) { if (!Array.isArray(options.flags)) options.flags = [options.flags]; - cmd += " (\\" + options.flags.join(' \\') + ")"; + if (options.flags.length > 0) + cmd += " (\\" + options.flags.join(' \\') + ")"; } if (options.date) { if (!isDate(options.date)) @@ -863,9 +934,9 @@ ImapConnection.prototype.append = function(data, options, cb) { cmd += (Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data)); cmd += '}'; var self = this, step = 1; - this._send(cmd, function(err) { + this._send(cmd, function(err, info) { if (err || step++ === 2) - return cb(err); + return cb(err, info); self._state.conn.write(data); self._state.conn.write(CRLF); self.debug&&self.debug('\n==> ' + inspect(data.toString()) + '\n'); @@ -896,7 +967,7 @@ ImapConnection.prototype._sort = function(which, sorts, options, cb) { throw new Error('Expected array with at least one sort criteria'); if (!Array.isArray(options)) throw new Error('Expected array for search options'); - if (!this._serverSupports('SORT')) + if (!this.serverSupports('SORT')) return cb(new Error('Sorting is not supported on the server')); var criteria = sorts.map(function(criterion) { @@ -991,9 +1062,9 @@ ImapConnection.prototype._fetch = function(which, uids, options, what, cb) { for (var i = 0, wp, pprefix, len = what.length; i < len; ++i) { wp = what[i]; parse = true; - if (wp.id !== undefined && !/^(?:[\d]+[\.]{0,1})*[\d]+$/.test(''+wp.id)) + if (wp.id !== undefined && !RE_PARTID.test(''+wp.id)) throw new Error('Invalid part id: ' + wp.id); - if (( (wp.headers + if (( (typeof wp.headers === 'object' && (!wp.headers.fields || (Array.isArray(wp.headers.fields) && wp.headers.fields.length === 0) @@ -1001,7 +1072,7 @@ ImapConnection.prototype._fetch = function(which, uids, options, what, cb) { && wp.headers.parse === false ) || - (wp.headersNot + (typeof wp.headersNot === 'object' && (!wp.headersNot.fields || (Array.isArray(wp.headersNot.fields) && wp.headersNot.fields.length === 0) @@ -1155,7 +1226,7 @@ ImapConnection.prototype._fetch = function(which, uids, options, what, cb) { } // always fetch GMail-specific bits of information when on GMail - if (this._serverSupports('X-GM-EXT-1')) + if (this.serverSupports('X-GM-EXT-1')) extensions = 'X-GM-THRID X-GM-MSGID X-GM-LABELS '; var cmd = which; @@ -1233,7 +1304,7 @@ ImapConnection.prototype.delLabels = function(uids, labels, cb) { }; ImapConnection.prototype._storeLabels = function(which, uids, labels, mode, cb) { - if (!this._serverSupports('X-GM-EXT-1')) + if (!this.serverSupports('X-GM-EXT-1')) throw new Error('Server must support X-GM-EXT-1 capability'); if (this._state.status !== STATES.BOXSELECTED) throw new Error('No mailbox is currently selected'); @@ -1284,46 +1355,47 @@ ImapConnection.prototype._move = function(which, uids, boxTo, cb) { throw new Error('Cannot move message: ' + 'server does not allow deletion of messages'); } else { - this._copy(which, uids, boxTo, function ccb(err, reentryCount, deletedUIDs, - counter) { - if (err) - return cb(err); - - counter = counter || 0; - // Make sure we don't expunge any messages marked as Deleted except the - // one we are moving - if (reentryCount === undefined) { - self.search(['DELETED'], function(e, result) { - ccb(e, 1, result); - }); - } else if (reentryCount === 1) { - if (counter < deletedUIDs.length) { - self.delFlags(deletedUIDs[counter], 'Deleted', function(e) { - process.nextTick(function() { - ccb(e, reentryCount, deletedUIDs, counter + 1); - }); + this._copy(which, uids, boxTo, + function ccb(err, info, reentryCount, deletedUIDs, counter) { + if (err) + return cb(err, info); + + counter = counter || 0; + // Make sure we don't expunge any messages marked as Deleted except the + // one we are moving + if (reentryCount === undefined) { + self.search(['DELETED'], function(e, result) { + ccb(e, info, 1, result); }); - } else - ccb(err, reentryCount + 1, deletedUIDs); - } else if (reentryCount === 2) { - self.addFlags(uids, 'Deleted', function(e) { - ccb(e, reentryCount + 1, deletedUIDs); - }); - } else if (reentryCount === 3) { - self.removeDeleted(function(e) { - ccb(e, reentryCount + 1, deletedUIDs); - }); - } else if (reentryCount === 4) { - if (counter < deletedUIDs.length) { - self.addFlags(deletedUIDs[counter], 'Deleted', function(e) { - process.nextTick(function() { - ccb(e, reentryCount, deletedUIDs, counter + 1); + } else if (reentryCount === 1) { + if (counter < deletedUIDs.length) { + self.delFlags(deletedUIDs[counter], 'Deleted', function(e) { + process.nextTick(function() { + ccb(e, info, reentryCount, deletedUIDs, counter + 1); + }); }); + } else + ccb(err, info, reentryCount + 1, deletedUIDs); + } else if (reentryCount === 2) { + self.addFlags(uids, 'Deleted', function(e) { + ccb(e, info, reentryCount + 1, deletedUIDs); }); - } else - cb(); + } else if (reentryCount === 3) { + self.removeDeleted(function(e) { + ccb(e, info, reentryCount + 1, deletedUIDs); + }); + } else if (reentryCount === 4) { + if (counter < deletedUIDs.length) { + self.addFlags(deletedUIDs[counter], 'Deleted', function(e) { + process.nextTick(function() { + ccb(e, info, reentryCount, deletedUIDs, counter + 1); + }); + }); + } else + cb(err, info); + } } - }); + ); } }; @@ -1372,7 +1444,7 @@ ImapConnection.prototype.__defineGetter__('seq', function() { // Private/Internal Functions -ImapConnection.prototype._serverSupports = function(capability) { +ImapConnection.prototype.serverSupports = function(capability) { return (this.capabilities.indexOf(capability) > -1); }; @@ -1435,13 +1507,13 @@ ImapConnection.prototype._login = function(cb) { }; if (this._state.status === STATES.NOAUTH) { - if (this._serverSupports('LOGINDISABLED')) + if (this.serverSupports('LOGINDISABLED')) return cb(new Error('Logging in is disabled on this server')); - if (this._serverSupports('AUTH=XOAUTH') && this._options.xoauth) { + if (this.serverSupports('AUTH=XOAUTH') && this._options.xoauth) { this._send('AUTHENTICATE XOAUTH ' + utils.escape(this._options.xoauth), fnReturn); - } else if (this._serverSupports('AUTH=XOAUTH2') && this._options.xoauth2) { + } else if (this.serverSupports('AUTH=XOAUTH2') && this._options.xoauth2) { this._send('AUTHENTICATE XOAUTH2 ' + utils.escape(this._options.xoauth2), fnReturn); } else if (this._options.username && this._options.password) { @@ -1467,7 +1539,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; @@ -1503,47 +1574,59 @@ 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 || reqs.length > 1) + && idle.state === IDLE_IDLING) { + idle.state = IDLE_DONE; + 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(); } };