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 an appointment.
 27  *
 28  */
 29 
 30 /**
 31  * @class
 32  * This class represents a calendar item.
 33  *
 34  * @param	{ZmList}	list		the list
 35  * @param	{Boolean}	noinit		if <code>true</code>, do not initialize the appointment
 36  *
 37  * @extends ZmCalItem
 38  */
 39 ZmAppt = function(list, noinit) {
 40 
 41 	ZmCalItem.call(this, ZmItem.APPT, list);
 42 	if (noinit) { return; }
 43 
 44 	this.freeBusy = "B"; 														// Free/Busy status (F|B|T|O) (free/busy/tentative/outofoffice)
 45 	this.privacy = "PUB";														// Privacy class (PUB|PRI|CON) (public/private/confidential)
 46 	this.transparency = "O";
 47 	this.startDate = new Date();
 48 	this.endDate = new Date(this.startDate.getTime() + ZmCalViewController.DEFAULT_APPOINTMENT_DURATION);
 49 	this.otherAttendees = false;
 50 	this.rsvp = true;
 51     this.inviteNeverSent = true;
 52 
 53 	// attendees by type
 54 	this._attendees = {};
 55 	this._attendees[ZmCalBaseItem.PERSON]	= [];
 56 	this._attendees[ZmCalBaseItem.LOCATION]	= [];
 57 	this._attendees[ZmCalBaseItem.EQUIPMENT]= [];
 58 
 59 	this.origAttendees = null;	// list of ZmContact
 60 	this.origLocations = null;	// list of ZmResource
 61 	this.origEquipment = null;	// list of ZmResource
 62 
 63 	// forward address
 64 	this._fwdAddrs = {};    
 65 };
 66 
 67 ZmAppt.prototype = new ZmCalItem;
 68 ZmAppt.prototype.constructor = ZmAppt;
 69 
 70 
 71 // Consts
 72 
 73 ZmAppt.MODE_DRAG_OR_SASH		= ++ZmCalItem.MODE_LAST;
 74 ZmAppt.ATTENDEES_SEPARATOR		= "; ";
 75 
 76 ZmAppt.ACTION_SAVE = "SAVE";
 77 ZmAppt.ACTION_SEND = "SEND";
 78 
 79 // Public methods
 80 
 81 ZmAppt.prototype.toString =
 82 function() {
 83 	return "ZmAppt";
 84 };
 85 
 86 /**
 87  * Gets the attendees.
 88  * 
 89  * @param	{constant}		type		the type
 90  * @return	{Array}		an array of attendee objects
 91  * 
 92  * @see		ZmCalBaseItem.PERSON
 93  * @see		ZmCalBaseItem.LOCATION
 94  * @see		ZmCalBaseItem.EQUIPMENT
 95  */
 96 ZmAppt.prototype.getAttendees =
 97 function(type) {
 98 	return this._attendees[type];
 99 };
100 
101 /**
102  * Gets the attendee as text.
103  * 
104  * @param	{constant}		type		the type
105  * @param	{Boolean}		inclDispName		if <code>true</code>, include the display name
106  * 
107  * @return	{String}	the attendee string
108  */
109 ZmAppt.prototype.getAttendeesText =
110 function(type, inclDispName) {
111 	return ZmApptViewHelper.getAttendeesString(this._attendees[type], type, inclDispName);
112 };
113 
114 /**
115  * Checks if the appointment has attendees of the specified type.
116  * 
117  * @param	{constant}		type		the type
118  * @return	{Boolean}	<code>true</code> if the appointment has 1 or more attendees
119  */
120 ZmAppt.prototype.hasAttendeeForType =
121 function(type) {
122 	return (this._attendees[type].length > 0);
123 };
124 
125 /**
126  * Checks if the appointment has any attendees.
127  * 
128  * @return	{Boolean}	<code>true</code> if the appointment has 1 or more attendees
129  */
130 ZmAppt.prototype.hasAttendees =
131 function() {
132 	return this.hasAttendeeForType(ZmCalBaseItem.PERSON) ||
133 		   this.hasAttendeeForType(ZmCalBaseItem.LOCATION) ||
134 		   this.hasAttendeeForType(ZmCalBaseItem.EQUIPMENT);
135 };
136 
137 ZmAppt.prototype.setForwardAddress =
138 function(addrs) {
139 	this._fwdAddrs = addrs;
140 };
141 
142 ZmAppt.prototype.getForwardAddress =
143 function() {
144 	return this._fwdAddrs;
145 };
146 
147 // Public methods
148 
149 ZmAppt.loadOfflineData =
150 function(apptInfo, list) {
151     var appt = new ZmAppt(list);
152     var recurrence;
153     var alarmActions;
154     var subObjects = {_recurrence:ZmRecurrence, alarmActions:AjxVector};
155     for (var prop in apptInfo) {
156         // PROBLEM: The indexeddb serialization/deserialization does not recreate the actual objects - for example,
157         // a AjxVector is recreated as an object containing an array.  We really want a more generalized means, but
158         // for the moment do custom deseralization here.   Also, assuming only one sublevel of custom objects
159         if (subObjects[prop]) {
160             var obj = new subObjects[prop]();
161             for (var rprop in apptInfo[prop]) {
162                 obj[rprop] = apptInfo[prop][rprop];
163             }
164             appt[prop] = obj;
165         } else {
166             appt[prop] = apptInfo[prop];
167         }
168     }
169 
170     return appt;
171 }
172 
173 /**
174  * Used to make our own copy because the form will modify the date object by 
175  * calling its setters instead of replacing it with a new date object.
176  * 
177  * @private
178  */
179 ZmApptClone = function() { };
180 ZmAppt.quickClone = 
181 function(appt) {
182 	ZmApptClone.prototype = appt;
183 
184 	var newAppt = new ZmApptClone();
185 	newAppt.startDate = new Date(appt.startDate.getTime());
186 	newAppt.endDate = new Date(appt.endDate.getTime());
187 	newAppt._uniqId = Dwt.getNextId();
188 
189 	newAppt.origAttendees = AjxUtil.createProxy(appt.origAttendees);
190 	newAppt.origLocations = AjxUtil.createProxy(appt.origLocations);
191 	newAppt.origEquipment = AjxUtil.createProxy(appt.origEquipment);
192 	newAppt._validAttachments = AjxUtil.createProxy(appt._validAttachments);
193 
194 	if (newAppt._orig == null) {
195 		newAppt._orig = appt;
196 	}
197 	newAppt.type = ZmItem.APPT;
198 	newAppt.rsvp = appt.rsvp;
199 
200 	newAppt.freeBusy = appt.freeBusy;
201     if (appt.isRecurring()) {
202         newAppt._recurrence = appt.getRecurrence();
203     }
204 
205     return newAppt;
206 };
207 
208 ZmAppt.createFromDom =
209 function(apptNode, args, instNode, noCache) {
210 	var appt = new ZmAppt(args.list);
211 	appt._loadFromDom(apptNode, (instNode || {}));
212     if (appt.id && !noCache) {
213         appCtxt.cacheSet(appt.id, appt);
214     }
215 	return appt;
216 };
217 
218 /**
219  * Gets the folder.
220  * 
221  * @return	{ZmFolder}		the folder
222  */
223 ZmAppt.prototype.getFolder =
224 function() {
225 	return appCtxt.getById(this.folderId);
226 };
227 
228 /**
229  * Gets the tool tip. If it needs to make a server call, returns a callback instead.
230  *
231  * @param {ZmController}	controller	the controller
232  * 
233  * @return	{Hash|String}	the callback {Hash} or tool tip
234  */
235 ZmAppt.prototype.getToolTip =
236 function(controller) {
237 	var appt = this.apptClone || this._orig || this;
238 	var needDetails = (!appt._toolTip || (appt.otherAttendees && !appt.ptstHashMap));
239 	if (needDetails) {
240         return {callback:appt._getToolTip.bind(appt, controller), loading:false};
241 	} else {
242 		return appt._toolTip || appt._getToolTip(controller);
243 	}
244 };
245 
246 ZmAppt.prototype._getToolTip =
247 function(controller, callback) {
248 
249 	// getDetails() of original appt will reset the start date/time and will break the ui layout
250 	this.apptClone = ZmAppt.quickClone(this);
251 	var respCallback = this._handleResponseGetToolTip.bind(this.apptClone, controller, callback); //run the callback on the clone - otherwise we lost data such as freeBusy
252 	this.apptClone.getDetails(null, respCallback);
253 };
254 
255 ZmAppt.prototype._handleResponseGetToolTip =
256 function(controller, callback) {
257 	var organizer = this.getOrganizer();
258 	var sentBy = this.getSentBy();
259 	var userName = appCtxt.get(ZmSetting.USERNAME);
260 	if (sentBy || (organizer && organizer != userName)) {
261 		organizer = (this.message && this.message.invite && this.message.invite.getOrganizerName()) || organizer;
262 		if (sentBy) {
263 			var contactsApp = appCtxt.getApp(ZmApp.CONTACTS);
264 			var contact = contactsApp && contactsApp.getContactByEmail(sentBy);
265 			sentBy = (contact && contact.getFullName()) || sentBy;
266 		}
267 	} else {
268 		organizer = null;
269 		sentBy = null;
270 	}
271 
272 	var params = {
273 		appt: this,
274 		cal: (this.folderId != ZmOrganizer.ID_CALENDAR && controller) ? controller.getCalendar() : null,
275 		organizer: organizer,
276 		sentBy: sentBy,
277 		when: this.getDurationText(false, false),
278 		location: this.getLocation(),
279 		width: "250",
280 		hideAttendees: true
281 	};
282 
283 	this.updateParticipantStatus();
284 	if (this.ptstHashMap != null) {
285 		var ptstStatus = {};
286 		var statusAttendees;
287 		var hideAttendees = true;
288 
289 		statusAttendees = ptstStatus[ZmMsg.ptstAccept] = this._getPtstStatus(ZmCalBaseItem.PSTATUS_ACCEPT);
290 		hideAttendees = hideAttendees && !statusAttendees.count;
291 
292 		statusAttendees = ptstStatus[ZmMsg.ptstDeclined] = this._getPtstStatus(ZmCalBaseItem.PSTATUS_DECLINED);
293 		hideAttendees = hideAttendees && !statusAttendees.count;
294 
295 		statusAttendees = ptstStatus[ZmMsg.ptstTentative] = this._getPtstStatus(ZmCalBaseItem.PSTATUS_TENTATIVE);
296 		hideAttendees = hideAttendees && !statusAttendees.count;
297 
298 		statusAttendees = ptstStatus[ZmMsg.ptstNeedsAction] = this._getPtstStatus(ZmCalBaseItem.PSTATUS_NEEDS_ACTION);
299 		hideAttendees = hideAttendees && !statusAttendees.count;
300 		params.hideAttendees = hideAttendees;
301 		params.ptstStatus = ptstStatus;
302 
303 		var attendees = [];
304 		if (!this.rsvp) {
305 			var personAttendees = this._attendees[ZmCalBaseItem.PERSON];
306 			for (var i = 0; i < personAttendees.length; i++) {
307 				var attendee = personAttendees[i];
308 				attendees.push(attendee.getAttendeeText(null, true));
309 			}
310 			params.attendeesText = this.getAttendeeToolTipString(attendees);
311 		}
312 	}
313 
314 	var toolTip = this._toolTip = AjxTemplate.expand("calendar.Appointment#Tooltip", params);
315 	if (callback) {
316 		callback.run(toolTip);
317 	} else {
318 		return toolTip;
319 	}
320 };
321 
322 ZmAppt.prototype._getPtstStatus =
323 function(ptstHashKey) {
324 	var ptstString = this.ptstHashMap[ptstHashKey];
325 
326 	return {
327 		count: ptstString ? ptstString.length : 0,
328 		attendees: this.getAttendeeToolTipString(ptstString)
329 	};
330 };
331 
332 ZmAppt.prototype.getAttendeeToolTipString =
333 function(val) {
334 	var str;
335 	var maxLimit = 10;
336 	if (val && val.length > maxLimit) {
337 		var origLength = val.length;
338 		var newParts = val.splice(0, maxLimit);
339 		str = newParts.join(",") + " ("+ (origLength - maxLimit) +" " +  ZmMsg.more + ")" ;
340 	} else if (val) {
341 		str = val.join(",");
342 	}
343 	return str;
344 };
345 
346 /**
347  * Gets the summary for proposed time
348  *
349  * @param	{Boolean}	isHtml		if <code>true</code>, format as html
350  * @return	{String}	the summary
351  */
352 ZmAppt.prototype.getProposedTimeSummary =
353 function(isHtml) {
354 	var orig = this._orig || this;
355 
356 	var buf = [];
357 	var i = 0;
358 
359 	if (!this._summaryHtmlLineFormatter) {
360 		this._summaryHtmlLineFormatter = new AjxMessageFormat("<tr><th align='left'>{0}</th><td>{1} {2}</td></tr>");
361 		this._summaryTextLineFormatter = new AjxMessageFormat("{0} {1} {2}");
362 	}
363 
364 	var formatter = isHtml ? this._summaryHtmlLineFormatter : this._summaryTextLineFormatter;
365 
366 	if (isHtml) {
367 		buf[i++] = "<p>\n<table border='0'>\n";
368 	}
369 
370 	var params = [ZmMsg.subjectLabel, this.name, ""];
371 
372 	buf[i++] = formatter.format(params);
373 	buf[i++] = "\n";
374 
375 	if (isHtml) {
376 		buf[i++] = "</table>";
377 	}
378 	buf[i++] = "\n";
379 	if (isHtml) {
380 		buf[i++] = "<p>\n<table border='0'>\n";
381 	}
382 
383 	i = this.getApptTimeSummary(buf, i, isHtml, true);
384 
385 	if (isHtml) {
386 		buf[i++] = "</table>\n";
387 	}
388 	buf[i++] = isHtml ? "<div>" : "\n\n";
389 	buf[i++] = ZmItem.NOTES_SEPARATOR;
390 
391 	// bug fix #7835 - add <br> after DIV otherwise Outlook lops off 1st char
392 	buf[i++] = isHtml ? "</div><br>" : "\n\n";
393 
394 	return buf.join("");
395 };
396 
397 /**
398  * Gets the summary.
399  *
400  * @param	{Boolean}	isHtml		if <code>true</code>, format as html
401  * @return	{String}	the summary
402  */
403 ZmAppt.prototype.getSummary =
404 function(isHtml) {
405 
406 	if (this.isProposeTimeMode) {
407 		return this.getProposedTimeSummary(isHtml);
408 	}
409 
410 	var orig = this._orig || this;
411 
412 	var isEdit = !this.inviteNeverSent && (this.viewMode == ZmCalItem.MODE_EDIT ||
413 				  this.viewMode == ZmCalItem.MODE_EDIT_SINGLE_INSTANCE ||
414 				  this.viewMode == ZmCalItem.MODE_EDIT_SERIES);
415 
416 	var buf = [];
417 	var i = 0;
418 
419 	if (!this._summaryHtmlLineFormatter) {
420 		this._summaryHtmlLineFormatter = new AjxMessageFormat("<tr><th align='left'>{0}</th><td>{1} {2}</td></tr>");
421 		this._summaryTextLineFormatter = new AjxMessageFormat("{0} {1} {2}");
422 	}
423 	var formatter = isHtml ? this._summaryHtmlLineFormatter : this._summaryTextLineFormatter;
424 
425 	if (isHtml) {
426 		buf[i++] = "<p>\n<table border='0'>\n";
427 	}
428 	var modified = isEdit && (orig.getName() != this.getName());
429 	var params = [ ZmMsg.subjectLabel, AjxStringUtil.htmlEncode(this.name), modified ? ZmMsg.apptModifiedStamp : "" ];
430 	buf[i++] = formatter.format(params);
431 	buf[i++] = "\n";
432 
433 	var userName = appCtxt.get(ZmSetting.USERNAME), displayName;
434 	var mailFromAddress = this.getMailFromAddress();
435 	if (mailFromAddress) {
436 		userName = mailFromAddress;
437 	} else if(this.identity) {
438 		userName = this.identity.sendFromAddress;
439 		displayName = this.identity.sendFromDisplay;
440 	}
441 	var organizer = this.organizer ? this.organizer : userName;
442 	var orgEmail = (!this.organizer && displayName)
443 		? (new AjxEmailAddress(userName, null, displayName)).toString()
444 		: ZmApptViewHelper.getAddressEmail(organizer).toString();
445 	var orgText = isHtml ? AjxStringUtil.htmlEncode(orgEmail) : orgEmail;
446 	var params = [ ZmMsg.organizer + ":", orgText, "" ];
447 	buf[i++] = formatter.format(params);
448 	buf[i++] = "\n";
449 	if (this.getFolder().isRemote() && this.identity) {
450 		var identity = this.identity;
451 		orgEmail = new AjxEmailAddress(identity.sendFromAddress , null, identity.sendFromDisplay);
452 		orgEmail = orgEmail.toString();
453 		orgText = isHtml ? AjxStringUtil.htmlEncode(orgEmail) : orgEmail;
454 		buf[i++] = formatter.format([ZmMsg.sentBy+":", orgText, ""]);
455 		buf[i++] = "\n";
456 	}
457 	if (isHtml) {
458 		buf[i++] = "</table>";
459 	}
460 	buf[i++] = "\n";
461 	if (isHtml) {
462 		buf[i++] = "<p>\n<table border='0'>\n";
463 	}
464 
465 	var locationLabel = this.getLocation();
466 	var locationText = isHtml ? AjxStringUtil.htmlEncode(locationLabel) : locationLabel;
467 	var origLocationLabel = orig ? orig.getLocation() : "";
468 	var emptyLocation = (locationLabel == origLocationLabel && origLocationLabel == "");
469 	if (!emptyLocation || this.isForwardMode) {
470 		params = [ZmMsg.locationLabel, locationText, (isEdit && locationLabel != origLocationLabel && !this.isForwardMode ) ? ZmMsg.apptModifiedStamp : ""];
471 		buf[i++] = formatter.format(params);
472 		buf[i++] = "\n";
473 	}
474 
475 	var location = this.getAttendeesText(ZmCalBaseItem.LOCATION, true);
476 	if (location) {
477 		var origLocationText = ZmApptViewHelper.getAttendeesString(this.origLocations, ZmCalBaseItem.LOCATION, true);
478 		modified = (isEdit && (origLocationText != location));
479 		var resourcesText = isHtml ? AjxStringUtil.htmlEncode(location) : location;
480 		params = [ZmMsg.resourcesLabel, resourcesText, modified ? ZmMsg.apptModifiedStamp : ""];
481 		buf[i++] = formatter.format(params);
482 		buf[i++] = "\n";
483 	}
484 
485 	var equipment = this.getAttendeesText(ZmCalBaseItem.EQUIPMENT, true);
486 	if (equipment) {
487 		var origEquipmentText = ZmApptViewHelper.getAttendeesString(this.origEquipment, ZmCalBaseItem.EQUIPMENT, true);
488 		modified = (isEdit && (origEquipmentText != equipment));
489 		var equipmentText = isHtml ? AjxStringUtil.htmlEncode(equipment) : equipment;
490 		params = [ZmMsg.resourcesLabel, equipmentText, modified ? ZmMsg.apptModifiedStamp : "" ];
491 		buf[i++] = formatter.format(params);
492 		buf[i++] = "\n";
493 	}
494 
495 	i = this.getApptTimeSummary(buf, i, isHtml, isEdit);
496 	i = this.getRecurrenceSummary(buf, i, isHtml, isEdit);
497 
498 	if (this._attendees[ZmCalBaseItem.PERSON].length) {
499 		if (isHtml) {
500 			buf[i++] = "</table>\n<p>\n<table border='0'>";
501 		}
502 		buf[i++] = "\n";
503 		var reqAttString = ZmApptViewHelper.getAttendeesByRole(this._attendees[ZmCalBaseItem.PERSON], ZmCalBaseItem.PERSON, ZmCalItem.ROLE_REQUIRED, 10);
504 		var optAttString = ZmApptViewHelper.getAttendeesByRole(this._attendees[ZmCalBaseItem.PERSON], ZmCalBaseItem.PERSON, ZmCalItem.ROLE_OPTIONAL, 10);
505 		var reqAttText = isHtml ? AjxStringUtil.htmlEncode(reqAttString) : reqAttString;
506 		var optAttText = isHtml ? AjxStringUtil.htmlEncode(optAttString) : optAttString;
507 
508 		var attendeeTitle = (optAttString == "") ? ZmMsg.invitees : ZmMsg.requiredInvitees ;
509 		params = [ attendeeTitle + ":", reqAttText, "" ];
510 		buf[i++] = formatter.format(params);
511 		buf[i++] = "\n";
512 
513 		params = [ ZmMsg.optionalInvitees + ":", optAttText, "" ];
514 		if (optAttString != "") {
515 			buf[i++] = formatter.format(params);
516 		}
517 
518 	}
519 	if (isHtml) {
520 		buf[i++] = "</table>\n";
521 	}
522 	buf[i++] = isHtml ? "<div>" : "\n\n";
523 	buf[i++] = ZmItem.NOTES_SEPARATOR;
524 	// bug fix #7835 - add <br> after DIV otherwise Outlook lops off 1st char
525 	buf[i++] = isHtml ? "</div><br>" : "\n\n";
526 
527 	return buf.join("");
528 };
529 
530 /**
531  * Checks whether appointment needs a recurrence info in summary
532  *
533  * @return	{Boolean}	returns whether appointment needs recurrence summary
534  */
535 ZmAppt.prototype.needsRecurrenceSummary =
536 function() {
537 	return this._recurrence.repeatType != "NON" &&
538 			this.viewMode != ZmCalItem.MODE_EDIT_SINGLE_INSTANCE &&
539 			this.viewMode != ZmCalItem.MODE_DELETE_INSTANCE;
540 };
541 
542 /**
543  * Returns an object with layout coordinates for this appointment.
544  */
545 ZmAppt.prototype.getLayoutInfo =
546 function() {
547 	return this._layout;
548 };
549 
550 /**
551  * Gets the appointment time summary.
552  *
553  * @param	{Array}	    buf		    buffer array to fill summary content
554  * @param	{Integer}	i		    buffer array index to start filling
555  * @param	{Boolean}	isHtml		if <code>true</code>, format as html
556  * @param	{Boolean}	isEdit		if view mode is edit/edit instance/edit series
557  * @return	{String}	the appointment time summary
558  */
559 ZmAppt.prototype.getApptTimeSummary =
560 function(buf, i, isHtml, isEdit) {
561 	var formatter = isHtml ? this._summaryHtmlLineFormatter : this._summaryTextLineFormatter;
562 	var orig = this._orig || this;
563 	var s = this.startDate;
564 	var e = this.endDate;
565 
566 	if (this.viewMode == ZmCalItem.MODE_DELETE_INSTANCE) {
567 		s = this.getUniqueStartDate();
568 		e = this.getUniqueEndDate();
569 	}
570 
571 	if (this.needsRecurrenceSummary())
572 	{
573 		var hasTime = isEdit
574 			? ((orig.startDate.getTime() != s.getTime()) || (orig.endDate.getTime() != e.getTime()))
575 			: false;
576 		params = [ ZmMsg.time + ":", this._getTextSummaryTime(isEdit, ZmMsg.time, null, s, e, hasTime), "" ];
577 		buf[i++] = formatter.format(params);
578 	}
579 	else if (s.getFullYear() == e.getFullYear() &&
580 			 s.getMonth() == e.getMonth() &&
581 			 s.getDate() == e.getDate())
582 	{
583 		var hasTime = isEdit
584 			? ((orig.startDate.getTime() != this.startDate.getTime()) || (orig.endDate.getTime() != this.endDate.getTime()))
585 			: false;
586 		params = [ ZmMsg.time + ":", this._getTextSummaryTime(isEdit, ZmMsg.time, s, s, e, hasTime), "" ];
587 		buf[i++] = formatter.format(params);
588 	}
589 	else
590 	{
591 		var hasTime = isEdit ? (orig.startDate.getTime() != this.startDate.getTime()) : false;
592 		params = [ ZmMsg.startLabel, this._getTextSummaryTime(isEdit, ZmMsg.start, s, s, null, hasTime), "" ];
593 		buf[i++] = formatter.format(params);
594 
595 		hasTime = isEdit ? (orig.endDate.getTime() != this.endDate.getTime()) : false;
596 		params = [ ZmMsg.endLabel, this._getTextSummaryTime(isEdit, ZmMsg.end, e, null, e, hasTime), "" ];
597 		buf[i++] = formatter.format(params);
598 	}
599 
600 	return i;
601 };
602 
603 /**
604  * Gets the recurrence summary.
605  *
606  * @param	{Array}	    buf		    buffer array to fill summary content
607  * @param	{Integer}	i		    buffer array index to start filling
608  * @param	{Boolean}	isHtml		if <code>true</code>, format as html
609  * @param	{Boolean}	isEdit		if view mode is edit/edit instance/edit series
610  * @return	{String}	the recurrence summary
611  */
612 ZmAppt.prototype.getRecurrenceSummary =
613 function(buf, i, isHtml, isEdit) {
614 	var formatter = isHtml ? this._summaryHtmlLineFormatter : this._summaryTextLineFormatter;
615 	var orig = this._orig || this;
616 
617 	if (this.needsRecurrenceSummary()) {
618 		var modified = false;
619 		if (isEdit) {
620 			modified = orig._recurrence.repeatType != this._recurrence.repeatType ||
621 					orig._recurrence.repeatCustom != this._recurrence.repeatCustom ||
622 					orig._recurrence.repeatCustomType != this._recurrence.repeatCustomType ||
623 					orig._recurrence.repeatCustomCount != this._recurrence.repeatCustomCount ||
624 					orig._recurrence.repeatCustomOrdinal != this._recurrence.repeatCustomOrdinal ||
625 					orig._recurrence.repeatCustomDayOfWeek != this._recurrence.repeatCustomDayOfWeek ||
626 					orig._recurrence.repeatCustomMonthDay != this._recurrence.repeatCustomMonthDay ||
627 					orig._recurrence.repeatEnd != this._recurrence.repeatEnd ||
628 					orig._recurrence.repeatEndType != this._recurrence.repeatEndType ||
629 					orig._recurrence.repeatEndCount != this._recurrence.repeatEndCount ||
630 					orig._recurrence.repeatEndDate != this._recurrence.repeatEndDate ||
631 					orig._recurrence.repeatWeeklyDays != this._recurrence.repeatWeeklyDays ||
632 					orig._recurrence.repeatMonthlyDayList != this._recurrence.repeatMonthlyDayList ||
633 					orig._recurrence.repeatYearlyMonthsList != this._recurrence.repeatYearlyMonthsList;
634 		}
635 		params = [ ZmMsg.recurrence, ":", this._recurrence.getBlurb(), modified ? ZmMsg.apptModifiedStamp : "" ];
636 		buf[i++] = formatter.format(params);
637 		buf[i++] = "\n";
638 	}
639 	return i;
640 };
641 
642 /**
643 * Sets the attendees (person, location, or equipment) for this appt.
644 *
645 * @param {Array}	list	the list of email {String}, {@link AjxEmailAddress}, {@link ZmContact}, or {@link ZmResource}
646 * @param	{constant}	type		the type
647 */
648 ZmAppt.prototype.setAttendees =
649 function(list, type) {
650 	this._attendees[type] = [];
651 	list = (list instanceof Array) ? list : [list];
652 	for (var i = 0; i < list.length; i++) {
653 		var attendee = ZmApptViewHelper.getAttendeeFromItem(list[i], type);
654 		if (attendee) {
655 			this._attendees[type].push(attendee);
656 		}
657 	}
658 };
659 
660 ZmAppt.prototype.setFromMailMessage =
661 function(message, subject) {
662 	ZmCalItem.prototype.setFromMailMessage.call(this, message, subject);
663 
664 	// Only unique names in the attendee list, plus omit our own name
665 	var account = appCtxt.multiAccounts ? message.getAccount() : null;
666 	var used = {};
667 	used[appCtxt.get(ZmSetting.USERNAME, null, account)] = true;
668 	var addrs = message.getAddresses(AjxEmailAddress.FROM, used, true);
669 	addrs.addList(message.getAddresses(AjxEmailAddress.CC, used, true));
670 	addrs.addList(message.getAddresses(AjxEmailAddress.TO, used, true));
671 	this._attendees[ZmCalBaseItem.PERSON] = addrs.getArray();
672 };
673 
674 
675 ZmAppt.prototype.setFromMailMessageInvite =
676 function(message) {
677 	var invite = message.invite;
678 	var viewMode = (!invite.isRecurring()) ? ZmCalItem.MODE_FORWARD : ZmCalItem.MODE_FORWARD_SERIES;
679 
680 	if (invite.isRecurring() && invite.isException()) {
681 		viewMode = ZmCalItem.MODE_FORWARD_SINGLE_INSTANCE;
682 	}
683 
684 	this.setFromMessage(message, viewMode);
685 	this.name = message.subject;
686 	this.location = message.invite.getLocation();
687 	this.allDayEvent = message.invite.isAllDayEvent();
688 	if (message.apptId) {
689 		this.invId = message.apptId;
690 	}
691 
692 	this.uid = message.invite.components ? message.invite.components[0].uid : null;
693 
694 	if (this.isForwardMode) {
695 		this.forwardInviteMsgId = message.id;
696 		if (!invite.isOrganizer()) {
697 			this.name = ZmMsg.fwd + ": " + message.subject;
698 		}
699 		this.status = invite.components ? invite.components[0].status : ZmCalendarApp.STATUS_CONF;
700 	}
701 
702 	if (this.isProposeTimeMode) {
703 		this.proposeInviteMsgId = message.id;
704 		//bug: 49315 - use local timezone while proposing time
705 		this.convertToLocalTimezone();
706 		if (!this.ridZ) {
707 			this.ridZ = message.invite.components ? message.invite.components[0].ridZ : null;
708 		}
709 		this.seq = message.invite.getSequenceNo();
710 	}
711 };
712 
713 /**
714  * Checks if the appointment is private.
715  * 
716  * @return	{Boolean}	<code>true</code> if the appointment is private
717  */
718 ZmAppt.prototype.isPrivate =
719 function() {
720 	return (this.privacy != "PUB");
721 };
722 
723 ZmAppt.prototype.setPrivacy =
724 function(privacy) {
725 	this.privacy = privacy;
726 };
727 
728 // Private / Protected methods
729 
730 ZmAppt.prototype._setExtrasFromMessage =
731 function(message) {
732     ZmCalItem.prototype._setExtrasFromMessage.apply(this, arguments);
733 
734 	this.freeBusy = message.invite.getFreeBusy();
735 	this.privacy = message.invite.getPrivacy();
736 
737 	var ptstReplies = {};
738 	this._replies = message.invite.getReplies();
739 	if (this._replies) {
740 		for (var i = 0; i < this._replies.length; i++) {
741 			var name = this._replies[i].at;
742 			var ptst = this._replies[i].ptst;
743 			if (name && ptst) {
744 				ptstReplies[name] = ptst;
745 			}
746 		}
747 	}
748 
749 	// parse out attendees for this invite
750 	this._attendees[ZmCalBaseItem.PERSON] = [];
751 	this.origAttendees = [];
752 	var rsvp;
753 	var attendees = message.invite.getAttendees();
754 	if (attendees) {
755 		var ac = window.parentAppCtxt || window.appCtxt;
756 		for (var i = 0; i < attendees.length; i++) {
757 			var att = attendees[i];
758 			var addr = att.a;
759 			var name = att.d;
760 			var email = new AjxEmailAddress(addr, null, name, null, att.isGroup, att.isGroup && att.exp);
761 			ac.setIsExpandableDL(att.a, email.canExpand);
762             if (att.rsvp) {
763 				rsvp = true;
764 			}
765 			var type = att.isGroup ? ZmCalBaseItem.GROUP : ZmCalBaseItem.PERSON;
766 			var attendee = ZmApptViewHelper.getAttendeeFromItem(email, type);
767 			if (attendee) {
768 				attendee.setParticipantStatus(ptstReplies[addr] || att.ptst);
769 				attendee.setParticipantRole(att.role || ZmCalItem.ROLE_REQUIRED);
770 				this._attendees[ZmCalBaseItem.PERSON].push(attendee);
771 				this.origAttendees.push(attendee);
772 			}
773 		}
774 	}
775 
776 	// location/equpiment are known as resources now
777 	this._attendees[ZmCalBaseItem.LOCATION] = [];
778 	this.origLocations = [];
779 	this._ptstLocationMap = {};
780 
781 	this._attendees[ZmCalBaseItem.EQUIPMENT] = [];
782 	this.origEquipment = [];
783 
784 	var resources = message.invite.getResources();	// returns all the invite's resources
785 	if (resources) {
786 		for (var i = 0; i < resources.length; i++) {
787 			var addr = resources[i].a;
788 			var resourceName = resources[i].d;
789 			var ptst = resources[i].ptst;
790 			if (resourceName && ptst && (this._ptstLocationMap[resourceName] != null)) {
791 				this._ptstLocationMap[resourceName].setParticipantStatus(ptstReplies[addr] || ptst);
792 			}
793 			if (resources[i].rsvp) {
794 				rsvp = true;
795 			}
796 			// if multiple resources are present (i.e. aliases) select first one
797 			var resourceEmail = AjxEmailAddress.split(resources[i].a)[0];
798 
799 			var location = ZmApptViewHelper.getAttendeeFromItem(resourceEmail, ZmCalBaseItem.LOCATION, false, false, true);
800 			if (location || this.isLocationResource(resourceEmail, resources[i].d)) {
801                 if(!location) location = ZmApptViewHelper.getAttendeeFromItem(resourceEmail, ZmCalBaseItem.LOCATION);
802                 if(!location) continue;
803                 if(resources[i].d) location.setAttr(ZmResource.F_locationName, resources[i].d);
804 
805                 location.setParticipantStatus(ptstReplies[resourceEmail] || ptst);
806 				this._attendees[ZmCalBaseItem.LOCATION].push(location);
807 				this.origLocations.push(location);
808 			} else {
809 				var equipment = ZmApptViewHelper.getAttendeeFromItem(resourceEmail, ZmCalBaseItem.EQUIPMENT);
810 				if (equipment) {
811 					equipment.setParticipantStatus(ptstReplies[resourceEmail] || ptst);
812 					this._attendees[ZmCalBaseItem.EQUIPMENT].push(equipment);
813 					this.origEquipment.push(equipment);
814 				}
815 			}
816 		}
817 	}
818 
819 	this.rsvp = rsvp;
820 	if (message.invite.hasOtherAttendees()) {
821 		if (this._orig) {
822 			this._orig.setRsvp(rsvp);
823 		}
824 	}
825 
826     // bug 53414: For a personal appt. consider inviteNeverSent=true always.
827     // Wish this was handled by server.
828     if(!this.isDraft && !this.hasAttendees()){
829         this.inviteNeverSent = true;
830     }
831 
832     if (!this.status) {
833         this.status = message.invite.getStatus();
834     }
835 
836     if (!this.transparency) {
837         this.transparency = message.invite.getTransparency();
838     }
839 };
840 
841 ZmAppt.prototype.isLocationResource =
842 function(resourceEmail, displayName) {
843 	var locationStr = this.location;
844     var items = AjxEmailAddress.split(locationStr);
845 
846     for (var i = 0; i < items.length; i++) {
847 
848         var item = AjxStringUtil.trim(items[i]);
849         if (!item) { continue; }
850 
851         if(displayName == item) return true;
852 
853         var contact = AjxEmailAddress.parse(item);
854         if (!contact) { continue; }
855 
856         var name = contact.getName() || contact.getDispName();
857 
858         if(resourceEmail == contact.getAddress() || displayName == name) return true;
859     }
860 
861     return false;
862 };
863 
864 ZmAppt.prototype._getTextSummaryTime =
865 function(isEdit, fieldstr, extDate, start, end, hasTime) {
866 	var showingTimezone = appCtxt.get(ZmSetting.CAL_SHOW_TIMEZONE);
867 
868 	var buf = [];
869 	var i = 0;
870 
871 	if (extDate) {
872 		buf[i++] = AjxDateUtil.longComputeDateStr(extDate);
873 		buf[i++] = ", ";
874 	}
875 	if (this.isAllDayEvent()) {
876 		buf[i++] = ZmMsg.allDay;
877 	} else {
878 		var formatter = AjxDateFormat.getTimeInstance();
879 		if (start) {
880 			buf[i++] = formatter.format(start);
881 		}
882 		if (start && end) {
883 			buf[i++] = " - ";
884 		}
885 		if (end) {
886 			buf[i++] = formatter.format(end);
887 		}
888 		//if (showingTimezone) { Commented for bug 13897
889 			buf[i++] = " ";
890 			buf[i++] = AjxTimezone.getLongName(AjxTimezone.getClientId(this.timezone));
891 		//}
892 	}
893 	// NOTE: This relies on the fact that setModel creates a clone of the
894 	//		 appointment object and that the original object is saved in 
895 	//		 the clone as the _orig property.
896 	if (isEdit && ((this._orig && this._orig.isAllDayEvent() != this.isAllDayEvent()) || hasTime)) {
897 		buf[i++] = " ";
898 		buf[i++] = ZmMsg.apptModifiedStamp;
899 	}
900 	buf[i++] = "\n";
901 
902 	return buf.join("");
903 };
904 
905 ZmAppt.prototype._loadFromDom =
906 function(calItemNode, instNode) {
907 	ZmCalItem.prototype._loadFromDom.call(this, calItemNode, instNode);
908 
909 	this.privacy = this._getAttr(calItemNode, instNode, "class");
910 	this.transparency = this._getAttr(calItemNode, instNode, "transp");
911 	this.otherAttendees = this._getAttr(calItemNode, instNode, "otherAtt");
912 	this.location = this._getAttr(calItemNode, instNode, "loc");
913     this.isDraft = this._getAttr(calItemNode, instNode, "draft");
914     this.inviteNeverSent = this._getAttr(calItemNode, instNode, "neverSent") || false;
915     this.hasEx = this._getAttr(calItemNode, instNode, "hasEx") || false;
916 };
917 
918 ZmAppt.prototype._getRequestNameForMode =
919 function(mode, isException) {
920 	switch (mode) {
921 		case ZmCalItem.MODE_NEW:
922 		case ZmCalItem.MODE_NEW_FROM_QUICKADD:
923 		case ZmAppt.MODE_DRAG_OR_SASH:
924 			return "CreateAppointmentRequest";
925 
926 		case ZmCalItem.MODE_EDIT_SINGLE_INSTANCE:
927 			return !isException
928 				? "CreateAppointmentExceptionRequest"
929 				: "ModifyAppointmentRequest";
930 
931 		case ZmCalItem.MODE_EDIT:
932 		case ZmCalItem.MODE_EDIT_SERIES:
933 			return "ModifyAppointmentRequest";
934 
935 		case ZmCalItem.MODE_DELETE:
936 		case ZmCalItem.MODE_DELETE_SERIES:
937 		case ZmCalItem.MODE_DELETE_INSTANCE:
938 			return "CancelAppointmentRequest";
939 			
940 		case ZmCalItem.MODE_PURGE:
941 			return "ItemActionRequest";
942 			
943 		case ZmCalItem.MODE_FORWARD:
944 		case ZmCalItem.MODE_FORWARD_SERIES:
945 		case ZmCalItem.MODE_FORWARD_SINGLE_INSTANCE:
946 			return "ForwardAppointmentRequest";
947 
948 		case ZmCalItem.MODE_FORWARD_INVITE:
949 			return "ForwardAppointmentInviteRequest";
950 
951 		case ZmCalItem.MODE_GET:
952 			return "GetAppointmentRequest";
953 
954 		case ZmCalItem.MODE_PROPOSE_TIME:
955 			return "CounterAppointmentRequest";
956 	}
957 
958 	return null;
959 };
960 
961 ZmAppt.prototype._addExtrasToRequest =
962 function(request, comp) {
963 	ZmCalItem.prototype._addExtrasToRequest.call(this, request, comp);
964 
965     comp.fb = this.freeBusy;
966     comp['class'] = this.privacy; //using ['class'] to avoid build error as class is reserved word
967     comp.transp = this.transparency;
968     //Add Draft flag
969     var draftFlag = false;
970     if(!this.isSend && this.hasAttendees()){
971         draftFlag = this.isDraft || this.makeDraft;
972     }
973     comp.draft = draftFlag ? 1 : 0;
974 
975     if(!this.isSend && this.hasAttendees()){
976         request.echo = "1";
977     }
978 };
979 
980 ZmAppt.prototype.setRsvp =
981 function(rsvp) {
982    this.rsvp = rsvp;
983 };
984 
985 ZmAppt.prototype.shouldRsvp =
986 function() {
987 	return this.rsvp;
988 };
989 
990 ZmAppt.prototype.updateParticipantStatus =
991 function() {
992 	if (this._orig) {
993 		return this._orig.updateParticipantStatus();
994 	}
995 
996 	var ptstHashMap = {};
997 	var personAttendees = this._attendees[ZmCalBaseItem.PERSON];
998 	for (var i = 0; i < personAttendees.length; i++) {
999 		var attendee = personAttendees[i];
1000 		var ptst = attendee.getParticipantStatus() || "NE";
1001 		if (!ptstHashMap[ptst]) {
1002 			ptstHashMap[ptst] = [];
1003 		}
1004 		ptstHashMap[ptst].push(attendee.getAttendeeText(null, true));
1005 	}
1006 	this.ptstHashMap = ptstHashMap;
1007 };
1008 
1009 ZmAppt.prototype.addAttendeesToChckConflictsRequest =
1010 function(request) {
1011     var type,
1012         usr,
1013         i,
1014         attendee,
1015         address;
1016 	for (type in this._attendees) {
1017         //consider only location & equipments for conflict check
1018         if (type == ZmCalBaseItem.PERSON) {
1019             continue;
1020         }
1021 
1022 		if (this._attendees[type] && this._attendees[type].length) {
1023             usr = request.usr = [];
1024 
1025             for (i = 0; i < this._attendees[type].length; i++) {
1026 				//this._addAttendeeToSoap(soapDoc, inv, m, notifyList, this._attendees[type][i], type);
1027 				attendee = this._attendees[type][i];
1028 				address = null;
1029 				if (attendee._inviteAddress) {
1030 					address = attendee._inviteAddress;
1031 					delete attendee._inviteAddress;
1032 				} else {
1033 					address = attendee.getEmail();
1034 				}
1035 				if (!address) continue;
1036 
1037 				if (address instanceof Array) {
1038 					address = address[0];
1039 				}
1040 				usr.push({
1041                     name : address
1042                 });
1043 			}
1044 		}
1045 	}
1046 };
1047 
1048 ZmAppt.prototype.send =
1049 function(attachmentId, callback, errorCallback, notifyList){
1050     this._mode = ZmAppt.ACTION_SEND;
1051     this.isSend = true;
1052     ZmCalItem.prototype.save.call(this, attachmentId, callback, errorCallback, notifyList);
1053 };
1054 
1055 ZmAppt.prototype.save =
1056 function(attachmentId, callback, errorCallback, notifyList, makeDraft){
1057     this._mode = ZmAppt.ACTION_SAVE;
1058     this.isSend = false;
1059     this.makeDraft = AjxUtil.isUndefined(makeDraft) ? this.hasAttendees() : makeDraft;
1060     ZmCalItem.prototype.save.call(this, attachmentId, callback, errorCallback, notifyList);
1061 };
1062 
1063 ZmAppt.prototype._doCancel =
1064 function(mode, callback, msg, batchCmd, result){
1065     this._mode = ZmAppt.ACTION_SEND;
1066     this.isSend = true;
1067     ZmCalItem.prototype._doCancel.call(this, mode, callback, msg, batchCmd, result);
1068 };
1069 
1070 ZmAppt.prototype._sendCancelMsg =
1071 function(callback){
1072     this.send(null, callback);  
1073 };
1074 
1075 ZmAppt.prototype._addAttendeesToRequest =
1076 function(inv, m, notifyList, onBehalfOf, request) {
1077     var dispNamesNotifyList = this._dispNamesNotifyList = {};
1078 	for (var type in this._attendees) {
1079 		if (this._attendees[type] && this._attendees[type].length) {
1080 			for (var i = 0; i < this._attendees[type].length; i++) {
1081 				this._addAttendeeToRequest(inv, m, notifyList, this._attendees[type][i], type, request);
1082 			}
1083 		}
1084 	}
1085 
1086 	// if we have a separate list of email addresses to notify, do it here
1087 	if (this._sendNotificationMail && this.isOrganizer() && m && notifyList && this.isSend) {
1088 		for (var i = 0; i < notifyList.length; i++) {
1089 			var e,
1090                 address = notifyList[i],
1091                 dispName = dispNamesNotifyList[address];
1092 
1093             e = {
1094                 a : address,
1095                 t : AjxEmailAddress.toSoapType[AjxEmailAddress.TO]
1096             };
1097             if (dispName) {
1098                 e.p = dispName;
1099             }
1100             m.e.push(e);
1101 		}
1102 	}
1103 
1104 	if (this.isOrganizer()) {
1105 		// call base class LAST
1106 		ZmCalItem.prototype._addAttendeesToRequest.call(this, inv, m, notifyList, onBehalfOf);
1107 	}
1108     delete this._dispNamesNotifyList;
1109 };
1110 
1111 ZmAppt.prototype._addAttendeeToRequest =
1112 function(inv, m, notifyList, attendee, type, request) {
1113 	var address;
1114 	if (attendee._inviteAddress) {
1115 		address = attendee._inviteAddress;
1116 		delete attendee._inviteAddress;
1117 	} else {
1118 		address = attendee.getLookupEmail() || attendee.getEmail();
1119 	}
1120 	if (!address) return;
1121 
1122 	var dispName = attendee.getFullName();
1123 	if (inv) {
1124 		var at = {};
1125 		// for now make attendees optional, until UI has a way of setting this
1126 		var role = ZmCalItem.ROLE_NON_PARTICIPANT;
1127 		if (type == ZmCalBaseItem.PERSON) {
1128 			role = attendee.getParticipantRole() ? attendee.getParticipantRole() : ZmCalItem.ROLE_REQUIRED;
1129 		}
1130 		at.role = role;
1131 		var ptst = attendee.getParticipantStatus();
1132 		if (!ptst || type === ZmCalBaseItem.PERSON && this.dndUpdate) {  //Bug 56639 - special case for drag-n-drop since the ptst was not updated correctly as we didn't have the informations about attendees and changes.
1133 			ptst = ZmCalBaseItem.PSTATUS_NEEDS_ACTION
1134 		}
1135 		if (notifyList) {
1136 			var attendeeFound = false;
1137 			for (var i = 0; i < notifyList.length; i++) {
1138 				if (address == notifyList[i]) {
1139 					attendeeFound = true;
1140 					break;
1141 				}
1142 			}
1143 			ptst = attendeeFound
1144 				? ZmCalBaseItem.PSTATUS_NEEDS_ACTION
1145 				: (attendee.getParticipantStatus() || ZmCalBaseItem.PSTATUS_NEEDS_ACTION);
1146             if(attendeeFound && dispName) {
1147                 // If attendees is found in notify list and has display name,
1148                 // add it to object for future reference
1149                 this._dispNamesNotifyList[address] = dispName;
1150             }
1151 		}
1152 		at.ptst = ptst;
1153 
1154 		var rsvpVal = this.rsvp ? "1" : "0";
1155 		if (type != ZmCalBaseItem.PERSON) {
1156 			at.cutype = ZmCalendarApp.CUTYPE_RESOURCE;
1157 			if(this.isOrganizer()) {
1158 				rsvpVal = "1";
1159 			}
1160 		}
1161 		if (this._cancelFutureInstances) {
1162 			rsvpVal = "0";
1163 		}
1164 		at.rsvp = rsvpVal;
1165 
1166 		if (address instanceof Array) {
1167 			address = address[0];
1168 		}
1169 		at.a = address;
1170 
1171 		if (dispName) {
1172 			at.d = dispName;
1173 		}
1174         inv.at.push(at);
1175 	}
1176 
1177 	// set email to notify if notifyList not provided
1178 	if (this._sendNotificationMail && this.isOrganizer() && m && !notifyList && !this.__newFolderId && this.isSend) {
1179         var e = {};
1180         e.a = address;
1181 		if (dispName) {
1182             e.p = dispName;
1183 		}
1184         e.t = AjxEmailAddress.toSoapType[AjxEmailAddress.TO];
1185         m.e.push(e);
1186 	}
1187 };
1188 
1189 ZmAppt.prototype.replaceAttendee =
1190 function(oldAttendee,newAttendee){
1191    var attendees = this._attendees[ZmCalBaseItem.PERSON];
1192    if(attendees && attendees.length){
1193     for(var a=0;a<attendees.length;a++){
1194         if(attendees[a].getEmail()==oldAttendee){
1195             attendees[a]=this._createAttendeeFromMail(newAttendee);
1196             break;
1197         }
1198     }
1199    }
1200    this._attendees[ZmCalBaseItem.PERSON]=attendees;
1201 }
1202 
1203 ZmAppt.prototype._createAttendeeFromMail=
1204 function(mailId){
1205     var attendee=new ZmContact(null);
1206     attendee.initFromEmail(mailId);
1207     return attendee;
1208 }
1209 
1210 ZmAppt.prototype._getInviteFromError =
1211 function(result) {
1212 	return (result._data.GetAppointmentResponse.appt[0].inv[0]);
1213 };
1214 
1215 
1216 ZmAppt.prototype.forwardInvite =
1217 function(callback, errorCallback, mode) {
1218     var jsonObj = {},
1219         requestName = this._getRequestNameForMode(ZmCalItem.MODE_FORWARD_INVITE, this.isException),
1220         request = jsonObj[requestName] = {
1221             _jsns : "urn:zimbraMail"
1222         },
1223         m = request.m = {},
1224         accountName = this.getRemoteFolderOwner(),
1225         mailFromAddress = this.getMailFromAddress(),
1226         e = m.e = [],
1227         addrs = this._fwdAddrs,
1228         attendee,
1229         address,
1230         name,
1231         i;
1232 
1233 	if (this.forwardInviteMsgId) {
1234         request.id = this.forwardInviteMsgId;
1235 	}
1236 
1237 	m.su = this.name;
1238 	this.isForwardMode = true;
1239 	this._addNotesToRequest(m);
1240 
1241 	if (this.isOrganizer() && !accountName && mailFromAddress) {
1242         e.push({
1243             a : mailFromAddress,
1244 		    t : AjxEmailAddress.toSoapType[AjxEmailAddress.FROM]
1245         });
1246 	}
1247 
1248 	for (i = 0; i < addrs.length; i++) {
1249 		attendee = addrs[i];
1250         name = "";
1251 
1252 		if (attendee._inviteAddress) {
1253 			address = attendee._inviteAddress;
1254 			delete attendee._inviteAddress;
1255 		}
1256         else if (attendee.isAjxEmailAddress) {
1257 			address = attendee.address;
1258 			name = attendee.dispName || attendee.name
1259 		}
1260         else if (attendee instanceof ZmContact) {
1261 			address = attendee.getEmail();
1262 			name = attendee.getFullName();
1263 		}
1264 		if (!address) {
1265             continue;
1266         }
1267 		if (address instanceof Array) {
1268 			address = address[0];
1269 		}
1270 
1271 		this._addAddressToRequest(m, address, AjxEmailAddress.toSoapType[AjxEmailAddress.TO], name);
1272 	}
1273 
1274 	this._sendRequest(null, accountName, callback, errorCallback, jsonObj, requestName);
1275 };
1276 
1277 ZmAppt.prototype.setForwardMode =
1278 function(forwardMode) {
1279 	this.isForwardMode = forwardMode;
1280 };
1281 
1282 ZmAppt.prototype.setProposeTimeMode =
1283 function(isProposeTimeMode) {
1284 	this.isProposeTimeMode = isProposeTimeMode;
1285 };
1286 
1287 ZmAppt.prototype.sendCounterAppointmentRequest =
1288 function(callback, errorCallback, viewMode) {
1289 	var mode = ZmCalItem.MODE_PROPOSE_TIME,
1290         jsonObj = {},
1291         requestName = this._getRequestNameForMode(mode, this.isException),
1292         request = jsonObj[requestName] = {
1293             _jsns : "urn:zimbraMail"
1294         },
1295         m = request.m = {},
1296         e = m.e = [],
1297         inv = m.inv = {},
1298         comps = inv.comp = [],
1299         comp = inv.comp[0] = {},
1300         calendar = this.getFolder(),
1301     	acct = calendar.getAccount(),
1302     	accountName = this.getRemoteFolderOwner(),
1303     	localAcctName = this.getFolder().getAccount().name,
1304         cif = this._currentlyLoaded && this._currentlyLoaded.cif,
1305     	isOnBehalfOf = accountName && localAcctName && localAcctName != accountName,
1306     	mailFromAddress = this.getMailFromAddress(),
1307         orgEmail,
1308         orgAddress,
1309         exceptId,
1310         me,
1311         organizer,
1312         user,
1313         org,
1314         orgName;
1315 
1316     this._addInviteAndCompNum(request);
1317     m.su = ZmMsg.subjectNewTime + ": " + this.name;
1318     if (this.isOrganizer() && !accountName && mailFromAddress) {
1319 		e.push({
1320             a : mailFromAddress,
1321 		    t : AjxEmailAddress.toSoapType[AjxEmailAddress.FROM]
1322         });
1323 	} else if (isOnBehalfOf || cif) {
1324         e.push({
1325             a : isOnBehalfOf ? accountName: cif,
1326             t : AjxEmailAddress.toSoapType[AjxEmailAddress.FROM]
1327         });
1328         e.push({
1329             a : localAcctName,
1330             t : AjxEmailAddress.toSoapType[AjxEmailAddress.SENDER]
1331         });
1332     }
1333 
1334 	if(this.organizer) {
1335 		orgEmail = ZmApptViewHelper.getOrganizerEmail(this.organizer);
1336 		orgAddress = orgEmail.getAddress();
1337 		e.push({
1338             a : orgAddress,
1339 		    t : AjxEmailAddress.toSoapType[AjxEmailAddress.TO]
1340         });
1341 	}
1342     //Do not add exceptId if propose new time for series
1343 	if (this.ridZ && viewMode != ZmCalItem.MODE_EDIT_SERIES) {
1344 		exceptId = comp.exceptId = {};
1345 		exceptId.d = this.ridZ;
1346 	}
1347 
1348 	// subject/location
1349 	comp.name = this.name;
1350 	if (this.uid != null && this.uid != -1) {
1351 		comp.uid = this.uid;
1352 	}
1353 
1354 	if (this.seq) {
1355 		comp.seq = this.seq;
1356 	}
1357 
1358 	this._addDateTimeToRequest(request, comp);
1359 	this._addNotesToRequest(m);
1360 
1361 	// set organizer - but not for local account
1362 	if (!(appCtxt.isOffline && acct.isMain)) {
1363 		me = (appCtxt.multiAccounts) ? acct.getEmail() : appCtxt.get(ZmSetting.USERNAME);
1364 		user = mailFromAddress || me;
1365 		organizer = this.organizer || user;
1366 		org = comp.or = {};
1367 		org.a = organizer;
1368 		if (calendar.isRemote()) {
1369 			org.sentBy = user; // if on-behalf of, set sentBy
1370 		}
1371 		orgEmail = ZmApptViewHelper.getOrganizerEmail(this.organizer);
1372 		orgName = orgEmail.getName();
1373 		if (orgName) {
1374             org.d = orgName;
1375         }
1376 	}
1377 
1378 	this._sendRequest(null, accountName, callback, errorCallback, jsonObj, requestName);
1379 };
1380 
1381 ZmAppt.prototype.forward =
1382 function(callback, errorCallback) {
1383 	var mode = ZmCalItem.MODE_FORWARD,
1384 	    needsExceptionId = this.isException;
1385 
1386 	if (this.viewMode == ZmCalItem.MODE_EDIT_SINGLE_INSTANCE) {
1387 		mode = ZmCalItem.MODE_FORWARD_SINGLE_INSTANCE;
1388 		if (!this.isException) {
1389 			needsExceptionId = true;
1390 		}
1391 	} else if(this.viewMode == ZmCalItem.MODE_EDIT_SERIES) {
1392 		mode = ZmCalItem.MODE_FORWARD_SERIES;
1393 	}
1394 
1395 	if (this.forwardInviteMsgId) {
1396 		this.forwardInvite(callback, errorCallback, mode);
1397 		return;
1398 	}
1399 
1400     var jsonObj = {},
1401         requestName = this._getRequestNameForMode(mode, this.isException),
1402         request = jsonObj[requestName] = {
1403             _jsns : "urn:zimbraMail"
1404         },
1405         exceptId,
1406         message,
1407         invite,
1408         exceptIdInfo,
1409         allDay,
1410         sd,
1411         tz,
1412         timezone,
1413         m = request.m = {},
1414         accountName = this.getRemoteFolderOwner(),
1415     	mailFromAddress = this.getMailFromAddress(),
1416         e = m.e = [],
1417         addrs = this._fwdAddrs,
1418         attendee,
1419         address,
1420         name,
1421         i;
1422 
1423 	if (this.uid != null && this.uid != -1) {
1424         request.id = this.id;
1425 	}
1426 
1427 	if (needsExceptionId) {
1428 		exceptId = request.exceptId = {};
1429 		if (this.isException) {
1430 			message = this.message ? this.message : null;
1431 			invite = (message && message.invite) ? message.invite : null;
1432 			exceptIdInfo = invite.getExceptId();
1433 			exceptId.d = exceptIdInfo.d;
1434 			if (exceptIdInfo.tz) {
1435 				exceptId.tz = exceptIdInfo.tz;
1436 			}
1437 		} else {
1438 			allDay = this._orig ? this._orig.allDayEvent : this.allDayEvent;
1439 			if (allDay != "1") {
1440 				sd = AjxDateUtil.getServerDateTime(this.getOrigStartDate(), this.startsInUTC);
1441 				// bug fix #4697 (part 2)
1442 				timezone = this.getOrigTimezone();
1443 				if (!this.startsInUTC && timezone) {
1444 					exceptId.tz = timezone;
1445 				}
1446 				exceptId.d = sd;
1447 			} else {
1448 				sd = AjxDateUtil.getServerDate(this.getOrigStartDate());
1449 				exceptId.d = sd;
1450 			}
1451 		}
1452 	}
1453 
1454 	if (this.timezone) {
1455 		var clientId = AjxTimezone.getClientId(this.timezone);
1456 		ZmTimezone.set(request, clientId, null, true);
1457 		tz = this.timezone;
1458 	}
1459 
1460     m.su = this.name;
1461     this.isForwardMode = true;
1462 	this._addNotesToRequest(m);
1463 
1464 	if (this.isOrganizer() && !accountName && mailFromAddress) {
1465 		e.push({
1466             a : mailFromAddress,
1467 		    t : AjxEmailAddress.toSoapType[AjxEmailAddress.FROM]
1468         });
1469 	}
1470 
1471 	for (i = 0; i < addrs.length; i++) {
1472 		attendee = addrs[i];
1473 		if (!attendee) { continue; }
1474 
1475         name = "";
1476 		if (attendee._inviteAddress) {
1477 			address = attendee._inviteAddress;
1478 			delete attendee._inviteAddress;
1479 		} else if(attendee.isAjxEmailAddress){
1480 			address = attendee.address;
1481 			name = attendee.dispName || attendee.name
1482 		} else if(attendee instanceof ZmContact){
1483 			address = attendee.getEmail();
1484 			name = attendee.getFullName();
1485 		}
1486 		if (!address) { continue; }
1487 		if (address instanceof Array) {
1488 			address = address[0];
1489 		}
1490 		this._addAddressToRequest(m, address, AjxEmailAddress.toSoapType[AjxEmailAddress.TO], name);
1491 	}
1492 
1493 	this._sendRequest(null, accountName, callback, errorCallback, jsonObj, requestName);
1494 };
1495 
1496 ZmAppt.prototype._addAddressToRequest =
1497 function(m, addr, type, name) {
1498 	var e = {};
1499 	e.a = addr;
1500 	e.t = type;
1501 	if (name) {
1502         e.p = name;
1503 	}
1504     m.e.push(e);
1505 };
1506 
1507 ZmAppt.prototype.setProposedInvite =
1508 function(invite) {
1509 	this.proposedInvite = invite;
1510 };
1511 
1512 ZmAppt.prototype.getRecurrenceFromInvite =
1513 function(invite) {
1514 	return (invite && invite.comp && invite.comp[0]) ? invite.comp[0].recur : null;
1515 };
1516 
1517 ZmAppt.prototype.setInvIdFromProposedInvite =
1518 function(invites, proposedInvite) {
1519 	var proposalRidZ = proposedInvite.getRecurrenceId();
1520 
1521 	if (proposedInvite.components[0].ridZ) {
1522 		// search all the invites for an appointment
1523 		for (var i=0; i < invites.length; i++) {
1524 			var inv = invites[i];
1525 			if (inv.comp[0].ridZ  == proposalRidZ) {
1526 				this.invId = this.id + "-" + inv.id;
1527 				break;
1528 			}
1529 		}
1530 
1531 		// if new time is proposed for creating an exceptional instance - no
1532 		// matching invites will be found
1533 		if (!this.invId) {
1534 			this.invId = this.id + "-" + invites[0].id;
1535 			this.ridZ = proposalRidZ;
1536 			var invite = ZmInvite.createFromDom(invites);
1537 			if (invite.isRecurring()) {
1538 				this.isException = true;
1539 				this.recurring = this.getRecurrenceFromInvite(invites[0]);
1540 				this._origStartDate = proposedInvite.getStartDateFromExceptId();
1541 			}
1542 		}
1543 	} else {
1544 		this.invId = this.id + "-" + invites[0].id;
1545 	}
1546 };
1547 
1548 /**
1549  * clears the recurrence.
1550  */
1551 ZmCalItem.prototype.clearRecurrence =
1552 function() {
1553     this._recurrence = new ZmRecurrence(this);
1554     this.recurring = false;
1555 };
1556 
1557 ZmAppt.loadById = function(id, callback, errorCallback) {
1558     return ZmAppt.__load(id, null, callback, errorCallback);
1559 };
1560 ZmAppt.loadByUid = function(uid, callback, errorCallback) {
1561     return ZmAppt.__load(null, uid, callback, errorCallback);
1562 };
1563 
1564 ZmAppt.__load = function(id, uid, callback, errorCallback) {
1565     var req = { _jsns: "urn:zimbraMail", includeContent: 1 };
1566     if (id) req.id = id;
1567     else if (uid) req.uid = uid;
1568     var params = {
1569         jsonObj: { GetAppointmentRequest: req },
1570         accountName: appCtxt.multiAccounts ? appCtxt.accountList.mainAccount.name : null,
1571         asyncMode: Boolean(callback),
1572         callback: new AjxCallback(ZmAppt.__loadResponse, [callback]),
1573         errorCallback: errorCallback
1574     };
1575     var resp = appCtxt.getAppController().sendRequest(params);
1576     if (!callback) {
1577         return ZmAppt.__loadResponse(null, resp);
1578     }
1579 };
1580 ZmAppt.__loadResponse = function(callback, resp) {
1581     var data = resp && resp._data;
1582     var response = data && data.GetAppointmentResponse;
1583     var apptNode = response && response.appt;
1584     apptNode = apptNode && apptNode[0];
1585     if (!apptNode) return null;
1586 
1587     var appt = new ZmAppt();
1588     appt._loadFromDom(apptNode, {});
1589     if (apptNode.inv) {
1590         // HACK: There doesn't seem to be any direct way to load an appt
1591         // HACK: by id/uid. So I initialize the appt object with the node
1592         // HACK: in the response and then fake a message with the invite
1593         // HACK: data to initialize the rest of it.
1594         var message = {
1595             invite: new ZmInvite.createFromDom(apptNode.inv),
1596             getBodyPart: function(mimeType) {
1597                 return (mimeType == ZmMimeTable.TEXT_HTML ? apptNode.descHtml : apptNode.desc) || "";
1598             }
1599         }
1600         appt.setFromMessage(message);
1601     }
1602 
1603     if (callback) {
1604         callback.run(appt);
1605     }
1606     return appt;
1607 };
1608 
1609 /*
1610  * Checks whether there is any Daylight Savings change happens on appointment end date.
1611  */
1612 ZmAppt.prototype.checkDSTChangeOnEndDate = function(){
1613     var endDate = this.endDate;
1614     var eOffset = endDate.getTimezoneOffset();
1615     var prevDay = new Date(endDate);
1616     prevDay.setTime(endDate.getTime() - AjxDateUtil.MSEC_PER_DAY);
1617     var prevDayOffset = prevDay.getTimezoneOffset();
1618     var diffOffset = prevDayOffset - eOffset;
1619     return diffOffset;
1620 };
1621