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). * **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: * The following message flags are valid criterion and do not require values:
* 'ANSWERED' - Messages with the \Answered flag set. * 'ANSWERED' - Messages with the \Answered flag set.
* 'DELETED' - Messages with the \Deleted 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: * The following are valid criterion that require an Integer value:
* 'LARGER' - Messages with a size larger than the specified number of bytes. * 'LARGER' - Messages with a size larger than the specified number of bytes.
* 'SMALLER' - Messages with a size smaller 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: * **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 * **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: A bunch of things not yet implemented in no particular order:
* Support AUTH=CRAM-MD5/AUTH=CRAM_MD5 authentication * 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 IMAP keywords (with a workaround for gmail's lack of support for IMAP keywords)
* Support additional IMAP commands/extensions: * Support additional IMAP commands/extensions:
* APPEND (is this really useful?)
* GETQUOTA (via QUOTA extension -- http://tools.ietf.org/html/rfc2087) * GETQUOTA (via QUOTA extension -- http://tools.ietf.org/html/rfc2087)
* UNSELECT (via UNSELECT extension -- http://tools.ietf.org/html/rfc3691) * 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) * 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'); throw new Error('No mailbox is currently selected');
if (!Array.isArray(options)) if (!Array.isArray(options))
throw new Error('Expected array for search options'); throw new Error('Expected array for search options');
var searchargs = '', months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; this._send('UID SEARCH' + buildSearchQuery(options), cb);
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);
}; };
ImapConnection.prototype.fetch = function(uid, options, cb) { ImapConnection.prototype.fetch = function(uid, options, cb) {
@ -570,6 +489,99 @@ ImapConnection.prototype._send = function(cmdstr, cb, bypass) {
/****** Utility Functions ******/ /****** 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) { function parseFetch(str, literalData, fetchData) {
// passed in str === "... {xxxx}" or "... {xxxx} ..." or just "..." // passed in str === "... {xxxx}" or "... {xxxx} ..." or just "..."
// where ... is any number of key-value pairs // 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; var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options, name, src, copy;
// Handle a deep copy situation // Handle a deep copy situation
if ( typeof target === "boolean" ) { if (typeof target === "boolean") {
deep = target; deep = target;
target = arguments[1] || {}; target = arguments[1] || {};
// skip the boolean and the target // skip the boolean and the target
i = 2; i = 2;
} }
// Handle case when target is a string or something (possible in deep copy) // Handle case when target is a string or something (possible in deep copy)
if ( typeof target !== "object" && !typeof target === 'function') { if (typeof target !== "object" && !typeof target === 'function')
target = {}; target = {};
}
var isPlainObject = function( obj ) { var isPlainObject = function(obj) {
// Must be an Object. // Must be an Object.
// Because of IE, we also have to check the presence of the constructor property. // 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 // 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; return false;
var has_own_constructor = hasOwnProperty.call(obj, "constructor"); var has_own_constructor = hasOwnProperty.call(obj, "constructor");
var has_is_property_of_method = hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf"); var has_is_property_of_method = hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf");
// Not own constructor property must be Object // 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; return false;
// Own properties are enumerated firstly, so to speed up, // Own properties are enumerated firstly, so to speed up,
// if last one is own, then all properties are own. // if last one is own, then all properties are own.
var last_key; var last_key;
for ( key in obj ) for (key in obj)
last_key = key; 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 // Only deal with non-null/undefined values
if ( (options = arguments[ i ]) !== null ) { if ((options = arguments[i]) !== null) {
// Extend the base object // Extend the base object
for ( name in options ) { for (name in options) {
src = target[ name ]; src = target[name];
copy = options[ name ]; copy = options[name];
// Prevent never-ending loop // Prevent never-ending loop
if ( target === copy ) if (target === copy)
continue; continue;
// Recurse if we're merging object literal values or arrays // Recurse if we're merging object literal values or arrays
if ( deep && copy && ( isPlainObject(copy) || Array.isArray(copy) ) ) { if (deep && copy && (isPlainObject(copy) || Array.isArray(copy))) {
var clone = src && ( isPlainObject(src) || Array.isArray(src) ) ? src : Array.isArray(copy) ? [] : {}; var clone = src && (isPlainObject(src) || Array.isArray(src)) ? src : Array.isArray(copy) ? [] : {};
// Never move original objects, clone them // Never move original objects, clone them
target[ name ] = extend( deep, clone, copy ); target[name] = extend(deep, clone, copy);
// Don't bring in undefined values // Don't bring in undefined values
} else if ( typeof copy !== "undefined" ) } else if (typeof copy !== "undefined")
target[ name ] = copy; target[name] = copy;
} }
} }
} }

Loading…
Cancel
Save