1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc.
  5  *
  6  * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at: https://www.zimbra.com/license
  9  * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15
 10  * have been added to cover use of software over a computer network and provide for limited attribution
 11  * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B.
 12  *
 13  * Software distributed under the License is distributed on an "AS IS" basis,
 14  * WITHOUT WARRANTY OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing rights and limitations under the License.
 16  * The Original Code is Zimbra Open Source Web Client.
 17  * The Initial Developer of the Original Code is Zimbra, Inc.  All rights to the Original Code were
 18  * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015.
 19  *
 20  * All portions of the code are Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @overview
 26  * This file defines an organizer.
 27  */
 28 
 29 /**
 30 * Creates an empty organizer.
 31 * @class
 32 * This class represents an "organizer", which is something used to classify or contain
 33 * items. So far, that's either a tag or a folder. Tags and folders are represented as
 34 * a tree structure, though tags are flat and have only one level below the root. Folders
 35 * can be nested.
 36 *
 37 * @author Conrad Damon
 38 *
 39 * @param	{Hash}	params		a hash of parameters
 40 * @param {constant}	params.type		the organizer type
 41 * @param {int}		params.id			the numeric ID
 42 * @param {String}	params.name		the name
 43 * @param {ZmOrganizer}	params.parent		the parent organizer
 44 * @param {ZmTree}	params.tree		the tree model that contains this organizer
 45 * @param {constant}	params.color		the color for this organizer
 46 * @param {String}	params.rgb		the color for this organizer, as HTML RGB value
 47 * @param {Boolean}	params.link		<code>true</code> if this organizer is shared
 48 * @param {Boolean}	params.broken		<code>true</code> if this link is broken
 49 * @param {int}	params.numUnread	the number of unread items for this organizer
 50 * @param {int}	params.numTotal	the number of items for this organizer
 51 * @param {Boolean}	params.noTooltip	do not show tooltip 
 52 * @param {int}	params.sizeTotal	the total size of organizer's items
 53 * @param {String}	params.url		the URL for this organizer's feed
 54 * @param {String}	params.owner		the owner for this organizer
 55 * @param {String}	params.oname		the owner's name for this organizer
 56 * @param {String}	params.zid		the Zimbra ID of owner, if remote folder
 57 * @param {String}	params.rid		the remote ID of organizer, if remote folder
 58 * @param {String}	params.restUrl	the REST URL of this organizer.
 59 * @param {String}	params.newOp		the name of operation run by button in overview header
 60 * @param {ZmZimbraAccount}	params.account	the account this organizer belongs to
 61 */
 62 ZmOrganizer = function(params) {
 63 
 64 	if (arguments.length == 0) { return; }
 65 
 66 	this.type = params.type;
 67 	var id = this.id = params.id;
 68 	this.nId = ZmOrganizer.normalizeId(id);
 69 	this.name = ZmFolder.MSG_KEY[this.nId] ? ZmMsg[ZmFolder.MSG_KEY[this.nId]] : params.name;
 70 	this._systemName = this.nId < 256 && params.name;
 71 	this.parent = params.parent;
 72 	this.tree = params.tree;
 73 	this.numUnread = params.numUnread || 0;
 74 	this.numTotal = params.numTotal || 0;
 75 	this.noTooltip = params.noTooltip;
 76 	this.sizeTotal = params.sizeTotal || 0;
 77 	this.url = params.url;
 78 	this.owner = params.owner;
 79 	this.oname = params.oname;
 80 	this.link = params.link || (Boolean(params.zid)) || (this.parent && this.parent.link);
 81 	this.isMountpoint = params.link;
 82 	this.zid = params.zid;
 83 	this.rid = params.rid;
 84 	this.restUrl = params.restUrl;
 85 	this.account = params.account;
 86     this.perm = params.perm;
 87 	this.noSuchFolder = params.broken; // Is this a link to some folder that ain't there.
 88 	this._isAdmin = this._isReadOnly = this._hasPrivateAccess = null;
 89     this.retentionPolicy = params.retentionPolicy;
 90 	this.webOfflineSyncDays = params.webOfflineSyncDays;
 91 
 92 	this.color =
 93         params.color ||
 94         (this.parent && this.parent.color) ||
 95         ZmOrganizer.ORG_COLOR[id] ||
 96         ZmOrganizer.ORG_COLOR[this.nId] ||
 97         ZmOrganizer.DEFAULT_COLOR[this.type] ||
 98         ZmOrganizer.C_NONE
 99     ;
100 	this.isColorCustom = params.rgb != null; //set so we know if the user chose a custom color (to distiguish from basic color or none
101 	this.rgb =
102         params.rgb ||
103         ZmOrganizer.COLOR_VALUES[this.color] ||
104         ZmOrganizer.COLOR_VALUES[ZmOrganizer.ORG_DEFAULT_COLOR]
105     ;
106 
107 	if (appCtxt.isOffline && !this.account && this.id == this.nId) {
108 		this.account = appCtxt.accountList.mainAccount;
109 	}
110 
111 	if (id && params.tree) {
112 		appCtxt.cacheSet(id, this);
113 		if (this.link) {
114 			// also store under ID that items use for parent folder ("l" attribute in node)
115 			appCtxt.cacheSet([this.zid, this.rid].join(":"), this);
116 		}
117 	}
118 
119 	this.children = new AjxVector();
120 };
121 
122 ZmOrganizer.prototype.isZmOrganizer = true;
123 ZmOrganizer.prototype.toString = function() { return "ZmOrganizer"; };
124 
125 // global organizer types
126 ZmOrganizer.TAG					= ZmEvent.S_TAG;
127 ZmOrganizer.SEARCH				= ZmEvent.S_SEARCH;
128 ZmOrganizer.SHARE               = ZmEvent.S_SHARE;
129 ZmOrganizer.MOUNTPOINT			= ZmEvent.S_MOUNTPOINT;
130 ZmOrganizer.ZIMLET				= ZmEvent.S_ZIMLET;
131 
132 // folder IDs defined in com.zimbra.cs.mailbox.Mailbox
133 // Note: since these are defined as Numbers, and IDs come into our system as Strings,
134 // we need to use == for comparisons (instead of ===, which will fail)
135 ZmOrganizer.ID_ROOT				= 1;
136 ZmOrganizer.ID_INBOX			= 2;
137 ZmOrganizer.ID_TRASH			= 3;
138 ZmOrganizer.ID_SPAM				= 4;
139 ZmOrganizer.ID_ADDRBOOK			= 7;
140 ZmOrganizer.ID_CALENDAR			= 10;
141 ZmOrganizer.ID_AUTO_ADDED 		= 13;
142 ZmOrganizer.ID_CHATS			= 14;
143 ZmOrganizer.ID_TASKS			= 15;
144 ZmOrganizer.ID_BRIEFCASE		= 16;
145 ZmOrganizer.ID_ALL_MAILBOXES	= 249; 
146 ZmOrganizer.ID_NOTIFICATION_MP	= 250;
147 ZmOrganizer.ID_SYNC_FAILURES	= 252;		// offline only
148 ZmOrganizer.ID_OUTBOX    		= 254;		// offline only
149 ZmOrganizer.ID_ZIMLET			= -1000;	// zimlets need a range.  start from -1000 incrementing up.
150 ZmOrganizer.ID_ATTACHMENTS		= -17;		// Attachments View
151 ZmOrganizer.ID_DLS				= -18;
152 
153 // fields that can be part of a displayed organizer
154 ZmOrganizer.F_NAME				= "name";
155 ZmOrganizer.F_UNREAD			= "unread";
156 ZmOrganizer.F_TOTAL				= "total";
157 ZmOrganizer.F_SIZE				= "size";
158 ZmOrganizer.F_COLOR				= "color";
159 ZmOrganizer.F_RGB				= "rgb";
160 ZmOrganizer.F_QUERY				= "query";
161 ZmOrganizer.F_SHARES			= "shares";
162 ZmOrganizer.F_FLAGS				= "flags";
163 ZmOrganizer.F_REST_URL			= "rest";
164 ZmOrganizer.F_PERMS				= "perms";
165 ZmOrganizer.F_RNAME				= "rname";	// remote name
166 
167 // server representation of org flags
168 ZmOrganizer.FLAG_CHECKED			= "#";
169 ZmOrganizer.FLAG_DISALLOW_SUBFOLDER	= "o";
170 ZmOrganizer.FLAG_EXCLUDE_FREE_BUSY	= "b";
171 ZmOrganizer.FLAG_IMAP_SUBSCRIBED	= "*";
172 ZmOrganizer.FLAG_OFFLINE_GLOBAL		= "g";
173 ZmOrganizer.FLAG_OFFLINE_SYNCABLE	= "y";
174 ZmOrganizer.FLAG_OFFLINE_SYNCING	= "~";
175 ZmOrganizer.ALL_FLAGS = [
176 	ZmOrganizer.FLAG_CHECKED,
177 	ZmOrganizer.FLAG_IMAP_SUBSCRIBED,
178 	ZmOrganizer.FLAG_EXCLUDE_FREE_BUSY,
179 	ZmOrganizer.FLAG_DISALLOW_SUBFOLDER,
180 	ZmOrganizer.FLAG_OFFLINE_GLOBAL,
181 	ZmOrganizer.FLAG_OFFLINE_SYNCABLE,
182 	ZmOrganizer.FLAG_OFFLINE_SYNCING
183 ];
184 
185 // org property for each flag
186 ZmOrganizer.FLAG_PROP = {};
187 ZmOrganizer.FLAG_PROP[ZmOrganizer.FLAG_CHECKED]				= "isChecked";
188 ZmOrganizer.FLAG_PROP[ZmOrganizer.FLAG_IMAP_SUBSCRIBED]		= "imapSubscribed";
189 ZmOrganizer.FLAG_PROP[ZmOrganizer.FLAG_EXCLUDE_FREE_BUSY]	= "excludeFreeBusy";
190 ZmOrganizer.FLAG_PROP[ZmOrganizer.FLAG_DISALLOW_SUBFOLDER]	= "disallowSubFolder";
191 ZmOrganizer.FLAG_PROP[ZmOrganizer.FLAG_OFFLINE_GLOBAL]		= "isOfflineGlobalSearch";
192 ZmOrganizer.FLAG_PROP[ZmOrganizer.FLAG_OFFLINE_SYNCABLE]	= "isOfflineSyncable";
193 ZmOrganizer.FLAG_PROP[ZmOrganizer.FLAG_OFFLINE_SYNCING]		= "isOfflineSyncing";
194 
195 // Following chars invalid in organizer names: " : / [anything less than " "]
196 ZmOrganizer.VALID_NAME_CHARS = "[^\\x00-\\x1F\\x7F:\\/\\\"]";
197 ZmOrganizer.VALID_PATH_CHARS = "[^\\x00-\\x1F\\x7F:\\\"]"; // forward slash is OK in path
198 ZmOrganizer.VALID_NAME_RE = new RegExp('^' + ZmOrganizer.VALID_NAME_CHARS + '+$');
199 
200 ZmOrganizer.MAX_NAME_LENGTH			= 128;	// max allowed by server
201 ZmOrganizer.MAX_DISPLAY_NAME_LENGTH	= 30;	// max we will show
202 
203 // color constants (server stores a number)
204 ZmOrganizer.C_NONE				= 0;
205 ZmOrganizer.C_BLUE				= 1;
206 ZmOrganizer.C_CYAN				= 2;
207 ZmOrganizer.C_GREEN				= 3;
208 ZmOrganizer.C_PURPLE			= 4;
209 ZmOrganizer.C_RED				= 5;
210 ZmOrganizer.C_YELLOW			= 6;
211 ZmOrganizer.C_PINK				= 7;
212 ZmOrganizer.C_GRAY				= 8;
213 ZmOrganizer.C_ORANGE			= 9;
214 ZmOrganizer.MAX_COLOR			= ZmOrganizer.C_ORANGE;
215 ZmOrganizer.ORG_DEFAULT_COLOR 	= ZmOrganizer.C_GRAY;
216 
217 ZmOrganizer.COLOR_VALUES = [
218 	null,
219 	ZmMsg.colorBlue,
220 	ZmMsg.colorCyan,
221 	ZmMsg.colorGreen,
222 	ZmMsg.colorPurple,
223 	ZmMsg.colorRed,
224 	ZmMsg.colorYellow,
225 	ZmMsg.colorPink,
226 	ZmMsg.colorGray,
227 	ZmMsg.colorOrange
228 ];
229 
230 // color names
231 ZmOrganizer.COLOR_TEXT = {};
232 ZmOrganizer.COLOR_TEXT[ZmOrganizer.C_NONE]		= ZmMsg.none;
233 ZmOrganizer.COLOR_TEXT[ZmOrganizer.C_ORANGE]	= ZmMsg.orange;
234 ZmOrganizer.COLOR_TEXT[ZmOrganizer.C_BLUE]		= ZmMsg.blue;
235 ZmOrganizer.COLOR_TEXT[ZmOrganizer.C_CYAN]		= ZmMsg.cyan;
236 ZmOrganizer.COLOR_TEXT[ZmOrganizer.C_GREEN]		= ZmMsg.green;
237 ZmOrganizer.COLOR_TEXT[ZmOrganizer.C_PURPLE]	= ZmMsg.purple;
238 ZmOrganizer.COLOR_TEXT[ZmOrganizer.C_RED]		= ZmMsg.red;
239 ZmOrganizer.COLOR_TEXT[ZmOrganizer.C_YELLOW]	= ZmMsg.yellow;
240 ZmOrganizer.COLOR_TEXT[ZmOrganizer.C_PINK]		= ZmMsg.pink;
241 ZmOrganizer.COLOR_TEXT[ZmOrganizer.C_GRAY]		= ZmMsg.gray;
242 
243 // list of colors and text for populating a color select menu
244 ZmOrganizer.COLORS = [];
245 ZmOrganizer.COLOR_CHOICES = [];
246 (function() {
247 	for (var i = 0; i <= ZmOrganizer.MAX_COLOR; i++) {
248 		var color = ZmOrganizer.COLOR_TEXT[i];
249 		ZmOrganizer.COLORS.push(color);
250 		ZmOrganizer.COLOR_CHOICES.push( { value:i, label:color } );
251 	}
252 })();
253 
254 
255 ZmOrganizer.MSG_KEY 		= {};		// keys for org names
256 ZmOrganizer.ROOT_MSG_KEY	= {};		// key for name of root (used as tree header)
257 ZmOrganizer.ITEM_ORGANIZER 	= {};		// primary organizer for item types
258 ZmOrganizer.DEFAULT_FOLDER 	= {};		// default folder for org type
259 ZmOrganizer.SOAP_CMD 		= {};		// SOAP command for modifying an org
260 ZmOrganizer.FIRST_USER_ID 	= {};		// lowest valid user ID for an org type
261 ZmOrganizer.PRECONDITION 	= {};		// setting that this org type depends on
262 ZmOrganizer.HAS_COLOR 		= {};		// whether an org uses colors
263 ZmOrganizer.DEFAULT_COLOR 	= {};		// default color for each org type
264 ZmOrganizer.ORG_COLOR 		= {};		// color overrides by ID
265 ZmOrganizer.APP 			= {};		// App responsible for organizer
266 ZmOrganizer.ORG_CLASS 		= {};		// constructor for organizer
267 ZmOrganizer.ORG_PACKAGE 	= {};		// package required to construct organizer
268 ZmOrganizer.CREATE_FUNC 	= {};		// function that creates this organizer
269 ZmOrganizer.LABEL 			= {};		// msg key for text for tree view header item
270 ZmOrganizer.ITEMS_KEY 		= {};		// msg key for text describing contents
271 ZmOrganizer.TREE_TYPE 		= {};		// type of server data tree that contains this type of organizer
272 ZmOrganizer.VIEWS 			= {};		// views by org type
273 ZmOrganizer.VIEW_HASH		= {};		// view hash by org type
274 ZmOrganizer.TYPE 			= {};		// types by view (reverse map of above)
275 ZmOrganizer.FOLDER_KEY 		= {};		// keys for label "[org] folder"
276 ZmOrganizer.MOUNT_KEY 		= {};		// keys for label "mount [org]"
277 ZmOrganizer.DEFERRABLE 		= {};		// creation can be deferred to app launch
278 ZmOrganizer.PATH_IN_NAME	= {};		// if true, provide full path when asked for name
279 ZmOrganizer.OPEN_SETTING	= {};		// setting that controls whether the tree view is open
280 ZmOrganizer.NEW_OP			= {};		// name of operation for new button in tree header (optional)
281 ZmOrganizer.DISPLAY_ORDER	= {};		// sort number to determine order of tree view (optional)
282 ZmOrganizer.HIDE_EMPTY		= {};		// if true, hide tree header if tree is empty
283 ZmOrganizer.SHAREABLE 		= {};		// allow share or not
284 
285 ZmOrganizer.APP2ORGANIZER	= {};		// organizer types, keyed by app name
286 ZmOrganizer.APP2ORGANIZER_R = {};		// app names, keyed by app organizer type
287 
288 // allowed permission bits
289 ZmOrganizer.PERM_READ		= "r";
290 ZmOrganizer.PERM_WRITE		= "w";
291 ZmOrganizer.PERM_INSERT		= "i";
292 ZmOrganizer.PERM_DELETE		= "d";
293 ZmOrganizer.PERM_ADMIN		= "a";
294 ZmOrganizer.PERM_WORKFLOW	= "x";
295 ZmOrganizer.PERM_PRIVATE	= "p";
296 
297 // Retention Policy Elements - Keep or Purge
298 ZmOrganizer.RETENTION_KEEP  = "keep";
299 ZmOrganizer.RETENTION_PURGE = "purge";
300 
301 // Abstract methods
302 
303 /**
304  * Stores information about the given organizer type.
305  * 
306  * @param {constant}	org				the organizer type
307  * @param {Hash}	params			a hash of parameters
308  * @param	{constant}	app				the app that handles this org type
309  * @param	{String}	nameKey			the msg key for org name
310  * @param	{constant}	precondition		the setting that this org type depends on
311  * @param	{int}	defaultFolder		the folder ID of default folder for this org
312  * @param	{String}	soapCmd			the SOAP command for acting on this org
313  * @param	{int}	firstUserId		the minimum ID for a user instance of this org
314  * @param	{String}	orgClass			the name of constructor for this org
315  * @param	{String}	orgPackage		the name of smallest package with org class
316  * @param	{String}	treeController	the name of associated tree controller
317  * @param	{String}	labelKey			the msg key for label in overview
318  * @param	{String}	itemsKey			the msg key for text describing contents
319  * @param	{Boolean}	hasColor			<code>true</code> if org has color associated with it
320  * @param	{constant}	defaultColor		the default color for org in overview
321  * @param	{Array}	orgColor			the color override by ID (in pairs)
322  * @param	{constant}	treeType			the type of data tree (from server) that contains this org
323  * @param	{String}	views				the associated folder views (JSON)
324  * @param	{String}	folderKey			the msg key for folder props dialog
325  * @param	{String}	mountKey			the msg key for folder mount dialog
326  * @param	{String}	createFunc		the name of function for creating this org
327  * @param	{String}	compareFunc		the name of function for comparing instances of this org
328  * @param	{Boolean}	deferrable		if <code>true</code>, creation can be deferred to app launch
329  * @param	{Boolean}	pathInName		if <code>true</code>, provide full path when asked for name
330  * @param	{constant}	openSetting		the setting that controls whether the tree view is open
331  * @param	{int}	displayOrder		the number that is used when sorting the display of trees. (Lower number means higher display.)
332  * @param	{Boolean}	hideEmpty			if <code>true</code>, hide tree header if tree is empty
333  */
334 ZmOrganizer.registerOrg =
335 function(org, params) {
336 	if (params.nameKey)			{ ZmOrganizer.MSG_KEY[org]				= params.nameKey; }
337 	if (params.app)				{
338 		ZmOrganizer.APP[org] = params.app;
339 		if (!ZmOrganizer.APP2ORGANIZER[params.app]) {
340 			ZmOrganizer.APP2ORGANIZER[params.app] = [];
341 		}
342 		ZmOrganizer.APP2ORGANIZER[params.app].push(org);
343         ZmOrganizer.APP2ORGANIZER_R[org] = params.app;
344 	}
345 	if (params.defaultFolder)	{ ZmOrganizer.DEFAULT_FOLDER[org]		= params.defaultFolder; }
346 	if (params.precondition)	{ ZmOrganizer.PRECONDITION[org]			= params.precondition; }
347 	if (params.soapCmd)			{ ZmOrganizer.SOAP_CMD[org]				= params.soapCmd; }
348 	if (params.firstUserId)		{ ZmOrganizer.FIRST_USER_ID[org]		= params.firstUserId; }
349 	if (params.orgClass)		{ ZmOrganizer.ORG_CLASS[org]			= params.orgClass; }
350 	if (params.orgPackage)		{ ZmOrganizer.ORG_PACKAGE[org]			= params.orgPackage; }
351 	if (params.labelKey)		{ ZmOrganizer.LABEL[org]				= params.labelKey; }
352 	if (params.itemsKey)		{ ZmOrganizer.ITEMS_KEY[org]			= params.itemsKey; }
353 	if (params.hasColor)		{ ZmOrganizer.HAS_COLOR[org]			= params.hasColor; }
354 	if (params.views)			{ ZmOrganizer.VIEWS[org]				= params.views; }
355 	if (params.folderKey)		{ ZmOrganizer.FOLDER_KEY[org]			= params.folderKey; }
356 	if (params.mountKey)		{ ZmOrganizer.MOUNT_KEY[org]			= params.mountKey; }
357 	if (params.deferrable)		{ ZmOrganizer.DEFERRABLE[org]			= params.deferrable; }
358 	if (params.pathInName)		{ ZmOrganizer.PATH_IN_NAME[org]			= params.pathInName; }
359 	if (params.openSetting)		{ ZmOrganizer.OPEN_SETTING[org]			= params.openSetting; }
360 	if (params.newOp)			{ ZmOrganizer.NEW_OP[org]				= params.newOp; }
361 	if (params.displayOrder)	{ ZmOrganizer.DISPLAY_ORDER[org]		= params.displayOrder; }
362 	if (params.hideEmpty)		{ ZmOrganizer.HIDE_EMPTY[org]			= params.hideEmpty; }
363 	ZmOrganizer.SHAREABLE[org]	= !params.disableShare; 
364 
365 	if (!appCtxt.isChildWindow || params.childWindow ) {
366 		if (params.compareFunc)		{ ZmTreeView.COMPARE_FUNC[org]			= params.compareFunc; }
367 		if (params.treeController)	{ ZmOverviewController.CONTROLLER[org]	= params.treeController; }
368 	}
369 
370 	ZmOrganizer.TREE_TYPE[org] = params.treeType || org; // default to own type
371 	ZmOrganizer.CREATE_FUNC[org] = params.createFunc || "ZmOrganizer.create";
372 
373 	if (params.views) {
374 		ZmOrganizer.VIEW_HASH[org] = AjxUtil.arrayAsHash(ZmOrganizer.VIEWS[org]);
375 	}
376 
377 	if (params.hasColor) {
378 		ZmOrganizer.DEFAULT_COLOR[org] = (params.defaultColor != null)
379 			? params.defaultColor
380 			: ZmOrganizer.ORG_DEFAULT_COLOR;
381 	}
382 
383 	if (params.orgColor) {
384 		for (var id in params.orgColor) {
385 			ZmOrganizer.ORG_COLOR[id] = params.orgColor[id];
386 		}
387 	}
388 
389 	if (params.dropTargets) {
390 		if (!ZmApp.DROP_TARGETS[params.app]) {
391 			ZmApp.DROP_TARGETS[params.app] = {};
392 		}
393 		ZmApp.DROP_TARGETS[params.app][org] = params.dropTargets;
394 	}
395 };
396 
397 ZmOrganizer.sortCompare = function(organizerA, organizerB) {};
398 
399 /**
400  * nulls value that is the default color for the type.
401  * @param value
402  */
403 ZmOrganizer.getColorValue =
404 function(value, type) {
405 	// no need to save color if missing or default
406 	if (value == ZmOrganizer.DEFAULT_COLOR[type]) {
407 		return null;
408 	}
409 
410 	return value;
411 };
412 
413 /**
414  * Creates an organizer via <code><CreateFolderRequest></code>. Attribute pairs can
415  * be passed in and will become attributes of the folder node in the request.
416  * 
417  * @param {Hash}	params	a hash of parameters
418  */
419 ZmOrganizer.create =
420 function(params) {
421 	var jsonObj = {CreateFolderRequest:{_jsns:"urn:zimbraMail"}};
422 	var folder = jsonObj.CreateFolderRequest.folder = {};
423 	var errorCallback = params.errorCallback || new AjxCallback(null, ZmOrganizer._handleErrorCreate, params);
424 	var type = params.type;
425 
426 	// set attributes
427 	params.view = params.view || ZmOrganizer.VIEWS[type] ? ZmOrganizer.VIEWS[type][0] : null;
428 	for (var i in params) {
429 		if (i == "type" || i == "errorCallback" || i == "account") { continue; }
430 
431 		var value = params[i];
432 		if (value) {
433 			folder[i] = value;
434 		}
435 	}
436 	//adding support to asyncMode == false didn't eventually help me, but why not keep it?
437 	var asyncMode = params.asyncMode === undefined ? true : params.asyncMode; //default is true
438 
439 	return appCtxt.getAppController().sendRequest({
440 		jsonObj: jsonObj,
441 		asyncMode: asyncMode,
442 		accountName: (params.account && params.account.name),
443 		callback: params.callback,
444 		callbackAfterNotifications: params.callbackAfterNotifications, 
445 		errorCallback: errorCallback
446 	});
447 };
448 
449 /**
450  * @private
451  */
452 ZmOrganizer._handleErrorCreate =
453 function(params, ex) {
454 	if (!params.url && !params.name) { return false; }
455 	
456 	var msg;
457 	if (params.name && (ex.code == ZmCsfeException.MAIL_ALREADY_EXISTS)) {
458 		var type = appCtxt.getFolderTree(appCtxt.getActiveAccount()).getFolderTypeByName(params.name);
459         msg = AjxMessageFormat.format(ZmMsg.errorAlreadyExists, [params.name, type.toLowerCase()]);
460 	} else if (params.url) {
461 		var errorMsg = (ex.code == ZmCsfeException.SVC_RESOURCE_UNREACHABLE) ? ZmMsg.feedUnreachable : ZmMsg.feedInvalid;
462 		msg = AjxMessageFormat.format(errorMsg, params.url);
463 	}
464 
465 	if (msg) {
466 		ZmOrganizer._showErrorMsg(msg);
467 		return true;
468 	}
469 
470 	return false;
471 };
472 
473 /**
474  * @private
475  */
476 ZmOrganizer._showErrorMsg =
477 function(msg) {
478 	var msgDialog = appCtxt.getMsgDialog();
479 	msgDialog.reset();
480 	msgDialog.setMessage(AjxStringUtil.htmlEncode(msg), DwtMessageDialog.CRITICAL_STYLE);
481 	msgDialog.popup();
482 };
483 
484 /**
485  * Gets the folder.
486  * 
487  * @param	{String}	id		the folder id
488  * @param	{AjxCallback}	callback	the callback
489  * @param	{ZmBatchCommand}	batchCmd	the batch command or <code>null</code> for none
490  */
491 ZmOrganizer.getFolder =
492 function(id, callback, batchCmd) {
493 	var jsonObj = {GetFolderRequest:{_jsns:"urn:zimbraMail"}};
494 	var request = jsonObj.GetFolderRequest;
495 	request.folder = {l:id};
496 	var respCallback = new AjxCallback(null, ZmOrganizer._handleResponseGetFolder, [callback]);
497 	if (batchCmd) {
498 		batchCmd.addRequestParams(jsonObj, respCallback);
499 	} else {
500 		appCtxt.getRequestMgr().sendRequest({jsonObj:jsonObj, asyncMode:true, callback:respCallback});
501 	}
502 };
503 
504 /**
505  * @private
506  */
507 ZmOrganizer._handleResponseGetFolder =
508 function(callback, result) {
509 	var resp = result.getResponse().GetFolderResponse;
510 	var folderObj = (resp && resp.folder && resp.folder[0]) ||
511 					(resp && resp.link && resp.link[0]);
512 	var folder;
513 	if (folderObj) {
514 		folder = appCtxt.getById(folderObj.id);
515 		if (folder) {
516 			folder.clearShares();
517 			folder._setSharesFromJs(folderObj);
518 		} else {
519 			var parent = appCtxt.getById(folderObj.l);
520 			folder = ZmFolderTree.createFromJs(parent, folderObj, appCtxt.getFolderTree(), "folder");
521 		}
522 	}
523 	if (callback) {
524 		callback.run(folder);
525 	}
526 };
527 
528 /**
529  * Gets the folder.
530  * 
531  * @param	{AjxCallback}	callback	the callback
532  * @param	{ZmBatchCommand}	batchCmd	the batch command or <code>null</code> for none
533  */
534 ZmOrganizer.prototype.getFolder =
535 function(callback, batchCmd) {
536 	ZmOrganizer.getFolder(this.id, callback, batchCmd);
537 };
538 
539 
540 // Static methods
541 
542 /**
543  * Gets the view name by organizer type.
544  * 
545  * @param	{String}	organizerType		the organizer type
546  * @return	{String}	the view
547  */
548 ZmOrganizer.getViewName =
549 function(organizerType) {
550 	return ZmOrganizer.VIEWS[organizerType][0];
551 };
552 
553 /**
554  * Checks an organizer (folder or tag) offlineSyncInterval for validity.
555  *
556  * @param {String}	value		offlineSyncInterval
557  * @return	{String}	<code>null</code> if the offlineSyncInterval is valid or an error message if the name is invalid
558  */
559 ZmOrganizer.checkWebOfflineSyncDays =
560 function(value) {
561     if (isNaN(value)) {	return ZmMsg.invalidFolderSyncInterval; }
562     var interval = parseInt(value);
563 	if (interval < 0 ||  interval > 30) {
564 		return ZmMsg.invalidFolderSyncInterval;
565 	}
566 	return null;
567 };
568 
569 /**
570  * Checks an organizer (folder or tag) name for validity.
571  *
572  * @param {String}	name		an organizer name
573  * @return	{String}	<code>null</code> if the name is valid or an error message if the name is invalid
574  */
575 ZmOrganizer.checkName =
576 function(name) {
577 	if (name.length == 0) {	return ZmMsg.nameEmpty; }
578 
579 	if (name.length > ZmOrganizer.MAX_NAME_LENGTH) {
580 		return AjxMessageFormat.format(ZmMsg.nameTooLong, ZmOrganizer.MAX_NAME_LENGTH);
581 	}
582 
583 	if (!ZmOrganizer.VALID_NAME_RE.test(name)) {
584 		return AjxMessageFormat.format(ZmMsg.errorInvalidName, AjxStringUtil.htmlEncode(name));
585 	}
586 
587 	return null;
588 };
589 
590 /**
591  * Checks a URL (a folder or calendar feed, for example) for validity.
592  *
593  * @param {String}	url	a URL
594  * @return	{String}	<code>null</code> if valid or an error message
595  */
596 ZmOrganizer.checkUrl =
597 function(url) {
598 	// TODO: be friendly and prepend "http://" when it's missing
599 	if (!url.match(/^[a-zA-Z]+:\/\/.*$/i)) {
600 		return ZmMsg.errorUrlMissing;
601 	}
602 
603 	return null;
604 };
605 
606 /**
607  * @private
608  */
609 ZmOrganizer.checkSortArgs =
610 function(orgA, orgB) {
611 	if (!orgA && !orgB) return 0;
612 	if (orgA && !orgB) return 1;
613 	if (!orgA && orgB) return -1;
614 	return null;
615 };
616 
617 /**
618  * @private
619  */
620 ZmOrganizer.checkColor =
621 function(color) {
622 	return ((color != null) && (color >= 0 && color <= ZmOrganizer.MAX_COLOR))
623 		? color : ZmOrganizer.ORG_DEFAULT_COLOR;
624 };
625 
626 /**
627  * Gets the system ID for the given system ID and account. Unless this
628  * is a child account, the system ID is returned unchanged. For child
629  * accounts, the ID consists of the account ID and the local ID.
630  * 
631  * @param {int}	id		the ID of a system organizer
632  * @param {ZmZimbraAccount}	account	the account
633  * @param {Boolean}		force		<code>true</code> to generate the fully qualified ID even if this is the main account
634  * @return	{String}	the ID
635  */
636 ZmOrganizer.getSystemId =
637 function(id, account, force) {
638 	account = account || appCtxt.getActiveAccount();
639 	if ((account && !account.isMain) || force) {
640 		return ((typeof(id) == "string") && (id.indexOf(":") != -1) || !id)
641 			? id : ([account.id, id].join(":"));
642 	}
643 	return id;
644 };
645 
646 /**
647  * Normalizes the id by stripping the account ID portion from a system ID for a child account, which
648  * can then be used to check against known system IDs. Any non-system ID is
649  * returned unchanged (if type is provided).
650  *
651  * @param {String}	id	ID of an organizer
652  * @param {constant}	type	the type of organizer
653  * @return	{String}	the resulting id
654  */
655 ZmOrganizer.normalizeId =
656 function(id, type) {
657 	if (typeof(id) != "string") { return id; }
658 	var idx = id.indexOf(":");
659 	var localId = (idx == -1) ? id : id.substr(idx + 1);
660 	return (type && (localId >= ZmOrganizer.FIRST_USER_ID[type])) ? id : localId;
661 };
662 
663 /**
664  * Parses an id into an object with fields for account and normalized id
665  *
666  * @param {String}	id		the ID of an organizer
667  * @param {Object}	result	an optional object in which the result is stored
668  * @return	{Object}	the resulting ID
669  */
670 ZmOrganizer.parseId =
671 function(id, result) {
672 	var ac = window.parentAppCtxt || window.appCtxt;
673 
674 	result = result || {};
675 	if (id == null) { return result; }
676 	var idx = (typeof id == "string") ? id.indexOf(":") : -1;
677 	if (idx == -1) {
678 		result.account = ac.accountList.mainAccount;
679 		result.id = id;
680 	} else {
681 		result.acctId = id.substring(0, idx);
682 		result.account = ac.accountList.getAccount(result.acctId);
683 		result.id = id.substr(idx + 1);
684 	}
685 	return result;
686 };
687 
688 // Public methods
689 
690 /**
691 * Gets the name of this organizer.
692 *
693 * @param {Boolean}	showUnread		<code>true</code> to display the number of unread items (in parens)
694 * @param {int}	maxLength		the length (in chars) to truncate the name to
695 * @param {Boolean}	noMarkup		if <code>true</code>, don't return any HTML
696 * @param {Boolean}	useSystemName	if <code>true</code>, don't use translated version of name
697 * @return	{String}	the name
698 */
699 ZmOrganizer.prototype.getName = 
700 function(showUnread, maxLength, noMarkup, useSystemName, useOwnerName, defaultRootType) {
701 	if (this.nId == ZmFolder.ID_ROOT) {
702 		var type = defaultRootType || this.type;
703 		return (ZmOrganizer.LABEL[type])
704 			? ZmMsg[ZmOrganizer.LABEL[type]] : "";
705 	}
706 	var name = (useSystemName && this._systemName) || (useOwnerName && this.oname) || this.name || "";
707 	if (ZmOrganizer.PATH_IN_NAME[this.type] && this.path) {
708 		name = [this.path, name].join("/");
709 	}
710 	name = (maxLength && name.length > maxLength)
711 		? name.substring(0, maxLength - 3) + "..." : name;
712 	return this._markupName(name, showUnread, noMarkup);
713 };
714 
715 /**
716 * Gets the full path as a string.
717 *
718 * @param {Boolean}	includeRoot		<code>true</code> to include root name at the beginning of the path
719 * @param {Boolean}	showUnread		<code>true</code> to display the number of unread items (in parens)
720 * @param {int}	maxLength		the length (in chars) to truncate the name to
721 * @param {Boolean}	noMarkup		if <code>true</code>, do not return any HTML
722 * @param {Boolean}	useSystemName	if <code>true</code>, use untranslated version of system folder names
723 * @return	{String}	the path
724 */
725 ZmOrganizer.prototype.getPath = 
726 function(includeRoot, showUnread, maxLength, noMarkup, useSystemName, useOwnerName) {
727 	var parent = this.parent;
728 	var path = this.getName(showUnread, maxLength, noMarkup, useSystemName, useOwnerName);
729 	while (parent && ((parent.nId != ZmOrganizer.ID_ROOT) || includeRoot)) {
730 		path = parent.getName(showUnread, maxLength, noMarkup, useSystemName, useOwnerName) + ZmFolder.SEP + path;
731 		parent = parent.parent;
732 	}
733 
734 	return path;
735 };
736 
737 /**
738  * Gets the tooltip. The tooltip shows number of unread items, total messages and the total size.
739  *
740  * @param {Boolean}	force		if <code>true</code>, don't use cached tooltip
741  * @return	{String}	the tooltip
742  */
743 ZmOrganizer.prototype.getToolTip =
744 function(force) {
745 	if (this.noTooltip) {
746 		return null;
747 	}
748     if (!this._tooltip || force) {
749 		var itemText = this._getItemsText();
750 		var unreadLabel = this._getUnreadLabel();
751 		var subs = {name:this.name, itemText:itemText, numTotal:this.numTotal, sizeTotal:this.sizeTotal, numUnread:this.numUnread, unreadLabel:unreadLabel};
752 		this._tooltip = AjxTemplate.expand("share.App#FolderTooltip", subs);
753 	}
754 	return this._tooltip;
755 };
756 
757 /**
758  * Gets the full path, suitable for use in search expressions.
759  *
760  * @return	{String}	the path
761  */
762 ZmOrganizer.prototype.getSearchPath =
763 function(useOwnerName) {
764 	return (this.nId != ZmOrganizer.ID_ROOT)
765 		? this.getPath(null, null, null, true, true, useOwnerName) : "/";
766 };
767 
768 /**
769  * Gets the URL.
770  * 
771  * @return	{String}	the URL
772  * 
773  * @deprecated use {@link getRestUrl}
774  */
775 ZmOrganizer.prototype.getUrl =
776 function() {
777 	return this.getRestUrl();
778 };
779 
780 /**
781  * Gets the sync URL.
782  * 
783  * @return		{String}	the URL
784  */
785 ZmOrganizer.prototype.getSyncUrl =
786 function() {
787 	return url;
788 };
789 
790 /**
791  * Gets the remote ID.
792  * 
793  * @return	{String}	the ID
794  */
795 ZmOrganizer.prototype.getRemoteId =
796 function() {
797 	if (!this._remoteId) {
798 		this._remoteId = (this.isRemote() && this.zid && this.rid)
799 			? (this.zid + ":" + this.rid)
800 			: this.id;
801 	}
802 	return this._remoteId;
803 };
804 
805 
806 /**
807  * Gets the REST URL.
808  * 
809  * @return	{String}	the URL
810  */
811 ZmOrganizer.prototype.getRestUrl =
812 function(noRemote) {
813 
814 	var restUrl = appCtxt.get(ZmSetting.REST_URL);
815 	if (restUrl && (!this.isRemote() || noRemote)) { //for remote - this does not work. either use this.restUrl (if set, which is for shared folder, but not for sub-folders) or call _generateRestUrl which seems to work for subfodlers of shared as well.
816 		var path = AjxStringUtil.urlEncode(this.getSearchPath()).replace("#","%23").replace(";", "%3B"); // User may type in a # in a folder name, but that's not ok for our urls
817 		// return REST URL as seen by the GetInfoResponse
818 		return ([restUrl, "/", path].join(""));
819 	}
820 
821 	// return REST URL as seen by server - this is the remote (isRemote() true) case - shared folder.
822 	if (this.restUrl) {
823 		return this.restUrl;
824 	}
825 
826 	// if server doesn't tell us what URL to use, do our best to generate
827 	var url = this._generateRestUrl();
828 	DBG.println(AjxDebug.DBG3, "NO REST URL FROM SERVER. GENERATED URL: " + url);
829 
830 	return url;
831 };
832 
833 /**
834  * Gets the OWNER'S REST URL,used to fetch resturl of shared folders.
835  *
836  * @return	{String}	the URL
837  */
838 ZmOrganizer.prototype.getOwnerRestUrl =
839 function(){
840   var restUrl=this.restUrl;
841   var path = AjxStringUtil.urlEncode(this.oname).replace("#","%23");
842 
843   // return REST URL as seen by the GetInfoResponse
844   return ([restUrl, "/", path].join(""));
845 };
846 
847 ZmOrganizer.prototype._generateRestUrl =
848 function() {
849 	var loc = document.location;
850 	var uname = appCtxt.get(ZmSetting.USERNAME);
851 	var host = loc.host;
852 	var m = uname.match(/^(.*)@(.*)$/);
853 
854 	host = (m && m[2]) || host;
855 
856 	// REVISIT: What about port? For now assume other host uses same port
857 	if (loc.port && loc.port != 80) {
858 		host = host + ":" + loc.port;
859 	}
860 
861 	var path = AjxStringUtil.urlEncode(this.getSearchPath()).replace("#","%23"); // User may type in a # in a folder name, but that's not ok for our urls
862 		
863 	return [
864 		loc.protocol, "//", host, "/service/user/", uname, "/",	path
865 	].join("");
866 };
867 
868 /**
869  * Gets the account.
870  * 
871  * @return	{ZmZimbraAccount}	the account
872  */
873 ZmOrganizer.prototype.getAccount =
874 function() {
875 	if (appCtxt.multiAccounts) {
876 		if (!this.account) {
877 			this.account = ZmOrganizer.parseId(this.id).account;
878 		}
879 		// bug 46364:
880 		// still no account?! Must be remote organizer, keep checking parent
881 		if (!this.account) {
882 			var parent = this.parent;
883 			while (parent && !this.account) {
884 				this.account = parent.getAccount();
885 				parent = parent.parent;
886 			}
887 		}
888 		return this.account;
889 	}
890 
891 	return (this.account || appCtxt.accountList.mainAccount);
892 };
893 
894 /**
895  * Gets the shares.
896  * 
897  * @return	{Array}	an array of shares
898  */
899 ZmOrganizer.prototype.getShares =
900 function() {
901 	return this.shares;
902 };
903 
904 /**
905  * Adds the share.
906  * 
907  * @param	{Object}	share		the share to add
908  */
909 ZmOrganizer.prototype.addShare =
910 function(share) {
911 	this.shares = this.shares || [];
912 	this.shares.push(share);
913 
914 	var curAcct = appCtxt.getActiveAccount();
915 	var curZid = curAcct && curAcct.id;
916 	var shareId = share.grantee && share.grantee.id;
917 	if (shareId && (shareId == curZid)) {
918 		this._mainShare = share;
919 	}
920 };
921 
922 /**
923  * Clears all shares.
924  * 
925  */
926 ZmOrganizer.prototype.clearShares =
927 function() {
928 	if (this.shares && this.shares.length) {
929 		for (var i = 0; i < this.shares.length; i++) {
930 			this.shares[i] = null;
931 		}
932 	}
933 	this.shares = null;
934 	this._mainShare = null;
935 };
936 
937 /**
938  * Gets the share granted to the current user.
939  * 
940  * @return	{String}	the main share
941  */
942 ZmOrganizer.prototype.getMainShare =
943 function() {
944 	if (!this._mainShare) {
945 		var curAcct = appCtxt.getActiveAccount();
946 		var curZid = curAcct && curAcct.id;
947 		if (curZid && this.shares && this.shares.length) {
948 			for (var i = 0; i < this.shares.length; i++) {
949 				var share = this.shares[i];
950 				var id = share && share.grantee && share.grantee.id;
951 				if (id && id == curZid) {
952 					this._mainShare = share;
953 					break;
954 				}
955 			}
956 		}
957 	}
958 	return this._mainShare;
959 };
960 
961 /**
962  * Checks if the organizer supports sharing.
963  * 
964  * @return	{Boolean}	<code>true</code> if the organizer supports sharing
965  */
966 ZmOrganizer.prototype.supportsSharing =
967 function() {
968 	// overload per organizer type
969 	return true;
970 };
971 
972 /**
973  * Checks if the organizer supports pulbic access.
974  * 
975  * @return	{Boolean}	<code>true</code> if the organizer supports public access
976  */
977 ZmOrganizer.prototype.supportsPublicAccess =
978 function() {
979 	// overload per organizer type
980 	return true;
981 };
982 
983 /**
984  * Checks if the organizer supports private permission.
985  * 
986  * @return	{Boolean}	<code>true</code> if the organizer supports private permission
987  */
988 ZmOrganizer.prototype.supportsPrivatePermission =
989 function() {
990 	// overload per organizer type
991 	return false;
992 };
993 
994 /**
995  * Gets the icon.
996  * 
997  * @return	{String}	the icon
998  */
999 ZmOrganizer.prototype.getIcon = function() {};
1000 
1001 /**
1002  * Gets the color of the organizer
1003  *
1004  * @return	{String}	the color
1005  */
1006 ZmOrganizer.prototype.getColor =
1007 function() {
1008     return this.rgb || ZmOrganizer.COLOR_VALUES[this.color];
1009 }
1010 
1011 
1012 /**
1013  * Gets the icon with color
1014  * 
1015  * @return	{String}	the icon
1016  */
1017 ZmOrganizer.prototype.getIconWithColor =
1018 function() {
1019 	var icon = this.getIcon() || "";
1020 	var color = this.getColor();
1021 	return color ? [icon,color].join(",color=") : icon;
1022 };
1023 
1024 // Actions
1025 
1026 /**
1027  * Renames the organizer.
1028  * 
1029  * @param	{String}	name		the name
1030  * @param	{AjxCallback}	callback		the callback
1031  * @param	{AjxCallback}	errorCallback		the error callback
1032  * @param	{ZmBatchCommand}	batchCmd		the batch command
1033  */
1034 ZmOrganizer.prototype.rename =
1035 function(name, callback, errorCallback, batchCmd) {
1036 	if (name == this.name) { return; }
1037 	var params = {
1038 		action: "rename",
1039 		attrs: {name: name},
1040 		callback: callback,
1041 		errorCallback: errorCallback,
1042 		batchCmd: batchCmd
1043 	};
1044 	this._organizerAction(params);
1045 };
1046 
1047 /**
1048  * Sets the web offline sync interval.
1049  *
1050  * @param	{String}	        interval		the web offline sync interval
1051  * @param	{AjxCallback}	    callback		the callback
1052  * @param	{AjxCallback}	    errorCallback   the error callback
1053  * @param   {ZmBatchCommand}    batchCmd        optional batch command
1054  */
1055 ZmOrganizer.prototype.setOfflineSyncInterval =
1056 function(interval, callback, errorCallback, batchCmd) {
1057 	if (this.webOfflineSyncDays == interval) { return; }
1058 
1059 	this._organizerAction({action: "webofflinesyncdays", attrs: {numDays: interval}, callback: callback,
1060                            errorCallback: errorCallback, batchCmd: batchCmd});
1061 };
1062 
1063 /**
1064  * Sets the color.
1065  * 
1066  * @param	{String}	        color		    the color
1067  * @param	{AjxCallback}	    callback		the callback
1068  * @param	{AjxCallback}	    errorCallback   the error callback
1069  * @param   {ZmBatchCommand}    batchCmd        optional batch command
1070  */
1071 ZmOrganizer.prototype.setColor =
1072 function(color, callback, errorCallback, batchCmd) {
1073 	var color = ZmOrganizer.checkColor(color);
1074 	if (!this.isColorChanged(color)) { return; }
1075 
1076 	this._organizerAction({action: "color", attrs: {color: color}, callback: callback,
1077                            errorCallback: errorCallback, batchCmd: batchCmd});
1078 };
1079 
1080 /**
1081  * Sets the RGB color.
1082  * 
1083  * @param	{Object}	        rgb		        the rgb
1084  * @param	{AjxCallback}	    callback		the callback
1085  * @param	{AjxCallback}	    errorCallback	the error callback
1086  * @param   {ZmBatchCommand}    batchCmd        optional batch command
1087  */
1088 ZmOrganizer.prototype.setRGB = function(rgb, callback, errorCallback, batchCmd) {
1089 	if (!this.isColorChanged(rgb)) { return; }
1090 	this._organizerAction({action: "color", attrs: {rgb: rgb}, callback: callback,
1091                            errorCallback: errorCallback, batchCmd: batchCmd});
1092 };
1093 
1094 
1095 ZmOrganizer.prototype.getRetentionPolicy =
1096 function(policyElement) {
1097     var policy = null;
1098     if (this.retentionPolicy && this.retentionPolicy[0] && this.retentionPolicy[0][policyElement] &&
1099         this.retentionPolicy[0][policyElement][0]       && this.retentionPolicy[0][policyElement][0].policy &&
1100         this.retentionPolicy[0][policyElement][0].policy[0]) {
1101         policy = this.retentionPolicy[0][policyElement][0].policy[0];
1102     }
1103     return policy;
1104 }
1105 
1106 ZmOrganizer.prototype.getRetentionPolicyLifetimeMsec =
1107 function(policy) {
1108     if (policy) {
1109         // Apply the keep (retention) period
1110         var lifetime = policy.lifetime;
1111         var amount = parseInt(lifetime);
1112         // Intervals taken from DateUtil.java.
1113         var interval = lifetime.slice(lifetime.length-1);
1114         var lifetimeMsec = 0;
1115         switch (interval) {
1116             case  "d": lifetimeMsec = amount * AjxDateUtil.MSEC_PER_DAY;    break;
1117             case  "h": lifetimeMsec = amount * AjxDateUtil.MSEC_PER_HOUR;   break;
1118             case  "m": lifetimeMsec = amount * AjxDateUtil.MSEC_PER_MINUTE; break;
1119             case  "s": lifetimeMsec = amount * 1000; break;
1120             case "ms": lifetimeMsec = amount;  break;
1121             default  : lifetimeMsec = amount * 1000; break;
1122         }
1123     }
1124     return lifetimeMsec;
1125 }
1126 
1127 /**
1128  * Sets the Retention Policy.
1129  *
1130  * @param	{Object}	        retentionPolicy     the new retention policy
1131  * @param	{AjxCallback}	    callback		    the callback
1132  * @param	{AjxCallback}	    errorCallback	    the error callback
1133  * @param   {ZmBatchCommand}    batchCmd            optional batch command
1134  */
1135 ZmOrganizer.prototype.setRetentionPolicy = function(newRetentionPolicy, callback, errorCallback, batchCmd) {
1136     var keepPolicy  = this.getRetentionPolicy(ZmOrganizer.RETENTION_KEEP);
1137     var purgePolicy = this.getRetentionPolicy(ZmOrganizer.RETENTION_PURGE);
1138     if (!this.policiesDiffer(keepPolicy,  newRetentionPolicy.keep) &&
1139         !this.policiesDiffer(purgePolicy, newRetentionPolicy.purge)) {
1140         // No updated policy specified or no changes.
1141         return;
1142     }
1143 
1144 	var cmd = ZmOrganizer.SOAP_CMD[this.type] + "Request";
1145 	var request = {
1146 		_jsns: "urn:zimbraMail",
1147 		action : {
1148 			op: "retentionpolicy",
1149 			id: this.id,
1150 			retentionPolicy: {
1151 				keep: {},
1152 				purge: {}
1153 			}
1154 		}
1155 	};
1156 	var jsonObj = {};
1157 	jsonObj[cmd] = request;
1158 
1159 	var retentionNode = request.action.retentionPolicy;
1160 
1161     if (newRetentionPolicy.keep) {
1162         this._addPolicy(retentionNode.keep, newRetentionPolicy.keep);
1163     }
1164     if (newRetentionPolicy.purge) {
1165         this._addPolicy(retentionNode.purge, newRetentionPolicy.purge);
1166     }
1167 
1168 	if (batchCmd) {
1169         batchCmd.addRequestParams(jsonObj, callback, errorCallback);
1170  	}
1171 	else {
1172 		var accountName;
1173 		if (appCtxt.multiAccounts) {
1174 			accountName = (this.account)
1175 				? this.account.name : appCtxt.accountList.mainAccount.name;
1176 		}
1177 		appCtxt.getAppController().sendRequest({
1178 			jsonObj:       jsonObj,
1179 			asyncMode:     true,
1180 			accountName:   accountName,
1181 			callback:      callback,
1182 			errorCallback: errorCallback
1183 		});
1184 	}
1185 
1186 };
1187 
1188 ZmOrganizer.prototype.policiesDiffer =
1189 function(policyA, policyB) {
1190     var differ = false;
1191     if ((policyA && !policyB) || (!policyA && policyB)) {
1192         differ = true;
1193     } else if (policyA) {
1194         // Old and new specified
1195         if (policyA.type != policyB.type) {
1196             differ = true;
1197         } else {
1198             if (policyA.type == "user") {
1199                 differ = policyA.lifetime != policyB.lifetime;
1200             } else {
1201                 // System policy
1202                 differ = policyA.id != policyB.id;
1203             }
1204         }
1205     }
1206     return differ;
1207 }
1208 
1209 ZmOrganizer.prototype._addPolicy =
1210 function(node, policy) {
1211 	var policyNode = node.policy = {};
1212 	for (var attr in policy) {
1213 		if (AjxEnv.isIE) {
1214 			policy[attr] += ""; //To string
1215 		}
1216 
1217 		policyNode[attr] = policy[attr];
1218 	}
1219 };
1220 
1221 /**
1222  * Returns color number b/w 0-9 for a given color code
1223  *
1224  * @param	{String}	color	The color (usually in #43eded format
1225  * @return {int} Returns 0-9 for a standard color and returns -1 for custom color
1226  */
1227 ZmOrganizer.getStandardColorNumber =
1228 function(color) {
1229 	if (String(color).match(/^#/)) {
1230 		var len = ZmOrganizer.COLOR_VALUES.length;
1231 		for(var i =0; i < len; i++) {
1232 			var currentVal = ZmOrganizer.COLOR_VALUES[i];
1233 			if(currentVal && currentVal == color) {
1234 				return i;
1235 			}
1236 		}
1237 	} else if(color <= 9 && color >= 0) {
1238 		return color;
1239 	}
1240 	return -1;
1241 };
1242 
1243 /**
1244  * Returns true if the color is changed
1245  *
1246  * @param	{String/int}	color	The color (usually in #rgb format or numeric color code
1247  * @return {Boolean} Returns true if the color is changed
1248  */
1249 ZmOrganizer.prototype.isColorChanged =
1250 function(color) {
1251     var isNewColorCustom = ZmOrganizer.getStandardColorNumber(color) === -1,
1252         isPrevColorCustom = this.isColorCustom;
1253     if ((isNewColorCustom && !isPrevColorCustom) ||
1254         (!isNewColorCustom && isPrevColorCustom) ) {
1255         //Custom changed to standard or standard changed to custom
1256         return true;
1257     }
1258     else if (isNewColorCustom && isPrevColorCustom) {
1259         //If both are custom colors check the rgb codes
1260         return color != this.rgb;
1261     }
1262     else if (!isNewColorCustom && !isPrevColorCustom){
1263         //If both are standard check the numeric color codes
1264         return color != this.color;
1265     }
1266     //default fallback
1267     return false;
1268 };
1269 
1270 /**
1271  * Updates the folder. Although it is possible to use this method to change just about any folder
1272  * attribute, it should only be used to set multiple attributes at once since it
1273  * has extra overhead on the server.
1274  *
1275  * @param {Hash}	attrs		the attributes
1276  */
1277 ZmOrganizer.prototype.update =
1278 function(attrs) {
1279 	this._organizerAction({action: "update", attrs: attrs});
1280 };
1281 
1282 /**
1283  * Assigns the organizer a new parent, moving it within its tree.
1284  *
1285  * @param {ZmOrganizer}	newParent		the new parent of this organizer
1286  * @param {boolean}		noUndo			if true, action is not undoable
1287  */
1288 ZmOrganizer.prototype.move =
1289 function(newParent, noUndo, batchCmd) {
1290 
1291 	var newId = (newParent.nId > 0)
1292 		? newParent.id
1293 		: ZmOrganizer.getSystemId(ZmOrganizer.ID_ROOT);
1294 
1295 	if ((newId == this.id || newId == this.parent.id) ||
1296 		(this.type == ZmOrganizer.FOLDER && (ZmOrganizer.normalizeId(newId, this.type) == ZmFolder.ID_SPAM)) ||
1297 		(newParent.isChildOf(this)))
1298 	{
1299 		return;
1300 	}
1301 	var params = {};
1302 	params.batchCmd = batchCmd;
1303 	params.actionTextKey = 'actionMoveOrganizer';
1304 	params.orgName = this.getName(false, false, true, false, false, this.type);
1305 	if (newId == ZmOrganizer.ID_TRASH) {
1306 		params.actionArg = ZmMsg.trash;
1307 		params.action = "trash";
1308 		params.noUndo = noUndo;
1309 	}
1310 	else {
1311 		if (newParent.account && newParent.account.isLocal()) {
1312 			newId = [ZmAccount.LOCAL_ACCOUNT_ID, newId].join(":");
1313 		}
1314 		params.actionArg = newParent.getName(false, false, true, false, false, this.type);
1315 		params.action = "move";
1316 		params.attrs = {l: newId};
1317 		params.noUndo = noUndo;
1318 	}
1319 	this._organizerAction(params);
1320 };
1321 
1322 /**
1323  * Deletes an organizer. If it's a folder, the server deletes any contents and/or
1324  * subfolders. If the organizer is "Trash" or "Spam", the server deletes and re-creates the
1325  * folder. In that case, we do not bother to remove it from the UI (and we ignore
1326  * creates on system folders).
1327  *
1328  */
1329 ZmOrganizer.prototype._delete =
1330 function(batchCmd) {
1331 	DBG.println(AjxDebug.DBG1, "deleting: " + AjxStringUtil.htmlEncode(this.name) + ", ID: " + this.id);
1332 	var isEmptyOp = ((this.type == ZmOrganizer.FOLDER || this.type == ZmOrganizer.ADDRBOOK || this.type == ZmOrganizer.BRIEFCASE) &&
1333 					 (this.nId == ZmFolder.ID_SPAM || this.nId == ZmFolder.ID_TRASH));
1334 	// make sure we're not deleting a system object (unless we're emptying SPAM or TRASH)
1335 	if (this.isSystem() && !isEmptyOp) return;
1336 
1337 	var action = isEmptyOp ? "empty" : "delete";
1338 	this._organizerAction({action: action, batchCmd: batchCmd});
1339 };
1340 
1341 /**
1342  * Empties the organizer.
1343  *
1344  * @param	{Boolean}	doRecursive		<code>true</code> to recursively empty the organizer
1345  * @param	{ZmBatchCommand}	batchCmd	the batch command
1346  * @param	{Object}	callback
1347  * @param	{number}	timeout		the timeout(in seconds)
1348  * @param	{AjxCallback}	errorCallback		the callback to run after timeout
1349  * @param	{Boolean}	noBusyOverlay		if <code>true</code>, do not show busy overlay
1350  */
1351 ZmOrganizer.prototype.empty =
1352 function(doRecursive, batchCmd, callback, timeout, errorCallback, noBusyOverlay) {
1353 	doRecursive = doRecursive || false;
1354 
1355 	var isEmptyOp = ((this.type == ZmOrganizer.FOLDER || this.type == ZmOrganizer.ADDRBOOK) &&
1356 					 (this.nId == ZmFolder.ID_SPAM ||
1357 					  this.nId == ZmFolder.ID_TRASH ||
1358 					  this.nId == ZmFolder.ID_CHATS ||
1359 					  this.nId == ZmOrganizer.ID_SYNC_FAILURES));
1360 
1361 	// make sure we're not emptying a system object (unless it's SPAM/TRASH/SYNCFAILURES)
1362 	if (this.isSystem() && !isEmptyOp) { return; }
1363 
1364 	var params = {
1365 		action: "empty",
1366 		batchCmd: batchCmd,
1367 		callback: callback,
1368 		timeout: timeout,
1369 		errorCallback: errorCallback,
1370 		noBusyOverlay: noBusyOverlay
1371 	};
1372 	params.attrs = (this.nId == ZmFolder.ID_TRASH)
1373 		? {recursive:true}
1374 		: {recursive:doRecursive};
1375 
1376 	if (this.isRemote()) {
1377 		params.id = this.getRemoteId();
1378 	}
1379 
1380 	this._organizerAction(params);
1381 };
1382 
1383 /**
1384  * Marks all items as "read".
1385  *
1386  * @param	{ZmBatchCommand}	batchCmd	the batch command
1387  */
1388 ZmOrganizer.prototype.markAllRead =
1389 function(batchCmd) {
1390 	var id = this.isRemote() ? this.getRemoteId() : null;
1391 	this._organizerAction({action: "read", id: id, attrs: {l: this.id}, batchCmd:batchCmd});
1392 };
1393 
1394 /**
1395  * Synchronizes the organizer.
1396  *
1397  */
1398 ZmOrganizer.prototype.sync =
1399 function() {
1400 	this._organizerAction({action: "sync"});
1401 };
1402 
1403 // Notification handling
1404 
1405 /**
1406  * Handles delete notification.
1407  *
1408  */
1409 ZmOrganizer.prototype.notifyDelete =
1410 function() {
1411 	// select next reasonable organizer if the currently selected organizer is
1412 	// the one being deleted or is a descendent of the one being deleted
1413 	var tc = appCtxt.getOverviewController().getTreeController(this.type);
1414 	var treeView = tc && tc.getTreeView(appCtxt.getCurrentApp().getOverviewId());
1415 
1416 	// treeview returns array of organizers for checkbox style trees
1417 	var organizers = treeView && treeView.getSelected();
1418 	if (organizers) {
1419 		if (!(organizers instanceof Array)) organizers = [organizers];
1420 		for (var i = 0; i <  organizers.length; i++) {
1421 			var organizer = organizers[i];
1422 			if (organizer && (organizer == this || organizer.isChildOf(this))) {
1423 				var folderId = this.parent.id;
1424 				if (this.parent.nId == ZmOrganizer.ID_ROOT) {
1425 					folderId = ZmOrganizer.getSystemId(this.getDefaultFolderId());
1426 				}
1427 				var skipNotify = false;
1428 				treeView.setSelected(folderId, skipNotify);
1429 			}
1430 		}
1431 	}
1432 
1433 	// perform actual delete
1434 	this.deleteLocal();
1435 	this._notify(ZmEvent.E_DELETE);
1436 };
1437 
1438 /**
1439  * Handles create notification.
1440  */
1441 ZmOrganizer.prototype.notifyCreate = function() {};
1442 
1443 /**
1444 * Handles modifications to fields that organizers have in general. Note that
1445 * the notification object may contain multiple notifications.
1446 *
1447 * @param {Object}	obj		a "modified" notification
1448 * @param {Hash}	details	the event details
1449 */
1450 ZmOrganizer.prototype.notifyModify =
1451 function(obj, details) {
1452 	var doNotify = false;
1453 	var details = details || {};
1454 	var fields = {};
1455 	if (obj.name != null && (this.name != obj.name || this.id != obj.id)) {
1456 		if (obj.id == this.id) {
1457 			details.oldName = this.name;
1458 			this.name = obj.name;
1459 			fields[ZmOrganizer.F_NAME] = true;
1460 			this.parent.children.sort(eval(ZmTreeView.COMPARE_FUNC[this.type]));
1461 		} else {
1462 			// rename of a remote folder
1463 			details.newName = obj.name;
1464 			fields[ZmOrganizer.F_RNAME] = true;
1465 		}
1466 		doNotify = true;
1467 	}
1468 	if (obj.u != null && this.numUnread != obj.u) {
1469 		this.numUnread = obj.u;
1470 		fields[ZmOrganizer.F_UNREAD] = true;
1471 		doNotify = true;
1472 	}
1473 	if (obj.n != null && this.numTotal != obj.n) {
1474 		this.numTotal = obj.n;
1475 		fields[ZmOrganizer.F_TOTAL] = true;
1476 		doNotify = true;
1477 	}
1478 	if (obj.s != null && this.sizeTotal != obj.s) {
1479 		this.sizeTotal = obj.s;
1480 		fields[ZmOrganizer.F_SIZE] = true;
1481 		doNotify = true;
1482 	}
1483 	if ((obj.rgb != null || obj.color != null) && !obj._isRemote) {
1484         var color = obj.color || obj.rgb;
1485 		if (this.isColorChanged(color)) {
1486 			this.isColorCustom = obj.rgb != null;
1487 			this.color = obj.color;
1488             this.rgb = obj.rgb || ZmOrganizer.COLOR_VALUES[color];
1489 			fields[ZmOrganizer.F_COLOR] = true;
1490             fields[ZmOrganizer.F_RGB] = true;
1491 		}
1492 		doNotify = true;
1493 	}
1494 	if (obj.f != null && !obj._isRemote) {
1495 		var oflags = this._setFlags().split("").sort().join("");
1496 		var nflags = obj.f.split("").sort().join("");
1497 		if (oflags != nflags) {
1498 			this._parseFlags(obj.f);
1499 			fields[ZmOrganizer.F_FLAGS] = true;
1500 			doNotify = true;
1501 		}
1502 	}
1503 	if (obj.rest != null && this.restUrl != obj.rest && !obj._isRemote) {
1504 		this.restUrl = obj.rest;
1505 		fields[ZmOrganizer.F_REST_URL] = true;
1506 		doNotify = true;
1507 	}
1508 	// if shares changed, do wholesale replace
1509 	if (obj.acl) {
1510 		this.clearShares();
1511 		if (obj.acl.grant && obj.acl.grant.length) {
1512 			AjxDispatcher.require("Share");
1513 			for (var i = 0; i < obj.acl.grant.length; i++) {
1514 				share = ZmShare.createFromJs(this, obj.acl.grant[i]);
1515 				this.addShare(share);
1516 			}
1517 		}
1518 		fields[ZmOrganizer.F_SHARES] = true;
1519 		doNotify = true;
1520 	}
1521 	if (obj.perm && obj._isRemote) {
1522 		fields[ZmOrganizer.F_PERMS] = true;
1523 		doNotify = true;
1524 
1525 		// clear acl-related flags so they are recalculated
1526 		this._isAdmin = this._isReadOnly = this._hasPrivateAccess = null;
1527 	}
1528     if (obj.retentionPolicy) {
1529         // Only displayed in a modal dialog - no need to doNotify
1530         if (obj.retentionPolicy[0].keep || obj.retentionPolicy[0].purge) {
1531             this.retentionPolicy = obj.retentionPolicy;
1532         } else {
1533             this.retentionPolicy = null;
1534         }
1535     }
1536 	if (obj.hasOwnProperty("webOfflineSyncDays")) {
1537 		this.webOfflineSyncDays = obj.webOfflineSyncDays;
1538 	}
1539 
1540 	// Send out composite MODIFY change event
1541 	if (doNotify) {
1542 		details.fields = fields;
1543 		this._notify(ZmEvent.E_MODIFY, details);
1544 	}
1545 
1546 	if (this.parent && obj.l != null && obj.l != this.parent.id) {
1547 		var newParent = this._getNewParent(obj.l);
1548 		if (newParent) {
1549 			this.reparent(newParent);
1550 			this._notify(ZmEvent.E_MOVE);
1551 			// could be moving search between Folders and Searches - make sure
1552 			// it has the correct tree
1553 			this.tree = newParent.tree;
1554 		}
1555 	}
1556 };
1557 
1558 // Local change handling
1559 
1560 /**
1561  * Deletes the organizer (local). Cleans up a deleted organizer:
1562  *
1563  * <ul>
1564  * <li>remove from parent's list of children</li>
1565  * <li>remove from item cache</li>
1566  * <li>perform above two steps for each child</li>
1567  * <li>clear list of children</li>
1568  * </ul>
1569  *
1570  */
1571 ZmOrganizer.prototype.deleteLocal =
1572 function() {
1573 	this.parent.children.remove(this);
1574 	var a = this.children.getArray();
1575 	var sz = this.children.size();
1576 	for (var i = 0; i < sz; i++) {
1577 		var org = a[i];
1578 		if (org) { org.deleteLocal(); }
1579 	}
1580 	this.children.removeAll();
1581 };
1582 
1583 /**
1584  * Checks if the organizer has a child with the given name.
1585  *
1586  * @param {String}	name		the name of the organizer to look for
1587  * @return	{Boolean}	<code>true</code> if the organizer has a child
1588  */
1589 ZmOrganizer.prototype.hasChild =
1590 function(name) {
1591 	return (this.getChild(name) != null);
1592 };
1593 
1594 /**
1595 * Gets the child with the given name
1596 *
1597 * @param {String}	name		the name of the organizer to look for
1598 * @return	{String}	the name of the child or <code>null</code> if no child has the name
1599 */
1600 ZmOrganizer.prototype.getChild =
1601 function(name) {
1602 	name = name ? name.toLowerCase() : "";
1603 	var a = this.children.getArray();
1604 	var sz = this.children.size();
1605 	for (var i = 0; i < sz; i++) {
1606 		if (a[i] && a[i].name && (a[i].name.toLowerCase() == name)) {
1607 			return a[i];
1608 		}
1609 	}
1610 
1611 	return null;
1612 };
1613 
1614 /**
1615 * Gets the child with the given path
1616 *
1617 * @param {String}	path		the path of the organizer to look for
1618 * @return	{String}	the child or <code>null</code> if no child has the path
1619 */
1620 ZmOrganizer.prototype.getChildByPath =
1621 function(path) {
1622 	// get starting organizer
1623 	var organizer = this;
1624 	if (path.match(/^\//)) {
1625 		while (organizer.nId != ZmOrganizer.ID_ROOT) {
1626 			organizer = organizer.parent;
1627 		}
1628 		path = path.substr(1);
1629 	}
1630 
1631 	// if no path, return current organizer
1632 	if (path.length == 0) return organizer;
1633 
1634 	// walk descendent axis to find organizer specified by path
1635 	var parts = path.split('/');
1636 	var i = 0;
1637 	while (i < parts.length) {
1638 		var part = parts[i++];
1639 		var child = organizer.getChild(part);
1640 		if (child == null) {
1641 			return null;
1642 		}
1643 		organizer = child;
1644 	}
1645 	return organizer;
1646 };
1647 
1648 /**
1649  * Changes the parent of this organizer. Note that the new parent passed
1650  * in may be <code>null</code>, which makes this organizer an orphan.
1651  *
1652  * @param {ZmOrganizer}	newParent		the new parent
1653  */
1654 ZmOrganizer.prototype.reparent =
1655 function(newParent) {
1656 	if (this.parent) {
1657 		this.parent.children.remove(this);
1658 	}
1659 	if (newParent) {
1660 		newParent.children.add(this);
1661 	}
1662 	this.parent = newParent;
1663 };
1664 
1665 /**
1666  * Gets the organizer with the given ID, searching recursively through
1667  * child organizers. The preferred method for getting an organizer by ID
1668  * is to use <code>appCtxt.getById()</code>.
1669  *
1670  * @param {String}	id		the ID to search for
1671  * @return	{ZmOrganizer}	the organizer or <code>null</code> if not found
1672  */
1673 ZmOrganizer.prototype.getById =
1674 function(id) {
1675 	if (this.link && id && typeof(id) == "string") {
1676 		var ids = id.split(":");
1677 		if (this.zid == ids[0] && this.rid == ids[1])
1678 			return this;
1679 	}
1680 
1681 	if (this.id == id || this.nId == id) {
1682 		return this;
1683 	}
1684 
1685 	var organizer;
1686 	var a = this.children.getArray();
1687 	var sz = this.children.size();
1688 	for (var i = 0; i < sz; i++) {
1689 		if (organizer = a[i].getById(id)) {
1690 			return organizer;
1691 		}
1692 	}
1693 	return null;
1694 };
1695 
1696 /**
1697  * Gets the first organizer found with the given name, starting from the root.
1698  *
1699  * @param {String}	name		the name to search for
1700  * @return	{ZmOrganizer}	the organizer
1701  */
1702 ZmOrganizer.prototype.getByName =
1703 function(name, skipImap) {
1704 	return this._getByName(name.toLowerCase(), skipImap);
1705 };
1706 
1707 /**
1708  * Gets a list of organizers with the given type.
1709  *
1710  * @param {constant}	type			the desired organizer type
1711  * @return	{Array}	an array of {ZmOrganizer} objects
1712  */
1713 ZmOrganizer.prototype.getByType =
1714 function(type) {
1715 	var list = [];
1716 	this._getByType(type, list);
1717 	return list;
1718 };
1719 
1720 /**
1721  * @private
1722  */
1723 ZmOrganizer.prototype._getByType =
1724 function(type, list) {
1725 	if (this.type == type) {
1726 		list.push(this);
1727 	}
1728 	var a = this.children.getArray();
1729 	for (var i = 0; i < a.length; i++) {
1730 		if (a[i]) {
1731 			a[i]._getByType(type, list);
1732 		}
1733 	}
1734 };
1735 
1736 /**
1737  * Gets the organizer with the given path.
1738  *
1739  * @param {String}	path			the path to search for
1740  * @param {Boolean}	useSystemName	if <code>true</code>, use untranslated version of system folder names
1741  * @return	{ZmOrganizer}	the organizer
1742  */
1743 ZmOrganizer.prototype.getByPath =
1744 function(path, useSystemName) {
1745 	return this._getByPath(path.toLowerCase(), useSystemName);
1746 };
1747 
1748 /**
1749  * Test the path of this folder and then descendants against the given path, case insensitively.
1750  *
1751  * @private
1752  */
1753 ZmOrganizer.prototype._getByPath =
1754 function(path, useSystemName) {
1755 	if (this.nId == ZmFolder.ID_TAGS) { return null; }
1756 
1757 	if (path == this.getPath(false, false, null, true, useSystemName).toLowerCase()) {
1758 		return this;
1759 	}
1760 
1761 	var a = this.children.getArray();
1762 	for (var i = 0; i < a.length; i++) {
1763 		var organizer = a[i]._getByPath(path, useSystemName);
1764 		if (organizer) {
1765 			return organizer;
1766 		}
1767 	}
1768 	return null;
1769 };
1770 
1771 /**
1772  * Gets the number of children of this organizer.
1773  *
1774  * @return	{int}	the size
1775  */
1776 ZmOrganizer.prototype.size =
1777 function() {
1778 	return this.children.size();
1779 };
1780 
1781 /**
1782  * Checks if the given organizer is a descendant of this one.
1783  *
1784  * @param {ZmOrganizer}	organizer		a possible descendant of ours
1785  * @return	{Boolean}	<code>if the given organizer is a descendant; <code>false</code> otherwise
1786  */
1787 ZmOrganizer.prototype.isChildOf =
1788 function (organizer) {
1789 	var parent = this.parent;
1790 	while (parent) {
1791 		if (parent == organizer) {
1792 			return true;
1793 		}
1794 		parent = parent.parent;
1795 	}
1796 	return false;
1797 };
1798 
1799 /**
1800  * Gets the organizer with the given ID (looks in this organizer tree).
1801  *
1802  * @param {int}	parentId	the ID of the organizer to find
1803  * @return	{ZmOrganizer}	the organizer
1804  *
1805  * @private
1806  */
1807 ZmOrganizer.prototype._getNewParent =
1808 function(parentId) {
1809 	return appCtxt.getById(parentId);
1810 };
1811 
1812 /**
1813  * Checks if the organizer with the given ID is under this organizer.
1814  *
1815  * @param	{String}	id		the ID
1816  * @return	{Boolean}	<code>true</code> if the organizer is under this organizer
1817  */
1818 ZmOrganizer.prototype.isUnder =
1819 function(id) {
1820 	id = id.toString();
1821 	if (this.nId == id || (this.isRemote() && this.rid == id)) { return true; }
1822 
1823 	var parent = this.parent;
1824 	while (parent && parent.nId != ZmOrganizer.ID_ROOT) {
1825 		if (parent.nId == id) {
1826 			return true;
1827 		}
1828 		parent = parent.parent;
1829 	}
1830 	return false;
1831 };
1832 
1833 /**
1834  * Checks if this organizer is in "Trash".
1835  *
1836  * @return	{Boolean}	<code>true</code> if in "Trash"
1837  */
1838 ZmOrganizer.prototype.isInTrash =
1839 function() {
1840 	return this.isUnder(ZmOrganizer.ID_TRASH);
1841 };
1842 
1843 /**
1844  * Checks if permissions are allowed.
1845  *
1846  * @return	{Boolean}	<code>true</code> if permissions are allowed
1847  */
1848 ZmOrganizer.prototype.isPermAllowed =
1849 function(perm) {
1850 	if (this.perm) {
1851 		var positivePerms = this.perm.replace(/-./g, "");
1852 		return (positivePerms.indexOf(perm) != -1);
1853 	}
1854 	return false;
1855 };
1856 
1857 /**
1858  * Checks if the organizer is read-only.
1859  *
1860  * @return	{Boolean}	<code>true</code> if read-only
1861  */
1862 ZmOrganizer.prototype.isReadOnly =
1863 function() {
1864 	if (this._isReadOnly == null) {
1865 		var share = this.getMainShare();
1866 		this._isReadOnly = (share != null)
1867 			? (this.isRemote() && !share.isWrite())
1868 			: (this.isRemote() && this.isPermAllowed(ZmOrganizer.PERM_READ) && !this.isPermAllowed(ZmOrganizer.PERM_WRITE));
1869 	}
1870 	return this._isReadOnly;
1871 };
1872 
1873 /**
1874  * Checks if admin.
1875  *
1876  * @return	{Boolean}	<code>true</code> if this organizer is admin
1877  */
1878 ZmOrganizer.prototype.isAdmin =
1879 function() {
1880 	if (this._isAdmin == null) {
1881 		var share = this.getMainShare();
1882 		this._isAdmin = (share != null)
1883 			? (this.isRemote() && share.isAdmin())
1884 			: (this.isRemote() && this.isPermAllowed(ZmOrganizer.PERM_ADMIN));
1885 	}
1886 	return this._isAdmin;
1887 };
1888 
1889 /**
1890  * Checks if the organizer has private access.
1891  *
1892  * @return	{Boolean}	<code>true</code> if has private access
1893  */
1894 ZmOrganizer.prototype.hasPrivateAccess =
1895 function() {
1896 	if (this._hasPrivateAccess == null) {
1897 		var share = this.getMainShare();
1898 		this._hasPrivateAccess = (share != null)
1899 			? (this.isRemote() && share.hasPrivateAccess())
1900 			: (this.isRemote() && this.isPermAllowed(ZmOrganizer.PERM_PRIVATE));
1901 	}
1902 	return this._hasPrivateAccess;
1903 };
1904 
1905 /**
1906  * Checks if the organizer is "remote". That applies to mountpoints (links),
1907  * the folders they represent, and any subfolders we know about.
1908  *
1909  * @return	{Boolean}	<code>true</code> if the organizer is "remote"
1910  */
1911 ZmOrganizer.prototype.isRemote =
1912 function() {
1913 	if (this._isRemote == null) {
1914 		if (this.zid != null) {
1915 			this._isRemote = true;
1916 		} else {
1917 			if (appCtxt.multiAccounts) {
1918 				var account = this.account;
1919 				var parsed = ZmOrganizer.parseId(this.id);
1920 
1921 				if (!account) {
1922 					if (parsed.account && parsed.account.isMain) {
1923 						this._isRemote = false;
1924 						return this._isRemote;
1925 					} else {
1926 						account = appCtxt.getActiveAccount();
1927 					}
1928 				}
1929 				this._isRemote = Boolean(!parsed.account || (parsed.account && (parsed.account != account)));
1930 			} else {
1931 				var id = String(this.id);
1932 				this._isRemote = ((id.indexOf(":") != -1) && (id.indexOf(appCtxt.getActiveAccount().id) != 0));
1933 			}
1934 		}
1935 	}
1936 	return this._isRemote;
1937 };
1938 
1939 ZmOrganizer.prototype.isRemoteRoot = function() {
1940 	return this.isRemote() && (this.rid == ZmOrganizer.ID_ROOT);
1941 }
1942 
1943 /**
1944  * Checks if the organizer is a system tag or folder.
1945  *
1946  * @return	{Boolean}	<code>true</code> if system tag or folder
1947  */
1948 ZmOrganizer.prototype.isSystem =
1949 function () {
1950 	return (this.nId < ZmOrganizer.FIRST_USER_ID[this.type]);
1951 };
1952 
1953 ZmOrganizer.prototype.isDefault =
1954 function () {
1955 	return this.nId == this.getDefaultFolderId();
1956 };
1957 
1958 ZmOrganizer.prototype.getDefaultFolderId =
1959 function() {
1960 	return ZmOrganizer.DEFAULT_FOLDER[this.type];
1961 };
1962 
1963 ZmOrganizer.prototype.isTrash =
1964 function () {
1965 	return this.nId == ZmFolder.ID_TRASH;
1966 };
1967 
1968 
1969 /**
1970  * Checks if the organizer gets its contents from an external feed.
1971  *
1972  * @return	{Boolean}	<code>true</code>  if from external feed
1973  */
1974 ZmOrganizer.prototype.isFeed =
1975 function () {
1976 	return Boolean(this.url);
1977 };
1978 
1979 /** Returns true if organizer has feeds. */
1980 ZmOrganizer.prototype.hasFeeds = function() { return false; };
1981 
1982 /**
1983  * Checks if this folder maps to a datasource. If type is given, returns
1984  * true if folder maps to a datasource *and* is of the given type.
1985  *
1986  * @param	{constant}	type			the type (see {@link ZmAccount.TYPE_POP} or {@link ZmAccount.TYPE_IMAP})
1987  * @param	{Boolean}	checkParent		if <code>true</code>, walk-up the parent chain
1988  * @return	{Boolean}	<code>true</code> if this folder maps to a datasource
1989  */
1990 ZmOrganizer.prototype.isDataSource =
1991 function(type, checkParent) {
1992 	var dss = this.getDataSources(type, checkParent);
1993 	return (dss && dss.length > 0);
1994 };
1995 
1996 /**
1997  * Gets the data sources this folder maps to. If type is given,
1998  * returns non-null result only if folder maps to datasource(s) *and* is of the
1999  * given type.
2000  *
2001  * @param	{constant}	type			the type (see {@link ZmAccount.TYPE_POP} or {@link ZmAccount.TYPE_IMAP})
2002  * @param	{Boolean}	checkParent		if <code>true</code>, walk-up the parent chain
2003  * @return	{Array}	the data sources this folder maps to or <code>null</code> for none
2004  */
2005 ZmOrganizer.prototype.getDataSources =
2006 function(type, checkParent) {
2007 	if (!appCtxt.get(ZmSetting.MAIL_ENABLED)) { return null; }
2008 
2009 	var dsc = appCtxt.getDataSourceCollection();
2010 	var dataSources = dsc.getByFolderId(this.nId, type);
2011 
2012 	if (dataSources.length == 0) {
2013 		return (checkParent && this.parent)
2014 			? this.parent.getDataSources(type, checkParent)
2015 			: null;
2016 	}
2017 
2018 	return dataSources;
2019 };
2020 
2021 /**
2022  * Gets the owner.
2023  *
2024  * @return	{String}	the owner
2025  */
2026 ZmOrganizer.prototype.getOwner =
2027 function() {
2028 	return this.owner || (this.parent && this.parent.getOwner()) || appCtxt.get(ZmSetting.USERNAME);
2029 };
2030 
2031 /**
2032  * Gets the sort index.
2033  *
2034  * @return	{int}	the sort index
2035  */
2036 ZmOrganizer.getSortIndex =
2037 function(child, sortFunction) {
2038 	if (!(child && child.parent && sortFunction)) { return null; }
2039 	var children = child.parent.children.getArray();
2040 	for (var i = 0; i < children.length; i++) {
2041 		var test = sortFunction(child, children[i]);
2042 		if (test == -1) {
2043 			return i;
2044 		}
2045 	}
2046 	return i;
2047 };
2048 
2049 /**
2050  * Sends a request to the server. Note that it's done asynchronously, but
2051  * there is no callback given. Hence, an organizer action is the last thing
2052  * done before returning to the event loop. The result of the action is
2053  * handled via notifications.
2054  *
2055  * @param {String}	action		the operation to perform
2056  * @param {Hash}	attrs		a hash of additional attributes to set in the request
2057  * @param {ZmBatchCommand}	batchCmd	the batch command that contains this request
2058  *
2059  * @private
2060  */
2061 ZmOrganizer.prototype._organizerAction =
2062 function(params) {
2063 
2064 	var cmd = ZmOrganizer.SOAP_CMD[this.type] + "Request";
2065 	var request = {
2066 		_jsns: "urn:zimbraMail",
2067 		action : {
2068 			op: params.action,
2069 			id: params.id || this.id
2070 		}
2071 	};
2072 	var jsonObj = {};
2073 	jsonObj[cmd] = request;
2074 
2075 	for (var attr in params.attrs) {
2076 		if (AjxEnv.isIE) {
2077 			params.attrs[attr] += ""; //To string
2078 		}
2079 		request.action[attr] = params.attrs[attr];
2080 	}
2081 	var actionController = appCtxt.getActionController();
2082 	actionController.dismiss();
2083 	var actionLogItem = (!params.noUndo && actionController && actionController.actionPerformed({op: params.action, id: params.id || this.id, attrs: params.attrs})) || null;
2084 	var respCallback = new AjxCallback(this, this._handleResponseOrganizerAction, [params, actionLogItem]);
2085 	if (params.batchCmd) {
2086         params.batchCmd.addRequestParams(jsonObj, respCallback, params.errorCallback);
2087  	}
2088 	else {
2089 		var accountName;
2090 		if (appCtxt.multiAccounts) {
2091 			accountName = (this.account)
2092 				? this.account.name : appCtxt.accountList.mainAccount.name;
2093 		}
2094 		appCtxt.getAppController().sendRequest({
2095 			jsonObj: jsonObj,
2096 			asyncMode: true,
2097 			accountName: accountName,
2098 			callback: respCallback,
2099 			errorCallback: params.errorCallback,
2100 			timeout: params.timeout,
2101 			noBusyOverlay: params.noBusyOverlay
2102 		});
2103 	}
2104 };
2105 
2106 /**
2107  * @private
2108  */
2109 ZmOrganizer.prototype._handleResponseOrganizerAction =
2110 function(params, actionLogItem, result) {
2111 
2112 	if (actionLogItem) {
2113 		actionLogItem.setComplete();
2114 	}
2115 	if (params.callback) {
2116 		params.callback.run(result);
2117 	}
2118 	if (params.actionTextKey) {
2119 		var actionController = appCtxt.getActionController();
2120 		var summary = ZmOrganizer.getActionSummary({
2121 			actionTextKey:  params.actionTextKey,
2122 			numItems:       params.numItems || 1,
2123 			type:           this.type,
2124 			orgName:        params.orgName,
2125 			actionArg:      params.actionArg
2126 		});
2127 		var undoLink = actionLogItem && actionController && actionController.getUndoLink(actionLogItem);
2128 		if (undoLink && actionController) {
2129 			actionController.onPopup();
2130 			appCtxt.setStatusMsg({msg: summary+undoLink, transitions: actionController.getStatusTransitions()});
2131 		} else {
2132 			appCtxt.setStatusMsg(summary);
2133 		}
2134 	}
2135 };
2136 
2137 /**
2138  * Returns a string describing an action, intended for display as toast to tell the
2139  * user what they just did.
2140  *
2141  * @param   {Object}        params          hash of params:
2142  *          {String}        type            organizer type (ZmOrganizer.*)
2143  *          {String}        actionTextKey   ZmMsg key for text string describing action
2144  *          {String}        orgName         name of the organizer that was affected
2145  *          {String}        actionArg       (optional) additional argument
2146  *
2147  * @return {String}     action summary
2148  */
2149 ZmOrganizer.getActionSummary =
2150 function(params) {
2151 
2152 	var type = params.type,
2153 		typeKey = ZmOrganizer.FOLDER_KEY[type],
2154 		typeText = ZmMsg[typeKey],
2155 		capKey = AjxStringUtil.capitalize(typeKey),
2156 		alternateKey = params.actionTextKey + capKey,
2157 		text = ZmMsg[alternateKey] || ZmMsg[params.actionTextKey],
2158 		orgName = AjxStringUtil.htmlEncode(params.orgName),
2159 		arg = AjxStringUtil.htmlEncode(params.actionArg);
2160 
2161 	return AjxMessageFormat.format(text, [ typeText, orgName, arg ]);
2162 };
2163 
2164 /**
2165  * Test the name of this organizer and then descendants against the given name, case insensitively.
2166  * 
2167  * @private
2168  */
2169 ZmOrganizer.prototype._getByName =
2170 function(name, skipImap) {
2171 	if (this.name && name == this.name.toLowerCase()) {
2172 		return this;
2173 	}
2174 
2175 	var organizer;
2176 	var a = this.children.getArray();
2177 	var sz = this.children.size();
2178 	for (var i = 0; i < sz; i++) {
2179 		if (organizer = a[i]._getByName(name, skipImap)) {
2180 			if (skipImap && organizer.isDataSource(ZmAccount.TYPE_IMAP, true)) {
2181 				continue;
2182 			}
2183 			return organizer;
2184 		}
2185 	}
2186 	return null;
2187 };
2188 
2189 /**
2190  * Takes a string of flag chars and applies them to this organizer.
2191  * 
2192  * @private
2193  */
2194 ZmOrganizer.prototype._parseFlags =
2195 function(str) {
2196 	for (var i = 0; i < ZmOrganizer.ALL_FLAGS.length; i++) {
2197 		var flag = ZmOrganizer.ALL_FLAGS[i];
2198 		this[ZmOrganizer.FLAG_PROP[flag]] = (Boolean(str && (str.indexOf(flag) != -1)));
2199 	}
2200 };
2201 
2202 /**
2203  * Converts this organizer's flag-related props into a string of flag chars.
2204  * 
2205  * @private
2206  */
2207 ZmOrganizer.prototype._setFlags =
2208 function() {
2209 	var flags = "";
2210 	for (var i = 0; i < ZmOrganizer.ALL_FLAGS.length; i++) {
2211 		var flag = ZmOrganizer.ALL_FLAGS[i];
2212 		var prop = ZmOrganizer.FLAG_PROP[flag];
2213 		if (this[prop]) {
2214 			flags = flags + flag;
2215 		}
2216 	}
2217 	return flags;
2218 };
2219 
2220 /**
2221  * Adds a change listener.
2222  * 
2223  * @param	{AjxListener}	the listener
2224  */
2225 ZmOrganizer.prototype.addChangeListener =
2226 function(listener) {
2227 	this.tree.addChangeListener(listener);
2228 };
2229 
2230 /**
2231  * Removes a change listener.
2232  * 
2233  * @param	{AjxListener}	the listener
2234  */
2235 ZmOrganizer.prototype.removeChangeListener =
2236 function(listener) {
2237 	this.tree.removeChangeListener(listener);
2238 };
2239 
2240 /**
2241  * @private
2242  */
2243 ZmOrganizer.prototype._setSharesFromJs =
2244 function(obj) {
2245 
2246 	// a folder object will have an acl with grants if this user has
2247 	// shared it, or if it has been shared to this user with admin rights
2248 	if (obj.acl && obj.acl.grant && obj.acl.grant.length > 0) {
2249 		AjxDispatcher.require("Share");
2250 		for (var i = 0; i < obj.acl.grant.length; i++) {
2251 			var grant = obj.acl.grant[i];
2252 			this.addShare(ZmShare.createFromJs(this, grant));
2253 		}
2254 	}
2255 };
2256 
2257 /**
2258  * Handle notifications through the tree.
2259  * 
2260  * @private
2261  */
2262 ZmOrganizer.prototype._notify =
2263 function(event, details) {
2264 
2265 	if (details) {
2266 		details.organizers = [this];
2267 	} else {
2268 		details = {organizers: [this]};
2269 	}
2270 	this.tree._evt.type = this.type;	// all folder types are in a single tree
2271 	this.tree._notify(event, details);
2272 };
2273 
2274 /**
2275  * Gets a marked-up version of the name.
2276  *
2277  * @param {String}	name			the name to mark up
2278  * @param {Boolean}	showUnread		if <code>true</code>, display the number of unread items (in parens)
2279  * @param {Boolean}	noMarkup		if <code>true</code>, do not return any HTML
2280  * 
2281  * @private
2282  */
2283 ZmOrganizer.prototype._markupName = 
2284 function(name, showUnread, noMarkup) {
2285 	if (!noMarkup) {
2286 		name = AjxStringUtil.htmlEncode(name, true);
2287 	}
2288 	if (showUnread && this.hasUnreadDescendent()) {
2289 		if (this.numUnread > 0) {
2290             name = AjxMessageFormat.format(ZmMsg.folderUnread, [name, this.numUnread]);
2291 		}
2292 		if (!noMarkup) {
2293 			name = ["<span style='font-weight:bold'>", name, "</span>"].join("");
2294 		}
2295 	}
2296 	if (this.noSuchFolder && !noMarkup) {
2297 		name = ["<del>", name, "</del>"].join("");
2298 	}
2299 	return name;
2300 };
2301 
2302 /**
2303  * @private
2304  */
2305 ZmOrganizer.prototype._getItemsText =
2306 function() {
2307 	var result = ZmMsg[ZmOrganizer.ITEMS_KEY[this.type]];
2308 	if (!result || this.isTrash()) {
2309 		result = ZmMsg.items;
2310 	}
2311 	return result;
2312 };
2313 
2314 ZmOrganizer.prototype._getUnreadLabel = 
2315 function() {
2316 	return ZmMsg.unread;	
2317 };
2318 
2319 /**
2320  * Returns true if any descendent folders have unread messages.
2321  *
2322  * @returns {boolean}   true if any descendent folders have unread messages
2323  */
2324 ZmOrganizer.prototype.hasUnreadDescendent = function() {
2325 
2326 	if (this.numUnread > 0) {
2327 		return true;
2328 	}
2329 
2330 	var a = this.children.getArray(),
2331 		sz = this.children.size();
2332 
2333 	for (var i = 0; i < sz; i++) {
2334 		if (a[i].hasUnreadDescendent()) {
2335 			return true;
2336 		}
2337 	}
2338 
2339 	return false;
2340 };
2341