diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..bb5c8c1c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/node_modules +.idea \ No newline at end of file diff --git a/lib/Connection.js b/lib/Connection.js index 57b83384..24750f4a 100644 --- a/lib/Connection.js +++ b/lib/Connection.js @@ -64,6 +64,7 @@ function Connection(config) { tlsOptions: config.tlsOptions, autotls: config.autotls, user: config.user, + email: config.email, password: config.password, xoauth: config.xoauth, xoauth2: config.xoauth2, @@ -102,12 +103,13 @@ Connection.prototype.connect = function() { socket.setKeepAlive(true); this._sock = undefined; this._tagcount = 0; + //TODO: why? no clearTimeout. no checks. do not re-connect, initialize new instance. this._tmrConn = undefined; this._tmrKeepalive = undefined; this._tmrAuth = undefined; this._queue = []; this._box = undefined; - this._idle = { started: undefined, enabled: false }; + this._idle = {started: undefined, enabled: false}; this._parser = undefined; this._curReq = undefined; this.delimiter = undefined; @@ -123,9 +125,9 @@ Connection.prototype.connect = function() { tlsOptions.socket = socket; } - if (config.tls) + if (config.tls){ this._sock = tls.connect(tlsOptions, onconnect); - else { + }else { socket.once('connect', onconnect); this._sock = socket; } @@ -134,11 +136,14 @@ Connection.prototype.connect = function() { clearTimeout(self._tmrConn); self.state = 'connected'; self.debug && self.debug('[connection] Connected to host'); + //WORKERDEATH#2 self._tmrAuth = setTimeout(function() { - var err = new Error('Timed out while authenticating with server'); - err.source = 'timeout-auth'; - self.emit('error', err); - socket.destroy(); + if(self && self.listeners && self.listeners('error') && self.listeners('error').length > 0){ + var err = new Error('Timed out while authenticating with server for ' + config.email); + err.source = 'timeout-auth'; + self.emit('error', err); + } + socket && socket.destroy(); }, config.authTimeout); } @@ -150,6 +155,10 @@ Connection.prototype.connect = function() { self.emit('error', err); }; this._sock.on('error', this._onError); + //SIEGE 31 + if(config.tls) + socket.on('error', this._onError); + this._onSocketTimeout = function() { clearTimeout(self._tmrConn); @@ -161,38 +170,53 @@ Connection.prototype.connect = function() { var err = new Error('Socket timed out while talking to server'); err.source = 'socket-timeout'; self.emit('error', err); + self.emit('close'); socket.destroy(); }; - this._sock.on('timeout', this._onSocketTimeout); + + socket.on('timeout', this._onSocketTimeout); socket.setTimeout(config.socketTimeout); + if(config.tls){ + this._sock.on('timeout', this._onSocketTimeout); + this._sock.setTimeout(config.socketTimeout); + } + - socket.once('close', function(had_err) { + this._onSocketClose = function (had_err) { clearTimeout(self._tmrConn); clearTimeout(self._tmrAuth); clearTimeout(self._tmrKeepalive); self.state = 'disconnected'; self.debug && self.debug('[connection] Closed'); self.emit('close', had_err); - }); + }; + socket.once('close', this._onSocketClose); - socket.once('end', function() { - clearTimeout(self._tmrConn); - clearTimeout(self._tmrAuth); - clearTimeout(self._tmrKeepalive); - self.state = 'disconnected'; - self.debug && self.debug('[connection] Ended'); - self.emit('end'); - }); + //SIEGE 31.late + if(config.tls) + this._sock.once('close', this._onSocketClose); + + + this._onSocketEnd = function() { + clearTimeout(self._tmrConn); + clearTimeout(self._tmrAuth); + clearTimeout(self._tmrKeepalive); + self.state = 'disconnected'; + self.debug && self.debug('[connection] Ended'); + self.emit('end'); + }; + socket.once('end', this._onSocketEnd); + + //SIEGE 31.very.very.late + if(config.tls) + this._sock.once('end', this._onSocketEnd); this._parser = parser = new Parser(this._sock, this.debug); - parser.on('untagged', function(info) { - self._resUntagged(info); - }); - parser.on('tagged', function(info) { - self._resTagged(info); - }); + parser.on('untagged', self._resUntagged.bind(self)); + parser.on('tagged', self._resTagged.bind(self)); parser.on('body', function(stream, info) { + self.emit('alive'); var msg = self._curReq.fetchCache[info.seqno], toget; if (msg === undefined) { @@ -223,6 +247,9 @@ Connection.prototype.connect = function() { stream.resume(); // a body we didn't ask for? }); parser.on('continue', function(info) { + //SIEGE 31 + if(self._fuckUp('parseContinue')) return; + self.emit('alive'); var type = self._curReq.type; if (type === 'IDLE') { if (self._queue.length @@ -253,6 +280,7 @@ Connection.prototype.connect = function() { parser.on('other', function(line) { var m; if (m = RE_IDLENOOPRES.exec(line)) { + self.emit('alive'); // no longer idling self._idle.enabled = false; self._idle.started = undefined; @@ -274,12 +302,14 @@ Connection.prototype.connect = function() { self._processQueue(); } }); - + //WORKERDEATH#1 this._tmrConn = setTimeout(function() { - var err = new Error('Timed out while connecting to server'); - err.source = 'timeout'; - self.emit('error', err); - socket.destroy(); + // if(self && self.listeners && self.listeners('error') && self.listeners('error').length > 0){ + var err = new Error('Timed out while connecting to server'); + err.source = 'timeout'; + self.emit('error', err); + // } + socket && socket.destroy(); }, config.connTimeout); socket.connect(config.port, config.host); @@ -295,12 +325,14 @@ Connection.prototype.destroy = function() { this._sock && this._sock.destroy(); }; -Connection.prototype.end = function() { +Connection.prototype.end = function(cb) { var self = this; this._enqueue('LOGOUT', function() { self._queue = []; self._curReq = undefined; self._sock.end(); + //SIEGE 31 + cb && cb(); }); }; @@ -419,6 +451,10 @@ Connection.prototype.openBox = function(name, readOnly, cb) { self._box = undefined; cb(err); } else { + if(!self._box){ + self._createCurrentBox(); + self.notifyEdited('openBox'); + } self._box.name = name; cb(err, self._box); } @@ -514,8 +550,12 @@ Connection.prototype.getSubscribedBoxes = function(namespace, cb) { }; Connection.prototype.status = function(boxName, cb) { - if (this._box && this._box.name === boxName) - throw new Error('Cannot call status on currently selected mailbox'); + if(typeof(boxName) == 'function'){ + cb = boxName; + boxName = null; + } + + boxName = boxName || (this._box && this._box.name) || ''; boxName = escape(utf7.encode(''+boxName)); @@ -791,17 +831,23 @@ Connection.prototype._fetch = function(which, uids, options) { fetching = [], i, len, key; - if (this.serverSupports('X-GM-EXT-1')) { + if (this.serverSupports('X-GM-EXT-1') && options.gmail !== false) { fetching.push('X-GM-THRID'); fetching.push('X-GM-MSGID'); fetching.push('X-GM-LABELS'); } - if (this.serverSupports('CONDSTORE') && !this._box.nomodseq) + if (this.serverSupports('CONDSTORE') && !this._box.nomodseq && options.modseq !== false) fetching.push('MODSEQ'); fetching.push('UID'); - fetching.push('FLAGS'); - fetching.push('INTERNALDATE'); + + if (options.flags !== false) { + fetching.push('FLAGS'); + } + + if (options.date !== false) { + fetching.push('INTERNALDATE'); + } var modifiers; @@ -1215,6 +1261,7 @@ Connection.prototype.__defineGetter__('seq', function() { Connection.prototype._resUntagged = function(info) { var type = info.type, i, len, box, attrs, key; + if(this._curReq) this.emit('alive'); if (type === 'bye') this._sock.end(); @@ -1241,7 +1288,7 @@ Connection.prototype._resUntagged = function(info) { cbargs.push([]); cbargs[0].push(info.text); } else if (type === 'recent') { - if (!this._box && RE_OPENBOX.test(this._curReq.type)) + if (!this._box && this._curReq && RE_OPENBOX.test(this._curReq.type)) this._createCurrentBox(); if (this._box) this._box.messages.new = info.num; @@ -1260,7 +1307,7 @@ Connection.prototype._resUntagged = function(info) { this._sock.end(); } } else if (type === 'exists') { - if (!this._box && RE_OPENBOX.test(this._curReq.type)) + if (!this._box && this._curReq && RE_OPENBOX.test(this._curReq.type)) this._createCurrentBox(); if (this._box) { var prev = this._box.messages.total, @@ -1268,7 +1315,10 @@ Connection.prototype._resUntagged = function(info) { this._box.messages.total = now; if (now > prev && this.state === 'authenticated') { this._box.messages.new = now - prev; - this.emit('mail', this._box.messages.new); + for(var x = now, seqs = []; x > prev; seqs.push(x--)); + this.emit('mail', seqs); + }else{ + this.emit('exists', now); } } } else if (type === 'expunge') { @@ -1389,6 +1439,7 @@ Connection.prototype._resUntagged = function(info) { } this._curReq.cbargs.push(box); } else if (type === 'fetch') { + if(this._fuckUp('_resUntagged')) return; if (/^(?:UID )?FETCH/.test(this._curReq.fullcmd)) { // FETCH response sent as result of FETCH request var msg = this._curReq.fetchCache[info.num], @@ -1469,7 +1520,20 @@ Connection.prototype._resUntagged = function(info) { } }; +Connection.prototype._fuckUp = function(place){ + if(!this._curReq){ + var err = new Error('no _curReq in connection '+ place); + err.source = 'imap-curreq'; + this.emit('error', err); + this._sock.end(); + return true; + } + return false; +} + Connection.prototype._resTagged = function(info) { + if(this._fuckUp('_resTagged')) return; + this.emit('alive'); var req = this._curReq, err; this._curReq = undefined; @@ -1502,6 +1566,20 @@ Connection.prototype._resTagged = function(info) { if (err) bodyEmitter.emit('error', err); process.nextTick(function() { + + //destel: emit 'attributes' and 'end' events for messages which are not yet ended + for (var sn in req.fetchCache) { + if (req.fetchCache.hasOwnProperty(sn)) { + var item = req.fetchCache[sn] + if (!item.ended) { + item.ended = true + item.msgEmitter.emit('attributes', item.attrs); + item.msgEmitter.emit('end'); + } + } + } + //end + bodyEmitter.emit('end'); }); } else { @@ -1582,7 +1660,7 @@ Connection.prototype._doKeepaliveTimer = function(immediate) { this._tmrKeepalive = setTimeout(timerfn, interval); }; -Connection.prototype._login = function() { +Connection.prototype._login = function(without_starttls) { var self = this, checkedNS = false; var reentry = function(err) { @@ -1623,19 +1701,20 @@ Connection.prototype._login = function() { } else reentry(); }; - - if (self.serverSupports('STARTTLS') - && (self._config.autotls === 'always' - || (self._config.autotls === 'required' - && self.serverSupports('LOGINDISABLED')))) { + if(!without_starttls){ + if (self.serverSupports('STARTTLS') + && (self._config.autotls === 'always' + || (self._config.autotls === 'required' + && self.serverSupports('LOGINDISABLED')))) { self._starttls(); return; - } + } - if (self.serverSupports('LOGINDISABLED')) { - err = new Error('Logging in is disabled on this server'); - err.source = 'authentication'; - return reentry(err); + if (self.serverSupports('LOGINDISABLED')) { + err = new Error('Logging in is disabled on this server'); + err.source = 'authentication'; + return reentry(err); + } } var cmd; @@ -1687,12 +1766,15 @@ Connection.prototype._starttls = function() { tlsOptions.socket = self._sock; self._sock = tls.connect(tlsOptions, function() { - self._login(); + self._login(true); }); self._sock.on('error', self._onError); - self._sock.on('timeout', this._onSocketTimeout); - self._sock.setTimeout(config.socketTimeout); + self._sock.on('timeout', self._onSocketTimeout); + self._sock.once('close', self._onSocketClose); + self._sock.once('end', self._onSocketEnd); + + self._sock.setTimeout(this._config.socketTimeout); self._parser.setStream(self._sock); }); @@ -1713,6 +1795,8 @@ Connection.prototype._processQueue = function() { prefix = this._curReq.type; else prefix = 'A' + (this._tagcount++); + //163.com fix + //prefix = 'C' + (this._tagcount++); var out = prefix + ' ' + this._curReq.fullcmd; this.debug && this.debug('=> ' + inspect(out)); @@ -1782,6 +1866,7 @@ module.exports = Connection; // utilities ------------------------------------------------------------------- function escape(str) { + //SIEGE 31 - REALY CANCELED - bad place to protect, take a look deeper return str.replace(RE_BACKSLASH, '\\\\').replace(RE_DBLQUOTE, '\\"'); } diff --git a/lib/Parser.js b/lib/Parser.js index d6d673e8..1cc201bf 100644 --- a/lib/Parser.js +++ b/lib/Parser.js @@ -3,6 +3,7 @@ var EventEmitter = require('events').EventEmitter, || require('readable-stream').Readable, inherits = require('util').inherits, inspect = require('util').inspect; +//var domain = require('domain'); var utf7 = require('utf7').imap, jsencoding; // lazy-loaded @@ -12,13 +13,17 @@ var CH_LF = 10, EMPTY_READCB = function(n) {}, RE_INTEGER = /^\d+$/, RE_PRECEDING = /^(?:(?:\*|A\d+) )|\+ ?/, + //163.com fix + //RE_PRECEDING = /^(?:(?:\*|C\d+) )|\+ ?/, RE_BODYLITERAL = /BODY\[(.*)\] \{(\d+)\}$/i, RE_BODYINLINEKEY = /^BODY\[(.*)\]$/i, RE_SEQNO = /^\* (\d+)/, RE_LISTCONTENT = /^\((.*)\)$/, RE_LITERAL = /\{(\d+)\}$/, RE_UNTAGGED = /^\* (?:(OK|NO|BAD|BYE|FLAGS|ID|LIST|LSUB|SEARCH|STATUS|CAPABILITY|NAMESPACE|PREAUTH|SORT|THREAD|ESEARCH|QUOTA|QUOTAROOT)|(\d+) (EXPUNGE|FETCH|RECENT|EXISTS))(?:(?: \[([^\]]+)\])?(?: (.+))?)?$/i, - RE_TAGGED = /^A(\d+) (OK|NO|BAD) (?:\[([^\]]+)\] )?(.+)$/i, + RE_TAGGED = /^A(\d+) (OK|NO|BAD) ?(?:\[([^\]]+)\] )?(.*)$/i, + //163.com fix + //RE_TAGGED = /^C(\d+) (OK|NO|BAD) (?:\[([^\]]+)\] )?(.+)$/i, RE_CONTINUE = /^\+(?: (?:\[([^\]]+)\] )?(.+))?$/i, RE_CRLF = /\r\n/g, RE_HDR = /^([^:]+):[ \t]?(.+)?$/, @@ -76,6 +81,20 @@ Parser.prototype.setStream = function(stream) { this._stream.on('readable', this._cbReadable); }; +Parser.prototype.log_error = function(err, source){ + //return console.log('Fuck it up'); + var path = err && err.stack && err.stack.split && err.stack.split('\n')[1]; + console.error('json '+JSON.stringify({ + error:{ + message: err.message, + name: err.name, + stack: path ? path.trim() : err.stack + }, + source: source, + message: 'Email fault' + })); +}; + Parser.prototype._tryread = function(n) { if (this._stream.readable) { var r = this._stream.read(n); @@ -84,6 +103,7 @@ Parser.prototype._tryread = function(n) { }; Parser.prototype._parse = function(data) { + //this.debug && this.debug('<> ' + data.toString()); var i = 0, datalen = data.length, idxlf; if (this._literallen > 0) { @@ -95,13 +115,17 @@ Parser.prototype._parse = function(data) { this._literallen = 0; this._body = undefined; body._read = EMPTY_READCB; - if (datalen > litlen) - body.push(data.slice(0, litlen)); - else - body.push(data); + if (datalen > litlen) { + this.debug && this.debug('<= ' + inspect(data.slice(0, litlen).toString())); + body.push(data.slice(0, litlen)); + }else { + this.debug && this.debug('<= ' + inspect(data.toString())); + body.push(data); + } body.push(null); } else { this._literallen -= datalen; + this.debug && this.debug('<= ' + inspect(data.toString())); var r = body.push(data); if (!r) { body._read = this._cbReadable; @@ -129,13 +153,17 @@ Parser.prototype._parse = function(data) { this._buffer = this._buffer.trim(); i = idxlf + 1; - this.debug && this.debug('<= ' + inspect(this._buffer)); + !this._body && this.debug && this.debug('<= ' + inspect(this._buffer)); if (RE_PRECEDING.test(this._buffer)) { var firstChar = this._buffer[0]; - if (firstChar === '*') + if (firstChar === '*') { + //here we become ready for broken first line + //needed for cases when the answer is the email body + //if((i < data.length) && (data[i] == 32)) continue; this._resUntagged(); - else if (firstChar === 'A') + }else if (firstChar === 'A') + //}else if (firstChar === 'C')//163.com this._resTagged(); else if (firstChar === '+') this._resContinue(); @@ -428,7 +456,9 @@ function parseFetch(text, literals, seqno) { var list = parseExpr(text, literals)[0], attrs = {}, m, body; // list is [KEY1, VAL1, KEY2, VAL2, .... KEYn, VALn] for (var i = 0, len = list.length, key, val; i < len; i += 2) { - key = list[i].toLowerCase(); + //SIEGE + //null.toString(); + key = list[i] && list[i].toString().toLowerCase(); val = list[i + 1]; if (key === 'envelope') val = parseFetchEnvelope(val); @@ -474,7 +504,11 @@ function parseBodyStructure(cur, literals, prefix, partID) { prefix + (prefix !== '' ? '.' : '') + (partID++).toString(), 1)); } - part = { type: cur[next++].toLowerCase() }; + //SIEGE 2 + //part=6;part = part.toLowerCase(); + //LATE SIEGE too: + //undefined.toString() + part = { type: (cur[next++] || '').toString().toLowerCase() }; if (partLen > next) { if (Array.isArray(cur[next])) { part.params = {}; @@ -493,21 +527,29 @@ function parseBodyStructure(cur, literals, prefix, partID) { partID: (prefix !== '' ? prefix : '1'), // required fields as per RFC 3501 -- null or otherwise - type: cur[0].toLowerCase(), subtype: cur[1].toLowerCase(), + //SIEGE 3 + type: cur[0] && cur[0].toLowerCase() || '', subtype: cur[1].toLowerCase(), params: null, id: cur[3], description: cur[4], encoding: cur[5], size: cur[6] }; } else { // type information for malformed multipart body - part = { type: cur[0].toLowerCase(), params: null }; + //SIEGE + //null.toLowerCase(); + part = { type: cur[0] && cur[0].toLowerCase() || '', params: null }; cur.splice(1, 0, null); ++partLen; next = 2; } if (Array.isArray(cur[2])) { part.params = {}; - for (i = 0, len = cur[2].length; i < len; i += 2) - part.params[cur[2][i].toLowerCase()] = cur[2][i + 1]; + for (i = 0, len = cur[2].length; i < len; i += 2){ + //Cannot read property toLowerCase of NULL + if(cur[2][i]) + //SIEGE - undefined is not a function + part.params[cur[2][i].toString().toLowerCase()] = cur[2][i + 1]; + } + if (cur[1] === null) ++next; } @@ -587,8 +629,22 @@ function parseStructExtra(part, partLen, cur, next) { } function parseFetchEnvelope(list) { + if(!list) return { + date: new Date( 0), + subject: '', + from: [], + sender: [], + replyTo: [], + to: [], + cc: [], + bcc: [], + inReplyTo: '', + messageId: '' + }; return { - date: new Date(list[0]), + //SIEGE 2 + //date: null[0], + date: new Date(list && list[0] || 0), subject: decodeWords(list[1]), from: parseEnvelopeAddresses(list[2]), sender: parseEnvelopeAddresses(list[3]), @@ -608,6 +664,9 @@ function parseEnvelopeAddresses(list) { var inGroup = false, curGroup; for (var i = 0, len = list.length, addr; i < len; ++i) { addr = list[i]; + //SIEGE + if(!Array.isArray(addr)) continue; + //addr = undefined; if (addr[2] === null) { // end of group addresses inGroup = false; if (curGroup) { @@ -658,6 +717,10 @@ function parseExpr(o, literals, result, start, useBrackets) { o = { str: o }; isTop = true; } + //instead of UNCANCELLED SIEGE + //Cannot read property 'str' of null|undefined + if(!o) o = {}; + if(!o.str) o.str = ''; for (var i = start, len = o.str.length; i < len; ++i) { if (!inQuote) { if (isBody) { @@ -889,7 +952,7 @@ function decodeWords(str, state) { state.replaces = []; var bytes, m, next, i, j, leni, lenj, seq, replaces = [], lastReplace = {}; - + // join consecutive q-encoded words that have the same charset first while (m = RE_ENCWORD.exec(str)) { seq = { @@ -940,19 +1003,22 @@ function decodeWords(str, state) { decodeBytes(bytes, m.charset, m.index, m.length, m.pendoffset, state, next && next.buf); } - +//VERY LATE SIEGE + str = str ? str.toString() : ''; // perform the actual replacements for (i = state.replaces.length - 1; i >= 0; --i) { seq = state.replaces[i]; if (Array.isArray(seq)) { for (j = 0, lenj = seq.length; j < lenj; ++j) { str = str.substring(0, seq[j].fromOffset) - + seq[j].val + + seq[j].val.toString() + str.substring(seq[j].toOffset); } } else { + //VERY LATE SIEGE + //Parser.js:1029 str = str.substring(0, seq.fromOffset) ^ TypeError: undefined is not a function str = str.substring(0, seq.fromOffset) - + seq.val + + seq.val.toString() + str.substring(seq.toOffset); } } diff --git a/test/test-parser.js b/test/test-parser.js index 70d3883d..8338faf3 100644 --- a/test/test-parser.js +++ b/test/test-parser.js @@ -253,6 +253,29 @@ var CR = '\r', LF = '\n', CRLF = CR + LF; bodySHA1s: ['1f96faf50f6410f99237791f9e3b89454bf93fa7'], what: 'Untagged FETCH (body)' }, + { source: ['* 12 FETCH (BODY[HEADER]'+CRLF+' {344}', CRLF, + 'Date: Wed, 17 Jul 1996 02:23:25 -0700 (PDT)', CRLF, + 'From: Terry Gray ', CRLF, + 'Subject: IMAP4rev1 WG mtg summary and minutes', CRLF, + 'To: imap@cac.washington.edu', CRLF, + 'cc: minutes@CNRI.Reston.VA.US, John Klensin ', CRLF, + 'Message-Id: ', CRLF, + 'MIME-Version: 1.0', CRLF, + 'Content-Type: TEXT/PLAIN; CHARSET=US-ASCII', CRLF, CRLF, + ')', CRLF], + expected: [ { seqno: 12, + which: 'HEADER', + size: 344 + }, + { type: 'fetch', + num: 12, + textCode: undefined, + text: {} + } + ], + bodySHA1s: ['1f96faf50f6410f99237791f9e3b89454bf93fa7'], + what: 'Untagged FETCH (body) with broken first line' + }, { source: ['* 12 FETCH (BODY[TEXT] "IMAP is terrible")', CRLF], expected: [ { seqno: 12, which: 'TEXT',