Implement "OR" for message search.

fork
Brian White 14 years ago
parent e45b904800
commit 4af7472a7b

@ -202,7 +202,7 @@ ImapConnection Functions
* **closeBox**(Function) - _(void)_ - Closes the currently open mailbox. **Any messages marked as \Deleted in the mailbox will be removed if the mailbox was NOT opened in read-only mode.** Also, logging out or opening another mailbox without closing the current one first will NOT cause deleted messages to be removed. The Function parameter is the callback with one parameter: the error (null if none).
* **search**(Array, Function) - _(void)_ - Searches the currently open mailbox for messages using specific criterion. The Function parameter is the callback with three parameters: the error (null if none), the _Box_ object of the currently open mailbox, and an Array containing the message IDs matching the search criterion. The Array parameter is a list of Arrays containing the criterion (and also value(s) for some types of criteria) to be used. Prefix the criteria name with an "!" to negate. For example, to search for unread messages since April 20, 2010 you could use: [ ['UNSEEN'], ['SINCE', 'April 20, 2010'] ]
* **search**(Array, Function) - _(void)_ - Searches the currently open mailbox for messages using specific criterion. The Function parameter is the callback with three parameters: the error (null if none), the _Box_ object of the currently open mailbox, and an Array containing the message IDs matching the search criterion. The Array parameter is a list of Arrays containing the criterion (and any required arguments) to be used. Prefix the criteria name with an "!" to negate. For example, to search for unread messages since April 20, 2010 you could use: [ ['UNSEEN'], ['SINCE', 'April 20, 2010'] ]. To search for messages that are EITHER unread OR are dated April 20, 2010 or later, you could use: [ ['OR', ['UNSEEN'], ['SINCE', 'April 20, 2010'] ] ].
* The following message flags are valid criterion and do not require values:
* 'ANSWERED' - Messages with the \Answered flag set.
* 'DELETED' - Messages with the \Deleted flag set.
@ -236,6 +236,7 @@ ImapConnection Functions
* The following are valid criterion that require an Integer value:
* 'LARGER' - Messages with a size larger than the specified number of bytes.
* 'SMALLER' - Messages with a size smaller than the specified number of bytes.
* **Note:** By default, all criterion are ANDed together. You can use the special 'OR' on **two** criterion to find messages matching either search criteria (see example above).
* **fetch**(Integer, Object, Function) - _(void)_ - Fetches the message with the message ID specified by the Integer parameter in the currently open mailbox. The Function parameter is the callback with three parameters: the error (null if none), the _Box_ object of the currently open mailbox, and the _FetchResult_ containing the result of the fetch request. The Object parameter is a set of options used to determine how and what exactly to fetch. The valid options are:
* **markSeen** - A Boolean indicating whether to mark the message as read when fetching it. **Default:** false
@ -257,11 +258,8 @@ TODO
A bunch of things not yet implemented in no particular order:
* Support AUTH=CRAM-MD5/AUTH=CRAM_MD5 authentication
* OR searching ability with () grouping
* HEADER.FIELDS.NOT capability during FETCH using "!" prefix
* Support IMAP keywords (with a workaround for gmail's lack of support for IMAP keywords)
* Support additional IMAP commands/extensions:
* APPEND (is this really useful?)
* GETQUOTA (via QUOTA extension -- http://tools.ietf.org/html/rfc2087)
* UNSELECT (via UNSELECT extension -- http://tools.ietf.org/html/rfc3691)
* LIST (and XLIST via XLIST extension -- http://groups.google.com/group/Gmail-Help-POP-and-IMAP-en/browse_thread/thread/a154105c54f020fb)

@ -307,88 +307,7 @@ ImapConnection.prototype.search = function(options, cb) {
throw new Error('No mailbox is currently selected');
if (!Array.isArray(options))
throw new Error('Expected array for search options');
var searchargs = '', months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
for (var i=0,len=options.length; i<len; i++) {
var criteria = options[i], args = null, modifier = ' ';
if (typeof criteria === 'string')
criteria = criteria.toUpperCase();
else if (Array.isArray(criteria)) {
if (criteria.length > 1)
args = criteria.slice(1);
if (criteria.length > 0)
criteria = criteria[0].toUpperCase();
} else
throw new Error('Unexpected search option data type. Expected string, number, or array. Got: ' + typeof criteria);
if (criteria[0] === '!') {
modifier += 'NOT ';
criteria = criteria.substr(1);
}
switch(criteria) {
case 'ANSWERED':
case 'DELETED':
case 'DRAFT':
case 'FLAGGED':
case 'NEW':
case 'SEEN':
case 'RECENT':
case 'OLD':
case 'UNANSWERED':
case 'UNDELETED':
case 'UNDRAFT':
case 'UNFLAGGED':
case 'UNSEEN':
searchargs += modifier + criteria;
break;
case 'BCC':
case 'BODY':
case 'CC':
case 'FROM':
case 'SUBJECT':
case 'TEXT':
case 'TO':
if (!args || args.length !== 1)
throw new Error('Incorrect number of arguments for search option: ' + criteria);
searchargs += modifier + criteria + ' "' + escape(''+args[0]) + '"';
break;
case 'BEFORE':
case 'ON':
case 'SENTBEFORE':
case 'SENTON':
case 'SENTSINCE':
case 'SINCE':
if (!args || args.length !== 1)
throw new Error('Incorrect number of arguments for search option: ' + criteria);
else if (!(args[0] instanceof Date)) {
if ((args[0] = new Date(args[0])).toString() === 'Invalid Date')
throw new Error('Search option argument must be a Date object or a parseable date');
}
searchargs += modifier + criteria + ' ' + args[0].getDate() + '-' + months[args[0].getMonth()] + '-' + args[0].getFullYear();
break;
/*case 'KEYWORD':
case 'UNKEYWORD':
if (!args || args.length !== 1)
throw new Error('Incorrect number of arguments for search option: ' + criteria);
searchargs += modifier + criteria + ' ' + args[0];
break;*/
case 'LARGER':
case 'SMALLER':
if (!args || args.length !== 1)
throw new Error('Incorrect number of arguments for search option: ' + criteria);
var num = parseInt(args[0]);
if (isNaN(num))
throw new Error('Search option argument must be a number');
searchargs += modifier + criteria + ' ' + args[0];
break;
case 'HEADER':
if (!args || args.length !== 2)
throw new Error('Incorrect number of arguments for search option: ' + criteria);
searchargs += modifier + criteria + ' "' + escape(''+args[0]) + '" "' + escape(''+args[1]) + '"';
break;
default:
throw new Error('Unexpected search option: ' + criteria);
}
}
this._send('UID SEARCH' + searchargs, cb);
this._send('UID SEARCH' + buildSearchQuery(options), cb);
};
ImapConnection.prototype.fetch = function(uid, options, cb) {
@ -570,6 +489,99 @@ ImapConnection.prototype._send = function(cmdstr, cb, bypass) {
/****** Utility Functions ******/
function buildSearchQuery(options, isOrChild) {
var searchargs = '', months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
for (var i=0,len=options.length; i<len; i++) {
var criteria = (isOrChild ? options : options[i]), args = null, modifier = (isOrChild ? '' : ' ');
if (typeof criteria === 'string')
criteria = criteria.toUpperCase();
else if (Array.isArray(criteria)) {
if (criteria.length > 1)
args = criteria.slice(1);
if (criteria.length > 0)
criteria = criteria[0].toUpperCase();
} else
throw new Error('Unexpected search option data type. Expected string, number, or array. Got: ' + typeof criteria);
if (criteria === 'OR') {
if (args.length !== 2)
throw new Error('OR must have exactly two arguments');
searchargs += ' OR (' + buildSearchQuery(args[0], true) + ') (' + buildSearchQuery(args[1], true) + ')'
} else {
if (criteria[0] === '!') {
modifier += 'NOT ';
criteria = criteria.substr(1);
}
switch(criteria) {
case 'ANSWERED':
case 'DELETED':
case 'DRAFT':
case 'FLAGGED':
case 'NEW':
case 'SEEN':
case 'RECENT':
case 'OLD':
case 'UNANSWERED':
case 'UNDELETED':
case 'UNDRAFT':
case 'UNFLAGGED':
case 'UNSEEN':
searchargs += modifier + criteria;
break;
case 'BCC':
case 'BODY':
case 'CC':
case 'FROM':
case 'SUBJECT':
case 'TEXT':
case 'TO':
if (!args || args.length !== 1)
throw new Error('Incorrect number of arguments for search option: ' + criteria);
searchargs += modifier + criteria + ' "' + escape(''+args[0]) + '"';
break;
case 'BEFORE':
case 'ON':
case 'SENTBEFORE':
case 'SENTON':
case 'SENTSINCE':
case 'SINCE':
if (!args || args.length !== 1)
throw new Error('Incorrect number of arguments for search option: ' + criteria);
else if (!(args[0] instanceof Date)) {
if ((args[0] = new Date(args[0])).toString() === 'Invalid Date')
throw new Error('Search option argument must be a Date object or a parseable date string');
}
searchargs += modifier + criteria + ' ' + args[0].getDate() + '-' + months[args[0].getMonth()] + '-' + args[0].getFullYear();
break;
/*case 'KEYWORD':
case 'UNKEYWORD':
if (!args || args.length !== 1)
throw new Error('Incorrect number of arguments for search option: ' + criteria);
searchargs += modifier + criteria + ' ' + args[0];
break;*/
case 'LARGER':
case 'SMALLER':
if (!args || args.length !== 1)
throw new Error('Incorrect number of arguments for search option: ' + criteria);
var num = parseInt(args[0]);
if (isNaN(num))
throw new Error('Search option argument must be a number');
searchargs += modifier + criteria + ' ' + args[0];
break;
case 'HEADER':
if (!args || args.length !== 2)
throw new Error('Incorrect number of arguments for search option: ' + criteria);
searchargs += modifier + criteria + ' "' + escape(''+args[0]) + '" "' + escape(''+args[1]) + '"';
break;
default:
throw new Error('Unexpected search option: ' + criteria);
}
}
if (isOrChild)
break;
}
return searchargs;
}
function parseFetch(str, literalData, fetchData) {
// passed in str === "... {xxxx}" or "... {xxxx} ..." or just "..."
// where ... is any number of key-value pairs
@ -964,64 +976,63 @@ function extend() {
var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options, name, src, copy;
// Handle a deep copy situation
if ( typeof target === "boolean" ) {
deep = target;
target = arguments[1] || {};
// skip the boolean and the target
i = 2;
if (typeof target === "boolean") {
deep = target;
target = arguments[1] || {};
// skip the boolean and the target
i = 2;
}
// Handle case when target is a string or something (possible in deep copy)
if ( typeof target !== "object" && !typeof target === 'function') {
target = {};
}
if (typeof target !== "object" && !typeof target === 'function')
target = {};
var isPlainObject = function( obj ) {
var isPlainObject = function(obj) {
// Must be an Object.
// Because of IE, we also have to check the presence of the constructor property.
// Make sure that DOM nodes and window objects don't pass through, as well
if ( !obj || toString.call(obj) !== "[object Object]" || obj.nodeType || obj.setInterval )
if (!obj || toString.call(obj) !== "[object Object]" || obj.nodeType || obj.setInterval)
return false;
var has_own_constructor = hasOwnProperty.call(obj, "constructor");
var has_is_property_of_method = hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf");
// Not own constructor property must be Object
if ( obj.constructor && !has_own_constructor && !has_is_property_of_method)
if (obj.constructor && !has_own_constructor && !has_is_property_of_method)
return false;
// Own properties are enumerated firstly, so to speed up,
// if last one is own, then all properties are own.
var last_key;
for ( key in obj )
for (key in obj)
last_key = key;
return typeof last_key === "undefined" || hasOwnProperty.call( obj, last_key );
return typeof last_key === "undefined" || hasOwnProperty.call(obj, last_key);
};
for ( ; i < length; i++ ) {
for (; i < length; i++) {
// Only deal with non-null/undefined values
if ( (options = arguments[ i ]) !== null ) {
if ((options = arguments[i]) !== null) {
// Extend the base object
for ( name in options ) {
src = target[ name ];
copy = options[ name ];
for (name in options) {
src = target[name];
copy = options[name];
// Prevent never-ending loop
if ( target === copy )
if (target === copy)
continue;
// Recurse if we're merging object literal values or arrays
if ( deep && copy && ( isPlainObject(copy) || Array.isArray(copy) ) ) {
var clone = src && ( isPlainObject(src) || Array.isArray(src) ) ? src : Array.isArray(copy) ? [] : {};
if (deep && copy && (isPlainObject(copy) || Array.isArray(copy))) {
var clone = src && (isPlainObject(src) || Array.isArray(src)) ? src : Array.isArray(copy) ? [] : {};
// Never move original objects, clone them
target[ name ] = extend( deep, clone, copy );
target[name] = extend(deep, clone, copy);
// Don't bring in undefined values
} else if ( typeof copy !== "undefined" )
target[ name ] = copy;
} else if (typeof copy !== "undefined")
target[name] = copy;
}
}
}

Loading…
Cancel
Save