From 008332036f812dd8b312b0363f425f36c7efd1e9 Mon Sep 17 00:00:00 2001 From: mscdex Date: Fri, 28 Jun 2013 16:51:05 -0400 Subject: [PATCH] add support for ESEARCH --- README.md | 19 ++++++++++++++++++- lib/Connection.js | 27 +++++++++++++++++++++++++++ lib/Parser.js | 23 ++++++++++++++++++++++- test/test-parser.js | 28 ++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5f6447c..c212027 100644 --- a/README.md +++ b/README.md @@ -669,6 +669,24 @@ Extensions Supported * 'TO' - The mailbox of the **first** "to" address. +* **ESEARCH** + + * Server capability: ESEARCH + + * Additional Connection functions + + * **esearch**(< _array_ >criteria, < _array_ >options, < _function_ >callback) - _(void)_ - A variant of search() that can return metadata about results. `callback` has 2 parameters: < _Error_ >err, < _object_ >info. `info` has possible keys: 'all', 'min', 'max', 'count'. Valid `options`: + + * 'ALL' - Retrieves sequence numbers or UIDs in a compact form (e.g. [2, '10:11'] instead of search()'s [2, 10, 11]) that match the criteria. + + * 'MIN' - Retrieves the lowest sequence number or UID that satisfies the criteria. + + * 'MAX' - Retrieves the highest sequence number or UID that satisfies the criteria. + + * 'COUNT' - Retrieves the number of messages that satisfy the criteria. + + Note: specifying no `options` or [] is the same as ['ALL'] + TODO ---- @@ -676,7 +694,6 @@ TODO Several things not yet implemented in no particular order: * Support STARTTLS -* Support AUTH=CRAM-MD5/AUTH=CRAM_MD5 authentication * Support additional IMAP commands/extensions: * NOTIFY (via NOTIFY extension -- http://tools.ietf.org/html/rfc5465) * STATUS addition to LIST (via LIST-STATUS extension -- http://tools.ietf.org/html/rfc5819) diff --git a/lib/Connection.js b/lib/Connection.js index 75e7432..d054177 100644 --- a/lib/Connection.js +++ b/lib/Connection.js @@ -375,6 +375,30 @@ Connection.prototype._search = function(which, options, cb) { this._enqueue(which + 'SEARCH' + buildSearchQuery(options, this._caps), cb); }; +Connection.prototype.esearch = function(criteria, options, cb) { + this._search('UID ', criteria, options, cb); +}; + +Connection.prototype._esearch = function(which, criteria, options, cb) { + if (this._box === undefined) + throw new Error('No mailbox is currently selected'); + else if (!Array.isArray(criteria)) + throw new Error('Expected array for search options'); + + var searchQuery = buildSearchQuery(criteria, this._caps); + + if (typeof options === 'function') { + cb = options; + options = ''; + } else if (!options) + options = ''; + + if (Array.isArray(options)) + options = options.join(' '); + + this._enqueue(which + 'SEARCH RETURN (' + options + ') ' + searchQuery, cb); +}; + Connection.prototype.sort = function(sorts, options, cb) { this._sort('UID ', sorts, options, cb); }; @@ -717,6 +741,9 @@ Connection.prototype.__defineGetter__('seq', function() { search: function(options, cb) { self._search('', options, cb); }, + esearch: function(criteria, options, cb) { + self._esearch('', criteria, options, cb); + }, sort: function(sorts, options, cb) { self._sort('', sorts, options, cb); } diff --git a/lib/Parser.js b/lib/Parser.js index a54c533..6bf404e 100644 --- a/lib/Parser.js +++ b/lib/Parser.js @@ -13,7 +13,7 @@ var CH_LF = 10, RE_SEQNO = /^\* (\d+)/, RE_LISTCONTENT = /^\((.*)\)$/, RE_LITERAL = /\{(\d+)\}$/, - RE_UNTAGGED = /^\* (?:(OK|NO|BAD|BYE|FLAGS|LIST|LSUB|SEARCH|STATUS|CAPABILITY|NAMESPACE|PREAUTH|SORT)|(\d+) (EXPUNGE|FETCH|RECENT|EXISTS))(?: (?:\[([^\]]+)\] )?(.+))?$/i, + RE_UNTAGGED = /^\* (?:(OK|NO|BAD|BYE|FLAGS|LIST|LSUB|SEARCH|STATUS|CAPABILITY|NAMESPACE|PREAUTH|SORT|ESEARCH)|(\d+) (EXPUNGE|FETCH|RECENT|EXISTS))(?: (?:\[([^\]]+)\] )?(.+))?$/i, RE_TAGGED = /^A(\d+) (OK|NO|BAD) (?:\[([^\]]+)\] )?(.+)$/i, RE_CONTINUE = /^\+ (?:\[([^\]]+)\] )?(.+)$/i, RE_CRLF = /\r\n/g, @@ -219,6 +219,8 @@ Parser.prototype._resUntagged = function() { val = parseFetch(m[5], this._literals); else if (type === 'namespace') val = parseNamespaces(m[5], this._literals); + else if (type === 'esearch') + val = parseESearch(m[5], this._literals); else val = m[5]; @@ -269,6 +271,25 @@ function parseTextCode(text, literals) { return { key: r[0], val: r[1] }; } +function parseESearch(text, literals) { + var r = parseExpr(text.toUpperCase().replace('UID', ''), literals), + attrs = {}; + + // RFC4731 unfortunately is lacking on documentation, so we're going to + // assume that the response text always begins with (TAG "A123") and skip that + // part ... + + for (var i = 1, len = r.length, key, val; i < len; i += 2) { + key = r[i].toLowerCase(); + val = r[i + 1]; + if (key === 'all') + val = val.split(','); + attrs[key] = val; + } + + return attrs; +} + function parseBoxList(text, literals) { var r = parseExpr(text, literals); return { diff --git a/test/test-parser.js b/test/test-parser.js index 48b19e1..5cde81f 100644 --- a/test/test-parser.js +++ b/test/test-parser.js @@ -360,6 +360,34 @@ var CR = '\r', LF = '\n', CRLF = CR + LF; ], what: 'Untagged FETCH (flags, date, size, envelope, body[structure])' }, + // EXTENSIONS ---------------------------------------------------------------- + { source: ['* ESEARCH (TAG "A285") UID MIN 7 MAX 3800', CRLF], + expected: [ { type: 'esearch', + num: undefined, + textCode: undefined, + text: { min: 7, max: 3800 } + } + ], + what: 'ESearch UID, 2 items' + }, + { source: ['* ESEARCH (TAG "A284") MIN 4', CRLF], + expected: [ { type: 'esearch', + num: undefined, + textCode: undefined, + text: { min: 4 } + } + ], + what: 'ESearch 1 item' + }, + { source: ['* ESEARCH (TAG "A283") ALL 2,10:11', CRLF], + expected: [ { type: 'esearch', + num: undefined, + textCode: undefined, + text: { all: [ '2', '10:11' ] } + } + ], + what: 'ESearch ALL list' + }, ].forEach(function(v) { var ss = new require('stream').Readable(), p, result = []; ss._read = function(){};