First go at CONDSTORE support

fork
mscdex 11 years ago
parent 00caa8f7b5
commit 88359f9f43

@ -558,6 +558,7 @@ Connection Instance Methods
* **markSeen** - < _boolean_ > - Mark message(s) as read when fetched. **Default:** false
* **struct** - < _boolean_ > - Fetch the message structure. **Default:** false
* **size** - < _boolean_ > - Fetch the RFC822 size. **Default:** false
* **modifiers** - < _object_ > - Fetch modifiers defined by IMAP extensions. **Default:** (none)
* **bodies** - < _mixed_ > - A string or Array of strings containing the body part section to fetch. **Default:** (none) Example sections:
* 'HEADER' - The message header
@ -596,12 +597,12 @@ Extensions Supported
* Server capability: X-GM-EXT-1
* search() criteria extensions
* search() criteria extensions:
* X-GM-RAW: string value which allows you to use Gmail's web interface search syntax, such as: "has:attachment in:unread"
* X-GM-THRID: allows you to search for a specific conversation/thread id which is associated with groups of messages
* X-GM-MSGID: allows you to search for a specific message given its account-wide unique id
* X-GM-LABELS: string value which allows you to search for specific messages that have the given label applied
* **X-GM-RAW** - < _string_ > - Gmail's custom search syntax. Example: 'has:attachment in:unread'
* **X-GM-THRID** - < _string_ > - Conversation/thread id
* **X-GM-MSGID** - < _string_ > - Account-wide unique id
* **X-GM-LABELS** - < _string_ > - Gmail label
* fetch() will automatically retrieve the thread id, unique message id, and labels (named 'x-gm-thrid', 'x-gm-msgid', 'x-gm-labels' respectively)
@ -680,6 +681,42 @@ Extensions Supported
}
```
* **RFC4551**
* Server capability: CONDSTORE
* Connection event 'update' info may contain the additional property:
* **modseq** - < _string_ > - The new modification sequence value for the message.
* search() criteria extensions:
* **MODSEQ** - < _string_ > - Modification sequence value. If this criteria is used, the callback parameters are then: < _Error_ >err, < _array_ >UIDs, < _string_ >modseq. The `modseq` callback parameter is the highest modification sequence value of all the messages identified in the search results.
* fetch() will automatically retrieve the modification sequence value (named 'modseq') for each message.
* fetch() modifier:
* **CHANGEDSINCE** - < _string_ > - Only fetch messages that have changed since the specified modification sequence.
* The _Box_ type can now have the following property when using openBox() or status():
* **highestmodseq** - < _string_ > - The highest modification sequence value of all messages in the mailbox.
* Additional Connection instance methods (seqno-based counterparts exist):
* **addFlagsSince**(< _mixed_ >source, < _mixed_ >flags, < _string_ >modseq, < _function_ >callback) - _(void)_ - Adds flag(s) to message(s) that have not changed since `modseq`. `source` can be a message UID, a message UID range (e.g. '2504:2507' or '\*' or '2504:\*'), or an _array_ of message UIDs and/or message UID ranges. `flags` is either a single flag or an _array_ of flags. `callback` has 1 parameter: < _Error_ >err.
* **delFlagsSince**(< _mixed_ >source, < _mixed_ >flags, < _function_ >callback) - _(void)_ - Removes flag(s) from message(s) that have not changed since `modseq`. `source` can be a message UID, a message UID range (e.g. '2504:2507' or '\*' or '2504:\*'), or an _array_ of message UIDs and/or message UID ranges. `flags` is either a single flag or an _array_ of flags. `callback` has 1 parameter: < _Error_ >err.
* **setFlagsSince**(< _mixed_ >source, < _mixed_ >flags, < _function_ >callback) - _(void)_ - Sets the flag(s) for message(s) that have not changed since `modseq`. `source` can be a message UID, a message UID range (e.g. '2504:2507' or '\*' or '2504:\*'), or an _array_ of message UIDs and/or message UID ranges. `flags` is either a single flag or an _array_ of flags. `callback` has 1 parameter: < _Error_ >err.
* **addKeywordsSince**(< _mixed_ >source, < _mixed_ >keywords, < _function_ >callback) - _(void)_ - Adds keyword(s) to message(s) that have not changed since `modseq`. `source` can be a message UID, a message UID range (e.g. '2504:2507' or '\*' or '2504:\*'), or an _array_ of message UIDs and/or message UID ranges. `keywords` is either a single keyword or an _array_ of keywords. `callback` has 1 parameter: < _Error_ >err.
* **delKeywordsSince**(< _mixed_ >source, < _mixed_ >keywords, < _function_ >callback) - _(void)_ - Removes keyword(s) from message(s) that have not changed since `modseq`. `source` can be a message UID, a message UID range (e.g. '2504:2507' or '\*' or '2504:\*'), or an _array_ of message UIDs and/or message UID ranges. `keywords` is either a single keyword or an _array_ of keywords. `callback` has 1 parameter: < _Error_ >err.
* **setKeywordsSince**(< _mixed_ >source, < _mixed_ >keywords, < _function_ >callback) - _(void)_ - Sets keyword(s) for message(s) that have not changed since `modseq`. `source` can be a message UID, a message UID range (e.g. '2504:2507' or '\*' or '2504:\*'), or an _array_ of message UIDs and/or message UID ranges. `keywords` is either a single keyword or an _array_ of keywords. `callback` has 1 parameter: < _Error_ >err.
TODO
----
@ -689,5 +726,4 @@ Several things not yet implemented in no particular order:
* Support additional IMAP commands/extensions:
* NOTIFY (via NOTIFY extension -- RFC5465)
* STATUS addition to LIST (via LIST-STATUS extension -- RFC5819)
* CONDSTORE (RFC4551)
* QRESYNC (RFC5162)

@ -296,7 +296,12 @@ Connection.prototype.openBox = function(name, readOnly, cb) {
cmd = (readOnly ? 'EXAMINE' : 'SELECT'),
self = this;
this._enqueue(cmd + ' "' + encname + '"', function(err) {
cmd += ' "' + encname + '"';
if (this.serverSupports('CONDSTORE'))
cmd += ' (CONDSTORE)';
this._enqueue(cmd, function(err) {
if (err) {
self._box = undefined;
cb(err);
@ -382,7 +387,14 @@ Connection.prototype.status = function(boxName, cb) {
boxName = escape(utf7.encode(''+boxName));
this._enqueue('STATUS "' + boxName + '" (MESSAGES RECENT UNSEEN UIDVALIDITY)',
var info = [ 'MESSAGES', 'RECENT', 'UNSEEN', 'UIDVALIDITY' ];
if (this.serverSupports('CONDSTORE'))
info.push('HIGHESTMODSEQ');
info = info.join(' ');
this._enqueue('STATUS "' + boxName + '" (' + info + ')',
cb);
};
@ -492,6 +504,8 @@ Connection.prototype._store = function(which, uids, cfg, cb) {
uids = uids.join(',');
var modifiers = '';
if (cfg.modseq !== undefined)
modifiers += 'UNCHANGEDSINCE ' + cfg.modseq + ' ';
this._enqueue(which + 'STORE ' + uids + ' '
+ modifiers
@ -618,6 +632,8 @@ Connection.prototype._fetch = function(which, uids, options) {
fetching.push('X-GM-MSGID');
fetching.push('X-GM-LABELS');
}
if (this.serverSupports('CONDSTORE'))
fetching.push('MODSEQ');
fetching.push('UID');
fetching.push('FLAGS');
@ -644,6 +660,19 @@ Connection.prototype._fetch = function(which, uids, options) {
cmd += ')';
var modifiers = options.modifiers,
modkeys = (typeof modifiers === 'object' ? Object.keys(modifiers) : []),
modstr = ' (';
for (var i = 0, len = modkeys.length, key; i < len; ++i) {
key = modkeys[i].toUpperCase();
if (key === 'CHANGEDSINCE' && this.serverSupports('CONDSTORE'))
modstr += key + ' ' + modifiers[modkeys[i]] + ' ';
}
if (modstr.length > 2) {
cmd += modstr.substring(0, modstr.length - 1);
cmd += ')';
}
this._enqueue(cmd);
var req = this._queue[this._queue.length - 1];
req.fetchCache = {};
@ -864,6 +893,48 @@ Connection.prototype._thread = function(which, algorithm, criteria, cb) {
req.lines = lines;
}
};
Connection.prototype.addFlagsSince = function(uids, flags, modseq, cb) {
this._store('UID ',
uids,
{ mode: '+', flags: flags, modseq: modseq },
cb);
};
Connection.prototype.delFlagsSince = function(uids, flags, modseq, cb) {
this._store('UID ',
uids,
{ mode: '-', flags: flags, modseq: modseq },
cb);
};
Connection.prototype.setFlagsSince = function(uids, flags, modseq, cb) {
this._store('UID ',
uids,
{ mode: '', flags: flags, modseq: modseq },
cb);
};
Connection.prototype.addKeywordsSince = function(uids, keywords, modseq, cb) {
this._store('UID ',
uids,
{ mode: '+', keywords: keywords, modseq: modseq },
cb);
};
Connection.prototype.delKeywordsSince = function(uids, keywords, modseq, cb) {
this._store('UID ',
uids,
{ mode: '-', keywords: keywords, modseq: modseq },
cb);
};
Connection.prototype.setKeywordsSince = function(uids, keywords, modseq, cb) {
this._store('UID ',
uids,
{ mode: '', keywords: keywords, modseq: modseq },
cb);
};
// END Extension methods =======================================================
// Namespace for seqno-based commands
@ -903,6 +974,7 @@ Connection.prototype.__defineGetter__('seq', function() {
self._search('', options, cb);
},
// Extensions ==============================================================
delLabels: function(seqnos, labels, cb) {
self._storeLabels('', seqnos, labels, '-', cb);
},
@ -922,6 +994,44 @@ Connection.prototype.__defineGetter__('seq', function() {
},
thread: function(algorithm, criteria, cb) {
self._thread('', algorithm, criteria, cb);
},
delKeywordsSince: function(seqnos, keywords, modseq, cb) {
self._store('',
seqnos,
{ mode: '-', keywords: keywords, modseq: modseq },
cb);
},
addKeywordsSince: function(seqnos, keywords, modseq, cb) {
self._store('',
seqnos,
{ mode: '+', keywords: keywords, modseq: modseq },
cb);
},
setKeywordsSince: function(seqnos, keywords, modseq, cb) {
self._store('',
seqnos,
{ mode: '', keywords: keywords, modseq: modseq },
cb);
},
delFlagsSince: function(seqnos, flags, modseq, cb) {
self._store('',
seqnos,
{ mode: '-', flags: flags, modseq: modseq },
cb);
},
addFlagsSince: function(seqnos, flags, modseq, cb) {
self._store('',
seqnos,
{ mode: '+', flags: flags, modseq: modseq },
cb);
},
setFlagsSince: function(seqnos, flags, modseq, cb) {
self._store('',
seqnos,
{ mode: '', flags: flags, modseq: modseq },
cb);
}
};
});
@ -937,9 +1047,16 @@ Connection.prototype._resUntagged = function(info) {
this._caps = info.text.map(function(v) { return v.toUpperCase(); });
else if (type === 'preauth')
this.state = 'authenticated';
else if (type === 'search' || type === 'sort' || type === 'thread')
else if (type === 'sort' || type === 'thread')
this._curReq.cbargs.push(info.text);
else if (type === 'quota') {
else if (type === 'search') {
if (info.text.results !== undefined) {
// CONDSTORE-modified search results
this._curReq.cbargs.push(info.text.results);
this._curReq.cbargs.push(info.text.modseq);
} else
this._curReq.cbargs.push(info.text);
} else if (type === 'quota') {
var cbargs = this._curReq.cbargs;
if (!cbargs.length)
cbargs.push([]);
@ -995,6 +1112,8 @@ Connection.prototype._resUntagged = function(info) {
this._box.uidvalidity = info.textCode.val;
else if (key === 'UIDNEXT')
this._box.uidnext = info.textCode.val;
else if (key === 'HIGHESTMODSEQ')
this._box.highestmodseq = ''+info.textCode.val;
else if (key === 'PERMANENTFLAGS') {
var idx, permFlags, keywords;
this._box.permFlags = permFlags = info.textCode.val;
@ -1067,6 +1186,8 @@ Connection.prototype._resUntagged = function(info) {
box.messages.total = attrs.messages;
if (attrs.uidvalidity !== undefined)
box.uidvalidity = attrs.uidvalidity;
if (attrs.highestmodseq !== undefined) // CONDSTORE
box.highestmodseq = ''+attrs.highestmodseq;
}
this._curReq.cbargs.push(box);
} else if (type === 'fetch') {
@ -1570,11 +1691,11 @@ function buildSearchQuery(options, extensions, info, isOrChild) {
validateUIDList(args);
searchargs += modifier + criteria + ' ' + args.join(',');
break;
// -- Extensions criteria --
// Extensions ==========================================================
case 'X-GM-MSGID': // Gmail unique message ID
case 'X-GM-THRID': // Gmail thread ID
if (extensions.indexOf('X-GM-EXT-1') === -1)
throw new Error('IMAP extension not available: ' + criteria);
throw new Error('IMAP extension not available for: ' + criteria);
if (!args || args.length !== 1)
throw new Error('Incorrect number of arguments for search option: '
+ criteria);
@ -1587,7 +1708,7 @@ function buildSearchQuery(options, extensions, info, isOrChild) {
break;
case 'X-GM-RAW': // Gmail search syntax
if (extensions.indexOf('X-GM-EXT-1') === -1)
throw new Error('IMAP extension not available: ' + criteria);
throw new Error('IMAP extension not available for: ' + criteria);
if (!args || args.length !== 1)
throw new Error('Incorrect number of arguments for search option: '
+ criteria);
@ -1598,7 +1719,15 @@ function buildSearchQuery(options, extensions, info, isOrChild) {
break;
case 'X-GM-LABELS': // Gmail labels
if (extensions.indexOf('X-GM-EXT-1') === -1)
throw new Error('IMAP extension not available: ' + criteria);
throw new Error('IMAP extension not available for: ' + criteria);
if (!args || args.length !== 1)
throw new Error('Incorrect number of arguments for search option: '
+ criteria);
searchargs += modifier + criteria + ' ' + args[0];
break;
case 'MODSEQ':
if (extensions.indexOf('CONDSTORE') === -1)
throw new Error('IMAP extension not available for: ' + criteria);
if (!args || args.length !== 1)
throw new Error('Incorrect number of arguments for search option: '
+ criteria);

@ -20,7 +20,8 @@ var CH_LF = 10,
RE_CRLF = /\r\n/g,
RE_HDR = /^([^:]+):[ \t]?(.+)?$/,
RE_ENCWORD = /=\?([^?]*?)\?([qb])\?(.*?)\?=/gi,
RE_QENC = /(?:=([a-fA-F0-9]{2}))|_/g;
RE_QENC = /(?:=([a-fA-F0-9]{2}))|_/g,
RE_SEARCH_MODSEQ = /^(.+) \(MODSEQ (.+?)\)$/i;
function Parser(stream, debug) {
if (!(this instanceof Parser))
@ -213,13 +214,22 @@ Parser.prototype._resUntagged = function() {
|| type === 'capability'
|| type === 'sort') {
if (m[5]) {
if (m[5][0] === '(')
val = RE_LISTCONTENT.exec(m[5])[1].split(' ');
else
val = m[5].split(' ');
if (type === 'search' && RE_SEARCH_MODSEQ.test(m[5])) {
// CONDSTORE search response
var p = RE_SEARCH_MODSEQ.exec(m[5]);
val = {
results: p[1].split(' ');
modseq: p[2]
};
} else {
if (m[5][0] === '(')
val = RE_LISTCONTENT.exec(m[5])[1].split(' ');
else
val = m[5].split(' ');
if (type === 'search' || type === 'sort')
val = val.map(function(v) { return parseInt(v, 10); });
if (type === 'search' || type === 'sort')
val = val.map(function(v) { return parseInt(v, 10); });
}
} else
val = [];
} else if (type === 'thread') {
@ -395,6 +405,8 @@ function parseFetch(text, literals) {
val = parseFetchEnvelope(val);
else if (key === 'internaldate')
val = new Date(val);
else if (key === 'modseq') // always a list of one value
val = ''+val[0];
else if (key === 'body' || key === 'bodystructure')
val = parseBodyStructure(val);
attrs[key] = val;

Loading…
Cancel
Save