var util = require('util'), Socket = require('net').Socket, EventEmitter = require('events').EventEmitter; var parsers = require('./imap.parsers'), utils = require('./imap.utilities'); var emptyFn = function() {}, CRLF = '\r\n', STATES = { NOCONNECT: 0, NOAUTH: 1, AUTH: 2, BOXSELECTING: 3, BOXSELECTED: 4 }, BOX_ATTRIBS = ['NOINFERIORS', 'NOSELECT', 'MARKED', 'UNMARKED'], reFetch = /^\* (\d+) FETCH .+? \{(\d+)\}\r\n/; var IDLE_NONE = 1, IDLE_WAIT = 2, IDLE_READY = 3; function ImapConnection (options) { if (!(this instanceof ImapConnection)) return new ImapConnection(options); EventEmitter.call(this); this._options = { username: '', password: '', host: 'localhost', port: 143, secure: false, connTimeout: 10000, // connection timeout in msecs debug: false }; this._state = { status: STATES.NOCONNECT, conn: null, curId: 0, requests: [], numCapRecvs: 0, isReady: false, isIdle: true, tmrKeepalive: null, tmoKeepalive: 10000, tmrConn: null, curData: null, curExpected: 0, curXferred: 0, box: { _uidnext: 0, _flags: [], _newKeywords: false, validity: 0, keywords: [], permFlags: [], name: null, messages: { total: 0, new: 0 } }, ext: { // Capability-specific state info idle: { MAX_WAIT: 1740000, // 29 mins in ms state: IDLE_NONE, timeWaited: 0 // ms } } }; this._options = utils.extend(true, this._options, options); if (typeof this._options.debug === 'function') this.debug = this._options.debug; else this.debug = emptyFn; this.delim = null; this.namespaces = { personal: [], other: [], shared: [] }; this.capabilities = []; }; util.inherits(ImapConnection, EventEmitter); exports.ImapConnection = ImapConnection; ImapConnection.prototype.connect = function(loginCb) { var self = this, fnInit = function() { // First get pre-auth capabilities, including server-supported auth // mechanisms self._send('CAPABILITY', function() { // Next, attempt to login self._login(function(err, reentry) { if (err) { loginCb(err); return; } // Next, get the list of available namespaces if supported if (!reentry && self.capabilities.indexOf('NAMESPACE') > -1) { var fnMe = arguments.callee; // Re-enter this function after we've obtained the available // namespaces self._send('NAMESPACE', function(e) { fnMe.call(this, e, true); }); return; } // Lastly, get the top-level mailbox hierarchy delimiter used by the // server self._send('LIST "" ""', loginCb); }); }); }; loginCb = loginCb || emptyFn; this._reset(); this._state.conn = new Socket(); this._state.conn.setKeepAlive(true); if (this._options.secure) { // TODO: support STARTTLS this._state.conn.cleartext = utils.setSecure(this._state.conn); this._state.conn.on('secure', function() { self.debug('Secure connection made.'); }); } else this._state.conn.cleartext = this._state.conn; this._state.conn.on('connect', function() { clearTimeout(self._state.tmrConn); self.debug('Connected to host.'); self._state.conn.cleartext.write(''); self._state.status = STATES.NOAUTH; }); this._state.conn.on('end', function() { self._reset(); self.debug('FIN packet received. Disconnecting...'); self.emit('end'); }); function errorHandler(err) { clearTimeout(self._state.tmrConn); if (self._state.status === STATES.NOCONNECT) loginCb(new Error('Unable to connect. Reason: ' + err)); self.emit('error', err); self.debug('Error occurred: ' + err); } //this._state.conn.on('error', errorHandler); this._state.conn.cleartext.on('error', errorHandler); this._state.conn.on('close', function(had_error) { self._reset(); self.debug('Connection forcefully closed.'); self.emit('close', had_error); }); this._state.conn.on('ready', fnInit); this._state.conn.cleartext.on('data', function(data) { if (data.length === 0) return; var trailingCRLF = false, literalInfo; self.debug('\n<>: ' + util.inspect(data.toString()) + '\n'); if (self._state.curExpected === 0) { if (utils.bufferIndexOf(data, CRLF) === -1) { if (self._state.curData) self._state.curData = utils.bufferAppend(self._state.curData, data); else self._state.curData = data; return; } if (self._state.curData && self._state.curData.length) { data = utils.bufferAppend(self._state.curData, data); self._state.curData = null; } } // Don't mess with incoming data if it's part of a literal var strdata; if (self._state.curExpected > 0) { var curReq = self._state.requests[0]; if (!curReq._done) { var chunk = data; self._state.curXferred += data.length; if (self._state.curXferred > self._state.curExpected) { var pos = data.length - (self._state.curXferred - self._state.curExpected), extra = data.slice(pos); if (pos > 0) chunk = data.slice(0, pos); else chunk = undefined; data = extra; curReq._done = 1; } if (chunk && chunk.length) { if (curReq._msgtype === 'headers') { chunk.copy(self._state.curData, curReq.curPos, 0); curReq.curPos += chunk.length; } else curReq._msg.emit('data', chunk); } } if (curReq._done) { var restDesc; if (curReq._done === 1) { if (curReq._msgtype === 'headers') curReq._headers = self._state.curData.toString(); self._state.curData = null; curReq._done = true; } if (self._state.curData) self._state.curData = utils.bufferAppend(self._state.curData, data); else self._state.curData = data; if (restDesc = self._state.curData.toString().match(/^(.*?)\)\r\n/)) { if (restDesc[1]) { restDesc[1] = restDesc[1].trim(); if (restDesc[1].length) restDesc[1] = ' ' + restDesc[1]; } else restDesc[1] = ''; parsers.parseFetch(curReq._desc + restDesc[1], curReq._headers, curReq._msg); var curData = self._state.curData; data = curData.slice(utils.bufferIndexOf(curData, CRLF) + 2); curReq._done = false; self._state.curXferred = 0; self._state.curExpected = 0; self._state.curData = null; curReq._msg.emit('end'); if (data.length && data[0] === 42/* '*' */) { self._state.conn.cleartext.emit('data', data); return; } } else return; } else return; } else if (self._state.curExpected === 0 && (literalInfo = (strdata = data.toString()).match(reFetch))) { self._state.curExpected = parseInt(literalInfo[2], 10); var idxCRLF = utils.bufferIndexOf(data, CRLF), curReq = self._state.requests[0], type = /BODY\[(.*)\](?:\<\d+\>)?/.exec(strdata.substring(0, idxCRLF)), msg = new ImapMessage(), desc = strdata.substring(utils.bufferIndexOf(data, '(') + 1, idxCRLF) .trim(); msg.seqno = parseInt(literalInfo[1], 10); type = type[1]; curReq._desc = desc; curReq._msg = msg; curReq._fetcher.emit('message', msg); curReq._msgtype = (type.indexOf('HEADER') === 0 ? 'headers' : 'body'); if (curReq._msgtype === 'headers') { self._state.curData = new Buffer(self._state.curExpected); curReq.curPos = 0; } self._state.conn.cleartext.emit('data', data.slice(idxCRLF + 2)); return; } if (data.length === 0) return; var endsInCRLF = (data[data.length-2] === 13 && data[data.length-1] === 10); data = utils.bufferSplit(data, CRLF); // Defer any extra server responses found in the incoming data if (data.length > 1) { for (var i=1,len=data.length; i -1) { self._state.box._newKeywords = true; permFlags.splice(idx, 1); } self._state.box.keywords = keywords = permFlags .filter(function(flag) { return (flag[0] !== '\\'); }); for (var i=0; i -1 ) || (self._state.isIdle && self._state.ext.idle.state === IDLE_READY); switch (data[2]) { case 'EXISTS': // mailbox total message count var prev = self._state.box.messages.total, now = parseInt(data[1]); self._state.box.messages.total = now; if (self._state.status !== STATES.BOXSELECTING && now > prev) { self._state.box.messages.new = now-prev; self.emit('mail', self._state.box.messages.new); // new mail } break; case 'RECENT': // messages marked with the \Recent flag (i.e. new messages) self._state.box.messages.new = parseInt(data[1]); break; case 'EXPUNGE': // confirms permanent deletion of a single message if (self._state.box.messages.total > 0) self._state.box.messages.total--; if (isUnsolicited) self.emit('deleted', parseInt(data[1], 10)); break; default: // fetches without header or body (part) retrievals if (/^FETCH/.test(data[2])) { var msg = new ImapMessage(); parsers.parseFetch(data[2].substring(data[2].indexOf("(") + 1, data[2].lastIndexOf(")") ), "", msg); msg.seqno = parseInt(data[1], 10); if (self._state.requests.length && self._state.requests[0].command.indexOf('FETCH') > -1) { var curReq = self._state.requests[0]; curReq._fetcher.emit('message', msg); msg.emit('end'); } else if (isUnsolicited) self.emit('msgupdate', msg); } } } } } else if (data[0][0] === 'A' || data[0] === '+') { // Tagged server response or continuation response if (data[0] === '+' && self._state.ext.idle.state === IDLE_WAIT) { self._state.ext.idle.state = IDLE_READY; return process.nextTick(function() { self._send(); }); } var sendBox = false; clearTimeout(self._state.tmrKeepalive); if (self._state.status === STATES.BOXSELECTING) { if (data[1] === 'OK') { sendBox = true; self._state.status = STATES.BOXSELECTED; } else { self._state.status = STATES.AUTH; self._resetBox(); } } if (self._state.requests[0].command.indexOf('RENAME') > -1) { self._state.box.name = self._state.box._newName; delete self._state.box._newName; sendBox = true; } if (typeof self._state.requests[0].callback === 'function') { var err = null; var args = self._state.requests[0].args, cmd = self._state.requests[0].command; if (data[0] === '+') { if (cmd.indexOf('APPEND') !== 0) { err = new Error('Unexpected continuation'); err.type = 'continuation'; err.request = cmd; } else return self._state.requests[0].callback(); } else if (data[1] !== 'OK') { err = new Error('Error while executing request: ' + data[2]); err.type = data[1]; err.request = cmd; } else if (self._state.status === STATES.BOXSELECTED) { if (sendBox) // SELECT, EXAMINE, RENAME args.unshift(self._state.box); // 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([]); } args.unshift(err); self._state.requests[0].callback.apply({}, args); } var recentCmd = self._state.requests[0].command; self._state.requests.shift(); if (self._state.requests.length === 0 && recentCmd !== 'LOGOUT') { if (self._state.status === STATES.BOXSELECTED && self.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.state === IDLE_READY) { 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 if (self.capabilities.indexOf('IDLE') === -1) self._noop(); } }, self._state.tmoKeepalive); } else process.nextTick(function() { self._send(); }); self._state.isIdle = true; } else if (data[0] === 'IDLE') { if (self._state.requests.length) process.nextTick(function() { self._send(); }); self._state.isIdle = false; self._state.ext.idle.state = IDLE_NONE; self._state.ext.idle.timeWaited = 0; } else { // unknown response } }); this._state.conn.connect(this._options.port, this._options.host); this._state.tmrConn = setTimeout(this._fnTmrConn.bind(this), this._options.connTimeout, loginCb); }; ImapConnection.prototype.isAuthenticated = function() { return this._state.status >= STATES.AUTH; }; ImapConnection.prototype.logout = function(cb) { if (this._state.status >= STATES.NOAUTH) this._send('LOGOUT', cb); else throw new Error('Not connected'); }; ImapConnection.prototype.openBox = function(name, readOnly, cb) { if (this._state.status < STATES.AUTH) throw new Error('Not connected or authenticated'); if (this._state.status === STATES.BOXSELECTED) this._resetBox(); if (cb === undefined) { if (readOnly === undefined) cb = emptyFn; else cb = readOnly; readOnly = false; } this._state.status = STATES.BOXSELECTING; this._state.box.name = name; this._send((readOnly ? 'EXAMINE' : 'SELECT') + ' "' + utils.escape(name) + '"', cb); }; // also deletes any messages in this box marked with \Deleted ImapConnection.prototype.closeBox = function(cb) { var self = this; if (this._state.status !== STATES.BOXSELECTED) throw new Error('No mailbox is currently selected'); this._send('CLOSE', function(err) { if (!err) { self._state.status = STATES.AUTH; self._resetBox(); } cb(err); }); }; ImapConnection.prototype.removeDeleted = function(cb) { if (this._state.status !== STATES.BOXSELECTED) throw new Error('No mailbox is currently selected'); cb = arguments[arguments.length-1]; this._send('EXPUNGE', cb); }; ImapConnection.prototype.getBoxes = function(namespace, cb) { cb = arguments[arguments.length-1]; if (arguments.length !== 2) namespace = ''; this._send(((this.capabilities.indexOf('XLIST') == -1) ? 'LIST' : 'XLIST') + ' "' + utils.escape(namespace) + '" "*"', cb); }; ImapConnection.prototype.addBox = function(name, cb) { cb = arguments[arguments.length-1]; if (typeof name !== 'string' || name.length === 0) throw new Error('Mailbox name must be a string describing the full path' + ' of a new mailbox to be created'); this._send('CREATE "' + utils.escape(name) + '"', cb); }; ImapConnection.prototype.delBox = function(name, cb) { cb = arguments[arguments.length-1]; if (typeof name !== 'string' || name.length === 0) throw new Error('Mailbox name must be a string describing the full path' + ' of an existing mailbox to be deleted'); this._send('DELETE "' + utils.escape(name) + '"', cb); }; ImapConnection.prototype.renameBox = function(oldname, newname, cb) { cb = arguments[arguments.length-1]; if (typeof oldname !== 'string' || oldname.length === 0) throw new Error('Old mailbox name must be a string describing the full path' + ' of an existing mailbox to be renamed'); else if (typeof newname !== 'string' || newname.length === 0) throw new Error('New mailbox name must be a string describing the full path' + ' of a new mailbox to be renamed to'); if (this._state.status === STATES.BOXSELECTED && oldname === this._state.box.name && oldname !== 'INBOX') this._state.box._newName = oldname; this._send('RENAME "' + utils.escape(oldname) + '" "' + utils.escape(newname) + '"', cb); }; ImapConnection.prototype.search = function(options, cb) { this._search('UID ', options, cb); }; ImapConnection.prototype._search = function(which, options, cb) { if (this._state.status !== STATES.BOXSELECTED) throw new Error('No mailbox is currently selected'); if (!Array.isArray(options)) throw new Error('Expected array for search options'); this._send(which + 'SEARCH' + utils.buildSearchQuery(options, this.capabilities), cb); }; ImapConnection.prototype.append = function(data, options, cb) { if (typeof options === 'function') { cb = options; options = {}; } options = options || {}; if (!('mailbox' in options)) { if (this._state.status !== STATES.BOXSELECTED) throw new Error('No mailbox specified or currently selected'); else options.mailbox = this._state.box.name } cmd = 'APPEND "' + utils.escape(options.mailbox) + '"'; if ('flags' in options) { if (!Array.isArray(options.flags)) options.flags = Array(options.flags); cmd += " (\\" + options.flags.join(' \\') + ")"; } if ('date' in options) { if (!(options.date instanceof Date)) throw new Error('Expected null or Date object for date'); cmd += ' "' + options.date.getDate() + '-' + utils.MONTHS[options.date.getMonth()] + '-' + options.date.getFullYear(); cmd += ' ' + ('0' + options.date.getHours()).slice(-2) + ':' + ('0' + options.date.getMinutes()).slice(-2) + ':' + ('0' + options.date.getSeconds()).slice(-2); cmd += ((options.date.getTimezoneOffset() > 0) ? ' -' : ' +' ); cmd += ('0' + (-options.date.getTimezoneOffset() / 60)).slice(-2); cmd += ('0' + (-options.date.getTimezoneOffset() % 60)).slice(-2); cmd += '"'; } cmd += ' {'; cmd += (Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data)); cmd += '}'; var self = this, step = 1; this._send(cmd, function(err) { if (err || step++ === 2) return cb(err); self._state.conn.cleartext.write(data); self._state.conn.cleartext.write(CRLF); self.debug('\n<>: ' + util.inspect(data.toString()) + '\n'); }); }; ImapConnection.prototype.fetch = function(uids, options) { return this._fetch('UID ', uids, options); }; ImapConnection.prototype._fetch = function(which, uids, options) { if (this._state.status !== STATES.BOXSELECTED) throw new Error('No mailbox is currently selected'); if (uids === undefined || uids === null || (Array.isArray(uids) && uids.length === 0)) throw new Error('Nothing to fetch'); if (!Array.isArray(uids)) uids = [uids]; utils.validateUIDList(uids); var opts = { markSeen: false, request: { struct: true, headers: true, // \_______ at most one of these can be used for any given // _______ fetch request body: false // / } }, toFetch, bodyRange = '', self = this; if (typeof options !== 'object') options = {}; utils.extend(true, opts, options); if (!Array.isArray(opts.request.headers)) { if (Array.isArray(opts.request.body)) { var rangeInfo; if (opts.request.body.length !== 2) throw new Error("Expected Array of length 2 for body byte range"); else if (typeof opts.request.body[1] !== 'string' || !(rangeInfo = /^([\d]+)\-([\d]+)$/.exec(opts.request.body[1])) || parseInt(rangeInfo[1]) >= parseInt(rangeInfo[2])) throw new Error("Invalid body byte range format"); bodyRange = '<' + parseInt(rangeInfo[1]) + '.' + parseInt(rangeInfo[2]) + '>'; opts.request.body = opts.request.body[0]; } if (typeof opts.request.headers === 'boolean' && opts.request.headers === true) { // fetches headers only toFetch = 'HEADER'; } else if (typeof opts.request.body === 'boolean' && opts.request.body === true) { // fetches the whole entire message text (minus the headers), including // all message parts toFetch = 'TEXT'; } else if (typeof opts.request.body === 'string') { if (opts.request.body.toUpperCase() === 'FULL') { // fetches the whole entire message (including the headers) toFetch = ''; } else if (/^([\d]+[\.]{0,1})*[\d]+$/.test(opts.request.body)) { // specific message part identifier, e.g. '1', '2', '1.1', '1.2', etc toFetch = opts.request.body; } else throw new Error("Invalid body partID format"); } } else { // fetch specific headers only toFetch = 'HEADER.FIELDS (' + opts.request.headers.join(' ').toUpperCase() + ')'; } var extensions = ''; if (this.capabilities.indexOf('X-GM-EXT-1') > -1) extensions = 'X-GM-THRID X-GM-MSGID X-GM-LABELS '; this._send(which + 'FETCH ' + uids.join(',') + ' (' + extensions + 'UID FLAGS INTERNALDATE' + (opts.request.struct ? ' BODYSTRUCTURE' : '') + (typeof toFetch === 'string' ? ' BODY' + (!opts.markSeen ? '.PEEK' : '') + '[' + toFetch + ']' + bodyRange : '') + ')', function(e) { var fetcher = self._state.requests[0]._fetcher; if (e && fetcher) fetcher.emit('error', e); else if (e && !fetcher) self.emit('error', e); else if (fetcher) fetcher.emit('end'); } ); var imapFetcher = new ImapFetch(); this._state.requests[this._state.requests.length-1]._fetcher = imapFetcher; return imapFetcher; }; ImapConnection.prototype.addFlags = function(uids, flags, cb) { this._store('UID ', uids, flags, true, cb); }; ImapConnection.prototype.delFlags = function(uids, flags, cb) { this._store('UID ', uids, flags, false, cb); }; ImapConnection.prototype.addKeywords = function(uids, flags, cb) { return this._addKeywords('UID ', uids, flags, cb); }; ImapConnection.prototype._addKeywords = function(which, uids, flags, cb) { if (!this._state.box._newKeywords) throw new Error('This mailbox does not allow new keywords to be added'); this._store(which, uids, flags, true, cb); }; ImapConnection.prototype.delKeywords = function(uids, flags, cb) { this._store('UID ', uids, flags, false, cb); }; ImapConnection.prototype.copy = function(uids, boxTo, cb) { return this._copy('UID ', uids, boxTo, cb); }; ImapConnection.prototype._copy = function(which, uids, boxTo, cb) { if (this._state.status !== STATES.BOXSELECTED) throw new Error('No mailbox is currently selected'); if (!Array.isArray(uids)) uids = [uids]; utils.validateUIDList(uids); this._send(which + 'COPY ' + uids.join(',') + ' "' + utils.escape(boxTo) + '"', cb); }; ImapConnection.prototype.move = function(uids, boxTo, cb) { return this._move('UID ', uids, boxTo, cb); }; ImapConnection.prototype._move = function(which, uids, boxTo, cb) { var self = this; if (this._state.status !== STATES.BOXSELECTED) throw new Error('No mailbox is currently selected'); if (self._state.box.permFlags.indexOf('Deleted') === -1) { throw new Error('Cannot move message: ' + 'server does not allow deletion of messages'); } else { self._copy(which, uids, boxTo, function(err, reentryCount, deletedUIDs, counter) { if (err) { cb(err); return; } var fnMe = arguments.callee; 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) { fnMe.call(this, e, 1, result); }); } else if (reentryCount === 1) { if (counter < deletedUIDs.length) { self.delFlags(deletedUIDs[counter], 'DELETED', function(e) { process.nextTick(function() { fnMe.call(this, e, reentryCount, deletedUIDs, counter+1); }); }); } else fnMe.call(this, err, reentryCount+1, deletedUIDs); } else if (reentryCount === 2) { self.addFlags(uids, 'Deleted', function(e) { fnMe.call(this, e, reentryCount+1, deletedUIDs); }); } else if (reentryCount === 3) { self.removeDeleted(function(e) { fnMe.call(this, e, reentryCount+1, deletedUIDs); }); } else if (reentryCount === 4) { if (counter < deletedUIDs.length) { self.addFlags(deletedUIDs[counter], 'DELETED', function(e) { process.nextTick(function() { fnMe.call(this, e, reentryCount, deletedUIDs, counter+1); }); }); } else cb(); } }); } }; /* Namespace for seqno-based commands */ ImapConnection.prototype.__defineGetter__('seq', function() { var self = this; return { move: function(seqnos, boxTo, cb) { return self._move('', seqnos, boxTo, cb); }, copy: function(seqnos, boxTo, cb) { return self._copy('', seqnos, boxTo, cb); }, delKeywords: function(seqnos, flags, cb) { self._store('', seqnos, flags, false, cb); }, addKeywords: function(seqnos, flags, cb) { return self._addKeywords('', seqnos, flags, cb); }, delFlags: function(seqnos, flags, cb) { self._store('', seqnos, flags, false, cb); }, addFlags: function(seqnos, flags, cb) { self._store('', seqnos, flags, true, cb); }, fetch: function(seqnos, options) { return self._fetch('', seqnos, options); }, search: function(options, cb) { self._search('', options, cb); } }; }); /****** Private Functions ******/ ImapConnection.prototype._fnTmrConn = function(loginCb) { loginCb(new Error('Connection timed out')); this._state.conn.destroy(); } ImapConnection.prototype._store = function(which, uids, flags, isAdding, cb) { var isKeywords = (arguments.callee.caller === this.addKeywords || arguments.callee.caller === this.delKeywords); if (this._state.status !== STATES.BOXSELECTED) throw new Error('No mailbox is currently selected'); if (uids === undefined) throw new Error('The message ID(s) must be specified'); if (!Array.isArray(uids)) uids = [uids]; utils.validateUIDList(uids); if ((!Array.isArray(flags) && typeof flags !== 'string') || (Array.isArray(flags) && flags.length === 0)) throw new Error((isKeywords ? 'Keywords' : 'Flags') + ' argument must be a string or a non-empty Array'); if (!Array.isArray(flags)) flags = [flags]; for (var i=0; i= STATES.AUTH) this._send('NOOP'); }; ImapConnection.prototype._send = function(cmdstr, cb, bypass) { if (cmdstr !== undefined && !bypass) this._state.requests.push({ command: cmdstr, callback: cb, args: [] }); if (this._state.ext.idle.state === IDLE_WAIT) return; if ((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); if (this._state.ext.idle.state === IDLE_READY && cmd !== 'DONE') 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 prefix = 'IDLE '; this._state.ext.idle.state = IDLE_WAIT; } if (cmd !== 'IDLE' && cmd !== 'DONE') prefix = 'A' + ++this._state.curId + ' '; this._state.conn.cleartext.write(prefix); this._state.conn.cleartext.write(cmd); this._state.conn.cleartext.write(CRLF); this.debug('\n<>: ' + prefix + cmd + '\n'); } }; function ImapMessage() {} util.inherits(ImapMessage, EventEmitter); function ImapFetch() {} util.inherits(ImapFetch, EventEmitter);