summaryrefslogtreecommitdiffstats
path: root/comm/calendar/test/browser/invitations
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /comm/calendar/test/browser/invitations
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/calendar/test/browser/invitations')
-rw-r--r--comm/calendar/test/browser/invitations/browser.ini31
-rw-r--r--comm/calendar/test/browser/invitations/browser_attachedPublishEvent.js72
-rw-r--r--comm/calendar/test/browser/invitations/browser_icsAttachment.js71
-rw-r--r--comm/calendar/test/browser/invitations/browser_identityPrompt.js144
-rw-r--r--comm/calendar/test/browser/invitations/browser_imipBar.js199
-rw-r--r--comm/calendar/test/browser/invitations/browser_imipBarCancel.js129
-rw-r--r--comm/calendar/test/browser/invitations/browser_imipBarEmail.js168
-rw-r--r--comm/calendar/test/browser/invitations/browser_imipBarExceptionCancel.js137
-rw-r--r--comm/calendar/test/browser/invitations/browser_imipBarExceptionOnly.js262
-rw-r--r--comm/calendar/test/browser/invitations/browser_imipBarExceptions.js288
-rw-r--r--comm/calendar/test/browser/invitations/browser_imipBarRepeat.js218
-rw-r--r--comm/calendar/test/browser/invitations/browser_imipBarRepeatCancel.js186
-rw-r--r--comm/calendar/test/browser/invitations/browser_imipBarRepeatUpdates.js247
-rw-r--r--comm/calendar/test/browser/invitations/browser_imipBarUpdates.js223
-rw-r--r--comm/calendar/test/browser/invitations/browser_invitationDisplayNew.js257
-rw-r--r--comm/calendar/test/browser/invitations/browser_unsupportedFreq.js107
-rw-r--r--comm/calendar/test/browser/invitations/data/cancel-repeat-event.eml49
-rw-r--r--comm/calendar/test/browser/invitations/data/cancel-single-event.eml78
-rw-r--r--comm/calendar/test/browser/invitations/data/exception-major.eml49
-rw-r--r--comm/calendar/test/browser/invitations/data/exception-minor.eml49
-rw-r--r--comm/calendar/test/browser/invitations/data/meet-meeting-invite.eml384
-rw-r--r--comm/calendar/test/browser/invitations/data/message-containing-event.eml44
-rw-r--r--comm/calendar/test/browser/invitations/data/message-non-invite.eml115
-rw-r--r--comm/calendar/test/browser/invitations/data/outlook-test-invite.eml102
-rw-r--r--comm/calendar/test/browser/invitations/data/repeat-event.eml49
-rw-r--r--comm/calendar/test/browser/invitations/data/repeat-update-major.eml49
-rw-r--r--comm/calendar/test/browser/invitations/data/repeat-update-minor.eml49
-rw-r--r--comm/calendar/test/browser/invitations/data/single-event.eml78
-rw-r--r--comm/calendar/test/browser/invitations/data/teams-meeting-invite.eml167
-rw-r--r--comm/calendar/test/browser/invitations/data/update-major.eml78
-rw-r--r--comm/calendar/test/browser/invitations/data/update-minor.eml78
-rw-r--r--comm/calendar/test/browser/invitations/head.js942
32 files changed, 5099 insertions, 0 deletions
diff --git a/comm/calendar/test/browser/invitations/browser.ini b/comm/calendar/test/browser/invitations/browser.ini
new file mode 100644
index 0000000000..7c7aa6af46
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/browser.ini
@@ -0,0 +1,31 @@
+[default]
+head = ../head.js head.js
+prefs =
+ calendar.item.promptDelete=false
+ calendar.timezone.local=UTC
+ calendar.timezone.useSystemTimezone=false
+ calendar.week.start=0
+ mail.provider.suppress_dialog_on_startup=true
+ mail.spotlight.firstRunDone=true
+ mail.winsearch.firstRunDone=true
+ mailnews.start_page.override_url=about:blank
+ mailnews.start_page.url=about:blank
+subsuite = thunderbird
+support-files = data/**
+
+[browser_attachedPublishEvent.js]
+[browser_icsAttachment.js]
+skip-if = os == 'win'
+[browser_identityPrompt.js]
+[browser_imipBar.js]
+[browser_imipBarCancel.js]
+[browser_imipBarEmail.js]
+[browser_imipBarExceptionCancel.js]
+[browser_imipBarExceptionOnly.js]
+[browser_imipBarExceptions.js]
+[browser_imipBarRepeat.js]
+[browser_imipBarRepeatCancel.js]
+[browser_imipBarRepeatUpdates.js]
+[browser_imipBarUpdates.js]
+[browser_invitationDisplayNew.js]
+[browser_unsupportedFreq.js]
diff --git a/comm/calendar/test/browser/invitations/browser_attachedPublishEvent.js b/comm/calendar/test/browser/invitations/browser_attachedPublishEvent.js
new file mode 100644
index 0000000000..af121a8032
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/browser_attachedPublishEvent.js
@@ -0,0 +1,72 @@
+/* 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/. */
+
+/**
+ * Test that attached events - NOT invites - works properly.
+ * These are attached VCALENDARs that have METHOD:PUBLISH.
+ */
+"use strict";
+
+var { CalendarTestUtils } = ChromeUtils.import(
+ "resource://testing-common/calendar/CalendarTestUtils.jsm"
+);
+
+var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
+
+var gCalendar;
+
+/**
+ * Initialize account, identity and calendar.
+ */
+add_setup(async function () {
+ let receiverAcct = MailServices.accounts.createAccount();
+ receiverAcct.incomingServer = MailServices.accounts.createIncomingServer(
+ "receiver",
+ "example.com",
+ "imap"
+ );
+ let receiverIdentity = MailServices.accounts.createIdentity();
+ receiverIdentity.email = "john.doe@example.com";
+ receiverAcct.addIdentity(receiverIdentity);
+ gCalendar = CalendarTestUtils.createCalendar("EventTestCal");
+
+ registerCleanupFunction(() => {
+ CalendarTestUtils.removeCalendar(gCalendar);
+ MailServices.accounts.removeAccount(receiverAcct, true);
+ });
+});
+
+/**
+ * Test that opening a message containing an event with iTIP method "PUBLISH"
+ * shows the correct UI.
+ * The party crashing dialog should not show.
+ */
+add_task(async function test_event_from_eml() {
+ let file = new FileUtils.File(getTestFilePath("data/message-non-invite.eml"));
+
+ let win = await openMessageFromFile(file);
+ let aboutMessage = win.document.getElementById("messageBrowser").contentWindow;
+ let imipBar = aboutMessage.document.getElementById("imip-bar");
+
+ await TestUtils.waitForCondition(() => !imipBar.collapsed);
+ info("Ok, iMIP bar is showing");
+
+ let imipAddButton = aboutMessage.document.getElementById("imipAddButton");
+ Assert.ok(!imipAddButton.hidden, "Add button should show");
+
+ EventUtils.synthesizeMouseAtCenter(imipAddButton, {}, aboutMessage);
+
+ // Make sure the event got added, without showing the party crashing dialog.
+ await TestUtils.waitForCondition(async () => {
+ let event = await gCalendar.getItem("1e5fd4e6-bc52-439c-ac76-40da54f57c77@secure.example.com");
+ return event;
+ });
+
+ await TestUtils.waitForCondition(() => imipAddButton.hidden, "Add button should hide");
+
+ let imipDetailsButton = aboutMessage.document.getElementById("imipDetailsButton");
+ Assert.ok(!imipDetailsButton.hidden, "Details button should show");
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/comm/calendar/test/browser/invitations/browser_icsAttachment.js b/comm/calendar/test/browser/invitations/browser_icsAttachment.js
new file mode 100644
index 0000000000..11bde9144d
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/browser_icsAttachment.js
@@ -0,0 +1,71 @@
+/* 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/. */
+
+/**
+ * Test TB can be set as default calendar app.
+ */
+
+/**
+ * Set TB as default calendar app.
+ */
+add_setup(function () {
+ let shellSvc = Cc["@mozilla.org/mail/shell-service;1"].getService(Ci.nsIShellService);
+ shellSvc.setDefaultClient(false, shellSvc.CALENDAR);
+ ok(shellSvc.isDefaultClient(false, shellSvc.CALENDAR), "setDefaultClient works");
+});
+
+/**
+ * Test when opening an ics attachment, TB should be shown as an option.
+ */
+add_task(async function test_ics_attachment() {
+ let file = new FileUtils.File(getTestFilePath("data/message-containing-event.eml"));
+ let msgWindow = await openMessageFromFile(file);
+ let aboutMessage = msgWindow.document.getElementById("messageBrowser").contentWindow;
+ let promise = BrowserTestUtils.promiseAlertDialog(
+ null,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ {
+ async callback(dialogWindow) {
+ ok(true, "unknownContentType dialog opened");
+ let dialogElement = dialogWindow.document.querySelector("dialog");
+ let acceptButton = dialogElement.getButton("accept");
+ return new Promise(resolve => {
+ let observer = new MutationObserver(mutationList => {
+ mutationList.forEach(async mutation => {
+ if (mutation.attributeName == "disabled" && !acceptButton.disabled) {
+ is(acceptButton.disabled, false, "Accept button enabled");
+ if (AppConstants.platform != "macosx") {
+ let bundle = Services.strings.createBundle(
+ "chrome://branding/locale/brand.properties"
+ );
+ let name = bundle.GetStringFromName("brandShortName");
+ // macOS requires extra step in Finder to set TB as default calendar app.
+ ok(
+ dialogWindow.document.getElementById("openHandler").label.includes(name),
+ `${name} is the default calendar app`
+ );
+ }
+
+ // Should really click acceptButton and test
+ // calender-ics-file-dialog is opened. But on local, a new TB
+ // instance is started and this test will fail.
+ dialogElement.getButton("cancel").click();
+ resolve();
+ }
+ });
+ });
+ observer.observe(acceptButton, { attributes: true });
+ });
+ },
+ }
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ aboutMessage.document.getElementById("attachmentName"),
+ {},
+ aboutMessage
+ );
+ await promise;
+
+ await BrowserTestUtils.closeWindow(msgWindow);
+});
diff --git a/comm/calendar/test/browser/invitations/browser_identityPrompt.js b/comm/calendar/test/browser/invitations/browser_identityPrompt.js
new file mode 100644
index 0000000000..e2d6fe3115
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/browser_identityPrompt.js
@@ -0,0 +1,144 @@
+/* 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/. */
+
+/**
+ * Tests for the calender-itip-identity dialog.
+ */
+
+"use strict";
+
+var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+var { CalendarTestUtils } = ChromeUtils.import(
+ "resource://testing-common/calendar/CalendarTestUtils.jsm"
+);
+
+let receiverAcct;
+let receiverIdentity;
+let gInbox;
+let calendar;
+
+registerCleanupFunction(() => {
+ CalendarTestUtils.removeCalendar(calendar);
+ MailServices.accounts.removeIncomingServer(receiverAcct.incomingServer, true);
+ MailServices.accounts.removeAccount(receiverAcct);
+});
+
+/**
+ * Initialize account, identity and calendar.
+ */
+add_setup(async function () {
+ if (MailServices.accounts.accounts.length == 0) {
+ MailServices.accounts.createLocalMailAccount();
+ }
+
+ let rootFolder = MailServices.accounts.localFoldersServer.rootFolder;
+ if (!rootFolder.containsChildNamed("Inbox")) {
+ rootFolder.createSubfolder("Inbox", null);
+ }
+ gInbox = rootFolder.getChildNamed("Inbox");
+
+ receiverAcct = MailServices.accounts.createAccount();
+ receiverAcct.incomingServer = MailServices.accounts.createIncomingServer(
+ "receiver",
+ "example.com",
+ "imap"
+ );
+ receiverIdentity = MailServices.accounts.createIdentity();
+ receiverIdentity.email = "receiver@example.com";
+ receiverAcct.addIdentity(receiverIdentity);
+
+ calendar = CalendarTestUtils.createCalendar("Test");
+
+ let copyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyFileMessage(
+ new FileUtils.File(getTestFilePath("data/meet-meeting-invite.eml")),
+ gInbox,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+ await copyListener.promise;
+});
+
+/**
+ * Tests that the identity prompt shows when accepting an invitation to an
+ * event with an identity no calendar is configured to use.
+ */
+add_task(async function testInvitationIdentityPrompt() {
+ let tabmail = document.getElementById("tabmail");
+ let about3Pane = tabmail.currentAbout3Pane;
+ about3Pane.displayFolder(gInbox.URI);
+ about3Pane.threadTree.selectedIndex = 0;
+
+ let dialogPromise = BrowserTestUtils.promiseAlertDialog(
+ null,
+ "chrome://calendar/content/calendar-itip-identity-dialog.xhtml",
+ {
+ async callback(win) {
+ // Select the identity we want to use.
+ let menulist = win.document.getElementById("identity-menu");
+ for (let i = 0; i < menulist.itemCount; i++) {
+ let target = menulist.getItemAtIndex(i);
+ if (target.value == receiverIdentity.fullAddress) {
+ menulist.selectedIndex = i;
+ }
+ }
+
+ win.document.querySelector("dialog").getButton("accept").click();
+ },
+ }
+ );
+
+ // Override this function to intercept the attempt to send the email out.
+ let sendItemsArgs = [];
+ let getImipTransport = cal.itip.getImipTransport;
+ cal.itip.getImipTransport = () => ({
+ scheme: "mailto",
+ type: "email",
+ sendItems(receipientArray, item, sender) {
+ sendItemsArgs = [receipientArray, item, sender];
+ return true;
+ },
+ });
+
+ let aboutMessage = tabmail.currentAboutMessage;
+ let acceptButton = aboutMessage.document.getElementById("imipAcceptButton");
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(acceptButton),
+ "waiting for accept button to become visible"
+ );
+ EventUtils.synthesizeMouseAtCenter(acceptButton, {}, aboutMessage);
+ await dialogPromise;
+
+ let event;
+ await TestUtils.waitForCondition(async () => {
+ event = await calendar.getItem("65m17hsdolmotv3kvmrtg40ont@google.com");
+ return event && sendItemsArgs.length;
+ });
+
+ // Restore this function.
+ cal.itip.getImipTransport = getImipTransport;
+
+ let id = `mailto:${receiverIdentity.email}`;
+ Assert.ok(event, "event was added to the calendar successfully");
+ Assert.ok(event.getAttendeeById(id), "selected identity was added to the attendee list");
+ Assert.equal(
+ event.getProperty("X-MOZ-INVITED-ATTENDEE"),
+ id,
+ "X-MOZ-INVITED-ATTENDEE is set to the selected identity"
+ );
+
+ let [recipientArray, , sender] = sendItemsArgs;
+ Assert.equal(recipientArray.length, 1, "one recipient for the reply");
+ Assert.equal(recipientArray[0].id, "mailto:example@gmail.com", "recipient is event organizer");
+ Assert.equal(sender.id, id, "sender is the identity selected");
+});
diff --git a/comm/calendar/test/browser/invitations/browser_imipBar.js b/comm/calendar/test/browser/invitations/browser_imipBar.js
new file mode 100644
index 0000000000..c9a21a6d2b
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/browser_imipBar.js
@@ -0,0 +1,199 @@
+/* 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/. */
+
+/**
+ * Tests for receiving event invitations via the imip-bar.
+ */
+"use strict";
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { CalItipDefaultEmailTransport } = ChromeUtils.import(
+ "resource:///modules/CalItipEmailTransport.jsm"
+);
+var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
+
+var { CalendarTestUtils } = ChromeUtils.import(
+ "resource://testing-common/calendar/CalendarTestUtils.jsm"
+);
+
+let identity;
+let calendar;
+let transport;
+
+/**
+ * Initialize account, identity and calendar.
+ */
+add_setup(async function () {
+ let account = MailServices.accounts.createAccount();
+ account.incomingServer = MailServices.accounts.createIncomingServer(
+ "receiver",
+ "example.com",
+ "imap"
+ );
+ identity = MailServices.accounts.createIdentity();
+ identity.email = "receiver@example.com";
+ account.addIdentity(identity);
+
+ await CalendarTestUtils.setCalendarView(window, "month");
+ window.goToDate(cal.createDateTime("20220316T191602Z"));
+
+ calendar = CalendarTestUtils.createCalendar("Test");
+ transport = new EmailTransport(account, identity);
+
+ let getImipTransport = cal.itip.getImipTransport;
+ cal.itip.getImipTransport = () => transport;
+
+ let deleteMgr = Cc["@mozilla.org/calendar/deleted-items-manager;1"].getService(
+ Ci.calIDeletedItems
+ ).wrappedJSObject;
+ let markDeleted = deleteMgr.markDeleted;
+ deleteMgr.markDeleted = () => {};
+
+ registerCleanupFunction(() => {
+ MailServices.accounts.removeAccount(account, true);
+ cal.itip.getImipTransport = getImipTransport;
+ deleteMgr.markDeleted = markDeleted;
+ CalendarTestUtils.removeCalendar(calendar);
+ });
+});
+
+/**
+ * Tests accepting an invitation and sending a response.
+ */
+add_task(async function testAcceptWithResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml")));
+ await clickAction(win, "imipAcceptButton");
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await doImipBarActionTest(
+ {
+ calendar,
+ transport,
+ identity,
+ partStat: "ACCEPTED",
+ },
+ event
+ );
+
+ await calendar.deleteItem(event);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests tentatively accepting an invitation and sending a response.
+ */
+add_task(async function testTentativeWithResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml")));
+ await clickAction(win, "imipTentativeButton");
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await doImipBarActionTest(
+ {
+ calendar,
+ transport,
+ identity,
+ partStat: "TENTATIVE",
+ },
+ event
+ );
+
+ await calendar.deleteItem(event);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests declining an invitation and sending a response.
+ */
+add_task(async function testDeclineWithResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml")));
+ await clickAction(win, "imipDeclineButton");
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await doImipBarActionTest(
+ {
+ calendar,
+ transport,
+ identity,
+ partStat: "DECLINED",
+ },
+ event
+ );
+
+ await calendar.deleteItem(event);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests accepting an invitation without sending a response.
+ */
+add_task(async function testAcceptWithoutResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml")));
+ await clickMenuAction(win, "imipAcceptButton", "imipAcceptButton_AcceptDontSend");
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await doImipBarActionTest(
+ {
+ calendar,
+ transport,
+ identity,
+ partStat: "ACCEPTED",
+ noReply: true,
+ },
+ event
+ );
+ await calendar.deleteItem(event);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests tentatively accepting an invitation without sending a response.
+ */
+add_task(async function testTentativeWithoutResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml")));
+ await clickMenuAction(win, "imipTentativeButton", "imipTentativeButton_TentativeDontSend");
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await doImipBarActionTest(
+ {
+ calendar,
+ transport,
+ identity,
+ partStat: "TENTATIVE",
+ noReply: true,
+ },
+ event
+ );
+
+ await calendar.deleteItem(event);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests declining an invitation without sending a response.
+ */
+add_task(async function testDeclineWithoutResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml")));
+ await clickMenuAction(win, "imipDeclineButton", "imipDeclineButton_DeclineDontSend");
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await doImipBarActionTest(
+ {
+ calendar,
+ transport,
+ identity,
+ partStat: "DECLINED",
+ noReply: true,
+ },
+ event
+ );
+
+ await calendar.deleteItem(event);
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/comm/calendar/test/browser/invitations/browser_imipBarCancel.js b/comm/calendar/test/browser/invitations/browser_imipBarCancel.js
new file mode 100644
index 0000000000..3cde7d4656
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/browser_imipBarCancel.js
@@ -0,0 +1,129 @@
+/* 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/. */
+
+/**
+ * Tests for processing cancellations via the imip-bar.
+ */
+
+"use strict";
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
+
+var { CalendarTestUtils } = ChromeUtils.import(
+ "resource://testing-common/calendar/CalendarTestUtils.jsm"
+);
+
+let identity;
+let calendar;
+let transport;
+
+/**
+ * Initialize account, identity and calendar.
+ */
+add_setup(async function () {
+ let account = MailServices.accounts.createAccount();
+ account.incomingServer = MailServices.accounts.createIncomingServer(
+ "receiver",
+ "example.com",
+ "imap"
+ );
+ identity = MailServices.accounts.createIdentity();
+ identity.email = "receiver@example.com";
+ account.addIdentity(identity);
+
+ await CalendarTestUtils.setCalendarView(window, "month");
+ window.goToDate(cal.createDateTime("20220316T191602Z"));
+
+ calendar = CalendarTestUtils.createCalendar("Test");
+ transport = new EmailTransport(account, identity);
+
+ let getImipTransport = cal.itip.getImipTransport;
+ cal.itip.getImipTransport = () => transport;
+
+ let deleteMgr = Cc["@mozilla.org/calendar/deleted-items-manager;1"].getService(
+ Ci.calIDeletedItems
+ ).wrappedJSObject;
+ let markDeleted = deleteMgr.markDeleted;
+ deleteMgr.markDeleted = () => {};
+
+ registerCleanupFunction(() => {
+ MailServices.accounts.removeAccount(account, true);
+ cal.itip.getImipTransport = getImipTransport;
+ deleteMgr.markDeleted = markDeleted;
+ CalendarTestUtils.removeCalendar(calendar);
+ });
+});
+
+/**
+ * Tests accepting a cancellation to an already accepted event.
+ */
+add_task(async function testCancelAccepted() {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/single-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, "imipAcceptButton");
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await BrowserTestUtils.closeWindow(win);
+ await doCancelTest({
+ transport,
+ calendar,
+ event,
+ });
+});
+
+/**
+ * Tests accepting a cancellation to tentatively accepted event.
+ */
+add_task(async function testCancelTentative() {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/single-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, "imipTentativeButton");
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await BrowserTestUtils.closeWindow(win);
+ await doCancelTest({
+ transport,
+ calendar,
+ event,
+ });
+});
+
+/**
+ * Tests accepting a cancellation to an already declined event.
+ */
+add_task(async function testCancelDeclined() {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/single-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, "imipDeclineButton");
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await BrowserTestUtils.closeWindow(win);
+ await doCancelTest({
+ transport,
+ calendar,
+ event,
+ });
+});
+
+/**
+ * Tests the handling of a cancellation when the event was not processed
+ * previously.
+ */
+add_task(async function testUnprocessedCancel() {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/cancel-single-event.eml"));
+ let win = await openImipMessage(invite);
+
+ // There should be no buttons present because there is no action to take.
+ // Note: the imip-bar message "This message contains an event that has already been processed" is
+ // misleading.
+ for (let button of [...win.document.querySelectorAll("#imip-view-toolbar > toolbarbutton")]) {
+ Assert.ok(button.hidden, `${button.id} is hidden`);
+ }
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/comm/calendar/test/browser/invitations/browser_imipBarEmail.js b/comm/calendar/test/browser/invitations/browser_imipBarEmail.js
new file mode 100644
index 0000000000..a3816b65dd
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/browser_imipBarEmail.js
@@ -0,0 +1,168 @@
+/* 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/. */
+
+/**
+ * Test that the IMIP bar behaves properly for eml files with invites.
+ */
+
+/* eslint-disable @microsoft/sdl/no-insecure-url */
+
+function getFileFromChromeURL(leafName) {
+ let ChromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIChromeRegistry);
+
+ let url = Services.io.newURI(getRootDirectory(gTestPath) + leafName);
+ info(url.spec);
+ let fileURL = ChromeRegistry.convertChromeURL(url).QueryInterface(Ci.nsIFileURL);
+ return fileURL.file;
+}
+
+/**
+ * Test that when opening a message containing a Teams meeting invite
+ * works as it should.
+ */
+add_task(async function test_event_from_eml() {
+ let file = getFileFromChromeURL("data/teams-meeting-invite.eml");
+
+ let msgWindow = await openMessageFromFile(file);
+ let aboutMessage = msgWindow.document.getElementById("messageBrowser").contentWindow;
+
+ await TestUtils.waitForCondition(
+ () => !aboutMessage.document.getElementById("imip-bar").collapsed
+ );
+ info("Ok, iMIP bar is showing");
+
+ // The contentDocument has both the imipHTMLDetails HTML part generated by us,
+ // and the regular HTML part generated by the sender (the server).
+ let links = [
+ ...msgWindow.content.document.getElementById("imipHTMLDetails").querySelectorAll("a"),
+ ];
+
+ Assert.equal(links.length, 3, "The 3 links should show");
+
+ // Check the links and their text
+ Assert.equal(
+ links[0].href,
+ "https://teams.microsoft.com/l/meetup-join/19%3ameeting_MGU5NmI2ZGYtOWZmOC00Y2ZmLWJlOTItNjUxNjA5YjUyYTYy%40thread.v2/0?context=%7b%22Tid%22%3a%222fd0c1c5-28e1-40c4-9f0d-a0363ca80a3c%22%2c%22Oid%22%3a%2214464d09-ceb8-458c-a61c-717f1e5c66c5%22%7d",
+ "link0 href"
+ );
+ Assert.equal(
+ links[0].textContent,
+ "<https://teams.microsoft.com/l/meetup-join/19%3ameeting_MGU5NmI2ZGYtOWZmOC00Y2ZmLWJlOTItNjUxNjA5YjUyYTYy%40thread.v2/0?context=%7b%22Tid%22%3a%222fd0c1c5-28e1-40c4-9f0d-a0363ca80a3c%22%2c%22Oid%22%3a%2214464d09-ceb8-458c-a61c-717f1e5c66c5%22%7d>",
+ "link0 textContent"
+ );
+
+ Assert.equal(links[1].href, "https://aka.ms/JoinTeamsMeeting", "link1 href");
+ Assert.equal(links[1].textContent, "<https://aka.ms/JoinTeamsMeeting>", "link1 textContent");
+
+ Assert.equal(
+ links[2].href,
+ "https://teams.microsoft.com/meetingOptions/?organizerId=14464d09-ceb8-458c-a61c-717f1e5c66c5&tenantId=2fd0c1c5-28e1-40c4-9f0d-a0363ca80a3c&threadId=19_meeting_MGU5NmI2ZGYtOWZmOC00Y2ZmLWJlOTItNjUxNjA5YjUyYTYy@thread.v2&messageId=0&language=fi-FI",
+ "link2 href"
+ );
+ Assert.equal(
+ links[2].textContent,
+ "<https://teams.microsoft.com/meetingOptions/?organizerId=14464d09-ceb8-458c-a61c-717f1e5c66c5&tenantId=2fd0c1c5-28e1-40c4-9f0d-a0363ca80a3c&threadId=19_meeting_MGU5NmI2ZGYtOWZmOC00Y2ZmLWJlOTItNjUxNjA5YjUyYTYy@thread.v2&messageId=0&language=fi-FI>",
+ "link2 textContent"
+ );
+
+ await BrowserTestUtils.closeWindow(msgWindow);
+
+ Assert.ok(true, "test_event_from_eml test ran to completion");
+});
+
+/**
+ * Test that when opening a message containing a Meet meeting invite
+ * works as it should.
+ */
+add_task(async function test_event_from_eml() {
+ let file = getFileFromChromeURL("data/meet-meeting-invite.eml");
+
+ let msgWindow = await openMessageFromFile(file);
+ let aboutMessage = msgWindow.document.getElementById("messageBrowser").contentWindow;
+
+ await TestUtils.waitForCondition(
+ () => !aboutMessage.document.getElementById("imip-bar").collapsed
+ );
+ info("Ok, iMIP bar is showing");
+
+ // The contentDocument has both the imipHTMLDetails HTML part generated by us,
+ // and the regular HTML part generated by the sender (the server).
+ let links = [
+ ...msgWindow.content.document.getElementById("imipHTMLDetails").querySelectorAll("a"),
+ ];
+
+ Assert.equal(links.length, 4, "The 4 links should show");
+
+ // Check the links and their text
+ Assert.equal(links[0].href, "mailto:foo@example.com", "link0 href");
+ Assert.equal(links[0].textContent, "<foo@example.com>", "link0 textContent");
+
+ Assert.equal(links[1].href, "http://example.com/?foo=bar", "link1 href");
+ Assert.equal(links[1].textContent, "http://example.com?foo=bar", "link1 textContent");
+
+ Assert.equal(links[2].href, "https://meet.google.com/pyb-ndcu-hhc", "link1 href");
+ Assert.equal(links[2].textContent, "https://meet.google.com/pyb-ndcu-hhc", "link1 textContent");
+
+ Assert.equal(
+ links[3].href,
+ "https://calendar.google.com/calendar/event?action=VIEW&eid=NjVtMTdoc2RvbG1vdHYza3ZtcnRnNDBvbnQgbWFnbnVzLm1lbGluQGh1dC5maQ&tok=MjEjYmVydGF0aGVib3RAZ21haWwuY29tZTg2NGFjYmNjYWE1MjVlZWJmY2UzYmRmMDAyNWU0MDkzNDAxZjRhZg&ctz=Europe%2FHelsinki&hl=sv&es=1",
+ "link2 href"
+ );
+ Assert.equal(
+ links[3].textContent,
+ "https://calendar.google.com/calendar/event?action=VIEW&eid=NjVtMTdoc2RvbG1vdHYza3ZtcnRnNDBvbnQgbWFnbnVzLm1lbGluQGh1dC5maQ&tok=MjEjYmVydGF0aGVib3RAZ21haWwuY29tZTg2NGFjYmNjYWE1MjVlZWJmY2UzYmRmMDAyNWU0MDkzNDAxZjRhZg&ctz=Europe%2FHelsinki&hl=sv&es=1",
+ "link2 textContent"
+ );
+
+ await BrowserTestUtils.closeWindow(msgWindow);
+
+ Assert.ok(true, "test_event_from_eml test ran to completion");
+});
+
+/**
+ * Test that when opening a message containing an outlook invite with "empty"
+ * content works as it should.
+ */
+add_task(async function test_outlook_event_from_eml() {
+ let file = getFileFromChromeURL("data/outlook-test-invite.eml");
+
+ let msgWindow = await openMessageFromFile(file);
+ let aboutMessage = msgWindow.document.getElementById("messageBrowser").contentWindow;
+
+ await TestUtils.waitForCondition(
+ () => !aboutMessage.document.getElementById("imip-bar").collapsed
+ );
+ info("Ok, iMIP bar is showing");
+
+ let details = msgWindow.content.document.getElementById("imipHTMLDetails");
+
+ Assert.equal(
+ details.getAttribute("open"),
+ "open",
+ "Details should be expanded when the message doesn't include good details"
+ );
+
+ await BrowserTestUtils.closeWindow(msgWindow);
+
+ Assert.ok(true, "test_outlook_event_from_eml test ran to completion");
+});
+
+/**
+ * Test that when opening a message containing an event, the IMIP bar shows.
+ */
+add_task(async function test_event_from_eml() {
+ let file = getFileFromChromeURL("data/message-containing-event.eml");
+
+ let msgWindow = await openMessageFromFile(file);
+ let aboutMessage = msgWindow.document.getElementById("messageBrowser").contentWindow;
+
+ await TestUtils.waitForCondition(
+ () => !aboutMessage.document.getElementById("imip-bar").collapsed
+ );
+ info("Ok, iMIP bar is showing");
+
+ await BrowserTestUtils.closeWindow(msgWindow);
+
+ Assert.ok(true, "test_event_from_eml test ran to completion");
+});
diff --git a/comm/calendar/test/browser/invitations/browser_imipBarExceptionCancel.js b/comm/calendar/test/browser/invitations/browser_imipBarExceptionCancel.js
new file mode 100644
index 0000000000..7800e742ca
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/browser_imipBarExceptionCancel.js
@@ -0,0 +1,137 @@
+/* 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/. */
+
+/**
+ * Tests for processing cancellations to recurring event exceptions.
+ */
+
+"use strict";
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
+
+var { CalendarTestUtils } = ChromeUtils.import(
+ "resource://testing-common/calendar/CalendarTestUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ CalEvent: "resource:///modules/CalEvent.jsm",
+});
+
+let identity;
+let calendar;
+let transport;
+
+/**
+ * Initialize account, identity and calendar.
+ */
+add_setup(async function () {
+ requestLongerTimeout(3);
+ let account = MailServices.accounts.createAccount();
+ account.incomingServer = MailServices.accounts.createIncomingServer(
+ "receiver",
+ "example.com",
+ "imap"
+ );
+ identity = MailServices.accounts.createIdentity();
+ identity.email = "receiver@example.com";
+ account.addIdentity(identity);
+
+ await CalendarTestUtils.setCalendarView(window, "month");
+ window.goToDate(cal.createDateTime("20220316T191602Z"));
+
+ calendar = CalendarTestUtils.createCalendar("Test");
+ transport = new EmailTransport(account, identity);
+ let getImipTransport = cal.itip.getImipTransport;
+ cal.itip.getImipTransport = () => transport;
+
+ let deleteMgr = Cc["@mozilla.org/calendar/deleted-items-manager;1"].getService(
+ Ci.calIDeletedItems
+ ).wrappedJSObject;
+
+ let markDeleted = deleteMgr.markDeleted;
+ deleteMgr.markDeleted = () => {};
+
+ registerCleanupFunction(() => {
+ MailServices.accounts.removeAccount(account, true);
+ cal.itip.getImipTransport = getImipTransport;
+ deleteMgr.markDeleted = markDeleted;
+ CalendarTestUtils.removeCalendar(calendar);
+ });
+});
+
+/**
+ * Tests cancelling an exception works.
+ */
+add_task(async function testCancelException() {
+ for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) {
+ await doCancelExceptionTest({
+ calendar,
+ transport,
+ identity,
+ partStat,
+ recurrenceId: "20220317T110000Z",
+ isRecurring: true,
+ });
+ }
+});
+
+/**
+ * Tests cancelling an event with only an exception processed works.
+ */
+add_task(async function testCancelExceptionOnly() {
+ for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) {
+ let win = await openImipMessage(
+ new FileUtils.File(getTestFilePath("data/exception-major.eml"))
+ );
+ await clickAction(win, actionIds.single.button[partStat]);
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 5, 1)).item;
+ await BrowserTestUtils.closeWindow(win);
+ await doCancelTest({
+ calendar,
+ event,
+ transport,
+ identity,
+ });
+ }
+});
+
+/**
+ * Tests processing a cancellation for a recurring event works when only an
+ * exception was processed previously.
+ */
+add_task(async function testCancelSeriesWithExceptionOnly() {
+ for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) {
+ let win = await openImipMessage(
+ new FileUtils.File(getTestFilePath("data/exception-major.eml"))
+ );
+ await clickMenuAction(
+ win,
+ actionIds.single.button[partStat],
+ actionIds.single.noReply[partStat]
+ );
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 5, 1)).item;
+ await BrowserTestUtils.closeWindow(win);
+
+ let cancel = new FileUtils.File(getTestFilePath("data/cancel-repeat-event.eml"));
+ let cancelWin = await openImipMessage(cancel);
+ let aboutMessage = cancelWin.document.getElementById("messageBrowser").contentWindow;
+
+ let deleteButton = aboutMessage.document.getElementById("imipDeleteButton");
+ Assert.ok(!deleteButton.hidden, `#${deleteButton.id} button shown`);
+ EventUtils.synthesizeMouseAtCenter(deleteButton, {}, aboutMessage);
+ await BrowserTestUtils.closeWindow(cancelWin);
+ await CalendarTestUtils.monthView.waitForNoItemAt(window, 3, 5, 1);
+ Assert.ok(!(await calendar.getItem(event.id)), "event was deleted");
+
+ Assert.equal(
+ transport.sentItems.length,
+ 0,
+ "itip subsystem did not attempt to send a response"
+ );
+ Assert.equal(transport.sentMsgs.length, 0, "no call was made into the mail subsystem");
+ }
+});
diff --git a/comm/calendar/test/browser/invitations/browser_imipBarExceptionOnly.js b/comm/calendar/test/browser/invitations/browser_imipBarExceptionOnly.js
new file mode 100644
index 0000000000..88ad0b3c41
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/browser_imipBarExceptionOnly.js
@@ -0,0 +1,262 @@
+/* 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/. */
+
+/**
+ * Tests for receiving an invitation exception but the original event was not
+ * processed first.
+ */
+"use strict";
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
+
+var { CalendarTestUtils } = ChromeUtils.import(
+ "resource://testing-common/calendar/CalendarTestUtils.jsm"
+);
+
+let identity;
+let calendar;
+let transport;
+
+/**
+ * Initialize account, identity and calendar.
+ */
+add_setup(async function () {
+ requestLongerTimeout(5);
+ let account = MailServices.accounts.createAccount();
+ account.incomingServer = MailServices.accounts.createIncomingServer(
+ "receiver",
+ "example.com",
+ "imap"
+ );
+ identity = MailServices.accounts.createIdentity();
+ identity.email = "receiver@example.com";
+ account.addIdentity(identity);
+
+ await CalendarTestUtils.setCalendarView(window, "month");
+ window.goToDate(cal.createDateTime("20220316T191602Z"));
+
+ calendar = CalendarTestUtils.createCalendar("Test");
+ transport = new EmailTransport(account, identity);
+
+ let getImipTransport = cal.itip.getImipTransport;
+ cal.itip.getImipTransport = () => transport;
+
+ let deleteMgr = Cc["@mozilla.org/calendar/deleted-items-manager;1"].getService(
+ Ci.calIDeletedItems
+ ).wrappedJSObject;
+ let markDeleted = deleteMgr.markDeleted;
+ deleteMgr.markDeleted = () => {};
+
+ registerCleanupFunction(() => {
+ MailServices.accounts.removeAccount(account, true);
+ cal.itip.getImipTransport = getImipTransport;
+ deleteMgr.markDeleted = markDeleted;
+ CalendarTestUtils.removeCalendar(calendar);
+ });
+});
+
+/**
+ * Tests accepting a minor exception and sending a response.
+ */
+add_task(async function testMinorAcceptWithResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-minor.eml")));
+ await clickAction(win, "imipAcceptButton");
+ await doExceptionOnlyTest({
+ calendar,
+ transport,
+ identity,
+ partStat: "ACCEPTED",
+ });
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests tentatively accepting a minor exception and sending a response.
+ */
+add_task(async function testMinorTentativeWithResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-minor.eml")));
+ await clickAction(win, "imipTentativeButton");
+ await doExceptionOnlyTest({
+ calendar,
+ transport,
+ identity,
+ partStat: "TENTATIVE",
+ });
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests declining a minor exception and sending a response.
+ */
+add_task(async function testMinorDeclineWithResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-minor.eml")));
+ await clickAction(win, "imipDeclineButton");
+ await doExceptionOnlyTest({
+ calendar,
+ transport,
+ identity,
+ partStat: "DECLINED",
+ });
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests accepting a minor exception without sending a response.
+ */
+add_task(async function testMinorAcceptWithoutResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-minor.eml")));
+ await clickMenuAction(win, "imipAcceptButton", "imipAcceptButton_AcceptDontSend");
+ await doExceptionOnlyTest({
+ calendar,
+ transport,
+ identity,
+ partStat: "ACCEPTED",
+ noReply: true,
+ });
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests tentatively accepting a minor exception without sending a response.
+ */
+add_task(async function testMinorTentativeWithoutResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-minor.eml")));
+ await clickMenuAction(win, "imipTentativeButton", "imipTentativeButton_TentativeDontSend");
+ await doExceptionOnlyTest({
+ calendar,
+ transport,
+ identity,
+ partStat: "TENTATIVE",
+ noReply: true,
+ });
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests declining a minor exception without sending a response.
+ */
+add_task(async function testMinorDeclineWithoutResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-minor.eml")));
+ await clickMenuAction(win, "imipDeclineButton", "imipDeclineButton_DeclineDontSend");
+ await doExceptionOnlyTest({
+ calendar,
+ transport,
+ identity,
+ partStat: "DECLINED",
+ noReply: true,
+ });
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests accepting a major exception and sending a response.
+ */
+add_task(async function testMajorAcceptWithResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-major.eml")));
+ await clickAction(win, "imipAcceptButton");
+ await doExceptionOnlyTest({
+ calendar,
+ transport,
+ identity,
+ partStat: "ACCEPTED",
+ isMajor: true,
+ });
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests tentatively accepting a major exception and sending a response.
+ */
+add_task(async function testMajorTentativeWithResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-major.eml")));
+ await clickAction(win, "imipTentativeButton");
+ await doExceptionOnlyTest({
+ calendar,
+ transport,
+ identity,
+ partStat: "TENTATIVE",
+ isMajor: true,
+ });
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests declining a major exception and sending a response.
+ */
+add_task(async function testMajorDeclineWithResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-major.eml")));
+ await clickAction(win, "imipDeclineButton");
+ await doExceptionOnlyTest({
+ calendar,
+ transport,
+ identity,
+ partStat: "DECLINED",
+ isMajor: true,
+ });
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests accepting a major exception without sending a response.
+ */
+add_task(async function testMajorAcceptWithoutResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-major.eml")));
+ await clickMenuAction(win, "imipAcceptButton", "imipAcceptButton_AcceptDontSend");
+ await doExceptionOnlyTest({
+ calendar,
+ transport,
+ identity,
+ partStat: "ACCEPTED",
+ noReply: true,
+ isMajor: true,
+ });
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests tentatively accepting a major exception without sending a response.
+ */
+add_task(async function testMajorTentativeWithoutResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-major.eml")));
+ await clickMenuAction(win, "imipTentativeButton", "imipTentativeButton_TentativeDontSend");
+ await doExceptionOnlyTest({
+ calendar,
+ transport,
+ identity,
+ partStat: "TENTATIVE",
+ noReply: true,
+ isMajor: true,
+ });
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests declining a major exception without sending a response.
+ */
+add_task(async function testMajorDeclineWithoutResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-major.eml")));
+ await clickMenuAction(win, "imipDeclineButton", "imipDeclineButton_DeclineDontSend");
+ await doExceptionOnlyTest({
+ calendar,
+ transport,
+ identity,
+ partStat: "DECLINED",
+ noReply: true,
+ isMajor: true,
+ });
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/comm/calendar/test/browser/invitations/browser_imipBarExceptions.js b/comm/calendar/test/browser/invitations/browser_imipBarExceptions.js
new file mode 100644
index 0000000000..2cdf18ed59
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/browser_imipBarExceptions.js
@@ -0,0 +1,288 @@
+/* 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/. */
+
+/**
+ * Tests for handling exceptions to recurring event invitations via the imip-bar.
+ */
+
+"use strict";
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
+
+var { CalendarTestUtils } = ChromeUtils.import(
+ "resource://testing-common/calendar/CalendarTestUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ CalEvent: "resource:///modules/CalEvent.jsm",
+});
+
+let identity;
+let calendar;
+let transport;
+
+/**
+ * Initialize account, identity and calendar.
+ */
+add_setup(async function () {
+ requestLongerTimeout(5);
+ let account = MailServices.accounts.createAccount();
+ account.incomingServer = MailServices.accounts.createIncomingServer(
+ "receiver",
+ "example.com",
+ "imap"
+ );
+ identity = MailServices.accounts.createIdentity();
+ identity.email = "receiver@example.com";
+ account.addIdentity(identity);
+
+ await CalendarTestUtils.setCalendarView(window, "month");
+ window.goToDate(cal.createDateTime("20220316T191602Z"));
+
+ calendar = CalendarTestUtils.createCalendar("Test");
+ transport = new EmailTransport(account, identity);
+ let getImipTransport = cal.itip.getImipTransport;
+ cal.itip.getImipTransport = () => transport;
+
+ let deleteMgr = Cc["@mozilla.org/calendar/deleted-items-manager;1"].getService(
+ Ci.calIDeletedItems
+ ).wrappedJSObject;
+
+ let markDeleted = deleteMgr.markDeleted;
+ deleteMgr.markDeleted = () => {};
+
+ registerCleanupFunction(() => {
+ MailServices.accounts.removeAccount(account, true);
+ cal.itip.getImipTransport = getImipTransport;
+ deleteMgr.markDeleted = markDeleted;
+ CalendarTestUtils.removeCalendar(calendar);
+ });
+});
+
+/**
+ * Tests a minor update exception to an already accepted recurring event.
+ */
+add_task(async function testMinorUpdateExceptionToAccepted() {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, "imipAcceptRecurrencesButton");
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMinorExceptionTest({
+ transport,
+ calendar,
+ partStat: "ACCEPTED",
+ });
+});
+
+/**
+ * Tests a minor update exception to an already tentatively accepted recurring
+ * event.
+ */
+add_task(async function testMinorUpdateExceptionToTentative() {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, "imipTentativeRecurrencesButton");
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMinorExceptionTest({
+ transport,
+ calendar,
+ partStat: "TENTATIVE",
+ });
+});
+
+/**
+ * Tests a minor update exception to an already declined recurring declined
+ * event.
+ */
+add_task(async function testMinorUpdateExceptionToDeclined() {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, "imipDeclineRecurrencesButton");
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMinorExceptionTest({
+ transport,
+ calendar,
+ partStat: "DECLINED",
+ });
+});
+
+/**
+ * Tests a major update exception to an already accepted event.
+ */
+add_task(async function testMajorExceptionToAcceptedWithResponse() {
+ for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, "imipAcceptRecurrencesButton");
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMajorExceptionTest({
+ transport,
+ identity,
+ calendar,
+ partStat,
+ });
+ }
+});
+
+/**
+ * Tests a major update exception to an already tentatively accepted event.
+ */
+add_task(async function testMajorExceptionToTentativeWithResponse() {
+ for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, "imipTentativeRecurrencesButton");
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMajorExceptionTest({
+ transport,
+ identity,
+ calendar,
+ partStat,
+ });
+ }
+});
+
+/**
+ * Tests a major update exception to an already declined event.
+ */
+add_task(async function testMajorExceptionToDeclinedWithResponse() {
+ for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, "imipDeclineRecurrencesButton");
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMajorExceptionTest({
+ transport,
+ identity,
+ calendar,
+ isRecurring: true,
+ partStat,
+ });
+ }
+});
+
+/**
+ * Tests a major update exception to an already accepted event without sending
+ * a reply.
+ */
+add_task(async function testMajorExecptionToAcceptedWithoutResponse() {
+ for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickMenuAction(
+ win,
+ "imipAcceptRecurrencesButton",
+ "imipAcceptRecurrencesButton_AcceptDontSend"
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMajorExceptionTest({
+ transport,
+ calendar,
+ isRecurring: true,
+ partStat,
+ noReply: true,
+ });
+ }
+});
+
+/**
+ * Tests a major update exception to an already tentatively accepted event
+ * without sending a reply.
+ */
+add_task(async function testMajorUpdateToTentativeWithoutResponse() {
+ for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickMenuAction(
+ win,
+ "imipTentativeRecurrencesButton",
+ "imipTentativeRecurrencesButton_TentativeDontSend"
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMajorExceptionTest({
+ transport,
+ calendar,
+ isRecurring: true,
+ partStat,
+ noReply: true,
+ });
+ }
+});
+
+/**
+ * Tests a major update exception to a declined event without sending a reply.
+ */
+add_task(async function testMajorUpdateToDeclinedWithoutResponse() {
+ for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickMenuAction(
+ win,
+ "imipDeclineRecurrencesButton",
+ "imipDeclineRecurrencesButton_DeclineDontSend"
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMajorExceptionTest({
+ transport,
+ calendar,
+ isRecurring: true,
+ partStat,
+ noReply: true,
+ });
+ }
+});
+
+/**
+ * Tests a major update exception to an event where the participation status
+ * is still "NEEDS-ACTION". Here we want to ensure action is only taken on the
+ * target exception date and not the other dates.
+ */
+add_task(async function testMajorUpdateToNeedsAction() {
+ for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) {
+ transport.reset();
+
+ // Extract the event from the .eml file and manually add it to the calendar.
+ let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml"));
+ let srcText = await IOUtils.readUTF8(invite.path);
+ let ics = srcText.match(
+ /--00000000000080f3da05db4aef59[\S\s]+--00000000000080f3da05db4aef59/g
+ )[0];
+ ics = ics.split("--00000000000080f3da05db4aef59").join("");
+ ics = ics.replaceAll(/Content-(Type|Transfer-Encoding)?: .*/g, "");
+
+ let event = new CalEvent(ics);
+
+ // This will not be set because we manually added the event.
+ event.setProperty("x-moz-received-dtstamp", "20220316T191602Z");
+
+ await calendar.addItem(event);
+ await CalendarTestUtils.monthView.waitForItemAt(window, 3, 5, 1).item;
+ await doMajorExceptionTest({
+ transport,
+ identity,
+ calendar,
+ isRecurring: true,
+ partStat,
+ });
+ }
+});
diff --git a/comm/calendar/test/browser/invitations/browser_imipBarRepeat.js b/comm/calendar/test/browser/invitations/browser_imipBarRepeat.js
new file mode 100644
index 0000000000..c14ff2c0a5
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/browser_imipBarRepeat.js
@@ -0,0 +1,218 @@
+/* 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/. */
+
+/**
+ * Tests for receiving recurring event invitations via the imip-bar.
+ */
+"use strict";
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
+
+var { CalendarTestUtils } = ChromeUtils.import(
+ "resource://testing-common/calendar/CalendarTestUtils.jsm"
+);
+
+let identity;
+let calendar;
+let transport;
+
+/**
+ * Initialize account, identity and calendar.
+ */
+add_setup(async function () {
+ let account = MailServices.accounts.createAccount();
+ account.incomingServer = MailServices.accounts.createIncomingServer(
+ "receiver",
+ "example.com",
+ "imap"
+ );
+ identity = MailServices.accounts.createIdentity();
+ identity.email = "receiver@example.com";
+ account.addIdentity(identity);
+
+ await CalendarTestUtils.setCalendarView(window, "month");
+ window.goToDate(cal.createDateTime("20220316T191602Z"));
+
+ calendar = CalendarTestUtils.createCalendar("Test");
+ transport = new EmailTransport(account, identity);
+
+ let getImipTransport = cal.itip.getImipTransport;
+ cal.itip.getImipTransport = () => transport;
+
+ let deleteMgr = Cc["@mozilla.org/calendar/deleted-items-manager;1"].getService(
+ Ci.calIDeletedItems
+ ).wrappedJSObject;
+ let markDeleted = deleteMgr.markDeleted;
+ deleteMgr.markDeleted = () => {};
+
+ registerCleanupFunction(() => {
+ MailServices.accounts.removeAccount(account, true);
+ cal.itip.getImipTransport = getImipTransport;
+ deleteMgr.markDeleted = markDeleted;
+ CalendarTestUtils.removeCalendar(calendar);
+ });
+});
+
+/**
+ * Tests accepting an invitation to a recurring event and sending a response.
+ */
+add_task(async function testAcceptRecurringWithResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/repeat-event.eml")));
+ await clickAction(win, "imipAcceptRecurrencesButton");
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await doImipBarActionTest(
+ {
+ calendar,
+ transport,
+ identity,
+ isRecurring: true,
+ partStat: "ACCEPTED",
+ },
+ event
+ );
+
+ await calendar.deleteItem(event.parentItem);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests tentatively accepting an invitation to a recurring event and sending a
+ * response.
+ */
+add_task(async function testTentativeRecurringWithResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/repeat-event.eml")));
+ await clickAction(win, "imipTentativeRecurrencesButton");
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await doImipBarActionTest(
+ {
+ calendar,
+ transport,
+ identity,
+ isRecurring: true,
+ partStat: "TENTATIVE",
+ },
+ event
+ );
+
+ await calendar.deleteItem(event.parentItem);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests declining an invitation to a recurring event and sending a response.
+ */
+add_task(async function testDeclineRecurringWithResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/repeat-event.eml")));
+ await clickAction(win, "imipDeclineRecurrencesButton");
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+
+ await doImipBarActionTest(
+ {
+ calendar,
+ transport,
+ identity,
+ isRecurring: true,
+ partStat: "DECLINED",
+ },
+ event
+ );
+
+ await calendar.deleteItem(event.parentItem);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests accepting an invitation to a recurring event without sending a response.
+ */
+add_task(async function testAcceptRecurringWithoutResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/repeat-event.eml")));
+ await clickMenuAction(
+ win,
+ "imipAcceptRecurrencesButton",
+ "imipAcceptRecurrencesButton_AcceptDontSend"
+ );
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await doImipBarActionTest(
+ {
+ calendar,
+ transport,
+ identity,
+ isRecurring: true,
+ partStat: "ACCEPTED",
+ noReply: true,
+ },
+ event
+ );
+
+ await calendar.deleteItem(event.parentItem);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests tentatively accepting an invitation to a recurring event without sending
+ * a response.
+ */
+add_task(async function testTentativeRecurringWithoutResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/repeat-event.eml")));
+ await clickMenuAction(
+ win,
+ "imipTentativeRecurrencesButton",
+ "imipTentativeRecurrencesButton_TentativeDontSend"
+ );
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await doImipBarActionTest(
+ {
+ calendar,
+ transport,
+ identity,
+ isRecurring: true,
+ partStat: "TENTATIVE",
+ noReply: true,
+ },
+ event
+ );
+
+ await calendar.deleteItem(event.parentItem);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests declining an invitation to a recurring event without sending a response.
+ */
+add_task(async function testDeclineRecurrencesWithoutResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/repeat-event.eml")));
+ await clickMenuAction(
+ win,
+ "imipDeclineRecurrencesButton",
+ "imipDeclineRecurrencesButton_DeclineDontSend"
+ );
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await doImipBarActionTest(
+ {
+ calendar,
+ transport,
+ identity,
+ isRecurring: true,
+ partStat: "DECLINED",
+ noReply: true,
+ },
+ event
+ );
+
+ await calendar.deleteItem(event.parentItem);
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/comm/calendar/test/browser/invitations/browser_imipBarRepeatCancel.js b/comm/calendar/test/browser/invitations/browser_imipBarRepeatCancel.js
new file mode 100644
index 0000000000..1ab50cc739
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/browser_imipBarRepeatCancel.js
@@ -0,0 +1,186 @@
+/* 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/. */
+
+/**
+ * Tests for processing cancellations to recurring invitations via the imip-bar.
+ */
+"use strict";
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
+
+var { CalendarTestUtils } = ChromeUtils.import(
+ "resource://testing-common/calendar/CalendarTestUtils.jsm"
+);
+
+let identity;
+let calendar;
+let transport;
+
+/**
+ * Initialize account, identity and calendar.
+ */
+add_setup(async function () {
+ requestLongerTimeout(5);
+ let account = MailServices.accounts.createAccount();
+ account.incomingServer = MailServices.accounts.createIncomingServer(
+ "receiver",
+ "example.com",
+ "imap"
+ );
+ identity = MailServices.accounts.createIdentity();
+ identity.email = "receiver@example.com";
+ account.addIdentity(identity);
+
+ await CalendarTestUtils.setCalendarView(window, "month");
+ window.goToDate(cal.createDateTime("20220316T191602Z"));
+
+ calendar = CalendarTestUtils.createCalendar("Test");
+ transport = new EmailTransport(account, identity);
+
+ let getImipTransport = cal.itip.getImipTransport;
+ cal.itip.getImipTransport = () => transport;
+
+ let deleteMgr = Cc["@mozilla.org/calendar/deleted-items-manager;1"].getService(
+ Ci.calIDeletedItems
+ ).wrappedJSObject;
+ let markDeleted = deleteMgr.markDeleted;
+ deleteMgr.markDeleted = () => {};
+
+ registerCleanupFunction(() => {
+ MailServices.accounts.removeAccount(account, true);
+ cal.itip.getImipTransport = getImipTransport;
+ deleteMgr.markDeleted = markDeleted;
+ CalendarTestUtils.removeCalendar(calendar);
+ });
+});
+
+/**
+ * Tests accepting a cancellation to an already accepted recurring event.
+ */
+add_task(async function testCancelAcceptedRecurring() {
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/repeat-event.eml")));
+ await clickAction(win, "imipAcceptRecurrencesButton");
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await BrowserTestUtils.closeWindow(win);
+ await doCancelTest({
+ calendar,
+ event,
+ transport,
+ isRecurring: true,
+ });
+});
+
+/**
+ * Tests accepting a cancellation to an already tentatively accepted event.
+ */
+add_task(async function testCancelTentativeRecurring() {
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/repeat-event.eml")));
+ await clickAction(win, "imipTentativeRecurrencesButton");
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await BrowserTestUtils.closeWindow(win);
+ await doCancelTest({
+ calendar,
+ event,
+ transport,
+ identity,
+ isRecurring: true,
+ });
+});
+
+/**
+ * Tests accepting a cancellation to an already declined recurring event.
+ */
+add_task(async function testCancelDeclinedRecurring() {
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/repeat-event.eml")));
+ await clickAction(win, "imipDeclineRecurrencesButton");
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await BrowserTestUtils.closeWindow(win);
+ await doCancelTest({
+ calendar,
+ event,
+ transport,
+ identity,
+ isRecurring: true,
+ });
+});
+
+/**
+ * Tests accepting a cancellation to a single occurrence of an already accepted
+ * recurring event.
+ */
+add_task(async function testCancelAcceptedOccurrence() {
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/repeat-event.eml")));
+ await clickAction(win, "imipAcceptRecurrencesButton");
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await BrowserTestUtils.closeWindow(win);
+ await doCancelTest({
+ calendar,
+ event,
+ transport,
+ isRecurring: true,
+ recurrenceId: "20220317T110000Z",
+ });
+ await calendar.deleteItem(event.parentItem);
+});
+
+/**
+ * Tests accepting a cancellation to a single occurrence of an already tentatively
+ * accepted event.
+ */
+add_task(async function testCancelTentativeOccurrence() {
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/repeat-event.eml")));
+ await clickAction(win, "imipTentativeRecurrencesButton");
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await BrowserTestUtils.closeWindow(win);
+ await doCancelTest({
+ calendar,
+ event,
+ transport,
+ identity,
+ isRecurring: true,
+ recurrenceId: "20220317T110000Z",
+ });
+ await calendar.deleteItem(event.parentItem);
+});
+
+/**
+ * Tests accepting a cancellation to a single occurrence of an already declined
+ * recurring event.
+ */
+add_task(async function testCancelDeclinedOccurrence() {
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/repeat-event.eml")));
+ await clickAction(win, "imipDeclineRecurrencesButton");
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await BrowserTestUtils.closeWindow(win);
+ await doCancelTest({
+ calendar,
+ event,
+ transport,
+ identity,
+ isRecurring: true,
+ recurrenceId: "20220317T110000Z",
+ });
+ await calendar.deleteItem(event.parentItem);
+});
+
+/**
+ * Tests the handling of a cancellation when the event was not processed
+ * previously.
+ */
+add_task(async function testUnprocessedCancel() {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/cancel-repeat-event.eml"));
+ let win = await openImipMessage(invite);
+ for (let button of [...win.document.querySelectorAll("#imip-view-toolbar > toolbarbutton")]) {
+ Assert.ok(button.hidden, `${button.id} is hidden`);
+ }
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/comm/calendar/test/browser/invitations/browser_imipBarRepeatUpdates.js b/comm/calendar/test/browser/invitations/browser_imipBarRepeatUpdates.js
new file mode 100644
index 0000000000..7f0d16f627
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/browser_imipBarRepeatUpdates.js
@@ -0,0 +1,247 @@
+/* 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/. */
+
+/**
+ * Tests for receiving minor and major updates to recurring event invitations
+ * via the imip-bar.
+ */
+
+"use strict";
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
+
+var { CalendarTestUtils } = ChromeUtils.import(
+ "resource://testing-common/calendar/CalendarTestUtils.jsm"
+);
+
+let identity;
+let calendar;
+let transport;
+
+/**
+ * Initialize account, identity and calendar.
+ */
+add_setup(async function () {
+ requestLongerTimeout(5);
+ let account = MailServices.accounts.createAccount();
+ account.incomingServer = MailServices.accounts.createIncomingServer(
+ "receiver",
+ "example.com",
+ "imap"
+ );
+ identity = MailServices.accounts.createIdentity();
+ identity.email = "receiver@example.com";
+ account.addIdentity(identity);
+
+ await CalendarTestUtils.setCalendarView(window, "month");
+ window.goToDate(cal.createDateTime("20220316T191602Z"));
+
+ calendar = CalendarTestUtils.createCalendar("Test");
+ transport = new EmailTransport(account, identity);
+ let getImipTransport = cal.itip.getImipTransport;
+ cal.itip.getImipTransport = () => transport;
+
+ let deleteMgr = Cc["@mozilla.org/calendar/deleted-items-manager;1"].getService(
+ Ci.calIDeletedItems
+ ).wrappedJSObject;
+ let markDeleted = deleteMgr.markDeleted;
+ deleteMgr.markDeleted = () => {};
+
+ registerCleanupFunction(() => {
+ MailServices.accounts.removeAccount(account, true);
+ cal.itip.getImipTransport = getImipTransport;
+ deleteMgr.markDeleted = markDeleted;
+ CalendarTestUtils.removeCalendar(calendar);
+ });
+});
+
+/**
+ * Tests a minor update to an already accepted event.
+ */
+add_task(async function testMinorUpdateToAccepted() {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, "imipAcceptRecurrencesButton");
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMinorUpdateTest({
+ transport,
+ calendar,
+ isRecurring: true,
+ partStat: "ACCEPTED",
+ });
+});
+
+/**
+ * Tests a minor update to an already tentatively accepted event.
+ */
+add_task(async function testMinorUpdateToTentative() {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, "imipTentativeRecurrencesButton");
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMinorUpdateTest({
+ transport,
+ calendar,
+ isRecurring: true,
+ partStat: "TENTATIVE",
+ });
+});
+
+/**
+ * Tests a minor update to an already declined event.
+ */
+add_task(async function testMinorUpdateToDeclined() {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, "imipDeclineRecurrencesButton");
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMinorUpdateTest({ transport, calendar, isRecurring: true, invite, partStat: "DECLINED" });
+});
+
+/**
+ * Tests a major update to an already accepted event.
+ */
+add_task(async function testMajorUpdateToAcceptedWithResponse() {
+ for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, "imipAcceptRecurrencesButton");
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMajorUpdateTest({
+ transport,
+ identity,
+ calendar,
+ isRecurring: true,
+ partStat,
+ });
+ }
+});
+
+/**
+ * Tests a major update to an already tentatively accepted event.
+ */
+add_task(async function testMajorUpdateToTentativeWithResponse() {
+ for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, "imipTentativeRecurrencesButton");
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMajorUpdateTest({
+ transport,
+ identity,
+ calendar,
+ isRecurring: true,
+ partStat,
+ });
+ }
+});
+
+/**
+ * Tests a major update to an already declined event.
+ */
+add_task(async function testMajorUpdateToDeclinedWithResponse() {
+ for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, "imipDeclineRecurrencesButton");
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMajorUpdateTest({
+ transport,
+ identity,
+ calendar,
+ isRecurring: true,
+ partStat,
+ });
+ }
+});
+
+/**
+ * Tests a major update to an already accepted event without replying to the
+ * update.
+ */
+add_task(async function testMajorUpdateToAcceptedWithoutResponse() {
+ for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickMenuAction(
+ win,
+ "imipAcceptRecurrencesButton",
+ "imipAcceptRecurrencesButton_AcceptDontSend"
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMajorUpdateTest({
+ transport,
+ calendar,
+ isRecurring: true,
+ partStat,
+ noReply: true,
+ });
+ }
+});
+
+/**
+ * Tests a major update to an already tentatively accepted event without replying
+ * to the update.
+ */
+add_task(async function testMajorUpdateToTentativeWithoutResponse() {
+ for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickMenuAction(
+ win,
+ "imipTentativeRecurrencesButton",
+ "imipTentativeRecurrencesButton_TentativeDontSend"
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMajorUpdateTest({
+ transport,
+ calendar,
+ isRecurring: true,
+ partStat,
+ noReply: true,
+ });
+ }
+});
+
+/**
+ * Tests a major update to an already declined event.
+ */
+add_task(async function testMajorUpdateToDeclinedWithoutResponse() {
+ for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickMenuAction(
+ win,
+ "imipDeclineRecurrencesButton",
+ "imipDeclineRecurrencesButton_DeclineDontSend"
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMajorUpdateTest({
+ transport,
+ calendar,
+ isRecurring: true,
+ partStat,
+ noReply: true,
+ });
+ }
+});
diff --git a/comm/calendar/test/browser/invitations/browser_imipBarUpdates.js b/comm/calendar/test/browser/invitations/browser_imipBarUpdates.js
new file mode 100644
index 0000000000..d0f5018e89
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/browser_imipBarUpdates.js
@@ -0,0 +1,223 @@
+/* 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/. */
+
+/**
+ * Tests for receiving minor and major updates to invitations via the imip-bar.
+ */
+
+"use strict";
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
+
+var { CalendarTestUtils } = ChromeUtils.import(
+ "resource://testing-common/calendar/CalendarTestUtils.jsm"
+);
+
+let identity;
+let calendar;
+let transport;
+
+/**
+ * Initialize account, identity and calendar.
+ */
+add_setup(async function () {
+ requestLongerTimeout(5);
+ let account = MailServices.accounts.createAccount();
+ account.incomingServer = MailServices.accounts.createIncomingServer(
+ "receiver",
+ "example.com",
+ "imap"
+ );
+ identity = MailServices.accounts.createIdentity();
+ identity.email = "receiver@example.com";
+ account.addIdentity(identity);
+
+ await CalendarTestUtils.setCalendarView(window, "month");
+ window.goToDate(cal.createDateTime("20220316T191602Z"));
+
+ calendar = CalendarTestUtils.createCalendar("Test");
+ transport = new EmailTransport(account, identity);
+
+ let getImipTransport = cal.itip.getImipTransport;
+ cal.itip.getImipTransport = () => transport;
+
+ let deleteMgr = Cc["@mozilla.org/calendar/deleted-items-manager;1"].getService(
+ Ci.calIDeletedItems
+ ).wrappedJSObject;
+ let markDeleted = deleteMgr.markDeleted;
+ deleteMgr.markDeleted = () => {};
+
+ registerCleanupFunction(() => {
+ MailServices.accounts.removeAccount(account, true);
+ cal.itip.getImipTransport = getImipTransport;
+ deleteMgr.markDeleted = markDeleted;
+ CalendarTestUtils.removeCalendar(calendar);
+ });
+});
+
+/**
+ * Tests a minor update to an already accepted event.
+ */
+add_task(async function testMinorUpdateToAccepted() {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/single-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, "imipAcceptButton");
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMinorUpdateTest({
+ transport,
+ calendar,
+ partStat: "ACCEPTED",
+ });
+});
+
+/**
+ * Tests a minor update to an already tentatively accepted event.
+ */
+add_task(async function testMinorUpdateToTentative() {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/single-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, "imipTentativeButton");
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMinorUpdateTest({ transport, calendar, invite, partStat: "TENTATIVE" });
+});
+
+/**
+ * Tests a minor update to an already declined event.
+ */
+add_task(async function testMinorUpdateToDeclined() {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/single-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, "imipDeclineButton");
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMinorUpdateTest({ transport, calendar, invite, partStat: "DECLINED" });
+});
+
+/**
+ * Tests a major update to an already accepted event.
+ */
+add_task(async function testMajorUpdateToAcceptedWithResponse() {
+ for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/single-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, "imipAcceptButton");
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMajorUpdateTest({
+ transport,
+ identity,
+ calendar,
+ partStat,
+ });
+ }
+});
+
+/**
+ * Tests a major update to an already tentatively accepted event.
+ */
+add_task(async function testMajorUpdateToTentativeWithResponse() {
+ for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/single-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, "imipTentativeButton");
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMajorUpdateTest({
+ transport,
+ identity,
+ calendar,
+ partStat,
+ });
+ }
+});
+
+/**
+ * Tests a major update to an already declined event.
+ */
+add_task(async function testMajorUpdateToDeclinedWithResponse() {
+ for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/single-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, "imipDeclineButton");
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMajorUpdateTest({
+ transport,
+ identity,
+ calendar,
+ partStat,
+ });
+ }
+});
+
+/**
+ * Tests a major update to an already accepted event without replying to the
+ * update.
+ */
+add_task(async function testMajorUpdateToAcceptedWithoutResponse() {
+ for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/single-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, "imipAcceptButton");
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMajorUpdateTest({
+ transport,
+ calendar,
+ partStat,
+ noReply: true,
+ });
+ }
+});
+
+/**
+ * Tests a major update to an already tentatively accepted event without replying
+ * to the update.
+ */
+add_task(async function testMajorUpdateToTentativeWithoutResponse() {
+ for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/single-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, "imipTentativeButton");
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMajorUpdateTest({
+ transport,
+ calendar,
+ partStat,
+ noReply: true,
+ });
+ }
+});
+
+/**
+ * Tests a major update to an already declined event.
+ */
+add_task(async function testMajorUpdateToDeclinedWithoutResponse() {
+ for (let partStat of ["ACCEPTED", "TENTATIVE", "DECLINED"]) {
+ transport.reset();
+ let invite = new FileUtils.File(getTestFilePath("data/single-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, "imipDeclineButton");
+
+ await BrowserTestUtils.closeWindow(win);
+ await doMajorUpdateTest({
+ transport,
+ calendar,
+ partStat,
+ noReply: true,
+ });
+ }
+});
diff --git a/comm/calendar/test/browser/invitations/browser_invitationDisplayNew.js b/comm/calendar/test/browser/invitations/browser_invitationDisplayNew.js
new file mode 100644
index 0000000000..a7b3f833de
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/browser_invitationDisplayNew.js
@@ -0,0 +1,257 @@
+/* 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/. */
+
+/**
+ * Tests for the invitation panel display with new events.
+ */
+"use strict";
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { CalItipDefaultEmailTransport } = ChromeUtils.import(
+ "resource:///modules/CalItipEmailTransport.jsm"
+);
+var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
+
+var { CalendarTestUtils } = ChromeUtils.import(
+ "resource://testing-common/calendar/CalendarTestUtils.jsm"
+);
+
+let identity;
+let calendar;
+let transport;
+
+/**
+ * Initialize account, identity and calendar.
+ */
+add_setup(async function () {
+ let account = MailServices.accounts.createAccount();
+ account.incomingServer = MailServices.accounts.createIncomingServer(
+ "receiver",
+ "example.com",
+ "imap"
+ );
+ identity = MailServices.accounts.createIdentity();
+ identity.email = "receiver@example.com";
+ account.addIdentity(identity);
+
+ await CalendarTestUtils.setCalendarView(window, "month");
+ window.goToDate(cal.createDateTime("20220316T191602Z"));
+
+ calendar = CalendarTestUtils.createCalendar("Test");
+ transport = new EmailTransport(account, identity);
+
+ let getImipTransport = cal.itip.getImipTransport;
+ cal.itip.getImipTransport = () => transport;
+
+ let deleteMgr = Cc["@mozilla.org/calendar/deleted-items-manager;1"].getService(
+ Ci.calIDeletedItems
+ ).wrappedJSObject;
+ let markDeleted = deleteMgr.markDeleted;
+ deleteMgr.markDeleted = () => {};
+
+ Services.prefs.setBoolPref("calendar.itip.newInvitationDisplay", true);
+ registerCleanupFunction(() => {
+ MailServices.accounts.removeAccount(account, true);
+ cal.itip.getImipTransport = getImipTransport;
+ deleteMgr.markDeleted = markDeleted;
+ CalendarTestUtils.removeCalendar(calendar);
+ Services.prefs.setBoolPref("calendar.itip.newInvitationDisplay", false);
+ });
+});
+
+/**
+ * Tests the invitation panel shows the correct data when loaded with a new
+ * invitation.
+ */
+add_task(async function testShowPanelData() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml")));
+ let panel = win.document
+ .getElementById("messageBrowser")
+ .contentDocument.querySelector("calendar-invitation-panel");
+
+ if (panel.ownerDocument.hasPendingL10nMutations) {
+ await BrowserTestUtils.waitForEvent(panel.ownerDocument, "L10nMutationsFinished");
+ }
+
+ let notification = panel.shadowRoot.querySelector("notification-message");
+ compareShownPanelValues(notification.shadowRoot, {
+ ".notification-message": "You have been invited to this event.",
+ ".notification-button-container > button": "More",
+ });
+
+ compareShownPanelValues(panel.shadowRoot, {
+ "#title": "Single Event",
+ "#location": "Somewhere",
+ "#partStatTotal": "3 participants",
+ '[data-l10n-id="calendar-invitation-panel-partstat-accepted"]': "1 yes",
+ '[data-l10n-id="calendar-invitation-panel-partstat-needs-action"]': "2 pending",
+ "#attendees li:nth-of-type(1)": "Sender <sender@example.com>",
+ "#attendees li:nth-of-type(2)": "Receiver <receiver@example.com>",
+ "#attendees li:nth-of-type(3)": "Other <other@example.com>",
+ "#description": "An event invitation.",
+ });
+
+ Assert.ok(!panel.shadowRoot.querySelector("#actionButtons").hidden, "action buttons shown");
+ for (let indicator of [
+ ...panel.shadowRoot.querySelectorAll("calendar-invitation-change-indicator"),
+ ]) {
+ Assert.ok(indicator.hidden, `${indicator.id} is hidden`);
+ }
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests accepting an invitation and sending a response.
+ */
+add_task(async function testAcceptWithResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml")));
+ let panel = win.document
+ .getElementById("messageBrowser")
+ .contentDocument.querySelector("calendar-invitation-panel");
+
+ await clickPanelAction(panel, "acceptButton");
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await doImipBarActionTest(
+ {
+ calendar,
+ transport,
+ identity,
+ partStat: "ACCEPTED",
+ },
+ event
+ );
+ await calendar.deleteItem(event);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests tentatively accepting an invitation and sending a response.
+ */
+add_task(async function testTentativeWithResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml")));
+ let panel = win.document
+ .getElementById("messageBrowser")
+ .contentDocument.querySelector("calendar-invitation-panel");
+
+ await clickPanelAction(panel, "tentativeButton");
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await doImipBarActionTest(
+ {
+ calendar,
+ transport,
+ identity,
+ partStat: "TENTATIVE",
+ },
+ event
+ );
+
+ await calendar.deleteItem(event);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests declining an invitation and sending a response.
+ */
+add_task(async function testDeclineWithResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml")));
+ let panel = win.document
+ .getElementById("messageBrowser")
+ .contentDocument.querySelector("calendar-invitation-panel");
+
+ await clickPanelAction(panel, "declineButton");
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await doImipBarActionTest(
+ {
+ calendar,
+ transport,
+ identity,
+ partStat: "DECLINED",
+ },
+ event
+ );
+ await calendar.deleteItem(event);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests accepting an invitation without sending a response.
+ */
+add_task(async function testAcceptWithoutResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml")));
+ let panel = win.document
+ .getElementById("messageBrowser")
+ .contentDocument.querySelector("calendar-invitation-panel");
+
+ await clickPanelAction(panel, "acceptButton", false);
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await doImipBarActionTest(
+ {
+ calendar,
+ transport,
+ identity,
+ partStat: "ACCEPTED",
+ noSend: true,
+ },
+ event
+ );
+ await calendar.deleteItem(event);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests tentatively accepting an invitation without sending a response.
+ */
+add_task(async function testTentativeWithoutResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml")));
+ let panel = win.document
+ .getElementById("messageBrowser")
+ .contentDocument.querySelector("calendar-invitation-panel");
+
+ await clickPanelAction(panel, "tentativeButton", false);
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await doImipBarActionTest(
+ {
+ calendar,
+ transport,
+ identity,
+ partStat: "TENTATIVE",
+ noSend: true,
+ },
+ event
+ );
+ await calendar.deleteItem(event);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests declining an invitation without sending a response.
+ */
+add_task(async function testDeclineWithoutResponse() {
+ transport.reset();
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/single-event.eml")));
+ let panel = win.document
+ .getElementById("messageBrowser")
+ .contentDocument.querySelector("calendar-invitation-panel");
+
+ await clickPanelAction(panel, "declineButton", false);
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ await doImipBarActionTest(
+ {
+ calendar,
+ transport,
+ identity,
+ partStat: "DECLINED",
+ noSend: true,
+ },
+ event
+ );
+ await calendar.deleteItem(event);
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/comm/calendar/test/browser/invitations/browser_unsupportedFreq.js b/comm/calendar/test/browser/invitations/browser_unsupportedFreq.js
new file mode 100644
index 0000000000..2d05ed66dc
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/browser_unsupportedFreq.js
@@ -0,0 +1,107 @@
+/* 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/. */
+
+/**
+ * Tests for ensuring the application does not hang after processing an
+ * unsupported FREQ value.
+ */
+"use strict";
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
+
+var { CalendarTestUtils } = ChromeUtils.import(
+ "resource://testing-common/calendar/CalendarTestUtils.jsm"
+);
+
+let calendar;
+
+/**
+ * Initialize account, identity and calendar.
+ */
+add_setup(async function () {
+ let account = MailServices.accounts.createAccount();
+ account.incomingServer = MailServices.accounts.createIncomingServer(
+ "receiver",
+ "example.com",
+ "imap"
+ );
+
+ let identity = MailServices.accounts.createIdentity();
+ identity.email = "receiver@example.com";
+ account.addIdentity(identity);
+
+ await CalendarTestUtils.setCalendarView(window, "month");
+ window.goToDate(cal.createDateTime("20220316T191602Z"));
+
+ calendar = CalendarTestUtils.createCalendar("Test");
+ registerCleanupFunction(() => {
+ MailServices.accounts.removeAccount(account, true);
+ CalendarTestUtils.removeCalendar(calendar);
+ });
+});
+
+/**
+ * Runs the test using the provided FREQ value.
+ *
+ * @param {string} freq Either "SECONDLY" or "MINUTELY"
+ */
+async function doFreqTest(freq) {
+ let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml"));
+ let srcText = await IOUtils.readUTF8(invite.path);
+ let tmpFile = FileTestUtils.getTempFile(`${freq}.eml`);
+
+ srcText = srcText.replace(/RRULE:.*/g, `RRULE:FREQ=${freq}`);
+ srcText = srcText.replace(/UID:.*/g, `UID:${freq}`);
+ await IOUtils.writeUTF8(tmpFile.path, srcText);
+
+ let win = await openImipMessage(tmpFile);
+ await clickMenuAction(
+ win,
+ "imipAcceptRecurrencesButton",
+ "imipAcceptRecurrencesButton_AcceptDontSend"
+ );
+
+ // Give the view time to refresh and create any occurrences.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 5000));
+ await BrowserTestUtils.closeWindow(win);
+
+ let dayBoxItems = document.querySelectorAll("calendar-month-day-box-item");
+ Assert.equal(dayBoxItems.length, 1, "only one occurrence displayed");
+
+ let [dayBox] = dayBoxItems;
+ let { item } = dayBox;
+ Assert.equal(item.title, "Repeat Event");
+ Assert.equal(item.startDate.icalString, "20220316T110000Z");
+
+ let summaryDialog = await CalendarTestUtils.viewItem(window, dayBox);
+ Assert.equal(
+ summaryDialog.document.querySelector(".repeat-details").textContent,
+ "Repeat details unknown",
+ "repeat details not shown"
+ );
+
+ await BrowserTestUtils.closeWindow(summaryDialog);
+ await calendar.deleteItem(item.parentItem);
+ await TestUtils.waitForCondition(
+ () => document.querySelectorAll("calendar-month-day-box-item").length == 0
+ );
+}
+
+/**
+ * Tests accepting an invitation using the FREQ=SECONDLY value does not render
+ * the application unusable.
+ */
+add_task(async function testSecondly() {
+ return doFreqTest("SECONDLY");
+});
+
+/**
+ * Tests accepting an invitation using the FREQ=MINUTELY value does not render
+ * the application unusable.
+ */
+add_task(async function testMinutely() {
+ return doFreqTest("MINUTELY");
+});
diff --git a/comm/calendar/test/browser/invitations/data/cancel-repeat-event.eml b/comm/calendar/test/browser/invitations/data/cancel-repeat-event.eml
new file mode 100644
index 0000000000..03f298525b
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/data/cancel-repeat-event.eml
@@ -0,0 +1,49 @@
+MIME-Version: 1.0
+Date: Mon, 28 Mar 2022 17:49:35 +0000
+Subject: Invitation: Repeat Event @ Daily from 2pm to 3pm 3 times (AST) (receiver@example.com)
+From: sender@example.com
+To: receiver@example.com
+Content-Type: multipart/mixed; boundary="00000000000080f3db05db4aef5b"
+
+--00000000000080f3db05db4aef5b
+Content-Type: multipart/alternative; boundary="00000000000080f3da05db4aef59"
+
+--00000000000080f3da05db4aef59
+Content-Type: text/calendar; charset="UTF-8"; method=CANCEL
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+METHOD:CANCEL
+BEGIN:VEVENT
+DTSTART:20220316T110000Z
+DTEND:20220316T113000Z
+RRULE:FREQ=DAILY;WKST=SU;COUNT=3;INTERVAL=1
+DTSTAMP:20220316T191602Z
+UID:02e79b96
+ORGANIZER;CN=Sender;
+ EMAIL=sender@example.com:mailto:sender@example.com
+ATTENDEE;CN=Sender;
+ EMAIL=sender@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=ACCEPTED:mailto:sender@example.com
+ATTENDEE;CN=Receiver;EMAIL=receiver@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=NEEDS-ACTION:mailto:receiver@example.com
+ATTENDEE;CN=Other;EMAIL=other@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=NEEDS-ACTION:mailto:other@example.com
+CREATED:20220328T174934Z
+LAST-MODIFIED:20220328T174934Z
+LOCATION:Somewhere
+SEQUENCE:1
+STATUS:CANCELLED
+SUMMARY:Repeat Event
+DESCRIPTION:An event invitation.
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER:-P1D
+DESCRIPTION:This is an event reminder
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+
+--00000000000080f3da05db4aef59--
+--00000000000080f3db05db4aef5b--
diff --git a/comm/calendar/test/browser/invitations/data/cancel-single-event.eml b/comm/calendar/test/browser/invitations/data/cancel-single-event.eml
new file mode 100644
index 0000000000..afb4edb99d
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/data/cancel-single-event.eml
@@ -0,0 +1,78 @@
+MIME-Version: 1.0
+Content-Transfer-Encoding: binary
+Content-Type: multipart/mixed; boundary="_----------=_1647458162153312762582"
+Date: Wed, 16 Mar 2022 15:16:02 -0400
+To: receiver@example.com
+Subject: Cancellation: Single Event @ Wed, Mar 16 2022 11:00 AST
+From: Sender <sender@example.com>
+
+This is a multi-part message in MIME format.
+
+--_----------=_1647458162153312762582
+MIME-Version: 1.0
+Content-Transfer-Encoding: binary
+Content-Type: multipart/alternative; boundary="_----------=_1647458162153312762583"
+Date: Wed, 16 Mar 2022 15:16:02 -0400
+
+This is a multi-part message in MIME format.
+
+--_----------=_1647458162153312762583
+MIME-Version: 1.0
+Content-Disposition: inline
+Content-Length: 227
+Content-Transfer-Encoding: binary
+Content-Type: text/plain; charset="utf-8"
+Date: Wed, 16 Mar 2022 15:16:02 -0400
+
+Single Event
+
+When:
+ Wed, Mar 16 2022
+ 11:00 - 12:00 AST
+Where:
+ Somewhere
+
+--_----------=_1647458162153312762583
+MIME-Version: 1.0
+Content-Disposition: inline
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/calendar; charset="utf-8"; method=CANCEL
+Date: Wed, 16 Mar 2022 15:16:02 -0400
+
+BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:CANCEL
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:02e79b96
+SEQUENCE:1
+DTSTAMP:20220317T191602Z
+CREATED:20220316T191532Z
+DTSTART:20220316T110000Z
+DTEND:20220316T113000Z
+DURATION:PT1H
+PRIORITY:0
+SUMMARY:Single Event
+DESCRIPTION:An event invitation.
+LOCATION:Somewhere
+STATUS:CANCELLED
+TRANSP:OPAQUE
+CLASS:PUBLIC
+ORGANIZER;CN=3DSender;
+ EMAIL=3Dsender@example.com:mailto:sender@example.com
+ATTENDEE;CN=3DSender;
+ EMAIL=3Dsender@example.com;CUTYPE=3DINDIVIDUAL;
+ PARTSTAT=3DACCEPTED:mailto:sender@example.com
+ATTENDEE;CN=Receiver;EMAIL=3Dreceiver@example.com;CUTYPE=3DINDIVIDUAL;
+ PARTSTAT=3DNEEDS-ACTION:mailto:receiver@example.com
+ATTENDEE;CN=Other;EMAIL=other@example.com;CUTYPE=3DINDIVIDUAL;
+ PARTSTAT=3DNEEDS-ACTION:mailto:other@example.com
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER:-P1D
+DESCRIPTION:This is an event reminder
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+
+--_----------=_1647458162153312762583--
diff --git a/comm/calendar/test/browser/invitations/data/exception-major.eml b/comm/calendar/test/browser/invitations/data/exception-major.eml
new file mode 100644
index 0000000000..07f48e64bd
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/data/exception-major.eml
@@ -0,0 +1,49 @@
+MIME-Version: 1.0
+Date: Mon, 28 Mar 2022 17:49:35 +0000
+Subject: Exception Major
+From: sender@example.com
+To: receiver@example.com
+Content-Type: multipart/mixed; boundary="00000000000080f3db05db4aef5b"
+
+--00000000000080f3db05db4aef5b
+Content-Type: multipart/alternative; boundary="00000000000080f3da05db4aef59"
+
+--00000000000080f3da05db4aef59
+Content-Type: text/calendar; charset="UTF-8"; method=REQUEST
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+METHOD:REQUEST
+BEGIN:VEVENT
+DTSTART:20220317T050000Z
+DTEND:20220317T053000Z
+RECURRENCE-ID:20220317T110000Z
+DTSTAMP:20220316T191602Z
+UID:02e79b96
+ORGANIZER;CN=Sender;
+ EMAIL=sender@example.com:mailto:sender@example.com
+ATTENDEE;CN=Sender;
+ EMAIL=sender@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=ACCEPTED;RSVP=FALSE:mailto:sender@example.com
+ATTENDEE;CN=Receiver;EMAIL=receiver@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:receiver@example.com
+ATTENDEE;CN=Other;EMAIL=other@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:other@example.com
+CREATED:20220328T174934Z
+LAST-MODIFIED:20220328T174934Z
+LOCATION:Somewhere
+SEQUENCE:2
+STATUS:CONFIRMED
+SUMMARY:Repeat Event
+DESCRIPTION:An event invitation.
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER:-P1D
+DESCRIPTION:This is an event reminder
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+
+--00000000000080f3da05db4aef59--
+--00000000000080f3db05db4aef5b--
diff --git a/comm/calendar/test/browser/invitations/data/exception-minor.eml b/comm/calendar/test/browser/invitations/data/exception-minor.eml
new file mode 100644
index 0000000000..7cc38d29d3
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/data/exception-minor.eml
@@ -0,0 +1,49 @@
+MIME-Version: 1.0
+Date: Mon, 28 Mar 2022 17:49:35 +0000
+Subject: Exception Minor
+From: sender@example.com
+To: receiver@example.com
+Content-Type: multipart/mixed; boundary="00000000000080f3db05db4aef5b"
+
+--00000000000080f3db05db4aef5b
+Content-Type: multipart/alternative; boundary="00000000000080f3da05db4aef59"
+
+--00000000000080f3da05db4aef59
+Content-Type: text/calendar; charset="UTF-8"; method=REQUEST
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+METHOD:REQUEST
+BEGIN:VEVENT
+DTSTART:20220317T110000Z
+DTEND:20220317T113000Z
+RECURRENCE-ID:20220317T110000Z
+DTSTAMP:20220318T191602Z
+UID:02e79b96
+ORGANIZER;CN=Sender;
+ EMAIL=sender@example.com:mailto:sender@example.com
+ATTENDEE;CN=Sender;
+ EMAIL=sender@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=ACCEPTED;RSVP=FALSE:mailto:sender@example.com
+ATTENDEE;CN=Receiver;EMAIL=receiver@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:receiver@example.com
+ATTENDEE;CN=Other;EMAIL=other@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:other@example.com
+CREATED:20220328T174934Z
+LAST-MODIFIED:20220328T174934Z
+LOCATION:Exception location
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY:Exception title
+DESCRIPTION:Exception description
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER:-P1D
+DESCRIPTION:Exception description.
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+
+--00000000000080f3da05db4aef59--
+--00000000000080f3db05db4aef5b--
diff --git a/comm/calendar/test/browser/invitations/data/meet-meeting-invite.eml b/comm/calendar/test/browser/invitations/data/meet-meeting-invite.eml
new file mode 100644
index 0000000000..8587cd803a
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/data/meet-meeting-invite.eml
@@ -0,0 +1,384 @@
+Sender: Google Kalender <calendar-notification@google.com>
+Message-ID: <0000000000008c6d7005be1c767c@google.com>
+Date: Mon, 22 Mar 2021 09:12:20 +0000
+Subject: Meet invite (HTML)
+From: example@gmail.com
+To: homer@example.com
+Content-Type: multipart/mixed; boundary="0000000000008c6d6205be1c767e"
+Return-Path: example@gmail.com
+MIME-Version: 1.0
+
+--0000000000008c6d6205be1c767e
+Content-Type: multipart/alternative; boundary="0000000000008c6d6005be1c767c"
+
+--0000000000008c6d6005be1c767c
+Content-Type: text/plain; charset="UTF-8"; format=flowed; delsp=yes
+Content-Transfer-Encoding: base64
+
+RHUgaGFyIGJsaXZpdCBpbmJqdWRlbiB0aWxsIGbDtmxqYW5kZSBow6RuZGVsc2UuCgpUaXRlbDog
+TWVlZWVldCBtZSBIVE1MClRoaXMgaXMgYSB0ZXN0LiBCb2xkLiBJdGFsaWMuJm5ic3A7V2lsbCBk
+aXNjdXNzIGFkZHJlc3MgZm9yIGVtYWlsICAKJmx0O2Zvb0BleGFtcGxlLmNvbSZndDsgYW5kIGh0
+dHA6Ly9leGFtcGxlLmNvbT9mb289YmFyLgpOw6RyOiBtw6VuIGRlbiAyMiBtYXJzIDIwMjEgMTE6
+MzBhbSDigJMgMTI6MzBwbSDDlnN0ZXVyb3BlaXNrIHRpZCAtIEhlbHNpbmdmb3JzCgpBbnNsdXRu
+aW5nc2luZm86IEFuc2x1dCB0aWxsIEdvb2dsZSBNZWV0Cmh0dHBzOi8vbWVldC5nb29nbGUuY29t
+L3B5Yi1uZGN1LWhoYz9ocz0yMjQKCkthbGVuZGVyOiBob21lckBleGFtcGxlLmNvbQpWZW06CiAg
+ICAgKiBleGFtcGxlQGdtYWlsLmNvbeKAkyBvcmdhbmlzYXTDtnIKICAgICAqIGhvbWVyQGV4YW1w
+bGUuY29tCgpJbmZvcm1hdGlvbiBvbSBow6RuZGVsc2VuOiAgCmh0dHBzOi8vY2FsZW5kYXIuZ29v
+Z2xlLmNvbS9jYWxlbmRhci9ldmVudD9hY3Rpb249VklFVyZlaWQ9TmpWdE1UZG9jMlJ2YkcxdmRI
+WXphM1p0Y25Sbk5EQnZiblFnYldGbmJuVnpMbTFsYkdsdVFHaDFkQzVtYVEmdG9rPU1qRWpZbVZ5
+ZEdGMGFHVmliM1JBWjIxaGFXd3VZMjl0WlRnMk5HRmpZbU5qWVdFMU1qVmxaV0ptWTJVelltUm1N
+REF5TldVME1Ea3pOREF4WmpSaFpnJmN0ej1FdXJvcGUlMkZIZWxzaW5raSZobD1zdiZlcz0wCgpJ
+bmJqdWRhbiBmcsOlbiBHb29nbGUgS2FsZW5kZXI6IGh0dHBzOi8vY2FsZW5kYXIuZ29vZ2xlLmNv
+bS9jYWxlbmRhci8KCkRldHRhIGUtcG9zdG1lZGRlbGFuZGUgaGFyIHNraWNrYXRzIHRpbGwga29u
+dG90IGhvbWVyQGV4YW1wbGUuY29tICAKZWZ0ZXJzb20gZHUgw6RyIGRlbHRhZ2FyZSB2aWQgZGVu
+bmEgaMOkbmRlbHNlLgoKT20gZHUgaW50ZSB2aWxsIGbDpSB1cHBkYXRlcmluZ2FyIG9tIGRlbm5h
+IGjDpG5kZWxzZSBpIGZyYW10aWRlbiBrYW4gZHUgdGFja2EgIApuZWogdGlsbCBkZW5uYSBow6Ru
+ZGVsc2UuIER1IGthbiDDpHZlbiByZWdpc3RyZXJhIGRpZyBmw7ZyIGF0dCBmw6UgZXR0ICAKR29v
+Z2xlLWtvbnRvIHDDpSBodHRwczovL2NhbGVuZGFyLmdvb2dsZS5jb20vY2FsZW5kYXIvIG9jaCBr
+b250cm9sbGVyYSAgCmF2aXNlcmluZ3NpbnN0w6RsbG5pbmdhcm5hIGbDtnIgaGVsYSBrYWxlbmRl
+cm4uCgpPbSBkdSB2aWRhcmViZWZvcmRyYXIgZGVuIGjDpHIgaW5ianVkYW4ga2FuIGRldCBnw7Zy
+YSBkZXQgbcO2amxpZ3QgZsO2ciBhbGxhICAKbW90dGFnYXJlIGF0dCBza2lja2EgZXR0IHN2YXIg
+dGlsbCBvcmdhbmlzYXTDtnJlbiBvY2ggbMOkZ2dhcyB0aWxsIHDDpSAgCmfDpHN0bGlzdGFuLCBi
+anVkYSBpbiBhbmRyYSBvYXZzZXR0IGRlcmFzIGVnZW4gaW5ianVkbmluZ3NzdGF0dXMgZWxsZXIg
+IAptb2RpZmllcmEgZGl0dCBPU0EuIEzDpHMgbWVyIHDDpSAgCmh0dHBzOi8vc3VwcG9ydC5nb29n
+bGUuY29tL2NhbGVuZGFyL2Fuc3dlci8zNzEzNSNmb3J3YXJkaW5nCg==
+--0000000000008c6d6005be1c767c
+Content-Type: text/html; charset="UTF-8"
+Content-Transfer-Encoding: quoted-printable
+
+<html>
+<head>
+<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8">
+</head>
+<body>
+<span itemscope=3D"" itemtype=3D"http://schema.org/InformAction"><span styl=
+e=3D"display:none" itemprop=3D"about" itemscope=3D"" itemtype=3D"http://sch=
+ema.org/Person">
+<meta itemprop=3D"description" content=3D"Inbjudan fr=C3=A5n example@gm=
+ail.com">
+</span><span itemprop=3D"object" itemscope=3D"" itemtype=3D"http://schema.o=
+rg/Event">
+<div style=3D"">
+<table cellspacing=3D"0" cellpadding=3D"8" border=3D"0" summary=3D"" style=
+=3D"width:100%;font-family:Arial,Sans-serif;border:1px Solid #ccc;border-wi=
+dth:1px 2px 2px 1px;background-color:#fff;">
+<tbody>
+<tr>
+<td>
+<meta itemprop=3D"eventStatus" content=3D"http://schema.org/EventScheduled"=
+>
+<h4 style=3D"padding:6px 0;margin:0 0 4px 0;font-family:Arial,Sans-serif;fo=
+nt-size:13px;line-height:1.4;border:1px Solid #fff;background:#fff;color:#0=
+90;font-weight:normal">
+<strong>Du har blivit inbjuden till f=C3=B6ljande h=C3=A4ndelse.</strong></=
+h4>
+<div style=3D"padding:2px"><span itemprop=3D"publisher" itemscope=3D"" item=
+type=3D"http://schema.org/Organization">
+<meta itemprop=3D"name" content=3D"Google Calendar">
+</span>
+<meta itemprop=3D"eventId/googleCalendar" content=3D"65m17hsdolmotv3kvmrtg4=
+0ont">
+<h3 style=3D"padding:0 0 6px 0;margin:0;font-family:Arial,Sans-serif;font-s=
+ize:16px;font-weight:bold;color:#222">
+<span itemprop=3D"name">Meeeeet me HTML</span></h3>
+<table style=3D"display:inline-table" cellpadding=3D"0" cellspacing=3D"0" b=
+order=3D"0" summary=3D"Uppgifter om h=C3=A4ndelse">
+<tbody>
+<tr>
+<td style=3D"padding:0 1em 10px 0;font-family:Arial,Sans-serif;font-size:13=
+px;color:#888;white-space:nowrap;width:90px" valign=3D"top">
+<div><i style=3D"font-style:normal">N=C3=A4r</i></div>
+</td>
+<td style=3D"padding-bottom:10px;font-family:Arial,Sans-serif;font-size:13p=
+x;color:#222" valign=3D"top">
+<div style=3D"text-indent:-1px"><time itemprop=3D"startDate" datetime=3D"20=
+210322T093000Z"></time><time itemprop=3D"endDate" datetime=3D"20210322T1030=
+00Z"></time>m=C3=A5n den 22 mars 2021 11:30am =E2=80=93 12:30pm
+<span style=3D"color:#888">=C3=96steuropeisk tid - Helsingfors</span></div>
+</td>
+</tr>
+<tr>
+<td style=3D"padding:0 1em 4px 0;font-family:Arial,Sans-serif;font-size:13p=
+x;color:#888;white-space:nowrap;width:90px" valign=3D"top">
+<div><i style=3D"font-style:normal">Anslutningsinfo</i></div>
+</td>
+<td style=3D"padding-bottom:4px;font-family:Arial,Sans-serif;font-size:13px=
+;color:#222" valign=3D"top">
+<div style=3D"text-indent:-1px">Anslut till Google Meet</div>
+</td>
+</tr>
+<tr>
+<td style=3D"padding:0 1em 10px 0;font-family:Arial,Sans-serif;font-size:13=
+px;color:#888;white-space:nowrap;width:90px">
+</td>
+<td style=3D"padding-bottom:10px;font-family:Arial,Sans-serif;font-size:13p=
+x;color:#222" valign=3D"top">
+<div style=3D"text-indent:-1px">
+<div style=3D"text-indent:-1px"><span itemprop=3D"potentialaction" itemscop=
+e=3D"" itemtype=3D"http://schema.org/JoinAction"><span itemprop=3D"name" co=
+ntent=3D"meet.google.com/pyb-ndcu-hhc"><span itemprop=3D"target" itemscope=
+=3D"" itemtype=3D"http://schema.org/EntryPoint"><span itemprop=3D"url" cont=
+ent=3D"https://meet.google.com/pyb-ndcu-hhc?hs=3D224"><span itemprop=3D"htt=
+pMethod" content=3D"GET"><a href=3D"https://meet.google.com/pyb-ndcu-hhc?hs=
+=3D224" style=3D"color:#20c;white-space:nowrap" target=3D"_blank">meet.goog=
+le.com/pyb-ndcu-hhc</a></span></span></span></span></span>
+</div>
+</div>
+</td>
+</tr>
+<tr>
+<td style=3D"padding:0 1em 10px 0;font-family:Arial,Sans-serif;font-size:13=
+px;color:#888;white-space:nowrap;width:90px" valign=3D"top">
+<div><i style=3D"font-style:normal">Kalender</i></div>
+</td>
+<td style=3D"padding-bottom:10px;font-family:Arial,Sans-serif;font-size:13p=
+x;color:#222" valign=3D"top">
+<div style=3D"text-indent:-1px">homer@example.com</div>
+</td>
+</tr>
+<tr>
+<td style=3D"padding:0 1em 10px 0;font-family:Arial,Sans-serif;font-size:13=
+px;color:#888;white-space:nowrap;width:90px" valign=3D"top">
+<div><i style=3D"font-style:normal">Vem</i></div>
+</td>
+<td style=3D"padding-bottom:10px;font-family:Arial,Sans-serif;font-size:13p=
+x;color:#222" valign=3D"top">
+<table cellspacing=3D"0" cellpadding=3D"0">
+<tbody>
+<tr>
+<td style=3D"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px=
+;color:#222;width:10px">
+<div style=3D"text-indent:-1px"><span style=3D"font-family:Courier New,mono=
+space">=E2=80=A2</span></div>
+</td>
+<td style=3D"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px=
+;color:#222">
+<div style=3D"text-indent:-1px">
+<div>
+<div style=3D"margin:0 0 0.3em 0"><span itemprop=3D"attendee" itemscope=3D"=
+" itemtype=3D"http://schema.org/Person"><span itemprop=3D"name" class=3D"no=
+translate">example@gmail.com</span>
+<meta itemprop=3D"email" content=3D"example@gmail.com">
+</span><span itemprop=3D"organizer" itemscope=3D"" itemtype=3D"http://schem=
+a.org/Person">
+<meta itemprop=3D"name" content=3D"example@gmail.com">
+<meta itemprop=3D"email" content=3D"example@gmail.com">
+</span><span style=3D"font-size:11px;color:#888">=E2=80=93 organisat=C3=B6r=
+</span></div>
+</div>
+</div>
+</td>
+</tr>
+<tr>
+<td style=3D"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px=
+;color:#222;width:10px">
+<div style=3D"text-indent:-1px"><span style=3D"font-family:Courier New,mono=
+space">=E2=80=A2</span></div>
+</td>
+<td style=3D"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px=
+;color:#222">
+<div style=3D"text-indent:-1px">
+<div>
+<div style=3D"margin:0 0 0.3em 0"><span itemprop=3D"attendee" itemscope=3D"=
+" itemtype=3D"http://schema.org/Person"><span itemprop=3D"name" class=3D"no=
+translate">homer@example.com</span>
+<meta itemprop=3D"email" content=3D"homer@example.com">
+</span></div>
+</div>
+</div>
+</td>
+</tr>
+</tbody>
+</table>
+</td>
+</tr>
+</tbody>
+</table>
+<div style=3D"float:right;font-weight:bold;font-size:13px"><a href=3D"https=
+://calendar.google.com/calendar/event?action=3DVIEW&amp;eid=3DNjVtMTdoc2Rvb=
+G1vdHYza3ZtcnRnNDBvbnQgbWFnbnVzLm1lbGluQGh1dC5maQ&amp;tok=3DMjEjYmVydGF0aGV=
+ib3RAZ21haWwuY29tZTg2NGFjYmNjYWE1MjVlZWJmY2UzYmRmMDAyNWU0MDkzNDAxZjRhZg&amp=
+;ctz=3DEurope%2FHelsinki&amp;hl=3Dsv&amp;es=3D0" style=3D"color:#20c;white-=
+space:nowrap" itemprop=3D"url">mer
+ information =C2=BB</a><br>
+</div>
+<div style=3D"padding-bottom:15px;font-family:Arial,Sans-serif;font-size:13=
+px;color:#222;white-space:pre-wrap!important;white-space:-moz-pre-wrap!impo=
+rtant;white-space:-pre-wrap!important;white-space:-o-pre-wrap!important;whi=
+te-space:pre;word-wrap:break-word">
+<span>This is a test. <b>Bold</b>. <i>Italic</i>. <br>
+<br>
+Will discuss address for email &lt;<a href=3D"mailto:foo@example.com" targe=
+t=3D"_blank">foo@example.com</a>&gt; and
+<a href=3D"https://www.google.com/url?q=3Dhttp%3A%2F%2Fexample.com%3Ffoo%3D=
+bar&amp;sa=3DD&amp;ust=3D1616836340813000&amp;usg=3DAOvVaw04gjO0O3Bf1tJs9vs=
+BMj3x" target=3D"_blank">
+http://example.com?foo=3Dbar</a>.</span>
+<meta itemprop=3D"description" content=3D"This is a test. Bold. Italic.&nbs=
+p;Will discuss address for email &lt;foo@example.com&gt; and http://example=
+.com?foo=3Dbar.">
+</div>
+</div>
+<p style=3D"color:#222;font-size:13px;margin:0"><span style=3D"color:#888">=
+Ska du delta (homer@example.com)?
+</span><wbr><strong><span itemprop=3D"potentialaction" itemscope=3D"" itemt=
+ype=3D"http://schema.org/RsvpAction">
+<meta itemprop=3D"attendance" content=3D"http://schema.org/RsvpAttendance/Y=
+es">
+<span itemprop=3D"handler" itemscope=3D"" itemtype=3D"http://schema.org/Htt=
+pActionHandler"><link itemprop=3D"method" href=3D"http://schema.org/HttpReq=
+uestMethod/GET"><a href=3D"https://calendar.google.com/calendar/event?actio=
+n=3DRESPOND&amp;eid=3DNjVtMTdoc2RvbG1vdHYza3ZtcnRnNDBvbnQgbWFnbnVzLm1lbGluQ=
+Gh1dC5maQ&amp;rst=3D1&amp;tok=3DMjEjYmVydGF0aGVib3RAZ21haWwuY29tZTg2NGFjYmN=
+jYWE1MjVlZWJmY2UzYmRmMDAyNWU0MDkzNDAxZjRhZg&amp;ctz=3DEurope%2FHelsinki&amp=
+;hl=3Dsv&amp;es=3D0" style=3D"color:#20c;white-space:nowrap" itemprop=3D"ur=
+l">Ja</a></span></span><span style=3D"margin:0 0.4em;font-weight:normal">
+ - </span><span itemprop=3D"potentialaction" itemscope=3D"" itemtype=3D"htt=
+p://schema.org/RsvpAction">
+<meta itemprop=3D"attendance" content=3D"http://schema.org/RsvpAttendance/M=
+aybe">
+<span itemprop=3D"handler" itemscope=3D"" itemtype=3D"http://schema.org/Htt=
+pActionHandler"><link itemprop=3D"method" href=3D"http://schema.org/HttpReq=
+uestMethod/GET"><a href=3D"https://calendar.google.com/calendar/event?actio=
+n=3DRESPOND&amp;eid=3DNjVtMTdoc2RvbG1vdHYza3ZtcnRnNDBvbnQgbWFnbnVzLm1lbGluQ=
+Gh1dC5maQ&amp;rst=3D3&amp;tok=3DMjEjYmVydGF0aGVib3RAZ21haWwuY29tZTg2NGFjYmN=
+jYWE1MjVlZWJmY2UzYmRmMDAyNWU0MDkzNDAxZjRhZg&amp;ctz=3DEurope%2FHelsinki&amp=
+;hl=3Dsv&amp;es=3D0" style=3D"color:#20c;white-space:nowrap" itemprop=3D"ur=
+l">Kanske</a></span></span><span style=3D"margin:0 0.4em;font-weight:normal=
+">
+ - </span><span itemprop=3D"potentialaction" itemscope=3D"" itemtype=3D"htt=
+p://schema.org/RsvpAction">
+<meta itemprop=3D"attendance" content=3D"http://schema.org/RsvpAttendance/N=
+o">
+<span itemprop=3D"handler" itemscope=3D"" itemtype=3D"http://schema.org/Htt=
+pActionHandler"><link itemprop=3D"method" href=3D"http://schema.org/HttpReq=
+uestMethod/GET"><a href=3D"https://calendar.google.com/calendar/event?actio=
+n=3DRESPOND&amp;eid=3DNjVtMTdoc2RvbG1vdHYza3ZtcnRnNDBvbnQgbWFnbnVzLm1lbGluQ=
+Gh1dC5maQ&amp;rst=3D2&amp;tok=3DMjEjYmVydGF0aGVib3RAZ21haWwuY29tZTg2NGFjYmN=
+jYWE1MjVlZWJmY2UzYmRmMDAyNWU0MDkzNDAxZjRhZg&amp;ctz=3DEurope%2FHelsinki&amp=
+;hl=3Dsv&amp;es=3D0" style=3D"color:#20c;white-space:nowrap" itemprop=3D"ur=
+l">Nej</a></span></span></strong>
+<wbr><a href=3D"https://calendar.google.com/calendar/event?action=3DVIEW&am=
+p;eid=3DNjVtMTdoc2RvbG1vdHYza3ZtcnRnNDBvbnQgbWFnbnVzLm1lbGluQGh1dC5maQ&amp;=
+tok=3DMjEjYmVydGF0aGVib3RAZ21haWwuY29tZTg2NGFjYmNjYWE1MjVlZWJmY2UzYmRmMDAyN=
+WU0MDkzNDAxZjRhZg&amp;ctz=3DEurope%2FHelsinki&amp;hl=3Dsv&amp;es=3D0" style=
+=3D"color:#20c;white-space:nowrap" itemprop=3D"url">fler
+ alternativ =C2=BB</a></p>
+</td>
+</tr>
+<tr>
+<td style=3D"background-color:#f6f6f6;color:#888;border-top:1px Solid #ccc;=
+font-family:Arial,Sans-serif;font-size:11px">
+<p>Inbjudan fr=C3=A5n <a href=3D"https://calendar.google.com/calendar/" tar=
+get=3D"_blank" style=3D"">
+Google Kalender</a></p>
+<p>Detta e-postmeddelande har skickats till kontot homer@example.com efte=
+rsom du =C3=A4r deltagare vid denna h=C3=A4ndelse.</p>
+<p>Om du inte vill f=C3=A5 uppdateringar om denna h=C3=A4ndelse i framtiden=
+ kan du tacka nej till denna h=C3=A4ndelse. Du kan =C3=A4ven registrera dig=
+ f=C3=B6r att f=C3=A5 ett Google-konto p=C3=A5 https://calendar.google.com/=
+calendar/ och kontrollera aviseringsinst=C3=A4llningarna f=C3=B6r hela kale=
+ndern.</p>
+<p>Om du vidarebefordrar den h=C3=A4r inbjudan kan det g=C3=B6ra det m=C3=
+=B6jligt f=C3=B6r alla mottagare att skicka ett svar till organisat=C3=B6re=
+n och l=C3=A4ggas till p=C3=A5 g=C3=A4stlistan, bjuda in andra oavsett dera=
+s egen inbjudningsstatus eller modifiera ditt OSA.
+<a href=3D"https://support.google.com/calendar/answer/37135#forwarding">L=
+=C3=A4s mer</a>.</p>
+</td>
+</tr>
+</tbody>
+</table>
+</div>
+</span></span>
+</body>
+</html>
+
+--0000000000008c6d6005be1c767c
+Content-Type: text/calendar; charset="UTF-8"; method=REQUEST
+Content-Transfer-Encoding: quoted-printable
+
+BEGIN:VCALENDAR
+PRODID:-//Google Inc//Google Calendar 70.9054//EN
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+DTSTART:20210322T093000Z
+DTEND:20210322T103000Z
+DTSTAMP:20210322T091220Z
+ORGANIZER;CN=3Dexample@gmail.com:mailto:example@gmail.com
+UID:65m17hsdolmotv3kvmrtg40ont@google.com
+ATTENDEE;CUTYPE=3DINDIVIDUAL;ROLE=3DREQ-PARTICIPANT;PARTSTAT=3DACCEPTED;RSV=
+P=3DTRUE
+ ;CN=3Dexample@gmail.com;X-NUM-GUESTS=3D0:mailto:example@gmail.com
+ATTENDEE;CUTYPE=3DINDIVIDUAL;ROLE=3DREQ-PARTICIPANT;PARTSTAT=3DNEEDS-ACTION=
+;RSVP=3D
+ TRUE;CN=3Dhomer@example.com;X-NUM-GUESTS=3D0:mailto:homer@example.com
+X-MICROSOFT-CDO-OWNERAPPTID:-410050292
+CREATED:20210322T091220Z
+DESCRIPTION:This is a test. <b>Bold</b>. <i>Italic</i>.&nbsp\;<br><br>Will=
+=20
+ discuss address for email &lt\;foo@example.com&gt\; and http://example.com=
+?
+ foo=3Dbar.\n\n-::~:~::~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:=
+~:~
+ :~:~:~:~:~:~:~:~::~:~::-\n=C3=84ndra inte det h=C3=A4r avsnittet i beskriv=
+ningen.\n\n
+ Den h=C3=A4r h=C3=A4ndelsen har ett videosamtal.\nG=C3=A5 med: https://mee=
+t.google.com/pyb
+ -ndcu-hhc\n\nVisa din h=C3=A4ndelse p=C3=A5 https://calendar.google.com/ca=
+lendar/even
+ t?action=3DVIEW&eid=3DNjVtMTdoc2RvbG1vdHYza3ZtcnRnNDBvbnQgbWFnbnVzLm1lbGlu=
+QGh1d
+ C5maQ&tok=3DMjEjYmVydGF0aGVib3RAZ21haWwuY29tZTg2NGFjYmNjYWE1MjVlZWJmY2UzYm=
+RmM
+ DAyNWU0MDkzNDAxZjRhZg&ctz=3DEurope%2FHelsinki&hl=3Dsv&es=3D1.\n-::~:~::~:~=
+:~:~:~:
+ ~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~::~:~::-
+LAST-MODIFIED:20210322T091220Z
+LOCATION:
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY:Meeeeet me HTML
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+
+--0000000000008c6d6005be1c767c--
+
+--0000000000008c6d6205be1c767e
+Content-Type: application/ics; name="invite.ics"
+Content-Disposition: attachment; filename="invite.ics"
+Content-Transfer-Encoding: base64
+
+QkVHSU46VkNBTEVOREFSClBST0RJRDotLy9Hb29nbGUgSW5jLy9Hb29nbGUgQ2FsZW5kYXIgNzAu
+OTA1NC8vRU4KVkVSU0lPTjoyLjAKQ0FMU0NBTEU6R1JFR09SSUFOCk1FVEhPRDpSRVFVRVNUCkJF
+R0lOOlZFVkVOVApEVFNUQVJUOjIwMjEwMzIyVDA5MzAwMFoKRFRFTkQ6MjAyMTAzMjJUMTAzMDAw
+WgpEVFNUQU1QOjIwMjEwMzIyVDA5MTIyMFoKT1JHQU5JWkVSO0NOPWV4YW1wbGVAZ21haWwuY29t
+Om1haWx0bzpleGFtcGxlQGdtYWlsLmNvbQpVSUQ6NjVtMTdoc2RvbG1vdHYza3ZtcnRnNDBvbnRA
+Z29vZ2xlLmNvbQpBVFRFTkRFRTtDVVRZUEU9SU5ESVZJRFVBTDtST0xFPVJFUS1QQVJUSUNJUEFO
+VDtQQVJUU1RBVD1BQ0NFUFRFRDtSU1ZQPVRSVUUKIDtDTj1leGFtcGxlQGdtYWlsLmNvbTtYLU5V
+TS1HVUVTVFM9MDptYWlsdG86ZXhhbXBsZUBnbWFpbC5jb20KQVRURU5ERUU7Q1VUWVBFPUlORElW
+SURVQUw7Uk9MRT1SRVEtUEFSVElDSVBBTlQ7UEFSVFNUQVQ9TkVFRFMtQUNUSU9OO1JTVlA9CiBU
+UlVFO0NOPWhvbWVyQGV4YW1wbGUuY29tO1gtTlVNLUdVRVNUUz0wOm1haWx0bzpob21lckBleGFt
+cGxlLmNvbQpYLU1JQ1JPU09GVC1DRE8tT1dORVJBUFBUSUQ6LTQxMDA1MDI5MgpDUkVBVEVEOjIw
+MjEwMzIyVDA5MTIyMFoKREVTQ1JJUFRJT046VGhpcyBpcyBhIHRlc3QuIDxiPkJvbGQ8L2I+LiA8
+aT5JdGFsaWM8L2k+LiZuYnNwXDs8YnI+PGJyPldpbGwgCiBkaXNjdXNzIGFkZHJlc3MgZm9yIGVt
+YWlsICZsdFw7Zm9vQGV4YW1wbGUuY29tJmd0XDsgYW5kIGh0dHA6Ly9leGFtcGxlLmNvbT8KIGZv
+bz1iYXIuXG5cbi06On46fjo6fjp+On46fjp+On46fjp+On46fjp+On46fjp+On46fjp+On46fjp+
+On46fjp+On46fjp+On46fgogOn46fjp+On46fjp+On46fjo6fjp+OjotXG7DhG5kcmEgaW50ZSBk
+ZXQgaMOkciBhdnNuaXR0ZXQgaSBiZXNrcml2bmluZ2VuLlxuXG4KIERlbiBow6RyIGjDpG5kZWxz
+ZW4gaGFyIGV0dCB2aWRlb3NhbXRhbC5cbkfDpSBtZWQ6IGh0dHBzOi8vbWVldC5nb29nbGUuY29t
+L3B5YgogLW5kY3UtaGhjXG5cblZpc2EgZGluIGjDpG5kZWxzZSBww6UgaHR0cHM6Ly9jYWxlbmRh
+ci5nb29nbGUuY29tL2NhbGVuZGFyL2V2ZW4KIHQ/YWN0aW9uPVZJRVcmZWlkPU5qVnRNVGRvYzJS
+dmJHMXZkSFl6YTNadGNuUm5OREJ2Ym5RZ2JXRm5iblZ6TG0xbGJHbHVRR2gxZAogQzVtYVEmdG9r
+PU1qRWpZbVZ5ZEdGMGFHVmliM1JBWjIxaGFXd3VZMjl0WlRnMk5HRmpZbU5qWVdFMU1qVmxaV0pt
+WTJVelltUm1NCiBEQXlOV1UwTURrek5EQXhaalJoWmcmY3R6PUV1cm9wZSUyRkhlbHNpbmtpJmhs
+PXN2JmVzPTEuXG4tOjp+On46On46fjp+On46fjoKIH46fjp+On46fjp+On46fjp+On46fjp+On46
+fjp+On46fjp+On46fjp+On46fjp+On46fjp+On46fjp+On46On46fjo6LQpMQVNULU1PRElGSUVE
+OjIwMjEwMzIyVDA5MTIyMFoKTE9DQVRJT046ClNFUVVFTkNFOjAKU1RBVFVTOkNPTkZJUk1FRApT
+VU1NQVJZOk1lZWVlZXQgbWUgSFRNTApUUkFOU1A6T1BBUVVFCkVORDpWRVZFTlQKRU5EOlZDQUxF
+TkRBUgo=
+
+--0000000000008c6d6205be1c767e--
diff --git a/comm/calendar/test/browser/invitations/data/message-containing-event.eml b/comm/calendar/test/browser/invitations/data/message-containing-event.eml
new file mode 100644
index 0000000000..d27c2976db
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/data/message-containing-event.eml
@@ -0,0 +1,44 @@
+From: ExampleStore <noreply@example.com>
+Date: Wed, 24 Aug 2016 16:40:06 -0400
+Subject: ExampleStore - booking 01.09.2016 @ 09.25 - 09.50
+Content-Type: multipart/mixed;
+ boundary="_=aspNetEmail=_51bed191ceac49f7a22392ea84b6ef35"
+To: <foo@example.com>
+Message-ID: <df0f52ae-d3dc-4b89-bc3e-67fc4f6e8552@example.com>
+MIME-Version: 1.0
+
+--_=aspNetEmail=_51bed191ceac49f7a22392ea84b6ef35
+Content-Type: multipart/alternative;
+ boundary="_=ALT_=aspNetEmail=_51bed191ceac49f7a22392ea84b6ef35"
+
+--_=ALT_=aspNetEmail=_51bed191ceac49f7a22392ea84b6ef35
+Content-Type: text/plain; charset="UTF-8"
+
+Remember your booking @ 09.25
+
+--_=ALT_=aspNetEmail=_51bed191ceac49f7a22392ea84b6ef35
+Content-Type: text/html; charset="UTF-8"
+
+<html>
+<body>
+<p>You have a booking for 9.25</p>
+</body>
+</html>
+
+--_=ALT_=aspNetEmail=_51bed191ceac49f7a22392ea84b6ef35--
+
+--_=aspNetEmail=_51bed191ceac49f7a22392ea84b6ef35
+Content-Type: TeXt/CaLeNdAr; method=PUBLISH; charset=UTF-8
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename="booking.ics"
+
+QkVHSU46VkNBTEVOREFSDQpWRVJTSU9OOjIuMA0KTUVUSE9EOlBVQkxJU0gNClBST0RJRDpleGFt
+cGxlLmNvbQ0KQkVHSU46VkVWRU5UDQpEVFNUQVJUOjIwMTYwOTAxVDA2MjUwMFoNCkRURU5EOjIw
+MTYwOTAxVDA2NTAwMFoNCkRUU1RBTVA6MjAxNjA4MjRUMjA0MDAwWg0KVUlEOjIwMTYwODI0VDIw
+NDAwMFotNTY4ODYwMjgwQGV4YW1wbGUuY29tDQpTVU1NQVJZOkhhaXJjdXQNCk9SR0FOSVpFUjpt
+YWlsdG86c29tZW9uZUBleGFtcGxlLmNvbQ0KREVTQ1JJUFRJT046SGFpcmN1dCtzdHlsaW5nDQpM
+T0NBVElPTjpTb21ld2hlcmUNClRSQU5TUDpPUEFRVUUNClNFUVVFTkNFOjANCkNMQVNTOlBVQkxJ
+Qw0KQkVHSU46VkFMQVJNDQpUUklHR0VSOi1QVDYwTQ0KQUNUSU9OOkFVRElPDQpERVNDUklQVElP
+TjpSZW1pbmRlcg0KRU5EOlZBTEFSTQ0KRU5EOlZFVkVOVA0KRU5EOlZDQUxFTkRBUg==
+
+--_=aspNetEmail=_51bed191ceac49f7a22392ea84b6ef35--
diff --git a/comm/calendar/test/browser/invitations/data/message-non-invite.eml b/comm/calendar/test/browser/invitations/data/message-non-invite.eml
new file mode 100644
index 0000000000..cf391f445a
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/data/message-non-invite.eml
@@ -0,0 +1,115 @@
+Date: Sun, 28 Nov 2021 21:39:31 +0000
+From: Jane <noreply@example.com>
+To: <john.doe@example.com>
+Message-ID: <1074020157.32201638135571450.JavaMail.root@hki-example-prod-app-004>
+Subject: We're having a party - you're NOT invited
+Content-Type: multipart/mixed;
+ boundary="----=_Part_6440_2094089067.1638135571440"
+MIME-Version: 1.0
+
+------=_Part_6440_2094089067.1638135571440
+Content-Type: multipart/related;
+ boundary="----=_Part_6441_499243807.1638135571440"
+
+------=_Part_6441_499243807.1638135571440
+Content-Type: text/plain; charset="UTF-8"
+
+Hey, we're having a party! You're not invited ;)
+
+------=_Part_6441_499243807.1638135571440--
+
+------=_Part_6440_2094089067.1638135571440
+Content-Type: text/calendar; charset="utf-8"; name="event.ics"
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: attachment; filename="event.ics"
+
+BEGIN:VCALENDAR
+PRODID:-//EXAMPLE:COM//iCal4j 1.0.5.2//EN
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:PUBLISH
+BEGIN:VTIMEZONE
+TZID:Europe/Helsinki
+TZURL:http://tzurl.org/zoneinfo/Europe/Helsinki
+X-LIC-LOCATION:Europe/Helsinki
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0300
+TZNAME:EEST
+DTSTART:19830327T030000
+RRULE:FREQ=3DYEARLY;BYMONTH=3D3;BYDAY=3D-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0300
+TZOFFSETTO:+0200
+TZNAME:EET
+DTSTART:19961027T040000
+RRULE:FREQ=3DYEARLY;BYMONTH=3D10;BYDAY=3D-1SU
+END:STANDARD
+BEGIN:STANDARD
+TZOFFSETFROM:+013952
+TZOFFSETTO:+013952
+TZNAME:HMT
+DTSTART:18780531T000000
+RDATE:18780531T000000
+END:STANDARD
+BEGIN:STANDARD
+TZOFFSETFROM:+013952
+TZOFFSETTO:+0200
+TZNAME:EET
+DTSTART:19210501T000000
+RDATE:19210501T000000
+END:STANDARD
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0300
+TZNAME:EEST
+DTSTART:19420403T000000
+RDATE:19420403T000000
+RDATE:19810329T020000
+RDATE:19820328T020000
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0300
+TZOFFSETTO:+0200
+TZNAME:EET
+DTSTART:19421003T000000
+RDATE:19421003T000000
+RDATE:19810927T030000
+RDATE:19820926T030000
+RDATE:19830925T040000
+RDATE:19840930T040000
+RDATE:19850929T040000
+RDATE:19860928T040000
+RDATE:19870927T040000
+RDATE:19880925T040000
+RDATE:19890924T040000
+RDATE:19900930T040000
+RDATE:19910929T040000
+RDATE:19920927T040000
+RDATE:19930926T040000
+RDATE:19940925T040000
+RDATE:19950924T040000
+END:STANDARD
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0200
+TZNAME:EET
+DTSTART:19830101T000000
+RDATE:19830101T000000
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTAMP:20211128T213931Z
+DTSTART;TZID=3DEurope/Helsinki:20211129T105500
+DTEND;TZID=3DEurope/Helsinki:20211129T110000
+SUMMARY:Party at John's house\, Helsinki
+ORGANIZER;CN=3DJANE:mailto:noreply@example.com
+UID:1e5fd4e6-bc52-439c-ac76-40da54f57c77@secure.example.com
+SEQUENCE:3
+STATUS:CONFIRMED
+LAST-MODIFIED:20211128T213931Z
+END:VEVENT
+END:VCALENDAR
+
+------=_Part_6440_2094089067.1638135571440--
diff --git a/comm/calendar/test/browser/invitations/data/outlook-test-invite.eml b/comm/calendar/test/browser/invitations/data/outlook-test-invite.eml
new file mode 100644
index 0000000000..de07a9b873
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/data/outlook-test-invite.eml
@@ -0,0 +1,102 @@
+From: Marge <marge@example.org>
+To: Homer <homer@example.org>
+Subject: Testaus
+Thread-Topic: Testaus
+Thread-Index: AdfdgAAehFDfsPyXTommZqRYgeMqiQAABo4Q
+Date: Fri, 19 Nov 2021 20:00:31 +0000
+Message-ID: <HE1P190MB0540579ABA18FCE320901B31E39C9@HE1P190MB0540.EURP190.PROD.OUTLOOK.COM>
+Accept-Language: en-US
+Content-Language: en-US
+Content-Type: multipart/alternative;
+ boundary="_000_HE1P190MB0540579ABA18FCE320901B31E39C9HE1P190MB0540EURP_"
+MIME-Version: 1.0
+
+--_000_HE1P190MB0540579ABA18FCE320901B31E39C9HE1P190MB0540EURP_
+Content-Type: text/plain; charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+
+
+--_000_HE1P190MB0540579ABA18FCE320901B31E39C9HE1P190MB0540EURP_
+Content-Type: text/html; charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+<html xmlns:v=3D"urn:schemas-microsoft-com:vml" xmlns:o=3D"urn:schemas-micr=
+osoft-com:office:office" xmlns:w=3D"urn:schemas-microsoft-com:office:word" =
+xmlns:m=3D"http://schemas.microsoft.com/office/2004/12/omml" xmlns=3D"http:=
+//www.w3.org/TR/REC-html40"><head>
+<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Diso-8859-=
+1">
+<meta name=3D"Generator" content=3D"Microsoft Word 15 (filtered medium)">
+<style><!--
+/* Font Definitions */
+@font-face
+ {font-family:"Cambria Math";
+ panose-1:2 4 5 3 5 4 6 3 2 4;}
+@font-face
+ {font-family:Calibri;
+ panose-1:2 15 5 2 2 2 4 3 2 4;}
+/* Style Definitions */
+p.MsoNormal, li.MsoNormal, div.MsoNormal
+ {margin:0cm;
+ font-size:11.0pt;
+ font-family:"Calibri",sans-serif;
+ mso-fareast-language:EN-US;}
+span.EmailStyle18
+ {mso-style-type:personal-compose;
+ font-family:"Calibri",sans-serif;
+ color:windowtext;}
+.MsoChpDefault
+ {mso-style-type:export-only;
+ font-size:10.0pt;}
+@page WordSection1
+ {size:612.0pt 792.0pt;
+ margin:70.85pt 2.0cm 70.85pt 2.0cm;}
+div.WordSection1
+ {page:WordSection1;}
+--></style><!--[if gte mso 9]><xml>
+<o:shapedefaults v:ext=3D"edit" spidmax=3D"1026" />
+</xml><![endif]--><!--[if gte mso 9]><xml>
+<o:shapelayout v:ext=3D"edit">
+<o:idmap v:ext=3D"edit" data=3D"1" />
+</o:shapelayout></xml><![endif]-->
+</head>
+<body lang=3D"FI" link=3D"#0563C1" vlink=3D"#954F72" style=3D"word-wrap:bre=
+ak-word">
+<div class=3D"WordSection1">
+<p class=3D"MsoNormal"><o:p>&nbsp;</o:p></p>
+</div>
+</body>
+</html>
+
+--_000_HE1P190MB0540579ABA18FCE320901B31E39C9HE1P190MB0540EURP_
+Content-Type: text/calendar; charset="utf-8"; method=REQUEST
+Content-Transfer-Encoding: base64
+
+QkVHSU46VkNBTEVOREFSCk1FVEhPRDpSRVFVRVNUClBST0RJRDpNaWNyb3NvZnQgRXhjaGFuZ2Ug
+U2VydmVyIDIwMTAKVkVSU0lPTjoyLjAKQkVHSU46VlRJTUVaT05FClRaSUQ6RkxFIFN0YW5kYXJk
+IFRpbWUKQkVHSU46U1RBTkRBUkQKRFRTVEFSVDoxNjAxMDEwMVQwNDAwMDAKVFpPRkZTRVRGUk9N
+OiswMzAwClRaT0ZGU0VUVE86KzAyMDAKUlJVTEU6RlJFUT1ZRUFSTFk7SU5URVJWQUw9MTtCWURB
+WT0tMVNVO0JZTU9OVEg9MTAKRU5EOlNUQU5EQVJECkJFR0lOOkRBWUxJR0hUCkRUU1RBUlQ6MTYw
+MTAxMDFUMDMwMDAwClRaT0ZGU0VURlJPTTorMDIwMApUWk9GRlNFVFRPOiswMzAwClJSVUxFOkZS
+RVE9WUVBUkxZO0lOVEVSVkFMPTE7QllEQVk9LTFTVTtCWU1PTlRIPTMKRU5EOkRBWUxJR0hUCkVO
+RDpWVElNRVpPTkUKQkVHSU46VkVWRU5UCk9SR0FOSVpFUjtDTj1NYXJnZTptYWlsdG86bWFyZ2VA
+ZXhhbXBsZS5vcmcKQVRURU5ERUU7Uk9MRT1SRVEtUEFSVElDSVBBTlQ7UEFSVFNUQVQ9TkVFRFMt
+QUNUSU9OO1JTVlA9VFJVRTtDTj1Ib20KIGVyOm1haWx0bzpob21lckBleGFtcGxlLm9yZwpERVND
+UklQVElPTjtMQU5HVUFHRT1lbi1VUzpcbgpVSUQ6MDMwMDAwMDA4MjAwRTAwMDc0QzVCNzEwMUE4
+MkUwMDgwMDAwMDAwMDYwQjYwOEQwOTBEREQ3MDEwMDAwMDAwMDAwMDAwMDAKIDAxMDAwMDAwMDRC
+RTBDRkZBNTRCQ0Y2NEU5NTZFMzQxNDMzNjJDM0MwClNVTU1BUlk7TEFOR1VBR0U9ZW4tVVM6VGVz
+dGF1cwpEVFNUQVJUO1RaSUQ9RkxFIFN0YW5kYXJkIFRpbWU6MjAyMTExMjdUMDkwMDAwCkRURU5E
+O1RaSUQ9RkxFIFN0YW5kYXJkIFRpbWU6MjAyMTExMjdUMDkzMDAwCkNMQVNTOlBVQkxJQwpQUklP
+UklUWTo1CkRUU1RBTVA6MjAyMTExMTlUMjAwMDI5WgpUUkFOU1A6T1BBUVVFClNUQVRVUzpDT05G
+SVJNRUQKU0VRVUVOQ0U6MApMT0NBVElPTjtMQU5HVUFHRT1lbi1VUzoKWC1NSUNST1NPRlQtQ0RP
+LUFQUFQtU0VRVUVOQ0U6MApYLU1JQ1JPU09GVC1DRE8tT1dORVJBUFBUSUQ6LTcyMDEyODAyNwpY
+LU1JQ1JPU09GVC1DRE8tQlVTWVNUQVRVUzpURU5UQVRJVkUKWC1NSUNST1NPRlQtQ0RPLUlOVEVO
+REVEU1RBVFVTOkJVU1kKWC1NSUNST1NPRlQtQ0RPLUFMTERBWUVWRU5UOkZBTFNFClgtTUlDUk9T
+T0ZULUNETy1JTVBPUlRBTkNFOjEKWC1NSUNST1NPRlQtQ0RPLUlOU1RUWVBFOjAKWC1NSUNST1NP
+RlQtRE9OT1RGT1JXQVJETUVFVElORzpGQUxTRQpYLU1JQ1JPU09GVC1ESVNBTExPVy1DT1VOVEVS
+OkZBTFNFClgtTUlDUk9TT0ZULUxPQ0FUSU9OUzpbXQpCRUdJTjpWQUxBUk0KREVTQ1JJUFRJT046
+UkVNSU5ERVIKVFJJR0dFUjtSRUxBVEVEPVNUQVJUOi1QVDE1TQpBQ1RJT046RElTUExBWQpFTkQ6
+VkFMQVJNCkVORDpWRVZFTlQKRU5EOlZDQUxFTkRBUgo=
+
+--_000_HE1P190MB0540579ABA18FCE320901B31E39C9HE1P190MB0540EURP_--
diff --git a/comm/calendar/test/browser/invitations/data/repeat-event.eml b/comm/calendar/test/browser/invitations/data/repeat-event.eml
new file mode 100644
index 0000000000..9247e6575b
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/data/repeat-event.eml
@@ -0,0 +1,49 @@
+MIME-Version: 1.0
+Date: Mon, 28 Mar 2022 17:49:35 +0000
+Subject: Invitation: Repeat Event @ Daily from 2pm to 3pm 3 times (AST) (receiver@example.com)
+From: sender@example.com
+To: receiver@example.com
+Content-Type: multipart/mixed; boundary="00000000000080f3db05db4aef5b"
+
+--00000000000080f3db05db4aef5b
+Content-Type: multipart/alternative; boundary="00000000000080f3da05db4aef59"
+
+--00000000000080f3da05db4aef59
+Content-Type: text/calendar; charset="UTF-8"; method=REQUEST
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+METHOD:REQUEST
+BEGIN:VEVENT
+DTSTART:20220316T110000Z
+DTEND:20220316T113000Z
+RRULE:FREQ=DAILY;WKST=SU;COUNT=3;INTERVAL=1
+DTSTAMP:20220316T191602Z
+UID:02e79b96
+ORGANIZER;CN=Sender;
+ EMAIL=sender@example.com:mailto:sender@example.com
+ATTENDEE;CN=Sender;
+ EMAIL=sender@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=ACCEPTED;RSVP=FALSE:mailto:sender@example.com
+ATTENDEE;CN=Receiver;EMAIL=receiver@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:receiver@example.com
+ATTENDEE;CN=Other;EMAIL=other@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:other@example.com
+CREATED:20220328T174934Z
+LAST-MODIFIED:20220328T174934Z
+LOCATION:Somewhere
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY:Repeat Event
+DESCRIPTION:An event invitation.
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER:-P1D
+DESCRIPTION:This is an event reminder
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+
+--00000000000080f3da05db4aef59--
+--00000000000080f3db05db4aef5b--
diff --git a/comm/calendar/test/browser/invitations/data/repeat-update-major.eml b/comm/calendar/test/browser/invitations/data/repeat-update-major.eml
new file mode 100644
index 0000000000..61fe9f5022
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/data/repeat-update-major.eml
@@ -0,0 +1,49 @@
+MIME-Version: 1.0
+Date: Mon, 28 Mar 2022 17:49:35 +0000
+Subject: Repeat Update Major
+From: sender@example.com
+To: receiver@example.com
+Content-Type: multipart/mixed; boundary="00000000000080f3db05db4aef5b"
+
+--00000000000080f3db05db4aef5b
+Content-Type: multipart/alternative; boundary="00000000000080f3da05db4aef59"
+
+--00000000000080f3da05db4aef59
+Content-Type: text/calendar; charset="UTF-8"; method=REQUEST
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+METHOD:REQUEST
+BEGIN:VEVENT
+DTSTART:20220316T050000Z
+DTEND:20220316T053000Z
+RRULE:FREQ=DAILY;WKST=SU;COUNT=3;INTERVAL=1
+DTSTAMP:20220316T191602Z
+UID:02e79b96
+ORGANIZER;CN=Sender;
+ EMAIL=sender@example.com:mailto:sender@example.com
+ATTENDEE;CN=Sender;
+ EMAIL=sender@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=ACCEPTED;RSVP=FALSE:mailto:sender@example.com
+ATTENDEE;CN=Receiver;EMAIL=receiver@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:receiver@example.com
+ATTENDEE;CN=Other;EMAIL=other@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:other@example.com
+CREATED:20220328T174934Z
+LAST-MODIFIED:20220328T174934Z
+LOCATION:Somewhere
+SEQUENCE:2
+STATUS:CONFIRMED
+SUMMARY:Repeat Event
+DESCRIPTION:An event invitation.
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER:-P1D
+DESCRIPTION:This is an event reminder
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+
+--00000000000080f3da05db4aef59--
+--00000000000080f3db05db4aef5b--
diff --git a/comm/calendar/test/browser/invitations/data/repeat-update-minor.eml b/comm/calendar/test/browser/invitations/data/repeat-update-minor.eml
new file mode 100644
index 0000000000..a6ad357553
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/data/repeat-update-minor.eml
@@ -0,0 +1,49 @@
+MIME-Version: 1.0
+Date: Mon, 28 Mar 2022 17:49:35 +0000
+Subject: Invitation: Repeat Update Minor
+From: sender@example.com
+To: receiver@example.com
+Content-Type: multipart/mixed; boundary="00000000000080f3db05db4aef5b"
+
+--00000000000080f3db05db4aef5b
+Content-Type: multipart/alternative; boundary="00000000000080f3da05db4aef59"
+
+--00000000000080f3da05db4aef59
+Content-Type: text/calendar; charset="UTF-8"; method=REQUEST
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+METHOD:REQUEST
+BEGIN:VEVENT
+DTSTART:20220316T110000Z
+DTEND:20220316T113000Z
+RRULE:FREQ=DAILY;WKST=SU;COUNT=3;INTERVAL=1
+DTSTAMP:20220318T191602Z
+UID:02e79b96
+ORGANIZER;CN=Sender;
+ EMAIL=sender@example.com:mailto:sender@example.com
+ATTENDEE;CN=Sender;
+ EMAIL=sender@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=ACCEPTED;RSVP=FALSE:mailto:sender@example.com
+ATTENDEE;CN=Receiver;EMAIL=receiver@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:receiver@example.com
+ATTENDEE;CN=Other;EMAIL=other@example.com;CUTYPE=INDIVIDUAL;
+ PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:other@example.com
+CREATED:20220328T174934Z
+LAST-MODIFIED:20220328T174934Z
+LOCATION:Updated location
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY:Updated Event
+DESCRIPTION:Updated description.
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER:-P1D
+DESCRIPTION:Updated description.
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+
+--00000000000080f3da05db4aef59--
+--00000000000080f3db05db4aef5b--
diff --git a/comm/calendar/test/browser/invitations/data/single-event.eml b/comm/calendar/test/browser/invitations/data/single-event.eml
new file mode 100644
index 0000000000..14418c2c79
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/data/single-event.eml
@@ -0,0 +1,78 @@
+MIME-Version: 1.0
+Content-Transfer-Encoding: binary
+Content-Type: multipart/mixed; boundary="_----------=_1647458162153312762582"
+Date: Wed, 16 Mar 2022 15:16:02 -0400
+To: receiver@example.com
+Subject: Invitation: Single Event @ Wed, Mar 16 2022 11:00 AST
+From: Sender <sender@example.com>
+
+This is a multi-part message in MIME format.
+
+--_----------=_1647458162153312762582
+MIME-Version: 1.0
+Content-Transfer-Encoding: binary
+Content-Type: multipart/alternative; boundary="_----------=_1647458162153312762583"
+Date: Wed, 16 Mar 2022 15:16:02 -0400
+
+This is a multi-part message in MIME format.
+
+--_----------=_1647458162153312762583
+MIME-Version: 1.0
+Content-Disposition: inline
+Content-Length: 227
+Content-Transfer-Encoding: binary
+Content-Type: text/plain; charset="utf-8"
+Date: Wed, 16 Mar 2022 15:16:02 -0400
+
+Single Event
+
+When:
+ Wed, Mar 16 2022
+ 11:00 - 12:00 AST
+Where:
+ Somewhere
+
+--_----------=_1647458162153312762583
+MIME-Version: 1.0
+Content-Disposition: inline
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/calendar; charset="utf-8"; method="REQUEST"
+Date: Wed, 16 Mar 2022 15:16:02 -0400
+
+BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:02e79b96
+SEQUENCE:0
+DTSTAMP:20220316T191602Z
+CREATED:20220316T191532Z
+DTSTART:20220316T110000Z
+DTEND:20220316T113000Z
+DURATION:PT1H
+PRIORITY:0
+SUMMARY:Single Event
+DESCRIPTION:An event invitation.
+LOCATION:Somewhere
+STATUS:CONFIRMED
+TRANSP:OPAQUE
+CLASS:PUBLIC
+ORGANIZER;CN=3DSender;
+ EMAIL=3Dsender@example.com:mailto:sender@example.com
+ATTENDEE;CN=3DSender;
+ EMAIL=3Dsender@example.com;CUTYPE=3DINDIVIDUAL;
+ PARTSTAT=3DACCEPTED;RSVP=3DFALSE:mailto:sender@example.com
+ATTENDEE;CN=Receiver;EMAIL=3Dreceiver@example.com;CUTYPE=3DINDIVIDUAL;
+ PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailto:receiver@example.com
+ATTENDEE;CN=Other;EMAIL=other@example.com;CUTYPE=3DINDIVIDUAL;
+ PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailto:other@example.com
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER:-P1D
+DESCRIPTION:This is an event reminder
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+
+--_----------=_1647458162153312762583--
diff --git a/comm/calendar/test/browser/invitations/data/teams-meeting-invite.eml b/comm/calendar/test/browser/invitations/data/teams-meeting-invite.eml
new file mode 100644
index 0000000000..777486ec87
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/data/teams-meeting-invite.eml
@@ -0,0 +1,167 @@
+From: Marge <marge@example.com>
+To: bart@example.com, homer@example.com
+Subject: Teams meeting
+Thread-Topic: Teams meeting
+Thread-Index: AdbIy2RnFnEYrGmq80aB3RiaEcOS6w==
+Date: Wed, 2 Dec 2020 16:52:34 +0000
+Message-ID: <HE1PR0802MB228346BE1576FEAB8A7F32328EF30@HE1PR0802MB2283.eurprd08.prod.outlook.com>
+Accept-Language: fi-FI, en-US
+Content-Language: en-US
+X-MS-Has-Attach:
+X-MS-TNEF-Correlator:
+Content-Type: multipart/alternative;
+ boundary="_000_HE1PR0802MB228346BE1576FEAB8A7F32328EF30HE1PR0802MB2283_"
+X-Spam-Flag: No
+Return-Path: marge@example.com
+MIME-Version: 1.0
+
+--_000_HE1PR0802MB228346BE1576FEAB8A7F32328EF30HE1PR0802MB2283_
+Content-Type: text/plain; charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+
+
+
+___________________________________________________________________________=
+_____
+Microsoft Teams -kokous
+Liity tietokoneella tai mobiilisovelluksella
+Liity kokoukseen napsauttamalla t=E4t=E4<https://teams.microsoft.com/l/meet=
+up-join/19%3ameeting_MHU5NmI5ZGAtOWZmOC00Y2ZmLWJlOTItNjUxNjA5YjUyYTYy%40thr=
+ead.v2/0?context=3D%7b%33Tid%22%3a%222fd0c1c5-28e1-40c4-9f0d-a0363ca80a3c%2=
+2%2c%22Oid%22%3a%2214464d09-ceb8-458c-a61c-717f1e5c66c5%22%7d>
+Lis=E4tietoja<https://aka.ms/JoinTeamsMeeting> | Kokousasetukset<https://te=
+ams.microsoft.com/meetingOptions/?organizerId=3D14464d09-ceb8-458c-a61c-717=
+f1e5c66c5&tenantId=3D2fd0c1c5-28e1-40c4-9f0d-a0363ca80a3c&threadId=3D19_mee=
+ting_MHU5NmI5ZGAtOWZmOC00Y2ZmLWJlOTItNjUxNjA5YjUyYTYy@thread.v2&messageId=
+=3D0&language=3Dfi-FI>
+___________________________________________________________________________=
+_____
+
+--_000_HE1PR0802MB228346BE1576FEAB8A7F32328EF30HE1PR0802MB2283_
+Content-Type: text/html; charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+<html>
+<head>
+<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Diso-8859-=
+1">
+</head>
+<body>
+<div><br>
+<br>
+<br>
+<div style=3D"width:100%;height: 20px;"><span style=3D"white-space:nowrap;c=
+olor:#5F5F5F;opacity:.36;">________________________________________________=
+________________________________</span>
+</div>
+<div class=3D"me-email-text" style=3D"color:#252424;font-family:'Segoe UI',=
+'Helvetica Neue',Helvetica,Arial,sans-serif;">
+<div style=3D"margin-top: 24px; margin-bottom: 20px;"><span style=3D"font-s=
+ize: 24px; color:#252424">Microsoft Teams -kokous</span>
+</div>
+<div style=3D"margin-bottom: 20px;">
+<div style=3D"margin-top: 0px; margin-bottom: 0px; font-weight: bold"><span=
+ style=3D"font-size: 14px; color:#252424">Liity tietokoneella tai mobiiliso=
+velluksella</span>
+</div>
+<a class=3D"me-email-headline" style=3D"font-size: 14px;font-family:'Segoe =
+UI Semibold','Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif;text-de=
+coration: underline;color: #6264a7;" href=3D"https://teams.microsoft.com/l/=
+meetup-join/19%3ameeting_MHU5NmI5ZGAtOWZmOC00Y2ZmLWJlOTItNjUxNjA5YjUyYTYy%4=
+0thread.v2/0?context=3D%7b%33Tid%22%3a%222fd0c1c5-28e1-40c4-9f0d-a0363ca80a=
+3c%22%2c%22Oid%22%3a%2214464d09-ceb8-458c-a61c-717f1e5c66c5%22%7d" target=
+=3D"_blank" rel=3D"noreferrer noopener">Liity
+ kokoukseen napsauttamalla t=E4t=E4</a> </div>
+<div style=3D"margin-bottom: 24px;margin-top: 20px;"><a class=3D"me-email-l=
+ink" style=3D"font-size: 14px;text-decoration: underline;color: #6264a7;fon=
+t-family:'Segoe UI','Helvetica Neue',Helvetica,Arial,sans-serif;" target=3D=
+"_blank" href=3D"https://aka.ms/JoinTeamsMeeting" rel=3D"noreferrer noopene=
+r">Lis=E4tietoja</a>
+ | <a class=3D"me-email-link" style=3D"font-size: 14px;text-decoration: und=
+erline;color: #6264a7;font-family:'Segoe UI','Helvetica Neue',Helvetica,Ari=
+al,sans-serif;" target=3D"_blank" href=3D"https://teams.microsoft.com/meeti=
+ngOptions/?organizerId=3D14464d09-ceb8-458c-a61c-717f1e5c66c5&amp;tenantId=
+=3D2fd0c1c5-28e1-40c4-9f0d-a0363ca80a3c&amp;threadId=3D19_meeting_MGU5NmI2Z=
+GYtOWZmOC00Y2ZmLWJlOTItNjUxNjA5YjUyYTYy@thread.v2&amp;messageId=3D0&amp;lan=
+guage=3Dfi-FI" rel=3D"noreferrer noopener">
+Kokousasetukset</a> </div>
+</div>
+<div style=3D"font-size: 14px; margin-bottom: 4px;font-family:'Segoe UI','H=
+elvetica Neue',Helvetica,Arial,sans-serif;">
+</div>
+<div style=3D"font-size: 12px;"></div>
+<div style=3D"width:100%;height: 20px;"><span style=3D"white-space:nowrap;c=
+olor:#5F5F5F;opacity:.36;">________________________________________________=
+________________________________</span>
+</div>
+</div>
+</body>
+</html>
+
+--_000_HE1PR0802MB228346BE1576FEAB8A7F32328EF30HE1PR0802MB2283_
+Content-Type: text/calendar; charset="utf-8"; method=REQUEST
+Content-Transfer-Encoding: base64
+
+QkVHSU46VkNBTEVOREFSCk1FVEhPRDpSRVFVRVNUClBST0RJRDpNaWNyb3NvZnQgRXhjaGFuZ2Ug
+U2VydmVyIDIwMTAKVkVSU0lPTjoyLjAKQkVHSU46VlRJTUVaT05FClRaSUQ6RkxFIFN0YW5kYXJk
+IFRpbWUKQkVHSU46U1RBTkRBUkQKRFRTVEFSVDoxNjAxMDEwMVQwNDAwMDAKVFpPRkZTRVRGUk9N
+OiswMzAwClRaT0ZGU0VUVE86KzAyMDAKUlJVTEU6RlJFUT1ZRUFSTFk7SU5URVJWQUw9MTtCWURB
+WT0tMVNVO0JZTU9OVEg9MTAKRU5EOlNUQU5EQVJECkJFR0lOOkRBWUxJR0hUCkRUU1RBUlQ6MTYw
+MTAxMDFUMDMwMDAwClRaT0ZGU0VURlJPTTorMDIwMApUWk9GRlNFVFRPOiswMzAwClJSVUxFOkZS
+RVE9WUVBUkxZO0lOVEVSVkFMPTE7QllEQVk9LTFTVTtCWU1PTlRIPTMKRU5EOkRBWUxJR0hUCkVO
+RDpWVElNRVpPTkUKQkVHSU46VkVWRU5UCk9SR0FOSVpFUjtDTj1NYXJnZTptYWlsdG86bWFyZ2VA
+ZXhhbXBsZS5jb20KQVRURU5ERUU7Uk9MRT1SRVEtUEFSVElDSVBBTlQ7UEFSVFNUQVQ9TkVFRFMt
+QUNUSU9OO1JTVlA9VFJVRTtDTj1iYXJ0QGUKIGV4YW1wbGUuY29tOm1haWx0bzpiYXJ0QGV4YW1w
+bGUuY29tCkFUVEVOREVFO1JPTEU9UkVRLVBBUlRJQ0lQQU5UO1BBUlRTVEFUPU5FRURTLUFDVElP
+TjtSU1ZQPVRSVUU7Q049aG9tZXJAZXgKIGFtcGxlLmNvbTptYWlsdG86aG9tZXJAZXhhbXBsZS5j
+b20KCkRFU0NSSVBUSU9OO0xBTkdVQUdFPWVuLVVTOlxuXG5cbl9fX19fX19fX19fX19fX19fX19f
+X19fX19fX19fX19fX19fX19fX19fXwogX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19f
+X19fX19cbk1pY3Jvc29mdCBUZWFtcyAta29rb3VzXG5MaWl0eSB0aWUKIHRva29uZWVsbGEgdGFp
+IG1vYmlpbGlzb3ZlbGx1a3NlbGxhXG5MaWl0eSBrb2tvdWtzZWVuIG5hcHNhdXR0YW1hbGxhIHTD
+pHQKIMOkPGh0dHBzOi8vdGVhbXMubWljcm9zb2Z0LmNvbS9sL21lZXR1cC1qb2luLzE5JTNhbWVl
+dGluZ19NR1U1Tm1JMlpHWXRPV1ptCiBPQzAwWTJabUxXSmxPVEl0TmpVeE5qQTVZalV5WVRZeSU0
+MHRocmVhZC52Mi8wP2NvbnRleHQ9JTdiJTIyVGlkJTIyJTNhJTIyMgogZmQwYzFjNS0yOGUxLTQw
+YzQtOWYwZC1hMDM2M2NhODBhM2MlMjIlMmMlMjJPaWQlMjIlM2ElMjIxNDQ2NGQwOS1jZWI4LTQ1
+OGMKIC1hNjFjLTcxN2YxZTVjNjZjNSUyMiU3ZD5cbkxpc8OkdGlldG9qYTxodHRwczovL2FrYS5t
+cy9Kb2luVGVhbXNNZWV0aW5nPiB8CiAgS29rb3VzYXNldHVrc2V0PGh0dHBzOi8vdGVhbXMubWlj
+cm9zb2Z0LmNvbS9tZWV0aW5nT3B0aW9ucy8/b3JnYW5pemVySWQ9MQogNDQ2NGQwOS1jZWI4LTQ1
+OGMtYTYxYy03MTdmMWU1YzY2YzUmdGVuYW50SWQ9MmZkMGMxYzUtMjhlMS00MGM0LTlmMGQtYTAz
+NjMKIGNhODBhM2MmdGhyZWFkSWQ9MTlfbWVldGluZ19NR1U1Tm1JMlpHWXRPV1ptT0MwMFkyWm1M
+V0psT1RJdE5qVXhOakE1WWpVeVlUCiBZeUB0aHJlYWQudjImbWVzc2FnZUlkPTAmbGFuZ3VhZ2U9
+ZmktRkk+XG5fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fXwogX19fX19fX19fX19fX19f
+X19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fXG4KVUlEOjA1NjAwMDAwODIwMEUwMDA3
+NEM1QjcxMDFBODJFMDA4MDAwMDAwMDAxQUY4NEM2NENCQzhENjAxMDAwMDAwMDAwMDAwMDAwCiAw
+MTAwMDAwMDA0MDNCNUFDMTBBMEVCNDQ0QTk0N0QyQjQ5OUE0Qjk4QwpTVU1NQVJZO0xBTkdVQUdF
+PWVuLVVTOlRlYW1zIG1lZXRpbmcKRFRTVEFSVDtUWklEPUZMRSBTdGFuZGFyZCBUaW1lOjIwMjAx
+MjAyVDE5MDAwMApEVEVORDtUWklEPUZMRSBTdGFuZGFyZCBUaW1lOjIwMjAxMjAyVDIxMDAwMApD
+TEFTUzpQVUJMSUMKUFJJT1JJVFk6NQpEVFNUQU1QOjIwMjAxMjAyVDE2NTEzNFoKVFJBTlNQOk9Q
+QVFVRQpTVEFUVVM6Q09ORklSTUVEClNFUVVFTkNFOjAKTE9DQVRJT047TEFOR1VBR0U9ZW4tVVM6
+ClgtTUlDUk9TT0ZULUNETy1BUFBULVNFUVVFTkNFOjAKWC1NSUNST1NPRlQtQ0RPLU9XTkVSQVBQ
+VElEOjIxMTg5MTUzNTQKWC1NSUNST1NPRlQtQ0RPLUJVU1lTVEFUVVM6VEVOVEFUSVZFClgtTUlD
+Uk9TT0ZULUNETy1JTlRFTkRFRFNUQVRVUzpCVVNZClgtTUlDUk9TT0ZULUNETy1BTExEQVlFVkVO
+VDpGQUxTRQpYLU1JQ1JPU09GVC1DRE8tSU1QT1JUQU5DRToxClgtTUlDUk9TT0ZULUNETy1JTlNU
+VFlQRTowClgtTUlDUk9TT0ZULVNLWVBFVEVBTVNNRUVUSU5HVVJMOmh0dHBzOi8vdGVhbXMubWlj
+cm9zb2Z0LmNvbS9sL21lZXR1cC1qb2luLwogMTklM2FtZWV0aW5nX01HVTVObUkyWkdZdE9XWm1P
+QzAwWTJabUxXSmxPVEl0TmpVeE5qQTVZalV5WVRZeSU0MHRocmVhZC52Mi8KIDA/Y29udGV4dD0l
+N2IlMjJUaWQlMjIlM2ElMjIyZmQwYzFjNS0xOGUxLTQwYzQtOWYwZC1hMDM2M2NhODBhM2MlMjIl
+MmMlMjJPCiBpZCUyMiUzYSUyMjE0NDY0ZDA5LWNlYjgtNDU4Yy1hNjFjLTcxN2YxZTVjNjZjNSUy
+MiU3ZApYLU1JQ1JPU09GVC1TQ0hFRFVMSU5HU0VSVklDRVVQREFURVVSTDpodHRwczovL3NjaGVk
+dWxlci50ZWFtcy5taWNyb3NvZnQuY28KIG0vdGVhbXMvMmZkMGMxYzUtMjhlMS00MGM0LTlmMGQt
+YWIzNjNjYTgwYTNjLzE0NDY0ZDA5LWNlYjgtNDU4Yy1hNjFjLTcxN2YxCiBlNWM2NmM1LzE5X21l
+ZXRpbmdfTUdVNU5tSTJaR1l0T1dabU9DMDBZMlptTFdKbE9USXROalV4TmpBNVlqVXlZVFl5QHRo
+cmVhZAogLnYyLzAKWC1NSUNST1NPRlQtU0tZUEVURUFNU1BST1BFUlRJRVM6eyJjaWQiOiIxOTpt
+ZWV0aW5nX01HVTVObUkyWkdZdE9XWm1PQzAwWTJaCiBtTFdKbE9USXROalV4TmpBNVlqVXlZVFl5
+QHRocmVhZC52MiJcLCJyaWQiOjBcLCJtaWQiOjBcLCJ1aWQiOm51bGxcLCJwcml2YQogdGUiOnRy
+dWVcLCJ0eXBlIjowfQpYLU1JQ1JPU09GVC1PTkxJTkVNRUVUSU5HQ09ORkxJTks6Y29uZjpzaXA6
+bWFyZ2VAZXhhbXBsZS5jb21cO2dydXVcO29wCiBhcXVlPWFwcDpjb25mOmZvY3VzOmlkOnRlYW1z
+OjI6MCExOTptZWV0aW5nX01HVTVNbUkyWkdZdE9XWm1PQzAwWTJabUxXSmxPVAogSXROalV4TmpB
+NVlqVXlZVFl5LXRocmVhZC52MiExNDQ2NGQwOWNlYjg0NThjYTYxYzcxN2YxZTVjNjZjNSEyZmQw
+YzFjNTI4ZTEKIDQwYzQ5ZjBkYTAzNjNjYTgwYTNjClgtTUlDUk9TT0ZULU9OTElORU1FRVRJTkdJ
+TkZPUk1BVElPTjp7Ik9ubGluZU1lZXRpbmdDaGFubmVsSWQiOm51bGxcLCJPbmxpbgogZU1lZXRp
+bmdQcm92aWRlciI6M30KWC1NSUNST1NPRlQtRE9OT1RGT1JXQVJETUVFVElORzpGQUxTRQpYLU1J
+Q1JPU09GVC1ESVNBTExPVy1DT1VOVEVSOkZBTFNFClgtTUlDUk9TT0ZULUxPQ0FUSU9OUzpbXQpC
+RUdJTjpWQUxBUk0KREVTQ1JJUFRJT046UkVNSU5ERVIKVFJJR0dFUjtSRUxBVEVEPVNUQVJUOi1Q
+VDE1TQpBQ1RJT046RElTUExBWQpFTkQ6VkFMQVJNCkVORDpWRVZFTlQKRU5EOlZDQUxFTkRBUgo=
+
+--_000_HE1PR0802MB228346BE1576FEAB8A7F32328EF30HE1PR0802MB2283_--
diff --git a/comm/calendar/test/browser/invitations/data/update-major.eml b/comm/calendar/test/browser/invitations/data/update-major.eml
new file mode 100644
index 0000000000..04754798b2
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/data/update-major.eml
@@ -0,0 +1,78 @@
+MIME-Version: 1.0
+Content-Transfer-Encoding: binary
+Content-Type: multipart/mixed; boundary="_----------=_1647458162153312762582"
+Date: Wed, 16 Mar 2022 15:16:02 -0400
+To: receiver@example.com
+Subject: Update Major
+From: Sender <sender@example.com>
+
+This is a multi-part message in MIME format.
+
+--_----------=_1647458162153312762582
+MIME-Version: 1.0
+Content-Transfer-Encoding: binary
+Content-Type: multipart/alternative; boundary="_----------=_1647458162153312762583"
+Date: Wed, 16 Mar 2022 15:16:02 -0400
+
+This is a multi-part message in MIME format.
+
+--_----------=_1647458162153312762583
+MIME-Version: 1.0
+Content-Disposition: inline
+Content-Length: 227
+Content-Transfer-Encoding: binary
+Content-Type: text/plain; charset="utf-8"
+Date: Wed, 16 Mar 2022 15:16:02 -0400
+
+Single Event
+
+When:
+ Wed, Mar 16 2022
+ 11:00 - 12:00 AST
+Where:
+ Somewhere
+
+--_----------=_1647458162153312762583
+MIME-Version: 1.0
+Content-Disposition: inline
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/calendar; charset="utf-8"; method="REQUEST"
+Date: Wed, 16 Mar 2022 15:16:02 -0400
+
+BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:02e79b96
+SEQUENCE:2
+DTSTAMP:20220316T191602Z
+CREATED:20220316T191532Z
+DTSTART:20220316T050000Z
+DTEND:20220316T053000Z
+DURATION:PT1H
+PRIORITY:0
+SUMMARY:Single Event
+DESCRIPTION:An event invitation.
+LOCATION:Somewhere
+STATUS:CONFIRMED
+TRANSP:OPAQUE
+CLASS:PUBLIC
+ORGANIZER;CN=3DSender;
+ EMAIL=3Dsender@example.com:mailto:sender@example.com
+ATTENDEE;CN=3DSender;
+ EMAIL=3Dsender@example.com;CUTYPE=3DINDIVIDUAL;
+ PARTSTAT=3DACCEPTED;RSVP=3DFALSE:mailto:sender@example.com
+ATTENDEE;CN=Receiver;EMAIL=3Dreceiver@example.com;CUTYPE=3DINDIVIDUAL;
+ PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailto:receiver@example.com
+ATTENDEE;CN=Other;EMAIL=other@example.com;CUTYPE=3DINDIVIDUAL;
+ PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailto:other@example.com
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER:-P1D
+DESCRIPTION:This is an event reminder
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+
+--_----------=_1647458162153312762583--
diff --git a/comm/calendar/test/browser/invitations/data/update-minor.eml b/comm/calendar/test/browser/invitations/data/update-minor.eml
new file mode 100644
index 0000000000..afeb8e9ba0
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/data/update-minor.eml
@@ -0,0 +1,78 @@
+MIME-Version: 1.0
+Content-Transfer-Encoding: binary
+Content-Type: multipart/mixed; boundary="_----------=_1647458162153312762582"
+Date: Wed, 16 Mar 2022 15:16:02 -0400
+To: receiver@example.com
+Subject: Update Minor
+From: Sender <sender@example.com>
+
+This is a multi-part message in MIME format.
+
+--_----------=_1647458162153312762582
+MIME-Version: 1.0
+Content-Transfer-Encoding: binary
+Content-Type: multipart/alternative; boundary="_----------=_1647458162153312762583"
+Date: Wed, 16 Mar 2022 15:16:02 -0400
+
+This is a multi-part message in MIME format.
+
+--_----------=_1647458162153312762583
+MIME-Version: 1.0
+Content-Disposition: inline
+Content-Length: 227
+Content-Transfer-Encoding: binary
+Content-Type: text/plain; charset="utf-8"
+Date: Wed, 16 Mar 2022 15:16:02 -0400
+
+Single Event
+
+When:
+ Wed, Mar 16 2022
+ 11:00 - 12:00 AST
+Where:
+ Somewhere
+
+--_----------=_1647458162153312762583
+MIME-Version: 1.0
+Content-Disposition: inline
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/calendar; charset="utf-8"; method="REQUEST"
+Date: Wed, 16 Mar 2022 15:16:02 -0400
+
+BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:02e79b96
+SEQUENCE:0
+DTSTAMP:20220318T191602Z
+CREATED:20220316T191532Z
+DTSTART:20220316T110000Z
+DTEND:20220316T113000Z
+DURATION:PT1H
+PRIORITY:0
+SUMMARY:Updated Event
+DESCRIPTION:Updated description.
+LOCATION:Updated location
+STATUS:CONFIRMED
+TRANSP:OPAQUE
+CLASS:PUBLIC
+ORGANIZER;CN=3DSender;
+ EMAIL=3Dsender@example.com:mailto:sender@example.com
+ATTENDEE;CN=3DSender;
+ EMAIL=3Dsender@example.com;CUTYPE=3DINDIVIDUAL;
+ PARTSTAT=3DACCEPTED;RSVP=3DFALSE:mailto:sender@example.com
+ATTENDEE;CN=Receiver;EMAIL=3Dreceiver@example.com;CUTYPE=3DINDIVIDUAL;
+ PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailto:receiver@example.com
+ATTENDEE;CN=Other;EMAIL=other@example.com;CUTYPE=3DINDIVIDUAL;
+ PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailto:other@example.com
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER:-P1D
+DESCRIPTION:Updated description.
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+
+--_----------=_1647458162153312762583--
diff --git a/comm/calendar/test/browser/invitations/head.js b/comm/calendar/test/browser/invitations/head.js
new file mode 100644
index 0000000000..24835c3021
--- /dev/null
+++ b/comm/calendar/test/browser/invitations/head.js
@@ -0,0 +1,942 @@
+/* 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/. */
+
+/**
+ * Common functions for the imip-bar tests.
+ *
+ * Note that these tests are heavily tied to the .eml files found in the data
+ * folder.
+ */
+
+"use strict";
+
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { CalItipDefaultEmailTransport } = ChromeUtils.import(
+ "resource:///modules/CalItipEmailTransport.jsm"
+);
+var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
+
+var { FileTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/FileTestUtils.sys.mjs"
+);
+var { CalendarTestUtils } = ChromeUtils.import(
+ "resource://testing-common/calendar/CalendarTestUtils.jsm"
+);
+
+registerCleanupFunction(async () => {
+ // Some tests that open new windows don't return focus to the main window
+ // in a way that satisfies mochitest, and the test times out.
+ Services.focus.focusedWindow = window;
+ document.body.focus();
+});
+
+class EmailTransport extends CalItipDefaultEmailTransport {
+ sentItems = [];
+
+ sentMsgs = [];
+
+ getMsgSend() {
+ let { sentMsgs } = this;
+ return {
+ sendMessageFile(
+ userIdentity,
+ accountKey,
+ composeFields,
+ messageFile,
+ deleteSendFileOnCompletion,
+ digest,
+ deliverMode,
+ msgToReplace,
+ listener,
+ statusFeedback,
+ smtpPassword
+ ) {
+ sentMsgs.push({
+ userIdentity,
+ accountKey,
+ composeFields,
+ messageFile,
+ deleteSendFileOnCompletion,
+ digest,
+ deliverMode,
+ msgToReplace,
+ listener,
+ statusFeedback,
+ smtpPassword,
+ });
+ },
+ };
+ }
+
+ sendItems(recipients, itipItem, fromAttendee) {
+ this.sentItems.push({ recipients, itipItem, fromAttendee });
+ return super.sendItems(recipients, itipItem, fromAttendee);
+ }
+
+ reset() {
+ this.sentItems = [];
+ this.sentMsgs = [];
+ }
+}
+
+async function openMessageFromFile(file) {
+ let fileURL = Services.io
+ .newFileURI(file)
+ .mutate()
+ .setQuery("type=application/x-message-display")
+ .finalize();
+
+ let winPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ window.openDialog(
+ "chrome://messenger/content/messageWindow.xhtml",
+ "_blank",
+ "all,chrome,dialog=no,status,toolbar",
+ fileURL
+ );
+ let win = await winPromise;
+ await BrowserTestUtils.waitForEvent(win, "MsgLoaded");
+ await TestUtils.waitForCondition(() => Services.focus.activeWindow == win);
+ return win;
+}
+
+/**
+ * Opens an iMIP message file and waits for the imip-bar to appear.
+ *
+ * @param {nsIFile} file
+ * @returns {Window}
+ */
+async function openImipMessage(file) {
+ let win = await openMessageFromFile(file);
+ let aboutMessage = win.document.getElementById("messageBrowser").contentWindow;
+ let imipBar = aboutMessage.document.getElementById("imip-bar");
+ await TestUtils.waitForCondition(() => !imipBar.collapsed, "imip-bar shown");
+
+ if (Services.prefs.getBoolPref("calendar.itip.newInvitationDisplay")) {
+ // CalInvitationDisplay.show() does some async activities before the panel is added.
+ await TestUtils.waitForCondition(
+ () =>
+ win.document
+ .getElementById("messageBrowser")
+ .contentDocument.querySelector("calendar-invitation-panel"),
+ "calendar-invitation-panel shown"
+ );
+ }
+ return win;
+}
+
+/**
+ * Clicks on one of the imip-bar action buttons.
+ *
+ * @param {Window} win
+ * @param {string} id
+ */
+async function clickAction(win, id) {
+ let aboutMessage = win.document.getElementById("messageBrowser").contentWindow;
+ let action = aboutMessage.document.getElementById(id);
+ await TestUtils.waitForCondition(() => !action.hidden, `button "#${id}" shown`);
+
+ EventUtils.synthesizeMouseAtCenter(action, {}, aboutMessage);
+ await TestUtils.waitForCondition(() => action.hidden, `button "#${id}" hidden`);
+}
+
+/**
+ * Clicks on one of the imip-bar actions from a dropdown menu.
+ *
+ * @param {Window} win The window the imip message is opened in.
+ * @param {string} buttonId The id of the <toolbarbutton> containing the menu.
+ * @param {string} actionId The id of the menu item to click.
+ */
+async function clickMenuAction(win, buttonId, actionId) {
+ let aboutMessage = win.document.getElementById("messageBrowser").contentWindow;
+ let actionButton = aboutMessage.document.getElementById(buttonId);
+ await TestUtils.waitForCondition(() => !actionButton.hidden, `"${buttonId}" shown`);
+
+ let actionMenu = actionButton.querySelector("menupopup");
+ let menuShown = BrowserTestUtils.waitForEvent(actionMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(actionButton.querySelector("dropmarker"), {}, aboutMessage);
+ await menuShown;
+ actionMenu.activateItem(aboutMessage.document.getElementById(actionId));
+ await TestUtils.waitForCondition(() => actionButton.hidden, `action menu "#${buttonId}" hidden`);
+}
+
+const unpromotedProps = ["location", "description", "sequence", "x-moz-received-dtstamp"];
+
+/**
+ * An object where the keys are paths/selectors and the values are the values
+ * we expect to encounter.
+ *
+ * @typedef {object} Comparable
+ */
+
+/**
+ * Compares the paths specified in the expected object against the provided
+ * actual object.
+ *
+ * @param {object} actual This is expected to be a calIEvent or calIAttendee but
+ * can also be an array of both etc.
+ * @param {Comparable} expected
+ */
+function compareProperties(actual, expected, prefix = "") {
+ Assert.equal(typeof actual, "object", `${prefix || "provided value"} is an object`);
+ for (let [key, value] of Object.entries(expected)) {
+ if (key.includes(".")) {
+ let keys = key.split(".");
+ let head = keys[0];
+ let tail = keys.slice(1).join(".");
+ compareProperties(actual[head], { [tail]: value }, [prefix, head].filter(k => k).join("."));
+ continue;
+ }
+
+ let path = [prefix, key].filter(k => k).join(".");
+ let actualValue = unpromotedProps.includes(key) ? actual.getProperty(key) : actual[key];
+ Assert.equal(actualValue, value, `property "${path}" is "${value}"`);
+ }
+}
+
+/**
+ * Compares the text contents of the selectors specified on the inviatation panel
+ * to the expected value for each.
+ *
+ * @param {ShadowRoot} root The invitation panel's ShadowRoot instance.
+ * @param {Comparable} expected
+ */
+function compareShownPanelValues(root, expected) {
+ for (let [key, value] of Object.entries(expected)) {
+ value = Array.isArray(value) ? value.join("") : value;
+ Assert.equal(
+ root.querySelector(key).textContent.trim(),
+ value,
+ `property "${key}" is "${value}"`
+ );
+ }
+}
+
+/**
+ * Clicks on one of the invitation panel action buttons.
+ *
+ * @param {Window} panel
+ * @param {string} id
+ * @param {boolean} sendResponse
+ */
+async function clickPanelAction(panel, id, sendResponse = true) {
+ let promise = BrowserTestUtils.promiseAlertDialogOpen(sendResponse ? "accept" : "cancel");
+ let button = panel.shadowRoot.getElementById(id);
+ EventUtils.synthesizeMouseAtCenter(button, {}, panel.ownerGlobal);
+ await promise;
+ await BrowserTestUtils.waitForEvent(panel.ownerGlobal, "onItipItemActionFinished");
+}
+
+/**
+ * Tests that an attempt to reply to the organizer of the event with the correct
+ * details occurred.
+ *
+ * @param {EmailTransport} transport
+ * @param {nsIdentity} identity
+ * @param {string} partStat
+ */
+async function doReplyTest(transport, identity, partStat) {
+ info("Verifying the attempt to send a response uses the correct data");
+ Assert.equal(transport.sentItems.length, 1, "itip subsystem attempted to send a response");
+ compareProperties(transport.sentItems[0], {
+ "recipients.0.id": "mailto:sender@example.com",
+ "itipItem.responseMethod": "REPLY",
+ "fromAttendee.id": "mailto:receiver@example.com",
+ "fromAttendee.participationStatus": partStat,
+ });
+
+ // The itipItem is used to generate the iTIP data in the message body.
+ info("Verifying the reply calItipItem attendee list");
+ let replyItem = transport.sentItems[0].itipItem.getItemList()[0];
+ let replyAttendees = replyItem.getAttendees();
+ Assert.equal(replyAttendees.length, 1, "reply has one attendee");
+ compareProperties(replyAttendees[0], {
+ id: "mailto:receiver@example.com",
+ participationStatus: partStat,
+ });
+
+ info("Verifying the call to the message subsystem");
+ Assert.equal(transport.sentMsgs.length, 1, "transport sent 1 message");
+ compareProperties(transport.sentMsgs[0], {
+ userIdentity: identity,
+ "composeFields.from": "receiver@example.com",
+ "composeFields.to": "Sender <sender@example.com>",
+ });
+ Assert.ok(transport.sentMsgs[0].messageFile.exists(), "message file was created");
+}
+
+/**
+ * @typedef {object} ImipBarActionTestConf
+ *
+ * @property {calICalendar} calendar The calendar used for the test.
+ * @property {calIItipTranport} transport The transport used for the test.
+ * @property {nsIIdentity} identity The identity expected to be used to
+ * send the reply.
+ * @property {boolean} isRecurring Indicates whether to treat the event as a
+ * recurring event or not.
+ * @property {string} partStat The participationStatus of the receiving user to
+ * expect.
+ * @property {boolean} noReply If true, do not expect an attempt to send a reply.
+ * @property {boolean} noSend If true, expect the reply attempt to stop after the
+ * user is prompted.
+ * @property {boolean} isMajor For update tests indicates if the changes expected
+ * are major or minor.
+ */
+
+/**
+ * Test the properties of an event created from the imip-bar and optionally, the
+ * attempt to send a reply.
+ *
+ * @param {ImipBarActionTestConf} conf
+ * @param {calIEvent|calIEvent[]} item
+ */
+async function doImipBarActionTest(conf, event) {
+ let { calendar, transport, identity, partStat, isRecurring, noReply, noSend } = conf;
+ let events = [event];
+ let startDates = ["20220316T110000Z"];
+ let endDates = ["20220316T113000Z"];
+
+ if (isRecurring) {
+ startDates = [...startDates, "20220317T110000Z", "20220318T110000Z"];
+ endDates = [...endDates, "20220317T113000Z", "20220318T113000Z"];
+ events = event.parentItem.recurrenceInfo.getOccurrences(
+ cal.createDateTime("19700101"),
+ cal.createDateTime("30000101"),
+ Infinity
+ );
+ Assert.equal(events.length, 3, "reccurring event has 3 occurrences");
+ }
+
+ info("Verifying relevant properties of each event occurrence");
+ for (let [index, occurrence] of events.entries()) {
+ compareProperties(occurrence, {
+ id: "02e79b96",
+ title: isRecurring ? "Repeat Event" : "Single Event",
+ "calendar.name": calendar.name,
+ ...(isRecurring ? { "recurrenceId.icalString": startDates[index] } : {}),
+ "startDate.icalString": startDates[index],
+ "endDate.icalString": endDates[index],
+ description: "An event invitation.",
+ location: "Somewhere",
+ sequence: "0",
+ "x-moz-received-dtstamp": "20220316T191602Z",
+ "organizer.id": "mailto:sender@example.com",
+ status: "CONFIRMED",
+ });
+
+ // Alarms should be ignored.
+ Assert.equal(
+ occurrence.getAlarms().length,
+ 0,
+ `${isRecurring ? "occurrence" : "event"} has no reminders`
+ );
+
+ info("Verifying attendee list and participation status");
+ let attendees = occurrence.getAttendees();
+ compareProperties(attendees, {
+ "0.id": "mailto:sender@example.com",
+ "0.participationStatus": "ACCEPTED",
+ "1.participationStatus": partStat,
+ "1.id": "mailto:receiver@example.com",
+ "2.id": "mailto:other@example.com",
+ "2.participationStatus": "NEEDS-ACTION",
+ });
+ }
+
+ if (noReply) {
+ Assert.equal(
+ transport.sentItems.length,
+ 0,
+ "itip subsystem did not attempt to send a response"
+ );
+ }
+ if (noReply || noSend) {
+ Assert.equal(transport.sentMsgs.length, 0, "no call was made into the mail subsystem");
+ return;
+ }
+ await doReplyTest(transport, identity, partStat);
+}
+
+/**
+ * Tests the recognition and application of a minor update to an existing event.
+ * An update is considered minor if the SEQUENCE property has not changed but
+ * the DTSTAMP has.
+ *
+ * @param {ImipBarActionTestConf} conf
+ */
+async function doMinorUpdateTest(conf) {
+ let { transport, calendar, partStat, isRecurring } = conf;
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem;
+ let prevEventIcs = event.icalString;
+
+ transport.reset();
+
+ let updatePath = isRecurring ? "data/repeat-update-minor.eml" : "data/update-minor.eml";
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath(updatePath)));
+ let aboutMessage = win.document.getElementById("messageBrowser").contentWindow;
+ let updateButton = aboutMessage.document.getElementById("imipUpdateButton");
+ Assert.ok(!updateButton.hidden, `#${updateButton.id} button shown`);
+ EventUtils.synthesizeMouseAtCenter(updateButton, {}, aboutMessage);
+
+ await TestUtils.waitForCondition(async () => {
+ event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem;
+ return event.icalString != prevEventIcs;
+ }, "event updated");
+
+ await BrowserTestUtils.closeWindow(win);
+
+ let events = [event];
+ let startDates = ["20220316T110000Z"];
+ let endDates = ["20220316T113000Z"];
+ if (isRecurring) {
+ startDates = [...startDates, "20220317T110000Z", "20220318T110000Z"];
+ endDates = [...endDates, "20220317T113000Z", "20220318T113000Z"];
+ events = event.recurrenceInfo.getOccurrences(
+ cal.createDateTime("19700101"),
+ cal.createDateTime("30000101"),
+ Infinity
+ );
+ Assert.equal(events.length, 3, "reccurring event has 3 occurrences");
+ }
+
+ info("Verifying relevant properties of each event occurrence");
+ for (let [index, occurrence] of events.entries()) {
+ compareProperties(occurrence, {
+ id: "02e79b96",
+ title: "Updated Event",
+ "calendar.name": calendar.name,
+ ...(isRecurring ? { "recurrenceId.icalString": startDates[index] } : {}),
+ "startDate.icalString": startDates[index],
+ "endDate.icalString": endDates[index],
+ description: "Updated description.",
+ location: "Updated location",
+ sequence: "0",
+ "x-moz-received-dtstamp": "20220318T191602Z",
+ "organizer.id": "mailto:sender@example.com",
+ status: "CONFIRMED",
+ });
+
+ // Note: It seems we do not keep the order of the attendees list for updates.
+ let attendees = occurrence.getAttendees();
+ compareProperties(attendees, {
+ "0.id": "mailto:sender@example.com",
+ "0.participationStatus": "ACCEPTED",
+ "1.id": "mailto:other@example.com",
+ "1.participationStatus": "NEEDS-ACTION",
+ "2.participationStatus": partStat,
+ "2.id": "mailto:receiver@example.com",
+ });
+ }
+
+ Assert.equal(transport.sentItems.length, 0, "itip subsystem did not attempt to send a response");
+ Assert.equal(transport.sentMsgs.length, 0, "no call was made into the mail subsystem");
+ await calendar.deleteItem(event);
+}
+
+const actionIds = {
+ single: {
+ button: {
+ ACCEPTED: "imipAcceptButton",
+ TENTATIVE: "imipTentativeButton",
+ DECLINED: "imipDeclineButton",
+ },
+ noReply: {
+ ACCEPTED: "imipAcceptButton_AcceptDontSend",
+ TENTATIVE: "imipTentativeButton_TentativeDontSend",
+ DECLINED: "imipDeclineButton_DeclineDontSend",
+ },
+ },
+ recurring: {
+ button: {
+ ACCEPTED: "imipAcceptRecurrencesButton",
+ TENTATIVE: "imipTentativeRecurrencesButton",
+ DECLINED: "imipDeclineRecurrencesButton",
+ },
+ noReply: {
+ ACCEPTED: "imipAcceptRecurrencesButton_AcceptDontSend",
+ TENTATIVE: "imipTentativeRecurrencesButton_TentativeDontSend",
+ DECLINED: "imipDeclineRecurrencesButton_DeclineDontSend",
+ },
+ },
+};
+
+/**
+ * Tests the recognition and application of a major update to an existing event.
+ * An update is considered major if the SEQUENCE property has changed. For major
+ * updates, the imip-bar prompts the user to re-confirm their attendance.
+ *
+ * @param {ImipBarActionTestConf} conf
+ */
+async function doMajorUpdateTest(conf) {
+ let { transport, identity, calendar, partStat, isRecurring, noReply } = conf;
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem;
+ let prevEventIcs = event.icalString;
+
+ transport.reset();
+
+ let updatePath = isRecurring ? "data/repeat-update-major.eml" : "data/update-major.eml";
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath(updatePath)));
+ let actions = isRecurring ? actionIds.recurring : actionIds.single;
+ if (noReply) {
+ let { button, noReply } = actions;
+ await clickMenuAction(win, button[partStat], noReply[partStat]);
+ } else {
+ await clickAction(win, actions.button[partStat]);
+ }
+
+ await TestUtils.waitForCondition(async () => {
+ event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem;
+ return event.icalString != prevEventIcs;
+ }, "event updated");
+
+ await BrowserTestUtils.closeWindow(win);
+
+ if (noReply) {
+ Assert.equal(
+ transport.sentItems.length,
+ 0,
+ "itip subsystem did not attempt to send a response"
+ );
+ Assert.equal(transport.sentMsgs.length, 0, "no call was made into the mail subsystem");
+ } else {
+ await doReplyTest(transport, identity, partStat);
+ }
+
+ let events = [event];
+ let startDates = ["20220316T050000Z"];
+ let endDates = ["20220316T053000Z"];
+ if (isRecurring) {
+ startDates = [...startDates, "20220317T050000Z", "20220318T050000Z"];
+ endDates = [...endDates, "20220317T053000Z", "20220318T053000Z"];
+ events = event.recurrenceInfo.getOccurrences(
+ cal.createDateTime("19700101"),
+ cal.createDateTime("30000101"),
+ Infinity
+ );
+ Assert.equal(events.length, 3, "reccurring event has 3 occurrences");
+ }
+
+ for (let [index, occurrence] of events.entries()) {
+ compareProperties(occurrence, {
+ id: "02e79b96",
+ title: isRecurring ? "Repeat Event" : "Single Event",
+ "calendar.name": calendar.name,
+ ...(isRecurring ? { "recurrenceId.icalString": startDates[index] } : {}),
+ "startDate.icalString": startDates[index],
+ "endDate.icalString": endDates[index],
+ description: "An event invitation.",
+ location: "Somewhere",
+ sequence: "2",
+ "x-moz-received-dtstamp": "20220316T191602Z",
+ "organizer.id": "mailto:sender@example.com",
+ status: "CONFIRMED",
+ });
+
+ let attendees = occurrence.getAttendees();
+ compareProperties(attendees, {
+ "0.id": "mailto:sender@example.com",
+ "0.participationStatus": "ACCEPTED",
+ "1.id": "mailto:other@example.com",
+ "1.participationStatus": "NEEDS-ACTION",
+ "2.participationStatus": partStat,
+ "2.id": "mailto:receiver@example.com",
+ });
+ }
+ await calendar.deleteItem(event);
+}
+
+/**
+ * Tests the recognition and application of a minor update exception to an
+ * existing recurring event.
+ *
+ * @param {ImipBarActionTestConf} conf
+ */
+async function doMinorExceptionTest(conf) {
+ let { transport, calendar, partStat } = conf;
+ let recurrenceId = cal.createDateTime("20220317T110000Z");
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem;
+ let originalProps = {
+ id: "02e79b96",
+ "recurrenceId.icalString": "20220317T110000Z",
+ title: event.title,
+ "calendar.name": calendar.name,
+ "startDate.icalString": event.startDate.icalString,
+ "endDate.icalString": event.endDate.icalString,
+ description: event.getProperty("DESCRIPTION"),
+ location: event.getProperty("LOCATION"),
+ sequence: "0",
+ "x-moz-received-dtstamp": event.getProperty("x-moz-received-dtstamp"),
+ "organizer.id": "mailto:sender@example.com",
+ status: "CONFIRMED",
+ };
+
+ Assert.ok(
+ !event.recurrenceInfo.getExceptionFor(recurrenceId),
+ `no exception exists for ${recurrenceId}`
+ );
+
+ transport.reset();
+
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-minor.eml")));
+ let aboutMessage = win.document.getElementById("messageBrowser").contentWindow;
+ let updateButton = aboutMessage.document.getElementById("imipUpdateButton");
+ Assert.ok(!updateButton.hidden, `#${updateButton.id} button shown`);
+ EventUtils.synthesizeMouseAtCenter(updateButton, {}, aboutMessage);
+
+ let exception;
+ await TestUtils.waitForCondition(async () => {
+ event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem;
+ exception = event.recurrenceInfo.getExceptionFor(recurrenceId);
+ return exception;
+ }, "event exception applied");
+
+ await BrowserTestUtils.closeWindow(win);
+
+ Assert.equal(transport.sentItems.length, 0, "itip subsystem did not attempt to send a response");
+ Assert.equal(transport.sentMsgs.length, 0, "no call was made into the mail subsystem");
+
+ info("Verifying relevant properties of the exception");
+ compareProperties(exception, {
+ id: "02e79b96",
+ "recurrenceId.icalString": "20220317T110000Z",
+ title: "Exception title",
+ "calendar.name": calendar.name,
+ "startDate.icalString": "20220317T110000Z",
+ "endDate.icalString": "20220317T113000Z",
+ description: "Exception description",
+ location: "Exception location",
+ sequence: "0",
+ "x-moz-received-dtstamp": "20220318T191602Z",
+ "organizer.id": "mailto:sender@example.com",
+ status: "CONFIRMED",
+ });
+
+ compareProperties(exception.getAttendees(), {
+ "0.id": "mailto:sender@example.com",
+ "0.participationStatus": "ACCEPTED",
+ "1.id": "mailto:other@example.com",
+ "1.participationStatus": "NEEDS-ACTION",
+ "2.id": "mailto:receiver@example.com",
+ "2.participationStatus": partStat,
+ });
+
+ let occurrences = event.recurrenceInfo.getOccurrences(
+ cal.createDateTime("19700101"),
+ cal.createDateTime("30000101"),
+ Infinity
+ );
+ Assert.equal(occurrences.length, 3, "reccurring event still has 3 occurrences");
+
+ info("Verifying relevant properties of the other occurrences");
+
+ let startDates = ["20220316T110000Z", "20220317T110000Z", "20220318T110000Z"];
+ let endDates = ["20220316T113000Z", "20220317T113000Z", "20220318T113000Z"];
+ for (let [index, occurrence] of occurrences.entries()) {
+ if (occurrence.startDate.compare(recurrenceId) == 0) {
+ continue;
+ }
+ compareProperties(occurrence, {
+ ...originalProps,
+ "recurrenceId.icalString": startDates[index],
+ "startDate.icalString": startDates[index],
+ "endDate.icalString": endDates[index],
+ });
+
+ let attendees = occurrence.getAttendees();
+ compareProperties(attendees, {
+ "0.id": "mailto:sender@example.com",
+ "0.participationStatus": "ACCEPTED",
+ "1.id": "mailto:receiver@example.com",
+ "1.participationStatus": partStat,
+ "2.id": "mailto:other@example.com",
+ "2.participationStatus": "NEEDS-ACTION",
+ });
+ }
+
+ await calendar.deleteItem(event);
+}
+
+/**
+ * Tests the recognition and application of a major update exception to an
+ * existing recurring event.
+ *
+ * @param {ImipBarActionTestConf} conf
+ */
+async function doMajorExceptionTest(conf) {
+ let { transport, identity, calendar, partStat, noReply } = conf;
+ let recurrenceId = cal.createDateTime("20220317T110000Z");
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem;
+ let originalProps = {
+ id: "02e79b96",
+ "recurrenceId.icalString": "20220317T110000Z",
+ title: event.title,
+ "calendar.name": calendar.name,
+ "startDate.icalString": event.startDate.icalString,
+ "endDate.icalString": event.endDate.icalString,
+ description: event.getProperty("DESCRIPTION"),
+ location: event.getProperty("LOCATION"),
+ sequence: "0",
+ "x-moz-received-dtstamp": event.getProperty("x-moz-received-dtstamp"),
+ "organizer.id": "mailto:sender@example.com",
+ status: "CONFIRMED",
+ };
+ let originalPartStat = event
+ .getAttendees()
+ .find(att => att.id == "mailto:receiver@example.com").participationStatus;
+
+ Assert.ok(
+ !event.recurrenceInfo.getExceptionFor(recurrenceId),
+ `no exception exists for ${recurrenceId}`
+ );
+
+ transport.reset();
+
+ let win = await openImipMessage(new FileUtils.File(getTestFilePath("data/exception-major.eml")));
+ if (noReply) {
+ let { button, noReply } = actionIds.single;
+ await clickMenuAction(win, button[partStat], noReply[partStat]);
+ } else {
+ await clickAction(win, actionIds.single.button[partStat]);
+ }
+
+ let exception;
+ await TestUtils.waitForCondition(async () => {
+ event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem;
+ exception = event.recurrenceInfo.getExceptionFor(recurrenceId);
+ return exception;
+ }, "event exception applied");
+
+ await BrowserTestUtils.closeWindow(win);
+
+ if (noReply) {
+ Assert.equal(
+ transport.sentItems.length,
+ 0,
+ "itip subsystem did not attempt to send a response"
+ );
+ Assert.equal(transport.sentMsgs.length, 0, "no call was made into the mail subsystem");
+ } else {
+ await doReplyTest(transport, identity, partStat);
+ }
+
+ info("Verifying relevant properties of the exception");
+
+ compareProperties(exception, {
+ ...originalProps,
+ "startDate.icalString": "20220317T050000Z",
+ "endDate.icalString": "20220317T053000Z",
+ sequence: "2",
+ });
+
+ compareProperties(exception.getAttendees(), {
+ "0.id": "mailto:sender@example.com",
+ "0.participationStatus": "ACCEPTED",
+ "1.id": "mailto:other@example.com",
+ "1.participationStatus": "NEEDS-ACTION",
+ "2.id": "mailto:receiver@example.com",
+ "2.participationStatus": partStat,
+ });
+
+ let occurrences = event.recurrenceInfo.getOccurrences(
+ cal.createDateTime("19700101"),
+ cal.createDateTime("30000101"),
+ Infinity
+ );
+ Assert.equal(occurrences.length, 3, "reccurring event still has 3 occurrences");
+
+ info("Verifying relevant properties of the other occurrences");
+
+ let startDates = ["20220316T110000Z", "20220317T110000Z", "20220318T110000Z"];
+ let endDates = ["20220316T113000Z", "20220317T113000Z", "20220318T113000Z"];
+ for (let [index, occurrence] of occurrences.entries()) {
+ if (occurrence.startDate.icalString == "20220317T050000Z") {
+ continue;
+ }
+ compareProperties(occurrence, {
+ ...originalProps,
+ "recurrenceId.icalString": startDates[index],
+ "startDate.icalString": startDates[index],
+ "endDate.icalString": endDates[index],
+ });
+
+ let attendees = occurrence.getAttendees();
+ compareProperties(attendees, {
+ "0.id": "mailto:sender@example.com",
+ "0.participationStatus": "ACCEPTED",
+ "1.id": "mailto:receiver@example.com",
+ "1.participationStatus": originalPartStat,
+ "2.id": "mailto:other@example.com",
+ "2.participationStatus": "NEEDS-ACTION",
+ });
+ }
+
+ await calendar.deleteItem(event);
+}
+
+/**
+ * Test the properties of an event created from a minor or major exception where
+ * we have not added the original event and optionally, the attempt to send a
+ * reply.
+ *
+ * @param {ImipBarActionTestConf} conf
+ */
+async function doExceptionOnlyTest(conf) {
+ let { calendar, transport, identity, partStat, noReply, isMajor } = conf;
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 5, 1)).item;
+
+ // Exceptions are still created as recurring events.
+ Assert.ok(event != event.parentItem, "event created is a recurring event");
+ let occurrences = event.parentItem.recurrenceInfo.getOccurrences(
+ cal.createDateTime("10000101"),
+ cal.createDateTime("30000101"),
+ Infinity
+ );
+ Assert.equal(occurrences.length, 1, "parent item only has one occurrence");
+ Assert.ok(occurrences[0] == event, "occurrence is the event exception");
+
+ info("Verifying relevant properties of the event");
+ compareProperties(event, {
+ id: "02e79b96",
+ title: isMajor ? event.title : "Exception title",
+ "calendar.name": calendar.name,
+ "recurrenceId.icalString": "20220317T110000Z",
+ "startDate.icalString": isMajor ? "20220317T050000Z" : "20220317T110000Z",
+ "endDate.icalString": isMajor ? "20220317T053000Z" : "20220317T113000Z",
+ description: isMajor ? event.getProperty("DESCRIPTION") : "Exception description",
+ location: isMajor ? event.getProperty("LOCATION") : "Exception location",
+ sequence: isMajor ? "2" : "0",
+ "x-moz-received-dtstamp": isMajor
+ ? event.getProperty("x-moz-received-dtstamp")
+ : "20220318T191602Z",
+ "organizer.id": "mailto:sender@example.com",
+ status: "CONFIRMED",
+ });
+
+ // Alarms should be ignored.
+ Assert.equal(event.getAlarms().length, 0, "event has no reminders");
+
+ info("Verifying attendee list and participation status");
+ let attendees = event.getAttendees();
+ compareProperties(attendees, {
+ "0.id": "mailto:sender@example.com",
+ "0.participationStatus": "ACCEPTED",
+ "1.participationStatus": partStat,
+ "1.id": "mailto:receiver@example.com",
+ "2.id": "mailto:other@example.com",
+ "2.participationStatus": "NEEDS-ACTION",
+ });
+
+ if (noReply) {
+ Assert.equal(
+ transport.sentItems.length,
+ 0,
+ "itip subsystem did not attempt to send a response"
+ );
+ Assert.equal(transport.sentMsgs.length, 0, "no call was made into the mail subsystem");
+ } else {
+ await doReplyTest(transport, identity, partStat);
+ }
+ await calendar.deleteItem(event.parentItem);
+}
+
+/**
+ * Tests the recognition and application of a cancellation to an existing event.
+ *
+ * @param {ImipBarActionTestConf} conf
+ */
+async function doCancelTest({ transport, calendar, isRecurring, event, recurrenceId }) {
+ transport.reset();
+
+ let eventId = event.id;
+ if (isRecurring) {
+ // wait for the other occurrences to appear.
+ await CalendarTestUtils.monthView.waitForItemAt(window, 3, 5, 1);
+ await CalendarTestUtils.monthView.waitForItemAt(window, 3, 6, 1);
+ }
+
+ let cancellationPath = isRecurring
+ ? "data/cancel-repeat-event.eml"
+ : "data/cancel-single-event.eml";
+
+ let cancelMsgFile = new FileUtils.File(getTestFilePath(cancellationPath));
+ if (recurrenceId) {
+ let srcTxt = await IOUtils.readUTF8(cancelMsgFile.path);
+ srcTxt = srcTxt.replaceAll(/RRULE:.+/g, `RECURRENCE-ID:${recurrenceId}`);
+ srcTxt = srcTxt.replaceAll(/SEQUENCE:.+/g, "SEQUENCE:3");
+ cancelMsgFile = FileTestUtils.getTempFile("cancel-occurrence.eml");
+ await IOUtils.writeUTF8(cancelMsgFile.path, srcTxt);
+ }
+
+ let win = await openImipMessage(cancelMsgFile);
+ let aboutMessage = win.document.getElementById("messageBrowser").contentWindow;
+ let deleteButton = aboutMessage.document.getElementById("imipDeleteButton");
+ Assert.ok(!deleteButton.hidden, `#${deleteButton.id} button shown`);
+ EventUtils.synthesizeMouseAtCenter(deleteButton, {}, aboutMessage);
+
+ if (isRecurring && recurrenceId) {
+ // Expects a single occurrence to be cancelled.
+
+ let occurrences;
+ await TestUtils.waitForCondition(async () => {
+ let { parentItem } = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item;
+ occurrences = parentItem.recurrenceInfo.getOccurrences(
+ cal.createDateTime("19700101"),
+ cal.createDateTime("30000101"),
+ Infinity
+ );
+ return occurrences.length == 2;
+ }, "occurrence was deleted");
+
+ Assert.ok(
+ occurrences.every(occ => occ.recurrenceId && occ.recurrenceId != recurrenceId),
+ `occurrence "${recurrenceId}" removed`
+ );
+ Assert.ok(!!(await calendar.getItem(eventId)), "event was not deleted");
+ } else {
+ await CalendarTestUtils.monthView.waitForNoItemAt(window, 3, 4, 1);
+
+ if (isRecurring) {
+ await CalendarTestUtils.monthView.waitForNoItemAt(window, 3, 5, 1);
+ await CalendarTestUtils.monthView.waitForNoItemAt(window, 3, 6, 1);
+ }
+
+ await TestUtils.waitForCondition(async () => {
+ let result = await calendar.getItem(eventId);
+ return !result;
+ }, "event was deleted");
+ }
+
+ await BrowserTestUtils.closeWindow(win);
+ Assert.equal(transport.sentItems.length, 0, "itip subsystem did not attempt to send a response");
+ Assert.equal(transport.sentMsgs.length, 0, "no call was made into the mail subsystem");
+}
+
+/**
+ * Tests processing of cancellations to exceptions to recurring events.
+ *
+ * @param {ImipBarActionTestConf} conf
+ */
+async function doCancelExceptionTest(conf) {
+ let { partStat, recurrenceId, calendar } = conf;
+ let invite = new FileUtils.File(getTestFilePath("data/repeat-event.eml"));
+ let win = await openImipMessage(invite);
+ await clickAction(win, actionIds.recurring.button[partStat]);
+
+ let event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem;
+ await BrowserTestUtils.closeWindow(win);
+
+ let update = new FileUtils.File(getTestFilePath("data/exception-major.eml"));
+ let updateWin = await openImipMessage(update);
+ await clickAction(updateWin, actionIds.single.button[partStat]);
+
+ let exception;
+ await TestUtils.waitForCondition(async () => {
+ event = (await CalendarTestUtils.monthView.waitForItemAt(window, 3, 4, 1)).item.parentItem;
+ exception = event.recurrenceInfo.getExceptionFor(cal.createDateTime(recurrenceId));
+ return !!exception;
+ }, "exception applied");
+
+ await BrowserTestUtils.closeWindow(updateWin);
+ await doCancelTest({ ...conf, event });
+ await calendar.deleteItem(event);
+}