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 contains the contacts application class.
 27  */
 28 
 29 /**
 30  * Creates and initializes the contacts application.
 31  * @class
 32  * The contacts app manages the creation and display of contacts, which are grouped
 33  * into address books.
 34  * 
 35  * @param	{DwtControl}	container		the container
 36  * @param	{ZmController}	parentController	the parent controller
 37  * 
 38  * @author Conrad Damon
 39  * 
 40  * @extends		ZmApp
 41  */
 42 ZmContactsApp = function(container, parentController) {
 43 
 44 	ZmApp.call(this, ZmApp.CONTACTS, container, parentController);
 45 
 46 	this.contactsLoaded = {};
 47 	this._contactList = {};		// canonical list by acct ID
 48 	this._initialized = false;
 49 
 50 	// contact lookup caches
 51 	this._byEmail	= {};
 52 	this._byPhone	= {};
 53 
 54 	// cache fetched distribution lists
 55 	this._dlCache	= {};
 56 };
 57 
 58 ZmContactsApp.prototype = new ZmApp;
 59 ZmContactsApp.prototype.constructor = ZmContactsApp;
 60 
 61 ZmContactsApp.prototype.isZmContactsApp = true;
 62 ZmContactsApp.prototype.toString = function() { return "ZmContactsApp"; };
 63 
 64 
 65 // Organizer and item-related constants
 66 ZmEvent.S_CONTACT				= ZmId.ITEM_CONTACT;
 67 ZmEvent.S_GROUP					= ZmId.ITEM_GROUP;
 68 ZmItem.CONTACT					= ZmEvent.S_CONTACT;
 69 ZmItem.GROUP					= ZmEvent.S_GROUP;
 70 ZmItem.GAL						= ZmId.ITEM_GAL_CONTACT;
 71 /**
 72  * Defines the "address book" organizer.
 73  */
 74 ZmOrganizer.ADDRBOOK			= ZmId.ORG_ADDRBOOK;
 75 
 76 // App-related constants
 77 /**
 78  * Defines the "address book" application.
 79  */
 80 ZmApp.CONTACTS							= ZmId.APP_CONTACTS;
 81 ZmApp.CLASS[ZmApp.CONTACTS]				= "ZmContactsApp";
 82 ZmApp.SETTING[ZmApp.CONTACTS]			= ZmSetting.CONTACTS_ENABLED;
 83 ZmApp.UPSELL_SETTING[ZmApp.CONTACTS]	= ZmSetting.CONTACTS_UPSELL_ENABLED;
 84 ZmApp.LOAD_SORT[ZmApp.CONTACTS]			= 30;
 85 ZmApp.QS_ARG[ZmApp.CONTACTS]			= "contacts";
 86 
 87 // search menu
 88 ZmContactsApp.SEARCHFOR_CONTACTS 	= 1;
 89 ZmContactsApp.SEARCHFOR_GAL 		= 2;
 90 ZmContactsApp.SEARCHFOR_PAS			= 3; // PAS = personal and shared
 91 ZmContactsApp.SEARCHFOR_FOLDERS		= 4;
 92 
 93 ZmContactsApp.SEARCHFOR_MAX 		= 50;
 94 
 95 
 96 // Construction
 97 
 98 /**
 99  * @private
100  */
101 ZmContactsApp.prototype._defineAPI =
102 function() {
103 	AjxDispatcher.setPackageLoadFunction("ContactsCore", new AjxCallback(this, this._postLoadCore));
104 	AjxDispatcher.setPackageLoadFunction("Contacts", new AjxCallback(this, this._postLoad, ZmOrganizer.ADDRBOOK));
105 	AjxDispatcher.registerMethod("GetContacts", "ContactsCore", new AjxCallback(this, this.getContactList));
106 	AjxDispatcher.registerMethod("GetContactsForAllAccounts", "ContactsCore", new AjxCallback(this, this.getContactListForAllAccounts));
107 	AjxDispatcher.registerMethod("GetContactListController", ["ContactsCore", "Contacts"], new AjxCallback(this, this.getContactListController));
108 	AjxDispatcher.registerMethod("GetContactController", ["ContactsCore", "Contacts"], new AjxCallback(this, this.getContactController));
109 };
110 
111 /**
112  * @private
113  */
114 ZmContactsApp.prototype._registerSettings =
115 function(settings) {
116 	var settings = settings || appCtxt.getSettings();
117 	settings.registerSetting("AUTO_ADD_ADDRESS",				{name: "zimbraPrefAutoAddAddressEnabled", type: ZmSetting.T_PREF, dataType: ZmSetting.D_BOOLEAN, defaultValue: false, isGlobal: true});
118 	settings.registerSetting("AUTOCOMPLETE_LIMIT",				{name: "zimbraContactAutoCompleteMaxResults", type:ZmSetting.T_COS, dataType: ZmSetting.D_INT, defaultValue: 20});
119 	settings.registerSetting("AUTOCOMPLETE_ON_COMMA",			{name: "zimbraPrefAutoCompleteQuickCompletionOnComma", type: ZmSetting.T_PREF, dataType:ZmSetting.D_BOOLEAN, defaultValue: true});
120 	settings.registerSetting("AUTOCOMPLETE_SHARE",				{name: "zimbraPrefShareContactsInAutoComplete", type: ZmSetting.T_PREF, dataType: ZmSetting.D_BOOLEAN, defaultValue: false});
121 	settings.registerSetting("AUTOCOMPLETE_SHARED_ADDR_BOOKS",	{name: "zimbraPrefSharedAddrBookAutoCompleteEnabled", type: ZmSetting.T_PREF, dataType: ZmSetting.D_BOOLEAN, defaultValue: false});
122 	settings.registerSetting("EXPORT",							{type: ZmSetting.T_PREF, dataType: ZmSetting.D_NONE});
123 	settings.registerSetting("GAL_AUTOCOMPLETE",				{name: "zimbraPrefGalAutoCompleteEnabled", type: ZmSetting.T_PREF, dataType: ZmSetting.D_BOOLEAN, defaultValue: false});
124 	settings.registerSetting("IMPORT",							{type: ZmSetting.T_PREF, dataType: ZmSetting.D_NONE});
125 	settings.registerSetting("MAX_CONTACTS",					{name: "zimbraContactMaxNumEntries", type: ZmSetting.T_COS, dataType: ZmSetting.D_INT, defaultValue: 0});
126 	settings.registerSetting("NEW_ADDR_BOOK_ENABLED",			{name: "zimbraFeatureNewAddrBookEnabled", type:ZmSetting.T_COS, dataType: ZmSetting.D_BOOLEAN, defaultValue: true});
127     // TODO: Make real COS setting? 
128 	settings.registerSetting("PHONETIC_CONTACT_FIELDS",         {type: ZmSetting.T_COS, dataType: ZmSetting.D_BOOLEAN, defaultValue: /^ja/.test(AjxEnv.DEFAULT_LOCALE)});
129 	settings.registerSetting("DETAILED_CONTACT_SEARCH_ENABLED",	{name: "zimbraFeatureContactsDetailedSearchEnabled", type: ZmSetting.T_COS, dataType: ZmSetting.D_BOOLEAN, defaultValue: false});
130 };
131 
132 /**
133  * @private
134  */
135 ZmContactsApp.prototype._registerPrefs =
136 function() {
137 	var sections = {
138 		CONTACTS: {
139 			title: ZmMsg.addressBook,
140 			icon: "ContactsApp",
141 			templateId: "prefs.Pages#Contacts",
142 			priority: 70,
143 			precondition: ZmSetting.CONTACTS_ENABLED,
144 			prefs: [
145 				ZmSetting.AUTO_ADD_ADDRESS,
146 				ZmSetting.AUTOCOMPLETE_ON_COMMA,
147 				ZmSetting.AUTOCOMPLETE_SHARE,
148 				ZmSetting.AUTOCOMPLETE_SHARED_ADDR_BOOKS,
149 				ZmSetting.EXPORT,
150 				ZmSetting.GAL_AUTOCOMPLETE,
151 				ZmSetting.INITIALLY_SEARCH_GAL,
152 				ZmSetting.IMPORT
153 			]
154 		}
155 	};
156 	for (var id in sections) {
157 		ZmPref.registerPrefSection(id, sections[id]);
158 	}
159 
160 	ZmPref.registerPref("AUTO_ADD_ADDRESS", {
161 		displayName:		ZmMsg.autoAddContacts,
162 		displayContainer:	ZmPref.TYPE_CHECKBOX
163 	});
164 
165 	ZmPref.registerPref("AUTOCOMPLETE_ON_COMMA", {
166 		displayName:		ZmMsg.autocompleteOnComma,
167 		displayContainer:	ZmPref.TYPE_CHECKBOX
168 	});
169 
170 	ZmPref.registerPref("AUTOCOMPLETE_SHARE", {
171 		displayName:		ZmMsg.autocompleteShare,
172 		displayContainer:	ZmPref.TYPE_CHECKBOX
173 	});
174 
175 	ZmPref.registerPref("AUTOCOMPLETE_SHARED_ADDR_BOOKS", {
176 		displayName:		ZmMsg.autocompleteSharedAddrBooks,
177 		displayContainer:	ZmPref.TYPE_CHECKBOX
178 	});
179 
180 	ZmPref.registerPref("EXPORT", {
181 		loadFunction:		ZmPref.loadCsvFormats,
182 		displayContainer:	ZmPref.TYPE_EXPORT
183 	});
184 
185 	ZmPref.registerPref("GAL_AUTOCOMPLETE", {
186 		displayName:		ZmMsg.galAutocomplete,
187 		displayContainer:	ZmPref.TYPE_CHECKBOX,
188 		precondition:       [ ZmSetting.GAL_AUTOCOMPLETE_ENABLED, ZmSetting.GAL_ENABLED ]
189 	});
190 
191 	ZmPref.registerPref("IMPORT", {
192 		displayName:		ZmMsg.importFromCSV,
193 		displayContainer:	ZmPref.TYPE_IMPORT
194 	});
195 
196 	ZmPref.registerPref("INITIALLY_SEARCH_GAL", {
197 		displayName:		ZmMsg.initiallySearchGal,
198 		displayContainer:	ZmPref.TYPE_CHECKBOX,
199 		precondition:       function() {
200 								return appCtxt.get(ZmSetting.GAL_ENABLED) && appCtxt.getActiveAccount().isZimbraAccount;
201 							}
202 	});
203 };
204 
205 /**
206  * @private
207  */
208 ZmContactsApp.prototype._createVirtualFolders =
209 function() {
210 	if (!window.ZmContactList || !window.ZmAddrBook) {
211 		return; //do it only if it's loaded. If not, it will be loaded when called from ZmContactList.prototype._handleResponseLoad
212 	}
213 	ZmContactList.addDlFolder();
214 };
215 
216 
217 /**
218  * @private
219  */
220 ZmContactsApp.prototype._registerOperations =
221 function() {
222 	ZmOperation.registerOp(ZmId.OP_CONTACT);	// placeholder
223 	ZmOperation.registerOp(ZmId.OP_EDIT_CONTACT, {textKey:"AB_EDIT_CONTACT", image:"Edit", shortcut:ZmKeyMap.EDIT});
224 //	ZmOperation.registerOp(ZmId.OP_MOUNT_ADDRBOOK, {textKey:"mountAddrBook", image:"ContactsFolder"});
225 	ZmOperation.registerOp(ZmId.OP_NEW_ADDRBOOK, {textKey:"newAddrBook", tooltipKey:"newAddrBookTooltip", image:"NewContactsFolder"}, ZmSetting.NEW_ADDR_BOOK_ENABLED);
226 	ZmOperation.registerOp(ZmId.OP_NEW_CONTACT, {textKey:"newContact", tooltipKey:"newContactTooltip", image:"NewContact", shortcut:ZmKeyMap.NEW_CONTACT}, ZmSetting.CONTACTS_ENABLED);
227 	ZmOperation.registerOp(ZmId.OP_NEW_GROUP, {textKey:"newGroup", tooltipKey:"newGroupTooltip", image:"NewGroup"}, ZmSetting.CONTACTS_ENABLED);
228 	ZmOperation.registerOp(ZmId.OP_NEW_DISTRIBUTION_LIST, {textKey:"newDistList", tooltipKey:"newDistListTooltip", image:"NewGroup"}, ZmSetting.CONTACTS_ENABLED);
229 	ZmOperation.registerOp(ZmId.OP_PRINT_CONTACT, {textKey:"printContact", image:"Print", shortcut:ZmKeyMap.PRINT}, ZmSetting.PRINT_ENABLED);
230 	ZmOperation.registerOp(ZmId.OP_PRINT_ADDRBOOK, {textKey:"printAddrBook", image:"Print"}, ZmSetting.PRINT_ENABLED);
231 	ZmOperation.registerOp(ZmId.OP_SHARE_ADDRBOOK, {textKey:"shareAddrBook", image:"SharedContactsFolder"});
232 };
233 
234 /**
235  * @private
236  */
237 ZmContactsApp.prototype._registerItems =
238 function() {
239 	ZmItem.registerItem(ZmItem.CONTACT,
240 						{app:			ZmApp.CONTACTS,
241 						 nameKey:		"contact",
242 						 icon:			"Contact",
243 						 soapCmd:		"ContactAction",
244 						 itemClass:		"ZmContact",
245 						 node:			"cn",
246 						 organizer:		ZmOrganizer.ADDRBOOK,
247 						 dropTargets:	[ZmOrganizer.TAG, ZmOrganizer.ZIMLET, ZmOrganizer.ADDRBOOK],
248 						 searchType:	"contact",
249 						 resultsList:
250 		AjxCallback.simpleClosure(function(search) {
251 			AjxDispatcher.require("ContactsCore");
252 			return new ZmContactList(search, search ? search.isGalSearch || search.isGalAutocompleteSearch : null);
253 		}, this)
254 						});
255 
256 	ZmItem.registerItem(ZmItem.GROUP,
257 						{nameKey:	"group",
258 						 icon:		"Group",
259 						 soapCmd:	"ContactAction"
260 						});
261 
262 	ZmItem.registerItem(ZmItem.GAL, {app: ZmApp.CONTACTS});
263 };
264 
265 /**
266  * @private
267  */
268 ZmContactsApp.prototype._registerOrganizers =
269 function() {
270 	var orgColor = {};
271 //	orgColor[ZmFolder.ID_AUTO_ADDED] = ZmOrganizer.C_YELLOW;
272 	
273 	ZmOrganizer.registerOrg(ZmOrganizer.ADDRBOOK,
274 							{app:				ZmApp.CONTACTS,
275 							 nameKey:			"addressBook",
276 							 defaultFolder:		ZmOrganizer.ID_ADDRBOOK,
277 							 soapCmd:			"FolderAction",
278 							 firstUserId:		256,
279 							 orgClass:			"ZmAddrBook",
280 							 orgPackage:		"ContactsCore",
281 							 treeController:	"ZmAddrBookTreeController",
282 							 labelKey:			"contactLists",
283 							 itemsKey:			"contacts",
284 							 hasColor:			true,
285 							 defaultColor:		ZmOrganizer.C_NONE,
286 							 orgColor:			orgColor,
287 							 treeType:			ZmOrganizer.FOLDER,
288 							 dropTargets:		[ZmOrganizer.ADDRBOOK],
289 							 views:				["contact"],
290 							 folderKey:			"contactsFolder",
291 							 mountKey:			"mountAddrBook",
292 							 createFunc:		"ZmOrganizer.create",
293 							 compareFunc:		"ZmFolder.sortCompareNonMail",
294 							 displayOrder:		100,
295 							 newOp:             ZmOperation.NEW_ADDRBOOK,
296 							 deferrable:		true
297 							});
298 };
299 
300 /**
301  * @private
302  */
303 ZmContactsApp.prototype._setupSearchToolbar =
304 function() {
305 	ZmSearchToolBar.addMenuItem(ZmItem.CONTACT,
306 								{msgKey:		"contacts",
307 								 tooltipKey:	"searchPersonalContacts",
308 								 icon:			"Contact",
309 								 shareIcon:		"SharedContactsFolder",
310 								 id:			ZmId.getMenuItemId(ZmId.SEARCH, ZmId.ITEM_CONTACT)
311 								});
312 
313 	ZmSearchToolBar.addMenuItem(ZmId.SEARCH_GAL,
314 								{msgKey:		"GAL",
315 								 tooltipKey:	"searchGALContacts",
316 								 icon:			"GAL",
317 								 setting:		ZmSetting.GAL_ENABLED,
318 								 id:			ZmId.getMenuItemId(ZmId.SEARCH, ZmId.SEARCH_GAL),
319 								 disableOffline:true
320 								});
321 };
322 
323 /**
324  * @private
325  */
326 ZmContactsApp.prototype._registerApp =
327 function() {
328 	var newItemOps = {};
329 	newItemOps[ZmOperation.NEW_CONTACT]	= "contact";
330 	newItemOps[ZmOperation.NEW_GROUP]	= "group";
331 	if (appCtxt.createDistListAllowed) {
332 		newItemOps[ZmOperation.NEW_DISTRIBUTION_LIST] = "distributionList";
333 	}
334 
335 	var newOrgOps = {};
336 	newOrgOps[ZmOperation.NEW_ADDRBOOK] = "contactsFolder";
337 
338 	var actionCodes = {};
339 	actionCodes[ZmKeyMap.NEW_CONTACT] = ZmOperation.NEW_CONTACT;
340 
341 	ZmApp.registerApp(ZmApp.CONTACTS,
342 							 {mainPkg:				"Contacts",
343 							  nameKey:				"addressBook",
344 							  icon:					"ContactsApp",
345 							  textPrecedence:		40,
346 							  chooserTooltipKey:	"goToContacts",
347 							  viewTooltipKey:		"displayContacts",
348 							  defaultSearch:		ZmItem.CONTACT,
349 							  organizer:			ZmOrganizer.ADDRBOOK,
350 							  overviewTrees:		[ZmOrganizer.ADDRBOOK, ZmOrganizer.SEARCH, ZmOrganizer.TAG],
351 							  searchTypes:			[ZmItem.CONTACT],
352 							  newItemOps:			newItemOps,
353 							  newOrgOps:			newOrgOps,
354 							  actionCodes:			actionCodes,
355 							  gotoActionCode:		ZmKeyMap.GOTO_CONTACTS,
356 							  newActionCode:		ZmKeyMap.NEW_CONTACT,
357 							  trashViewOp:			ZmOperation.SHOW_ONLY_CONTACTS,
358 							  chooserSort:			20,
359 							  defaultSort:			40,
360 							  upsellUrl:			ZmSetting.CONTACTS_UPSELL_URL,
361 							  //quickCommandType:		ZmQuickCommand[ZmId.ITEM_CONTACT],
362 							  searchResultsTab:		true
363 							  });
364 };
365 
366 
367 // App API
368 
369 /**
370  * Checks for the creation of an address book or a mount point to one. Regular
371  * contact creates are handed to the canonical list.
372  * 
373  * @param {Hash}	creates	a hash of create notifications
374  * 
375  * @private
376  */
377 ZmContactsApp.prototype.createNotify =
378 function(creates, force) {
379 	if (!creates["folder"] && !creates["cn"] && !creates["link"]) { return; }
380 	if (!force && !this._noDefer && this._deferNotifications("create", creates)) { return; }
381 
382 	for (var name in creates) {
383 		var list = creates[name];
384 		if (list && list.length) {
385 			for (var i = 0; i < list.length; i++) {
386 				var create = list[i];
387 				if (appCtxt.cacheGet(create.id)) { continue; }
388 
389 				if (name == "folder") {
390 					this._handleCreateFolder(create, ZmOrganizer.ADDRBOOK);
391 				} else if (name == "link") {
392 					this._handleCreateLink(create, ZmOrganizer.ADDRBOOK);
393 				} else if (name == "cn") {
394 					//note- this is updating the view list. The canonical is upadated
395 					// in ZmContact.prototype._handleResponseCreate. See bug 81055
396 					var clc = AjxDispatcher.run("GetContactListController");
397 					if (clc._folderId == ZmFolder.ID_DLS) {
398 						//the simplest solution I could think of to the messy problem that the clcList in this case is GAL and thus
399 						//the contact becomes GAL (in memory) even though it's not on the server. Then it's cached and when going to the contacts it would get an exeption when clicked
400 						//if the user is viewing the DLs folder, they will see the new contact they created anyway when clicking on the "contacts" folder (or whatever other folder they created it in)
401 						continue;
402 					}
403 					var clcList = (clc && clc.getFolderId()) ? clc.getList() : new ZmContactList(null);
404 					if (appCtxt.multiAccounts && clcList.search && clcList.search.folderId != create.l) {
405 						continue;
406 					}
407 					clcList.notifyCreate(create);
408 					var context = window.parentAppCtxt || window.appCtxt;
409 					context.clearAutocompleteCache(ZmAutocomplete.AC_TYPE_CONTACT);
410 					create._handled = true;
411 				}
412 			}
413 		}
414 	}
415 };
416 
417 ZmContactsApp.prototype.modifyNotify =
418 function(modifies, force) {
419 	if (!modifies["cn"]) { return; }
420 	if (!force && !this._noDefer && this._deferNotifications("modify", modifies)) { return; }
421 
422 	this._batchNotify(modifies["cn"]);
423 };
424 
425 /**
426  * @private
427  */
428 ZmContactsApp.prototype.postNotify =
429 function(notify) {
430 	if (this._checkReplenishListView) {
431 		this._checkReplenishListView._checkReplenish();
432 		this._checkReplenishListView = null;
433 	}
434 };
435 
436 /**
437  * @private
438  */
439 ZmContactsApp.prototype.handleOp =
440 function(op) {
441 	if (!appCtxt.isWebClientOffline()) {
442 		switch (op) {
443 			case ZmOperation.NEW_CONTACT:
444 			case ZmOperation.NEW_DISTRIBUTION_LIST:
445 			case ZmOperation.NEW_GROUP: {
446 				var type = (op == ZmOperation.NEW_CONTACT) ? null : ZmItem.GROUP;
447 				var loadCallback = new AjxCallback(this, this._handleLoadNewItem, [type, op == ZmOperation.NEW_DISTRIBUTION_LIST]);
448 				AjxDispatcher.require(["ContactsCore", "Contacts"], false, loadCallback, null, true);
449 				break;
450 			}
451 			case ZmOperation.NEW_ADDRBOOK: {
452 				var loadCallback = new AjxCallback(this, this._handleLoadNewAddrBook);
453 				AjxDispatcher.require(["ContactsCore", "Contacts"], false, loadCallback, null, true);
454 				break;
455 			}
456 		}
457 	}
458 };
459 
460 /**
461  * @private
462  */
463 ZmContactsApp.prototype._handleLoadNewItem =
464 function(type, isDl) {
465 	var contact = new ZmContact(null, null, type, isDl);
466 	AjxDispatcher.run("GetContactController").show(contact);
467 };
468 
469 /**
470  * @private
471  */
472 ZmContactsApp.prototype._handleLoadNewAddrBook =
473 function() {
474 	appCtxt.getAppViewMgr().popView(true, ZmId.VIEW_LOADING);	// pop "Loading..." page
475 	var dialog = appCtxt.getNewAddrBookDialog();
476 	if (!this._newAddrBookCb) {
477 		this._newAddrBookCb = new AjxCallback(this, this._newAddrBookCallback);
478 	}
479 	ZmController.showDialog(dialog, this._newAddrBookCb);
480 };
481 
482 // Public methods
483 
484 /**
485  * Activates the application.
486  * 
487  * @param	{Object}	active 	(not used)
488  * 
489  */
490 ZmContactsApp.prototype.activate =
491 function(active) {
492 	ZmApp.prototype.activate.apply(this, arguments);
493 };
494 
495 ZmContactsApp.prototype.getNewButtonProps =
496 function() {
497 	return {
498 		text:		ZmMsg.newContact,
499 		tooltip:	ZmMsg.createNewContact,
500 		icon:		"NewContact",
501 		iconDis:	"NewContactDis",
502 		defaultId:	ZmOperation.NEW_CONTACT,
503         disabled:   !this.containsWritableFolder()
504 	};
505 };
506 
507 /**
508  * Launches the application.
509  * 
510  * @param	{Object}	params		(not used)
511  * @param	{AjxCallback}	callback	the callback
512  */
513 ZmContactsApp.prototype.launch =
514 function(params, callback) {
515 	this._setLaunchTime(this.toString(), new Date());
516     var loadCallback = new AjxCallback(this, this._handleLoadLaunch, callback);
517     // sync load to prevent race condition
518 	AjxDispatcher.require(["ContactsCore", "Contacts"], false, loadCallback, null, true);
519 };
520 
521 /**
522  * @private
523  */
524 ZmContactsApp.prototype._handleLoadLaunch =
525 function(callback) {
526     var query = "in:contacts";
527     if(appCtxt.isExternalAccount()) {
528         query = "inid:" + this.getDefaultFolderId();
529 
530     }
531 	this._contactsSearch(query, callback);
532 };
533 
534 /**
535  * @private
536  */
537 ZmContactsApp.prototype._contactsSearch =
538 function(query, callback) {
539 	var params = {
540 		searchFor:	ZmId.ITEM_CONTACT,
541 		query:		query,
542 		limit:		this.getLimit(),
543 		types:		[ZmId.ITEM_CONTACT],
544 		callback:	callback
545 	};
546 	var sc = appCtxt.getSearchController();
547 	sc.searchAllAccounts = false;
548 	sc.search(params);
549 };
550 
551 /**
552  * Gets the limit for the search triggered by the application launch or an overview click.
553  * 
554  * @param	{Boolean}	offset	if <code>true</code> app has offset
555  * @return	{int}	the limit
556  */
557 ZmContactsApp.prototype.getLimit =
558 function(offset) {
559 	// return enough for us to get a scroll bar since we are pageless
560 	var limit = appCtxt.get(ZmSetting.PAGE_SIZE);
561 	return offset ? limit : 2 * limit;
562 };
563 
564 /**
565  * Gets the initial search type.
566  * 
567  * @return	{constant}	the search (see {@link ZmId}<code>.SEARCH_</code> constants)
568  */
569 ZmContactsApp.prototype.getInitialSearchType =
570 function() {
571 	var list = appCtxt.getCurrentList();
572 	return (list && (list instanceof ZmContactList) && list.isGal)
573 		? ZmId.SEARCH_GAL : null;
574 };
575 
576 /**
577  * Shows the search results.
578  * 
579  * @param	{Object}					results					the results
580  * @param	{AjxCallback}				callback				the callback
581  * @param 	{ZmSearchResultsController}	searchResultsController	owning controller
582  */
583 ZmContactsApp.prototype.showSearchResults =
584 function(results, callback, searchResultsController) {
585 	var loadCallback = this._handleLoadShowSearchResults.bind(this, results, callback, searchResultsController);
586 	AjxDispatcher.require("Contacts", false, loadCallback, null, true);
587 };
588 
589 /**
590  * @private
591  */
592 ZmContactsApp.prototype._handleLoadShowSearchResults =
593 function(results, callback, searchResultsController) {
594 	var search = results && results.search;
595 	var folderId = search && search.isSimple() && search.folderId;
596 	var isInGal = search && (search.contactSource == ZmId.SEARCH_GAL);
597 	var sessionId = searchResultsController ? searchResultsController.getCurrentViewId() : ZmApp.MAIN_SESSION;
598 	var controller = AjxDispatcher.run("GetContactListController", sessionId, searchResultsController);
599 	controller.show(results, isInGal, folderId);
600 	this._setLoadedTime(this.toString(), new Date());
601 	if (callback) {
602 		callback.run(controller);
603 	}
604 };
605 
606 ZmContactsApp.prototype.runRefresh =
607 function() {
608 	var clc = AjxDispatcher.run("GetContactListController");
609 	clc.runRefresh();
610 };
611 
612 
613 /**
614  * Sets the app as active.
615  * 
616  * @param	{Boolean}	active	if <code>true</code> active and shows application
617  */
618 ZmContactsApp.prototype.setActive =
619 function(active) {
620 	if (active) {
621 		var clc = AjxDispatcher.run("GetContactListController");
622 		clc.show();
623 	}
624 };
625 
626 /**
627  * Checks if the contact list is loaded for the specified account.
628  * 
629  * @param	{String}	acctId	the account id
630  * @return	{Boolean}	<code>true</code> if contact list is loaded
631  */
632 ZmContactsApp.prototype.isContactListLoaded =
633 function(acctId) {
634 	var aid = (acctId || appCtxt.getActiveAccount().id);
635 	return (this._contactList[aid] && this._contactList[aid].isLoaded);
636 };
637 
638 /**
639  * Gets the contact with the given address, if any. If it's not in our cache
640  * and we are given a callback, we do a search. If a search is performed then any
641  * addresses in the Address Lookup Group are also searched for.
642  *
643  * @param {String}	address			an email address
644  * @param {AjxCallback}	callback	the callback to run
645  * @return	{ZmContact}	the contact
646  * 
647  * @see		#setAddrLookupGroup
648  */
649 ZmContactsApp.prototype.getContactByEmail =
650 function(address, callback) {
651 	if (!address) { return null; }
652 	var addr = address.toLowerCase();
653 	var contact = this._byEmail[addr];
654 
655 	// if we have a failed search for this address, or have loaded all contacts,
656 	// don't bother doing a search
657 	if (!contact && this._notFound(addr)) {
658 		this._removeAddrFromLookupGroup(addr);
659 		if (callback) { callback.run(null); }
660 		return null;
661 	}
662 
663 	// found a cached contact, return it
664 	if (contact) {
665 		this._removeAddrFromLookupGroup(addr);
666 		contact = this._realizeContact(contact);
667 		contact._lookupEmail = address;	// so caller knows which address matched
668 		if (callback) { callback.run(contact); }
669 		return contact;
670 	}
671 
672 	// search for contact
673 	if (callback) {
674 		var search = null,
675 			isGroupSearch = false,
676 			lookupAddrs = [];
677 		if (this._addrLookupHash && this._addrLookupHash[addr]) {
678 			if (this._addrLookupList) {
679 				for (var i = 0; i < this._addrLookupList.length; i++) {
680 					lookupAddrs.push(this._addrLookupList[i]);
681 				}
682 				search = this._getSearchForAddresses(this._addrLookupList);
683 				isGroupSearch = true;
684 				this._addrLookupList = null;
685 			}
686 			this._addrLookupHash[addr].push(callback);
687 		} else {
688 			search = this._getSearchForAddresses([address]);
689 		}
690 
691 		if (search) {
692 			var respCallback = new AjxCallback(this, this._handleResponseSearch, [isGroupSearch ? lookupAddrs : addr, isGroupSearch, callback]);
693 			search.execute({callback:respCallback, noBusyOverlay:true});
694 		}
695 	}
696 };
697 
698 /**
699  * @private
700  */
701 ZmContactsApp.prototype._handleResponseSearch =
702 function(addr, isGroupSearch, callback, result) {
703 	var resp = result.getResponse();
704 	var contactList = resp && resp.getResults(ZmItem.CONTACT);
705 	if (isGroupSearch) {
706 		var list = contactList.getArray();
707 		for (var i = 0; i < list.length; i++) {
708 			this._updateLookupCache(list[i]);
709 		}
710 		for (var i = 0; i < addr.length; i++) {
711 			var a = addr[i];
712 			if (!this._byEmail[a]) {
713 				this._updateLookupCache(null, a); // Make sure there's a null entry in the map for the address.	
714 			}
715 			var callbacks = this._addrLookupHash[a];
716 			if (callbacks && callbacks.length) {
717 				for (var j = 0; j < callbacks.length; j++) {
718 					callbacks[j].run(this._byEmail[a]);
719 				}
720 			}
721 			this._removeAddrFromLookupGroup(a);
722 		}
723 	} else {
724 		var contact = contactList ? contactList.get(0) : null;	// return null if not found
725 		this._updateLookupCache(contact, addr);
726 		this._byEmail[addr] = contact;
727 		callback.run(contact);
728 	}
729 };
730 
731 /**
732  * Gets the contacts with the given addresses, if any. If there are addresses not in our cache
733  * and we are given a callback, we do a search. Unlike {@link #getContactByEmail}, this method does not
734  * use or modify the Address Lookup Group.
735  *
736  * @param {Array}	addresses	an array of {@link AjxEmailAddress} objects
737  * @param {AjxCallback}	callback	the callback to run
738  * @return	{Array}	an array of [{@link AjxEmailAddress}, {@link ZmContact}] pairs.
739  * 
740  * @see		#setAddrLookupGroup
741  */
742 ZmContactsApp.prototype.getContactsByEmails =
743 function(addresses, callback) {
744 	// Go through the addresses, separating known ones from unknown.
745 	var resultArray = [],
746 		searchAddresses = null,
747 		searchAddressStrings = null;
748 	for (var i = 0, count = addresses.length; i < count; i++) {
749 		var address = addresses[i];
750 		var contact = this.getContactByEmail(address.getAddress());
751 		if (contact || contact === null) {
752 			resultArray.push({ address: address, contact: contact });
753 		} else {
754 			searchAddresses = searchAddresses || [];
755 			searchAddressStrings = searchAddressStrings || [];
756 			searchAddresses.push(address);
757 			searchAddressStrings.push(address.getAddress());
758 		}
759 	}
760 
761 	// See if we can exit without performing a search.
762 	if (!callback) {
763 		return resultArray;
764 	}
765 	if (!searchAddresses) {
766 		callback.run(resultArray);
767 		return resultArray;
768 	}
769 
770 	// Perform the search.
771 	var search = this._getSearchForAddresses(searchAddressStrings);
772 	var respCallback = new AjxCallback(this, this._handleResponseSearchByEmails, [searchAddresses, resultArray, callback]);
773 	search.execute({callback:respCallback});
774 };
775 
776 /**
777  * @private
778  */
779 ZmContactsApp.prototype._handleResponseSearchByEmails =
780 function(addresses, resultArray, callback, result) {
781 	// get contact list
782 	var resp = result.getResponse();
783 	var list = resp && resp.getResults(ZmItem.CONTACT);
784 	if (!list) callback.run(resultArray);
785 
786 	// get contact emails
787 	for (var index = 0, count = list.size(); index < count; index++) {
788 		var contact = list.get(index);
789 		for (var i = 1; true; i++) {
790 			var aname = ZmContact.getAttributeName(ZmContact.F_email, i);
791 			var avalue = contact.getAttr(aname);
792 			if (!avalue) break;
793 			this._byEmail[avalue] = contact;
794 		}
795 	}
796 
797 	// Fill in the results.
798 	for (var i = 0, count = addresses.length; i < count; i++) {
799 		var address = addresses[i];
800 		var contact = this.getContactByEmail(address.getAddress());
801 		resultArray.push({ address: address, contact: contact });
802 	}
803 	callback.run(resultArray);
804 };
805 
806 /**
807  * @private
808  */
809 ZmContactsApp.prototype._getSearchForAddresses =
810 function(addrs) {
811 	var buffer;
812 	if (addrs.length == 1) {
813 		buffer = ["to:", addrs[0], " not #type:group"];
814 	} else {
815 		buffer = ["("];
816 		for (var i = 0, count = addrs.length; i < count; i++) {
817 			if (i > 0) {
818 				buffer.push(" OR ");
819 			}
820 			buffer.push("to:");
821 			buffer.push(addrs[i]);
822 		}
823 		buffer.push(") not #type:group");
824 	}
825 	var params = {
826 		query: buffer.join(""),
827 		limit: addrs.length * 2,
828 		types: AjxVector.fromArray([ZmItem.CONTACT])
829 	};
830 	return new ZmSearch(params);
831 };
832 
833 ZmContactsApp.prototype._notFound =
834 function(contact) {
835 	return (contact === null || Boolean(this._contactList[appCtxt.getActiveAccount().id]));
836 };
837 
838 /**
839  * Sets up a list of email addresses to use to find their contacts with a single search. The addresses passed
840  * in can either be raw email addresses (strings), or {@link AjxEmailAddress} objects. A list of the addresses is kept
841  * so that it can later be used to create a single search query. Each address will also keep track of the
842  * callbacks that will need to be run with its search result (it's a list of callbacks since the same address
843  * may be used in more than one context).
844  * <p>
845  * One example of this group approach is in rendering a message header, where each email address in the header
846  * is rendered based on whether it maps to a contact. The group approach lets us do a single search rather than
847  * several.
848  * </p>
849  *
850  * @param {Array}	addrs		a list of email addresses to look up
851  */
852 ZmContactsApp.prototype.setAddrLookupGroup =
853 function(addrs) {
854 	this._addrLookupList = [];
855 	this._addrLookupHash = {};
856 	if (addrs && addrs.length) {
857 		for (var i = 0; i < addrs.length; i++) {
858 			if (addrs[i]) {
859 				var addr = addrs[i].address || addrs[i];
860 				addr = (addr && AjxUtil.isString(addr)) ? addr.toLowerCase() : null;
861 				if (addr && !this._addrLookupHash[addr]) {
862 					this._addrLookupList.push(addr);
863 					this._addrLookupHash[addr] = [];
864 				}
865 			}
866 		}
867 	}
868 };
869 
870 /**
871  * @private
872  */
873 ZmContactsApp.prototype._removeAddrFromLookupGroup =
874 function(addr) {
875 	if (!(this._addrLookupList && this._addrLookupList.length)) { return; }
876 	AjxUtil.arrayRemove(this._addrLookupList, addr);
877 	delete this._addrLookupHash[addr];
878 };
879 
880 /**
881  * @private
882  */
883 ZmContactsApp.prototype._updateLookupCache =
884 function(contact, addr) {
885 	if (addr) {
886 		this._byEmail[addr] = contact;
887 	}
888 	if (contact) {
889 		for (var i = 1; true; i++) {
890 			var aname = ZmContact.getAttributeName(ZmContact.F_email, i);
891 			var avalue = contact.getAttr(aname);
892 			if (!avalue) break;
893 			this._byEmail[avalue.toLowerCase()] = contact;
894 		}
895 	}
896 };
897 
898 /**
899  * Gets information about the contact with the given phone number, if any.
900  * Canonical list only.
901  *
902  * @param {String}	phone	the phone number
903  * @return	{Object}	an object with contact = the contact & field = the field with the matching phone number
904  */
905 ZmContactsApp.prototype.getContactByPhone =
906 function(phone) {
907 	if (!phone) { return null; }
908 	var digits = phone.replace(/[^\d]/g, '');
909 	var data = this._phoneToContact[digits];
910 	if (data) {
911 		data.contact = this._realizeContact(data.contact);
912 	}
913 	return data;
914 };
915 
916 /**
917  * @private
918  */
919 ZmContactsApp.prototype._realizeContact =
920 function(contact) {
921 	var acctId = appCtxt.getActiveAccount().id;
922 	var cl = this._contactList[acctId];
923 	return cl ? cl._realizeContact(contact) : contact;
924 };
925 
926 /**
927  * @private
928  */
929 ZmContactsApp.prototype.updateCache =
930 function(contact, doAdd) {
931 
932 	this._updateHash(contact, doAdd, ZmContact.EMAIL_FIELDS, this._byEmail);
933 	if (appCtxt.get(ZmSetting.VOICE_ENABLED)) {
934 		this._updateHash(contact, doAdd, ZmContact.PHONE_FIELDS, this._byPhone, true, true);
935 	}
936 };
937 
938 /**
939  * @private
940  */
941 ZmContactsApp.prototype._updateHash =
942 function(contact, doAdd, fields, hash, includeField, isNumeric) {
943 
944 	for (var index = 0; index < fields.length; index++) {
945 		var field = fields[index];
946 		for (var i = 1; true; i++) {
947 			var aname = ZmContact.getAttributeName(field, i);
948 			var avalue = ZmContact.getAttr(contact, aname);
949 			if (!avalue) break;
950 			avalue = isNumeric ? avalue.replace(/[^\d]/g, '') : avalue.toLowerCase();
951 			if (doAdd) {
952 				hash[avalue] = includeField ? {contact:contact, field:aname} : contact;
953 			} else {
954 				delete hash[avalue];
955 			}
956 		}
957 	}
958 };
959 
960 /**
961  * Used in multi-account to load contacts for all of user's accounts.
962  * 
963  * @private
964  */
965 ZmContactsApp.prototype.getContactListForAllAccounts =
966 function() {
967 	var enabled = [];
968 	var list = appCtxt.accountList.visibleAccounts;
969 	for (var i = 0; i < list.length; i++) {
970 		if (appCtxt.get(ZmSetting.CONTACTS_ENABLED, null, list[i])) {
971 			enabled.push(list[i]);
972 		}
973 	}
974 
975 	if (enabled.length > 0) {
976 		this._loadContactsForAccount(enabled);
977 	}
978 };
979 
980 /**
981  * @private
982  */
983 ZmContactsApp.prototype._loadContactsForAccount =
984 function(accounts) {
985 	var acct = accounts.shift();
986 	if (acct) {
987 		var callback = new AjxCallback(this, this._loadContactsForAccount, [accounts]);
988 		this.getContactList(callback, null, acct);
989 	}
990 };
991 
992 /**
993  * Gets a {@link ZmContactList} with all of the user's local contacts. If that's a
994  * large number, performance may be slow.
995  * 
996  * @param {AjxCallback}	callback			the callback to trigger after contact list loaded
997  * @param {AjxCallback}	errorCallback		the callback to trigger in the event of an error
998  * @param {ZmZimbraAccount}	account		the account to fetch contacts for
999  * @return	{ZmContactList}	the contact list
1000  */
1001 ZmContactsApp.prototype.getContactList =
1002 function(callback, errorCallback, account) {
1003 	var acctId = (account && account.id) || appCtxt.getActiveAccount().id;
1004 	if (!this._contactList[acctId]) {
1005 		try {
1006 			// check if a parent controller exists and ask it for the contact list
1007 			if (this._parentController) {
1008 				this._contactList[acctId] = this._parentController.getApp(ZmApp.CONTACTS).getContactList();
1009 			} else {
1010 				this._contactList[acctId] = new ZmContactList(null);
1011 				var respCallback = new AjxCallback(this, this._handleResponseGetContactList, [callback]);
1012 				var accountName = (account && account.getEmail());
1013 				this._contactList[acctId].load(respCallback, errorCallback, accountName);
1014 			}
1015 			return this._contactList[acctId];
1016 		} catch (ex) {
1017 			this._contactList[acctId] = null;
1018 			throw ex;
1019 		}
1020 	} else {
1021 		if (callback && callback.isAjxCallback) {
1022 			callback.run(this._contactList[acctId]);
1023 		}
1024 		return this._contactList[acctId];
1025 	}
1026 };
1027 
1028 /**
1029  * @private
1030  */
1031 ZmContactsApp.prototype._handleResponseGetContactList =
1032 function(callback) {
1033 	var acctId = appCtxt.getActiveAccount().id;
1034 	this.contactsLoaded[acctId] = true;
1035 
1036 	if (callback) {
1037 		callback.run(this._contactList[acctId]);
1038 	}
1039 };
1040 
1041 /**
1042  * Gets the GAL contact list. NOTE: calling method should handle exceptions.
1043  * 
1044  * @return	{ZmContactList}	the contact list
1045  */
1046 ZmContactsApp.prototype.getGalContactList =
1047 function() {
1048 	if (!this._galContactList) {
1049 		try {
1050 			this._galContactList = new ZmContactList(null, true);
1051 			this._galContactList.load();
1052 		} catch (ex) {
1053 			this._galContactList = null;
1054 			throw ex;
1055 		}
1056 	}
1057 	return this._galContactList;
1058 };
1059 
1060 /**
1061  * @private
1062  */
1063 ZmContactsApp.prototype.createFromVCard =
1064 function(msgId, vcardPartId) {
1065 	var contact = new ZmContact(null);
1066 	contact.createFromVCard(msgId, vcardPartId);
1067 };
1068 
1069 /**
1070  * Gets the contact list controller.
1071  * 
1072  * @return	{ZmContactListController}	the controller
1073  */
1074 ZmContactsApp.prototype.getContactListController =
1075 function(sessionId, searchResultsController) {
1076 	return this.getSessionController({controllerClass:			"ZmContactListController",
1077 									  sessionId:				sessionId || ZmApp.MAIN_SESSION,
1078 									  searchResultsController:	searchResultsController});
1079 };
1080 
1081 /**
1082  * Gets the contact controller.
1083  *
1084  * @return	{ZmContactController}	the controller
1085  */
1086 ZmContactsApp.prototype.getContactController =
1087 function(sessionId) {
1088 	return this.getSessionController({controllerClass:	"ZmContactController",
1089 									  sessionId:		sessionId});
1090 };
1091 
1092 /**
1093  * @private
1094  */
1095 ZmContactsApp.prototype._newAddrBookCallback =
1096 function(parent, name, color) {
1097 	// REVISIT: Do we really want to close the dialog before we
1098 	//          know if the create succeeds or fails?
1099 	var dialog = appCtxt.getNewAddrBookDialog();
1100 	dialog.popdown();
1101 
1102 	var oc = appCtxt.getOverviewController();
1103 	oc.getTreeController(ZmOrganizer.ADDRBOOK)._doCreate(parent, name, color);
1104 };
1105 
1106 ZmContactsApp.prototype.getDL =
1107 function(addr) {
1108 	return this._dlCache[addr];
1109 };
1110 
1111 ZmContactsApp.prototype.cacheDL =
1112 function(addr, dl) {
1113 	this._dlCache[addr] = dl;
1114 };
1115 
1116 /**
1117  * Adds/remove contacts from the contact list hash
1118  * @param contact  {Object}     contact object
1119  * @param doDelete {boolean}    true to delete from hash
1120  */
1121 ZmContactsApp.prototype.updateIdHash =
1122 function(contact, doDelete) {
1123 	var id = contact.id;
1124 	var hash = this.getContactList().getIdHash();
1125 	if (!doDelete) {
1126 		hash[id] = contact;
1127 	}
1128 	else {
1129 		delete hash[id];
1130 	}
1131 };
1132 
1133 /**
1134  * Online to Offline or Offline to Online; Called from ZmApp.activate and from ZmOffline.enableApps, disableApps
1135  */
1136 ZmContactsApp.prototype.resetWebClientOfflineOperations =
1137 function() {
1138 	ZmApp.prototype.resetWebClientOfflineOperations.apply(this);
1139 	var contactListController = this.getContactListController();
1140     var currentToolbar = contactListController && contactListController.getCurrentToolbar();
1141     if (contactListController && currentToolbar) {
1142 	    contactListController._resetOperations(currentToolbar);
1143     }
1144 	var overview = this.getOverview();
1145 	var distributionList = overview && overview.getTreeItemById(ZmFolder.ID_DLS);// Distribution Lists folder Id
1146 	if (distributionList) {
1147 		distributionList.setVisible(!appCtxt.isWebClientOffline());
1148 	}
1149 };
1150