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 list class.
 27  * 
 28  */
 29 
 30 /**
 31  * Create a new, empty contact list.
 32  * @class
 33  * This class represents a list of contacts. In general, the list is the result of a
 34  * search. It may be the result of a <code><GetContactsRequest></code>, which returns all of the user's
 35  * local contacts. That list is considered to be canonical.
 36  * <p>
 37  * Loading of all local contacts has been optimized by delaying the creation of {@link ZmContact} objects until
 38  * they are needed. That has a big impact on IE, and not much on Firefox. Loading a subset
 39  * of attributes did not have much impact on load time, probably because a large majority
 40  * of contacts contain only those minimal fields.</p>
 41  *
 42  * @author Conrad Damon
 43  *
 44  * @param {ZmSearch}	search	the search that generated this list
 45  * @param {Boolean}	isGal		if <code>true</code>, this is a list of GAL contacts
 46  * @param {constant}	type		the item type
 47  * 
 48  * @extends		ZmList
 49  */
 50 ZmContactList = function(search, isGal, type) {
 51 
 52 	if (arguments.length == 0) { return; }
 53 	type = type || ZmItem.CONTACT;
 54 	ZmList.call(this, type, search);
 55 
 56 	this.isGal = (isGal === true);
 57 	this.isCanonical = false;
 58 	this.isLoaded = false;
 59 
 60 	this._app = appCtxt.getApp(ZmApp.CONTACTS);
 61 	if (!this._app) { 
 62 		this._emailToContact = this._phoneToContact = {};
 63 		return;
 64 	}
 65 	this._emailToContact = this._app._byEmail;
 66 	this._phoneToContact = this._app._byPhone;
 67 
 68 	this._alwaysUpdateHashes = true; // Should we update the phone & IM fast-lookup hashes even when account features don't require it? (bug #60411)
 69 };
 70 
 71 ZmContactList.prototype = new ZmList;
 72 ZmContactList.prototype.constructor = ZmContactList;
 73 
 74 ZmContactList.prototype.isZmContactList = true;
 75 ZmContactList.prototype.toString = function() { return "ZmContactList"; };
 76 
 77 
 78 
 79 
 80 // Constants
 81 
 82 // Support for loading user's local contacts from a large string
 83 
 84 ZmContactList.URL = "/Contacts";	// REST URL for loading user's local contacts
 85 ZmContactList.URL_ARGS = { fmt: 'cf', t: 2, all: 'all' }; // arguments for the URL above
 86 ZmContactList.CONTACT_SPLIT_CHAR	= '\u001E';	// char for splitting string into contacts
 87 ZmContactList.FIELD_SPLIT_CHAR		= '\u001D';	// char for splitting contact into fields
 88 // fields that belong to a contact rather than its attrs
 89 ZmContactList.IS_CONTACT_FIELD = {"id":true, "l":true, "d":true, "fileAsStr":true, "rev":true};
 90 
 91 
 92 
 93 /**
 94  * @private
 95  */
 96 ZmContactList.prototype.addLoadedCallback =
 97 function(callback) {
 98 	if (this.isLoaded) {
 99 		callback.run();
100 		return;
101 	}
102 	if (!this._loadedCallbacks) {
103 		this._loadedCallbacks = [];
104 	}
105 	this._loadedCallbacks.push(callback);
106 };
107 
108 /**
109  * @private
110  */
111 ZmContactList.prototype._finishLoading =
112 function() {
113 	DBG.timePt("done loading " + this.size() + " contacts");
114 	this.isLoaded = true;
115 	if (this._loadedCallbacks) {
116 		var callback;
117 		while (callback = this._loadedCallbacks.shift()) {
118 			callback.run();
119 		}
120 	}
121 };
122 
123 /**
124  * Retrieves the contacts from the back end, and parses the response. The list is then sorted.
125  * This method is used only by the canonical list of contacts, in order to load their content.
126  * <p>
127  * Loading a minimal set of attributes did not result in a significant performance gain.
128  * </p>
129  * 
130  * @private
131  */
132 ZmContactList.prototype.load =
133 function(callback, errorCallback, accountName) {
134 	// only the canonical list gets loaded
135 	this.isCanonical = true;
136 	var respCallback = new AjxCallback(this, this._handleResponseLoad, [callback]);
137 	DBG.timePt("requesting contact list", true);
138     if(appCtxt.isExternalAccount()) {
139         //Do not make a call in case of external user
140         //The rest url constructed wont exist in case of external user
141         if (callback) {
142 		    callback.run();
143 	    }
144         return;
145     }
146 	var args = ZmContactList.URL_ARGS;
147 
148 	// bug 74609: suppress overzealous caching by IE
149 	if (AjxEnv.isIE) {
150 		args = AjxUtil.hashCopy(args);
151 		args.sid = ZmCsfeCommand.getSessionId();
152 	}
153 
154 	var params = {asyncMode:true, noBusyOverlay:true, callback:respCallback, errorCallback:errorCallback, offlineCallback:callback};
155 	params.restUri = AjxUtil.formatUrl({
156 		path:["/home/", (accountName || appCtxt.getUsername()),
157 	          ZmContactList.URL].join(""),
158 	    qsArgs: args, qsReset:true
159 	});
160 	DBG.println(AjxDebug.DBG1, "loading contacts from " + params.restUri);
161 	appCtxt.getAppController().sendRequest(params);
162 
163 	ZmContactList.addDlFolder();
164 	
165 };
166 
167 /**
168  * @private
169  */
170 ZmContactList.prototype._handleResponseLoad =
171 function(callback, result) {
172 	DBG.timePt("got contact list");
173 	var text = result.getResponse();
174     if (text && typeof text !== 'string'){
175         text = text._data;
176     }
177 	var derefList = [];
178 	if (text) {
179 		var contacts = text.split(ZmContactList.CONTACT_SPLIT_CHAR);
180 		var derefBatchCmd = new ZmBatchCommand(true, null, true);
181 		for (var i = 0, len = contacts.length; i < len; i++) {
182 			var fields = contacts[i].split(ZmContactList.FIELD_SPLIT_CHAR);
183 			var contact = {}, attrs = {};
184 			var groupMembers = [];
185 			var foundDeref = false;
186 			for (var j = 0, len1 = fields.length; j < len1; j += 2) {
187 				if (ZmContactList.IS_CONTACT_FIELD[fields[j]]) {
188 					contact[fields[j]] = fields[j + 1];
189 				} else {
190 					var value = fields[j+1];
191 					switch (fields[j]) {
192 						case ZmContact.F_memberC:
193 							groupMembers.push({type: ZmContact.GROUP_CONTACT_REF, value: value});
194 							foundDeref = true; //load shared contacts
195 							break;
196 						case ZmContact.F_memberG:
197 							groupMembers.push({type: ZmContact.GROUP_GAL_REF, value: value});
198 							foundDeref = true;
199 							break;
200 						case ZmContact.F_memberI:
201 							groupMembers.push({type: ZmContact.GROUP_INLINE_REF, value: value});
202 							foundDeref = true;
203 							break;
204 						default:
205 							attrs[fields[j]] = value;
206 					}
207 				}
208 			}
209 			if (attrs[ZmContact.F_type] === "group") { //set only for group.
210 				attrs[ZmContact.F_groups] = groupMembers;
211 			}
212 			if (foundDeref) {
213 				//batch group members for deref loading
214 				var dummy = new ZmContact(contact["id"], this);
215 				derefBatchCmd.add(new AjxCallback(dummy, dummy.load, [null, null, derefBatchCmd, true]));
216 			}
217 			contact._attrs = attrs;
218 			this._addContact(contact);
219 		}
220 		derefBatchCmd.run();
221 	}
222 
223 	this._finishLoading();
224 
225 	if (callback) {
226 		callback.run();
227 	}
228 };
229 
230 /**
231  * @static
232  */
233 ZmContactList.addDlFolder =
234 function() {
235 
236 	if (!appCtxt.get(ZmSetting.DLS_FOLDER_ENABLED)) {
237 		return;
238 	}
239 
240 	var dlsFolder = appCtxt.getById(ZmOrganizer.ID_DLS);
241 
242 	var root = appCtxt.getById(ZmOrganizer.ID_ROOT);
243 	if (!root) { return; }
244 
245 	if (dlsFolder && root.getById(ZmOrganizer.ID_DLS)) {
246 		//somehow (after a refresh block, can be reprod using $set:refresh. ZmClientCmdHandler.prototype.execute_refresh) the DLs folder object is removed from under the root (but still cached in appCtxt). So making sure it's there.
247 		return;
248 	}
249 
250 	if (!dlsFolder) {
251 		var params = {
252 			id: ZmOrganizer.ID_DLS,
253 			name: ZmMsg.distributionLists,
254 			parent: root,
255 			tree: root.tree,
256 			type: ZmOrganizer.ADDRBOOK,
257 			numTotal: null, //we don't know how many
258 			noTooltip: true //so don't show tooltip
259 		};
260 
261 		dlsFolder = new ZmAddrBook(params);
262 		root.children.add(dlsFolder);
263 		dlsFolder._isDL = true;
264 	}
265 	else {
266 		//the dls folder object exists but no longer as a child of the root.
267 		dlsFolder.parent = root;
268 		root.children.add(dlsFolder); //any better way to do this?
269 	}
270 
271 };
272 
273 ZmContactList.prototype.add = 
274 function(item, index) {
275 	if (!item.id || !this._idHash[item.id]) {
276 		this._vector.add(item, index);
277 		if (item.id) {
278 			this._idHash[item.id] = item;
279 		}
280 		this._updateHashes(item, true);
281 	}
282 };
283 
284 ZmContactList.prototype.cache = 
285 function(offset, newList) {
286 	var getId = function(){
287 		return this.id;
288 	}
289 	var exists = function(obj) {
290 		return this._vector.containsLike(obj, getId);
291 	}
292 	var unique = newList.sub(exists, this);
293 
294 	this.getVector().merge(offset, unique);
295 	// reparent each item within new list, and add it to ID hash
296 	var list = unique.getArray();
297 	for (var i = 0; i < list.length; i++) {
298 		var item = list[i];
299 		item.list = this;
300 		if (item.id) {
301 			this._idHash[item.id] = item;
302 		}
303 	}
304 };
305 
306 /**
307  * @private
308  */
309 ZmContactList.prototype._addContact =
310 function(contact) {
311 
312 	// note that we don't create a ZmContact here (optimization)
313 	contact.list = this;
314 	this._updateHashes(contact, true);
315 	var fn = [], fl = [];
316 	if (contact._attrs[ZmContact.F_firstName])	{ fn.push(contact._attrs[ZmContact.F_firstName]); }
317 	if (contact._attrs[ZmContact.F_middleName])	{ fn.push(contact._attrs[ZmContact.F_middleName]); }
318 	if (contact._attrs[ZmContact.F_lastName])	{ fn.push(contact._attrs[ZmContact.F_lastName]); }
319 	if (fn.length) {
320 		contact._attrs[ZmContact.X_fullName] = fn.join(" ");
321 	}
322 	if (contact._attrs[ZmContact.F_firstName])	{ fl.push(contact._attrs[ZmContact.F_firstName]); }
323 	if (contact._attrs[ZmContact.F_lastName])	{ fl.push(contact._attrs[ZmContact.F_lastName]); }
324 	contact._attrs[ZmContact.X_firstLast] = fl.join(" ");
325 
326 	this.add(contact);
327 };
328 
329 /**
330  * Converts an anonymous contact object (contained by the JS returned by load request)
331  * into a ZmContact, and updates the containing list if it is the canonical one.
332  *
333  * @param {Object}	contact		a contact
334  * @param {int}	idx		the index of contact in canonical list
335  * 
336  * @private
337  */
338 ZmContactList.prototype._realizeContact =
339 function(contact, idx) {
340 
341 	if (contact instanceof ZmContact) { return contact; }
342 	if (contact && contact.type == ZmItem.CONTACT) { return contact; }	// instanceof often fails in new window
343 
344 	var args = {list:this};
345 	var obj = eval(ZmList.ITEM_CLASS[this.type]);
346 	var realContact = obj && obj.createFromDom(contact, args);
347 
348 	if (this.isCanonical) {
349 		var a = this.getArray();
350 		idx = idx || this.getIndexById(contact.id);
351 		a[idx] = realContact;
352 		this._updateHashes(realContact, true);
353 		this._idHash[contact.id] = realContact;
354 	}
355 
356 	return realContact;
357 };
358 
359 /**
360  * Finds the array index for the contact with the given ID.
361  *
362  * @param {int}	id		the contact ID
363  * @return	{int}	the index
364  * @private
365  */
366 ZmContactList.prototype.getIndexById =
367 function(id) {
368 	var a = this.getArray();
369 	for (var i = 0; i < a.length; i++) {
370 		if (a[i].id == id) {
371 			return i;
372 		}
373 	}
374 	return null;
375 };
376 
377 /**
378  * Override in order to make sure the contacts have been realized. We don't
379  * call realizeContact() since this is not the canonical list.
380  *
381  * @param {int}	offset		the starting index
382  * @param {int}	limit		the size of sublist
383  * @return	{AjxVector}	a vector of {@link ZmContact} objects
384  */
385 ZmContactList.prototype.getSubList =
386 function(offset, limit, folderId) {
387 	if (folderId && this.isCanonical) {
388 		// only collect those contacts that belong to the given folderId if provided
389 		var newlist = [];
390 		var sublist = this.getArray();
391 		var offsetCount = 0;
392 		this.setHasMore(false);
393 
394 		for (var i = 0; i < sublist.length; i++) {
395 			sublist[i] = this._realizeContact(sublist[i], i);
396 			var folder = appCtxt.getById(sublist[i].folderId);
397 			if (folder && folder.nId == ZmOrganizer.normalizeId(folderId)) {
398 				if (offsetCount >= offset) {
399 					if (newlist.length == limit) {
400 						this.setHasMore(true);
401 						break;
402 					}
403 					newlist.push(sublist[i]);
404 				}
405 				offsetCount++;
406 			}
407 		}
408 
409 		return AjxVector.fromArray(newlist);
410 	} else {
411 		var vec = ZmList.prototype.getSubList.call(this, offset, limit);
412 		if (vec) {
413 			var a = vec.getArray();
414 			for (var i = 0; i < a.length; i++) {
415 				a[i] = this._realizeContact(a[i], offset + i);
416 			}
417 		}
418 
419 		return vec;
420 	}
421 };
422 
423 /**
424  * Override in order to make sure the contact has been realized. Canonical list only.
425  *
426  * @param {int}	id		the contact ID
427  * @return	{ZmContact}	the contact or <code>null</code> if not found
428  */
429 ZmContactList.prototype.getById =
430 function(id) {
431 	if (!id || !this.isCanonical) return null;
432 
433 	var contact = this._idHash[id];
434 	return contact ? this._realizeContact(contact) : null;
435 };
436 
437 /**
438  * Gets the contact with the given address, if any (canonical list only).
439  *
440  * @param {String}	address	an email address
441  * @return	{ZmContact}	the contact or <code>null</code> if not found
442  */
443 ZmContactList.prototype.getContactByEmail =
444 function(address) {
445 	if (!address || !this.isCanonical) return null;
446 
447 	var contact = this._emailToContact[address.toLowerCase()];
448 	if (contact) {
449 		contact = this._realizeContact(contact);
450 		contact._lookupEmail = address;	// so caller knows which address matched
451 		return contact;
452 	} else {
453 		return null;
454 	}
455 };
456 
457 /**
458  * Gets information about the contact with the given phone number, if any (canonical list only).
459  *
460  * @param {String}	phone	the phone number
461  * @return	{Hash}	an object with <code>contact</code> = the contact & <code>field</code> = the field with the matching phone number
462  */
463 ZmContactList.prototype.getContactByPhone =
464 function(phone) {
465 	if (!phone || !this.isCanonical) return null;
466 
467 	var digits = this._getPhoneDigits(phone);
468 	var data = this._phoneToContact[digits];
469 	if (data) {
470 		data.contact = this._realizeContact(data.contact);
471 		return data;
472 	} else {
473 		return null;
474 	}
475 };
476 
477 /**
478  * Moves a list of items to the given folder.
479  * <p>
480  * This method calls the base class for normal "moves" UNLESS we're dealing w/
481  * shared items (or folder) in which case we must send a CREATE request for the
482  * given folder to the server followed by a hard delete of the shared contact.
483  * </p>
484  *
485  * @param {Hash}	params		a hash of parameters
486  * @param	{Array}       params.items			a list of items to move
487  * @param	{ZmFolder}	params.folder		the destination folder
488  * @param	{Hash}	       params.attrs		the additional attrs for SOAP command
489  * @param	{Boolean}	params.outOfTrash	if <code>true</code>, we are moving contacts out of trash
490  */
491 ZmContactList.prototype.moveItems =
492 function(params) {
493 
494 	params = Dwt.getParams(arguments, ["items", "folder", "attrs", "outOfTrash"]);
495 	params.items = AjxUtil.toArray(params.items);
496 
497 	var moveBatchCmd = new ZmBatchCommand(true, null, true);
498 	var loadBatchCmd = new ZmBatchCommand(true, null, true);
499 	var softMove = [];
500 
501 	// if the folder we're moving contacts to is a shared folder, then dont bother
502 	// checking whether each item is shared or not
503 	if (params.items[0] && params.items[0] instanceof ZmItem) {
504 		for (var i = 0; i < params.items.length; i++) {
505 			var contact = params.items[i];
506 
507 			if (contact.isReadOnly()) { continue; }
508 
509 			softMove.push(contact);
510 		}
511 	} else {
512 		softMove = params.items;
513 	}
514 
515 	// for "soft" moves, handle moving out of Trash differently
516 	if (softMove.length > 0) {
517 		var params1 = AjxUtil.hashCopy(params);
518 		params1.attrs = params.attrs || {};
519 		var toFolder = params.folder;
520 		params1.attrs.l = toFolder.isRemote() ? toFolder.getRemoteId() : toFolder.id;
521 		params1.action = "move";
522         params1.accountName = appCtxt.multiAccounts && appCtxt.accountList.mainAccount.name;
523         if (params1.folder.id == ZmFolder.ID_TRASH) {
524             params1.actionTextKey = 'actionTrash';
525             // bug: 47389 avoid moving to local account's Trash folder.
526             params1.accountName = appCtxt.multiAccounts && params.items[0].getAccount().name;
527         } else {
528             params1.actionTextKey = 'actionMove';
529             params1.actionArg = toFolder.getName(false, false, true);
530         }
531 		params1.callback = params.outOfTrash && new AjxCallback(this, this._handleResponseMoveItems, params);
532 
533 		this._itemAction(params1);
534 	}
535 };
536 
537 /**
538  * @private
539  */
540 ZmContactList.prototype._handleResponseMoveBatchCmd =
541 function(result) {
542 	var resp = result.getResponse().BatchResponse.ContactActionResponse;
543 	// XXX: b/c the server does not return notifications for actions done on
544 	//      shares, we manually notify - TEMP UNTIL WE GET BETTER SERVER SUPPORT
545 	var ids = resp[0].action.id.split(",");
546 	for (var i = 0; i < ids.length; i++) {
547 		var contact = appCtxt.cacheGet(ids[i]);
548 		if (contact && contact.isShared()) {
549 			contact.notifyDelete();
550 			appCtxt.cacheRemove(ids[i]);
551 		}
552 	}
553 };
554 
555 /**
556  * @private
557  */
558 ZmContactList.prototype._handleResponseLoadMove =
559 function(moveBatchCmd, params) {
560 	var deleteCmd = new AjxCallback(this, this._itemAction, [params]);
561 	moveBatchCmd.add(deleteCmd);
562 
563 	var respCallback = new AjxCallback(this, this._handleResponseMoveBatchCmd);
564 	moveBatchCmd.run(respCallback);
565 };
566 
567 /**
568  * @private
569  */
570 ZmContactList.prototype._handleResponseBatchLoad =
571 function(batchCmd, folder, result, contact) {
572 	batchCmd.add(this._getCopyCmd(contact, folder));
573 };
574 
575 /**
576  * @private
577  */
578 ZmContactList.prototype._getCopyCmd =
579 function(contact, folder) {
580 	var temp = new ZmContact(null, this);
581 	for (var j in contact.attr) {
582 		temp.attr[j] = contact.attr[j];
583 	}
584 	temp.attr[ZmContact.F_folderId] = folder.id;
585 
586 	return new AjxCallback(temp, temp.create, [temp.attr]);
587 };
588 
589 /**
590  * Deletes contacts after checking that this is not a GAL list.
591  *
592  * @param {Hash}	params		a hash of parameters
593  * @param	{Array}	       params.items			the list of items to delete
594  * @param	{Boolean}	params.hardDelete	if <code>true</code>, force physical removal of items
595  * @param	{Object}	params.attrs			the additional attrs for SOAP command
596  */
597 ZmContactList.prototype.deleteItems =
598 function(params) {
599 	if (this.isGal) {
600 		if (ZmContactList.deleteGalItemsAllowed(params.items)) {
601 			this._deleteDls(params.items);
602 			return;
603 		}
604 		DBG.println(AjxDebug.DBG1, "Cannot delete GAL contacts that are not DLs");
605 		return;
606 	}
607 	ZmList.prototype.deleteItems.call(this, params);
608 };
609 
610 ZmContactList.deleteGalItemsAllowed =
611 function(items) {
612 	var deleteDomainsAllowed = appCtxt.createDistListAllowedDomainsMap;
613 	if (items.length == 0) {
614 		return false; //need a special case since we don't want to enable the "delete" button for 0 items.
615 	}
616 	for (var i = 0; i < items.length; i++) {
617 		var contact = items[i];
618 		var email = contact.getEmail();
619 		var domain = email.split("@")[1];
620 		var isDL = contact && contact.isDistributionList();
621 		//see bug 71368 and also bug 79672 - the !contact.dlInfo is in case somehow dlInfo is missing - so unfortunately if that happens (can't repro) - let's not allow to delete since we do not know if it's an owner
622 		if (!isDL || !deleteDomainsAllowed[domain] || !contact.dlInfo || !contact.dlInfo.isOwner) {
623 			return false;
624 		}
625 	}
626 	return true;
627 };
628 
629 ZmContactList.prototype._deleteDls =
630 function(items, confirmDelete) {
631 
632 	if (!confirmDelete) {
633 		var callback = this._deleteDls.bind(this, items, true);
634 		this._popupDeleteWarningDialog(callback, false, items.length);
635 		return;
636 	}
637 
638 	var reqs = [];
639 	for (var i = 0; i < items.length; i++) {
640 		var contact = items[i];
641 		var email = contact.getEmail();
642 		reqs.push({
643 				_jsns: "urn:zimbraAccount",
644 				dl: {by: "name",
645 					 _content: contact.getEmail()
646 				},
647 				action: {
648 					op: "delete"
649 				}
650 			});
651 	}
652 	var jsonObj = {
653 		BatchRequest: {
654 			_jsns: "urn:zimbra",
655 			DistributionListActionRequest: reqs
656 		}
657 	};
658 	var respCallback = this._deleteDlsResponseHandler.bind(this, items);
659 	appCtxt.getAppController().sendRequest({jsonObj: jsonObj, asyncMode: true, callback: respCallback});
660 
661 };
662 
663 ZmContactList.prototype._deleteDlsResponseHandler =
664 function(items) {
665 	if (appCtxt.getCurrentView().isZmGroupView) {
666 		//this is the case we were editing the DL (different than viewing it in the DL list, in which case it's the contactListController).
667 		//so we now need to pop up the view.
668 		this.controller.popView();
669 	}
670 
671 	appCtxt.setStatusMsg(items.length == 1 ? ZmMsg.dlDeleted : ZmMsg.dlsDeleted);
672 
673 	for (var i = 0; i < items.length; i++) {
674 		var item = items[i];
675 		item.clearDlInfo();
676 		item._notify(ZmEvent.E_DELETE);
677 	}
678 };
679 
680 
681 
682 /**
683  * Sets the is GAL flag.
684  * 
685  * @param	{Boolean}	isGal		<code>true</code> if contact list is GAL
686  */
687 ZmContactList.prototype.setIsGal =
688 function(isGal) {
689 	this.isGal = isGal;
690 };
691 
692 ZmContactList.prototype.notifyCreate =
693 function(node) {
694 	var obj = eval(ZmList.ITEM_CLASS[this.type]);
695 	if (obj) {
696 		var item = obj.createFromDom(node, {list:this});
697 		var index = this._sortIndex(item);
698 		// only add if it sorts into this list
699 		var listSize = this.size();
700 		var visible = false;
701 		if (index < listSize || listSize == 0 || (index==listSize && !this._hasMore)) {
702 			this.add(item, index);
703 			this.createLocal(item);
704 			visible = true;
705 		}
706 		this._notify(ZmEvent.E_CREATE, {items: [item], sortIndex: index, visible: visible});
707 	}
708 };
709 
710 /**
711  * Moves the items.
712  * 
713  * @param	{Array}	items		an array of {@link ZmContact} objects
714  * @param	{String}	folderId	the folder id
715  */
716 ZmContactList.prototype.moveLocal =
717 function(items, folderId) {
718 	// don't remove any contacts from the canonical list
719 	if (!this.isCanonical)
720 		ZmList.prototype.moveLocal.call(this, items, folderId);
721 	if (folderId == ZmFolder.ID_TRASH) {
722 		for (var i = 0; i < items.length; i++) {
723 			this._updateHashes(items[i], false);
724 		}
725 	}
726 };
727 
728 /**
729  * Deletes the items.
730  * 
731  * @param	{Array}	items		an array of {@link ZmContact} objects
732  */
733 ZmContactList.prototype.deleteLocal =
734 function(items) {
735 	ZmList.prototype.deleteLocal.call(this, items);
736 	for (var i = 0; i < items.length; i++) {
737 		this._updateHashes(items[i], false);
738 	}
739 };
740 
741 /**
742  * Handle modified contact.
743  * 
744  * @private
745  */
746 ZmContactList.prototype.modifyLocal =
747 function(item, details) {
748 	if (details) {
749 		// notify item's list
750 		this._evt.items = details.items = [item];
751 		this._evt.item = details.contact; //somehow this was set to something obsolete. What a mess. Also note that item is Object while details.contact is ZmContact
752 		this._notify(ZmEvent.E_MODIFY, details);
753 	}
754 
755 	var contact = details.contact;
756 	if (this.isCanonical || contact.attr[ZmContact.F_email] != details.oldAttr[ZmContact.F_email]) {
757 		// Remove traces of old contact - NOTE: we pass in null for the ID on
758 		// PURPOSE to avoid overwriting the existing cached contact
759 		var oldContact = new ZmContact(null, this);
760 		oldContact.id = details.contact.id;
761 		oldContact.attr = details.oldAttr;
762 		this._updateHashes(oldContact, false);
763 
764 		// add new contact to hashes
765 		this._updateHashes(contact, true);
766 	}
767 
768 	// place in correct position in list
769 	if (details.fileAsChanged) {
770 		this.remove(contact);
771 		var index = this._sortIndex(contact);
772 		var listSize = this.size();
773 		if (index < listSize || listSize == 0 || (index == listSize && !this._hasMore)) {
774 			this.add(contact, index);
775 		}
776 	}
777 
778 	// reset addrbook property
779 	if (contact.addrbook && (contact.addrbook.id != contact.folderId)) {
780 		contact.addrbook = appCtxt.getById(contact.folderId);
781 	}
782 };
783 
784 /**
785  * Creates the item local.
786  * 
787  * @param	{ZmContact}	item		the item
788  */
789 ZmContactList.prototype.createLocal =
790 function(item) {
791 	this._updateHashes(item, true);
792 };
793 
794 /**
795  * @private
796  */
797 ZmContactList.prototype._updateHashes =
798 function(contact, doAdd) {
799 
800 	this._app.updateCache(contact, doAdd);
801 
802 	// Update email hash.
803 	for (var index = 0; index < ZmContact.EMAIL_FIELDS.length; index++) {
804 		var field = ZmContact.EMAIL_FIELDS[index];
805 		for (var i = 1; true; i++) {
806 			var aname = ZmContact.getAttributeName(field, i);
807 			var avalue = ZmContact.getAttr(contact, aname);
808 			if (!avalue) break;
809 			if (doAdd) {
810 				this._emailToContact[avalue.toLowerCase()] = contact;
811 			} else {
812 				delete this._emailToContact[avalue.toLowerCase()];
813 			}
814 		}
815 	}
816 
817 	// Update phone hash.
818 	if (appCtxt.get(ZmSetting.VOICE_ENABLED) || this._alwaysUpdateHashes) {
819 		for (var index = 0; index < ZmContact.PHONE_FIELDS.length; index++) {
820 			var field = ZmContact.PHONE_FIELDS[index];
821 			for (var i = 1; true; i++) {
822 				var aname = ZmContact.getAttributeName(field, i);
823 				var avalue = ZmContact.getAttr(contact, aname);
824 				if (!avalue) break;
825 				var digits = this._getPhoneDigits(avalue);
826 				if (digits) {
827 					if (doAdd) {
828 						this._phoneToContact[avalue] = {contact: contact, field: aname};
829 					} else {
830 						delete this._phoneToContact[avalue];
831 					}
832 				}
833 			}
834 		}
835 	}
836 };
837 
838 /**
839  * Strips all non-digit characters from a phone number.
840  * 
841  * @private
842  */
843 ZmContactList.prototype._getPhoneDigits =
844 function(phone) {
845 	return phone.replace(/[^\d]/g, '');
846 };
847 
848 /**
849  * Returns the position at which the given contact should be inserted in this list.
850  * 
851  * @private
852  */
853 ZmContactList.prototype._sortIndex =
854 function(contact) {
855 	var a = this._vector.getArray();
856 	for (var i = 0; i < a.length; i++) {
857 		if (ZmContact.compareByFileAs(a[i], contact) > 0) {
858 			return i;
859 		}
860 	}
861 	return a.length;
862 };
863 
864 /**
865  * Gets the list ID hash
866  * @return idHash {Ojbect} list ID hash
867  */
868 ZmContactList.prototype.getIdHash =
869 function() {
870 	return this._idHash;
871 }
872 
873 /**
874  * @private
875  */
876 ZmContactList.prototype._handleResponseModifyItem =
877 function(item, result) {
878 	// NOTE: we overload and do nothing b/c base class does more than we want
879 	//       (since everything is handled by notifications)
880 };
881