diff options
Diffstat (limited to '')
-rw-r--r-- | comm/calendar/base/modules/utils/calInvitationUtils.jsm | 875 |
1 files changed, 875 insertions, 0 deletions
diff --git a/comm/calendar/base/modules/utils/calInvitationUtils.jsm b/comm/calendar/base/modules/utils/calInvitationUtils.jsm new file mode 100644 index 0000000000..732c6bbf6c --- /dev/null +++ b/comm/calendar/base/modules/utils/calInvitationUtils.jsm @@ -0,0 +1,875 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); +var { recurrenceRule2String } = ChromeUtils.import( + "resource:///modules/calendar/calRecurrenceUtils.jsm" +); +var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); + +const lazy = {}; +ChromeUtils.defineModuleGetter( + lazy, + "CalRecurrenceDate", + "resource:///modules/CalRecurrenceDate.jsm" +); +ChromeUtils.defineModuleGetter(lazy, "MailStringUtils", "resource:///modules/MailStringUtils.jsm"); + +const EXPORTED_SYMBOLS = ["calinvitation"]; + +var calinvitation = { + /** + * Returns a header title for an ITIP item depending on the response method + * + * @param {calItipItem} aItipItem the itip item to check + * @returns {string} the header title + */ + getItipHeader(aItipItem) { + let header; + + if (aItipItem) { + let item = aItipItem.getItemList()[0]; + let summary = item.getProperty("SUMMARY") || ""; + let organizer = item.organizer; + let organizerString = organizer ? organizer.commonName || organizer.toString() : ""; + + switch (aItipItem.responseMethod) { + case "REQUEST": + header = cal.l10n.getLtnString("itipRequestBody", [organizerString, summary]); + break; + case "CANCEL": + header = cal.l10n.getLtnString("itipCancelBody", [organizerString, summary]); + break; + case "COUNTER": + // falls through + case "REPLY": { + let attendees = item.getAttendees(); + let sender = cal.itip.getAttendeesBySender(attendees, aItipItem.sender); + if (sender.length == 1) { + if (aItipItem.responseMethod == "COUNTER") { + header = cal.l10n.getLtnString("itipCounterBody", [sender[0].toString(), summary]); + } else { + let statusString = + sender[0].participationStatus == "DECLINED" + ? "itipReplyBodyDecline" + : "itipReplyBodyAccept"; + header = cal.l10n.getLtnString(statusString, [sender[0].toString()]); + } + } else { + header = ""; + } + break; + } + case "DECLINECOUNTER": + header = cal.l10n.getLtnString("itipDeclineCounterBody", [organizerString, summary]); + break; + } + } + + if (!header) { + header = cal.l10n.getLtnString("imipHtml.header"); + } + + return header; + }, + + _createAddedElement(doc) { + let el = doc.createElement("ins"); + el.classList.add("added"); + return el; + }, + + _createRemovedElement(doc) { + let el = doc.createElement("del"); + el.classList.add("removed"); + return el; + }, + + /** + * Creates new icon and text label for the given event attendee. + * + * @param {Document} doc - The document the new label will belong to. + * @param {calIAttendee} attendee - The attendee to create the label for. + * @param {calIAttendee[]} attendeeList - The full list of attendees for the + * event. + * @param {calIAttendee} [oldAttendee] - The previous version of this attendee + * for this event. + * @param {calIAttendee[]} [attendeeList] - The previous list of attendees for + * this event. This is not optional if oldAttendee is given. + * + * @returns {HTMLDivElement} - The new attendee label. + */ + createAttendeeLabel(doc, attendee, attendeeList, oldAttendee, oldAttendeeList) { + let userType = attendee.userType || "INDIVIDUAL"; + let role = attendee.role || "REQ-PARTICIPANT"; + let partstat = attendee.participationStatus || "NEEDS-ACTION"; + + let modified = + oldAttendee && + ((oldAttendee.userType || "INDIVIDUAL") != userType || + (oldAttendee.role || "REQ-PARTICIPANT") != role || + (oldAttendee.participationStatus || "NEEDS-ACTION") != partstat); + + // resolve delegatees/delegators to display also the CN + let del = cal.itip.resolveDelegation(attendee, attendeeList); + if (oldAttendee && !modified) { + let oldDel = cal.itip.resolveDelegation(oldAttendee, oldAttendeeList); + modified = oldDel.delegatees !== del.delegatees || oldDel.delegator !== del.delegator; + } + + let userTypeString = cal.l10n.getLtnString("imipHtml.attendeeUserType2." + userType, [ + attendee.toString(), + ]); + let roleString = cal.l10n.getLtnString("imipHtml.attendeeRole2." + role, [userTypeString]); + let partstatString = cal.l10n.getLtnString("imipHtml.attendeePartStat2." + partstat, [ + attendee.commonName || attendee.toString(), + del.delegatees, + ]); + let tooltip = cal.l10n.getLtnString("imipHtml.attendee.combined", [roleString, partstatString]); + + let name = attendee.toString(); + if (del.delegators) { + name += " " + cal.l10n.getLtnString("imipHtml.attendeeDelegatedFrom", [del.delegators]); + } + + let attendeeLabel = doc.createElement("div"); + attendeeLabel.classList.add("attendee-label"); + // NOTE: tooltip will not appear when the top level is XUL. + attendeeLabel.setAttribute("title", tooltip); + attendeeLabel.setAttribute("attendeeid", attendee.id); + attendeeLabel.setAttribute("tabindex", "0"); + + if (modified) { + attendeeLabel.classList.add("modified"); + } + + // FIXME: Replace icon with an img element with src and alt. The current + // problem is that the icon image is set in CSS on the itip-icon class + // with a background image that changes with the role attribute. This is + // generally inaccessible (see Bug 1702560). + let icon = doc.createElement("div"); + icon.classList.add("itip-icon"); + icon.setAttribute("partstat", partstat); + icon.setAttribute("usertype", userType); + icon.setAttribute("attendeerole", role); + attendeeLabel.appendChild(icon); + + let text = doc.createElement("div"); + text.classList.add("attendee-name"); + text.appendChild(doc.createTextNode(name)); + attendeeLabel.appendChild(text); + + return attendeeLabel; + }, + + /** + * Create an new list item element for an attendee, to be used as a child of + * an "attendee-list" element. + * + * @param {Document} doc - The document the new list item will belong to. + * @param {Element} attendeeLabel - The attendee label to place within the + * list item. + * + * return {HTMLLIElement} - The attendee list item. + */ + createAttendeeListItem(doc, attendeeLabel) { + let listItem = doc.createElement("li"); + listItem.classList.add("attendee-list-item"); + listItem.appendChild(attendeeLabel); + return listItem; + }, + + /** + * Creates a new element that lists the given attendees. + * + * @param {Document} doc - The document the new list will belong to. + * @param {calIAttendee[]} attendees - The attendees to create the list for. + * @param {calIAttendee[]} [oldAttendees] - A list of attendees for a + * previous version of the event. + * + * @returns {HTMLUListElement} - The list of attendees. + */ + createAttendeesList(doc, attendees, oldAttendees) { + let list = doc.createElement("ul"); + list.classList.add("attendee-list"); + + let oldAttendeeData; + if (oldAttendees) { + oldAttendeeData = []; + for (let attendee of oldAttendees) { + let data = { attendee, item: null }; + oldAttendeeData.push(data); + } + } + + for (let attendee of attendees) { + let attendeeLabel; + let oldData; + if (oldAttendeeData) { + oldData = oldAttendeeData.find(old => old.attendee.id == attendee.id); + if (oldData) { + // Same attendee. + attendeeLabel = this.createAttendeeLabel( + doc, + attendee, + attendees, + oldData.attendee, + oldAttendees + ); + } else { + // Added attendee. + attendeeLabel = this._createAddedElement(doc); + attendeeLabel.appendChild(this.createAttendeeLabel(doc, attendee, attendees)); + } + } else { + attendeeLabel = this.createAttendeeLabel(doc, attendee, attendees); + } + let listItem = this.createAttendeeListItem(doc, attendeeLabel); + if (oldData) { + oldData.item = listItem; + } + list.appendChild(listItem); + } + + if (oldAttendeeData) { + let next = null; + // Traverse from the end of the list to the start. + for (let i = oldAttendeeData.length - 1; i >= 0; i--) { + let data = oldAttendeeData[i]; + if (!data.item) { + // Removed attendee. + let attendeeLabel = this._createRemovedElement(doc); + attendeeLabel.appendChild(this.createAttendeeLabel(doc, data.attendee, attendees)); + let listItem = this.createAttendeeListItem(doc, attendeeLabel); + data.item = listItem; + + // Insert the removed attendee list item *before* the list item that + // corresponds to the attendee that follows this attendee in the + // oldAttendees list. + // + // NOTE: by traversing from the end of the list to the start, we are + // prioritising being next to the attendee that follows us, rather + // than being next to the attendee that precedes us in the oldAttendee + // list. + // + // Specifically, if a new attendee is added between these two old + // neighbours, the added attendee will be shown earlier than the + // removed attendee in the list. + // + // E.g., going from the list + // [first@person, removed@person, second@person] + // to + // [first@person, added@person, second@person] + // will be shown as + // first@person + // + added@person + // - removed@person + // second@person + // because the removed@person's uses second@person as their reference + // point. + // + // NOTE: next.item is always non-null because next.item is always set + // by the end of the last loop. + list.insertBefore(listItem, next ? next.item : null); + } + next = data; + } + } + + return list; + }, + + /** + * Returns the html representation of the event as a DOM document. + * + * @param {calIItemBase} event - The event to parse into html. + * @param {calItipItem} itipItem - The itip item, which contains the event. + * @returns {Document} The html representation of the event. + */ + createInvitationOverlay(event, itipItem) { + // Creates HTML using the Node strings in the properties file + const parser = new DOMParser(); + let doc = parser.parseFromString(calinvitation.htmlTemplate, "text/html"); + this.updateInvitationOverlay(doc, event, itipItem); + return doc; + }, + + /** + * Update the document created by createInvitationOverlay to show the new + * event details, and optionally show changes in the event against an older + * version of it. + * + * For example, this can be used for email invitations to update the invite to + * show the most recent version of the event found in the calendar, whilst + * also showing the event details that were removed since the original email + * invitation. I.e. contrasting the event found in the calendar with the event + * found within the email. Alternatively, if the email invitation is newer + * than the event found in the calendar, you can switch the comparison around. + * (As used in imip-bar.js.) + * + * @param {Document} doc - The document to update, previously created through + * createInvitationOverlay. + * @param {calIItemBase} event - The newest version of the event. + * @param {calItipItem} itipItem - The itip item, which contains the event. + * @param {calIItemBase} [oldEvent] - A previous version of the event to + * show as updated. + */ + updateInvitationOverlay(doc, event, itipItem, oldEvent) { + let headerDescr = doc.getElementById("imipHtml-header"); + if (headerDescr) { + headerDescr.textContent = calinvitation.getItipHeader(itipItem); + } + + let formatter = cal.dtz.formatter; + + /** + * Set whether the given field should be shown. + * + * @param {string} fieldName - The name of the field. + * @param {boolean} show - Whether the field should be shown. + */ + let showField = (fieldName, show) => { + let row = doc.getElementById("imipHtml-" + fieldName + "-row"); + if (row.hidden && show) { + // Make sure the field name is set. + doc.getElementById("imipHtml-" + fieldName + "-descr").textContent = cal.l10n.getLtnString( + "imipHtml." + fieldName + ); + } + row.hidden = !show; + }; + + /** + * Set the given element to display the given value. + * + * @param {Element} element - The element to display the value within. + * @param {string} value - The value to show. + * @param {boolean} [convert=false] - Whether the value will need converting + * to a sanitised document fragment. + * @param {string} [html] - The html to use as the value. This is only used + * if convert is set to true. + */ + let setElementValue = (element, value, convert = false, html) => { + if (convert) { + element.appendChild(cal.view.textToHtmlDocumentFragment(value, doc, html)); + } else { + element.textContent = value; + } + }; + + /** + * Set the given field. + * + * If oldEvent is set, and the new value differs from the old one, it will + * be shown as added and/or removed content. + * + * If neither events have a value, the field will be hidden. + * + * @param {string} fieldName - The name of the field to set. + * @param {Function} getValue - A method to retrieve the field value from an + * event. Should return a string, or a falsey value if the event has no + * value for this field. + * @param {boolean} [convert=false] - Whether the value will need converting + * to a sanitised document fragment. + * @param {Function} [getHtml] - A method to retrieve the value as a html. + */ + let setField = (fieldName, getValue, convert = false, getHtml) => { + let cell = doc.getElementById("imipHtml-" + fieldName + "-content"); + while (cell.lastChild) { + cell.lastChild.remove(); + } + let value = getValue(event); + let oldValue = oldEvent && getValue(oldEvent); + let html = getHtml && getHtml(event); + let oldHtml = oldEvent && getHtml && getHtml(event); + if (oldEvent && (oldValue || value) && oldValue !== value) { + // Different values, with at least one being truthy. + showField(fieldName, true); + if (!oldValue) { + let added = this._createAddedElement(doc); + setElementValue(added, value, convert, html); + cell.appendChild(added); + } else if (!value) { + let removed = this._createRemovedElement(doc); + setElementValue(removed, oldValue, convert, oldHtml); + cell.appendChild(removed); + } else { + let added = this._createAddedElement(doc); + setElementValue(added, value, convert, html); + let removed = this._createRemovedElement(doc); + setElementValue(removed, oldValue, convert, oldHtml); + + cell.appendChild(added); + cell.appendChild(doc.createElement("br")); + cell.appendChild(removed); + } + } else if (value) { + // Same truthy value. + showField(fieldName, true); + setElementValue(cell, value, convert, html); + } else { + showField(fieldName, false); + } + }; + + setField("summary", ev => ev.title, true); + setField("location", ev => ev.getProperty("LOCATION"), true); + + let kDefaultTimezone = cal.dtz.defaultTimezone; + setField("when", ev => { + if (ev.recurrenceInfo) { + let startDate = ev.startDate?.getInTimezone(kDefaultTimezone) ?? null; + let endDate = ev.endDate?.getInTimezone(kDefaultTimezone) ?? null; + let repeatString = recurrenceRule2String( + ev.recurrenceInfo, + startDate, + endDate, + startDate.isDate + ); + if (repeatString) { + return repeatString; + } + } + return formatter.formatItemInterval(ev); + }); + + setField("canceledOccurrences", ev => { + if (!ev.recurrenceInfo) { + return null; + } + let formattedExDates = []; + + // Show removed instances + for (let exc of ev.recurrenceInfo.getRecurrenceItems()) { + if ( + (exc instanceof lazy.CalRecurrenceDate || exc instanceof Ci.calIRecurrenceDate) && + exc.isNegative + ) { + // This is an EXDATE + let excDate = exc.date.getInTimezone(kDefaultTimezone); + formattedExDates.push(formatter.formatDateTime(excDate)); + } + } + if (formattedExDates.length > 0) { + return formattedExDates.join("\n"); + } + return null; + }); + + let dateComptor = (a, b) => a.startDate.compare(b.startDate); + + setField("modifiedOccurrences", ev => { + if (!ev.recurrenceInfo) { + return null; + } + let modifiedOccurrences = []; + + for (let exc of ev.recurrenceInfo.getRecurrenceItems()) { + if ( + (exc instanceof lazy.CalRecurrenceDate || exc instanceof Ci.calIRecurrenceDate) && + !exc.isNegative + ) { + // This is an RDATE, close enough to a modified occurrence + let excItem = ev.recurrenceInfo.getOccurrenceFor(exc.date); + cal.data.binaryInsert(modifiedOccurrences, excItem, dateComptor, true); + } + } + for (let recurrenceId of ev.recurrenceInfo.getExceptionIds()) { + let exc = ev.recurrenceInfo.getExceptionFor(recurrenceId); + let excLocation = exc.getProperty("LOCATION"); + + // Only show modified occurrence if start, duration or location + // has changed. + exc.QueryInterface(Ci.calIEvent); + if ( + exc.startDate.compare(exc.recurrenceId) != 0 || + exc.duration.compare(ev.duration) != 0 || + excLocation != ev.getProperty("LOCATION") + ) { + cal.data.binaryInsert(modifiedOccurrences, exc, dateComptor, true); + } + } + + if (modifiedOccurrences.length > 0) { + let evLocation = ev.getProperty("LOCATION"); + return modifiedOccurrences + .map(occ => { + let formattedExc = formatter.formatItemInterval(occ); + let occLocation = occ.getProperty("LOCATION"); + if (occLocation != evLocation) { + formattedExc += + " (" + cal.l10n.getLtnString("imipHtml.newLocation", [occLocation]) + ")"; + } + return formattedExc; + }) + .join("\n"); + } + return null; + }); + + setField( + "description", + // We remove the useless "Outlookism" squiggle. + ev => ev.descriptionText?.replace("*~*~*~*~*~*~*~*~*~*", ""), + true, + ev => ev.descriptionHTML + ); + + setField("url", ev => ev.getProperty("URL"), true); + setField( + "attachments", + ev => { + // ATTACH - we only display URI but no BINARY type attachments here + let links = []; + for (let attachment of ev.getAttachments()) { + if (attachment.uri) { + links.push(attachment.uri.spec); + } + } + return links.join("\n"); + }, + true + ); + + // ATTENDEE and ORGANIZER fields + let attendees = event.getAttendees(); + let oldAttendees = oldEvent?.getAttendees(); + + let organizerCell = doc.getElementById("imipHtml-organizer-cell"); + while (organizerCell.lastChild) { + organizerCell.lastChild.remove(); + } + + let organizer = event.organizer; + if (oldEvent) { + let oldOrganizer = oldEvent.organizer; + if (!organizer && !oldOrganizer) { + showField("organizer", false); + } else { + showField("organizer", true); + + let removed = false; + let added = false; + if (!organizer) { + removed = true; + } else if (!oldOrganizer) { + added = true; + } else if (organizer.id !== oldOrganizer.id) { + removed = true; + added = true; + } else { + // Same organizer, potentially modified. + organizerCell.appendChild( + this.createAttendeeLabel(doc, organizer, attendees, oldOrganizer, oldAttendees) + ); + } + // Append added first. + if (added) { + let addedEl = this._createAddedElement(doc); + addedEl.appendChild(this.createAttendeeLabel(doc, organizer, attendees)); + organizerCell.appendChild(addedEl); + } + if (removed) { + let removedEl = this._createRemovedElement(doc); + removedEl.appendChild(this.createAttendeeLabel(doc, oldOrganizer, oldAttendees)); + organizerCell.appendChild(removedEl); + } + } + } else if (!organizer) { + showField("organizer", false); + } else { + showField("organizer", true); + organizerCell.appendChild(this.createAttendeeLabel(doc, organizer, attendees)); + } + + let attendeesCell = doc.getElementById("imipHtml-attendees-cell"); + while (attendeesCell.lastChild) { + attendeesCell.lastChild.remove(); + } + + // Hide if we have no attendees, and neither does the old event. + if (attendees.length == 0 && (!oldEvent || oldAttendees.length == 0)) { + showField("attendees", false); + } else { + // oldAttendees is undefined if oldEvent is undefined. + showField("attendees", true); + attendeesCell.appendChild(this.createAttendeesList(doc, attendees, oldAttendees)); + } + }, + + /** + * Returns the header section for an invitation email. + * + * @param {string} aMessageId the message id to use for that email + * @param {nsIMsgIdentity} aIdentity the identity to use for that email + * @returns {string} the source code of the header section of the email + */ + getHeaderSection(aMessageId, aIdentity, aToList, aSubject) { + let recipient = aIdentity.fullName + " <" + aIdentity.email + ">"; + let from = aIdentity.fullName.length + ? cal.email.validateRecipientList(recipient) + : aIdentity.email; + let header = + "MIME-version: 1.0\r\n" + + (aIdentity.replyTo + ? "Return-path: " + calinvitation.encodeMimeHeader(aIdentity.replyTo, true) + "\r\n" + : "") + + "From: " + + calinvitation.encodeMimeHeader(from, true) + + "\r\n" + + (aIdentity.organization + ? "Organization: " + calinvitation.encodeMimeHeader(aIdentity.organization) + "\r\n" + : "") + + "Message-ID: " + + aMessageId + + "\r\n" + + "To: " + + calinvitation.encodeMimeHeader(aToList, true) + + "\r\n" + + "Date: " + + calinvitation.getRfc5322FormattedDate() + + "\r\n" + + "Subject: " + + calinvitation.encodeMimeHeader(aSubject.replace(/(\n|\r\n)/, "|")) + + "\r\n"; + let validRecipients; + if (aIdentity.doCc) { + validRecipients = cal.email.validateRecipientList(aIdentity.doCcList); + if (validRecipients != "") { + header += "Cc: " + calinvitation.encodeMimeHeader(validRecipients, true) + "\r\n"; + } + } + if (aIdentity.doBcc) { + validRecipients = cal.email.validateRecipientList(aIdentity.doBccList); + if (validRecipients != "") { + header += "Bcc: " + calinvitation.encodeMimeHeader(validRecipients, true) + "\r\n"; + } + } + return header; + }, + + /** + * Returns a datetime string according to section 3.3 of RfC5322 + * + * @param {Date} [optional] Js Date object to format; if not provided current DateTime is used + * @returns {string} Datetime string with a modified tz-offset notation compared to + * Date.toString() like "Fri, 20 Nov 2015 09:45:36 +0100" + */ + getRfc5322FormattedDate(aDate = null) { + let date = aDate || new Date(); + let str = date + .toString() + .replace( + /^(\w{3}) (\w{3}) (\d{2}) (\d{4}) ([0-9:]{8}) GMT([+-])(\d{4}).*$/, + "$1, $3 $2 $4 $5 $6$7" + ); + // according to section 3.3 of RfC5322, +0000 should be used for defined timezones using + // UTC time, while -0000 should indicate a floating time instead + let timezone = cal.dtz.defaultTimezone; + if (timezone && timezone.isFloating) { + str.replace(/\+0000$/, "-0000"); + } + return str; + }, + + /** + * Converts a given unicode text to utf-8 and normalizes line-breaks to \r\n + * + * @param {string} aText a unicode encoded string + * @returns {string} the converted uft-8 encoded string + */ + encodeUTF8(aText) { + return calinvitation.convertFromUnicode(aText).replace(/(\r\n)|\n/g, "\r\n"); + }, + + /** + * Converts a given unicode text + * + * @param {string} aSrc unicode text to convert + * @returns {string} the converted string + */ + convertFromUnicode(aSrc) { + return lazy.MailStringUtils.stringToByteString(aSrc); + }, + + /** + * Converts a header to a mime encoded header + * + * @param {string} aHeader a header to encode + * @param {boolean} aIsEmail if enabled, only the CN but not the email address gets + * converted - default value is false + * @returns {string} the encoded string + */ + encodeMimeHeader(aHeader, aIsEmail = false) { + let fieldNameLen = aHeader.indexOf(": ") + 2; + return MailServices.mimeConverter.encodeMimePartIIStr_UTF8( + aHeader, + aIsEmail, + fieldNameLen, + Ci.nsIMimeConverter.MIME_ENCODED_WORD_SIZE + ); + }, + + /** + * Parses a counterproposal to extract differences to the existing event + * + * @param {calIEvent|calITodo} aProposedItem The counterproposal + * @param {calIEvent|calITodo} aExistingItem The item to compare with + * @returns {JSObject} Objcet of result and differences of parsing + * @returns {string} JsObject.result.type Parsing result: OK|OLDVERSION|ERROR|NODIFF + * @returns {string} JsObject.result.descr Parsing result description + * @returns {Array} JsObject.differences Array of objects consisting of property, proposed + * and original properties. + * @returns {string} JsObject.comment A comment of the attendee, if any + */ + parseCounter(aProposedItem, aExistingItem) { + let isEvent = aProposedItem.isEvent(); + // atm we only support a subset of properties, for a full list see RfC 5546 section 3.2.7 + let properties = ["SUMMARY", "LOCATION", "DTSTART", "DTEND", "COMMENT"]; + if (!isEvent) { + cal.LOG("Parsing of counterproposals is currently only supported for events."); + properties = []; + } + + let diff = []; + let status = { descr: "", type: "OK" }; + // As required in https://tools.ietf.org/html/rfc5546#section-3.2.7 a valid counterproposal + // is referring to as existing UID and must include the same sequence number and organizer as + // the original request being countered + if ( + aProposedItem.id == aExistingItem.id && + aProposedItem.organizer && + aExistingItem.organizer && + aProposedItem.organizer.id == aExistingItem.organizer.id + ) { + let proposedSequence = aProposedItem.getProperty("SEQUENCE") || 0; + let existingSequence = aExistingItem.getProperty("SEQUENCE") || 0; + if (existingSequence >= proposedSequence) { + if (existingSequence > proposedSequence) { + // in this case we prompt the organizer with the additional information that the + // received proposal refers to an outdated version of the event + status.descr = "This is a counterproposal to an already rescheduled event."; + status.type = "OUTDATED"; + } else if (aProposedItem.stampTime.compare(aExistingItem.stampTime) == -1) { + // now this is the same sequence but the proposal is not based on the latest + // update of the event - updated events may have minor changes, while for major + // ones there has been a rescheduling + status.descr = "This is a counterproposal not based on the latest event update."; + status.type = "NOTLATESTUPDATE"; + } + for (let prop of properties) { + let newValue = aProposedItem.getProperty(prop) || null; + let oldValue = aExistingItem.getProperty(prop) || null; + if ( + (["DTSTART", "DTEND"].includes(prop) && newValue.toString() != oldValue.toString()) || + (!["DTSTART", "DTEND"].includes(prop) && newValue != oldValue) + ) { + diff.push({ + property: prop, + proposed: newValue, + original: oldValue, + }); + } + } + } else { + status.descr = "Invalid sequence number in counterproposal."; + status.type = "ERROR"; + } + } else { + status.descr = "Mismatch of uid or organizer in counterproposal."; + status.type = "ERROR"; + } + if (status.type != "ERROR" && !diff.length) { + status.descr = "No difference in counterproposal detected."; + status.type = "NODIFF"; + } + return { result: status, differences: diff }; + }, + + /** + * The HTML template used to format invitations for display. + * This used to be in a separate file (invitation-template.xhtml) and should + * probably be moved back there. But loading on-the-fly was causing a nasty + * C++ reentrancy issue (see bug 1679299). + */ + htmlTemplate: `<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <link rel="stylesheet" href="chrome://messagebody/skin/imip.css" /> + <link rel="stylesheet" href="chrome://messagebody/skin/calendar-attendees.css" /> + </head> + <body> + <details id="imipHTMLDetails" class="invitation-details"> + <summary id="imipHtml-header"></summary> + <div class="invitation-border"> + <table class="invitation-table"> + <tr id="imipHtml-summary-row" hidden="hidden"> + <th id="imipHtml-summary-descr" class="description" scope="row"></th> + <td id="imipHtml-summary-content" class="content"></td> + </tr> + <tr id="imipHtml-location-row" hidden="hidden"> + <th id="imipHtml-location-descr" class="description" scope="row"></th> + <td id="imipHtml-location-content" class="content"></td> + </tr> + <tr id="imipHtml-when-row" hidden="hidden"> + <th id="imipHtml-when-descr" class="description" scope="row"></th> + <td id="imipHtml-when-content" class="content"></td> + </tr> + <tr id="imipHtml-canceledOccurrences-row" hidden="hidden"> + <th id="imipHtml-canceledOccurrences-descr" + class="description" + scope="row"> + </th> + <td id="imipHtml-canceledOccurrences-content" class="content"></td> + </tr> + <tr id="imipHtml-modifiedOccurrences-row" hidden="hidden"> + <th id="imipHtml-modifiedOccurrences-descr" + class="description" + scope="row"> + </th> + <td id="imipHtml-modifiedOccurrences-content" class="content"></td> + </tr> + <tr id="imipHtml-organizer-row" hidden="hidden"> + <th id="imipHtml-organizer-descr" + class="description" + scope="row"> + </th> + <td id="imipHtml-organizer-cell" class="content"></td> + </tr> + <tr id="imipHtml-description-row" hidden="hidden"> + <th id="imipHtml-description-descr" + class="description" + scope="row"> + </th> + <td id="imipHtml-description-content" class="content"></td> + </tr> + <tr id="imipHtml-attachments-row" hidden="hidden"> + <th id="imipHtml-attachments-descr" + class="description" + scope="row"></th> + <td id="imipHtml-attachments-content" class="content"></td> + </tr> + <tr id="imipHtml-comment-row" hidden="hidden"> + <th id="imipHtml-comment-descr" class="description" scope="row"></th> + <td id="imipHtml-comment-content" class="content"></td> + </tr> + <tr id="imipHtml-attendees-row" hidden="hidden"> + <th id="imipHtml-attendees-descr" + class="description" + scope="row"> + </th> + <td id="imipHtml-attendees-cell" class="content"></td> + </tr> + <tr id="imipHtml-url-row" hidden="hidden"> + <th id="imipHtml-url-descr" class="description" scope="row"></th> + <td id="imipHtml-url-content" class="content"></td> + </tr> + </table> + </div> + </details> + </body> +</html> +`, +}; |