diff --git a/imap.js b/lib/imap.js similarity index 56% rename from imap.js rename to lib/imap.js index d7d07cb..2765b2a 100644 --- a/imap.js +++ b/lib/imap.js @@ -1,7 +1,12 @@ -var util = require('util'), net = require('net'), - tls = require('tls'), EventEmitter = require('events').EventEmitter, - Socket = net.Socket; -var emptyFn = function() {}, CRLF = '\r\n', +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, @@ -9,8 +14,6 @@ var emptyFn = function() {}, CRLF = '\r\n', BOXSELECTING: 3, BOXSELECTED: 4 }, BOX_ATTRIBS = ['NOINFERIORS', 'NOSELECT', 'MARKED', 'UNMARKED'], - MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', - 'Oct', 'Nov', 'Dec'], reFetch = /^\* (\d+) FETCH .+? \{(\d+)\}\r\n/; var IDLE_NONE = 1, @@ -64,7 +67,7 @@ function ImapConnection (options) { } } }; - this._options = extend(true, this._options, options); + this._options = utils.extend(true, this._options, options); if (typeof this._options.debug === 'function') this.debug = this._options.debug; @@ -74,6 +77,7 @@ function ImapConnection (options) { this.namespaces = { personal: [], other: [], shared: [] }; this.capabilities = []; }; + util.inherits(ImapConnection, EventEmitter); exports.ImapConnection = ImapConnection; @@ -103,7 +107,9 @@ ImapConnection.prototype.connect = function(loginCb) { }); }); }; + loginCb = loginCb || emptyFn; + this._reset(); this._state.conn = new Socket(); @@ -111,15 +117,12 @@ ImapConnection.prototype.connect = function(loginCb) { if (this._options.secure) { // TODO: support STARTTLS - this._state.conn.cleartext = this._state.conn.setSecure(); + this._state.conn.cleartext = utils.setSecure(this._state.conn); this._state.conn.on('secure', function() { self.debug('Secure connection made.'); }); - //this._state.conn.cleartext.setEncoding('utf8'); - } else { - //this._state.conn.setEncoding('utf8'); + } else this._state.conn.cleartext = this._state.conn; - } this._state.conn.on('connect', function() { clearTimeout(self._state.tmrConn); @@ -127,11 +130,13 @@ ImapConnection.prototype.connect = function(loginCb) { 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) @@ -139,13 +144,17 @@ ImapConnection.prototype.connect = function(loginCb) { 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) { @@ -154,15 +163,15 @@ ImapConnection.prototype.connect = function(loginCb) { self.debug('\n<>: ' + util.inspect(data.toString()) + '\n'); if (self._state.curExpected === 0) { - if (bufferIndexOf(data, CRLF) === -1) { + if (utils.bufferIndexOf(data, CRLF) === -1) { if (self._state.curData) - self._state.curData = bufferAppend(self._state.curData, data); + self._state.curData = utils.bufferAppend(self._state.curData, data); else self._state.curData = data; return; } if (self._state.curData && self._state.curData.length) { - data = bufferAppend(self._state.curData, data); + data = utils.bufferAppend(self._state.curData, data); self._state.curData = null; } } @@ -206,7 +215,7 @@ ImapConnection.prototype.connect = function(loginCb) { } if (self._state.curData) - self._state.curData = bufferAppend(self._state.curData, data); + self._state.curData = utils.bufferAppend(self._state.curData, data); else self._state.curData = data; @@ -217,9 +226,10 @@ ImapConnection.prototype.connect = function(loginCb) { restDesc[1] = ' ' + restDesc[1]; } else restDesc[1] = ''; - parseFetch(curReq._desc + restDesc[1], curReq._headers, curReq._msg); - data = self._state.curData.slice(bufferIndexOf(self._state.curData, CRLF) - + 2); + 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; @@ -236,11 +246,12 @@ ImapConnection.prototype.connect = function(loginCb) { } else if (self._state.curExpected === 0 && (literalInfo = (strdata = data.toString()).match(reFetch))) { self._state.curExpected = parseInt(literalInfo[2], 10); - var idxCRLF = bufferIndexOf(data, CRLF), + 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(bufferIndexOf(data, '(')+1, idxCRLF).trim(); + desc = strdata.substring(utils.bufferIndexOf(data, '(') + 1, idxCRLF) + .trim(); msg.seqno = parseInt(literalInfo[1], 10); type = type[1]; curReq._desc = desc; @@ -258,7 +269,7 @@ ImapConnection.prototype.connect = function(loginCb) { if (data.length === 0) return; var endsInCRLF = (data[data.length-2] === 13 && data[data.length-1] === 10); - data = bufferSplit(data, CRLF); + data = utils.bufferSplit(data, CRLF); // Defer any extra server responses found in the incoming data if (data.length > 1) { @@ -278,7 +289,7 @@ ImapConnection.prototype.connect = function(loginCb) { } } - data = data[0].toString().explode(' ', 3); + data = utils.explode(data[0].toString(), ' ', 3); if (data[0] === '*') { // Untagged server response if (self._state.status === STATES.NOAUTH) { @@ -302,7 +313,10 @@ ImapConnection.prototype.connect = function(loginCb) { case 'CAPABILITY': if (self._state.numCapRecvs < 2) self._state.numCapRecvs++; - self.capabilities = data[2].split(' ').map(up); + self.capabilities = data[2].split(' ') + .map(function(s) { + return s.toUpperCase(); + }); break; case 'FLAGS': if (self._state.status === STATES.BOXSELECTING) { @@ -322,27 +336,26 @@ ImapConnection.prototype.connect = function(loginCb) { else if (result = /^\[UIDNEXT (\d+)\]/i.exec(data[2])) self._state.box._uidnext = result[1]; else if (result = /^\[PERMANENTFLAGS \((.*)\)\]/i.exec(data[2])) { - self._state.box.permFlags = result[1].split(' '); - var idx; + var idx, permFlags, keywords; + self._state.box.permFlags = permFlags = result[1].split(' '); if ((idx = self._state.box.permFlags.indexOf('\\*')) > -1) { self._state.box._newKeywords = true; - self._state.box.permFlags.splice(idx, 1); + permFlags.splice(idx, 1); } - self._state.box.keywords = self._state.box.permFlags - .filter(function(flag) { - return (flag[0] !== '\\'); - }); - for (var i=0; i -1) || - (self._state.isIdle && self._state.ext.idle.state === IDLE_READY); + var isUnsolicited = + (self._state.requests[0] + && self._state.requests[0].command.indexOf('NOOP') > -1 + ) + || + (self._state.isIdle && self._state.ext.idle.state === IDLE_READY); switch (data[2]) { case 'EXISTS': // mailbox total message count @@ -421,9 +439,9 @@ ImapConnection.prototype.connect = function(loginCb) { // fetches without header or body (part) retrievals if (/^FETCH/.test(data[2])) { var msg = new ImapMessage(); - parseFetch(data[2].substring(data[2].indexOf("(")+1, - data[2].lastIndexOf(")")), - "", msg); + 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) { @@ -560,7 +578,8 @@ ImapConnection.prototype.openBox = function(name, readOnly, cb) { this._state.status = STATES.BOXSELECTING; this._state.box.name = name; - this._send((readOnly ? 'EXAMINE' : 'SELECT') + ' "' + escape(name) + '"', cb); + this._send((readOnly ? 'EXAMINE' : 'SELECT') + ' "' + utils.escape(name) + + '"', cb); }; // also deletes any messages in this box marked with \Deleted @@ -589,7 +608,8 @@ 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') + ' "' + escape(namespace) + '" "*"', cb); + this._send(((this.capabilities.indexOf('XLIST') == -1) ? 'LIST' : 'XLIST') + + ' "' + utils.escape(namespace) + '" "*"', cb); }; ImapConnection.prototype.addBox = function(name, cb) { @@ -597,7 +617,7 @@ ImapConnection.prototype.addBox = function(name, cb) { 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); + this._send('CREATE "' + utils.escape(name) + '"', cb); }; ImapConnection.prototype.delBox = function(name, cb) { @@ -605,7 +625,7 @@ ImapConnection.prototype.delBox = function(name, cb) { 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); + this._send('DELETE "' + utils.escape(name) + '"', cb); }; ImapConnection.prototype.renameBox = function(oldname, newname, cb) { @@ -620,19 +640,21 @@ ImapConnection.prototype.renameBox = function(oldname, newname, cb) { && oldname === this._state.box.name && oldname !== 'INBOX') this._state.box._newName = oldname; - this._send('RENAME "' + escape(oldname) + '" "' + escape(newname) + '"', cb); + 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' - + buildSearchQuery(options, this.capabilities), cb); + + utils.buildSearchQuery(options, this.capabilities), cb); }; ImapConnection.prototype.append = function(data, options, cb) { @@ -647,20 +669,24 @@ ImapConnection.prototype.append = function(data, options, cb) { else options.mailbox = this._state.box.name } - cmd = 'APPEND "'+escape(options.mailbox)+'"'; + cmd = 'APPEND "' + utils.escape(options.mailbox) + '"'; if ('flags' in options) { if (!Array.isArray(options.flags)) options.flags = Array(options.flags); - cmd += " (\\"+options.flags.join(' \\')+")"; + 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()+'-'+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.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 += ('0' + (-options.date.getTimezoneOffset() / 60)).slice(-2); + cmd += ('0' + (-options.date.getTimezoneOffset() % 60)).slice(-2); cmd += '"'; } cmd += ' {'; @@ -674,11 +700,12 @@ ImapConnection.prototype.append = function(data, options, cb) { 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'); @@ -689,7 +716,7 @@ ImapConnection.prototype._fetch = function(which, uids, options) { if (!Array.isArray(uids)) uids = [uids]; - validateUIDList(uids); + utils.validateUIDList(uids); var opts = { markSeen: false, @@ -702,7 +729,7 @@ ImapConnection.prototype._fetch = function(which, uids, options) { }, toFetch, bodyRange = '', self = this; if (typeof options !== 'object') options = {}; - extend(true, opts, options); + utils.extend(true, opts, options); if (!Array.isArray(opts.request.headers)) { if (Array.isArray(opts.request.body)) { @@ -777,6 +804,7 @@ ImapConnection.prototype.delFlags = function(uids, flags, 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'); @@ -790,6 +818,7 @@ ImapConnection.prototype.delKeywords = function(uids, flags, 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'); @@ -797,14 +826,16 @@ ImapConnection.prototype._copy = function(which, uids, boxTo, cb) { if (!Array.isArray(uids)) uids = [uids]; - validateUIDList(uids); + utils.validateUIDList(uids); - this._send(which + 'COPY ' + uids.join(',') + ' "' + escape(boxTo) + '"', cb); + 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) @@ -908,7 +939,7 @@ ImapConnection.prototype._store = function(which, uids, flags, isAdding, cb) { if (!Array.isArray(uids)) uids = [uids]; - validateUIDList(uids); + utils.validateUIDList(uids); if ((!Array.isArray(flags) && typeof flags !== 'string') || (Array.isArray(flags) && flags.length === 0)) @@ -959,18 +990,21 @@ ImapConnection.prototype._login = function(cb) { if (this.capabilities.indexOf('LOGINDISABLED') !== -1) return cb(new Error('Logging in is disabled on this server')); - if (this.capabilities.indexOf('AUTH=XOAUTH') !== -1 && 'xoauth' in this._options) - this._send('AUTHENTICATE XOAUTH ' + escape(this._options.xoauth), fnReturn); - else if (this._options.username !== undefined && - this._options.password !== undefined) { - this._send('LOGIN "' + escape(this._options.username) + '" "' - + escape(this._options.password) + '"', fnReturn); + if (this.capabilities.indexOf('AUTH=XOAUTH') !== -1 + && 'xoauth' in this._options) { + this._send('AUTHENTICATE XOAUTH ' + utils.escape(this._options.xoauth), + fnReturn); + } else if (this._options.username !== undefined + && this._options.password !== undefined) { + this._send('LOGIN "' + utils.escape(this._options.username) + '" "' + + utils.escape(this._options.password) + '"', fnReturn); } else { return cb(new Error('No supported authentication method(s) available. ' + 'Unable to login.')); } } }; + ImapConnection.prototype._reset = function() { clearTimeout(this._state.tmrKeepalive); clearTimeout(this._state.tmrConn); @@ -987,6 +1021,7 @@ ImapConnection.prototype._reset = function() { this.capabilities = []; this._resetBox(); }; + ImapConnection.prototype._resetBox = function() { this._state.box._uidnext = 0; this._state.box.validity = 0; @@ -998,10 +1033,12 @@ ImapConnection.prototype._resetBox = function() { this._state.box.messages.total = 0; this._state.box.messages.new = 0; }; + ImapConnection.prototype._noop = function() { if (this._state.status >= 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: [] }); @@ -1030,684 +1067,6 @@ ImapConnection.prototype._send = function(cmdstr, cb, bypass) { function ImapMessage() {} util.inherits(ImapMessage, EventEmitter); + function ImapFetch() {} util.inherits(ImapFetch, EventEmitter); - -/****** Utility Functions ******/ - -function buildSearchQuery(options, extensions, isOrChild) { - var searchargs = ''; - 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], extensions, true) + ') (' - + buildSearchQuery(args[1], extensions, true) + ')' - } else { - if (criteria[0] === '!') { - modifier += 'NOT '; - criteria = criteria.substr(1); - } - switch(criteria) { - // -- Standard criteria -- - case 'ALL': - 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); - validateUIDList(args); - searchargs += modifier + criteria + ' ' + args.join(','); - break; - // -- Extensions criteria -- - case 'X-GM-MSGID': // Gmail unique message ID - case 'X-GM-THRID': // Gmail thread ID - if (extensions.indexOf('X-GM-EXT-1') === -1) - throw new Error('IMAP extension not available: ' + criteria); - var val; - if (!args || args.length !== 1) - throw new Error('Incorrect number of arguments for search option: ' - + criteria); - else { - val = ''+args[0]; - if (!(/^\d+$/.test(args[0]))) - throw new Error('Invalid value'); - } - searchargs += modifier + criteria + ' ' + val; - break; - case 'X-GM-RAW': // Gmail search syntax - if (extensions.indexOf('X-GM-EXT-1') === -1) - throw new Error('IMAP extension not available: ' + criteria); - if (!args || args.length !== 1) - throw new Error('Incorrect number of arguments for search option: ' - + criteria); - searchargs += modifier + criteria + ' "' + escape(''+args[0]) + '"'; - break; - case 'X-GM-LABELS': // Gmail labels - if (extensions.indexOf('X-GM-EXT-1') === -1) - throw new Error('IMAP extension not available: ' + criteria); - if (!args || args.length !== 1) - throw new Error('Incorrect number of arguments for search option: ' - + criteria); - searchargs += modifier + criteria + ' ' + args[0]; - 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/number 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)) { - // some IMAP extensions utilize large (64-bit) integers, which JavaScript - // can't handle natively, so we'll just keep it as a string if it's too big - var val = parseInt(str, 10); - return (val.toString() === str ? val : str); - } 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 (var key in obj) - last_key = key; - - return 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 (copy !== undefined) - target[name] = copy; - } - } - } - - // Return the modified object - return target; -}; - -function bufferAppend(buf1, buf2) { - var newBuf = new Buffer(buf1.length + buf2.length); - buf1.copy(newBuf, 0, 0); - if (Buffer.isBuffer(buf2)) - buf2.copy(newBuf, buf1.length, 0); - else if (Array.isArray(buf2)) { - for (var i=buf1.length, len=buf2.length; i buf.length) - return [buf]; - var search = !Array.isArray(str) - ? str.split('').map(function(el) { return el.charCodeAt(0); }) - : str, - searchLen = search.length, - ret = [], pos, start = 0; - - while ((pos = bufferIndexOf(buf, search, start)) > -1) { - ret.push(buf.slice(start, pos)); - start = pos + searchLen; - } - if (!ret.length) - ret = [buf]; - else if (start < buf.length) - ret.push(buf.slice(start)); - - return ret; -}; - -function bufferIndexOf(buf, str, start) { - if (str.length > buf.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=buf.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.emit('error', e); - } - - function onclose() { - socket.removeListener('error', onerror); - socket.removeListener('close', onclose); - } - - socket.on('error', onerror); - socket.on('close', onclose); - - return cleartext; -} diff --git a/lib/imap.parsers.js b/lib/imap.parsers.js new file mode 100644 index 0000000..b4c3997 --- /dev/null +++ b/lib/imap.parsers.js @@ -0,0 +1,283 @@ +var utils = require('./imap.utilities'); + +exports.convStr = function(str) { + if (str[0] === '"') + return str.substring(1, str.length-1); + else if (str === 'NIL') + return null; + else if (/^\d+$/.test(str)) { + // some IMAP extensions utilize large (64-bit) integers, which JavaScript + // can't handle natively, so we'll just keep it as a string if it's too big + var val = parseInt(str, 10); + return (val.toString() === str ? val : str); + } else + return str; +} + +exports.parseNamespaces = function(str, namespaces) { + var result = exports.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 = exports.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 + exports.parseStructExtra(part, partLen, cur, next); + ret.unshift(part); + } + return ret; +} + +exports.parseStructExtra = function(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]; + } +} + +exports.parseExpr = function(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(exports.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 = exports.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(exports.convStr(o.str.substring(lastPos+1))); + } + return (isTop ? result : start); +} diff --git a/lib/imap.utilities.js b/lib/imap.utilities.js new file mode 100644 index 0000000..aae21f0 --- /dev/null +++ b/lib/imap.utilities.js @@ -0,0 +1,391 @@ +var tls = require('tls'); + +exports.MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', + 'Oct', 'Nov', 'Dec']; + +exports.setSecure = function(tcpSocket) { + var pair = tls.createSecurePair(), + cleartext; + + pair.encrypted.pipe(tcpSocket); + tcpSocket.pipe(pair.encrypted); + pair.fd = tcpSocket.fd; + + cleartext = pair.cleartext; + cleartext.socket = tcpSocket; + cleartext.encrypted = pair.encrypted; + + function onerror(e) { + if (cleartext._controlReleased) + cleartext.emit('error', e); + } + + function onclose() { + tcpSocket.removeListener('error', onerror); + tcpSocket.removeListener('close', onclose); + } + + tcpSocket.on('error', onerror); + tcpSocket.on('close', onclose); + + pair.on('secure', function() { + process.nextTick(function() { cleartext.socket.emit('secure'); }); + }); + + cleartext._controlReleased = true; + return cleartext; +}; + +/** + * 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 + */ +exports.extend = function() { + // 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 (var key in obj) + last_key = key; + + return 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 (copy !== undefined) + target[name] = copy; + } + } + } + + // Return the modified object + return target; +}; + +exports.bufferAppend = function(buf1, buf2) { + var newBuf = new Buffer(buf1.length + buf2.length); + buf1.copy(newBuf, 0, 0); + if (Buffer.isBuffer(buf2)) + buf2.copy(newBuf, buf1.length, 0); + else if (Array.isArray(buf2)) { + for (var i=buf1.length, len=buf2.length; i buf.length) + return [buf]; + var search = !Array.isArray(str) + ? str.split('').map(function(el) { return el.charCodeAt(0); }) + : str, + searchLen = search.length, + ret = [], pos, start = 0; + + while ((pos = exports.bufferIndexOf(buf, search, start)) > -1) { + ret.push(buf.slice(start, pos)); + start = pos + searchLen; + } + if (!ret.length) + ret = [buf]; + else if (start < buf.length) + ret.push(buf.slice(start)); + + return ret; +}; + +exports.bufferIndexOf = function(buf, str, start) { + if (str.length > buf.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=buf.length; i= searchLen) { + if (searchLen > 1) { + for (j=1; j -1) + break; + } + } + return ret; +}; + +exports.explode = function(str, 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 str.split(delimiter); + else if (limit < 0) + return false; + else if (limit > 0) { + var splitted = str.split(delimiter); + var partA = splitted.splice(0, limit - 1); + var partB = splitted.join(delimiter); + partA.push(partB); + return partA; + } + + return false; +} + +exports.isNotEmpty = function(str) { + return str.trim().length > 0; +} + +exports.escape = function(str) { + return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); +} + +exports.unescape = function(str) { + return str.replace(/\\"/g, '"').replace(/\\\\/g, '\\'); +} + +exports.buildSearchQuery = function(options, extensions, isOrChild) { + var searchargs = ''; + 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], extensions, true) + ') (' + + buildSearchQuery(args[1], extensions, true) + ')' + } else { + if (criteria[0] === '!') { + modifier += 'NOT '; + criteria = criteria.substr(1); + } + switch(criteria) { + // -- Standard criteria -- + case 'ALL': + 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() + '-' + + exports.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); + validateUIDList(args); + searchargs += modifier + criteria + ' ' + args.join(','); + break; + // -- Extensions criteria -- + case 'X-GM-MSGID': // Gmail unique message ID + case 'X-GM-THRID': // Gmail thread ID + if (extensions.indexOf('X-GM-EXT-1') === -1) + throw new Error('IMAP extension not available: ' + criteria); + var val; + if (!args || args.length !== 1) + throw new Error('Incorrect number of arguments for search option: ' + + criteria); + else { + val = ''+args[0]; + if (!(/^\d+$/.test(args[0]))) + throw new Error('Invalid value'); + } + searchargs += modifier + criteria + ' ' + val; + break; + case 'X-GM-RAW': // Gmail search syntax + if (extensions.indexOf('X-GM-EXT-1') === -1) + throw new Error('IMAP extension not available: ' + criteria); + if (!args || args.length !== 1) + throw new Error('Incorrect number of arguments for search option: ' + + criteria); + searchargs += modifier + criteria + ' "' + escape(''+args[0]) + '"'; + break; + case 'X-GM-LABELS': // Gmail labels + if (extensions.indexOf('X-GM-EXT-1') === -1) + throw new Error('IMAP extension not available: ' + criteria); + if (!args || args.length !== 1) + throw new Error('Incorrect number of arguments for search option: ' + + criteria); + searchargs += modifier + criteria + ' ' + args[0]; + break; + default: + throw new Error('Unexpected search option: ' + criteria); + } + } + if (isOrChild) + break; + } + return searchargs; +} + +exports.validateUIDList = function(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/number must be an integer, "*", or a range: ' + + uids[i]); + } else if (typeof uids[i] !== 'number') + uids[i] = intval; + } +} \ No newline at end of file diff --git a/package.json b/package.json index 061b14a..169a28f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "version": "0.3.2", "author": "Brian White ", "description": "An IMAP module for node.js that makes communicating with IMAP servers easy", - "main": "./imap", + "main": "./lib/imap", "engines": { "node" : ">=0.4.0" }, "keywords": [ "imap", "mail", "email", "reader", "client" ], "licenses": [ { "type": "MIT", "url": "http://github.com/mscdex/node-imap/raw/master/LICENSE" } ],