1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 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, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @overview
 26  * This file defines a calendar appointment invite.
 27  *
 28  */
 29 
 30 /**
 31  * Creates an invite.
 32  * @class
 33  * This class represents an invite to a calendar appointment.
 34  * 
 35  * @extends	ZmModel
 36  */
 37 ZmInvite = function() {
 38 	ZmModel.call(this);
 39 };
 40 
 41 ZmInvite.prototype = new ZmModel;
 42 ZmInvite.prototype.constructor = ZmInvite;
 43 
 44 
 45 // Consts
 46 ZmInvite.CHANGES_LOCATION	= "location";
 47 ZmInvite.CHANGES_SUBJECT	= "subject";
 48 ZmInvite.CHANGES_RECURRENCE	= "recurrence";
 49 ZmInvite.CHANGES_TIME		= "time";
 50 ZmInvite.TASK		= "task";
 51 
 52 
 53 /**
 54  * Returns a string representation of the object.
 55  * 
 56  * @return		{String}		a string representation of the object
 57  */
 58 ZmInvite.prototype.toString = 
 59 function() {
 60 	return "ZmInvite: name=" + this.name + " id=" + this.id;
 61 };
 62 
 63 /**
 64  * Function that will be used to send requests. This should be set via ZmInvite.setSendFunction.
 65  * 
 66  * @private
 67  */
 68 ZmInvite._sendFun = null;
 69 
 70 // Class methods
 71 
 72 /**
 73  * Creates the invite from the DOM.
 74  * 
 75  * @param	{Object}	node	the node
 76  * @return	{ZmInvite}	the newly created invite
 77  */
 78 ZmInvite.createFromDom = 
 79 function(node) {
 80 	var invite = new ZmInvite();
 81 	invite.components = node[0].comp;
 82 	invite.replies = node[0].replies;
 83     invite.id = node[0].id;
 84 	// not sure why components are null .. but.
 85 	if (invite.components == null) {
 86 		invite.components = [{}];
 87 		invite.components.empty = true;
 88 	}
 89 	var inv = node[0];
 90 	if (inv.tz) {
 91 		for (var i = 0; i < inv.tz.length; i++) {
 92 			// get known rule
 93 			var tz = inv.tz[i];
 94 			var rule = AjxTimezone.getRule(tz.id);
 95 
 96 			// get known rule that exactly matches tz definition
 97 			if (!rule) {
 98 				var tzrule = {
 99 					standard: tz.standard ? AjxUtil.createProxy(tz.standard[0]) : {},
100 					daylight: tz.daylight ? AjxUtil.createProxy(tz.daylight[0]) : null
101 				};
102 				tzrule.standard.offset = tz.stdoff;
103 				delete tzrule.standard._object_;
104 				if (tz.daylight) {
105 					tzrule.daylight.offset = tz.dayoff;
106 					delete tzrule.daylight._object_;
107 				}
108 
109 				rule = AjxTimezone.getRule(tz.id, tzrule);
110 				if (rule) {
111 					var alias = AjxUtil.createProxy(rule);
112 					alias.aliasId = rule.clientId;
113 					alias.clientId = tz.id;
114 					alias.serverId = tz.id;
115 					AjxTimezone.addRule(alias);
116 				}
117 			}
118 
119 			// add custom rule to known list
120 			if (!rule) {
121 				rule = { clientId: tz.id, serverId: tz.id, autoDetected: true };
122 				if (tz.daylight) {
123 					rule.standard = AjxUtil.createProxy(tz.standard[0]);
124 					rule.standard.offset = tz.stdoff;
125 					rule.standard.trans = AjxTimezone.createTransitionDate(rule.standard);
126 
127 					rule.daylight = AjxUtil.createProxy(tz.daylight[0]);
128 					rule.daylight.offset = tz.dayoff;
129 					rule.daylight.trans = AjxTimezone.createTransitionDate(rule.daylight);
130 				}
131 				else {
132 					rule.standard = { offset: tz.stdoff };
133 				}
134 				AjxTimezone.addRule(rule);
135 			}
136 		}
137 	}
138 	invite.type = inv && inv.type ? inv.type : "appt";
139 	return invite;
140 };
141 
142 /**
143  * Sets the message id.
144  * 
145  * @param	{String}	id		the message id
146  */
147 ZmInvite.prototype.setMessageId = 
148 function (id) {
149 	this.msgId = id;
150 };
151 
152 /**
153  * Gets the message id.
154  * 
155  * @return	{String}	the message id
156  */
157 ZmInvite.prototype.getMessageId = 
158 function() {
159 	return this.msgId;
160 };
161 
162 /**
163  * Gets the component.
164  * 
165  * @param	{String}	id	the component id
166  * @return	{Object}	the component
167  */
168 ZmInvite.prototype.getComponent = 
169 function(id) {
170 	return this.components[id];
171 };
172 
173 /**
174  * Gets the components.
175  * 
176  * @return	{Array}	an array of components
177  */
178 ZmInvite.prototype.getComponents = 
179 function () {
180 	return this.components;
181 };
182 
183 /**
184  * Checks if the invite has multiple components.
185  * 
186  * @return	{Boolean}	<code>true</code> if the invite has one or more components
187  */
188 ZmInvite.prototype.hasMultipleComponents = 
189 function() {
190 	return (this.components.length > 1);
191 };
192 
193 /**
194  * Checks if the invite has other attendees.
195  * 
196  * @param	{int}	compNum		the component number
197  * @return	{Boolean}	<code>true</code> if the invite has more than one other attendee
198  */
199 ZmInvite.prototype.hasOtherAttendees =
200 function(compNum) {
201 	var cn = compNum || 0;
202 	return this.components[cn].at && this.components[cn].at.length > 0;
203 };
204 
205 /**
206  * Checks if the invite has other individual (non-location & resource) attendees.
207  *
208  * @param	{int}	compNum		the component number
209  * @return	{Boolean}	<code>true</code> if the invite has more than one other individual attendee
210  */
211 ZmInvite.prototype.hasOtherIndividualAttendees =
212 function(compNum) {
213     var cn  = compNum || 0;
214     var att = this.components[cn].at;
215     var otherFound = false;
216 
217     if (att && att.length) {
218         for (var i = 0; i < att.length; i++) {
219             if (!att[i].cutype || (att[i].cutype == ZmCalendarApp.CUTYPE_INDIVIDUAL)) {
220                 otherFound = true;
221                 break;
222             }
223         }
224     }
225     return otherFound;
226 };
227 
228 /**
229  * Gets the event name.
230  *  
231  * @param	{int}	compNum		the component number
232  * @return	{String}	the name or <code>null</code> for none
233  */
234 ZmInvite.prototype.getEventName = 
235 function(compNum) {
236 	var cn = compNum || 0;
237 	return this.components[cn] ? this.components[cn].name : null;
238 };
239 
240 /**
241  * Gets the alarm.
242  * 
243  * @param	{int}	compNum		the component number
244  * @return	{String}	the alarm or <code>null</code> for none
245  */
246 ZmInvite.prototype.getAlarm = 
247 function(compNum) {
248 	var cn = compNum || 0;
249 	return this.components[cn] ? this.components[cn].alarm : null;
250 };
251 
252 /**
253  * Gets the invite method.
254  * 
255  * @param	{int}	compNum		the component number
256  * @return	{String} the method or <code>null</code> for none
257  */
258 ZmInvite.prototype.getInviteMethod =
259 function(compNum) {
260 	var cn = compNum || 0;
261 	return this.components[cn] ? this.components[cn].method : null;
262 };
263 
264 /**
265  * Gets the sequence no
266  *
267  * @param	{int}	compNum		the component number
268  * @return	{String} the sequence no
269  */
270 ZmInvite.prototype.getSequenceNo =
271 function(compNum) {
272 	var cn = compNum || 0;
273 	return this.components[cn] ? this.components[cn].seq : null;
274 };
275 
276 /**
277  * Gets the organizer email.
278  * 
279  * @param	{int}	compNum		the component number
280  * @return	{String}	the organizer email or <code>null</code> for none
281  */
282 ZmInvite.prototype.getOrganizerEmail =
283 function(compNum) {
284 	var cn = compNum || 0;
285 	return (this.components[cn] && this.components[cn].or && this.components[cn].or.url)
286 		? (this.components[cn].or.url.replace("MAILTO:", "")) : null;
287 };
288 
289 /**
290  * Gets the organizer name.
291  * 
292  * @param	{int}	compNum		the component number
293  * @return	{String}	the organizer name or <code>null</code> for none
294  */
295 ZmInvite.prototype.getOrganizerName = 
296 function(compNum) {
297 	var cn = compNum || 0;
298 	return (this.components[cn] && this.components[cn].or)
299 		? (this.components[cn].or.d || this.components[cn].or.url) : null;
300 };
301 
302 /**
303  * Gets the sent by.
304  * 
305  * @param	{int}	compNum		the component number
306  * @return	{String}	the sent by or <code>null</code> for none
307  */
308 ZmInvite.prototype.getSentBy =
309 function(compNum) {
310 	var cn = compNum || 0;
311 	return (this.components[cn] && this.components[cn].or)
312 		? this.components[cn].or.sentBy : null;
313 };
314 
315 /**
316  * Checks if is organizer.
317  * 
318  * @param	{int}	compNum		the component number
319  * @return	{Boolean}	<code>true</code> if is organizer
320  */
321 ZmInvite.prototype.isOrganizer =
322 function(compNum) {
323 	var cn = compNum || 0;
324 	return this.components[cn] ? (!!this.components[cn].isOrg) : false;
325 };
326 
327 /**
328  * Gets the RSVP.
329  * 
330  * @param	{int}	compNum		the component number
331  * @return	{String}	the RSVP or <code>null</code> for none
332  */
333 ZmInvite.prototype.shouldRsvp =
334 function(compNum){
335 	var cn = compNum || 0;
336 	return this.components[cn] ? this.components[cn].rsvp : null;
337 };
338 
339 /**
340  * Gets the recurrence.
341  * 
342  * @param	{int}	compNum		the component number
343  * @return	{ZmRecurrence}	the recurrence
344  */
345 ZmInvite.prototype.getRecurrenceRules = 
346 function(compNum) {
347 	var cn = compNum || 0;
348 	return this.components[cn].recur;
349 };
350 
351 /**
352  * Gets the attendees.
353  * 
354  * @param	{int}	compNum		the component number
355  * @return	{Array}	an array of attendees or an empty array for none
356  */
357 ZmInvite.prototype.getAttendees =
358 function(compNum) {
359 	var cn = compNum || 0;
360 	var att = this.components[cn].at;
361 	var list = [];
362 
363 	if (!(att && att.length)) { return list; }
364 
365 	for (var i = 0; i < att.length; i++) {
366 		if (!att[i].cutype || (att[i].cutype == ZmCalendarApp.CUTYPE_INDIVIDUAL)) {
367 			list.push(att[i]);
368 		}
369 	}
370 	return list;
371 };
372 
373 /**
374  * Gets the replies.
375  * 
376  * @param	{int}	compNum		the component number
377  * @return	{String}	the reply
378  */
379 ZmInvite.prototype.getReplies =
380 function(compNum) {
381 	var cn = compNum || 0;
382 	return (this.replies && this.replies[cn]) ? this.replies[cn].reply : null;
383 };
384 
385 /**
386  * Gets the resources.
387  * 
388  * @param	{int}	compNum		the component number
389  * @return	{Array}	an array of resources
390  */
391 ZmInvite.prototype.getResources =
392 function(compNum) {
393 	var cn = compNum || 0;
394 	var att = this.components[cn].at;
395 	var list = [];
396 
397 	if (!(att && att.length)) { return list; }
398 
399 	for (var i = 0; i < att.length; i++) {
400 		if (att[i].cutype == ZmCalendarApp.CUTYPE_RESOURCE || 
401 		    att[i].cutype == ZmCalendarApp.CUTYPE_ROOM      ) {
402 			list.push(att[i]);
403 		}
404 	}
405 	return list;
406 };
407 
408 /**
409  * Gets the except id.
410  * 
411  * @param	{int}	compNum		the component number
412  * @return	{String}	the except id
413  */
414 ZmInvite.prototype.getExceptId =
415 function(compNum) {
416 	var cn = compNum || 0;
417 	return (this.components[cn] && this.components[cn].exceptId)
418 		? this.components[cn].exceptId[0] : null;
419 };
420 
421 /**
422  * Gets the appointment id.
423  * 
424  * @param	{int}	compNum		the component number
425  * @return {String}	the id
426  */
427 ZmInvite.prototype.getAppointmentId =
428 function(compNum) {
429 	var cn = compNum || 0;
430 	return this.components[cn].apptId;
431 };
432 
433 /**
434  * Gets the status.
435  *
436  * @param	{int}	compNum		the component number
437  * @return {String}	the status
438  */
439 ZmInvite.prototype.getStatus =
440 function(compNum) {
441 	var cn = compNum || 0;
442 	return this.components[cn].status;
443 };
444 
445 /**
446  * Gets the transparency.
447  *
448  * @param	{int}	compNum		the component number
449  * @return {String}	the transparent value
450  */
451 ZmInvite.prototype.getTransparency = 
452 function(compNum) {
453 	var cn = compNum || 0;
454 	return this.components[cn].transp;
455 };
456 
457 /**
458  * Checks if the invite is empty.
459  * 
460  * @return	{Boolean}	<code>true</code> if the invite is empty
461  */
462 ZmInvite.prototype.isEmpty =
463 function() {
464 	return Boolean(this.components.empty);
465 };
466 
467 /**
468  * Checks if the invite is an exception.
469  * 
470  * @param	{int}	compNum		the component number
471  * @return	{Boolean}	<code>true</code> if exception
472  */
473 ZmInvite.prototype.isException = 
474 function(compNum) {
475 	var cn = compNum || 0;
476 	return this.components[cn] ? this.components[cn].ex : false;
477 };
478 
479 /**
480  * Checks if the invite is recurring.
481  * 
482  * @param	{int}	compNum		the component number
483  * @return	{Boolean}	<code>true</code> if recurring
484  * @see		#getRecurrenceRules
485  */
486 ZmInvite.prototype.isRecurring =
487 function(compNum) {
488 	var cn = compNum || 0;
489 	return this.components[cn] ? this.components[cn].recur : false;
490 };
491 
492 /**
493  * Checks if the invite is an all day event.
494  * 
495  * @param	{int}	compNum		the component number
496  * @return	{Boolean}	<code>true</code> if an all day event
497  */
498 ZmInvite.prototype.isAllDayEvent = 
499 function(compNum) {
500 	var cn = compNum || 0;
501 	return this.components[cn] ? this.components[cn].allDay == "1" : false;
502 };
503 
504 /**
505  * Checks if the invite is multi-day.
506  * 
507  * @param	{int}	compNum		the component number
508  * @return	{Boolean}	<code>true</code> if the invite is multi-day
509  */
510 ZmInvite.prototype.isMultiDay =
511 function(compNum) {
512 	var cn = compNum || 0;
513 	var sd = this.getServerStartDate(cn);
514 	var ed = this.getServerEndDate(cn);
515 
516     if(!sd) return false;
517 
518 	return (sd.getDate() != ed.getDate()) || (sd.getMonth() != ed.getMonth()) || (sd.getFullYear() != ed.getFullYear());
519 };
520 
521 /**
522  * Gets the description html.
523  * 
524  * @param	{int}	compNum		the component number
525  * @return	{String}	the description html or <code>null</code> for none
526  */
527 ZmInvite.prototype.getComponentDescriptionHtml =
528 function(compNum) {
529 	var cn = compNum || 0;
530 	var comp = this.components[cn];
531 	if (comp == null) { return; }
532 
533 	var desc = comp.descHtml;
534 	var content = desc && desc[0]._content || null;
535 	if (!content) {
536 		var txtContent = comp.desc;
537         txtContent = (txtContent && txtContent[0]._content) || null;
538         content = txtContent ? AjxStringUtil.convertToHtml(txtContent) : null;
539 		if (!content) {
540 			var msg = appCtxt.getById(this.getMessageId());
541 			if (msg && msg.hasContentType) {
542 				content = msg.hasContentType(ZmMimeTable.TEXT_HTML) ? msg.getBodyContent(ZmMimeTable.TEXT_HTML) : null;
543 				if (!content) {
544 					txtContent = msg.getTextBodyPart();
545 					content = txtContent ? AjxStringUtil.convertToHtml(txtContent) : null;
546 				}
547 			}
548 		}
549 		if (!content) {
550 			content = this.getApptSummary(true);
551 		}
552 	}
553     if (!content) {
554         var comment = this.getComponentComment();
555         content = comment && AjxStringUtil.convertToHtml(comment);
556     }
557 	return content;
558 };
559 
560 /**
561  * Gets the description.
562  * 
563  * @param	{int}	compNum		the component number
564  * @return	{String}	the description or <code>null</code> for none
565  */
566 ZmInvite.prototype.getComponentDescription =
567 function(compNum) {
568 	var cn = compNum || 0;
569 	var comp = this.components[cn];
570 	if (comp == null) { return; }
571 
572 	var desc = comp.desc;
573 	var content = desc && desc[0]._content || null;
574 	if (!content) {
575 		content = this.getComponentComment();
576 	}
577 	if (!content) {
578 		var htmlContent = comp.descHtml;
579 		htmlContent = (htmlContent && htmlContent[0]._content) || null;
580 		if (!htmlContent && this.type != ZmInvite.TASK) {
581 			content = this.getApptSummary();
582 		}
583 	}
584 	return content;
585 };
586 
587 /**
588  * Gets the comment.
589  * 
590  * @param	{int}	compNum		the component number
591  * @return	{String}	the comment or <code>null</code> for none
592  */
593 ZmInvite.prototype.getComponentComment =
594 function(compNum) {
595 	var cn = compNum || 0;
596 	var comp = this.components[cn];
597 	if (comp == null) { return; }
598 
599 	var comment = comp.comment;
600 	return comment && comment[0]._content || null;
601 };
602 
603 /**
604  * Gets the server end time.
605  * 
606  * @param	{int}	compNum		the component number
607  * @return	{String}	the end time
608  */
609 ZmInvite.prototype.getServerEndTime =
610 function(compNum) {
611 	var cn = compNum || 0;
612 	if (this.components[cn] == null) { return; }
613 
614 	if (this._serverEndTime == null) {
615 		if (this.components[cn].e != null ) {
616 			this._serverEndTime = this.components[cn].e[0].d;
617 		} else if (this.components[cn].s) {
618 			// get the duration
619 			var dur	= this.components[cn].dur;
620 			var dd		= dur && dur[0].d || 0;
621 			var weeks	= dur && dur[0].w || 0;
622 			var hh		= dur && dur[0].h || 0;
623 			var mm		= dur && dur[0].m || 0;
624 			var ss		= dur && dur[0].s || 0;
625 			var t = parseInt(ss) + (parseInt(mm) * 60) + (parseInt(hh) * 3600) + (parseInt(dd) * 24 * 3600) + (parseInt(weeks) * 7 * 24 * 3600);
626 			// parse the start date
627 			var start = this.components[cn].s[0].d;
628 			var yyyy = parseInt(start.substr(0,4), 10);
629 			var MM = parseInt(start.substr(4,2), 10);
630 			var dd = parseInt(start.substr(6,2), 10);
631 			var d = new Date(yyyy, MM -1, dd);
632 			if (start.charAt(8) == 'T') {
633 				hh = parseInt(start.substr(9,2), 10);
634 				mm = parseInt(start.substr(11,2), 10);
635 				ss = parseInt(start.substr(13,2), 10);
636 				d.setHours(hh, mm, ss, 0);
637 			}
638 			// calculate the end date -- start + offset;
639 			var endDate = new Date(d.getTime() + (t * 1000));
640 
641 			// put the end date into server DURATION format.
642 			MM = AjxDateUtil._pad(d.getMonth() + 1);
643 			dd = AjxDateUtil._pad(d.getDate());
644 			hh = AjxDateUtil._pad(d.getHours());
645 			mm = AjxDateUtil._pad(d.getMinutes());
646 			ss = AjxDateUtil._pad(d.getSeconds());
647 			yyyy = d.getFullYear();
648 			this._serverEndTime = [yyyy,MM,dd,"T",hh,mm,ss].join("");
649 		}
650 	}
651 	return this._serverEndTime;
652 };
653 
654 /**
655  * Gets the server end date.
656  * 
657  * @param	{int}	compNum		the component number
658  * @return	{Date}	the end date
659  */
660 ZmInvite.prototype.getServerEndDate =
661 function(compNum, noSpecialUtcCase) {
662 	var cn = compNum || 0;
663     return AjxDateUtil.parseServerDateTime(this.getServerEndTime(cn), noSpecialUtcCase);
664 };
665 
666 /**
667  * Gets the server start time.
668  *
669  * @param	{int}	compNum		the component number
670  * @return	{Date}	the start time
671  */
672 ZmInvite.prototype.getServerStartTime = 
673 function(compNum) {
674 	var cn = compNum || 0;
675 	return this.components[cn] && this.components[cn].s
676 		? this.components[cn].s[0].d : null;
677 };
678 
679 /**
680  * Gets the server start date.
681  * 
682  * @param	{int}	compNum		the component number
683  * @return	{Date}	the start date
684  */
685 ZmInvite.prototype.getServerStartDate =
686 function(compNum, noSpecialUtcCase) {
687 	var cn = compNum || 0;
688     return AjxDateUtil.parseServerDateTime(this.getServerStartTime(cn), noSpecialUtcCase);
689 };
690 
691 /**
692  * Gets start date from exception ID.
693  *
694  * @param	{int}	compNum		the component number
695  */
696 
697 ZmInvite.prototype.getStartDateFromExceptId =
698 function(compNum) {
699 	var cn = compNum || 0;
700     return AjxDateUtil.parseServerDateTime(this.components[cn] && this.components[cn].exceptId
701 		? this.components[cn].exceptId[0].d : null);
702 };
703 
704 /**
705  * Gets the server start time timezone.
706  * 
707  * @param	{int}	compNum		the component number
708  * @return	{String}	the timezone
709  */
710 ZmInvite.prototype.getServerStartTimeTz = 
711 function(compNum) {
712 	var cn = compNum || 0;
713 	if (this.components[cn] == null) { return; }
714 
715 	if (this._serverStartTimeZone == null) {
716 		var startTime = this.getServerStartTime();
717 		this._serverStartTimeZone = startTime && startTime.charAt(startTime.length -1) == 'Z'
718 			? AjxTimezone.GMT_NO_DST
719 			: (this.components[cn].s ? this.components[cn].s[0].tz : null);
720 	}
721 	return this._serverStartTimeZone;
722 };
723 
724 /**
725  * Gets the server end time timezone.
726  * 
727  * @param	{int}	compNum		the component number
728  * @return	{String}	the timezone
729  */
730 ZmInvite.prototype.getServerEndTimeTz = 
731 function(compNum) {
732 	var cn = compNum || 0;
733 	var endComp = this.components[cn] && this.components[cn].e;
734 	if (!endComp) { return null; }
735 
736 	if (!this._serverEndTimeZone) {
737 		var endTime = this.getServerEndTime();
738 		this._serverEndTimeZone = (endTime && endTime.charAt(endTime.length -1) == 'Z')
739 			? AjxTimezone.GMT_NO_DST : endComp[0].tz;
740 	}
741 	return this._serverEndTimeZone;
742 };
743 
744 /**
745  * Gets the duration text.
746  * 
747  * @param	{int}		compNum			the component number
748  * @param	{Boolean}	emptyAllDay		<code>true</code> to return an empty string "" if all day event.
749  * @param	{Boolean}	startOnly		<code>true</code> to include start only
750  * @param	{Boolean}	isText			<code>true</code> to return as text, not html
751  * @param	{Date}		startDate		Optional. Start date to use instead of the original start date
752  * @param	{Date}		endDate			Optional. End date to use instead of the original end date
753  *
754  * @return	{String}	the duration
755  */
756 ZmInvite.prototype.getDurationText =
757 function(compNum, emptyAllDay, startOnly, isText, startDate, endDate) {
758 	var component = this.components[compNum];
759 	var sd = startDate || this.getServerStartDate(compNum);
760 	var ed = endDate || this.getServerEndDate(compNum);
761 	if (!sd && !ed) { return ""; }
762 
763 	// all day
764 	if (this.isAllDayEvent(compNum)) {
765 		if (emptyAllDay) { return ""; }
766 
767 		if (this.isMultiDay(compNum)) {
768 			var dateFormatter = AjxDateFormat.getDateInstance();
769 			var startDay = dateFormatter.format(sd);
770 			var endDay = dateFormatter.format(ed);
771 
772 			if (!ZmInvite._daysFormatter) {
773 				ZmInvite._daysFormatter = new AjxMessageFormat(ZmMsg.durationDays);
774 			}
775 			return ZmInvite._daysFormatter.format([startDay, endDay]);
776 		} 
777 		return sd ? AjxDateFormat.getDateInstance(AjxDateFormat.FULL).format(sd) : "";
778 	}
779 
780     var dateFormatter = AjxDateFormat.getDateInstance(AjxDateFormat.FULL);
781     var timeFormatter = AjxDateFormat.getTimeInstance(AjxDateFormat.SHORT);
782 
783     var a = sd ? [dateFormatter.format(sd), isText ? " " : "<br>"] : [];
784     if (startOnly) {
785         a.push(sd ? timeFormatter.format(sd) : "");
786 	}
787 	else {
788         var startHour = sd ? timeFormatter.format(sd) : "";
789 		var endHour = timeFormatter.format(ed);
790 
791 		if (!ZmInvite._hoursFormatter) {
792 			ZmInvite._hoursFormatter = new AjxMessageFormat(ZmMsg.durationHours);
793 		}
794 		a.push(ZmInvite._hoursFormatter.format([startHour, endHour]));
795 	}
796 	return a.join("");
797 };
798 
799 /**
800  * Gets the name.
801  * 
802  * @param	{int}	compNum		the component number
803  * @return	{String}	the name
804  */
805 ZmInvite.prototype.getName = 
806 function(compNum) {
807 	var cn = compNum || 0;
808 	return this.components[cn] ? this.components[cn].name : null;
809 };
810 
811 /**
812  * Gets the free busy.
813  * 
814  * @param	{int}	compNum		the component number
815  * @return	{String}	the free busy
816  */
817 ZmInvite.prototype.getFreeBusy =
818 function(compNum) {
819 	var cn = compNum || 0;
820 	return this.components[cn] ? this.components[cn].fb : null;
821 };
822 
823 /**
824  * Gets the privacy.
825  * 
826  * @param	{int}	compNum		the component number
827  * @return	{String}	the privacy
828  */
829 ZmInvite.prototype.getPrivacy =
830 function(compNum) {
831 	var cn = compNum || 0;
832 	return this.components[cn] ? this.components[cn]["class"] : null;
833 };
834 
835 /**
836  * Gets the x-prop.
837  * 
838  * @param	{int}	compNum		the component number
839  * @return	{String}	the x-prop
840  */
841 ZmInvite.prototype.getXProp =
842 function(compNum) {
843 	var cn = compNum || 0;
844 	return this.components[cn] ? this.components[cn]["xprop"] : null;
845 };
846 
847 /**
848  * Gets the location.
849  * 
850  * @param	{int}	compNum		the component number
851  * @return	{String}	the location
852  */
853 ZmInvite.prototype.getLocation =
854 function(compNum) {
855 	var cn = compNum || 0;
856 	return this.components[cn] ? this.components[cn].loc : null;
857 };
858 
859 /**
860  * Gets the recurrence id (ridZ) - applicable to recurring appointment .
861  *
862  * @param	{int}	compNum		the component number
863  * @return	{String}	the recurrence id, null for non-recurring appointment
864  */
865 ZmInvite.prototype.getRecurrenceId =
866 function(compNum) {
867 	var cn = compNum || 0;
868 	return this.components[cn] ? this.components[cn].ridZ : null;
869 };
870 
871 /**
872  * Gets the tool tip in HTML for this invite.
873  * 
874  * <p>
875  * <strong>Note:</strong> This method assumes that there are currently one and only one component object on the invite.
876  * </p>
877  * 
878  * @return	{String}	the tool tip
879  */
880 ZmInvite.prototype.getToolTip =
881 function() {
882 	if (this._toolTip)
883 		return this._toolTip;
884 
885 	var compNum = 0;
886 
887 	var html = [];
888 	var idx = 0;
889 
890 	html[idx++] = "<table cellpadding=0 cellspacing=0 border=0 >";
891 	html[idx++] = "<tr valign='center'><td colspan=2 align='left'>";
892 	html[idx++] = "<div style='border-bottom: 1px solid black;'>";
893 	html[idx++] = "<table cellpadding=0 cellspacing=0 border=0 width=100%>";
894 	html[idx++] = "<tr valign='center'><td><b>";
895 
896 	// IMGHACK - added outer table for new image changes...
897 	html[idx++] = "<div style='white-space:nowrap'><table border=0 cellpadding=0 cellspacing=0 style='display:inline'><tr>";
898 	if (this.hasOtherAttendees(compNum)) {
899 		html[idx++] = "<td>";
900 		html[idx++] = AjxImg.getImageHtml("ApptMeeting");
901 		html[idx++] = "</td>";
902 	}
903 
904 	if (this.isException(compNum)) {
905 		html[idx++] = "<td>";
906 		html[idx++] = AjxImg.getImageHtml("ApptException");
907 		html[idx++] = "</td>";
908 	}
909 	else if (this.isRecurring(compNum)) {
910 		html[idx++] = "<td>";
911 		html[idx++] = AjxImg.getImageHtml("ApptRecur");
912 		html[idx++] = "</td>";
913 	}
914 
915 	html[idx++] = "</tr></table> ";
916 	html[idx++] = AjxStringUtil.htmlEncode(this.getName(compNum));
917 	html[idx++] = " </div></b></td><td align='right'>";
918 	html[idx++] = AjxImg.getImageHtml("Appointment");
919 	html[idx++] = "</td></table></div></td></tr>";
920 
921 	var when = this.getDurationText(compNum, false, false);
922 	idx = this._addEntryRow(ZmMsg.when, when, html, idx, false, null, true);
923 	if (this.isRecurring(compNum)) {
924 		if (!this._recurBlurb) {
925 			AjxDispatcher.require(["MailCore", "CalendarCore"]);
926 			var recur = new ZmRecurrence();
927 			recur.parse(this.getRecurrenceRules(compNum));
928 			this._recurBlurb = recur.getBlurb();
929 		}
930 		idx = this._addEntryRow(ZmMsg.repeats, this._recurBlurb, html, idx, true, null, true);
931 	}
932 	idx = this._addEntryRow(ZmMsg.location, this.getLocation(compNum), html, idx, false);
933 
934 	html[idx++] = "</table>";
935 	this._toolTip = html.join("");
936 
937 	return this._toolTip;
938 };
939 
940 /**
941  * Gets the Appt summary.
942  *
943  * @param	{Boolean}	isHtml	<code>true</code> to return summary as HTML
944  * @return	{String}	the appt summary
945  */
946 ZmInvite.prototype.getApptSummary =
947 function(isHtml) {
948 	var msg = appCtxt.getById(this.getMessageId());
949 	var appt;
950 
951 	if (msg) {
952 		AjxDispatcher.require(["MailCore", "CalendarCore"]);
953 		appt = new ZmAppt();
954 		appt.setFromMessage(msg);
955 	}
956 
957 	return appt ? appt.getSummary(isHtml) : this.getSummary(isHtml);
958 };
959 
960 /**
961  * Gets the summary.
962  * 
963  * @param	{Boolean}	isHtml	<code>true</code> to return summary as HTML
964  * @return	{String}	the summary
965  */
966 ZmInvite.prototype.getSummary =
967 function(isHtml) {
968 	if (this.isRecurring()) {
969 		if (!this._recurBlurb) {
970 			AjxDispatcher.require(["MailCore", "CalendarCore"]);
971 			var recur = new ZmRecurrence();
972 			recur.setRecurrenceRules(this.getRecurrenceRules(), this.getServerStartDate());
973 			this._recurBlurb = recur.getBlurb();
974 		}
975 	}
976 
977 	var buf = [];
978 	var i = 0;
979 
980 	if (!this._summaryHtmlLineFormatter) {
981 		this._summaryHtmlLineFormatter = new AjxMessageFormat("<tr><th align='left'>{0}</th><td>{1} {2}</td></tr>");
982 		this._summaryTextLineFormatter = new AjxMessageFormat("{0} {1} {2}");
983 	}
984 	var formatter = isHtml ? this._summaryHtmlLineFormatter : this._summaryTextLineFormatter;
985 
986 	var params = [];
987 
988 	if (isHtml) {
989 		buf[i++] = "<p>\n<table border='0'>\n";
990 	}
991 
992 	var orgName = this.getOrganizerName();
993 	if (orgName) {
994 		params = [ZmMsg.organizerLabel, orgName, ""];
995 		buf[i++] = formatter.format(params);
996 		buf[i++] = "\n";
997 	}
998 
999 	var whenSummary = this.getDurationText(0, false, false, true);
1000 	if (whenSummary) {
1001 		params = [ZmMsg.whenLabel, whenSummary, ""];
1002 		buf[i++] = formatter.format(params);
1003 		buf[i++] = "\n";
1004 	}
1005 
1006 	var locationSummary = this.getLocation();
1007 	if (locationSummary) {
1008 		params = [ZmMsg.locationLabel, locationSummary, ""];
1009 		buf[i++] = formatter.format(params);
1010 		buf[i++] = "\n";
1011 	}
1012 
1013 	if (this._recurBlurb) {
1014 		params = [ZmMsg.repeatLabel, this._recurBlurb, ""];
1015 		buf[i++] = formatter.format(params);
1016 		buf[i++] = "\n";
1017 	}
1018 
1019 	if (isHtml) {
1020 		buf[i++] = "</table>\n";
1021 	}
1022 	buf[i++] = isHtml ? "<div>" : "\n\n";
1023 	buf[i++] = ZmItem.NOTES_SEPARATOR;
1024 	// bug fix #7835 - add <br> after DIV otherwise Outlook lops off 1st char
1025 	buf[i++] = isHtml ? "</div><br>" : "\n\n";
1026 
1027 	return buf.join("");
1028 };
1029 
1030 /**
1031  * Adds a row to the tool tip.
1032  * 
1033  * @private
1034  */
1035 ZmInvite.prototype._addEntryRow =
1036 function(field, data, html, idx, wrap, width, asIs) {
1037 	if (data != null && data != "") {
1038 		html[idx++] = "<tr valign='top'><td align='right' style='padding-right: 5px;'><b><div style='white-space:nowrap'>";
1039 		html[idx++] = AjxMessageFormat.format(ZmMsg.makeLabel, AjxStringUtil.htmlEncode(field));
1040 		html[idx++] = "</div></b></td><td align='left'><div style='white-space:";
1041 		html[idx++] = wrap ? "wrap;" : "nowrap;";
1042 		if (width) {
1043 			html[idx++] = "width:";
1044 			html[idx++] = width;
1045 			html[idx++] = "px;";
1046 		}
1047 		html[idx++] = "'>";
1048 		html[idx++] = asIs ? data : AjxStringUtil.htmlEncode(data);
1049 		html[idx++] = "</div></td></tr>";
1050 	}
1051 	return idx;
1052 };
1053 
1054 /**
1055  * Checks the invite has acceptable components.
1056  * 
1057  * @return	{Boolean}	<code>true</code> if the invite has acceptable components
1058  */
1059 ZmInvite.prototype.hasAcceptableComponents =
1060 function() {
1061 	for (var i  in this.components) {
1062 		if (this.getStatus(i) != ZmCalendarApp.STATUS_CANC) {
1063 			return true;
1064 		}
1065 	}
1066 
1067 	return false;
1068 };
1069 
1070 /**
1071  * Checks the invite has a reply method.
1072  * 
1073  * @param	{int}	compNum		the component number
1074  * @return	{Boolean}	<code>true</code> if the invite has a method that REQUIRES a reply (ironically NOT REPLY method but rather REQUEST or PUBLISH)
1075  */
1076 ZmInvite.prototype.hasInviteReplyMethod =
1077 function(compNum) {
1078 	var methodName = this.getInviteMethod(compNum);
1079 	var publishOrRequest = (methodName == ZmCalendarApp.METHOD_REQUEST ||
1080 							methodName == ZmCalendarApp.METHOD_PUBLISH);
1081 	return ((methodName == null) || publishOrRequest);
1082 };
1083 
1084 /**
1085  * Checks the invite has a counter method.
1086  *
1087  * @param	{int}	    compNum		the component number
1088  * @return	{Boolean}	<code>true</code> if the invite has a counter method
1089  */
1090 ZmInvite.prototype.hasCounterMethod =
1091 function(compNum) {
1092 	return (this.getInviteMethod(compNum) == ZmCalendarApp.METHOD_COUNTER);
1093 };
1094 
1095 /**
1096  * returns proposed time from counter invite
1097  *
1098  * @param	{int}	    compNum		the component number
1099  * @return	{string}	proposed time as formatted string
1100  */
1101 ZmInvite.prototype.getProposedTimeStr =
1102 function(compNum) {
1103 	var methodName = this.getInviteMethod(compNum);
1104 	if (methodName == ZmCalendarApp.METHOD_COUNTER) {
1105 		return this.getDurationText(compNum, false, false, true);
1106 	}
1107 	return "";
1108 };
1109 
1110 ZmInvite.prototype.getChanges =
1111 function(compNum) {
1112 	var cn = compNum || 0;
1113 	var changesStr = this.components[cn] && this.components[cn].changes;
1114 	var changesArr = changesStr && changesStr.split(",");
1115 	if (changesArr && changesArr.length > 0) {
1116 		var changes = {};
1117 		for (var i = 0; i < changesArr.length; i++) {
1118 			changes[changesArr[i]] = true;
1119 		}
1120 		return changes;
1121 	}
1122 
1123 	return null;
1124 };
1125 
1126 /**
1127  * Returns true if this invite has attendees, one of which replied back with an
1128  * "actioned" response (e.g. accept/decline/tentative)
1129  */
1130 ZmInvite.prototype.hasAttendeeResponse =
1131 function() {
1132 	var att = this.getAttendees();
1133 	return (att.length > 0 && att[0].ptst != ZmCalBaseItem.PSTATUS_NEEDS_ACTION);
1134 };
1135 
1136 /**
1137  * Checks if this invite has html description.
1138  *
1139  * @return	{Boolean}	<code>true</code> if this invite has HTML description
1140  */
1141 ZmInvite.prototype.isHtmlInvite =
1142 function() {
1143 	var comp = this.getComponent(0);
1144 	var htmlContent = comp && comp.descHtml;
1145 	return (htmlContent && htmlContent[0] && htmlContent[0]._content) ? true : false;
1146 };
1147