Compare commits

...

3 Commits

Author SHA1 Message Date
Sven Slootweg 91714b7dda WIP 3 years ago
Sven Slootweg c345fdd74d WIP 3 years ago
Sven Slootweg 3357108b52 WIP 3 years ago

@ -2,10 +2,21 @@
const Promise = require("bluebird");
const defaultValue = require("default-value");
const asExpression = require("as-expression");
const matchValue = require("match-value");
const unreachable = require("@joepie91/unreachable");
const { validateArguments } = require("@validatem/core");
const required = require("@validatem/required");
const anyProperty = require("@validatem/any-property");
const isString = require("@validatem/is-string");
const isFunction = require("@validatem/is-function");
const { command, unsafeRaw, already7Bit } = require("./util/command");
const pInterval = require("./util/p-interval");
const createFetchTaskTracker = require("./util/fetch-task");
const createBoxTreeBuilder = require("./util/box-tree-builder");
const ensureArray = require("./util/ensure-array");
var tls = require('tls'),
Socket = require('net').Socket,
@ -1234,117 +1245,360 @@ Object.defineProperty(Connection.prototype, 'seq', { get: function() {
};
}});
// type: type,
// num: num,
// textCode: textCode,
// text: val
function parseTypes(typeString) {
if (typeString === NoRequest || typeString === AnyRequest) {
return [ typeString ];
} else {
return typeString
.split(/\s*,\s*/)
.map((type) => type.toUpperCase());
}
}
Connection.prototype._resUntagged = function({ type, num, textCode, text: payload }) {
var i, len, box, destinationKey;
function createCommandHandlers(_rules) {
let [ rules ] = validateArguments(arguments, {
rules: anyProperty({
key: [ required ],
value: [ required, {
untagged: anyProperty({
key: [ isString ],
value: [ required, isFunction ]
}),
tagged: [ isFunction ]
}]
})
});
let untaggedHandlers = new Map();
let taggedHandlers = new Map();
if (type === 'bye') {
this._sock.end();
} 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(payload);
} else if (type === 'search') {
if (payload.results !== undefined) {
// CONDSTORE-modified search results
this._curReq.cbargs.push(payload.results);
this._curReq.cbargs.push(payload.modseq);
function setUntaggedHandler(requestType, responseType, handler) {
if (!untaggedHandlers.has(requestType)) {
untaggedHandlers.set(requestType, new Map());
}
let handlers = untaggedHandlers.get(requestType);
if (!handlers.has(responseType)) {
handlers.set(responseType, handler);
} else {
throw new Error(`Duplicate handler definition for untagged type '${requestType}' -> '${responseType}'`);
}
}
function setTaggedHandler(requestType, handler) {
if (!taggedHandlers.has(requestType)) {
taggedHandlers.set(requestType, handler);
} else {
this._curReq.cbargs.push(payload);
throw new Error(`Duplicate handler definition for tagged type`);
}
} else if (type === 'quota') {
var cbargs = this._curReq.cbargs;
if (!cbargs.length) {
cbargs.push([]);
}
function getRequestType(request) {
return (request != null)
? request.type.toUpperCase()
: NoRequest;
}
function getTaggedHandler(request) {
return taggedHandlers.get(getRequestType(request));
}
function getUntaggedHandler(request, responseType) {
let typeHandlers = untaggedHandlers.get(getRequestType(request));
let anyRequestHandlers = untaggedHandlers.get(AnyRequest);
// console.log({
// requestType: getRequestType(request),
// responseType: responseType,
// anyHandler: anyRequestHandlers.get(responseType)
// });
if (typeHandlers != null && typeHandlers.has(responseType)) {
return typeHandlers.get(responseType);
} else if (anyRequestHandlers != null && anyRequestHandlers.has(responseType)) {
return anyRequestHandlers.get(responseType);
}
}
cbargs[0].push(payload);
} else if (type === 'recent') {
if (!this._box && RE_OPENBOX.test(this._curReq.type)) {
this._createCurrentBox();
for (let [ typeString, options ] of allEntries(rules)) {
for (let type of parseTypes(typeString)) {
if (options.untagged != null) {
for (let [ responseTypeString, handler ] of allEntries(options.untagged)) {
for (let responseType of parseTypes(responseTypeString)) {
setUntaggedHandler(type, responseType, handler);
}
}
}
if (options.tagged != null) {
setTaggedHandler(type, options.tagged);
}
}
}
// REFACTOR: Eventually remove `.call(this` hackery
// REFACTOR: Remove all the toUpperCase stuff and normalize this in the core instead, so that we can just *assume* it to be upper-case here
return {
canHandleUntagged: function (request, data) {
return (getUntaggedHandler(request, data.type.toUpperCase()) != null);
},
canHandleTagged: function (request, _data) {
return (getTaggedHandler(request) != null);
},
handleUntagged: function (request, data) {
let handler = getUntaggedHandler(request, data.type.toUpperCase());
if (this._box) {
this._box.messages.new = num;
return handler.call(this, request, data);
},
handleTagged: function (request, data) {
let handler = getTaggedHandler(request);
return handler.call(this, request, data);
}
} else if (type === 'flags') {
if (!this._box && RE_OPENBOX.test(this._curReq.type)) {
this._createCurrentBox();
};
}
function allEntries(object) {
// The equivalent of Object.entries but also including Symbol keys
return Reflect.ownKeys(object).map((key) => {
return [ key, object[key] ];
});
}
let NoRequest = Symbol("NoRequest");
let AnyRequest = Symbol("AnyRequest");
// FIXME: Strip "UID" prefix from command names before matching, as these only affect the output format, but not the type of response
// Exception: EXPUNGE is allowed during UID commands, but not during their non-UID equivalents: https://datatracker.ietf.org/doc/html/rfc3501#page-73
let commandHandlers = createCommandHandlers({
[AnyRequest]: {
untagged: {
"BYE": function (_request, _) {
this._sock.end();
},
"CAPABILITY": function (_request, { payload }) {
this._caps = payload.map((v) => v.toUpperCase());
},
"NAMESPACE": function (_request, { payload }) {
this.namespaces = payload;
},
"PREAUTH": function (_request, _) {
this.state = 'authenticated';
},
"EXPUNGE": function (_request, { sequenceNumber }) {
if (this._box != null) {
if (this._box.messages.total > 0) {
this._box.messages.total -= 1;
}
this.emit('expunge', sequenceNumber);
}
},
}
},
"STATUS": {
untagged: {
"STATUS": function (request, { payload }) {
// REFACTOR: Improve this?
let attrs = defaultValue(payload.attrs, {});
let box = {
name: payload.name,
uidnext: defaultValue(attrs.uidnext, 0),
uidvalidity: defaultValue(attrs.uidvalidity, 0),
messages: {
total: defaultValue(attrs.messages, 0),
new: defaultValue(attrs.recent, 0),
unseen: defaultValue(attrs.unseen, 0)
},
// CONDSTORE
highestmodseq: (attrs.highestmodseq != null)
? String(attrs.highestmodseq)
: undefined
};
if (this._box) {
this._box.flags = payload;
request.legacyArgs.push(box);
Object.assign(request.responseData, box);
}
}
} else if (type === 'bad' || type === 'no') {
if (this.state === 'connected' && !this._curReq) {
clearTimeout(this._connectionTimeout);
clearTimeout(this._authenticationTimeout);
var err = new Error('Received negative welcome: ' + payload);
err.source = 'protocol';
this.emit('error', err);
this._sock.end();
},
"LIST, XLIST, LSUB": {
untagged: {
"LIST, XLIST, LSUB": function (request, { payload }) {
if (request.delimiter === undefined) {
request.delimiter = payload.delimiter;
} else {
if (request.boxBuilder == null) {
request.boxBuilder = createBoxTreeBuilder();
}
request.boxBuilder.add(payload);
}
}
},
tagged: function (request, _) {
// FIXME: Check request types for correctness
let boxTree = (request.boxBuilder != null)
? request.boxBuilder.done()
: {}; // No response items were received
request.legacyArgs.push(boxTree);
request.responseData.boxTree = boxTree;
}
} else if (type === 'exists') {
if (!this._box && RE_OPENBOX.test(this._curReq.type)) {
this._createCurrentBox();
},
"ID": {
// https://datatracker.ietf.org/doc/html/rfc2971
// Used for communicating server/client name, version, etc.
untagged: {
"ID": function (request, { payload }) {
request.responseData.serverVersion = payload;
request.legacyArgs.push(payload);
}
}
if (this._box) {
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;
this.emit('mail', this._box.messages.new);
},
"ESEARCH": {
// https://datatracker.ietf.org/doc/html/rfc4731 / https://datatracker.ietf.org/doc/html/rfc7377
untagged: {
"ESEARCH": function (request, { payload }) {
Object.assign(request.responseData, payload); // Protocol-defined attributes. TODO: Improve the key names for this? Or is there extensibility?
request.legacyArgs.push(payload);
}
}
},
"SORT": {
// https://datatracker.ietf.org/doc/html/rfc5256
untagged: {
"SORT": function (request, { payload }) {
request.responseData.UIDs = payload;
request.legacyArgs.push(payload);
}
}
},
"THREAD": {
// https://datatracker.ietf.org/doc/html/rfc5256
untagged: {
"THREAD": function (request, { payload }) {
request.responseData.threads = payload; // FIXME: Work out the exact format
request.legacyArgs.push(payload);
}
}
} else if (type === 'expunge') {
if (this._box) {
if (this._box.messages.total > 0) {
--this._box.messages.total;
},
"SEARCH": {
untagged: {
"SEARCH": function (request, { payload }) {
if (payload.results !== undefined) {
// CONDSTORE-modified search results
request.legacyArgs.push(payload.results);
request.legacyArgs.push(payload.modseq);
} else {
request.legacyArgs.push(payload);
}
}
}
},
"GETQUOTA, GETQUOTAROOT": {
// https://datatracker.ietf.org/doc/html/rfc2087
untagged: {
"QUOTA": function (request, { payload }) {
ensureArray(request.responseData, "quota");
request.responseData.quota.push(payload);
ensureArray(request.legacyArgs, 0);
request.legacyArgs[0].push(payload);
},
"QUOTAROOT": function (_request, _) {
throw new Error(`Not implemented`);
}
}
},
"SELECT, EXAMINE": {
untagged: {
"RECENT": function (_request, { sequenceNumber }) {
this._ensureBox();
// FIXME: This conditional is always true?
if (this._box) {
this._box.messages.new = sequenceNumber;
}
},
"FLAGS": function (_request, { payload }) {
this._ensureBox();
// FIXME: This conditional is always true?
if (this._box) {
this._box.flags = payload;
}
},
"EXISTS": function (_request, { sequenceNumber }) {
this._ensureBox();
// FIXME: This conditional is always true?
if (this._box) {
var prev = this._box.messages.total, now = sequenceNumber;
this._box.messages.total = now;
if (now > prev && this.state === 'authenticated') {
this._box.messages.new = now - prev;
this.emit('mail', this._box.messages.new);
}
}
},
}
},
});
this.emit('expunge', num);
Connection.prototype._ensureBox = function () {
if (!this._box) {
if (RE_OPENBOX.test(this._curReq.type)) {
this._resetCurrentBox();
} else {
throw new Error(`Received a box-related response while not processing a box-related request`);
}
}
};
// type: type,
// num: num, -- sequence number of the affected nessage, used for FETCH and EXPUNGE only (message-data) and maybe RECENT and EXISTS (mailbox-data)?
// textCode: textCode,
// text: val
// NOTE: responseData is meant to contain machine-readable data, payload is meant to contain human-readable data, but in practice payload is also often machine-parsed
Connection.prototype._resUntagged = function({ type, num: sequenceNumber, textCode: responseData, text: payload }) {
// console.log("resUntagged", { type, num: sequenceNumber, payload, textCode: responseData });
var i, len, box, destinationKey;
let response = { type, sequenceNumber, payload };
if (commandHandlers.canHandleUntagged(this._curReq, response)) {
// FIXME: Include other fields
commandHandlers.handleUntagged.call(this, this._curReq, response);
} else if (type === 'ok') {
if (this.state === 'connected' && !this._curReq) {
this._login();
} else if (typeof textCode === 'string' && textCode.toUpperCase() === 'ALERT') {
} else if (typeof responseData === 'string' && responseData.toUpperCase() === 'ALERT') {
this.emit('alert', payload);
}
else if (this._curReq
&& textCode
&& (RE_OPENBOX.test(this._curReq.type))) {
} else if (this._curReq && responseData && (RE_OPENBOX.test(this._curReq.type))) {
// we're opening a mailbox
if (!this._box) {
this._createCurrentBox();
this._resetCurrentBox();
}
if (textCode.key) {
destinationKey = textCode.key.toUpperCase();
} else {
destinationKey = textCode;
}
let destinationKey = (responseData.key != null)
? responseData.key.toUpperCase()
: responseData;
if (destinationKey === 'UIDVALIDITY') {
this._box.uidvalidity = textCode.val;
this._box.uidvalidity = responseData.val;
} else if (destinationKey === 'UIDNEXT') {
this._box.uidnext = textCode.val;
this._box.uidnext = responseData.val;
} else if (destinationKey === 'HIGHESTMODSEQ') {
this._box.highestmodseq = ''+textCode.val;
this._box.highestmodseq = ''+responseData.val;
} else if (destinationKey === 'PERMANENTFLAGS') {
var idx, permFlags, keywords;
this._box.permFlags = permFlags = textCode.val;
this._box.permFlags = permFlags = responseData.val;
if ((idx = this._box.permFlags.indexOf('\\*')) > -1) {
this._box.newKeywords = true;
@ -1356,138 +1610,79 @@ Connection.prototype._resUntagged = function({ type, num, textCode, text: payloa
for (i = 0, len = keywords.length; i < len; ++i) {
permFlags.splice(permFlags.indexOf(keywords[i]), 1);
}
} else if (destinationKey === 'UIDNOTSTICKY')
} else if (destinationKey === 'UIDNOTSTICKY') {
this._box.persistentUIDs = false;
else if (destinationKey === 'NOMODSEQ')
} else if (destinationKey === 'NOMODSEQ') {
this._box.nomodseq = true;
} 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 = payload.delimiter;
} else {
if (this._curReq.cbargs.length === 0) {
this._curReq.cbargs.push({});
}
box = {
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) {
box.special_use_attrib = SPECIAL_USE_ATTRIBUTES[i];
}
}
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]]) {
curChildren[path[i]] = {};
}
if (!curChildren[path[i]].children) {
curChildren[path[i]].children = {};
}
parent = curChildren[path[i]];
curChildren = curChildren[path[i]].children;
}
box.parent = parent;
}
if (curChildren[name]) {
box.children = curChildren[name].children;
}
curChildren[name] = box;
} else if (typeof responseData === 'string' && responseData.toUpperCase() === 'UIDVALIDITY') {
this.emit('uidvalidity', payload);
}
} else if (type === 'bad' || type === 'no') {
if (this.state === 'connected' && !this._curReq) {
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 === 'status') {
let attrs = defaultValue(payload.attrs, {});
box = {
name: payload.name,
uidnext: defaultValue(attrs.uidnext, 0),
uidvalidity: defaultValue(attrs.uidvalidity, 0),
messages: {
total: defaultValue(attrs.messages, 0),
new: defaultValue(attrs.recent, 0),
unseen: defaultValue(attrs.unseen, 0)
},
// CONDSTORE
highestmodseq: (attrs.highestmodseq != null)
? String(attrs.highestmodseq)
: undefined
};
// 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
let task = this._curReq.fetchCache.get(num);
let task = this._curReq.fetchCache.get(sequenceNumber);
// 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);
task = this._curReq.fetchCache.create(sequenceNumber, this._curReq.fetching.slice());
this._curReq.bodyEmitter.emit('message', task.emitter, sequenceNumber);
}
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', num, payload);
this.emit('update', sequenceNumber, payload);
}
}
};
Connection.prototype._resTagged = function(info) {
var req = this._curReq;
Connection.prototype._resTagged = function({ type, tagnum, text: payload, textCode: responseCode }) {
// console.log("resTagged", { type, tagnum, payload, textCode: responseCode });
// REFACTOR: textCode: either just the key, or a {key, val} object
var request = this._curReq;
if (req != null) {
if (request != null) {
var err;
this._curReq = undefined;
if (info.type === 'no' || info.type === 'bad') {
// TODO: Can info.text be an empty string?
let errorText = defaultValue(info.text, req.oauthError);
if (type === 'no' || type === 'bad') {
// TODO: Can text be an empty string?
let errorText = defaultValue(payload, request.oauthError);
err = Object.assign(new Error(errorText), {
type: info.type,
text: info.textCode,
type: type,
text: responseCode,
source: "protocol"
});
} else if (this._box != null) {
if (req.type === 'EXAMINE' || req.type === 'SELECT') {
if (request.type === 'EXAMINE' || request.type === 'SELECT') {
this._box.readOnly = (
typeof info.textCode === 'string'
&& info.textCode.toUpperCase() === 'READ-ONLY'
typeof responseCode === 'string'
&& responseCode.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) {
req.cbargs.push([]);
if (RE_UIDCMD_HASRESULTS.test(request.fullcmd) && request.cbargs.length === 0) {
request.cbargs.push([]);
}
}
if (req.bodyEmitter) {
var bodyEmitter = req.bodyEmitter;
if (request.bodyEmitter != null) {
var bodyEmitter = request.bodyEmitter;
if (err) {
bodyEmitter.emit('error', err);
@ -1497,19 +1692,50 @@ Connection.prototype._resTagged = function(info) {
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]
req.cbargs.push(info.textCode.val[1]);
} else if (key === 'COPYUID') { // [uidvalidity, sourceUIDs, destUIDs]
req.cbargs.push(info.textCode.val[2]);
let extraArguments = asExpression(() => {
if (responseCode != null && responseCode.key != null) {
return matchValue(responseCode.key.toUpperCase(), {
// [uidvalidity, newUID]
APPENDUID: [ responseCode.val[1] ],
// [uidvalidity, sourceUIDs, destUIDs]
COPYUID: [ responseCode.val[2] ],
_: []
});
} else {
return [];
}
});
if (responseCode != null && responseCode.key != null) {
// FIXME: This eventually should replace the extraArguments array stuff
matchValue(responseCode.key.toUpperCase(), {
APPENDUID: () => {
request.responseData.newUID = responseCode.val[1];
},
COPYUID: () => {
// FIXME: Parsing? Looks like it will be multiple items
request.responseData.destinationUIDs = responseCode.val[2];
}
});
}
if (req.cb != null) {
req.cb.apply(this, req.cbargs);
let response = { type, payload };
if (commandHandlers.canHandleTagged(request, response)) {
// FIXME: Add other fields with a sensible name
commandHandlers.handleTagged.call(this, request, response);
}
// console.dir({ done: request.cbargs }, { depth: null, colors: true });
if (request.cb2 != null) {
request.cb.apply(this, request.responseData);
} else if (request.cb != null) {
request.cb.apply(this, [
err,
... request.cbargs,
... extraArguments
]);
}
}
@ -1528,7 +1754,7 @@ Connection.prototype._resTagged = function(info) {
}
};
Connection.prototype._createCurrentBox = function() {
Connection.prototype._resetCurrentBox = function() {
this._box = {
name: '',
flags: [],
@ -1744,25 +1970,31 @@ Connection.prototype._sockWriteAppendData = function(appendData)
this._sock.write(CRLF);
};
Connection.prototype._enqueue = function(fullcmd, promote, cb) {
Connection.prototype._enqueue = function(fullcmd, promote, cb, newAPI) {
// TODO: Remove variability
if (typeof promote === 'function') {
cb = promote;
promote = false;
}
var info = {
type: fullcmd.match(RE_CMD)[1],
fullcmd: fullcmd,
cb: cb,
cbargs: []
},
self = this;
var request = {
type: fullcmd.match(RE_CMD)[1],
fullcmd: fullcmd,
cb: (newAPI) ? null : cb,
cb2: (newAPI) ? cb : null,
cbargs: [],
responseData: {}
};
// Alias
request.legacyArgs = request.cbargs;
var self = this;
if (promote) {
this._queue.unshift(info);
this._queue.unshift(request);
} else {
this._queue.push(info);
this._queue.push(request);
}
if (!this._curReq
@ -1796,7 +2028,7 @@ Connection.prototype._enqueue2 = function (command, options = {}) {
return this._enqueueAsync(string, insertInFront);
} else {
// TODO: Use `unreachable`
throw new Error(`Must use a command template string`);
throw unreachable(`Must use a command template string`);
}
};

@ -1,3 +1,5 @@
/* eslint-disable */ /* FIXME */
var EventEmitter = require('events').EventEmitter,
ReadableStream = require('stream').Readable
|| require('readable-stream').Readable,

@ -0,0 +1,48 @@
"use strict";
const lastItem = require("last-item");
const createNamedTreeBuilder = require("./named-tree-builder");
const specialUseAttributes = new Set([
'\\All',
'\\Archive',
'\\Drafts',
'\\Flagged',
'\\Important',
'\\Junk',
'\\Sent',
'\\Trash'
]);
// TODO: Eventually make this progressively updateable so that the results of multiple LIST commands can be combined over time?
module.exports = function createBoxTreeBuilder() {
let treeBuilder = createNamedTreeBuilder({
childrenKey: "children",
parentKey: "parent",
treatRootAsParent: false
});
return {
add: function (item) {
let { flags, delimiter } = item;
let path = (delimiter != null)
? item.name.split(delimiter)
: [ item.name ];
treeBuilder.add(path, {
name: lastItem(path),
path: path,
attributes: flags,
delimiter: delimiter,
specialUseAttribute: flags.find((attribute) => specialUseAttributes.has(attribute)),
children: null,
parent: null
});
},
done: function () {
return treeBuilder.done().children;
}
};
};

@ -0,0 +1,7 @@
"use strict";
module.exports = function ensureArrau(object, property) {
if (object[property] == null) {
object[property] = [];
}
};

@ -0,0 +1,7 @@
"use strict";
module.exports = function ensureObject(object, property) {
if (object[property] == null) {
object[property] = {};
}
};

@ -0,0 +1,76 @@
"use strict";
const defaultValue = require("default-value");
const asExpression = require("as-expression");
const ensureObject = require("./ensure-object");
// FIXME: Move to stand-alone package, clearly document that it is not order-sensitive
module.exports = function createNamedTreeBuilder(options = {}) {
let parentKey = options.parentKey;
let childrenKey = options.childrenKey;
let treatRootAsParent = defaultValue(options.treatRootAsParent, true);
let root = {};
let done = false;
return {
add: function (path, item) {
if (done === true) {
throw new Error(`done() was called on the builder; no further modifications are possible`);
} else {
let lastItem = root;
for (let i = 0; i < path.length; i++) {
let segment = path[i];
// eslint-disable-next-line no-loop-func
let childrenContainer = asExpression(() => {
if (childrenKey != null) {
ensureObject(lastItem, childrenKey);
return lastItem[childrenKey];
} else {
return lastItem;
}
});
ensureObject(childrenContainer, segment);
let child = childrenContainer[segment];
let setParent = (
parentKey != null
&& child[parentKey] == null
&& (treatRootAsParent || i >= 1)
);
if (setParent) {
child[parentKey] = lastItem;
}
if (i === path.length - 1) {
// Last segment, this is where we want to put the item data
child = childrenContainer[segment] = {
... item,
... child
};
}
lastItem = child;
}
}
},
done: function () {
done = true;
return root;
}
};
};
// let builder = module.exports({ childrenKey: "children", parentKey: "parent" });
// builder.add([], { description: "root" });
// builder.add(["a"], { description: "root -> a" });
// builder.add(["b", "c"], { description: "root -> b -> c" });
// builder.add(["b"], { description: "root -> b" });
// builder.add(["a", "c"], { description: "root -> a -> c" });
// builder.add(["a", "c", "d"], { description: "root -> a -> c -> d" });
// console.dir(builder.done(), { depth: null });

@ -5,9 +5,18 @@
"description": "An IMAP module for node.js that makes communicating with IMAP servers easy",
"main": "./lib/Connection",
"dependencies": {
"@joepie91/unreachable": "^1.0.0",
"@validatem/any-property": "^0.1.3",
"@validatem/core": "^0.3.15",
"@validatem/is-function": "^0.1.0",
"@validatem/is-string": "^1.0.0",
"@validatem/required": "^0.1.1",
"as-expression": "^1.0.0",
"bluebird": "^3.7.2",
"default-value": "^1.0.0",
"last-item": "^1.0.0",
"map-obj": "^4.2.1",
"match-value": "^1.1.0",
"p-defer": "^3.0.0",
"p-try": "^2.2.0",
"readable-stream": "1.1.x",

@ -0,0 +1,138 @@
"use strict";
// FIXME: Publish as stand-alone `reference-tree` package, clearly document that it is not order-sensitive
function isObject(value) {
return (value != null && typeof value === "object");
}
function isLabelReference(value) {
return (isObject(value) && value.__referenceTree_reference != null);
}
function isParentReference(value) {
return (isObject(value) && value.__referenceTree_parent != null);
}
function hasLabel(value) {
return (isObject(value) && value.__referenceTree_label != null);
}
module.exports = {
build: function buildReferenceTree(object) {
// NOTE: Mutates input object!
let stack = [];
let labelledItems = new Map();
let seen = new Set();
let onFirstPass = true;
let doSecondPass = false;
function handleItem(container, key) {
let value = container[key];
if (!seen.has(value)) {
seen.add(value);
if (hasLabel(value)) {
let label = value.__referenceTree_label;
delete value.__referenceTree_label;
labelledItems.set(label, value);
}
if (isLabelReference(value)) {
let label = value.__referenceTree_reference;
if (labelledItems.has(label)) {
container[key] = labelledItems.get(label);
} else if (onFirstPass) {
// We haven't seen the referenced item yet; this typically happens when either the reference is defined before the label, or when there are cyclical references
doSecondPass = true;
} else {
throw new Error(`Encountered a label '${label}' that doesn't exist anywhere in the tree`);
}
} else if (isParentReference(value)) {
let levels = value.__referenceTree_parent;
if (levels > stack.length) {
throw new Error(`Tried to access a parent ${levels} steps away, but there are only ${stack.length} parents`);
} else {
let parentIndex = stack.length - levels;
container[key] = stack[parentIndex];
}
} else {
stack.push(container);
processValue(value);
stack.pop();
}
}
}
function processObject(object) {
for (let key of Object.keys(object)) {
handleItem(object, key);
}
}
function processArray(array) {
for (let i = 0; i < array.length; i++) {
handleItem(array, i);
}
}
function processValue(value) {
if (Array.isArray(value)) {
return processArray(value);
} else if (isObject(value)) {
return processObject(value);
}
}
processValue(object);
if (doSecondPass === true) {
onFirstPass = false;
seen = new Set();
processValue(object);
}
return object;
},
Parent: function (levels = 1) {
return {
__referenceTree_parent: levels
};
},
Label: function (label, object) {
object.__referenceTree_label = label;
return object;
},
Reference: function (label) {
return {
__referenceTree_reference: label
};
}
};
// let { build, Label, Reference, Parent } = module.exports;
// let result = build({
// foo: "bar",
// a: Label("A", {
// letter: "A",
// foo: Reference("B")
// }),
// b: Label("B", {
// letter: "B",
// foo: Reference("A"),
// children: [{
// qux: "quz"
// }, {
// qux: Parent(2),
// quz: Reference("B")
// }]
// }),
// });
// console.dir(result, { depth: 4 });
// console.log(result.b.children[1].qux === result.b.children[1].quz);

@ -0,0 +1,313 @@
"use strict";
const tap = require("tap");
const { build, Parent } = require("../reference-tree");
const createBoxTreeBuilder = require("../../lib/util/box-tree-builder");
let input = [{
flags: ['\\HasNoChildren'],
delimiter: '/',
name: 'confirmed-spam'
}, {
flags: ['\\HasNoChildren', '\\Trash'],
delimiter: '/',
name: 'Trash'
}, {
flags: ['\\HasNoChildren'],
delimiter: '/',
name: 'SpamLikely'
}, {
flags: ['\\HasNoChildren'],
delimiter: '/',
name: 'Spam'
}, {
flags: ['\\HasNoChildren'],
delimiter: '/',
name: 'Sent Items'
}, {
flags: ['\\HasNoChildren'],
delimiter: '/',
name: 'Archive'
}, {
flags: ['\\HasNoChildren', '\\Drafts'],
delimiter: '/',
name: 'Drafts'
}, {
flags: ['\\HasNoChildren'],
delimiter: '/',
name: 'Notes'
}, {
flags: ['\\HasNoChildren'],
delimiter: '/',
name: 'TeamViewer'
}, {
flags: ['\\HasNoChildren', '\\Sent'],
delimiter: '/',
name: 'Sent Messages'
}, {
flags: ['\\HasNoChildren'],
delimiter: '/',
name: 'confirmed-ham'
}, {
flags: ['\\Noselect', '\\HasChildren'],
delimiter: '/',
name: 'Public'
}, {
flags: ['\\HasNoChildren'],
delimiter: '/',
name: 'Public/office3'
}, {
flags: ['\\HasNoChildren'],
delimiter: '/',
name: 'Public/office4'
}, {
flags: ['\\HasNoChildren'],
delimiter: '/',
name: 'Public/support'
}, {
flags: ['\\HasNoChildren'],
delimiter: '/',
name: 'Public/root'
}, {
flags: ['\\HasNoChildren'],
delimiter: '/',
name: 'Public/updates'
}, {
flags: ['\\HasNoChildren'],
delimiter: '/',
name: 'Public/postmaster'
}, {
flags: ['\\Noselect', '\\HasChildren'],
delimiter: '/',
name: 'Shared'
}, {
flags: ['\\Noselect', '\\HasChildren'],
delimiter: '/',
name: 'Shared/d.marteva'
}, {
flags: ['\\HasNoChildren'],
delimiter: '/',
name: 'Shared/d.marteva/INBOX'
}, {
flags: ['\\HasNoChildren'],
delimiter: '/',
name: 'INBOX'
}];
let expected = build({
'confirmed-spam': {
name: "confirmed-spam",
path: [ "confirmed-spam" ],
attributes: ['\\HasNoChildren'],
delimiter: '/',
children: null,
parent: null,
specialUseAttribute: undefined
},
Trash: {
name: "Trash",
path: [ "Trash" ],
attributes: ['\\HasNoChildren', '\\Trash'],
delimiter: '/',
children: null,
parent: null,
specialUseAttribute: '\\Trash'
},
SpamLikely: {
name: "SpamLikely",
path: [ "SpamLikely" ],
attributes: ['\\HasNoChildren'],
delimiter: '/',
children: null,
parent: null,
specialUseAttribute: undefined
},
Spam: {
name: "Spam",
path: [ "Spam" ],
attributes: ['\\HasNoChildren'],
delimiter: '/',
children: null,
parent: null,
specialUseAttribute: undefined
},
'Sent Items': {
name: "Sent Items",
path: [ "Sent Items" ],
attributes: ['\\HasNoChildren'],
delimiter: '/',
children: null,
parent: null,
specialUseAttribute: undefined
},
Archive: {
name: "Archive",
path: [ "Archive" ],
attributes: ['\\HasNoChildren'],
delimiter: '/',
children: null,
parent: null,
specialUseAttribute: undefined
},
Drafts: {
name: "Drafts",
path: [ "Drafts" ],
attributes: ['\\HasNoChildren', '\\Drafts'],
delimiter: '/',
children: null,
parent: null,
specialUseAttribute: '\\Drafts'
},
Notes: {
name: "Notes",
path: [ "Notes" ],
attributes: ['\\HasNoChildren'],
delimiter: '/',
children: null,
parent: null,
specialUseAttribute: undefined
},
TeamViewer: {
name: "TeamViewer",
path: [ "TeamViewer" ],
attributes: ['\\HasNoChildren'],
delimiter: '/',
children: null,
parent: null,
specialUseAttribute: undefined
},
'Sent Messages': {
name: "Sent Messages",
path: [ "Sent Messages" ],
attributes: ['\\HasNoChildren', '\\Sent'],
delimiter: '/',
children: null,
parent: null,
specialUseAttribute: '\\Sent'
},
'confirmed-ham': {
name: "confirmed-ham",
path: [ "confirmed-ham" ],
attributes: ['\\HasNoChildren'],
delimiter: '/',
children: null,
parent: null,
specialUseAttribute: undefined
},
Public: {
name: "Public",
path: [ "Public" ],
attributes: ['\\Noselect', '\\HasChildren'],
delimiter: '/',
children: {
office3: {
name: "office3",
path: [ "Public", "office3" ],
attributes: ['\\HasNoChildren'],
delimiter: '/',
children: null,
parent: Parent(2),
specialUseAttribute: undefined
},
office4: {
name: "office4",
path: [ "Public", "office4" ],
attributes: ['\\HasNoChildren'],
delimiter: '/',
children: null,
parent: Parent(2),
specialUseAttribute: undefined
},
support: {
name: "support",
path: [ "Public", "support" ],
attributes: ['\\HasNoChildren'],
delimiter: '/',
children: null,
parent: Parent(2),
specialUseAttribute: undefined
},
root: {
name: "root",
path: [ "Public", "root" ],
attributes: ['\\HasNoChildren'],
delimiter: '/',
children: null,
parent: Parent(2),
specialUseAttribute: undefined
},
updates: {
name: "updates",
path: [ "Public", "updates" ],
attributes: ['\\HasNoChildren'],
delimiter: '/',
children: null,
parent: Parent(2),
specialUseAttribute: undefined
},
postmaster: {
name: "postmaster",
path: [ "Public", "postmaster" ],
attributes: ['\\HasNoChildren'],
delimiter: '/',
children: null,
parent: Parent(2),
specialUseAttribute: undefined
}
},
parent: null,
specialUseAttribute: undefined
},
Shared: {
name: "Shared",
path: [ "Shared" ],
attributes: ['\\Noselect', '\\HasChildren'],
delimiter: '/',
children: {
'd.marteva': {
name: "d.marteva",
path: [ "Shared", "d.marteva" ],
attributes: ['\\Noselect', '\\HasChildren'],
delimiter: '/',
children: {
INBOX: {
name: "INBOX",
path: [ "Shared", "d.marteva", "INBOX" ],
attributes: ['\\HasNoChildren'],
delimiter: '/',
children: null,
parent: Parent(2),
specialUseAttribute: undefined
}
},
parent: Parent(2),
specialUseAttribute: undefined
}
},
parent: null,
specialUseAttribute: undefined
},
INBOX: {
name: "INBOX",
path: [ "INBOX" ],
attributes: ['\\HasNoChildren'],
delimiter: '/',
children: null,
parent: null,
specialUseAttribute: undefined
}
});
tap.test("build-box-tree", (test) => {
let builder = createBoxTreeBuilder();
for (let item of input) {
builder.add(item);
}
test.same(builder.done(), expected);
// Needed to convince node-tap that the test is completed, for some reason...
return Promise.resolve();
});

@ -0,0 +1,111 @@
"use strict";
const Promise = require("bluebird");
const tap = require("tap");
const pEvent = require("p-event");
const IMAP = require("../../lib/Connection");
const lines = require("../lines");
const createMockServer = require("../mock-server");
tap.test("list", (test) => {
return testFetch(test);
});
function testFetch(test) {
let steps = [{
expected: 'A0 CAPABILITY',
response: lines([
'* CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA CHILDREN',
'A0 OK Thats all she wrote!',
]),
}, {
expected: 'A1 LOGIN "foo" "bar"',
response: lines([
'* CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA CHILDREN UIDPLUS MOVE',
'A1 OK authenticated (Success)',
]),
}, {
expected: 'A2 NAMESPACE',
response: lines([
'* NAMESPACE (("" "/")) NIL NIL',
'A2 OK Success',
]),
}, {
expected: 'A3 LIST "" ""',
response: lines([
'* LIST (\\HasNoChildren) "/" confirmed-spam',
'* LIST (\\HasNoChildren \\Trash) "/" Trash',
'* LIST (\\HasNoChildren) "/" SpamLikely',
'* LIST (\\HasNoChildren) "/" Spam',
'* LIST (\\HasNoChildren) "/" "Sent Items"',
'* LIST (\\HasNoChildren) "/" Archive',
'* LIST (\\HasNoChildren \\Drafts) "/" Drafts',
'* LIST (\\HasNoChildren) "/" Notes',
'* LIST (\\HasNoChildren) "/" TeamViewer',
'* LIST (\\HasNoChildren \\Sent) "/" "Sent Messages"',
'* LIST (\\HasNoChildren) "/" confirmed-ham',
'* LIST (\\Noselect \\HasChildren) "/" Public',
'* LIST (\\HasNoChildren) "/" Public/office3',
'* LIST (\\HasNoChildren) "/" Public/office4',
'* LIST (\\HasNoChildren) "/" Public/support',
'* LIST (\\HasNoChildren) "/" Public/root',
'* LIST (\\HasNoChildren) "/" Public/updates',
'* LIST (\\HasNoChildren) "/" Public/postmaster',
'* LIST (\\Noselect \\HasChildren) "/" Shared',
'* LIST (\\Noselect \\HasChildren) "/" Shared/d.marteva',
'* LIST (\\HasNoChildren) "/" Shared/d.marteva/INBOX',
'* LIST (\\HasNoChildren) "/" INBOX',
'A3 OK Success',
]),
}, {
expected: 'A4 EXAMINE "INBOX"',
response: lines([
'* FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)',
'* OK [PERMANENTFLAGS ()] Flags permitted.',
'* OK [UIDVALIDITY 2] UIDs valid.',
'* 685 EXISTS',
'* 0 RECENT',
'* OK [UIDNEXT 4422] Predicted next UID.',
'A4 OK [READ-ONLY] INBOX selected. (Success)',
]),
}, {
expected: "A5 LOGOUT",
response: lines([
'* BYE LOGOUT Requested',
'A5 OK good day (Success)',
]),
}];
return Promise.try(() => {
return createMockServer({
steps: steps,
test: test
});
}).then(({ server, port, finalize }) => {
const client = new IMAP({
user: "foo",
password: "bar",
host: "127.0.0.1",
port: port,
keepalive: false
});
Promise.promisifyAll(client, { multiArgs: true });
client.connect();
return Promise.try(() => {
return pEvent(client, "ready");
}).then(() => {
server.close(); // Stop listening for new clients
return client.openBoxAsync("INBOX", true);
}).tap(() => {
client.end();
return pEvent(client, "end");
}).then(() => {
finalize();
});
});
}

@ -321,6 +321,11 @@
resolved "https://registry.yarnpkg.com/@joepie91/eslint-config/-/eslint-config-1.1.0.tgz#9397e6ce0a010cb57dcf8aef8754d3a5ce0ae36a"
integrity sha512-XliasRSUfOz1/bAvTBaUlCjWDbceCW4y1DnvFfW7Yw9p2FbNRR0w8WoPdTxTCjKuoZ7/OQMeBxIe2y9Qy6rbYw==
"@joepie91/unreachable@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@joepie91/unreachable/-/unreachable-1.0.0.tgz#8032bb8a5813e81bbbe516cb3031d60818526687"
integrity sha512-vZRJ5UDq4mqP1vgSrcOLD3aIfS/nzwsvGFOOHv5sj5fa1Ss0dT1xnIzrXKLD9pu5EcUvF3K6n6jdaMW8uXpNEQ==
"@types/prop-types@*":
version "15.7.4"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
@ -345,6 +350,152 @@
resolved "https://registry.yarnpkg.com/@types/yoga-layout/-/yoga-layout-1.9.2.tgz#efaf9e991a7390dc081a0b679185979a83a9639a"
integrity sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw==
"@validatem/annotate-errors@^0.1.2":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@validatem/annotate-errors/-/annotate-errors-0.1.2.tgz#fa9152bb30f4f42b69496b527e38f0c31ff605a9"
integrity sha512-EuX7pzdYI/YpTmZcgdPG481Oi3elAg8JWh/LYXuE1h6MaZk3A8eP5DD33/l7EoKzrysn6y8nCsqNa1ngei562w==
dependencies:
"@validatem/match-validation-error" "^0.1.0"
"@validatem/any-property@^0.1.0", "@validatem/any-property@^0.1.3":
version "0.1.3"
resolved "https://registry.yarnpkg.com/@validatem/any-property/-/any-property-0.1.3.tgz#fc7768c1922a8bacff9369ae48913672e5350f52"
integrity sha512-jYWxif5ff9pccu7566LIQ/4+snlApXEJUimBywzAriBgS3r4eDBbz3oZFHuiPmhxNK/NNof5YUS+L6Sk3zaMfg==
dependencies:
"@validatem/annotate-errors" "^0.1.2"
"@validatem/combinator" "^0.1.0"
"@validatem/error" "^1.0.0"
"@validatem/validation-result" "^0.1.1"
"@validatem/virtual-property" "^0.1.0"
default-value "^1.0.0"
"@validatem/combinator@^0.1.0":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@validatem/combinator/-/combinator-0.1.2.tgz#eab893d55f1643b9c6857eaf6ff7ed2a728e89ff"
integrity sha512-vE8t1tNXknmN62FlN6LxQmA2c6TwVKZ+fl/Wit3H2unFdOhu7SZj2kRPGjAXdK/ARh/3svYfUBeD75pea0j1Sw==
"@validatem/core@^0.3.15":
version "0.3.15"
resolved "https://registry.yarnpkg.com/@validatem/core/-/core-0.3.15.tgz#645a0734dbc6efa3a5c39c62c5f2d8fa773f89f3"
integrity sha512-4nBLGzgpPrPsZ5DDXDXwL5p+GUEvpAFt6I3/YUHoah+ckYmKNh9qwmWKkFZHxJVdRrTewGFRj0FPw5fqje1yxA==
dependencies:
"@validatem/annotate-errors" "^0.1.2"
"@validatem/any-property" "^0.1.0"
"@validatem/error" "^1.0.0"
"@validatem/match-validation-error" "^0.1.0"
"@validatem/match-versioned-special" "^0.1.0"
"@validatem/match-virtual-property" "^0.1.0"
"@validatem/normalize-rules" "^0.1.0"
"@validatem/required" "^0.1.0"
"@validatem/validation-result" "^0.1.1"
"@validatem/virtual-property" "^0.1.0"
as-expression "^1.0.0"
assure-array "^1.0.0"
create-error "^0.3.1"
default-value "^1.0.0"
execall "^2.0.0"
flatten "^1.0.3"
indent-string "^4.0.0"
is-arguments "^1.0.4"
supports-color "^7.1.0"
syncpipe "^1.0.0"
"@validatem/error@^1.0.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@validatem/error/-/error-1.1.0.tgz#bef46e7066c39761b494ebe3eec2ecdc7348f4ed"
integrity sha512-gZJEoZq1COi/8/5v0fVKQ9uX54x5lb5HbV7mzIOhY6dqjmLNfxdQmpECZPQrCAOpcRkRMJ7zaFhq4UTslpY9yA==
"@validatem/has-shape@^0.1.0":
version "0.1.8"
resolved "https://registry.yarnpkg.com/@validatem/has-shape/-/has-shape-0.1.8.tgz#dff0f0449c12b96d150091b7a980154d810ae63d"
integrity sha512-x2i8toW1uraFF2Vl6WBl4CScbBeg5alrtoCKMyXbJkHf2B5QxL/ftUh2RQRcBzx6U0i7KUb8vdShcWAa+fehRQ==
dependencies:
"@validatem/annotate-errors" "^0.1.2"
"@validatem/combinator" "^0.1.0"
"@validatem/error" "^1.0.0"
"@validatem/validation-result" "^0.1.1"
array-union "^2.1.0"
as-expression "^1.0.0"
assure-array "^1.0.0"
default-value "^1.0.0"
flatten "^1.0.3"
"@validatem/is-function@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@validatem/is-function/-/is-function-0.1.0.tgz#15a2e95259dc5e32256e8c21872455661437d069"
integrity sha512-UtVrwTGhaIdIJ0mPG5XkAmYZUeWgRoMP1G9ZEHbKvAZJ4+SXf/prC0jPgE0pw+sPjdQG4hblsXSfo/9Bf3PGdQ==
dependencies:
"@validatem/error" "^1.0.0"
is-callable "^1.1.5"
"@validatem/is-plain-object@^0.1.0":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@validatem/is-plain-object/-/is-plain-object-0.1.1.tgz#b7a3ef8ef960882c7c41e84ed709fa0bfb932e93"
integrity sha512-aNGbNIbKRpYI0lRBczlTBbiA+nqN52ADAASdySKg2/QeSCVtYS4uOIeCNIJRAgXe/5sUnLTuL4pgq628uAl7Kw==
dependencies:
"@validatem/error" "^1.0.0"
is-plain-obj "^2.1.0"
"@validatem/is-string@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@validatem/is-string/-/is-string-1.0.0.tgz#cc4a464f3bbc797aa7c7e124d11e5959b67636fd"
integrity sha512-j6fXuTgOrq94RbjSWeOzeKEcZ2ftnrG2ZHU16cGwC+gYn/st8JECXEpxSeZG8PJpn7V+PFaZeCKW9sJhonE1pA==
dependencies:
"@validatem/error" "^1.0.0"
is-string "^1.0.5"
"@validatem/match-special@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@validatem/match-special/-/match-special-0.1.0.tgz#4e0c28f1aee5bf53c1ef30bbf8c755d4946ae0ff"
integrity sha512-TFiq9Wk/1Hoja4PK85WwNYnwBXk3+Lgoj59ZIMxm2an1qmNYp8j+BnSvkKBflba451yIn6V1laU9NJf+/NYZgw==
"@validatem/match-validation-error@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@validatem/match-validation-error/-/match-validation-error-0.1.0.tgz#fa87f5f1836e7c1d9bf6b75b2addf0a5b21e4c1e"
integrity sha512-6akGTk7DdulOreyqDiGdikwRSixQz/AlvARSX18dcWaTFc79KxCLouL2hyoFcor9IIUhu5RTY4/i756y4T1yxA==
dependencies:
"@validatem/match-versioned-special" "^0.1.0"
"@validatem/match-versioned-special@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@validatem/match-versioned-special/-/match-versioned-special-0.1.0.tgz#2eacc48debecdbbe7e3d02f0c0a665afaea9bedf"
integrity sha512-xoOTY0bdA2ELj+ntcDVJ8YyMEFIJpjZ4HNPL9lGcbnRFwJBhQcHUAhUpZwkMxu02zH9wkNM1FvYGHxPz40745Q==
"@validatem/match-virtual-property@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@validatem/match-virtual-property/-/match-virtual-property-0.1.0.tgz#4de2de1075987b5f3b356d3f2bcf6c0be5b5fb83"
integrity sha512-ssd3coFgwbLuqvZftLZTy3eHN0TFST8oTS2XTViQdXJPXVoJmwEKBpFhXgwnb5Ly1CE037R/KWpjhd1TP/56kQ==
"@validatem/normalize-rules@^0.1.0":
version "0.1.3"
resolved "https://registry.yarnpkg.com/@validatem/normalize-rules/-/normalize-rules-0.1.3.tgz#59fd6193b1091ff97b5c723b32c9bb1fe2a9dc9c"
integrity sha512-HHPceAP2ce9NWymIZrgLCTzpdwXNRBCCB5H6ZPc5ggOrbmh4STpT83fLazleHtvYNlqgXZ4GjQOvCwrjaM+qEA==
dependencies:
"@validatem/has-shape" "^0.1.0"
"@validatem/is-plain-object" "^0.1.0"
"@validatem/match-special" "^0.1.0"
assure-array "^1.0.0"
default-value "^1.0.0"
flatten "^1.0.3"
is-plain-obj "^2.1.0"
"@validatem/required@^0.1.0", "@validatem/required@^0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@validatem/required/-/required-0.1.1.tgz#64f4a87333fc5955511634036b7f8948ed269170"
integrity sha512-vI4NzLfay4RFAzp7xyU34PHb8sAo6w/3frrNh1EY9Xjnw2zxjY5oaxwmbFP1jVevBE6QQEnKogtzUHz/Zuvh6g==
"@validatem/validation-result@^0.1.1":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@validatem/validation-result/-/validation-result-0.1.2.tgz#4e75cfd87305fc78f8d05ac84921a2c99a0348e0"
integrity sha512-okmP8JarIwIgfpaVcvZGuQ1yOsLKT3Egt49Ynz6h1MAeGsP/bGHXkkXtbiWOVsk5Tzku5vDVFSrFnF+5IEHKxw==
dependencies:
default-value "^1.0.0"
"@validatem/virtual-property@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@validatem/virtual-property/-/virtual-property-0.1.0.tgz#880540dfd149f98ecf1095d93912e34443381fe4"
integrity sha512-JUUvWtdqoSkOwlsl20oB3qFHYIL05a/TAfdY4AJcs55QeVTiX5iI1b8IoQW644sIWWooBuLv+XwoxjRsQFczlQ==
acorn-jsx@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b"
@ -466,11 +617,21 @@ argparse@^1.0.7:
dependencies:
sprintf-js "~1.0.2"
array-union@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
arrify@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
as-expression@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/as-expression/-/as-expression-1.0.0.tgz#7bc620ca4cb2fe0ee90d86729bd6add33b8fd831"
integrity sha512-Iqh4GxNUfxbJdGn6b7/XMzc8m1Dz2ZHouBQ9DDTzyMRO3VPPIAXeoY/sucRxxxXKbUtzwzWZSN6jPR3zfpYHHA==
asn1@~0.2.3:
version "0.2.4"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
@ -613,6 +774,14 @@ caching-transform@^4.0.0:
package-hash "^4.0.0"
write-file-atomic "^3.0.0"
call-bind@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
dependencies:
function-bind "^1.1.1"
get-intrinsic "^1.0.2"
caller-callsite@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134"
@ -758,6 +927,13 @@ cliui@^6.0.0:
strip-ansi "^6.0.0"
wrap-ansi "^6.2.0"
clone-regexp@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/clone-regexp/-/clone-regexp-2.2.0.tgz#7d65e00885cd8796405c35a737e7a86b7429e36f"
integrity sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==
dependencies:
is-regexp "^2.0.0"
code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
@ -837,6 +1013,11 @@ coveralls@^3.0.11:
minimist "^1.2.5"
request "^2.88.2"
create-error@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/create-error/-/create-error-0.3.1.tgz#69810245a629e654432bf04377360003a5351a23"
integrity sha1-aYECRaYp5lRDK/BDdzYAA6U1GiM=
cross-spawn@^7.0.0, cross-spawn@^7.0.2:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@ -1084,6 +1265,13 @@ events-to-array@^1.0.1:
resolved "https://registry.yarnpkg.com/events-to-array/-/events-to-array-1.1.2.tgz#2d41f563e1fe400ed4962fe1a4d5c6a7539df7f6"
integrity sha1-LUH1Y+H+QA7Uli/hpNXGp1Od9/Y=
execall@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/execall/-/execall-2.0.0.tgz#16a06b5fe5099df7d00be5d9c06eecded1663b45"
integrity sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow==
dependencies:
clone-regexp "^2.1.0"
extend@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
@ -1163,6 +1351,11 @@ flatted@^3.1.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469"
integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==
flatten@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b"
integrity sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==
foreground-child@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-2.0.0.tgz#71b32800c9f15aa8f2f83f4a6bd9bff35d861a53"
@ -1205,6 +1398,11 @@ fsevents@~2.3.2:
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
function-loop@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/function-loop/-/function-loop-2.0.1.tgz#799c56ced01698cf12a1b80e4802e9dafc2ebada"
@ -1225,6 +1423,15 @@ get-caller-file@^2.0.1:
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
get-intrinsic@^1.0.2:
version "1.1.1"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==
dependencies:
function-bind "^1.1.1"
has "^1.0.3"
has-symbols "^1.0.1"
get-package-type@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
@ -1315,6 +1522,18 @@ has-flag@^4.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
has-symbols@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
has@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
dependencies:
function-bind "^1.1.1"
hasha@^5.0.0:
version "5.2.2"
resolved "https://registry.yarnpkg.com/hasha/-/hasha-5.2.2.tgz#a48477989b3b327aea3c04f53096d816d97522a1"
@ -1417,6 +1636,13 @@ ink@^2.6.0, ink@^2.7.1:
wrap-ansi "^6.2.0"
yoga-layout-prebuilt "^1.9.3"
is-arguments@^1.0.4:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.0.tgz#62353031dfbee07ceb34656a6bde59efecae8dd9"
integrity sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==
dependencies:
call-bind "^1.0.0"
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
@ -1424,6 +1650,11 @@ is-binary-path@~2.1.0:
dependencies:
binary-extensions "^2.0.0"
is-callable@^1.1.5:
version "1.2.3"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e"
integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==
is-ci@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
@ -1465,11 +1696,26 @@ is-number@^7.0.0:
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
is-plain-obj@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
is-regexp@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-2.1.0.tgz#cd734a56864e23b956bf4e7c66c396a4c0b22c2d"
integrity sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==
is-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==
is-string@^1.0.5:
version "1.0.6"
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.6.tgz#3fe5d5992fb0d93404f32584d4b0179a71b54a5f"
integrity sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==
is-typedarray@^1.0.0, is-typedarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
@ -1628,6 +1874,11 @@ jsprim@^1.2.2:
json-schema "0.2.3"
verror "1.10.0"
last-item@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/last-item/-/last-item-1.0.0.tgz#54e5fbb09e46f5ebcb3b334e1ca0e015fbda633b"
integrity sha512-1F6OpdMLea5/489SjQj2UVP4cXb9qpMxuUU4xHpUH86PnhGldKf7oDtu8hy7a+3GCdvikEFtP4y5PoPsYVnC6g==
lcov-parse@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/lcov-parse/-/lcov-parse-1.0.0.tgz#eb0d46b54111ebc561acb4c408ef9363bdc8f7e0"
@ -1723,6 +1974,11 @@ map-obj@^4.2.1:
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.2.1.tgz#e4ea399dbc979ae735c83c863dd31bdf364277b7"
integrity sha512-+WA2/1sPmDj1dlvvJmB5G6JKfY9dpn7EVBUL06+y6PoljPkh+6V1QihwxNkbcGxCRjt2b0F9K0taiCuo7MbdFQ==
match-value@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/match-value/-/match-value-1.1.0.tgz#ad311ef8bbe2d344a53ec3104e28fe221984b98e"
integrity sha512-NOvpobcmkX+l9Eb6r2s3BkR1g1ZwzExDFdXA9d6p1r1O1olLbo88KuzMiBmg43xSpodfm7I6Hqlx2OoySquEgg==
mime-db@1.48.0:
version "1.48.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d"

Loading…
Cancel
Save