summaryrefslogtreecommitdiffstats
path: root/comm/calendar/base/content/dialogs/calendar-dialog-utils.js
blob: d002bcdf5c374a8c8c9a3c566a3e4cba525a866f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
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);
  }
}