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 item.
 27  */
 28 
 29 /**
 30  * Creates an item.
 31  * @class
 32  * An item is a piece of data that may contain user content. Most items are taggable. Currently,
 33  * the following things are items: conversation, message, attachment, appointment, and contact.
 34  * <br/>
 35  * <br/>
 36  * An item typically appears in the context of a containing list. Its event handling
 37  * is generally handled by the list so we avoid having the same listeners on each item. If we
 38  * create a context where an item stands alone outside a list context, then the item will have
 39  * its own listeners and do its own notification handling.
 40  *
 41  * @author Conrad Damon
 42  * 
 43  * @param {constant}	type		type of object (conv, msg, etc)
 44  * @param {int}			id			the unique id
 45  * @param {ZmList}		list		a list that contains this item
 46  * @param {Boolean}		noCache		if <code>true</code>, do not cache this item
 47  * 
 48  * @extends		ZmModel
 49  */
 50 ZmItem = function(type, id, list, noCache) {
 51 
 52 	if (arguments.length == 0) { return; }
 53 	ZmModel.call(this, type);
 54 
 55 	this.type = type;
 56 	this.id = id;
 57 	this.list = list;
 58 	this._list = {};
 59 
 60     // number of views using this item
 61     this.refCount = 0;
 62 
 63 	this.tags = [];
 64 	this.tagHash = {};
 65 	this.folderId = 0;
 66 
 67 	// make sure the cached item knows which lists it is in, even if those other lists
 68 	// have separate instances of this item - propagate view IDs from currently cached item
 69 	var curItem = appCtxt.getById(id);
 70 	if (curItem) {
 71 		this._list = AjxUtil.hashCopy(curItem._list);
 72         if (!list) {
 73             // No list specified, preserve the previous list
 74             this.list = curItem.list;
 75         }
 76 	}
 77 	if (list) {
 78 		this._list[list.id] = true;
 79 	}
 80 	
 81 	if (id && !noCache) {
 82 		appCtxt.cacheSet(id, this);
 83 	}
 84 };
 85 
 86 ZmItem.prototype = new ZmModel;
 87 ZmItem.prototype.constructor = ZmItem;
 88 
 89 ZmItem.prototype.isZmItem = true;
 90 ZmItem.prototype.toString = function() { return "ZmItem"; };
 91 
 92 
 93 ZmItem.APP 				= {};	// App responsible for item
 94 ZmItem.MSG_KEY 			= {};	// Type names
 95 ZmItem.ICON 			= {};	// Representative icons
 96 ZmItem.RESULTS_LIST 	= {};	// Function for creating search results list
 97 
 98 // fields that can be part of a displayed item
 99 ZmItem.F_ACCOUNT		= ZmId.FLD_ACCOUNT;
100 ZmItem.F_ATTACHMENT		= ZmId.FLD_ATTACHMENT;
101 ZmItem.F_CAPACITY		= ZmId.FLD_CAPACITY;
102 ZmItem.F_COMPANY		= ZmId.FLD_COMPANY;
103 ZmItem.F_DATE			= ZmId.FLD_DATE;
104 ZmItem.F_DEPARTMENT		= ZmId.FLD_DEPARTMENT;
105 ZmItem.F_EMAIL			= ZmId.FLD_EMAIL;
106 ZmItem.F_EXPAND			= ZmId.FLD_EXPAND;
107 ZmItem.F_FILE_TYPE		= ZmId.FLD_FILE_TYPE;
108 ZmItem.F_FLAG			= ZmId.FLD_FLAG;
109 ZmItem.F_FOLDER			= ZmId.FLD_FOLDER;
110 ZmItem.F_FRAGMENT		= ZmId.FLD_FRAGMENT;
111 ZmItem.F_FROM			= ZmId.FLD_FROM;
112 ZmItem.F_HOME_PHONE		= ZmId.FLD_HOME_PHONE;
113 ZmItem.F_ID				= ZmId.FLD_ID;
114 ZmItem.F_INDEX			= ZmId.FLD_INDEX;
115 ZmItem.F_ITEM_ROW		= ZmId.FLD_ITEM_ROW;
116 ZmItem.F_ITEM_ROW_3PANE	= ZmId.FLD_ITEM_ROW_3PANE;
117 ZmItem.F_LOCATION		= ZmId.FLD_LOCATION;
118 ZmItem.F_NAME			= ZmId.FLD_NAME;
119 ZmItem.F_NOTES			= ZmId.FLD_NOTES;
120 ZmItem.F_PARTICIPANT	= ZmId.FLD_PARTICIPANT;
121 ZmItem.F_PCOMPLETE		= ZmId.FLD_PCOMPLETE;
122 ZmItem.F_PRIORITY		= ZmId.FLD_PRIORITY;
123 ZmItem.F_RECURRENCE		= ZmId.FLD_RECURRENCE;
124 ZmItem.F_SELECTION		= ZmId.FLD_SELECTION;
125 ZmItem.F_SELECTION_CELL	= ZmId.FLD_SELECTION_CELL;
126 ZmItem.F_SIZE			= ZmId.FLD_SIZE;
127 ZmItem.F_SORTED_BY		= ZmId.FLD_SORTED_BY;	// placeholder for 3-pane view
128 ZmItem.F_STATUS			= ZmId.FLD_STATUS;
129 ZmItem.F_READ			= ZmId.FLD_READ;
130 ZmItem.F_MUTE			= ZmId.FLD_MUTE;
131 ZmItem.F_SUBJECT		= ZmId.FLD_SUBJECT;
132 ZmItem.F_TAG			= ZmId.FLD_TAG;
133 ZmItem.F_TAG_CELL		= ZmId.FLD_TAG_CELL;
134 ZmItem.F_TO             = ZmId.FLD_TO;
135 ZmItem.F_TYPE			= ZmId.FLD_TYPE;
136 ZmItem.F_VERSION        = ZmId.FLD_VERSION;
137 ZmItem.F_WORK_PHONE		= ZmId.FLD_WORK_PHONE;
138 ZmItem.F_LOCK           = ZmId.FLD_LOCK;
139 ZmItem.F_MSG_PRIORITY   = ZmId.FLD_MSG_PRIORITY;
140 ZmItem.F_APP_PASSCODE_CREATED = ZmId.FLD_CREATED;
141 ZmItem.F_APP_PASSCODE_LAST_USED = ZmId.FLD_LAST_USED;
142 
143 // Action requests for different items
144 ZmItem.SOAP_CMD = {};
145 
146 // Item fields (for modify events)
147 ZmItem.TAGS_FIELD = 1;
148 
149 // Item flags
150 ZmItem.FLAG_ATTACH				= "a";
151 ZmItem.FLAG_FLAGGED				= "f";
152 ZmItem.FLAG_FORWARDED			= "w";
153 ZmItem.FLAG_ISDRAFT 			= "d";
154 ZmItem.FLAG_ISSCHEDULED 		= "c";
155 ZmItem.FLAG_ISSENT				= "s";
156 ZmItem.FLAG_READ_RECEIPT_SENT	= "n";
157 ZmItem.FLAG_REPLIED				= "r";
158 ZmItem.FLAG_UNREAD				= "u";
159 ZmItem.FLAG_MUTE				= "(";
160 ZmItem.FLAG_LOW_PRIORITY		= "?";
161 ZmItem.FLAG_HIGH_PRIORITY		= "!";
162 ZmItem.FLAG_PRIORITY            = "+"; //msg prioritization
163 ZmItem.FLAG_NOTE                = "t"; //specially for notes
164 ZmItem.FLAG_OFFLINE_CREATED     = "o";
165 
166 ZmItem.ALL_FLAGS = [
167 	ZmItem.FLAG_FLAGGED,
168 	ZmItem.FLAG_ATTACH,
169 	ZmItem.FLAG_UNREAD,
170 	ZmItem.FLAG_MUTE,
171 	ZmItem.FLAG_REPLIED,
172 	ZmItem.FLAG_FORWARDED,
173 	ZmItem.FLAG_ISSENT,
174 	ZmItem.FLAG_READ_RECEIPT_SENT,
175 	ZmItem.FLAG_ISDRAFT,
176 	ZmItem.FLAG_ISSCHEDULED,
177 	ZmItem.FLAG_HIGH_PRIORITY,
178 	ZmItem.FLAG_LOW_PRIORITY,
179 	ZmItem.FLAG_PRIORITY,
180     ZmItem.FLAG_NOTE,
181     ZmItem.FLAG_OFFLINE_CREATED
182 ];
183 
184 // Map flag to item property
185 ZmItem.FLAG_PROP = {};
186 ZmItem.FLAG_PROP[ZmItem.FLAG_ATTACH]			= "hasAttach";
187 ZmItem.FLAG_PROP[ZmItem.FLAG_FLAGGED]			= "isFlagged";
188 ZmItem.FLAG_PROP[ZmItem.FLAG_FORWARDED]			= "isForwarded";
189 ZmItem.FLAG_PROP[ZmItem.FLAG_ISDRAFT] 			= "isDraft";
190 ZmItem.FLAG_PROP[ZmItem.FLAG_ISSCHEDULED] 		= "isScheduled";
191 ZmItem.FLAG_PROP[ZmItem.FLAG_ISSENT]			= "isSent";
192 ZmItem.FLAG_PROP[ZmItem.FLAG_READ_RECEIPT_SENT]	= "readReceiptSent";
193 ZmItem.FLAG_PROP[ZmItem.FLAG_REPLIED]			= "isReplied";
194 ZmItem.FLAG_PROP[ZmItem.FLAG_UNREAD]			= "isUnread";
195 ZmItem.FLAG_PROP[ZmItem.FLAG_MUTE]			    = "isMute";
196 ZmItem.FLAG_PROP[ZmItem.FLAG_LOW_PRIORITY]		= "isLowPriority";
197 ZmItem.FLAG_PROP[ZmItem.FLAG_HIGH_PRIORITY]		= "isHighPriority";
198 ZmItem.FLAG_PROP[ZmItem.FLAG_PRIORITY]          = "isPriority";
199 ZmItem.FLAG_PROP[ZmItem.FLAG_NOTE]              = "isNote";
200 ZmItem.FLAG_PROP[ZmItem.FLAG_OFFLINE_CREATED]   = "isOfflineCreated";
201 
202 // DnD actions this item is allowed
203 
204 /**
205  * Defines the "move" action.
206  * 
207  * @see		#getDefaultDndAction
208  */
209 ZmItem.DND_ACTION_MOVE = 1 << 0;
210 /**
211  * Defines the "copy" action.
212  * 
213  * @see		#getDefaultDndAction
214  */
215 ZmItem.DND_ACTION_COPY = 1 << 1;
216 /**
217  * Defines the "move & copy" action.
218  * 
219  * @see		#getDefaultDndAction
220  */
221 ZmItem.DND_ACTION_BOTH = ZmItem.DND_ACTION_MOVE | ZmItem.DND_ACTION_COPY;
222 
223 /**
224  * Defines the notes separator which is used by items
225  * (such as calendar or share invites) that have notes.
226  * 
227  */
228 ZmItem.NOTES_SEPARATOR			= "*~*~*~*~*~*~*~*~*~*";
229 
230 /**
231  * Registers an item and stores information about the given item type.
232  *
233  * @param {constant}	item		the item type
234  * @param	{Hash}	params			a hash of parameters
235  * @param {constant}	params.app			the app that handles this item type
236  * @param {String}		params.nameKey		the message key for item name
237  * @param {String}		params.icon			the name of item icon class
238  * @param {String}		params.soapCmd		the SOAP command for acting on this item
239  * @param {String}		params.itemClass	the name of class that represents this item
240  * @param {String}		params.node			the SOAP response node for this item
241  * @param {constant}	params.organizer	the associated organizer
242  * @param {String}		params.searchType	the associated type in SearchRequest
243  * @param {function}	params.resultsList	the function that returns a {@link ZmList} for holding search results of this type
244  */
245 ZmItem.registerItem =
246 function(item, params) {
247 	if (params.app)				{ ZmItem.APP[item]					= params.app; }
248 	if (params.nameKey)			{ ZmItem.MSG_KEY[item]				= params.nameKey; }
249 	if (params.icon)			{ ZmItem.ICON[item]					= params.icon; }
250 	if (params.soapCmd)			{ ZmItem.SOAP_CMD[item]				= params.soapCmd; }
251 	if (params.itemClass)		{ ZmList.ITEM_CLASS[item]			= params.itemClass; }
252 	if (params.node)			{ ZmList.NODE[item]					= params.node; }
253 	if (params.organizer)		{ ZmOrganizer.ITEM_ORGANIZER[item]	= params.organizer; }
254 	if (params.searchType)		{ ZmSearch.TYPE[item]				= params.searchType; }
255 	if (params.resultsList)		{ ZmItem.RESULTS_LIST[item]			= params.resultsList; }
256 
257 	if (params.node) {
258 		ZmList.ITEM_TYPE[params.node] = item;
259 	}
260 
261 	if (params.dropTargets) {
262 		if (!ZmApp.DROP_TARGETS[params.app]) {
263 			ZmApp.DROP_TARGETS[params.app] = {};
264 		}
265 		ZmApp.DROP_TARGETS[params.app][item] = params.dropTargets;
266 	}
267 };
268 
269 /**
270 * Gets an item id by taking a normalized id (or an item id) and returning the item id.
271 * 
272 * @param	{String}	id		the normalized id
273 * @return	{String}	the item id
274 */
275 ZmItem.getItemId =
276 function(id) {
277 	if (!id) {
278 		return id;
279 	}
280 	if (!ZmItem.SHORT_ID_RE) {
281 		var shell = DwtShell.getShell(window);
282 		ZmItem.SHORT_ID_RE = new RegExp(appCtxt.get(ZmSetting.USERID) + ':', "gi");
283 	}
284 	return id.replace(ZmItem.SHORT_ID_RE, '');
285 };
286 
287 // abstract methods
288 /**
289  * Creates an item.
290  * 
291  * @param	{Hash}	args		the arguments
292  */
293 ZmItem.prototype.create = function(args) {};
294 /**
295  * Modifies an item.
296  * 
297  * @param	{Hash}	mods		the arguments
298  */
299 ZmItem.prototype.modify = function(mods) {};
300 
301 /**
302  * Gets the item by id.
303  *
304  * @param {String}	id		an item id
305  * @return	{ZmItem}	the item
306  */
307 ZmItem.prototype.getById =
308 function(id) {
309 	if (id == this.id) {
310 		return this;
311 	}
312 };
313 
314 ZmItem.prototype.getAccount =
315 function() {
316 	if (!this.account) {
317 		var account;
318 
319 		if (this.folderId) {
320 			var ac = window.parentAppCtxt || window.appCtxt;
321 			var folder = ac.getById(this.folderId);
322 			account = folder && folder.getAccount();
323 		}
324 
325 		if (!account) {
326 			var parsed = ZmOrganizer.parseId(this.id);
327 			account = parsed && parsed.account;
328 		}
329 		this.account = account;
330 	}
331 	return this.account;
332 };
333 
334 /**
335  * Clears the item.
336  * 
337  */
338 ZmItem.prototype.clear = function() {
339 
340     // only clear data if no views are using this item
341     if (this.refCount <= 1) {
342         this._evtMgr.removeAll(ZmEvent.L_MODIFY);
343         if (this.tags.length) {
344             for (var i = 0; i < this.tags.length; i++) {
345                 this.tags[i] = null;
346             }
347             this.tags = [];
348         }
349         for (var i in this.tagHash) {
350             this.tagHash[i] = null;
351         }
352         this.tagHash = {};
353     }
354 
355     this.refCount--;
356 };
357 
358 /**
359  * Caches the item.
360  * 
361  * @return	{Boolean}	<code>true</code> if the item is placed into cache; <code>false</code> otherwise
362  */
363 ZmItem.prototype.cache =
364 function(){
365   if (this.id) {
366       appCtxt.cacheSet(this.id, this);
367       return true;
368   }
369   return false;  
370 };
371 
372 /**
373  * Checks if the item has a given tag.
374  * 
375  * @param {String}		tagName		tag name
376  * @return	{Boolean}	<code>true</code> is this item has the given tag.
377  */
378 ZmItem.prototype.hasTag =
379 function(tagName) {
380 	return (this.tagHash[tagName] == true);
381 };
382 
383 /**
384  * is it possible to add a tag to this item?
385  * @param tagName
386  * @returns {boolean}
387  */
388 ZmItem.prototype.canAddTag =
389 function(tagName) {
390 	return !this.hasTag(tagName);
391 };
392 
393 
394 /**
395 * Gets the folder id that contains this item, if available.
396 * 
397 * @return	{String}	the folder id or <code>null</code> for none
398 */
399 ZmItem.prototype.getFolderId =
400 function() {
401 	return this.folderId;
402 };
403 
404 /**
405  * @deprecated
406  * Use getRestUrl
407  * 
408  * @private
409  * @see		#getRestUrl
410  */
411 ZmItem.prototype.getUrl =
412 function() {
413 	return this.getRestUrl();
414 };
415 
416 /**
417  * Gets the rest url for this item.
418  * 
419  * @return	{String}	the url
420  */
421 ZmItem.prototype.getRestUrl =
422 function() {
423 	// return REST URL as seen by server
424 	if (this.restUrl) {
425 		return this.restUrl;
426 	}
427 
428 	// if server doesn't tell us what URL to use, do our best to generate
429 	var organizerType = ZmOrganizer.ITEM_ORGANIZER[this.type];
430 	var organizer = appCtxt.getById(this.folderId);
431 	var url = organizer
432 		? ([organizer.getRestUrl(), "/", AjxStringUtil.urlComponentEncode(this.name)].join(""))
433 		: null;
434 
435 	DBG.println(AjxDebug.DBG3, "NO REST URL FROM SERVER. GENERATED URL: " + url);
436 
437 	return url;
438 };
439 
440 /**
441 * Gets the appropriate tag image info for this item.
442 * 
443 * @return	{String}	the tag image info
444 */
445 ZmItem.prototype.getTagImageInfo =
446 function() {
447 	return this.getTagImageFromNames(this.getVisibleTags());
448 };
449 
450 /**
451  * @deprecated
452  * */
453 ZmItem.prototype.getTagImageFromIds =
454 function(tagIds) {
455 	var tagImageInfo;
456 
457 	if (!tagIds || tagIds.length == 0) {
458 		tagImageInfo = "Blank_16";
459 	} else if (tagIds.length == 1) {
460         tagImageInfo = this.getTagImage(tagIds[0]);
461 	} else {
462 		tagImageInfo = "TagStack";
463 	}
464 
465 	return tagImageInfo;
466 };
467 
468 ZmItem.prototype.getVisibleTags =
469 function() {
470     if(!appCtxt.get(ZmSetting.TAGGING_ENABLED)){
471         return [];
472     }
473     return this.tags;
474 	//todo - do we need anything from this?
475 //    var searchAll = appCtxt.getSearchController().searchAllAccounts;
476 //    if (!searchAll && this.isShared()) {
477 //        return [];
478 //    } else {
479 //        return this.tags;
480 //    }
481 };
482 
483 ZmItem.prototype.getTagImageFromNames =
484 function(tags) {
485 
486 	if (!tags || tags.length == 0) {
487 		return "Blank_16";
488 	}
489 	if (tags.length == 1) {
490         return this.getTagImage(tags[0]);
491 	} 
492 
493 	return "TagStack";
494 };
495 
496 
497 ZmItem.prototype.getTagImage =
498 function(tagName) {
499 	//todo - I don't think we need the qualified/normalized/whatever id anymore.
500 //	var tagFullId = (!this.getAccount().isMain)
501 //		? ([this.getAccount().id, tagName].join(":"))
502 //		: (ZmOrganizer.getSystemId(tagName));
503 	var tagList = appCtxt.getAccountTagList(this);
504 
505 	var tag = tagList.getByNameOrRemote(tagName);
506     return tag ? tag.getIconWithColor() : "Blank_16";
507 };
508 
509 /**
510 * Gets the default action to use when dragging this item. This method
511 * is meant to be overloaded for items that are read-only and can only be copied.
512 *
513 * @param {Boolean}		forceCopy		If set, default DnD action is a copy
514 * @return	{Object}	the action
515 */
516 ZmItem.prototype.getDefaultDndAction =
517 function(forceCopy) {
518 	return (this.isReadOnly() || forceCopy)
519 		? ZmItem.DND_ACTION_COPY
520 		: ZmItem.DND_ACTION_MOVE;
521 };
522 
523 /**
524 * Checks if this item is read-only. This method should be
525 * overloaded by the derived object to determine what "read-only" means.
526 * 
527 * @return	{Boolean}	the read-only status
528 */
529 ZmItem.prototype.isReadOnly =
530 function() {
531 	return false;
532 };
533 
534 /**
535  * Checks if this item is shared.
536  * 
537  * @return	{Boolean}	<code>true</code> if this item is shared (remote)
538  */
539 ZmItem.prototype.isShared =
540 function() {
541 	if (this._isShared == null) {
542 		if (this.id === -1) {
543 			this._isShared = false;
544 		} else {
545 			this._isShared = appCtxt.isRemoteId(this.id);
546 		}
547 	}
548 	return this._isShared;
549 };
550 
551 // Notification handling
552 
553 // For delete and modify notifications, we first apply the notification to this item. Then we
554 // see if the item is a member of any other lists. If so, we have those other copies of this
555 // item handle the notification as well. Each will notify through the list that created it.
556 
557 ZmItem.prototype.notifyDelete =
558 function() {
559 	this._notifyDelete();
560 	for (var listId in this._list) {
561 		var list = appCtxt.getById(listId);
562 		if (!list || (this.list && listId == this.list.id)) { continue; }
563 		var ctlr = list.controller;
564 		if (!ctlr || ctlr.inactive || (ctlr.getList().id != listId)) { continue; }
565 		var doppleganger = list.getById(this.id);
566 		if (doppleganger) {
567 			doppleganger._notifyDelete();
568 		}
569 	}
570 };
571 
572 ZmItem.prototype._notifyDelete =
573 function() {
574 	this.deleteLocal();
575 	if (this.list) {
576 		this.list.deleteLocal([this]);
577 	}
578 	this._notify(ZmEvent.E_DELETE);
579 };
580 
581 ZmItem.prototype.notifyModify =
582 function(obj, batchMode) {
583 	this._notifyModify(obj, batchMode);
584 	for (var listId in this._list) {
585 		var list = listId ? appCtxt.getById(listId) : null;
586 		if (!list || (this.list && (listId == this.list.id))) { continue; }
587 		var ctlr = list.controller;
588 		if (!ctlr || ctlr.inactive || (ctlr.getList().id != listId)) { continue; }
589 		var doppleganger = list.getById(this.id);
590 		if (doppleganger) {
591 			doppleganger._notifyModify(obj, batchMode);
592 		}
593 	}
594 };
595 
596 /**
597  * Handles a modification notification.
598  *
599  * @param {Object}	obj			the item with the changed attributes/content
600  * @param {boolean}	batchMode	if true, return event type and don't notify
601  */
602 ZmItem.prototype._notifyModify =
603 function(obj, batchMode) {
604 	// empty string is meaningful here, it means no tags
605 	if (obj.tn != null) {
606 		this._parseTagNames(obj.tn);
607 		this._notify(ZmEvent.E_TAGS);
608 	}
609 	// empty string is meaningful here, it means no flags
610 	if (obj.f != null) {
611 		var flags = this._getFlags();
612 		var origFlags = {};
613 		for (var i = 0; i < flags.length; i++) {
614 			origFlags[flags[i]] = this[ZmItem.FLAG_PROP[flags[i]]];
615 		}
616 		this._parseFlags(obj.f);
617 		var changedFlags = [];
618 		for (var i = 0; i < flags.length; i++) {
619 			var on = this[ZmItem.FLAG_PROP[flags[i]]];
620 			if (origFlags[flags[i]] != on) {
621 				changedFlags.push(flags[i]);
622 			}
623 		}
624 		if (changedFlags.length) {
625 			this._notify(ZmEvent.E_FLAGS, {flags: changedFlags});
626 		}
627 	}
628 	if (obj.l != null && obj.l != this.folderId) {
629 		var details = {oldFolderId:this.folderId};
630 		this.moveLocal(obj.l);
631 		if (this.list) {
632 			this.list.moveLocal([this], obj.l);
633 		}
634 		if (batchMode) {
635 			delete obj.l;			// folder has been handled
636 			return ZmEvent.E_MOVE;
637 		} else {
638 			this._notify(ZmEvent.E_MOVE, details);
639 		}
640 	}
641 };
642 
643 // Local change handling
644 
645 /**
646  * Applies the given flag change to this item by setting a boolean property.
647  *
648  * @param {constant}	flag	the flag that changed
649  * @param {Boolean}	on		<code>true</code> if the flag is now set
650  */
651 ZmItem.prototype.flagLocal =
652 function(flag, on) {
653 	this[ZmItem.FLAG_PROP[flag]] = on;
654 };
655 
656 /**
657  * Sets the given flag change to this item. Both the flags string and the
658  * flag properties are affected.
659  *
660  * @param {constant}	flag	the flag that changed
661  * @param {Boolean}	on	<code>true</code> if the flag is now set
662  *
663  * @return	{String}		the new flags string
664  */
665 ZmItem.prototype.setFlag =
666 function(flag, on) {
667 	this.flagLocal(flag, on);
668 	var flags = this.flags || "";
669 	if (on && flags.indexOf(flag) == -1) {
670 		flags = flags + flag;
671 	} else if (!on && flags.indexOf(flag) != -1) {
672 		flags = flags.replace(flag, "");
673 	}
674 	this.flags = flags;
675 
676 	return flags;
677 };
678 
679 /**
680  * Adds or removes the given tag for this item.
681  *
682  * @param {Object}		tag		tag name
683  * @param {Boolean}		doTag		<code>true</code> if tag is being added; <code>false</code> if it is being removed
684  * @return	{Boolean}	<code>true</code> to notify
685  */
686 ZmItem.prototype.tagLocal =
687 function(tag, doTag) {
688 	var bNotify = false;
689 	if (doTag) {
690 		if (!this.tagHash[tag]) {
691 			bNotify = true;
692 			this.tags.push(tag);
693 			this.tagHash[tag] = true;
694 		}
695 	} else {
696 		for (var i = 0; i < this.tags.length; i++) {
697 			if (this.tags[i] == tag) {
698 				this.tags.splice(i, 1);
699 				delete this.tagHash[tag];
700 				bNotify = true;
701 				break;
702 			}
703 		}
704 	}
705 	
706 	return bNotify;
707 };
708 
709 /**
710  * Removes all tags.
711  * 
712  */
713 ZmItem.prototype.removeAllTagsLocal =
714 function() {
715 	this.tags = [];
716 	for (var i in this.tagHash) {
717 		delete this.tagHash[i];
718 	}
719 };
720 
721 /**
722  * Deletes local, in case an item wants to do something while being deleted.
723  */
724 ZmItem.prototype.deleteLocal = function() {};
725 
726 /**
727  * Moves the item.
728  * 
729  * @param	{String}	folderId
730  * @param	{AjxCallback}	callback		the callback
731  * @param	{AjxCallback}	errorCallback	the callback on error
732  * @return	{Object}		the result of the move
733  */
734 ZmItem.prototype.move =
735 function(folderId, callback, errorCallback) {
736 	return ZmItem.move(this.id, folderId, callback, errorCallback);
737 };
738 
739 /**
740  * Moves the item.
741  * 
742  * @return	{Object}		the result of the move
743  */
744 ZmItem.move =
745 function(itemId, folderId, callback, errorCallback, accountName) {
746 	var json = {
747 		ItemActionRequest: {
748 			_jsns: "urn:zimbraMail",
749 			action: {
750 				id:	itemId instanceof Array ? itemId.join() : itemId,
751 				op:	"move",
752 				l:	folderId
753 			}
754 		}
755 	};
756 
757 	var params = {
758 		jsonObj:		json,
759 		asyncMode:		Boolean(callback),
760 		callback:		callback,
761 		errorCallback:	errorCallback,
762 		accountName:	accountName
763 	};
764 	return appCtxt.getAppController().sendRequest(params);
765 };
766 
767 /**
768  * Updates the folder for this item.
769  *
770  * @param {String}		folderId		the new folder ID
771  */
772 ZmItem.prototype.moveLocal =
773 function(folderId) {
774 	this.folderId = folderId;
775 };
776 
777 /**
778  * Takes a comma-separated list of tag IDs and applies the tags to this item.
779  * 
780  * @private
781  */
782 ZmItem.prototype._parseTags =
783 function(str) {	
784 	this.tags = [];
785 	this.tagHash = {};
786 	if (str && str.length) {
787 		var tags = str.split(",");
788 		for (var i = 0; i < tags.length; i++) {
789 			var tagId = Number(tags[i]);
790 			if (tagId >= ZmOrganizer.FIRST_USER_ID[ZmOrganizer.TAG])
791 				this.tagLocal(tagId, true);
792 		}
793 	}
794 };
795 
796 /**
797  * Takes a comma-separated list of tag names and applies the tags to this item.
798  *
799  * @private
800  */
801 ZmItem.prototype._parseTagNames =
802 function(str) {
803 	this.tags = [];
804 	this.tagHash = {};
805 	if (!str || !str.length) {
806 		return;
807 	}
808 	
809 	// server escapes comma with backslash
810 	str = str.replace(/\\,/g, "\u001D");
811 	var tags = str.split(",");
812 	
813 	for (var i = 0; i < tags.length; i++) {
814 		var tagName = tags[i].replace("\u001D", ",");
815 		this.tagLocal(tagName, true);
816 	}
817 };
818 
819 /**
820  * Takes a string of flag chars and applies them to this item.
821  * 
822  * @private
823  */
824 ZmItem.prototype._parseFlags =
825 function(str) {
826 	this.flags = str;
827 	for (var i = 0; i < ZmItem.ALL_FLAGS.length; i++) {
828 		var flag = ZmItem.ALL_FLAGS[i];
829 		var on = (str && (str.indexOf(flag) != -1)) ? true : false;
830 		this.flagLocal(flag, on);
831 	}
832 };
833 
834 // Listener notification
835 
836 /**
837  * Notify the list as well as this item.
838  * 
839  * @private
840  */
841 ZmItem.prototype._notify =
842 function(event, details) {
843 	this._doNotify(event, details);
844 };
845 
846 ZmItem.prototype._setupNotify =
847 function() {
848     this._doNotify();
849 }
850 
851 ZmItem.prototype._doNotify =
852 function(event, details) {
853 	if (this._evt) {
854 		this._evt.item = this;
855 		if (event != null) {
856 			ZmModel.prototype._notify.call(this, event, details);
857 		}
858 	} else {
859 		var idText = "";
860 		if (this.type && this.id) {
861 			idText = ": item = " + this.type + "(" + this.id + ")";
862 		}
863 		DBG.println(AjxDebug.DBG1, "ZmItem._doNotify, missing _evt" + idText);
864 	}
865     if (this.list) {
866         this.list._evt.item = this;
867         this.list._evt.items = [this];
868         if (event != null) {
869             if (details) {
870                 details.items = [this];
871             } else {
872                 details = {items: [this]};
873             }
874             this.list._notify(event, details);
875         }
876     }
877 };
878 
879 /**
880  * Returns a list of flags that apply to this type of item.
881  * 
882  * @private
883  */
884 ZmItem.prototype._getFlags =
885 function() {
886 	return [ZmItem.FLAG_FLAGGED, ZmItem.FLAG_ATTACH];
887 };
888 
889 /**
890  * Rename the item.
891  *
892  * @param	{String}	newName
893  * @param	{AjxCallback}	callback		the callback
894  * @param	{AjxCallback}	errorCallback	the callback on error
895  * @return	{Object}		the result of the move
896  */
897 ZmItem.prototype.rename =
898 function(newName, callback, errorCallback) {
899 	return ZmItem.rename(this.id, newName, callback, errorCallback);
900 };
901 
902 /**
903  * Rename the item.
904  *
905  * @return	{Object}		the result of the move
906  */
907 ZmItem.rename =
908 function(itemId, newName, callback, errorCallback, accountName) {
909     var json = {
910 		ItemActionRequest: {
911 			_jsns: "urn:zimbraMail",
912 			action: {
913 				id:	itemId instanceof Array ? itemId[0] : itemId,
914 				op:	"rename",
915 				name:	newName
916 			}
917 		}
918 	};	
919 
920 	var params = {
921 		jsonObj:		json,
922 		asyncMode:		Boolean(callback),
923 		callback:		callback,
924 		errorCallback:	errorCallback,
925 		accountName:	accountName
926 	};
927 	return appCtxt.getAppController().sendRequest(params);
928 };
929 
930 ZmItem.prototype.getSortedTags =
931 function() {
932 	var numTags = this.tags && this.tags.length;
933 	if (numTags) {
934 		var tagList = appCtxt.getAccountTagList(this);
935 		var ta = [];
936 		for (var i = 0; i < numTags; i++) {
937 			var tag = tagList.getByNameOrRemote(this.tags[i]);
938 			//tag could be missing if this was called when deleting a whole tag (not just untagging one message). So this makes sure we don't have a null item.
939 			if (!tag) {
940 				continue;
941 			}
942 			ta.push(tag);
943 		}
944 		ta.sort(ZmTag.sortCompare);
945 		return ta;
946 	}
947 	return null;
948 };
949 
950