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