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  * Creates a new, empty mail list controller.
 26  * @constructor
 27  * @class
 28  * This class encapsulates controller behavior that is common to lists of mail items.
 29  * Operations such as replying and marking read/unread are supported.
 30  *
 31  * @author Conrad Damon
 32  *
 33  * @param {DwtControl}					container					the containing shell
 34  * @param {ZmApp}						mailApp						the containing application
 35  * @param {constant}					type						type of controller
 36  * @param {string}						sessionId					the session id
 37  * @param {ZmSearchResultsController}	searchResultsController		containing controller
 38  * 
 39  * @extends		ZmListController
 40  */
 41 ZmMailListController = function(container, mailApp, type, sessionId, searchResultsController) {
 42 
 43 	if (arguments.length == 0) { return; }
 44 	ZmListController.apply(this, arguments);
 45 
 46 	this._setStatics();
 47 
 48 	this._listeners[ZmOperation.SHOW_ORIG] = this._showOrigListener.bind(this);
 49 
 50 	this._listeners[ZmOperation.MARK_READ] = this._markReadListener.bind(this);
 51 	this._listeners[ZmOperation.MARK_UNREAD] = this._markUnreadListener.bind(this);
 52 	this._listeners[ZmOperation.FLAG] = this._flagListener.bind(this, true);
 53 	this._listeners[ZmOperation.UNFLAG] = this._flagListener.bind(this, false);
 54 	//fixed bug:15460 removed reply and forward menu.
 55 	if (appCtxt.get(ZmSetting.REPLY_MENU_ENABLED)) {
 56 		this._listeners[ZmOperation.REPLY] = this._replyListener.bind(this);
 57 		this._listeners[ZmOperation.REPLY_ALL] = this._replyListener.bind(this);
 58 	}
 59 
 60 	if (appCtxt.get(ZmSetting.FORWARD_MENU_ENABLED)) {
 61 		this._listeners[ZmOperation.FORWARD] = this._forwardListener.bind(this);
 62 		this._listeners[ZmOperation.FORWARD_CONV] = this._forwardConvListener.bind(this);
 63 	}
 64 	this._listeners[ZmOperation.REDIRECT] = new AjxListener(this, this._redirectListener);
 65 	this._listeners[ZmOperation.EDIT] = this._editListener.bind(this, false);
 66 	this._listeners[ZmOperation.EDIT_AS_NEW] = this._editListener.bind(this, true);
 67 	this._listeners[ZmOperation.MUTE_CONV] = this._muteConvListener.bind(this);
 68 	this._listeners[ZmOperation.UNMUTE_CONV] = this._unmuteConvListener.bind(this);
 69 
 70 	if (appCtxt.get(ZmSetting.SPAM_ENABLED)) {
 71 		this._listeners[ZmOperation.SPAM] = this._spamListener.bind(this);
 72 	}
 73 
 74 	this._listeners[ZmOperation.DETACH] = this._detachListener.bind(this);
 75 	this._inviteReplyListener = this._inviteReplyHandler.bind(this);
 76 	this._shareListener = this._shareHandler.bind(this);
 77 	this._subscribeListener = this._subscribeHandler.bind(this);
 78 
 79 	this._acceptShareListener = this._acceptShareHandler.bind(this);
 80 	this._declineShareListener = this._declineShareHandler.bind(this);
 81 
 82 	this._listeners[ZmOperation.ADD_FILTER_RULE]	= this._filterListener.bind(this, false, null);
 83 	this._listeners[ZmOperation.CREATE_APPT]		= this._createApptListener.bind(this);
 84 	this._listeners[ZmOperation.CREATE_TASK]		= this._createTaskListener.bind(this);
 85 
 86 };
 87 
 88 ZmMailListController.prototype = new ZmListController;
 89 ZmMailListController.prototype.constructor = ZmMailListController;
 90 
 91 ZmMailListController.prototype.isZmMailListController = true;
 92 ZmMailListController.prototype.toString = function() { return "ZmMailListController"; };
 93 
 94 ZmMailListController.GROUP_BY_ITEM		= {};	// item type to search for
 95 ZmMailListController.GROUP_BY_SETTING	= {};	// associated setting on server
 96 
 97 // Stuff for the View menu
 98 ZmMailListController.GROUP_BY_ICON		= {};
 99 ZmMailListController.GROUP_BY_MSG_KEY	= {};
100 ZmMailListController.GROUP_BY_SHORTCUT	= {};
101 ZmMailListController.GROUP_BY_VIEWS		= [];
102 
103 // reading pane options
104 ZmMailListController.READING_PANE_TEXT = {};
105 ZmMailListController.READING_PANE_TEXT[ZmSetting.RP_OFF]	= ZmMsg.readingPaneOff;
106 ZmMailListController.READING_PANE_TEXT[ZmSetting.RP_BOTTOM]	= ZmMsg.readingPaneAtBottom;
107 ZmMailListController.READING_PANE_TEXT[ZmSetting.RP_RIGHT]	= ZmMsg.readingPaneOnRight;
108 
109 ZmMailListController.READING_PANE_ICON = {};
110 ZmMailListController.READING_PANE_ICON[ZmSetting.RP_OFF]	= "SplitPaneOff";
111 ZmMailListController.READING_PANE_ICON[ZmSetting.RP_BOTTOM]	= "SplitPane";
112 ZmMailListController.READING_PANE_ICON[ZmSetting.RP_RIGHT]	= "SplitPaneVertical";
113 
114 // conv order options
115 ZmMailListController.CONV_ORDER_DESC	= ZmSearch.DATE_DESC;
116 ZmMailListController.CONV_ORDER_ASC		= ZmSearch.DATE_ASC;
117 
118 ZmMailListController.CONV_ORDER_TEXT = {};
119 ZmMailListController.CONV_ORDER_TEXT[ZmMailListController.CONV_ORDER_DESC]	= ZmMsg.convOrderDescending;
120 ZmMailListController.CONV_ORDER_TEXT[ZmMailListController.CONV_ORDER_ASC]	= ZmMsg.convOrderAscending;
121 
122 // convert key mapping to folder to search
123 ZmMailListController.ACTION_CODE_TO_FOLDER = {};
124 ZmMailListController.ACTION_CODE_TO_FOLDER[ZmKeyMap.GOTO_INBOX]		= ZmFolder.ID_INBOX;
125 ZmMailListController.ACTION_CODE_TO_FOLDER[ZmKeyMap.GOTO_DRAFTS]	= ZmFolder.ID_DRAFTS;
126 ZmMailListController.ACTION_CODE_TO_FOLDER[ZmKeyMap.GOTO_JUNK]		= ZmFolder.ID_SPAM;
127 ZmMailListController.ACTION_CODE_TO_FOLDER[ZmKeyMap.GOTO_SENT]		= ZmFolder.ID_SENT;
128 ZmMailListController.ACTION_CODE_TO_FOLDER[ZmKeyMap.GOTO_TRASH]		= ZmFolder.ID_TRASH;
129 
130 // convert key mapping to folder to move to
131 ZmMailListController.ACTION_CODE_TO_FOLDER_MOVE = {};
132 ZmMailListController.ACTION_CODE_TO_FOLDER_MOVE[ZmKeyMap.MOVE_TO_INBOX]	= ZmFolder.ID_INBOX;
133 ZmMailListController.ACTION_CODE_TO_FOLDER_MOVE[ZmKeyMap.MOVE_TO_TRASH]	= ZmFolder.ID_TRASH;
134 ZmMailListController.ACTION_CODE_TO_FOLDER_MOVE[ZmKeyMap.MOVE_TO_JUNK]	= ZmFolder.ID_SPAM;
135 
136 // convert key mapping to view menu item
137 ZmMailListController.ACTION_CODE_TO_MENU_ID = {};
138 ZmMailListController.ACTION_CODE_TO_MENU_ID[ZmKeyMap.READING_PANE_OFF]		= ZmSetting.RP_OFF;
139 ZmMailListController.ACTION_CODE_TO_MENU_ID[ZmKeyMap.READING_PANE_BOTTOM]	= ZmSetting.RP_BOTTOM;
140 ZmMailListController.ACTION_CODE_TO_MENU_ID[ZmKeyMap.READING_PANE_RIGHT]	= ZmSetting.RP_RIGHT;
141 
142 ZmMailListController.ACTION_CODE_WHICH = {};
143 ZmMailListController.ACTION_CODE_WHICH[ZmKeyMap.FIRST_UNREAD]	= DwtKeyMap.SELECT_FIRST;
144 ZmMailListController.ACTION_CODE_WHICH[ZmKeyMap.LAST_UNREAD]	= DwtKeyMap.SELECT_LAST;
145 ZmMailListController.ACTION_CODE_WHICH[ZmKeyMap.NEXT_UNREAD]	= DwtKeyMap.SELECT_NEXT;
146 ZmMailListController.ACTION_CODE_WHICH[ZmKeyMap.PREV_UNREAD]	= DwtKeyMap.SELECT_PREV;
147 
148 ZmMailListController.viewToTab = {};
149 
150 
151 // Public methods
152 
153 /**
154  * Handles switching views based on action from view menu.
155  *
156  * @param {constant}	view		the id of the new view
157  * @param {Boolean}	force		if <code>true</code>, always redraw view
158  */
159 ZmMailListController.prototype.switchView = function(view, force) {
160 
161 	if ((view == ZmId.VIEW_TRAD || view == ZmId.VIEW_CONVLIST) && view != this.getCurrentViewType()) {
162 		if (appCtxt.multiAccounts) {
163 			delete this._showingAccountColumn;
164 		}
165 
166 		var groupBy = ZmMailListController.GROUP_BY_SETTING[view];
167 		if (this.isSearchResults) {
168 			appCtxt.getApp(ZmApp.SEARCH).setGroupMailBy(groupBy);
169 		}
170 		else {
171 			this._app.setGroupMailBy(groupBy);
172 		}
173 
174 		var folderId = this._currentSearch && this._currentSearch.folderId;
175 		
176 		var sortBy = appCtxt.get(ZmSetting.SORTING_PREF, folderId || view);
177 		if (view == ZmId.VIEW_CONVLIST && (sortBy == ZmSearch.NAME_DESC || sortBy == ZmSearch.NAME_ASC)) {
178 			sortBy =  appCtxt.get(ZmSetting.SORTING_PREF, view); //go back to sortBy for view
179 			appCtxt.set(ZmSetting.SORTING_PREF, sortBy, folderId); //force folderId sorting
180 		}
181 		if (this._mailListView && !appCtxt.isExternalAccount()) {
182 			//clear the groups to address "from" grouping for conversation
183 			if (folderId) {
184 				var currentGroup = this._mailListView.getGroup(folderId);
185 				if (currentGroup && currentGroup.id == ZmId.GROUPBY_FROM) {
186 					this._mailListView.setGroup(ZmId.GROUPBY_NONE);
187 				}
188 			}		
189 		}
190 		
191 		this._currentSearch.clearCursor();
192 		var limit = this._listView[this._currentViewId].getLimit();
193 		var getHtml = appCtxt.get(ZmSetting.VIEW_AS_HTML);
194 		var groupByItem = (view == ZmId.VIEW_TRAD) ? ZmItem.MSG : ZmItem.CONV;
195 		var params = {
196 			types:			[groupByItem],
197 			offset:			0,
198 			limit:			limit,
199 			sortBy:			sortBy,
200 			getHtml:		getHtml,
201 			isViewSwitch:	true
202 		};
203 		appCtxt.getSearchController().redoSearch(this._currentSearch, null, params);
204 	}
205 };
206 
207 // override if reading pane is supported
208 ZmMailListController.prototype._setupReadingPaneMenu = function() {};
209 ZmMailListController.prototype._setupConvOrderMenu = function() {};
210 
211 /**
212  * Checks if the reading pane is "on".
213  * 
214  * @return	{Boolean}	<code>true</code> if the reading pane is "on"
215  */
216 ZmMailListController.prototype.isReadingPaneOn =
217 function() {
218 	return (this._getReadingPanePref() != ZmSetting.RP_OFF);
219 };
220 
221 /**
222  * Checks if the reading pane is "on" right.
223  * 
224  * @return	{Boolean}	<code>true</code> if the reading pane is "on" right.
225  */
226 ZmMailListController.prototype.isReadingPaneOnRight =
227 function() {
228 	return (this._getReadingPanePref() == ZmSetting.RP_RIGHT);
229 };
230 
231 ZmMailListController.prototype._getReadingPanePref =
232 function() {
233 	return (this._readingPaneLoc || appCtxt.get(ZmSetting.READING_PANE_LOCATION));
234 };
235 
236 ZmMailListController.prototype._setReadingPanePref =
237 function(value) {
238 	if (this.isSearchResults || appCtxt.isExternalAccount()) {
239 		this._readingPaneLoc = value;
240 	}
241 	else {
242 		appCtxt.set(ZmSetting.READING_PANE_LOCATION, value);
243 	}
244 };
245 
246 ZmMailListController.prototype.getKeyMapName =
247 function() {
248 	return ZmKeyMap.MAP_MAIL;
249 };
250 
251 // We need to stay in sync with what's allowed by _resetOperations
252 // TODO: we should just find out if an operation was enabled via _resetOperations
253 ZmMailListController.prototype.handleKeyAction =
254 function(actionCode, ev) {
255 	DBG.println(AjxDebug.DBG3, "ZmMailListController.handleKeyAction");
256 
257     var lv = this._listView[this._currentViewId];
258     var num = lv.getSelectionCount();
259 
260     var item;
261     if (num == 1 && !this.isDraftsFolder()) {
262         var sel = this._listView[this._currentViewId].getSelection();
263         if (sel && sel.length) {
264             item = sel[0];
265         }
266     }
267 
268     var folder = this._getSearchFolder();
269 	var isSyncFailures = this.isSyncFailuresFolder();
270 	var isDrafts = (item && item.isDraft && (item.type != ZmId.ITEM_CONV || item.numMsgs == 1)) || this.isDraftsFolder();
271 	var isFeed = (folder && folder.isFeed());
272     var isExternalAccount = appCtxt.isExternalAccount();
273 
274 	switch (actionCode) {
275 
276 		case ZmKeyMap.FORWARD:
277 			if (!isDrafts && !isExternalAccount) {
278 				this._doAction({action:ZmOperation.FORWARD, foldersToOmit:ZmMailApp.getFoldersToOmit()});
279 			}
280 			break;
281 
282 		case ZmKeyMap.GET_MAIL:
283 			this._checkMailListener();
284 			break;
285 
286 		case ZmKeyMap.GOTO_INBOX:
287 		case ZmKeyMap.GOTO_DRAFTS:
288 		case ZmKeyMap.GOTO_JUNK:
289 		case ZmKeyMap.GOTO_SENT:
290 		case ZmKeyMap.GOTO_TRASH:
291             if (isExternalAccount) { break; }
292 			if (actionCode == ZmKeyMap.GOTO_JUNK && !appCtxt.get(ZmSetting.SPAM_ENABLED)) { break; }
293 			this._folderSearch(ZmMailListController.ACTION_CODE_TO_FOLDER[actionCode]);
294 			break;
295 
296 		case ZmKeyMap.MOVE_TO_INBOX:
297 		case ZmKeyMap.MOVE_TO_TRASH:
298 		case ZmKeyMap.MOVE_TO_JUNK:
299 			if (isSyncFailures || isExternalAccount) { break; }
300 			if (actionCode == ZmKeyMap.MOVE_TO_JUNK && !appCtxt.get(ZmSetting.SPAM_ENABLED)) { break; }
301 			if (num && !(isDrafts && actionCode != ZmKeyMap.MOVE_TO_TRASH)) {
302 			 	var folderId = ZmMailListController.ACTION_CODE_TO_FOLDER_MOVE[actionCode];
303 				folder = appCtxt.getById(folderId);
304 				var items = lv.getSelection();
305 				this._doMove(items, folder);
306 			}
307 			break;
308 
309 		case ZmKeyMap.REPLY:
310 		case ZmKeyMap.REPLY_ALL:
311 			if (!isDrafts && !isExternalAccount && (num == 1) && !isSyncFailures && !isFeed) {
312 				this._doAction({action:ZmMailListController.ACTION_CODE_TO_OP[actionCode], foldersToOmit:ZmMailApp.getFoldersToOmit()});
313 			}
314 			break;
315 
316 		case ZmKeyMap.SELECT_ALL:
317 			lv.selectAll(true);
318 			this._resetToolbarOperations();
319 			break;
320 	
321 		case ZmKeyMap.SPAM:
322             if (isExternalAccount) { break; }
323 			if (num && !isDrafts && !isExternalAccount && !isSyncFailures && appCtxt.get(ZmSetting.SPAM_ENABLED) && (folder && !folder.isReadOnly())) {
324 				this._spamListener();
325 			}
326 			break;
327 
328 		case ZmKeyMap.MUTE_UNMUTE_CONV:
329             // Mute/Unmute Code removed for IM will be added for JP
330 			break;
331 
332         case ZmKeyMap.MARK_READ:
333 			if (this._isPermissionDenied(folder)) {
334 				break;
335 			}
336 			this._markReadListener();
337 			break;
338 
339 		case ZmKeyMap.MARK_UNREAD:
340 			if (this._isPermissionDenied(folder)) {
341 				break;
342 			}
343 			this._markUnreadListener();
344 			break;
345 
346 		case ZmKeyMap.FLAG:
347 			if (this._isPermissionDenied(folder)) {
348 				break;
349 			}
350 			this._doFlag(this.getItems());
351 			break;
352 
353 		case ZmKeyMap.VIEW_BY_CONV:
354 			if (!isSyncFailures && appCtxt.get(ZmSetting.CONVERSATIONS_ENABLED)) {
355 				this.switchView(ZmId.VIEW_CONVLIST);
356 			}
357 			break;
358 
359 		case ZmKeyMap.VIEW_BY_MSG:
360 			if (!isSyncFailures) {
361 				this.switchView(ZmId.VIEW_TRAD);
362 			}
363 			break;
364 
365 		case ZmKeyMap.READING_PANE_BOTTOM:
366 		case ZmKeyMap.READING_PANE_RIGHT:
367 		case ZmKeyMap.READING_PANE_OFF:
368 			var menuId = ZmMailListController.ACTION_CODE_TO_MENU_ID[actionCode];
369 			this.switchView(menuId, true);
370 			this._updateViewMenu(menuId, this._readingPaneViewMenu);
371 			break;
372 
373 		case ZmKeyMap.SHOW_FRAGMENT:
374 			if (num == 1) {
375 				var item = lv.getSelection()[0];
376                 if (!item) { break; }
377                 var id = lv._getFieldId(item, ZmItem.F_SUBJECT);
378                 var subjectField = document.getElementById(id);
379                 if (subjectField) {
380                     var loc = Dwt.getLocation(subjectField);
381 					var frag;
382 					// TODO: refactor / clean up
383 					if ((item.type == ZmItem.MSG && item.isInvite() && item.needsRsvp()) ||
384                         (item.type == ZmId.ITEM_CONV && this.getMsg() && this.getMsg().isInvite() && this.getMsg().needsRsvp()))
385                     {
386 						frag = item.invite ? item.invite.getToolTip() : this.getMsg().invite.getToolTip();
387 					} else {
388 						frag = item.fragment ? item.fragment : ZmMsg.fragmentIsEmpty;
389 						if (frag != "") { lv.setToolTipContent(AjxStringUtil.htmlEncode(frag), true); }
390 					}
391 					var tooltip = this._shell.getToolTip();
392 					tooltip.popdown();
393 					if (frag != "") {
394 						tooltip.setContent(frag);
395 						tooltip.popup(loc.x, loc.y);
396 					}
397 				}
398 			}
399 			break;
400 
401 		case ZmKeyMap.NEXT_UNREAD:
402 		case ZmKeyMap.PREV_UNREAD:
403 			this.lastListAction = actionCode;
404 
405 		case ZmKeyMap.FIRST_UNREAD:
406 		case ZmKeyMap.LAST_UNREAD:
407 			var unreadItem = this._getUnreadItem(ZmMailListController.ACTION_CODE_WHICH[actionCode]);
408 			if (unreadItem) {
409 				this._selectItem(lv, unreadItem);
410 			}
411 			break;
412 	
413 		default:
414 			return ZmListController.prototype.handleKeyAction.apply(this, arguments);
415 	}
416 	return true;
417 };
418 
419 ZmMailListController.prototype._isPermissionDenied =
420 function(folder) {
421 	var isExternalAccount = appCtxt.isExternalAccount();
422 
423 	if (isExternalAccount || (folder && folder.isReadOnly())) {
424 		appCtxt.setStatusMsg(ZmMsg.errorPermission);
425 		return true;
426 	}
427 	return false;
428 };
429 
430 ZmMailListController.prototype._selectItem =
431 function(listView, item) {
432 	listView._unmarkKbAnchorElement(true);
433 	listView.setSelection(item);
434 	var el = listView._getElFromItem(item);
435 	if (el) {
436 		listView._scrollList(el);
437 	}
438 };
439 
440 ZmMailListController.prototype.mapSupported =
441 function(map) {
442 	return (map == "list");
443 };
444 
445 /**
446  * Sends the read receipt.
447  * 
448  * @param	{ZmMailMsg}		msg			the message
449  */
450 ZmMailListController.prototype.sendReadReceipt =
451 function(msg) {
452 
453 	if (!appCtxt.get(ZmSetting.MAIL_READ_RECEIPT_ENABLED) || msg.readReceiptSent || msg.isSent) {
454 		return;
455 	}
456 
457 	var rrPref = appCtxt.get(ZmSetting.MAIL_SEND_READ_RECEIPTS);
458 
459 	if (rrPref == ZmMailApp.SEND_RECEIPT_PROMPT) {
460 		var dlg = appCtxt.getYesNoMsgDialog();
461 		dlg.registerCallback(DwtDialog.YES_BUTTON, this._sendReadReceipt, this, [msg, dlg]);
462 		dlg.registerCallback(DwtDialog.NO_BUTTON, this._sendReadReceiptNotified, this, [msg, dlg]);
463 		dlg.setMessage(ZmMsg.readReceiptSend, DwtMessageDialog.WARNING_STYLE);
464 		dlg.popup();
465 	} else if (rrPref == ZmMailApp.SEND_RECEIPT_ALWAYS) {
466 		msg.readReceiptSent = true;
467         this._sendReadReceipt(msg);
468 	}
469 };
470 
471 ZmMailListController.prototype._sendReadReceipt =
472 function(msg, dlg) {
473 	if (dlg) {
474 		dlg.popdown();
475 	}
476 	msg.sendReadReceipt(this._handleSendReadReceipt.bind(this, msg));
477 };
478 
479 ZmMailListController.prototype._handleSendReadReceipt =
480 function(msg) {
481 	appCtxt.setStatusMsg(ZmMsg.readReceiptSent);
482     this._sendReadReceiptNotified(msg);
483 };
484 
485 ZmMailListController.prototype._sendReadReceiptNotified =
486 function(msg, dlg) {
487 	var callback = dlg ? (new AjxCallback(dlg, dlg.popdown)) : null;
488 	var flags = msg.setFlag(ZmItem.FLAG_READ_RECEIPT_SENT, true);
489 	msg.list.flagItems({items:[msg], op:"update", value:flags, callback:callback});
490 };
491 
492 ZmMailListController.prototype._updateViewMenu = function(id, menu) {
493 
494 	var viewBtn = this.getCurrentToolbar().getButton(ZmOperation.VIEW_MENU);
495 	menu = menu || (viewBtn && viewBtn.getMenu());
496 	if (menu) {
497 		var mi = menu.getItemById(ZmOperation.MENUITEM_ID, id);
498 		if (mi) {
499 			mi.setChecked(true, true);
500 		}
501 	}
502 
503 	// Create "Display" submenu here since it's only needed for multi-column
504 	if (!this._colHeaderViewMenu && this._mailListView.isMultiColumn()) {
505 		this._colHeaderViewMenu = this._setupColHeaderViewMenu(this._currentView, this._viewMenu);
506 	}
507 
508 	if (this._colHeaderMenuItem && (id === ZmSetting.RP_OFF || id === ZmSetting.RP_BOTTOM || id === ZmSetting.RP_RIGHT)) {
509 		this._colHeaderMenuItem.setVisible(this._mailListView.isMultiColumn());
510 	}
511 };
512 
513 // Private and protected methods
514 
515 ZmMailListController.prototype._initialize =
516 function(view) {
517 	this._setActiveSearch(view);
518 
519 	// call base class
520 	ZmListController.prototype._initialize.call(this, view);
521 };
522 
523 ZmMailListController.prototype._initializeParticipantActionMenu =
524 function() {
525 	if (!this._participantActionMenu) {
526 		var menuItems = this._participantOps();
527 		menuItems.push(ZmOperation.SEP);
528 		var ops = this._getActionMenuOps();
529 		if (ops && ops.length) {
530 			menuItems = menuItems.concat(ops);
531 		}
532 
533 		this._participantActionMenu = new ZmActionMenu({parent:this._shell, menuItems:menuItems, controller:this,
534 														context:this._currentViewId, menuType:ZmId.MENU_PARTICIPANT});
535         if (appCtxt.get(ZmSetting.SEARCH_ENABLED)) {
536             this._setSearchMenu(this._participantActionMenu);
537         }
538 		this._addMenuListeners(this._participantActionMenu);
539 		this._participantActionMenu.addPopdownListener(this._menuPopdownListener);
540 		this._setupTagMenu(this._participantActionMenu);
541 
542 		//notify Zimlet before showing 
543 		appCtxt.notifyZimlets("onParticipantActionMenuInitialized", [this, this._participantActionMenu]);
544 	}
545 	return this._participantActionMenu;
546 };
547 
548 ZmMailListController.prototype._initializeDraftsActionMenu =
549 function() {
550     if (!this._draftsActionMenu) {
551 		var menuItems = [
552 			ZmOperation.EDIT,
553 			ZmOperation.SEP,
554 			ZmOperation.TAG_MENU, ZmOperation.DELETE, ZmOperation.PRINT,
555 			ZmOperation.SEP,
556 			ZmOperation.SHOW_ORIG
557 		];
558 		this._draftsActionMenu = new ZmActionMenu({parent:this._shell, menuItems:menuItems,
559 												   context:this._currentViewId, menuType:ZmId.MENU_DRAFTS});
560         if (appCtxt.get(ZmSetting.SEARCH_ENABLED)) {
561             this._setSearchMenu(this._draftsActionMenu);
562         }
563 		this._addMenuListeners(this._draftsActionMenu);
564 		this._draftsActionMenu.addPopdownListener(this._menuPopdownListener);
565 		this._setupTagMenu(this._draftsActionMenu);
566 		appCtxt.notifyZimlets("onDraftsActionMenuInitialized", [this, this._draftsActionMenu]);
567 	}
568 };
569 
570 ZmMailListController.prototype._setDraftSearchMenu =
571 function(address, item, ev){
572    if (address && appCtxt.get(ZmSetting.SEARCH_ENABLED) && (ev.field == ZmItem.F_PARTICIPANT || ev.field == ZmItem.F_FROM)){
573         if (!this._draftsActionMenu.getOp(ZmOperation.SEARCH_MENU)) {
574             ZmOperation.addOperation(this._draftsActionMenu, ZmOperation.SEARCH_MENU, [ZmOperation.SEARCH_MENU, ZmOperation.SEP], 0);
575             this._setSearchMenu(this._draftsActionMenu);
576         }
577         if (item && (item.getAddresses(AjxEmailAddress.TO).getArray().length + item.getAddresses(AjxEmailAddress.CC).getArray().length) > 1){
578             ZmOperation.setOperation(this._draftsActionMenu.getSearchMenu(), ZmOperation.SEARCH_TO, ZmOperation.SEARCH_TO, ZmMsg.findEmailToRecipients);
579             ZmOperation.setOperation(this._draftsActionMenu.getSearchMenu(), ZmOperation.SEARCH, ZmOperation.SEARCH, ZmMsg.findEmailFromRecipients);
580         }
581         else{
582             ZmOperation.setOperation(this._draftsActionMenu.getSearchMenu(), ZmOperation.SEARCH_TO, ZmOperation.SEARCH_TO, ZmMsg.findEmailToRecipient);
583             ZmOperation.setOperation(this._draftsActionMenu.getSearchMenu(), ZmOperation.SEARCH, ZmOperation.SEARCH, ZmMsg.findEmailFromRecipient);
584         }
585      }
586      else if (this._draftsActionMenu.getOp(ZmOperation.SEARCH_MENU)) {
587             this._draftsActionMenu = null;
588             this._initializeDraftsActionMenu();
589      }
590 };
591 
592 ZmMailListController.prototype._getToolBarOps =
593 function() {
594 	var list = [];
595 	list.push(ZmOperation.SEP);
596 	list = list.concat(this._msgOps());
597 	list.push(ZmOperation.SEP);
598 	list = list.concat(this._deleteOps());
599 	list.push(ZmOperation.SEP);
600 	list.push(ZmOperation.MOVE_MENU);
601 	list.push(ZmOperation.TAG_MENU);
602 	return list;
603 };
604 
605 ZmMailListController.prototype._getRightSideToolBarOps =
606 function() {
607 	if (appCtxt.isChildWindow) {
608 		return [];
609 	}
610 	return [ZmOperation.VIEW_MENU];
611 };
612 
613 
614 ZmMailListController.prototype._showDetachInSecondary =
615 function() {
616 	return true;
617 };
618 
619 ZmMailListController.prototype._getSecondaryToolBarOps =
620 function() {
621 
622 	var list = [],
623 		viewType = this.getCurrentViewType();
624 
625 	list.push(ZmOperation.PRINT);
626 	list.push(ZmOperation.SEP);
627 	list = list.concat(this._flagOps());
628 	list.push(ZmOperation.SEP);
629     list.push(ZmOperation.REDIRECT);
630     list.push(ZmOperation.EDIT_AS_NEW);
631     list.push(ZmOperation.SEP);
632 	list = list.concat(this._createOps());
633 	list.push(ZmOperation.SEP);
634 	list = list.concat(this._otherOps(true));
635 	if (viewType === ZmId.VIEW_TRAD) {
636 		list.push(ZmOperation.SHOW_CONV);
637 	}
638 
639 	return list;
640 };
641 
642 
643 ZmMailListController.prototype._initializeToolBar =
644 function(view, className) {
645 
646 	if (!this._toolbar[view]) {
647 		ZmListController.prototype._initializeToolBar.call(this, view, className);
648 		this._createViewMenu(view);
649 		this._setReplyText(this._toolbar[view]);
650 //		this._toolbar[view].addOp(ZmOperation.FILLER);
651 		if (!appCtxt.isChildWindow) {
652 			this._initializeNavToolBar(view);
653 		}
654 	}
655 
656 	if (!appCtxt.isChildWindow) {
657 		this._setupViewMenu(view);
658 	}
659 	this._setupDeleteButton(this._toolbar[view]);
660 	if (appCtxt.get(ZmSetting.SPAM_ENABLED)) {
661 		this._setupSpamButton(this._toolbar[view]);
662 	}
663     this._setupPrintButton(this._toolbar[view]);
664 };
665 
666 ZmMailListController.prototype._getNumTotal =
667 function(){
668 	// Yuck, remove "of Total" from Nav toolbar at lower resolutions
669 	if (AjxEnv.is1024x768orLower) {
670 		 return null;
671 	}
672 	return ZmListController.prototype._getNumTotal.call(this);
673 };
674 
675 ZmMailListController.prototype._initializeActionMenu =
676 function() {
677 	var isInitialized = (this._actionMenu != null);
678 	ZmListController.prototype._initializeActionMenu.call(this);
679 
680 	if (this._actionMenu) {
681 		this._setupSpamButton(this._actionMenu);
682 	}
683 	//notify Zimlet before showing
684 	appCtxt.notifyZimlets("onActionMenuInitialized", [this, this._actionMenu]);
685 };
686 
687 // Groups of mail-related operations
688 
689 ZmMailListController.prototype._flagOps =
690 function() {
691 	return [ZmOperation.MARK_READ, ZmOperation.MARK_UNREAD, ZmOperation.FLAG, ZmOperation.UNFLAG];
692 };
693 
694 ZmMailListController.prototype._msgOps =
695 function() {
696 	var list = [];
697 
698 	list.push(ZmOperation.EDIT); // hidden except for Drafts
699 
700 	if (appCtxt.get(ZmSetting.REPLY_MENU_ENABLED)) {
701 		list.push(ZmOperation.REPLY, ZmOperation.REPLY_ALL);
702 	}
703 
704 	if (appCtxt.get(ZmSetting.FORWARD_MENU_ENABLED)) {
705 		list.push(ZmOperation.FORWARD);
706 	}
707 	return list;
708 };
709 
710 ZmMailListController.prototype._deleteOps =
711 function() {
712 	return [this.getDeleteOperation(), ZmOperation.SPAM];
713 };
714 
715 ZmMailListController.prototype._createOps =
716 function() {
717 	var list = [];
718 	if (appCtxt.get(ZmSetting.FILTERS_ENABLED)) {
719 		list.push(ZmOperation.ADD_FILTER_RULE);
720 	}
721 	if (appCtxt.get(ZmSetting.CALENDAR_ENABLED)) {
722 		list.push(ZmOperation.CREATE_APPT);
723 	}
724 	if (appCtxt.get(ZmSetting.TASKS_ENABLED)) {
725 		list.push(ZmOperation.CREATE_TASK);
726 	}
727 	//list.push(ZmOperation.QUICK_COMMANDS);
728 	return list;
729 };
730 
731 ZmMailListController.prototype._otherOps =
732 function(isSecondary) {
733 	var list = [];
734 	if (!appCtxt.isChildWindow && (!isSecondary || this._showDetachInSecondary()) && appCtxt.get(ZmSetting.DETACH_MAILVIEW_ENABLED) && !appCtxt.isExternalAccount()) {
735 		list.push(ZmOperation.DETACH);
736 	}
737 	list.push(ZmOperation.SHOW_ORIG);
738 	return list;
739 };
740 
741 
742 
743 ZmMailListController.prototype.getDeleteOperation =
744 function() {
745 	return ZmOperation.DELETE;
746 };
747 
748 ZmMailListController.prototype._setActiveSearch =
749 function(view) {
750 	// save info. returned by search result
751 	if (this._activeSearch) {
752 		if (this._list) {
753 			this._list.setHasMore(this._activeSearch.getAttribute("more"));
754 		}
755 		if (this._listView[view]) {
756 			this._listView[view].offset = parseInt(this._activeSearch.getAttribute("offset"));
757 		}
758 	}
759 };
760 
761 
762 /**
763  * checks whether some of the selected messages are read and unread. returns it as a 2 flag object with "hasRead" and "hasUnread" attributes.
764  *
765  * @private
766  */
767 ZmMailListController.prototype._getReadStatus =
768 function() {
769 
770 	var status = {hasRead : false, hasUnread : false}
771 
772 	// dont bother checking for read/unread state for read-only folders
773 	var folder = this._getSearchFolder();
774 	if (folder && folder.isReadOnly()) {
775 		return status;
776 	}
777 
778 	var items = this.getItems();
779 
780 	for (var i = 0; i < items.length; i++) {
781 		var item = items[i];
782 		// TODO: refactor / clean up
783 		if (item.type == ZmItem.MSG) {
784 			status[item.isUnread ? "hasUnread" : "hasRead"] = true;
785 			status[item.isFlagged ? "hasFlagged" : "hasUnflagged"] = true;
786 		}
787 		else if (item.type == ZmItem.CONV) {
788 			status.hasUnread = status.hasUnread || item.hasFlag(ZmItem.FLAG_UNREAD, true);
789 			status.hasRead = status.hasRead || item.hasFlag(ZmItem.FLAG_UNREAD, false);
790 			status.hasUnflagged = status.hasUnflagged || item.hasFlag(ZmItem.FLAG_FLAGGED, false);
791 			status.hasFlagged = status.hasFlagged || item.hasFlag(ZmItem.FLAG_FLAGGED, true);
792 		}
793 		if (status.hasUnread && status.hasRead) {
794 			break;
795 		}
796 	}
797 
798 	return status;
799 };
800 
801 
802 ZmMailListController.prototype._getConvMuteStatus =
803 function() {
804 	var items = this.getItems();
805     var status = {
806                     hasMuteConv: false,
807                     hasUnmuteConv: false
808                 },
809         item,
810         i;
811     for (i=0; i<items.length; i++) {
812         item = items[i];
813         if (item.isMute) {
814             status.hasMuteConv = true;
815         }
816         else {
817             status.hasUnmuteConv = true;
818         }
819     }
820     return status;
821 };
822 
823 /**
824  * Dynamically enable/disable the mark read/unread menu items.
825  *
826  * @private
827  */
828 ZmMailListController.prototype._enableReadUnreadToolbarActions =
829 function() {
830 	var menu = this.getCurrentToolbar().getActionsMenu();
831 	this._enableFlags(menu);
832 };
833 
834 ZmMailListController.prototype._enableMuteUnmuteToolbarActions =
835 function() {
836 	var menu = this.getCurrentToolbar().getActionsMenu();
837 	this._enableMuteUnmute(menu);
838 };
839 
840 ZmMailListController.prototype._actionsButtonListener =
841 function(ev) {
842 	this._enableReadUnreadToolbarActions();
843 	this._enableMuteUnmuteToolbarActions();
844 	ZmBaseController.prototype._actionsButtonListener.call(this, ev);
845 };
846 
847 // List listeners
848 
849 ZmMailListController.prototype._listSelectionListener =
850 function(ev) {
851 	// offline: when opening a message in Outbox, move it to the appropriate
852 	// account's Drafts folder first
853 	var search = appCtxt.getCurrentSearch();
854 	if (appCtxt.isOffline &&
855 		ev.detail == DwtListView.ITEM_DBL_CLICKED &&
856 		ev.item && ev.item.isDraft &&
857 		search && search.folderId == ZmFolder.ID_OUTBOX)
858 	{
859 		var account = ev.item.account || ZmOrganizer.parseId(ev.item.id).account;
860 		var folder = appCtxt.getById(ZmOrganizer.getSystemId(ZmFolder.ID_DRAFTS, account));
861 		this._list.moveItems({items:[ev.item], folder:folder});
862 		return false;
863 	}
864 	var folderId = ev.item.folderId || (search && search.folderId);
865 	var folder = folderId && appCtxt.getById(folderId);
866 
867 	if (ev.field === ZmItem.F_FLAG && this._isPermissionDenied(folder)) {
868 		return true;
869 	}
870 	if (ev.field === ZmItem.F_READ) {
871 		if (!this._isPermissionDenied(folder)) {
872 			this._doMarkRead([ev.item], ev.item.isUnread);
873 		}
874 		return true;
875 	}
876 	else {
877 		return ZmListController.prototype._listSelectionListener.apply(this, arguments);
878 	}
879 };
880 
881 // Based on context, enable read/unread operation, add/edit contact.
882 ZmMailListController.prototype._listActionListener =
883 function(ev) {
884 
885 	ZmListController.prototype._listActionListener.call(this, ev);
886 
887 	var items = this._listView[this._currentViewId].getSelection();
888 	var folder = this._getSearchFolder();
889 
890 	// bug fix #3602
891 	var address = (appCtxt.get(ZmSetting.CONTACTS_ENABLED) && ev.field == ZmItem.F_PARTICIPANT)
892 		? ev.detail
893 		: ((ev.item && ev.item.isZmMailMsg) ? ev.item.getAddress(AjxEmailAddress.FROM) : null);
894 
895 	var email = address && address.getAddress();
896 
897 	var item = (items && items.length == 1) ? items[0] : null;
898 	if (this.isDraftsFolder() || (item && item.isDraft && item.type != ZmId.ITEM_CONV)) { //note that we never treat a conversation as a draft for actions. See also bug 64494
899 		// show drafts menu
900 		this._initializeDraftsActionMenu();
901         this._setDraftSearchMenu(address, item, ev);
902         if (address)
903             this._actionEv.address = address;
904 		this._setTagMenu(this._draftsActionMenu);
905         this._resetOperations(this._draftsActionMenu, items.length);
906 		appCtxt.notifyZimlets("onMailActionMenuResetOperations", [this, this._draftsActionMenu]);
907 		this._draftsActionMenu.popup(0, ev.docX, ev.docY);
908 	}
909 	else if (!appCtxt.isExternalAccount() && email && items.length == 1 &&
910 			(appCtxt.get(ZmSetting.CONTACTS_ENABLED) && (ev.field == ZmItem.F_PARTICIPANT || ev.field == ZmItem.F_FROM)))
911 	{
912 		// show participant menu
913 		this._initializeParticipantActionMenu();
914 		this._setTagMenu(this._participantActionMenu);
915 		this._actionEv.address = address;
916 		this._setupSpamButton(this._participantActionMenu);
917 		this._resetOperations(this._participantActionMenu, items.length);
918 		appCtxt.notifyZimlets("onMailActionMenuResetOperations", [this, this._participantActionMenu]);
919 		this._enableFlags(this._participantActionMenu);
920 		this._enableMuteUnmute(this._participantActionMenu);
921 		var imItem = this._participantActionMenu.getOp(ZmOperation.IM);
922 		var contactsApp = appCtxt.getApp(ZmApp.CONTACTS);
923 		if (contactsApp) {
924 			this._loadContactForMenu(this._participantActionMenu, address, ev, imItem);
925 		}
926 		else if (imItem) {
927 			// since contacts app is disabled, we won't be making a server call
928 			ZmImApp.updateImMenuItemByAddress(imItem, address, true);
929 			this._participantActionMenu.popup(0, ev.docX, ev.docY);
930 		}
931 	}
932     else if (this.isOutboxFolder()) {
933         // show drafts menu
934         //this._initializeOutboxsActionMenu();
935     } else {
936 		var actionMenu = this.getActionMenu();
937 		this._setupSpamButton(actionMenu);
938 		this._enableFlags(actionMenu);
939 		this._enableMuteUnmute(actionMenu);
940 		appCtxt.notifyZimlets("onMailActionMenuResetOperations", [this, actionMenu]);
941 		actionMenu.popup(0, ev.docX, ev.docY);
942 		if (ev.ersatz) {
943 			// menu popped up via keyboard nav
944 			actionMenu.setSelectedItem(0);
945 		}
946 	}
947 
948     if (!folder) {
949         //might have come from searching on sent items and want to stay in search sent view (i.e. recipient instead of sender)
950         folder = this._getActiveSearchFolder();
951     }
952 
953     if (folder && (folder.nId == ZmFolder.ID_SENT  &&
954                   (this._participantActionMenu && this._participantActionMenu.getOp(ZmOperation.SEARCH_MENU)))) {
955         if (item && (item.getAddresses(AjxEmailAddress.TO).getArray().length + item.getAddresses(AjxEmailAddress.CC).getArray().length) > 1){
956             ZmOperation.setOperation(this._participantActionMenu.getSearchMenu(), ZmOperation.SEARCH_TO, ZmOperation.SEARCH_TO, ZmMsg.findEmailToRecipients);
957             ZmOperation.setOperation(this._participantActionMenu.getSearchMenu(), ZmOperation.SEARCH, ZmOperation.SEARCH, ZmMsg.findEmailFromRecipients);
958         }
959         else{
960             ZmOperation.setOperation(this._participantActionMenu.getSearchMenu(), ZmOperation.SEARCH_TO, ZmOperation.SEARCH_TO, ZmMsg.findEmailToRecipient);
961             ZmOperation.setOperation(this._participantActionMenu.getSearchMenu(), ZmOperation.SEARCH, ZmOperation.SEARCH, ZmMsg.findEmailFromRecipient);
962         }
963     }
964     else if (this._participantActionMenu && this._participantActionMenu.getOp(ZmOperation.SEARCH_MENU)) {
965         ZmOperation.setOperation(this._participantActionMenu.getSearchMenu(), ZmOperation.SEARCH_TO, ZmOperation.SEARCH_TO, ZmMsg.findEmailToSender);
966         ZmOperation.setOperation(this._participantActionMenu.getSearchMenu(), ZmOperation.SEARCH, ZmOperation.SEARCH, ZmMsg.findEmailFromSender);
967     }
968 };
969 
970 // Operation listeners
971 
972 ZmMailListController.prototype._markReadListener =
973 function(ev) {
974 	var callback = this._getMarkReadCallback();
975 	this._doMarkRead(this._listView[this._currentViewId].getSelection(), true, callback);
976 };
977 
978 ZmMailListController.prototype._showOrigListener =
979 function() {
980 	var msg = this.getMsg();
981 	if (!msg) { return; }
982 
983 	setTimeout(this._showMsgSource.bind(this, msg), 100); // Other listeners are focusing the main window, so delay the window opening for just a bit
984 };
985 
986 ZmMailListController.prototype._showMsgSource =
987 function(msg) {
988 	var msgFetchUrl = appCtxt.get(ZmSetting.CSFE_MSG_FETCHER_URI) + "&view=text&id=" + msg.id + (msg.partId ? "&part=" + msg.partId : "");
989 
990 	// create a new window w/ generated msg based on msg id
991 	window.open(msgFetchUrl, "_blank", "menubar=yes,resizable=yes,scrollbars=yes");
992 };
993 
994 
995 /**
996  * Per bug #7257, read receipt must be sent if user explicitly marks a message
997  * read under the following conditions:
998  *
999  * 0. read receipt is requested, user agrees to send it
1000  * 1. reading pane is on
1001  * 2. mark as read preference is set to "never"
1002  * 3. the message currently being read in the reading pane is in the list of
1003  *    convs/msgs selected for mark as read
1004  *
1005  * If all these conditions are met, a callback to run sendReadReceipt() is returned.
1006  * 
1007  * @private
1008  */
1009 ZmMailListController.prototype._getMarkReadCallback =
1010 function() {
1011 	var view = this._listView[this._currentViewId];
1012 	var items = view.getSelection();
1013 
1014 	if (this.isReadingPaneOn() && appCtxt.get(ZmSetting.MARK_MSG_READ) == -1) {
1015 		// check if current message being read is the message in the selection list
1016 		var msg = view.parent.getItemView && view.parent.getItemView().getItem();
1017 		if (msg && msg.readReceiptRequested) {
1018 			for (var i = 0; i < items.length; i++) {
1019 				var item = items[i];
1020 				var itemId = (item.id < 0) ? (item.id * (-1)) : item.id;
1021 				if (itemId == msg.id) {
1022 					return this.sendReadReceipt.bind(this, msg);
1023 				}
1024 			}
1025 		}
1026 	}
1027 	return null;
1028 };
1029 
1030 ZmMailListController.prototype._markUnreadListener =
1031 function(ev) {
1032 
1033 	appCtxt.killMarkReadTimer();
1034 
1035 	this._doMarkRead(this._listView[this._currentViewId].getSelection(), false);
1036 };
1037 
1038 /**
1039  * flags or unflags (based on the status of the first item. See doFlag)
1040  * @param ev
1041  * @private
1042  */
1043 ZmMailListController.prototype._flagListener =
1044 function(on) {
1045 	this._doFlag(this._listView[this._currentViewId].getSelection(), on);
1046 };
1047 
1048 
1049 ZmMailListController.prototype._replyListener =
1050 function(ev) {
1051 	var action = ev.item.getData(ZmOperation.KEY_ID);
1052 	if (!action || action == ZmOperation.REPLY_MENU) {
1053 		action = ZmOperation.REPLY;
1054 	}
1055 
1056 	this._doAction({ev: ev, action: action, foldersToOmit: ZmMailApp.getReplyFoldersToOmit()});
1057 };
1058 
1059 ZmMailListController.prototype._forwardListener =
1060 function(ev) {
1061 	var action = ev.item.getData(ZmOperation.KEY_ID);
1062 	this._doAction({ev: ev, action: action, foldersToOmit: ZmMailApp.getReplyFoldersToOmit()});
1063 };
1064 
1065 ZmMailListController.prototype._forwardConvListener = function(ev) {
1066 	this._doAction({ev: ev, action: ZmOperation.FORWARD_CONV, foldersToOmit: ZmMailApp.getReplyFoldersToOmit()});
1067 };
1068 
1069 // This method may be called with a null ev parameter
1070 ZmMailListController.prototype._doAction =
1071 function(params) {
1072 
1073 	// get msg w/ addrs to select identity from - don't load it yet (no callback)
1074 	// for special handling of multiple forwarded messages, see _handleResponseDoAction
1075 	var msg = params.msg || this.getMsg(params);
1076 	if (!msg) {
1077 		return;
1078 	}
1079 
1080 	// use resolved msg to figure out identity/persona to use for compose
1081 	var collection = appCtxt.getIdentityCollection();
1082 	var identity = collection.selectIdentity(msg);
1083 
1084 	var action = params.action;
1085 	if (!action || action == ZmOperation.FORWARD_MENU || action == ZmOperation.FORWARD)	{
1086 		params.origAction = action;
1087 		action = params.action = (appCtxt.get(ZmSetting.FORWARD_INCLUDE_ORIG) == ZmSetting.INC_ATTACH)
1088 			? ZmOperation.FORWARD_ATT : ZmOperation.FORWARD_INLINE;
1089 
1090 		if (msg.isInvite()) {
1091 			action = params.action = ZmOperation.FORWARD_ATT;
1092 		}
1093 	}
1094 	if (action === ZmOperation.FORWARD_CONV) {
1095 		params.origAction = action;
1096 		// need to remember conv since a single right-clicked item has its selection cleared when
1097 		// the action menu is popped down during the request to load the conv
1098 		var selection = this.getSelection();
1099 		params.conv = selection && selection.length === 1 ? selection[0] : null;
1100 		action = params.action = ZmOperation.FORWARD_ATT;
1101 	}
1102 
1103 	// if html compose is allowed and if opening draft always request html
1104 	//   otherwise check if user prefers html or
1105 	//   msg hasn't been loaded yet and user prefers format of orig msg
1106 	var htmlEnabled = appCtxt.get(ZmSetting.HTML_COMPOSE_ENABLED);
1107 	var prefersHtml = (appCtxt.get(ZmSetting.COMPOSE_AS_FORMAT) == ZmSetting.COMPOSE_HTML);
1108 	var sameFormat = appCtxt.get(ZmSetting.COMPOSE_SAME_FORMAT);
1109 	params.getHtml = (htmlEnabled && (action == ZmOperation.DRAFT || (prefersHtml || (!msg._loaded && sameFormat))));
1110 	if (action == ZmOperation.DRAFT) {
1111 		params.listController = this;
1112 		//always reload the draft msg except offline created msg
1113         if (!msg.isOfflineCreated) {
1114             params.forceLoad = true;
1115         }
1116 	}
1117 
1118 	// bug: 38928 - if user viewed entire truncated message, fetch the whole
1119 	// thing when replying/forwarding
1120 	if (action != ZmOperation.NEW_MESSAGE && action != ZmOperation.DRAFT && msg.viewEntireMessage) {
1121 		params.noTruncate = true;
1122 		params.forceLoad = true;
1123 	}
1124 
1125 	if (action == ZmOperation.DRAFT || action == ZmOperation.FORWARD_INLINE ||
1126             action == ZmOperation.REPLY || action == ZmOperation.REPLY_ALL) {
1127 		var bp = msg.getBodyPart();
1128 		if ((bp && bp.isTruncated) || !msg._loaded) {
1129 			params.noTruncate = true;
1130 			params.forceLoad = true;
1131 		}
1132 	}
1133 
1134 	if (params.msg) {
1135 		this._handleResponseDoAction(params, params.msg);
1136 	}
1137 	else {
1138 		var respCallback = this._handleResponseDoAction.bind(this, params);
1139 		// TODO: pointless to load msg when forwarding as att
1140 		this._getLoadedMsg(params, respCallback);
1141 	}
1142 };
1143 
1144 ZmMailListController.prototype._handleResponseDoAction =
1145 function(params, msg, finalChoice) {
1146 
1147 	if (!msg) { return; }
1148 
1149 	msg._instanceDate = params.instanceDate;
1150 
1151 	params.inNewWindow = (!appCtxt.isChildWindow && this._app._inNewWindow(params.ev));
1152 
1153     if (msg.list && msg.isUnread && !appCtxt.getById(msg.folderId).isReadOnly()) {
1154         msg.list.markRead({items:[msg], value:true});
1155     }
1156 
1157 	// check to see if we're forwarding multiple msgs, in which case we do them as attachments;
1158 	// also check to see if we're forwarding an invite; if so, go to appt compose
1159 	var action = params.action;
1160 	if (action == ZmOperation.FORWARD_ATT || action == ZmOperation.FORWARD_INLINE) {
1161 		var selection, selCount;
1162 		if (params.msg) {
1163 			selCount = 1
1164 		}
1165 		else {
1166 			var cview = this._listView[this._currentViewId];
1167 			if (cview) {
1168 				selection = params.conv ? [ params.conv ] : cview.getSelection();
1169 				selCount = params.conv ? 1 : selection.length;
1170 			}
1171 		}
1172 		// bug 43428 - invitation should be forwarded using appt forward view
1173 		if (selCount == 1 && msg.forwardAsInvite()) {
1174 			var ac = window.parentAppCtxt || window.appCtxt;
1175 			if (ac.get(ZmSetting.CALENDAR_ENABLED)) {
1176 				var controller = ac.getApp(ZmApp.CALENDAR).getCalController();
1177 				controller.forwardInvite(msg);
1178 				if (appCtxt.isChildWindow) {
1179 					window.close();
1180 				}
1181 				return;
1182 			}
1183 		}
1184 
1185 		// forward multiple msgs as attachments
1186 		if (selCount > 1 || params.origAction === ZmOperation.FORWARD_CONV) {
1187 			action = params.action = ZmOperation.FORWARD_ATT;
1188 			this._handleLoadMsgs(params, selection);
1189 			return;
1190 		}
1191 	}
1192 	else if (appCtxt.isOffline && action == ZmOperation.DRAFT) {
1193 		var folder = appCtxt.getById(msg.folderId);
1194 		params.accountName = folder && folder.getAccount().name;
1195 	}
1196 	else if (action == ZmOperation.DECLINE_PROPOSAL) {
1197 		params.subjOverride = this._getInviteReplySubject(action) + msg.subject;
1198 	}
1199 
1200 	params.msg = msg;
1201 	AjxDispatcher.run("Compose", params);
1202 };
1203 
1204 
1205 ZmMailListController.prototype._redirectListener =
1206 function(ev) {
1207 
1208     var action = ev.item.getData(ZmOperation.KEY_ID);
1209     var msg = this.getMsg({ev:ev, action:action});
1210     if (!msg) return;
1211 
1212     var redirectDialog = appCtxt.getMailRedirectDialog();
1213     var redirectDialogCB = this._redirectCallback.bind(this, msg);
1214     ZmController.showDialog(redirectDialog, redirectDialogCB);
1215 };
1216 
1217 
1218 ZmMailListController.prototype._redirectCallback =
1219 function(msg) {
1220 	if (!msg) return;
1221 
1222 	var redirectDialog = appCtxt.getMailRedirectDialog();
1223 	var addrs = redirectDialog.getAddrs();
1224 	// Code copied from ZmComposeView.  Should consolidate along with the
1225 	// ZmRecipient code, i.e. the corresponding recipient controller code.
1226 	if (addrs.gotAddress) {
1227         if (addrs[ZmRecipients.BAD].size()) {
1228             // Any bad addresses?  If there are bad ones, ask the user if they want to send anyway.
1229             var bad = AjxStringUtil.htmlEncode(addrs[ZmRecipients.BAD].toString(AjxEmailAddress.SEPARATOR));
1230             var badMsg = AjxMessageFormat.format(ZmMsg.compBadAddresses, bad);
1231             var cd = appCtxt.getOkCancelMsgDialog();
1232             cd.reset();
1233             cd.setMessage(badMsg, DwtMessageDialog.WARNING_STYLE);
1234             cd.registerCallback(DwtDialog.OK_BUTTON, this._badRedirectAddrsOkCallback, this, [addrs, cd, msg]);
1235             cd.registerCallback(DwtDialog.CANCEL_BUTTON, this._badRedirectAddrsCancelCallback, this, cd);
1236             cd.setVisible(true); // per fix for bug 3209
1237             cd.popup();
1238         } else {
1239             redirectDialog.popdown();
1240             msg.redirect(addrs, this._handleSendRedirect.bind(this));
1241          }
1242     } else {
1243         redirectDialog.popdown();
1244     }
1245 };
1246 
1247 // User has agreed to send message with bad addresses
1248 ZmMailListController.prototype._badRedirectAddrsOkCallback =
1249 function(addrs, dialog, msg) {
1250     dialog.popdown();
1251     appCtxt.getMailRedirectDialog().popdown();
1252     msg.redirect(addrs, this._handleSendRedirect.bind(this));
1253 };
1254 
1255 
1256 // User has declined to send message with bad addresses - popdown the bad addr dialog,
1257 // returning to the redirect dialog
1258 ZmMailListController.prototype._badRedirectAddrsCancelCallback =
1259 function(dialog) {
1260     dialog.popdown();
1261 };
1262 
1263 ZmMailListController.prototype._handleLoadMsgs =
1264 function(params, selection) {
1265 
1266 	var msgIds = new AjxVector(),
1267 		foldersToOmit = params.foldersToOmit || {};
1268 
1269 	for (var i = 0; i < selection.length; i++) {
1270 		var item = selection[i];
1271 		if (item.type == ZmItem.CONV) {
1272 			for (var j = 0; j < item.msgIds.length; j++) {
1273 				var msgId = item.msgIds[j];
1274 				if (!foldersToOmit[item.msgFolder[msgId]]) {
1275 					msgIds.add(msgId);
1276 				}
1277 			}
1278 		}
1279 		else {
1280 			if (!msgIds.contains(item.id)) {
1281 				msgIds.add(item.id);
1282 			}
1283 		}
1284 	}
1285 	params.msgIds = msgIds.getArray();
1286     params.selectedMessages = selection;
1287 
1288 	AjxDispatcher.run("Compose", params);
1289 };
1290 
1291 ZmMailListController.prototype._handleSendRedirect =
1292 function() {
1293     appCtxt.setStatusMsg(ZmMsg.redirectSent, ZmStatusView.LEVEL_INFO, null);
1294 };
1295 
1296 /**
1297  * Marks a mail item read if appropriate, possibly after a delay. An arg can be passed so that this function
1298  * just returns whether the item can be marked read now, which typically results in the setting of the "read"
1299  * flag in a retrieval request to the server. After that, it can be called without that arg in order to mark
1300  * the item read after a delay if necessary.
1301  * 
1302  * @param {ZmMailItem}	item		msg or conv
1303  * @param {boolean}		check		if true, return true if msg can be marked read now, without marking it read
1304  */
1305 ZmMailListController.prototype._handleMarkRead =
1306 function(item, check) {
1307 
1308 	var convView = this._convView;
1309 	var waitOnMarkRead = convView && convView.isWaitOnMarkRead();
1310 	if (item && item.isUnread && !waitOnMarkRead) {
1311 		if (!item.isReadOnly() && !appCtxt.isExternalAccount()) {
1312 			var markRead = appCtxt.get(ZmSetting.MARK_MSG_READ);
1313 			if (markRead == ZmSetting.MARK_READ_NOW) {
1314 				if (check) {
1315 					return true;
1316 				}
1317 				else {
1318 					// msg was cached as unread, mark it read now
1319 					this._doMarkRead([item], true);
1320 				}
1321 			} else if (markRead > 0) {
1322 				if (!appCtxt.markReadAction) {
1323 					appCtxt.markReadAction = new AjxTimedAction(this, this._markReadAction);
1324 				}
1325 				appCtxt.markReadAction.args = [item];
1326 				appCtxt.markReadActionId = AjxTimedAction.scheduleAction(appCtxt.markReadAction, markRead * 1000);
1327 			}
1328 		}
1329 	}
1330 	return false;
1331 };
1332 
1333 ZmMailListController.prototype._markReadAction =
1334 function(msg) {
1335 	this._doMarkRead([msg], true);
1336 };
1337 
1338 ZmMailListController.prototype._doMarkRead =
1339 function(items, on, callback, forceCallback) {
1340 
1341 	var params = {items:items, value:on, callback:callback, noBusyOverlay: true};
1342 	var list = params.list = this._getList(params.items);
1343     params.forceCallback = forceCallback;
1344 	this._setupContinuation(this._doMarkRead, [on, callback], params);
1345 	list.markRead(params);
1346 };
1347 
1348 ZmMailListController.prototype._doMarkMute =
1349 function(items, on, callback, forceCallback) {
1350 
1351 	var params = {items:items, value:on, callback:callback};
1352 	var list = params.list = this._getList(params.items);
1353     params.forceCallback = forceCallback;
1354 	this._setupContinuation(this._doMarkMute, [on, callback], params);
1355 	list.markMute(params);
1356 };
1357 
1358 /**
1359 * Marks the given items as "spam" or "not spam". Items marked as spam are moved to
1360 * the Junk folder. If items are being moved out of the Junk folder, they will be
1361 * marked "not spam", and the destination folder may be provided. It defaults to Inbox
1362 * if not present.
1363 *
1364 * @param items			[Array]			a list of items to move
1365 * @param markAsSpam		[boolean]		spam or not spam
1366 * @param folder			[ZmFolder]		destination folder
1367 */
1368 ZmMailListController.prototype._doSpam =
1369 function(items, markAsSpam, folder) {
1370 
1371 	this._listView[this._currentViewId]._itemToSelect = this._getNextItemToSelect();
1372 	items = AjxUtil.toArray(items);
1373 
1374 	var params = {items:items,
1375 					markAsSpam:markAsSpam,
1376 					folder:folder,
1377 					childWin:appCtxt.isChildWindow && window,
1378 					closeChildWin: appCtxt.isChildWindow};
1379 
1380 	var allDoneCallback = this._getAllDoneCallback();
1381 	var list = params.list = this._getList(params.items);
1382 	this._setupContinuation(this._doSpam, [markAsSpam, folder], params, allDoneCallback);
1383 	list.spamItems(params);
1384 };
1385 
1386 ZmMailListController.prototype._inviteReplyHandler =
1387 function(ev) {
1388 	var ac = window.parentAppCtxt || window.appCtxt;
1389 
1390 	this._listView[this._currentViewId]._itemToSelect = this._getNextItemToSelect();
1391 	ac.getAppController().focusContentPane();
1392 
1393 	var type = ev._inviteReplyType;
1394 	if (type == ZmOperation.PROPOSE_NEW_TIME ) {
1395 		ac.getApp(ZmApp.CALENDAR).getCalController().proposeNewTime(ev._msg);
1396 		if (appCtxt.isChildWindow) {
1397 			window.close();
1398 		}
1399 	}
1400 	else if (type == ZmOperation.ACCEPT_PROPOSAL) {
1401 		this._acceptProposedTime(ev._inviteComponentId, ev._msg);
1402 	}
1403 	else if (type == ZmOperation.DECLINE_PROPOSAL) {
1404 		this._declineProposedTime(ev._inviteComponentId, ev._msg);
1405 	}
1406 	else if (type == ZmOperation.INVITE_REPLY_ACCEPT ||
1407 			type == ZmOperation.EDIT_REPLY_CANCEL ||
1408 			type == ZmOperation.INVITE_REPLY_DECLINE ||
1409 			type == ZmOperation.INVITE_REPLY_TENTATIVE)
1410 	{
1411 		this._editInviteReply(ZmMailListController.INVITE_REPLY_MAP[type], ev._inviteComponentId, null, null, ev._inviteReplyFolderId);
1412 	}
1413 	else {
1414 		var callback = new AjxCallback(this, this._handleInviteReplySent);
1415 		var accountName = ac.multiAccounts && ac.accountList.mainAccount.name;
1416 		this._sendInviteReply(type, ev._inviteComponentId, null, accountName, null, ev._msg, ev._inviteReplyFolderId, callback);
1417 	}
1418 	return false;
1419 };
1420 
1421 ZmMailListController.prototype._handleInviteReplySent =
1422 function(result, newPtst) {
1423 	var inviteMsgView = this.getCurrentView().getInviteMsgView();
1424 	if (!inviteMsgView || !newPtst) {
1425 		return;
1426 	}
1427 	inviteMsgView.enableToolbarButtons(newPtst);
1428 	inviteMsgView.updatePtstMsg(newPtst);
1429 };
1430 
1431 ZmMailListController.prototype._shareHandler =
1432 function(ev) {
1433 	var msg = this.getMsg();
1434 	var fromAddr = msg ? msg.getAddress(AjxEmailAddress.FROM).address : null;
1435 
1436 	if (ev._buttonId == ZmOperation.SHARE_ACCEPT) {
1437 		var acceptDialog = appCtxt.getAcceptShareDialog();
1438 		acceptDialog.setAcceptListener(this._acceptShareListener);
1439 		acceptDialog.popup(ev._share, fromAddr);
1440 	} else if (ev._buttonId == ZmOperation.SHARE_DECLINE) {
1441 		var declineDialog = appCtxt.getDeclineShareDialog();
1442 		declineDialog.setDeclineListener(this._declineShareListener);
1443 		declineDialog.popup(ev._share, fromAddr);
1444 	}
1445 };
1446 
1447 ZmMailListController.prototype._subscribeHandler =
1448 function(ev) {
1449 	var req = ev._subscribeReq;
1450 	var statusMsg;
1451 	var approve = false;
1452 	if (ev._buttonId == ZmOperation.SUBSCRIBE_APPROVE) {
1453 		statusMsg = req.subscribe ? ZmMsg.dlSubscribeRequestApproved : ZmMsg.dlUnsubscribeRequestApproved;
1454 		approve = true;
1455 	}
1456 	else if (ev._buttonId == ZmOperation.SUBSCRIBE_REJECT) {
1457 		statusMsg = req.subscribe ? ZmMsg.dlSubscribeRequestRejected : ZmMsg.dlUnsubscribeRequestRejected;
1458 	}
1459 
1460 	var jsonObj = {
1461 		DistributionListActionRequest: {
1462 			_jsns: "urn:zimbraAccount",
1463 			dl: {by: "name",
1464 				 _content: req.dl.email
1465 			},
1466 			action: {op: approve ? "acceptSubsReq" : "rejectSubsReq",
1467 					 subsReq: {op: req.subscribe ? "subscribe" : "unsubscribe",
1468 							   _content: req.user.email
1469 					 		  }
1470 					}
1471 		}
1472 	};
1473 	var respCallback = this._subscribeResponseHandler.bind(this, statusMsg);
1474 	appCtxt.getAppController().sendRequest({jsonObj: jsonObj, asyncMode: true, callback: respCallback});
1475 
1476 	
1477 
1478 };
1479 
1480 ZmMailListController.prototype._subscribeResponseHandler =
1481 function(statusMsg, ev) {
1482 	var msg = this.getMsg();
1483 	this._removeActionMsg(msg);
1484 	appCtxt.setStatusMsg(statusMsg);
1485 };
1486 
1487 
1488 ZmMailListController.prototype._acceptShareHandler =
1489 function(ev) {
1490 	var msg = appCtxt.getById(ev._share._msgId);
1491 	this._removeActionMsg(msg);
1492 };
1493 
1494 ZmMailListController.prototype._removeActionMsg =
1495 function(msg) {
1496 	var folder = appCtxt.getById(ZmFolder.ID_TRASH);
1497 
1498 	this._listView[this._currentViewId]._itemToSelect = this._getNextItemToSelect();
1499 	var list = msg.list || this.getList();
1500 	var callback = (appCtxt.isChildWindow)
1501 		? (new AjxCallback(this, this._handleAcceptShareInNewWindow)) : null;
1502 	list.moveItems({items: [msg], folder: folder, callback: callback, closeChildWin: appCtxt.isChildWindow});
1503 };
1504 
1505 ZmMailListController.prototype._declineShareHandler = ZmMailListController.prototype._acceptShareHandler;
1506 
1507 ZmMailListController.prototype._handleAcceptShareInNewWindow =
1508 function() {
1509 	window.close();
1510 };
1511 
1512 ZmMailListController.prototype._createViewMenu =
1513 function(view) {
1514 	var btn = this._toolbar[view].getButton(ZmOperation.VIEW_MENU);
1515 	if (!btn) { return; }
1516 
1517 	btn.setMenu(new AjxCallback(this, this._setupViewMenuItems, [view, btn]));
1518 	btn.noMenuBar = true;
1519 };
1520 
1521 
1522 ZmMailListController.prototype._setupViewMenu =
1523 function(view) {
1524 
1525 	// always reset the view menu button icon to reflect the current view
1526 	var btn = this._toolbar[view].getButton(ZmOperation.VIEW_MENU);
1527 	if (btn) {
1528 		var viewType = appCtxt.getViewTypeFromId(view);
1529 		btn.setImage(ZmMailListController.GROUP_BY_ICON[viewType]);
1530 	}
1531 };
1532 
1533 ZmMailListController.prototype._setupViewMenuItems =
1534 function(view, btn) {
1535 
1536 	var menu = this._viewMenu = new ZmPopupMenu(btn, null, null, this);
1537 	btn.setMenu(menu);
1538     var isExternalAccount = appCtxt.isExternalAccount(),
1539 	    convsEnabled = appCtxt.get(ZmSetting.CONVERSATIONS_ENABLED);
1540 
1541 	if (convsEnabled && this.supportsGrouping()) {
1542 		this._setupGroupByMenuItems(view, menu);
1543 	}
1544 	if (menu.getItemCount() > 0) {
1545 		new DwtMenuItem({
1546 			parent: menu,
1547 			style:  DwtMenuItem.SEPARATOR_STYLE
1548 		});
1549 	}
1550 	this._readingPaneViewMenu = this._setupReadingPaneMenu(view, menu);
1551 	if (!isExternalAccount && convsEnabled) {
1552 		this._convOrderViewMenu = this._setupConvOrderMenu(view, menu);
1553 	}
1554 
1555     // add sort and group by menus only if we have headers (not in standalone conv view)
1556     if (this.supportsSorting()) {
1557 	    this._sortViewMenu = this._setupSortViewMenu(view, menu);
1558 	    this._groupByViewMenu = this._mailListView._getGroupByActionMenu(menu, true, true);
1559     }
1560 
1561 	return menu;
1562 };
1563 
1564 ZmMailListController.prototype._setupSortViewMenu = function(view, menu) {
1565 
1566 	var	sortMenuItem = menu.createMenuItem(Dwt.getNextId("SORT_"), {
1567 			text:           ZmMsg.sortBy,
1568 			style:          DwtMenuItem.NO_STYLE
1569 		}),
1570 		sortMenu = this._mailListView._setupSortMenu(sortMenuItem, false);
1571 
1572 	sortMenuItem.setMenu(sortMenu);
1573 
1574 	return sortMenu;
1575 };
1576 
1577 ZmMailListController.prototype._setupColHeaderViewMenu = function(view, menu) {
1578 
1579 	var	colHeaderMenuItem = this._colHeaderMenuItem = menu.createMenuItem(Dwt.getNextId("COL_HEADER_"), {
1580 			text:           ZmMsg.display,
1581 			style:          DwtMenuItem.NO_STYLE
1582 		}),
1583 		colHeaderMenu = ZmListView.prototype._getActionMenuForColHeader.call(this._mailListView, true, colHeaderMenuItem, "view");
1584 
1585 	colHeaderMenuItem.setMenu(colHeaderMenu);
1586 
1587 	return colHeaderMenu;
1588 };
1589 
1590 // If we're in the Trash or Junk folder, change the "Delete" button tooltip
1591 ZmMailListController.prototype._setupDeleteButton =
1592 function(parent) {
1593 	var folder = this._getSearchFolder();
1594 	var inTrashFolder = (folder && folder.nId == ZmFolder.ID_TRASH);
1595     var inJunkFolder =  (folder && folder.nId == ZmFolder.ID_SPAM);
1596     var deleteButton = parent.getButton(ZmOperation.DELETE);
1597 	var deleteMenuButton = parent.getButton(ZmOperation.DELETE_MENU);
1598 	var tooltip = (inTrashFolder || inJunkFolder) ? ZmMsg.deletePermanentTooltip : ZmMsg.deleteTooltip;
1599 	if (deleteButton) {
1600 		deleteButton.setToolTipContent(ZmOperation.getToolTip(ZmOperation.DELETE, this.getKeyMapName(), tooltip), true);
1601 	}
1602 	if (deleteMenuButton) {
1603 		deleteMenuButton.setToolTipContent(ZmOperation.getToolTip(ZmOperation.DELETE_MENU, this.getKeyMapName(), tooltip), true);
1604 	}
1605 };
1606 
1607 // If we're in the Spam folder, the "Spam" button becomes the "Not Spam" button
1608 ZmMailListController.prototype._setupSpamButton =
1609 function(parent) {
1610 	if (!parent) { return; }
1611 
1612 	var item = parent.getOp(ZmOperation.SPAM);
1613 	if (item) {
1614 		var folderId = this._getSearchFolderId();
1615 		var folder = appCtxt.getById(folderId);
1616 		var inSpamFolder = ((folder && folder.nId == ZmFolder.ID_SPAM) ||
1617 							(!folder && folderId == ZmFolder.ID_SPAM)  ||
1618                             (this._currentSearch && this._currentSearch.folderId == ZmFolder.ID_SPAM)); // fall back
1619 		var inPopupMenu = (parent.isZmActionMenu);
1620 		if (parent.isZmButtonToolBar) {
1621 			//might still be in a popup if it's in the Actions menu. That's the case now but I do this generically so it works if one day we move it as a main button (might want to do that in the spam folder at least)
1622 			inPopupMenu = parent.getActionsMenu() && parent.getActionsMenu().getOp(ZmOperation.SPAM);
1623 		}
1624 
1625 		if (inPopupMenu) {
1626 			item.setText(inSpamFolder ? ZmMsg.notJunkMarkLabel : ZmMsg.junkMarkLabel);
1627 		} else {
1628 			item.setText(inSpamFolder ? ZmMsg.notJunkLabel : ZmMsg.junkLabel);
1629 		}
1630 		item.setImage(inSpamFolder ? 'NotJunk' : 'JunkMail');
1631 		if (item.setToolTipContent) {
1632 			var tooltip = inSpamFolder ? ZmMsg.notJunkTooltip : ZmMsg.junkTooltip;
1633 			item.setToolTipContent(ZmOperation.getToolTip(ZmOperation.SPAM, this.getKeyMapName(), tooltip), true);
1634 		}
1635 		item.isMarkAsSpam = !inSpamFolder;
1636 	}
1637 };
1638 
1639 // set tooltip for print button
1640 ZmMailListController.prototype._setupPrintButton =
1641 function(parent) {
1642     if (!parent) { return; }
1643 
1644     var item = parent.getOp(ZmOperation.PRINT);
1645     if (item) {
1646         item.setToolTipContent(ZmMsg.printMultiTooltip, true);
1647     }
1648 };
1649 
1650 
1651 
1652 /**
1653  * Gets the selected message.
1654  * 
1655  * @param	{Hash}	params		a hash of parameters
1656  * @return	{ZmMailMsg|ZmConv}		the selected message
1657  */
1658 ZmMailListController.prototype.getMsg =
1659 function(params) {
1660 	var sel = this._listView[this._currentViewId].getSelection();
1661 	return (sel && sel.length) ? sel[0] : null;
1662 };
1663 
1664 ZmMailListController.prototype._filterListener =
1665 function(isAddress, rule) {
1666 
1667 	if (isAddress) {
1668 		this._handleResponseFilterListener(rule, this._actionEv.address);
1669 	}
1670 	else {
1671 		this._getLoadedMsg(null, this._handleResponseFilterListener.bind(this, rule));
1672 	}
1673 };
1674 
1675 
1676 ZmMailListController.prototype._setAddToFilterMenu =
1677 function(parent) {
1678 	if (this._filterMenu) {
1679 		return;
1680 	}
1681 
1682 	var menuItem = parent.getOp(ZmOperation.ADD_TO_FILTER_RULE);
1683 	this._filterMenu = new ZmPopupMenu(menuItem);
1684 	menuItem.setMenu(this._filterMenu);
1685 
1686 	this._rules = AjxDispatcher.run("GetFilterRules");
1687 	this._rules.addChangeListener(this._rulesChangeListener.bind(this));
1688 	this._resetFilterMenu();
1689 };
1690 
1691 ZmMailListController.prototype._resetFilterMenu =
1692 function() {
1693 	var filterItems = this._filterMenu.getItems();
1694 	while (filterItems.length > 0) {
1695 		this._filterMenu.removeChild(filterItems[0]);
1696 	}
1697 	this._rules.loadRules(false, this._populateFiltersMenu.bind(this));
1698 };
1699 
1700 ZmMailListController.prototype._populateFiltersMenu =
1701 function(results){
1702 	var filters = results.getResponse();
1703 	var menu = this._filterMenu;
1704 
1705 	var newItem = new DwtMenuItem({parent: menu});
1706 	newItem.setText(ZmMsg.newFilter);
1707 	newItem.setImage("Plus");
1708 	newItem.addSelectionListener(this._filterListener.bind(this, true, null));
1709 
1710 	if (!filters.size()) {
1711 		return;
1712 	}
1713 	menu.createSeparator();
1714 
1715 	for (var i = 0; i < filters.size(); i++) {
1716 		var rule = filters.get(i);
1717 		var mi = new DwtMenuItem({parent: menu});
1718 		mi.setText(AjxStringUtil.clipByLength(rule.name, 20));
1719 		mi.addSelectionListener(this._filterListener.bind(this, true, rule));
1720 	}
1721 };
1722 
1723 ZmMailListController.prototype._rulesChangeListener =
1724 function(ev){
1725 	if (ev.handled || ev.type !== ZmEvent.S_FILTER) {
1726 		return;
1727 	}
1728 
1729 	this._resetFilterMenu();
1730 	ev.handled = true;
1731 };
1732 
1733 ZmMailListController.prototype._createApptListener =
1734 function() {
1735 	this._getLoadedMsg(null, this._handleResponseNewApptListener.bind(this));
1736 };
1737 
1738 ZmMailListController.prototype._createTaskListener =
1739 function() {
1740 	this._getLoadedMsg(null, this._handleResponseNewTaskListener.bind(this));
1741 };
1742 
1743 ZmMailListController.prototype._handleResponseNewApptListener =
1744 function(msg) {
1745 	if (!msg) { return; }
1746 	if (msg.cloneOf) {
1747 		msg = msg.cloneOf;
1748 	}
1749 	var w = appCtxt.isChildWindow ? window.opener : window;
1750     var calController = w.AjxDispatcher.run("GetCalController");
1751     calController.newApptFromMailItem(msg, new Date());
1752 	if (appCtxt.isChildWindow) {
1753 		window.close();
1754 	}
1755 };
1756 
1757 ZmMailListController.prototype._handleResponseNewTaskListener =
1758 function(msg) {
1759 	if (!msg) { return; }
1760 	if (msg.cloneOf) {
1761 		msg = msg.cloneOf;
1762 	}
1763 	var w = appCtxt.isChildWindow ? window.opener : window;
1764     var aCtxt = appCtxt.isChildWindow ? parentAppCtxt : appCtxt;
1765 	w.AjxDispatcher.require(["TasksCore", "Tasks"]);
1766     aCtxt.getApp(ZmApp.TASKS).newTaskFromMailItem(msg, new Date());
1767 	if (appCtxt.isChildWindow) {
1768 		window.close();
1769 	}
1770 };
1771 
1772 ZmMailListController.prototype._handleResponseFilterListener =
1773 function(rule, msgOrAddr) {
1774 
1775 	if (!msgOrAddr) {
1776 		return;
1777 	}
1778 
1779 	// arg can be ZmMailMsg or String (address)
1780 	var msg = msgOrAddr.isZmMailMsg ? msgOrAddr : null;
1781 
1782 	if (msg && msg.cloneOf) {
1783 		msg = msg.cloneOf;
1784 	}
1785 	if (appCtxt.isChildWindow) {
1786 		var mailListController = window.opener.AjxDispatcher.run("GetMailListController");
1787 		mailListController._handleResponseFilterListener(rule, msgOrAddr);
1788 		window.close();
1789 		return;
1790 	}
1791 	
1792 	AjxDispatcher.require(["PreferencesCore", "Preferences"]);
1793 	var editMode = !!rule;
1794 	if (rule) {
1795 		//this is important, without this, in case the user goes to the Filters page, things get messed up and trying to save an
1796 		// edited filter complains about the existence of a filter with the same name.
1797 		rule = this._rules.getRuleByName(rule.name) || rule;
1798 	}
1799 	else {
1800 		rule = new ZmFilterRule();
1801 	}
1802 
1803 	if (msg) {
1804 		var listId = msg.getListIdHeader();
1805 		if (listId) {
1806 			rule.addCondition(ZmFilterRule.TEST_HEADER, ZmFilterRule.OP_CONTAINS, listId, ZmMailMsg.HDR_LISTID);
1807 		}
1808 		else {
1809 			var from = msg.getAddress(AjxEmailAddress.FROM);
1810 			if (from) {
1811 				var subjMod = ZmFilterRule.C_ADDRESS_VALUE[ZmFilterRule.C_FROM];
1812 				rule.addCondition(ZmFilterRule.TEST_ADDRESS, ZmFilterRule.OP_CONTAINS, from.address, subjMod);
1813 			}
1814 			var cc = msg.getAddress(AjxEmailAddress.CC);
1815 			if (cc)	{
1816 				var subjMod = ZmFilterRule.C_ADDRESS_VALUE[ZmFilterRule.C_CC];
1817 				rule.addCondition(ZmFilterRule.TEST_ADDRESS, ZmFilterRule.OP_CONTAINS, cc.address, subjMod);
1818 			}
1819 			var xZimbraDL = msg.getXZimbraDLHeader();
1820 			if (xZimbraDL && xZimbraDL.good) {
1821 				var arr = xZimbraDL.good.getArray();
1822 				var max = arr.length < 5 ? arr.length : 5; //limit number of X-Zimbra-DL ids
1823 				for (var i=0; i < max; i++) {
1824 					rule.addCondition(ZmFilterRule.TEST_HEADER, ZmFilterRule.OP_CONTAINS, arr[i].address, ZmMailMsg.HDR_XZIMBRADL);
1825 				}
1826 			}
1827 			var subj = msg.subject;
1828 			if (subj) {
1829 				var subjMod = ZmFilterRule.C_HEADER_VALUE[ZmFilterRule.C_SUBJECT];
1830 				rule.addCondition(ZmFilterRule.TEST_HEADER, ZmFilterRule.OP_IS, subj, subjMod);
1831 			}
1832 			rule.setGroupOp(ZmFilterRule.GROUP_ALL);
1833 		}
1834 	}
1835 	else {
1836 		var subjMod = ZmFilterRule.C_ADDRESS_VALUE[ZmFilterRule.C_FROM];
1837 		rule.addCondition(ZmFilterRule.TEST_ADDRESS, ZmFilterRule.OP_CONTAINS, msgOrAddr.isAjxEmailAddress ? msgOrAddr.address : msgOrAddr, subjMod);
1838 	}
1839 
1840 	if (!editMode) {
1841 		rule.addAction(ZmFilterRule.A_KEEP);
1842 	}
1843 
1844 	var accountName = appCtxt.multiAccounts && msg && msg.getAccount().name,
1845 		folder = msg && appCtxt.getById(msg.getFolderId()),
1846 		outgoing = !!(folder && folder.isOutbound());
1847 
1848 	appCtxt.getFilterRuleDialog().popup(rule, editMode, null, accountName, outgoing);
1849 };
1850 
1851 /**
1852  * Returns the selected msg, ensuring that it's loaded.
1853  * 
1854  * @private
1855  */
1856 ZmMailListController.prototype._getLoadedMsg =
1857 function(params, callback) {
1858 	params = params || {};
1859 	var msg = this.getMsg(params);
1860 	if (!msg) {
1861 		callback.run();
1862 	}
1863 	if (msg._loaded && !params.forceLoad) {
1864 		callback.run(msg);
1865 	} else {
1866 		if (msg.id == this._pendingMsg) { return; }
1867 		msg._loadPending = true;
1868 		this._pendingMsg = msg.id;
1869 		params.markRead = (params.markRead != null) ? params.markRead : this._handleMarkRead(msg, true);
1870 		// use prototype in callback because these functions are overridden by ZmConvListController
1871 		var respCallback = new AjxCallback(this, ZmMailListController.prototype._handleResponseGetLoadedMsg, [callback, msg]);
1872 		msg.load({getHtml:params.getHtml, markRead:params.markRead, callback:respCallback, noBusyOverlay:false, forceLoad: params.forceLoad, noTruncate: params.noTruncate});
1873 	}
1874 };
1875 
1876 ZmMailListController.prototype._handleResponseGetLoadedMsg =
1877 function(callback, msg) {
1878 	if (this._pendingMsg && (msg.id != this._pendingMsg)) { return; }
1879 	msg._loadPending = false;
1880 	this._pendingMsg = null;
1881 	callback.run(msg);
1882 };
1883 
1884 ZmMailListController.prototype._getInviteReplyBody =
1885 function(type, instanceDate, isResourceInvite) {
1886 	var replyBody;
1887 
1888 	if (instanceDate) {
1889 		switch (type) {
1890 			case ZmOperation.REPLY_ACCEPT:		replyBody = ZmMsg.defaultInviteReplyAcceptInstanceMessage; break;
1891 			case ZmOperation.REPLY_CANCEL:		replyBody = ZmMsg.apptInstanceCanceled; break;
1892 			case ZmOperation.REPLY_DECLINE:		replyBody = ZmMsg.defaultInviteReplyDeclineInstanceMessage; break;
1893 			case ZmOperation.REPLY_TENTATIVE: 	replyBody = ZmMsg.defaultInviteReplyTentativeInstanceMessage; break;
1894 		}
1895 
1896 		if (isResourceInvite) {
1897 			switch (type) {
1898 				case ZmOperation.REPLY_ACCEPT:		replyBody = ZmMsg.defaultInviteReplyResourceAcceptInstanceMessage; break;
1899 				case ZmOperation.REPLY_CANCEL:		replyBody = ZmMsg.apptInstanceCanceled; break;
1900 				case ZmOperation.REPLY_DECLINE:		replyBody = ZmMsg.defaultInviteReplyResourceDeclineInstanceMessage; break;
1901 				case ZmOperation.REPLY_TENTATIVE: 	replyBody = ZmMsg.defaultInviteReplyResourceTentativeInstanceMessage; break;
1902 			}
1903 		}
1904 
1905 		if (replyBody) {
1906 			return AjxMessageFormat.format(replyBody, instanceDate);
1907 		}
1908 	}
1909 	switch (type) {
1910 		case ZmOperation.REPLY_ACCEPT:		replyBody = ZmMsg.defaultInviteReplyAcceptMessage; break;
1911 		case ZmOperation.REPLY_CANCEL:		replyBody = ZmMsg.apptCanceled; break;
1912 		case ZmOperation.DECLINE_PROPOSAL:  replyBody = ""; break;
1913 		case ZmOperation.REPLY_DECLINE:		replyBody = ZmMsg.defaultInviteReplyDeclineMessage; break;
1914 		case ZmOperation.REPLY_TENTATIVE: 	replyBody = ZmMsg.defaultInviteReplyTentativeMessage; break;
1915 		case ZmOperation.REPLY_NEW_TIME: 	replyBody = ZmMsg.defaultInviteReplyNewTimeMessage;	break;
1916 	}
1917 
1918 	if (isResourceInvite) {
1919 		switch (type) {
1920 			case ZmOperation.REPLY_ACCEPT:		replyBody = ZmMsg.defaultInviteReplyResourceAcceptMessage; break;
1921 			case ZmOperation.REPLY_CANCEL:		replyBody = ZmMsg.apptCanceled; break;
1922 			case ZmOperation.REPLY_DECLINE:		replyBody = ZmMsg.defaultInviteReplyResourceDeclineMessage; break;
1923 			case ZmOperation.REPLY_TENTATIVE: 	replyBody = ZmMsg.defaultInviteReplyResourceTentativeMessage; break;
1924 			case ZmOperation.REPLY_NEW_TIME: 	replyBody = ZmMsg.defaultInviteReplyNewTimeMessage;	break;
1925 		}
1926 	}
1927 
1928 	//format the escaped apostrophe in ZmMsg entry
1929 	if (replyBody) {
1930 		replyBody =  AjxMessageFormat.format(replyBody, []);
1931 	}
1932 	return replyBody;
1933 };
1934 
1935 ZmMailListController.prototype._getInviteReplySubject =
1936 function(type) {
1937 	var replySubject = null;
1938 	switch (type) {
1939 		case ZmOperation.REPLY_ACCEPT:		replySubject = ZmMsg.subjectAccept + ": "; break;
1940 		case ZmOperation.DECLINE_PROPOSAL:	replySubject = ZmMsg.subjectDecline + " - "; break;
1941 		case ZmOperation.REPLY_DECLINE:		replySubject = ZmMsg.subjectDecline + ": "; break;
1942 		case ZmOperation.REPLY_TENTATIVE:	replySubject = ZmMsg.subjectTentative + ": "; break;
1943 		case ZmOperation.REPLY_NEW_TIME:	replySubject = ZmMsg.subjectNewTime + ": "; break;
1944 	}
1945 	return replySubject;
1946 };
1947 
1948 ZmMailListController.prototype._editInviteReply =
1949 function(action, componentId, instanceDate, accountName, acceptFolderId) {
1950 	var replyBody = this._getInviteReplyBody(action, instanceDate);
1951 	this._doAction({action:action, extraBodyText:replyBody, instanceDate:instanceDate, accountName:accountName, acceptFolderId: acceptFolderId});
1952 };
1953 
1954 ZmMailListController.prototype._acceptProposedTime =
1955 function(componentId, origMsg) {
1956 	var invite = origMsg.invite;
1957 	var apptId = invite.getAppointmentId();
1958 	var ac = window.parentAppCtxt || window.appCtxt;
1959 	var controller = ac.getApp(ZmApp.CALENDAR).getCalController();
1960 	var callback = new AjxCallback(this, this._handleAcceptDeclineProposedTime, [origMsg]);
1961 	controller.acceptProposedTime(apptId, invite, appCtxt.isChildWindow ? null : callback);
1962 	if (appCtxt.isChildWindow) {
1963 		window.close();
1964 	}
1965 };
1966 
1967 ZmMailListController.prototype._declineProposedTime =
1968 function(componentId, origMsg) {
1969 	var replyBody = this._getInviteReplyBody(ZmOperation.DECLINE_PROPOSAL);
1970 	var callback = new AjxCallback(this, this._handleAcceptDeclineProposedTime, [origMsg]);
1971 	this._doAction({action:ZmOperation.DECLINE_PROPOSAL, extraBodyText:replyBody, instanceDate:null, sendMsgCallback: callback});
1972 };
1973 
1974 ZmMailListController.prototype._handleAcceptDeclineProposedTime =
1975 function(origMsg) {
1976 	this._doDelete([origMsg]);
1977 };
1978 
1979 ZmMailListController.prototype._sendInviteReply =
1980 function(type, componentId, instanceDate, accountName, ignoreNotify, origMsg, acceptFolderId, callback) {
1981 	var msg = new ZmMailMsg();
1982 	AjxDispatcher.require(["MailCore", "CalendarCore"]);
1983 
1984 	msg._origMsg = origMsg || this.getMsg();
1985 	msg.inviteMode = type;
1986 	msg.isReplied = true;
1987 	msg.isForwarded = false;
1988 	msg.isInviteReply = true;
1989 	msg.acceptFolderId = acceptFolderId;
1990 	msg.folderId = msg._origMsg.folderId;
1991 
1992 	var replyActionMode = ZmMailListController.REPLY_ACTION_MAP[type] ? ZmMailListController.REPLY_ACTION_MAP[type] : type;
1993 	var replyBody = this._getInviteReplyBody(replyActionMode, instanceDate, msg._origMsg.isResourceInvite());
1994 	if (replyBody != null) {
1995 		var dummyAppt = new ZmAppt();
1996 		dummyAppt.setFromMessage(msg._origMsg);
1997 
1998 		var tcontent = dummyAppt.getTextSummary() + "\n" + replyBody;
1999 		var textPart = new ZmMimePart();
2000 		textPart.setContentType(ZmMimeTable.TEXT_PLAIN);
2001 		textPart.setContent(tcontent);
2002 
2003 		var hcontent = dummyAppt.getHtmlSummary() + "<p>" + replyBody + "</p>";
2004 		var htmlPart = new ZmMimePart();
2005 		htmlPart.setContentType(ZmMimeTable.TEXT_HTML);
2006 		htmlPart.setContent(hcontent);
2007 
2008 		var topPart = new ZmMimePart();
2009 		topPart.setContentType(ZmMimeTable.MULTI_ALT);
2010 		topPart.children.add(textPart);
2011 		topPart.children.add(htmlPart);
2012 
2013 		msg.setTopPart(topPart);
2014 	}
2015 	var subject = this._getInviteReplySubject(replyActionMode) + msg._origMsg.invite.getEventName();
2016 	if (subject != null) {
2017 		msg.setSubject(subject);
2018 	}
2019 	var errorCallback = new AjxCallback(this, this._handleErrorInviteReply);
2020 	msg.sendInviteReply(true, componentId, callback, errorCallback, instanceDate, accountName, ignoreNotify);
2021 };
2022 
2023 ZmMailListController.prototype._handleErrorInviteReply =
2024 function(result) {
2025 	if (result.code == ZmCsfeException.MAIL_NO_SUCH_ITEM) {
2026 		var dialog = appCtxt.getErrorDialog();
2027 		dialog.setMessage(ZmMsg.inviteOutOfDate);
2028 		dialog.popup(null, true);
2029 		return true;
2030 	}
2031 };
2032 
2033 ZmMailListController.prototype._spamListener =
2034 function(ev) {
2035 	var items = this._listView[this._currentViewId].getSelection();
2036 	var button = this.getCurrentToolbar().getButton(ZmOperation.SPAM);
2037 
2038 	this._doSpam(items, button.isMarkAsSpam);
2039 };
2040 
2041 ZmMailListController.prototype._detachListener =
2042 function(ev, callback) {
2043 	var msg = this.getMsg();
2044 	if (msg) {
2045 		if (msg._loaded) {
2046 			ZmMailMsgView.detachMsgInNewWindow(msg, false, this);
2047 			// always mark a msg read if it is displayed in its own window
2048 			if (msg.isUnread && !appCtxt.getById(msg.folderId).isReadOnly()) {
2049 				msg.list.markRead({items:[msg], value:true});
2050 			}
2051 		} else {
2052 			ZmMailMsgView.rfc822Callback(msg.id, null, this);
2053 		}
2054 	}
2055 	if (callback) { callback.run(); }
2056 };
2057 
2058 ZmMailListController.prototype._printListener =
2059 function(ev) {
2060 	var listView = this._listView[this._currentViewId];
2061 	var items = listView.getSelection();
2062 	items = AjxUtil.toArray(items);
2063 	var ids = [];
2064 	var showImages = false;
2065 	for (var i = 0; i < items.length; i++) {
2066 		var item = items[i];
2067 		// always extract out the msg ids from the conv
2068 		if (item.toString() == "ZmConv") {
2069 			// get msg ID in case of virtual conv.
2070 			// item.msgIds.length is inconsistent, so checking if conv id is negative.
2071 			if (appCtxt.isOffline && item.id.split(":")[1]<0) {
2072 				ids.push(item.msgIds[0]);
2073 			} else {
2074 				ids.push("C:"+item.id);
2075 			}
2076 			if (item.isZmConv) {
2077 				var msgList = item.getMsgList();
2078 				for(var j=0; j<msgList.length; j++) {
2079 					if(msgList[j].showImages) {
2080 						showImages = true;
2081 						break;
2082 					}
2083 				}
2084 			}
2085 		} else {
2086 			ids.push(item.id);
2087 			if (item.showImages) {
2088 				showImages = true;
2089 			}
2090 		}
2091 	}
2092 	var url = ("/h/printmessage?id=" + ids.join(",")) + "&tz=" + AjxTimezone.getServerId(AjxTimezone.DEFAULT);
2093 	if (appCtxt.get(ZmSetting.DISPLAY_EXTERNAL_IMAGES) || showImages) {
2094 		url = url+"&xim=1";
2095 	}
2096     if (appCtxt.isOffline) {
2097         var acctName = items[0].getAccount().name;
2098         url+="&acct=" + acctName ;
2099     }
2100     window.open(appContextPath+url, "_blank");
2101 };
2102 
2103 ZmMailListController.prototype._editListener =
2104 function(isEditAsNew, ev) {
2105     this._doAction({ev:ev, action:ZmOperation.DRAFT, isEditAsNew:isEditAsNew});
2106 };
2107 
2108 ZmMailListController.prototype._muteUnmuteConvListener =
2109 function(ev) {
2110     var status = this._getConvMuteStatus();
2111     if (status.hasUnmuteConv) {
2112         this._muteConvListener();
2113     }
2114     else {
2115         this._unmuteConvListener();
2116     }
2117 };
2118 
2119 ZmMailListController.prototype._muteConvListener =
2120 function(ev) {
2121     var listView = this._listView[this._currentView];
2122 	var items = listView.getSelection();
2123 	items = AjxUtil.toArray(items);
2124     var markReadcallback = this._getMarkReadCallback();
2125     var callback = new AjxCallback(this, this._handleMuteUnmuteConvResponse, [markReadcallback, ZmId.OP_MUTE_CONV]);
2126     this._doMarkMute(items, true, callback, true);
2127 };
2128 
2129 ZmMailListController.prototype._unmuteConvListener =
2130 function(ev) {
2131     var listView = this._listView[this._currentView];
2132 	var items = listView.getSelection();
2133 	items = AjxUtil.toArray(items);
2134     var convListView = this._mailListView || this._parentController._mailListView;
2135     //When a conv is unmuted it needs to be rearranged in the list as per its sorting order. convListCallback will handle it.
2136     var convListCallback = null;
2137     if(convListView && convListView.toString() == "ZmConvListView") {
2138         convListCallback = new AjxCallback(convListView, convListView.handleUnmuteConv, items);
2139     }
2140     var callback = new AjxCallback(this, this._handleMuteUnmuteConvResponse, [convListCallback, ZmId.OP_UNMUTE_CONV]);
2141     this._doMarkMute(items, false, callback, true);
2142 };
2143 
2144 ZmMailListController.prototype._handleMuteUnmuteConvResponse =
2145 function(callback, actionId, result) {
2146     if(callback != null) {
2147         callback.run();
2148     }
2149 };
2150 
2151 ZmMailListController.prototype._checkMailListener =
2152 function() {
2153 	if (appCtxt.isOffline) {
2154 		var callback = new AjxCallback(this, this._handleSyncAll);
2155 		appCtxt.accountList.syncAll(callback);
2156 	}
2157 
2158 	var folder = this._getSearchFolder();
2159 	var isFeed = (folder && folder.isFeed());
2160 	if (isFeed) {
2161 		folder.sync();
2162 	} else {
2163 		var hasExternalAccounts = false;
2164 		if (!appCtxt.isOffline) {
2165 			var isEnabled = appCtxt.get(ZmSetting.POP_ACCOUNTS_ENABLED) || appCtxt.get(ZmSetting.IMAP_ACCOUNTS_ENABLED);
2166 			if (folder && !isFeed && isEnabled) {
2167 				var dataSources = folder.getDataSources(null, true);
2168 				if (dataSources) {
2169 					hasExternalAccounts = true;
2170 					var dsCollection = AjxDispatcher.run("GetDataSourceCollection");
2171 					dsCollection.importMail(dataSources);
2172 				}
2173 			}
2174 		}
2175 
2176 		if ((folder && folder.nId == ZmFolder.ID_INBOX) || !hasExternalAccounts) {
2177 			appCtxt.getAppController().sendNoOp();
2178 		}
2179 	}
2180 };
2181 
2182 ZmMailListController.prototype._handleSyncAll =
2183 function() {
2184 	//doesn't do anything now after I removed the appCtxt.get(ZmSetting.GET_MAIL_ACTION) == ZmSetting.GETMAIL_ACTION_DEFAULT preference stuff
2185 };
2186 
2187 ZmMailListController.prototype.runRefresh =
2188 function() {
2189 	this._checkMailListener();
2190 };
2191 
2192 ZmMailListController.prototype._sendReceiveListener =
2193 function(ev) {
2194 	var account = appCtxt.accountList.getAccount(ev.item.getData(ZmOperation.MENUITEM_ID));
2195 	if (account) {
2196 		account.sync();
2197 	}
2198 };
2199 
2200 ZmMailListController.prototype._folderSearch =
2201 function(folderId) {
2202 	appCtxt.getSearchController().search({query:"in:" + ZmFolder.QUERY_NAME[folderId]});
2203 };
2204 
2205 // Miscellaneous
2206 
2207 // Adds "By Conversation" and "By Message" to a view menu
2208 ZmMailListController.prototype._setupGroupByMenuItems =
2209 function(view, menu) {
2210 
2211 	for (var i = 0; i < ZmMailListController.GROUP_BY_VIEWS.length; i++) {
2212 		var id = ZmMailListController.GROUP_BY_VIEWS[i];
2213 		var mi = menu.createMenuItem(id, {image:	ZmMailListController.GROUP_BY_ICON[id],
2214 										  text:		ZmMsg[ZmMailListController.GROUP_BY_MSG_KEY[id]],
2215 										  shortcut:	ZmMailListController.GROUP_BY_SHORTCUT[id],
2216 										  style:	DwtMenuItem.RADIO_STYLE});
2217 		mi.setData(ZmOperation.MENUITEM_ID, id);
2218 		mi.addSelectionListener(this._listeners[ZmOperation.VIEW]);
2219 		if (id == this.getDefaultViewType()) {
2220 			mi.setChecked(true, true);
2221 		}
2222 	}
2223 };
2224 
2225 ZmMailListController.prototype._setReplyText =
2226 function(parent) {
2227 	if (parent && appCtxt.get(ZmSetting.REPLY_MENU_ENABLED)) {
2228 		var op = parent.getOp(ZmOperation.REPLY_MENU);
2229 		if (op) {
2230 			var menu = op.getMenu();
2231 			var replyOp = menu.getOp(ZmOperation.REPLY);
2232 			replyOp.setText(ZmMsg.replySender);
2233 		}
2234 	}
2235 };
2236 
2237 ZmMailListController.prototype._resetOperations =
2238 function(parent, num) {
2239 
2240 	ZmListController.prototype._resetOperations.call(this, parent, num);
2241 
2242 	var isWebClientOffline = appCtxt.isWebClientOffline();
2243 	parent.enable(ZmOperation.PRINT, (num > 0) && !isWebClientOffline );
2244 	parent.enable(ZmOperation.SHOW_ORIG, !isWebClientOffline);
2245 
2246 	if (this.isSyncFailuresFolder()) {
2247 		parent.enableAll(false);
2248 		parent.enable([ZmOperation.NEW_MENU], true);
2249 		parent.enable([ZmOperation.DELETE, ZmOperation.FORWARD], num > 0);
2250 		return;
2251 	}
2252 
2253 	var item;
2254 	if (num == 1 && !this.isDraftsFolder()) {
2255 		var sel = this._listView[this._currentViewId].getSelection();
2256 		if (sel && sel.length) {
2257 			item = sel[0];
2258 		}
2259 	}
2260 	
2261 	// If one item is selected, use its folder; otherwise check if search was constrained to a folder
2262 	var itemFolder = item && item.folderId && appCtxt.getById(item.folderId);
2263 	var folder = itemFolder;
2264 	if (!folder) {
2265 		var folderId = this._getSearchFolderId(true);
2266 		folder = folderId && appCtxt.getById(folderId);
2267 	}
2268 
2269 	var isDrafts = (item && item.isDraft && (item.type != ZmId.ITEM_CONV || item.numMsgs == 1)) || this.isDraftsFolder();
2270 	var isFeed = (folder && folder.isFeed());
2271 	var isReadOnly = (folder && folder.isReadOnly());
2272     var isOutboxFolder = this.isOutboxFolder();
2273 	parent.setItemVisible(ZmOperation.EDIT, (isDrafts || isOutboxFolder) && (!folder || !folder.isReadOnly()));
2274 	parent.setItemVisible(ZmOperation.EDIT_AS_NEW, !(isDrafts || isOutboxFolder));
2275 
2276 	parent.setItemVisible(ZmOperation.REDIRECT, !(isDrafts || isOutboxFolder));
2277 	parent.enable(ZmOperation.REDIRECT, !(isDrafts || isOutboxFolder || isWebClientOffline));
2278 
2279 	parent.setItemVisible(ZmOperation.MARK_READ, !(isDrafts || isOutboxFolder));
2280 	parent.setItemVisible(ZmOperation.MARK_UNREAD, !(isDrafts || isOutboxFolder));
2281 	parent.setItemVisible(ZmOperation.FLAG, !(isDrafts || isOutboxFolder));
2282 	parent.setItemVisible(ZmOperation.UNFLAG, !(isDrafts || isOutboxFolder));
2283 	parent.setItemVisible(ZmOperation.SPAM, !(isDrafts || isOutboxFolder));
2284 	parent.setItemVisible(ZmOperation.DETACH, !(isDrafts || isOutboxFolder));
2285 
2286 	parent.enable(ZmOperation.MOVE_MENU, !(isDrafts || isOutboxFolder) && num > 0);
2287 
2288 	parent.enable(ZmOperation.DETACH, (appCtxt.get(ZmSetting.DETACH_MAILVIEW_ENABLED) && !(isDrafts || isOutboxFolder || isWebClientOffline) && num == 1));
2289 
2290 	/*if (parent.isZmActionMenu) {
2291 		parent.setItemVisible(ZmOperation.QUICK_COMMANDS, !isDrafts && parent._hasQuickCommands);
2292 	} else {
2293 		parent.setItemVisible(ZmOperation.QUICK_COMMANDS, !isDrafts);
2294 	} */
2295 
2296 	parent.setItemVisible(ZmOperation.ADD_FILTER_RULE, !(isDrafts || isOutboxFolder));
2297 	parent.setItemVisible(ZmOperation.CREATE_APPT, !(isDrafts || isOutboxFolder));
2298 	parent.setItemVisible(ZmOperation.CREATE_TASK, !(isDrafts || isOutboxFolder));
2299     parent.setItemVisible(ZmOperation.ACTIONS_MENU, !isOutboxFolder);
2300 
2301 	// bug fix #37154 - disable non-applicable buttons if rfc/822 message
2302 	var isRfc822 = appCtxt.isChildWindow && window.newWindowParams && window.newWindowParams.isRfc822;
2303 	if (isRfc822 || (isReadOnly && num > 0)) {
2304 		parent.enable([ZmOperation.DELETE, ZmOperation.MOVE, ZmOperation.MOVE_MENU, ZmOperation.SPAM, ZmOperation.TAG_MENU], false);
2305 	} else {
2306 		parent.enable([ZmOperation.REPLY, ZmOperation.REPLY_ALL], (!(isDrafts || isOutboxFolder) && !isFeed && num == 1));
2307 		parent.enable([ZmOperation.VIEW_MENU], true);
2308 		parent.enable([ZmOperation.FORWARD, ZmOperation.SPAM], (!(isDrafts || isOutboxFolder) && num > 0));
2309 	}
2310 
2311 	if (this._draftsActionMenu) {
2312 		var editMenu = this._draftsActionMenu.getOp(ZmOperation.EDIT);
2313 		if (editMenu) {
2314 			// Enable|disable 'edit' context menu item based on selection count
2315 			editMenu.setEnabled(num == 1 && (this.isDraftsFolder() || !isReadOnly));
2316 		}
2317 	}
2318 
2319 	var search = appCtxt.getCurrentSearch();
2320 	if (appCtxt.multiAccounts && num > 1 && search && search.isMultiAccount()) {
2321 		parent.enable(ZmOperation.TAG_MENU, false);
2322 	}
2323 
2324     if (appCtxt.isExternalAccount()) {
2325         parent.enable(
2326                         [
2327                             ZmOperation.REPLY,
2328                             ZmOperation.REPLY_ALL,
2329                             ZmOperation.FORWARD,
2330                             ZmOperation.EDIT_AS_NEW,
2331                             ZmOperation.REDIRECT,
2332                             ZmOperation.MARK_READ,
2333                             ZmOperation.MARK_UNREAD,
2334                             ZmOperation.SPAM,
2335                             ZmOperation.MOVE,
2336                             ZmOperation.MOVE_MENU,
2337                             ZmOperation.DELETE,
2338                             ZmOperation.DETACH,
2339                             ZmOperation.ADD_FILTER_RULE,
2340                             ZmOperation.CREATE_APPT,
2341                             ZmOperation.SEARCH_TO,
2342                             ZmOperation.SEARCH,
2343                             ZmOperation.CREATE_TASK
2344                         ],
2345                         false
2346                     );
2347         parent.setItemVisible(ZmOperation.TAG_MENU, false);
2348     }
2349 
2350 	this._cleanupToolbar(parent);
2351 };
2352 
2353 ZmMailListController.prototype._showMailItem =
2354 function() {
2355 	var avm = appCtxt.getAppViewMgr();
2356 	this._setup(this._currentViewId);
2357 	var elements = this.getViewElements(this._currentViewId, this._view[this._currentViewId]);
2358 
2359 	var curView = avm.getCurrentViewId();
2360 	var tabId = ZmMailListController.viewToTab[curView] || Dwt.getNextId();
2361 	ZmMailListController.viewToTab[this._currentViewId] = tabId;
2362 	var viewParams = {
2363 		view:		this._currentViewId,
2364 		viewType:	this._currentViewType,
2365 		elements:	elements,
2366 		hide:		this._elementsToHide,
2367 		clear:		appCtxt.isChildWindow,
2368 		tabParams:	this._getTabParams(tabId, this._tabCallback.bind(this))
2369 	};
2370 	var buttonText = (this._conv && this._conv.subject) ? this._conv.subject.substr(0, ZmAppViewMgr.TAB_BUTTON_MAX_TEXT) : (this._msg && this._msg.subject && this._msg.subject.substr(0, ZmAppViewMgr.TAB_BUTTON_MAX_TEXT)) ||   ZmMsgController.DEFAULT_TAB_TEXT;
2371 
2372 	this._setView(viewParams);
2373 	avm.setTabTitle(this._currentViewId, buttonText);
2374 	this._resetOperations(this._toolbar[this._currentViewId], 1); // enable all buttons
2375 	this._resetNavToolBarButtons();
2376 };
2377 
2378 
2379 /**
2380  * if parent is a toolbar, it might have an actionsMenu. If it does, we can clean up the separators in that menu.
2381  * (to prevent multiple consecutive separators, etc)
2382  * @param parent
2383  */
2384 ZmMailListController.prototype._cleanupToolbar =
2385 function(parent) {
2386 	//cleanup the separators of the toolbar Actions menu
2387 	if (!parent.getActionsMenu) {
2388 		return;
2389 	}
2390 	var actionsMenu = parent.getActionsMenu();
2391 	if (!actionsMenu) {
2392 		return;
2393 	}
2394 	actionsMenu.cleanupSeparators();
2395 };
2396 
2397 
2398 
2399 // Enable mark read/unread as appropriate.
2400 ZmMailListController.prototype._enableFlags =
2401 function(menu) {
2402     if(appCtxt.isExternalAccount()) {
2403         menu.enable([ZmOperation.MARK_READ, ZmOperation.MARK_UNREAD, ZmOperation.FLAG, ZmOperation.UNFLAG], false);
2404         return;
2405     }
2406 	var status = this._getReadStatus();
2407 	menu.enable(ZmOperation.MARK_READ, status.hasUnread);
2408 	menu.enable(ZmOperation.MARK_UNREAD, status.hasRead);
2409 	menu.enable(ZmOperation.FLAG, status.hasUnflagged);
2410 	menu.enable(ZmOperation.UNFLAG, status.hasFlagged);
2411 
2412     if (appCtxt.isWebClientOffline()) {
2413         menu.enable([ZmOperation.ADD_FILTER_RULE,ZmOperation.CREATE_APPT, ZmOperation.CREATE_TASK], false);
2414     }
2415 };
2416 
2417 // Enable mark read/unread as appropriate.
2418 ZmMailListController.prototype._enableMuteUnmute =
2419 function(menu) {
2420     menu.enable([ZmOperation.UNMUTE_CONV, ZmOperation.MUTE_CONV], false);
2421     if (appCtxt.isExternalAccount() || appCtxt.isChildWindow || this._app.getGroupMailBy() === ZmItem.MSG) {
2422         return;
2423     }
2424     var status = this._getConvMuteStatus();
2425     if (status.hasMuteConv && status.hasUnmuteConv) {
2426         menu.enable(ZmOperation.UNMUTE_CONV, true);
2427         menu.enable(ZmOperation.MUTE_CONV, true);
2428     }
2429 	else if (status.hasMuteConv) {
2430          menu.enable(ZmOperation.UNMUTE_CONV, true);
2431     }
2432     else {
2433          menu.enable(ZmOperation.MUTE_CONV, true);
2434     }
2435 };
2436 
2437 /**
2438 * This method is actually called by a pushed view's controller when a user
2439 * attempts to page conversations (from CV) or messages (from MV ala TV).
2440 * We want the underlying view (CLV or MLV) to update itself silently as it
2441 * feeds the next/prev conv/msg to its respective controller.
2442 *
2443 * @param {ZmItem}	currentItem	the current item
2444 * @param {Boolean}	forward		if <code>true</code>, get next item rather than previous
2445 * 
2446 * @private
2447 */
2448 ZmMailListController.prototype.pageItemSilently =
2449 function(currentItem, forward, msgController) {
2450 
2451 	var newItem = this._getNextItem(currentItem, forward);
2452 	if (newItem) {
2453 		if (msgController) {
2454 			msgController.inactive = true; //make it inactive so it can be reused instead of creating a new one for each item paged.
2455 		}
2456 		var lv = this._listView[this._currentViewId];
2457 		lv.emulateDblClick(newItem);
2458 	}
2459 };
2460 
2461 ZmMailListController.prototype._getNextItem =
2462 function(currentItem, forward) {
2463 
2464 	var list = this._list.getArray();
2465 	var len = list.length;
2466 	for (var i = 0; i < len; i++) {
2467 		if (currentItem == list[i]) {
2468 			break;
2469 		}
2470 	}
2471 	if (i == len) { return; }
2472 
2473 	var itemIdx = forward ? i + 1 : i - 1;
2474 
2475 	if (itemIdx >= len) {
2476 		//we are looking for the next item after the current list, not yet loaded
2477 		if (!this._list.hasMore()) {
2478 			return;
2479 		}
2480 		this._paginate(this._currentViewId, true, itemIdx);
2481 		return;
2482 	}
2483 	return list[itemIdx];
2484 };
2485 
2486 /**
2487  * Selects and displays an item that has been loaded into a page that's
2488  * not visible (eg getting the next conv from within the last conv on a page).
2489  *
2490  * @param view			[constant]		current view
2491  * @param saveSelection	[boolean]		if true, maintain current selection
2492  * @param loadIndex		[int]			index of item to show
2493  * @param result			[ZmCsfeResult]	result of SOAP request
2494  * 
2495  * @private
2496  */
2497 ZmMailListController.prototype._handleResponsePaginate =
2498 function(view, saveSelection, loadIndex, offset, result) {
2499 	ZmListController.prototype._handleResponsePaginate.apply(this, arguments);
2500 
2501 	var newItem = loadIndex ? this._list.getVector().get(loadIndex) : null;
2502 	if (newItem) {
2503 		this._listView[this._currentViewId].emulateDblClick(newItem);
2504 	}
2505 };
2506 
2507 ZmMailListController.prototype._getMenuContext =
2508 function() {
2509 	return this.getCurrentViewId();
2510 };
2511 
2512 // Flag mail items(override ZmListController to add hook to zimletMgr
2513 ZmMailListController.prototype._doFlag =
2514 function(items, on) {
2515 	ZmListController.prototype._doFlag.call(this, items, on);
2516 	appCtxt.notifyZimlets("onMailFlagClick", [items, on]);
2517 };
2518 
2519 // Tag/untag items(override ZmListController to add hook to zimletMgr
2520 ZmMailListController.prototype._doTag =
2521 function(items, tag, doTag) {
2522 	ZmListController.prototype._doTag.call(this, items, tag, doTag);
2523 	appCtxt.notifyZimlets("onTagAction", [items, tag, doTag]);
2524 };
2525 
2526 
2527 /**
2528  * Returns the next/previous/first/last unread item in the list, based on what's
2529  * currently selected.
2530  *
2531  * @param which		[constant]		DwtKeyMap constant for selecting next/previous/first/last
2532  * @param type		[constant]*		if present, only return this type of item
2533  * @param noBump	[boolean]*		if true, start with currently selected item
2534  * 
2535  * @private
2536  */
2537 ZmMailListController.prototype._getUnreadItem =
2538 function(which, type, noBump) {
2539 
2540 	var lv = this._listView[this._currentViewId];
2541 	var vec = lv.getList(true);
2542 	var list = vec && vec.getArray();
2543 	var size = list && list.length;
2544 	if (!size) { return; }
2545 
2546 	var start, index;
2547 	if (which == DwtKeyMap.SELECT_FIRST) {
2548 		index = 0;
2549 	} else if (which == DwtKeyMap.SELECT_LAST) {
2550 		index = list.length - 1;
2551 	} else {
2552 		var sel = lv.getSelection();
2553 		var start, index;
2554 		if (sel && sel.length) {
2555 			start = (which == DwtKeyMap.SELECT_NEXT) ? sel[sel.length - 1] : sel[0];
2556 		} else {
2557 			start = (which == DwtKeyMap.SELECT_NEXT) ? list[0] : list[list.length - 1];
2558 		}
2559 		if (start) {
2560 			var startIndex = lv.getItemIndex(start, true);
2561 			if (sel && sel.length && !noBump) {
2562 				index = (which == DwtKeyMap.SELECT_NEXT) ? startIndex + 1 : startIndex - 1;
2563 			} else {
2564 				index = startIndex;
2565 			}
2566 		}
2567 	}
2568 
2569 	var unreadItem = null;
2570 	while ((index >= 0 && index < size) && !unreadItem) {
2571 		var item = list[index];
2572 		if (item.isUnread && (!type || item.type == type)) {
2573 			unreadItem = item;
2574 		} else {
2575 			index = (which == DwtKeyMap.SELECT_NEXT || which == DwtKeyMap.SELECT_FIRST) ? index + 1 : index - 1;
2576 		}
2577 	}
2578 
2579 	return unreadItem;
2580 };
2581 
2582 ZmMailListController.prototype._getNextItemToSelect = function() {};
2583 
2584 ZmMailListController.prototype.addTrustedAddr =
2585 function(value, callback, errorCallback) {
2586     var soapDoc = AjxSoapDoc.create("ModifyPrefsRequest", "urn:zimbraAccount"),
2587         node,
2588         i;
2589 
2590     for(i=0; i<value.length;i++) {
2591         node = soapDoc.set("pref", AjxStringUtil.trim(value[i]));
2592         node.setAttribute("name", "zimbraPrefMailTrustedSenderList");
2593     }
2594 
2595     return appCtxt.getAppController().sendRequest({
2596        soapDoc: soapDoc,
2597        asyncMode: true,
2598        callback: callback,
2599        errorCallback: errorCallback
2600     });
2601 };
2602 
2603 /**
2604  * @private
2605  */
2606 ZmMailListController.prototype._getActiveSearchFolderId =
2607 function() {
2608 	var s = this._activeSearch && this._activeSearch.search;
2609 	return s && s.folderId;
2610 };
2611 
2612 /**
2613  * @private
2614  */
2615 ZmMailListController.prototype._getActiveSearchFolder =
2616 function() {
2617 	var id = this._getActiveSearchFolderId();
2618 	return id && appCtxt.getById(id);
2619 };
2620 
2621 /* ZmMailListController.prototype._quickCommandMenuHandler = function(evt, batchCmd) {
2622     var selectedItems = this.getItems();
2623 
2624     ZmListController.prototype._quickCommandMenuHandler.call(this, evt);
2625 
2626     if (!selectedItems || !selectedItems.length) {return;}
2627 
2628     var menuItem = evt.dwtObj;
2629     var quickCommand = menuItem.getData(Dwt.KEY_OBJECT);
2630     if (!quickCommand) {return;}
2631             
2632     var actions = quickCommand.actions;
2633     var len = actions.length;
2634     for (var i = 0; i < len; i++) {
2635         var action = actions[i];
2636         if (!action.isActive) {continue;}
2637         var actionValue = action.value;
2638         if (action.type == ZmQuickCommandAction[ZmFilterRule.A_NAME_FLAG]) {
2639             if (actionValue == "read" || actionValue == "unread") {
2640                 this._doMarkRead(selectedItems, (actionValue == "read"));
2641             }
2642         }
2643     }
2644 };
2645 */
2646 
2647 /**
2648 * Deletes one or more items from the list.
2649 *
2650 * @param items			[Array]			list of items to delete
2651 * @param hardDelete		[boolean]*		if true, physically delete items
2652 * @param attrs			[Object]*		additional attrs for SOAP command
2653 * @param confirmDelete  [Boolean]       user already confirmed hard delete (see ZmBriefcaseController.prototype._doDelete and ZmBriefcaseController.prototype._doDelete2)
2654 *
2655 * @private
2656 */
2657 ZmMailListController.prototype._doDelete =
2658 function(items, hardDelete, attrs, confirmDelete) {
2659 
2660     var messages = AjxUtil.toArray(items);
2661     if (!messages.length) { return; }
2662 
2663     // Check if need to warn the user about violating the keep retention policy.  If a warning
2664     // dialog is displayed, the callback from that dialog allows the user to delete messages
2665     var warningIssued = this._doRetentionPolicyWarning(messages,
2666         ZmListController.prototype._doDelete, [hardDelete, attrs, false]);
2667     if (!warningIssued) {
2668         // No retention policy, or all the chosen messages fall outside the retention period.
2669         ZmListController.prototype._doDelete.call(this, messages, hardDelete, attrs, confirmDelete);
2670     }
2671 };
2672 
2673 ZmMailListController.prototype._doMove =
2674 function(items, destinationFolder, attrs, isShiftKey) {
2675     var messages = AjxUtil.toArray(items);
2676     if (!messages.length) { return; }
2677 
2678     var warningIssued = false;
2679 
2680     if (destinationFolder && (destinationFolder.nId == ZmFolder.ID_TRASH)) {
2681         // Check if need to warn the user about violating the keep retention policy.  If a warning
2682         // dialog is displayed, the callback from that dialog allows the user to trash messages
2683         warningIssued = this._doRetentionPolicyWarning(messages,
2684             ZmListController.prototype._doMove, [destinationFolder, attrs, isShiftKey]);
2685     }
2686     if (!warningIssued) {
2687         // No retention policy, or all the chosen messages fall outside the retention period.
2688         ZmListController.prototype._doMove.call(this, items, destinationFolder, attrs, isShiftKey);
2689     }
2690 }
2691 
2692 ZmMailListController.prototype._doRetentionPolicyWarning =
2693 function(messages, callbackFunc, args) {
2694     var numWithinRetention = 0;
2695     var folder;
2696     var keepPolicy;
2697     var now = new Date();
2698     var validMessages = [];
2699     var policyStartMsec = {};
2700     for (var i = 0; i < messages.length; i++) {
2701         var folderId = messages[i].folderId;
2702         if (!policyStartMsec[folderId]) {
2703             policyStartMsec[folderId] = -1;
2704             folder = appCtxt.getById(folderId);
2705             keepPolicy = (folder ? folder.getRetentionPolicy(ZmOrganizer.RETENTION_KEEP) : null);
2706             if (keepPolicy) {
2707                 // Calculate the current start of this folder's keep (retention) period
2708                 var keepLifetimeMsec = folder.getRetentionPolicyLifetimeMsec(keepPolicy);
2709                 policyStartMsec[folderId] = now.getTime() - keepLifetimeMsec;
2710             }
2711         }
2712         if (policyStartMsec[folderId] > 0) {
2713             // Determine which messages are not affected by the retention policy (i.e.
2714             // their age exceeds that mandated by the policy)
2715             if (messages[i].date < policyStartMsec[folderId]) {
2716                 validMessages.push(messages[i]);
2717             }
2718         } else {
2719             // The message's folder does not have a retention policy
2720             validMessages.push(messages[i]);
2721         }
2722     }
2723 
2724     numWithinRetention = messages.length - validMessages.length;
2725     if (numWithinRetention > 0) {
2726         // Create the base warning text
2727         var warningMsg = ((numWithinRetention == 1) ?
2728                             ZmMsg.retentionKeepWarning :
2729                             AjxMessageFormat.format(ZmMsg.retentionKeepWarnings,[numWithinRetention.toString()])) +
2730                          "<BR><BR>";
2731 
2732         if (validMessages.length == 0) {
2733             // All the chosen messages fall within the retention period
2734             this._showSimpleRetentionWarning(warningMsg, messages, callbackFunc, args);
2735         } else {
2736             // A mix of messages - some outside the retention period, some within.
2737             warningMsg += ZmMsg.retentionDeleteAllExplanation + "<BR><BR>" +
2738                           ((validMessages.length == 1) ?
2739                               ZmMsg.retentionDeleteValidExplanation :
2740                               AjxMessageFormat.format(ZmMsg.retentionDeleteValidExplanations,[validMessages.length.toString()]));
2741             this._showRetentionWarningDialog(warningMsg, messages, validMessages, callbackFunc, args);
2742         }
2743     }
2744 
2745     return numWithinRetention != 0;
2746 }
2747 
2748 ZmMailListController.prototype._showSimpleRetentionWarning =
2749 function(warningMsg, messages, callbackFunc, args) {
2750     warningMsg += (messages.length == 1) ? ZmMsg.retentionDeleteOne :
2751                                            ZmMsg.retentionDeleteMultiple;
2752     // This assumes that the first parameter of the OK function is the messages
2753     // to be processed, followed by other arbitrary parameters
2754     var okArgs = [messages].concat(args);
2755     var callback = new AjxCallback(this, callbackFunc,okArgs);
2756 
2757     var okCancelDialog = appCtxt.getOkCancelMsgDialog();
2758     okCancelDialog.registerCallback(DwtDialog.OK_BUTTON,
2759         this._handleRetentionWarningOK, this, [okCancelDialog, callback]);
2760     okCancelDialog.setMessage(warningMsg, DwtMessageDialog.WARNING_STYLE);
2761     okCancelDialog.setVisible(true);
2762     okCancelDialog.popup();
2763 }
2764 
2765 
2766 ZmMailListController.prototype._showRetentionWarningDialog =
2767 function(warningMsg, messages, validMessages, callbackFunc, args) {
2768     var retentionDialog = appCtxt.getRetentionWarningDialog();
2769 	retentionDialog.reset();
2770 
2771     var callback;
2772     // This assumes that the first parameter of the OK function is the messages
2773     // to be processed, followed by other arbitrary parameters
2774     var allArgs = [messages].concat(args);
2775     callback = new AjxCallback(this, callbackFunc, allArgs);
2776     retentionDialog.registerCallback(ZmRetentionWarningDialog.DELETE_ALL_BUTTON,
2777         this._handleRetentionWarningOK, this, [retentionDialog, callback]);
2778 
2779     var oldArgs = [validMessages].concat(args);
2780     callback = new AjxCallback(this, callbackFunc, oldArgs);
2781     retentionDialog.registerCallback(ZmRetentionWarningDialog.DELETE_VALID_BUTTON,
2782         this._handleRetentionWarningOK, this, [retentionDialog, callback]);
2783 
2784     retentionDialog.setMessage(warningMsg, DwtMessageDialog.WARNING_STYLE);
2785     retentionDialog.setVisible(true);
2786     retentionDialog.popup();
2787 };
2788 
2789 ZmMailListController.prototype._handleRetentionWarningOK =
2790 function(dialog, callback) {
2791     dialog.popdown();
2792     callback.run();
2793 };
2794 
2795 // done here since operations may not be defined at parse time
2796 ZmMailListController.prototype._setStatics = function() {
2797 
2798 	if (!ZmMailListController.INVITE_REPLY_MAP) {
2799 
2800 		ZmMailListController.INVITE_REPLY_MAP = {};
2801 		ZmMailListController.INVITE_REPLY_MAP[ZmOperation.INVITE_REPLY_ACCEPT]		= ZmOperation.REPLY_ACCEPT;
2802 		ZmMailListController.INVITE_REPLY_MAP[ZmOperation.INVITE_REPLY_DECLINE]		= ZmOperation.REPLY_DECLINE;
2803 		ZmMailListController.INVITE_REPLY_MAP[ZmOperation.INVITE_REPLY_TENTATIVE]	= ZmOperation.REPLY_TENTATIVE;
2804 
2805 		ZmMailListController.REPLY_ACTION_MAP = {};
2806 		ZmMailListController.REPLY_ACTION_MAP[ZmOperation.REPLY_ACCEPT_NOTIFY]		= ZmOperation.REPLY_ACCEPT;
2807 		ZmMailListController.REPLY_ACTION_MAP[ZmOperation.REPLY_ACCEPT_IGNORE]		= ZmOperation.REPLY_ACCEPT;
2808 		ZmMailListController.REPLY_ACTION_MAP[ZmOperation.REPLY_DECLINE_NOTIFY]		= ZmOperation.REPLY_DECLINE;
2809 		ZmMailListController.REPLY_ACTION_MAP[ZmOperation.REPLY_DECLINE_IGNORE]		= ZmOperation.REPLY_DECLINE;
2810 		ZmMailListController.REPLY_ACTION_MAP[ZmOperation.REPLY_TENTATIVE_NOTIFY]	= ZmOperation.REPLY_TENTATIVE;
2811 		ZmMailListController.REPLY_ACTION_MAP[ZmOperation.REPLY_TENTATIVE_IGNORE]	= ZmOperation.REPLY_TENTATIVE;
2812 
2813 		// convert key mapping to operation
2814 		ZmMailListController.ACTION_CODE_TO_OP = {};
2815 		ZmMailListController.ACTION_CODE_TO_OP[ZmKeyMap.REPLY]			= ZmOperation.REPLY;
2816 		ZmMailListController.ACTION_CODE_TO_OP[ZmKeyMap.REPLY_ALL]		= ZmOperation.REPLY_ALL;
2817 		ZmMailListController.ACTION_CODE_TO_OP[ZmKeyMap.FORWARD_INLINE]	= ZmOperation.FORWARD_INLINE;
2818 		ZmMailListController.ACTION_CODE_TO_OP[ZmKeyMap.FORWARD_ATT]	= ZmOperation.FORWARD_ATT;
2819 	}
2820 };
2821 
2822 // Mail can be grouped by msg or conv
2823 ZmMailListController.prototype.supportsGrouping = function() {
2824     return true;
2825 };
2826