1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc.
  5  *
  6  * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at: https://www.zimbra.com/license
  9  * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15
 10  * have been added to cover use of software over a computer network and provide for limited attribution
 11  * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B.
 12  *
 13  * Software distributed under the License is distributed on an "AS IS" basis,
 14  * WITHOUT WARRANTY OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing rights and limitations under the License.
 16  * The Original Code is Zimbra Open Source Web Client.
 17  * The Initial Developer of the Original Code is Zimbra, Inc.  All rights to the Original Code were
 18  * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015.
 19  *
 20  * All portions of the code are Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @overview
 26  * This file defines the search controller.
 27  *
 28  */
 29 
 30 /**
 31  * Creates a search controller.
 32  * @class
 33  * This class represents the search controller.
 34  * 
 35  * @param {DwtControl}	container	the top-level container
 36  * @extends	ZmController
 37  */
 38 ZmSearchController = function(container) {
 39 
 40 	ZmController.call(this, container);
 41 
 42 	this._inited = false;
 43 	this._contactSource = ZmItem.CONTACT;
 44 	this._results = null;
 45 
 46 	if (appCtxt.get(ZmSetting.SEARCH_ENABLED)) {
 47 		this._setView();
 48 	}
 49 };
 50 
 51 ZmSearchController.prototype = new ZmController;
 52 ZmSearchController.prototype.constructor = ZmSearchController;
 53 
 54 ZmSearchController.prototype.isZmSearchController = true;
 55 ZmSearchController.prototype.toString = function() { return "ZmSearchController"; };
 56 
 57 // Consts
 58 ZmSearchController.QUERY_ISREMOTE = "is:remote OR is:local";
 59 
 60 
 61 /**
 62  * Gets the search tool bar.
 63  * 
 64  * @return	{ZmButtonToolBar}		the tool bar
 65  */
 66 ZmSearchController.prototype.getSearchToolbar =
 67 function() {
 68 	return this._searchToolBar;
 69 };
 70 
 71 /**
 72  * Performs a search by date.
 73  * 
 74  * @param	{Date}	d		the date or <code>d</code> for now
 75  * @param	{String}	searchFor	the search for string
 76  */
 77 ZmSearchController.prototype.dateSearch =
 78 function(d, searchFor) {
 79 	d = d || new Date();
 80     var formatter = AjxDateFormat.getDateInstance(AjxDateFormat.SHORT);
 81     var date = formatter.format(d);
 82 	var groupBy = appCtxt.getApp(ZmApp.MAIL).getGroupMailBy();
 83 	var query = "date:" + date;
 84 	this.search({
 85 		query:			query,
 86 		types:			[groupBy],
 87 		searchFor:		searchFor,
 88 		origin:			ZmId.SEARCH,
 89 		userInitiated:	true
 90 	});
 91 };
 92 
 93 /**
 94  * Performs a search by from address.
 95  * 
 96  * @param	{String}	address		the from address
 97  */
 98 ZmSearchController.prototype.fromSearch =
 99 function(address) {
100 
101 	// always search for mail when doing a "from: <address>" search
102 	var groupBy = appCtxt.getApp(ZmApp.MAIL).getGroupMailBy();
103 	var terms = AjxUtil.map(AjxUtil.toArray(address), function(addr) {
104 		return "from:" + ((addr && addr.isAjxEmailAddress) ? addr.getAddress() : addr);
105 	});
106 	
107 	this.search({
108 		query:			terms.join(" OR "),
109 		types:			[groupBy],
110 		origin:			ZmId.SEARCH,
111 		userInitiated:	true
112 	});
113 };
114 
115 
116 /**
117  * Performs a search by to address.
118  *
119  * @param	{String}	address		the to address
120  */
121 ZmSearchController.prototype.toSearch =
122 function(address) {
123 
124 	// always search for mail when doing a "tocc: <address>" search
125 	var groupBy = appCtxt.getApp(ZmApp.MAIL).getGroupMailBy();
126 	var terms = AjxUtil.map(AjxUtil.toArray(address), function(addr) {
127 		return "tocc:" + ((addr && addr.isAjxEmailAddress) ? addr.getAddress() : addr);
128 	});
129 
130 	var params = {
131 		types:			[groupBy],
132 		origin:			ZmId.SEARCH,
133 		userInitiated:	true,
134 		query:			terms.join(" OR ")
135 	}
136     if (this.currentSearch && this.currentSearch.folderId == ZmFolder.ID_SENT) {
137 		if (terms.length > 1) {
138 			params.query = "(" + params.query + ")";
139 		}
140 		params.query = "in:sent AND " + params.query;
141 	}
142     this.search(params);
143 };
144 
145 /**
146  * Sets the search field.
147  * 
148  * @param	{String}	searchString	the search string
149  */
150 ZmSearchController.prototype.setSearchField =
151 function(searchString) {
152 	if (appCtxt.get(ZmSetting.SHOW_SEARCH_STRING) && this._searchToolBar) {
153 		this._searchToolBar.setSearchFieldValue(searchString);
154 	} else {
155 		this._currentQuery = searchString;
156 	}
157 };
158 
159 /**
160  * Gets the search field value.
161  * 
162  * @return	{String}	the search field value or an empty string
163  */
164 ZmSearchController.prototype.getSearchFieldValue =
165 function() {
166 	return this._searchToolBar ? this._searchToolBar.getSearchFieldValue() : "";
167 };
168 
169 ZmSearchController.prototype.setEnabled =
170 function(enabled) {
171 	if (this._searchToolBar) {
172 		this._searchToolBar.setEnabled(enabled);
173 	}
174 };
175 
176 /**
177  * Sets the default type. This method provides a programmatic way to set the search type.
178  *
179  * @param {Object}	type		the search type to set as the default
180  */
181 ZmSearchController.prototype.setDefaultSearchType =
182 function(type) {
183 	if (!this._searchToolBar) {
184 		return;
185 	}
186 	var menu = this._searchToolBar.getButton(ZmSearchToolBar.TYPES_BUTTON).getMenu();
187 	menu.checkItem(ZmOperation.MENUITEM_ID, type);
188 	this._searchMenuListener(null, type, true);
189 };
190 
191 /**
192  * @private
193  */
194 ZmSearchController.prototype._setView =
195 function() {
196 
197 	// Create search panel - a composite is needed because the search builder
198 	// element (ZmBrowseView) is added to it (can't add it to the toolbar)
199 	this.searchPanel = new DwtComposite({
200 				parent:		this._container,
201 				className:	"SearchPanel",
202 				posStyle:	Dwt.ABSOLUTE_STYLE
203 			});
204 
205 	this._searchToolBar = new ZmMainSearchToolBar({
206 				parent:	this.searchPanel,
207 				id:		ZmId.SEARCH_TOOLBAR
208 			});
209 
210 	this._createTabGroup();
211 	this._tabGroup.addMember(this._searchToolBar.getChildren());
212 	
213 	// Register keyboard callback for search field
214 	this._searchToolBar.registerEnterCallback(this._toolbarSearch.bind(this));
215 
216 	// Button listeners
217 	this._searchToolBar.addSelectionListener(ZmSearchToolBar.SEARCH_BUTTON, this._searchButtonListener.bind(this));
218 };
219 
220 /**
221  * @private
222  */
223 ZmSearchController.prototype._addMenuListeners =
224 function(menu) {
225 	// Menu listeners
226 	var searchMenuListener = this._searchMenuListener.bind(this);
227 	var items = menu.getItems();
228 	for (var i = 0; i < items.length; i++) {
229 		var item = items[i];
230 		item.addSelectionListener(searchMenuListener);
231 		var mi = item.getData(ZmOperation.MENUITEM_ID);
232 		// set mail as default search
233 		if (mi == ZmId.SEARCH_MAIL) {
234 			item.setChecked(true, true);
235 		}
236 	}
237 };
238 
239 /**
240  * Performs a search and displays the results.
241  *
242  * @param {Hash}	params		a hash of parameters:
243  * 
244  * @param {String}		query						the search string
245  * @param {constant}	searchFor					the semantic type to search for
246  * @param {Array}		types						the item types to search for
247  * @param {constant}	sortBy						the sort constraint
248  * @param {int}			offset						the starting point in list of matching items
249  * @param {int}			limit						the maximum number of items to return
250  * @param {int}			searchId					the ID of owning search folder (if any)
251  * @param {Boolean}		noRender					if <code>true</code>, results will not be passed to controller
252  * @param {Boolean}		userText					if <code>true</code>, text was typed by user into search box
253  * @param {AjxCallback}	callback					the async callback
254  * @param {AjxCallback}	errorCallback				the async callback to run if there is an exception
255  * @param {Object}		response					the canned JSON response (no request will be made)
256  * @param {boolean}		skipUpdateSearchToolbar     don't update the search toolbar (e.g. from the ZmDumpsterDialog where the search is called from its own search toolbar
257  * @param {string}		origin						indicates what initiated the search
258  * @param {string}		sessionId					session ID of search results tab (if search came from one)
259  * @param {Boolean}		noGal     if true, don't search GAL. This is to override the this._contactSource value in contacts search, specifically for clicking on TAGS. 
260  *
261  */
262 ZmSearchController.prototype.search =
263 function(params) {
264 
265 	// if the search string starts with "$set:" then it is a command to the client
266 	if (params.query && (params.query.indexOf("$set:") == 0 || params.query.indexOf("$cmd:") == 0)) {
267 		appCtxt.getClientCmdHandler().execute((params.query.substr(5)), this);
268 		return;
269 	}
270 
271 	params.searchAllAccounts = this.searchAllAccounts;
272 	var respCallback = this._handleResponseSearch.bind(this, params.callback);
273 	this._doSearch(params, params.noRender, respCallback, params.errorCallback);
274 };
275 
276 /**
277  * @private
278  */
279 ZmSearchController.prototype._handleResponseSearch =
280 function(callback, result) {
281 	if (callback) {
282 		callback.run(result);
283 	}
284 };
285 
286 /**
287  * Performs the given search. It takes a ZmSearch, rather than constructing one out of the currently selected menu
288  * choices. Aside from re-executing a search, it can be used to perform a canned search.
289  *
290  * @param {ZmSearch}	search		the search object
291  * @param {Boolean}		noRender		if <code>true</code>, results will not be passed to controller
292  * @param {Object}	changes		the hash of changes to make to search
293  * @param {AjxCallback}	callback		the async callback
294  * @param {AjxCallback}	errorCallback	the async callback to run if there is an exception
295  */
296 ZmSearchController.prototype.redoSearch =
297 function(search, noRender, changes, callback, errorCallback) {
298 
299 	var params = {};
300 	params.query		= search.query;
301 	params.queryHint	= search.queryHint;
302 	params.types		= search.types;
303 	params.forceTypes	= search.forceTypes;
304 	params.sortBy		= search.sortBy;
305 	params.offset		= search.offset;
306 	params.limit		= search.limit;
307 	params.fetch		= search.fetch;
308 	params.searchId		= search.searchId;
309 	params.lastSortVal	= search.lastSortVal;
310 	params.endSortVal	= search.endSortVal;
311 	params.lastId		= search.lastId;
312 	params.soapInfo		= search.soapInfo;
313 	params.accountName	= search.accountName;
314 	params.searchFor	= this._searchFor;
315 	params.idsOnly		= search.idsOnly;
316 	params.inDumpster   = search.inDumpster;
317 	params.userInitiated = search.userInitiated;
318 	params.sessionId	= search.sessionId;
319     params.isEmpty      = search.isEmpty;
320 	params.markRead     = search.markRead;
321 
322 	if (changes) {
323 		for (var key in changes) {
324 			params[key] = changes[key];
325 		}
326 	}
327 
328 	this._doSearch(params, noRender, callback, errorCallback);
329 };
330 
331 /**
332  * Resets search for all accounts.
333  * 
334  */
335 ZmSearchController.prototype.resetSearchAllAccounts =
336 function() {
337 	var button = this.searchAllAccounts && this._searchToolBar.getButton(ZmSearchToolBar.TYPES_BUTTON);
338 	var menu = button && button.getMenu();
339 	var allAccountsMI = menu && menu.getItemById(ZmOperation.MENUITEM_ID, ZmId.SEARCH_ALL_ACCOUNTS);
340 
341 	if (allAccountsMI) {
342 		allAccountsMI.setChecked(false, true);
343 
344 		var selItem = menu.getSelectedItem();
345 		var icon = this._inclSharedItems
346 			? this._getSharedImage(selItem) : selItem.getImage();
347 		button.setImage(icon);
348 
349 		this.searchAllAccounts = false;
350 	}
351 };
352 
353 /**
354  * Resets the search toolbar. This is used by the offline client to "reset" the toolbar whenever user
355  * switches between accounts.
356  * 
357  */
358 ZmSearchController.prototype.resetSearchToolbar =
359 function() {
360 	var smb = this._searchToolBar.getButton(ZmSearchToolBar.TYPES_BUTTON);
361 	var mi = smb ? smb.getMenu().getItemById(ZmOperation.MENUITEM_ID, ZmId.SEARCH_GAL) : null;
362 	if (mi) {
363 		mi.setVisible(appCtxt.getActiveAccount().isZimbraAccount);
364 	}
365 };
366 
367 /**
368  * Gets the item type, based on searchFor. The type is the same as the searchFor, except for mail in which the type is either msg or conv based on view.
369  *
370  * @param   {String}	searchFor		   general description of what to search for
371  * @param   {Boolean}   userInitiated      true if using a search tab
372  * @return	{String}	type
373  * 
374  * @see		#search
375  */
376 ZmSearchController.prototype.getTypeFromSearchFor =
377 function(searchFor, userInitiated) {
378 
379 	var type = searchFor;
380 
381 	if (searchFor === ZmId.SEARCH_MAIL) {
382         var ac = window.parentAppCtxt || window.appCtxt,
383             app = ac.getApp(userInitiated ? ZmApp.SEARCH : ZmApp.MAIL);
384 		type = app ? app.getGroupMailBy() : ZmItem.MSG;
385 	}
386 
387 	return type;
388 };
389 
390 /**
391  * Get the searchFor var which is the same as type except for mail, in which case the type is either msg or conv but searchFor is mail.
392  *
393  * @param {String} type	type of items to search for
394  * @return	{String}	searchFor
395  *
396  * @see		#search
397  */
398 ZmSearchController.prototype.getSearchForFromType =
399 function(type) {
400 	return (type === ZmItem.MSG || type === ZmItem.CONV) ? ZmId.SEARCH_MAIL : type;
401 };
402 
403 /**
404  * Selects the appropriate item in the overview based on the search. Selection only happens
405  * if the search was a simple search for a folder, tag, or saved search. A check is done to
406  * make sure that item is not already selected, so selection should only occur for a query
407  * manually run by the user.
408  *
409  * @param {ZmSearch}	searchObj		the current search
410  */
411 ZmSearchController.prototype.updateOverview = function(searchObj) {
412 
413 	var search = searchObj || appCtxt.getCurrentSearch();
414     if (!search) {
415         return;
416     }
417 
418 	var id, type;
419 	if (search.isSimple() || search.searchId) {
420 		if (search.searchId) {
421 			id = this._getNormalizedId(search.searchId);
422 			type = ZmOrganizer.SEARCH;
423 		}
424         else if (search.folderId) {
425 			id = this._getNormalizedId(search.folderId);
426 			var folderTree = appCtxt.getFolderTree(),
427 			    folder = folderTree && folderTree.getById(id);
428 
429             type = ZmOrganizer.ITEM_ORGANIZER[search.searchFor] || (folder && folder.type) || ZmOrganizer.FOLDER;
430 		}
431         else if (search.tagId) {
432 			id = this._getNormalizedId(search.tagId);
433 			type = ZmOrganizer.TAG;
434 		}
435 
436 		if (type) {
437 			var app = appCtxt.getCurrentApp();
438 			var overview = app && app.getOverview();
439 			if (overview) {
440 				overview.setSelected(id, type);
441 			}
442 		}
443 	}
444 };
445 
446 /**
447  * @private
448  */
449 ZmSearchController.prototype._getSuitableSortBy =
450 function(type) {
451 	var sortBy;
452 
453 	var viewType;
454 	switch (type) {
455 		case ZmItem.CONV:		viewType = ZmId.VIEW_CONVLIST; break;
456 		case ZmItem.MSG:		viewType = ZmId.VIEW_TRAD; break;
457 		case ZmItem.CONTACT:	viewType = ZmId.VIEW_CONTACT_SIMPLE; break;
458 		case ZmItem.APPT:		viewType = ZmId.VIEW_CAL; break;
459 		case ZmItem.TASK:		viewType = ZmId.VIEW_TASKLIST; break;
460 		case ZmId.SEARCH_GAL:	viewType = ZmId.VIEW_CONTACT_SIMPLE; break;
461 		case ZmItem.BRIEFCASE_ITEM:	viewType = ZmId.VIEW_BRIEFCASE_DETAIL; break;
462 		// more types go here as they are suported...
463 	}
464 
465 	if (viewType) {
466 		sortBy = appCtxt.get(ZmSetting.SORTING_PREF, viewType);
467 	}
468 	//bug:1108 & 43789#c19 (changelist 290073) since sort-by-[RCPT|ATTACHMENT|FLAG|PRIORITY] gives exception with querystring.
469 	// Avoided [RCPT|ATTACHMENT|FLAG|PRIORITY] sorting with querysting instead used date sorting
470 	var queryString = this._searchToolBar.getSearchFieldValue();
471 	if (queryString && queryString.length > 0) {
472 		if (sortBy === ZmSearch.RCPT_ASC || sortBy === ZmSearch.RCPT_DESC) {
473 			sortBy = sortBy === ZmSearch.RCPT_ASC ? ZmSearch.DATE_ASC : ZmSearch.DATE_DESC;
474 		}
475 		else if (sortBy === ZmSearch.FLAG_ASC || sortBy === ZmSearch.FLAG_DESC) {
476 			sortBy = sortBy === ZmSearch.FLAG_ASC ? ZmSearch.DATE_ASC : ZmSearch.DATE_DESC;
477 		}
478 		else if (sortBy === ZmSearch.ATTACH_ASC || sortBy === ZmSearch.ATTACH_DESC) {
479 			sortBy = sortBy === ZmSearch.ATTACH_ASC ? ZmSearch.DATE_ASC : ZmSearch.DATE_DESC;
480 		}
481 		else if (sortBy === ZmSearch.PRIORITY_ASC || sortBy === ZmSearch.PRIORITY_DESC) {
482 			sortBy = sortBy === ZmSearch.PRIORITY_ASC ? ZmSearch.DATE_ASC : ZmSearch.DATE_DESC;
483 		}
484 	}
485 
486 	return sortBy;
487 };
488 
489 /**
490  * Performs the search.
491  *
492  * @param {Hash}	params		a hash of params for the search
493  * @param {String}	params.searchFor	the search for
494  * @param {String}	params.query	the search query
495  * @param {String}	params.userText	the user text
496  * @param {Array}	params.type		an array of types
497  * @param {Boolean}	params.forceTypes	use the types we pass, do not override (in case of mail) to the current user's view pref (MSG vs. CONV).
498  * @param {boolean}	params.inclSharedItems		overrides this._inclSharedItems - see ZmTagsHelper._tagClick
499  * @param {boolean} params.forceSearch     Ignores special processing and just executes the search.
500  * @param {Boolean}	noRender		if <code>true</code>, the search results will not be rendered
501  * @param {AjxCallback}	callback		the callback
502  * @param {AjxCallback}	errorCallback	the error callback
503  * @param {boolean} params.skipUpdateSearchToolbar     don't update the search toolbar (e.g. from the ZmDumpsterDialog where the search is called from its own search toolbar
504  * @param {Boolean}		noGal     if true, don't search GAL. This is to override the this._contactSource value in contacts search, specifically for clicking on TAGS.
505  *
506  * @see	#search
507  * 
508  * @private
509  */
510 ZmSearchController.prototype._doSearch =
511 function(params, noRender, callback, errorCallback) {
512 
513 	var searchFor = this._searchFor = params.searchFor || this._searchFor || ZmSearchToolBar.MENU_ITEMS[0];
514 	appCtxt.notifyZimlets("onSearch", [params.query]);
515 
516 	if (!params.skipUpdateSearchToolbar && this._searchToolBar) {
517 		var value = (appCtxt.get(ZmSetting.SHOW_SEARCH_STRING) || params.userText)
518 			? params.query : null;
519 		this._searchToolBar.setSearchFieldValue(value || "");
520 
521 		// bug: 42512 - deselect global inbox if searching via search toolbar
522 		if (appCtxt.multiAccounts && params.userText && this.searchAllAccounts) {
523 			appCtxt.getCurrentApp().getOverviewContainer().deselectAll();
524 		}
525 	}
526 
527 	// get types from search type if not passed in explicitly
528 	// Note - types is now always one value (used to allow all types case, but not anymore).
529 	var types = params.types;
530 	// Support calling it with null, scalar, array or vector, to make sure different clients of this method work.
531 	var type = !types ? searchFor : AjxUtil.toArray(types)[0];
532 
533 	//now make sure the searchFor matches the type (searchFor can be taken from the toolbar, but it's not always what we want, for example
534 	//in the case of saved search)
535 	searchFor = this.getSearchForFromType(type);
536 
537 	//this makes sure for mail we get the type from the user's setting (CONV/MSG).
538 	if (!params.forceTypes) {
539 		type = this.getTypeFromSearchFor(searchFor, params.userInitiated);
540 	}
541 
542 	var types = AjxVector.fromArray([type]); //need this Vector (one item) only for couple more usages below that I'm afraid to change now.
543 
544 	if (searchFor == ZmId.SEARCH_MAIL) {
545 		params = appCtxt.getApp(ZmApp.MAIL).getSearchParams(params);
546 	}
547 
548 	if (searchFor == ZmItem.TASK) {
549 		var tlc = AjxDispatcher.run("GetTaskListController");
550 		params.allowableTaskStatus = tlc && tlc.getAllowableTaskStatus();
551 	}
552 
553 	if (params.searchAllAccounts && !params.queryHint) {
554 		params.queryHint = appCtxt.accountList.generateQuery(null, types);
555 		params.accountName = appCtxt.accountList.mainAccount.name;
556 	}
557 	else if (params.inclSharedItems || this._inclSharedItems) {
558 		// a query hint is part of the query that the user does not see
559 		params.queryHint = ZmSearchController.generateQueryForShares(type);
560 	}
561 
562 	// only set contact source if we are searching for contacts
563 	params.contactSource = !params.noGal && (type === ZmItem.CONTACT || type === ZmId.SEARCH_GAL)
564 		? this._contactSource : null;
565 	if (params.contactSource == ZmId.SEARCH_GAL) {
566 		params.expandDL = true;
567 	}
568 
569 	// find suitable sort by value if not given one (and if applicable)
570 	params.sortBy = params.sortBy || this._getSuitableSortBy(type);
571 	params.types = types;
572 	var search = new ZmSearch(params);
573 
574 	// force drafts folder into msg view
575 	//Also force dumpster search into msg view
576 	if (searchFor === ZmId.SEARCH_MAIL && (params.inDumpster || (!params.isViewSwitch && search.folderId && search.folderId == ZmFolder.ID_DRAFTS))) {
577 		search.types = AjxVector.fromArray([ZmItem.MSG]);
578 		search.isDefaultToMessageView = true;
579 	}
580 
581 	var respCallback = this._handleResponseDoSearch.bind(this, search, noRender, callback, params.noUpdateOverview);
582     var offlineCallback = this._handleOfflineDoSearch.bind(this, search, respCallback);
583     if (search.folderId == ZmFolder.ID_OUTBOX) {
584         var offlineRequest = true;
585     }
586 	if (!errorCallback) {
587 		errorCallback = this._handleErrorDoSearch.bind(this, search);
588 		if (!params.errorCallback) {
589 			params.errorCallback = errorCallback;
590 		}
591 	}
592 
593 	// calendar searching is special so hand it off if necessary
594 	search.calController = null;
595 	if (searchFor == ZmItem.APPT && !params.forceSearch && !params.inDumpster) {
596 		var searchResultsController, sessionId;
597 		if (search.userInitiated && ZmApp.SEARCH_RESULTS_TAB[ZmApp.CALENDAR]) {
598 			searchResultsController = appCtxt.getApp(ZmApp.SEARCH).getSearchResultsController(search.sessionId, ZmApp.CALENDAR);
599 			sessionId = searchResultsController.getCurrentViewId();
600 		}
601 		var controller = AjxDispatcher.run("GetCalController", sessionId, searchResultsController);
602 		if (controller && type === ZmItem.APPT) {
603 			search.calController = controller;
604 			controller.handleUserSearch(params, respCallback);
605 		} else {
606             search.execute({offlineCache:params && params.offlineCache, callback:respCallback, errorCallback:errorCallback, offlineCallback:offlineCallback, offlineRequest:offlineRequest});
607         }
608 	} else {
609 		search.execute({offlineCache:params && params.offlineCache, callback:respCallback, errorCallback:errorCallback, offlineCallback:offlineCallback, offlineRequest:offlineRequest});
610 	}
611 };
612 
613 /**
614  * Takes the search result and hands it to the appropriate controller for display.
615  *
616  * @param {ZmSearch}	search			contains search info used to run search against server
617  * @param {Boolean}		noRender		<code>true</code> to skip rendering results
618  * @param {AjxCallback}	callback		the callback to run after processing search response
619  * @param {Boolean}	noUpdateOverview	<code>true</code> to skip updating the overview
620  * @param {ZmCsfeResult}	result			the search results
621  */
622 ZmSearchController.prototype._handleResponseDoSearch =
623 function(search, noRender, callback, noUpdateOverview, result) {
624 
625 	DBG.println("s", "SEARCH was user initiated: " + Boolean(search.userInitiated));
626 	var results = result && result.getResponse();
627 	if (!results) { return; }
628 
629 	if (!results.type) {
630 		results.type = search.types.get(0);
631 	}
632 
633 	this.currentSearch = search;
634 	DBG.timePt("execute search", true);
635 
636 	if (!noRender) {
637 		this._showResults(results, search, noUpdateOverview);
638 	}
639 
640 	if (callback) {
641 		callback.run(result);
642 	}
643 };
644 
645 /**
646  * Takes the search result and hands it to the appropriate controller for display.
647  *
648  * @param {ZmSearch}	search			contains search info used to run search against server
649  * @param {AjxCallback}	callback		the callback to run after generating offline result
650  */
651 ZmSearchController.prototype._handleOfflineDoSearch =
652 function(search, callback) {
653 	//force webclient offline mode into msg view for mail search
654 	if (search.types && search.types.replaceObject(ZmItem.CONV, ZmItem.MSG)) {
655 		search.isDefaultToMessageView = true;
656 	}
657     var respCallback = this._handleOfflineResponseDoSearch.bind(this, search, callback);
658     ZmOfflineDB.search(search, respCallback);
659 };
660 
661 /**
662  * @private
663  */
664 ZmSearchController.prototype._showResults =
665 function(results, search, noUpdateOverview) {
666 
667 	this._results = results = (results && results.isZmSearchResult) ? results : new ZmSearchResult(search);
668 
669 	DBG.timePt("handle search results");
670 
671     var ac = window.parentAppCtxt || window.appCtxt;
672 	if (ac.get(ZmSetting.SAVED_SEARCHES_ENABLED)) {
673 		var saveBtn = this._searchToolBar && this._searchToolBar.getButton(ZmSearchToolBar.SAVE_BUTTON);
674 		if (saveBtn) {
675 			saveBtn.setEnabled(this._contactSource != ZmId.SEARCH_GAL);
676 		}
677 	}
678 
679 	var app = search.calController ? ac.getApp(ZmApp.CALENDAR) : ac.getApp(ZmItem.APP[results.type]) || ac.getCurrentApp();
680 	var appName = app.getName();
681 	if (search.userInitiated && ZmApp.SEARCH_RESULTS_TAB[appName]) {
682 		var ctlr = (search.calController && search.calController.searchResultsController) ||
683 					ac.getApp(ZmApp.SEARCH).getSearchResultsController(search.sessionId, appName);
684 		DBG.println("sr", "New search results controller: " + ctlr.viewId);
685 		ctlr.show(results, search.calController);
686 		this._searchToolBar.setSearchFieldValue("");
687 	}
688 	else if (app.showSearchResults) {
689 		// show results based on type - may invoke package load
690 		var loadCallback = this._handleLoadShowResults.bind(this, results, search, noUpdateOverview);
691 		app.currentSearch = search;
692 		app.currentQuery = search.query;
693 		app.showSearchResults(results, loadCallback);
694 	}
695 };
696 
697 // Opens a new, empty search tab
698 ZmSearchController.prototype.openNewSearchTab = function() {
699 	this._toolbarSearch({
700 		isEmpty:    true,
701 		origin:     ZmId.SEARCH
702 	});
703 };
704 
705 /**
706  * @private
707  */
708 ZmSearchController.prototype._handleLoadShowResults =
709 function(results, search, noUpdateOverview) {
710 	appCtxt.setCurrentList(results.getResults(results.type));
711 	if (!noUpdateOverview) {
712 		this.updateOverview(search);
713 	}
714 	DBG.timePt("render search results");
715 };
716 
717 /**
718  * Handle a few minor errors where we show an empty result set and issue a
719  * status message to indicate why the query failed. Those errors are: no such
720  * folder, no such tag, and bad query. If it's a "no such folder" error caused
721  * by the deletion of a folder backing a mountpoint, we pass it along for
722  * special handling by ZmZimbraMail.
723  *
724  * @private
725  */
726 ZmSearchController.prototype._handleErrorDoSearch =
727 function(search, ex) {
728 	DBG.println(AjxDebug.DBG1, "Search exception: " + ex.code);
729     if (ex.code == ZmCsfeException.MAIL_NO_SUCH_TAG ||
730 		ex.code == ZmCsfeException.MAIL_QUERY_PARSE_ERROR ||
731 		ex.code == ZmCsfeException.MAIL_TOO_MANY_TERMS ||
732 		(ex.code == ZmCsfeException.MAIL_NO_SUCH_FOLDER && !(ex.data.itemId && ex.data.itemId.length)))
733 	{
734 		var msg = ex.getErrorMsg();
735 		appCtxt.setStatusMsg(msg, ZmStatusView.LEVEL_WARNING);
736 		return true;
737 	}
738 	return false;
739 };
740 
741 /**
742  * Provides a string to add to the query when the search includes shared items.
743  * 
744  * @param {String} type		item type
745  * 
746  * @private
747  */
748 ZmSearchController.generateQueryForShares =
749 function(type, account) {
750 	var ac = window.parentAppCtxt || window.appCtxt;
751 	var list = [];
752 	var app = ac.getApp(ZmItem.APP[type]);
753 	if (!app) {
754 		return null;
755 	}
756 	var ids = app.getRemoteFolderIds(account);
757 	for (var i = 0; i < ids.length; i++) {
758 		var id = ids[i];
759 		var idText = AjxUtil.isNumeric(id) ? id : ['"', id, '"'].join("");
760 		list.push("inid:" + idText);
761 	}
762 
763 	if (list.length > 0) {
764 		list.push("is:local");
765 		return list.join(" OR ");
766 	}
767 
768 	return null;
769 };
770 
771 // called when the search button has been pressed
772 ZmSearchController.prototype._searchButtonListener =
773 function(ev) {
774 	this._toolbarSearch({
775 				ev:				ev,
776 				zimletEvent:	"onSearchButtonClick",
777 				origin:			ZmId.SEARCH
778 			});
779 };
780 
781 /**
782  * Runs a search based on the state of the toolbar.
783  * 
784  * @param {Hash}	params		a hash of parameters:
785  * 
786  * @param {Event}		ev							browser event	
787  * @param {string}		zimletEvent					type of notification to send zimlets
788  * @param {string}		query						search string (optional, overrides input field)
789  * @param {Boolean}     isEmpty                     force a search for ""
790  * @param {string}		origin						indicates what initiated the search
791  * @param {string}		sessionId					session ID of search results tab (if search came from one)
792  * @param {boolean}		skipUpdateSearchToolbar     don't update the search toolbar (e.g. from the ZmDumpsterDialog where the search is called from its own search toolbar
793  * @param {String}		sortBy
794  * 
795  * @private
796  */
797 ZmSearchController.prototype._toolbarSearch =
798 function(params) {
799 
800 	// find out if the custom search menu item is selected and pass it the event
801 	var result = params.searchFor || this._searchToolBar.getSearchType();
802 	if (result && result.listener) {
803 		result.listener.run(params.ev);
804 	} else {
805 		var queryString = !params.isEmpty ? params.query || this._searchToolBar.getSearchFieldValue() : "";
806 		var userText = (queryString.length > 0);
807 		if (queryString) {
808 			this._currentQuery = null;
809 		} else {
810 			queryString = this._currentQuery || "";
811 		}
812 
813 		appCtxt.notifyZimlets(params.zimletEvent, [queryString]);
814 		var searchParams = {
815 			query:						queryString,
816 			userText:					userText,
817 			userInitiated:				true,
818 			getHtml:					appCtxt.get(ZmSetting.VIEW_AS_HTML),
819 			searchFor:					result,
820 			skipUpdateSearchToolbar:	params.skipUpdateSearchToolbar,
821 			origin:						params.origin,
822 			sessionId:					params.sessionId,
823 			errorCallback:				params.errorCallback,
824 			sortBy:						params.sortBy,
825 			isEmpty:					params.isEmpty || !queryString
826 		};
827 		this.search(searchParams);
828 	}
829 };
830 
831 /**
832  * @private
833  */
834 ZmSearchController.prototype._searchMenuListener =
835 function(ev, id, noFocus) {
836 	var btn = this._searchToolBar.getButton(ZmSearchToolBar.TYPES_BUTTON);
837 	if (!btn) { return; }
838 
839 	var menu = btn.getMenu();
840 	var item = ev ? ev.item : (menu.getItemById(ZmOperation.MENUITEM_ID, id));
841 
842 	if (!item || (!!(item._style & DwtMenuItem.SEPARATOR_STYLE))) { return; }
843 	id = item.getData(ZmOperation.MENUITEM_ID);
844 
845 	var selItem = menu.getSelectedItem();
846 	var sharedMI = menu.getItemById(ZmOperation.MENUITEM_ID, ZmId.SEARCH_SHARED);
847 
848 	// enable shared menu item if not a gal search
849 	if (id == ZmId.SEARCH_GAL) {
850 		this._contactSource = ZmId.SEARCH_GAL;
851 		if (sharedMI) {
852 			sharedMI.setChecked(false, true);
853 			sharedMI.setEnabled(false);
854 		}
855 	} else {
856 		if (sharedMI) {
857 			// we allow user to check "Shared Items" for appointments since it
858 			// is based on whats checked in their tree view
859 			if (id == ZmItem.APPT || id == ZmId.SEARCH_CUSTOM) {
860 				if (this._sharedMenuItemChecked == null) {
861 					this._sharedMenuItemChecked = sharedMI.getChecked();
862 				}
863 				sharedMI.setChecked(false, true);
864 				sharedMI.setEnabled(false);
865 			} else {
866 				sharedMI.setEnabled(true);
867 				if (this._sharedMenuItemChecked) {
868 					sharedMI.setChecked(true, true);
869 				}
870 				this._sharedMenuItemChecked = null;
871 			}
872 		}
873 		this._contactSource = ZmItem.CONTACT;
874 	}
875 	this._inclSharedItems = sharedMI && sharedMI.getChecked();
876 
877 	// search all accounts? Only applies to multi-account mbox
878 	var allAccountsMI = menu.getItemById(ZmOperation.MENUITEM_ID, ZmId.SEARCH_ALL_ACCOUNTS);
879 	if (allAccountsMI) {
880 		if (id == ZmItem.APPT) {
881 			this.resetSearchAllAccounts();
882 			allAccountsMI.setEnabled(false);
883 		} else {
884 			allAccountsMI.setEnabled(true);
885 			this.searchAllAccounts = allAccountsMI && allAccountsMI.getChecked();
886 		}
887 	}
888 
889 	if (id == ZmId.SEARCH_SHARED) {
890 		var icon = this.searchAllAccounts
891 			? allAccountsMI.getImage() : selItem.getImage();
892 
893 		if (this._inclSharedItems) {
894 			icon = this._getSharedImage(selItem);
895 		}
896 
897 		btn.setImage(icon);
898 	}
899 	else if (id == ZmId.SEARCH_ALL_ACCOUNTS) {
900 		var icon = (this.searchAllAccounts && !this._inclSharedItems)
901 			? item.getImage()
902 			: (this._inclSharedItems) ? this._getSharedImage(selItem) : selItem.getImage();
903 		btn.setImage(icon);
904 	}
905 	else {
906 		// only set search for if a "real" search-type menu item was clicked
907 		this._searchFor = id;
908 		var icon = item.getImage();
909 
910 		if (this._inclSharedItems) {
911 			icon = this._getSharedImage(selItem);
912 		}
913 		else if (this.searchAllAccounts) {
914 			icon = allAccountsMI.getImage();
915 		}
916 
917 		btn.setImage(icon);
918 	}
919 
920 	// set button tooltip
921 	var tooltip = ZmMsg[ZmSearchToolBar.TT_MSG_KEY[id]];
922 	if (id != ZmId.SEARCH_SHARED && id != ZmId.SEARCH_ALL_ACCOUNTS) {
923 		btn.setToolTipContent(tooltip);
924 		btn.setAttribute('aria-label', tooltip);
925 	}
926 	
927 	if (!noFocus) {
928 		// restore focus to INPUT if user changed type
929 		setTimeout(this._searchToolBar.focus.bind(this._searchToolBar), 10);
930 	}
931 };
932 
933 /**
934  * @private
935  */
936 ZmSearchController.prototype._getSharedImage =
937 function(selItem) {
938 	var selItemId = selItem && selItem.getData(ZmOperation.MENUITEM_ID);
939 	return (selItemId && ZmSearchToolBar.SHARE_ICON[selItemId])
940 		? ZmSearchToolBar.SHARE_ICON[selItemId]
941 		: ZmSearchToolBar.ICON[selItemId]; //use regular icon if no share icon
942 };
943 
944 /**
945  * @private
946  */
947 ZmSearchController.prototype._getNormalizedId =
948 function(id) {
949 	var nid = id;
950 
951 	var acct = appCtxt.getActiveAccount();
952 	if (!acct.isMain && id.indexOf(":") == -1) {
953 		nid = acct.id + ":" + id;
954 	}
955 
956 	return nid;
957 };
958 
959 /**
960  * Takes the search result and hands it to the appropriate controller for display.
961  *
962  * @param {ZmSearch}	search			contains search info used to run search against server
963  * @param {AjxCallback}	callback		online response callback to run after generating search response
964  * @param {Object}	    result			the object stored in indexedDB
965  *
966  * @private
967  */
968 ZmSearchController.prototype._handleOfflineResponseDoSearch =
969 function(search, callback, result) {
970 
971     if (search.sortBy === ZmSearch.DATE_DESC) {
972 		//Sort by received date descending
973 		result.sort(function(a, b) {
974 			return b.d - a.d;
975 	    });
976     }
977 	else if (search.sortBy === ZmSearch.DATE_ASC) {
978 	    //Sort by received date ascending
979 	    result.sort(function(a, b) {
980 		    return a.d - b.d;
981 	    });
982     }
983 
984     var searchResult = new ZmSearchResult(search);
985     if (search.searchFor === ZmId.SEARCH_MAIL || search.parsedSearchFor === ZmId.SEARCH_MAIL) {
986         search.types =  new AjxVector([ZmItem.MSG]);
987         searchResult.set({m : result});
988     }
989     else if (search.searchFor === ZmItem.CONTACT || search.contactSource === ZmItem.CONTACT) {
990         searchResult.set({cn : result});
991     }
992     var zmCsfeResult = new ZmCsfeResult(searchResult);
993     callback(zmCsfeResult);
994 
995     if (search.folderId == ZmFolder.ID_OUTBOX || search.folderId == ZmFolder.ID_DRAFTS) {
996 		ZmOffline.updateFolderCountCallback(search.folderId, result.length);
997     }
998 };
999 
1000 ZmSearchController.prototype._addOfflineDrafts =
1001 function(search, result) {
1002     var callback = this._addOfflineDraftsCallback.bind(this, search, result);
1003     var key = {methodName : "SaveDraftRequest"};
1004     ZmOfflineDB.getItemInRequestQueue(key, callback, callback);
1005 };
1006 
1007 ZmSearchController.prototype._addOfflineDraftsCallback =
1008 function(search, result, newResult) {
1009     var respEl = ZmOffline.generateMsgResponse(newResult);
1010     this._handleResponseDoIndexedDBSearch(search, [].concat(result).concat(respEl));
1011 };
1012