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 contact class. 27 */ 28 29 if (!window.ZmContact) { 30 /** 31 * Creates an empty contact. 32 * @class 33 * This class represents a contact (typically a person) with all its associated versions 34 * of email address, home and work addresses, phone numbers, etc. Contacts can be filed/sorted 35 * in different ways, with the default being Last, First. A contact is an item, so 36 * it has tagging and flagging support, and belongs to a list. 37 * <p> 38 * Most of a contact's data is kept in attributes. These include name, phone, etc. Meta-data and 39 * data common to items are not kept in attributes. These include flags, tags, folder, and 40 * modified/created dates. Since the attribute data for contacts is loaded only once, a contact 41 * gets its attribute values from that canonical list. 42 * </p> 43 * 44 * @param {int} id the unique ID 45 * @param {ZmContactList} list the list that contains this contact 46 * @param {constant} type the item type 47 * @param {object} newDl true if this is a new DL 48 * 49 * @extends ZmItem 50 */ 51 ZmContact = function(id, list, type, newDl) { 52 if (arguments.length == 0) { return; } 53 54 type = type || ZmItem.CONTACT; 55 ZmItem.call(this, type, id, list); 56 57 this.attr = {}; 58 this.isGal = (this.list && this.list.isGal) || newDl; 59 if (newDl) { 60 this.folderId = ZmFolder.ID_DLS; 61 this.dlInfo = { isMember: false, 62 isOwner: true, 63 subscriptionPolicy: null, 64 unsubscriptionPolicy: null, 65 description: "", 66 displayName: "", 67 notes: "", 68 hideInGal: false, 69 mailPolicy: null, 70 owners: [appCtxt.get(ZmSetting.USERNAME)] 71 }; 72 73 } 74 75 this.participants = new AjxVector(); // XXX: need to populate this guy (see ZmConv) 76 }; 77 78 ZmContact.prototype = new ZmItem; 79 ZmContact.prototype.constructor = ZmContact; 80 ZmContact.prototype.isZmContact = true; 81 82 // fields 83 ZmContact.F_anniversary = "anniversary"; 84 ZmContact.F_assistantPhone = "assistantPhone"; 85 ZmContact.F_attachment = "attachment"; 86 ZmContact.F_birthday = "birthday"; 87 ZmContact.F_callbackPhone = "callbackPhone"; 88 ZmContact.F_carPhone = "carPhone"; 89 ZmContact.F_company = "company"; 90 ZmContact.F_companyPhone = "companyPhone"; 91 ZmContact.F_custom = "custom"; 92 ZmContact.F_description = "description"; 93 ZmContact.F_department = "department"; 94 ZmContact.F_dlist = "dlist"; // Group fields 95 ZmContact.F_dlDisplayName = "dldisplayname"; //DL 96 ZmContact.F_dlDesc = "dldesc"; //DL 97 ZmContact.F_dlHideInGal = "dlhideingal"; //DL 98 ZmContact.F_dlNotes = "dlnotes"; //DL 99 ZmContact.F_dlSubscriptionPolicy = "dlsubspolicy"; //DL 100 ZmContact.F_dlMailPolicy = "dlmailpolicy"; //DL 101 ZmContact.F_dlMailPolicySpecificMailers = "dlmailpolicyspecificmailers"; //DL 102 ZmContact.F_dlUnsubscriptionPolicy = "dlunsubspolicy"; //DL 103 ZmContact.F_dlListOwners = "dllistowners"; //DL 104 ZmContact.F_email = "email"; 105 ZmContact.F_email2 = "email2"; 106 ZmContact.F_email3 = "email3"; 107 ZmContact.F_email4 = "email4"; 108 ZmContact.F_email5 = "email5"; 109 ZmContact.F_email6 = "email6"; 110 ZmContact.F_email7 = "email7"; 111 ZmContact.F_email8 = "email8"; 112 ZmContact.F_email9 = "email9"; 113 ZmContact.F_email10 = "email10"; 114 ZmContact.F_email11 = "email11"; 115 ZmContact.F_email12 = "email12"; 116 ZmContact.F_email13 = "email13"; 117 ZmContact.F_email14 = "email14"; 118 ZmContact.F_email15 = "email15"; 119 ZmContact.F_email16 = "email16"; 120 ZmContact.F_fileAs = "fileAs"; 121 ZmContact.F_firstName = "firstName"; 122 ZmContact.F_folderId = "folderId"; 123 ZmContact.F_groups = "groups"; //group members 124 ZmContact.F_homeCity = "homeCity"; 125 ZmContact.F_homeCountry = "homeCountry"; 126 ZmContact.F_homeFax = "homeFax"; 127 ZmContact.F_homePhone = "homePhone"; 128 ZmContact.F_homePhone2 = "homePhone2"; 129 ZmContact.F_homePostalCode = "homePostalCode"; 130 ZmContact.F_homeState = "homeState"; 131 ZmContact.F_homeStreet = "homeStreet"; 132 ZmContact.F_homeURL = "homeURL"; 133 ZmContact.F_image = "image"; // contact photo 134 ZmContact.F_imAddress = "imAddress"; // IM addresses 135 ZmContact.F_imAddress1 = "imAddress1"; // IM addresses 136 ZmContact.F_imAddress2 = "imAddress2"; 137 ZmContact.F_imAddress3 = "imAddress3"; 138 ZmContact.F_jobTitle = "jobTitle"; 139 ZmContact.F_lastName = "lastName"; 140 ZmContact.F_maidenName = "maidenName"; 141 ZmContact.F_memberC = "memberC"; 142 ZmContact.F_memberG = "memberG"; 143 ZmContact.F_memberI = "memberI"; 144 ZmContact.F_middleName = "middleName"; 145 ZmContact.F_mobilePhone = "mobilePhone"; 146 ZmContact.F_namePrefix = "namePrefix"; 147 ZmContact.F_nameSuffix = "nameSuffix"; 148 ZmContact.F_nickname = "nickname"; 149 ZmContact.F_notes = "notes"; 150 ZmContact.F_otherCity = "otherCity"; 151 ZmContact.F_otherCountry = "otherCountry"; 152 ZmContact.F_otherFax = "otherFax"; 153 ZmContact.F_otherPhone = "otherPhone"; 154 ZmContact.F_otherPostalCode = "otherPostalCode"; 155 ZmContact.F_otherState = "otherState"; 156 ZmContact.F_otherStreet = "otherStreet"; 157 ZmContact.F_otherURL = "otherURL"; 158 ZmContact.F_pager = "pager"; 159 ZmContact.F_phoneticFirstName = "phoneticFirstName"; 160 ZmContact.F_phoneticLastName = "phoneticLastName"; 161 ZmContact.F_phoneticCompany = "phoneticCompany"; 162 ZmContact.F_type = "type"; 163 ZmContact.F_workAltPhone = "workAltPhone"; 164 ZmContact.F_workCity = "workCity"; 165 ZmContact.F_workCountry = "workCountry"; 166 ZmContact.F_workEmail1 = "workEmail1"; 167 ZmContact.F_workEmail2 = "workEmail2"; 168 ZmContact.F_workEmail3 = "workEmail3"; 169 ZmContact.F_workFax = "workFax"; 170 ZmContact.F_workMobile = "workMobile"; 171 ZmContact.F_workPhone = "workPhone"; 172 ZmContact.F_workPhone2 = "workPhone2"; 173 ZmContact.F_workPostalCode = "workPostalCode"; 174 ZmContact.F_workState = "workState"; 175 ZmContact.F_workStreet = "workStreet"; 176 ZmContact.F_workURL = "workURL"; 177 ZmContact.F_imagepart = "imagepart"; // New field for bug 73146 - Contacts call does not return the image information 178 ZmContact.F_zimletImage = "zimletImage"; 179 ZmContact.X_fileAs = "fileAs"; // extra fields 180 ZmContact.X_firstLast = "firstLast"; 181 ZmContact.X_fullName = "fullName"; 182 ZmContact.X_vcardXProps = "vcardXProps"; 183 ZmContact.X_outlookUserField = "outlookUserField"; 184 ZmContact.MC_cardOwner = "cardOwner"; // My card fields 185 ZmContact.MC_workCardMessage = "workCardMessage"; 186 ZmContact.MC_homeCardMessage = "homeCardMessage"; 187 ZmContact.MC_homePhotoURL = "homePhotoURL"; 188 ZmContact.MC_workPhotoURL = "workPhotoURL"; 189 ZmContact.GAL_MODIFY_TIMESTAMP = "modifyTimeStamp"; // GAL fields 190 ZmContact.GAL_CREATE_TIMESTAMP = "createTimeStamp"; 191 ZmContact.GAL_ZIMBRA_ID = "zimbraId"; 192 ZmContact.GAL_OBJECT_CLASS = "objectClass"; 193 ZmContact.GAL_MAIL_FORWARD_ADDRESS = "zimbraMailForwardingAddress"; 194 ZmContact.GAL_CAL_RES_TYPE = "zimbraCalResType"; 195 ZmContact.GAL_CAL_RES_LOC_NAME = "zimbraCalResLocationDisplayName"; 196 197 // file as 198 (function() { 199 var i = 1; 200 ZmContact.FA_LAST_C_FIRST = i++; 201 ZmContact.FA_FIRST_LAST = i++; 202 ZmContact.FA_COMPANY = i++; 203 ZmContact.FA_LAST_C_FIRST_COMPANY = i++; 204 ZmContact.FA_FIRST_LAST_COMPANY = i++; 205 ZmContact.FA_COMPANY_LAST_C_FIRST = i++; 206 ZmContact.FA_COMPANY_FIRST_LAST = i++; 207 ZmContact.FA_CUSTOM = i++; 208 })(); 209 210 // Field information 211 212 ZmContact.ADDRESS_FIELDS = [ 213 // NOTE: sync with field order in ZmEditContactView's templates 214 ZmContact.F_homeCity, 215 ZmContact.F_homeCountry, 216 ZmContact.F_homePostalCode, 217 ZmContact.F_homeState, 218 ZmContact.F_homeStreet, 219 ZmContact.F_workCity, 220 ZmContact.F_workCountry, 221 ZmContact.F_workPostalCode, 222 ZmContact.F_workState, 223 ZmContact.F_workStreet, 224 ZmContact.F_otherCity, 225 ZmContact.F_otherCountry, 226 ZmContact.F_otherPostalCode, 227 ZmContact.F_otherState, 228 ZmContact.F_otherStreet 229 ]; 230 ZmContact.EMAIL_FIELDS = [ 231 ZmContact.F_email, 232 ZmContact.F_workEmail1, 233 ZmContact.F_workEmail2, 234 ZmContact.F_workEmail3 235 ]; 236 ZmContact.IM_FIELDS = [ 237 ZmContact.F_imAddress 238 ]; 239 ZmContact.OTHER_FIELDS = [ 240 // NOTE: sync with field order in ZmEditContactView's templates 241 ZmContact.F_birthday, 242 ZmContact.F_anniversary, 243 ZmContact.F_custom 244 ]; 245 ZmContact.PHONE_FIELDS = [ 246 // NOTE: sync with field order in ZmEditContactView's templates 247 ZmContact.F_mobilePhone, 248 ZmContact.F_workPhone, 249 ZmContact.F_workFax, 250 ZmContact.F_companyPhone, 251 ZmContact.F_homePhone, 252 ZmContact.F_homeFax, 253 ZmContact.F_pager, 254 ZmContact.F_callbackPhone, 255 ZmContact.F_assistantPhone, 256 ZmContact.F_carPhone, 257 ZmContact.F_otherPhone, 258 ZmContact.F_otherFax, 259 ZmContact.F_workAltPhone, 260 ZmContact.F_workMobile 261 ]; 262 ZmContact.PRIMARY_FIELDS = [ 263 // NOTE: sync with field order in ZmEditContactView's templates 264 ZmContact.F_image, 265 ZmContact.F_namePrefix, 266 ZmContact.F_firstName, 267 ZmContact.F_phoneticFirstName, 268 ZmContact.F_middleName, 269 ZmContact.F_maidenName, 270 ZmContact.F_lastName, 271 ZmContact.F_phoneticLastName, 272 ZmContact.F_nameSuffix, 273 ZmContact.F_nickname, 274 ZmContact.F_jobTitle, 275 ZmContact.F_department, 276 ZmContact.F_company, 277 ZmContact.F_phoneticCompany, 278 ZmContact.F_fileAs, 279 ZmContact.F_folderId, 280 ZmContact.F_notes 281 ]; 282 ZmContact.URL_FIELDS = [ 283 // NOTE: sync with field order in ZmEditContactView's templates 284 ZmContact.F_homeURL, 285 ZmContact.F_workURL, 286 ZmContact.F_otherURL 287 ]; 288 ZmContact.GAL_FIELDS = [ 289 ZmContact.GAL_MODIFY_TIMESTAMP, 290 ZmContact.GAL_CREATE_TIMESTAMP, 291 ZmContact.GAL_ZIMBRA_ID, 292 ZmContact.GAL_OBJECT_CLASS, 293 ZmContact.GAL_MAIL_FORWARD_ADDRESS, 294 ZmContact.GAL_CAL_RES_TYPE, 295 ZmContact.GAL_CAL_RES_LOC_NAME, 296 ZmContact.F_type 297 ]; 298 ZmContact.MYCARD_FIELDS = [ 299 ZmContact.MC_cardOwner, 300 ZmContact.MC_homeCardMessage, 301 ZmContact.MC_homePhotoURL, 302 ZmContact.MC_workCardMessage, 303 ZmContact.MC_workPhotoURL 304 ]; 305 ZmContact.X_FIELDS = [ 306 ZmContact.X_firstLast, 307 ZmContact.X_fullName, 308 ZmContact.X_vcardXProps 309 ]; 310 311 312 ZmContact.IGNORE_NORMALIZATION = []; 313 314 ZmContact.ADDR_PREFIXES = ["work","home","other"]; 315 ZmContact.ADDR_SUFFIXES = ["Street","City","State","PostalCode","Country"]; 316 317 ZmContact.updateFieldConstants = function() { 318 319 for (var i = 0; i < ZmContact.ADDR_PREFIXES.length; i++) { 320 for (var j = 0; j < ZmContact.ADDR_SUFFIXES.length; j++) { 321 ZmContact.IGNORE_NORMALIZATION.push(ZmContact.ADDR_PREFIXES[i] + ZmContact.ADDR_SUFFIXES[j]); 322 } 323 } 324 325 ZmContact.DISPLAY_FIELDS = [].concat( 326 ZmContact.ADDRESS_FIELDS, 327 ZmContact.EMAIL_FIELDS, 328 ZmContact.IM_FIELDS, 329 ZmContact.OTHER_FIELDS, 330 ZmContact.PHONE_FIELDS, 331 ZmContact.PRIMARY_FIELDS, 332 ZmContact.URL_FIELDS 333 ); 334 335 ZmContact.IGNORE_FIELDS = [].concat( 336 ZmContact.GAL_FIELDS, 337 ZmContact.MYCARD_FIELDS, 338 ZmContact.X_FIELDS, 339 [ZmContact.F_imagepart] 340 ); 341 342 ZmContact.ALL_FIELDS = [].concat( 343 ZmContact.DISPLAY_FIELDS, ZmContact.IGNORE_FIELDS 344 ); 345 346 ZmContact.IS_DATE = {}; 347 ZmContact.IS_DATE[ZmContact.F_birthday] = true; 348 ZmContact.IS_DATE[ZmContact.F_anniversary] = true; 349 350 ZmContact.IS_IGNORE = AjxUtil.arrayAsHash(ZmContact.IGNORE_FIELDS); 351 352 // number of distribution list members to fetch at a time 353 ZmContact.DL_PAGE_SIZE = 100; 354 355 ZmContact.GROUP_CONTACT_REF = "C"; 356 ZmContact.GROUP_GAL_REF = "G"; 357 ZmContact.GROUP_INLINE_REF = "I"; 358 }; // updateFieldConstants() 359 ZmContact.updateFieldConstants(); 360 361 /** 362 * This structure can be queried to determine if the first 363 * entry in a multi-value entry is suffixed with "1". Most 364 * attributes add a numerical suffix to all but the first 365 * entry. 366 * <p> 367 * <strong>Note:</strong> 368 * In most cases, {@link ZmContact#getAttributeName} is a better choice. 369 */ 370 ZmContact.IS_ADDONE = {}; 371 ZmContact.IS_ADDONE[ZmContact.F_custom] = true; 372 ZmContact.IS_ADDONE[ZmContact.F_imAddress] = true; 373 ZmContact.IS_ADDONE[ZmContact.X_outlookUserField] = true; 374 375 /** 376 * Gets an indexed attribute name taking into account if the field 377 * with index 1 should append the "1" or not. Code should call this 378 * function in lieu of accessing {@link ZmContact.IS_ADDONE} directly. 379 */ 380 ZmContact.getAttributeName = function(name, index) { 381 index = index || 1; 382 return index > 1 || ZmContact.IS_ADDONE[name] ? name+index : name; 383 }; 384 385 /** 386 * Returns a string representation of the object. 387 * 388 * @return {String} a string representation of the object 389 */ 390 ZmContact.prototype.toString = 391 function() { 392 return "ZmContact"; 393 }; 394 395 // Class methods 396 397 /** 398 * Creates a contact from an XML node. 399 * 400 * @param {Object} node a "cn" XML node 401 * @param {Hash} args args to pass to the constructor 402 * @return {ZmContact} the contact 403 */ 404 ZmContact.createFromDom = 405 function(node, args) { 406 // check global cache for this item first 407 var contact = appCtxt.cacheGet(node.id); 408 409 // make sure the revision hasnt changed, otherwise contact is out of date 410 if (contact == null || (contact && contact.rev != node.rev)) { 411 contact = new ZmContact(node.id, args.list); 412 if (args.isGal) { 413 contact.isGal = args.isGal; 414 } 415 contact._loadFromDom(node); 416 //update the canonical list 417 appCtxt.getApp(ZmApp.CONTACTS).getContactList().add(contact); 418 } else { 419 if (node.m) { 420 contact.attr[ZmContact.F_groups] = node.m; 421 } 422 if (node.ref) { 423 contact.ref = node.ref; 424 } 425 if (node.tn) { 426 contact._parseTagNames(node.tn); 427 } 428 AjxUtil.hashUpdate(contact.attr, node._attrs); // merge new attrs just in case we don't have them 429 contact.list = args.list || new ZmContactList(null); 430 contact._list = {}; 431 contact._list[contact.list.id] = true; 432 } 433 434 return contact; 435 }; 436 437 /** 438 * Compares two contacts based on how they are filed. Intended for use by 439 * sort methods. 440 * 441 * @param {ZmContact} a a contact 442 * @param {ZmContact} b a contact 443 * @return {int} 0 if the contacts are the same; 1 if "a" is before "b"; -1 if "b" is before "a" 444 */ 445 ZmContact.compareByFileAs = 446 function(a, b) { 447 var aFileAs = (a instanceof ZmContact) ? a.getFileAs(true) : ZmContact.computeFileAs(a._attrs).toLowerCase(); 448 var bFileAs = (b instanceof ZmContact) ? b.getFileAs(true) : ZmContact.computeFileAs(b._attrs).toLowerCase(); 449 450 if (!bFileAs || (aFileAs > bFileAs)) return 1; 451 if (aFileAs < bFileAs) return -1; 452 return 0; 453 }; 454 455 /** 456 * Figures out the filing string for the contact according to the chosen method. 457 * 458 * @param {ZmContact|Hash} contact a contact or a hash of contact attributes 459 */ 460 ZmContact.computeFileAs = 461 function(contact) { 462 /* 463 * Bug 98176: To keep the same logic of generating the FileAs contact 464 * label string between the Ajax client, and HTML client, when the 465 * computeFileAs(), and fileAs*() functions are modified, please 466 * change the corresponding functions defined in the autoComplete.tag 467 */ 468 var attr = (contact instanceof ZmContact) ? contact.getAttrs() : contact; 469 if (!attr) return; 470 471 if (attr[ZmContact.F_dlDisplayName]) { 472 //this is only DL case. But since this is sometimes just the attrs, 473 //I can't always use isDistributionList method. 474 return attr[ZmContact.F_dlDisplayName]; 475 } 476 477 var val = parseInt(attr.fileAs); 478 var fa; 479 var idx = 0; 480 481 switch (val) { 482 case ZmContact.FA_LAST_C_FIRST: // Last, First 483 default: { 484 // if GAL contact, use full name instead (bug fix #4850,4009) 485 if (contact && contact.isGal) { 486 if (attr.fullName) { // bug fix #27428 - if fullName is Array, return first 487 return (attr.fullName instanceof Array) ? attr.fullName[0] : attr.fullName; 488 } 489 return ((attr.email instanceof Array) ? attr.email[0] : attr.email); 490 } 491 fa = ZmContact.fileAsLastFirst(attr.firstName, attr.lastName, attr.fullName, attr.nickname); 492 } 493 break; 494 495 case ZmContact.FA_FIRST_LAST: { // First Last 496 fa = ZmContact.fileAsFirstLast(attr.firstName, attr.lastName, attr.fullName, attr.nickname); 497 } 498 break; 499 500 case ZmContact.FA_COMPANY: { // Company 501 if (attr.company) fa = attr.company; 502 } 503 break; 504 505 case ZmContact.FA_LAST_C_FIRST_COMPANY: { // Last, First (Company) 506 var name = ZmContact.fileAsLastFirst(attr.firstName, attr.lastName, attr.fullName, attr.nickname); 507 fa = ZmContact.fileAsNameCompany(name, attr.company); 508 } 509 break; 510 511 case ZmContact.FA_FIRST_LAST_COMPANY: { // First Last (Company) 512 var name = ZmContact.fileAsFirstLast(attr.firstName, attr.lastName, attr.fullName, attr.nickname); 513 fa = ZmContact.fileAsNameCompany(name, attr.company); 514 } 515 break; 516 517 case ZmContact.FA_COMPANY_LAST_C_FIRST: { // Company (Last, First) 518 var name = ZmContact.fileAsLastFirst(attr.firstName, attr.lastName); 519 fa = ZmContact.fileAsCompanyName(name, attr.company); 520 } 521 break; 522 523 case ZmContact.FA_COMPANY_FIRST_LAST: { // Company (First Last) 524 var name = ZmContact.fileAsFirstLast(attr.firstName, attr.lastName); 525 fa = ZmContact.fileAsCompanyName(name, attr.company); 526 } 527 break; 528 529 case ZmContact.FA_CUSTOM: { // custom looks like this: "8:foobar" 530 return attr.fileAs.substring(2); 531 } 532 break; 533 } 534 return fa || attr.fullName || ""; 535 }; 536 537 /** 538 * Name printing helper "First Last". 539 * 540 * @param {String} first the first name 541 * @param {String} last the last name 542 * @param {String} fullname the fullname 543 * @param {String} nickname the nickname 544 * @return {String} the name format 545 */ 546 ZmContact.fileAsFirstLast = 547 function(first, last, fullname, nickname) { 548 if (first && last) 549 return AjxMessageFormat.format(ZmMsg.fileAsFirstLast, [first, last]); 550 return first || last || fullname || nickname || ""; 551 }; 552 553 /** 554 * Name printing helper "Last, First". 555 * 556 * @param {String} first the first name 557 * @param {String} last the last name 558 * @param {String} fullname the fullname 559 * @param {String} nickname the nickname 560 * @return {String} the name format 561 */ 562 ZmContact.fileAsLastFirst = 563 function(first, last, fullname, nickname) { 564 if (first && last) 565 return AjxMessageFormat.format(ZmMsg.fileAsLastFirst, [first, last]); 566 return last || first || fullname || nickname || ""; 567 }; 568 569 /** 570 * Name printing helper "Name (Company)". 571 * 572 * @param {String} name the contact name 573 * @param {String} company the company 574 * @return {String} the name format 575 */ 576 ZmContact.fileAsNameCompany = 577 function(name, company) { 578 if (name && company) 579 return AjxMessageFormat.format(ZmMsg.fileAsNameCompany, [name, company]); 580 if (company) 581 return AjxMessageFormat.format(ZmMsg.fileAsCompanyAsSecondaryOnly, [company]); 582 return name; 583 }; 584 585 /** 586 * Name printing helper "Company (Name)". 587 * 588 * @param {String} name the contact name 589 * @param {String} company the company 590 * @return {String} the name format 591 */ 592 ZmContact.fileAsCompanyName = 593 function(name, company) { 594 if (company && name) 595 return AjxMessageFormat.format(ZmMsg.fileAsCompanyName, [name, company]); 596 if (name) 597 return AjxMessageFormat.format(ZmMsg.fileAsNameAsSecondaryOnly, [name]); 598 return company; 599 }; 600 601 /** 602 * Computes the custom file as string by prepending "8:" to the given custom fileAs string. 603 * 604 * @param {Hash} customFileAs a set of contact attributes 605 * @return {String} the name format 606 */ 607 ZmContact.computeCustomFileAs = 608 function(customFileAs) { 609 return [ZmContact.FA_CUSTOM, ":", customFileAs].join(""); 610 }; 611 612 /* 613 * 614 * These next few static methods handle a contact that is either an anonymous 615 * object or an actual ZmContact. The former is used to optimize loading. The 616 * anonymous object is upgraded to a ZmContact when needed. 617 * 618 */ 619 620 /** 621 * Gets an attribute. 622 * 623 * @param {ZmContact} contact the contact 624 * @param {String} attr the attribute 625 * @return {Object} the attribute value or <code>null</code> for none 626 */ 627 ZmContact.getAttr = 628 function(contact, attr) { 629 return (contact instanceof ZmContact) 630 ? contact.getAttr(attr) 631 : (contact && contact._attrs) ? contact._attrs[attr] : null; 632 }; 633 634 /** 635 * returns the prefix of a string in the format "abc123". (would return "abc"). If the string is all number, it's a special case and returns the string itself. e.g. "234" would return "234". 636 */ 637 ZmContact.getPrefix = function(s) { 638 var trimmed = s.replace(/\d+$/, ""); 639 if (trimmed === "") { 640 //number only - don't trim. The number is the prefix. 641 return s; 642 } 643 return trimmed; 644 }; 645 646 /** 647 * Normalizes the numbering of the given attribute names and 648 * returns a new object with the re-numbered attributes. For 649 * example, if the attributes contains a "foo2" but no "foo", 650 * then the "foo2" attribute will be renamed to "foo" in the 651 * returned object. 652 * 653 * @param {Hash} attrs a hash of attributes to normalize. 654 * @param {String} [prefix] if specified, only the the attributes that match the given prefix will be returned 655 * @param {Array} [ignore] if specified, the attributes that are present in the array will not be normalized 656 * @return {Hash} a hash of normalized attributes 657 */ 658 ZmContact.getNormalizedAttrs = function(attrs, prefix, ignore) { 659 var nattrs = {}; 660 if (attrs) { 661 // normalize attribute numbering 662 var names = AjxUtil.keys(attrs); 663 names.sort(ZmContact.__BY_ATTRIBUTE); 664 var a = {}; 665 for (var i = 0; i < names.length; i++) { 666 var name = names[i]; 667 // get current count 668 var nprefix = ZmContact.getPrefix(name); 669 if (prefix && prefix != nprefix) continue; 670 if (AjxUtil.isArray(ignore) && AjxUtil.indexOf(ignore, nprefix)!=-1) { 671 nattrs[name] = attrs[name]; 672 } else { 673 if (!a[nprefix]) a[nprefix] = 0; 674 // normalize, if needed 675 var nname = ZmContact.getAttributeName(nprefix, ++a[nprefix]); 676 nattrs[nname] = attrs[name]; 677 } 678 } 679 } 680 return nattrs; 681 }; 682 683 ZmContact.__RE_ATTRIBUTE = /^(.*?)(\d+)$/; 684 ZmContact.__BY_ATTRIBUTE = function(a, b) { 685 var aa = a.match(ZmContact.__RE_ATTRIBUTE) || [a,a,1]; 686 var bb = b.match(ZmContact.__RE_ATTRIBUTE) || [b,b,1]; 687 return aa[1] == bb[1] ? Number(aa[2]) - Number(bb[2]) : aa[1].localeCompare(bb[1]); 688 }; 689 690 /** 691 * Sets the attribute. 692 * 693 * @param {ZmContact} contact the contact 694 * @param {String} attr the attribute 695 * @param {Object} value the attribute value 696 */ 697 ZmContact.setAttr = 698 function(contact, attr, value) { 699 if (contact instanceof ZmContact) 700 contact.setAttr(attr, value); 701 else 702 contact._attrs[attr] = value; 703 }; 704 705 /** 706 * Checks if the contact is in the trash. 707 * 708 * @param {ZmContact} contact the contact 709 * @return {Boolean} <code>true</code> if in trash 710 */ 711 ZmContact.isInTrash = 712 function(contact) { 713 var folderId = (contact instanceof ZmContact) ? contact.folderId : contact.l; 714 var folder = appCtxt.getById(folderId); 715 return (folder && folder.isInTrash()); 716 }; 717 718 /** 719 * @private 720 */ 721 ZmContact.prototype.load = 722 function(callback, errorCallback, batchCmd, deref) { 723 var jsonObj = {GetContactsRequest:{_jsns:"urn:zimbraMail"}}; 724 if (deref) { 725 jsonObj.GetContactsRequest.derefGroupMember = "1"; 726 } 727 var request = jsonObj.GetContactsRequest; 728 request.cn = [{id:this.id}]; 729 730 var respCallback = new AjxCallback(this, this._handleLoadResponse, [callback]); 731 732 if (batchCmd) { 733 var jsonObj = {GetContactsRequest:{_jsns:"urn:zimbraMail"}}; 734 if (deref) { 735 jsonObj.GetContactsRequest.derefGroupMember = "1"; 736 } 737 jsonObj.GetContactsRequest.cn = {id:this.id}; 738 batchCmd.addRequestParams(jsonObj, respCallback, errorCallback); 739 } else { 740 appCtxt.getAppController().sendRequest({jsonObj:jsonObj, 741 asyncMode:true, 742 callback:respCallback, 743 errorCallback:errorCallback}); 744 } 745 }; 746 747 /** 748 * @private 749 */ 750 ZmContact.prototype._handleLoadResponse = 751 function(callback, result) { 752 var resp = result.getResponse().GetContactsResponse; 753 754 // for now, we just assume only one contact was requested at a time 755 var contact = resp.cn[0]; 756 this.attr = contact._attrs; 757 if (contact.m) { 758 for (var i = 0; i < contact.m.length; i++) { 759 //cache contacts from contact groups (e.g. GAL contacts, shared contacts have not already been cached) 760 var member = contact.m[i]; 761 var isGal = false; 762 if (member.type == ZmContact.GROUP_GAL_REF) { 763 isGal = true; 764 } 765 if (member.cn && member.cn.length > 0) { 766 var memberContact = member.cn[0]; 767 memberContact.ref = memberContact.ref || (isGal && member.value); //we sometimes don't get "ref" but the "value" for GAL is the ref. 768 var loadMember = ZmContact.createFromDom(memberContact, {list: this.list, isGal: isGal}); //pass GAL so fileAS gets set correctly 769 loadMember.isDL = isGal && loadMember.attr[ZmContact.F_type] == "group"; 770 appCtxt.cacheSet(member.value, loadMember); 771 } 772 773 } 774 this._loadFromDom(contact); //load group 775 } 776 this.isLoaded = true; 777 if (callback) { 778 callback.run(contact, this); 779 } 780 }; 781 782 /** 783 * @private 784 */ 785 ZmContact.prototype.clear = 786 function() { 787 // bug fix #41666 - override base class method and do nothing 788 }; 789 790 /** 791 * Checks if the contact attributes are empty. 792 * 793 * @return {Boolean} <code>true</code> if empty 794 */ 795 ZmContact.prototype.isEmpty = 796 function() { 797 for (var i in this.attr) { 798 return false; 799 } 800 return true; 801 }; 802 803 /** 804 * Checks if the contact is shared. 805 * 806 * @return {Boolean} <code>true</code> if shared 807 */ 808 ZmContact.prototype.isShared = 809 function() { 810 return this.addrbook && this.addrbook.link; 811 }; 812 813 /** 814 * Checks if the contact is read-only. 815 * 816 * @return {Boolean} <code>true</code> if read-only 817 */ 818 ZmContact.prototype.isReadOnly = 819 function() { 820 if (this.isGal) { return true; } 821 822 return this.isShared() 823 ? this.addrbook && this.addrbook.isReadOnly() 824 : false; 825 }; 826 827 /** 828 * Checks if the contact is locked. This is different for DLs than read-only. 829 * 830 * @return {Boolean} <code>true</code> if read-only 831 */ 832 ZmContact.prototype.isLocked = 833 function() { 834 if (!this.isDistributionList()) { 835 return this.isReadOnly(); 836 } 837 if (!this.dlInfo) { 838 return false; //rare case after editing by an owner if the fileAsChanged, the new dl Info still not read, and the layout re-done. So don't show the lock. 839 } 840 var dlInfo = this.dlInfo; 841 if (dlInfo.isOwner) { 842 return false; 843 } 844 if (dlInfo.isMember) { 845 return dlInfo.unsubscriptionPolicy == ZmContactSplitView.SUBSCRIPTION_POLICY_REJECT; 846 } 847 return dlInfo.subscriptionPolicy == ZmContactSplitView.SUBSCRIPTION_POLICY_REJECT; 848 }; 849 850 /** 851 * Checks if the contact is a group. 852 * 853 * @return {Boolean} <code>true</code> if a group 854 */ 855 ZmContact.prototype.isGroup = 856 function() { 857 return this.getAttr(ZmContact.F_type) == "group" || this.type == ZmItem.GROUP; 858 }; 859 860 /** 861 * Checks if the contact is a DL. 862 * 863 * @return {Boolean} <code>true</code> if a group 864 */ 865 ZmContact.prototype.isDistributionList = 866 function() { 867 return this.isGal && this.isGroup(); 868 }; 869 870 871 // parses "groups" attr into AjxEmailAddress objects stored in 3 vectors (all, good, and bad) 872 /** 873 * Gets the group members. 874 * 875 * @return {AjxVector} the group members or <code>null</code> if not group 876 */ 877 ZmContact.prototype.getGroupMembers = 878 function() { 879 var allMembers = this.getAllGroupMembers(); 880 var addrs = []; 881 for (var i = 0; i < allMembers.length; i++) { 882 addrs.push(allMembers[i].toString()); 883 } 884 return AjxEmailAddress.parseEmailString(addrs.join(", ")); 885 }; 886 887 /** 888 * parses "groups" attr into an AjxEmailAddress with a few extra attributes (see ZmContactsHelper._wrapInlineContact) 889 * 890 * @return {AjxVector} the group members or <code>null</code> if not group 891 */ 892 ZmContact.prototype.getAllGroupMembers = 893 function() { 894 895 if (this.isDistributionList()) { 896 return this.dlMembers; 897 } 898 899 var addrs = []; 900 901 var groupMembers = this.attr[ZmContact.F_groups]; 902 if (!groupMembers){ 903 return AjxEmailAddress.parseEmailString(this.attr[ZmContact.F_email]); //I doubt this is needed or works correctly, but I keep this logic from before. If we don't have the group members, how can we return the group email instead? 904 } 905 for (var i = 0; i < groupMembers.length; i++) { 906 var member = groupMembers[i]; 907 var type = member.type; 908 var value = member.value; 909 if (type == ZmContact.GROUP_INLINE_REF) { 910 addrs.push(ZmContactsHelper._wrapInlineContact(value)); 911 } 912 else { 913 var contact = ZmContact.getContactFromCache(value); //TODO: handle contacts not cached? 914 if (!contact) { 915 DBG.println(AjxDebug.DBG1, "Disregarding uncached contact: " + value); 916 continue; 917 } 918 var ajxEmailAddress = ZmContactsHelper._wrapContact(contact); 919 if (ajxEmailAddress && type === ZmContact.GROUP_CONTACT_REF) { 920 ajxEmailAddress.groupRefValue = value; //don't normalize value 921 } 922 if (ajxEmailAddress) { 923 addrs.push(ajxEmailAddress); 924 } 925 } 926 } 927 return addrs; 928 }; 929 930 931 ZmContact.prototype.gatherExtraDlStuff = 932 function(callback) { 933 if (this.dlInfo && !this.dlInfo.isMinimal) { 934 //already there, skip to next step, loading DL Members 935 this.loadDlMembers(callback); 936 return; 937 } 938 var callbackFromGettingInfo = this._handleGetDlInfoResponse.bind(this, callback); 939 this.loadDlInfo(callbackFromGettingInfo); 940 }; 941 942 943 ZmContact.prototype._handleGetDlInfoResponse = 944 function(callback, result) { 945 var response = result._data.GetDistributionListResponse; 946 var dl = response.dl[0]; 947 var attrs = dl._attrs; 948 var isMember = dl.isMember; 949 var isOwner = dl.isOwner; 950 var mailPolicySpecificMailers = []; 951 this.dlInfo = { isMember: isMember, 952 isOwner: isOwner, 953 subscriptionPolicy: attrs.zimbraDistributionListSubscriptionPolicy, 954 unsubscriptionPolicy: attrs.zimbraDistributionListUnsubscriptionPolicy, 955 description: attrs.description || "", 956 displayName: attrs.displayName || "", 957 notes: attrs.zimbraNotes || "", 958 hideInGal: attrs.zimbraHideInGal == "TRUE", 959 mailPolicy: isOwner && this._getMailPolicy(dl, mailPolicySpecificMailers), 960 owners: isOwner && this._getOwners(dl)}; 961 this.dlInfo.mailPolicySpecificMailers = mailPolicySpecificMailers; 962 963 this.loadDlMembers(callback); 964 }; 965 966 ZmContact.prototype.loadDlMembers = 967 function(callback) { 968 if ((!appCtxt.get("EXPAND_DL_ENABLED") || this.dlInfo.hideInGal) && !this.dlInfo.isOwner) { 969 // can't get members if dl has zimbraHideInGal true, and not owner 970 //also, if zimbraFeatureDistributionListExpandMembersEnabled is false - also do not show the members (again unless it's the owner) 971 this.dlMembers = []; 972 if (callback) { 973 callback(); 974 } 975 return; 976 } 977 if (this.dlMembers) { 978 //already there - just callback 979 if (callback) { 980 callback(); 981 } 982 return; 983 } 984 var respCallback = this._handleGetDlMembersResponse.bind(this, callback); 985 this.getAllDLMembers(respCallback); 986 }; 987 988 989 ZmContact.prototype._handleGetDlMembersResponse = 990 function(callback, result) { 991 var list = result.list; 992 if (!list) { 993 this.dlMembers = []; 994 callback(); 995 return; 996 } 997 var members = []; 998 for (var i = 0; i < list.length; i++) { 999 members.push({type: ZmContact.GROUP_INLINE_REF, 1000 value: list[i], 1001 address: list[i]}); 1002 } 1003 1004 this.dlMembers = members; 1005 callback(); 1006 }; 1007 1008 ZmContact.prototype._getOwners = 1009 function(dl) { 1010 var owners = dl.owners[0].owner; 1011 var ownersArray = []; 1012 for (var i = 0; i < owners.length; i++) { 1013 var owner = owners[i].name; 1014 ownersArray.push(owner); //just the email address, I think and hope. 1015 } 1016 return ownersArray; 1017 }; 1018 1019 ZmContact.prototype._getMailPolicy = 1020 function(dl, specificMailers) { 1021 var mailPolicy; 1022 1023 var rights = dl.rights[0].right; 1024 var right = rights[0]; 1025 var grantees = right.grantee; 1026 if (!grantees) { 1027 return ZmGroupView.MAIL_POLICY_ANYONE; 1028 } 1029 for (var i = 0; i < grantees.length; i++) { 1030 var grantee = grantees[i]; 1031 1032 mailPolicy = ZmGroupView.GRANTEE_TYPE_TO_MAIL_POLICY_MAP[grantee.type]; 1033 1034 if (mailPolicy == ZmGroupView.MAIL_POLICY_SPECIFIC) { 1035 specificMailers.push(grantee.name); 1036 } 1037 else if (mailPolicy == ZmGroupView.MAIL_POLICY_ANYONE) { 1038 break; 1039 } 1040 else if (mailPolicy == ZmGroupView.MAIL_POLICY_INTERNAL) { 1041 break; 1042 } 1043 else if (mailPolicy == ZmGroupView.MAIL_POLICY_MEMBERS) { 1044 if (grantee.name == this.getEmail()) { 1045 //this means only members of this DL can send. 1046 break; 1047 } 1048 else { 1049 //must be another DL, and we do allow it, so treat it as regular user. 1050 specificMailers.push(grantee.name); 1051 mailPolicy = ZmGroupView.MAIL_POLICY_SPECIFIC; 1052 } 1053 } 1054 } 1055 mailPolicy = mailPolicy || ZmGroupView.MAIL_POLICY_ANYONE; 1056 1057 return mailPolicy; 1058 }; 1059 1060 1061 ZmContact.prototype.loadDlInfo = 1062 function(callback) { 1063 var soapDoc = AjxSoapDoc.create("GetDistributionListRequest", "urn:zimbraAccount", null); 1064 soapDoc.setMethodAttribute("needOwners", "1"); 1065 soapDoc.setMethodAttribute("needRights", "sendToDistList"); 1066 var elBy = soapDoc.set("dl", this.getEmail()); 1067 elBy.setAttribute("by", "name"); 1068 1069 appCtxt.getAppController().sendRequest({soapDoc: soapDoc, asyncMode: true, callback: callback}); 1070 }; 1071 1072 ZmContact.prototype.toggleSubscription = 1073 function(callback) { 1074 var soapDoc = AjxSoapDoc.create("SubscribeDistributionListRequest", "urn:zimbraAccount", null); 1075 soapDoc.setMethodAttribute("op", this.dlInfo.isMember ? "unsubscribe" : "subscribe"); 1076 var elBy = soapDoc.set("dl", this.getEmail()); 1077 elBy.setAttribute("by", "name"); 1078 appCtxt.getAppController().sendRequest({soapDoc: soapDoc, asyncMode: true, callback: callback}); 1079 }; 1080 1081 1082 1083 /** 1084 * Returns the contact id. If includeUserZid is true it will return the format zid:id 1085 * @param includeUserZid {boolean} true to include the zid prefix for the contact id 1086 * @return {String} contact id string 1087 */ 1088 ZmContact.prototype.getId = 1089 function(includeUserZid) { 1090 1091 if (includeUserZid) { 1092 return this.isShared() ? this.id : appCtxt.accountList.mainAccount.id + ":" + this.id; 1093 } 1094 1095 return this.id; 1096 }; 1097 /** 1098 * Gets the icon. 1099 * @param {ZmAddrBook} addrBook address book of contact 1100 * @return {String} the icon 1101 */ 1102 ZmContact.prototype.getIcon = 1103 function(addrBook) { 1104 if (this.isDistributionList()) { return "DistributionList"; } 1105 if (this.isGal) { return "GALContact"; } 1106 if (this.isShared() || (addrBook && addrBook.link)) { return "SharedContact"; } 1107 if (this.isGroup()) { return "Group"; } 1108 return "Contact"; 1109 }; 1110 1111 ZmContact.prototype.getIconLarge = 1112 function() { 1113 if (this.isDistributionList()) { 1114 return "Group_48"; 1115 } 1116 //todo - get a big version of ImgGalContact.png 1117 // if (this.isGal) { 1118 // } 1119 return "Person_48"; 1120 }; 1121 1122 /** 1123 * Gets the folder id. 1124 * 1125 * @return {String} the folder id 1126 */ 1127 ZmContact.prototype.getFolderId = 1128 function() { 1129 return this.isShared() 1130 ? this.folderId.split(":")[0] 1131 : this.folderId; 1132 }; 1133 1134 /** 1135 * Gets the attribute. 1136 * 1137 * @param {String} name the attribute name 1138 * @return {String} the value 1139 */ 1140 ZmContact.prototype.getAttr = 1141 function(name) { 1142 var val = this.attr[name]; 1143 return val ? ((val instanceof Array) ? val[0] : val) : ""; 1144 }; 1145 1146 /** 1147 * Sets the attribute. 1148 * 1149 * @param {String} name the attribute name 1150 * @param {String} value the attribute value 1151 */ 1152 ZmContact.prototype.setAttr = 1153 function(name, value) { 1154 this.attr[name] = value; 1155 }; 1156 1157 /** 1158 * Sets the participant status. 1159 * 1160 * @param {String} value the participant status value 1161 */ 1162 ZmContact.prototype.setParticipantStatus = 1163 function(ptst) { 1164 this.participantStatus = ptst; 1165 }; 1166 1167 /** 1168 * gets the participant status. 1169 * 1170 * @return {String} the value 1171 */ 1172 ZmContact.prototype.getParticipantStatus = 1173 function() { 1174 return this.participantStatus; 1175 }; 1176 1177 /** 1178 * Sets the participant role. 1179 * 1180 * @param {String} value the participant role value 1181 */ 1182 ZmContact.prototype.setParticipantRole = 1183 function(role) { 1184 this.participantRole = role; 1185 }; 1186 1187 /** 1188 * gets the participant role. 1189 * 1190 * @return {String} the value 1191 */ 1192 ZmContact.prototype.getParticipantRole = 1193 function() { 1194 return this.participantRole; 1195 }; 1196 1197 /** 1198 * Removes the attribute. 1199 * 1200 * @param {String} name the attribute name 1201 */ 1202 ZmContact.prototype.removeAttr = 1203 function(name) { 1204 delete this.attr[name]; 1205 }; 1206 1207 /** 1208 * Gets the contact attributes. 1209 * 1210 * @param {String} [prefix] if specified, only the the attributes that match the given prefix will be returned 1211 * @return {Hash} a hash of attribute/value pairs 1212 */ 1213 ZmContact.prototype.getAttrs = function(prefix) { 1214 var attrs = this.attr; 1215 if (prefix) { 1216 attrs = {}; 1217 for (var aname in this.attr) { 1218 var namePrefix = ZmContact.getPrefix(aname); 1219 if (namePrefix === prefix) { 1220 attrs[aname] = this.attr[aname]; 1221 } 1222 } 1223 } 1224 return attrs; 1225 }; 1226 1227 /** 1228 * Gets a normalized set of attributes where the attribute 1229 * names have been re-numbered as needed. For example, if the 1230 * attributes contains a "foo2" but no "foo", then the "foo2" 1231 * attribute will be renamed to "foo" in the returned object. 1232 * <p> 1233 * <strong>Note:</strong> 1234 * This method is expensive so should be called once and 1235 * cached temporarily as needed instead of being called 1236 * for each normalized attribute that is needed. 1237 * 1238 * @param {String} [prefix] if specified, only the 1239 * the attributes that match the given 1240 * prefix will be returned. 1241 * @return {Hash} a hash of attribute/value pairs 1242 */ 1243 ZmContact.prototype.getNormalizedAttrs = function(prefix) { 1244 return ZmContact.getNormalizedAttrs(this.attr, prefix, ZmContact.IGNORE_NORMALIZATION); 1245 }; 1246 1247 /** 1248 * Creates a contact from the given set of attributes. Used to create contacts on 1249 * the fly (rather than by loading them). This method is called by a list's <code>create()</code> 1250 * method. 1251 * <p> 1252 * If this is a GAL contact, we assume it is being added to the contact list.</p> 1253 * 1254 * @param {Hash} attr the attribute/value pairs for this contact 1255 * @param {ZmBatchCommand} batchCmd the batch command that contains this request 1256 * @param {boolean} isAutoCreate true if this is a auto create and toast message should not be shown 1257 */ 1258 ZmContact.prototype.create = 1259 function(attr, batchCmd, isAutoCreate) { 1260 1261 if (this.isDistributionList()) { 1262 this._createDl(attr); 1263 return; 1264 } 1265 1266 var jsonObj = {CreateContactRequest:{_jsns:"urn:zimbraMail"}}; 1267 var request = jsonObj.CreateContactRequest; 1268 var cn = request.cn = {}; 1269 1270 var folderId = attr[ZmContact.F_folderId] || ZmFolder.ID_CONTACTS; 1271 var folder = appCtxt.getById(folderId); 1272 if (folder && folder.isRemote()) { 1273 folderId = folder.getRemoteId(); 1274 } 1275 cn.l = folderId; 1276 cn.a = []; 1277 cn.m = []; 1278 1279 for (var name in attr) { 1280 if (name == ZmContact.F_folderId || 1281 name == "objectClass" || 1282 name == "zimbraId" || 1283 name == "createTimeStamp" || 1284 name == "modifyTimeStamp") { continue; } 1285 1286 if (name == ZmContact.F_groups) { 1287 this._addContactGroupAttr(cn, attr); 1288 } 1289 else { 1290 this._addRequestAttr(cn, name, attr[name]); 1291 } 1292 } 1293 1294 this._addRequestAttr(cn, ZmContact.X_fullName, ZmContact.computeFileAs(attr)); 1295 1296 var respCallback = new AjxCallback(this, this._handleResponseCreate, [attr, batchCmd != null, isAutoCreate]); 1297 1298 if (batchCmd) { 1299 batchCmd.addRequestParams(jsonObj, respCallback); 1300 } else { 1301 appCtxt.getAppController().sendRequest({jsonObj:jsonObj, asyncMode:true, callback:respCallback}); 1302 } 1303 }; 1304 1305 /** 1306 * @private 1307 */ 1308 ZmContact.prototype._handleResponseCreate = 1309 function(attr, isBatchMode, isAutoCreate, result) { 1310 // dont bother processing creates when in batch mode (just let create 1311 // notifications handle them) 1312 if (isBatchMode) { return; } 1313 1314 var resp = result.getResponse().CreateContactResponse; 1315 cn = resp ? resp.cn[0] : null; 1316 var id = cn ? cn.id : null; 1317 if (id) { 1318 this._fileAs = null; 1319 this._fullName = null; 1320 this.id = id; 1321 this.modified = cn.md; 1322 this.folderId = cn.l || ZmOrganizer.ID_ADDRBOOK; 1323 for (var a in attr) { 1324 if (!(attr[a] == undefined || attr[a] == '')) 1325 this.setAttr(a, attr[a]); 1326 } 1327 var groupMembers = cn ? cn.m : null; 1328 if (groupMembers) { 1329 this.attr[ZmContact.F_groups] = groupMembers; 1330 cn._attrs[ZmContact.F_groups] = groupMembers; 1331 } 1332 if (!isAutoCreate) { 1333 var msg = this.isGroup() ? ZmMsg.groupCreated : ZmMsg.contactCreated; 1334 appCtxt.getAppController().setStatusMsg(msg); 1335 } 1336 //update the canonical list. (this includes adding to the _idHash like before (bug 44132) calling updateIdHash. But calling that left the list inconcistant. 1337 appCtxt.getApp(ZmApp.CONTACTS).getContactList().add(cn); 1338 } else { 1339 var msg = this.isGroup() ? ZmMsg.errorCreateGroup : ZmMsg.errorCreateContact; 1340 var detail = ZmMsg.errorTryAgain + "\n" + ZmMsg.errorContact; 1341 appCtxt.getAppController().setStatusMsg(msg, ZmStatusView.LEVEL_CRITICAL, detail); 1342 } 1343 }; 1344 1345 /** 1346 * Creates a contct from a VCF part of a message. 1347 * 1348 * @param {String} msgId the message 1349 * @param {String} vcardPartId the vcard part id 1350 */ 1351 ZmContact.prototype.createFromVCard = 1352 function(msgId, vcardPartId) { 1353 var jsonObj = {CreateContactRequest:{_jsns:"urn:zimbraMail"}}; 1354 var cn = jsonObj.CreateContactRequest.cn = {l:ZmFolder.ID_CONTACTS}; 1355 cn.vcard = {mid:msgId, part:vcardPartId}; 1356 1357 var params = { 1358 jsonObj: jsonObj, 1359 asyncMode: true, 1360 callback: (new AjxCallback(this, this._handleResponseCreateVCard)), 1361 errorCallback: (new AjxCallback(this, this._handleErrorCreateVCard)) 1362 }; 1363 1364 appCtxt.getAppController().sendRequest(params); 1365 }; 1366 1367 /** 1368 * @private 1369 */ 1370 ZmContact.prototype._handleResponseCreateVCard = 1371 function(result) { 1372 appCtxt.getAppController().setStatusMsg(ZmMsg.contactCreated); 1373 }; 1374 1375 /** 1376 * @private 1377 */ 1378 ZmContact.prototype._handleErrorCreateVCard = 1379 function(ex) { 1380 appCtxt.getAppController().setStatusMsg(ZmMsg.errorCreateContact, ZmStatusView.LEVEL_CRITICAL); 1381 }; 1382 1383 /** 1384 * Updates contact attributes. 1385 * 1386 * @param {Hash} attr a set of attributes and new values 1387 * @param {AjxCallback} callback the callback 1388 * @param {boolean} isAutoSave true if it is a auto save and toast should not be displayed. 1389 */ 1390 ZmContact.prototype.modify = 1391 function(attr, callback, isAutoSave, batchCmd) { 1392 if (this.isDistributionList()) { 1393 this._modifyDl(attr); 1394 return; 1395 } 1396 if (this.list.isGal) { return; } 1397 1398 // change force to 0 and put up dialog if we get a MODIFY_CONFLICT fault? 1399 var jsonObj = {ModifyContactRequest:{_jsns:"urn:zimbraMail", replace:"0", force:"1"}}; 1400 var cn = jsonObj.ModifyContactRequest.cn = {id:this.id}; 1401 cn.a = []; 1402 cn.m = []; 1403 var continueRequest = false; 1404 1405 for (var name in attr) { 1406 if (name == ZmContact.F_folderId) { continue; } 1407 if (name == ZmContact.F_groups) { 1408 this._addContactGroupAttr(cn, attr); 1409 } 1410 else { 1411 this._addRequestAttr(cn, name, (attr[name] && attr[name].value) || attr[name]); 1412 } 1413 continueRequest = true; 1414 } 1415 1416 // bug: 45026 1417 if (ZmContact.F_firstName in attr || ZmContact.F_lastName in attr || ZmContact.F_company in attr || ZmContact.X_fileAs in attr) { 1418 var contact = {}; 1419 var fields = [ZmContact.F_firstName, ZmContact.F_lastName, ZmContact.F_company, ZmContact.X_fileAs]; 1420 for (var i = 0; i < fields.length; i++) { 1421 var field = fields[i]; 1422 var value = attr[field]; 1423 contact[field] = value != null ? value : this.getAttr(field); 1424 } 1425 var fullName = ZmContact.computeFileAs(contact); 1426 this._addRequestAttr(cn, ZmContact.X_fullName, fullName); 1427 } 1428 1429 if (continueRequest) { 1430 if (batchCmd) { 1431 batchCmd.addRequestParams(jsonObj, null, null); //no need for response callback for current use-case (batch modifying zimlet image) 1432 } 1433 else { 1434 var respCallback = this._handleResponseModify.bind(this, attr, callback, isAutoSave); 1435 appCtxt.getAppController().sendRequest({jsonObj: jsonObj, asyncMode: true, callback: respCallback}); 1436 } 1437 1438 } else { 1439 if (attr[ZmContact.F_folderId]) { 1440 this._setFolder(attr[ZmContact.F_folderId]); 1441 } 1442 } 1443 }; 1444 1445 ZmContact.prototype._createDl = 1446 function(attr) { 1447 1448 this.attr = attr; //this is mainly important for the email. attr is not set before this. 1449 1450 var createDlReq = this._getCreateDlReq(attr); 1451 1452 var reqs = []; 1453 1454 this._addMemberModsReqs(reqs, attr); 1455 1456 this._addMailPolicyAndOwnersReqs(reqs, attr); 1457 1458 var jsonObj = { 1459 BatchRequest: { 1460 _jsns: "urn:zimbra", 1461 CreateDistributionListRequest: createDlReq, 1462 DistributionListActionRequest: reqs 1463 } 1464 }; 1465 var respCallback = this._createDlResponseHandler.bind(this); 1466 appCtxt.getAppController().sendRequest({jsonObj: jsonObj, asyncMode: true, callback: respCallback}); 1467 1468 }; 1469 1470 ZmContact.prototype._addMailPolicyAndOwnersReqs = 1471 function(reqs, attr) { 1472 1473 var mailPolicy = attr[ZmContact.F_dlMailPolicy]; 1474 if (mailPolicy) { 1475 reqs.push(this._getSetMailPolicyReq(mailPolicy, attr[ZmContact.F_dlMailPolicySpecificMailers])); 1476 } 1477 1478 var listOwners = attr[ZmContact.F_dlListOwners]; 1479 if (listOwners) { 1480 reqs.push(this._getSetOwnersReq(listOwners)); 1481 } 1482 1483 1484 }; 1485 1486 1487 1488 ZmContact.prototype._addMemberModsReqs = 1489 function(reqs, attr) { 1490 var memberModifications = attr[ZmContact.F_groups]; 1491 var adds = []; 1492 var removes = []; 1493 if (memberModifications) { 1494 for (var i = 0; i < memberModifications.length; i++) { 1495 var mod = memberModifications[i]; 1496 var col = (mod.op == "+" ? adds : removes); 1497 col.push(mod); 1498 } 1499 } 1500 1501 if (adds.length > 0) { 1502 reqs.push(this._getAddOrRemoveReq(adds, true)); 1503 } 1504 if (removes.length > 0) { 1505 reqs.push(this._getAddOrRemoveReq(removes, false)); 1506 } 1507 }; 1508 1509 ZmContact.prototype._modifyDl = 1510 function(attr) { 1511 var reqs = []; 1512 1513 var newEmail = attr[ZmContact.F_email]; 1514 1515 var emailChanged = false; 1516 if (newEmail !== undefined) { 1517 emailChanged = true; 1518 reqs.push(this._getRenameDlReq(newEmail)); 1519 this.setAttr(ZmContact.F_email, newEmail); 1520 } 1521 1522 var modDlReq = this._getModifyDlAttributesReq(attr); 1523 if (modDlReq) { 1524 reqs.push(modDlReq); 1525 } 1526 1527 var displayName = attr[ZmContact.F_dlDisplayName]; 1528 if (displayName !== undefined) { 1529 this.setAttr(ZmContact.F_dlDisplayName, displayName); 1530 } 1531 1532 var oldFileAs = this.getFileAs(); 1533 this._resetCachedFields(); 1534 var fileAsChanged = oldFileAs != this.getFileAs(); 1535 1536 this._addMemberModsReqs(reqs, attr); 1537 1538 this._addMailPolicyAndOwnersReqs(reqs, attr); 1539 1540 if (reqs.length == 0) { 1541 this._modifyDlResponseHandler(false, null); //pretend it was saved 1542 return; 1543 } 1544 var jsonObj = { 1545 BatchRequest: { 1546 _jsns: "urn:zimbra", 1547 DistributionListActionRequest: reqs 1548 } 1549 }; 1550 var respCallback = this._modifyDlResponseHandler.bind(this, fileAsChanged || emailChanged); //there's some issue with fileAsChanged so adding the emailChanged to be on safe side 1551 appCtxt.getAppController().sendRequest({jsonObj: jsonObj, asyncMode: true, callback: respCallback}); 1552 1553 }; 1554 1555 ZmContact.prototype._getAddOrRemoveReq = 1556 function(members, add) { 1557 var req = { 1558 _jsns: "urn:zimbraAccount", 1559 dl: {by: "name", 1560 _content: this.getEmail() 1561 }, 1562 action: { 1563 op: add ? "addMembers" : "removeMembers", 1564 dlm: [] 1565 } 1566 }; 1567 for (var i = 0; i < members.length; i++) { 1568 var member = members[i]; 1569 req.action.dlm.push({_content: member.email}); 1570 } 1571 return req; 1572 1573 }; 1574 1575 1576 ZmContact.prototype._getRenameDlReq = 1577 function(name) { 1578 return { 1579 _jsns: "urn:zimbraAccount", 1580 dl: {by: "name", 1581 _content: this.getEmail() 1582 }, 1583 action: { 1584 op: "rename", 1585 newName: {_content: name} 1586 } 1587 }; 1588 }; 1589 1590 ZmContact.prototype._getSetOwnersReq = 1591 function(owners) { 1592 var ownersPart = []; 1593 for (var i = 0; i < owners.length; i++) { 1594 ownersPart.push({ 1595 type: ZmGroupView.GRANTEE_TYPE_USER, 1596 by: "name", 1597 _content: owners[i] 1598 }); 1599 } 1600 return { 1601 _jsns: "urn:zimbraAccount", 1602 dl: {by: "name", 1603 _content: this.getEmail() 1604 }, 1605 action: { 1606 op: "setOwners", 1607 owner: ownersPart 1608 } 1609 }; 1610 }; 1611 1612 ZmContact.prototype._getSetMailPolicyReq = 1613 function(mailPolicy, specificMailers) { 1614 var grantees = []; 1615 if (mailPolicy == ZmGroupView.MAIL_POLICY_SPECIFIC) { 1616 for (var i = 0; i < specificMailers.length; i++) { 1617 grantees.push({ 1618 type: ZmGroupView.GRANTEE_TYPE_EMAIL, 1619 by: "name", 1620 _content: specificMailers[i] 1621 }); 1622 } 1623 } 1624 else if (mailPolicy == ZmGroupView.MAIL_POLICY_ANYONE) { 1625 grantees.push({ 1626 type: ZmGroupView.GRANTEE_TYPE_PUBLIC 1627 }); 1628 } 1629 else if (mailPolicy == ZmGroupView.MAIL_POLICY_INTERNAL) { 1630 grantees.push({ 1631 type: ZmGroupView.GRANTEE_TYPE_ALL 1632 }); 1633 } 1634 else if (mailPolicy == ZmGroupView.MAIL_POLICY_MEMBERS) { 1635 grantees.push({ 1636 type: ZmGroupView.GRANTEE_TYPE_GROUP, 1637 by: "name", 1638 _content: this.getEmail() 1639 }); 1640 } 1641 else { 1642 throw "invalid mailPolicy value " + mailPolicy; 1643 } 1644 1645 return { 1646 _jsns: "urn:zimbraAccount", 1647 dl: {by: "name", 1648 _content: this.getEmail() 1649 }, 1650 action: { 1651 op: "setRights", 1652 right: { 1653 right: "sendToDistList", 1654 grantee: grantees 1655 } 1656 } 1657 }; 1658 1659 }; 1660 1661 ZmContact.prototype._addDlAttribute = 1662 function(attrs, mods, name, soapAttrName) { 1663 var attr = mods[name]; 1664 if (attr === undefined) { 1665 return; 1666 } 1667 attrs.push({n: soapAttrName, _content: attr}); 1668 }; 1669 1670 ZmContact.prototype._getDlAttributes = 1671 function(mods) { 1672 var attrs = []; 1673 this._addDlAttribute(attrs, mods, ZmContact.F_dlDisplayName, "displayName"); 1674 this._addDlAttribute(attrs, mods, ZmContact.F_dlDesc, "description"); 1675 this._addDlAttribute(attrs, mods, ZmContact.F_dlNotes, "zimbraNotes"); 1676 this._addDlAttribute(attrs, mods, ZmContact.F_dlHideInGal, "zimbraHideInGal"); 1677 this._addDlAttribute(attrs, mods, ZmContact.F_dlSubscriptionPolicy, "zimbraDistributionListSubscriptionPolicy"); 1678 this._addDlAttribute(attrs, mods, ZmContact.F_dlUnsubscriptionPolicy, "zimbraDistributionListUnsubscriptionPolicy"); 1679 1680 return attrs; 1681 }; 1682 1683 1684 ZmContact.prototype._getCreateDlReq = 1685 function(attr) { 1686 return { 1687 _jsns: "urn:zimbraAccount", 1688 name: attr[ZmContact.F_email], 1689 a: this._getDlAttributes(attr), 1690 dynamic: false 1691 }; 1692 }; 1693 1694 ZmContact.prototype._getModifyDlAttributesReq = 1695 function(attr) { 1696 var modAttrs = this._getDlAttributes(attr); 1697 if (modAttrs.length == 0) { 1698 return null; 1699 } 1700 return { 1701 _jsns: "urn:zimbraAccount", 1702 dl: {by: "name", 1703 _content: this.getEmail() 1704 }, 1705 action: { 1706 op: "modify", 1707 a: modAttrs 1708 } 1709 }; 1710 }; 1711 1712 ZmContact.prototype._modifyDlResponseHandler = 1713 function(fileAsChanged, result) { 1714 if (this._handleErrorDl(result)) { 1715 return; 1716 } 1717 appCtxt.setStatusMsg(ZmMsg.dlSaved); 1718 1719 //for DLs we reload from the server since the server does not send notifications. 1720 this.clearDlInfo(); 1721 1722 var details = { 1723 fileAsChanged: fileAsChanged 1724 }; 1725 1726 this._popView(fileAsChanged); 1727 1728 this._notify(ZmEvent.E_MODIFY, details); 1729 }; 1730 1731 ZmContact.prototype._createDlResponseHandler = 1732 function(result) { 1733 if (this._handleErrorDl(result, true)) { 1734 this.attr = {}; //since above in _createDl, we set it to new values prematurely. which would affect next gathering of modified attributes. 1735 return; 1736 } 1737 appCtxt.setStatusMsg(ZmMsg.distributionListCreated); 1738 1739 this._popView(true); 1740 }; 1741 1742 ZmContact.prototype._popView = 1743 function(updateDlList) { 1744 var controller = AjxDispatcher.run("GetContactController"); 1745 controller.popView(true); 1746 if (!updateDlList) { 1747 return; 1748 } 1749 var clc = AjxDispatcher.run("GetContactListController"); 1750 if (clc.getFolderId() != ZmFolder.ID_DLS) { 1751 return; 1752 } 1753 ZmAddrBookTreeController.dlFolderClicked(); //This is important in case of new DL created OR a renamed DL, so it would reflect in the list. 1754 }; 1755 1756 ZmContact.prototype._handleErrorDl = 1757 function(result, creation) { 1758 if (!result) { 1759 return false; 1760 } 1761 var batchResp = result.getResponse().BatchResponse; 1762 var faults = batchResp.Fault; 1763 if (!faults) { 1764 return false; 1765 } 1766 var ex = ZmCsfeCommand.faultToEx(faults[0]); 1767 var controller = AjxDispatcher.run("GetContactController"); 1768 controller.popupErrorDialog(creation ? ZmMsg.dlCreateFailed : ZmMsg.dlModifyFailed, ex); 1769 return true; 1770 1771 }; 1772 1773 ZmContact.prototype.clearDlInfo = 1774 function () { 1775 this.dlMembers = null; 1776 this.dlInfo = null; 1777 var app = appCtxt.getApp(ZmApp.CONTACTS); 1778 app.cacheDL(this.getEmail(), null); //clear the cache for this DL. 1779 appCtxt.cacheRemove(this.getId()); //also some other cache. 1780 }; 1781 1782 /** 1783 * @private 1784 */ 1785 ZmContact.prototype._handleResponseModify = 1786 function(attr, callback, isAutoSave, result) { 1787 var resp = result.getResponse().ModifyContactResponse; 1788 var cn = resp ? resp.cn[0] : null; 1789 var id = cn ? cn.id : null; 1790 var groupMembers = cn ? cn.m : null; 1791 if (groupMembers) { 1792 this.attr[ZmContact.F_groups] = groupMembers; 1793 cn._attrs[ZmContact.F_groups] = groupMembers; 1794 } 1795 1796 if (id && id == this.id) { 1797 if (!isAutoSave) { 1798 appCtxt.setStatusMsg(this.isGroup() ? ZmMsg.groupSaved : ZmMsg.contactSaved); 1799 } 1800 // was this contact moved to another folder? 1801 if (attr[ZmContact.F_folderId] && this.folderId != attr[ZmContact.F_folderId]) { 1802 this._setFolder(attr[ZmContact.F_folderId]); 1803 } 1804 appCtxt.getApp(ZmApp.CONTACTS).updateIdHash(cn, false); 1805 } else { 1806 var detail = ZmMsg.errorTryAgain + "\n" + ZmMsg.errorContact; 1807 appCtxt.getAppController().setStatusMsg(ZmMsg.errorModifyContact, ZmStatusView.LEVEL_CRITICAL, detail); 1808 } 1809 // NOTE: we no longer process callbacks here since notification handling 1810 // takes care of everything 1811 }; 1812 1813 /** 1814 * @private 1815 */ 1816 ZmContact.prototype._handleResponseMove = 1817 function(newFolderId, resp) { 1818 var newFolder = newFolderId && appCtxt.getById(newFolderId); 1819 var count = 1; 1820 if (newFolder) { 1821 appCtxt.setStatusMsg(ZmList.getActionSummary({ 1822 actionTextKey: 'actionMove', 1823 numItems: count, 1824 type: ZmItem.CONTACT, 1825 actionArg: newFolder.name 1826 })); 1827 } 1828 1829 this._notify(ZmEvent.E_MODIFY, resp); 1830 }; 1831 1832 /** 1833 * @private 1834 */ 1835 ZmContact.prototype._setFolder = 1836 function(newFolderId) { 1837 var folder = appCtxt.getById(this.folderId); 1838 var fId = folder ? folder.nId : null; 1839 if (fId == newFolderId) { return; } 1840 1841 // moving out of a share or into one is handled differently (create then hard delete) 1842 var newFolder = appCtxt.getById(newFolderId); 1843 if (this.isShared() || (newFolder && newFolder.link)) { 1844 if (this.list) { 1845 this.list.moveItems({items:[this], folder:newFolder}); 1846 } 1847 } else { 1848 var jsonObj = {ContactActionRequest:{_jsns:"urn:zimbraMail"}}; 1849 jsonObj.ContactActionRequest.action = {id:this.id, op:"move", l:newFolderId}; 1850 var respCallback = new AjxCallback(this, this._handleResponseMove, [newFolderId]); 1851 var accountName = appCtxt.multiAccounts && appCtxt.accountList.mainAccount.name; 1852 appCtxt.getAppController().sendRequest({jsonObj:jsonObj, asyncMode:true, callback:respCallback, accountName:accountName}); 1853 } 1854 }; 1855 1856 /** 1857 * @private 1858 */ 1859 ZmContact.prototype.notifyModify = 1860 function(obj, batchMode) { 1861 1862 var result = ZmItem.prototype.notifyModify.apply(this, arguments); 1863 1864 var context = window.parentAppCtxt || window.appCtxt; 1865 context.clearAutocompleteCache(ZmAutocomplete.AC_TYPE_CONTACT); 1866 1867 if (result) { 1868 return result; 1869 } 1870 1871 // cache old fileAs/fullName before resetting them 1872 var oldFileAs = this.getFileAs(); 1873 var oldFullName = this.getFullName(); 1874 this._resetCachedFields(); 1875 1876 var oldAttrCache = {}; 1877 if (obj._attrs) { 1878 // remove attrs that were not returned back from the server 1879 var oldAttrs = this.getAttrs(); 1880 for (var a in oldAttrs) { 1881 oldAttrCache[a] = oldAttrs[a]; 1882 if (obj._attrs[a] == null) 1883 this.removeAttr(a); 1884 } 1885 1886 // set attrs returned by server 1887 for (var a in obj._attrs) { 1888 this.setAttr(a, obj._attrs[a]); 1889 } 1890 if (obj.m) { 1891 this.setAttr(ZmContact.F_groups, obj.m); 1892 } 1893 } 1894 1895 var details = { 1896 attr: obj._attrs, 1897 oldAttr: oldAttrCache, 1898 fullNameChanged: (this.getFullName() != oldFullName), 1899 fileAsChanged: (this.getFileAs() != oldFileAs), 1900 contact: this 1901 }; 1902 1903 // update this contact's list per old/new attrs 1904 for (var listId in this._list) { 1905 var list = listId && appCtxt.getById(listId); 1906 if (!list) { continue; } 1907 list.modifyLocal(obj, details); 1908 } 1909 1910 this._notify(ZmEvent.E_MODIFY, obj); 1911 }; 1912 1913 /** 1914 * @private 1915 */ 1916 ZmContact.prototype.notifyDelete = 1917 function() { 1918 ZmItem.prototype.notifyDelete.call(this); 1919 var context = window.parentAppCtxt || window.appCtxt; 1920 context.clearAutocompleteCache(ZmAutocomplete.AC_TYPE_CONTACT); 1921 }; 1922 1923 /** 1924 * Initializes this contact using an email address. 1925 * 1926 * @param {AjxEmailAddress|String} email an email address or an email string 1927 * @param {Boolean} strictName if <code>true</code>, do not try to set name from user portion of address 1928 */ 1929 ZmContact.prototype.initFromEmail = 1930 function(email, strictName) { 1931 if (email instanceof AjxEmailAddress) { 1932 this.setAttr(ZmContact.F_email, email.getAddress()); 1933 this._initFullName(email, strictName); 1934 } else { 1935 this.setAttr(ZmContact.F_email, email); 1936 } 1937 }; 1938 1939 /** 1940 * Initializes this contact using a phone number. 1941 * 1942 * @param {String} phone the phone string 1943 * @param {String} field the field or company phone if <code>null</code> 1944 */ 1945 ZmContact.prototype.initFromPhone = 1946 function(phone, field) { 1947 this.setAttr(field || ZmContact.F_companyPhone, phone); 1948 }; 1949 1950 /** 1951 * Gets the email address. 1952 * 1953 * @param {boolean} asObj if true, return an AjxEmailAddress 1954 * 1955 * @return the email address 1956 */ 1957 ZmContact.prototype.getEmail = 1958 function(asObj) { 1959 1960 var email = (this.getAttr(ZmContact.F_email) || 1961 this.getAttr(ZmContact.F_workEmail1) || 1962 this.getAttr(ZmContact.F_email2) || 1963 this.getAttr(ZmContact.F_workEmail2) || 1964 this.getAttr(ZmContact.F_email3) || 1965 this.getAttr(ZmContact.F_workEmail3)); 1966 1967 if (asObj) { 1968 email = AjxEmailAddress.parse(email); 1969 if(email){ 1970 email.isGroup = this.isGroup(); 1971 email.canExpand = this.canExpand; 1972 } 1973 } 1974 1975 return email; 1976 }; 1977 1978 /** 1979 * Returns user's phone number 1980 * @return {String} phone number 1981 */ 1982 ZmContact.prototype.getPhone = 1983 function() { 1984 var phone = (this.getAttr(ZmContact.F_mobilePhone) || 1985 this.getAttr(ZmContact.F_workPhone) || 1986 this.getAttr(ZmContact.F_homePhone) || 1987 this.getAttr(ZmContact.F_otherPhone)); 1988 return phone; 1989 }; 1990 1991 1992 /** 1993 * Gets the lookup email address, when an contact object is located using email address we store 1994 * the referred email address in this variable for easy lookup 1995 * 1996 * @param {boolean} asObj if true, return an AjxEmailAddress 1997 * 1998 * @return the lookup address 1999 */ 2000 ZmContact.prototype.getLookupEmail = 2001 function(asObj) { 2002 var email = this._lookupEmail; 2003 2004 if (asObj && email) { 2005 email = AjxEmailAddress.parse(email); 2006 email.isGroup = this.isGroup(); 2007 email.canExpand = this.canExpand; 2008 } 2009 2010 return email; 2011 }; 2012 2013 /** 2014 * Gets the emails. 2015 * 2016 * @return {Array} an array of all valid emails for this contact 2017 */ 2018 ZmContact.prototype.getEmails = 2019 function() { 2020 var emails = []; 2021 var attrs = this.getAttrs(); 2022 for (var index = 0; index < ZmContact.EMAIL_FIELDS.length; index++) { 2023 var field = ZmContact.EMAIL_FIELDS[index]; 2024 for (var i = 1; true; i++) { 2025 var aname = ZmContact.getAttributeName(field, i); 2026 if (!attrs[aname]) break; 2027 emails.push(attrs[aname]); 2028 } 2029 } 2030 return emails; 2031 }; 2032 2033 /** 2034 * Gets the full name. 2035 * 2036 * @return {String} the full name 2037 */ 2038 ZmContact.prototype.getFullName = 2039 function(html) { 2040 var fullNameHtml = null; 2041 if (!this._fullName || html) { 2042 var fullName = this.getAttr(ZmContact.X_fullName); // present if GAL contact 2043 if (fullName) { 2044 this._fullName = (fullName instanceof Array) ? fullName[0] : fullName; 2045 } 2046 else { 2047 this._fullName = this.getFullNameForDisplay(false); 2048 } 2049 2050 if (html) { 2051 fullNameHtml = this.getFullNameForDisplay(html); 2052 } 2053 } 2054 2055 // as a last resort, set it to fileAs 2056 if (!this._fullName) { 2057 this._fullName = this.getFileAs(); 2058 } 2059 2060 return fullNameHtml || this._fullName; 2061 }; 2062 2063 /* 2064 * Gets the fullname for display -- includes (if applicable): prefix, first, middle, maiden, last, suffix 2065 * 2066 * @param {boolean} if phonetic fields should be used 2067 * @return {String} the fullname for display 2068 */ 2069 ZmContact.prototype.getFullNameForDisplay = 2070 function(html){ 2071 if (this.isDistributionList()) { 2072 //I'm not sure where that fullName is set sometime to the display name. This is so complicated 2073 // I'm trying to set attr[ZmContact.F_dlDisplayName] to the display name but in soem cases it's not. 2074 return this.getAttr(ZmContact.F_dlDisplayName) || this.getAttr("fullName"); 2075 } 2076 var prefix = this.getAttr(ZmContact.F_namePrefix); 2077 var first = this.getAttr(ZmContact.F_firstName); 2078 var middle = this.getAttr(ZmContact.F_middleName); 2079 var maiden = this.getAttr(ZmContact.F_maidenName); 2080 var last = this.getAttr(ZmContact.F_lastName); 2081 var suffix = this.getAttr(ZmContact.F_nameSuffix); 2082 var pattern = ZmMsg.fullname; 2083 if (suffix) { 2084 pattern = maiden ? ZmMsg.fullnameMaidenSuffix : ZmMsg.fullnameSuffix; 2085 } 2086 else if (maiden) { 2087 pattern = ZmMsg.fullnameMaiden; 2088 } 2089 if (appCtxt.get(ZmSetting.LOCALE_NAME) === "ja") { 2090 var fileAsId = this.getAttr(ZmContact.F_fileAs); 2091 if (!AjxUtil.isEmpty(fileAsId) && fileAsId !== "1" && fileAsId !== "4" && fileAsId !== "6") { 2092 /* When Japanese locale is selected, in the most every case, the name should be 2093 * displayed as "Last First" which is set by the default pattern (ZmMsg_ja.fullname). 2094 * But if the contact entry's fileAs field explicitly specifies the display 2095 * format as "First Last", we should override the pattern to lay it out so. 2096 * For other locales, it is not necessary to override the pattern: The default pattern is 2097 * already set as "First Last", and even the FileAs specifies as "Last, First", the display 2098 * name is always expected to be displayed as "First Last". 2099 */ 2100 pattern = "{0} {1} {2} {4}"; 2101 } 2102 } 2103 var formatter = new AjxMessageFormat(pattern); 2104 var args = [prefix,first,middle,maiden,last,suffix]; 2105 if (!html){ 2106 return AjxStringUtil.trim(formatter.format(args), true); 2107 } 2108 2109 return this._getFullNameHtml(formatter, args); 2110 }; 2111 2112 /** 2113 * @param formatter 2114 * @param parts {Array} Name parts: [prefix,first,middle,maiden,last,suffix] 2115 */ 2116 ZmContact.prototype._getFullNameHtml = function(formatter, parts) { 2117 var a = []; 2118 var segments = formatter.getSegments(); 2119 for (var i = 0; i < segments.length; i++) { 2120 var segment = segments[i]; 2121 if (segment instanceof AjxFormat.TextSegment) { 2122 a.push(segment.format()); 2123 continue; 2124 } 2125 // NOTE: Assume that it's a AjxMessageFormat.MessageSegment 2126 // NOTE: if not a AjxFormat.TextSegment. 2127 var index = segment.getIndex(); 2128 var base = parts[index]; 2129 var text = ZmContact.__RUBY_FIELDS[index] && this.getAttr(ZmContact.__RUBY_FIELDS[index]); 2130 a.push(AjxStringUtil.htmlRubyEncode(base, text)); 2131 } 2132 return a.join(""); 2133 }; 2134 ZmContact.__RUBY_FIELDS = [ 2135 null, ZmContact.F_phoneticFirstName, null, null, 2136 ZmContact.F_phoneticLastName, null 2137 ]; 2138 2139 /** 2140 * Gets the tool tip for this contact. 2141 * 2142 * @param {String} email the email address 2143 * @param {Boolean} isGal (not used) 2144 * @param {String} hint the hint text 2145 * @return {String} the tool tip in HTML 2146 */ 2147 ZmContact.prototype.getToolTip = 2148 function(email, isGal, hint) { 2149 // XXX: we dont cache tooltip info anymore since its too dynamic :/ 2150 // i.e. IM status can change anytime so always rebuild tooltip and bug 13834 2151 var subs = { 2152 contact: this, 2153 entryTitle: this.getFileAs(), 2154 hint: hint 2155 }; 2156 2157 return (AjxTemplate.expand("abook.Contacts#Tooltip", subs)); 2158 }; 2159 2160 /** 2161 * Gets the filing string for this contact, computing it if necessary. 2162 * 2163 * @param {Boolean} lower <code>true</code> to use lower case 2164 * @return {String} the file as string 2165 */ 2166 ZmContact.prototype.getFileAs = 2167 function(lower) { 2168 // update/null if modified 2169 if (!this._fileAs) { 2170 this._fileAs = ZmContact.computeFileAs(this); 2171 this._fileAsLC = this._fileAs ? this._fileAs.toLowerCase() : null; 2172 } 2173 // if for some reason fileAsLC is not set even though fileAs is, reset it 2174 if (lower && !this._fileAsLC) { 2175 this._fileAsLC = this._fileAs.toLowerCase(); 2176 } 2177 return lower ? this._fileAsLC : this._fileAs; 2178 }; 2179 2180 /** 2181 * Gets the filing string for this contact, from the email address (used in case no name exists). 2182 * todo - maybe return this from getFileAs, but there are a lot of callers to getFileAs, and not sure 2183 * of the implications on all the use-cases. 2184 * 2185 * @return {String} the file as string 2186 */ 2187 ZmContact.prototype.getFileAsNoName = function() { 2188 return [ZmMsg.noName, this.getEmail()].join(" "); 2189 }; 2190 2191 /** 2192 * Gets the header. 2193 * 2194 * @return {String} the header 2195 */ 2196 ZmContact.prototype.getHeader = 2197 function() { 2198 return this.id ? this.getFileAs() : ZmMsg.newContact; 2199 }; 2200 2201 ZmContact.NO_MAX_IMAGE_WIDTH = ZmContact.NO_MAX_IMAGE_HEIGHT = - 1; 2202 2203 /** 2204 * Get the image URL. 2205 * 2206 * Please note that maxWidth and maxHeight are hints, as they have no 2207 * effect on Zimlet-supplied images. 2208 * 2209 * maxWidth {int} max pixel width (optional - default 48, or pass ZmContact.NO_MAX_IMAGE_WIDTH if full size image is required) 2210 * maxHeight {int} max pixel height (optional - default to maxWidth, or pass ZmContact.NO_MAX_IMAGE_HEIGHT if full size image is required) 2211 * @return {String} the image URL 2212 */ 2213 ZmContact.prototype.getImageUrl = 2214 function(maxWidth, maxHeight) { 2215 var image = this.getAttr(ZmContact.F_image); 2216 var imagePart = image && image.part || this.getAttr(ZmContact.F_imagepart); //see bug 73146 2217 2218 if (!imagePart) { 2219 return this.getAttr(ZmContact.F_zimletImage); //return zimlet populated image only if user-uploaded image is not there. 2220 } 2221 var msgFetchUrl = appCtxt.get(ZmSetting.CSFE_MSG_FETCHER_URI); 2222 var maxWidthStyle = ""; 2223 if (maxWidth !== ZmContact.NO_MAX_IMAGE_WIDTH) { 2224 maxWidth = maxWidth || 48; 2225 maxWidthStyle = ["&max_width=", maxWidth].join(""); 2226 } 2227 var maxHeightStyle = ""; 2228 if (maxHeight !== ZmContact.NO_MAX_IMAGE_HEIGHT) { 2229 maxHeight = maxHeight || 2230 (maxWidth !== ZmContact.NO_MAX_IMAGE_WIDTH ? maxWidth : 48); 2231 maxHeightStyle = ["&max_height=", maxHeight].join(""); 2232 } 2233 return [msgFetchUrl, "&id=", this.id, "&part=", imagePart, maxWidthStyle, maxHeightStyle, "&t=", (new Date()).getTime()].join(""); 2234 }; 2235 2236 ZmContact.prototype.addModifyZimletImageToBatch = 2237 function(batchCmd, image) { 2238 var attr = {}; 2239 if (this.getAttr(ZmContact.F_zimletImage) === image) { 2240 return; //no need to update if same 2241 } 2242 attr[ZmContact.F_zimletImage] = image; 2243 batchCmd.add(this.modify.bind(this, attr, null, true)); 2244 }; 2245 2246 /** 2247 * Gets the company field. Company field has a getter b/c fileAs may be the Company name so 2248 * company field should return "last, first" name instead *or* prepend the title 2249 * if fileAs is not Company (assuming it exists). 2250 * 2251 * @return {String} the company 2252 */ 2253 ZmContact.prototype.getCompanyField = 2254 function() { 2255 2256 var attrs = this.getAttrs(); 2257 if (attrs == null) return null; 2258 2259 var fa = parseInt(attrs.fileAs); 2260 var val = []; 2261 var idx = 0; 2262 2263 if (fa == ZmContact.FA_LAST_C_FIRST || fa == ZmContact.FA_FIRST_LAST) { 2264 // return the title, company name 2265 if (attrs.jobTitle) { 2266 val[idx++] = attrs.jobTitle; 2267 if (attrs.company) 2268 val[idx++] = ", "; 2269 } 2270 if (attrs.company) 2271 val[idx++] = attrs.company; 2272 2273 } else if (fa == ZmContact.FA_COMPANY) { 2274 // return the first/last name 2275 if (attrs.lastName) { 2276 val[idx++] = attrs.lastName; 2277 if (attrs.firstName) 2278 val[idx++] = ", "; 2279 } 2280 2281 if (attrs.firstName) 2282 val[idx++] = attrs.firstName; 2283 2284 if (attrs.jobTitle) 2285 val[idx++] = " (" + attrs.jobTitle + ")"; 2286 2287 } else { 2288 // just return the title 2289 if (attrs.jobTitle) { 2290 val[idx++] = attrs.jobTitle; 2291 // and/or company name if applicable 2292 if (attrs.company && (attrs.fileAs == null || fa == ZmContact.FA_LAST_C_FIRST || fa == ZmContact.FA_FIRST_LAST)) 2293 val[idx++] = ", "; 2294 } 2295 if (attrs.company && (attrs.fileAs == null || fa == ZmContact.FA_LAST_C_FIRST || fa == ZmContact.FA_FIRST_LAST)) 2296 val[idx++] = attrs.company; 2297 } 2298 if (val.length == 0) return null; 2299 return val.join(""); 2300 }; 2301 2302 /** 2303 * Gets the work address. 2304 * 2305 * @param {Object} instance (not used) 2306 * @return {String} the work address 2307 */ 2308 ZmContact.prototype.getWorkAddrField = 2309 function(instance) { 2310 var attrs = this.getAttrs(); 2311 return this._getAddressField(attrs.workStreet, attrs.workCity, attrs.workState, attrs.workPostalCode, attrs.workCountry); 2312 }; 2313 2314 /** 2315 * Gets the home address. 2316 * 2317 * @param {Object} instance (not used) 2318 * @return {String} the home address 2319 */ 2320 ZmContact.prototype.getHomeAddrField = 2321 function(instance) { 2322 var attrs = this.getAttrs(); 2323 return this._getAddressField(attrs.homeStreet, attrs.homeCity, attrs.homeState, attrs.homePostalCode, attrs.homeCountry); 2324 }; 2325 2326 /** 2327 * Gets the other address. 2328 * 2329 * @param {Object} instance (not used) 2330 * @return {String} the other address 2331 */ 2332 ZmContact.prototype.getOtherAddrField = 2333 function(instance) { 2334 var attrs = this.getAttrs(); 2335 return this._getAddressField(attrs.otherStreet, attrs.otherCity, attrs.otherState, attrs.otherPostalCode, attrs.otherCountry); 2336 }; 2337 2338 /** 2339 * Gets the address book. 2340 * 2341 * @return {ZmAddrBook} the address book 2342 */ 2343 ZmContact.prototype.getAddressBook = 2344 function() { 2345 if (!this.addrbook) { 2346 this.addrbook = appCtxt.getById(this.folderId); 2347 } 2348 return this.addrbook; 2349 }; 2350 2351 /** 2352 * @private 2353 */ 2354 ZmContact.prototype._getAddressField = 2355 function(street, city, state, zipcode, country) { 2356 if (street == null && city == null && state == null && zipcode == null && country == null) return null; 2357 2358 var html = []; 2359 var idx = 0; 2360 2361 if (street) { 2362 html[idx++] = street; 2363 if (city || state || zipcode) 2364 html[idx++] = "\n"; 2365 } 2366 2367 if (city) { 2368 html[idx++] = city; 2369 if (state) 2370 html[idx++] = ", "; 2371 else if (zipcode) 2372 html[idx++] = " "; 2373 } 2374 2375 if (state) { 2376 html[idx++] = state; 2377 if (zipcode) 2378 html[idx++] = " "; 2379 } 2380 2381 if (zipcode) 2382 html[idx++] = zipcode; 2383 2384 if (country) 2385 html[idx++] = "\n" + country; 2386 2387 return html.join(""); 2388 }; 2389 2390 /** 2391 * Sets the full name based on an email address. 2392 * 2393 * @private 2394 */ 2395 ZmContact.prototype._initFullName = 2396 function(email, strictName) { 2397 var name = email.getName(); 2398 name = AjxStringUtil.trim(name.replace(AjxEmailAddress.commentPat, '')); // strip comment (text in parens) 2399 2400 if (name && name.length) { 2401 this._setFullName(name, [" "]); 2402 } else if (!strictName) { 2403 name = email.getAddress(); 2404 if (name && name.length) { 2405 var i = name.indexOf("@"); 2406 if (i == -1) return; 2407 name = name.substr(0, i); 2408 this._setFullName(name, [".", "_"]); 2409 } 2410 } 2411 }; 2412 2413 /** 2414 * Tries to extract a set of name components from the given text, with the 2415 * given list of possible delimiters. The first delimiter contained in the 2416 * text will be used. If none are found, the first delimiter in the list is used. 2417 * 2418 * @private 2419 */ 2420 ZmContact.prototype._setFullName = 2421 function(text, delims) { 2422 var delim = delims[0]; 2423 for (var i = 0; i < delims.length; i++) { 2424 if (text.indexOf(delims[i]) != -1) { 2425 delim = delims[i]; 2426 break; 2427 } 2428 } 2429 var parts = text.split(delim); 2430 var func = this["__setFullName_"+AjxEnv.DEFAULT_LOCALE] || this.__setFullName; 2431 func.call(this, parts, text, delims); 2432 }; 2433 2434 ZmContact.prototype.__setFullName = function(parts, text, delims) { 2435 this.setAttr(ZmContact.F_firstName, parts[0]); 2436 if (parts.length == 2) { 2437 this.setAttr(ZmContact.F_lastName, parts[1]); 2438 } else if (parts.length == 3) { 2439 this.setAttr(ZmContact.F_middleName, parts[1]); 2440 this.setAttr(ZmContact.F_lastName, parts[2]); 2441 } 2442 }; 2443 ZmContact.prototype.__setFullName_ja = function(parts, text, delims) { 2444 if (parts.length > 2) { 2445 this.__setFullName(parts, text, delims); 2446 return; 2447 } 2448 // TODO: Perhaps do some analysis to auto-detect Japanese vs. 2449 // TODO: non-Japanese names. For example, if the name text is 2450 // TODO: comprised of kanji, treat it as "last first"; else if 2451 // TODO: first part is all uppercase, treat it as "last first"; 2452 // TODO: else treat it as "first last". 2453 this.setAttr(ZmContact.F_lastName, parts[0]); 2454 if (parts.length > 1) { 2455 this.setAttr(ZmContact.F_firstName, parts[1]); 2456 } 2457 }; 2458 ZmContact.prototype.__setFullName_ja_JP = ZmContact.prototype.__setFullName_ja; 2459 2460 /** 2461 * @private 2462 */ 2463 ZmContact.prototype._addRequestAttr = 2464 function(cn, name, value) { 2465 var a = {n:name}; 2466 if (name == ZmContact.F_image && AjxUtil.isString(value) && value.length) { 2467 // handle contact photo 2468 if (value.indexOf("aid_") != -1) { 2469 a.aid = value.substring(4); 2470 } else { 2471 a.part = value.substring(5); 2472 } 2473 } else { 2474 a._content = value || ""; 2475 } 2476 2477 if (value instanceof Array) { 2478 if (!cn._attrs) 2479 cn._attrs = {}; 2480 cn._attrs[name] = value || ""; 2481 } 2482 else { 2483 if (!cn.a) 2484 cn.a = []; 2485 cn.a.push(a); 2486 } 2487 }; 2488 2489 ZmContact.prototype._addContactGroupAttr = 2490 function(cn, group) { 2491 var groupMembers = group[ZmContact.F_groups]; 2492 for (var i = 0; i < groupMembers.length; i++) { 2493 var member = groupMembers[i]; 2494 if (!cn.m) { 2495 cn.m = []; 2496 } 2497 2498 var m = {type: member.type, value: member.value}; //for the JSON object this is all we need. 2499 if (member.op) { 2500 m.op = member.op; //this is only for modify, not for create. 2501 } 2502 cn.m.push(m); 2503 } 2504 }; 2505 2506 /** 2507 * Reset computed fields. 2508 * 2509 * @private 2510 */ 2511 ZmContact.prototype._resetCachedFields = 2512 function() { 2513 this._fileAs = this._fileAsLC = this._fullName = null; 2514 }; 2515 2516 /** 2517 * Parse contact node. 2518 * 2519 * @private 2520 */ 2521 ZmContact.prototype._loadFromDom = 2522 function(node) { 2523 this.isLoaded = true; 2524 this.rev = node.rev; 2525 this.sf = node.sf || node._attrs.sf; 2526 if (!this.isGal) { 2527 this.folderId = node.l; 2528 } 2529 this.created = node.cd; 2530 this.modified = node.md; 2531 2532 this.attr = node._attrs || {}; 2533 if (node.m) { 2534 this.attr[ZmContact.F_groups] = node.m; 2535 } 2536 2537 this.ref = node.ref || this.attr.dn; //bug 78425 2538 2539 // for shared contacts, we get these fields outside of the attr part 2540 if (node.email) { this.attr[ZmContact.F_email] = node.email; } 2541 if (node.email2) { this.attr[ZmContact.F_email2] = node.email2; } 2542 if (node.email3) { this.attr[ZmContact.F_email3] = node.email3; } 2543 2544 // in case attrs are coming in from an external GAL, make an effort to map them, including multivalued ones 2545 this.attr = ZmContact.mapAttrs(this.attr); 2546 2547 //the attr groups is returned as [] so check both null and empty array to set the type 2548 var groups = this.attr[ZmContact.F_groups]; 2549 if(!groups || (groups instanceof Array && groups.length == 0)) { 2550 this.type = ZmItem.CONTACT; 2551 } 2552 else { 2553 this.type = ZmItem.GROUP; 2554 } 2555 2556 // check if the folderId is found in our address book (otherwise, we assume 2557 // this contact to be a shared contact) 2558 var ac = window.parentAppCtxt || window.appCtxt; 2559 this.addrbook = ac.getById(this.folderId); 2560 2561 this._parseTagNames(node.tn); 2562 2563 // dont process flags for shared contacts until we get server support 2564 if (!this.isShared()) { 2565 this._parseFlags(node.f); 2566 } else { 2567 // shared contacts are never fully loaded since we never cache them 2568 this.isLoaded = false; 2569 } 2570 2571 // bug: 22174 2572 // We ignore the server's computed file-as property and instead 2573 // format it based on the user's locale. 2574 this._fileAs = ZmContact.computeFileAs(this); 2575 2576 // Is this a distribution list? 2577 this.isDL = this.isDistributionList(); 2578 if (this.isDL) { 2579 this.dlInfo = { //this is minimal DL info, available mainly to allow to know whether to show the lock or not. 2580 isMinimal: true, 2581 isMember: node.isMember, 2582 isOwner: node.isOwner, 2583 subscriptionPolicy: this.attr.zimbraDistributionListSubscriptionPolicy, 2584 unsubscriptionPolicy: this.attr.zimbraDistributionListUnsubscriptionPolicy, 2585 displayName: node.d || "", 2586 hideInGal: this.attr.zimbraHideInGal == "TRUE" 2587 }; 2588 2589 this.canExpand = node.exp !== false; //default to true, since most cases this is implicitly true if not returned. See bug 94867 2590 var emails = this.getEmails(); 2591 var ac = window.parentAppCtxt || window.appCtxt; 2592 for (var i = 0; i < emails.length; i++) { 2593 ac.setIsExpandableDL(emails[i], this.canExpand); 2594 } 2595 } 2596 }; 2597 2598 /** 2599 * Gets display text for an attendee. Prefers name over email. 2600 * 2601 * @param {constant} type the attendee type 2602 * @param {Boolean} shortForm if <code>true</code>, return only name or email 2603 * @return {String} the attendee 2604 */ 2605 ZmContact.prototype.getAttendeeText = 2606 function(type, shortForm) { 2607 var email = this.getEmail(true); 2608 return (email?email.toString(shortForm || (type && type != ZmCalBaseItem.PERSON)):""); 2609 }; 2610 2611 /** 2612 * Gets display text for an attendee. Prefers name over email. 2613 * 2614 * @param {constant} type the attendee type 2615 * @param {Boolean} shortForm if <code>true</code>, return only name or email 2616 * @return {String} the attendee 2617 */ 2618 ZmContact.prototype.getAttendeeKey = 2619 function() { 2620 var email = this.getLookupEmail() || this.getEmail(); 2621 var name = this.getFullName(); 2622 return email ? email : name; 2623 }; 2624 2625 /** 2626 * Gets the unknown fields. 2627 * 2628 * @param {function} [sortByNameFunc] sort by function 2629 * @return {Array} an array of field name/value pairs 2630 */ 2631 ZmContact.prototype.getUnknownFields = function(sortByNameFunc) { 2632 var map = ZmContact.__FIELD_MAP; 2633 if (!map) { 2634 map = ZmContact.__FIELD_MAP = {}; 2635 for (var i = 0; i < ZmContact.DISPLAY_FIELDS; i++) { 2636 map[ZmContact.DISPLAY_FIELDS[i]] = true; 2637 } 2638 } 2639 var fields = []; 2640 var attrs = this.getAttrs(); 2641 for (var aname in attrs) { 2642 var field = ZmContact.getPrefix(aname); 2643 if (map[aname]) continue; 2644 fields.push(field); 2645 } 2646 return this.getFields(fields, sortByNameFunc); 2647 }; 2648 2649 /** 2650 * Gets the fields. 2651 * 2652 * @param {Array} field the fields 2653 * @param {function} [sortByNameFunc] sort by function 2654 * @return {Array} an array of field name/value pairs 2655 */ 2656 ZmContact.prototype.getFields = 2657 function(fields, sortByNameFunc) { 2658 // TODO: [Q] Should sort function handle just the field names or the attribute names? 2659 var selection; 2660 var attrs = this.getAttrs(); 2661 for (var index = 0; index < fields.length; index++) { 2662 for (var i = 1; true; i++) { 2663 var aname = ZmContact.getAttributeName(fields[index], i); 2664 if (!attrs[aname]) break; 2665 if (!selection) selection = {}; 2666 selection[aname] = attrs[aname]; 2667 } 2668 } 2669 if (sortByNameFunc && selection) { 2670 var keys = AjxUtil.keys(selection); 2671 keys.sort(sortByNameFunc); 2672 var nfields = {}; 2673 for (var i = 0; i < keys; i++) { 2674 var key = keys[i]; 2675 nfields[key] = fields[key]; 2676 } 2677 selection = nfields; 2678 } 2679 return selection; 2680 }; 2681 2682 /** 2683 * Returns a list of distribution list members for this contact. Only the 2684 * requested range is returned. 2685 * 2686 * @param offset {int} offset into list to start at 2687 * @param limit {int} number of members to fetch and return 2688 * @param callback {AjxCallback} callback to run with results 2689 */ 2690 ZmContact.prototype.getDLMembers = 2691 function(offset, limit, callback) { 2692 2693 var result = {list:[], more:false, isDL:{}}; 2694 if (!this.isDL) { return result; } 2695 2696 var email = this.getEmail(); 2697 var app = appCtxt.getApp(ZmApp.CONTACTS); 2698 var dl = app.getDL(email); 2699 if (!dl) { 2700 dl = result; 2701 dl.more = true; 2702 app.cacheDL(email, dl); 2703 } 2704 2705 limit = limit || ZmContact.DL_PAGE_SIZE; 2706 var start = offset || 0; 2707 var end = (offset + limit) - 1; 2708 2709 // see if we already have the requested members, or know that we don't 2710 if (dl.list.length >= end + 1 || !dl.more) { 2711 var list = dl.list.slice(offset, end + 1); 2712 result = {list:list, more:dl.more || (dl.list.length > end + 1), isDL:dl.isDL}; 2713 DBG.println("dl", "found cached DL members"); 2714 this._handleResponseGetDLMembers(start, limit, callback, result); 2715 return; 2716 } 2717 2718 DBG.println("dl", "server call " + offset + " / " + limit); 2719 if (!dl.total || (offset < dl.total)) { 2720 var jsonObj = {GetDistributionListMembersRequest:{_jsns:"urn:zimbraAccount", offset:offset, limit:limit}}; 2721 var request = jsonObj.GetDistributionListMembersRequest; 2722 request.dl = {_content: this.getEmail()}; 2723 var respCallback = new AjxCallback(this, this._handleResponseGetDLMembers, [offset, limit, callback]); 2724 appCtxt.getAppController().sendRequest({jsonObj:jsonObj, asyncMode:true, callback:respCallback}); 2725 } else { 2726 this._handleResponseGetDLMembers(start, limit, callback, result); 2727 } 2728 }; 2729 2730 ZmContact.prototype._handleResponseGetDLMembers = 2731 function(offset, limit, callback, result, resp) { 2732 2733 if (resp || !result.list) { 2734 var list = []; 2735 resp = resp || result.getResponse(); //if response is passed, take it. Otherwise get it from result 2736 resp = resp.GetDistributionListMembersResponse; 2737 var dl = appCtxt.getApp(ZmApp.CONTACTS).getDL(this.getEmail()); 2738 var more = dl.more = resp.more; 2739 var isDL = {}; 2740 var members = resp.dlm; 2741 if (members && members.length) { 2742 for (var i = 0, len = members.length; i < len; i++) { 2743 var member = members[i]._content; 2744 list.push(member); 2745 dl.list[offset + i] = member; 2746 if (members[i].isDL) { 2747 isDL[member] = dl.isDL[member] = true; 2748 } 2749 } 2750 } 2751 dl.total = resp.total; 2752 DBG.println("dl", list.join("<br>")); 2753 var result = {list:list, more:more, isDL:isDL}; 2754 } 2755 DBG.println("dl", "returning list of " + result.list.length + ", more is " + result.more); 2756 if (callback) { 2757 callback.run(result); 2758 } 2759 else { //synchronized case - see ZmContact.prototype.getDLMembers above 2760 return result; 2761 } 2762 }; 2763 2764 /** 2765 * Returns a list of all the distribution list members for this contact. 2766 * 2767 * @param callback {AjxCallback} callback to run with results 2768 */ 2769 ZmContact.prototype.getAllDLMembers = 2770 function(callback) { 2771 2772 var result = {list:[], more:false, isDL:{}}; 2773 if (!this.isDL) { return result; } 2774 2775 var dl = appCtxt.getApp(ZmApp.CONTACTS).getDL(this.getEmail()); 2776 if (dl && !dl.more) { 2777 result = {list:dl.list.slice(), more:false, isDL:dl.isDL}; 2778 callback.run(result); 2779 return; 2780 } 2781 2782 var nextCallback = new AjxCallback(this, this._getNextDLChunk, [callback]); 2783 this.getDLMembers(dl ? dl.list.length : 0, null, nextCallback); 2784 }; 2785 2786 ZmContact.prototype._getNextDLChunk = 2787 function(callback, result) { 2788 2789 var dl = appCtxt.getApp(ZmApp.CONTACTS).getDL(this.getEmail()); 2790 if (result.more) { 2791 var nextCallback = new AjxCallback(this, this._getNextDLChunk, [callback]); 2792 this.getDLMembers(dl.list.length, null, nextCallback); 2793 } else { 2794 result.list = dl.list.slice(); 2795 callback.run(result); 2796 } 2797 }; 2798 2799 /** 2800 * Gets the contact from cache handling parsing of contactId 2801 * 2802 * @param contactId {String} contact id 2803 * @return contact {ZmContact} contact or null 2804 * @private 2805 */ 2806 ZmContact.getContactFromCache = 2807 function(contactId) { 2808 var userZid = appCtxt.accountList.mainAccount.id; 2809 var contact = null; 2810 if (contactId && contactId.indexOf(userZid + ":") !=-1) { 2811 //strip off the usersZid to pull from cache 2812 var arr = contactId.split(userZid + ":"); 2813 contact = arr && arr.length > 1 ? appCtxt.cacheGet(arr[1]) : appCtxt.cacheGet(contactId); 2814 } 2815 else { 2816 contact = appCtxt.cacheGet(contactId); 2817 } 2818 if (contact instanceof ZmContact) { 2819 return contact; 2820 } 2821 return null; 2822 }; 2823 2824 // For mapAttrs(), prepare a hash where each key is the base name of an attr (without an ending number and lowercased), 2825 // and the value is a numerically sorted list of attr names in their original form. 2826 ZmContact.ATTR_VARIANTS = {}; 2827 ZmContact.IGNORE_ATTR_VARIANT = {}; 2828 ZmContact.IGNORE_ATTR_VARIANT[ZmContact.F_groups] = true; 2829 2830 ZmContact.initAttrVariants = function(attrClass) { 2831 var keys = Object.keys(attrClass), 2832 len = keys.length, key, i, attr, 2833 attrs = []; 2834 2835 // first, grab all the attr names 2836 var ignoreVariant = attrClass.IGNORE_ATTR_VARIANT || {}; 2837 for (i = 0; i < len; i++) { 2838 key = keys[i]; 2839 if (key.indexOf('F_') === 0) { 2840 attr = attrClass[key]; 2841 if (!ignoreVariant[attr]) { 2842 attrs.push(attr); 2843 } 2844 } 2845 } 2846 2847 // sort numerically, eg so that we get ['email', 'email2', 'email10'] in right order 2848 var numRegex = /^([a-zA-Z]+)(\d+)$/; 2849 attrs.sort(function(a, b) { 2850 var aMatch = a.match(numRegex), 2851 bMatch = b.match(numRegex); 2852 // check if both are numbered attrs with same base 2853 if (aMatch && bMatch && aMatch[1] === bMatch[1]) { 2854 return aMatch[2] - bMatch[2]; 2855 } 2856 else { 2857 return a > b ? 1 : (a < b ? -1 : 0); 2858 } 2859 }); 2860 2861 // construct hash mapping generic base name to its iterated attr names 2862 var attr, base; 2863 for (i = 0; i < attrs.length; i++) { 2864 attr = attrs[i]; 2865 base = attr.replace(/\d+$/, '').toLowerCase(); 2866 if (!ZmContact.ATTR_VARIANTS[base]) { 2867 ZmContact.ATTR_VARIANTS[base] = []; 2868 } 2869 ZmContact.ATTR_VARIANTS[base].push(attr); 2870 } 2871 }; 2872 ZmContact.initAttrVariants(ZmContact); 2873 2874 /** 2875 * Takes a hash of attrs and values and maps it to our attr names as best as it can. Scalar attrs will map if they 2876 * have the same name or only differ by case. A multivalued attr will map to a set of our attributes that share the 2877 * same case-insensitive base name. Some examples: 2878 * 2879 * FIRSTNAME: "Mildred" => firstName: "Mildred" 2880 * email: ['a', 'b'] => email: 'a', 2881 * email2: 'b' 2882 * WorkEmail: ['y', 'z'] => workEmail1: 'y', 2883 * workEmail2: 'z' 2884 * IMaddress: ['f', 'g'] => imAddress1: 'f', 2885 * imAddress2: 'g' 2886 * 2887 * @param {Object} attrs hash of attr names/values 2888 * 2889 * @returns {Object} hash of attr names/values using known attr names ZmContact.F_* 2890 */ 2891 ZmContact.mapAttrs = function(attrs) { 2892 2893 var attr, value, baseAttrs, newAttrs = {}; 2894 for (attr in attrs) { 2895 value = attrs[attr]; 2896 if (value) { 2897 baseAttrs = ZmContact.ATTR_VARIANTS[attr.toLowerCase()]; 2898 if (baseAttrs) { 2899 value = AjxUtil.toArray(value); 2900 var len = Math.min(value.length, baseAttrs.length), i; 2901 for (i = 0; i < len; i++) { 2902 newAttrs[baseAttrs[i]] = value[i]; 2903 } 2904 } else { 2905 // Any overlooked/ignored attributes are simply passed along 2906 newAttrs[attr] = value; 2907 } 2908 } 2909 } 2910 return newAttrs; 2911 }; 2912 2913 // these need to be kept in sync with ZmContact.F_* 2914 ZmContact._AB_FIELD = { 2915 firstName: ZmMsg.AB_FIELD_firstName, // file as info 2916 lastName: ZmMsg.AB_FIELD_lastName, 2917 middleName: ZmMsg.AB_FIELD_middleName, 2918 fullName: ZmMsg.AB_FIELD_fullName, 2919 jobTitle: ZmMsg.AB_FIELD_jobTitle, 2920 company: ZmMsg.AB_FIELD_company, 2921 department: ZmMsg.AB_FIELD_department, 2922 email: ZmMsg.AB_FIELD_email, // email addresses 2923 email2: ZmMsg.AB_FIELD_email2, 2924 email3: ZmMsg.AB_FIELD_email3, 2925 imAddress1: ZmMsg.AB_FIELD_imAddress1, // IM addresses 2926 imAddress2: ZmMsg.AB_FIELD_imAddress2, 2927 imAddress3: ZmMsg.AB_FIELD_imAddress3, 2928 image: ZmMsg.AB_FIELD_image, // contact photo 2929 attachment: ZmMsg.AB_FIELD_attachment, 2930 workStreet: ZmMsg.AB_FIELD_street, // work address info 2931 workCity: ZmMsg.AB_FIELD_city, 2932 workState: ZmMsg.AB_FIELD_state, 2933 workPostalCode: ZmMsg.AB_FIELD_postalCode, 2934 workCountry: ZmMsg.AB_FIELD_country, 2935 workURL: ZmMsg.AB_FIELD_URL, 2936 workPhone: ZmMsg.AB_FIELD_workPhone, 2937 workPhone2: ZmMsg.AB_FIELD_workPhone2, 2938 workFax: ZmMsg.AB_FIELD_workFax, 2939 assistantPhone: ZmMsg.AB_FIELD_assistantPhone, 2940 companyPhone: ZmMsg.AB_FIELD_companyPhone, 2941 callbackPhone: ZmMsg.AB_FIELD_callbackPhone, 2942 homeStreet: ZmMsg.AB_FIELD_street, // home address info 2943 homeCity: ZmMsg.AB_FIELD_city, 2944 homeState: ZmMsg.AB_FIELD_state, 2945 homePostalCode: ZmMsg.AB_FIELD_postalCode, 2946 homeCountry: ZmMsg.AB_FIELD_country, 2947 homeURL: ZmMsg.AB_FIELD_URL, 2948 homePhone: ZmMsg.AB_FIELD_homePhone, 2949 homePhone2: ZmMsg.AB_FIELD_homePhone2, 2950 homeFax: ZmMsg.AB_FIELD_homeFax, 2951 mobilePhone: ZmMsg.AB_FIELD_mobilePhone, 2952 pager: ZmMsg.AB_FIELD_pager, 2953 carPhone: ZmMsg.AB_FIELD_carPhone, 2954 otherStreet: ZmMsg.AB_FIELD_street, // other info 2955 otherCity: ZmMsg.AB_FIELD_city, 2956 otherState: ZmMsg.AB_FIELD_state, 2957 otherPostalCode: ZmMsg.AB_FIELD_postalCode, 2958 otherCountry: ZmMsg.AB_FIELD_country, 2959 otherURL: ZmMsg.AB_FIELD_URL, 2960 otherPhone: ZmMsg.AB_FIELD_otherPhone, 2961 otherFax: ZmMsg.AB_FIELD_otherFax, 2962 notes: ZmMsg.notes, // misc fields 2963 birthday: ZmMsg.AB_FIELD_birthday 2964 }; 2965 2966 ZmContact._AB_FILE_AS = { 2967 1: ZmMsg.AB_FILE_AS_lastFirst, 2968 2: ZmMsg.AB_FILE_AS_firstLast, 2969 3: ZmMsg.AB_FILE_AS_company, 2970 4: ZmMsg.AB_FILE_AS_lastFirstCompany, 2971 5: ZmMsg.AB_FILE_AS_firstLastCompany, 2972 6: ZmMsg.AB_FILE_AS_companyLastFirst, 2973 7: ZmMsg.AB_FILE_AS_companyFirstLast 2974 }; 2975 2976 } // if (!window.ZmContact) 2977