1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 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) 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 a Zimbra calendar item.
 27  *
 28  */
 29 
 30 /**
 31  * @class
 32  * This class represents a calendar item.
 33  *
 34  * @param	{constant}	type		the item type
 35  * @param	{ZmList}	list		the list
 36  * @param	{int}	id				the task id
 37  * @param	{String}	folderId	the folder id
 38  *
 39  * @extends ZmCalBaseItem
 40  */
 41 ZmCalItem = function(type, list, id, folderId) {
 42 	if (arguments.length == 0) { return; }
 43 
 44 	ZmCalBaseItem.call(this, type, list, id, folderId);
 45 
 46 	this.notesTopPart = null; // ZmMimePart containing children w/ message parts
 47 	this.attachments = null;
 48 	this.viewMode = ZmCalItem.MODE_NEW;
 49 	this._recurrence = new ZmRecurrence(this);
 50 	this._noBusyOverlay = null;
 51     this._sendNotificationMail = true;
 52     this.identity = null;
 53     this.isProposeTimeMode = false;
 54     this.isForwardMode = false;
 55 	this.alarmActions = new AjxVector();
 56 	this.alarmActions.add(ZmCalItem.ALARM_DISPLAY);
 57 	this._useAbsoluteReminder = false;
 58     this._ignoreVersion=false; //to ignore revision related attributes(ms & rev) during version conflict
 59 };
 60 
 61 ZmCalItem.prototype = new ZmCalBaseItem;
 62 ZmCalItem.prototype.constructor = ZmCalItem;
 63 
 64 /**
 65  * Returns a string representation of the object.
 66  * 
 67  * @return		{String}		a string representation of the object
 68  */
 69 ZmCalItem.prototype.toString =
 70 function() {
 71 	return "ZmCalItem";
 72 };
 73 
 74 // Consts
 75 
 76 /**
 77  * Defines the "new" mode.
 78  */
 79 ZmCalItem.MODE_NEW					    = "NEW"; // Changing those constants from numbers to strings to be easier for debugging. I could not deal with 2,3 etc anymore.
 80 /**
 81  * Defines the "edit" mode.
 82  */
 83 ZmCalItem.MODE_EDIT					    = "EDIT";
 84 
 85 /**
 86  * Defines the "copy single instance" mode.
 87  */
 88 ZmCalItem.MODE_COPY_SINGLE_INSTANCE	    = "COPY_INST";
 89 
 90 /**
 91  * Defines the "edit single instance" mode.
 92  */
 93 ZmCalItem.MODE_EDIT_SINGLE_INSTANCE	    = "EDIT_INST";
 94 /**
 95  * Defines the "edit series" mode.
 96  */
 97 ZmCalItem.MODE_EDIT_SERIES			    = "EDIT_SER";
 98 /**
 99  * Defines the "delete" mode.
100  */
101 ZmCalItem.MODE_DELETE				    = "DELETE";
102 /**
103  * Defines the "delete instance" mode.
104  */
105 ZmCalItem.MODE_DELETE_INSTANCE		    = "DELETE_INST";
106 /**
107  * Defines the "delete series" mode.
108  */
109 ZmCalItem.MODE_DELETE_SERIES		    = "DELETE_SER";
110 /**
111  * Defines the "new from quick" mode.
112  */
113 ZmCalItem.MODE_NEW_FROM_QUICKADD 	    = "NEW_FROM_QUICK";
114 /**
115  * Defines the "get" mode.
116  */
117 ZmCalItem.MODE_GET					    = "GET";
118 /**
119  * Defines the "forward" mode.
120  */
121 ZmCalItem.MODE_FORWARD				    = "FORWARD";
122 /**
123  * Defines the "forward single instance" mode.
124  */
125 ZmCalItem.MODE_FORWARD_SINGLE_INSTANCE	= "FORWARD_INST";
126 /**
127  * Defines the "forward series" mode.
128  */
129 ZmCalItem.MODE_FORWARD_SERIES			= "FORWARD_SER";
130 /**
131  * Defines the "forward" mode.
132  */
133 ZmCalItem.MODE_FORWARD_INVITE			= "FORWARD_INV";
134 /**
135  * Defines the "propose" mode.
136  */
137 ZmCalItem.MODE_PROPOSE_TIME 			= "PROPOSE_TIME";
138 
139 /**
140  * Defines the "purge" (delete from trash) mode.
141  */
142 ZmCalItem.MODE_PURGE 					= 15; //keeping this and the last one as 15 as I am not sure if it's a bug or intentional that they are the same
143 
144 /**
145  * Defines the "last" mode index constant.
146  */
147 ZmCalItem.MODE_LAST					    = 15;
148 
149 ZmCalItem.FORWARD_MAPPING = {};
150 ZmCalItem.FORWARD_MAPPING[ZmCalItem.MODE_FORWARD]                   = ZmCalItem.MODE_EDIT;
151 ZmCalItem.FORWARD_MAPPING[ZmCalItem.MODE_FORWARD_SINGLE_INSTANCE]   = ZmCalItem.MODE_EDIT_SINGLE_INSTANCE;
152 ZmCalItem.FORWARD_MAPPING[ZmCalItem.MODE_FORWARD_SERIES]            = ZmCalItem.MODE_EDIT_SERIES;
153 ZmCalItem.FORWARD_MAPPING[ZmCalItem.MODE_FORWARD_INVITE]            = ZmCalItem.MODE_EDIT;
154 
155 /**
156  * Defines the "low" priority.
157  */
158 ZmCalItem.PRIORITY_LOW				= 9;
159 ZmCalItem.PRIORITY_LOW_RANGE		= [6,7,8,9];
160 
161 /**
162  * Defines the "normal" priority.
163  */
164 ZmCalItem.PRIORITY_NORMAL			= 5;
165 ZmCalItem.PRIORITY_NORMAL_RANGE		= [0,5];
166 /**
167  * Defines the "high" priority.
168  */
169 ZmCalItem.PRIORITY_HIGH				= 1;
170 ZmCalItem.PRIORITY_HIGH_RANGE		= [1,2,3,4];
171 
172 /**
173  * Defines the "chair" role.
174  */
175 ZmCalItem.ROLE_CHAIR				= "CHA";
176 /**
177  * Defines the "required" role.
178  */
179 ZmCalItem.ROLE_REQUIRED				= "REQ";
180 /**
181  * Defines the "optional" role.
182  */
183 ZmCalItem.ROLE_OPTIONAL				= "OPT";
184 /**
185  * Defines the "non-participant" role.
186  */
187 ZmCalItem.ROLE_NON_PARTICIPANT		= "NON";
188 
189 ZmCalItem.SERVER_WEEK_DAYS			= ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
190 
191 ZmCalItem.ATTACHMENT_CHECKBOX_NAME	= "__calAttCbox__";
192 ZmCalItem.ATT_LINK_IMAGE            = "mainImage";
193 ZmCalItem.ATT_LINK_MAIN			    = "main";
194 ZmCalItem.ATT_LINK_DOWNLOAD		    = "download";
195 
196 /**
197  * Defines "minutes "reminder units.
198  */
199 ZmCalItem.REMINDER_UNIT_MINUTES     = "minutes";
200 /**
201  * Defines "hours" reminder units.
202  */
203 ZmCalItem.REMINDER_UNIT_HOURS       = "hours";
204 /**
205  * Defines "days" reminder units.
206  */
207 ZmCalItem.REMINDER_UNIT_DAYS        = "days";
208 /**
209  * Defines "weeks" reminder units.
210  */
211 ZmCalItem.REMINDER_UNIT_WEEKS       = "weeks";
212 /**
213  * Defines "none" reminder.
214  */
215 ZmCalItem.REMINDER_NONE             = "none";
216 
217 // Alarm actions
218 ZmCalItem.ALARM_DISPLAY	= "DISPLAY";
219 ZmCalItem.ALARM_EMAIL	= "EMAIL";
220 ZmCalItem.ALARM_DEVICE_EMAIL = "DEVICE_EMAIL"; // SMS
221 
222 // Duration Checks
223 ZmCalItem.MSEC_LIMIT_PER_WEEK  = AjxDateUtil.MSEC_PER_DAY * 7;
224 // Because recurrences can be on the first (or 2nd, 3rd...) Day-of-week of a
225 // month, play it safe and make the limit 5 weeks
226 ZmCalItem.MSEC_LIMIT_PER_MONTH = AjxDateUtil.MSEC_PER_DAY * 7 * 5;
227 ZmCalItem.MSEC_LIMIT_PER_YEAR  = AjxDateUtil.MSEC_PER_DAY * 366;
228 
229 
230 // Getters
231 
232 /**
233  * @private
234  */
235 ZmCalItem.prototype.getCompNum			= function() { return this.compNum || "0"; };
236 
237 /**
238  * Gets the folder.
239  * 
240  * @return	{Object}	the folder
241  */
242 ZmCalItem.prototype.getFolder			= function() { };						// override if necessary
243 
244 /**
245  * Gets the organizer.
246  * 
247  * @return	{String}	the organizer
248  */
249 ZmCalItem.prototype.getOrganizer 		= function() { return this.organizer || ""; };
250 
251 /**
252  * Gets the organizer name.
253  *
254  * @return	{String}	the organizer name
255  */
256 ZmCalItem.prototype.getOrganizerName 	= function() { return this.organizerName; };
257 
258 
259 /**
260  * Gets the sent by.
261  * 
262  * @return	{String}	the sent by
263  */
264 ZmCalItem.prototype.getSentBy           = function() { return this.sentBy || ""; };
265 
266 /**
267  * Gets the original start date.
268  * 
269  * @return	{Date}	the original start date
270  */
271 ZmCalItem.prototype.getOrigStartDate 	= function() { return this._origStartDate || this.startDate; };
272 
273 /**
274  * Gets the original start time.
275  * 
276  * @return	{Date}	the original start time
277  */
278 ZmCalItem.prototype.getOrigStartTime 	= function() { return this.getOrigStartDate().getTime(); };
279 
280 /**
281  * Gets the original end date.
282  *
283  * @return	{Date}	the original end date
284  */
285 ZmCalItem.prototype.getOrigEndDate 	= function() { return this._origEndDate || this.endDate; };
286 
287 /**
288  * Gets the original end time.
289  *
290  * @return	{Date}	the original end time
291  */
292 ZmCalItem.prototype.getOrigEndTime 	= function() { return this.getOrigEndDate().getTime(); };
293 
294 /**
295  * Gets the original calendar item.
296  *
297  * @return	{ZmCalItem}	the original calendar item
298  */
299 ZmCalItem.prototype.getOrig 	        = function() { return this._orig; };
300 
301 /**
302  * Gets the original timezone.
303  * 
304  * @return	{Date}	the original timezone
305  */
306 ZmCalItem.prototype.getOrigTimezone     = function() { return this._origTimezone || this.timezone; };
307 
308 /**
309  * Gets the recurrence "blurb".
310  * 
311  * @return	{String}	the recurrence blurb
312  * @see		ZmRecurrence
313  */
314 ZmCalItem.prototype.getRecurBlurb		= function() { return this._recurrence.getBlurb(); };
315 
316 /**
317  * Gets the recurrence.
318  * 
319  * @return	{ZmRecurrence}	the recurrence
320  */
321 ZmCalItem.prototype.getRecurrence		= function() { return this._recurrence; };
322 
323 /**
324  * Gets the recurrence "type".
325  * 
326  * @return	{String}	the recurrence type
327  * @see		ZmRecurrence
328  */
329 ZmCalItem.prototype.getRecurType		= function() { return this._recurrence.repeatType; };
330 
331 /**
332  * Gets the timezone.
333  * 
334  * @return	{AjxTimezone}	the timezone
335  */
336 ZmCalItem.prototype.getTimezone         = function() { return this.timezone; };
337 
338 /**
339  * Gets the summary.
340  * 
341  * @param	{Boolean}	isHtml		<code>true</code> to return as html
342  * @return	{String}	the summary
343  */
344 ZmCalItem.prototype.getSummary			= function(isHtml) { };					// override if necessary
345 
346 /**
347  * Gets the tool tip.
348  * 
349  * @param	{ZmController}		controller		the controller
350  * @return	{String}	the tool tip
351  */
352 ZmCalItem.prototype.getToolTip			= function(controller) { };				// override if necessary
353 /**
354  * Checks if this item has a custom recurrence.
355  * 
356  * @return	{Boolean}	<code>true</code> for a custom recurrence
357  */
358 ZmCalItem.prototype.isCustomRecurrence 	= function() { return this._recurrence.repeatCustom == "1" || this._recurrence.repeatEndType != "N"; };
359 /**
360  * Checks if this item is an organizer.
361  * 
362  * @return	{Boolean}	<code>true</code> for an organizer
363  */
364 ZmCalItem.prototype.isOrganizer 		= function() { return (typeof(this.isOrg) === 'undefined') || (this.isOrg == true); };
365 /**
366  * Checks if this item is recurring.
367  * 
368  * @return	{Boolean}	<code>true</code> for recurrence
369  */
370 ZmCalItem.prototype.isRecurring 		= function() { return (this.recurring || (this._rawRecurrences != null)); };
371 /**
372  * Checks if this item has attachments.
373  * 
374  * @return	{Boolean}	<code>true</code> if this item has attachments
375  */
376 ZmCalItem.prototype.hasAttachments 		= function() { return this.getAttachments() != null; };
377 /**
378  * Checks if this item has attendee type.
379  * 
380  * @return	{Boolean}	always returns <code>false</code>; override if necessary
381  */
382 ZmCalItem.prototype.hasAttendeeForType	= function(type) { return false; };		// override if necessary
383 /**
384  * Checks if this item has attendees.
385  * 
386  * @return	{Boolean}	always returns <code>false</code>; override if necessary
387  */
388 ZmCalItem.prototype.hasAttendees    	= function() { return false; }; 		// override if necessary
389 /**
390  * Checks if this item has person attendees.
391  * 
392  * @return	{Boolean}	always returns <code>false</code>; override if necessary
393  */
394 ZmCalItem.prototype.hasPersonAttendees	= function() { return false; };			// override if necessary
395 
396 // Setters
397 /**
398  * Sets all day event.
399  * 
400  * @param	{Boolean}	isAllDay	<code>true</code> for an all day event
401  */
402 ZmCalItem.prototype.setAllDayEvent 		= function(isAllDay) 	{ this.allDayEvent = isAllDay ? "1" : "0"; };
403 /**
404  * Sets the name.
405  * 
406  * @param	{String}	newName			the name
407  */
408 ZmCalItem.prototype.setName 			= function(newName) 	{ this.name = newName; };
409 /**
410  * Sets the organizer.
411  * 
412  * @param	{String}	organizer			the organizer
413  */
414 ZmCalItem.prototype.setOrganizer 		= function(organizer) 	{ this.organizer = organizer != "" ? organizer : null; };
415 /**
416  * Sets the repeat type.
417  * 
418  * @param	{constant}	repeatType			the repeat type
419  */
420 ZmCalItem.prototype.setRecurType		= function(repeatType)	{ this._recurrence.repeatType = repeatType; };
421 /**
422  * Sets the item type.
423  * 
424  * @param	{constant}	newType			the item type
425  */
426 ZmCalItem.prototype.setType 			= function(newType) 	{ this.type = newType; };
427 /**
428  * Sets the original timezone.
429  * 
430  * @param	{Object}	timezone		the timezone
431  */
432 ZmCalItem.prototype.setOrigTimezone     = function(timezone)    { this._origTimezone = timezone; };
433 
434 /**
435  * Sets the folder id.
436  * 
437  * @param	{String}	folderId		the folder id
438  */
439 ZmCalItem.prototype.setFolderId =
440 function(folderId) {
441 	this.folderId = folderId || ZmOrganizer.ID_CALENDAR;
442 };
443 
444 /**
445  * Gets the "local" folder id even for remote folders. Otherwise, just use <code>this.folderId</code>.
446  * 
447  * @return	{ZmFolder|String}		the folder or folder id
448  */
449 ZmCalItem.prototype.getLocalFolderId =
450 function() {
451 	var fid = this.folderId;
452 	if (this.isShared()) {
453 		var folder = appCtxt.getById(this.folderId);
454 		if (folder)
455 			fid = folder.id;
456 	}
457 	return fid;
458 };
459 
460 /**
461  * Sets the end date.
462  * 
463  * @param	{Date}	endDate		the end date
464  * @param	{Boolean}	keepCache	if <code>true</code>, keep the cache; <code>false</code> to reset the cache
465  */
466 ZmCalItem.prototype.setEndDate =
467 function(endDate, keepCache) {
468     if (this._origEndDate == null && this.endDate != null && this.endDate != "") {
469         this._origEndDate = new Date(this.endDate.getTime());
470     }
471 	this.endDate = new Date(endDate instanceof Date ? endDate.getTime(): endDate);
472 	if (!keepCache)
473 		this._resetCached();
474 };
475 
476 /**
477  * Sets the start date.
478  * 
479  * @param	{Date}	startDate		the start date
480  * @param	{Boolean}	keepCache	if <code>true</code>, keep the cache; <code>false</code> to reset the cache
481  */
482 ZmCalItem.prototype.setStartDate =
483 function(startDate, keepCache) {
484 	if (this._origStartDate == null && this.startDate != null && this.startDate != "") {
485 		this._origStartDate = new Date(this.startDate.getTime());
486 	}
487 	this.startDate = new Date(startDate instanceof Date ? startDate.getTime() : startDate);
488 
489 	if (!keepCache) {
490 		this._resetCached();
491 	}
492 
493 	// recurrence should reflect start date
494 	if (this.recurring && this._recurrence) {
495 		this._recurrence.setRecurrenceStartTime(this.startDate.getTime());
496 	}
497 };
498 
499 /**
500  * Sets the timezone.
501  * 
502  * @param	{AjxTimezone}	timezone	the timezone
503  * @param	{Boolean}	keepCache	if <code>true</code>, keep the cache; <code>false</code> to reset the cache
504  */
505 ZmCalItem.prototype.setTimezone =
506 function(timezone, keepCache) {
507 	if (this._origTimezone == null) {
508 		this._origTimezone = timezone;
509 	}
510 	this.timezone = timezone;
511 	if (!keepCache) {
512 		this._resetCached();
513 	}
514 };
515 
516 /**
517  * Sets the end timezone.
518  *
519  * @param	{AjxTimezone}	timezone	the timezone
520  */
521 ZmCalItem.prototype.setEndTimezone =
522 function(timezone) {
523 	if (this._origEndTimezone == null) {
524 		this._origEndTimezone = timezone;
525 	}
526 	this.endTimezone = timezone;
527 };
528 
529 /**
530  * Sets the view mode, and resets any other fields that should not be set for that view mode.
531  * 
532  * @param	{constant}	mode		the mode (see <code>ZmCalItem.MODE_</code> constants)
533  */
534 ZmCalItem.prototype.setViewMode =
535 function(mode) {
536 	this.viewMode = mode || ZmCalItem.MODE_NEW;
537 
538 	if (this.viewMode == ZmCalItem.MODE_EDIT_SINGLE_INSTANCE)
539 		this._recurrence.repeatType = "NON";
540 };
541 
542 /**
543  * Gets the view mode
544  */
545 ZmCalItem.prototype.getViewMode =
546 function(mode) {
547 	return this.viewMode;
548 };
549 
550 /**
551  * Gets the notes part. This method will walk the notesParts array looking for
552  * the first part that matches given content type.
553  * 
554  * @param	{constant}	contentType		the content type (see {@link ZmMimeTable.TEXT_PLAIN})	
555  * @return	{String}	the content
556  * 
557  * @see	ZmMimeTable
558  */
559 ZmCalItem.prototype.getNotesPart =
560 function(contentType) {
561 	if (this.notesTopPart) {
562 		var ct = contentType || ZmMimeTable.TEXT_PLAIN;
563 		var content = this.notesTopPart.getContentForType(ct);
564 
565 		// if requested content type not found, try the other
566 		if (!content) {
567 			if (ct == ZmMimeTable.TEXT_PLAIN) {
568 				var div = document.createElement("div");
569 				content = this.notesTopPart.getContentForType(ZmMimeTable.TEXT_HTML);
570 				div.innerHTML = content || "";
571 				var text = AjxStringUtil.convertHtml2Text(div);
572 				return text.substring(1); // above func prepends \n due to div
573 			} else if (ct == ZmMimeTable.TEXT_HTML) {
574 				content = AjxStringUtil.convertToHtml(this.notesTopPart.getContentForType(ZmMimeTable.TEXT_PLAIN));
575 			}
576 		}
577 		return content;
578 	} else {
579 		return this.fragment;
580 	}
581 };
582 
583 /**
584  * Gets the remote folder owner.
585  * 
586  * @return {String}	the "owner" of remote/shared item folder this item belongs to
587  */
588 ZmCalItem.prototype.getRemoteFolderOwner =
589 function() {
590 	// bug fix #18855 - dont return the folder owner if moving betw. accounts
591 	var controller = AjxDispatcher.run("GetCalController");
592 	if (controller.isMovingBetwAccounts(this, this.folderId)) {
593 		return null;
594 	}
595 
596 	var folder = this.getFolder();
597 	var owner = folder && folder.link && folder.owner;
598 
599     var acct = (!owner && appCtxt.multiAccounts && folder.getAccount());
600 	if (acct) {
601 		owner = acct.name;
602 	}
603 	return owner;
604 };
605 
606 /**
607  * Checks if the item is read-only.
608  * 
609  * @return	{Boolean}	<code>true</code> if the item is read-only
610  */
611 ZmCalItem.prototype.isReadOnly =
612 function() {
613 	var folder = this.getFolder();
614 
615 	if (appCtxt.multiAccounts) {
616 		var orgAcct = appCtxt.accountList.getAccountByEmail(this.organizer);
617 		var calAcct = appCtxt.accountList.getAccountByEmail(folder.getAccount().getEmail());
618 		if (orgAcct == calAcct) {
619 			return false;
620 		}
621 	}
622    // TODO: Correct this method in order to return fasle for users with manager/admin rights
623 	return (!this.isOrganizer() || (folder.link && folder.isReadOnly()));
624 };
625 
626 /**
627  * Checks if the folder containing the item is read-only by the .
628  *
629  * @return	{Boolean}	<code>true</code> if the folder is read-only
630  */
631 ZmCalItem.prototype.isFolderReadOnly =
632 function() {
633 	var folder = this.getFolder();
634     return (folder && folder.isReadOnly());
635 };
636 
637 /*
638 *   To check whether version has been ignored
639 * */
640 ZmCalItem.prototype.isVersionIgnored=function(){
641     return this._ignoreVersion;
642 }
643 
644 /*
645 *   Method to set _ignoreVersion as true when conflict arises and false otherwise.
646 *   If true, the next soap request wont be sent with revision related attributes like ms&rev.
647 * */
648 ZmCalItem.prototype.setIgnoreVersion=function(isIgnorable){
649     this._ignoreVersion=isIgnorable;
650 }
651 
652 /**
653  * Resets the repeat weekly days.
654  */
655 ZmCalItem.prototype.resetRepeatWeeklyDays =
656 function() {
657 	if (this.startDate) {
658 		this._recurrence.repeatWeeklyDays = [ZmCalItem.SERVER_WEEK_DAYS[this.startDate.getDay()]];
659 	}
660 };
661 
662 /**
663  * Resets the repeat monthly day months list.
664  */
665 ZmCalItem.prototype.resetRepeatMonthlyDayList =
666 function() {
667 	if (this.startDate) {
668 		this._recurrence.repeatMonthlyDayList = [this.startDate.getDate()];
669 	}
670 };
671 
672 /**
673  * Resets the repeat yearly months list.
674  */
675 ZmCalItem.prototype.resetRepeatYearlyMonthsList =
676 function(mo) {
677 	this._recurrence.repeatYearlyMonthsList = mo;
678 };
679 
680 /**
681  * Resets the repeat custom day of week.
682  */
683 ZmCalItem.prototype.resetRepeatCustomDayOfWeek =
684 function() {
685 	if (this.startDate) {
686 		this._recurrence.repeatCustomDayOfWeek = ZmCalItem.SERVER_WEEK_DAYS[this.startDate.getDay()];
687 	}
688 };
689 
690 /**
691  * Checks if the item is overlapping.
692  * 
693  * @param	{ZmCalItem}	other		the other item to check
694  * @param	{Boolean}	checkFolder	<code>true</code> to check the folder id
695  * @return	{Boolean}	<code>true</code> if the items overlap; <code>false</code> if the items do not overlap or the item folder ids do not match
696  */
697 ZmCalItem.prototype.isOverlapping =
698 function(other, checkFolder) {
699 	if (checkFolder && this.folderId != other.folderId) { return false; }
700 
701 	var tst = this.getStartTime();
702 	var tet = this.getEndTime();
703 	var ost = other.getStartTime();
704 	var oet = other.getEndTime();
705 
706 	return (tst < oet) && (tet > ost);
707 };
708 
709 /**
710  * Checks if this item is in range.
711  * 
712  * @param	{Date}	startTime	the start range
713  * @param	{Date}	endTime	the end range
714  * @return	{Boolean}	<code>true</code> if the item is in range
715  */
716 ZmCalItem.prototype.isInRange =
717 function(startTime, endTime) {
718 	var tst = this.getStartTime();
719 	var tet = this.getEndTime();
720 	return (tst < endTime && tet > startTime);
721 };
722 
723 /**
724  * Checks whether the duration of this item is valid.
725  *
726  * @return	{Boolean}	<code>true</code> if the item possess valid duration.
727  */
728 ZmCalItem.prototype.isValidDuration =
729 function(){
730 
731     var startTime = this.getStartTime();
732     var endTime = this.getEndTime();
733 
734     if(this.endTimezone && this.endTimezone!=this.timezone){
735       var startOffset = AjxTimezone.getRule(this.timezone).standard.offset;
736       var endOffset = AjxTimezone.getRule(this.endTimezone).standard.offset;
737 
738       startTime = startTime - (startOffset*60000);
739       endTime = endTime - (endOffset*60000);
740     }
741 
742     return (startTime<=endTime);
743 
744 }
745 /**
746  * Checks whether the duration of this item is valid with respect to the
747  * recurrence period.  For example, if the item repeats daily, its duration
748  * should not be longer than a day.
749  *
750  * This can get very complicated due to custom repeat rules.  So the
751  * limitation is just set on the repeat type.  The purpose is to prevent
752  * (as has happened) someone creating a repeating appt where they set the
753  * duration to be the span the appt is in effect over a year instead of its
754  * duration during the day.  For example, repeat daily, start = Jan 1 2014,
755  * end = July 1 2014.   See Bug 87993.
756  *
757  * @return	{Boolean}	<code>true</code> if the item possess valid duration.
758  */
759 ZmCalItem.prototype.isValidDurationRecurrence = function() {
760 	var valid     = true;
761 	var recurType = this.getRecurType();
762 	var duration  = this.getDuration();
763 	switch (recurType) {
764 		case ZmRecurrence.DAILY:   valid = duration <= AjxDateUtil.MSEC_PER_DAY;       break;
765 		case ZmRecurrence.WEEKLY:  valid = duration <= ZmCalItem.MSEC_LIMIT_PER_WEEK;  break;
766 		case ZmRecurrence.MONTHLY: valid = duration <= ZmCalItem.MSEC_LIMIT_PER_MONTH; break;
767 		case ZmRecurrence.YEARLY:  valid = duration <= ZmCalItem.MSEC_LIMIT_PER_YEAR;  break;
768 		default: break;
769 	}
770 	return valid;
771 }
772 
773 /**
774  * @private
775  */
776 ZmCalItem.prototype.parseAlarmData =
777 function() {
778 	if (!this.alarmData) { return; }
779 
780 	for (var i = 0; i < this.alarmData.length; i++) {
781 		var alarm = this.alarmData[i].alarm;
782 		if (alarm) {
783 			for (var j = 0; j < alarm.length; j++) {
784 				this.parseAlarm(alarm[j]);
785 			}
786 		}
787 	}
788 };
789 
790 /**
791  * @private
792  */
793 ZmCalItem.prototype.parseAlarm =
794 function(tmp) {
795 	if (!tmp) { return; }
796 
797 	var s, m, h, d, w;
798 	var trigger = tmp.trigger;
799 	var rel = (trigger && (trigger.length > 0)) ? trigger[0].rel : null;
800     s = (rel && (rel.length > 0)) ? rel[0].s : null;
801 	m = (rel && (rel.length > 0)) ? rel[0].m : null;
802 	d = (rel && (rel.length > 0)) ? rel[0].d : null;
803 	h = (rel && (rel.length > 0)) ? rel[0].h : null;
804 	w = (rel && (rel.length > 0)) ? rel[0].w : null;
805 
806     this._reminderMinutes = -1;
807 	if (tmp.action == ZmCalItem.ALARM_DISPLAY) {
808         if (s == 0) { // at time of event
809             this._reminderMinutes = 0;
810         }
811 		if (m != null) {
812 			this._reminderMinutes = m;
813             this._origReminderUnits = ZmCalItem.REMINDER_UNIT_MINUTES;
814 		}
815 		if (h != null) {
816 			h = parseInt(h);
817 			this._reminderMinutes = h * 60;
818             this._origReminderUnits = ZmCalItem.REMINDER_UNIT_HOURS;
819 		}
820 		if (d != null) {
821 			d = parseInt(d);
822 			this._reminderMinutes = d * 24 * 60;
823             this._origReminderUnits = ZmCalItem.REMINDER_UNIT_DAYS;
824 		}
825         if (w != null) {
826 			w = parseInt(w);
827 			this._reminderMinutes = w * 7 * 24 * 60;
828             this._origReminderUnits = ZmCalItem.REMINDER_UNIT_WEEKS;
829 		}
830 	}
831 };
832 
833 /**
834  * Checks if the start date is in range.
835  * 
836  * @param	{Date}	startTime	the start time of the range
837  * @param	{Date}	endTime		the end time of the range
838  * @return {Boolean}	<code>true</code> if the start date of this item is within range
839  */
840 ZmCalItem.prototype.isStartInRange =
841 function(startTime, endTime) {
842 	var tst = this.getStartTime();
843 	return (tst < endTime && tst >= startTime);
844 };
845 
846 /**
847  * Checks if the end date is in range.
848  * 
849  * @param	{Date}	startTime	the start time of the range
850  * @param	{Date}	endTime		the end time of the range
851  * @return {Boolean}	<code>true</code> if the end date of this item is within range
852  */
853 ZmCalItem.prototype.isEndInRange =
854 function(startTime, endTime) {
855 	var tet = this.getEndTime();
856 	return (tet <= endTime && tet > startTime);
857 };
858 
859 /**
860  * Sets the date range.
861  * 
862  * @param	{Hash}	rangeObject		a hash of <code>startDate</code> and <code>endDate</code>
863  * @param	{Object}	instance	not used
864  * @param	{Object}	parentValue	not used
865  * @param	{Object}	refPath	not used
866  */
867 ZmCalItem.prototype.setDateRange =
868 function (rangeObject, instance, parentValue, refPath) {
869 	var s = rangeObject.startDate;
870 	var e = rangeObject.endDate;
871 	this.endDate.setTime(rangeObject.endDate.getTime());
872 	this.startDate.setTime(rangeObject.startDate.getTime());
873 };
874 
875 /**
876  * Gets the date range.
877  * 
878  * @param	{Object}	instance	not used
879  * @param	{Object}	current		not used
880  * @param	{Object}	refPath		not used
881  * @return	{Hash}	a hash of <code>startDate</code> and <code>endDate</code>
882  */
883 ZmCalItem.prototype.getDateRange =
884 function(instance, current, refPath) {
885 	return { startDate:this.startDate, endDate: this.endDate };
886 };
887 
888 /**
889  * Sets the attachments.
890  * 
891  * @param	{String}	ids		a comma delimited string of ids
892  */
893 ZmCalItem.prototype.setAttachments =
894 function(ids) {
895 	this.attachments = [];
896 
897 	if (ids && ids.length > 0) {
898 		var split = ids.split(',');
899 		for (var i = 0 ; i < split.length; i++) {
900 			this.attachments[i] = { id:split[i] };
901 		}
902 	}
903 };
904 
905 /**
906  * Gets the attachments.
907  * 
908  * @return	{Array}	an array of attachments or <code>null</code> for none
909  */
910 ZmCalItem.prototype.getAttachments =
911 function() {
912 	var attachs = this.message ? this.message.attachments : null;
913 	if (attachs) {
914 		if (this._validAttachments == null) {
915 			this._validAttachments = [];
916 			for (var i = 0; i < attachs.length; ++i) {
917 				if (this.message.isRealAttachment(attachs[i]) || attachs[i].contentType == ZmMimeTable.TEXT_CAL) {
918 					this._validAttachments.push(attachs[i]);
919 				}
920 			}
921 		}
922 		return this._validAttachments.length > 0 ? this._validAttachments : null;
923 	}
924 	return null;
925 };
926 
927 /**
928  * Removes an attachment.
929  * 
930  * @param	{Object}	part	the attachment part to remove
931  */
932 ZmCalItem.prototype.removeAttachment =
933 function(part) {
934 	if (this._validAttachments && this._validAttachments.length > 0) {
935 		for (var i = 0; i < this._validAttachments.length; i++) {
936 			if (this._validAttachments[i].part == part) {
937 				this._validAttachments.splice(i,1);
938 				break;
939 			}
940 		}
941 	}
942 };
943 
944 /**
945  * Gets the start hour in short date format.
946  * 
947  * @return	{String}	the start hour
948  */
949 ZmCalItem.prototype.getShortStartHour =
950 function() {
951 	var formatter = AjxDateFormat.getTimeInstance(AjxDateFormat.SHORT);
952 	return formatter.format(this.startDate);
953 };
954 
955 /**
956  * Gets the unique start date.
957  * 
958  * @return	{Date}	the start date
959  */
960 ZmCalItem.prototype.getUniqueStartDate =
961 function() {
962 	if (this._uniqueStartDate == null && this.uniqStartTime) {
963 		this._uniqueStartDate = new Date(this.uniqStartTime);
964 	}
965 	return this._uniqueStartDate;
966 };
967 
968 /**
969  * Gets the unique end date.
970  * 
971  * @return	{Date}	the end date
972  */
973 ZmCalItem.prototype.getUniqueEndDate =
974 function() {
975 	if (this._uniqueEndDate == null && this.uniqStartTime) {
976 		this._uniqueEndDate = new Date(this.uniqStartTime + this.getDuration());
977 	}
978 	return this._uniqueEndDate;
979 };
980 
981 /**
982  * Gets the details.
983  * 
984  * @param	{constant}	viewMode	the view mode
985  * @param	{AjxCallback}	callback	the callback
986  * @param	{AjxCallback}	errorCallback	the callback on error
987  * @param	{Boolean}	ignoreOutOfDate		if <code>true</code>, ignore out of date items
988  * @param	{Boolean}	noBusyOverlay		if <code>true</code>, no busy overlay
989  * @param	{ZmBatchCommand}	batchCmd			set if part of a batch operation
990  */
991 ZmCalItem.prototype.getDetails =
992 function(viewMode, callback, errorCallback, ignoreOutOfDate, noBusyOverlay, batchCmd) {
993 	var mode = viewMode || this.viewMode;
994 
995 	var seriesMode = mode == ZmCalItem.MODE_EDIT_SERIES;
996     var fetchSeriesMsg = (seriesMode && this.message && !this.message.seriesMode);
997 	if (this.message == null || fetchSeriesMsg) {
998 		var id = seriesMode ? (this.seriesInvId || this.invId || this.id) : this.invId;
999 		this.message = new ZmMailMsg(id);
1000 		if (this._orig) {
1001 			this._orig.message = this.message;
1002 		}
1003 		var respCallback = new AjxCallback(this, this._handleResponseGetDetails, [mode, this.message, callback]);
1004 		var respErrorCallback = (!ignoreOutOfDate)
1005 			? (new AjxCallback(this, this._handleErrorGetDetails, [mode, callback, errorCallback]))
1006 			: errorCallback;
1007 
1008 		var acct = appCtxt.isOffline && this.getFolder().getAccount();
1009 		var params = {
1010 			callback: respCallback,
1011 			errorCallback: respErrorCallback,
1012 			noBusyOverlay: noBusyOverlay,
1013 			ridZ: (seriesMode ? null : this.ridZ),
1014 			batchCmd: batchCmd,
1015 			accountName: (acct && acct.name)
1016 		};
1017 		this.message.load(params);
1018 	} else {
1019 		this.setFromMessage(this.message, mode);
1020 		if (callback) {
1021 			callback.run();
1022 		}
1023 	}
1024 };
1025 
1026 /**
1027  * @private
1028  */
1029 ZmCalItem.prototype.convertToLocalTimezone =
1030 function() {
1031     var apptTZ = this.getTimezone();
1032     var localTZ = AjxTimezone.getServerId(AjxTimezone.DEFAULT);
1033     var sd = this.startDate;
1034     var ed = this.endDate;
1035     if(apptTZ != localTZ) {
1036         var offset1 = AjxTimezone.getOffset(AjxTimezone.DEFAULT, sd);
1037         var offset2 = AjxTimezone.getOffset(AjxTimezone.getClientId(apptTZ), sd);
1038         sd.setTime(sd.getTime() + (offset1 - offset2)*60*1000);
1039         ed.setTime(ed.getTime() + (offset1 - offset2)*60*1000);
1040         this.setTimezone(localTZ);
1041         this.setEndTimezone(localTZ);
1042     }
1043 };
1044 
1045 
1046 /**
1047  * @private
1048  */
1049 ZmCalItem.prototype._handleResponseGetDetails =
1050 function(mode, message, callback, result) {
1051 	// msg content should be text, so no need to pass callback to setFromMessage()
1052 	this.setFromMessage(message, mode);
1053     message.seriesMode = (mode == ZmCalItem.MODE_EDIT_SERIES);
1054 
1055     //overwrite proposed time
1056     if(this._orig && this._orig.proposedInvite) {
1057         var invite = this._orig.proposedInvite;
1058         var start = invite.getServerStartTime();
1059         var end = invite.getServerEndTime();
1060         if (start) this.setStartDate(AjxDateUtil.parseServerDateTime(start, true));
1061         if (end) this.setEndDate(AjxDateUtil.parseServerDateTime(end, true));
1062 
1063         //set timezone from proposed invite
1064         var tz = invite.getServerStartTimeTz();
1065         this.setTimezone(tz || AjxTimezone.getServerId(AjxTimezone.DEFAULT));
1066 
1067         // record whether the start/end dates are in UTC
1068         this.startsInUTC = start ? start.charAt(start.length-1) == "Z" : null;
1069         this.endsInUTC = end && start ? end.charAt(start.length-1) == "Z" : null;
1070 
1071         //set all the fields that are not generated in GetAppointmentResponse - accept proposal mode
1072         this.status = invite.components[0].status;
1073 
1074         //convert proposed invite timezone to local timezone
1075         this.convertToLocalTimezone();
1076         this.isAcceptingProposal = true;
1077     }
1078 	if (callback) callback.run(result);
1079 };
1080 
1081 /**
1082  * @private
1083  */
1084 ZmCalItem.prototype._handleErrorGetDetails =
1085 function(mode, callback, errorCallback, ex) {
1086 	if (ex.code == "mail.INVITE_OUT_OF_DATE") {
1087         var jsonObj = {},
1088             requestName = this._getRequestNameForMode(ZmCalItem.MODE_GET),
1089             request = jsonObj[requestName] = {
1090                 _jsns : "urn:zimbraMail"
1091             },
1092             respCallback = new AjxCallback(this, this._handleErrorGetDetails2, [mode, callback, errorCallback]),
1093             params;
1094 
1095         request.id = this.id;
1096 		params = {
1097 			jsonObj: jsonObj,
1098 			asyncMode: true,
1099 			callback: respCallback,
1100 			errorCallback: errorCallback
1101 		};
1102 		appCtxt.getAppController().sendRequest(params);
1103 		return true;
1104 	}
1105 	if (ex.code == "account.ACCOUNT_INACTIVE") {
1106         var msg = ex.msg ? ex.msg.split(':') : null,
1107             acctEmailId = msg ? msg[1] : '',
1108             msgDlg = appCtxt.getMsgDialog();
1109         msgDlg.setMessage(AjxMessageFormat.format(ZmMsg.accountInactiveError, acctEmailId), DwtMessageDialog.CRITICAL_STYLE);
1110         msgDlg.popup();
1111 		return true;
1112 	}
1113 	if (errorCallback) {
1114 		return errorCallback.run(ex);
1115 	}
1116 	return false;
1117 };
1118 
1119 /**
1120  * @private
1121  */
1122 ZmCalItem.prototype._handleErrorGetDetails2 =
1123 function(mode, callback, errorCallback, result) {
1124 	// Update invId and force a message reload
1125 	var invite = this._getInviteFromError(result);
1126 	this.invId = [this.id, invite.id].join("-");
1127 	this.message = null;
1128 	var ignoreOutOfDate = true;
1129 	this.getDetails(mode, callback, errorCallback, ignoreOutOfDate);
1130 };
1131 
1132 /**
1133  * Sets the from message.
1134  * 
1135  * @param	{String}	message		the message
1136  * @param	{constant}	viewMode	the view mode
1137  * 
1138  * @private
1139  */
1140 ZmCalItem.prototype.setFromMessage =
1141 function(message, viewMode) {
1142 	if (message == this._currentlyLoaded) { return; }
1143 
1144 	if (message.invite) {
1145 		this.isOrg = message.invite.isOrganizer();
1146 		this.organizer = message.invite.getOrganizerEmail();
1147 		this.organizerName = message.invite.getOrganizerName();
1148 		this.sentBy = message.invite.getSentBy();
1149 		this.name = message.invite.getName() || message.subject;
1150 		this.isException = message.invite.isException();
1151         this.recurring =  message.invite.isRecurring();
1152         this.location = message.invite.getLocation();
1153         this.seq = message.invite.getSequenceNo();
1154         this.allDayEvent = message.invite.isAllDayEvent();
1155         if(message.invite.id) {
1156             this.invId = this.id + "-" + message.invite.id;
1157         }
1158 		this._setTimeFromMessage(message, viewMode);
1159 		this._setExtrasFromMessage(message);
1160 		this._setRecurrence(message);
1161 	}
1162 	this._setNotes(message);
1163 	this.getAttachments();
1164 
1165 	this._currentlyLoaded = message;
1166 };
1167 
1168 /**
1169  * Sets the required data from saved response
1170  *
1171  * @param	{Object} result create/moify appt response
1172  */
1173 ZmCalItem.prototype.setFromSavedResponse =
1174 function(result) {
1175     this.invId = result.invId;
1176     if(this.message) {
1177         this.message.rev = result.rev;
1178         this.message.ms = result.ms;
1179     }
1180 
1181     if(this.viewMode == ZmCalItem.MODE_EDIT_SINGLE_INSTANCE && !this.isException) {
1182         this.isException = true;
1183     }
1184 };
1185 
1186 /**
1187  * Sets the from mail message. This method gets called when a mail item
1188  * is dragged onto the item and we
1189  * need to load the mail item and parse the right parts to show in {@link ZmCalItemEditView}.
1190  * 
1191  * @param	{String}	message		the message
1192  * @param	{String}	subject		the subject
1193  * 
1194  * @private
1195  */
1196 ZmCalItem.prototype.setFromMailMessage =
1197 function(message, subject) {
1198 	this.name = subject;
1199 	this._setNotes(message);
1200 	// set up message so attachments work
1201 	this.message = message;
1202 	this.invId = message.id;
1203 };
1204 
1205 /**
1206  * Sets the notes (text/plain).
1207  * 
1208  * @param	{String}	notes		the notes
1209  */
1210 ZmCalItem.prototype.setTextNotes =
1211 function(notes) {
1212 	this.notesTopPart = new ZmMimePart();
1213 	this.notesTopPart.setContentType(ZmMimeTable.TEXT_PLAIN);
1214 	this.notesTopPart.setContent(notes);
1215 };
1216 
1217 /**
1218  * @private
1219  */
1220 ZmCalItem.prototype._setTimeFromMessage =
1221 function(message, viewMode) {
1222 	// For instance of recurring appointment, start date is generated from unique
1223 	// start time sent in appointment summaries. Associated message will contain
1224 	// only the original start time.
1225 	var start = message.invite.getServerStartTime();
1226 	var end = message.invite.getServerEndTime();
1227 	if (viewMode === ZmCalItem.MODE_EDIT_SINGLE_INSTANCE || viewMode === ZmCalItem.MODE_FORWARD_SINGLE_INSTANCE
1228 			|| viewMode === ZmCalItem.MODE_COPY_SINGLE_INSTANCE) {
1229 		var usd = this.getUniqueStartDate();
1230 		if (usd) {
1231 			this.setStartDate(usd);
1232 		}
1233 
1234 		var ued = this.getUniqueEndDate();
1235 		if (ued) {
1236 			if (this.isAllDayEvent() && viewMode === ZmCalItem.MODE_COPY_SINGLE_INSTANCE) {
1237 				//special case - copying and all day event. The day it ends is a one too many days. Creating a copy gets confused otherwise and adds that day.
1238 				ued.setDate(ued.getDate() - 1);
1239 			}
1240 			this.setEndDate(ued);
1241 		}
1242 		if (viewMode === ZmCalItem.MODE_COPY_SINGLE_INSTANCE) {
1243 			viewMode = ZmCalItem.MODE_EDIT_SINGLE_INSTANCE; // kinda hacky - the copy mode has run its course. Now treat it like edit mode. Would be less impact.
1244 		}
1245 	}
1246 	else {
1247 		if (start) this.setStartDate(AjxDateUtil.parseServerDateTime(start));
1248 		if (end) this.setEndDate(AjxDateUtil.parseServerDateTime(end));
1249 	}
1250 
1251 	// record whether the start/end dates are in UTC
1252 	this.startsInUTC = start ? start.charAt(start.length-1) == "Z" : null;
1253 	this.endsInUTC = end && start ? end.charAt(start.length-1) == "Z" : null;
1254 
1255 	// record timezone
1256     var timezone;
1257 	if (viewMode == ZmCalItem.MODE_EDIT_SINGLE_INSTANCE || viewMode == ZmCalItem.MODE_DELETE_INSTANCE || viewMode == ZmCalItem.MODE_FORWARD_SINGLE_INSTANCE) {
1258         timezone = AjxTimezone.getServerId(AjxTimezone.DEFAULT);
1259 		this.setTimezone(timezone);
1260 		this.setEndTimezone(timezone);
1261 	}
1262 	else {
1263 		var serverId = !this.startsInUTC && message.invite.getServerStartTimeTz();
1264         timezone = serverId || AjxTimezone.getServerId(AjxTimezone.DEFAULT);
1265 		this.setTimezone(timezone);
1266         var endServerId = !this.endsInUTC && message.invite.getServerEndTimeTz();
1267 		this.setEndTimezone(endServerId || AjxTimezone.getServerId(AjxTimezone.DEFAULT));
1268 		if(!this.startsInUTC && message.invite.getServerEndTimeTz()) this.setEndTimezone(message.invite.getServerEndTimeTz());
1269 	}
1270 
1271 	var tzrule = AjxTimezone.getRule(AjxTimezone.getClientId(this.getTimezone()));
1272 	if (tzrule) {
1273 		if (tzrule.aliasId) {
1274 			tzrule = AjxTimezone.getRule(tzrule.aliasId) || tzrule;
1275 		}
1276 		this.setTimezone(tzrule.serverId);
1277 	}
1278 
1279     tzrule = AjxTimezone.getRule(AjxTimezone.getClientId(this.endTimezone));
1280     if (tzrule) {
1281         if (tzrule.aliasId) {
1282             tzrule = AjxTimezone.getRule(tzrule.aliasId) || tzrule;
1283         }
1284         this.setEndTimezone(tzrule.serverId);
1285     }
1286 };
1287 
1288 /**
1289  * Override to add specific initialization but remember to call
1290  * the base implementation.
1291  *
1292  * @private
1293  */
1294 ZmCalItem.prototype._setExtrasFromMessage =
1295 function(message) {
1296     this._setAlarmFromMessage(message);
1297 };
1298 
1299 ZmCalItem.prototype._setAlarmFromMessage =
1300 function(message) {
1301     this._reminderMinutes = -1;
1302 	var alarm = message.invite.getAlarm();
1303 	if (alarm) {
1304 		for (var i = 0; i < alarm.length; i++) {
1305             var alarmInst = alarm[i];
1306             if (!alarmInst) continue;
1307 
1308             var action = alarmInst.action;
1309 			if (action == ZmCalItem.ALARM_DISPLAY) {
1310 				this.parseAlarm(alarmInst);
1311                 // NOTE: No need to add a display alarm because it's
1312                 // NOTE: added by default in the constructor.
1313                 continue;
1314 			}
1315 
1316             // NOTE: Both email and device-email/sms reminders appear
1317             // NOTE: as "EMAIL" alarms but we distinguish between them
1318             // NOTE: upon loading.
1319             if (action == ZmCalItem.ALARM_EMAIL) {
1320                 var emails = alarmInst.at;
1321                 if (!emails) continue;
1322                 for (var j = 0; j < emails.length; j++) {
1323                     var email = emails[j].a;
1324                     if (email == appCtxt.get(ZmSetting.CAL_DEVICE_EMAIL_REMINDERS_ADDRESS)) {
1325                         action = ZmCalItem.ALARM_DEVICE_EMAIL;
1326                     }
1327                     this.addReminderAction(action);
1328                 }
1329             }
1330 		}
1331 	}
1332 };
1333 
1334 /**
1335  * @private
1336  */
1337 ZmCalItem.prototype._setRecurrence =
1338 function(message) {
1339 	var recurRules = message.invite.getRecurrenceRules();
1340 
1341 	if (recurRules)
1342 		this._recurrence.parse(recurRules);
1343 
1344 	if (this._recurrence.repeatWeeklyDays == null)
1345 		this.resetRepeatWeeklyDays();
1346 
1347 	if (this._recurrence.repeatMonthlyDayList == null)
1348 		this.resetRepeatMonthlyDayList();
1349 };
1350 
1351 /**
1352  * We are removing starting 2 \n's for the bug 21823
1353  * XXX - this does not look very efficient
1354  * 
1355  * @private
1356  */
1357 ZmCalItem.prototype._getCleanHtml2Text = 
1358 function(dwtIframe) {
1359 	var textContent;
1360 	var idoc = dwtIframe ? dwtIframe.getDocument() : null;
1361 	var body = idoc ? idoc.body : null;
1362 	if (body) {
1363 		var html = body.innerHTML.replace(/\n/ig, "");
1364 		body.innerHTML = html.replace(/<!--.*-->/ig, "");
1365 		var firstChild = body.firstChild;
1366 		var removeN = (firstChild && firstChild.tagName && firstChild.tagName.toLocaleLowerCase() == "p");
1367 		textContent = AjxStringUtil.convertHtml2Text(body);
1368 		if (removeN) {
1369 			textContent = textContent.replace(/\n\n/i, "");
1370 		}
1371 	}
1372 	return textContent;
1373 };
1374 
1375 /**
1376  * @private
1377  */
1378 ZmCalItem.prototype._setNotes =
1379 function(message) {
1380 
1381     if(!(message.isZmMailMsg)) { return; }
1382 	this.notesTopPart = new ZmMimePart();
1383 
1384 	var htmlContent = message.getBodyContent(ZmMimeTable.TEXT_HTML);
1385 	if (htmlContent) {
1386 		htmlContent = htmlContent.replace(/<title\s*>.*\/title>/ig,"");
1387 		if (!this._includeEditReply) {
1388 			htmlContent = this._trimNotesSummary(htmlContent, true);
1389 		}
1390 	}
1391 
1392 	if (htmlContent) {
1393 		// create a temp iframe to create a proper DOM tree
1394 		var params = {parent:appCtxt.getShell(), hidden:true, html:htmlContent};
1395 		var textContent = message.getInviteDescriptionContentValue(ZmMimeTable.TEXT_PLAIN);
1396 		if (!textContent) { //only go through this pain if textContent is somehow not available from getInviteDescriptionContentValue (no idea if this could happen).
1397 			var dwtIframe = new DwtIframe(params);
1398 			textContent = this._getCleanHtml2Text(dwtIframe);
1399 			// bug: 23034 this hidden iframe under shell is adding more space
1400 			// which breaks calendar column view
1401 			var iframe = dwtIframe.getIframe();
1402 			if (iframe && iframe.parentNode) {
1403 				iframe.parentNode.removeChild(iframe);
1404 			}
1405 			delete dwtIframe;
1406 		}
1407 
1408         // create two more mp's for text and html content types
1409 		var textPart = new ZmMimePart();
1410 		textPart.setContentType(ZmMimeTable.TEXT_PLAIN);
1411 		textPart.setContent(textContent);
1412 
1413 		var htmlPart = new ZmMimePart();
1414 		htmlPart.setContentType(ZmMimeTable.TEXT_HTML);
1415 		htmlPart.setContent(htmlContent);
1416 
1417 		this.notesTopPart.setContentType(ZmMimeTable.MULTI_ALT);
1418 		this.notesTopPart.children.add(textPart);
1419 		this.notesTopPart.children.add(htmlPart);
1420 	} else {
1421 		var textContent = message.getBodyContent(ZmMimeTable.TEXT_PLAIN);
1422 		if (!this._includeEditReply) {
1423 			textContent = this._trimNotesSummary(textContent);
1424 		}
1425 		this.notesTopPart.setContentType(ZmMimeTable.TEXT_PLAIN);
1426 		this.notesTopPart.setContent(textContent);
1427 	}
1428 };
1429 
1430 /**
1431  * Gets the mail notification option.
1432  * 
1433  * @return	{Boolean}	<code>true</code> if the mail notification is set; <code>false</code> otherwise
1434  */
1435 ZmCalItem.prototype.getMailNotificationOption =
1436 function() {
1437     return this._sendNotificationMail;
1438 };
1439 
1440 /**
1441  * Sets the mail notification option.
1442  * 
1443  * @param	{Boolean}	sendNotificationMail	<code>true</code> to set the mail notification
1444  */
1445 ZmCalItem.prototype.setMailNotificationOption =
1446 function(sendNotificationMail) {
1447     this._sendNotificationMail = sendNotificationMail;    
1448 };
1449 
1450 /**
1451  * Sets the exception details to request
1452  *
1453  * @param	{Element}	comp	comp element of request object
1454  */
1455 ZmCalItem.prototype.addExceptionDetails =
1456 function(comp) {
1457     var exceptId = comp.exceptId = {},
1458         allDay = this._orig ? this._orig.allDayEvent : this.allDayEvent,
1459         timezone,
1460         sd;
1461 
1462     if (allDay != "1") {
1463         sd = AjxDateUtil.getServerDateTime(this.getOrigStartDate(), this.startsInUTC);
1464         // bug fix #4697 (part 2)
1465         timezone = this.getOrigTimezone();
1466         if (!this.startsInUTC && timezone) {
1467             exceptId.tz = timezone;
1468         }
1469         exceptId.d = sd;
1470     }
1471     else {
1472         sd = AjxDateUtil.getServerDate(this.getOrigStartDate());
1473         exceptId.d = sd;
1474     }
1475 };
1476 
1477 /**
1478  * Saves the item.
1479  * 
1480  * @param {String}	attachmentId 		the id of the already uploaded attachment
1481  * @param {AjxCallback}		callback 			the callback triggered once request for appointment save is complete
1482  * @param {AjxCallback}		errorCallback		the callback triggered if error during appointment save request
1483  * @param {Array}	notifyList 		the optional sublist of attendees to be notified (if different from original list of attendees)
1484 */
1485 ZmCalItem.prototype.save =
1486 function(attachmentId, callback, errorCallback, notifyList) {
1487 	var needsExceptionId = false,
1488         jsonObj = {},
1489         requestName = this._getRequestNameForMode(this.viewMode, this.isException),
1490         request = jsonObj[requestName] = {
1491             _jsns : "urn:zimbraMail"
1492         },
1493         accountName,
1494         invAndMsg,
1495         comp;
1496 
1497 	if (this.viewMode == ZmCalItem.MODE_EDIT_SINGLE_INSTANCE &&
1498 		!this.isException)
1499 	{
1500 		this._addInviteAndCompNum(request);
1501 		needsExceptionId = true;
1502 	}
1503 	else if (this.viewMode == ZmCalItem.MODE_EDIT ||
1504 			 this.viewMode == ZmCalItem.MODE_EDIT_SINGLE_INSTANCE || 
1505 			 this.viewMode == ZmCalItem.MODE_EDIT_SERIES)
1506 	{
1507 		this._addInviteAndCompNum(request);
1508 		needsExceptionId = this.isException;
1509 	}
1510 
1511 	accountName = this.getRemoteFolderOwner();
1512 	invAndMsg = this._setRequestAttributes(request, attachmentId, notifyList, accountName);
1513 
1514 	comp = invAndMsg.inv.comp[0];
1515 	if (needsExceptionId) {
1516         this.addExceptionDetails(comp);
1517 	} else {
1518 		// set recurrence rules for appointment (but not for exceptions!)
1519 		this._recurrence.setJson(comp);
1520 	}
1521 
1522 	//set alarm data
1523 	this._setAlarmData(comp);
1524 
1525 	this._sendRequest(null, accountName, callback, errorCallback, jsonObj, requestName);
1526 };
1527 
1528 ZmCalItem.prototype._setAlarmData =
1529 function(comp) {
1530 
1531 	var useAbs = this._useAbsoluteReminder,
1532         time = useAbs ? this._reminderAbs : this._reminderMinutes;
1533 
1534     if (time == null || time === -1) {
1535         return;
1536     }
1537 
1538     for (var i = 0, len = this.alarmActions.size(); i < len; i++) {
1539 		var email = null;
1540 		var action = this.alarmActions.get(i);
1541 		if (action == ZmCalItem.ALARM_EMAIL) {
1542 			email = appCtxt.get(ZmSetting.CAL_EMAIL_REMINDERS_ADDRESS);
1543 			if (!email) {
1544                 continue;
1545             }
1546 		}
1547         if (action == ZmCalItem.ALARM_DEVICE_EMAIL) {
1548             email = appCtxt.get(ZmSetting.CAL_DEVICE_EMAIL_REMINDERS_ADDRESS);
1549             if (!email) {
1550                 continue;
1551             }
1552             // NOTE: treat device email alarm as a standard email alarm
1553             action = ZmCalItem.ALARM_EMAIL;
1554         }
1555 		var alarms = comp.alarm = comp.alarm || [];
1556 		var alarm = {action: action};
1557 		alarms.push(alarm);
1558 		var trigger = alarm.trigger = {};
1559 		this._setReminderUnits(trigger, time);
1560 		this._addXPropsToAlarm(alarm);
1561 		if (email) {
1562 			alarm.at = {a: email};
1563 		}
1564 	}
1565 };
1566 
1567 /**
1568  * @private
1569  */
1570 ZmCalItem.prototype._setReminderUnits =
1571 function(trigger, time) {
1572 	time = time || 0;
1573 	var useAbs = this._useAbsoluteReminder,
1574         rel = trigger[useAbs ? "abs" : "rel"] = {};
1575 	if (useAbs) {
1576 		rel.d = time;
1577 	}
1578 	else {
1579 		rel.m = time;
1580 		//default option is to remind before appt start
1581 		rel.related = "START";
1582 		rel.neg = "1";
1583 	}
1584 };
1585 
1586 /**
1587  * @private
1588  */
1589 ZmCalItem.prototype._addXPropsToAlarm =
1590 function(alarmNode) {
1591 	if (!this.alarmData) {
1592         return;
1593     }
1594 	var alarmData = (this.alarmData && this.alarmData.length > 0)? this.alarmData[0] : null,
1595 	    alarm = alarmData ? alarmData.alarm : null,
1596 	    alarmInst = (alarm && alarm.length > 0) ? alarm[0] : null;
1597 
1598     this._setAlarmXProps(alarmInst, alarmNode);
1599 };
1600 
1601 /**
1602  * @private
1603  */
1604 ZmCalItem.prototype._setAlarmXProps =
1605 function(alarmInst, alarmNode)  {
1606     var xprops = (alarmInst && alarmInst.xprop) ? alarmInst.xprop : null,
1607         i,
1608         x,
1609         xprop;
1610 
1611     if (!xprops) {
1612         return;
1613     }
1614     // bug 28924: preserve x props
1615     xprops = (xprops instanceof Array) ? xprops : [xprops];
1616 
1617     for (i = 0; i < xprops.length; i++) {
1618         xprop = xprops[i];
1619         if (xprop && xprop.name) {
1620             x = alarmNode.xprop = {};
1621             x.name = xprop.name;
1622             if (xprop.value != null) {
1623                 x.value = xprop.value;
1624             }
1625             this._addXParamToRequest(x, xprop.xparam);
1626         }
1627     }
1628 };
1629 
1630 /**
1631  * Sets reminder minutes.
1632  * 
1633  * @param	{int}	minutes		the minutes
1634  */
1635 ZmCalItem.prototype.setReminderMinutes =
1636 function(minutes) {
1637 	this._reminderMinutes = minutes;
1638 };
1639 
1640 /**
1641  * Sets the reminder units
1642  * 
1643  * @param	{int}	reminderValue		the reminder value
1644  * @param	{int}	reminderUnits		the reminder units
1645  */
1646 ZmCalItem.prototype.setReminderUnits =
1647 function(reminderValue, reminderUnits, sendEmail) {
1648     if (!reminderValue) {
1649         this._reminderMinutes = 0;
1650         return;
1651     }
1652     reminderValue = parseInt(reminderValue + "");
1653 	this._reminderMinutes = ZmCalendarApp.convertReminderUnits(reminderValue, reminderUnits);
1654 	this._reminderSendEmail = sendEmail;
1655 };
1656 
1657 /**
1658  * Adds the given action to this appt's reminders. A type of action can only be added once.
1659  *
1660  * @param {constant}	action		alarm action
1661  */
1662 ZmCalItem.prototype.addReminderAction =
1663 function(action) {
1664 	this.alarmActions.add(action, null, true);
1665 };
1666 
1667 /**
1668  * Removes the given action from this appt's reminders.
1669  *
1670  * @param {constant}	action		alarm action
1671  */
1672 ZmCalItem.prototype.removeReminderAction =
1673 function(action) {
1674 	this.alarmActions.remove(action);
1675 };
1676 
1677 /**
1678  * Deletes/cancels appointment/invite
1679  *
1680  * @param {int}	mode		designated what kind of delete op is this?
1681  * @param {ZmMailMsg}		msg				the message to be sent in lieu of delete
1682  * @param {AjxCallback}		callback			the callback to trigger after delete
1683  * @param {AjxCallback}		errorCallback	the error callback to trigger
1684  * @param {ZmBatchCommand}	batchCmd		set if part of a batch operation
1685  */
1686 ZmCalItem.prototype.cancel =
1687 function(mode, msg, callback, errorCallback, batchCmd) {
1688 	this.setViewMode(mode);
1689 	if (msg) {
1690 		// REVISIT: We explicitly set the bodyParts of the message b/c
1691 		// ZmComposeView#getMsg only sets topPart on new message that's returned.
1692 		// And ZmCalItem#_setNotes calls ZmMailMsg#getBodyPart.
1693 		var bodyParts = [];
1694 		var childParts = (msg._topPart.contentType == ZmMimeTable.MULTI_ALT)
1695 			? msg._topPart.children.getArray()
1696 			: [msg._topPart];
1697 		for (var i = 0; i < childParts.length; i++) {
1698 			bodyParts.push(childParts[i]);
1699 		}
1700 		msg.setBodyParts(bodyParts);
1701 		this._setNotes(msg);
1702 		this._doCancel(mode, callback, msg, batchCmd);
1703 	} else {
1704 		// To get the attendees for this appointment, we have to get the message.
1705 		var respCallback = new AjxCallback(this, this._doCancel, [mode, callback, null, batchCmd]);
1706 		var cancelErrorCallback = new AjxCallback(this, this._handleCancelError, [mode, callback, errorCallback]);
1707 		if (this._blobInfoMissing && mode != ZmCalItem.MODE_DELETE_SERIES) {
1708 			this.showBlobMissingDlg();		
1709 		} else {
1710 			this.getDetails(null, respCallback, cancelErrorCallback);
1711 		}
1712 	}
1713 };
1714 
1715 /**
1716  * @private
1717  */
1718 ZmCalItem.prototype.showBlobMissingDlg =
1719 function() {
1720 	var msgDialog = appCtxt.getMsgDialog();
1721 	msgDialog.setMessage(ZmMsg.apptBlobMissing, DwtMessageDialog.INFO_STYLE);
1722 	msgDialog.popup();
1723 };
1724 
1725 /**
1726  * @private
1727  */
1728 ZmCalItem.prototype._handleCancelError = 
1729 function(mode, callback, errorCallback, ex) {
1730 
1731 	if (ex.code == "mail.NO_SUCH_BLOB") {
1732  		//bug: 19033, cannot delete instance of appt with missing blob info
1733  		if (this.isRecurring() && mode != ZmCalItem.MODE_DELETE_SERIES) {
1734 			this._blobInfoMissing = true;
1735 			this.showBlobMissingDlg();
1736 			return true;
1737  		} else {
1738 	 		this._doCancel(mode, callback, this.message);
1739  		}
1740  		return true;
1741  	}
1742 	
1743 	if (errorCallback) {
1744 		return errorCallback.run(ex);
1745 	}
1746 
1747 	return false;
1748 };
1749 
1750 /**
1751  * @private
1752  */
1753 ZmCalItem.prototype.setCancelFutureInstances =
1754 function(cancelFutureInstances) {
1755     this._cancelFutureInstances = cancelFutureInstances;    
1756 };
1757 
1758 ZmCalItem.prototype._sendCancelMsg =
1759 function(callback){
1760     this.save(null, callback);
1761 };
1762 
1763 /**
1764  * @private
1765  */
1766 ZmCalItem.prototype._doCancel =
1767 function(mode, callback, msg, batchCmd, result) {
1768     var folderId = this.getFolder().nId,
1769         jsonObj = {},
1770         requestName,
1771         request,
1772         action,
1773         accountName = this.getRemoteFolderOwner(),
1774         recurrence,
1775         untilDate,
1776         inst,
1777         allDay,
1778         format,
1779         clientId,
1780         m,
1781         e,
1782         i,
1783         j,
1784         type,
1785         vector,
1786         count,
1787         addr,
1788         subject,
1789         mailFromAddress,
1790         isOrganizer;
1791 
1792     if (folderId == ZmOrganizer.ID_TRASH) {
1793 		mode = ZmCalItem.MODE_PURGE;
1794         requestName = this._getRequestNameForMode(mode);
1795         request = jsonObj[requestName] = {
1796             _jsns : "urn:zimbraMail"
1797         };
1798         action = request.action = {};
1799 		action.op = "delete";
1800 		action.id = this.id;
1801 		if (batchCmd) {
1802 			batchCmd.addRequestParams(jsonObj, callback);
1803 		} else {
1804 			this._sendRequest(null, accountName, callback, null, jsonObj, requestName);
1805 		}
1806 	}
1807     else {
1808 	    if (mode == ZmCalItem.MODE_DELETE_SERIES && this._cancelFutureInstances && this.getOrigStartDate().getTime() != this.getStartTime()) {
1809 	
1810 	        recurrence = this._recurrence;
1811 	        untilDate = new Date(this.getOrigStartDate().getTime());
1812 	        untilDate.setTime(untilDate.getTime() - AjxDateUtil.MSEC_PER_DAY);
1813 	        recurrence.repeatEndDate = untilDate;
1814 	        recurrence.repeatEndType = "D";
1815 	
1816 	        this.viewMode = ZmCalItem.MODE_EDIT_SERIES;
1817 	        this._sendCancelMsg(callback);
1818 	        return;
1819 	    }
1820 		
1821 		if (mode == ZmCalItem.MODE_DELETE ||
1822 			mode == ZmCalItem.MODE_DELETE_SERIES ||
1823 			mode == ZmCalItem.MODE_DELETE_INSTANCE)
1824 		{
1825             requestName = this._getRequestNameForMode(mode);
1826             request = jsonObj[requestName] = {
1827                 _jsns : "urn:zimbraMail"
1828             };
1829 
1830 			this._addInviteAndCompNum(request);
1831 
1832 			// Exceptions should be treated as instances (bug 15817)
1833 			if (mode == ZmCalItem.MODE_DELETE_INSTANCE || this.isException) {
1834                 request.s = this.getOrigStartTime();
1835 				inst = request.inst = {};
1836 				allDay = this.isAllDayEvent();
1837 				format = allDay ? AjxDateUtil.getServerDate : AjxDateUtil.getServerDateTime;
1838 				inst.d = format(this.getOrigStartDate());
1839 				if (!allDay && this.timezone) {
1840 					inst.tz = this.timezone;
1841 
1842 					clientId = AjxTimezone.getClientId(this.timezone);
1843 					ZmTimezone.set(request, clientId, null, true);
1844 				}
1845 			}
1846             m = request.m = {};
1847             e = m.e = [];
1848             isOrganizer = this.isOrganizer();
1849             if (isOrganizer) {
1850                 if (!this.inviteNeverSent) {
1851                     // NOTE: We only use the explicit list of addresses if sending via
1852                     //       a message compose.
1853                     if (msg) {
1854                         for (i = 0; i < ZmMailMsg.ADDRS.length; i++) {
1855                             type = ZmMailMsg.ADDRS[i];
1856 
1857                             // if on-behalf-of, dont set the from address and
1858                             // don't set the reset-from (only valid when receiving a message)
1859                             if ((accountName && type == AjxEmailAddress.FROM) ||
1860                                 (type == AjxEmailAddress.RESENT_FROM)) {
1861                                 continue;
1862                             }
1863 
1864                             vector = msg.getAddresses(type);
1865                             count = vector.size();
1866                             for (j = 0; j < count; j++) {
1867                                 addr = vector.get(j);
1868                                 e.push({
1869                                     a: addr.getAddress(),
1870                                     t: AjxEmailAddress.toSoapType[type]
1871                                 });
1872                             }
1873                         }
1874 
1875                         // set from address to on-behalf-of if applicable
1876                         if (accountName) {
1877                             e.push({
1878                                 a: accountName,
1879                                 t: AjxEmailAddress.toSoapType[AjxEmailAddress.FROM]
1880                             });
1881                         }
1882                     }
1883                     else {
1884                         this._addAttendeesToRequest(null, m, null, accountName);
1885                     }
1886                 }
1887                 mailFromAddress = this.getMailFromAddress();
1888                 if (mailFromAddress) {
1889                     e.push({
1890                         a : mailFromAddress,
1891                         t : AjxEmailAddress.toSoapType[AjxEmailAddress.FROM]
1892                     });
1893                 }
1894             }
1895 	        subject = (msg && msg.subject) ? msg.subject : ([ZmMsg.cancelled, ": ", this.name].join(""));
1896             m.su = subject;
1897 			this._addNotesToRequest(m, true);
1898 
1899 			if (batchCmd) {
1900 				batchCmd.addRequestParams(jsonObj, callback);
1901 			}
1902             else {
1903 				this._sendRequest(null, accountName, callback, null, jsonObj, requestName);
1904 			}
1905 		}
1906         else {
1907 			if (callback) callback.run();
1908 		}
1909 	}
1910 };
1911 
1912 /**
1913  * Gets the mail from address.
1914  * 
1915  * @return	{String}	the address
1916  */
1917 ZmCalItem.prototype.getMailFromAddress =
1918 function() {
1919     var mailFromAddress = appCtxt.get(ZmSetting.MAIL_FROM_ADDRESS);
1920     if(mailFromAddress) {
1921         return (mailFromAddress instanceof Array) ? mailFromAddress[0] : mailFromAddress;
1922     }
1923 };
1924 
1925 // Returns canned text for meeting invites.
1926 // - Instances of recurring meetings should send out information that looks very
1927 //   much like a simple appointment.
1928 /**
1929  * Gets the summary as text.
1930  * 
1931  * @return	{String}	the summary
1932  */
1933 ZmCalItem.prototype.getTextSummary =
1934 function() {
1935 	return this.getSummary(false);
1936 };
1937 
1938 /**
1939  * Gets the summary as HTML.
1940  * 
1941  * @return	{String}	the summary
1942  */
1943 ZmCalItem.prototype.getHtmlSummary =
1944 function() {
1945 	return this.getSummary(true);
1946 };
1947 
1948 
1949 
1950 // Private / Protected methods
1951 
1952 /**
1953  * @private
1954  */
1955 ZmCalItem.prototype._getTextSummaryTime =
1956 function(isEdit, fieldstr, extDate, start, end, hasTime) {
1957 	var showingTimezone = appCtxt.get(ZmSetting.CAL_SHOW_TIMEZONE);
1958 
1959 	var buf = [];
1960 	var i = 0;
1961 
1962 	if (extDate) {
1963 		buf[i++] = AjxDateUtil.longComputeDateStr(extDate);
1964 		buf[i++] = ", ";
1965 	}
1966 	if (this.isAllDayEvent()) {
1967 		buf[i++] = ZmMsg.allDay;
1968 	} else {
1969 		var formatter = AjxDateFormat.getTimeInstance();
1970 		if (start)
1971 			buf[i++] = formatter.format(start);
1972 		if (start && end)
1973 			buf[i++] = " - ";
1974 		if (end)
1975 			buf[i++] = formatter.format(end);
1976 
1977 		if (showingTimezone) {
1978 			buf[i++] = " ";
1979 			buf[i++] = AjxTimezone.getLongName(AjxTimezone.getClientId(this.timezone));
1980 		}
1981 	}
1982 	// NOTE: This relies on the fact that setModel creates a clone of the
1983 	//		 appointment object and that the original object is saved in
1984 	//		 the clone as the _orig property.
1985 	if (isEdit && ((this._orig && this._orig.isAllDayEvent() != this.isAllDayEvent()) || hasTime)) {
1986 		buf[i++] = " ";
1987 		buf[i++] = ZmMsg.apptModifiedStamp;
1988 	}
1989 	buf[i++] = "\n";
1990 
1991 	return buf.join("");
1992 };
1993 
1994 /**
1995  * Uses indexOf() rather than a regex since IE didn't split on the regex correctly.
1996  * 
1997  * @private
1998  */
1999 ZmCalItem.prototype._trimNotesSummary =
2000 function(notes, isHtml) {
2001 	if (notes) {
2002 		var idx = notes.indexOf(ZmItem.NOTES_SEPARATOR);
2003 		if (idx != -1) {
2004 			notes = notes.substr(idx + ZmItem.NOTES_SEPARATOR.length);
2005             if (isHtml) {
2006                 // If HTML content is generated from text content \n are replaced with br
2007                 // Remove the leading <br> added
2008                 notes = notes.replace(/^<br><br>/i, "");
2009 				notes = notes.replace(/^<\/div><br>/i, "");
2010 				// Removes </body></html> if that is all that is left.  Reduces the html to "" in that case,
2011 				// so that later checks don't detect HTML notes.
2012 				notes = notes.replace(/^<\/body><\/html>/i, "");
2013 			}
2014             else {
2015                 notes = notes.replace(/^\n\n/i, "");
2016             }
2017 		}
2018 	}
2019 	return AjxStringUtil.trim(notes);
2020 };
2021 
2022 /**
2023  * @private
2024  */
2025 ZmCalItem.prototype._resetCached =
2026 function() {
2027 	delete this._startTimeUniqId; this._startTimeUniqId = null;
2028 	delete this._validAttachments; this._validAttachments = null;
2029 	delete this.tooltip; this.tooltip = null;
2030 };
2031 
2032 /**
2033  * @private
2034  */
2035 ZmCalItem.prototype._getTTDay =
2036 function(d) {
2037 	return DwtCalendar.getDayFormatter().format(d);
2038 };
2039 
2040 /**
2041  * @private
2042  */
2043 ZmCalItem.prototype._addInviteAndCompNum =
2044 function(request) {
2045     var id;
2046     if(this.message && !this.isVersionIgnored()){
2047         request.ms = this.message.ms;
2048         request.rev = this.message.rev;
2049 
2050     }
2051 	if (this.viewMode == ZmCalItem.MODE_EDIT_SERIES || this.viewMode == ZmCalItem.MODE_DELETE_SERIES) {
2052 		if (this.recurring && this.seriesInvId != null) {
2053             request.id = this.seriesInvId;
2054             request.comp = this.getCompNum();
2055 		}
2056 	} else {
2057 		if (this.invId != null && this.invId != -1) {
2058 			id =  this.invId;
2059 
2060 			// bug: 41530 - for offline, make sure id is fully qualified if moving across accounts
2061 			if (appCtxt.multiAccounts &&
2062 				this._orig &&
2063 				this._orig.getFolder().getAccount() != this.getFolder().getAccount())
2064 			{
2065 				id = ZmOrganizer.getSystemId(this.invId, this._orig.getFolder().getAccount(), true);
2066 			}
2067 
2068             request.id = id;
2069             request.comp = this.getCompNum();
2070 		}
2071 	}
2072 };
2073 
2074 /**
2075  * @private
2076  */
2077 ZmCalItem.prototype._getDefaultBlurb =
2078 function(cancel, isHtml) {
2079 	var buf = [];
2080 	var i = 0;
2081 	var singleInstance = this.viewMode == ZmCalItem.MODE_EDIT_SINGLE_INSTANCE ||
2082 						 this.viewMode == ZmCalItem.MODE_DELETE_INSTANCE;
2083 
2084 	if (isHtml) buf[i++] = "<h3>";
2085 
2086     if(this.isProposeTimeMode) {
2087         buf[i++] =  ZmMsg.subjectNewTime;
2088     }else if (cancel) {
2089 		buf[i++] = singleInstance ? ZmMsg.apptInstanceCanceled : ZmMsg.apptCanceled;
2090 	} else if(!this.isForwardMode || this.isOrganizer()){
2091 		if (!this.inviteNeverSent && ( this.viewMode == ZmCalItem.MODE_EDIT ||
2092 			this.viewMode == ZmCalItem.MODE_EDIT_SINGLE_INSTANCE ||
2093 			this.viewMode == ZmCalItem.MODE_EDIT_SERIES ) )
2094 		{
2095 			buf[i++] = singleInstance ? ZmMsg.apptInstanceModified : ZmMsg.apptModified;
2096 		}
2097 		else
2098 		{
2099 			buf[i++] = ZmMsg.apptNew;
2100 		}
2101 	}else {
2102         buf[i++] =  ZmMsg.apptForwarded;
2103     }
2104 
2105 	if (isHtml) buf[i++] = "</h3>";
2106 
2107 	buf[i++] = "\n\n";
2108 	buf[i++] = this.getSummary(isHtml);
2109 
2110 	return buf.join("");
2111 };
2112 
2113 // Server request calls
2114 
2115 /**
2116  * @private
2117  */
2118 ZmCalItem.prototype._getRequestNameForMode =
2119 function(mode, isException) {
2120 	// override
2121 };
2122 
2123 /**
2124  * @private
2125  */
2126 ZmCalItem.prototype._getInviteFromError =
2127 function(result) {
2128 	// override
2129 };
2130 
2131 /**
2132  * @private
2133  */
2134 ZmCalItem.prototype._setRequestAttributes =
2135 function(request, attachmentId, notifyList, accountName) {
2136 
2137 	var m = request.m = {},
2138 	    calendar = this.getFolder(),
2139         acct = calendar.getAccount(),
2140         isOnBehalfOf = accountName && acct && acct.name != accountName,
2141         mailFromAddress,
2142         identityUser,
2143         displayName,
2144         validAttLen,
2145         attachNode,
2146         organizer,
2147         isPrimary,
2148         identityC,
2149         identity,
2150         isRemote,
2151         addrObj,
2152         orgName,
2153         comps,
2154         comp,
2155         user,
2156         addr,
2157         inv,
2158         org,
2159         mid,
2160         me,
2161         e,
2162         i;
2163 	//m.setAttribute("l", (isOnBehalfOf ? this.getFolder().rid : this.folderId));
2164     m.l = (isOnBehalfOf ? this.getFolder().rid : this.folderId);
2165     inv = m.inv = {};
2166     e = m.e = [];
2167 	if (this.uid != null && this.uid != -1 && !this.isSharedCopy) {
2168         inv.uid = this.uid;
2169 	}
2170 
2171     comps = inv.comp = [];
2172     comp = comps[0] = {};
2173     comp.at = [];
2174 	// attendees
2175 	this._addAttendeesToRequest(comp, m, notifyList, accountName);
2176 
2177     identity = this.identity;
2178     isPrimary = identity == null || identity.isDefault;
2179     isRemote = calendar.isRemote();
2180 
2181     //FROM Address
2182 	mailFromAddress = this.getMailFromAddress();
2183 	if (this.isOrganizer() && !accountName && (mailFromAddress || isRemote || !isPrimary)) {
2184         if(mailFromAddress){
2185             addr = mailFromAddress;
2186         }else{
2187             if(isRemote){
2188                 addr = this.organizer;
2189             }else if(identity){
2190                 addr = identity.sendFromAddress;
2191                 displayName = identity.sendFromDisplay;
2192             }
2193         }
2194         addrObj = {
2195             a : addr,
2196             t : AjxEmailAddress.toSoapType[AjxEmailAddress.FROM]
2197         };
2198         if(!displayName && addr == appCtxt.get(ZmSetting.USERNAME)){
2199              displayName = appCtxt.get(ZmSetting.DISPLAY_NAME);
2200         }
2201         if(displayName){
2202             addrObj.p = displayName;
2203         }
2204         e.push(addrObj);
2205         if (identity && identity.isFromDataSource && !isRemote) {
2206             this._addIdentityFrom(identity, e, m);
2207         }
2208 	}
2209 
2210     //SENDER Address
2211     if (isRemote) {
2212         if (!identity) {
2213             identityC = appCtxt.getIdentityCollection();
2214             identity = identityC && identityC.defaultIdentity;
2215         }
2216         if (identity) {
2217             addr = identity.sendFromAddress;
2218             displayName = identity.sendFromDisplay;
2219         }
2220         else {
2221             addr = appCtxt.get(ZmSetting.USERNAME);
2222             displayName = appCtxt.get(ZmSetting.DISPLAY_NAME);
2223         }
2224         addrObj = {
2225             a : addr,
2226             t : AjxEmailAddress.toSoapType[AjxEmailAddress.SENDER]
2227         };
2228         if (displayName) {
2229             addrObj.p = displayName;
2230         }
2231         e.push(addrObj);
2232     }
2233 
2234 	this._addExtrasToRequest(request, comp);
2235 	this._addDateTimeToRequest(request, comp);
2236 	this._addXPropsToRequest(comp);
2237 	
2238 	// subject/location
2239     m.su = this.name;
2240     comp.name = this.name;
2241 	this._addLocationToRequest(comp);
2242 
2243 	// notes
2244 	this._addNotesToRequest(m);
2245 
2246 	// set organizer - but not for local account
2247 	if (!(appCtxt.isOffline && acct.isMain)) {
2248 		me = (appCtxt.multiAccounts) ? acct.getEmail() : appCtxt.get(ZmSetting.USERNAME);
2249         if (!identity) {
2250             identityC = appCtxt.getIdentityCollection(acct);
2251             identity = identityC && identityC.defaultIdentity;
2252         }        
2253         if (identity) { //If !Identity then consider the default identity
2254             identityUser = identity.sendFromAddress;
2255             displayName = identity.sendFromDisplay;
2256         }
2257 		user = mailFromAddress || identityUser || me;
2258 		organizer = this.organizer || user;
2259         org = comp.or = {};
2260         org.a = organizer;
2261 		if (isRemote) {
2262 			org.sentBy = user;  // if on-behalf of, set sentBy
2263 		}
2264         orgName = (organizer == identityUser) ? displayName : (ZmApptViewHelper.getAddressEmail(organizer)).getName();
2265 		if (orgName) {
2266             org.d = orgName;
2267         }
2268 	}
2269 
2270 	// handle attachments
2271 	this.flagLocal(ZmItem.FLAG_ATTACH, false);
2272 	this.getAttachments(); // bug 22874: make sure to populate _validAttachments
2273 	if (attachmentId != null ||
2274 		(this._validAttachments != null && this._validAttachments.length))
2275 	{
2276         attachNode = request.m.attach = {};
2277 		if (attachmentId){
2278             attachNode.aid = attachmentId;
2279 			this.flagLocal(ZmItem.FLAG_ATTACH, true);
2280 		}
2281 
2282 		if (this._validAttachments) {
2283 			validAttLen = this._validAttachments.length;
2284             attachNode.mp = [];
2285 			for (i = 0; i < validAttLen; i++) {
2286 
2287 				mid = (this.invId || this.message.id);
2288 				if ((mid.indexOf(":") < 0) && calendar.isRemote()) {
2289 					mid = (appCtxt.getActiveAccount().id + ":" + mid);
2290 				}
2291                 attachNode.mp.push({
2292                     mid : mid,
2293                     part : this._validAttachments[i].part
2294                 });
2295 			}
2296 			if (validAttLen > 0) {
2297 				this.flagLocal(ZmItem.FLAG_ATTACH, true);
2298 			}
2299 		}
2300 	}
2301 
2302 	return {'inv': inv, 'm': m };
2303 };
2304 
2305 ZmCalItem.prototype._addIdentityFrom =
2306 function(identity, e, m) {
2307     var dataSource = appCtxt.getDataSourceCollection().getById(identity.id),
2308         provider,
2309         doNotAddSender,
2310         addrObj,
2311         displayName;
2312 
2313     if (dataSource) {
2314         provider = ZmDataSource.getProviderForAccount(dataSource);
2315         doNotAddSender = provider && provider._nosender;
2316         // main account is "sender"
2317         if (!doNotAddSender) {
2318             e.push({
2319                 t : AjxEmailAddress.toSoapType[AjxEmailAddress.SENDER],
2320                 p : appCtxt.get(ZmSetting.DISPLAY_NAME) || ""
2321             });
2322         }
2323         // mail is "from" external account
2324         addrObj = {
2325             t : AjxEmailAddress.toSoapType[AjxEmailAddress.SENDER],
2326             a : dataSource.getEmail()
2327         };
2328         if (appCtxt.get(ZmSetting.DEFAULT_DISPLAY_NAME)) {
2329             displayName = dataSource.identity && dataSource.identity.sendFromDisplay;
2330             displayName = displayName || dataSource.userName || dataSource.getName();
2331             if(displayName) {
2332                 addrObj.p = displayName;
2333             }
2334         }
2335         e.push(addrObj);
2336     }
2337 };
2338 
2339 /**
2340  * @private
2341  */
2342 ZmCalItem.prototype._addExtrasToRequest =
2343 function(request, comp) {
2344 	if (this.priority) {
2345 		comp.priority = this.priority;
2346 	}
2347     comp.status = this.status;
2348 };
2349 
2350 /**
2351  * @private
2352  */
2353 ZmCalItem.prototype._addXPropsToRequest =
2354 function(comp) {
2355 	var message = this.message ? this.message : null,
2356 	    invite = (message && message.invite) ? message.invite : null,
2357         xprops = invite ? invite.getXProp() : null,
2358         xprop,
2359         x,
2360         i;
2361 	if (!xprops) { return; }
2362     comp.xprop = [];
2363 	// bug 16024: preserve x props
2364 	xprops = (xprops instanceof Array) ? xprops : [xprops];
2365 
2366 	for (i = 0; i < xprops.length; i++) {
2367 		xprop = xprops[i],
2368         x = {};
2369 		if (xprop && xprop.name) {
2370             x.name = xprop.name;
2371 			if (xprop.value != null) {
2372                 x.value = xprop.value;
2373 			}
2374 			this._addXParamToRequest(x, xprop.xparam);
2375             comp.xprop.push(x);
2376 		}		
2377 	}
2378 };
2379 
2380 /**
2381  * @private
2382  */
2383 ZmCalItem.prototype._addXParamToRequest =
2384 function(xprop, xparams) {
2385 	if (!xparams) {
2386         return;
2387     }
2388 
2389 	xparams = (xparams instanceof Array) ? xparams : [xparams];
2390     var xparam = xprop.xparam = [],
2391         xObj = {},
2392         j,
2393         x;
2394 
2395 	for (j = 0; j < xparams.length; j++) {
2396 		x = xparams[j];
2397         xObj = {};
2398 		if (x && x.name) {
2399             xObj.name = x.name;
2400 			if (x.value != null) {
2401                 xObj.value = x.value;
2402 			}
2403             xparam.push(xObj);
2404 		}
2405 	}
2406 };
2407 
2408 /**
2409  * @private
2410  */
2411 ZmCalItem.prototype._addDateTimeToRequest =
2412 function(request, comp) {
2413 	// always(?) set all day
2414     comp.allDay = this.allDayEvent + "";
2415 	// timezone
2416 	var tz,
2417         clientId,
2418         s,
2419         sd,
2420         e,
2421         ed;
2422 	if (this.timezone) {
2423 		clientId = AjxTimezone.getClientId(this.timezone);
2424 		ZmTimezone.set(request, clientId, null, true);
2425 		tz = this.timezone;
2426 	}
2427 
2428 	// start date
2429 	if (this.startDate) {
2430         s = comp.s = {};
2431 		if (!this.isAllDayEvent()) {
2432 			sd = AjxDateUtil.getServerDateTime(this.startDate, this.startsInUTC);
2433 
2434 			// set timezone if not utc date/time
2435 			if (!this.startsInUTC && tz && tz.length) {
2436                 s.tz = tz;
2437             }
2438             s.d = sd;
2439 		}
2440         else {
2441             s.d = AjxDateUtil.getServerDate(this.startDate);
2442 		}
2443 	}
2444 
2445 
2446     if(this.endTimezone) {
2447         tz = this.endTimezone;
2448     }
2449 
2450 	// end date
2451 	if (this.endDate) {
2452         e = comp.e = {};
2453 		if (!this.isAllDayEvent()) {
2454 			ed = AjxDateUtil.getServerDateTime(this.endDate, this.endsInUTC);
2455 
2456 			// set timezone if not utc date/time
2457 			if (!this.endsInUTC && tz && tz.length) {
2458 				e.tz = tz;
2459             }
2460             e.d = ed;
2461 
2462 		} else {
2463 			e.d = AjxDateUtil.getServerDate(this.endDate);
2464 		}
2465 	}
2466 };
2467 
2468 /**
2469  * @private
2470  */
2471 ZmCalItem.prototype._addAttendeesToRequest =
2472 function(inv, m, notifyList, accountName) {
2473 	// if this appt is on-behalf-of, set the from address to that person
2474     if (this.isOrganizer() && accountName) {
2475         m.e.push({
2476             a : accountName,
2477             t : AjxEmailAddress.toSoapType[AjxEmailAddress.FROM]
2478         });
2479     }
2480 };
2481 
2482 /**
2483  * @private
2484  */
2485 ZmCalItem.prototype._addNotesToRequest =
2486 function(m, cancel) {
2487 
2488 	var hasAttendees = this.hasAttendees(),
2489         tprefix = hasAttendees ? this._getDefaultBlurb(cancel) : "",
2490         hprefix = hasAttendees ? this._getDefaultBlurb(cancel, true) : "",
2491         mp = m.mp = {"mp" : []},
2492         numSubParts,
2493         part,
2494         pct,
2495         content,
2496         ntp,
2497         tcontent,
2498         hcontent,
2499         html,
2500         i;
2501 
2502     mp.ct = ZmMimeTable.MULTI_ALT;
2503 	numSubParts = this.notesTopPart ? this.notesTopPart.children.size() : 0;
2504 	if (numSubParts > 0) {
2505 		for (i = 0; i < numSubParts; i++) {
2506 			part = this.notesTopPart.children.get(i);
2507             pct = part.getContentType();
2508 
2509 			if (pct == ZmMimeTable.TEXT_HTML) {
2510                 var htmlContent = part.getContent();
2511                 htmlContent = AjxStringUtil.defangHtmlContent(htmlContent);
2512                 content = "<html><body id='htmlmode'>" + (this._includeEditReply ? htmlContent : AjxBuffer.concat(hprefix, htmlContent)) + "</body></html>";
2513 			} else {
2514 				content = this._includeEditReply ? part.getContent() : AjxBuffer.concat(tprefix, part.getContent());
2515 			}
2516             mp.mp.push({
2517                 ct : pct,
2518                 content : content
2519             });
2520 		}
2521 	} else {
2522         ntp = this.notesTopPart;
2523 		tcontent = ntp ? ntp.getContent() : "";
2524         pct = ntp ? ntp.getContentType() : ZmMimeTable.TEXT_PLAIN;
2525         mp.mp.push({
2526             ct : pct
2527         });
2528         if (pct == ZmMimeTable.TEXT_HTML) {
2529             //bug fix #9592 - html encode the text before setting it as the "HTML" part
2530             hcontent = AjxStringUtil.nl2br(AjxStringUtil.htmlEncode(tcontent));
2531             html = "<html><body>" + (this._includeEditReply ? hcontent : AjxBuffer.concat(hprefix, hcontent)) + "</body></html>";
2532             mp.mp[0].content = html;
2533         }
2534         else {
2535             mp.mp[0].content = (this._includeEditReply ? tcontent : AjxBuffer.concat(tprefix, tcontent));
2536         }
2537 	}
2538 };
2539 
2540 
2541 /**
2542  * Gets a string representation of the invite content.
2543  * 
2544  * @param       {Boolean}		isHtml	if <code>true</code>, get HTML content
2545  * @return		{String}		a string representation of the invite
2546  */
2547 ZmCalItem.prototype.getInviteDescription =
2548 function(isHtml) {
2549 	var hasAttendees = this.hasAttendees();
2550 	var tprefix = hasAttendees ? this.getSummary(false) : "";
2551 	var hprefix = hasAttendees ? this.getSummary(true) : "";
2552 
2553     var notes = this.getNotesPart(isHtml ? ZmMimeTable.TEXT_HTML : ZmMimeTable.TEXT_PLAIN);
2554     return AjxBuffer.concat(isHtml ? hprefix : tprefix, notes)    
2555 };
2556 
2557 /**
2558  * @private
2559  */
2560 ZmCalItem.prototype.setIncludeEditReply =
2561 function(includeEditReply) {
2562 	this._includeEditReply = includeEditReply;
2563 };
2564 
2565 /**
2566  * @private
2567  */
2568 ZmCalItem.prototype._sendRequest =
2569 function(soapDoc, accountName, callback, errorCallback, jsonObj, requestName) {
2570 	var responseName = soapDoc ? soapDoc.getMethod().nodeName.replace("Request", "Response") : requestName.replace("Request", "Response");
2571 	var respCallback = new AjxCallback(this, this._handleResponseSend, [responseName, callback]);
2572     if (!jsonObj) {
2573 	    appCtxt.getAppController().sendRequest({soapDoc:soapDoc, asyncMode:true, accountName:accountName, callback:respCallback, errorCallback:errorCallback});
2574     }
2575     else {
2576         appCtxt.getAppController().sendRequest({jsonObj:jsonObj, asyncMode:true, accountName:accountName, callback:respCallback, errorCallback:errorCallback});
2577     }
2578 };
2579 
2580 /**
2581  * @private
2582  */
2583 ZmCalItem.prototype._loadFromDom =
2584 function(calItemNode, instNode) {
2585 	ZmCalBaseItem.prototype._loadFromDom.call(this, calItemNode, instNode);
2586 
2587 	this.isOrg 			= this._getAttr(calItemNode, instNode, "isOrg");
2588 	var org				= calItemNode.or;
2589 	this.organizer		= org && org.a;
2590 	this.sentBy			= org && org.sentBy;
2591 	this.invId 			= this._getAttr(calItemNode, instNode, "invId");
2592 	this.compNum 		= this._getAttr(calItemNode, instNode, "compNum") || "0";
2593 	this.parseAlarmData(this.alarmData);
2594 	this.seriesInvId	= this.recurring ? calItemNode.invId : null;
2595 	this.ridZ 			= instNode && instNode.ridZ;
2596 
2597 	if (calItemNode.tn) {
2598 		this._parseTagNames(calItemNode.tn);
2599 	}
2600 	if (calItemNode.f) {
2601 		this._parseFlags(calItemNode.f);
2602 	}
2603 };
2604 
2605 // Callbacks
2606 
2607 /**
2608  * @private
2609  */
2610 ZmCalItem.prototype._handleResponseSend =
2611 function(respName, callback, result) {
2612 	var resp = result.getResponse();
2613 
2614 	// branch for different responses
2615 	var response = resp[respName];
2616 	if (response.uid != null) {
2617 		this.uid = response.uid;
2618 	}
2619 
2620     var msgNode;
2621     //echo=1 sends back echo response node, process it
2622     if(response.echo){
2623         msgNode = response.echo;
2624         if(msgNode.length > 0){
2625             msgNode = msgNode[0];
2626         }
2627     }
2628     msgNode = msgNode ? msgNode.m : response.m;
2629 	if (msgNode != null) {
2630         if(msgNode.length > 0){
2631             msgNode = msgNode[0];
2632         }
2633 		var oldInvId = this.invId;
2634 		this.invId = msgNode.id;
2635 		if (AjxUtil.isSpecified(oldInvId) && oldInvId != this.invId){
2636 			this.message = null;
2637         }else if(msgNode){
2638             this.message = new ZmMailMsg(msgNode.id);
2639             this.message._loadFromDom(msgNode);
2640             delete this._validAttachments;
2641             this._validAttachments = null;
2642             this.getAttachments();
2643         }
2644 	}
2645 
2646 	this._messageNode = null;
2647 
2648 	if (callback) {
2649 		callback.run(response);
2650 	}
2651 };
2652 
2653 ZmCalItem.prototype.processErrorSave =
2654 function(ex) {
2655 	// TODO: generalize error message for calItem instead of just Appt
2656     var status = {
2657         continueSave: false,
2658         errorMessage: ""
2659     };
2660 	if (ex.code == ZmCsfeException.MAIL_SEND_ABORTED_ADDRESS_FAILURE) {
2661 		var invalid = ex.getData(ZmCsfeException.MAIL_SEND_ADDRESS_FAILURE_INVALID);
2662 		var invalidMsg = (invalid && invalid.length)
2663 			? AjxMessageFormat.format(ZmMsg.apptSendErrorInvalidAddresses, AjxStringUtil.htmlEncode(invalid.join(", "))) : null;
2664 		status.errorMessage = ZmMsg.apptSendErrorAbort + "<br/>" + invalidMsg;
2665 	} else if (ex.code == ZmCsfeException.MAIL_SEND_PARTIAL_ADDRESS_FAILURE) {
2666 		var invalid = ex.getData(ZmCsfeException.MAIL_SEND_ADDRESS_FAILURE_INVALID);
2667 		status.errorMessage = (invalid && invalid.length)
2668 			? AjxMessageFormat.format(ZmMsg.apptSendErrorPartial, AjxStringUtil.htmlEncode(invalid.join(", ")))
2669 			: ZmMsg.apptSendErrorAbort;
2670 	} else if(ex.code == ZmCsfeException.MAIL_MESSAGE_TOO_BIG) {
2671         status.errorMessage = (this.type == ZmItem.TASK) ? ZmMsg.taskSaveErrorToobig : ZmMsg.apptSaveErrorToobig;
2672     } else if (ex.code == ZmCsfeException.MAIL_INVITE_OUT_OF_DATE) {
2673         if(!this.isVersionIgnored()){
2674             this.setIgnoreVersion(true);
2675             status.continueSave = true;
2676         }
2677         else{
2678             status.errorMessage = ZmMsg.inviteOutOfDate;
2679             this.setIgnoreVersion(false);
2680         }
2681     } else if (ex.code == ZmCsfeException.MAIL_NO_SUCH_CALITEM) {
2682         status.errorMessage = ex.getErrorMsg([ex.getData("itemId")]);
2683     } else if (ex.code == ZmCsfeException.MAIL_QUOTA_EXCEEDED) {
2684     		if(this.type == ZmItem.APPT){
2685                 status.errorMessage=ZmMsg.errorQuotaExceededAppt;
2686             } else if(this.type == ZmItem.TASK){
2687                 status.errorMessage=ZmMsg.errorQuotaExceededTask;
2688             }
2689     }
2690 	else if (ex.code === ZmCsfeException.MUST_BE_ORGANIZER) {
2691 		status.errorMessage = ZmMsg.mustBeOrganizer;
2692 	}
2693 
2694     return status;
2695 };
2696 
2697 ZmCalItem.prototype.setProposedTimeCallback =
2698 function(callback) {
2699     this._proposedTimeCallback = callback;
2700 };
2701 
2702 ZmCalItem.prototype.handlePostSaveCallbacks =
2703 function() {
2704     if(this._proposedTimeCallback) this._proposedTimeCallback.run(this);
2705     this.setIgnoreVersion(false);
2706 };
2707 
2708 // Static methods
2709 
2710 ZmCalItem.isPriorityHigh = function(priority) {
2711 	return AjxUtil.arrayContains(ZmCalItem.PRIORITY_HIGH_RANGE, priority);
2712 };
2713 ZmCalItem.isPriorityLow = function(priority) {
2714 	return AjxUtil.arrayContains(ZmCalItem.PRIORITY_LOW_RANGE, priority);
2715 };
2716 ZmCalItem.isPriorityNormal = function(priority) {
2717 	return AjxUtil.arrayContains(ZmCalItem.PRIORITY_NORMAL_RANGE, priority);
2718 };
2719 
2720 /**
2721  * Gets the priority label.
2722  * 
2723  * @param	{int}	priority		the priority (see <code>ZmCalItem.PRIORITY_</code> constants)
2724  * @return	{String}	the priority label
2725  * 
2726  */
2727 ZmCalItem.getLabelForPriority =
2728 function(priority) {
2729 	if (ZmCalItem.isPriorityLow(priority)) {
2730 		return ZmMsg.low;
2731 	}
2732 	if (ZmCalItem.isPriorityNormal(priority)) {
2733 		return ZmMsg.normal;
2734 	}
2735 	if (ZmCalItem.isPriorityHigh(priority)) {
2736 		return ZmMsg.high;
2737 	}
2738 	return "";
2739 };
2740 
2741 /**
2742  * Gets the priority image.
2743  * 
2744  * @param	{ZmTask}	task	the task
2745  * @param	{int}	id		the id
2746  * @return	{String}	the priority image
2747  */
2748 ZmCalItem.getImageForPriority =
2749 function(task, id) {
2750 	if (ZmCalItem.isPriorityLow(task.priority)) {
2751 			return id
2752 				? AjxImg.getImageHtml("PriorityLow_list", null, ["id='", id, "'"].join(""))
2753 				: AjxImg.getImageHtml("PriorityLow_list");
2754 	} else if (ZmCalItem.isPriorityHigh(task.priority)) {
2755 			return id
2756 				? AjxImg.getImageHtml("PriorityHigh_list", null, ["id='", id, "'"].join(""))
2757 				: AjxImg.getImageHtml("PriorityHigh_list");
2758 	}
2759 	return "";
2760 };
2761 
2762 /**
2763  * Gets the status label.
2764  * 
2765  * @param	{int}	status		the status (see <code>ZmCalendarApp.STATUS_</code> constants)
2766  * @return	{String}	the status label
2767  * 
2768  * @see	ZmCalendarApp
2769  */
2770 ZmCalItem.getLabelForStatus =
2771 function(status) {
2772 	switch (status) {
2773 		case ZmCalendarApp.STATUS_CANC: return ZmMsg.cancelled;
2774 		case ZmCalendarApp.STATUS_COMP: return ZmMsg.completed;
2775 		case ZmCalendarApp.STATUS_DEFR: return ZmMsg.deferred;
2776 		case ZmCalendarApp.STATUS_INPR: return ZmMsg.inProgress;
2777 		case ZmCalendarApp.STATUS_NEED: return ZmMsg.notStarted;
2778 		case ZmCalendarApp.STATUS_WAIT: return ZmMsg.waitingOn;
2779 	}
2780 	return "";
2781 };
2782 
2783 /**
2784  * Gets the participation status label.
2785  * 
2786  * @param	{int}	status		the status (see <code>ZmCalBaseItem.PSTATUS_</code> constants)
2787  * @return	{String}	the status label
2788  * 
2789  * @see	ZmCalBaseItem
2790  */
2791 ZmCalItem.getLabelForParticipationStatus =
2792 function(status) {
2793 	switch (status) {
2794 		case ZmCalBaseItem.PSTATUS_ACCEPT:		return ZmMsg.ptstAccept;
2795 		case ZmCalBaseItem.PSTATUS_DECLINED:	return ZmMsg.ptstDeclined;
2796 		case ZmCalBaseItem.PSTATUS_DEFERRED:	return ZmMsg.ptstDeferred;
2797 		case ZmCalBaseItem.PSTATUS_DELEGATED:	return ZmMsg.ptstDelegated;
2798 		case ZmCalBaseItem.PSTATUS_NEEDS_ACTION:return ZmMsg.ptstNeedsAction;
2799 		case ZmCalBaseItem.PSTATUS_COMPLETED:	return ZmMsg.completed;
2800 		case ZmCalBaseItem.PSTATUS_TENTATIVE:	return ZmMsg.ptstTentative;
2801 		case ZmCalBaseItem.PSTATUS_WAITING:		return ZmMsg.ptstWaiting;
2802 	}
2803 	return "";
2804 };
2805 
2806 /**
2807  * Gets the participation status icon.
2808  * 
2809  * @param	{int}	status		the status (see <code>ZmCalBaseItem.PSTATUS_</code> constants)
2810  * @return	{String}	the status icon or an empty string if status not set
2811  * 
2812  * @see	ZmCalBaseItem
2813  */
2814 ZmCalItem.getParticipationStatusIcon =
2815 function(status) {
2816 	switch (status) {
2817 		case ZmCalBaseItem.PSTATUS_ACCEPT:		return "Check";
2818 		case ZmCalBaseItem.PSTATUS_DECLINED:	return "Cancel";
2819 		case ZmCalBaseItem.PSTATUS_DEFERRED:	return "QuestionMark";
2820 		case ZmCalBaseItem.PSTATUS_DELEGATED:	return "Plus";
2821 		case ZmCalBaseItem.PSTATUS_NEEDS_ACTION:return "NeedsAction";
2822 		case ZmCalBaseItem.PSTATUS_COMPLETED:	return "Completed";
2823 		case ZmCalBaseItem.PSTATUS_TENTATIVE:	return "QuestionMark";
2824 		case ZmCalBaseItem.PSTATUS_WAITING:		return "Minus";
2825 	}
2826 	return "";
2827 };
2828 
2829 /**
2830  * @private
2831  */
2832 ZmCalItem._getTTDay =
2833 function(d, format) {
2834 	format = format || AjxDateFormat.SHORT;
2835 	var formatter = AjxDateFormat.getDateInstance();
2836 	return formatter.format(d);
2837 };
2838