|
|
|
@ -1,3 +1,12 @@
|
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
|
|
const Promise = require("bluebird");
|
|
|
|
|
const defaultValue = require("default-value");
|
|
|
|
|
|
|
|
|
|
const { command, unsafeRaw, already7Bit } = require("./util/command");
|
|
|
|
|
const pInterval = require("./util/p-interval");
|
|
|
|
|
const createFetchTaskTracker = require("./util/fetch-task");
|
|
|
|
|
|
|
|
|
|
var tls = require('tls'),
|
|
|
|
|
Socket = require('net').Socket,
|
|
|
|
|
EventEmitter = require('events').EventEmitter,
|
|
|
|
@ -77,9 +86,8 @@ function Connection(config) {
|
|
|
|
|
|
|
|
|
|
this._sock = config.socket || undefined;
|
|
|
|
|
this._tagcount = 0;
|
|
|
|
|
this._tmrConn = undefined;
|
|
|
|
|
this._tmrKeepalive = undefined;
|
|
|
|
|
this._tmrAuth = undefined;
|
|
|
|
|
this._connectionTimeout = undefined;
|
|
|
|
|
this._authenticationTimeout = undefined;
|
|
|
|
|
this._queue = [];
|
|
|
|
|
this._box = undefined;
|
|
|
|
|
this._idle = { started: undefined, enabled: false };
|
|
|
|
@ -103,9 +111,8 @@ Connection.prototype.connect = function() {
|
|
|
|
|
socket.setKeepAlive(true);
|
|
|
|
|
this._sock = undefined;
|
|
|
|
|
this._tagcount = 0;
|
|
|
|
|
this._tmrConn = undefined;
|
|
|
|
|
this._tmrKeepalive = undefined;
|
|
|
|
|
this._tmrAuth = undefined;
|
|
|
|
|
this._connectionTimeout = undefined;
|
|
|
|
|
this._authenticationTimeout = undefined;
|
|
|
|
|
this._queue = [];
|
|
|
|
|
this._box = undefined;
|
|
|
|
|
this._idle = { started: undefined, enabled: false };
|
|
|
|
@ -114,6 +121,7 @@ Connection.prototype.connect = function() {
|
|
|
|
|
this.delimiter = undefined;
|
|
|
|
|
this.namespaces = undefined;
|
|
|
|
|
this.state = 'disconnected';
|
|
|
|
|
this._cancelKeepaliveTimer = function () {};
|
|
|
|
|
|
|
|
|
|
if (config.tls) {
|
|
|
|
|
tlsOptions = {};
|
|
|
|
@ -132,10 +140,10 @@ Connection.prototype.connect = function() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onconnect() {
|
|
|
|
|
clearTimeout(self._tmrConn);
|
|
|
|
|
clearTimeout(self._connectionTimeout);
|
|
|
|
|
self.state = 'connected';
|
|
|
|
|
self.debug && self.debug('[connection] Connected to host');
|
|
|
|
|
self._tmrAuth = setTimeout(function() {
|
|
|
|
|
self._authenticationTimeout = setTimeout(function() {
|
|
|
|
|
var err = new Error('Timed out while authenticating with server');
|
|
|
|
|
err.source = 'timeout-auth';
|
|
|
|
|
self.emit('error', err);
|
|
|
|
@ -144,19 +152,23 @@ Connection.prototype.connect = function() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._onError = function(err) {
|
|
|
|
|
clearTimeout(self._tmrConn);
|
|
|
|
|
clearTimeout(self._tmrAuth);
|
|
|
|
|
clearTimeout(self._connectionTimeout);
|
|
|
|
|
clearTimeout(self._authenticationTimeout);
|
|
|
|
|
self.debug && self.debug('[connection] Error: ' + err);
|
|
|
|
|
err.source = 'socket';
|
|
|
|
|
self.emit('error', err);
|
|
|
|
|
};
|
|
|
|
|
this._sock.on('error', this._onError);
|
|
|
|
|
|
|
|
|
|
this._onSocketTimeout = function() {
|
|
|
|
|
clearTimeout(self._tmrConn);
|
|
|
|
|
clearTimeout(self._tmrAuth);
|
|
|
|
|
clearTimeout(self._tmrKeepalive);
|
|
|
|
|
this._teardownConnection = function () {
|
|
|
|
|
clearTimeout(self._connectionTimeout);
|
|
|
|
|
clearTimeout(self._authenticationTimeout);
|
|
|
|
|
self._cancelKeepaliveTimer();
|
|
|
|
|
self.state = 'disconnected';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this._onSocketTimeout = function() {
|
|
|
|
|
self._teardownConnection();
|
|
|
|
|
self.debug && self.debug('[connection] Socket timeout');
|
|
|
|
|
|
|
|
|
|
var err = new Error('Socket timed out while talking to server');
|
|
|
|
@ -168,19 +180,13 @@ Connection.prototype.connect = function() {
|
|
|
|
|
socket.setTimeout(config.socketTimeout);
|
|
|
|
|
|
|
|
|
|
socket.once('close', function(had_err) {
|
|
|
|
|
clearTimeout(self._tmrConn);
|
|
|
|
|
clearTimeout(self._tmrAuth);
|
|
|
|
|
clearTimeout(self._tmrKeepalive);
|
|
|
|
|
self.state = 'disconnected';
|
|
|
|
|
self._teardownConnection();
|
|
|
|
|
self.debug && self.debug('[connection] Closed');
|
|
|
|
|
self.emit('close', had_err);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
socket.once('end', function() {
|
|
|
|
|
clearTimeout(self._tmrConn);
|
|
|
|
|
clearTimeout(self._tmrAuth);
|
|
|
|
|
clearTimeout(self._tmrKeepalive);
|
|
|
|
|
self.state = 'disconnected';
|
|
|
|
|
self._teardownConnection();
|
|
|
|
|
self.debug && self.debug('[connection] Ended');
|
|
|
|
|
self.emit('end');
|
|
|
|
|
});
|
|
|
|
@ -194,30 +200,27 @@ Connection.prototype.connect = function() {
|
|
|
|
|
self._resTagged(info);
|
|
|
|
|
});
|
|
|
|
|
parser.on('body', function(stream, info) {
|
|
|
|
|
var msg = self._curReq.fetchCache[info.seqno], toget;
|
|
|
|
|
|
|
|
|
|
if (msg === undefined) {
|
|
|
|
|
msg = self._curReq.fetchCache[info.seqno] = {
|
|
|
|
|
msgEmitter: new EventEmitter(),
|
|
|
|
|
toget: self._curReq.fetching.slice(0),
|
|
|
|
|
attrs: {},
|
|
|
|
|
ended: false
|
|
|
|
|
};
|
|
|
|
|
var task = self._curReq.fetchCache.get(info.seqno), remainingKeys;
|
|
|
|
|
|
|
|
|
|
if (task == null) {
|
|
|
|
|
task = self._curReq.fetchCache.create(info.seqno, self._curReq.fetching.slice());
|
|
|
|
|
|
|
|
|
|
self._curReq.bodyEmitter.emit('message', msg.msgEmitter, info.seqno);
|
|
|
|
|
self._curReq.bodyEmitter.emit('message', task.emitter, info.seqno);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toget = msg.toget;
|
|
|
|
|
remainingKeys = task.getRemainingKeys();
|
|
|
|
|
|
|
|
|
|
// FIXME: Refactor below
|
|
|
|
|
|
|
|
|
|
// here we compare the parsed version of the expression inside BODY[]
|
|
|
|
|
// because 'HEADER.FIELDS (TO FROM)' really is equivalent to
|
|
|
|
|
// 'HEADER.FIELDS ("TO" "FROM")' and some servers will actually send the
|
|
|
|
|
// quoted form even if the client did not use quotes
|
|
|
|
|
var thisbody = parseExpr(info.which);
|
|
|
|
|
for (var i = 0, len = toget.length; i < len; ++i) {
|
|
|
|
|
if (_deepEqual(thisbody, toget[i])) {
|
|
|
|
|
toget.splice(i, 1);
|
|
|
|
|
msg.msgEmitter.emit('body', stream, info);
|
|
|
|
|
for (var i = 0, len = remainingKeys.length; i < len; ++i) {
|
|
|
|
|
if (_deepEqual(thisbody, remainingKeys[i])) {
|
|
|
|
|
remainingKeys.splice(i, 1);
|
|
|
|
|
task.emitter.emit('body', stream, info);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -240,7 +243,7 @@ Connection.prototype.connect = function() {
|
|
|
|
|
// now idling
|
|
|
|
|
self._idle.started = Date.now();
|
|
|
|
|
} else if (/^AUTHENTICATE XOAUTH/.test(self._curReq.fullcmd)) {
|
|
|
|
|
self._curReq.oauthError = new Buffer(info.text, 'base64').toString('utf8');
|
|
|
|
|
self._curReq.oauthError = Buffer.from(info.text, 'base64').toString('utf8');
|
|
|
|
|
self.debug && self.debug('=> ' + inspect(CRLF));
|
|
|
|
|
self._sock.write(CRLF);
|
|
|
|
|
} else if (type === 'APPEND') {
|
|
|
|
@ -257,7 +260,7 @@ Connection.prototype.connect = function() {
|
|
|
|
|
// no longer idling
|
|
|
|
|
self._idle.enabled = false;
|
|
|
|
|
self._idle.started = undefined;
|
|
|
|
|
clearTimeout(self._tmrKeepalive);
|
|
|
|
|
self._cancelKeepaliveTimer();
|
|
|
|
|
|
|
|
|
|
self._curReq = undefined;
|
|
|
|
|
|
|
|
|
@ -276,7 +279,7 @@ Connection.prototype.connect = function() {
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this._tmrConn = setTimeout(function() {
|
|
|
|
|
this._connectionTimeout = setTimeout(function() {
|
|
|
|
|
var err = new Error('Timed out while connecting to server');
|
|
|
|
|
err.source = 'timeout';
|
|
|
|
|
self.emit('error', err);
|
|
|
|
@ -297,15 +300,17 @@ Connection.prototype.serverSupports = function(cap) {
|
|
|
|
|
Connection.prototype.destroy = function() {
|
|
|
|
|
this._queue = [];
|
|
|
|
|
this._curReq = undefined;
|
|
|
|
|
this._sock && this._sock.end();
|
|
|
|
|
|
|
|
|
|
if (this._sock != null) {
|
|
|
|
|
this._sock.end();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Connection.prototype.end = function() {
|
|
|
|
|
var self = this;
|
|
|
|
|
this._enqueue('LOGOUT', function() {
|
|
|
|
|
self._queue = [];
|
|
|
|
|
self._curReq = undefined;
|
|
|
|
|
self._sock.end();
|
|
|
|
|
return Promise.try(() => {
|
|
|
|
|
return this._enqueue2(command`LOGOUT`);
|
|
|
|
|
}).then(() => {
|
|
|
|
|
return this.destroy();
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
@ -860,7 +865,7 @@ Connection.prototype._fetch = function(which, uids, options) {
|
|
|
|
|
|
|
|
|
|
this._enqueue(cmd);
|
|
|
|
|
var req = this._queue[this._queue.length - 1];
|
|
|
|
|
req.fetchCache = {};
|
|
|
|
|
req.fetchCache = createFetchTaskTracker();
|
|
|
|
|
req.fetching = fetching;
|
|
|
|
|
return (req.bodyEmitter = new EventEmitter());
|
|
|
|
|
};
|
|
|
|
@ -1229,58 +1234,73 @@ Object.defineProperty(Connection.prototype, 'seq', { get: function() {
|
|
|
|
|
};
|
|
|
|
|
}});
|
|
|
|
|
|
|
|
|
|
Connection.prototype._resUntagged = function(info) {
|
|
|
|
|
var type = info.type, i, len, box, attrs, key;
|
|
|
|
|
// type: type,
|
|
|
|
|
// num: num,
|
|
|
|
|
// textCode: textCode,
|
|
|
|
|
// text: val
|
|
|
|
|
|
|
|
|
|
Connection.prototype._resUntagged = function({ type, num, textCode, text: payload }) {
|
|
|
|
|
var i, len, box, destinationKey;
|
|
|
|
|
|
|
|
|
|
if (type === 'bye')
|
|
|
|
|
if (type === 'bye') {
|
|
|
|
|
this._sock.end();
|
|
|
|
|
else if (type === 'namespace')
|
|
|
|
|
this.namespaces = info.text;
|
|
|
|
|
else if (type === 'id')
|
|
|
|
|
this._curReq.cbargs.push(info.text);
|
|
|
|
|
else if (type === 'capability')
|
|
|
|
|
this._caps = info.text.map(function(v) { return v.toUpperCase(); });
|
|
|
|
|
else if (type === 'preauth')
|
|
|
|
|
} else if (type === 'namespace') {
|
|
|
|
|
this.namespaces = payload;
|
|
|
|
|
} else if (type === 'id') {
|
|
|
|
|
this._curReq.cbargs.push(payload);
|
|
|
|
|
} else if (type === 'capability') {
|
|
|
|
|
this._caps = payload.map((v) => v.toUpperCase());
|
|
|
|
|
} else if (type === 'preauth') {
|
|
|
|
|
this.state = 'authenticated';
|
|
|
|
|
else if (type === 'sort' || type === 'thread' || type === 'esearch')
|
|
|
|
|
this._curReq.cbargs.push(info.text);
|
|
|
|
|
else if (type === 'search') {
|
|
|
|
|
if (info.text.results !== undefined) {
|
|
|
|
|
} else if (type === 'sort' || type === 'thread' || type === 'esearch') {
|
|
|
|
|
this._curReq.cbargs.push(payload);
|
|
|
|
|
} else if (type === 'search') {
|
|
|
|
|
if (payload.results !== undefined) {
|
|
|
|
|
// CONDSTORE-modified search results
|
|
|
|
|
this._curReq.cbargs.push(info.text.results);
|
|
|
|
|
this._curReq.cbargs.push(info.text.modseq);
|
|
|
|
|
} else
|
|
|
|
|
this._curReq.cbargs.push(info.text);
|
|
|
|
|
this._curReq.cbargs.push(payload.results);
|
|
|
|
|
this._curReq.cbargs.push(payload.modseq);
|
|
|
|
|
} else {
|
|
|
|
|
this._curReq.cbargs.push(payload);
|
|
|
|
|
}
|
|
|
|
|
} else if (type === 'quota') {
|
|
|
|
|
var cbargs = this._curReq.cbargs;
|
|
|
|
|
if (!cbargs.length)
|
|
|
|
|
if (!cbargs.length) {
|
|
|
|
|
cbargs.push([]);
|
|
|
|
|
cbargs[0].push(info.text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cbargs[0].push(payload);
|
|
|
|
|
} else if (type === 'recent') {
|
|
|
|
|
if (!this._box && RE_OPENBOX.test(this._curReq.type))
|
|
|
|
|
if (!this._box && RE_OPENBOX.test(this._curReq.type)) {
|
|
|
|
|
this._createCurrentBox();
|
|
|
|
|
if (this._box)
|
|
|
|
|
this._box.messages.new = info.num;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this._box) {
|
|
|
|
|
this._box.messages.new = num;
|
|
|
|
|
}
|
|
|
|
|
} else if (type === 'flags') {
|
|
|
|
|
if (!this._box && RE_OPENBOX.test(this._curReq.type))
|
|
|
|
|
if (!this._box && RE_OPENBOX.test(this._curReq.type)) {
|
|
|
|
|
this._createCurrentBox();
|
|
|
|
|
if (this._box)
|
|
|
|
|
this._box.flags = info.text;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this._box) {
|
|
|
|
|
this._box.flags = payload;
|
|
|
|
|
}
|
|
|
|
|
} else if (type === 'bad' || type === 'no') {
|
|
|
|
|
if (this.state === 'connected' && !this._curReq) {
|
|
|
|
|
clearTimeout(this._tmrConn);
|
|
|
|
|
clearTimeout(this._tmrAuth);
|
|
|
|
|
var err = new Error('Received negative welcome: ' + info.text);
|
|
|
|
|
clearTimeout(this._connectionTimeout);
|
|
|
|
|
clearTimeout(this._authenticationTimeout);
|
|
|
|
|
var err = new Error('Received negative welcome: ' + payload);
|
|
|
|
|
err.source = 'protocol';
|
|
|
|
|
this.emit('error', err);
|
|
|
|
|
this._sock.end();
|
|
|
|
|
}
|
|
|
|
|
} else if (type === 'exists') {
|
|
|
|
|
if (!this._box && RE_OPENBOX.test(this._curReq.type))
|
|
|
|
|
if (!this._box && RE_OPENBOX.test(this._curReq.type)) {
|
|
|
|
|
this._createCurrentBox();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this._box) {
|
|
|
|
|
var prev = this._box.messages.total,
|
|
|
|
|
now = info.num;
|
|
|
|
|
var prev = this._box.messages.total, now = num;
|
|
|
|
|
this._box.messages.total = now;
|
|
|
|
|
if (now > prev && this.state === 'authenticated') {
|
|
|
|
|
this._box.messages.new = now - prev;
|
|
|
|
@ -1289,250 +1309,208 @@ Connection.prototype._resUntagged = function(info) {
|
|
|
|
|
}
|
|
|
|
|
} else if (type === 'expunge') {
|
|
|
|
|
if (this._box) {
|
|
|
|
|
if (this._box.messages.total > 0)
|
|
|
|
|
if (this._box.messages.total > 0) {
|
|
|
|
|
--this._box.messages.total;
|
|
|
|
|
this.emit('expunge', info.num);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.emit('expunge', num);
|
|
|
|
|
}
|
|
|
|
|
} else if (type === 'ok') {
|
|
|
|
|
if (this.state === 'connected' && !this._curReq)
|
|
|
|
|
if (this.state === 'connected' && !this._curReq) {
|
|
|
|
|
this._login();
|
|
|
|
|
else if (typeof info.textCode === 'string'
|
|
|
|
|
&& info.textCode.toUpperCase() === 'ALERT')
|
|
|
|
|
this.emit('alert', info.text);
|
|
|
|
|
} else if (typeof textCode === 'string' && textCode.toUpperCase() === 'ALERT') {
|
|
|
|
|
this.emit('alert', payload);
|
|
|
|
|
}
|
|
|
|
|
else if (this._curReq
|
|
|
|
|
&& info.textCode
|
|
|
|
|
&& textCode
|
|
|
|
|
&& (RE_OPENBOX.test(this._curReq.type))) {
|
|
|
|
|
// we're opening a mailbox
|
|
|
|
|
|
|
|
|
|
if (!this._box)
|
|
|
|
|
if (!this._box) {
|
|
|
|
|
this._createCurrentBox();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (info.textCode.key)
|
|
|
|
|
key = info.textCode.key.toUpperCase();
|
|
|
|
|
else
|
|
|
|
|
key = info.textCode;
|
|
|
|
|
|
|
|
|
|
if (key === 'UIDVALIDITY')
|
|
|
|
|
this._box.uidvalidity = info.textCode.val;
|
|
|
|
|
else if (key === 'UIDNEXT')
|
|
|
|
|
this._box.uidnext = info.textCode.val;
|
|
|
|
|
else if (key === 'HIGHESTMODSEQ')
|
|
|
|
|
this._box.highestmodseq = ''+info.textCode.val;
|
|
|
|
|
else if (key === 'PERMANENTFLAGS') {
|
|
|
|
|
if (textCode.key) {
|
|
|
|
|
destinationKey = textCode.key.toUpperCase();
|
|
|
|
|
} else {
|
|
|
|
|
destinationKey = textCode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (destinationKey === 'UIDVALIDITY') {
|
|
|
|
|
this._box.uidvalidity = textCode.val;
|
|
|
|
|
} else if (destinationKey === 'UIDNEXT') {
|
|
|
|
|
this._box.uidnext = textCode.val;
|
|
|
|
|
} else if (destinationKey === 'HIGHESTMODSEQ') {
|
|
|
|
|
this._box.highestmodseq = ''+textCode.val;
|
|
|
|
|
} else if (destinationKey === 'PERMANENTFLAGS') {
|
|
|
|
|
var idx, permFlags, keywords;
|
|
|
|
|
this._box.permFlags = permFlags = info.textCode.val;
|
|
|
|
|
this._box.permFlags = permFlags = textCode.val;
|
|
|
|
|
|
|
|
|
|
if ((idx = this._box.permFlags.indexOf('\\*')) > -1) {
|
|
|
|
|
this._box.newKeywords = true;
|
|
|
|
|
permFlags.splice(idx, 1);
|
|
|
|
|
}
|
|
|
|
|
this._box.keywords = keywords = permFlags.filter(function(f) {
|
|
|
|
|
return (f[0] !== '\\');
|
|
|
|
|
});
|
|
|
|
|
for (i = 0, len = keywords.length; i < len; ++i)
|
|
|
|
|
|
|
|
|
|
this._box.keywords = keywords = permFlags.filter((f) => f[0] !== '\\');
|
|
|
|
|
|
|
|
|
|
for (i = 0, len = keywords.length; i < len; ++i) {
|
|
|
|
|
permFlags.splice(permFlags.indexOf(keywords[i]), 1);
|
|
|
|
|
} else if (key === 'UIDNOTSTICKY')
|
|
|
|
|
}
|
|
|
|
|
} else if (destinationKey === 'UIDNOTSTICKY')
|
|
|
|
|
this._box.persistentUIDs = false;
|
|
|
|
|
else if (key === 'NOMODSEQ')
|
|
|
|
|
else if (destinationKey === 'NOMODSEQ')
|
|
|
|
|
this._box.nomodseq = true;
|
|
|
|
|
} else if (typeof info.textCode === 'string'
|
|
|
|
|
&& info.textCode.toUpperCase() === 'UIDVALIDITY')
|
|
|
|
|
this.emit('uidvalidity', info.text);
|
|
|
|
|
} else if (typeof textCode === 'string'
|
|
|
|
|
&& textCode.toUpperCase() === 'UIDVALIDITY')
|
|
|
|
|
this.emit('uidvalidity', payload);
|
|
|
|
|
} else if (type === 'list' || type === 'lsub' || type === 'xlist') {
|
|
|
|
|
if (this.delimiter === undefined)
|
|
|
|
|
this.delimiter = info.text.delimiter;
|
|
|
|
|
else {
|
|
|
|
|
if (this._curReq.cbargs.length === 0)
|
|
|
|
|
if (this.delimiter === undefined) {
|
|
|
|
|
this.delimiter = payload.delimiter;
|
|
|
|
|
} else {
|
|
|
|
|
if (this._curReq.cbargs.length === 0) {
|
|
|
|
|
this._curReq.cbargs.push({});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
box = {
|
|
|
|
|
attribs: info.text.flags,
|
|
|
|
|
delimiter: info.text.delimiter,
|
|
|
|
|
attribs: payload.flags,
|
|
|
|
|
delimiter: payload.delimiter,
|
|
|
|
|
children: null,
|
|
|
|
|
parent: null
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (i = 0, len = SPECIAL_USE_ATTRIBUTES.length; i < len; ++i)
|
|
|
|
|
if (box.attribs.indexOf(SPECIAL_USE_ATTRIBUTES[i]) > -1)
|
|
|
|
|
for (i = 0, len = SPECIAL_USE_ATTRIBUTES.length; i < len; ++i) {
|
|
|
|
|
if (box.attribs.indexOf(SPECIAL_USE_ATTRIBUTES[i]) > -1) {
|
|
|
|
|
box.special_use_attrib = SPECIAL_USE_ATTRIBUTES[i];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var name = info.text.name,
|
|
|
|
|
var name = payload.name,
|
|
|
|
|
curChildren = this._curReq.cbargs[0];
|
|
|
|
|
|
|
|
|
|
if (box.delimiter) {
|
|
|
|
|
var path = name.split(box.delimiter),
|
|
|
|
|
parent = null;
|
|
|
|
|
|
|
|
|
|
name = path.pop();
|
|
|
|
|
|
|
|
|
|
for (i = 0, len = path.length; i < len; ++i) {
|
|
|
|
|
if (!curChildren[path[i]])
|
|
|
|
|
if (!curChildren[path[i]]) {
|
|
|
|
|
curChildren[path[i]] = {};
|
|
|
|
|
if (!curChildren[path[i]].children)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!curChildren[path[i]].children) {
|
|
|
|
|
curChildren[path[i]].children = {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parent = curChildren[path[i]];
|
|
|
|
|
curChildren = curChildren[path[i]].children;
|
|
|
|
|
}
|
|
|
|
|
box.parent = parent;
|
|
|
|
|
}
|
|
|
|
|
if (curChildren[name])
|
|
|
|
|
|
|
|
|
|
if (curChildren[name]) {
|
|
|
|
|
box.children = curChildren[name].children;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
curChildren[name] = box;
|
|
|
|
|
}
|
|
|
|
|
} else if (type === 'status') {
|
|
|
|
|
let attrs = defaultValue(payload.attrs, {});
|
|
|
|
|
|
|
|
|
|
box = {
|
|
|
|
|
name: info.text.name,
|
|
|
|
|
uidnext: 0,
|
|
|
|
|
uidvalidity: 0,
|
|
|
|
|
name: payload.name,
|
|
|
|
|
uidnext: defaultValue(attrs.uidnext, 0),
|
|
|
|
|
uidvalidity: defaultValue(attrs.uidvalidity, 0),
|
|
|
|
|
messages: {
|
|
|
|
|
total: 0,
|
|
|
|
|
new: 0,
|
|
|
|
|
unseen: 0
|
|
|
|
|
}
|
|
|
|
|
total: defaultValue(attrs.messages, 0),
|
|
|
|
|
new: defaultValue(attrs.recent, 0),
|
|
|
|
|
unseen: defaultValue(attrs.unseen, 0)
|
|
|
|
|
},
|
|
|
|
|
// CONDSTORE
|
|
|
|
|
highestmodseq: (attrs.highestmodseq != null)
|
|
|
|
|
? String(attrs.highestmodseq)
|
|
|
|
|
: undefined
|
|
|
|
|
};
|
|
|
|
|
attrs = info.text.attrs;
|
|
|
|
|
|
|
|
|
|
if (attrs) {
|
|
|
|
|
if (attrs.recent !== undefined)
|
|
|
|
|
box.messages.new = attrs.recent;
|
|
|
|
|
if (attrs.unseen !== undefined)
|
|
|
|
|
box.messages.unseen = attrs.unseen;
|
|
|
|
|
if (attrs.messages !== undefined)
|
|
|
|
|
box.messages.total = attrs.messages;
|
|
|
|
|
if (attrs.uidnext !== undefined)
|
|
|
|
|
box.uidnext = attrs.uidnext;
|
|
|
|
|
if (attrs.uidvalidity !== undefined)
|
|
|
|
|
box.uidvalidity = attrs.uidvalidity;
|
|
|
|
|
if (attrs.highestmodseq !== undefined) // CONDSTORE
|
|
|
|
|
box.highestmodseq = ''+attrs.highestmodseq;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FIXME
|
|
|
|
|
this._curReq.cbargs.push(box);
|
|
|
|
|
} else if (type === 'fetch') {
|
|
|
|
|
if (/^(?:UID )?FETCH/.test(this._curReq.fullcmd)) {
|
|
|
|
|
// FETCH response sent as result of FETCH request
|
|
|
|
|
var msg = this._curReq.fetchCache[info.num],
|
|
|
|
|
keys = Object.keys(info.text),
|
|
|
|
|
keyslen = keys.length,
|
|
|
|
|
toget, msgEmitter, j;
|
|
|
|
|
|
|
|
|
|
if (msg === undefined) {
|
|
|
|
|
// simple case -- no bodies were streamed
|
|
|
|
|
toget = this._curReq.fetching.slice(0);
|
|
|
|
|
if (toget.length === 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
msgEmitter = new EventEmitter();
|
|
|
|
|
attrs = {};
|
|
|
|
|
|
|
|
|
|
this._curReq.bodyEmitter.emit('message', msgEmitter, info.num);
|
|
|
|
|
} else {
|
|
|
|
|
toget = msg.toget;
|
|
|
|
|
msgEmitter = msg.msgEmitter;
|
|
|
|
|
attrs = msg.attrs;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
i = toget.length;
|
|
|
|
|
if (i === 0) {
|
|
|
|
|
if (msg && !msg.ended) {
|
|
|
|
|
msg.ended = true;
|
|
|
|
|
process.nextTick(function() {
|
|
|
|
|
msgEmitter.emit('end');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let task = this._curReq.fetchCache.get(num);
|
|
|
|
|
|
|
|
|
|
if (keyslen > 0) {
|
|
|
|
|
while (--i >= 0) {
|
|
|
|
|
j = keyslen;
|
|
|
|
|
while (--j >= 0) {
|
|
|
|
|
if (keys[j].toUpperCase() === toget[i]) {
|
|
|
|
|
if (!RE_BODYPART.test(toget[i])) {
|
|
|
|
|
if (toget[i] === 'X-GM-LABELS') {
|
|
|
|
|
var labels = info.text[keys[j]];
|
|
|
|
|
for (var k = 0, lenk = labels.length; k < lenk; ++k)
|
|
|
|
|
labels[k] = (''+labels[k]).replace(RE_ESCAPE, '\\');
|
|
|
|
|
}
|
|
|
|
|
key = FETCH_ATTR_MAP[toget[i]];
|
|
|
|
|
if (!key)
|
|
|
|
|
key = toget[i].toLowerCase();
|
|
|
|
|
attrs[key] = info.text[keys[j]];
|
|
|
|
|
}
|
|
|
|
|
toget.splice(i, 1);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// FIXME: Refactor, probably make the task itself an event emitter
|
|
|
|
|
if (task == null) {
|
|
|
|
|
task = this._curReq.fetchCache.create(num, this._curReq.fetching.slice());
|
|
|
|
|
this._curReq.bodyEmitter.emit('message', task.emitter, num);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (toget.length === 0) {
|
|
|
|
|
if (msg)
|
|
|
|
|
msg.ended = true;
|
|
|
|
|
process.nextTick(function() {
|
|
|
|
|
msgEmitter.emit('attributes', attrs);
|
|
|
|
|
msgEmitter.emit('end');
|
|
|
|
|
});
|
|
|
|
|
} else if (msg === undefined) {
|
|
|
|
|
this._curReq.fetchCache[info.num] = {
|
|
|
|
|
msgEmitter: msgEmitter,
|
|
|
|
|
toget: toget,
|
|
|
|
|
attrs: attrs,
|
|
|
|
|
ended: false
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
task.processFetchResponse(payload);
|
|
|
|
|
} else {
|
|
|
|
|
// FETCH response sent as result of STORE request or sent unilaterally,
|
|
|
|
|
// treat them as the same for now for simplicity
|
|
|
|
|
this.emit('update', info.num, info.text);
|
|
|
|
|
this.emit('update', num, payload);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Connection.prototype._resTagged = function(info) {
|
|
|
|
|
var req = this._curReq, err;
|
|
|
|
|
|
|
|
|
|
if (!req)
|
|
|
|
|
return;
|
|
|
|
|
var req = this._curReq;
|
|
|
|
|
|
|
|
|
|
if (req != null) {
|
|
|
|
|
var err;
|
|
|
|
|
this._curReq = undefined;
|
|
|
|
|
|
|
|
|
|
if (info.type === 'no' || info.type === 'bad') {
|
|
|
|
|
var errtext;
|
|
|
|
|
if (info.text)
|
|
|
|
|
errtext = info.text;
|
|
|
|
|
else
|
|
|
|
|
errtext = req.oauthError;
|
|
|
|
|
err = new Error(errtext);
|
|
|
|
|
err.type = info.type;
|
|
|
|
|
err.textCode = info.textCode;
|
|
|
|
|
err.source = 'protocol';
|
|
|
|
|
} else if (this._box) {
|
|
|
|
|
// TODO: Can info.text be an empty string?
|
|
|
|
|
let errorText = defaultValue(info.text, req.oauthError);
|
|
|
|
|
|
|
|
|
|
err = Object.assign(new Error(errorText), {
|
|
|
|
|
type: info.type,
|
|
|
|
|
text: info.textCode,
|
|
|
|
|
source: "protocol"
|
|
|
|
|
});
|
|
|
|
|
} else if (this._box != null) {
|
|
|
|
|
if (req.type === 'EXAMINE' || req.type === 'SELECT') {
|
|
|
|
|
this._box.readOnly = (typeof info.textCode === 'string'
|
|
|
|
|
&& info.textCode.toUpperCase() === 'READ-ONLY');
|
|
|
|
|
this._box.readOnly = (
|
|
|
|
|
typeof info.textCode === 'string'
|
|
|
|
|
&& info.textCode.toUpperCase() === 'READ-ONLY'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
if (RE_UIDCMD_HASRESULTS.test(req.fullcmd) && req.cbargs.length === 0)
|
|
|
|
|
if (RE_UIDCMD_HASRESULTS.test(req.fullcmd) && req.cbargs.length === 0) {
|
|
|
|
|
req.cbargs.push([]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (req.bodyEmitter) {
|
|
|
|
|
var bodyEmitter = req.bodyEmitter;
|
|
|
|
|
if (err)
|
|
|
|
|
|
|
|
|
|
if (err) {
|
|
|
|
|
bodyEmitter.emit('error', err);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
process.nextTick(function() {
|
|
|
|
|
bodyEmitter.emit('end');
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
req.cbargs.unshift(err);
|
|
|
|
|
|
|
|
|
|
if (info.textCode && info.textCode.key) {
|
|
|
|
|
var key = info.textCode.key.toUpperCase();
|
|
|
|
|
if (key === 'APPENDUID') // [uidvalidity, newUID]
|
|
|
|
|
if (key === 'APPENDUID') { // [uidvalidity, newUID]
|
|
|
|
|
req.cbargs.push(info.textCode.val[1]);
|
|
|
|
|
else if (key === 'COPYUID') // [uidvalidity, sourceUIDs, destUIDs]
|
|
|
|
|
} else if (key === 'COPYUID') { // [uidvalidity, sourceUIDs, destUIDs]
|
|
|
|
|
req.cbargs.push(info.textCode.val[2]);
|
|
|
|
|
}
|
|
|
|
|
req.cb && req.cb.apply(this, req.cbargs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (req.cb != null) {
|
|
|
|
|
req.cb.apply(this, req.cbargs);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this._queue.length === 0
|
|
|
|
@ -1544,6 +1522,10 @@ Connection.prototype._resTagged = function(info) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._processQueue();
|
|
|
|
|
} else {
|
|
|
|
|
// TODO: Configurable logger
|
|
|
|
|
console.warn(`ignoring response because there is no pending request`);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Connection.prototype._createCurrentBox = function() {
|
|
|
|
@ -1565,47 +1547,55 @@ Connection.prototype._createCurrentBox = function() {
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Connection.prototype._doKeepaliveTimer = function(immediate) {
|
|
|
|
|
var self = this,
|
|
|
|
|
interval = this._config.keepalive.interval || KEEPALIVE_INTERVAL,
|
|
|
|
|
idleWait = this._config.keepalive.idleInterval || MAX_IDLE_WAIT,
|
|
|
|
|
forceNoop = this._config.keepalive.forceNoop || false,
|
|
|
|
|
timerfn = function() {
|
|
|
|
|
if (self._idle.enabled) {
|
|
|
|
|
// TODO: Refactor this better
|
|
|
|
|
Connection.prototype._sendKeepalive = function () {
|
|
|
|
|
let idleWait = this._config.keepalive.idleInterval || MAX_IDLE_WAIT;
|
|
|
|
|
let forceNoop = this._config.keepalive.forceNoop || false;
|
|
|
|
|
|
|
|
|
|
if (this._idle.enabled) {
|
|
|
|
|
// unlike NOOP, IDLE is only a valid command after authenticating
|
|
|
|
|
if (!self.serverSupports('IDLE')
|
|
|
|
|
|| self.state !== 'authenticated'
|
|
|
|
|
|| forceNoop)
|
|
|
|
|
self._enqueue('NOOP', true);
|
|
|
|
|
else {
|
|
|
|
|
if (self._idle.started === undefined) {
|
|
|
|
|
self._idle.started = 0;
|
|
|
|
|
self._enqueue('IDLE', true);
|
|
|
|
|
} else if (self._idle.started > 0) {
|
|
|
|
|
var timeDiff = Date.now() - self._idle.started;
|
|
|
|
|
if (!this.serverSupports('IDLE') || this.state !== 'authenticated' || forceNoop) {
|
|
|
|
|
// Ignore return
|
|
|
|
|
this._enqueue2(command`NOOP`, { insertInFront: true });
|
|
|
|
|
this._cancelKeepaliveTimer();
|
|
|
|
|
} else {
|
|
|
|
|
if (this._idle.started === undefined) {
|
|
|
|
|
this._idle.started = 0;
|
|
|
|
|
// Ignore return
|
|
|
|
|
this._enqueue2(command`IDLE`, { insertInFront: true });
|
|
|
|
|
} else if (this._idle.started > 0) {
|
|
|
|
|
var timeDiff = Date.now() - this._idle.started;
|
|
|
|
|
|
|
|
|
|
if (timeDiff >= idleWait) {
|
|
|
|
|
self._idle.enabled = false;
|
|
|
|
|
self.debug && self.debug('=> DONE');
|
|
|
|
|
self._sock.write('DONE' + CRLF);
|
|
|
|
|
this._idle.enabled = false;
|
|
|
|
|
this.debug && this.debug('=> DONE');
|
|
|
|
|
// ????
|
|
|
|
|
this._sock.write('DONE' + CRLF);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
self._tmrKeepalive = setTimeout(timerfn, interval);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (immediate)
|
|
|
|
|
timerfn();
|
|
|
|
|
else
|
|
|
|
|
this._tmrKeepalive = setTimeout(timerfn, interval);
|
|
|
|
|
// TODO: Get rid of
|
|
|
|
|
Connection.prototype._doKeepaliveTimer = function(immediate) {
|
|
|
|
|
let interval = this._config.keepalive.interval || KEEPALIVE_INTERVAL;
|
|
|
|
|
|
|
|
|
|
if (immediate) {
|
|
|
|
|
return this._sendKeepalive();
|
|
|
|
|
} else {
|
|
|
|
|
this._cancelKeepaliveTimer = pInterval(interval, () => {
|
|
|
|
|
this._sendKeepalive();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Connection.prototype._login = function() {
|
|
|
|
|
var self = this, checkedNS = false;
|
|
|
|
|
|
|
|
|
|
var reentry = function(err) {
|
|
|
|
|
clearTimeout(self._tmrAuth);
|
|
|
|
|
clearTimeout(self._authenticationTimeout);
|
|
|
|
|
if (err) {
|
|
|
|
|
self.emit('error', err);
|
|
|
|
|
return self._sock.end();
|
|
|
|
@ -1755,6 +1745,7 @@ Connection.prototype._sockWriteAppendData = function(appendData)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Connection.prototype._enqueue = function(fullcmd, promote, cb) {
|
|
|
|
|
// TODO: Remove variability
|
|
|
|
|
if (typeof promote === 'function') {
|
|
|
|
|
cb = promote;
|
|
|
|
|
promote = false;
|
|
|
|
@ -1768,10 +1759,11 @@ Connection.prototype._enqueue = function(fullcmd, promote, cb) {
|
|
|
|
|
},
|
|
|
|
|
self = this;
|
|
|
|
|
|
|
|
|
|
if (promote)
|
|
|
|
|
if (promote) {
|
|
|
|
|
this._queue.unshift(info);
|
|
|
|
|
else
|
|
|
|
|
} else {
|
|
|
|
|
this._queue.push(info);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!this._curReq
|
|
|
|
|
&& this.state !== 'disconnected'
|
|
|
|
@ -1785,7 +1777,7 @@ Connection.prototype._enqueue = function(fullcmd, promote, cb) {
|
|
|
|
|
&& this._sock.writable
|
|
|
|
|
&& this._idle.enabled) {
|
|
|
|
|
this._idle.enabled = false;
|
|
|
|
|
clearTimeout(this._tmrKeepalive);
|
|
|
|
|
this._cancelKeepaliveTimer();
|
|
|
|
|
if (this._idle.started > 0) {
|
|
|
|
|
// we've seen the continuation for our IDLE
|
|
|
|
|
this.debug && this.debug('=> DONE');
|
|
|
|
@ -1794,6 +1786,20 @@ Connection.prototype._enqueue = function(fullcmd, promote, cb) {
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Connection.prototype._enqueueAsync = Promise.promisify(Connection.prototype._enqueue);
|
|
|
|
|
|
|
|
|
|
Connection.prototype._enqueue2 = function (command, options = {}) {
|
|
|
|
|
let insertInFront = defaultValue(options.insertInFront, false);
|
|
|
|
|
|
|
|
|
|
if (command.toCommandString != null) {
|
|
|
|
|
let string = command.toCommandString();
|
|
|
|
|
return this._enqueueAsync(string, insertInFront);
|
|
|
|
|
} else {
|
|
|
|
|
// TODO: Use `unreachable`
|
|
|
|
|
throw new Error(`Must use a command template string`);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Connection.parseHeader = parseHeader; // from Parser.js
|
|
|
|
|
|
|
|
|
|
module.exports = Connection;
|
|
|
|
@ -1847,7 +1853,7 @@ function buildString(str) {
|
|
|
|
|
str = ''+str;
|
|
|
|
|
|
|
|
|
|
if (hasNonASCII(str)) {
|
|
|
|
|
var buf = new Buffer(str, 'utf8');
|
|
|
|
|
var buf = Buffer.from(str, 'utf8');
|
|
|
|
|
return '{' + buf.length + '}\r\n' + buf.toString('binary');
|
|
|
|
|
} else
|
|
|
|
|
return '"' + escape(str) + '"';
|
|
|
|
|