var util = require('util'), net = require('net'), tls = require('tls'), EventEmitter = require('events').EventEmitter; var emptyFn = function() {}, CRLF = "\r\n", debug=emptyFn, STATES = { NOCONNECT: 0, NOAUTH: 1, AUTH: 2, BOXSELECTING: 3, BOXSELECTED: 4 }, BOX_ATTRIBS = ['NOINFERIORS', 'NOSELECT', 'MARKED', 'UNMARKED']; 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: '', curExpected: 0, curXferred: 0, capabilities: [], box: { _uidnext: 0, _flags: [], _newKeywords: false, validity: 0, keywords: [], 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); if (typeof this._options.debug === 'function') debug = this._options.debug; this.delim = null; this.namespaces = { personal: [], other: [], shared: [] }; }; 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._state.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 = net.createConnection(this._options.port, this._options.host); this._state.tmrConn = setTimeout(this._fnTmrConn, this._options.connTimeout, loginCb); this._state.conn.setKeepAlive(true); if (this._options.secure) { // TODO: support STARTTLS this._state.conn.cleartext = this._state.conn.setSecure(); this._state.conn.on('secure', function() { debug('Secure connection made.'); }); this._state.conn.cleartext.setEncoding('utf8'); } else { this._state.conn.setEncoding('utf8'); this._state.conn.cleartext = this._state.conn; } this._state.conn.on('connect', function() { clearTimeout(self._state.tmrConn); debug('Connected to host.'); self._state.conn.cleartext.write(''); self._state.status = STATES.NOAUTH; }); this._state.conn.on('ready', function() { fnInit(); }); this._state.conn.cleartext.on('data', function(data) { var trailingCRLF = false, literalInfo, bypass = false; debug('<>: ' + util.inspect(data)); if (self._state.curExpected === 0) { if (data.indexOf(CRLF) === -1) { self._state.curData += data; return; } if (self._state.curData.length) { data = self._state.curData + data; self._state.curData = ''; } } // Don't mess with incoming data if it's part of a literal if (self._state.curExpected > 0) { var extra = '', curReq = self._state.requests[0]; if (!curReq._done) { self._state.curXferred += Buffer.byteLength(data, 'utf8'); if (self._state.curXferred <= self._state.curExpected) { if (curReq._msgtype === 'headers') // buffer headers since they're generally not large and are // processed anyway self._state.curData += data; else curReq._msg.emit('data', data); return; } var pos = Buffer.byteLength(data, 'utf8')-(self._state.curXferred-self._state.curExpected); extra = (new Buffer(data)).slice(pos).toString('utf8'); if (pos > 0) { if (curReq._msgtype === 'headers') { self._state.curData += (new Buffer(data)).slice(0, pos).toString('utf8'); curReq._msgheaders = self._state.curData; } else curReq._msg.emit('data', (new Buffer(data)).slice(0, pos).toString('utf8')); } self._state.curData = ''; data = extra; curReq._done = true; } // make sure we have at least ")\r\n" in the post-literal data if (data.indexOf(CRLF) === -1) { self._state.curData += data; return; } if (self._state.curData.length) data = self._state.curData + data; // add any additional k/v pairs that appear after the literal data var fetchdesc = curReq._fetchdesc + ' ' + data.substring(0, data.indexOf(CRLF)-1).trim(); parseFetch(fetchdesc, curReq._msgheaders, curReq._msg); data = data.substr(data.indexOf(CRLF)+2); self._state.curExpected = 0; self._state.curXferred = 0; self._state.curData = ''; curReq._done = false; curReq._msg.emit('end'); if (data[0] === '*') { // found additional responses, so don't try splitting the proceeding // response(s) for better performance in case they have literals too process.nextTick(function() { self._state.conn.cleartext.emit('data', data); }); return; } } else if (self._state.curExpected === 0 && (literalInfo = /\{(\d+)\}$/.exec(data.substr(0, data.indexOf(CRLF))))) { self._state.curExpected = parseInt(literalInfo[1]); var curReq = self._state.requests[0]; //if (/^UID FETCH/.test(curReq.command)) { var type = /BODY\[(.*)\](?:\<[\d]+\>)?/.exec(data.substr(0, data.indexOf(CRLF))), msg = new ImapMessage(); type = type[1]; parseFetch(data.substring(data.indexOf("(")+1, data.indexOf(CRLF)), "", msg); curReq._fetchdesc = data.substring(data.indexOf("(")+1, data.indexOf(CRLF)); curReq._msg = msg; curReq._fetcher.emit('message', msg); curReq._msgtype = (type.indexOf('HEADER') === 0 ? 'headers' : 'body'); self._state.conn.cleartext.emit('data', data.substr(data.indexOf(CRLF)+2)); //} return; } if (data.length === 0) return; data = data.split(CRLF).filter(isNotEmpty); // Defer any extra server responses found in the incoming data if (data.length > 1) { data.slice(1).forEach(function(line) { process.nextTick(function() { self._state.conn.cleartext.emit('data', line + CRLF); }); }); } data = data[0].explode(' ', 3); if (data[0] === '*') { // Untagged server response if (self._state.status === STATES.NOAUTH) { if (data[1] === 'PREAUTH') { // no need to login, the server pre-authenticated us self._state.status = STATES.AUTH; if (self._state.numCapRecvs === 0) self._state.numCapRecvs = 1; } else if (data[1] === 'NO' || data[1] === 'BAD' || data[1] === 'BYE') { self._state.conn.end(); return; } if (!self._state.isReady) { self._state.isReady = true; self._state.conn.emit('ready'); } // Restrict the type of server responses when unauthenticated if (data[1] !== 'CAPABILITY' && data[1] !== 'ALERT') return; } switch (data[1]) { case 'CAPABILITY': if (self._state.numCapRecvs < 2) self._state.numCapRecvs++; self._state.capabilities = data[2].split(' ').map(up); break; case 'FLAGS': if (self._state.status === STATES.BOXSELECTING) self._state.box._flags = data[2].substr(1, data[2].length-2).split(' ').map(function(flag) {return flag.substr(1);});; break; case 'OK': if ((result = /^\[ALERT\] (.*)$/i.exec(data[2])) !== null) self.emit('alert', result[1]); else if (self._state.status === STATES.BOXSELECTING) { var result; if ((result = /^\[UIDVALIDITY (\d+)\]$/i.exec(data[2])) !== null) self._state.box.validity = result[1]; else if ((result = /^\[UIDNEXT (\d+)\]$/i.exec(data[2])) !== null) self._state.box._uidnext = result[1]; else if ((result = /^\[PERMANENTFLAGS \((.*)\)\]$/i.exec(data[2])) !== null) { self._state.box.permFlags = result[1].split(' '); var idx; if ((idx = self._state.box.permFlags.indexOf('\\*')) > -1) { self._state.box._newKeywords = true; self._state.box.permFlags.splice(idx, 1); } self._state.box.keywords = self._state.box.permFlags.filter(function(flag) {return flag[0] !== '\\';}); for (var i=0; i -1;}), delim: (result[2] === 'NIL' ? false : result[2].substring(1, result[2].length-1)), children: null, parent: null }, name = result[3], curChildren = self._state.requests[0].args[0]; if (box.delim) { var path = name.split(box.delim).filter(isNotEmpty), parent = null; name = path.pop(); for (var i=0,len=path.length; i prev) { self._state.box.messages.new = now-prev; self.emit('mail', self._state.box.messages.new); // new mail notification } 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--; break; default: if (/^FETCH/.test(data[2])) { // fetches without header or body (part) retrievals var curReq = self._state.requests[0], msg = new ImapMessage(); parseFetch(data[2].substring(data[2].indexOf("(")+1, data[2].lastIndexOf(")")), "", msg); curReq._fetcher.emit('message', msg); msg.emit('end'); } } } } } else if (data[0].indexOf('A') === 0) { // Tagged server response 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.length === 1 && self._state.requests[0].command !== 'LOGOUT') { 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', self._send, 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', self._send, 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; 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[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); } else if (self._state.requests[0].command.indexOf("UID FETCH") === 0) self._state.requests[0]._fetcher.emit('end'); self._state.requests.shift(); process.nextTick(function() { self._send(); }); self._state.isIdle = true; } else if (data[0] === 'IDLE') { process.nextTick(function() { self._send(); }); self._state.isIdle = false; } else { // unknown response } }); this._state.conn.on('end', function() { self._reset(); debug('FIN packet received. Disconnecting...'); self.emit('end'); }); this._state.conn.on('error', function(err) { clearTimeout(self._state.tmrConn); if (self._state.status === STATES.NOCONNECT) loginCb(new Error('Unable to connect. Reason: ' + err)); self.emit('error', err); debug('Error occurred: ' + err); }); this._state.conn.on('close', function(had_error) { self._reset(); debug('Connection forcefully closed.'); self.emit('close', had_error); }); }; 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 (typeof cb === 'undefined') { if(typeof readOnly === 'undefined') { cb = emptyFn; } else { cb = readOnly; } readOnly = false; } this._state.status = STATES.BOXSELECTING; this._state.box.name = name; this._send((readOnly ? 'EXAMINE' : 'SELECT') + ' "' + escape(name) + '"', cb); }; ImapConnection.prototype.closeBox = function(cb) { // also deletes any messages in this box marked with \Deleted 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('LIST "' + 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 "' + 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 "' + 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 "' + escape(oldname) + '" "' + escape(newname) + '"', cb); }; ImapConnection.prototype.search = function(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('UID SEARCH' + buildSearchQuery(options), cb); }; ImapConnection.prototype.fetch = function(uids, options) { if (this._state.status !== STATES.BOXSELECTED) throw new Error('No mailbox is currently selected'); if (!Array.isArray(uids)) uids = [uids]; try { validateUIDList(uids); } catch(e) { throw e; } var defaults = { markSeen: false, request: { struct: true, headers: true, // \_______ at most one of these can be used for any given fetch request body: false // / } }, toFetch, bodyRange = ''; if (typeof options !== 'object') options = {}; options = extend(true, defaults, options); if (!Array.isArray(options.request.headers)) { if (Array.isArray(options.request.body)) { var rangeInfo; if (options.request.body.length !== 2) throw new Error("Expected Array of length 2 for body property for byte range"); else if (typeof options.request.body[1] !== 'string' || !(rangeInfo = /^([\d]+)\-([\d]+)$/.exec(options.request.body[1])) || parseInt(rangeInfo[1]) >= parseInt(rangeInfo[2])) throw new Error("Invalid body byte range format"); bodyRange = '<' + parseInt(rangeInfo[1]) + '.' + parseInt(rangeInfo[2]) + '>'; options.request.body = options.request.body[0]; } if (typeof options.request.headers === 'boolean' && options.request.headers === true) toFetch = 'HEADER'; // fetches headers only else if (typeof options.request.body === 'boolean' && options.request.body === true) toFetch = 'TEXT'; // fetches the whole entire message text (minus the headers), including all message parts else if (typeof options.request.body === 'string') { if (!/^([\d]+[\.]{0,1})*[\d]+$/.test(options.request.body)) throw new Error("Invalid body partID format"); toFetch = options.request.body; // specific message part identifier, e.g. '1', '2', '1.1', '1.2', etc } } else toFetch = 'HEADER.FIELDS (' + options.request.headers.join(' ').toUpperCase() + ')'; // fetch specific headers only this._send('UID FETCH ' + uids.join(',') + ' (FLAGS INTERNALDATE' + (options.request.struct ? ' BODYSTRUCTURE' : '') + (toFetch ? ' BODY' + (!options.markSeen ? '.PEEK' : '') + '[' + toFetch + ']' + bodyRange : '') + ')'); var imapFetcher = new ImapFetch(); this._state.requests[this._state.requests.length-1]._fetcher = imapFetcher; return imapFetcher; }; ImapConnection.prototype.addFlags = function(uids, flags, cb) { try { this._store(uids, flags, true, cb); } catch (err) { throw err; } }; ImapConnection.prototype.delFlags = function(uids, flags, cb) { try { this._store(uids, flags, false, cb); } catch (err) { throw err; } }; ImapConnection.prototype.addKeywords = function(uids, flags, cb) { if (!self._state.box._newKeywords) throw new Error('This mailbox does not allow new keywords to be added'); try { this._store(uids, flags, true, cb); } catch (err) { throw err; } }; ImapConnection.prototype.delKeywords = function(uids, flags, cb) { try { this._store(uids, flags, false, cb); } catch (err) { throw err; } }; ImapConnection.prototype.copy = function(uids, boxTo, cb) { if (this._state.status !== STATES.BOXSELECTED) throw new Error('No mailbox is currently selected'); if (!Array.isArray(uids)) uids = [uids]; try { validateUIDList(uids); } catch(e) { throw e; } this._send('UID COPY ' + uids.join(',') + ' "' + escape(boxTo) + '"', cb); }; ImapConnection.prototype.move = function(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(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 (typeof 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(); } }); } }; /****** Private Functions ******/ ImapConnection.prototype._fnTmrConn = function(loginCb) { loginCb(new Error('Connection timed out')); this._state.conn.destroy(); } ImapConnection.prototype._store = function(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 (typeof uids === 'undefined') throw new Error('The message ID(s) must be specified'); if (!Array.isArray(uids)) uids = [uids]; try { validateUIDList(uids); } catch(e) { throw e; } 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 (typeof cmdstr !== 'undefined' && !bypass) this._state.requests.push({ command: cmdstr, callback: cb, args: [] }); 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); 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; return; } 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); } }; function ImapMessage() {} util.inherits(ImapMessage, EventEmitter); function ImapFetch() {} util.inherits(ImapFetch, EventEmitter); /****** Utility Functions ******/ function buildSearchQuery(options, isOrChild) { var searchargs = '', months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; for (var i=0,len=options.length; i 1) args = criteria.slice(1); if (criteria.length > 0) criteria = criteria[0].toUpperCase(); } else throw new Error('Unexpected search option data type. Expected string or array. Got: ' + typeof criteria); if (criteria === 'OR') { if (args.length !== 2) throw new Error('OR must have exactly two arguments'); searchargs += ' OR (' + buildSearchQuery(args[0], true) + ') (' + buildSearchQuery(args[1], true) + ')' } else { if (criteria[0] === '!') { modifier += 'NOT '; criteria = criteria.substr(1); } switch(criteria) { case 'ANSWERED': case 'DELETED': case 'DRAFT': case 'FLAGGED': case 'NEW': case 'SEEN': case 'RECENT': case 'OLD': case 'UNANSWERED': case 'UNDELETED': case 'UNDRAFT': case 'UNFLAGGED': case 'UNSEEN': searchargs += modifier + criteria; break; case 'BCC': case 'BODY': case 'CC': case 'FROM': case 'SUBJECT': case 'TEXT': case 'TO': if (!args || args.length !== 1) throw new Error('Incorrect number of arguments for search option: ' + criteria); searchargs += modifier + criteria + ' "' + escape(''+args[0]) + '"'; break; case 'BEFORE': case 'ON': case 'SENTBEFORE': case 'SENTON': case 'SENTSINCE': case 'SINCE': if (!args || args.length !== 1) throw new Error('Incorrect number of arguments for search option: ' + criteria); else if (!(args[0] instanceof Date)) { if ((args[0] = new Date(args[0])).toString() === 'Invalid Date') throw new Error('Search option argument must be a Date object or a parseable date string'); } searchargs += modifier + criteria + ' ' + args[0].getDate() + '-' + months[args[0].getMonth()] + '-' + args[0].getFullYear(); break; case 'KEYWORD': case 'UNKEYWORD': if (!args || args.length !== 1) throw new Error('Incorrect number of arguments for search option: ' + criteria); searchargs += modifier + criteria + ' ' + args[0]; break; case 'LARGER': case 'SMALLER': if (!args || args.length !== 1) throw new Error('Incorrect number of arguments for search option: ' + criteria); var num = parseInt(args[0]); if (isNaN(num)) throw new Error('Search option argument must be a number'); searchargs += modifier + criteria + ' ' + args[0]; break; case 'HEADER': if (!args || args.length !== 2) throw new Error('Incorrect number of arguments for search option: ' + criteria); searchargs += modifier + criteria + ' "' + escape(''+args[0]) + '" "' + escape(''+args[1]) + '"'; break; case 'UID': if (!args) throw new Error('Incorrect number of arguments for search option: ' + criteria); args = args.slice(1); try { validateUIDList(args); } catch(e) { throw e; } searchargs += modifier + criteria + ' ' + args.join(','); break; default: throw new Error('Unexpected search option: ' + criteria); } } if (isOrChild) break; } return searchargs; } function validateUIDList(uids) { for (var i=0,len=uids.length,intval; i 1) uids = ['*']; break; } else if (/^(?:[\d]+|\*):(?:[\d]+|\*)$/.test(uids[i])) continue; } intval = parseInt(''+uids[i]); if (isNaN(intval)) throw new Error('Message ID must be an integer, "*", or a range: ' + uids[i]); else if (typeof uids[i] !== 'number') uids[i] = intval; } } function parseNamespaces(str, namespaces) { var result = parseExpr(str); for (var grp=0; grp<3; ++grp) { if (Array.isArray(result[grp])) { var vals = []; for (var i=0,len=result[grp].length; i 2) { // extension data val.extensions = []; for (var j=2,len2=result[grp][i].length; j next) { if (Array.isArray(cur[next])) { part.params = {}; for (var i=0,len=cur[next].length; i next && Array.isArray(cur[next])) { part.envelope = {}; for (var i=0,field,len=cur[next].length; i= 2 && i <= 7) { var val = cur[next][i]; if (Array.isArray(val)) { var addresses = [], inGroup = false, curGroup; for (var j=0,len2=val.length; j next && Array.isArray(cur[next])) { part.body = parseBodyStructure(cur[next], prefix + (prefix !== '' ? '.' : '') + (partID++).toString(), 1); } else part.body = null; ++next; } if ((part.type === 'text' || (part.type === 'message' && part.subtype === 'rfc822')) && partLen > next) part.lines = cur[next++]; if (partLen > next) part.md5 = cur[next++]; } // add any extra fields that may or may not be omitted entirely parseStructExtra(part, partLen, cur, next); ret.unshift(part); } return ret; } function parseStructExtra(part, partLen, cur, next) { if (partLen > next) { // disposition // null or a special k/v list with these kinds of values: // e.g.: ['Foo', null] // ['Foo', ['Bar', 'Baz']] // ['Foo', ['Bar', 'Baz', 'Bam', 'Pow']] if (Array.isArray(cur[next])) { part.disposition = {}; if (Array.isArray(cur[next][1])) { for (var i=0,len=cur[next][1].length; i next) { // language can be a string or a list of one or more strings, so let's // make this more consistent ... if (cur[next] !== null) part.language = (Array.isArray(cur[next]) ? cur[next] : [cur[next]]); else part.language = null; ++next; } if (partLen > next) part.location = cur[next++]; if (partLen > next) { // extension stuff introduced by later RFCs // this can really be any value: a string, number, or (un)nested list // let's not parse it for now ... part.extensions = cur[next]; } } String.prototype.explode = function(delimiter, limit) { if (arguments.length < 2 || arguments[0] === undefined || arguments[1] === undefined || !delimiter || delimiter === '' || typeof delimiter === 'function' || typeof delimiter === 'object') return false; delimiter = (delimiter === true ? '1' : delimiter.toString()); if (!limit || limit === 0) return this.split(delimiter); else if (limit < 0) return false; else if (limit > 0) { var splitted = this.split(delimiter); var partA = splitted.splice(0, limit - 1); var partB = splitted.join(delimiter); partA.push(partB); return partA; } return false; } function isNotEmpty(str) { return str.trim().length > 0; } function escape(str) { return str.replace('\\', '\\\\').replace('"', '\"'); } function unescape(str) { return str.replace('\"', '"').replace('\\\\', '\\'); } function up(str) { return str.toUpperCase(); } function parseExpr(o, result, start) { start = start || 0; var inQuote = false, lastPos = start - 1, isTop = false; if (!result) result = new Array(); if (typeof o === 'string') { var state = new Object(); state.str = o; o = state; isTop = true; } for (var i=start,len=o.str.length; i 0) result.push(convStr(o.str.substring(lastPos+1, i))); if (o.str[i] === ')' || o.str[i] === ']') return i; lastPos = i; } else if (o.str[i] === '(' || o.str[i] === '[') { var innerResult = []; i = parseExpr(o, innerResult, i+1); lastPos = i; result.push(innerResult); } } else if (o.str[i] === '"' && (o.str[i-1] && (o.str[i-1] !== '\\' || (o.str[i-2] && o.str[i-2] === '\\')))) inQuote = false; if (i+1 === len && len - (lastPos+1) > 0) result.push(convStr(o.str.substring(lastPos+1))); } return (isTop ? result : start); } function convStr(str) { if (str[0] === '"') return str.substring(1, str.length-1); else if (str === 'NIL') return null; else if (/^\d+$/.test(str)) return parseInt(str, 10); else return str; } function getNextIdxParen(str) { var inQuote = false, countParen = 0, lastIndex = -1; for (var i=1,len=str.length; i