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 defines a list of items.
 27  */
 28 
 29 /**
 30  * Creates an empty list of items of the given type.
 31  * @class
 32  * This class represents a list of items ({@link ZmItem} objects). Any SOAP method that can be
 33  * applied to a list of item IDs is represented here, so that we can perform an action
 34  * on multiple items with just one CSFE call. For the sake of convenience, a hash 
 35  * matching item IDs to items is maintained. Items are assumed to have an 'id'
 36  * property.
 37  * <br/>
 38  * <br/>
 39  * The calls are made asynchronously. We are assuming that any action taken will result
 40  * in a notification, so the action methods generally do not have an async callback 
 41  * chain and thus are leaf nodes. An exception is moving conversations. We don't
 42  * know enough from the ensuing notifications (which only indicate that messages have
 43  * moved), we need to update the UI based on the response.
 44  *
 45  * @author Conrad Damon
 46  * 
 47  * @param {constant}	type		the item type
 48  * @param {ZmSearch}	search	the search that generated this list
 49  * 
 50  * @extends	ZmModel
 51  */
 52 ZmList = function(type, search) {
 53 
 54 	if (arguments.length == 0) return;
 55 	ZmModel.call(this, type);
 56 
 57 	this.type = type;
 58 	this.search = search;
 59 	
 60 	this._vector = new AjxVector();
 61 	this._hasMore = false;
 62 	this._idHash = {};
 63 
 64 	var tagList = appCtxt.getTagTree();
 65 	if (tagList) {
 66 		this._tagChangeListener = new AjxListener(this, this._tagTreeChangeListener);
 67 		tagList.addChangeListener(this._tagChangeListener);
 68 	}
 69 	
 70 	this.id = "LIST" + ZmList.NEXT++;
 71 	appCtxt.cacheSet(this.id, this);
 72 };
 73 
 74 ZmList.prototype = new ZmModel;
 75 ZmList.prototype.constructor = ZmList;
 76 
 77 ZmList.prototype.isZmList = true;
 78 ZmList.prototype.toString = function() { return "ZmList"; };
 79 
 80 
 81 ZmList.NEXT = 1;
 82 
 83 // for item creation
 84 ZmList.ITEM_CLASS = {};
 85 
 86 // node names for item types
 87 ZmList.NODE = {};
 88 
 89 // item types based on node name (reverse map of above)
 90 ZmList.ITEM_TYPE = {};
 91 
 92 ZmList.CHUNK_SIZE	= 100;	// how many items to act on at a time via a server request
 93 ZmList.CHUNK_PAUSE	= 500;	// how long to pause to allow UI to catch up
 94 
 95 
 96 /**
 97  * Gets the item.
 98  * 
 99  * @param	{int}	index		the index
100  * @return	{ZmItem}	the index
101  */
102 ZmList.prototype.get =
103 function(index) {
104 	return this._vector.get(index);
105 };
106 
107 /**
108  * Adds an item to the list.
109  *
110  * @param {ZmItem}	item	the item to add
111  * @param {int}	index	the index at which to add the item (defaults to end of list)
112  */
113 ZmList.prototype.add = 
114 function(item, index) {
115 	this._vector.add(item, index);
116 	if (item.id) {
117 		this._idHash[item.id] = item;
118 	}
119 };
120 
121 /**
122  * Removes an item from the list.
123  *
124  * @param {ZmItem}	item	the item to remove
125  */
126 ZmList.prototype.remove = 
127 function(item) {
128 	this._vector.remove(item);
129 	if (item.id) {
130 		delete this._idHash[item.id];
131 	}
132 };
133 
134 /**
135  * Creates an item from the given arguments. A subclass may override
136  * <code>sortIndex()</code> to add it to a particular point in the list. By default, it
137  * will be added at the end.
138  *
139  * <p>
140  * The item will invoke a SOAP call, which generates a create notification from the
141  * server. That will be handled by notifyCreate(), which will call _notify()
142  * so that views can be updated.
143  * </p>
144  *
145  * @param {Hash}	args	a hash of arugments to pass along to the item constructor
146  * @return	{ZmItem}	the newly created item
147  */
148 ZmList.prototype.create =
149 function(args) {
150 	var item;
151 	var obj = eval(ZmList.ITEM_CLASS[this.type]);
152 	if (obj) {
153 		item = new obj(this);
154 		item.create(args);
155 	}
156 
157 	return item;
158 };
159 
160 /**
161  * Returns the number of items in the list.
162  * 
163  * @return	{int}	the number of items
164  */
165 ZmList.prototype.size = 
166 function() {
167 	return this._vector.size();
168 };
169 
170 /**
171  * Returns the index of the given item in the list.
172  * 
173  * @param	{ZmItem}	item		the item
174  * @return	{int}	the index
175  */
176 ZmList.prototype.indexOf = 
177 function(item) {
178 	return this._vector.indexOf(item);
179 };
180 
181 /**
182  * Gets if there are more items for this search.
183  * 
184  * @return	{Boolean}	<code>true</code> if there are more items
185  */
186 ZmList.prototype.hasMore = 
187 function() {
188 	return this._hasMore;
189 };
190 
191 /**
192  * Sets the "more" flag for this list.
193  *
194  * @param {Boolean}	bHasMore	<code>true</code> if there are more items
195  */
196 ZmList.prototype.setHasMore = 
197 function(bHasMore) {
198 	this._hasMore = bHasMore;
199 };
200 
201 /**
202  * Returns the list as an array.
203  * 
204  * @return	{Array}	an array of {ZmItem} objects
205  */
206 ZmList.prototype.getArray =
207 function() {
208 	return this._vector.getArray();
209 };
210 
211 /**
212  * Returns the list as a vector.
213  * 
214  * @return	{AjxVector}	a vector of {ZmItem} objects
215  */
216 ZmList.prototype.getVector =
217 function() {
218 	return this._vector;
219 };
220 
221 /**
222  * Gets the item with the given id.
223  *
224  * @param {String}	id		an item id
225  * 
226  * @return	{ZmItem}	the item
227  */
228 ZmList.prototype.getById =
229 function(id) {
230 	return this._idHash[id];
231 };
232 
233 /**
234  * Clears the list, including the id hash.
235  * 
236  */
237 ZmList.prototype.clear =
238 function() {
239 	// First, let each item run its clear() method
240 	var a = this.getArray();
241 	for (var i = 0; i < a.length; i++) {
242 		a[i].clear();
243 	}
244 
245 	this._evtMgr.removeAll(ZmEvent.L_MODIFY);
246 	this._vector.removeAll();
247 	for (var id in this._idHash) {
248 		this._idHash[id] = null;
249 	}
250 	this._idHash = {};
251 };
252 
253 /**
254  * Populates the list with elements created from the response to a SOAP command. Each
255  * node in the response should represent an item of the list's type. Items are added
256  * in the order they are received; no sorting is done.
257  *
258  * @param {Object}	respNode	an XML node whose children are item nodes
259  */
260 ZmList.prototype.set = 
261 function(respNode) {
262 	this.clear();
263 	var nodes = respNode.childNodes;
264 	var args = {list:this};
265 	for (var i = 0; i < nodes.length; i++) {
266 		var node = nodes[i];
267 		if (node.nodeName == ZmList.NODE[this.type]) {
268 			/// TODO: take this out, let view decide whether to show items in Trash
269 			if (parseInt(node.getAttribute("l")) == ZmFolder.ID_TRASH && (this.type != ZmItem.CONTACT))	{ continue; }
270 			var obj = eval(ZmList.ITEM_CLASS[this.type]);
271 			if (obj) {
272 				this.add(obj.createFromDom(node, args));
273 			}
274 		}
275 	}
276 };
277 
278 /**
279  * Adds an item to the list from the given XML node.
280  *
281  * @param {Object}	node	an XML node
282  * @param {Hash}	args	an optional list of arguments to pass to the item contructor
283  */
284 ZmList.prototype.addFromDom = 
285 function(node, args) {
286 	args = args || {};
287 	args.list = this;
288 	var obj = eval(ZmList.ITEM_CLASS[this.type]);
289 	if (obj) {
290 		this.add(obj.createFromDom(node, args));
291 	}
292 };
293 
294 /**
295  * Gets a vector containing a subset of items of this list.
296  *
297  * @param {int}		offset		the starting index
298  * @param {int}		limit		the size of sublist
299  * @return	{AjxVector}	the vector
300  */
301 ZmList.prototype.getSubList = 
302 function(offset, limit) {
303 	var subVector = null;
304 	var end = (offset + limit > this.size()) ? this.size() : offset + limit;
305 	var subList = this.getArray();
306 	if (offset < end) {
307 		subVector = AjxVector.fromArray(subList.slice(offset, end));
308 	}
309 	return subVector;
310 };
311 
312 /**
313  * Caches the list.
314  * 
315  * @param	{int}	offset	the index
316  * @param	{AjxVector}	newList		the new list
317  */
318 ZmList.prototype.cache = 
319 function(offset, newList) {
320 	this.getVector().merge(offset, newList);
321 	// reparent each item within new list, and add it to ID hash
322 	var list = newList.getArray();
323 	for (var i = 0; i < list.length; i++) {
324 		var item = list[i];
325 		item.list = this;
326 		if (item.id) {
327 			this._idHash[item.id] = item;
328 		}
329 	}
330 };
331 
332 // Actions
333 
334 /**
335  * Sets and unsets a flag for each of a list of items.
336  *
337  * @param 	{Hash}				params					a hash of parameters
338  * @param	{Array}     		params.items			a list of items to set/unset a flag for
339  * @param	{String}			params.op				the name of the flag operation ("flag" or "read")
340  * @param	{Boolean|String}	params.value			whether to set the flag, or for "update" the flags string
341  * @param	{AjxCallback}		params.callback			the callback to run after each sub-request
342  * @param	{closure}			params.finalCallback	the callback to run after all items have been processed
343  * @param	{int}				params.count			the starting count for number of items processed
344  * @param   {String}    		params.actionTextKey   	pattern for generating action summarykey to action summary message
345  */
346 ZmList.prototype.flagItems =
347 function(params) {
348 
349 	params = Dwt.getParams(arguments, ["items", "op", "value", "callback"]);
350 
351 	params.items = AjxUtil.toArray(params.items);
352 
353 	if (params.op == "update") {
354 		params.action = params.op;
355 		params.attrs = {f:params.value};
356 	} else {
357 		params.action = params.value ? params.op : "!" + params.op;
358 	}
359 
360     if (appCtxt.multiAccounts) {
361 		// check if we're flagging item from remote folder, in which case, always send
362 		// request on-behalf-of the account the item originally belongs to.
363         var folderId = this.search.folderId;
364         var fromFolder = folderId && appCtxt.getById(folderId);
365         if (fromFolder && fromFolder.isRemote()) {
366                 params.accountName = params.items[0].getAccount().name;
367         }
368 	}
369 
370 	this._itemAction(params);
371 };
372 
373 /**
374  * Tags or untags a list of items. A sanity check is done first, so that items
375  * aren't tagged redundantly, and so we don't try to remove a nonexistent tag.
376  *
377  * @param {Hash}		params					a hash of parameters
378  * @param {Array}		params.items			a list of items to tag/untag
379  * @param {String}  	params.tagId            ID of tag to add/remove
380  * @param {String}		params.tag  			the tag to add/remove from each item (optional)
381  * @param {Boolean}		params.doTag			<code>true</code> if adding the tag, <code>false</code> if removing it
382  * @param {AjxCallback}	params.callback			the callback to run after each sub-request
383  * @param {closure}		params.finalCallback	the callback to run after all items have been processed
384  * @param {int}			params.count			the starting count for number of items processed
385  */
386 ZmList.prototype.tagItems =
387 function(params) {
388 
389 	params = Dwt.getParams(arguments, ["items", "tagId", "doTag"]);
390 
391     var tagName = params.tagName || (params.tag && params.tag.name);
392 
393 	//todo - i hope this is no longer needed. I think the item we apply the tag to should determine the tag id on the server side.
394 //	// for multi-account mbox, normalize tagId
395 //	if (appCtxt.multiAccounts && !appCtxt.getActiveAccount().isMain) {
396 //		tagId = ZmOrganizer.normalizeId(tagId);
397 //	}
398 
399 	// only tag items that don't have the tag, and untag ones that do
400 	// always tag a conv, because we don't know if all items in the conv have the tag yet
401 	var items = AjxUtil.toArray(params.items);
402 	var items1 = [], doTag = params.doTag;
403 	if (items[0] && items[0] instanceof ZmItem) {
404 		for (var i = 0; i < items.length; i++) {
405 			var item = items[i];
406 			if ((doTag && (!item.hasTag(tagName) || item.type == ZmItem.CONV)) ||	(!doTag && item.hasTag(tagName))) {
407 				items1.push(item);
408 			}
409 		}
410 	} else {
411 		items1 = items;
412 	}
413 	params.items = items1;
414 	params.attrs = {tn: tagName};
415 	params.action = doTag ? "tag" : "!tag";
416     params.actionTextKey = doTag ? 'actionTag' : 'actionUntag';
417 	params.actionArg = params.tag && params.tag.name;
418 
419 	this._itemAction(params);
420 };
421 
422 /**
423  * Removes all tags from a list of items.
424  *
425  * @param	{Hash}			params					a hash of parameters
426  * @param	{Array}			params.items			a list of items to tag/untag
427  * @param	{AjxCallback}	params.callback			the callback to run after each sub-request
428  * @param	{closure}		params.finalCallback	the callback to run after all items have been processed
429  * @param	{int}			params.count			the starting count for number of items processed
430  */
431 ZmList.prototype.removeAllTags = 
432 function(params) {
433 
434 	params = (params && params.items) ? params : {items:params};
435 
436 	var items = AjxUtil.toArray(params.items);
437 	var items1 = [];
438 	if (items[0] && items[0] instanceof ZmItem) {
439 		for (var i = 0; i < items.length; i++) {
440 			var item = items[i];
441 			if (item.tags && item.tags.length) {
442 				items1.push(item);
443 			}
444 		}
445 	} else {
446 		items1 = items;
447 	}
448 
449 	params.items = items1;
450 	params.action = "update";
451 	params.attrs = {t: ""};
452     params.actionTextKey = 'actionRemoveTags';
453 
454 	this._itemAction(params);
455 };
456 
457 /**
458  * Moves a list of items to the given folder.
459  * <p>
460  * Search results are treated as though they're in a temporary folder, so that they behave as
461  * they would if they were in any other folder such as Inbox. When items that are part of search
462  * results are moved, they will disappear from the view, even though they may still satisfy the
463  * search.
464  * </p>
465  *
466  * @param	{Hash}			params					a hash of parameters
467  * @param	{Array}			params.items			a list of items to move
468  * @param	{ZmFolder}		params.folder			the destination folder
469  * @param	{Hash}			params.attrs			the additional attrs for SOAP command
470  * @param	{AjxCallback}	params.callback			the callback to run after each sub-request
471  * @param	{closure}		params.finalCallback	the callback to run after all items have been processed
472  * @param	{int}			params.count			the starting count for number of items processed
473  * @param	{boolean}		params.noUndo			true if the action is not undoable (e.g. performed as an undo)
474  */
475 ZmList.prototype.moveItems =
476 function(params) {
477 	
478 	params = Dwt.getParams(arguments, ["items", "folder", "attrs", "callback", "errorCallback" ,"finalCallback", "noUndo"]);
479 
480 	var params1 = AjxUtil.hashCopy(params);
481 	params1.items = AjxUtil.toArray(params.items);
482 	params1.attrs = params.attrs || {};
483 	params1.childWin = params.childWin;
484 	params1.closeChildWin = params.closeChildWin;
485 	
486 	if (params1.folder.id == ZmFolder.ID_TRASH) {
487 		params1.actionTextKey = 'actionTrash';
488 		params1.action = "trash";
489 	} else {
490 		params1.actionTextKey = 'actionMove';
491 		params1.actionArg = params.folder.getName(false, false, true);
492 		params1.action = "move";
493 		params1.attrs.l = params.folder.id;
494 	}
495 	params1.callback = new AjxCallback(this, this._handleResponseMoveItems, [params]);
496 	if (params.noToast) {
497 		params1.actionTextKey = null;
498 	}
499 
500     if (appCtxt.multiAccounts) {
501 		// Reset accountName for multi-account to be the respective account if we're
502 		// moving a draft out of Trash.
503 		// OR,
504 		// check if we're moving to or from a shared folder, in which case, always send
505 		// request on-behalf-of the account the item originally belongs to.
506 
507         var folderId = params.items[0].getFolderId && params.items[0].getFolderId();
508 
509         // on bulk delete, when the second chunk loads try to get folderId from the item id.
510         if (!folderId) {
511             var itemId = params.items[0] && params.items[0].id;
512             folderId = itemId && appCtxt.getById(itemId) && appCtxt.getById(itemId).folderId;
513         }
514         var fromFolder = appCtxt.getById(folderId);
515 		if ((params.items[0].isDraft && params.folder.id == ZmFolder.ID_DRAFTS) ||
516 			(params.folder.isRemote()) || (fromFolder && fromFolder.isRemote()))
517 		{
518 			params1.accountName = params.items[0].getAccount().name;
519 		}
520 	}
521 	//Error Callback
522 	params1.errorCallback = params.errorCallback;
523 
524 	if (this._handleDeleteFromSharedFolder(params, params1)) {
525 		return;
526 	}
527     
528 	this._itemAction(params1);
529 };
530 
531 ZmList.prototype._handleDeleteFromSharedFolder =
532 function(params, params1) {
533 
534 	// Bug 26103: when deleting an item in a folder shared to us, save a copy in our own trash
535 	if (params.folder && params.folder.id == ZmFolder.ID_TRASH) {
536 		var fromFolder;
537 		var toCopy = [];
538 		for (var i = 0; i < params.items.length; i++) {
539 			var item = params.items[i];
540 			var index = item.id.indexOf(":");
541 			if (index != -1) { //might be shared
542 				var acctId = item.id.substring(0, index);
543 				if (!appCtxt.accountList.getAccount(acctId)) {
544 					fromFolder = appCtxt.getById(item.folderId);
545 					// Don't do the copy if the source folder is shared with view only rights
546 					if (fromFolder && !fromFolder.isReadOnly()) {
547 						toCopy.push(item);
548 					}
549 				}
550 			}
551 		}
552 		if (toCopy.length) {
553 			var params2 = {
554 				items:			toCopy,
555 				folder:			params.folder, // Should refer to our own trash folder
556 				finalCallback:	this._itemAction.bind(this, params1, null),
557 				actionTextKey:	null
558 			};
559 			this.copyItems(params2);
560 			return true;
561 		}
562 	}
563 };
564 
565 /**
566  * @private
567  */
568 ZmList.prototype._handleResponseMoveItems =
569 function(params, result) {
570 
571 	var movedItems = result.getResponse();
572 	if (movedItems && movedItems.length && (movedItems[0] instanceof ZmItem)) {
573 		this.moveLocal(movedItems, params.folder.id);
574 		for (var i = 0; i < movedItems.length; i++) {
575 			var item = movedItems[i];
576 			var details = {oldFolderId:item.folderId};
577 			item.moveLocal(params.folder.id);
578 			//ZmModel.prototype._notify.call(item, ZmEvent.E_MOVE, details);
579 		}
580 		// batched change notification
581 		//todo - it's probably possible that different items have different _lists they are in
582 		// thus getting the lists just from the first item is not enough. But hopefully good
583 		// enough for the most common cases. Prior to this fix it was only taking the current list
584 		// the first item is in, so this is already better. :)
585 		var item = movedItems[0];
586 		for (var listId in item._list) {
587 			var ac = window.parentAppCtxt || appCtxt; //always get the list in the parent window. The child might be closed or closing, causing bugs.
588 			var list = ac.getById(listId);
589 			if (!list) {
590 				continue;
591 			}
592             list._evt.batchMode = true;
593             list._evt.item = item;	// placeholder
594             list._evt.items = movedItems;
595             list._notify(ZmEvent.E_MOVE, details);
596         }
597 	}
598 
599 	if (params.callback) {
600 		params.callback.run(result);
601 	}
602 };
603 
604 /**
605  * Copies a list of items to the given folder.
606  *
607  * @param {Hash}		params					the hash of parameters
608  * @param {Array}		params.items			a list of items to move
609  * @param {ZmFolder}	params.folder			the destination folder
610  * @param {Hash}		params.attrs			the additional attrs for SOAP command
611  * @param {closure}		params.finalCallback	the callback to run after all items have been processed
612  * @param {int}			params.count			the starting count for number of items processed
613  * @param {String}		params.actionTextKey	key to optional text to display in the confirmation toast instead of the default summary. May be set explicitly to null to disable the confirmation toast
614  */
615 ZmList.prototype.copyItems =
616 function(params) {
617 
618 	params = Dwt.getParams(arguments, ["items", "folder", "attrs", "actionTextKey"]);
619 
620 	params.items = AjxUtil.toArray(params.items);
621 	params.attrs = params.attrs || {};
622     if (!appCtxt.isExternalAccount()) {
623         params.attrs.l = params.folder.id;
624         params.action = "copy";
625         params.actionTextKey = 'itemCopied';
626     }
627     else {
628         params.action = 'trash';
629     }
630 	params.actionArg = params.folder.getName(false, false, true);
631 	params.callback = new AjxCallback(this, this._handleResponseCopyItems, params);
632 
633 	if (appCtxt.multiAccounts && params.folder.isRemote()) {
634 		params.accountName = params.items[0].getAccount().name;
635 	}
636 
637 	this._itemAction(params);
638 };
639 
640 /**
641  * @private
642  */
643 ZmList.prototype._handleResponseCopyItems =
644 function(params, result) {
645 	var resp = result.getResponse();
646 	if (resp.length > 0) {
647 		if (params.actionTextKey) {
648 			var msg = AjxMessageFormat.format(ZmMsg[params.actionTextKey], resp.length);
649 			appCtxt.getAppController().setStatusMsg(msg);
650 		}
651 	}
652 };
653 
654 /**
655  * Deletes one or more items from the list. Normally, deleting an item just
656  * moves it to the Trash (soft delete). However, if it's already in the Trash,
657  * it will be removed from the data store (hard delete).
658  *
659  * @param {Hash}	params		a hash of parameters
660  * @param	{Array}		params.items			list of items to delete
661  * @param	{Boolean}	params.hardDelete		<code>true</code> to force physical removal of items
662  * @param	{Object}	params.attrs			additional attrs for SOAP command
663  * @param	{window}	params.childWin			the child window this action is happening in
664  * @param	{closure}	params.finalCallback	the callback to run after all items have been processed
665  * @param	{int}		params.count			the starting count for number of items processed
666  * @param	{Boolean}	params.confirmDelete		the user confirmed hard delete
667  */
668 ZmList.prototype.deleteItems =
669 function(params) {
670 
671 	params = Dwt.getParams(arguments, ["items", "hardDelete", "attrs", "childWin"]);
672 
673 	var items = params.items = AjxUtil.toArray(params.items);
674 
675 	// figure out which items should be moved to Trash, and which should actually be deleted
676 	var toMove = [];
677 	var toDelete = [];
678 	if (params.hardDelete) {
679 		toDelete = items;
680 	} else if (items[0] && items[0] instanceof ZmItem) {
681 		for (var i = 0; i < items.length; i++) {
682 			var item = items[i];
683 			var folderId = item.getFolderId();
684 			var folder = appCtxt.getById(folderId);
685 			if (folder && folder.isHardDelete()) {
686 				toDelete.push(item);
687 			} else {
688 				toMove.push(item);
689 			}
690 		}
691 	} else {
692 		toMove = items;
693 	}
694 
695 	if (toDelete.length && !params.confirmDelete) {
696 		params.confirmDelete = true;
697 		var callback = ZmList.prototype.deleteItems.bind(this, params);
698 		this._popupDeleteWarningDialog(callback, toMove.length, toDelete.length);
699 		return;
700 	}
701 
702 	params.callback = params.childWin && new AjxCallback(this._handleDeleteNewWindowResponse, params.childWin);
703 
704 	// soft delete - items moved to Trash
705 	if (toMove.length) {
706 		if (appCtxt.multiAccounts) {
707 			var accounts = this._filterItemsByAccount(toMove);
708 			if (!params.callback) {
709 				params.callback = new AjxCallback(this, this._deleteAccountItems, [accounts, params]);
710 			}
711 			this._deleteAccountItems(accounts, params);
712 		}
713 		else {
714 			params.items = toMove;
715 			params.folder = appCtxt.getById(ZmFolder.ID_TRASH);
716 			this.moveItems(params);
717 		}
718 	}
719 
720 	// hard delete - items actually deleted from data store
721 	if (toDelete.length) {
722 		params.items = toDelete;
723 		params.action = "delete";
724         params.actionTextKey = 'actionDelete';
725 		this._itemAction(params);
726 	}
727 };
728 
729 
730 ZmList.prototype._popupDeleteWarningDialog =
731 function(callback, onlySome, count) {
732 	var dialog = appCtxt.getOkCancelMsgDialog();
733 	dialog.reset();
734 	dialog.setMessage(AjxMessageFormat.format(ZmMsg[onlySome ? "confirmDeleteSomeForever" : "confirmDeleteForever"], [count]), DwtMessageDialog.WARNING_STYLE); 
735 	dialog.registerCallback(DwtDialog.OK_BUTTON, this._deleteWarningDialogListener.bind(this, callback, dialog));
736 	dialog.associateEnterWithButton(DwtDialog.OK_BUTTON);
737 	dialog.popup(null, DwtDialog.OK_BUTTON);
738 };
739 
740 ZmList.prototype._deleteWarningDialogListener =
741 function(callback, dialog) {
742 	dialog.popdown();
743 	callback();
744 };
745 
746 
747 /**
748  * @private
749  */
750 ZmList.prototype._deleteAccountItems =
751 function(accounts, params) {
752 	var items;
753 	for (var i in accounts) {
754 		items = accounts[i];
755 		break;
756 	}
757 
758 	if (items) {
759 		delete accounts[i];
760 
761         var ac = window.parentAppCtxt || window.appCtxt;
762         params.accountName = ac.accountList.getAccount(i).name;
763 		params.items = items;
764 		params.folder = appCtxt.getById(ZmFolder.ID_TRASH);
765 
766 		this.moveItems(params);
767 	}
768 };
769 
770 /**
771  * @private
772  */
773 ZmList.prototype._filterItemsByAccount =
774 function(items) {
775 	// separate out the items based on which account they belong to
776 	var accounts = {};
777 	if (items[0] && items[0] instanceof ZmItem) {
778 		for (var i = 0; i < items.length; i++) {
779 			var item = items[i];
780 			var acctId = item.getAccount().id;
781 			if (!accounts[acctId]) {
782 				accounts[acctId] = [];
783 			}
784 			accounts[acctId].push(item);
785 		}
786 	} else {
787 		var id = appCtxt.accountList.mainAccount.id;
788 		accounts[id] = items;
789 	}
790 
791 	return accounts;
792 };
793 
794 /**
795  * @private
796  */
797 ZmList.prototype._handleDeleteNewWindowResponse =
798 function(childWin, result) {
799 	if (childWin) {
800 		childWin.close();
801 	}
802 };
803 
804 /**
805  * Applies the given list of modifications to the item.
806  *
807  * @param {ZmItem}	item			the item to modify
808  * @param {Hash}	mods			hash of new properties
809  * @param	{AjxCallback}	callback	the callback
810  */
811 ZmList.prototype.modifyItem =
812 function(item, mods, callback) {
813 	item.modify(mods, callback);
814 };
815 
816 // Notification handling
817 
818 /**
819  * Create notification.
820  * 
821  * @param	{Object}	node		not used
822  */
823 ZmList.prototype.notifyCreate =
824 function(node) {
825 	var obj = eval(ZmList.ITEM_CLASS[this.type]);
826 	if (obj) {
827 		var item = obj.createFromDom(node, {list:this});
828 		this.add(item, this._sortIndex(item));
829 		this.createLocal(item);
830 		this._notify(ZmEvent.E_CREATE, {items: [item]});
831 	}
832 };
833 
834 // Local change handling
835 
836 // These generic methods allow a derived class to perform the appropriate internal changes
837 
838 /**
839  * Modifies the items (local).
840  * 
841  * @param	{Array}	items		an array of items
842  * @param	{Object}	mods	a hash of properties to modify
843  */
844 ZmList.prototype.modifyLocal 		= function(items, mods) {};
845 
846 /**
847  * Creates the item (local).
848  * 
849  * @param	{ZmItem}	item	the item to create
850  */
851 ZmList.prototype.createLocal 		= function(item) {};
852 
853 // These are not currently used; will need support in ZmItem if they are.
854 ZmList.prototype.flagLocal 			= function(items, flag, state) {};
855 ZmList.prototype.tagLocal 			= function(items, tag, state) {};
856 ZmList.prototype.removeAllTagsLocal = function(items) {};
857 
858 // default action is to remove each deleted item from this list
859 /**
860  * Deletes the items (local).
861  * 
862  * @param	{Array}	items		an array of items
863  */
864 ZmList.prototype.deleteLocal =
865 function(items) {
866 	for (var i = 0; i < items.length; i++) {
867 		this.remove(items[i]);
868 	}
869 };
870 
871 // default action is to remove each moved item from this list
872 /**
873  * Moves the items (local).
874  * 
875  * @param	{Array}	items		an array of items
876  * @param	{String}	folderId	the folder id
877  */
878 ZmList.prototype.moveLocal = 
879 function(items, folderId) {
880 	for (var i = 0; i < items.length; i++) {
881 		this.remove(items[i]);
882 	}
883 };
884 
885 /**
886  * Performs an action on items via a SOAP request.
887  *
888  * @param {Hash}				params				a hash of parameters
889  * @param	{Array}				params.items			a list of items to act upon
890  * @param	{String}			params.action			the SOAP operation
891  * @param	{Object}			params.attrs			a hash of additional attrs for SOAP request
892  * @param	{AjxCallback}		params.callback			the async callback
893  * @param	{closure}			params.finalCallback	the callback to run after all items have been processed
894  * @param	{AjxCallback}		params.errorCallback	the async error callback
895  * @param	{String}			params.accountName		the account to send request on behalf of
896  * @param	{int}				params.count			the starting count for number of items processed
897  * @param	{ZmBatchCommand}	batchCmd				if set, request data is added to batch request
898  * @param	{boolean}			params.noUndo			true if the action is performed as an undo (not undoable)
899  * @param	{boolean}			params.safeMove			true if the action wants to resolve any conflicts before completion
900  */
901 ZmList.prototype._itemAction =
902 function(params, batchCmd) {
903 
904 	var result = this._getIds(params.items);
905 	var idHash = result.hash;
906 	var idList = result.list;
907 	if (!(idList && idList.length)) {
908 		if (params.callback) {
909 			params.callback.run(new ZmCsfeResult([]));
910 		}
911 		if (params.finalCallback) {
912 			params.finalCallback(params);
913 		}
914 		return;
915 	}
916 
917 	DBG.println("sa", "ITEM ACTION: " + idList.length + " items");
918 	var type;
919 	if (params.items.length == 1 && params.items[0] && params.items[0].type) {
920 		type = params.items[0].type;
921 	} else {
922 		type = this.type;
923 	}
924 	if (!type) { return; }
925 
926 	// set accountName for multi-account to be the main "local" account since we
927 	// assume actioned ID's will always be fully qualified
928 	if (!params.accountName && appCtxt.multiAccounts) {
929 		params.accountName = appCtxt.accountList.mainAccount.name;
930 	}
931 
932 	var soapCmd = ZmItem.SOAP_CMD[type] + "Request";
933 	var useJson = batchCmd ? batchCmd._useJson : true ;
934 	var request, action;
935 	if (useJson) {
936 		request = {};
937 		var urn = this._getActionNamespace();
938 		request[soapCmd] = {_jsns:urn};
939 		var action = request[soapCmd].action = {};
940 		action.op = params.action;
941 		for (var attr in params.attrs) {
942 			action[attr] = params.attrs[attr];
943 		}
944 	} else {
945 		request = AjxSoapDoc.create(soapCmd, this._getActionNamespace());
946 		action = request.set("action");
947 		action.setAttribute("op", params.action);
948 		for (var attr in params.attrs) {
949 			action.setAttribute(attr, params.attrs[attr]);
950 		}
951 	}
952     var ac =  window.parentAppCtxt || appCtxt;
953 	var actionController = ac.getActionController();
954 	var undoPossible = !params.noUndo && (this.type != ZmItem.CONV || this.search && this.search.folderId); //bug 74169 - since the convs might not be fully loaded we might not know where the messages are moved from at all. so no undo.
955 	var actionLogItem = (undoPossible && actionController && actionController.actionPerformed({op: params.action, ids: idList, attrs: params.attrs})) || null;
956 	var respCallback = new AjxCallback(this, this._handleResponseItemAction, [params.callback, actionLogItem]);
957 
958 	var params1 = {
959 		ids:			idList,
960 		idHash:			idHash,
961 		accountName:	params.accountName,
962 		request:		request,
963 		action:			action,
964 		type:			type,
965 		callback:		respCallback,
966 		finalCallback:	params.finalCallback,
967 		errorCallback:	params.errorCallback,
968 		batchCmd:		batchCmd,
969 		numItems:		params.count || 0,
970 		actionTextKey:	params.actionTextKey,
971 		actionArg:		params.actionArg,
972 		actionLogItem:	actionLogItem,
973 		childWin:		params.childWin,
974 		closeChildWin: 	params.closeChildWin,
975 		safeMove:		params.safeMove
976 	};
977 
978 	if (idList.length >= ZmList.CHUNK_SIZE) {
979 		var pdParams = {
980 			state:		ZmListController.PROGRESS_DIALOG_INIT,
981 			callback:	new AjxCallback(this, this._cancelAction, [params1])
982 		}
983 		ZmListController.handleProgress(pdParams);
984 	}
985 	
986 	this._doAction(params1);
987 };
988 
989 /**
990  * @private
991  */
992 ZmList.prototype._handleResponseItemAction =
993 function(callback, actionLogItem, items, result) {
994 	if (actionLogItem) {
995 		actionLogItem.setComplete();
996 	}
997 	
998 	if (callback) {
999 		result.set(items);
1000 		callback.run(result);
1001 	}
1002 };
1003 
1004 /**
1005  * @private
1006  */
1007 ZmList.prototype._doAction =
1008 function(params) {
1009 
1010 	var list = params.ids.splice(0, ZmList.CHUNK_SIZE);
1011 	var idStr = list.join(",");
1012 	var useJson = true;
1013 	if (params.action.setAttribute) {
1014 		params.action.setAttribute("id", idStr);
1015 		useJson = false;
1016 	} else {
1017 		params.action.id = idStr;
1018 	}
1019 	var more = Boolean(params.ids.length && !params.cancelled);
1020 
1021 	var respCallback = new AjxCallback(this, this._handleResponseDoAction, [params]);
1022     var isOutboxFolder = this.controller && this.controller.isOutboxFolder();
1023     var offlineCallback = this._handleOfflineResponseDoAction.bind(this, params, isOutboxFolder);
1024 
1025 	if (params.batchCmd) {
1026 		params.batchCmd.addRequestParams(params.request, respCallback, params.errorCallback);
1027 	} else {
1028 		var reqParams = {asyncMode:true, callback:respCallback, errorCallback: params.errorCallback, offlineCallback: offlineCallback, accountName:params.accountName, more:more};
1029 		if (useJson) {
1030 			reqParams.jsonObj = params.request;
1031 		} else {
1032 			reqParams.soapDoc = params.request;
1033 		}
1034 		if (params.safeMove) {
1035 			reqParams.useChangeToken = true;
1036 		}
1037         if (isOutboxFolder) {
1038             reqParams.offlineRequest = true;
1039         }
1040 		DBG.println("sa", "*** do action: " + list.length + " items");
1041 		params.reqId = appCtxt.getAppController().sendRequest(reqParams);
1042 	}
1043 };
1044 
1045 /**
1046  * @private
1047  */
1048 ZmList.prototype._handleResponseDoAction =
1049 function(params, result) {
1050 
1051 	var summary;
1052 	var response = result.getResponse();
1053 	var resp = response[ZmItem.SOAP_CMD[params.type] + "Response"];
1054 	if (resp && resp.action) {
1055 		var ids = resp.action.id.split(",");
1056 		if (ids) {
1057 			var items = [];
1058 			for (var i = 0; i < ids.length; i++) {
1059 				var item = params.idHash[ids[i]];
1060 				if (item) {
1061 					items.push(item);
1062 				}
1063 			}
1064 			params.numItems += items.length;
1065 			if (params.callback) {
1066 				params.callback.run(items, result);
1067 			}
1068 
1069 			if (params.actionTextKey) {
1070 				summary = ZmList.getActionSummary(params);
1071 				var pdParams = {
1072 					state:		ZmListController.PROGRESS_DIALOG_UPDATE,
1073 					summary:	summary
1074 				}
1075 				ZmListController.handleProgress(pdParams);
1076 			}
1077 		}
1078 	}
1079 
1080 	if (params.ids.length && !params.cancelled) {
1081 		DBG.println("sa", "item action setting up next chunk, remaining: " + params.ids.length);
1082 		AjxTimedAction.scheduleAction(new AjxTimedAction(this, this._doAction, [params]), ZmItem.CHUNK_PAUSE);
1083 	} else {
1084 		params.reqId = null;
1085 		params.actionSummary = summary;
1086 		if (params.finalCallback) {
1087 			// finalCallback is responsible for showing status or clearing dialog
1088 			DBG.println("sa", "item action running finalCallback");
1089 			params.finalCallback(params);
1090 		} else {
1091 			DBG.println("sa", "no final callback");
1092 			ZmListController.handleProgress({state:ZmListController.PROGRESS_DIALOG_CLOSE});
1093 			ZmBaseController.showSummary(params.actionSummary, params.actionLogItem, params.closeChildWin);
1094 		}
1095 	}
1096 };
1097 
1098 /**
1099  * @private
1100  */
1101 ZmList.prototype._handleOfflineResponseDoAction =
1102 function(params, isOutboxFolder, requestParams) {
1103 
1104     var action = params.action,
1105         callback = this._handleOfflineResponseDoActionCallback.bind(this, params, isOutboxFolder, requestParams.callback);
1106 
1107     if (isOutboxFolder && action.op === "trash") {
1108         var key = {
1109             methodName : "SendMsgRequest", //Outbox folder only contains offline sent emails
1110 			id : action.id.split(",")
1111         };
1112         ZmOfflineDB.deleteItemInRequestQueue(key, callback);
1113     }
1114     else {
1115         var obj = requestParams.jsonObj;
1116         obj.methodName = ZmItem.SOAP_CMD[params.type] + "Request";
1117         obj.id = action.id;
1118         ZmOfflineDB.setItem(obj, ZmOffline.REQUESTQUEUE, callback);
1119     }
1120 };
1121 
1122 /**
1123  * @private
1124  */
1125 ZmList.prototype._handleOfflineResponseDoActionCallback =
1126 function(params, isOutboxFolder, callback) {
1127 
1128     var data = {},
1129         header = this._generateOfflineHeader(params),
1130         result,
1131         hdr,
1132         notify;
1133 
1134     data[ZmItem.SOAP_CMD[params.type] + "Response"] = params.request[ZmItem.SOAP_CMD[params.type] + "Request"];
1135     result = new ZmCsfeResult(data, false, header);
1136     hdr = result.getHeader();
1137     if (callback) {
1138         callback.run(result);
1139     }
1140     if (hdr) {
1141         notify = hdr.context.notify[0];
1142         if (notify) {
1143             appCtxt._requestMgr._notifyHandler(notify);
1144             this._updateOfflineData(params, isOutboxFolder, notify);
1145         }
1146     }
1147 };
1148 
1149 /**
1150  * @private
1151  */
1152 ZmList.prototype._generateOfflineHeader =
1153 function(params) {
1154 
1155     var action = params.action,
1156         op = action.op,
1157         ids = action.id.split(","),
1158         idsLength = ids.length,
1159         id,
1160         msg,
1161         flags,
1162         folderId,
1163         folder,
1164         targetFolder,
1165         mObj,
1166         cObj,
1167         folderObj,
1168         m = [],
1169         c = [],
1170         folderArray = [],
1171         header;
1172 
1173     for (var i = 0; i < idsLength; i++) {
1174 
1175         id = ids[i];
1176         msg = this.getById(id);
1177         flags =  msg.flags || "";
1178         folderId = msg.getFolderId();
1179         folder = appCtxt.getById(folderId);
1180         mObj = {
1181             id : id
1182         };
1183         cObj = {
1184             id : "-" + mObj.id
1185         };
1186         folderObj = {
1187             id : folderId
1188         };
1189 
1190         switch (op)
1191         {
1192             case "flag":
1193                 mObj.f = flags + "f";
1194                 break;
1195             case "!flag":
1196                 mObj.f = flags.replace("f", "");
1197                 break;
1198             case "read":
1199                 mObj.f = flags.replace("u", "");
1200                 folderObj.u = folder.numUnread - 1;
1201                 break;
1202             case "!read":
1203                 mObj.f = flags + "u";
1204                 folderObj.u = folder.numUnread + 1;
1205                 break;
1206             case "trash":
1207                 mObj.l = ZmFolder.ID_TRASH;
1208                 break;
1209             case "spam":
1210                 mObj.l = ZmFolder.ID_SPAM;
1211                 break;
1212             case "!spam":
1213                 mObj.l = ZmFolder.ID_INBOX;// Have to set the old folder id. Currently point to inbox
1214                 break;
1215             case "move":
1216                 if (action.l) {
1217                     mObj.l = action.l;
1218                 }
1219                 folderObj.n = folder.numTotal - 1;
1220                 if (msg.isUnread && folder.numUnread > 1) {
1221                     folderObj.u = folder.numUnread - 1;
1222                 }
1223                 targetFolder = appCtxt.getById(mObj.l);
1224                 folderArray.push({
1225                     id : targetFolder.id,
1226                     n : targetFolder.numTotal + 1,
1227                     u : (msg.isUnread ? targetFolder.numUnread + 1 : targetFolder.numUnread)
1228                 });
1229                 break;
1230             case "tag":
1231                 msg.tags.push(action.tn);
1232                 mObj.tn = msg.tags.join();
1233                 break;
1234             case "!tag":
1235                 AjxUtil.arrayRemove(msg.tags, action.tn);
1236                 mObj.tn = msg.tags.join();
1237                 break;
1238             case "update":
1239                 if (action.t === "") {//Removing all tag names for a msg
1240                     mObj.tn = "";
1241                     mObj.t = "";
1242                 }
1243                 break;
1244         }
1245         m.push(mObj);
1246         c.push(cObj);
1247         folderArray.push(folderObj);
1248     }
1249 
1250     header = {
1251         context : {
1252             notify : [{
1253                 modified : {
1254                     m : m,
1255                     c : c,
1256                     folder : folderArray
1257                 }
1258             }]
1259         }
1260     };
1261 
1262     return header;
1263 };
1264 
1265 ZmList.prototype._updateOfflineData =
1266 function(params, isOutboxFolder, notify) {
1267 
1268     var modified = notify.modified;
1269     if (!modified) {
1270         return;
1271     }
1272 
1273     var m = modified.m;
1274     if (!m) {
1275         return;
1276     }
1277 
1278     var callback = this._updateOfflineDataCallback.bind(this, params, m);
1279     ZmOfflineDB.getItem(params.action.id.split(","), ZmApp.MAIL, callback);
1280 };
1281 
1282 ZmList.prototype._updateOfflineDataCallback =
1283 function(params, msgArray, result) {
1284     result = ZmOffline.recreateMsg(result);
1285     var newMsgArray = [];
1286     result.forEach(function(res) {
1287         msgArray.forEach(function(msg) {
1288             if (msg.id === res.id) {
1289                 newMsgArray.push($.extend(res, msg));
1290             }
1291         });
1292     });
1293     ZmOfflineDB.setItem(newMsgArray, ZmApp.MAIL);
1294 };
1295 
1296 /**
1297  * Returns a string describing an action, intended for display as toast to tell the
1298  * user what they just did.
1299  *
1300  * @param   {Object}        params          hash of params:
1301  *          {String}        type            item type (ZmItem.*)
1302  *          {Number}        numItems        number of items affected
1303  *          {String}        actionTextKey   ZmMsg key for text string describing action
1304  *          {String}        actionArg       (optional) additional argument
1305  *
1306  * @return {String}     action summary
1307  */
1308 ZmList.getActionSummary =
1309 function(params) {
1310 
1311 	var type = params.type,
1312 		typeKey = ZmItem.MSG_KEY[type],
1313 		typeText = ZmMsg[typeKey],
1314 		capKey = AjxStringUtil.capitalizeFirstLetter(typeKey),
1315 		countKey = 'type' + capKey,
1316 		num = params.numItems,
1317 		alternateKey = params.actionTextKey + capKey,
1318 		text = ZmMsg[alternateKey] || ZmMsg[params.actionTextKey],
1319 		countText = ZmMsg[countKey],
1320 		arg = AjxStringUtil.htmlEncode(params.actionArg),
1321 		textAuto = countText ? AjxMessageFormat.format(countText, num) : typeText,
1322 		textSingular = countText ? AjxMessageFormat.format(ZmMsg[countKey], 1) : typeText;
1323 
1324 	return AjxMessageFormat.format(text, [ num, textAuto, arg, textSingular ]);
1325 };
1326 
1327 /**
1328  * Cancel current server request if there is one, and set flag to
1329  * stop cascade of requests.
1330  *
1331  * @param {Hash}	params	a hash of parameters
1332  * 
1333  * @private
1334  */
1335 ZmList.prototype._cancelAction =
1336 function(params) {
1337 	params.cancelled = true;
1338 	if (params.reqId) {
1339 		appCtxt.getRequestMgr().cancelRequest(params.reqId);
1340 	}
1341 	if (params.finalCallback) {
1342 		params.finalCallback(params);
1343 	}
1344 	ZmListController.handleProgress({state:ZmListController.PROGRESS_DIALOG_CLOSE});
1345 };
1346 
1347 /**
1348  * @private
1349  */
1350 ZmList.prototype._getTypedItems =
1351 function(items) {
1352 	var typedItems = {};
1353 	for (var i = 0; i < items.length; i++) {
1354 		var type = items[i].type;
1355 		if (!typedItems[type]) {
1356 			typedItems[type] = [];
1357 		}
1358 		typedItems[type].push(items[i]);
1359 	}
1360 	return typedItems;
1361 };
1362 
1363 /**
1364  * Grab the IDs out of a list of items, and return them as both a string and a hash.
1365  * 
1366  * @private
1367  */
1368 ZmList.prototype._getIds =
1369 function(list) {
1370 
1371 	var idHash = {};
1372 	if (list instanceof ZmItem) {
1373 		list = [list];
1374 	}
1375 	
1376 	var ids = [];
1377 	if ((list && list.length)) {
1378 		for (var i = 0; i < list.length; i++) {
1379 			var item = list[i];
1380 			var id = item.id;
1381 			if (id) {
1382 				ids.push(id);
1383 				idHash[id] = item;
1384 			}
1385 		}
1386 	}
1387 
1388 	return {hash:idHash, list:ids};
1389 };
1390 
1391 /**
1392  * Returns the index at which the given item should be inserted into this list.
1393  * Subclasses should override to return a meaningful value.
1394  * 
1395  * @private
1396  */
1397 ZmList.prototype._sortIndex = 
1398 function(item) {
1399 	return 0;
1400 };
1401 
1402 /**
1403  * @private
1404  */
1405 ZmList.prototype._redoSearch = 
1406 function(ctlr) {
1407 	var sc = appCtxt.getSearchController();
1408 	sc.redoSearch(ctlr._currentSearch);
1409 };
1410 
1411 /**
1412  * @private
1413  */
1414 ZmList.prototype._getActionNamespace =
1415 function() {
1416 	return "urn:zimbraMail";
1417 };
1418 
1419 /**
1420  * @private
1421  */
1422 ZmList.prototype._folderTreeChangeListener = 
1423 function(ev) {
1424 	if (ev.type != ZmEvent.S_FOLDER) return;
1425 
1426 	var folder = ev.getDetail("organizers")[0];
1427 	var fields = ev.getDetail("fields");
1428 	var ctlr = appCtxt.getCurrentController();
1429 	var isCurrentList = (appCtxt.getCurrentList() == this);
1430 
1431 	if (ev.event == ZmEvent.E_DELETE &&
1432 		(ev.source instanceof ZmFolder) &&
1433 		ev.source.id == ZmFolder.ID_TRASH)
1434 	{
1435 		// user emptied trash - reset a bunch of stuff w/o having to redo the search
1436 		var curView = ctlr.getListView && ctlr.getListView();
1437 		if (curView) {
1438 			curView.offset = 0;
1439 		}
1440 		ctlr._resetNavToolBarButtons(view);
1441 	}
1442 	else if (isCurrentList && ctlr && ctlr._currentSearch &&
1443 			 (ev.event == ZmEvent.E_MOVE || (ev.event == ZmEvent.E_MODIFY) && fields && fields[ZmOrganizer.F_NAME]))
1444 	{
1445 		// on folder rename or move, update current query if folder is part of query
1446 		if (ctlr._currentSearch.replaceFolderTerm(ev.getDetail("oldPath"), folder.getPath())) {
1447 			appCtxt.getSearchController().setSearchField(ctlr._currentSearch.query);
1448 		}
1449 	}
1450 };
1451 
1452 /**
1453  * this method is for handling changes in the tag tree itself (tag rename, delete). In some places it is named _tagChangeListener.
1454  * the ZmListView equivalent is actually called ZmListView.prototype._tagChangeListener 
1455  * @private
1456  */
1457 ZmList.prototype._tagTreeChangeListener =
1458 function(ev) {
1459 	if (ev.type != ZmEvent.S_TAG) { return; }
1460 
1461 	var tag = ev.getDetail("organizers")[0];
1462 	var fields = ev.getDetail("fields");
1463 	var ctlr = appCtxt.getCurrentController();
1464 	if (!ctlr) { return; }
1465 
1466 	var a = this.getArray();
1467 
1468 	if ((ev.event == ZmEvent.E_MODIFY) && fields && fields[ZmOrganizer.F_NAME]) {
1469 		// on tag rename, update current query if tag is part of query
1470 		var oldName = ev.getDetail("oldName");
1471 		if (ctlr._currentSearch && ctlr._currentSearch.hasTagTerm(oldName)) {
1472 			ctlr._currentSearch.replaceTagTerm(oldName, tag.getName());
1473 			appCtxt.getSearchController().setSearchField(ctlr._currentSearch.query);
1474 		}
1475 
1476 		//since we tag (and map the tags) by name, replace the tag name in the list and hash of tags.
1477 		var newName = tag.name;
1478 		for (var i = 0; i < a.length; i++) {
1479 			var item = a[i]; //not using the following here as it didn't seem to work for contacts, the list is !isCanonical and null is returned, even though a[i] is fine ==> this.getById(a[i].id); // make sure item is realized (contact may not be)
1480 			if (!item || !item.isZmItem || !item.hasTag(oldName)) {
1481 				continue; //nothing to do if item does not have tag
1482 			}
1483 			if (item.isShared()) {
1484 				continue; //overview tag rename does not affect remote items tags
1485 			}
1486 			var tagHash = item.tagHash;
1487 			var tags = item.tags;
1488 			delete tagHash[oldName];
1489 			tagHash[newName] = true;
1490 			for (var j = 0 ; j < tags.length; j++) {
1491 				if (tags[j] == oldName) {
1492 					tags[j] = newName;
1493 					break;
1494 				}
1495 			}
1496 		}
1497 
1498 
1499 	} else if (ev.event == ZmEvent.E_DELETE) {
1500 		// Remove tag from any items that have it
1501 		var hasTagListener = this._evtMgr.isListenerRegistered(ZmEvent.L_MODIFY);
1502 		for (var i = 0; i < a.length; i++) {
1503 			var item = this.getById(a[i].id);	// make sure item is realized (contact may not be)
1504             if (item) {
1505                 if (item.isShared()) {
1506                     continue; //overview tag delete does not affect remote items tags
1507                 }
1508                 if (item.hasTag(tag.name)) {
1509                     item.tagLocal(tag.name, false);
1510                     if (hasTagListener) {
1511                         this._notify(ZmEvent.E_TAGS, {items:[item]});
1512                     }
1513                 }
1514             }
1515 		}
1516 
1517 		// If search results are based on this tag, keep them around so that user can still
1518 		// view msgs or open convs, but disable pagination and sorting since they're based
1519 		// on the current query.
1520 		if (ctlr._currentSearch && ctlr._currentSearch.hasTagTerm(tag.getName())) {
1521 			var viewId = appCtxt.getCurrentViewId();
1522 			var viewType = appCtxt.getCurrentViewType();
1523 			ctlr.enablePagination(false, viewId);
1524 			var view = ctlr.getListView && ctlr.getListView();
1525 			if (view && view.sortingEnabled) {
1526 				view.sortingEnabled = false;
1527 			}
1528 			if (viewType == ZmId.VIEW_CONVLIST) {
1529 				ctlr._currentSearch.query = "is:read is:unread";
1530 			}
1531 			ctlr._currentSearch.tagId = null;
1532 			appCtxt.getSearchController().setSearchField("");
1533 		}
1534 	}
1535 };
1536