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.
 28  *
 29  */
 30 
 31 /**
 32  * Creates a folder.
 33  * @class
 34  * This class represents a folder, which may contain mail. At some point, folders may be
 35  * able to contain contacts and/or appointments.
 36  *
 37  * @author Conrad Damon
 38  *
 39  * @param	{Hash}	params		a hash of parameters
 40  * @param {int}	params.id		the numeric ID
 41  * @param {String}	params.name		the name
 42  * @param {ZmOrganizer}	params.parent	the parent folder
 43  * @param {ZmTree}	params.tree		the tree model that contains this folder
 44  * @param {int}	params.numUnread	the number of unread items for this folder
 45  * @param {int}	params.numTotal		the number of items for this folder
 46  * @param {int}	params.sizeTotal	the total size of folder's items
 47  * @param {String}	params.url		the URL for this folder's feed
 48  * @param {String}	params.owner	the Owner for this organizer
 49  * @param {String}	params.oname	the Owner's name for this organizer, if remote folder
 50  * @param {String}	params.zid		the Zimbra ID of owner, if remote folder
 51  * @param {String}	params.rid		the Remote ID of organizer, if remote folder
 52  * @param {String}	params.restUrl	the REST URL of this organizer
 53  * 
 54  * @extends		ZmOrganizer
 55  */
 56 ZmFolder = function(params) {
 57 	if (arguments.length == 0) { return; }
 58 	params.type = params.type || ZmOrganizer.FOLDER;
 59 	ZmOrganizer.call(this, params);
 60 };
 61 
 62 ZmFolder.prototype = new ZmOrganizer;
 63 ZmFolder.prototype.constructor = ZmFolder;
 64 
 65 ZmFolder.prototype.isZmFolder = true;
 66 ZmFolder.prototype.toString = function() { return "ZmFolder"; };
 67 
 68 
 69 // needed to construct USER_ROOT if mail disabled
 70 ZmOrganizer.ORG_CLASS[ZmId.ORG_FOLDER] = "ZmFolder";
 71 
 72 ZmFolder.SEP 									= "/";							// path separator
 73 
 74 // system folders (see Mailbox.java in ZimbraServer for positive int consts)
 75 // Note: since these are defined as Numbers, and IDs come into our system as Strings,
 76 // we need to use == for comparisons (instead of ===, which will fail)
 77 ZmFolder.ID_LOAD_FOLDERS						= -3;							// special "Load remaining folders" placeholder
 78 ZmFolder.ID_OTHER								= -2;							// used for tcon value (see below)
 79 ZmFolder.ID_SEP									= -1;							// separator
 80 ZmFolder.ID_ROOT								= ZmOrganizer.ID_ROOT;
 81 ZmFolder.ID_INBOX								= ZmOrganizer.ID_INBOX;
 82 ZmFolder.ID_TRASH								= ZmOrganizer.ID_TRASH;
 83 ZmFolder.ID_SPAM								= ZmOrganizer.ID_SPAM;
 84 ZmFolder.ID_SENT								= 5;
 85 ZmFolder.ID_DRAFTS								= 6;
 86 ZmFolder.ID_CONTACTS							= ZmOrganizer.ID_ADDRBOOK;
 87 ZmFolder.ID_AUTO_ADDED							= ZmOrganizer.ID_AUTO_ADDED;
 88 ZmFolder.ID_TAGS	 							= 8;
 89 ZmFolder.ID_TASKS								= ZmOrganizer.ID_TASKS;
 90 ZmFolder.ID_SYNC_FAILURES						= ZmOrganizer.ID_SYNC_FAILURES;
 91 ZmFolder.ID_OUTBOX	 							= ZmOrganizer.ID_OUTBOX;
 92 ZmFolder.ID_CHATS	 							= ZmOrganizer.ID_CHATS;
 93 ZmFolder.ID_ATTACHMENTS                         = ZmOrganizer.ID_ATTACHMENTS;
 94 ZmFolder.ID_DLS									= ZmOrganizer.ID_DLS;
 95 
 96 // system folder names
 97 ZmFolder.MSG_KEY = {};
 98 ZmFolder.MSG_KEY[ZmFolder.ID_INBOX]				= "inbox";
 99 ZmFolder.MSG_KEY[ZmFolder.ID_TRASH]				= "trash";
100 ZmFolder.MSG_KEY[ZmFolder.ID_SPAM]				= "junk";
101 ZmFolder.MSG_KEY[ZmFolder.ID_SENT]				= "sent";
102 ZmFolder.MSG_KEY[ZmFolder.ID_DRAFTS]			= "drafts";
103 ZmFolder.MSG_KEY[ZmFolder.ID_CONTACTS]			= "contacts";
104 ZmFolder.MSG_KEY[ZmFolder.ID_AUTO_ADDED]		= "emailedContacts";
105 ZmFolder.MSG_KEY[ZmFolder.ID_TASKS]				= "tasks";
106 ZmFolder.MSG_KEY[ZmFolder.ID_TAGS]				= "tags";
107 ZmFolder.MSG_KEY[ZmOrganizer.ID_CALENDAR]		= "calendar";
108 ZmFolder.MSG_KEY[ZmOrganizer.ID_BRIEFCASE]		= "briefcase";
109 ZmFolder.MSG_KEY[ZmOrganizer.ID_CHATS]			= "chats";
110 ZmFolder.MSG_KEY[ZmOrganizer.ID_ALL_MAILBOXES]	= "allMailboxes";
111 ZmFolder.MSG_KEY[ZmFolder.ID_OUTBOX]			= "outbox";
112 ZmFolder.MSG_KEY[ZmFolder.ID_SYNC_FAILURES]		= "errorReports";
113 ZmFolder.MSG_KEY[ZmFolder.ID_ATTACHMENTS]       = "attachments";
114 
115 // system folder icons
116 ZmFolder.ICON = {};
117 ZmFolder.ICON[ZmFolder.ID_INBOX]				= "Inbox";
118 ZmFolder.ICON[ZmFolder.ID_TRASH]				= "Trash";
119 ZmFolder.ICON[ZmFolder.ID_SPAM]					= "SpamFolder";
120 ZmFolder.ICON[ZmFolder.ID_SENT]					= "SentFolder";
121 ZmFolder.ICON[ZmFolder.ID_SYNC_FAILURES]		= "SendReceive";
122 ZmFolder.ICON[ZmFolder.ID_OUTBOX]				= "Outbox";
123 ZmFolder.ICON[ZmFolder.ID_DRAFTS]				= "DraftFolder";
124 ZmFolder.ICON[ZmFolder.ID_CHATS]				= "ChatFolder";
125 ZmFolder.ICON[ZmFolder.ID_LOAD_FOLDERS]			= "Plus";
126 ZmFolder.ICON[ZmFolder.ID_ATTACHMENTS]          = "Attachment";
127 
128 // name to use within the query language
129 ZmFolder.QUERY_NAME = {};
130 ZmFolder.QUERY_NAME[ZmFolder.ID_INBOX]			= "inbox";
131 ZmFolder.QUERY_NAME[ZmFolder.ID_TRASH]			= "trash";
132 ZmFolder.QUERY_NAME[ZmFolder.ID_SPAM]			= "junk";
133 ZmFolder.QUERY_NAME[ZmFolder.ID_SENT]			= "sent";
134 ZmFolder.QUERY_NAME[ZmFolder.ID_OUTBOX]			= "outbox";
135 ZmFolder.QUERY_NAME[ZmFolder.ID_DRAFTS]			= "drafts";
136 ZmFolder.QUERY_NAME[ZmOrganizer.ID_CALENDAR]	= "calendar";
137 ZmFolder.QUERY_NAME[ZmFolder.ID_CONTACTS]		= "contacts";
138 ZmFolder.QUERY_NAME[ZmFolder.ID_TASKS]			= "tasks";
139 ZmFolder.QUERY_NAME[ZmFolder.ID_AUTO_ADDED]		= "Emailed Contacts";
140 ZmFolder.QUERY_NAME[ZmOrganizer.ID_BRIEFCASE]	= "briefcase";
141 ZmFolder.QUERY_NAME[ZmFolder.ID_CHATS]			= "chats";
142 ZmFolder.QUERY_NAME[ZmFolder.ID_SYNC_FAILURES]	= "Error Reports";
143 
144 ZmFolder.QUERY_ID = AjxUtil.valueHash(ZmFolder.QUERY_NAME);
145 
146 // order within the overview panel
147 ZmFolder.SORT_ORDER = {};
148 ZmFolder.SORT_ORDER[ZmFolder.ID_INBOX]			= 1;
149 ZmFolder.SORT_ORDER[ZmFolder.ID_CHATS]			= 2;
150 ZmFolder.SORT_ORDER[ZmFolder.ID_SENT]			= 3;
151 ZmFolder.SORT_ORDER[ZmFolder.ID_DRAFTS]			= 4;
152 ZmFolder.SORT_ORDER[ZmFolder.ID_SPAM]			= 5;
153 ZmFolder.SORT_ORDER[ZmFolder.ID_OUTBOX]			= 6;
154 ZmFolder.SORT_ORDER[ZmFolder.ID_TRASH]			= 7;
155 ZmFolder.SORT_ORDER[ZmFolder.ID_SYNC_FAILURES]	= 8;
156 ZmFolder.SORT_ORDER[ZmFolder.ID_SEP]			= 9;
157 ZmFolder.SORT_ORDER[ZmFolder.ID_ATTACHMENTS]    = 99; // Last
158 
159 // character codes for "tcon" attribute in conv action request, which controls
160 // which folders are affected
161 ZmFolder.TCON_CODE = {};
162 ZmFolder.TCON_CODE[ZmFolder.ID_TRASH]			= "t";
163 ZmFolder.TCON_CODE[ZmFolder.ID_SYNC_FAILURES]	= "o";
164 ZmFolder.TCON_CODE[ZmFolder.ID_SPAM]			= "j";
165 ZmFolder.TCON_CODE[ZmFolder.ID_SENT]			= "s";
166 ZmFolder.TCON_CODE[ZmFolder.ID_DRAFTS]			= "d";
167 ZmFolder.TCON_CODE[ZmFolder.ID_OTHER]			= "o";
168 
169 // folders that look like mail folders that we don't want to show
170 ZmFolder.HIDE_ID = {};
171 ZmFolder.HIDE_ID[ZmOrganizer.ID_CHATS]				= true;
172 ZmFolder.HIDE_ID[ZmOrganizer.ID_NOTIFICATION_MP]	= true;
173 
174 // Hide folders migrated from Outlook mailbox
175 ZmFolder.HIDE_NAME = {};
176 //ZmFolder.HIDE_NAME["Journal"]		= true;
177 //ZmFolder.HIDE_NAME["Notes"]		= true;
178 //ZmFolder.HIDE_NAME["Outbox"]		= true;
179 //ZmFolder.HIDE_NAME["Tasks"]		= true;
180 
181 // folders that contain mail from me instead of to me
182 ZmFolder.OUTBOUND = [ZmFolder.ID_SENT, ZmFolder.ID_OUTBOX, ZmFolder.ID_DRAFTS];
183 
184 // The extra-special, visible but untouchable outlook folder
185 ZmFolder.SYNC_ISSUES 							= "Sync Issues";
186 
187 // map name to ID
188 ZmFolder.QUERY_ID = {};
189 (function() {
190 	for (var i in ZmFolder.QUERY_NAME) {
191 		ZmFolder.QUERY_ID[ZmFolder.QUERY_NAME[i]] = i;
192 	}
193 })();
194 
195 /**
196  * Comparison function for folders. Intended for use on a list of user folders
197  * through a call to <code>Array.sort()</code>.
198  *
199  * @param {ZmFolder}	folderA		a folder
200  * @param {ZmFolder}	folderB		a folder
201  * @param {Boolean}		nonMail		this is sorting non mail tree.
202  * @return	{int} 0 if the folders match
203  */
204 ZmFolder.sortCompare =
205 function(folderA, folderB, nonMail) {
206 	var check = ZmOrganizer.checkSortArgs(folderA, folderB);
207 	if (check != null) { return check; }
208 
209 	// offline client wants POP folders above all else *unless* we are POP'ing into Inbox
210 	if (appCtxt.isOffline) {
211 		if (folderA.isDataSource(ZmAccount.TYPE_POP)) {
212 			if (folderA.id == ZmFolder.ID_INBOX) return -1;
213 			if (folderB.isDataSource(ZmAccount.TYPE_POP)) {
214 				if (folderA.name.toLowerCase() > folderB.name.toLowerCase()) { return 1; }
215 				if (folderA.name.toLowerCase() < folderB.name.toLowerCase()) { return -1; }
216 				return 0;
217 			}
218 			return -1;
219 		} else if (folderB.isDataSource(ZmAccount.TYPE_POP)) {
220 			return 1;
221 		}
222 	}
223 
224 	if (ZmFolder.SORT_ORDER[folderA.nId] && ZmFolder.SORT_ORDER[folderB.nId]) {
225 		return (ZmFolder.SORT_ORDER[folderA.nId] - ZmFolder.SORT_ORDER[folderB.nId]);
226 	}
227 
228 	// links (shared folders or mailboxes) appear after personal folders
229 	if (folderA.link !== folderB.link) {
230 		return folderA.link ? 1 : -1;
231 	}
232 
233 	if (nonMail) {
234 		//for nonp-mail apps, trash last of all things
235 		if (folderA.isTrash()) {
236 			return 1;
237 		}
238 		if (folderB.isTrash()) {
239 			return -1;
240 		}
241 		//system before non-system (except for trash)
242 		if (folderA.isSystem() && !folderB.isSystem()) {
243 			return -1;
244 		}
245 		if (!folderA.isSystem() && folderB.isSystem()) {
246 			return 1;
247 		}
248 		if (folderA.isSystem() && folderB.isSystem()) {
249 			//for 2 system folders, the default one is first, and the rest ordered alphabetically (again except for trash that appears after the user folders)
250 			if (folderA.isDefault()) {
251 				return -1;
252 			}
253 			if (folderB.isDefault()) {
254 				return 1;
255 			}
256 			//the other cases will be sorted by name below. Either 2 system or 2 user folders.
257 		}
258 	}
259 	else {
260 		if (!ZmFolder.SORT_ORDER[folderA.nId] && ZmFolder.SORT_ORDER[folderB.nId]) { return 1; }
261 		if (ZmFolder.SORT_ORDER[folderA.nId] && !ZmFolder.SORT_ORDER[folderB.nId]) { return -1; }
262 	}
263 
264 	if (folderA.name.toLowerCase() > folderB.name.toLowerCase()) { return 1; }
265 	if (folderA.name.toLowerCase() < folderB.name.toLowerCase()) { return -1; }
266 	return 0;
267 };
268 
269 
270 ZmFolder.sortCompareNonMail =
271 function(folderA, folderB) {
272 	return ZmFolder.sortCompare(folderA, folderB, true);
273 };
274 
275 /**
276  * Compares the folders by path.
277  * 
278  * @param {ZmFolder}	folderA		a folder
279  * @param {ZmFolder}	folderB		a folder
280  * @return	{int} 0 if the folders match
281  */
282 ZmFolder.sortComparePath =
283 function(folderA, folderB) {
284 
285 	var pathA = folderA && folderA.getPath(false, false, null, true, true);
286 	var pathB = folderB && folderB.getPath(false, false, null, true, true);
287 	var check = ZmOrganizer.checkSortArgs(pathA, pathB);
288 	if (check != null) { return check; }
289 
290 	if (ZmFolder.SORT_ORDER[folderA.nId] && ZmFolder.SORT_ORDER[folderB.nId]) {
291 		return (ZmFolder.SORT_ORDER[folderA.nId] - ZmFolder.SORT_ORDER[folderB.nId]);
292 	}
293 	if (!ZmFolder.SORT_ORDER[folderA.nId] && ZmFolder.SORT_ORDER[folderB.nId]) { return 1; }
294 	if (ZmFolder.SORT_ORDER[folderA.nId] && !ZmFolder.SORT_ORDER[folderB.nId]) { return -1; }
295 	if (pathA.toLowerCase() > pathB.toLowerCase()) { return 1; }
296 	if (pathA.toLowerCase() < pathB.toLowerCase()) { return -1; }
297 	return 0;
298 };
299 
300 /**
301  * Checks a folder name for validity. Note: that a name, rather than a path, is checked.
302  *
303  * @param {String}	name		the folder name
304  * @param {ZmFolder}	parent		the parent folder
305  * @return	{String} an error message if the name is invalid; <code>null</code>if the name is valid. 
306  */
307 ZmFolder.checkName =
308 function(name, parent) {
309 	var error = ZmOrganizer.checkName(name);
310 	if (error) { return error; }
311 
312 	// make sure path isn't same as a system folder
313 	parent = parent || appCtxt.getFolderTree().root;
314 	if (parent && (parent.id == ZmFolder.ID_ROOT)) {
315 		var lname = name.toLowerCase();
316 		for (var id in ZmFolder.MSG_KEY) {
317 			var sysname = ZmMsg[ZmFolder.MSG_KEY[id]];
318 			if (sysname && (lname == sysname.toLowerCase())) {
319 				return ZmMsg.folderNameReserved;
320 			}
321 		}
322 		/*if (lname == ZmFolder.SYNC_ISSUES.toLowerCase()) {
323 			return ZmMsg.folderNameReserved;
324 		}*/
325 	}
326 
327 	return null;
328 };
329 
330 /**
331  * Gets the "well-known" ID for a given folder name.
332  * 
333  * @param	{String}	folderName	the folder name
334  * @return	{String}	the id or <code>null</code> if not found
335  */
336 ZmFolder.getIdForName =
337 function(folderName) {
338 	var name = folderName.toLowerCase();
339 	for (var i in ZmFolder.MSG_KEY) {
340 		if (ZmFolder.MSG_KEY[i] == name) {
341 			return i;
342 		}
343 	}
344 	return null;
345 };
346 
347 /**
348  * Moves a folder. A user can move a folder to "Trash" even if there is already a folder in "Trash" with the
349  * same name. A new name will be generated for this folder and a rename is performed before the move.
350  * 
351  * @param	{ZmFolder}	newParent		the new parent
352  * @param	{boolean}	noUndo			true if the action should not be undoable
353  * @param	{String}	actionText		optional custom action text to display as summary
354  */
355 ZmFolder.prototype.move =
356 function(newParent, noUndo, actionText, batchCmd) {
357 	var origName = this.name;
358 	var name = this.name;
359 	while (newParent.hasChild(name)) {
360 		name = name + "_";
361 	}
362 	if (origName != name) {
363 		this.rename(name);
364 	}
365 	ZmOrganizer.prototype.move.call(this, newParent, noUndo, batchCmd);
366 };
367 
368 /**
369  * Sends <code><FolderActionRequest></code> to turn sync'ing on/off for IMAP folders. Currently,
370  * this is only used by Offline/ZDesktop client
371  *
372  * @param {Boolean}	syncIt		the flag indicating whether to sync this folder
373  * @param {AjxCallback}	callback		the callback to call once server request is successful
374  * @param {AjxCallback}	errorCallback	the callback to call if server returns error
375  */
376 ZmFolder.prototype.toggleSyncOffline =
377 function(callback, errorCallback) {
378 	if (!this.isOfflineSyncable) { return; }
379 
380 	var op = this.isOfflineSyncing ? "!syncon" : "syncon";
381 	var soapDoc = AjxSoapDoc.create("FolderActionRequest", "urn:zimbraMail");
382 	var actionNode = soapDoc.set("action");
383 	actionNode.setAttribute("op", op);
384 	actionNode.setAttribute("id", this.id);
385 
386 	var params = {
387 		soapDoc: soapDoc,
388 		asyncMode: true,
389 		callback: callback,
390 		errorCallback: errorCallback
391 	};
392 	appCtxt.getAppController().sendRequest(params);
393 };
394 
395 /**
396  * Checks folders recursively for feeds.
397  * 
398  * @return	{Boolean}	<code>true</code> for feeds
399  */
400 ZmFolder.prototype.hasFeeds =
401 function() {
402 	if (this.type != ZmOrganizer.FOLDER) { return false; }
403 
404 	var a = this.children.getArray();
405 	var sz = this.children.size();
406 	for (var i = 0; i < sz; i++) {
407 		if (a[i].isFeed()) {
408 			return true;
409 		}
410 		if (a[i].children && a[i].children.size() > 0) {
411 			return (a[i].hasFeeds && a[i].hasFeeds());
412 		}
413 	}
414 	return false;
415 };
416 
417 /**
418  * Checks if the folder has search.
419  * 
420  * @param	{String}	id	not used
421  * @return	{Boolean}	<code>true</code> if has search
422  */
423 ZmFolder.prototype.hasSearch =
424 function(id) {
425 	if (this.type == ZmOrganizer.SEARCH) { return true; }
426 
427 	var a = this.children.getArray();
428 	var sz = this.children.size();
429 	for (var i = 0; i < sz; i++) {
430 		if (a[i].hasSearch()) {
431 			return true;
432 		}
433 	}
434 
435 	return false;
436 };
437 
438 /**
439  * Checks if the folder supports public access. Override this method if you dont want a folder to be accessed publicly
440  * 
441  * @return	{Boolean}	always returns <code>true</code>
442  */
443 ZmFolder.prototype.supportsPublicAccess =
444 function() {
445 	return true;
446 };
447 
448 /**
449  * Handles the creation of a folder or search folder. This folder is the parent
450  * of the newly created folder. A folder may hold a folder or search folder,
451  * and a search folder may hold another search folder.
452  *
453  * @param {Object}	obj				a JS folder object from the notification
454  * @param {String}	elementType		the type of containing JSON element
455  * @param {Boolean}	skipNotify		<code>true</code> if notifying client should be ignored
456  */
457 ZmFolder.prototype.notifyCreate =
458 function(obj, elementType, skipNotify) {
459 	// ignore creates of system folders
460 	var nId = ZmOrganizer.normalizeId(obj.id);
461 	if (this.isSystem() && nId < ZmOrganizer.FIRST_USER_ID[this.type]) { return; }
462 
463 	var account = ZmOrganizer.parseId(obj.id).account;
464 	var folder = ZmFolderTree.createFromJs(this, obj, this.tree, elementType, null, account);
465 	if (folder) {
466 		var index = ZmOrganizer.getSortIndex(folder, eval(ZmTreeView.COMPARE_FUNC[this.type]));
467 		this.children.add(folder, index);
468 
469 		if (!skipNotify) {
470 			folder._notify(ZmEvent.E_CREATE);
471 		}
472 	}
473 };
474 
475 /**
476  * Provide some extra info in the change event about the former state
477  * of the folder. Note that we null out the field after setting up the
478  * change event, so the notification isn't also sent when the parent
479  * class's method is called.
480  *
481  * @param {Object}	obj	a "modified" notification
482  */
483 ZmFolder.prototype.notifyModify =
484 function(obj) {
485 	var details = {};
486 	var fields = {};
487 	var doNotify = false;
488 	if (obj.name != null && this.name != obj.name && obj.id == this.id) {
489 		details.oldPath = this.getPath();
490 		this.name = obj.name;
491 		fields[ZmOrganizer.F_NAME] = true;
492 		this.parent.children.sort(eval(ZmTreeView.COMPARE_FUNC[this.type]));
493 		doNotify = true;
494 		obj.name = null;
495 	}
496 	if (doNotify) {
497 		details.fields = fields;
498 		this._notify(ZmEvent.E_MODIFY, details);
499 	}
500 
501 	if (obj.l != null && (!this.parent || (obj.l != this.parent.id))) {
502 		var newParent = this._getNewParent(obj.l);
503 		if (newParent) {
504 			details.oldPath = this.getPath();
505 			this.reparent(newParent);
506 			this._notify(ZmEvent.E_MOVE, details);
507 			obj.l = null;
508 		}
509 	}
510 
511 	ZmOrganizer.prototype.notifyModify.apply(this, [obj]);
512 };
513 
514 /**
515  * Creates a query.
516  * 
517  * @param	{Boolean}	pathOnly	<code>true</code> if to use the path only
518  * @return	{String}	the query
519  */
520 ZmFolder.prototype.createQuery =
521 function(pathOnly) {
522 	if (!this.isRemote() && this.isSystem()) {
523 		var qName = ZmFolder.QUERY_NAME[this.nId] || this.getName(false, null, true, true) || this.name;
524 		// put quotes around folder names that consist of multiple words or have special characters.
525 		var quote = /^\w+$/.test(qName) ? "" : "\"";
526 		return pathOnly
527 			? qName
528 			: ("in:" + quote + qName + quote);
529 	}
530 
531 	var path = this.isSystem() ? ZmFolder.QUERY_NAME[this.nId] : this.name;
532 	var f = this.parent;
533 	while (f && (f.nId != ZmFolder.ID_ROOT) && f.name.length) {
534 		var name = (f.isSystem() && ZmFolder.QUERY_NAME[f.nId]) || f.name;
535 		path = name + "/" + path;
536 		f = f.parent;
537 	}
538 	path = '"' + path + '"';
539 	return pathOnly ? path : ("in:" + path);
540 };
541 
542 /**
543  * Gets the name.
544  * 
545  * @param	{Boolean}	 showUnread		<code>true</code> to show unread
546  * @param	{int}		maxLength		the max length
547  * @param	{Boolean}	noMarkup		<code>true</code> to not include markup
548  * @param	{Boolean}	useSystemName	<code>true</code> to use the system name
549  * 
550  * @return	{String}	the name
551  */
552 ZmFolder.prototype.getName =
553 function(showUnread, maxLength, noMarkup, useSystemName) {
554 	if (this.nId == ZmFolder.ID_DRAFTS ||
555 		this.nId == ZmFolder.ID_OUTBOX ||
556 		this.rid == ZmFolder.ID_DRAFTS)
557 	{
558 		var name = (useSystemName && this._systemName) ? this._systemName : this.name;
559 		if (showUnread && this.numTotal > 0) {
560 			name = AjxMessageFormat.format(ZmMsg.folderUnread, [name, this.numTotal]);
561 			if (!noMarkup) {
562 				name = ["<span style='font-weight:bold'>", name, "</span>"].join("");
563 			}
564 		}
565 		return name;
566 	}
567 	else {
568 		return ZmOrganizer.prototype.getName.apply(this, arguments);
569 	}
570 };
571 
572 /**
573  * Gets the icon.
574  * 
575  * @return	{String}	the icon
576  */
577 ZmFolder.prototype.getIcon =
578 function() {
579 	if (this.nId == ZmOrganizer.ID_ROOT)			{ return null; }
580 	if (ZmFolder.ICON[this.nId])					{ return ZmFolder.ICON[this.nId]; }
581 	if (this.isFeed())								{ return "RSS"; }
582 	if (this.isRemote())							{ return "SharedMailFolder"; }
583 	if (this.isDataSource(ZmAccount.TYPE_POP))		{ return "POPAccount"; }
584 
585 	// make a "best-effort" to map imap folders to a well-known icon
586 	// (parent will be the root imap folder)
587 	var mappedId = this.getSystemEquivalentFolderId();
588 	if (mappedId) {
589 		return ZmFolder.ICON[mappedId] || "Folder";
590 	}
591 
592 	return "Folder";
593 };
594 
595 ZmFolder.prototype.getSystemEquivalentFolderId =
596 function() {
597 	if (this.parent && this.parent.isDataSource(ZmAccount.TYPE_IMAP)) {
598 		return ZmFolder.getIdForName(this.name);
599 	}
600 	return null;
601 };
602 
603 ZmFolder.prototype.isSystemEquivalent =
604 function() {
605 	return this.getSystemEquivalentFolderId() != null;
606 };
607 
608 ZmFolder.prototype.mayContainFolderFromAccount =
609 function(otherAccount) {
610 	var thisAccount = this.getAccount();
611 	if (thisAccount == otherAccount) {
612 		return true;
613 	}
614 	return thisAccount.isLocal(); // can only move to local
615 
616 };
617 
618 /**
619  * Returns true if the given object(s) may be placed in this folder.
620  *
621  * If the object is a folder, check that:
622  * <ul>
623  * <li>We are not the immediate parent of the folder</li>
624  * <li>We are not a child of the folder</li>
625  * <li>We are not Spam or Drafts</li>
626  * <li>We don't already have a child with the folder's name (unless we are in Trash)</li>
627  * <li>We are not moving it into a folder of a different type</li>
628  * <li>We are not moving a folder into itself</li>
629  * </ul>
630  *
631  * If the object is an item or a list or items, check that:
632  * <ul>
633  * <li>We are not the Folders container</li>
634  * <li>We are not a search folder</li>
635  * <li>The items aren't already in this folder</li>
636  * <li>A contact can only be moved to Trash</li>
637  * <li> A draft can be moved to Trash or Drafts</li>
638  * <li>Non-drafts cannot be moved to Drafts</li>
639  * </ul>
640  *
641  * @param {Object}	what		the object(s) to possibly move into this folder (item or organizer)
642  * @param {constant}	folderType	the contextual folder type (for tree view root items)
643  * @param {boolean}	ignoreExisting  Set to true if checks for item presence in the folder should be skipped (e.g. when recovering deleted items)
644  */
645 ZmFolder.prototype.mayContain =
646 function(what, folderType, ignoreExisting) {
647 
648 	if (!what) {
649 		return true;
650 	}
651 	if (this.isFeed() /*|| this.isSyncIssuesFolder()*/) {
652 		return false;
653 	}
654 	// placeholder for showing a large number of folders
655 	if (this.id == ZmFolder.ID_LOAD_FOLDERS) {
656 		return false;
657 	}
658 
659 	var thisType = folderType || this.type;
660 	var invalid = false;
661 	if (what instanceof ZmFolder) {
662         invalid = ((what.parent === this && !ignoreExisting) || this.isChildOf(what) || this.nId == ZmFolder.ID_DRAFTS || this.nId == ZmFolder.ID_SPAM ||
663 				   (!this.isInTrash() && this.hasChild(what.name) && !ignoreExisting) ||
664 	               (what.type !== thisType && this.nId != ZmFolder.ID_TRASH) ||
665 				   (what.id === this.id) ||
666 				   (this.disallowSubFolder) ||
667 				   (appCtxt.multiAccounts && !this.mayContainFolderFromAccount(what.getAccount())) || // cannot move folders across accounts, unless the target is local
668                    (this.isRemote() && !this._remoteMoveOk(what)) ||
669 				   (what.isRemote() && !this._remoteMoveOk(what)));				// a remote folder can be DnD but not its children
670     } else {
671 		// An item or an array of items is being moved
672 		var items = AjxUtil.toArray(what);
673 		var item = items[0];
674 
675         // container can only have folders/searches or calendars
676 		if ((this.nId == ZmOrganizer.ID_ROOT && (what.type !== ZmOrganizer.CALENDAR)) ||
677              // nothing can be moved to outbox/sync failures folders
678 			 this.nId == ZmOrganizer.ID_OUTBOX ||
679 			 this.nId == ZmOrganizer.ID_SYNC_FAILURES)
680 		{
681 			invalid = true;
682 		} else if (thisType === ZmOrganizer.SEARCH) {
683 			invalid = true;														// can't drop items into saved searches
684 		} else if (item && (item.type === ZmItem.CONTACT) && item.isGal) {
685 			invalid = true;
686 		} else if (item && (item.type === ZmItem.CONV) && item.list && item.list.search && (item.list.search.folderId === this.id)) {
687 			invalid = true;														// convs which are a result of a search for this folder
688 		} else {																// checks that need to be done for each item
689 			for (var i = 0; i < items.length; i++) {
690 				var childItem = items[i];
691 				if (!childItem) {
692 					invalid = true;
693 					break;
694 				}
695 				if (Dwt.instanceOf(childItem, "ZmBriefcaseFolderItem")) {
696                      if (childItem.folder && childItem.folder.isRemote() && !childItem.folder.rid) {
697                         invalid = true;
698                         break;
699                      }
700                 } else if (item.type === ZmItem.MSG && childItem.isDraft && (this.nId != ZmFolder.ID_TRASH && this.nId != ZmFolder.ID_DRAFTS && this.rid != ZmFolder.ID_DRAFTS)) {
701 					// can move drafts only into Trash or Drafts
702 					invalid = true;
703 					break;
704 				} else if ((this.nId == ZmFolder.ID_DRAFTS || this.rid == ZmFolder.ID_DRAFTS) && !childItem.isDraft)	{
705 					// only drafts can be moved into Drafts
706 					invalid = true;
707 					break;
708 				}
709 			}
710 			// items in the "Sync Failures" folder cannot be dragged out
711 			if (appCtxt.isOffline && !invalid) {
712 				// bug: 41531 - don't allow items to be moved into exchange
713 				// account when moving across accounts
714 				var acct = this.getAccount();
715 				if (acct && item.getAccount() != acct &&
716 					(acct.type === ZmAccount.TYPE_MSE ||
717 					 acct.type === ZmAccount.TYPE_EXCHANGE))
718 				{
719 					invalid = true;
720 				}
721 				else {
722 					var cs = appCtxt.getCurrentSearch();
723 					var folder = cs && appCtxt.getById(cs.folderId);
724 					if (folder && folder.nId == ZmOrganizer.ID_SYNC_FAILURES) {
725 						invalid = true;
726 					}
727 				}
728 			}
729 
730 			// bug #42890 - disable moving to shared folders across accounts
731 			// until server bug is fixed
732 			if (appCtxt.multiAccounts && this.isRemote() &&
733 				what.getAccount && this.getAccount().id != what.getAccount().id)
734 			{
735 				invalid = true;
736 			}
737 
738 			// can't move items to folder they're already in; we're okay if we
739 			// have one item from another folder
740 			if (!invalid && !ignoreExisting) {
741 				if (item && item.folderId) {
742 					invalid = true;
743 					for (var i = 0; i < items.length; i++) {
744 						if (items[i].folderId != this.id) {
745 							invalid = false;
746 							break;
747 						}
748 					}
749 				}
750 			}
751 		}
752 		if (!invalid && this.link) {
753 			invalid = this.isReadOnly();										// cannot drop anything onto a read-only item
754 		}
755 	}
756 	return !invalid;
757 };
758 
759 /**
760  * Checks if this is the sync issues folder.
761  * 
762  * @return	{Boolean}	<code>true</code> if the folder is the one dealing with Outlook sync issues
763  */
764 
765 //Bug#68799 Removing special handling of the folder named "Sync Issues"
766 
767 /*ZmFolder.prototype.isSyncIssuesFolder =
768 function() {
769 	return (this.name == ZmFolder.SYNC_ISSUES);
770 };*/
771 
772 /**
773  * Checks if this folder required hard delete.
774  * 
775  * @return	{Boolean}	<code>true</code> if deleting items w/in this folder should be hard deleted.
776  */
777 ZmFolder.prototype.isHardDelete =
778 function() {
779 	return (this.isInTrash() || this.isInSpam() || (appCtxt.isOffline && this.isUnder(ZmOrganizer.ID_SYNC_FAILURES)));
780 };
781 
782 /**
783  * Checks if this folder is in spam folder.
784  * 
785  * @return	{Boolean}	<code>true</code> if in spam
786  */
787 ZmFolder.prototype.isInSpam =
788 function(){
789 	return this.isUnder(ZmFolder.ID_SPAM);
790 };
791 
792 /**
793  *
794  * @param {ZmFolder}	folder  the source folder
795  * 
796  * @return {Boolean}	<code>true/code> if the given remote folder can be moved into this remote folder.
797  * The source and the target folder must belong to the same account. The source
798  * must have delete permission and the target must have insert permission.
799  * 
800  * @private
801  */
802 ZmFolder.prototype._remoteMoveOk =
803 function(folder) {
804 	if (!this.isRemote() && folder.isMountpoint && folder.rid) { return true; }
805 	if (!this.link || !folder.link || this.getOwner() !== folder.getOwner()) { return false; }
806 	if (!this._folderActionOk(this, "isInsert")) {
807 		return false;
808 	}
809 	return this._folderActionOk(folder, "isDelete");
810 };
811 
812 ZmFolder.prototype._folderActionOk =
813 function(folder, func) {
814 	var share = folder.shares && folder.shares[0];
815 	if (!share) {
816 		//if shares is not set, default to readOnly.
817 		return !folder.isReadOnly();
818 	}
819 	return share[func]();
820 };
821 /**
822  * Returns true if this folder is for outbound mail.
823  */
824 ZmFolder.prototype.isOutbound =
825 function() {
826 	for (var i = 0; i < ZmFolder.OUTBOUND.length; i++) {
827 		if (this.isUnder(ZmFolder.OUTBOUND[i])) {
828 			return true;
829 		}
830 	}
831 	return false;
832 };
833 
834 
835 /**
836  * Sets the Global Mark Read flag.  When the user sets this flag, read flags are global for all
837  * shared instances of the folder. When not set, each user accessing the shared folder will maintain
838  * their own read/unread flag.
839  *
840  * @param	{Object}	        globalMarkRead		the globalMarkRead boolean flag
841  * @param	{AjxCallback}	    callback		    the callback
842  * @param	{AjxCallback}	    errorCallback		the error callback
843  * @param   {ZmBatchCommand}    batchCmd            optional batch command
844  */
845 ZmOrganizer.prototype.setGlobalMarkRead = function(globalMarkRead, callback, errorCallback, batchCmd) {
846 	if (this.globalMarkRead == globalMarkRead) { return; }
847     // TODO: Bug 59559, awaiting server side implementation (Bug 24567)
848     // TODO: - For ZmFolderPropsDialog and ZmSharePropsDialog:
849     // TODO:     Make sure that the attrName is indeed globalMarkRead - used in the dialogs
850     // TODO:     Make the globalMarkRead labels and controls visible.
851     // TODO: - Uncomment this once the server call is ready, make sure action/attrs are correct
852 	//this._organizerAction({action: "globalMarkRead", attrs: {globalMarkRead: globalMarkRead}, callback: callback,
853     //                       errorCallback: errorCallback, batchCmd: batchCmd});
854 };
855