1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc.
  5  *
  6  * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at: https://www.zimbra.com/license
  9  * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15
 10  * have been added to cover use of software over a computer network and provide for limited attribution
 11  * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B.
 12  *
 13  * Software distributed under the License is distributed on an "AS IS" basis,
 14  * WITHOUT WARRANTY OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing rights and limitations under the License.
 16  * The Original Code is Zimbra Open Source Web Client.
 17  * The Initial Developer of the Original Code is Zimbra, Inc.  All rights to the Original Code were
 18  * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015.
 19  *
 20  * All portions of the code are Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @overview
 26  * 
 27  * This file defines a folder tree controller.
 28  *
 29  */
 30 
 31 /**
 32  * Creates a folder tree controller.
 33  * @class
 34  * This class controls a tree display of folders.
 35  *
 36  * @param {Constant}	type			the type of organizer we are displaying/controlling ({@link ZmOrganizer.FOLDER} or {@link ZmOrganizer.SEARCH})
 37  * @param {DwtDropTarget}		dropTgt		the drop target for this type
 38  * 
 39  * @extends		ZmTreeController
 40  */
 41 ZmFolderTreeController = function(type, dropTgt) {
 42 
 43 	if (!arguments.length) { return; }
 44 
 45 	ZmTreeController.call(this, (type || ZmOrganizer.FOLDER));
 46 
 47 	this._listeners[ZmOperation.NEW_FOLDER]				= this._newListener.bind(this);
 48 	this._listeners[ZmOperation.PRIORITY_FILTER]		= this._priorityFilterListener.bind(this);
 49 	this._listeners[ZmOperation.RENAME_FOLDER]			= this._renameListener.bind(this);
 50 	this._listeners[ZmOperation.SHARE_FOLDER]			= this._shareFolderListener.bind(this);
 51 	this._listeners[ZmOperation.EMPTY_FOLDER]			= this._emptyListener.bind(this);
 52 	this._listeners[ZmOperation.RECOVER_DELETED_ITEMS]	= this._recoverListener.bind(this);
 53 	this._listeners[ZmOperation.SYNC_OFFLINE_FOLDER]	= this._syncOfflineFolderListener.bind(this);
 54 };
 55 
 56 ZmFolderTreeController.prototype = new ZmTreeController;
 57 ZmFolderTreeController.prototype.constructor = ZmFolderTreeController;
 58 
 59 ZmFolderTreeController.prototype.isZmFolderTreeController = true;
 60 ZmFolderTreeController.prototype.toString = function() { return "ZmFolderTreeController"; };
 61 
 62 // Public methods
 63 
 64 /**
 65  * Shows the folder tree with certain folders hidden.
 66  * 
 67  * @param	{Hash}	params		a hash of parameters
 68  * @param	{Array}	params.omit		an array of folder ids to omit
 69  * @param	{ZmAccount}	params.account		the account
 70  */
 71 ZmFolderTreeController.prototype.show =
 72 function(params) {
 73 
 74 	return ZmTreeController.prototype.show.call(this, params);
 75 };
 76 
 77 /**
 78 * Resets and enables/disables operations based on context.
 79 *
 80 * @param {DwtControl}	parent		the widget that contains the operations
 81 * @param {int}			id			the id of the currently selected/activated organizer
 82 */
 83 ZmFolderTreeController.prototype.resetOperations =
 84 function(parent, type, id) {
 85 
 86 	var emptyText = ZmMsg.emptyFolder; //ZmMsg.empty + (ZmFolder.MSG_KEY[id]?" "+ZmFolder.MSG_KEY[id] : "");
 87 	var folder = appCtxt.getById(id);
 88 	var hasContent = ((folder.numTotal > 0) || (folder.children && (folder.children.size() > 0)));
 89 
 90     // disable empty folder option for inbox, sent and drafts: bug 66656
 91     var isEmptyFolderAllowed = true;
 92     var y = folder.rid;
 93     if (y == ZmFolder.ID_ROOT || y == ZmFolder.ID_INBOX || y == ZmFolder.ID_SENT || y == ZmFolder.ID_DRAFTS) {
 94         isEmptyFolderAllowed = false;
 95     }
 96 
 97 	// user folder or Folders header
 98 	var nId = ZmOrganizer.normalizeId(id, this.type);
 99 	if (nId == ZmOrganizer.ID_ROOT || (!folder.isSystem() && !folder.isSystemEquivalent()) /*&& !folder.isSyncIssuesFolder()*/) {
100 		var isShareVisible = (!folder.link || folder.isAdmin());
101         if (appCtxt.isOffline) {
102             isShareVisible = !folder.getAccount().isMain && folder.getAccount().isZimbraAccount;
103         }
104 		parent.enableAll(true);
105 		var isSubFolderOfReadOnly = folder.parent && folder.parent.isReadOnly();
106 		parent.enable([ZmOperation.DELETE_WITHOUT_SHORTCUT, ZmOperation.MOVE, ZmOperation.EDIT_PROPS], !isSubFolderOfReadOnly);
107 		parent.enable(ZmOperation.SYNC, folder.isFeed()/* || folder.hasFeeds()*/);
108 		parent.enable(ZmOperation.SYNC_ALL, folder.isFeed() || folder.hasFeeds());
109 		parent.enable(ZmOperation.SHARE_FOLDER, isShareVisible);
110 		parent.enable(ZmOperation.EMPTY_FOLDER, ((hasContent || folder.link) && isEmptyFolderAllowed && !appCtxt.isExternalAccount()));	// numTotal is not set for shared folders
111 		parent.enable(ZmOperation.RENAME_FOLDER, !(isSubFolderOfReadOnly || folder.isDataSource() || appCtxt.isExternalAccount()));		// dont allow datasource'd folder to be renamed via overview
112 		parent.enable(ZmOperation.NEW_FOLDER, !(folder.disallowSubFolder || appCtxt.isExternalAccount()));
113 
114 		if (folder.isRemote() && folder.isReadOnly()) {
115 			parent.enable([ZmOperation.NEW_FOLDER, ZmOperation.MARK_ALL_READ, ZmOperation.EMPTY_FOLDER], false);
116 		}
117         if (appCtxt.isExternalAccount()) {
118 			parent.enable([ZmOperation.DELETE_WITHOUT_SHORTCUT, ZmOperation.MOVE], false);
119 		}
120 	}
121 	// system folder
122 	else {
123 		if (folder.isSystemEquivalent()) {
124 			nId = folder.getSystemEquivalentFolderId();
125 		}
126 		parent.enableAll(false);
127 		// can't create folders under Drafts or Junk
128 		if (!folder.disallowSubFolder &&
129 			(nId == ZmFolder.ID_INBOX ||
130 			 nId == ZmFolder.ID_SENT  ||
131 			 nId == ZmFolder.ID_TRASH))
132 		{
133 			parent.enable(ZmOperation.NEW_FOLDER, true);
134 		}
135 		// "Empty" for Chats, Junk and Trash
136 		if (nId == ZmFolder.ID_SPAM  ||
137 			nId == ZmFolder.ID_TRASH ||
138 			nId == ZmFolder.ID_CHATS)
139 		{
140 			if (nId == ZmFolder.ID_SPAM) {
141 				emptyText = ZmMsg.emptyJunk;
142 			} else if (nId == ZmFolder.ID_TRASH) {
143 				 emptyText = ZmMsg.emptyTrash;
144 			}
145 			parent.enable(ZmOperation.EMPTY_FOLDER, hasContent);
146 		}
147 		// only allow Inbox and Sent system folders to be share-able for now
148 		if (!folder.link && (nId == ZmFolder.ID_INBOX || nId == ZmFolder.ID_SENT || nId == ZmFolder.ID_DRAFTS)) {
149 			parent.enable([ZmOperation.SHARE_FOLDER, ZmOperation.EDIT_PROPS], true);
150 		}
151         if (appCtxt.multiAccounts) {
152             var isShareVisible = !folder.getAccount().isMain && folder.getAccount().isZimbraAccount;
153             if(nId == ZmFolder.ID_SPAM || nId == ZmFolder.ID_TRASH) {
154                 isShareVisible = false;
155             }
156             parent.enable([ZmOperation.SHARE_FOLDER, ZmOperation.EDIT_PROPS], isShareVisible);
157         }
158 		// bug fix #30435 - enable empty folder for sync failures folder
159 		if (appCtxt.isOffline && nId == ZmOrganizer.ID_SYNC_FAILURES && hasContent) {
160 			parent.enable(ZmOperation.EMPTY_FOLDER, true);
161 		}
162 	}
163 
164 	parent.enable(ZmOperation.OPEN_IN_TAB, true);
165 	parent.enable(ZmOperation.EXPAND_ALL, (folder.size() > 0));
166 	if (nId != ZmOrganizer.ID_ROOT && !folder.isReadOnly()) {
167 		// always enable for shared folders since we dont get this info from server
168 		parent.enable(ZmOperation.MARK_ALL_READ, !folder.isRemoteRoot() && (folder.numUnread > 0 || folder.link));
169 	}
170 
171 	var op = parent.getOp(ZmOperation.EMPTY_FOLDER);
172 	if (op) {
173 		op.setText(emptyText);
174 	}
175 
176     var isTrash = (nId == ZmOrganizer.ID_TRASH);
177 	// are there any external accounts associated to this folder?
178 	var button = parent.getOp(ZmOperation.SYNC);
179 	if (button) {
180 		var syncAllButton = parent.getOp(ZmOperation.SYNC_ALL);
181 		var hasFeeds = folder.hasFeeds();
182 		if (folder.isFeed()) {
183 			button.setEnabled(true);
184 			button.setVisible(true);
185 			button.setText(ZmMsg.checkFeed);
186 			if (syncAllButton) {
187 				syncAllButton.setEnabled(true);
188 				syncAllButton.setVisible(true);
189 				syncAllButton.setText(ZmMsg.checkAllFeed);
190 			}
191 		}
192 		else if (hasFeeds && !isTrash) {
193 			if (syncAllButton){
194 				syncAllButton.setEnabled(true);
195 				syncAllButton.setVisible(true);
196 				syncAllButton.setText(ZmMsg.checkAllFeed);
197 			}
198 		}
199 		else {
200 			var isEnabled = appCtxt.get(ZmSetting.POP_ACCOUNTS_ENABLED) || appCtxt.get(ZmSetting.IMAP_ACCOUNTS_ENABLED);
201 			if (!appCtxt.isOffline && isEnabled) {
202 				var dsCollection = AjxDispatcher.run("GetDataSourceCollection");
203 				var dataSources = dsCollection.getItemsFor(ZmOrganizer.normalizeId(folder.id));
204 				if (dataSources.length > 0) {
205 					button.setText(ZmMsg.checkExternalMail);
206 					button.setEnabled(true);
207 					button.setVisible(true);
208 				} else {
209 					button.setVisible(false);
210 				}
211 			}
212 			else {
213 				button.setVisible(false);
214 			}
215 
216 			if ((!hasFeeds || isTrash) && syncAllButton) {
217 				syncAllButton.setVisible(false);
218 			}
219 		}
220 	}
221 
222 	button = parent.getOp(ZmOperation.SYNC_OFFLINE_FOLDER);
223 	if (button) {
224 		if (!folder.isOfflineSyncable) {
225 			button.setVisible(false);
226 		} else {
227 			button.setVisible(true);
228 			button.setEnabled(true);
229 			var text = (folder.isOfflineSyncing)
230 				? ZmMsg.syncOfflineFolderOff : ZmMsg.syncOfflineFolderOn;
231 			button.setText(text);
232 		}
233 	}
234 	var priorityInboxEnabled = appCtxt.get(ZmSetting.PRIORITY_INBOX_ENABLED);
235 	var priorityInboxOp = parent.getOp(ZmOperation.PRIORITY_FILTER);
236 	if (priorityInboxOp) {
237 		priorityInboxOp.setVisible(priorityInboxEnabled);
238 		priorityInboxOp.setEnabled(priorityInboxEnabled);
239 	}
240 	this._enableRecoverDeleted(parent, isTrash);
241 
242 	// we always enable sharing in case we're in multi-mbox mode
243 	this._resetButtonPerSetting(parent, ZmOperation.SHARE_FOLDER, appCtxt.get(ZmSetting.SHARING_ENABLED));
244 };
245 
246 
247 // Private methods
248 
249 /**
250  * Returns ops available for "Folders" container.
251  * 
252  * @private
253  */
254 ZmFolderTreeController.prototype._getHeaderActionMenuOps =
255 function() {
256     if (appCtxt.isExternalAccount()) {
257         return [ZmOperation.EXPAND_ALL];
258     }
259 	return [
260 		ZmOperation.NEW_FOLDER,
261 		ZmOperation.SEP,
262 		ZmOperation.PRIORITY_FILTER,
263 		ZmOperation.EXPAND_ALL,
264 		ZmOperation.SYNC,
265 		ZmOperation.FIND_SHARES
266 	];
267 };
268 
269 /**
270  * Returns ops available for folder items.
271  * 
272  * @private
273  */
274 ZmFolderTreeController.prototype._getActionMenuOps = function() {
275 
276 	return [
277 		ZmOperation.NEW_FOLDER,
278 		ZmOperation.SYNC,
279 		ZmOperation.SYNC_ALL,
280 		ZmOperation.MARK_ALL_READ,
281 		ZmOperation.EMPTY_FOLDER,
282 		ZmOperation.RECOVER_DELETED_ITEMS,
283 		ZmOperation.SHARE_FOLDER,
284 		ZmOperation.MOVE,
285 		ZmOperation.DELETE_WITHOUT_SHORTCUT,
286 		ZmOperation.RENAME_FOLDER,
287 		ZmOperation.EDIT_PROPS,
288 		ZmOperation.SYNC_OFFLINE_FOLDER,
289 		ZmOperation.OPEN_IN_TAB,
290 		ZmOperation.EXPAND_ALL
291 	];
292 };
293 
294 /**
295  * @private
296  */
297 ZmFolderTreeController.prototype._getAllowedSubTypes =
298 function() {
299 	var types = {};
300 	types[ZmOrganizer.FOLDER] = true;
301 	types[ZmOrganizer.SEARCH] = true;
302 	return types;
303 };
304 
305 /**
306  * Returns a "New Folder" dialog.
307  * 
308  * @private
309  */
310 ZmFolderTreeController.prototype._getNewDialog =
311 function() {
312 	return appCtxt.getNewFolderDialog();
313 };
314 
315 /**
316  * Returns a "Rename Folder" dialog.
317  * 
318  * @private
319  */
320 ZmFolderTreeController.prototype._getRenameDialog =
321 function() {
322 	return appCtxt.getRenameFolderDialog();
323 };
324 
325 /**
326  * Called when a left click occurs (by the tree view listener). The folder that
327  * was clicked may be a search, since those can appear in Trash within the folder tree. The
328  * appropriate search will be performed.
329  *
330  * @param {ZmOrganizer}		folder		the folder or search that was clicked
331  * 
332  * @private
333  */
334 ZmFolderTreeController.prototype._itemClicked = function(folder, openInTab) {
335 
336 	// bug 41196 - turn off new mail notifier if inactive account folder clicked
337 	if (appCtxt.isOffline) {
338 		var acct = folder.getAccount();
339 		if (acct && acct.inNewMailMode) {
340 			acct.inNewMailMode = false;
341 			var allContainers = appCtxt.getOverviewController()._overviewContainer;
342 			for (var i in allContainers) {
343 				allContainers[i].updateAccountInfo(acct, true, true);
344 			}
345 		}
346 	}
347 
348 	if (folder.type == ZmOrganizer.SEARCH) {
349 		// if the clicked item is a search (within the folder tree), hand
350 		// it off to the search tree controller
351 		var stc = this._opc.getTreeController(ZmOrganizer.SEARCH);
352 		stc._itemClicked(folder, openInTab);
353 	} else if (folder.id == ZmFolder.ID_ATTACHMENTS) {
354 		var attController = AjxDispatcher.run("GetAttachmentsController");
355 		attController.show();
356 	}
357     else {
358 		var searchFor = ZmId.SEARCH_MAIL;
359 		if (folder.isInTrash()) {
360 			var app = appCtxt.getCurrentAppName();
361 			// if other apps add Trash to their folder tree, set appropriate type here:
362 			if (app == ZmApp.CONTACTS) {
363 				searchFor = ZmItem.CONTACT;
364 			}
365 		}
366 		var sc = appCtxt.getSearchController();
367 		var acct = folder.getAccount();
368 
369 		var sortBy = appCtxt.get(ZmSetting.SORTING_PREF, folder.nId);
370 		if (!sortBy) {
371 			sortBy = (sc.currentSearch && folder.nId == sc.currentSearch.folderId) ? null : ZmSearch.DATE_DESC;
372 		}
373 		else {
374 			//user may have saved folder with From search then switched views; don't allow From sort in conversation mode
375 			var groupMode = appCtxt.getApp(ZmApp.MAIL).getGroupMailBy();
376 			if (groupMode == ZmItem.CONV && (sortBy == ZmSearch.NAME_ASC || sortBy == ZmSearch.NAME_DESC)) {
377 				sortBy = appCtxt.get(ZmSetting.SORTING_PREF, appCtxt.getCurrentViewId());  //default to view preference
378 				if (!sortBy) {
379 					sortBy = ZmSearch.DATE_DESC; //default
380 				}
381 				appCtxt.set(ZmSetting.SORTING_PREF, sortBy, folder.nId);
382 			}
383 		}
384 		var params = {
385 			query:          folder.createQuery(),
386 			searchFor:      searchFor,
387 			getHtml:        folder.nId == ZmFolder.ID_DRAFTS || appCtxt.get(ZmSetting.VIEW_AS_HTML),
388 			types:          folder.nId == ZmOrganizer.ID_SYNC_FAILURES ? [ZmItem.MSG] : null, // for Sync Failures folder, always show in traditional view
389 			sortBy:         sortBy,
390 			accountName:    acct && acct.name,
391 			userInitiated:  openInTab,
392 			origin:         ZmId.SEARCH
393 		};
394 
395 		sc.resetSearchAllAccounts();
396 
397 		if (appCtxt.multiAccounts) {
398 			// make sure we have permissions for this folder (in case an "external"
399 			// server was down during account load)
400 			if (folder.link && folder.perm == null) {
401 				var folderTree = appCtxt.getFolderTree(acct);
402 				if (folderTree) {
403 					var callback = new AjxCallback(this, this._getPermissionsResponse, [params]);
404 					folderTree.getPermissions({callback:callback, folderIds:[folder.id]});
405 				}
406 				return;
407 			}
408 
409 			if (appCtxt.isOffline && acct.hasNotSynced() && !acct.__syncAsked) {
410 				acct.__syncAsked = true;
411 
412 				var dialog = appCtxt.getYesNoMsgDialog();
413 				dialog.registerCallback(DwtDialog.YES_BUTTON, this._syncAccount, this, [dialog, acct]);
414 				dialog.setMessage(ZmMsg.neverSyncedAsk, DwtMessageDialog.INFO_STYLE);
415 				dialog.popup();
416 			}
417 		}
418 
419 		sc.search(params);
420 	}
421 };
422 
423 /**
424  * @private
425  */
426 ZmFolderTreeController.prototype._syncAccount =
427 function(dialog, account) {
428 	dialog.popdown();
429 	account.sync();
430 };
431 
432 /**
433  * @private
434  */
435 ZmFolderTreeController.prototype._getPermissionsResponse =
436 function(params) {
437 	appCtxt.getSearchController().search(params);
438 };
439 
440 
441 // Actions
442 
443 /**
444  * @private
445  */
446 ZmFolderTreeController.prototype._doSync =
447 function(folder) {
448 	var dsc = AjxDispatcher.run("GetDataSourceCollection");
449 	var nFid = ZmOrganizer.normalizeId(folder.id);
450 	var dataSources = dsc.getItemsFor(nFid);
451 
452 	if (dataSources.length > 0) {
453 		dsc.importMailFor(nFid);
454 	}
455 	else {
456 		ZmTreeController.prototype._doSync.call(this, folder);
457 	}
458 };
459 
460 /**
461  * @private
462  */
463 ZmFolderTreeController.prototype._syncFeeds =
464 function(folder) {
465 	if (!appCtxt.isOffline && folder && !folder.isFeed()) {
466 		var dataSources = (appCtxt.get(ZmSetting.POP_ACCOUNTS_ENABLED) || appCtxt.get(ZmSetting.IMAP_ACCOUNTS_ENABLED))
467 			? folder.getDataSources(null, true) : null;
468 
469 		if (dataSources) {
470 			var dsc = AjxDispatcher.run("GetDataSourceCollection");
471 			dsc.importMail(dataSources);
472 			return;
473 		}
474 	}
475 
476 	ZmTreeController.prototype._syncFeeds.call(this, folder);
477 };
478 
479 /**
480  * Adds the new item to the tree.
481  *
482  * @param {ZmTreeView}		treeView		a tree view
483  * @param {DwtTreeItem}		parentNode		the node under which to add the new one
484  * @param {ZmOrganizer}		organizer		the organizer for the new node
485  * @param {int}				idx				theposition at which to add the new node
486  * @return	{DwtTreeItem}	the resulting item
487  * 
488  * @private
489  */
490 ZmFolderTreeController.prototype._addNew =
491 function(treeView, parentNode, organizer, idx) {
492 	if (ZmFolder.HIDE_ID[organizer.id]) {
493 		return false;
494 	}
495 	return treeView._addNew(parentNode, organizer, idx);
496 };
497 
498 // Listeners
499 
500 /**
501  * Deletes a folder. If the folder is in Trash, it is hard-deleted. Otherwise, it
502  * is moved to Trash (soft-delete). If the folder is Trash or Junk, it is emptied.
503  * A warning dialog will be shown before the Junk folder is emptied.
504  *
505  * @param {DwtUiEvent}	ev		the UI event
506  * 
507  * @private
508  */
509 ZmFolderTreeController.prototype._deleteListener =
510 function(ev) {
511 	var organizer = this._getActionedOrganizer(ev);
512 
513 	// bug fix #35405 - accounts with disallowSubFolder flag set (eg Yahoo) do not support moving folder to Trash
514 	var trashFolder = appCtxt.isOffline ? this.getDataTree().getById(ZmFolder.ID_TRASH) : null;
515 	if (trashFolder && trashFolder.disallowSubFolder && organizer.numTotal > 0) {
516 		var d = appCtxt.getMsgDialog();
517 		d.setMessage(ZmMsg.errorCannotDeleteFolder);
518 		d.popup();
519 		return;
520 	}
521 
522 	// TODO: not sure what SPAM is doing in here - can you delete it?
523 	if (organizer.nId == ZmFolder.ID_SPAM || organizer.isInTrash() || (trashFolder && trashFolder.disallowSubFolder)) {
524 		this._pendingActionData = organizer;
525 		var ds = this._deleteShield = appCtxt.getOkCancelMsgDialog();
526 		ds.reset();
527 		ds.registerCallback(DwtDialog.OK_BUTTON, this._deleteShieldYesCallback, this, organizer);
528 		ds.registerCallback(DwtDialog.CANCEL_BUTTON, this._clearDialog, this, this._deleteShield);
529 		var confirm;
530 		if (organizer.type === ZmOrganizer.SEARCH) {
531 			confirm = ZmMsg.confirmDeleteSavedSearch;
532 		}
533 		else if (organizer.nId == ZmFolder.ID_TRASH) {
534 			confirm = ZmMsg.confirmEmptyTrashFolder;
535 		}
536 		else if (organizer.nId == ZmFolder.ID_SPAM) {
537 			confirm = ZmMsg.confirmEmptyFolder;
538 		}
539 		else {
540 			// TODO: should probably split out msgs by folder type
541 			confirm = ZmMsg.confirmDeleteFolder;
542 		}
543 		var msg = AjxMessageFormat.format(confirm, organizer.getName());
544 		ds.setMessage(msg, DwtMessageDialog.WARNING_STYLE);
545 		ds.popup();
546 	}
547 	else {
548 		this._doMove(organizer, appCtxt.getById(ZmFolder.ID_TRASH));
549 	}
550 };
551 
552 /**
553  * Empties a folder.
554  * It removes all the items in the folder except sub-folders.
555  * If the folder is Trash, it empties even the sub-folders.
556  * A warning dialog will be shown before any folder is emptied.
557  *
558  * @param {DwtUiEvent}		ev		the UI event
559  * 
560  * @private
561  */
562 ZmFolderTreeController.prototype._emptyListener =
563 function(ev) {
564 	this._getEmptyShieldWarning(ev);
565 };
566 
567 ZmFolderTreeController.prototype._recoverListener =
568 function(ev) {
569 	appCtxt.getDumpsterDialog().popup(this._getSearchFor(), this._getSearchTypes());
570 };
571 
572 ZmFolderTreeController.prototype._getSearchFor =
573 function(ev) {
574 	return ZmId.SEARCH_MAIL; // Fallback value; subclasses should return differently
575 };
576 
577 ZmFolderTreeController.prototype._getSearchTypes =
578 function(ev) {
579 	return [ZmItem.MSG]; // Fallback value; subclasses should return differently
580 };
581 
582 /**
583  * Toggles on/off flag for syncing IMAP folder with server. Only for offline use.
584  *
585  * @param {DwtUiEvent}	ev	the UI event
586  * 
587  * @private
588  */
589 ZmFolderTreeController.prototype._syncOfflineFolderListener =
590 function(ev) {
591 	var folder = this._getActionedOrganizer(ev);
592 	if (folder) {
593 		folder.toggleSyncOffline();
594 	}
595 };
596 
597 /**
598  * Don't allow dragging of system folders.
599  *
600  * @param {DwtDragEvent}	ev		the drag event
601  * 
602  * @private
603  */
604 ZmFolderTreeController.prototype._dragListener =
605 function(ev) {
606 	if (ev.action == DwtDragEvent.DRAG_START) {
607 		var folder = ev.srcControl.getData(Dwt.KEY_OBJECT);
608 		ev.srcData = {data:folder, controller:this};
609 		if (!(folder instanceof ZmFolder) || folder.isSystem() /*|| folder.isSyncIssuesFolder()*/) {
610 			ev.operation = Dwt.DND_DROP_NONE;
611 		}
612 	}
613 };
614 
615 /**
616  * Handles the potential drop of something onto a folder. When something is dragged over
617  * a folder, returns true if a drop would be allowed. When something is actually dropped,
618  * performs the move. If items are being dropped, the source data is not the items
619  * themselves, but an object with the items (data) and their controller, so they can be
620  * moved appropriately.
621  *
622  * @param {DwtDropEvent}	ev		the drop event
623  * 
624  * @private
625  */
626 ZmFolderTreeController.prototype._dropListener =
627 function(ev) {
628 
629 	var dropFolder = ev.targetControl.getData(Dwt.KEY_OBJECT);
630 	var data = ev.srcData.data;
631 	var isShiftKey = (ev.shiftKey || ev.uiEvent.shiftKey);
632 
633 	if (ev.action == DwtDropEvent.DRAG_ENTER) {
634 		if (!data) {
635 			ev.doIt = false;
636 			return;
637 		}
638 		var type = ev.targetControl.getData(ZmTreeView.KEY_TYPE);
639 		if (data instanceof ZmFolder) {
640 			ev.doIt = dropFolder.mayContain(data, type) && !dropFolder.disallowSubFolder;
641 		} else if (data instanceof ZmTag) {
642 			ev.doIt = false; // tags cannot be moved
643 		} else {
644 			if (this._dropTgt.isValidTarget(data)) {
645 				ev.doIt = dropFolder.mayContain(data, type);
646 
647 				var action;
648 				var actionData = AjxUtil.toArray(data);
649 
650 				// walk thru the array and find out what action is allowed
651 				for (var i = 0; i < actionData.length; i++) {
652 					if (actionData[i] instanceof ZmItem) {
653 						action |= actionData[i].getDefaultDndAction(isShiftKey);
654 					}
655 				}
656 
657 				var plusDiv = document.getElementById(DwtId.DND_PLUS_ID);
658 				if (action && plusDiv) {
659 					// TODO - what if action is ZmItem.DND_ACTION_BOTH ??
660 					var isCopy = ((action & ZmItem.DND_ACTION_COPY) != 0);
661 					Dwt.setVisibility(plusDiv, isCopy);
662 				}
663 			} else {
664 				ev.doIt = false;
665 			}
666 		}
667 	} else if (ev.action == DwtDropEvent.DRAG_DROP) {
668 		if (data instanceof ZmFolder) {
669 			this._doMove(data, dropFolder);
670 		} else {
671 			var ctlr = ev.srcData.controller;
672 			var items = (data instanceof Array) ? data : [data];
673 			if (appCtxt.multiAccounts && !isShiftKey && !dropFolder.getAccount().isMain &&
674 				this._isMovingAcrossAccount(items, dropFolder))
675 			{
676 				var dialog = appCtxt.getYesNoMsgDialog();
677 				dialog.registerCallback(DwtDialog.YES_BUTTON, this._continueMovingAcrossAccount, this, [dialog, ctlr, items, dropFolder]);
678 				dialog.setMessage(ZmMsg.moveAcrossAccountWarning, DwtMessageDialog.WARNING_STYLE);
679 				dialog.popup();
680 			}
681 			else {
682 				ctlr._doMove(items, dropFolder, null, isShiftKey);
683 			}
684 		}
685 	}
686 };
687 
688 ZmFolderTreeController.prototype._isMovingAcrossAccount =
689 function(items, dropFolder) {
690 	for (var i = 0; i < items.length; i++) {
691 		var item = items[i];
692 		var itemAcct = item.getAccount();
693 		if (itemAcct && itemAcct != dropFolder.getAccount()) {
694 			return true;
695 		}
696 	}
697 	return false;
698 };
699 
700 ZmFolderTreeController.prototype._continueMovingAcrossAccount =
701 function(dialog, ctlr, items, dropFolder) {
702 	dialog.popdown();
703 	ctlr._doMove(items, dropFolder);
704 };
705 
706 
707 ZmTreeController.prototype._priorityFilterListener =
708 function(ev) {
709 	var priorityFilterDialog = appCtxt.getPriorityMessageFilterDialog();
710 	ZmController.showDialog(priorityFilterDialog);
711 };
712 
713 /**
714  * @private
715  */
716 ZmFolderTreeController.prototype._shareFolderListener =
717 function(ev) {
718 	this._pendingActionData = this._getActionedOrganizer(ev);
719 	appCtxt.getSharePropsDialog().popup(ZmSharePropsDialog.NEW, this._pendingActionData);
720 };
721 
722 // Miscellaneous
723 
724 /**
725  * Returns a title for moving a folder.
726  * 
727  * @return	{String}	the title
728  * @private
729  */
730 ZmFolderTreeController.prototype._getMoveDialogTitle =
731 function() {
732 	return AjxMessageFormat.format(ZmMsg.moveFolder, this._pendingActionData.name);
733 };
734