Sven Slootweg 2 months ago
parent
commit
3357108b52
  1. 402
      lib/Connection.js
  2. 2
      lib/Parser.js
  3. 48
      lib/util/box-tree-builder.js
  4. 80
      lib/util/named-tree-builder.js
  5. 4
      package.json
  6. 138
      test/reference-tree.js
  7. 313
      test/tests/box-tree.js
  8. 111
      test/tests/list.js
  9. 20
      yarn.lock

402
lib/Connection.js

@ -2,10 +2,14 @@
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 { 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");
var tls = require('tls'),
Socket = require('net').Socket,
@ -1234,25 +1238,166 @@ Object.defineProperty(Connection.prototype, 'seq', { get: function() {
};
}});
function createCommandHandlers(rules) {
let untaggedHandlers = new Map();
let taggedHandlers = new Map();
for (let [ types, options ] of Object.entries(rules)) {
let parsedTypes = types
.split(/\s*\s*/)
.map((type) => type.toUpperCase());
for (let type of parsedTypes) {
if (options.untagged != null) {
untaggedHandlers.set(type, options.untagged);
}
if (options.tagged != null) {
taggedHandlers.set(type, options.tagged);
}
}
}
// REFACTOR: Eventually remove `.call(this` hackery
return {
canHandleUntagged: function (request) {
return untaggedHandlers.has(request.type.toUpperCase());
},
canHandleTagged: function (request) {
return taggedHandlers.has(request.type.toUpperCase());
},
handleUntagged: function (request, data) {
let handler = untaggedHandlers.get(request.type.toUpperCase());
return handler.call(this, request, data);
},
handleTagged: function (request, data) {
let handler = taggedHandlers.get(request.type.toUpperCase());
return handler.call(this, request, data);
}
};
}
let commandHandlers = createCommandHandlers({
"LIST, XLIST, LSUB": {
untagged: function (request, { payload }) {
if (request.delimiter === undefined) {
request.delimiter = payload.delimiter;
} else {
if (request.boxBuilder == null) {
request.boxBuilder = createBoxTreeBuilder();
}
request._curReq.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;
}
},
"ID": {
untagged: function (request, { payload }) {
// https://datatracker.ietf.org/doc/html/rfc2971
// Used for communicating server/client name, version, etc.
request.responseData.serverVersion = payload;
request.legacyArgs.push(payload);
}
}
});
// type: type,
// num: num,
// 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
Connection.prototype._resUntagged = function({ type, num, textCode, text: payload }) {
// 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;
if (type === 'bye') {
if (this._curReq != null && commandHandlers.canHandleUntagged(this._curReq)) {
// FIXME: Include other fields
commandHandlers.handleUntagged.call(this, this._curReq, { sequenceNumber, payload });
} else 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') {
} else if (type === 'expunge') {
if (this._box) {
if (this._box.messages.total > 0) {
this._box.messages.total -= 1;
}
this.emit('expunge', sequenceNumber);
}
} else if (type === 'ok') {
if (this.state === 'connected' && !this._curReq) {
this._login();
} else if (typeof responseData === 'string' && responseData.toUpperCase() === 'ALERT') {
this.emit('alert', payload);
} else if (this._curReq && responseData && (RE_OPENBOX.test(this._curReq.type))) {
// we're opening a mailbox
if (!this._box) {
this._resetCurrentBox();
}
let destinationKey = (responseData.key != null)
? responseData.key.toUpperCase()
: responseData;
if (destinationKey === 'UIDVALIDITY') {
this._box.uidvalidity = responseData.val;
} else if (destinationKey === 'UIDNEXT') {
this._box.uidnext = responseData.val;
} else if (destinationKey === 'HIGHESTMODSEQ') {
this._box.highestmodseq = ''+responseData.val;
} else if (destinationKey === 'PERMANENTFLAGS') {
var idx, permFlags, keywords;
this._box.permFlags = permFlags = responseData.val;
if ((idx = this._box.permFlags.indexOf('\\*')) > -1) {
this._box.newKeywords = true;
permFlags.splice(idx, 1);
}
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 (destinationKey === 'UIDNOTSTICKY') {
this._box.persistentUIDs = false;
} else if (destinationKey === 'NOMODSEQ') {
this._box.nomodseq = true;
}
} else if (typeof responseData === 'string' && responseData.toUpperCase() === 'UIDVALIDITY') {
this.emit('uidvalidity', payload);
}
} else if (type === "esearch") {
// https://datatracker.ietf.org/doc/html/rfc4731 / https://datatracker.ietf.org/doc/html/rfc7377
Object.assign(this._curReq.responseData, payload); // Protocol-defined attributes. TODO: Improve the key names for this? Or is there extensibility?
this._curReq.cbargs.push(payload);
} else if (type === "sort") {
// https://datatracker.ietf.org/doc/html/rfc5256
this._curReq.responseData.UIDs = payload;
this._curReq.cbargs.push(payload);
} else if (type === 'thread') {
// https://datatracker.ietf.org/doc/html/rfc5256
this._curReq.responseData.threads = payload; // FIXME: Work out the exact format
this._curReq.cbargs.push(payload);
} else if (type === 'search') {
if (payload.results !== undefined) {
@ -1263,23 +1408,24 @@ Connection.prototype._resUntagged = function({ type, num, textCode, text: payloa
this._curReq.cbargs.push(payload);
}
} else if (type === 'quota') {
var cbargs = this._curReq.cbargs;
if (!cbargs.length) {
cbargs.push([]);
}
let responseData = this._curReq.responseData;
if (responseData.quota == null) { responseData.quota = []; };
responseData.quota.push(payload);
let cbargs = this._curReq.cbargs;
if (cbargs.length === 0) { cbargs.push([]); }
cbargs[0].push(payload);
} else if (type === 'recent') {
if (!this._box && RE_OPENBOX.test(this._curReq.type)) {
this._createCurrentBox();
this._resetCurrentBox();
}
if (this._box) {
this._box.messages.new = num;
this._box.messages.new = sequenceNumber;
}
} else if (type === 'flags') {
if (!this._box && RE_OPENBOX.test(this._curReq.type)) {
this._createCurrentBox();
this._resetCurrentBox();
}
if (this._box) {
@ -1296,124 +1442,17 @@ Connection.prototype._resUntagged = function({ type, num, textCode, text: payloa
}
} else if (type === 'exists') {
if (!this._box && RE_OPENBOX.test(this._curReq.type)) {
this._createCurrentBox();
this._resetCurrentBox();
}
if (this._box) {
var prev = this._box.messages.total, now = num;
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);
}
}
} else if (type === 'expunge') {
if (this._box) {
if (this._box.messages.total > 0) {
--this._box.messages.total;
}
this.emit('expunge', num);
}
} else if (type === 'ok') {
if (this.state === 'connected' && !this._curReq) {
this._login();
} else if (typeof textCode === 'string' && textCode.toUpperCase() === 'ALERT') {
this.emit('alert', payload);
}
else if (this._curReq
&& textCode
&& (RE_OPENBOX.test(this._curReq.type))) {
// we're opening a mailbox
if (!this._box) {
this._createCurrentBox();
}
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 = textCode.val;
if ((idx = this._box.permFlags.indexOf('\\*')) > -1) {
this._box.newKeywords = true;
permFlags.splice(idx, 1);
}
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 (destinationKey === 'UIDNOTSTICKY')
this._box.persistentUIDs = false;
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 (type === 'status') {
let attrs = defaultValue(payload.attrs, {});
@ -1437,57 +1476,59 @@ Connection.prototype._resUntagged = function({ type, num, textCode, text: payloa
} 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 +1538,48 @@ 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);
if (commandHandlers.canHandleTagged(request)) {
// FIXME: Add other fields with a sensible name
commandHandlers.handleTagged.call(this, request, { payload });
}
// 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 +1598,7 @@ Connection.prototype._resTagged = function(info) {
}
};
Connection.prototype._createCurrentBox = function() {
Connection.prototype._resetCurrentBox = function() {
this._box = {
name: '',
flags: [],
@ -1744,25 +1814,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 +1872,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`);
}
};

2
lib/Parser.js

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

48
lib/util/box-tree-builder.js

@ -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;
}
};
};

80
lib/util/named-tree-builder.js

@ -0,0 +1,80 @@
"use strict";
const defaultValue = require("default-value");
const asExpression = require("as-expression");
// FIXME: Move to stand-alone package, clearly document that it is not order-sensitive
function ensureObject(object, property) {
if (object[property] == null) {
object[property] = {};
}
}
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 });

4
package.json

@ -5,9 +5,13 @@
"description": "An IMAP module for node.js that makes communicating with IMAP servers easy",
"main": "./lib/Connection",
"dependencies": {
"@joepie91/unreachable": "^1.0.0",
"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",

138
test/reference-tree.js

@ -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);

313
test/tests/box-tree.js

@ -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();
});

111
test/tests/list.js

@ -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();
});
});
}

20
yarn.lock

@ -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"
@ -471,6 +476,11 @@ arrify@^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"
@ -1628,6 +1638,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 +1738,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