Sven Slootweg 3 years ago
parent 68c9001270
commit 7344b45be0

@ -1,3 +1,4 @@
{
"extends": "@joepie91/eslint-config"
"extends": "@joepie91/eslint-config",
"ignorePatterns": "test/**"
}

@ -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) + '"';

@ -440,7 +440,7 @@ function parseFetch(text, literals, seqno) {
val = parseBodyStructure(val);
else if (m = RE_BODYINLINEKEY.exec(list[i])) {
// a body was sent as a non-literal
val = new Buffer(''+val);
val = Buffer.from(''+val);
body = new ReadableStream();
body._readableState.sync = false;
body._read = EMPTY_READCB;
@ -764,7 +764,7 @@ function decodeBytes(buf, encoding, offset, mlen, pendoffset, state, nextBuf) {
if (state.encoding === encoding && state.consecutive) {
// concatenate buffer + current bytes in hopes of finally having
// something that's decodable
var newbuf = new Buffer(state.buffer.length + buf.length);
var newbuf = Buffer.alloc(state.buffer.length + buf.length);
state.buffer.copy(newbuf, 0);
buf.copy(newbuf, state.buffer.length);
buf = newbuf;
@ -795,7 +795,7 @@ function decodeBytes(buf, encoding, offset, mlen, pendoffset, state, nextBuf) {
// try to decode a lookahead buffer (current buffer + next buffer)
// and see if it starts with the decoded value of the current buffer.
// if not, the current buffer is partial
var lookahead, lookaheadBuf = new Buffer(buf.length + nextBuf.length);
var lookahead, lookaheadBuf = Buffer.alloc(buf.length + nextBuf.length);
buf.copy(lookaheadBuf);
nextBuf.copy(lookaheadBuf, buf.length);
try {
@ -924,17 +924,17 @@ function decodeWords(str, state) {
state.consecutive = m.consecutive;
if (m.encoding === 'q') {
// q-encoding, similar to quoted-printable
bytes = new Buffer(m.chunk.replace(RE_QENC, qEncReplacer), 'binary');
bytes = Buffer.from(m.chunk.replace(RE_QENC, qEncReplacer), 'binary');
next = undefined;
} else {
// base64
bytes = m.buf || new Buffer(m.chunk, 'base64');
bytes = m.buf || Buffer.from(m.chunk, 'base64');
next = replaces[i + 1];
if (next && next.consecutive && next.encoding === m.encoding
&& next.charset === m.charset) {
// we use the next base64 chunk, if any, to determine the integrity
// of the current chunk
next.buf = new Buffer(next.chunk, 'base64');
next.buf = Buffer.from(next.chunk, 'base64');
}
}
decodeBytes(bytes, m.charset, m.index, m.length, m.pendoffset, state,

@ -0,0 +1,55 @@
"use strict";
const utf7 = require('utf7').imap;
const RE_BACKSLASH = /\\/g;
const RE_DBLQUOTE = /"/g;
function escape(str) {
return str.replace(RE_BACKSLASH, '\\\\').replace(RE_DBLQUOTE, '\\"');
}
module.exports = {
command: function (strings, ... interpolations) {
return {
toCommandString: function () {
let processedInterpolations = interpolations.map((interpolation) => {
let isObject = interpolation != null && typeof interpolation === "object";
if (isObject && interpolation._isRaw) {
return interpolation.string;
} else if (isObject && interpolation._is7Bit) {
return escape(interpolation.string);
} else if (typeof interpolation === "string") {
return escape(utf7.encode(interpolation));
} else {
throw new Error(`Invalid input into command string: ${interpolation}`);
}
});
let combined = "";
strings.slice(0, -1).forEach((string, i) => {
combined += string;
combined += processedInterpolations[i];
});
combined += strings[strings.length - 1];
return combined;
}
};
},
unsafeRaw: function (string) {
return {
_isRaw: true,
string: string
};
},
already7Bit: function (string) {
return {
_is7Bit: true,
string: string
};
}
};

@ -0,0 +1,112 @@
"use strict";
const events = require("events");
const syncpipe = require("syncpipe");
const defaultValue = require("default-value");
const splitFilter = require("split-filter");
const mapObject = require("map-obj");
const pickObject = require("./pick-object");
const RE_BODYPART = /^BODY\[/;
const RE_ESCAPE = /\\\\/g;
const FETCH_ATTR_MAP = {
'RFC822.SIZE': 'size',
'BODY': 'struct',
'BODYSTRUCTURE': 'struct',
'ENVELOPE': 'envelope',
'INTERNALDATE': 'date'
};
function mapIncomingAttributeKey(key) {
return defaultValue(FETCH_ATTR_MAP[key], key.toLowerCase());
}
// FIXME: Get rid of the separate-emitter hackery here, and make the task itself an emitter instead, or find some other better way to wire things up
module.exports = function createFetchTaskTracker() {
let tasks = new Map();
return {
get: function (id) {
// TODO: Eventually make this fail hard, after the calling code has been fully refactored
return tasks.get(id);
},
create: function (id, keysToFetch) {
let emitter = new events.EventEmitter();
let task = createFetchTask({ emitter, keysToFetch });
// FIXME: Delete after completion
tasks.set(id, task);
return task;
},
getOrCreate: function (id, keysToFetch) {
if (tasks.has(id)) {
return this.get(id);
} else {
return this.create(id, keysToFetch);
}
}
};
};
function createFetchTask({ emitter, keysToFetch }) {
let attributes = {};
let endHandled = false;
return {
emitter: emitter,
getRemainingKeys: function () {
return keysToFetch;
},
processFetchResponse: function (payload) {
if (keysToFetch.length > 0) {
let caseMappedPayload = mapObject(payload, (key, value) => {
return [ key.toUpperCase(), value ];
});
let [ existingKeys, missingKeys ] = splitFilter(keysToFetch, (key) => caseMappedPayload[key] != null);
let relevantKeys = existingKeys.filter((key) => RE_BODYPART.test(key) === false);
let newAttributes = syncpipe(caseMappedPayload, [
(_) => pickObject(_, relevantKeys),
(_) => mapObject(_, (key, value) => {
return [
/* key */ mapIncomingAttributeKey(key),
/* value */ (key === 'X-GM-LABELS')
// TODO: Why is this special case needed?
? value.map((label) => String(label).replace(RE_ESCAPE, '\\'))
: value
];
})
]);
Object.assign(attributes, newAttributes);
keysToFetch = missingKeys;
let isDone = (missingKeys.length === 0);
if (isDone === true) {
endHandled = true;
// FIXME: Why nextTick?
process.nextTick(function() {
emitter.emit("attributes", attributes);
emitter.emit("end");
});
}
} else {
if (endHandled === false) {
endHandled = true;
// FIXME: Why nextTick?
process.nextTick(function() {
emitter.emit("end");
});
}
}
}
};
};

@ -0,0 +1,32 @@
"use strict";
// TODO: Make separate package
const pTry = require("p-try");
module.exports = function pInterval(interval, handler) {
let timer = setTimeout(doHandler, interval);
function doHandler() {
let startTime = Date.now();
pTry(() => {
return handler();
}).then(() => {
let timeElapsed = Date.now() - startTime();
let timeout = (timeElapsed < interval)
? interval - timeElapsed
: 1; // Be consistently async!
timer = setTimeout(doHandler, timeout);
});
}
return function cancelInterval() {
if (timer != null) {
clearTimeout(timer);
timer = null;
}
};
};

@ -0,0 +1,13 @@
"use strict";
module.exports = function pickObject(object, keys) {
let newObject = {};
for (let key of keys) {
if (key in object) {
newObject[key] = object[key];
}
}
return newObject;
};

@ -5,7 +5,14 @@
"description": "An IMAP module for node.js that makes communicating with IMAP servers easy",
"main": "./lib/Connection",
"dependencies": {
"bluebird": "^3.7.2",
"default-value": "^1.0.0",
"map-obj": "^4.2.1",
"p-defer": "^3.0.0",
"p-try": "^2.2.0",
"readable-stream": "1.1.x",
"split-filter": "^1.1.3",
"syncpipe": "^1.0.0",
"utf7": ">=1.0.2"
},
"scripts": {

@ -1,4 +1,5 @@
require('fs').readdirSync(__dirname).forEach(function(f) {
// if (f === "test-connection-fetch-dup.js")
if (f.substr(0, 5).toLowerCase() === 'test-')
require('./' + f);
});

@ -105,6 +105,11 @@ argparse@^1.0.7:
dependencies:
sprintf-js "~1.0.2"
assure-array@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/assure-array/-/assure-array-1.0.0.tgz#4f4ad16a87659d6200a4fb7103462033d216ec1f"
integrity sha1-T0rRaodlnWIApPtxA0YgM9IW7B8=
astral-regex@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
@ -115,6 +120,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
bluebird@^3.7.2:
version "3.7.2"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@ -200,6 +210,13 @@ deep-is@^0.1.3:
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
default-value@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/default-value/-/default-value-1.0.0.tgz#8c6f52a5a1193fe78fdc9f86eb71d16c9757c83a"
integrity sha1-jG9SpaEZP+eP3J+G63HRbJdXyDo=
dependencies:
es6-promise-try "0.0.1"
doctrine@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
@ -219,6 +236,11 @@ enquirer@^2.3.5:
dependencies:
ansi-colors "^4.1.1"
es6-promise-try@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/es6-promise-try/-/es6-promise-try-0.0.1.tgz#10f140dad27459cef949973e5d21a087f7274b20"
integrity sha1-EPFA2tJ0Wc75SZc+XSGgh/cnSyA=
escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
@ -522,6 +544,11 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
map-obj@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.2.1.tgz#e4ea399dbc979ae735c83c863dd31bdf364277b7"
integrity sha512-+WA2/1sPmDj1dlvvJmB5G6JKfY9dpn7EVBUL06+y6PoljPkh+6V1QihwxNkbcGxCRjt2b0F9K0taiCuo7MbdFQ==
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
@ -558,6 +585,16 @@ optionator@^0.9.1:
type-check "^0.4.0"
word-wrap "^1.2.3"
p-defer@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-3.0.0.tgz#d1dceb4ee9b2b604b1d94ffec83760175d4e6f83"
integrity sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==
p-try@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
@ -655,6 +692,11 @@ slice-ansi@^4.0.0:
astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0"
split-filter@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/split-filter/-/split-filter-1.1.3.tgz#c68cc598783d88f60d16e7b452dacfe95ba60539"
integrity sha512-2xXwhWeJUFrYE8CL+qoy9mCohu5/E+uglvpqL1FVXz1XbvTwivafVC6oTDeg/9ksOAxg6DvyCF44Dvf5crFU0w==
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
@ -700,6 +742,13 @@ supports-color@^7.1.0:
dependencies:
has-flag "^4.0.0"
syncpipe@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/syncpipe/-/syncpipe-1.0.0.tgz#170340f813150bc8fcb8878b1b9c71ea0ccd3727"
integrity sha512-cdiAFTnFJRvUaNPDc2n9CqoFvtIL3+JUMJZrC3kA3FzpugHOqu0TvkgNwmnxPZ5/WjAzMcfMS3xm+AO7rg/j/w==
dependencies:
assure-array "^1.0.0"
table@^6.0.4:
version "6.0.7"
resolved "https://registry.yarnpkg.com/table/-/table-6.0.7.tgz#e45897ffbcc1bcf9e8a87bf420f2c9e5a7a52a34"

Loading…
Cancel
Save