summaryrefslogtreecommitdiffstats
path: root/comm/calendar/base/modules/utils/calItipUtils.jsm
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--comm/calendar/base/modules/utils/calItipUtils.jsm2181
1 files changed, 2181 insertions, 0 deletions
diff --git a/comm/calendar/base/modules/utils/calItipUtils.jsm b/comm/calendar/base/modules/utils/calItipUtils.jsm
new file mode 100644
index 0000000000..fdfe8750c6
--- /dev/null
+++ b/comm/calendar/base/modules/utils/calItipUtils.jsm
@@ -0,0 +1,2181 @@
+/* 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/. */
+
+/**
+ * Scheduling and iTIP helper code
+ */
+
+// NOTE: This module should not be loaded directly, it is available when
+// including calUtils.jsm under the cal.itip namespace.
+
+const EXPORTED_SYMBOLS = ["calitip"];
+
+var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
+var { calendarDeactivator } = ChromeUtils.import(
+ "resource:///modules/calendar/calCalendarDeactivator.jsm"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
+
+const lazy = {};
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ CalAttendee: "resource:///modules/CalAttendee.jsm",
+ CalRelation: "resource:///modules/CalRelation.jsm",
+ CalItipDefaultEmailTransport: "resource:///modules/CalItipEmailTransport.jsm",
+ CalItipMessageSender: "resource:///modules/CalItipMessageSender.jsm",
+ CalItipOutgoingMessage: "resource:///modules/CalItipOutgoingMessage.jsm",
+});
+ChromeUtils.defineModuleGetter(lazy, "cal", "resource:///modules/calendar/calUtils.jsm");
+
+var calitip = {
+ /**
+ * Gets the sequence/revision number, either of the passed item or the last received one of an
+ * attendee; see <http://tools.ietf.org/html/draft-desruisseaux-caldav-sched-04#section-7.1>.
+ *
+ * @param {calIAttendee|calIItemBase} aItem - The item or attendee to get the sequence info
+ * from.
+ * @returns {number} The sequence number
+ */
+ getSequence(aItem) {
+ let seq = null;
+
+ if (calitip.isAttendee(aItem)) {
+ seq = aItem.getProperty("RECEIVED-SEQUENCE");
+ } else if (aItem) {
+ // Unless the below is standardized, we store the last original
+ // REQUEST/PUBLISH SEQUENCE in X-MOZ-RECEIVED-SEQUENCE to test against it
+ // when updates come in:
+ seq = aItem.getProperty("X-MOZ-RECEIVED-SEQUENCE");
+ if (seq === null) {
+ seq = aItem.getProperty("SEQUENCE");
+ }
+
+ // Make sure we don't have a pre Outlook 2007 appointment, but if we do
+ // use Microsoft's Sequence number. I <3 MS
+ if (seq === null || seq == "0") {
+ seq = aItem.getProperty("X-MICROSOFT-CDO-APPT-SEQUENCE");
+ }
+ }
+
+ if (seq === null) {
+ return 0;
+ }
+ seq = parseInt(seq, 10);
+ return isNaN(seq) ? 0 : seq;
+ },
+
+ /**
+ * Gets the stamp date-time, either of the passed item or the last received one of an attendee;
+ * see <http://tools.ietf.org/html/draft-desruisseaux-caldav-sched-04#section-7.2>.
+ *
+ * @param {calIAttendee|calIItemBase} aItem - The item or attendee to retrieve the stamp from
+ * @returns {calIDateTime} The timestamp for the item
+ */
+ getStamp(aItem) {
+ let dtstamp = null;
+
+ if (calitip.isAttendee(aItem)) {
+ let stamp = aItem.getProperty("RECEIVED-DTSTAMP");
+ if (stamp) {
+ dtstamp = lazy.cal.createDateTime(stamp);
+ }
+ } else if (aItem) {
+ // Unless the below is standardized, we store the last original
+ // REQUEST/PUBLISH DTSTAMP in X-MOZ-RECEIVED-DTSTAMP to test against it
+ // when updates come in:
+ let stamp = aItem.getProperty("X-MOZ-RECEIVED-DTSTAMP");
+ if (stamp) {
+ dtstamp = lazy.cal.createDateTime(stamp);
+ } else {
+ // xxx todo: are there similar X-MICROSOFT-CDO properties to be considered here?
+ dtstamp = aItem.stampTime;
+ }
+ }
+
+ return dtstamp;
+ },
+
+ /**
+ * Compares sequences and/or stamps of two items
+ *
+ * @param {calIItemBase|calIAttendee} aItem1 - The first item to compare
+ * @param {calIItemBase|calIAttendee} aItem2 - The second item to compare
+ * @returns {number} +1 if item2 is newer, -1 if item1 is newer
+ * or 0 if both are equal
+ */
+ compare(aItem1, aItem2) {
+ let comp = calitip.compareSequence(aItem1, aItem2);
+ if (comp == 0) {
+ comp = calitip.compareStamp(aItem1, aItem2);
+ }
+ return comp;
+ },
+
+ /**
+ * Compares sequences of two items
+ *
+ * @param {calIItemBase|calIAttendee} aItem1 - The first item to compare
+ * @param {calIItemBase|calIAttendee} aItem2 - The second item to compare
+ * @returns {number} +1 if item2 is newer, -1 if item1 is newer
+ * or 0 if both are equal
+ */
+ compareSequence(aItem1, aItem2) {
+ let seq1 = calitip.getSequence(aItem1);
+ let seq2 = calitip.getSequence(aItem2);
+ if (seq1 > seq2) {
+ return 1;
+ } else if (seq1 < seq2) {
+ return -1;
+ }
+ return 0;
+ },
+
+ /**
+ * Compares stamp of two items
+ *
+ * @param {calIItemBase|calIAttendee} aItem1 - The first item to compare
+ * @param {calIItemBase|calIAttendee} aItem2 - The second item to compare
+ * @returns {number} +1 if item2 is newer, -1 if item1 is newer
+ * or 0 if both are equal
+ */
+ compareStamp(aItem1, aItem2) {
+ let st1 = calitip.getStamp(aItem1);
+ let st2 = calitip.getStamp(aItem2);
+ if (st1 && st2) {
+ return st1.compare(st2);
+ } else if (!st1 && st2) {
+ return -1;
+ } else if (st1 && !st2) {
+ return 1;
+ }
+ return 0;
+ },
+
+ /**
+ * Creates an organizer calIAttendee object based on the calendar's configured organizer id.
+ *
+ * @param {calICalendar} aCalendar - The calendar to get the organizer id from
+ * @returns {calIAttendee} The organizer attendee
+ */
+ createOrganizer(aCalendar) {
+ let orgId = aCalendar.getProperty("organizerId");
+ if (!orgId) {
+ return null;
+ }
+ let organizer = new lazy.CalAttendee();
+ organizer.id = orgId;
+ organizer.commonName = aCalendar.getProperty("organizerCN");
+ organizer.role = "REQ-PARTICIPANT";
+ organizer.participationStatus = "ACCEPTED";
+ organizer.isOrganizer = true;
+ return organizer;
+ },
+
+ /**
+ * Checks if the given calendar is a scheduling calendar. This means it
+ * needs an organizer id and an itip transport. It should also be writable.
+ *
+ * @param {calICalendar} aCalendar - The calendar to check
+ * @returns {boolean} True, if its a scheduling calendar.
+ */
+ isSchedulingCalendar(aCalendar) {
+ return (
+ lazy.cal.acl.isCalendarWritable(aCalendar) &&
+ aCalendar.getProperty("organizerId") &&
+ aCalendar.getProperty("itip.transport")
+ );
+ },
+
+ /**
+ * Scope: iTIP message receiver
+ *
+ * Given an nsIMsgDBHdr and an imipMethod, set up the given itip item.
+ *
+ * @param {calIItemBase} itipItem - The item to set up
+ * @param {string} imipMethod - The received imip method
+ * @param {nsIMsgDBHdr} aMsgHdr - Information about the received email
+ */
+ initItemFromMsgData(itipItem, imipMethod, aMsgHdr) {
+ // set the sender of the itip message
+ itipItem.sender = calitip.getMessageSender(aMsgHdr);
+
+ // Get the recipient identity and save it with the itip item.
+ itipItem.identity = calitip.getMessageRecipient(aMsgHdr);
+
+ // We are only called upon receipt of an invite, so ensure that isSend
+ // is false.
+ itipItem.isSend = false;
+
+ // XXX Get these from preferences
+ itipItem.autoResponse = Ci.calIItipItem.USER;
+
+ if (imipMethod && imipMethod.length != 0 && imipMethod.toLowerCase() != "nomethod") {
+ itipItem.receivedMethod = imipMethod.toUpperCase();
+ } else {
+ // There is no METHOD in the content-type header (spec violation).
+ // Fall back to using the one from the itipItem's ICS.
+ imipMethod = itipItem.receivedMethod;
+ }
+ lazy.cal.LOG("iTIP method: " + imipMethod);
+
+ let isWritableCalendar = function (aCalendar) {
+ /* TODO: missing ACL check for existing items (require callback API) */
+ return (
+ calitip.isSchedulingCalendar(aCalendar) && lazy.cal.acl.userCanAddItemsToCalendar(aCalendar)
+ );
+ };
+
+ let writableCalendars = lazy.cal.manager.getCalendars().filter(isWritableCalendar);
+ if (writableCalendars.length > 0) {
+ let compCal = Cc["@mozilla.org/calendar/calendar;1?type=composite"].createInstance(
+ Ci.calICompositeCalendar
+ );
+ writableCalendars.forEach(compCal.addCalendar, compCal);
+ itipItem.targetCalendar = compCal;
+ }
+ },
+
+ /**
+ * Scope: iTIP message receiver
+ *
+ * Gets the suggested text to be shown when an imip item has been processed.
+ * This text is ready localized and can be displayed to the user.
+ *
+ * @param {number} aStatus - The status of the processing (i.e NS_OK, an error code)
+ * @param {number} aOperationType - An operation type from calIOperationListener
+ * @returns {string} The suggested text.
+ */
+ getCompleteText(aStatus, aOperationType) {
+ let text = "";
+ const cIOL = Ci.calIOperationListener;
+ if (Components.isSuccessCode(aStatus)) {
+ switch (aOperationType) {
+ case cIOL.ADD:
+ text = lazy.cal.l10n.getLtnString("imipAddedItemToCal2");
+ break;
+ case cIOL.MODIFY:
+ text = lazy.cal.l10n.getLtnString("imipUpdatedItem2");
+ break;
+ case cIOL.DELETE:
+ text = lazy.cal.l10n.getLtnString("imipCanceledItem2");
+ break;
+ }
+ } else {
+ text = lazy.cal.l10n.getLtnString("imipBarProcessingFailed", [aStatus.toString(16)]);
+ }
+ return text;
+ },
+
+ /**
+ * Scope: iTIP message receiver
+ *
+ * Gets a text describing the given itip method. The text is of the form
+ * "This Message contains a ... ".
+ *
+ * @param {string} method - The method to describe.
+ * @returns {string} The localized text about the method.
+ */
+ getMethodText(method) {
+ switch (method) {
+ case "REFRESH":
+ return lazy.cal.l10n.getLtnString("imipBarRefreshText");
+ case "REQUEST":
+ return lazy.cal.l10n.getLtnString("imipBarRequestText");
+ case "PUBLISH":
+ return lazy.cal.l10n.getLtnString("imipBarPublishText");
+ case "CANCEL":
+ return lazy.cal.l10n.getLtnString("imipBarCancelText");
+ case "REPLY":
+ return lazy.cal.l10n.getLtnString("imipBarReplyText");
+ case "COUNTER":
+ return lazy.cal.l10n.getLtnString("imipBarCounterText");
+ case "DECLINECOUNTER":
+ return lazy.cal.l10n.getLtnString("imipBarDeclineCounterText");
+ default:
+ lazy.cal.ERROR("Unknown iTIP method: " + method);
+ let appName = lazy.cal.l10n.getAnyString("branding", "brand", "brandShortName");
+ return lazy.cal.l10n.getLtnString("imipBarUnsupportedText2", [appName]);
+ }
+ },
+
+ /**
+ * Scope: iTIP message receiver
+ *
+ * Gets localized toolbar label about the message state and triggers buttons to show.
+ * This returns a JS object with the following structure:
+ *
+ * {
+ * label: "This is a desciptive text about the itip item",
+ * showItems: ["imipXXXButton", ...],
+ * hideItems: ["imipXXXButton_Option", ...]
+ * }
+ *
+ * @see processItipItem This takes the same parameters as its optionFunc.
+ * @param {calIItipItem} itipItem - The itipItem to query.
+ * @param {number} rc - The result of retrieving the item
+ * @param {Function} actionFunc - The action function.
+ * @param {calIItemBase[]} foundItems - An array of items found while searching for the item
+ * in subscribed calendars
+ * @returns {object} Return information about the options
+ */
+ getOptionsText(itipItem, rc, actionFunc, foundItems) {
+ let imipLabel = null;
+ if (itipItem.receivedMethod) {
+ imipLabel = calitip.getMethodText(itipItem.receivedMethod);
+ }
+ let data = { label: imipLabel, showItems: [], hideItems: [] };
+ let separateButtons = Services.prefs.getBoolPref(
+ "calendar.itip.separateInvitationButtons",
+ false
+ );
+
+ let disallowedCounter = false;
+ if (foundItems && foundItems.length) {
+ let disallow = foundItems[0].getProperty("X-MICROSOFT-DISALLOW-COUNTER");
+ disallowedCounter = disallow && disallow == "TRUE";
+ }
+ if (!calendarDeactivator.isCalendarActivated) {
+ // Calendar is deactivated (no calendars are enabled).
+ data.label = lazy.cal.l10n.getLtnString("imipBarCalendarDeactivated");
+ data.showItems.push("imipGoToCalendarButton", "imipMoreButton");
+ data.hideItems.push("imipMoreButton_SaveCopy");
+ } else if (rc == Ci.calIErrors.CAL_IS_READONLY) {
+ // No writable calendars, tell the user about it
+ data.label = lazy.cal.l10n.getLtnString("imipBarNotWritable");
+ data.showItems.push("imipGoToCalendarButton", "imipMoreButton");
+ data.hideItems.push("imipMoreButton_SaveCopy");
+ } else if (Components.isSuccessCode(rc) && !actionFunc) {
+ // This case, they clicked on an old message that has already been
+ // added/updated, we want to tell them that.
+ data.label = lazy.cal.l10n.getLtnString("imipBarAlreadyProcessedText");
+ if (foundItems && foundItems.length) {
+ data.showItems.push("imipDetailsButton");
+ if (itipItem.receivedMethod == "COUNTER" && itipItem.sender) {
+ if (disallowedCounter) {
+ data.label = lazy.cal.l10n.getLtnString("imipBarDisallowedCounterText");
+ } else {
+ let comparison;
+ for (let item of itipItem.getItemList()) {
+ let attendees = lazy.cal.itip.getAttendeesBySender(
+ item.getAttendees(),
+ itipItem.sender
+ );
+ if (attendees.length == 1) {
+ comparison = calitip.compareSequence(item, foundItems[0]);
+ if (comparison == 1) {
+ data.label = lazy.cal.l10n.getLtnString("imipBarCounterErrorText");
+ break;
+ } else if (comparison == -1) {
+ data.label = lazy.cal.l10n.getLtnString("imipBarCounterPreviousVersionText");
+ }
+ }
+ }
+ }
+ }
+ } else if (itipItem.receivedMethod == "REPLY") {
+ // The item has been previously removed from the available calendars or the calendar
+ // containing the item is not available
+ let delmgr = Cc["@mozilla.org/calendar/deleted-items-manager;1"].getService(
+ Ci.calIDeletedItems
+ );
+ let delTime = null;
+ let items = itipItem.getItemList();
+ if (items && items.length) {
+ delTime = delmgr.getDeletedDate(items[0].id);
+ }
+ if (delTime) {
+ data.label = lazy.cal.l10n.getLtnString("imipBarReplyToRecentlyRemovedItem", [
+ lazy.cal.dtz.formatter.formatTime(delTime),
+ ]);
+ } else {
+ data.label = lazy.cal.l10n.getLtnString("imipBarReplyToNotExistingItem");
+ }
+ } else if (itipItem.receivedMethod == "DECLINECOUNTER") {
+ data.label = lazy.cal.l10n.getLtnString("imipBarDeclineCounterText");
+ }
+ } else if (Components.isSuccessCode(rc)) {
+ lazy.cal.LOG("iTIP options on: " + actionFunc.method);
+ switch (actionFunc.method) {
+ case "PUBLISH:UPDATE":
+ case "REQUEST:UPDATE-MINOR":
+ data.label = lazy.cal.l10n.getLtnString("imipBarUpdateText");
+ // falls through
+ case "REPLY":
+ data.showItems.push("imipUpdateButton");
+ break;
+ case "PUBLISH":
+ data.showItems.push("imipAddButton");
+ break;
+ case "REQUEST:UPDATE":
+ case "REQUEST:NEEDS-ACTION":
+ case "REQUEST": {
+ let isRecurringMaster = false;
+ for (let item of itipItem.getItemList()) {
+ if (item.recurrenceInfo) {
+ isRecurringMaster = true;
+ }
+ }
+
+ if (actionFunc.method == "REQUEST:UPDATE") {
+ if (isRecurringMaster) {
+ data.label = lazy.cal.l10n.getLtnString("imipBarUpdateSeriesText");
+ } else if (itipItem.getItemList().length > 1) {
+ data.label = lazy.cal.l10n.getLtnString("imipBarUpdateMultipleText");
+ } else {
+ data.label = lazy.cal.l10n.getLtnString("imipBarUpdateText");
+ }
+ } else if (actionFunc.method == "REQUEST:NEEDS-ACTION") {
+ if (isRecurringMaster) {
+ data.label = lazy.cal.l10n.getLtnString("imipBarProcessedSeriesNeedsAction");
+ } else if (itipItem.getItemList().length > 1) {
+ data.label = lazy.cal.l10n.getLtnString("imipBarProcessedMultipleNeedsAction");
+ } else {
+ data.label = lazy.cal.l10n.getLtnString("imipBarProcessedNeedsAction");
+ }
+ }
+
+ if (itipItem.getItemList().length > 1 || isRecurringMaster) {
+ data.showItems.push("imipAcceptRecurrencesButton");
+ if (separateButtons) {
+ data.showItems.push("imipTentativeRecurrencesButton");
+ data.hideItems.push("imipAcceptRecurrencesButton_AcceptLabel");
+ data.hideItems.push("imipAcceptRecurrencesButton_TentativeLabel");
+ data.hideItems.push("imipAcceptRecurrencesButton_Tentative");
+ data.hideItems.push("imipAcceptRecurrencesButton_TentativeDontSend");
+ } else {
+ data.hideItems.push("imipTentativeRecurrencesButton");
+ data.showItems.push("imipAcceptRecurrencesButton_AcceptLabel");
+ data.showItems.push("imipAcceptRecurrencesButton_TentativeLabel");
+ data.showItems.push("imipAcceptRecurrencesButton_Tentative");
+ data.showItems.push("imipAcceptRecurrencesButton_TentativeDontSend");
+ }
+ data.showItems.push("imipDeclineRecurrencesButton");
+ } else {
+ data.showItems.push("imipAcceptButton");
+ if (separateButtons) {
+ data.showItems.push("imipTentativeButton");
+ data.hideItems.push("imipAcceptButton_AcceptLabel");
+ data.hideItems.push("imipAcceptButton_TentativeLabel");
+ data.hideItems.push("imipAcceptButton_Tentative");
+ data.hideItems.push("imipAcceptButton_TentativeDontSend");
+ } else {
+ data.hideItems.push("imipTentativeButton");
+ data.showItems.push("imipAcceptButton_AcceptLabel");
+ data.showItems.push("imipAcceptButton_TentativeLabel");
+ data.showItems.push("imipAcceptButton_Tentative");
+ data.showItems.push("imipAcceptButton_TentativeDontSend");
+ }
+ data.showItems.push("imipDeclineButton");
+ }
+ data.showItems.push("imipMoreButton");
+ // Use data.hideItems.push("idOfMenuItem") to hide specific menuitems
+ // from the dropdown menu of a button. This might be useful to remove
+ // a generally available option for a specific invitation, because the
+ // respective feature is not available for the calendar, the invitation
+ // is in or the feature is prohibited by the organizer
+ break;
+ }
+ case "CANCEL": {
+ data.showItems.push("imipDeleteButton");
+ break;
+ }
+ case "REFRESH": {
+ data.showItems.push("imipReconfirmButton");
+ break;
+ }
+ case "COUNTER": {
+ if (disallowedCounter) {
+ data.label = lazy.cal.l10n.getLtnString("imipBarDisallowedCounterText");
+ }
+ data.showItems.push("imipDeclineCounterButton");
+ data.showItems.push("imipRescheduleButton");
+ break;
+ }
+ default:
+ let appName = lazy.cal.l10n.getAnyString("branding", "brand", "brandShortName");
+ data.label = lazy.cal.l10n.getLtnString("imipBarUnsupportedText2", [appName]);
+ break;
+ }
+ } else {
+ let appName = lazy.cal.l10n.getAnyString("branding", "brand", "brandShortName");
+ data.label = lazy.cal.l10n.getLtnString("imipBarUnsupportedText2", [appName]);
+ }
+
+ return data;
+ },
+
+ /**
+ * Scope: iTIP message receiver
+ * Retrieves the message sender.
+ *
+ * @param {nsIMsgDBHdr} aMsgHdr - The message header to check.
+ * @returns {string} The email address of the intended recipient.
+ */
+ getMessageSender(aMsgHdr) {
+ let author = (aMsgHdr && aMsgHdr.author) || "";
+ let compFields = Cc["@mozilla.org/messengercompose/composefields;1"].createInstance(
+ Ci.nsIMsgCompFields
+ );
+ let addresses = compFields.splitRecipients(author, true);
+ if (addresses.length != 1) {
+ lazy.cal.LOG("No unique email address for lookup in message.\r\n" + lazy.cal.STACK(20));
+ }
+ return addresses[0] || null;
+ },
+
+ /**
+ * Scope: iTIP message receiver
+ *
+ * Retrieves the intended recipient for this message.
+ *
+ * @param {nsIMsgDBHdr} aMsgHdr - The message to check.
+ * @returns {string} The email of the intended recipient.
+ */
+ getMessageRecipient(aMsgHdr) {
+ if (!aMsgHdr) {
+ return null;
+ }
+
+ let identities;
+ if (aMsgHdr.accountKey) {
+ // First, check if the message has an account key. If so, we can use the
+ // account identities to find the correct recipient
+ identities = MailServices.accounts.getAccount(aMsgHdr.accountKey).identities;
+ } else if (aMsgHdr.folder) {
+ // Without an account key, we have to revert back to using the server
+ identities = MailServices.accounts.getIdentitiesForServer(aMsgHdr.folder.server);
+ }
+
+ let emailMap = {};
+ if (!identities || identities.length == 0) {
+ let identity;
+ // If we were not able to retrieve identities above, then we have no
+ // choice but to revert to the default identity.
+ let defaultAccount = MailServices.accounts.defaultAccount;
+ if (defaultAccount) {
+ identity = defaultAccount.defaultIdentity;
+ }
+ if (!identity) {
+ // If there isn't a default identity (i.e Local Folders is your
+ // default identity), then go ahead and use the first available
+ // identity.
+ let allIdentities = MailServices.accounts.allIdentities;
+ if (allIdentities.length > 0) {
+ identity = allIdentities[0];
+ } else {
+ // If there are no identities at all, we cannot get a recipient.
+ return null;
+ }
+ }
+ emailMap[identity.email.toLowerCase()] = true;
+ } else {
+ // Build a map of usable email addresses
+ for (let identity of identities) {
+ emailMap[identity.email.toLowerCase()] = true;
+ }
+ }
+
+ // First check the recipient list
+ let toList = MailServices.headerParser.makeFromDisplayAddress(aMsgHdr.recipients || "");
+ for (let recipient of toList) {
+ if (recipient.email.toLowerCase() in emailMap) {
+ // Return the first found recipient
+ return recipient;
+ }
+ }
+
+ // Maybe we are in the CC list?
+ let ccList = MailServices.headerParser.makeFromDisplayAddress(aMsgHdr.ccList || "");
+ for (let recipient of ccList) {
+ if (recipient.email.toLowerCase() in emailMap) {
+ // Return the first found recipient
+ return recipient;
+ }
+ }
+
+ // Hrmpf. Looks like delegation or maybe Bcc.
+ return null;
+ },
+
+ /**
+ * Executes an action from a calandar message.
+ *
+ * @param {nsIWindow} aWindow - The current window
+ * @param {string} aParticipantStatus - A partstat string as per RfC 5545
+ * @param {string} aResponse - Either 'AUTO', 'NONE' or 'USER', see
+ * calItipItem interface
+ * @param {Function} aActionFunc - The function to call to do the scheduling
+ * operation
+ * @param {calIItipItem} aItipItem - Scheduling item
+ * @param {array} aFoundItems - The items found when looking for the calendar item
+ * @param {Function} aUpdateFunction - A function to call which will update the UI
+ * @returns {boolean} true, if the action succeeded
+ */
+ executeAction(
+ aWindow,
+ aParticipantStatus,
+ aResponse,
+ aActionFunc,
+ aItipItem,
+ aFoundItems,
+ aUpdateFunction
+ ) {
+ // control to avoid processing _execAction on later user changes on the item
+ let isFirstProcessing = true;
+
+ /**
+ * Internal function to trigger an scheduling operation
+ *
+ * @param {Function} aActionFunc - The function to call to do the
+ * scheduling operation
+ * @param {calIItipItem} aItipItem - Scheduling item
+ * @param {nsIWindow} aWindow - The current window
+ * @param {string} aPartStat - partstat string as per RFC 5545
+ * @param {object} aExtResponse - JS object containing at least an responseMode
+ * property
+ * @returns {boolean} true, if the action succeeded
+ */
+ function _execAction(aActionFunc, aItipItem, aWindow, aPartStat, aExtResponse) {
+ let method = aActionFunc.method;
+ if (lazy.cal.itip.promptCalendar(aActionFunc.method, aItipItem, aWindow)) {
+ if (
+ method == "REQUEST" &&
+ !lazy.cal.itip.promptInvitedAttendee(aWindow, aItipItem, Ci.calIItipItem[aResponse])
+ ) {
+ return false;
+ }
+
+ let isDeclineCounter = aPartStat == "X-DECLINECOUNTER";
+ // filter out fake partstats
+ if (aPartStat.startsWith("X-")) {
+ aParticipantStatus = "";
+ }
+ // hide the buttons now, to disable pressing them twice...
+ if (aPartStat == aParticipantStatus) {
+ aUpdateFunction({ resetButtons: true });
+ }
+
+ let opListener = {
+ QueryInterface: ChromeUtils.generateQI(["calIOperationListener"]),
+ onOperationComplete(aCalendar, aStatus, aOperationType, aId, aDetail) {
+ isFirstProcessing = false;
+ if (Components.isSuccessCode(aStatus) && isDeclineCounter) {
+ // TODO: move the DECLINECOUNTER stuff to actionFunc
+ aItipItem.getItemList().forEach(aItem => {
+ // we can rely on the received itipItem to reply at this stage
+ // already, the checks have been done in cal.itip.processFoundItems
+ // when setting up the respective aActionFunc
+ let attendees = lazy.cal.itip.getAttendeesBySender(
+ aItem.getAttendees(),
+ aItipItem.sender
+ );
+ let status = true;
+ if (attendees.length == 1 && aFoundItems?.length) {
+ // we must return a message with the same sequence number as the
+ // counterproposal - to make it easy, we simply use the received
+ // item and just remove a comment, if any
+ try {
+ let item = aItem.clone();
+ item.calendar = aFoundItems[0].calendar;
+ item.deleteProperty("COMMENT");
+ // once we have full support to deal with for multiple items
+ // in a received invitation message, we should send this
+ // from outside outside of the forEach context
+ status = lazy.cal.itip.sendDeclineCounterMessage(
+ item,
+ "DECLINECOUNTER",
+ attendees,
+ {
+ value: false,
+ }
+ );
+ } catch (e) {
+ lazy.cal.ERROR(e);
+ status = false;
+ }
+ } else {
+ status = false;
+ }
+ if (!status) {
+ lazy.cal.ERROR("Failed to send DECLINECOUNTER reply!");
+ }
+ });
+ }
+ // For now, we just state the status for the user something very simple
+ let label = lazy.cal.itip.getCompleteText(aStatus, aOperationType);
+ aUpdateFunction({ label });
+
+ if (!Components.isSuccessCode(aStatus)) {
+ lazy.cal.showError(label);
+ return;
+ }
+
+ if (Services.prefs.getBoolPref("calendar.itip.newInvitationDisplay")) {
+ aWindow.dispatchEvent(
+ new CustomEvent("onItipItemActionFinished", { detail: aItipItem })
+ );
+ }
+ },
+ onGetResult(calendar, status, itemType, detail, items) {},
+ };
+
+ try {
+ aActionFunc(opListener, aParticipantStatus, aExtResponse);
+ } catch (exc) {
+ console.error(exc);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ if (aParticipantStatus == null) {
+ aParticipantStatus = "";
+ }
+ if (aParticipantStatus == "X-SHOWDETAILS" || aParticipantStatus == "X-RESCHEDULE") {
+ let counterProposal;
+ if (aFoundItems?.length) {
+ let item = aFoundItems[0].isMutable ? aFoundItems[0] : aFoundItems[0].clone();
+
+ if (aParticipantStatus == "X-RESCHEDULE") {
+ // TODO most of the following should be moved to the actionFunc defined in
+ // calItipUtils
+ let proposedItem = aItipItem.getItemList()[0];
+ let proposedRID = proposedItem.getProperty("RECURRENCE-ID");
+ if (proposedRID) {
+ // if this is a counterproposal for a specific occurrence, we use
+ // that to compare with
+ item = item.recurrenceInfo.getOccurrenceFor(proposedRID).clone();
+ }
+ let parsedProposal = lazy.cal.invitation.parseCounter(proposedItem, item);
+ let potentialProposers = lazy.cal.itip.getAttendeesBySender(
+ proposedItem.getAttendees(),
+ aItipItem.sender
+ );
+ let proposingAttendee = potentialProposers.length == 1 ? potentialProposers[0] : null;
+ if (
+ proposingAttendee &&
+ ["OK", "OUTDATED", "NOTLATESTUPDATE"].includes(parsedProposal.result.type)
+ ) {
+ counterProposal = {
+ attendee: proposingAttendee,
+ proposal: parsedProposal.differences,
+ oldVersion:
+ parsedProposal.result == "OLDVERSION" || parsedProposal.result == "NOTLATESTUPDATE",
+ onReschedule: () => {
+ aUpdateFunction({
+ label: lazy.cal.l10n.getLtnString("imipBarCounterPreviousVersionText"),
+ });
+ // TODO: should we hide the buttons in this case, too?
+ },
+ };
+ } else {
+ aUpdateFunction({
+ label: lazy.cal.l10n.getLtnString("imipBarCounterErrorText"),
+ resetButtons: true,
+ });
+ if (proposingAttendee) {
+ lazy.cal.LOG(parsedProposal.result.descr);
+ } else {
+ lazy.cal.LOG("Failed to identify the sending attendee of the counterproposal.");
+ }
+
+ return false;
+ }
+ }
+ // if this a rescheduling operation, we suppress the occurrence
+ // prompt here
+ aWindow.modifyEventWithDialog(
+ item,
+ aParticipantStatus != "X-RESCHEDULE",
+ null,
+ counterProposal
+ );
+ }
+ } else {
+ let response;
+ if (aResponse) {
+ if (aResponse == "AUTO" || aResponse == "NONE" || aResponse == "USER") {
+ response = { responseMode: Ci.calIItipItem[aResponse] };
+ }
+ // Open an extended response dialog to enable the user to add a comment, make a
+ // counterproposal, delegate the event or interact in another way.
+ // Instead of a dialog, this might be implemented as a separate container inside the
+ // imip-overlay as proposed in bug 458578
+ }
+ let delmgr = Cc["@mozilla.org/calendar/deleted-items-manager;1"].getService(
+ Ci.calIDeletedItems
+ );
+ let items = aItipItem.getItemList();
+ if (items && items.length) {
+ let delTime = delmgr.getDeletedDate(items[0].id);
+ let dialogText = lazy.cal.l10n.getLtnString("confirmProcessInvitation");
+ let dialogTitle = lazy.cal.l10n.getLtnString("confirmProcessInvitationTitle");
+ if (delTime && !Services.prompt.confirm(aWindow, dialogTitle, dialogText)) {
+ return false;
+ }
+ }
+
+ if (aParticipantStatus == "X-SAVECOPY") {
+ // we create and adopt copies of the respective events
+ let saveitems = aItipItem
+ .getItemList()
+ .map(lazy.cal.itip.getPublishLikeItemCopy.bind(lazy.cal));
+ if (saveitems.length > 0) {
+ let methods = { receivedMethod: "PUBLISH", responseMethod: "PUBLISH" };
+ let newItipItem = lazy.cal.itip.getModifiedItipItem(aItipItem, saveitems, methods);
+ // setup callback and trigger re-processing
+ let storeCopy = function (aItipItem, aRc, aActionFunc, aFoundItems) {
+ if (isFirstProcessing && aActionFunc && Components.isSuccessCode(aRc)) {
+ _execAction(aActionFunc, aItipItem, aWindow, aParticipantStatus);
+ }
+ };
+ lazy.cal.itip.processItipItem(newItipItem, storeCopy);
+ }
+ // we stop here to not process the original item
+ return false;
+ }
+ return _execAction(aActionFunc, aItipItem, aWindow, aParticipantStatus, response);
+ }
+ return false;
+ },
+
+ /**
+ * Scope: iTIP message receiver
+ *
+ * Prompt for the target calendar, if needed for the given method. This calendar will be set on
+ * the passed itip item.
+ *
+ * @param {string} aMethod - The method to check.
+ * @param {calIItipItem} aItipItem - The itip item to set the target calendar on.
+ * @param {DOMWindpw} aWindow - The window to open the dialog on.
+ * @returns {boolean} True, if a calendar was selected or no selection is needed.
+ */
+ promptCalendar(aMethod, aItipItem, aWindow) {
+ let needsCalendar = false;
+ let targetCalendar = null;
+ switch (aMethod) {
+ // methods that don't require the calendar chooser:
+ case "REFRESH":
+ case "REQUEST:UPDATE":
+ case "REQUEST:UPDATE-MINOR":
+ case "PUBLISH:UPDATE":
+ case "REPLY":
+ case "CANCEL":
+ case "COUNTER":
+ case "DECLINECOUNTER":
+ needsCalendar = false;
+ break;
+ default:
+ needsCalendar = true;
+ break;
+ }
+
+ if (needsCalendar) {
+ let calendars = lazy.cal.manager.getCalendars().filter(calitip.isSchedulingCalendar);
+
+ if (aItipItem.receivedMethod == "REQUEST") {
+ // try to further limit down the list to those calendars that
+ // are configured to a matching attendee;
+ let item = aItipItem.getItemList()[0];
+ let matchingCals = calendars.filter(
+ calendar => calitip.getInvitedAttendee(item, calendar) != null
+ );
+ // if there's none, we will show the whole list of calendars:
+ if (matchingCals.length > 0) {
+ calendars = matchingCals;
+ }
+ }
+
+ if (calendars.length == 0) {
+ let msg = lazy.cal.l10n.getLtnString("imipNoCalendarAvailable");
+ aWindow.alert(msg);
+ } else if (calendars.length == 1) {
+ // There's only one calendar, so it's silly to ask what calendar
+ // the user wants to import into.
+ targetCalendar = calendars[0];
+ } else {
+ // Ask what calendar to import into
+ let args = {};
+ args.calendars = calendars;
+ args.onOk = aCal => {
+ targetCalendar = aCal;
+ };
+ args.promptText = lazy.cal.l10n.getCalString("importPrompt");
+ aWindow.openDialog(
+ "chrome://calendar/content/chooseCalendarDialog.xhtml",
+ "_blank",
+ "chrome,titlebar,modal,resizable",
+ args
+ );
+ }
+
+ if (targetCalendar) {
+ aItipItem.targetCalendar = targetCalendar;
+ }
+ }
+
+ return !needsCalendar || targetCalendar != null;
+ },
+
+ /**
+ * Scope: iTIP message receiver
+ *
+ * Prompt for the invited attendee if we cannot automatically determine one.
+ * This will modify the items of the passed calIItipItem to ensure an invited
+ * attendee is available.
+ *
+ * Note: This is intended for the REQUEST/COUNTER methods.
+ *
+ * @param {Window} window - Used to prompt the user.
+ * @param {calIItipItem} itipItem - The itip item to ensure.
+ * @param {number} responseMode - One of the calIITipItem response mode
+ * constants indicating whether a response
+ * will be sent or not.
+ *
+ * @returns {boolean} True if an invited attendee is available for all
+ * items, false if otherwise.
+ */
+ promptInvitedAttendee(window, itipItem, responseMode) {
+ let cancelled = false;
+ for (let item of itipItem.getItemList()) {
+ let att = calitip.getInvitedAttendee(item, itipItem.targetCalendar);
+ if (!att) {
+ window.openDialog(
+ "chrome://calendar/content/calendar-itip-identity-dialog.xhtml",
+ "_blank",
+ "chrome,modal,resizable=no,centerscreen",
+ {
+ responseMode,
+ identities: MailServices.accounts.allIdentities.slice().sort((a, b) => {
+ if (a.email == itipItem.identity && b.email != itipItem.identity) {
+ return -1;
+ }
+ if (b.email == itipItem.identity && a.email != itipItem.identity) {
+ return 1;
+ }
+ return 0;
+ }),
+ onCancel() {
+ cancelled = true;
+ },
+ onOk(identity) {
+ att = new lazy.CalAttendee();
+ att.id = `mailto:${identity.email}`;
+ att.commonName = identity.fullName;
+ att.isOrganizer = false;
+ item.addAttendee(att);
+ },
+ }
+ );
+ }
+
+ if (cancelled) {
+ break;
+ }
+
+ if (att) {
+ let { stampTime, lastModifiedTime } = item;
+
+ // Set this so we know who accepted the event.
+ item.setProperty("X-MOZ-INVITED-ATTENDEE", att.id);
+
+ // Remove the dirty flag from the item.
+ item.setProperty("DTSTAMP", stampTime);
+ item.setProperty("LAST-MODIFIED", lastModifiedTime);
+ }
+ }
+
+ return !cancelled;
+ },
+
+ /**
+ * Clean up after the given iTIP item. This needs to be called once for each time
+ * processItipItem is called. May be called with a null itipItem in which case it will do
+ * nothing.
+ *
+ * @param {calIItipItem} itipItem - The iTIP item to clean up for.
+ */
+ cleanupItipItem(itipItem) {
+ if (itipItem) {
+ let itemList = itipItem.getItemList();
+ if (itemList.length > 0) {
+ // Again, we can assume the id is the same over all items per spec
+ ItipItemFinderFactory.cleanup(itemList[0].id);
+ }
+ }
+ },
+
+ /**
+ * Scope: iTIP message receiver
+ *
+ * Checks the passed iTIP item and calls the passed function with options offered. Be sure to
+ * call cleanupItipItem at least once after calling this function.
+ *
+ * The action func has a property |method| showing the options:
+ * REFRESH -- send the latest item (sent by attendee(s))
+ * PUBLISH -- initial publish, no reply (sent by organizer)
+ * PUBLISH:UPDATE -- update of a published item (sent by organizer)
+ * REQUEST -- initial invitation (sent by organizer)
+ * REQUEST:UPDATE -- rescheduling invitation, has major change (sent by organizer)
+ * REQUEST:UPDATE-MINOR -- update of invitation, minor change (sent by organizer)
+ * REPLY -- invitation reply (sent by attendee(s))
+ * CANCEL -- invitation cancel (sent by organizer)
+ * COUNTER -- counterproposal (sent by attendee)
+ * DECLINECOUNTER -- denial of a counterproposal (sent by organizer)
+ *
+ * @param {calIItipItem} itipItem - The iTIP item
+ * @param {Function} optionsFunc - The function being called with parameters: itipItem,
+ * resultCode, actionFunc
+ */
+ processItipItem(itipItem, optionsFunc) {
+ switch (itipItem.receivedMethod.toUpperCase()) {
+ case "REFRESH":
+ case "PUBLISH":
+ case "REQUEST":
+ case "CANCEL":
+ case "COUNTER":
+ case "DECLINECOUNTER":
+ case "REPLY": {
+ // Per iTIP spec (new Draft 4), multiple items in an iTIP message MUST have
+ // same ID, this simplifies our searching, we can just look for Item[0].id
+ let itemList = itipItem.getItemList();
+ if (!itipItem.targetCalendar) {
+ optionsFunc(itipItem, Ci.calIErrors.CAL_IS_READONLY);
+ } else if (itemList.length > 0) {
+ ItipItemFinderFactory.findItem(itemList[0].id, itipItem, optionsFunc);
+ } else if (optionsFunc) {
+ optionsFunc(itipItem, Cr.NS_OK);
+ }
+ break;
+ }
+ default: {
+ if (optionsFunc) {
+ optionsFunc(itipItem, Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+ break;
+ }
+ }
+ },
+
+ /**
+ * Scope: iTIP message sender
+ *
+ * Checks to see if e.g. attendees were added/removed or an item has been deleted and sends out
+ * appropriate iTIP messages.
+ *
+ * @param {number} aOpType - Type of operation - (e.g. ADD, MODIFY or DELETE)
+ * @param {calIItemBase} aItem - The updated item
+ * @param {calIItemBase} aOriginalItem - The original item
+ * @param {?object} aExtResponse - An object to provide additional
+ * parameters for sending itip messages as response
+ * mode, comments or a subset of recipients. Currently
+ * implemented attributes are:
+ * responseMode Response mode (long) as defined for autoResponse
+ * of calIItipItem. The default mode is USER (which
+ * will trigger displaying the previously known popup
+ * to ask the user whether to send)
+ */
+ checkAndSend(aOpType, aItem, aOriginalItem, aExtResponse = null) {
+ // `CalItipMessageSender` uses the presence of an "invited attendee"
+ // (representation of the current user) as an indication that this is an
+ // incoming invitation, so we need to avoid passing it if the current user
+ // is the event organizer.
+ let currentUserAsAttendee = null;
+ const itemCalendar = aItem.calendar;
+ if (
+ itemCalendar?.supportsScheduling &&
+ itemCalendar.getSchedulingSupport().isInvitation(aItem)
+ ) {
+ currentUserAsAttendee = this.getInvitedAttendee(aItem, itemCalendar);
+ }
+
+ const sender = new lazy.CalItipMessageSender(aOriginalItem, currentUserAsAttendee);
+ if (sender.buildOutgoingMessages(aOpType, aItem, aExtResponse)) {
+ sender.send(calitip.getImipTransport(aItem));
+ }
+ },
+
+ /**
+ * Bumps the SEQUENCE in case of a major change; XXX todo may need more fine-tuning.
+ *
+ * @param {calIItemBase} newItem - The new item to set the sequence on
+ * @param {calIItemBase} oldItem - The old item to get the previous version from.
+ * @returns {calIItemBase} The newly changed item
+ */
+ prepareSequence(newItem, oldItem) {
+ if (calitip.isInvitation(newItem)) {
+ return newItem; // invitation copies don't bump the SEQUENCE
+ }
+
+ if (newItem.recurrenceId && !oldItem.recurrenceId && oldItem.recurrenceInfo) {
+ // XXX todo: there's still the bug that modifyItem is called with mixed occurrence/parent,
+ // find original occurrence
+ oldItem = oldItem.recurrenceInfo.getOccurrenceFor(newItem.recurrenceId);
+ lazy.cal.ASSERT(oldItem, "unexpected!");
+ if (!oldItem) {
+ return newItem;
+ }
+ }
+
+ let hashMajorProps = function (aItem) {
+ const majorProps = {
+ DTSTART: true,
+ DTEND: true,
+ DURATION: true,
+ DUE: true,
+ RDATE: true,
+ RRULE: true,
+ EXDATE: true,
+ STATUS: true,
+ LOCATION: true,
+ };
+
+ let propStrings = [];
+ for (let item of lazy.cal.iterate.items([aItem])) {
+ for (let prop of lazy.cal.iterate.icalProperty(item.icalComponent)) {
+ if (prop.propertyName in majorProps) {
+ propStrings.push(item.recurrenceId + "#" + prop.icalString);
+ }
+ }
+ }
+ propStrings.sort();
+ return propStrings.join("");
+ };
+
+ let hash1 = hashMajorProps(newItem);
+ let hash2 = hashMajorProps(oldItem);
+ if (hash1 != hash2) {
+ newItem = newItem.clone();
+ // bump SEQUENCE, it never decreases (mind undo scenario here)
+ newItem.setProperty(
+ "SEQUENCE",
+ String(Math.max(calitip.getSequence(oldItem), calitip.getSequence(newItem)) + 1)
+ );
+ }
+
+ return newItem;
+ },
+
+ /**
+ * Returns a copy of an itipItem with modified properties and items build from scratch Use
+ * itipItem.clone() instead if only a simple copy is required
+ *
+ * @param {calIItipItem} aItipItem ItipItem to derive a new one from
+ * @param {calIItemBase[]} aItems calIEvent or calITodo items to be contained in the new itipItem
+ * @param {object} aProps Properties to be different in the new itipItem
+ * @returns {calIItipItem} The copied and modified item
+ */
+ getModifiedItipItem(aItipItem, aItems = [], aProps = {}) {
+ let itipItem = Cc["@mozilla.org/calendar/itip-item;1"].createInstance(Ci.calIItipItem);
+ let serializedItems = "";
+ for (let item of aItems) {
+ serializedItems += lazy.cal.item.serialize(item);
+ }
+ itipItem.init(serializedItems);
+
+ itipItem.autoResponse = "autoResponse" in aProps ? aProps.autoResponse : aItipItem.autoResponse;
+ itipItem.identity = "identity" in aProps ? aProps.identity : aItipItem.identity;
+ itipItem.isSend = "isSend" in aProps ? aProps.isSend : aItipItem.isSend;
+ itipItem.localStatus = "localStatus" in aProps ? aProps.localStatus : aItipItem.localStatus;
+ itipItem.receivedMethod =
+ "receivedMethod" in aProps ? aProps.receivedMethod : aItipItem.receivedMethod;
+ itipItem.responseMethod =
+ "responseMethod" in aProps ? aProps.responseMethod : aItipItem.responseMethod;
+ itipItem.targetCalendar =
+ "targetCalendar" in aProps ? aProps.targetCalendar : aItipItem.targetCalendar;
+
+ return itipItem;
+ },
+
+ /**
+ * A shortcut to send DECLINECOUNTER messages - for everything else use calitip.checkAndSend
+ *
+ * @param {calIItipItem} aItem - item to be sent
+ * @param {string} aMethod - iTIP method
+ * @param {calIAttendee[]} aRecipientsList - array of calIAttendee objects the message should be sent to
+ * @param {object} aAutoResponse - JS object whether the transport should ask before sending
+ * @returns {boolean} True
+ */
+ sendDeclineCounterMessage(aItem, aMethod, aRecipientsList, aAutoResponse) {
+ if (aMethod == "DECLINECOUNTER") {
+ return sendMessage(aItem, aMethod, aRecipientsList, aAutoResponse);
+ }
+ return false;
+ },
+
+ /**
+ * Returns a copy of an event that
+ * - has a relation set to the original event
+ * - has the same organizer but
+ * - has any attendee removed
+ * Intended to get a copy of a normal event invitation that behaves as if the PUBLISH method was
+ * chosen instead.
+ *
+ * @param {calIItemBase} aItem - Original item
+ * @param {?string} aUid - UID to use for the new item
+ * @returns {calIItemBase} The copied item for publishing
+ */
+ getPublishLikeItemCopy(aItem, aUid) {
+ // avoid changing aItem
+ let item = aItem.clone();
+ // reset to a new UUID if applicable
+ item.id = aUid || lazy.cal.getUUID();
+ // add a relation to the original item
+ let relation = new lazy.CalRelation();
+ relation.relId = aItem.id;
+ relation.relType = "SIBLING";
+ item.addRelation(relation);
+ // remove attendees
+ item.removeAllAttendees();
+ if (!aItem.isMutable) {
+ item = item.makeImmutable();
+ }
+ return item;
+ },
+
+ /**
+ * Tests whether the passed object is a calIAttendee instance. This function
+ * takes into consideration that the object may be be unwrapped and thus a
+ * CalAttendee instance
+ *
+ * @param {object} val - The object to test.
+ *
+ * @returns {boolean}
+ */
+ isAttendee(val) {
+ return val && (val instanceof Ci.calIAttendee || val instanceof lazy.CalAttendee);
+ },
+
+ /**
+ * Shortcut function to check whether an item is an invitation copy.
+ *
+ * @param {calIItemBase} aItem - The item to check for an invitation.
+ * @returns {boolean} True, if the item is an invitation.
+ */
+ isInvitation(aItem) {
+ let isInvitation = false;
+ let calendar = aItem.calendar;
+ if (calendar && calendar.supportsScheduling) {
+ isInvitation = calendar.getSchedulingSupport().isInvitation(aItem);
+ }
+ return isInvitation;
+ },
+
+ /**
+ * Shortcut function to check whether an item is an invitation copy and has a participation
+ * status of either NEEDS-ACTION or TENTATIVE.
+ *
+ * @param {calIAttendee|calIItemBase} aItem - either calIAttendee or calIItemBase
+ * @returns {boolean} True, if the attendee partstat is NEEDS-ACTION
+ * or TENTATIVE
+ */
+ isOpenInvitation(aItem) {
+ if (!calitip.isAttendee(aItem)) {
+ aItem = calitip.getInvitedAttendee(aItem);
+ }
+ if (aItem) {
+ switch (aItem.participationStatus) {
+ case "NEEDS-ACTION":
+ case "TENTATIVE":
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Resolves delegated-to/delegated-from calusers for a given attendee to also include the
+ * respective CNs if available in a given set of attendees
+ *
+ * @param {calIAttendee} aAttendee - The attendee to resolve the delegation information for
+ * @param {calIAttendee[]} aAttendees - An array of calIAttendee objects to look up
+ * @returns {object} An object with string attributes for delegators and delegatees
+ */
+ resolveDelegation(aAttendee, aAttendees) {
+ let attendees = aAttendees || [aAttendee];
+
+ // this will be replaced by a direct property getter in calIAttendee
+ let delegators = [];
+ let delegatees = [];
+ let delegatorProp = aAttendee.getProperty("DELEGATED-FROM");
+ if (delegatorProp) {
+ delegators = typeof delegatorProp == "string" ? [delegatorProp] : delegatorProp;
+ }
+ let delegateeProp = aAttendee.getProperty("DELEGATED-TO");
+ if (delegateeProp) {
+ delegatees = typeof delegateeProp == "string" ? [delegateeProp] : delegateeProp;
+ }
+
+ for (let att of attendees) {
+ let resolveDelegation = function (e, i, a) {
+ if (e == att.id) {
+ a[i] = att.toString();
+ }
+ };
+ delegators.forEach(resolveDelegation);
+ delegatees.forEach(resolveDelegation);
+ }
+ return {
+ delegatees: delegatees.join(", "),
+ delegators: delegators.join(", "),
+ };
+ },
+
+ /**
+ * Shortcut function to get the invited attendee of an item.
+ *
+ * @param {calIItemBase} aItem - Event or task to get the invited attendee for
+ * @param {?calICalendar} aCalendar - The calendar to use for checking, defaults to the item
+ * calendar
+ * @returns {?calIAttendee} The attendee that was invited
+ */
+ getInvitedAttendee(aItem, aCalendar) {
+ let id = aItem.getProperty("X-MOZ-INVITED-ATTENDEE");
+ if (id) {
+ return aItem.getAttendeeById(id);
+ }
+ if (!aCalendar) {
+ aCalendar = aItem.calendar;
+ }
+ let invitedAttendee = null;
+ if (aCalendar && aCalendar.supportsScheduling) {
+ invitedAttendee = aCalendar.getSchedulingSupport().getInvitedAttendee(aItem);
+ }
+ return invitedAttendee;
+ },
+
+ /**
+ * Returns all attendees from given set of attendees matching based on the attendee id
+ * or a sent-by parameter compared to the specified email address
+ *
+ * @param {calIAttendee[]} aAttendees - An array of calIAttendee objects
+ * @param {string} aEmailAddress - A string containing the email address for lookup
+ * @returns {calIAttendee[]} Returns an array of matching attendees
+ */
+ getAttendeesBySender(aAttendees, aEmailAddress) {
+ let attendees = [];
+ // we extract the email address to make it work also for a raw header value
+ let compFields = Cc["@mozilla.org/messengercompose/composefields;1"].createInstance(
+ Ci.nsIMsgCompFields
+ );
+ let addresses = compFields.splitRecipients(aEmailAddress, true);
+ if (addresses.length == 1) {
+ let searchFor = lazy.cal.email.prependMailTo(addresses[0]);
+ aAttendees.forEach(aAttendee => {
+ if ([aAttendee.id, aAttendee.getProperty("SENT-BY")].includes(searchFor)) {
+ attendees.push(aAttendee);
+ }
+ });
+ } else {
+ lazy.cal.WARN("No unique email address for lookup!");
+ }
+ return attendees;
+ },
+
+ /**
+ * Provides the transport to be used for an item based on the invited attendee
+ * or calendar.
+ *
+ * @param {calIItemBase} item
+ */
+ getImipTransport(item) {
+ let id = item.getProperty("X-MOZ-INVITED-ATTENDEE");
+
+ if (id) {
+ let email = id.split("mailto:").join("");
+ let identity = MailServices.accounts.allIdentities.find(identity => identity.email == email);
+
+ if (identity) {
+ let [server] = MailServices.accounts.getServersForIdentity(identity);
+
+ if (server) {
+ let account = MailServices.accounts.FindAccountForServer(server);
+ return new lazy.CalItipDefaultEmailTransport(account, identity);
+ }
+ }
+
+ // We did not find the identity or associated account
+ return null;
+ }
+
+ return item.calendar.getProperty("itip.transport");
+ },
+};
+
+/** local to this module file
+ * Sets the received info either on the passed attendee or item object.
+ *
+ * @param {calIItemBase|calIAttendee} item - The item to set info on
+ * @param {calIItipItem} itipItemItem - The received iTIP item
+ */
+function setReceivedInfo(item, itipItemItem) {
+ let isAttendee = calitip.isAttendee(item);
+ item.setProperty(
+ isAttendee ? "RECEIVED-SEQUENCE" : "X-MOZ-RECEIVED-SEQUENCE",
+ String(calitip.getSequence(itipItemItem))
+ );
+ let dtstamp = calitip.getStamp(itipItemItem);
+ if (dtstamp) {
+ item.setProperty(
+ isAttendee ? "RECEIVED-DTSTAMP" : "X-MOZ-RECEIVED-DTSTAMP",
+ dtstamp.getInTimezone(lazy.cal.dtz.UTC).icalString
+ );
+ }
+}
+
+/** local to this module file
+ * Takes over relevant item information from iTIP item and sets received info.
+ *
+ * @param {calIItemBase} item - The stored calendar item to update
+ * @param {calIItipItem} itipItemItem - The received item
+ * @returns {calIItemBase} A copy of the item with correct received info
+ */
+function updateItem(item, itipItemItem) {
+ /**
+ * Migrates some user data from the old to new item
+ *
+ * @param {calIItemBase} newItem - The new item to copy to
+ * @param {calIItemBase} oldItem - The old item to copy from
+ */
+ function updateUserData(newItem, oldItem) {
+ // preserve user settings:
+ newItem.generation = oldItem.generation;
+ newItem.clearAlarms();
+ for (let alarm of oldItem.getAlarms()) {
+ newItem.addAlarm(alarm);
+ }
+ newItem.alarmLastAck = oldItem.alarmLastAck;
+ let cats = oldItem.getCategories();
+ newItem.setCategories(cats);
+ }
+
+ let newItem = item.clone();
+ newItem.icalComponent = itipItemItem.icalComponent;
+ setReceivedInfo(newItem, itipItemItem);
+ updateUserData(newItem, item);
+
+ let recInfo = itipItemItem.recurrenceInfo;
+ if (recInfo) {
+ // keep care of installing all overridden items, and mind existing alarms, categories:
+ for (let rid of recInfo.getExceptionIds()) {
+ let excItem = recInfo.getExceptionFor(rid).clone();
+ lazy.cal.ASSERT(excItem, "unexpected!");
+ let newExc = newItem.recurrenceInfo.getOccurrenceFor(rid).clone();
+ newExc.icalComponent = excItem.icalComponent;
+ setReceivedInfo(newExc, itipItemItem);
+ let existingExcItem = item.recurrenceInfo && item.recurrenceInfo.getExceptionFor(rid);
+ if (existingExcItem) {
+ updateUserData(newExc, existingExcItem);
+ }
+ newItem.recurrenceInfo.modifyException(newExc, true);
+ }
+ }
+
+ return newItem;
+}
+
+/** local to this module file
+ * Copies the provider-specified properties from the itip item to the passed
+ * item. Special case property "METHOD" uses the itipItem's receivedMethod.
+ *
+ * @param {calIItipItem} itipItem - The itip item containing the receivedMethod.
+ * @param {calIItemBase} itipItemItem - The calendar item inside the itip item.
+ * @param {calIItemBase} item - The target item to copy to.
+ */
+function copyProviderProperties(itipItem, itipItemItem, item) {
+ // Copy over itip properties to the item if requested by the provider
+ let copyProps = item.calendar.getProperty("itip.copyProperties") || [];
+ for (let prop of copyProps) {
+ if (prop == "METHOD") {
+ // Special case, this copies over the received method
+ item.setProperty("METHOD", itipItem.receivedMethod.toUpperCase());
+ } else if (itipItemItem.hasProperty(prop)) {
+ // Otherwise just copy from the item contained in the itipItem
+ item.setProperty(prop, itipItemItem.getProperty(prop));
+ }
+ }
+}
+
+/** local to this module file
+ * Sends an iTIP message using the passed item's calendar transport.
+ *
+ * @param {calIEvent} aItem - item to be sent
+ * @param {string} aMethod - iTIP method
+ * @param {calIAttendee[]} aRecipientsList - array of calIAttendee objects the message should be sent to
+ * @param {object} autoResponse - inout object whether the transport should ask before sending
+ * @returns {boolean} True, if the message could be sent
+ */
+function sendMessage(aItem, aMethod, aRecipientsList, autoResponse) {
+ new lazy.CalItipOutgoingMessage(
+ aMethod,
+ aRecipientsList,
+ aItem,
+ calitip.getInvitedAttendee(aItem),
+ autoResponse
+ ).send(calitip.getImipTransport(aItem));
+}
+
+/** local to this module file
+ * An operation listener that is used on calendar operations which checks and sends further iTIP
+ * messages based on the calendar action.
+ *
+ * @param {object} aOpListener - operation listener to forward
+ * @param {calIItemBase} aOldItem - The previous item before modification (if any)
+ * @param {?object} aExtResponse - An object to provide additional parameters for sending itip
+ * messages as response mode, comments or a subset of
+ * recipients.
+ */
+function ItipOpListener(aOpListener, aOldItem, aExtResponse = null) {
+ this.mOpListener = aOpListener;
+ this.mOldItem = aOldItem;
+ this.mExtResponse = aExtResponse;
+}
+ItipOpListener.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["calIOperationListener"]),
+
+ mOpListener: null,
+ mOldItem: null,
+ mExtResponse: null,
+
+ onOperationComplete(aCalendar, aStatus, aOperationType, aId, aDetail) {
+ lazy.cal.ASSERT(Components.isSuccessCode(aStatus), "error on iTIP processing");
+ if (Components.isSuccessCode(aStatus)) {
+ calitip.checkAndSend(aOperationType, aDetail, this.mOldItem, this.mExtResponse);
+ }
+ if (this.mOpListener) {
+ this.mOpListener.onOperationComplete(aCalendar, aStatus, aOperationType, aId, aDetail);
+ }
+ },
+ onGetResult(calendar, status, itemType, detail, items) {},
+};
+
+/** local to this module file
+ * Add a parameter SCHEDULE-AGENT=CLIENT to the item before it is
+ * created or updated so that the providers knows scheduling will
+ * be handled by the client.
+ *
+ * @param {calIItemBase} item - item about to be added or updated
+ * @param {calICalendar} calendar - calendar into which the item is about to be added or updated
+ */
+function addScheduleAgentClient(item, calendar) {
+ if (calendar.getProperty("capabilities.autoschedule.supported") === true) {
+ if (item.organizer) {
+ item.organizer.setProperty("SCHEDULE-AGENT", "CLIENT");
+ }
+ }
+}
+
+var ItipItemFinderFactory = {
+ /** Map to save finder instances for given ids */
+ _findMap: {},
+
+ /**
+ * Create an item finder and track its progress. Be sure to clean up the
+ * finder for this id at some point.
+ *
+ * @param {string} aId - The item id to search for
+ * @param {calIIipItem} aItipItem - The iTIP item used for processing
+ * @param {Function} aOptionsFunc - The options function used for processing the found item
+ */
+ async findItem(aId, aItipItem, aOptionsFunc) {
+ this.cleanup(aId);
+ let finder = new ItipItemFinder(aId, aItipItem, aOptionsFunc);
+ this._findMap[aId] = finder;
+ return finder.findItem();
+ },
+
+ /**
+ * Clean up tracking for the given id. This needs to be called once for
+ * every time findItem is called.
+ *
+ * @param {string} aId - The item id to clean up for
+ */
+ cleanup(aId) {
+ if (aId in this._findMap) {
+ let finder = this._findMap[aId];
+ finder.destroy();
+ delete this._findMap[aId];
+ }
+ },
+};
+
+/** local to this module file
+ * An operation listener triggered by cal.itip.processItipItem() for lookup of the sent iTIP item's UID.
+ *
+ * @param {string} aId - The search identifier for the item to find
+ * @param {calIItipItem} itipItem - Sent iTIP item
+ * @param {Function} optionsFunc - Options func, see cal.itip.processItipItem()
+ */
+function ItipItemFinder(aId, itipItem, optionsFunc) {
+ this.mItipItem = itipItem;
+ this.mOptionsFunc = optionsFunc;
+ this.mSearchId = aId;
+}
+
+ItipItemFinder.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["calIObserver"]),
+
+ mSearchId: null,
+ mItipItem: null,
+ mOptionsFunc: null,
+ mFoundItems: null,
+
+ async findItem() {
+ this.mFoundItems = [];
+ this._unobserveChanges();
+
+ let foundItem = await this.mItipItem.targetCalendar.getItem(this.mSearchId);
+ if (foundItem) {
+ this.mFoundItems.push(foundItem);
+ }
+ this.processFoundItems();
+ },
+
+ _observeChanges(aCalendar) {
+ this._unobserveChanges();
+ this.mObservedCalendar = aCalendar;
+
+ if (this.mObservedCalendar) {
+ this.mObservedCalendar.addObserver(this);
+ }
+ },
+ _unobserveChanges() {
+ if (this.mObservedCalendar) {
+ this.mObservedCalendar.removeObserver(this);
+ this.mObservedCalendar = null;
+ }
+ },
+
+ onStartBatch() {},
+ onEndBatch() {},
+ onError() {},
+ onPropertyChanged() {},
+ onPropertyDeleting() {},
+ onLoad(aCalendar) {
+ // Its possible that the item was updated. We need to re-retrieve the
+ // items now.
+ this.findItem();
+ },
+
+ onModifyItem(aNewItem, aOldItem) {
+ let refItem = aOldItem || aNewItem;
+ if (refItem.id == this.mSearchId) {
+ // Check existing found items to see if it already exists
+ let found = false;
+ for (let [idx, item] of Object.entries(this.mFoundItems)) {
+ if (item.id == refItem.id && item.calendar.id == refItem.calendar.id) {
+ if (aNewItem) {
+ this.mFoundItems.splice(idx, 1, aNewItem);
+ } else {
+ this.mFoundItems.splice(idx, 1);
+ }
+ found = true;
+ break;
+ }
+ }
+
+ // If it hasn't been found and there is to add a item, add it to the end
+ if (!found && aNewItem) {
+ this.mFoundItems.push(aNewItem);
+ }
+ this.processFoundItems();
+ }
+ },
+
+ onAddItem(aItem) {
+ // onModifyItem is set up to also handle additions
+ this.onModifyItem(aItem, null);
+ },
+
+ onDeleteItem(aItem) {
+ // onModifyItem is set up to also handle deletions
+ this.onModifyItem(null, aItem);
+ },
+
+ destroy() {
+ this._unobserveChanges();
+ },
+
+ processFoundItems() {
+ let rc = Cr.NS_OK;
+ const method = this.mItipItem.receivedMethod.toUpperCase();
+ let actionMethod = method;
+ let operations = [];
+
+ if (this.mFoundItems.length > 0) {
+ // Save the target calendar on the itip item
+ this.mItipItem.targetCalendar = this.mFoundItems[0].calendar;
+ this._observeChanges(this.mItipItem.targetCalendar);
+
+ lazy.cal.LOG("iTIP on " + method + ": found " + this.mFoundItems.length + " items.");
+ switch (method) {
+ // XXX todo: there's still a potential flaw, if multiple PUBLISH/REPLY/REQUEST on
+ // occurrences happen at once; those lead to multiple
+ // occurrence modifications. Since those modifications happen
+ // implicitly on the parent (ics/memory/storage calls modifyException),
+ // the generation check will fail. We should really consider to allow
+ // deletion/modification/addition of occurrences directly on the providers,
+ // which would ease client code a lot.
+ case "REFRESH":
+ case "PUBLISH":
+ case "REQUEST":
+ case "REPLY":
+ case "COUNTER":
+ case "DECLINECOUNTER":
+ for (let itipItemItem of this.mItipItem.getItemList()) {
+ for (let item of this.mFoundItems) {
+ let rid = itipItemItem.recurrenceId; // XXX todo support multiple
+ if (rid) {
+ // actually applies to individual occurrence(s)
+ if (item.recurrenceInfo) {
+ item = item.recurrenceInfo.getOccurrenceFor(rid);
+ if (!item) {
+ continue;
+ }
+ } else {
+ // the item has been rescheduled with master:
+ itipItemItem = itipItemItem.parentItem;
+ }
+ }
+
+ switch (method) {
+ case "REFRESH": {
+ // xxx todo test
+ let attendees = itipItemItem.getAttendees();
+ lazy.cal.ASSERT(attendees.length == 1, "invalid number of attendees in REFRESH!");
+ if (attendees.length > 0) {
+ let action = function (opListener, partStat, extResponse) {
+ if (!item.organizer) {
+ let org = calitip.createOrganizer(item.calendar);
+ if (org) {
+ item = item.clone();
+ item.organizer = org;
+ }
+ }
+ sendMessage(
+ item,
+ "REQUEST",
+ attendees,
+ { responseMode: Ci.calIItipItem.AUTO } /* don't ask */
+ );
+ };
+ operations.push(action);
+ }
+ break;
+ }
+ case "PUBLISH":
+ lazy.cal.ASSERT(
+ itipItemItem.getAttendees().length == 0,
+ "invalid number of attendees in PUBLISH!"
+ );
+ if (
+ item.calendar.getProperty("itip.disableRevisionChecks") ||
+ calitip.compare(itipItemItem, item) > 0
+ ) {
+ let newItem = updateItem(item, itipItemItem);
+ let action = function (opListener, partStat, extResponse) {
+ return newItem.calendar.modifyItem(newItem, item).then(
+ item =>
+ opListener.onOperationComplete(
+ item.calendar,
+ Cr.NS_OK,
+ Ci.calIOperationListener.MODIFY,
+ item.id,
+ item
+ ),
+ e =>
+ opListener.onOperationComplete(
+ null,
+ e.result || Cr.NS_ERROR_FAILURE,
+ Ci.calIOperationListener.MODIFY,
+ null,
+ e
+ )
+ );
+ };
+ actionMethod = method + ":UPDATE";
+ operations.push(action);
+ }
+ break;
+ case "REQUEST": {
+ let newItem = updateItem(item, itipItemItem);
+ let att = calitip.getInvitedAttendee(newItem);
+ if (!att) {
+ // fall back to using configured organizer
+ att = calitip.createOrganizer(newItem.calendar);
+ if (att) {
+ att.isOrganizer = false;
+ }
+ }
+ if (att) {
+ let firstFoundItem = this.mFoundItems[0];
+
+ // Where the server automatically adds events to the calendar
+ // we may end up with a recurring invitation in the "NEEDS-ACTION"
+ // state. Upon receiving an exception for these, processFoundItems()
+ // will query the calendar and determine the actionMethod to
+ // be "REQUEST:NEEDS-ACTION" but process the entire series. To avoid
+ // that, we detect here if the itip item's item was indeed for
+ // the whole series or an exception.
+ if (firstFoundItem.recurrenceInfo && rid) {
+ firstFoundItem = firstFoundItem.recurrenceInfo.getOccurrenceFor(rid);
+ }
+
+ // again, fall back to using configured organizer if not found
+ let foundAttendee = firstFoundItem.getAttendeeById(att.id) || att;
+
+ // If the user hasn't responded to the invitation yet and we
+ // are viewing the current representation of the item, show the
+ // accept/decline buttons. This means newer events will show the
+ // "Update" button and older events will show the "already
+ // processed" text.
+ if (
+ foundAttendee.participationStatus == "NEEDS-ACTION" &&
+ (item.calendar.getProperty("itip.disableRevisionChecks") ||
+ calitip.compare(itipItemItem, item) == 0)
+ ) {
+ actionMethod = "REQUEST:NEEDS-ACTION";
+ operations.push((opListener, partStat, extResponse) => {
+ let changedItem = firstFoundItem.clone();
+ changedItem.removeAttendee(foundAttendee);
+ foundAttendee = foundAttendee.clone();
+ if (partStat) {
+ foundAttendee.participationStatus = partStat;
+ }
+ changedItem.addAttendee(foundAttendee);
+
+ let listener = new ItipOpListener(opListener, firstFoundItem, extResponse);
+ return changedItem.calendar.modifyItem(changedItem, firstFoundItem).then(
+ item =>
+ listener.onOperationComplete(
+ item.calendar,
+ Cr.NS_OK,
+ Ci.calIOperationListener.MODIFY,
+ item.id,
+ item
+ ),
+ e =>
+ listener.onOperationComplete(
+ null,
+ e.result || Cr.NS_ERROR_FAILURE,
+ Ci.calIOperationListener.MODIFY,
+ null,
+ e
+ )
+ );
+ });
+ } else if (
+ item.calendar.getProperty("itip.disableRevisionChecks") ||
+ calitip.compare(itipItemItem, item) > 0
+ ) {
+ addScheduleAgentClient(newItem, item.calendar);
+
+ let isMinorUpdate = calitip.getSequence(newItem) == calitip.getSequence(item);
+ actionMethod = isMinorUpdate ? method + ":UPDATE-MINOR" : method + ":UPDATE";
+ operations.push((opListener, partStat, extResponse) => {
+ if (!partStat) {
+ // keep PARTSTAT
+ let att_ = calitip.getInvitedAttendee(item);
+ partStat = att_ ? att_.participationStatus : "NEEDS-ACTION";
+ }
+ newItem.removeAttendee(att);
+ att = att.clone();
+ att.participationStatus = partStat;
+ newItem.addAttendee(att);
+
+ let listener = new ItipOpListener(opListener, item, extResponse);
+ return newItem.calendar.modifyItem(newItem, item).then(
+ item =>
+ listener.onOperationComplete(
+ item.calendar,
+ Cr.NS_OK,
+ Ci.calIOperationListener.MODIFY,
+ item.id,
+ item
+ ),
+ e =>
+ listener.onOperationComplete(
+ null,
+ e.result || Cr.NS_ERROR_FAILURE,
+ Ci.calIOperationListener.MODIFY,
+ null,
+ e
+ )
+ );
+ });
+ }
+ }
+ break;
+ }
+ case "DECLINECOUNTER":
+ // nothing to do right now, but once countering is implemented,
+ // we probably need some action here to remove the proposal from
+ // the countering attendee's calendar
+ break;
+ case "COUNTER":
+ case "REPLY": {
+ let attendees = itipItemItem.getAttendees();
+ if (method == "REPLY") {
+ lazy.cal.ASSERT(attendees.length == 1, "invalid number of attendees in REPLY!");
+ } else {
+ attendees = lazy.cal.itip.getAttendeesBySender(
+ attendees,
+ this.mItipItem.sender
+ );
+ lazy.cal.ASSERT(
+ attendees.length == 1,
+ "ambiguous resolution of replying attendee in COUNTER!"
+ );
+ }
+ // we get the attendee from the event stored in the calendar
+ let replyer = item.getAttendeeById(attendees[0].id);
+ if (!replyer && method == "REPLY") {
+ // We accepts REPLYs also from previously uninvited
+ // attendees, so we always have one for REPLY
+ replyer = attendees[0];
+ }
+ let noCheck = item.calendar.getProperty("itip.disableRevisionChecks");
+ let revCheck = false;
+ if (replyer && !noCheck) {
+ revCheck = calitip.compare(itipItemItem, replyer) > 0;
+ if (revCheck && method == "COUNTER") {
+ revCheck = calitip.compareSequence(itipItemItem, item) == 0;
+ }
+ }
+
+ if (replyer && (noCheck || revCheck)) {
+ let newItem = item.clone();
+ newItem.removeAttendee(replyer);
+ replyer = replyer.clone();
+ setReceivedInfo(replyer, itipItemItem);
+ let newPS = itipItemItem.getAttendeeById(replyer.id).participationStatus;
+ replyer.participationStatus = newPS;
+ newItem.addAttendee(replyer);
+
+ // Make sure the provider-specified properties are copied over
+ copyProviderProperties(this.mItipItem, itipItemItem, newItem);
+
+ let action = function (opListener, partStat, extResponse) {
+ // n.b.: this will only be processed in case of reply or
+ // declining the counter request - of sending the
+ // appropriate reply will be taken care within the
+ // opListener (defined in imip-bar.js)
+ // TODO: move that from imip-bar.js to here
+
+ let listener = newItem.calendar.getProperty("itip.notify-replies")
+ ? new ItipOpListener(opListener, item, extResponse)
+ : opListener;
+ return newItem.calendar.modifyItem(newItem, item).then(
+ item =>
+ listener.onOperationComplete(
+ item.calendar,
+ Cr.NS_OK,
+ Ci.calIOperationListener.MODIFY,
+ item.id,
+ item
+ ),
+ e =>
+ listener.onOperationComplete(
+ null,
+ e.result || Cr.NS_ERROR_FAILURE,
+ Ci.calIOperationListener.MODIFY,
+ null,
+ e
+ )
+ );
+ };
+ operations.push(action);
+ }
+ break;
+ }
+ }
+ }
+ }
+ break;
+ case "CANCEL": {
+ let modifiedItems = {};
+ for (let itipItemItem of this.mItipItem.getItemList()) {
+ for (let item of this.mFoundItems) {
+ let rid = itipItemItem.recurrenceId; // XXX todo support multiple
+ if (rid) {
+ // actually a CANCEL of occurrence(s)
+ if (item.recurrenceInfo) {
+ // collect all occurrence deletions into a single parent modification:
+ let newItem = modifiedItems[item.id];
+ if (!newItem) {
+ newItem = item.clone();
+ modifiedItems[item.id] = newItem;
+
+ // Make sure the provider-specified properties are copied over
+ copyProviderProperties(this.mItipItem, itipItemItem, newItem);
+
+ operations.push((opListener, partStat, extResponse) =>
+ newItem.calendar.modifyItem(newItem, item).then(
+ item =>
+ opListener.onOperationComplete(
+ item.calendar,
+ Cr.NS_OK,
+ Ci.calIOperationListener.MODIFY,
+ item.id,
+ item
+ ),
+ e =>
+ opListener.onOperationComplete(
+ null,
+ e.result || Cr.NS_ERROR_FAILURE,
+ Ci.calIOperationListener.MODIFY,
+ null,
+ e
+ )
+ )
+ );
+ }
+ newItem.recurrenceInfo.removeOccurrenceAt(rid);
+ } else if (item.recurrenceId && item.recurrenceId.compare(rid) == 0) {
+ // parentless occurrence to be deleted (future)
+ operations.push((opListener, partStat, extResponse) =>
+ item.calendar.deleteItem(item).then(
+ () =>
+ opListener.onOperationComplete(
+ item.calendar,
+ Cr.NS_OK,
+ Ci.calIOperationListener.DELETE,
+ item.id,
+ item
+ ),
+ e =>
+ opListener.onOperationComplete(
+ item.calendar,
+ e.result,
+ Ci.calIOperationListener.DELETE,
+ item.id,
+ e
+ )
+ )
+ );
+ }
+ } else {
+ operations.push((opListener, partStat, extResponse) =>
+ item.calendar.deleteItem(item).then(
+ () =>
+ opListener.onOperationComplete(
+ item.calendar,
+ Cr.NS_OK,
+ Ci.calIOperationListener.DELETE,
+ item.id,
+ item
+ ),
+ e =>
+ opListener.onOperationComplete(
+ item.calendar,
+ e.result,
+ Ci.calIOperationListener.DELETE,
+ item.id,
+ e
+ )
+ )
+ );
+ }
+ }
+ }
+ break;
+ }
+ default:
+ rc = Cr.NS_ERROR_NOT_IMPLEMENTED;
+ break;
+ }
+ } else {
+ // not found:
+ lazy.cal.LOG("iTIP on " + method + ": no existing items.");
+ // If the item was not found, observe the target calendar anyway.
+ // It will likely be the composite calendar, so we should update
+ // if an item was added or removed
+ this._observeChanges(this.mItipItem.targetCalendar);
+
+ for (let itipItemItem of this.mItipItem.getItemList()) {
+ switch (method) {
+ case "REQUEST":
+ case "PUBLISH": {
+ let action = (opListener, partStat, extResponse) => {
+ let newItem = itipItemItem.clone();
+ setReceivedInfo(newItem, itipItemItem);
+ newItem.parentItem.calendar = this.mItipItem.targetCalendar;
+ addScheduleAgentClient(newItem, this.mItipItem.targetCalendar);
+
+ if (partStat) {
+ if (partStat != "DECLINED") {
+ lazy.cal.alarms.setDefaultValues(newItem);
+ }
+
+ let att = calitip.getInvitedAttendee(newItem);
+ if (!att) {
+ lazy.cal.WARN(
+ `Encountered item without invited attendee! id=${newItem.id}, method=${method} Exiting...`
+ );
+ return null;
+ }
+ att.participationStatus = partStat;
+ } else {
+ lazy.cal.ASSERT(
+ itipItemItem.getAttendees().length == 0,
+ "invalid number of attendees in PUBLISH!"
+ );
+ lazy.cal.alarms.setDefaultValues(newItem);
+ }
+
+ let listener =
+ method == "REQUEST"
+ ? new ItipOpListener(opListener, null, extResponse)
+ : opListener;
+ return newItem.calendar.addItem(newItem).then(
+ item =>
+ listener.onOperationComplete(
+ newItem.calendar,
+ Cr.NS_OK,
+ Ci.calIOperationListener.ADD,
+ item.id,
+ item
+ ),
+ e =>
+ listener.onOperationComplete(
+ newItem.calendar,
+ e.result,
+ Ci.calIOperationListener.ADD,
+ newItem.id,
+ e
+ )
+ );
+ };
+ operations.push(action);
+ break;
+ }
+ case "CANCEL": // has already been processed
+ case "REPLY": // item has been previously removed from the calendar
+ case "COUNTER": // the item has been previously removed form the calendar
+ break;
+ default:
+ rc = Cr.NS_ERROR_NOT_IMPLEMENTED;
+ break;
+ }
+ }
+ }
+
+ lazy.cal.LOG("iTIP operations: " + operations.length);
+ let actionFunc = null;
+ if (operations.length > 0) {
+ actionFunc = function (opListener, partStat = null, extResponse = null) {
+ for (let operation of operations) {
+ try {
+ operation(opListener, partStat, extResponse);
+ } catch (exc) {
+ lazy.cal.ERROR(exc);
+ }
+ }
+ };
+ actionFunc.method = actionMethod;
+ }
+
+ this.mOptionsFunc(this.mItipItem, rc, actionFunc, this.mFoundItems);
+ },
+};