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, 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, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * Creates an empty list of mail items.
 26  * @constructor
 27  * @class
 28  * This class represents a list of mail items (conversations, messages, or
 29  * attachments). We retain a handle to the search that generated the list for
 30  * two reasons: so that we can redo the search if necessary, and so that we
 31  * can get the folder ID if this list represents folder contents.
 32  *
 33  * @author Conrad Damon
 34  * 
 35  * @param type		type of mail item (see ZmItem for constants)
 36  * @param search	the search that generated this list
 37  */
 38 ZmMailList = function(type, search) {
 39 
 40 	ZmList.call(this, type, search);
 41 
 42 	this.convId = null; // for msg list within a conv
 43 
 44 	// mail list can be changed via folder or tag action (eg "Mark All Read")
 45 	var folderTree = appCtxt.getFolderTree();
 46 	if (folderTree) {
 47 		this._folderChangeListener = new AjxListener(this, this._folderTreeChangeListener);
 48 		folderTree.addChangeListener(this._folderChangeListener);
 49 	}
 50 };
 51 
 52 ZmMailList.prototype = new ZmList;
 53 ZmMailList.prototype.constructor = ZmMailList;
 54 
 55 ZmMailList.prototype.isZmMailList = true;
 56 ZmMailList.prototype.toString = function() { return "ZmMailList"; };
 57 
 58 ZmMailList._SPECIAL_FOLDERS = [ZmFolder.ID_DRAFTS, ZmFolder.ID_TRASH, ZmFolder.ID_SPAM, ZmFolder.ID_SENT];
 59 ZmMailList._SPECIAL_FOLDERS_HASH = AjxUtil.arrayAsHash(ZmMailList._SPECIAL_FOLDERS);
 60 
 61 
 62 /**
 63  * Override so that we can specify "tcon" attribute for conv move - we don't want
 64  * to move messages in certain system folders as a side effect. Also, we need to
 65  * update the UI based on the response if we're moving convs, since the 
 66  * notifications only tell us about moved messages. This method should be called
 67  * only in response to explicit action by the user, in which case we want to
 68  * remove the conv row(s) from the list view (even if the conv still matches the
 69  * search).
 70  *
 71  * @param {Hash}	params		a hash of parameters
 72  *        items			[array]			a list of items to move
 73  *        folder		[ZmFolder]		destination folder
 74  *        attrs			[hash]			additional attrs for SOAP command
 75  *        callback		[AjxCallback]*	callback to run after each sub-request
 76  *        finalCallback	[closure]*		callback to run after all items have been processed
 77  *        count			[int]*			starting count for number of items processed
 78  *        fromFolderId  [String]*       optional folder to represent when calculating tcon. If unspecified, use current search folder nId
 79  *        
 80  * @private
 81  */
 82 ZmMailList.prototype.moveItems =
 83 function(params) {
 84 
 85 	if (this.type != ZmItem.CONV) {
 86 		return ZmList.prototype.moveItems.apply(this, arguments);
 87 	}
 88 
 89 	params = Dwt.getParams(arguments, ["items", "folder", "attrs", "callback", "finalCallback", "noUndo", "actionTextKey", "fromFolderId"]);
 90 	params.items = AjxUtil.toArray(params.items);
 91 
 92 	var params1 = AjxUtil.hashCopy(params);
 93 	delete params1.fromFolderId;
 94 
 95 	params1.attrs = {};
 96 	var tcon = this._getTcon(params.items, params.fromFolderId);
 97 	if (tcon) {
 98 		params1.attrs.tcon = tcon;
 99 	}
100 	params1.attrs.l = params.folder.id;
101 	params1.action = (params.folder.id == ZmFolder.ID_TRASH) ? "trash" : "move";
102     if (params1.folder.id == ZmFolder.ID_TRASH) {
103         params1.actionTextKey = params.actionTextKey || "actionTrash";
104     } else {
105         params1.actionTextKey = params.actionTextKey || "actionMove";
106         params1.actionArg = params1.folder.getName(false, false, true);
107     }
108 	params1.callback = new AjxCallback(this, this._handleResponseMoveItems, [params]);
109 
110 	if (appCtxt.multiAccounts) {
111 		// Reset accountName for multi-account to be the respective account if we're
112 		// moving a draft out of Trash.
113 		// OR,
114 		// check if we're moving to or from a shared folder, in which case, always send
115 		// request on-behalf-of the account the item originally belongs to.
116         var folderId = params.items[0].getFolderId && params.items[0].getFolderId();
117 
118         // on bulk delete, when the second chunk loads try to get folderId from the item id.
119         if (!folderId) {
120             var itemId = params.items[0] && params.items[0].id;
121             folderId = itemId && appCtxt.getById(itemId) && appCtxt.getById(itemId).folderId;
122         }
123         var fromFolder = folderId && appCtxt.getById(folderId);
124 		if ((params.items[0].isDraft && params.folder.id == ZmFolder.ID_DRAFTS) ||
125 			(params.folder.isRemote()) || (fromFolder && fromFolder.isRemote()))
126 		{
127 			params1.accountName = params.items[0].getAccount().name;
128 		}
129 	}
130 
131 	if (this._handleDeleteFromSharedFolder(params, params1)) {
132 		return;
133 	}
134 
135 	params1.safeMove = true; //Move only items currently seen by the client
136 	this._itemAction(params1);
137 };
138 
139 /**
140  * Marks items as "spam" or "not spam". If they're marked as "not spam", a target folder
141  * may be provided.
142  * @param {Hash}	params		a hash of parameters
143  *        items			[array]			a list of items
144  *        markAsSpam	[boolean]		if true, mark as "spam"
145  *        folder		[ZmFolder]		destination folder
146  *        childWin		[window]*		the child window this action is happening in
147  *        closeChildWin	[boolean]*		is the child window closed at the end of the action?
148  *        callback		[AjxCallback]*	callback to run after each sub-request
149  *        finalCallback	[closure]*		callback to run after all items have been processed
150  *        count			[int]*			starting count for number of items processed
151  * @private
152  */
153 ZmMailList.prototype.spamItems = 
154 function(params) {
155 
156 	var items = params.items = AjxUtil.toArray(params.items);
157 
158 	if (appCtxt.multiAccounts) {
159 		var accounts = this._filterItemsByAccount(items);
160 		this._spamAccountItems(accounts, params);
161 	} else {
162 		this._spamItems(params);
163 	}
164 };
165 
166 ZmMailList.prototype._spamAccountItems =
167 function(accounts, params) {
168 	var items;
169 	for (var i in accounts) {
170 		items = accounts[i];
171 		break;
172 	}
173 
174 	if (items) {
175 		delete accounts[i];
176 
177 		params.accountName = appCtxt.accountList.getAccount(i).name;
178 		params.items = items;
179 		params.callback = new AjxCallback(this, this._spamAccountItems, [accounts, params]);
180 
181 		this._spamItems(params);
182 	}
183 };
184 
185 ZmMailList.prototype._spamItems =
186 function(params) {
187 	params = Dwt.getParams(arguments, ["items", "markAsSpam", "folder", "childWin"]);
188 
189 	var params1 = AjxUtil.hashCopy(params);
190 
191 	params1.action = params.markAsSpam ? "spam" : "!spam";
192 	params1.attrs = {};
193 	if (this.type === ZmItem.CONV) {
194 		var tcon = this._getTcon(params.items);
195 		//the reason not to set "" as tcon is from bug 58727. (though I think it should have been a server fix).
196 		if (tcon) {
197 			params1.attrs.tcon = tcon;
198 		}
199 	}
200 	if (params.folder) {
201 		params1.attrs.l = params.folder.id;
202 	}
203 	params1.actionTextKey = params.markAsSpam ? 'actionMarkAsJunk' : 'actionMarkAsNotJunk';
204 
205 	params1.callback = new AjxCallback(this, this._handleResponseSpamItems, params);
206 	this._itemAction(params1);
207 };
208 
209 ZmMailList.prototype._handleResponseSpamItems =
210 function(params, result) {
211 
212 	var movedItems = result.getResponse();
213 	var summary;
214 	if (movedItems && movedItems.length) {
215 		var folderId = params.markAsSpam ? ZmFolder.ID_SPAM : (params.folder ? params.folder.id : ZmFolder.ID_INBOX);
216 		this.moveLocal(movedItems, folderId);
217 		var convs = {};
218 		for (var i = 0; i < movedItems.length; i++) {
219 			var item = movedItems[i];
220 			if (item.cid) {
221 				var conv = appCtxt.getById(item.cid);
222 				if (conv) {
223 					if (!convs[conv.id])
224 						convs[conv.id] = {conv:conv,msgs:[]};
225 					convs[conv.id].msgs.push(item);
226 				}
227 			}
228 			var details = {oldFolderId:item.folderId, fields:{}};
229 			details.fields[ZmItem.F_FRAGMENT] = true;
230 			item.moveLocal(folderId);
231 		}
232 
233 		for (var id in convs) {
234 			if (convs.hasOwnProperty(id)) {
235 				var conv = convs[id].conv;
236 				var msgs = convs[id].msgs;
237 				conv.updateFragment(msgs);
238 			}
239 		}
240 		//ZmModel.notifyEach(movedItems, ZmEvent.E_MOVE);
241 		
242 		var item = movedItems[0];
243 		var list = item.list;
244 		if (list) {
245 			list._evt.batchMode = true;
246 			list._evt.item = item;	// placeholder
247 			list._evt.items = movedItems;
248 			list._notify(ZmEvent.E_MOVE, details);
249 		}
250 		if (params.actionText) {
251 			summary = ZmList.getActionSummary(params);
252 		}
253 
254 		if (params.childWin) {
255 			params.childWin.close();
256 		}
257 	}
258 	params.actionSummary = summary;
259 	if (params.callback) {
260 		params.callback.run(result);
261 	}
262 };
263 
264 /**
265  * Override so that delete of a conv in Trash doesn't hard-delete its msgs in
266  * other folders. If we're in conv mode in Trash, we add a constraint of "t",
267  * meaning that the action is only applied to items (msgs) in the Trash.
268  *
269  * @param {Hash}		params		a hash of parameters
270  * @param  {Array}     params.items			list of items to delete
271  * @param {Boolean}      params.hardDelete	whether to force physical removal of items
272  * @param {Object}      params.attrs			additional attrs for SOAP command
273  * @param {window}       params.childWin		the child window this action is happening in
274  * @param	{Boolean}	params.confirmDelete		the user confirmed hard delete
275  *
276  * @private
277  */
278 ZmMailList.prototype.deleteItems =
279 function(params) {
280 
281 	params = Dwt.getParams(arguments, ["items", "hardDelete", "attrs", "childWin"]);
282 
283 	if (this.type == ZmItem.CONV) {
284 		var searchFolder = this.search ? appCtxt.getById(this.search.folderId) : null;
285 		if (searchFolder && searchFolder.isHardDelete()) {
286 
287 			if (!params.confirmDelete) {
288 				params.confirmDelete = true;
289 				var callback = ZmMailList.prototype.deleteItems.bind(this, params);
290 				this._popupDeleteWarningDialog(callback, false, params.items.length);
291 				return;
292 			}
293 
294 			var instantOn = appCtxt.getAppController().getInstantNotify();
295 			if (instantOn) {
296 				// bug fix #32005 - disable instant notify for ops that might take awhile
297 				appCtxt.getAppController().setInstantNotify(false);
298 				params.errorCallback = new AjxCallback(this, this._handleErrorDeleteItems);
299 			}
300 
301 			params.attrs = params.attrs || {};
302 			params.attrs.tcon = ZmFolder.TCON_CODE[searchFolder.nId];
303 			params.action = "delete";
304             params.actionTextKey = 'actionDelete';
305 			params.callback = new AjxCallback(this, this._handleResponseDeleteItems, instantOn);
306 			return this._itemAction(params);
307 		}
308 	}
309 	ZmList.prototype.deleteItems.call(this, params);
310 };
311 
312 ZmMailList.prototype._handleResponseDeleteItems =
313 function(instantOn, result) {
314 	var deletedItems = result.getResponse();
315 	if (deletedItems && deletedItems.length) {
316 		this.deleteLocal(deletedItems);
317 		for (var i = 0; i < deletedItems.length; i++) {
318 			deletedItems[i].deleteLocal();
319 		}
320 		// note: this happens before we process real notifications
321 		ZmModel.notifyEach(deletedItems, ZmEvent.E_DELETE);
322 	}
323 
324 	if (instantOn) {
325 		appCtxt.getAppController().setInstantNotify(true);
326 	}
327 };
328 
329 ZmMailList.prototype._handleErrorDeleteItems =
330 function() {
331 	appCtxt.getAppController().setInstantNotify(true);
332 };
333 
334 /**
335  * Only make the request for items whose state will be changed.
336  *
337  * @param {Hash}		params		a hash of parameters
338  *
339  *        items			[array]				a list of items to mark read/unread
340  *        value			[boolean]			if true, mark items read
341  *        callback		[AjxCallback]*		callback to run after each sub-request
342  *        finalCallback	[closure]*			callback to run after all items have been processed
343  *        count			[int]*				starting count for number of items processed
344  *        
345  * @private
346  */
347 ZmMailList.prototype.markRead =
348 function(params) {
349 
350 	var items = AjxUtil.toArray(params.items);
351 
352 	var items1;
353 	if (items[0] && items[0] instanceof ZmItem) {
354 		items1 = [];
355 		for (var i = 0; i < items.length; i++) {
356 			var item = items[i];
357 			if ((item.type == ZmItem.CONV && item.hasFlag(ZmItem.FLAG_UNREAD, params.value)) || (item.isUnread == params.value)) {
358 				items1.push(item);
359 			}
360 		}
361 	} else {
362 		items1 = items;
363 	}
364 
365 	if (items1.length) {
366 		params.items = items1;
367 		params.op = "read";
368 		if (items1.length > 1) {
369         	params.actionTextKey = params.value ? 'actionMarkRead' : 'actionMarkUnread';
370 		}
371 		this.flagItems(params);
372 	}
373     else if(params.forceCallback) {
374         if (params.callback) {
375 			params.callback.run(new ZmCsfeResult([]));
376 		}
377 		if (params.finalCallback) {
378 			params.finalCallback(params);
379 		}
380 		return;
381     }
382 };
383 
384 /**
385  * Only make the request for items whose state will be changed.
386  *
387  * @param {Hash}		params		a hash of parameters
388  *
389  *        items			[array]				a list of items to mark read/unread
390  *        value			[boolean]			if true, mark items read
391  *        callback		[AjxCallback]*		callback to run after each sub-request
392  *        finalCallback	[closure]*			callback to run after all items have been processed
393  *        count			[int]*				starting count for number of items processed
394  *
395  * @private
396  */
397 ZmMailList.prototype.markMute =
398 function(params) {
399 
400 	var items = AjxUtil.toArray(params.items);
401 
402 	var items1;
403 	if (items[0] && items[0] instanceof ZmItem) {
404 		items1 = [];
405 		for (var i = 0; i < items.length; i++) {
406 			var item = items[i];
407 			if (params.value != item.isMute) {
408 				items1.push(item);
409 			}
410 		}
411 	} else {
412 		items1 = items;
413 	}
414 
415 	if (items1.length) {
416 		params.items = items1;
417 		params.op = "mute";
418         params.actionTextKey = params.value ? 'actionMarkMute' : 'actionMarkUnmute';
419 		this.flagItems(params);
420 	}
421     else if(params.forceCallback) {
422         if (params.callback) {
423 			params.callback.run(new ZmCsfeResult([]));
424 		}
425 		if (params.finalCallback) {
426 			params.finalCallback(params);
427 		}
428 		return;
429     }
430 };
431 
432 // set "force" flag to true on actual hard deletes, so that msgs
433 // in a conv list are removed
434 ZmMailList.prototype.deleteLocal =
435 function(items) {
436 	for (var i = 0; i < items.length; i++) {
437 		this.remove(items[i], true);
438 	}
439 };
440 
441 // When a conv or msg is moved to Trash, it is marked read by the server.
442 ZmMailList.prototype.moveLocal =
443 function(items, folderId) {
444 	ZmList.prototype.moveLocal.call(this, items, folderId);
445 	if (folderId != ZmFolder.ID_TRASH) { return; }
446 
447 	var flaggedItems = [];
448 	for (var i = 0; i < items.length; i++) {
449 		if (items[i].isUnread) {
450 			items[i].flagLocal(ZmItem.FLAG_UNREAD, false);
451 			flaggedItems.push(items[i]);
452 		}
453 	}
454 	ZmModel.notifyEach(flaggedItems, ZmEvent.E_FLAGS, {flags:[ZmItem.FLAG_UNREAD]});
455 };
456 
457 ZmMailList.prototype.notifyCreate = 
458 function(convs, msgs) {
459 
460 	var createdItems = [];
461 	var newConvs = [];
462 	var newMsgs = [];
463 	var flaggedItems = [];
464 	var modifiedItems = [];
465 	var newConvId = {};
466 	var fields = {};
467 	var sortBy = this.search ? this.search.sortBy : null;
468 	var sortIndex = {};
469 	if (this.type == ZmItem.CONV) {
470 		// handle new convs first so we can set their fragments later from new msgs
471 		for (var id in convs) {
472 			AjxDebug.println(AjxDebug.NOTIFY, "ZmMailList: handling conv create " + id);
473 			if (this.getById(id)) {
474 				AjxDebug.println(AjxDebug.NOTIFY, "ZmMailList: conv already exists " + id);
475 				continue;
476 			}
477 			newConvId[id] = convs[id];
478 			var conv = convs[id];
479 			var convMatches =  this.search && this.search.matches(conv) && !conv.ignoreJunkTrash();
480 			if (convMatches) {
481 				if (!appCtxt.multiAccounts ||
482 					(appCtxt.multiAccounts && (this.search.isMultiAccount() || conv.getAccount() == appCtxt.getActiveAccount()))) 
483 				{
484 					// a new msg for this conv matches current search
485 					conv.list = this;
486 					newConvs.push(conv);
487 					AjxDebug.println(AjxDebug.NOTIFY, "ZmMailList: conv added " + id);
488 				}
489 				else {
490 					AjxDebug.println(AjxDebug.NOTIFY, "ZmMailList: conv failed account checks " + id);
491 				}
492 			}
493 			else {
494 				// debug info for bug 47589
495 				var query = this.search ? this.search.query : "";
496 				var ignore = conv.ignoreJunkTrash();
497 				AjxDebug.println(AjxDebug.NOTIFY, "ZmMailList: conv does not match search '" + query + "' or was ignored (" + ignore + "); match function:");
498 				if (!conv) {
499 					AjxDebug.println(AjxDebug.NOTIFY, "conv is null!");
500 				}
501 				else {
502 					var folders = AjxUtil.keys(conv.folders) || "";
503 					AjxDebug.println(AjxDebug.NOTIFY, "conv folders: " + folders.join(" "));
504 				}
505 			}
506 		}
507 
508 		// a new msg can hand us a new conv, and update a conv's info
509 		for (var id in msgs) {
510 			var msg = msgs[id];
511 			AjxDebug.println(AjxDebug.NOTIFY, "ZmMailList: CLV handling msg create " + id);
512 			var cid = msg.cid;
513 			var msgMatches =  this.search && this.search.matches(msg) && !msg.ignoreJunkTrash();
514 			AjxDebug.println(AjxDebug.NOTIFY, "ZmMailList: CLV msg matches: " + msgMatches);
515 			var isActiveAccount = (!appCtxt.multiAccounts || (appCtxt.multiAccounts && msg.getAccount() == appCtxt.getActiveAccount()));
516 			var conv = newConvId[cid] || this.getById(cid);
517 			var updateConv = false;
518 			if (msgMatches && isActiveAccount) {
519 				if (!conv) {
520 					// msg will have _convCreateNode if it is 2nd msg and caused promotion of virtual conv;
521 					// the conv node will have proper count and subject
522 					var args = {list:this};
523 					if (msg._convCreateNode) {
524 						if (msg._convCreateNode._newId) {
525 							msg._convCreateNode.id = msg._convCreateNode._newId;
526 						}
527 						//sometimes the conv is already in the app cache. Make sure not to re-create it and with the wrong msgs. This is slight improvement of bug 87861.
528 						conv = appCtxt.getById(cid);
529 						if (!conv) {
530 							conv = ZmConv.createFromDom(msg._convCreateNode, args);
531 						}
532 					}
533 					else {
534 						conv = appCtxt.getById(cid) || ZmConv.createFromMsg(msg, args);
535 					}
536 					newConvId[cid] = conv;
537 					conv.folders[msg.folderId] = true;
538 					newConvs.push(conv);
539 				}
540 				conv.list = this;
541 			}
542 			// make sure conv's msg list is up to date
543 			if (conv && !(conv.msgs && conv.msgs.getById(id))) {
544 				if (!conv.msgs) {
545 					conv.msgs = new ZmMailList(ZmItem.MSG);
546 					conv.msgs.addChangeListener(conv._listChangeListener);
547 				}
548 				msg.list = conv.msgs;
549 				if (!msg.isSent && msg.isUnread) {
550 					conv.isUnread = true;
551 					flaggedItems.push(conv);
552 				}
553 				// if the new msg matches current search, update conv date, fragment, and sort order
554 				if (msgMatches) {
555 					msg.inHitList = true;
556 				}
557 				if (msgMatches || ((msgMatches === null) && !msg.isSent)) {
558 					if (conv.fragment != msg.fragment) {
559 						conv.fragment = msg.fragment;
560 						fields[ZmItem.F_FRAGMENT] = true;
561 					}
562 					if (conv.date != msg.date) {
563 						conv.date = msg.date;
564 						// recalculate conv's sort position since we changed its date
565 						fields[ZmItem.F_DATE] = true;
566 					}
567 					if (conv.numMsgs === 1) {
568 						//there is only one message in this conv so set the size of conv to msg size
569 						conv.size = msg.size;
570 					}
571 					else {
572 						//So it shows the message count, and not the size (see ZmConvListView.prototype._getCellContents)
573 						//this size is no longer relevant (was set in the above if previously, see bug 87416)
574 						conv.size = null;
575 					}
576 					if (msg._convCreateNode) {
577 						//in case of single msg virtual conv promoted to a real conv - update the size
578 						// (in other cases of size it's updated elsewhere - see ZmConv.prototype.notifyModify, the server sends the update notification for the conv size)
579 						fields[ZmItem.F_SIZE] = true;
580 					}
581 					// conv gained a msg, may need to be moved to top/bottom
582 					if (!newConvId[conv.id] && this._vector.contains(conv)) {
583 						fields[ZmItem.F_INDEX] = true;
584 					}
585 					modifiedItems.push(conv);
586 				}
587 				AjxDebug.println(AjxDebug.NOTIFY, "ZmMailList: conv list accepted msg " + id);
588 				newMsgs.push(msg);
589 			}
590 		}
591 	} else if (this.type == ZmItem.MSG) {
592 		// add new msg to list
593 		for (var id in msgs) {
594 			var msg = msgs[id];
595 			var msgMatches =  this.search && this.search.matches(msg) && !msg.ignoreJunkTrash();
596 			AjxDebug.println(AjxDebug.NOTIFY, "ZmMailList: handling msg create " + id);
597 			if (this.getById(id)) {
598 				if (msgMatches) {
599 					var query = this.search ? this.search.query : "";
600 					var ignore = msg.ignoreJunkTrash();
601 					AjxDebug.println(AjxDebug.NOTIFY, "ZmMailList: msg does not match search '" + query + "' or was ignored (" + ignore + ")");
602 					msg.list = this; // Even though we have the msg in the list, it sometimes has its list wrong.
603 				}
604 				continue;
605 			}
606 			if (this.convId) { // MLV within CV
607 				if (msg.cid == this.convId && !this.getById(msg.id)) {
608 					msg.list = this;
609 					AjxDebug.println(AjxDebug.NOTIFY, "ZmMailList: msg list (CV) accepted msg " + id);
610 					newMsgs.push(msg);
611 				}
612 			} else { // MLV (traditional)
613 				if (msgMatches) {
614 					msg.list = this;
615 					AjxDebug.println(AjxDebug.NOTIFY, "ZmMailList: msg list (TV) accepted msg " + id);
616 					newMsgs.push(msg);
617 				}
618 			}
619 		}
620 	}
621 
622 	// sort item list in reverse so they show up in correct order when processed (oldest appears first)
623 	if (newConvs.length > 1) {
624 		ZmMailItem.sortBy = sortBy;
625 		newConvs.sort(ZmMailItem.sortCompare);
626 		newConvs.reverse();
627 	}
628 
629 	this._sortAndNotify(newConvs, sortBy, ZmEvent.E_CREATE);
630 	this._sortAndNotify(newMsgs, sortBy, ZmEvent.E_CREATE);
631 	ZmModel.notifyEach(flaggedItems, ZmEvent.E_FLAGS, {flags:[ZmItem.FLAG_UNREAD]});
632 	this._sortAndNotify(modifiedItems, sortBy, ZmEvent.E_MODIFY, {fields:fields});
633 	this._sortAndNotify(newMsgs, sortBy, ZmEvent.E_MODIFY, {fields:fields});
634 };
635 
636 /**
637 * Convenience method for adding messages to a conv on the fly. The specific use case for
638 * this is when a virtual conv becomes real. We basically add the new message(s) to the
639 * old (virtual) conv's message list.
640 *
641 * @param msgs		hash of messages to add
642 */
643 ZmMailList.prototype.addMsgs =
644 function(msgs) {
645 	var addedMsgs = [];
646 	for (var id in msgs) {
647 		var msg = msgs[id];
648 		if (msg.cid == this.convId) {
649 			this.add(msg, 0);
650 			msg.list = this;
651 			addedMsgs.push(msg);
652 		}
653 	}
654 	ZmModel.notifyEach(addedMsgs, ZmEvent.E_CREATE);
655 };
656 
657 
658 ZmMailList.prototype.removeAllItems = 
659 function() {
660 	this._vector = new AjxVector();
661 	this._idHash = {};
662 };
663 
664 
665 ZmMailList.prototype.remove = 
666 function(item, force) {
667 	// Don't really remove an item if this is a list of msgs of a conv b/c a
668 	// msg is always going to be part of a conv unless it's a hard delete!
669 	if (!this.convId || force) {
670 		ZmList.prototype.remove.call(this, item);
671 	}
672 };
673 
674 ZmMailList.prototype.clear =
675 function() {
676 	// remove listeners for this list from folder tree and tag list
677 	if (this._folderChangeListener) {
678 		var folderTree = appCtxt.getFolderTree();
679 		if (folderTree) {
680 			folderTree.removeChangeListener(this._folderChangeListener);
681 		}
682 	}
683 	if (this._tagChangeListener) {
684 		var tagTree = appCtxt.getTagTree();
685 		if (tagTree) {
686 			tagTree.removeChangeListener(this._tagChangeListener);
687 		}
688 	}
689 
690 	ZmList.prototype.clear.call(this);
691 };
692 
693 /**
694  * Gets the first msg in the list that's not in one of the given folders (if any).
695  * 
696  * @param {int}	offset	the starting point within list
697  * @param {int}	limit		the ending point within list
698  * @param {foldersToOmit}	A hash of folders to omit
699  * @return	{ZmMailMsg}		the message
700  */
701 ZmMailList.prototype.getFirstHit =
702 function(offset, limit, foldersToOmit) {
703 
704 	if (this.type !== ZmItem.MSG) {
705 		return null;
706 	}
707 
708 	var msg = null;	
709 	offset = offset || 0;
710 	limit = limit || appCtxt.get(ZmSetting.CONVERSATION_PAGE_SIZE);
711 	var numMsgs = this.size();
712 
713 	if (numMsgs > 0 && offset >= 0 && offset < numMsgs) {
714 		var end = (offset + limit > numMsgs) ? numMsgs : offset + limit;
715 		var list = this.getArray();
716 		for (var i = offset; i < end; i++) {
717 			if (!(foldersToOmit && list[i].folderId && foldersToOmit[list[i].folderId])) {
718 				msg = list[i];
719 				break;
720 			}
721 		}
722 		if (!msg) {
723 			msg = list[0];	// no qualifying messages, use first msg
724 		}
725 	}
726 	
727 	return msg;
728 };
729 
730 /**
731  * Returns the insertion point for the given item into this list. If we're not sorting by
732  * date, returns 0 (the item will be inserted at the top of the list).
733  *
734  * @param item		[ZmMailItem]	a mail item
735  * @param sortBy	[constant]		sort order
736  */
737 ZmMailList.prototype._getSortIndex =
738 function(item, sortBy) {
739 	if (!sortBy || (sortBy != ZmSearch.DATE_DESC && sortBy != ZmSearch.DATE_ASC)) {
740 		return 0;
741 	}
742 	
743 	var itemDate = parseInt(item.date);
744 	var a = this.getArray();
745 	// server always orders conv's msg list as DATE_DESC
746 	if (this.convId && sortBy == ZmSearch.DATE_ASC) {
747 		//create a temp array with reverse index and date
748 		var temp = [];
749 		for(var j = a.length - 1;j >=0;j--) {
750 			temp.push({date:a[j].date});
751 		}
752 		a = temp;
753 	}
754 	for (var i = 0; i < a.length; i++) {
755 		var date = parseInt(a[i].date);
756 		if ((sortBy == ZmSearch.DATE_DESC && (itemDate >= date)) ||
757 			(sortBy == ZmSearch.DATE_ASC && (itemDate <= date))) {
758 			return i;
759 		}
760 	}
761 	return i;
762 };
763 
764 ZmMailList.prototype._sortAndNotify =
765 function(items, sortBy, event, details) {
766 
767 	if (!(items && items.length)) { return; }
768 
769 	var itemType = items[0] && items[0].type;
770 	if ((this.type == ZmItem.MSG) && (itemType == ZmItem.CONV)) { return; }
771 
772 	details = details || {};
773 	var doSort = ((event == ZmEvent.E_CREATE) || (details.fields && details.fields[ZmItem.F_DATE]));
774 	for (var i = 0; i < items.length; i++) {
775 		var item = items[i];
776 		if (doSort) {
777 			var doAdd = (itemType == this.type);
778 			var listSortIndex = 0, viewSortIndex = 0;
779 			if (this.type == ZmItem.CONV && itemType == ZmItem.MSG) {
780 				//Bug 87861 - we still want to add the message to the conv even if the conv is not in this view. So look for it in appCtxt cache too. (case in point - it's in "sent" folder)
781 				var conv = this.getById(item.cid) || appCtxt.getById(item.cid);
782 				if (conv) {
783 					// server always orders msgs within a conv by DATE_DESC, so maintain that
784 					listSortIndex = conv.msgs._getSortIndex(item, ZmSearch.DATE_DESC);
785 					viewSortIndex = conv.msgs._getSortIndex(item, appCtxt.get(ZmSetting.CONVERSATION_ORDER));
786 					if (event == ZmEvent.E_CREATE) {
787 						conv.addMsg(item, listSortIndex);
788 					}
789 				}
790 			} else {
791 				viewSortIndex = listSortIndex = this._getSortIndex(item, sortBy);
792 			}
793 			if (event != ZmEvent.E_CREATE) {
794 				// if date changed, re-insert item into correct slot
795 				if (listSortIndex != this.indexOf(item)) {
796 					this.remove(item);
797 				} else {
798 					doAdd = false;
799 				}
800 			}
801 			if (doAdd) {
802 				this.add(item, listSortIndex);
803 			}
804 			details.sortIndex = viewSortIndex;
805 		}
806 		item._notify(event, details);
807 	}
808 };
809 
810 ZmMailList.prototype._isItemInSpecialFolder =
811 function(item) {
812 //	if (item.folderId) { //case of one message in conv, even if not loaded yet, we know the folder.
813 //		return ZmMailList._SPECIAL_FOLDERS_HASH[item.folderId];
814 //	}
815 	var msgs = item.msgs;
816 	if (!msgs) { //might not be loaded yet. In this case, tough luck - the tcon will be set as usual - based on searched folder, if set
817 		return false;
818 	}
819 	for (var i = 0; i < msgs.size(); i++) {
820 		var msg = msgs.get(i);
821 		var msgFolder = appCtxt.getById(msg.folderId);
822 		var msgFolderId = msgFolder && msgFolder.nId;
823 
824 		if (!ZmMailList._SPECIAL_FOLDERS_HASH[msgFolderId]) {
825 			return false;
826 		}
827 	}
828 	return true;
829 };
830 
831 ZmMailList.prototype._getTcon =
832 function(items, nFromFolderId) {
833 
834 	//if all items are in a special folder (draft/trash/spam/sent) - then just allow the move without any restriction
835 	var allItemsSpecial = true;
836 	for (var i = 0; i < items.length; i++) {
837 		if (!this._isItemInSpecialFolder(items[i])) {
838 			allItemsSpecial = false;
839 			break;
840 		}
841 	}
842 
843 	if (allItemsSpecial) {
844 		return "";
845 	}
846 
847 	var fromFolderId = nFromFolderId || (this.search && this.search.folderId);
848 	var	fromFolder = fromFolderId && appCtxt.getById(fromFolderId);
849 
850 	fromFolderId = fromFolder && fromFolder.nId;
851 	var tcon = [];
852 	for (i = 0; i < ZmMailList._SPECIAL_FOLDERS.length; i++) {
853 		var specialFolderId = ZmMailList._SPECIAL_FOLDERS[i];
854 		if (!fromFolder) {
855 			tcon.push(ZmFolder.TCON_CODE[specialFolderId]);
856 			continue;
857 		}
858 		// == instead of === since we compare numbers to strings and want conversion.
859 		if (fromFolderId == specialFolderId) {
860 			continue; //we're moving out of the special folder - allow  items under it
861 		}
862         var specialFolder;
863         // get folder object from qualified Ids for multi-account
864         if (appCtxt.multiAccounts) {
865             var acct  = items && items[0].getAccount && items[0].getAccount();
866             var acctId = acct ? acct.id : appCtxt.getActiveAccount().id;
867 			var fId = [acctId, ":", specialFolderId].join("");
868 			specialFolder = appCtxt.getById(fId);
869         }
870 		else {
871             specialFolder = appCtxt.getById(specialFolderId);
872         }
873 
874 		if (!fromFolder.isChildOf(specialFolder)) {
875 			//if origin folder (searched folder) not descendant of the special folder - add the tcon code - don't move items from under the special folder.
876 			tcon.push(ZmFolder.TCON_CODE[specialFolderId]);
877 		}
878 	}
879 	return (tcon.length) ?  ("-" + tcon.join("")) : "";
880 };
881 
882 // If this list is the result of a search that is constrained by the read
883 // status, and the user has marked all read in a folder, redo the search.
884 ZmMailList.prototype._folderTreeChangeListener = 
885 function(ev) {
886 	if (this.size() == 0) { return; }
887 
888 	var flag = ev.getDetail("flag");
889 	var view = appCtxt.getCurrentViewId();
890 	var ctlr = appCtxt.getCurrentController();
891 
892 	if (ev.event == ZmEvent.E_FLAGS && (flag == ZmItem.FLAG_UNREAD)) {
893 		if (this.type == ZmItem.CONV) {
894 			if ((view == ZmId.VIEW_CONVLIST) && ctlr._currentSearch.hasUnreadTerm()) {
895 				this._redoSearch(ctlr);
896 			}
897 		} else if (this.type == ZmItem.MSG) {
898 			if (view == ZmId.VIEW_TRAD && ctlr._currentSearch.hasUnreadTerm()) {
899 				this._redoSearch(ctlr);
900 			} else {
901 				var on = ev.getDetail("state");
902 				var organizer = ev.getDetail("item");
903 				var flaggedItems = [];
904 				var list = this.getArray();
905 				for (var i = 0; i < list.length; i++) {
906 					var msg = list[i];
907 					if ((organizer.type == ZmOrganizer.FOLDER && msg.folderId == organizer.id) ||
908 						(organizer.type == ZmOrganizer.TAG && msg.hasTag(organizer.id))) {
909 						msg.isUnread = on;
910 						flaggedItems.push(msg);
911 					}
912 				}
913 				ZmModel.notifyEach(flaggedItems, ZmEvent.E_FLAGS, {flags:[flag]});
914 			}
915 		}
916 	} else {
917 		ZmList.prototype._folderTreeChangeListener.call(this, ev);
918 	}
919 };
920 
921 ZmMailList.prototype._tagTreeChangeListener = 
922 function(ev) {
923 	if (this.size() == 0) return;
924 
925 	var flag = ev.getDetail("flag");
926 	if (ev.event == ZmEvent.E_FLAGS && (flag == ZmItem.FLAG_UNREAD)) {
927 		this._folderTreeChangeListener(ev);
928 	} else {
929 		ZmList.prototype._tagTreeChangeListener.call(this, ev);
930 	}
931 };
932