summaryrefslogtreecommitdiffstats
path: root/comm/calendar/base/content/dialogs/calendar-dialog-utils.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--comm/calendar/base/content/dialogs/calendar-dialog-utils.js662
1 files changed, 662 insertions, 0 deletions
diff --git a/comm/calendar/base/content/dialogs/calendar-dialog-utils.js b/comm/calendar/base/content/dialogs/calendar-dialog-utils.js
new file mode 100644
index 0000000000..d002bcdf5c
--- /dev/null
+++ b/comm/calendar/base/content/dialogs/calendar-dialog-utils.js
@@ -0,0 +1,662 @@
+/* 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/. */
+
+/* exported gInTab, gMainWindow, gTabmail, intializeTabOrWindowVariables,
+ * dispose, setDialogId, loadReminders, saveReminder,
+ * commonUpdateReminder, updateLink,
+ * adaptScheduleAgent, sendMailToOrganizer,
+ * openAttachmentFromItemSummary,
+ */
+
+/* import-globals-from ../item-editing/calendar-item-iframe.js */
+/* import-globals-from ../calendar-ui-utils.js */
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ CalAlarm: "resource:///modules/CalAlarm.jsm",
+});
+
+// Variables related to whether we are in a tab or a window dialog.
+var gInTab = false;
+var gMainWindow = null;
+var gTabmail = null;
+
+/**
+ * Initialize variables for tab vs window.
+ */
+function intializeTabOrWindowVariables() {
+ let args = window.arguments[0];
+ gInTab = args.inTab;
+ if (gInTab) {
+ gTabmail = parent.document.getElementById("tabmail");
+ gMainWindow = parent;
+ } else {
+ gMainWindow = parent.opener;
+ }
+}
+
+/**
+ * Dispose of controlling operations of this event dialog. Uses
+ * window.arguments[0].job.dispose()
+ */
+function dispose() {
+ let args = window.arguments[0];
+ if (args.job && args.job.dispose) {
+ args.job.dispose();
+ }
+}
+
+/**
+ * Sets the id of a Dialog to another value to allow different CSS styles
+ * to be used.
+ *
+ * @param aDialog The Dialog to be changed.
+ * @param aNewId The new ID as String.
+ */
+function setDialogId(aDialog, aNewId) {
+ aDialog.setAttribute("id", aNewId);
+ applyPersistedProperties(aDialog);
+}
+
+/**
+ * Apply the persisted properties from xulstore.json on a dialog based on the current dialog id.
+ * This needs to be invoked after changing a dialog id while loading to apply the values for the
+ * new dialog id.
+ *
+ * @param aDialog The Dialog to apply the property values for
+ */
+function applyPersistedProperties(aDialog) {
+ let xulStore = Services.xulStore;
+ // first we need to detect which properties are persisted
+ let persistedProps = aDialog.getAttribute("persist") || "";
+ if (persistedProps == "") {
+ return;
+ }
+ let propNames = persistedProps.split(" ");
+ let { outerWidth: width, outerHeight: height } = aDialog;
+ let doResize = false;
+ // now let's apply persisted values if applicable
+ for (let propName of propNames) {
+ if (xulStore.hasValue(aDialog.baseURI, aDialog.id, propName)) {
+ let propValue = xulStore.getValue(aDialog.baseURI, aDialog.id, propName);
+ if (propName == "width") {
+ width = propValue;
+ doResize = true;
+ } else if (propName == "height") {
+ height = propValue;
+ doResize = true;
+ } else {
+ aDialog.setAttribute(propName, propValue);
+ }
+ }
+ }
+
+ if (doResize) {
+ aDialog.ownerGlobal.resizeTo(width, height);
+ }
+}
+
+/**
+ * Create a calIAlarm from the given menuitem. The menuitem must have the
+ * following attributes: unit, length, origin, relation.
+ *
+ * @param {Element} aMenuitem - The menuitem to create the alarm from.
+ * @param {calICalendar} aCalendar - The calendar for getting the default alarm type.
+ * @returns The calIAlarm with information from the menuitem.
+ */
+function createReminderFromMenuitem(aMenuitem, aCalendar) {
+ let reminder = aMenuitem.reminder || new CalAlarm();
+ // clone immutable reminders if necessary to set default values
+ let isImmutable = !reminder.isMutable;
+ if (isImmutable) {
+ reminder = reminder.clone();
+ }
+ let offset = cal.createDuration();
+ offset[aMenuitem.getAttribute("unit")] = aMenuitem.getAttribute("length");
+ offset.normalize();
+ offset.isNegative = aMenuitem.getAttribute("origin") == "before";
+ reminder.related =
+ aMenuitem.getAttribute("relation") == "START"
+ ? Ci.calIAlarm.ALARM_RELATED_START
+ : Ci.calIAlarm.ALARM_RELATED_END;
+ reminder.offset = offset;
+ reminder.action = getDefaultAlarmType(aCalendar);
+ // make reminder immutable in case it was before
+ if (isImmutable) {
+ reminder.makeImmutable();
+ }
+ return reminder;
+}
+
+/**
+ * This function opens the needed dialogs to edit the reminder. Note however
+ * that calling this function from an extension is not recommended. To allow an
+ * extension to open the reminder dialog, set the menulist "item-alarm" to the
+ * custom menuitem and call updateReminder().
+ *
+ * @param {Element} reminderList - The reminder menu element.
+ * @param {calIEvent | calIToDo} calendarItem - The calendar item.
+ * @param {number} lastAlarmSelection - Index of previously selected item in the menu.
+ * @param {calICalendar} calendar - The calendar to use.
+ * @param {calITimezone} [timezone] - Timezone to use.
+ */
+function editReminder(
+ reminderList,
+ calendarItem,
+ lastAlarmSelection,
+ calendar,
+ timezone = cal.dtz.defaultTimezone
+) {
+ let customItem = reminderList.querySelector(".reminder-custom-menuitem");
+
+ let args = {
+ reminders: customItem.reminders,
+ item: calendarItem,
+ timezone,
+ calendar,
+ // While these are "just" callbacks, the dialog is opened modally, so aside
+ // from what's needed to set up the reminders, nothing else needs to be done.
+ onOk(reminders) {
+ customItem.reminders = reminders;
+ },
+ onCancel() {
+ reminderList.selectedIndex = lastAlarmSelection;
+ },
+ };
+
+ window.setCursor("wait");
+
+ // open the dialog modally
+ openDialog(
+ "chrome://calendar/content/calendar-event-dialog-reminder.xhtml",
+ "_blank",
+ "chrome,titlebar,modal,resizable,centerscreen",
+ args
+ );
+}
+
+/**
+ * Update the reminder details from the selected alarm. This shows a string
+ * describing the reminder set, or nothing in case a preselected reminder was
+ * chosen.
+ *
+ * @param {Element} reminderDetails - The reminder details element.
+ * @param {Element} reminderList - The reminder menu element.
+ * @param {calICalendar} calendar - The calendar.
+ */
+function updateReminderDetails(reminderDetails, reminderList, calendar) {
+ // find relevant elements in the document
+ let reminderMultipleLabel = reminderDetails.querySelector(".reminder-multiple-alarms-label");
+ let iconBox = reminderDetails.querySelector(".alarm-icons-box");
+ let reminderSingleLabel = reminderDetails.querySelector(".reminder-single-alarms-label");
+
+ let reminders = reminderList.querySelector(".reminder-custom-menuitem").reminders || [];
+
+ let actionValues = calendar.getProperty("capabilities.alarms.actionValues") || ["DISPLAY"];
+ let actionMap = {};
+ for (let action of actionValues) {
+ actionMap[action] = true;
+ }
+
+ // Filter out any unsupported action types.
+ reminders = reminders.filter(x => x.action in actionMap);
+
+ if (reminderList.value == "custom") {
+ // Depending on how many alarms we have, show either the "Multiple Alarms"
+ // label or the single reminder label.
+ reminderMultipleLabel.hidden = reminders.length < 2;
+ reminderSingleLabel.hidden = reminders.length > 1;
+
+ cal.alarms.addReminderImages(iconBox, reminders);
+
+ // If there is only one reminder, display the reminder string
+ if (reminders.length == 1) {
+ reminderSingleLabel.value = reminders[0].toString(window.calendarItem);
+ }
+ } else {
+ reminderMultipleLabel.setAttribute("hidden", "true");
+ reminderSingleLabel.setAttribute("hidden", "true");
+ if (reminderList.value == "none") {
+ // No reminder selected means show no icons.
+ while (iconBox.lastChild) {
+ iconBox.lastChild.remove();
+ }
+ } else {
+ // This is one of the predefined dropdown items. We should show a
+ // single icon in the icons box to tell the user what kind of alarm
+ // this will be.
+ let mockAlarm = new CalAlarm();
+ mockAlarm.action = getDefaultAlarmType(calendar);
+ cal.alarms.addReminderImages(iconBox, [mockAlarm]);
+ }
+ }
+}
+
+/**
+ * Check whether a reminder matches one of the default menu items or not.
+ *
+ * @param {calIAlarm} reminder - The reminder to match to a menu item.
+ * @param {Element} reminderList - The reminder menu element.
+ * @param {calICalendar} calendar - The current calendar, to get the default alarm type.
+ * @returns {boolean} True if the reminder matches a menu item, false if not.
+ */
+function matchCustomReminderToMenuitem(reminder, reminderList, calendar) {
+ let defaultAlarmType = getDefaultAlarmType(calendar);
+ let reminderPopup = reminderList.menupopup;
+ if (
+ reminder.related != Ci.calIAlarm.ALARM_RELATED_ABSOLUTE &&
+ reminder.offset &&
+ reminder.action == defaultAlarmType
+ ) {
+ // Exactly one reminder that's not absolute, we may be able to match up
+ // popup items.
+ let relation = reminder.related == Ci.calIAlarm.ALARM_RELATED_START ? "START" : "END";
+
+ // If the time duration for offset is 0, means the reminder is '0 minutes before'
+ let origin = reminder.offset.inSeconds == 0 || reminder.offset.isNegative ? "before" : "after";
+
+ let unitMap = {
+ days: 86400,
+ hours: 3600,
+ minutes: 60,
+ };
+
+ for (let menuitem of reminderPopup.children) {
+ if (
+ menuitem.localName == "menuitem" &&
+ menuitem.hasAttribute("length") &&
+ menuitem.getAttribute("origin") == origin &&
+ menuitem.getAttribute("relation") == relation
+ ) {
+ let unitMult = unitMap[menuitem.getAttribute("unit")] || 1;
+ let length = menuitem.getAttribute("length") * unitMult;
+
+ if (Math.abs(reminder.offset.inSeconds) == length) {
+ menuitem.reminder = reminder.clone();
+ reminderList.selectedItem = menuitem;
+ // We've selected an item, so we are done here.
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Load an item's reminders into the dialog.
+ *
+ * @param {calIAlarm[]} reminders - An array of alarms to load.
+ * @param {Element} reminderList - The reminders menulist element.
+ * @param {calICalendar} calendar - The calendar the item belongs to.
+ * @returns {number} Index of the selected item in reminders menu.
+ */
+function loadReminders(reminders, reminderList, calendar) {
+ // Select 'no reminder' by default.
+ reminderList.selectedIndex = 0;
+
+ if (!reminders || !reminders.length) {
+ // No reminders selected, we are done
+ return reminderList.selectedIndex;
+ }
+
+ if (
+ reminders.length > 1 ||
+ !matchCustomReminderToMenuitem(reminders[0], reminderList, calendar)
+ ) {
+ // If more than one alarm is selected, or we didn't find a matching item
+ // above, then select the "custom" item and attach the item's reminders to
+ // it.
+ reminderList.value = "custom";
+ reminderList.querySelector(".reminder-custom-menuitem").reminders = reminders;
+ }
+
+ // Return the selected index so it can be remembered.
+ return reminderList.selectedIndex;
+}
+
+/**
+ * Save the selected reminder into the passed item.
+ *
+ * @param {calIEvent | calITodo} item The calendar item to save the reminder into.
+ * @param {calICalendar} calendar - The current calendar.
+ * @param {Element} reminderList - The reminder menu element.
+ */
+function saveReminder(item, calendar, reminderList) {
+ // We want to compare the old alarms with the new ones. If these are not
+ // the same, then clear the snooze/dismiss times
+ let oldAlarmMap = {};
+ for (let alarm of item.getAlarms()) {
+ oldAlarmMap[alarm.icalString] = true;
+ }
+
+ // Clear the alarms so we can add our new ones.
+ item.clearAlarms();
+
+ if (reminderList.value != "none") {
+ let menuitem = reminderList.selectedItem;
+ let reminders;
+
+ if (menuitem.reminders) {
+ // Custom reminder entries carry their own reminder object with
+ // them. Make sure to clone in case these are the original item's
+ // reminders.
+
+ // XXX do we need to clone here?
+ reminders = menuitem.reminders.map(x => x.clone());
+ } else {
+ // Pre-defined entries specify the necessary information
+ // as attributes attached to the menuitem elements.
+ reminders = [createReminderFromMenuitem(menuitem, calendar)];
+ }
+
+ let alarmCaps = item.calendar.getProperty("capabilities.alarms.actionValues") || ["DISPLAY"];
+ let alarmActions = {};
+ for (let action of alarmCaps) {
+ alarmActions[action] = true;
+ }
+
+ // Make sure only alarms are saved that work in the given calendar.
+ reminders.filter(x => x.action in alarmActions).forEach(item.addAlarm, item);
+ }
+
+ // Compare alarms to see if something changed.
+ for (let alarm of item.getAlarms()) {
+ let ics = alarm.icalString;
+ if (ics in oldAlarmMap) {
+ // The new alarm is also in the old set, remember this
+ delete oldAlarmMap[ics];
+ } else {
+ // The new alarm is not in the old set, this means the alarms
+ // differ and we can break out.
+ oldAlarmMap[ics] = true;
+ break;
+ }
+ }
+
+ // If the alarms differ, clear the snooze/dismiss properties
+ if (Object.keys(oldAlarmMap).length > 0) {
+ let cmp = "X-MOZ-SNOOZE-TIME";
+
+ // Recurring item alarms potentially have more snooze props, remove them
+ // all.
+ let propsToDelete = [];
+ for (let [name] of item.properties) {
+ if (name.startsWith(cmp)) {
+ propsToDelete.push(name);
+ }
+ }
+
+ item.alarmLastAck = null;
+ propsToDelete.forEach(item.deleteProperty, item);
+ }
+}
+
+/**
+ * Get the default alarm type for the currently selected calendar. If the
+ * calendar supports DISPLAY alarms, this is the default. Otherwise it is the
+ * first alarm action the calendar supports.
+ *
+ * @param {calICalendar} calendar - The calendar to use.
+ * @returns {string} The default alarm type.
+ */
+function getDefaultAlarmType(calendar) {
+ let alarmCaps = calendar.getProperty("capabilities.alarms.actionValues") || ["DISPLAY"];
+ return alarmCaps.includes("DISPLAY") ? "DISPLAY" : alarmCaps[0];
+}
+
+/**
+ * Common update functions for both event dialogs. Called when a reminder has
+ * been selected from the menulist.
+ *
+ * @param {Element} reminderList - The reminders menu element.
+ * @param {calIEvent | calITodo} calendarItem - The calendar item.
+ * @param {number} lastAlarmSelection - Index of the previous selection in the reminders menu.
+ * @param {Element} reminderDetails - The reminder details element.
+ * @param {calITimezone} timezone - The relevant timezone.
+ * @param {boolean} suppressDialogs - If true, controls are updated without prompting
+ * for changes with the dialog
+ * @returns {number} Index of the item selected in the reminders menu.
+ */
+function commonUpdateReminder(
+ reminderList,
+ calendarItem,
+ lastAlarmSelection,
+ calendar,
+ reminderDetails,
+ timezone,
+ suppressDialogs
+) {
+ // if a custom reminder has been selected, we show the appropriate
+ // dialog in order to allow the user to specify the details.
+ // the result will be placed in the 'reminder-custom-menuitem' tag.
+ if (reminderList.value == "custom") {
+ // Clear the reminder icons first, this will make sure that while the
+ // dialog is open the default reminder image is not shown which may
+ // confuse users.
+ let iconBox = reminderDetails.querySelector(".alarm-icons-box");
+ while (iconBox.lastChild) {
+ iconBox.lastChild.remove();
+ }
+
+ // show the dialog. This call blocks until the dialog is closed. Don't
+ // pop up the dialog if aSuppressDialogs was specified or if this
+ // happens during initialization of the dialog
+ if (!suppressDialogs && reminderList.hasAttribute("last-value")) {
+ editReminder(reminderList, calendarItem, lastAlarmSelection, calendar, timezone);
+ }
+
+ if (reminderList.value == "custom") {
+ // Only do this if the 'custom' item is still selected. If the edit
+ // reminder dialog was canceled then the previously selected
+ // menuitem is selected, which may not be the custom menuitem.
+
+ // If one or no reminders were selected, we have a chance of mapping
+ // them to the existing elements in the dropdown.
+ let customItem = reminderList.selectedItem;
+ if (customItem.reminders.length == 0) {
+ // No reminder was selected
+ reminderList.value = "none";
+ } else if (customItem.reminders.length == 1) {
+ // We might be able to match the custom reminder with one of the
+ // default menu items.
+ matchCustomReminderToMenuitem(customItem.reminders[0], reminderList, calendar);
+ }
+ }
+ }
+
+ reminderList.setAttribute("last-value", reminderList.value);
+
+ // possibly the selected reminder conflicts with the item.
+ // for example an end-relation combined with a task without duedate
+ // is an invalid state we need to take care of. we take the same
+ // approach as with recurring tasks. in case the reminder is related
+ // to the entry date we check the entry date automatically and disable
+ // the checkbox. the same goes for end related reminder and the due date.
+ if (calendarItem.isTodo()) {
+ // In general, (re-)enable the due/entry checkboxes. This will be
+ // changed in case the alarms are related to START/END below.
+ enableElementWithLock("todo-has-duedate", "reminder-lock");
+ enableElementWithLock("todo-has-entrydate", "reminder-lock");
+
+ let menuitem = reminderList.selectedItem;
+ if (menuitem.value != "none") {
+ // In case a reminder is selected, retrieve the array of alarms from
+ // it, or create one from the currently selected menuitem.
+ let reminders = menuitem.reminders || [createReminderFromMenuitem(menuitem, calendar)];
+
+ // If a reminder is related to the entry date...
+ if (reminders.some(x => x.related == Ci.calIAlarm.ALARM_RELATED_START)) {
+ // ...automatically check 'has entrydate'.
+ if (!document.getElementById("todo-has-entrydate").checked) {
+ document.getElementById("todo-has-entrydate").checked = true;
+
+ // Make sure gStartTime is properly initialized
+ updateEntryDate();
+ }
+
+ // Disable the checkbox to indicate that we need the entry-date.
+ disableElementWithLock("todo-has-entrydate", "reminder-lock");
+ }
+
+ // If a reminder is related to the due date...
+ if (reminders.some(x => x.related == Ci.calIAlarm.ALARM_RELATED_END)) {
+ // ...automatically check 'has duedate'.
+ if (!document.getElementById("todo-has-duedate").checked) {
+ document.getElementById("todo-has-duedate").checked = true;
+
+ // Make sure gStartTime is properly initialized
+ updateDueDate();
+ }
+
+ // Disable the checkbox to indicate that we need the entry-date.
+ disableElementWithLock("todo-has-duedate", "reminder-lock");
+ }
+ }
+ }
+ updateReminderDetails(reminderDetails, reminderList, calendar);
+
+ // Return the current reminder drop down selection index so it can be remembered.
+ return reminderList.selectedIndex;
+}
+
+/**
+ * Updates the related link on the dialog. Currently only used by the
+ * read-only summary dialog.
+ *
+ * @param {string} itemUrlString - The calendar item URL as a string.
+ * @param {Element} linkRow - The row containing the link.
+ * @param {Element} urlLink - The link element itself.
+ */
+function updateLink(itemUrlString, linkRow, urlLink) {
+ let linkCommand = document.getElementById("cmd_toggle_link");
+
+ if (linkCommand) {
+ // Disable if there is no url.
+ linkCommand.disabled = !itemUrlString;
+ }
+
+ if ((linkCommand && linkCommand.getAttribute("checked") != "true") || !itemUrlString.length) {
+ // Hide if there is no url, or the menuitem was chosen so that the url
+ // should be hidden
+ linkRow.hidden = true;
+ } else {
+ let handler, uri;
+ try {
+ uri = Services.io.newURI(itemUrlString);
+ handler = Services.io.getProtocolHandler(uri.scheme);
+ } catch (e) {
+ // No protocol handler for the given protocol, or invalid uri
+ linkRow.hidden = true;
+ return;
+ }
+
+ // Only show if its either an internal protocol handler, or its external
+ // and there is an external app for the scheme
+ handler = cal.wrapInstance(handler, Ci.nsIExternalProtocolHandler);
+ let show = !handler || handler.externalAppExistsForScheme(uri.scheme);
+ linkRow.hidden = !show;
+
+ setTimeout(() => {
+ // HACK the url link doesn't crop when setting the value in onLoad
+ urlLink.setAttribute("value", itemUrlString);
+ urlLink.setAttribute("href", itemUrlString);
+ }, 0);
+ }
+}
+
+/**
+ * Adapts the scheduling responsibility for caldav servers according to RfC 6638
+ * based on forceEmailScheduling preference for the respective calendar
+ *
+ * @param {calIEvent|calIToDo} aItem - Item to apply the change on
+ */
+function adaptScheduleAgent(aItem) {
+ if (
+ aItem.calendar &&
+ aItem.calendar.type == "caldav" &&
+ aItem.calendar.getProperty("capabilities.autoschedule.supported")
+ ) {
+ let identity = aItem.calendar.getProperty("imip.identity");
+ let orgEmail = identity && identity.QueryInterface(Ci.nsIMsgIdentity).email;
+ let organizerAction = aItem.organizer && orgEmail && aItem.organizer.id == "mailto:" + orgEmail;
+ if (aItem.calendar.getProperty("forceEmailScheduling")) {
+ cal.LOG("Enforcing clientside email based scheduling.");
+ // for attendees, we change schedule-agent only in case of an
+ // organizer triggered action
+ if (organizerAction) {
+ aItem.getAttendees().forEach(aAttendee => {
+ // overwriting must always happen consistently for all
+ // attendees regarding SERVER or CLIENT but must not override
+ // e.g. NONE, so we only overwrite if the param is set to
+ // SERVER or doesn't exist
+ if (
+ aAttendee.getProperty("SCHEDULE-AGENT") == "SERVER" ||
+ !aAttendee.getProperty("SCHEDULE-AGENT")
+ ) {
+ aAttendee.setProperty("SCHEDULE-AGENT", "CLIENT");
+ aAttendee.deleteProperty("SCHEDULE-STATUS");
+ aAttendee.deleteProperty("SCHEDULE-FORCE-SEND");
+ }
+ });
+ } else if (
+ aItem.organizer &&
+ (aItem.organizer.getProperty("SCHEDULE-AGENT") == "SERVER" ||
+ !aItem.organizer.getProperty("SCHEDULE-AGENT"))
+ ) {
+ // for organizer, we change the schedule-agent only in case of
+ // an attendee triggered action
+ aItem.organizer.setProperty("SCHEDULE-AGENT", "CLIENT");
+ aItem.organizer.deleteProperty("SCHEDULE-STATUS");
+ aItem.organizer.deleteProperty("SCHEDULE-FORCE-SEND");
+ }
+ } else if (organizerAction) {
+ aItem.getAttendees().forEach(aAttendee => {
+ if (aAttendee.getProperty("SCHEDULE-AGENT") == "CLIENT") {
+ aAttendee.deleteProperty("SCHEDULE-AGENT");
+ }
+ });
+ } else if (aItem.organizer && aItem.organizer.getProperty("SCHEDULE-AGENT") == "CLIENT") {
+ aItem.organizer.deleteProperty("SCHEDULE-AGENT");
+ }
+ }
+}
+
+/**
+ * Extracts the item's organizer and opens a compose window to send the
+ * organizer an email.
+ *
+ * @param {calIEvent | calITodo} item - The calendar item.
+ */
+function sendMailToOrganizer(item) {
+ let organizer = item.organizer;
+ let email = cal.email.getAttendeeEmail(organizer, true);
+ let emailSubject = cal.l10n.getString("calendar-event-dialog", "emailSubjectReply", [item.title]);
+ let identity = item.calendar.getProperty("imip.identity");
+ cal.email.sendTo(email, emailSubject, null, identity);
+}
+
+/**
+ * Opens an attachment.
+ *
+ * @param {AUTF8String} aAttachmentId The hashId of the attachment to open.
+ * @param {calIEvent | calITodo} item The calendar item.
+ */
+function openAttachmentFromItemSummary(aAttachmentId, item) {
+ if (!aAttachmentId) {
+ return;
+ }
+ let attachments = item
+ .getAttachments()
+ .filter(aAttachment => aAttachment.hashId == aAttachmentId);
+
+ if (attachments.length && attachments[0].uri && attachments[0].uri.spec != "about:blank") {
+ Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService)
+ .loadURI(attachments[0].uri);
+ }
+}