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