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'], reFetch = /^\* \d+ FETCH .+? \{(\d+)\}\r\n/; 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, 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.bind(this), 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) { if (data.length === 0) return; var trailingCRLF = false, literalInfo; debug('\n<>: ' + util.inspect(data.toString()) + '\n'); if (self._state.curExpected === 0) { if (data.indexOf(CRLF) === -1) { if (self._state.curData) self._state.curData = self._state.curData.add(data); else self._state.curData = data; return; } if (self._state.curData && self._state.curData.length) { data = self._state.curData.add(data); self._state.curData = null; } } // Don't mess with incoming data if it's part of a literal 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') { self._state.curData.append(chunk, curReq.curPos); 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 = self._state.curData.add(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] = ''; parseFetch(curReq._desc + restDesc[1], curReq._headers, curReq._msg); data = self._state.curData.slice(self._state.curData.indexOf(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 = data.toString().match(reFetch))) { self._state.curExpected = parseInt(literalInfo[1], 10); var idxCRLF = data.indexOf(CRLF), curReq = self._state.requests[0], type = /BODY\[(.*)\](?:\<\d+\>)?/ .exec(data.toString().substring(0, idxCRLF)), msg = new ImapMessage(), desc = data.toString().substring(data.indexOf('(')+1, idxCRLF).trim(); 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 = data.split(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; 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 } 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: // fetches without header or body (part) retrievals if (/^FETCH/.test(data[2])) { 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[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); } 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._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); } else process.nextTick(function() { self._send(); }); self._state.isIdle = true; } else if (data[0] === 'IDLE') { if (self._state.requests.length > 0) 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); }; // 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('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 (typeof uids === undefined || typeof uids === null || (Array.isArray(uids) && uids.length === 0)) throw new Error('Nothing to fetch'); if (!Array.isArray(uids)) uids = [uids]; try { validateUIDList(uids); } catch(e) { throw e; } 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 = {}; 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() + ')'; } this._send('UID FETCH ' + uids.join(',') + ' (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) { 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('\n<>: ' + prefix + cmd + '\n'); } }; 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); 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 (typeof cur[1] === 'string' && 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(/\\/g, '\\\\').replace(/"/g, '\\"'); } function unescape(str) { return str.replace(/\\"/g, '"').replace(/\\\\/g, '\\'); } 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; } /** * Adopted from jquery's extend method. Under the terms of MIT License. * * http://code.jquery.com/jquery-1.4.2.js * * Modified by Brian White to use Array.isArray instead of the custom isArray * method */ function extend() { // copy reference to target object var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options, name, src, copy; // Handle a deep copy situation if (typeof target === "boolean") { deep = target; target = arguments[1] || {}; // skip the boolean and the target i = 2; } // Handle case when target is a string or something (possible in deep copy) if (typeof target !== "object" && !typeof target === 'function') target = {}; var isPlainObject = function(obj) { // Must be an Object. // Because of IE, we also have to check the presence of the constructor // property. // Make sure that DOM nodes and window objects don't pass through, as well if (!obj || toString.call(obj) !== "[object Object]" || obj.nodeType || obj.setInterval) return false; var has_own_constructor = hasOwnProperty.call(obj, "constructor"); var has_is_prop_of_method = hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf"); // Not own constructor property must be Object if (obj.constructor && !has_own_constructor && !has_is_prop_of_method) return false; // Own properties are enumerated firstly, so to speed up, // if last one is own, then all properties are own. var last_key; for (key in obj) last_key = key; return typeof last_key === "undefined" || hasOwnProperty.call(obj, last_key); }; for (; i < length; i++) { // Only deal with non-null/undefined values if ((options = arguments[i]) !== null) { // Extend the base object for (name in options) { src = target[name]; copy = options[name]; // Prevent never-ending loop if (target === copy) continue; // Recurse if we're merging object literal values or arrays if (deep && copy && (isPlainObject(copy) || Array.isArray(copy))) { var clone = src && (isPlainObject(src) || Array.isArray(src) ? src : (Array.isArray(copy) ? [] : {})); // Never move original objects, clone them target[name] = extend(deep, clone, copy); // Don't bring in undefined values } else if (typeof copy !== "undefined") target[name] = copy; } } } // Return the modified object return target; }; Buffer.prototype.append = function(buf, start) { buf.copy(this, start, 0); }; Buffer.prototype.add = function(buf) { var newBuf = new Buffer(this.length + buf.length); this.copy(newBuf, 0, 0); buf.copy(newBuf, this.length, 0); return newBuf; }; Buffer.prototype.split = function(str) { if ((typeof str !== 'string' && !Array.isArray(str)) || str.length === 0 || str.length > this.length) return [this]; var search = !Array.isArray(str) ? str.split('').map(function(el) { return el.charCodeAt(0); }) : str, searchLen = search.length, ret = [], pos, start = 0; while ((pos = this.indexOf(search, start)) > -1) { ret.push(this.slice(start, pos)); start = pos + searchLen; } if (!ret.length) ret = [this]; else if (start < this.length) ret.push(this.slice(start)); return ret; }; Buffer.prototype.indexOf = function(str, start) { if (str.length > this.length) return -1; var search = !Array.isArray(str) ? str.split('').map(function(el) { return el.charCodeAt(0); }) : str, searchLen = search.length, ret = -1, i, j, len; for (i=start||0,len=this.length; i= searchLen) { if (searchLen > 1) { for (j=1; j -1) break; } } return ret; }; net.Stream.prototype.setSecure = function() { var pair = tls.createSecurePair(); var cleartext = pipe(pair, this); pair.on('secure', function() { process.nextTick(function() { cleartext.socket.emit('secure'); }); }); cleartext._controlReleased = true; return cleartext; }; function pipe(pair, socket) { pair.encrypted.pipe(socket); socket.pipe(pair.encrypted); pair.fd = socket.fd; var cleartext = pair.cleartext; cleartext.socket = socket; cleartext.encrypted = pair.encrypted; function onerror(e) { if (cleartext._controlReleased) cleartext.socket.emit('error', e); } function onclose() { socket.removeListener('error', onerror); socket.removeListener('close', onclose); } socket.on('error', onerror); socket.on('close', onclose); return cleartext; }