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