Bug 1439868 - Move email/scheduling related functions into calEmailUtils.jsm and calItipUtils.jsm - manual changes. r=MakeMyDay
authorPhilipp Kewisch <mozilla@kewis.ch>
Wed, 21 Feb 2018 07:33:43 +0100
changeset 30537 6324de6a59c5eb3155902084341513013dce9ca0
parent 30534 4a08421a80cefbdfac98be07709687f69757c18e
child 30538 c496bccd43b0c2b33e64a8cde2323ccb2a9fbc89
push id2172
push usermozilla@kewis.ch
push dateSun, 15 Apr 2018 05:33:14 +0000
treeherdercomm-beta@33a67b0129b3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMakeMyDay
bugs1439868
Bug 1439868 - Move email/scheduling related functions into calEmailUtils.jsm and calItipUtils.jsm - manual changes. r=MakeMyDay MozReview-Commit-ID: 63EMQM4PT2V
calendar/.eslintrc.js
calendar/base/content/calendar-invitations-manager.js
calendar/base/content/dialogs/calendar-summary-dialog.js
calendar/base/modules/calEmailUtils.jsm
calendar/base/modules/calItipUtils.jsm
calendar/base/modules/calUtils.jsm
calendar/base/modules/calUtilsCompat.jsm
calendar/base/modules/moz.build
calendar/base/src/calTransactionManager.js
calendar/base/src/calUtils.js
calendar/lightning/content/imip-bar.js
calendar/providers/gdata/modules/calUtilsShim.jsm
calendar/test/unit/test_calitiputils.js
--- a/calendar/.eslintrc.js
+++ b/calendar/.eslintrc.js
@@ -475,10 +475,34 @@ module.exports = {
         "no-case-declarations": 2,
 
         // Enforce consistent indentation (4-space)
         "indent-legacy": [2, 4, { SwitchCase: 1, }],
 
         // The following rules will not be enabled currently, but are kept here for
         // easier updates in the future.
         "no-else-return": 0,
-    }
+    },
+    "overrides": [{
+        files: [
+            "base/modules/calEmailUtils.jsm",
+            "base/modules/calItipUtils.jsm",
+        ],
+        rules: {
+            "require-jsdoc": [2, { require: { ClassDeclaration: true } }],
+
+            "valid-jsdoc": [2, {
+                prefer: { returns: "return" },
+                preferType: {
+                    "boolean": "Boolean",
+                    "string": "String",
+                    "number": "Number",
+                    "object": "Object",
+                    "function": "Function",
+                    "map": "Map",
+                    "set": "Set",
+                    "date": "Date",
+                },
+                requireReturn: false
+            }],
+        }
+    }]
 };
--- a/calendar/base/content/calendar-invitations-manager.js
+++ b/calendar/base/content/calendar-invitations-manager.js
@@ -1,14 +1,13 @@
 /* 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/. */
 
 ChromeUtils.import("resource://calendar/modules/calUtils.jsm");
-ChromeUtils.import("resource://calendar/modules/calItipUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Preferences.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 /* exported getInvitationsManager */
 
 /**
  * This object contains functions to take care of manipulating requests.
  */
--- a/calendar/base/content/dialogs/calendar-summary-dialog.js
+++ b/calendar/base/content/dialogs/calendar-summary-dialog.js
@@ -2,17 +2,16 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* exported onLoad, onUnload, onAccept, onCancel, updatePartStat, browseDocument,
  *          sendMailToOrganizer, openAttachment, reply
  */
 
 ChromeUtils.import("resource://calendar/modules/calUtils.jsm");
-ChromeUtils.import("resource://calendar/modules/calItipUtils.jsm");
 ChromeUtils.import("resource://calendar/modules/calAlarmUtils.jsm");
 ChromeUtils.import("resource://calendar/modules/calRecurrenceUtils.jsm");
 
 /**
  * Sets up the summary dialog, setting all needed fields on the dialog from the
  * item received in the window arguments.
  */
 function onLoad() {
new file mode 100644
--- /dev/null
+++ b/calendar/base/modules/calEmailUtils.jsm
@@ -0,0 +1,211 @@
+/* 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/. */
+
+ChromeUtils.import("resource:///modules/mailServices.js");
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "cal", "resource://calendar/modules/calUtils.jsm", "cal");
+
+this.EXPORTED_SYMBOLS = ["calemail"]; /* exported calemail */
+
+var calemail = {
+    /**
+     * Convenience function to open the compose window pre-filled with the information from the
+     * parameters. These parameters are mostly raw header fields, see #createRecipientList function
+     * to create a recipient list string.
+     *
+     * @param {String} aRecipient       The email recipients string.
+     * @param {String} aSubject         The email subject.
+     * @param {String} aBody            The encoded email body text.
+     * @param {nsIMsgIdentity} aIdentity    The email identity to use for sending
+     */
+    sendTo: function(aRecipient, aSubject, aBody, aIdentity) {
+        let msgParams = Components.classes["@mozilla.org/messengercompose/composeparams;1"]
+                                  .createInstance(Components.interfaces.nsIMsgComposeParams);
+        let composeFields = Components.classes["@mozilla.org/messengercompose/composefields;1"]
+                                      .createInstance(Components.interfaces.nsIMsgCompFields);
+
+        composeFields.to = aRecipient;
+        composeFields.subject = aSubject;
+        composeFields.body = aBody;
+
+        msgParams.type = Components.interfaces.nsIMsgCompType.New;
+        msgParams.format = Components.interfaces.nsIMsgCompFormat.Default;
+        msgParams.composeFields = composeFields;
+        msgParams.identity = aIdentity;
+
+        MailServices.compose.OpenComposeWindowWithParams(null, msgParams);
+    },
+
+    /**
+     * Iterates all email identities and calls the passed function with identity and account.
+     * If the called function returns false, iteration is stopped.
+     *
+     * @param {Function} aFunc       The function to be called for each identity and account
+     */
+    iterateIdentities: function(aFunc) {
+        let accounts = MailServices.accounts.accounts;
+        for (let i = 0; i < accounts.length; ++i) {
+            let account = accounts.queryElementAt(i, Components.interfaces.nsIMsgAccount);
+            let identities = account.identities;
+            for (let j = 0; j < identities.length; ++j) {
+                let identity = identities.queryElementAt(j, Components.interfaces.nsIMsgIdentity);
+                if (!aFunc(identity, account)) {
+                    break;
+                }
+            }
+        }
+    },
+
+    /**
+     * Prepends a mailto: prefix to an email address like string
+     *
+     * @param  {String} aId     The string to prepend the prefix if not already there
+     * @return {String}         The string with prefix
+     */
+    prependMailTo: function(aId) {
+        return aId.replace(/^(?:mailto:)?(.*)@/i, "mailto:$1@");
+    },
+
+    /**
+     * Removes an existing mailto: prefix from an attendee id
+     *
+     * @param  {String} aId     The string to remove the prefix from if any
+     * @return {String}         The string without prefix
+     */
+    removeMailTo: function(aId) {
+        return aId.replace(/^mailto:/i, "");
+    },
+
+    /**
+     * Provides a string to use in email "to" header for given attendees
+     *
+     * @param  {calIAttendee[]} aAttendees          Array of calIAttendee's to check
+     * @return {String}                             Valid string to use in a 'to' header of an email
+     */
+    createRecipientList: function(aAttendees) {
+        let cbEmail = function(aVal) {
+            let email = calemail.getAttendeeEmail(aVal, true);
+            if (!email.length) {
+                cal.LOG("Dropping invalid recipient for email transport: " + aVal.toString());
+            }
+            return email;
+        };
+        return aAttendees.map(cbEmail)
+                         .filter(aVal => aVal.length > 0)
+                         .join(", ");
+    },
+
+    /**
+     * Returns a wellformed email string like 'attendee@example.net',
+     * 'Common Name <attendee@example.net>' or '"Name, Common" <attendee@example.net>'
+     *
+     * @param  {calIAttendee} aAttendee     The attendee to check
+     * @param  {Boolean} aIncludeCn         Whether or not to return also the CN if available
+     * @return {String}                     Valid email string or an empty string in case of error
+     */
+    getAttendeeEmail: function(aAttendee, aIncludeCn) {
+        // If the recipient id is of type urn, we need to figure out the email address, otherwise
+        // we fall back to the attendee id
+        let email = aAttendee.id.match(/^urn:/i) ? aAttendee.getProperty("EMAIL") || "" : aAttendee.id;
+        // Strip leading "mailto:" if it exists.
+        email = email.replace(/^mailto:/i, "");
+        // We add the CN if requested and available
+        let commonName = aAttendee.commonName;
+        if (aIncludeCn && email.length > 0 && commonName && commonName.length > 0) {
+            if (commonName.match(/[,;]/)) {
+                commonName = '"' + commonName + '"';
+            }
+            commonName = commonName + " <" + email + ">";
+            if (calemail.validateRecipientList(commonName) == commonName) {
+                email = commonName;
+            }
+        }
+        return email;
+    },
+
+    /**
+     * Returns a basically checked recipient list - malformed elements will be removed
+     *
+     * @param {String} aRecipients      A comma-seperated list of e-mail addresses
+     * @return {String}                 A validated comma-seperated list of e-mail addresses
+     */
+    validateRecipientList: function(aRecipients) {
+        let compFields = Components.classes["@mozilla.org/messengercompose/composefields;1"]
+                                   .createInstance(Components.interfaces.nsIMsgCompFields);
+        // Resolve the list considering also configured common names
+        let members = compFields.splitRecipients(aRecipients, false, {});
+        let list = [];
+        let prefix = "";
+        for (let member of members) {
+            if (prefix != "") {
+                // the previous member had no email address - this happens if a recipients CN
+                // contains a ',' or ';' (splitRecipients(..) behaves wrongly here and produces an
+                // additional member with only the first CN part of that recipient and no email
+                // address while the next has the second part of the CN and the according email
+                // address) - we still need to identify the original delimiter to append it to the
+                // prefix
+                let memberCnPart = member.match(/(.*) <.*>/);
+                if (memberCnPart) {
+                    let pattern = new RegExp(prefix + "([;,] *)" + memberCnPart[1]);
+                    let delimiter = aRecipients.match(pattern);
+                    if (delimiter) {
+                        prefix = prefix + delimiter[1];
+                    }
+                }
+            }
+            let parts = (prefix + member).match(/(.*)( <.*>)/);
+            if (parts) {
+                if (parts[2] == " <>") {
+                    // CN but no email address - we keep the CN part to prefix the next member's CN
+                    prefix = parts[1];
+                } else {
+                    // CN with email address
+                    let commonName = parts[1].trim();
+                    // in case of any special characters in the CN string, we make sure to enclose
+                    // it with dquotes - simple spaces don't require dquotes
+                    if (commonName.match(/[-[\]{}()*+?.,;\\^$|#\f\n\r\t\v]/)) {
+                        commonName = '"' + commonName.replace(/\\"|"/, "").trim() + '"';
+                    }
+                    list.push(commonName + parts[2]);
+                    prefix = "";
+                }
+            } else if (member.length) {
+                // email address only
+                list.push(member);
+                prefix = "";
+            }
+        }
+        return list.join(", ");
+    },
+
+    /**
+     * Check if the attendee object matches one of the addresses in the list. This
+     * is useful to determine whether the current user acts as a delegate.
+     *
+     * @param {calIAttendee} aRefAttendee   The reference attendee object
+     * @param {String[]} aAddresses         The list of addresses
+     * @return {Boolean}                    True, if there is a match
+     */
+    attendeeMatchesAddresses: function(aRefAttendee, aAddresses) {
+        let attId = aRefAttendee.id;
+        if (!attId.match(/^mailto:/i)) {
+            // Looks like its not a normal attendee, possibly urn:uuid:...
+            // Try getting the email through the EMAIL property.
+            let emailProp = aRefAttendee.getProperty("EMAIL");
+            if (emailProp) {
+                attId = emailProp;
+            }
+        }
+
+        attId = attId.toLowerCase().replace(/^mailto:/, "");
+        for (let address of aAddresses) {
+            if (attId == address.toLowerCase().replace(/^mailto:/, "")) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+};
--- a/calendar/base/modules/calItipUtils.jsm
+++ b/calendar/base/modules/calItipUtils.jsm
@@ -7,166 +7,174 @@ ChromeUtils.import("resource://calendar/
 ChromeUtils.import("resource://calendar/modules/calAlarmUtils.jsm");
 ChromeUtils.import("resource://calendar/modules/calIteratorUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Preferences.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 /**
  * Scheduling and iTIP helper code
  */
-this.EXPORTED_SYMBOLS = ["cal"]; // even though it's defined in calUtils.jsm, import needs this
-cal.itip = {
+this.EXPORTED_SYMBOLS = ["calitip"]; /* exported calitip */
+var calitip = {
     /**
-     * Gets the sequence/revision number, either of the passed item or
-     * the last received one of an attendee; see
-     * <http://tools.ietf.org/html/draft-desruisseaux-caldav-sched-04#section-7.1>.
+     * Gets the sequence/revision number, either of the passed item or the last received one of an
+     * attendee; see <http://tools.ietf.org/html/draft-desruisseaux-caldav-sched-04#section-7.1>.
+     *
+     * @param {calIAttendee|calIItemBase} aItem     The item or attendee to get the sequence info
+     *                                                from.
+     * @return {Number}                             The sequence number
      */
-    getSequence: function(item) {
+    getSequence: function(aItem) {
         let seq = null;
 
-        let wrappedItem = cal.wrapInstance(item, Components.interfaces.calIAttendee);
+        let wrappedItem = cal.wrapInstance(aItem, Components.interfaces.calIAttendee);
         if (wrappedItem) {
             seq = wrappedItem.getProperty("RECEIVED-SEQUENCE");
-        } else if (item) {
+        } else if (aItem) {
             // Unless the below is standardized, we store the last original
             // REQUEST/PUBLISH SEQUENCE in X-MOZ-RECEIVED-SEQUENCE to test against it
             // when updates come in:
-            seq = item.getProperty("X-MOZ-RECEIVED-SEQUENCE");
+            seq = aItem.getProperty("X-MOZ-RECEIVED-SEQUENCE");
             if (seq === null) {
-                seq = item.getProperty("SEQUENCE");
+                seq = aItem.getProperty("SEQUENCE");
             }
 
             // Make sure we don't have a pre Outlook 2007 appointment, but if we do
             // use Microsoft's Sequence number. I <3 MS
             if ((seq === null) || (seq == "0")) {
-                seq = item.getProperty("X-MICROSOFT-CDO-APPT-SEQUENCE");
+                seq = aItem.getProperty("X-MICROSOFT-CDO-APPT-SEQUENCE");
             }
         }
 
         if (seq === null) {
             return 0;
         } else {
             seq = parseInt(seq, 10);
             return (isNaN(seq) ? 0 : seq);
         }
     },
 
     /**
-     * Gets the stamp date-time, either of the passed item or
-     * the last received one of an attendee; see
-     * <http://tools.ietf.org/html/draft-desruisseaux-caldav-sched-04#section-7.2>.
+     * Gets the stamp date-time, either of the passed item or the last received one of an attendee;
+     * see <http://tools.ietf.org/html/draft-desruisseaux-caldav-sched-04#section-7.2>.
+     *
+     * @param {calIAttendee|calIItemBase} aItem     The item or attendee to retrieve the stamp from
+     * @return {calIDateTime}                       The timestamp for the item
      */
-    getStamp: function(item) {
+    getStamp: function(aItem) {
         let dtstamp = null;
 
-        let wrappedItem = cal.wrapInstance(item, Components.interfaces.calIAttendee);
+        let wrappedItem = cal.wrapInstance(aItem, Components.interfaces.calIAttendee);
         if (wrappedItem) {
             let stamp = wrappedItem.getProperty("RECEIVED-DTSTAMP");
             if (stamp) {
                 dtstamp = cal.createDateTime(stamp);
             }
-        } else if (item) {
+        } else if (aItem) {
             // Unless the below is standardized, we store the last original
             // REQUEST/PUBLISH DTSTAMP in X-MOZ-RECEIVED-DTSTAMP to test against it
             // when updates come in:
-            let stamp = item.getProperty("X-MOZ-RECEIVED-DTSTAMP");
+            let stamp = aItem.getProperty("X-MOZ-RECEIVED-DTSTAMP");
             if (stamp) {
                 dtstamp = cal.createDateTime(stamp);
             } else {
                 // xxx todo: are there similar X-MICROSOFT-CDO properties to be considered here?
-                dtstamp = item.stampTime;
+                dtstamp = aItem.stampTime;
             }
         }
 
         return dtstamp;
     },
 
     /**
      * Compares sequences and/or stamps of two items
      *
-     * @param {calIEvent|calIToDo|calIAttendee} aItem1
-     * @param {calIEvent|calIToDo|calIAttendee} aItem2
-     * @return {Integer} +1 if item2 is newer, -1 if item1 is newer or 0 if both are equal
+     * @param {calIItemBase|calIAttendee} aItem1        The first item to compare
+     * @param {calIItemBase|calIAttendee} aItem2        The second item to compare
+     * @return {Number}                                 +1 if item2 is newer, -1 if item1 is newer
+     *                                                    or 0 if both are equal
      */
     compare: function(aItem1, aItem2) {
-        let comp = cal.itip.compareSequence(aItem1, aItem2);
+        let comp = calitip.compareSequence(aItem1, aItem2);
         if (comp == 0) {
-            comp = cal.itip.compareStamp(aItem1, aItem2);
+            comp = calitip.compareStamp(aItem1, aItem2);
         }
         return comp;
     },
 
     /**
      * Compares sequences of two items
      *
-     * @param {calIEvent|calIToDo|calIAttendee} aItem1
-     * @param {calIEvent|calIToDo|calIAttendee} aItem2
-     * @return {Integer} +1 if item2 is newer, -1 if item1 is newer or 0 if both are equal
+     * @param {calIItemBase|calIAttendee} aItem1        The first item to compare
+     * @param {calIItemBase|calIAttendee} aItem2        The second item to compare
+     * @return {Number}                                 +1 if item2 is newer, -1 if item1 is newer
+     *                                                    or 0 if both are equal
      */
     compareSequence: function(aItem1, aItem2) {
-        let seq1 = cal.itip.getSequence(aItem1);
-        let seq2 = cal.itip.getSequence(aItem2);
+        let seq1 = calitip.getSequence(aItem1);
+        let seq2 = calitip.getSequence(aItem2);
         if (seq1 > seq2) {
             return 1;
         } else if (seq1 < seq2) {
             return -1;
         } else {
             return 0;
         }
     },
 
     /**
      * Compares stamp of two items
      *
-     * @param {calIEvent|calIToDo|calIAttendee} aItem1
-     * @param {calIEvent|calIToDo|calIAttendee} aItem2
-     * @return {Integer} +1 if item2 is newer, -1 if item1 is newer or 0 if both are equal
+     * @param {calIItemBase|calIAttendee} aItem1        The first item to compare
+     * @param {calIItemBase|calIAttendee} aItem2        The second item to compare
+     * @return {Number}                                 +1 if item2 is newer, -1 if item1 is newer
+     *                                                    or 0 if both are equal
      */
     compareStamp: function(aItem1, aItem2) {
-        let st1 = cal.itip.getStamp(aItem1);
-        let st2 = cal.itip.getStamp(aItem2);
+        let st1 = calitip.getStamp(aItem1);
+        let st2 = calitip.getStamp(aItem2);
         if (st1 && st2) {
             return st1.compare(st2);
         } else if (!st1 && st2) {
             return -1;
         } else if (st1 && !st2) {
             return 1;
         } else {
             return 0;
         }
     },
 
     /**
      * Checks if the given calendar is a scheduling calendar. This means it
      * needs an organizer id and an itip transport. It should also be writable.
      *
-     * @param calendar    The calendar to check
-     * @return            True, if its a scheduling calendar.
+     * @param {calICalendar} aCalendar      The calendar to check
+     * @return {Boolean}                    True, if its a scheduling calendar.
      */
-    isSchedulingCalendar: function(calendar) {
-        return cal.acl.isCalendarWritable(calendar) &&
-               calendar.getProperty("organizerId") &&
-               calendar.getProperty("itip.transport");
+    isSchedulingCalendar: function(aCalendar) {
+        return cal.acl.isCalendarWritable(aCalendar) &&
+               aCalendar.getProperty("organizerId") &&
+               aCalendar.getProperty("itip.transport");
     },
 
     /**
      * Scope: iTIP message receiver
      *
      * Given an nsIMsgDBHdr and an imipMethod, set up the given itip item.
      *
-     * @param itipItem    The item to set up
-     * @param imipMethod  The received imip method
-     * @param aMsgHdr     Information about the received email
+     * @param {calIItemBase} itipItem   The item to set up
+     * @param {String} imipMethod       The received imip method
+     * @param {nsIMsgDBHdr} aMsgHdr     Information about the received email
      */
     initItemFromMsgData: function(itipItem, imipMethod, aMsgHdr) {
         // set the sender of the itip message
-        itipItem.sender = cal.itip.getMessageSender(aMsgHdr);
+        itipItem.sender = calitip.getMessageSender(aMsgHdr);
 
         // Get the recipient identity and save it with the itip item.
-        itipItem.identity = cal.itip.getMessageRecipient(aMsgHdr);
+        itipItem.identity = calitip.getMessageRecipient(aMsgHdr);
 
         // We are only called upon receipt of an invite, so ensure that isSend
         // is false.
         itipItem.isSend = false;
 
         // XXX Get these from preferences
         itipItem.autoResponse = Components.interfaces.calIItipItem.USER;
 
@@ -175,17 +183,17 @@ cal.itip = {
         } else { // There is no METHOD in the content-type header (spec violation).
                  // Fall back to using the one from the itipItem's ICS.
             imipMethod = itipItem.receivedMethod;
         }
         cal.LOG("iTIP method: " + imipMethod);
 
         let isWritableCalendar = function(aCalendar) {
             /* TODO: missing ACL check for existing items (require callback API) */
-            return cal.itip.isSchedulingCalendar(aCalendar) &&
+            return calitip.isSchedulingCalendar(aCalendar) &&
                    cal.acl.userCanAddItemsToCalendar(aCalendar);
         };
 
         let writableCalendars = cal.getCalendarManager().getCalendars({}).filter(isWritableCalendar);
         if (writableCalendars.length > 0) {
             let compCal = Components.classes["@mozilla.org/calendar/calendar;1?type=composite"]
                                     .createInstance(Components.interfaces.calICompositeCalendar);
             writableCalendars.forEach(compCal.addCalendar, compCal);
@@ -194,21 +202,27 @@ cal.itip = {
     },
 
     /**
      * Scope: iTIP message receiver
      *
      * Gets the suggested text to be shown when an imip item has been processed.
      * This text is ready localized and can be displayed to the user.
      *
-     * @param aStatus         The status of the processing (i.e NS_OK, an error code)
-     * @param aOperationType  An operation type from calIOperationListener
-     * @return                The suggested text.
+     * @param {Number} aStatus         The status of the processing (i.e NS_OK, an error code)
+     * @param {Number} aOperationType  An operation type from calIOperationListener
+     * @return {String}                The suggested text.
      */
     getCompleteText: function(aStatus, aOperationType) {
+        /**
+         * Gets the string from the lightning bundle
+         * @param {String} strName      The string identifier name
+         * @param {String} param        An array of parameters for the strgin
+         * @return {String}             The translated string
+         */
         function _gs(strName, param) {
             return cal.calGetString("lightning", strName, param, "lightning");
         }
 
         let text = "";
         const cIOL = Components.interfaces.calIOperationListener;
         if (Components.isSuccessCode(aStatus)) {
             switch (aOperationType) {
@@ -223,20 +237,26 @@ cal.itip = {
     },
 
     /**
      * Scope: iTIP message receiver
      *
      * Gets a text describing the given itip method. The text is of the form
      * "This Message contains a ... ".
      *
-     * @param method      The method to describe.
-     * @return            The localized text about the method.
+     * @param {String} method      The method to describe.
+     * @return {String}            The localized text about the method.
      */
     getMethodText: function(method) {
+        /**
+         * Gets the string from the lightning bundle
+         * @param {String} strName      The string identifier name
+         * @param {String} param        An array of parameters for the strgin
+         * @return {String}             The translated string
+         */
         function _gs(strName) {
             return cal.calGetString("lightning", strName, null, "lightning");
         }
 
         switch (method) {
             case "REFRESH": return _gs("imipBarRefreshText");
             case "REQUEST": return _gs("imipBarRequestText");
             case "PUBLISH": return _gs("imipBarPublishText");
@@ -258,27 +278,36 @@ cal.itip = {
      *
      * {
      *    label: "This is a desciptive text about the itip item",
      *    buttons: ["imipXXXButton", ...],
      *    hideMenuItem: ["imipXXXButton_Option", ...]
      * }
      *
      * @see processItipItem   This takes the same parameters as its optionFunc.
-     * @param itipItem        The itipItem to query.
-     * @param rc              The result of retrieving the item
-     * @param actionFunc      The action function.
+     * @param {calIItipItem} itipItem       The itipItem to query.
+     * @param {Number} rc                   The result of retrieving the item
+     * @param {Function} actionFunc         The action function.
+     * @param {calIItemBase[]} foundItems   An array of items found while searching for the item
+     *                                        in subscribed calendars
+     * @return {Object}                     Return information about the options
      */
     getOptionsText: function(itipItem, rc, actionFunc, foundItems) {
+        /**
+         * Gets the string from the lightning bundle
+         * @param {String} strName      The string identifier name
+         * @param {String} aParam       An array of parameters for the strgin
+         * @return {String}             The translated string
+         */
         function _gs(strName, aParam=null) {
             return cal.calGetString("lightning", strName, aParam, "lightning");
         }
         let imipLabel = null;
         if (itipItem.receivedMethod) {
-            imipLabel = cal.itip.getMethodText(itipItem.receivedMethod);
+            imipLabel = calitip.getMethodText(itipItem.receivedMethod);
         }
         let data = { label: imipLabel, showItems: [], hideItems: [] };
         let separateButtons = Preferences.get("calendar.itip.separateInvitationButtons", false);
 
         let disallowedCounter = false;
         if (foundItems && foundItems.length) {
             let disallow = foundItems[0].getProperty("X-MICROSOFT-DISALLOW-COUNTER");
             disallowedCounter = disallow && disallow == "TRUE";
@@ -298,17 +327,17 @@ cal.itip = {
                     } else {
                         let comparison;
                         for (let item of itipItem.getItemList({})) {
                             let attendees = cal.getAttendeesBySender(
                                     item.getAttendees({}),
                                     itipItem.sender
                             );
                             if (attendees.length == 1) {
-                                comparison = cal.itip.compareSequence(item, foundItems[0]);
+                                comparison = calitip.compareSequence(item, foundItems[0]);
                                 if (comparison == 1) {
                                     data.label = _gs("imipBarCounterErrorText");
                                     break;
                                 } else if (comparison == -1) {
                                     data.label = _gs("imipBarCounterPreviousVersionText");
                                 }
                             }
                         }
@@ -440,18 +469,18 @@ cal.itip = {
 
         return data;
     },
 
     /**
      * Scope: iTIP message receiver
      * Retrieves the message sender.
      *
-     * @param {nsIMsgHdr} aMsgHdr     The message header to check.
-     * @return                        The email address of the intended recipient.
+     * @param {nsIMsgDBHdr} aMsgHdr     The message header to check.
+     * @return {String}                 The email address of the intended recipient.
      */
     getMessageSender: function(aMsgHdr) {
         let author = (aMsgHdr && aMsgHdr.author) || "";
         let compFields = Components.classes["@mozilla.org/messengercompose/composefields;1"]
                                    .createInstance(Components.interfaces.nsIMsgCompFields);
         let addresses = compFields.splitRecipients(author, true, {});
         if (addresses.length != 1) {
             cal.LOG("No unique email address for lookup in message.\r\n" + cal.STACK(20));
@@ -459,18 +488,18 @@ cal.itip = {
         return addresses[0] || null;
     },
 
     /**
      * Scope: iTIP message receiver
      *
      * Retrieves the intended recipient for this message.
      *
-     * @param aMsgHdr     The message to check.
-     * @return            The email of the intended recipient.
+     * @param {nsIMsgDBHdr} aMsgHdr     The message to check.
+     * @return {String}                 The email of the intended recipient.
      */
     getMessageRecipient: function(aMsgHdr) {
         if (!aMsgHdr) {
             return null;
         }
 
         let identities;
         let actMgr = MailServices.accounts;
@@ -529,24 +558,23 @@ cal.itip = {
 
         // Hrmpf. Looks like delegation or maybe Bcc.
         return null;
     },
 
     /**
      * Scope: iTIP message receiver
      *
-     * Prompt for the target calendar, if needed for the given method. This
-     * calendar will be set on the passed itip item.
+     * Prompt for the target calendar, if needed for the given method. This calendar will be set on
+     * the passed itip item.
      *
-     * @param aMethod       The method to check.
-     * @param aItipItem     The itip item to set the target calendar on.
-     * @param aWindow       The window to open the dialog on.
-     * @return              True, if a calendar was selected or no selection is
-     *                        needed.
+     * @param {String} aMethod          The method to check.
+     * @param {calIItipItem} aItipItem  The itip item to set the target calendar on.
+     * @param {DOMWindpw} aWindow       The window to open the dialog on.
+     * @return {Boolean}                True, if a calendar was selected or no selection is needed.
      */
     promptCalendar: function(aMethod, aItipItem, aWindow) {
         let needsCalendar = false;
         let targetCalendar = null;
         switch (aMethod) {
             // methods that don't require the calendar chooser:
             case "REFRESH":
             case "REQUEST:UPDATE":
@@ -559,23 +587,23 @@ cal.itip = {
                 needsCalendar = false;
                 break;
             default:
                 needsCalendar = true;
                 break;
         }
 
         if (needsCalendar) {
-            let calendars = cal.getCalendarManager().getCalendars({}).filter(cal.itip.isSchedulingCalendar);
+            let calendars = cal.getCalendarManager().getCalendars({}).filter(calitip.isSchedulingCalendar);
 
             if (aItipItem.receivedMethod == "REQUEST") {
                 // try to further limit down the list to those calendars that
                 // are configured to a matching attendee;
                 let item = aItipItem.getItemList({})[0];
-                let matchingCals = calendars.filter(calendar => cal.getInvitedAttendee(item, calendar) != null);
+                let matchingCals = calendars.filter(calendar => calitip.getInvitedAttendee(item, calendar) != null);
                 // if there's none, we will show the whole list of calendars:
                 if (matchingCals.length > 0) {
                     calendars = matchingCals;
                 }
             }
 
             if (calendars.length == 0) {
                 let msg = cal.calGetString("lightning", "imipNoCalendarAvailable", null, "lightning");
@@ -598,51 +626,53 @@ cal.itip = {
                 aItipItem.targetCalendar = targetCalendar;
             }
         }
 
         return !needsCalendar || targetCalendar != null;
     },
 
     /**
-     * Clean up after the given iTIP item. This needs to be called once for each
-     * time processItipItem is called. May be called with a null itipItem in
-     * which case it will do nothing.
+     * Clean up after the given iTIP item. This needs to be called once for each time
+     * processItipItem is called. May be called with a null itipItem in which case it will do
+     * nothing.
      *
-     * @param itipItem      The iTIP item to clean up for.
+     * @param {calIItipItem} itipItem      The iTIP item to clean up for.
      */
     cleanupItipItem: function(itipItem) {
         if (itipItem) {
             let itemList = itipItem.getItemList({});
             if (itemList.length > 0) {
                 // Again, we can assume the id is the same over all items per spec
                 ItipItemFinderFactory.cleanup(itemList[0].id);
             }
         }
     },
 
     /**
      * Scope: iTIP message receiver
      *
-     * Checks the passed iTIP item and calls the passed function with options offered.
-     * Be sure to call cleanupItipItem at least once after calling this function.
+     * Checks the passed iTIP item and calls the passed function with options offered. Be sure to
+     * call cleanupItipItem at least once after calling this function.
      *
-     * @param itipItem iTIP item
-     * @param optionsFunc function being called with parameters: itipItem, resultCode, actionFunc
-     *                    The action func has a property |method| showing the options:
-     *                    * REFRESH -- send the latest item (sent by attendee(s))
-     *                    * PUBLISH -- initial publish, no reply (sent by organizer)
-     *                    * PUBLISH:UPDATE -- update of a published item (sent by organizer)
-     *                    * REQUEST -- initial invitation (sent by organizer)
-     *                    * REQUEST:UPDATE -- rescheduling invitation, has major change (sent by organizer)
-     *                    * REQUEST:UPDATE-MINOR -- update of invitation, minor change (sent by organizer)
-     *                    * REPLY -- invitation reply (sent by attendee(s))
-     *                    * CANCEL -- invitation cancel (sent by organizer)
-     *                    * COUNTER -- counterproposal (sent by attendee)
-     *                    * DECLINECOUNTER -- denial of a counterproposal (sent by organizer)
+     * The action func has a property |method| showing the options:
+     *   * REFRESH -- send the latest item (sent by attendee(s))
+     *   * PUBLISH -- initial publish, no reply (sent by organizer)
+     *   * PUBLISH:UPDATE -- update of a published item (sent by organizer)
+     *   * REQUEST -- initial invitation (sent by organizer)
+     *   * REQUEST:UPDATE -- rescheduling invitation, has major change (sent by organizer)
+     *   * REQUEST:UPDATE-MINOR -- update of invitation, minor change (sent by organizer)
+     *   * REPLY -- invitation reply (sent by attendee(s))
+     *   * CANCEL -- invitation cancel (sent by organizer)
+     *   * COUNTER -- counterproposal (sent by attendee)
+     *   * DECLINECOUNTER -- denial of a counterproposal (sent by organizer)
+     *
+     * @param {calIItipItem} itipItem       The iTIP item
+     * @param {Function} optionsFunc        The function being called with parameters: itipItem,
+     *                                          resultCode, actionFunc
      */
     processItipItem: function(itipItem, optionsFunc) {
         switch (itipItem.receivedMethod.toUpperCase()) {
             case "REFRESH":
             case "PUBLISH":
             case "REQUEST":
             case "CANCEL":
             case "COUNTER":
@@ -667,22 +697,23 @@ cal.itip = {
                 break;
             }
         }
     },
 
     /**
      * Scope: iTIP message sender
      *
-     * Checks to see if e.g. attendees were added/removed or an item has been
-     * deleted and sends out appropriate iTIP messages.
-     * @param {Number}             aOpType        Type of operation - (e.g. ADD, MODIFY or DELETE)
-     * @param {calIEvent|calITodo} aItem          The updated item
-     * @param {calIEvent|calITodo} aOriginalItem  The original item
-     * @param {Object}             aExtResponse   [optional] An object to provide additional
+     * Checks to see if e.g. attendees were added/removed or an item has been deleted and sends out
+     * appropriate iTIP messages.
+     *
+     * @param {Number} aOpType                    Type of operation - (e.g. ADD, MODIFY or DELETE)
+     * @param {calIItemBase} aItem                The updated item
+     * @param {calIItemBase} aOriginalItem        The original item
+     * @param {?Object} aExtResponse              An object to provide additional
      *                                            parameters for sending itip messages as response
      *                                            mode, comments or a subset of recipients. Currently
      *                                            implemented attributes are:
      *                             * responseMode Response mode (long) as defined for autoResponse
      *                                            of calIItipItem. The default mode is USER (which
      *                                            will trigger displaying the previously known popup
      *                                            to ask the user whether to send)
      */
@@ -750,17 +781,17 @@ cal.itip = {
             cal.LOG("cal.itip.checkAndSend: no response mode provided, " +
                     "falling back to USER mode.\r\n" + cal.STACK(20));
         }
         if (autoResponse.mode == Ci.calIItipItem.NONE) {
             // we stop here and don't send anything if the user opted out before
             return;
         }
 
-        let invitedAttendee = cal.isInvitation(aItem) && cal.getInvitedAttendee(aItem);
+        let invitedAttendee = calitip.isInvitation(aItem) && calitip.getInvitedAttendee(aItem);
         if (invitedAttendee) { // actually is an invitation copy, fix attendee list to send REPLY
             /* We check if the attendee id matches one of of the
              * userAddresses. If they aren't equal, it means that
              * someone is accepting invitations on behalf of an other user. */
             if (aItem.calendar.aclEntry) {
                 let userAddresses = aItem.calendar.aclEntry.getUserAddresses({});
                 if (userAddresses.length > 0 &&
                     !cal.attendeeMatchesAddresses(invitedAttendee, userAddresses)) {
@@ -779,17 +810,17 @@ cal.itip = {
                     invitedAttendee.participationStatus = "DECLINED";
                 }
 
                 // We want to send a REPLY send if:
                 // - there has been a PARTSTAT change
                 // - in case of an organizer SEQUENCE bump we'd go and reconfirm our PARTSTAT
                 if (!origInvitedAttendee ||
                     (origInvitedAttendee.participationStatus != invitedAttendee.participationStatus) ||
-                    (aOriginalItem && (cal.itip.getSequence(aItem) != cal.itip.getSequence(aOriginalItem)))) {
+                    (aOriginalItem && (calitip.getSequence(aItem) != calitip.getSequence(aOriginalItem)))) {
                     aItem = aItem.clone();
                     aItem.removeAllAttendees();
                     aItem.addAttendee(invitedAttendee);
                     // we remove X-MS-OLK-SENDER to avoid confusing Outlook 2007+ (w/o Exchange)
                     // about the notification sender (see bug 603933)
                     if (aItem.hasProperty("X-MS-OLK-SENDER")) {
                         aItem.deleteProperty("X-MS-OLK-SENDER");
                     }
@@ -829,17 +860,17 @@ cal.itip = {
         if (aItem.getProperty("X-MOZ-SEND-INVITATIONS") != "TRUE") { // Only send invitations/cancellations
                                                                      // if the user checked the checkbox
             return;
         }
 
         // special handling for invitation with event status cancelled
         if (aItem.getAttendees({}).length > 0 &&
             aItem.getProperty("STATUS") == "CANCELLED") {
-            if (cal.itip.getSequence(aItem) > 0) {
+            if (calitip.getSequence(aItem) > 0) {
                 // make sure we send a cancellation and not an request
                 aOpType = Components.interfaces.calIOperationListener.DELETE;
             } else {
                 // don't send an invitation, if the event was newly created and has status cancelled
                 return;
             }
         }
 
@@ -874,20 +905,20 @@ cal.itip = {
                 canceledAttendees.push(cancAtt);
             }
         }
 
         // setting default value to control for sending (cancellation) messages
         // this will be set to false, once the user cancels sending manually
         let sendOut = true;
         // Check to see if some part of the item was updated, if so, re-send REQUEST
-        if (!aOriginalItem || (cal.itip.compare(aItem, aOriginalItem) > 0)) { // REQUEST
+        if (!aOriginalItem || (calitip.compare(aItem, aOriginalItem) > 0)) { // REQUEST
             // check whether it's a simple UPDATE (no SEQUENCE change) or real (RE)REQUEST,
             // in case of time or location/description change.
-            let isMinorUpdate = (aOriginalItem && (cal.itip.getSequence(aItem) == cal.itip.getSequence(aOriginalItem)));
+            let isMinorUpdate = (aOriginalItem && (calitip.getSequence(aItem) == calitip.getSequence(aOriginalItem)));
 
             if (!isMinorUpdate || !cal.item.compareContent(stripUserData(aItem), stripUserData(aOriginalItem))) {
                 let requestItem = aItem.clone();
                 if (!requestItem.organizer) {
                     requestItem.organizer = createOrganizer(requestItem.calendar);
                 }
 
                 // Fix up our attendees for invitations using some good defaults
@@ -936,19 +967,23 @@ cal.itip = {
             if (sendOut) {
                 sendMessage(cancelItem, "CANCEL", canceledAttendees, autoResponse);
             }
         }
     },
 
     /**
      * Bumps the SEQUENCE in case of a major change; XXX todo may need more fine-tuning.
+     *
+     * @param {calIItemBase} newItem        The new item to set the sequence on
+     * @param {calIItemBase} oldItem        The old item to get the previous version from.
+     * @return {calIItemBase}               The newly changed item
      */
     prepareSequence: function(newItem, oldItem) {
-        if (cal.isInvitation(newItem)) {
+        if (calitip.isInvitation(newItem)) {
             return newItem; // invitation copies don't bump the SEQUENCE
         }
 
         if (newItem.recurrenceId && !oldItem.recurrenceId && oldItem.recurrenceInfo) {
             // XXX todo: there's still the bug that modifyItem is called with mixed occurrence/parent,
             //           find original occurrence
             oldItem = oldItem.recurrenceInfo.getOccurrenceFor(newItem.recurrenceId);
             cal.ASSERT(oldItem, "unexpected!");
@@ -983,31 +1018,31 @@ cal.itip = {
         };
 
         let hash1 = hashMajorProps(newItem);
         let hash2 = hashMajorProps(oldItem);
         if (hash1 != hash2) {
             newItem = newItem.clone();
             // bump SEQUENCE, it never decreases (mind undo scenario here)
             newItem.setProperty("SEQUENCE",
-                                String(Math.max(cal.itip.getSequence(oldItem),
-                                                cal.itip.getSequence(newItem)) + 1));
+                                String(Math.max(calitip.getSequence(oldItem),
+                                                calitip.getSequence(newItem)) + 1));
         }
 
         return newItem;
     },
 
     /**
-     * Returns a copy of an itipItem with modified properties and items build from scratch
-     * Use itipItem.clone() instead if only a simple copy is required
+     * Returns a copy of an itipItem with modified properties and items build from scratch Use
+     * itipItem.clone() instead if only a simple copy is required
      *
      * @param  {calIItipItem} aItipItem  ItipItem to derive a new one from
-     * @param  {Array}        aItems     calIEvent or calITodo items to be contained in the new itipItem
-     * @param  {JsObject}     aProps     Properties to be different in the new itipItem
-     * @return {calIItipItem}
+     * @param  {calIItemBase[]} aItems   calIEvent or calITodo items to be contained in the new itipItem
+     * @param  {Object} aProps           Properties to be different in the new itipItem
+     * @return {calIItipItem}            The copied and modified item
      */
     getModifiedItipItem: function(aItipItem, aItems=[], aProps={}) {
         let itipItem = Components.classes["@mozilla.org/calendar/itip-item;1"]
                                  .createInstance(Components.interfaces.calIItipItem);
         let serializedItems = "";
         for (let item of aItems) {
             serializedItems += cal.item.serialize(item);
         }
@@ -1020,52 +1055,210 @@ cal.itip = {
         itipItem.receivedMethod = ("receivedMethod" in aProps) ? aProps.receivedMethod : aItipItem.receivedMethod;
         itipItem.responseMethod = ("responseMethod" in aProps) ? aProps.responseMethod : aItipItem.responseMethod;
         itipItem.targetCalendar = ("targetCalendar" in aProps) ? aProps.targetCalendar : aItipItem.targetCalendar;
 
         return itipItem;
     },
 
     /**
-     * A shortcut to send DECLINECOUNTER messages - for everything else use cal.itip.checkAndSend
+     * A shortcut to send DECLINECOUNTER messages - for everything else use calitip.checkAndSend
      *
-     * @param {calIItipItem} aItem            item to be sent
-     * @param {String}       aMethod          iTIP method
-     * @param {Array}        aRecipientsList  array of calIAttendee objects the message should be sent to
-     * @param {Object}       aAutoResponse    JS object whether the transport should ask before sending
+     * @param {calIItipItem} aItem              item to be sent
+     * @param {String} aMethod                  iTIP method
+     * @param {calIAttendee[]} aRecipientsList  array of calIAttendee objects the message should be sent to
+     * @param {Object} aAutoResponse            JS object whether the transport should ask before sending
+     * @return {Boolean}                        True
      */
     sendDeclineCounterMessage: function(aItem, aMethod, aRecipientsList, aAutoResponse) {
         if (aMethod == "DECLINECOUNTER") {
             return sendMessage(aItem, aMethod, aRecipientsList, aAutoResponse);
         }
         return false;
+    },
+
+    /**
+     * Returns a copy of an event that
+     * - has a relation set to the original event
+     * - has the same organizer but
+     * - has any attendee removed
+     * Intended to get a copy of a normal event invitation that behaves as if the PUBLISH method was
+     * chosen instead.
+     *
+     * @param {calIItemBase} aItem      Original item
+     * @param {?String} aUid            UID to use for the new item
+     * @return {calIItemBase}           The copied item for publishing
+     */
+    getPublishLikeItemCopy: function(aItem, aUid) {
+        // avoid changing aItem
+        let item = aItem.clone();
+        // reset to a new UUID if applicable
+        item.id = aUid || cal.getUUID();
+        // add a relation to the original item
+        let relation = cal.createRelation();
+        relation.relId = aItem.id;
+        relation.relType = "SIBLING";
+        item.addRelation(relation);
+        // remove attendees
+        item.removeAllAttendees();
+        if (!aItem.isMutable) {
+            item = item.makeImmutable();
+        }
+        return item;
+    },
+
+    /**
+     * Shortcut function to check whether an item is an invitation copy.
+     *
+     * @param {calIItemBase} aItem      The item to check for an invitation.
+     * @return {Boolean}                True, if the item is an invitation.
+     */
+    isInvitation: function(aItem) {
+        let isInvitation = false;
+        let calendar = cal.wrapInstance(aItem.calendar, Components.interfaces.calISchedulingSupport);
+        if (calendar) {
+            isInvitation = calendar.isInvitation(aItem);
+        }
+        return isInvitation;
+    },
+
+    /**
+     * Shortcut function to check whether an item is an invitation copy and has a participation
+     * status of either NEEDS-ACTION or TENTATIVE.
+     *
+     * @param {calIAttendee|calIItemBase} aItem     either calIAttendee or calIItemBase
+     * @return {Boolean}                            True, if the attendee partstat is NEEDS-ACTION
+     *                                                or TENTATIVE
+     */
+    isOpenInvitation: function(aItem) {
+        let wrappedItem = cal.wrapInstance(aItem, Components.interfaces.calIAttendee);
+        if (!wrappedItem) {
+            aItem = calitip.getInvitedAttendee(aItem);
+        }
+        if (aItem) {
+            switch (aItem.participationStatus) {
+                case "NEEDS-ACTION":
+                case "TENTATIVE":
+                    return true;
+            }
+        }
+        return false;
+    },
+
+
+    /**
+     * Resolves delegated-to/delegated-from calusers for a given attendee to also include the
+     * respective CNs if available in a given set of attendees
+     *
+     * @param {calIAttendee} aAttendee          The attendee to resolve the delegation information for
+     * @param {calIAttendee[]} aAttendees       An array of calIAttendee objects to look up
+     * @return {Object}                         An object with string attributes for delegators and delegatees
+     */
+    resolveDelegation: function(aAttendee, aAttendees) {
+        let attendees = aAttendees || [aAttendee];
+
+        // this will be replaced by a direct property getter in calIAttendee
+        let delegators = [];
+        let delegatees = [];
+        let delegatorProp = aAttendee.getProperty("DELEGATED-FROM");
+        if (delegatorProp) {
+            delegators = typeof delegatorProp == "string" ? [delegatorProp] : delegatorProp;
+        }
+        let delegateeProp = aAttendee.getProperty("DELEGATED-TO");
+        if (delegateeProp) {
+            delegatees = typeof delegateeProp == "string" ? [delegateeProp] : delegateeProp;
+        }
+
+        for (let att of attendees) {
+            let resolveDelegation = function(e, i, a) {
+                if (e == att.id) {
+                    a[i] = att.toString();
+                }
+            };
+            delegators.forEach(resolveDelegation);
+            delegatees.forEach(resolveDelegation);
+        }
+        return {
+            delegatees: delegatees.join(", "),
+            delegators: delegators.join(", ")
+        };
+    },
+
+    /**
+     * Shortcut function to get the invited attendee of an item.
+     *
+     * @param {calIItemBase} aItem          Event or task to get the invited attendee for
+     * @param {?calICalendar} aCalendar     The calendar to use for checking, defaults to the item
+     *                                        calendar
+     * @return {?calIAttendee}              The attendee that was invited
+     */
+    getInvitedAttendee: function(aItem, aCalendar) {
+        if (!aCalendar) {
+            aCalendar = aItem.calendar;
+        }
+        let invitedAttendee = null;
+        let calendar = cal.wrapInstance(aCalendar, Components.interfaces.calISchedulingSupport);
+        if (calendar) {
+            invitedAttendee = calendar.getInvitedAttendee(aItem);
+        }
+        return invitedAttendee;
+    },
+
+    /**
+     * Returns all attendees from given set of attendees matching based on the attendee id
+     * or a sent-by parameter compared to the specified email address
+     *
+     * @param {calIAttendee[]} aAttendees       An array of calIAttendee objects
+     * @param {String} aEmailAddress            A string containing the email address for lookup
+     * @return {calIAttendee[]}                 Returns an array of matching attendees
+     */
+    getAttendeesBySender: function(aAttendees, aEmailAddress) {
+        let attendees = [];
+        // we extract the email address to make it work also for a raw header value
+        let compFields = Components.classes["@mozilla.org/messengercompose/composefields;1"]
+                                   .createInstance(Components.interfaces.nsIMsgCompFields);
+        let addresses = compFields.splitRecipients(aEmailAddress, true, {});
+        if (addresses.length == 1) {
+            let searchFor = cal.prependMailTo(addresses[0]);
+            aAttendees.forEach(aAttendee => {
+                if ([aAttendee.id, aAttendee.getProperty("SENT-BY")].includes(searchFor)) {
+                    attendees.push(aAttendee);
+                }
+            });
+        } else {
+            cal.WARN("No unique email address for lookup!");
+        }
+        return attendees;
     }
 };
 
 /** local to this module file
  * Sets the received info either on the passed attendee or item object.
  *
- * @param item either  calIAttendee or calIItemBase
- * @param itipItemItem received iTIP item
+ * @param {calIItemBase|calIAttendee} item      The item to set info on
+ * @param {calIItipItem} itipItemItem           The received iTIP item
  */
 function setReceivedInfo(item, itipItemItem) {
     let wrappedItem = cal.wrapInstance(item, Components.interfaces.calIAttendee);
     item.setProperty(wrappedItem ? "RECEIVED-SEQUENCE"
                                  : "X-MOZ-RECEIVED-SEQUENCE",
-                                 String(cal.itip.getSequence(itipItemItem)));
-    let dtstamp = cal.itip.getStamp(itipItemItem);
+                                 String(calitip.getSequence(itipItemItem)));
+    let dtstamp = calitip.getStamp(itipItemItem);
     if (dtstamp) {
         item.setProperty(wrappedItem ? "RECEIVED-DTSTAMP"
                                      : "X-MOZ-RECEIVED-DTSTAMP",
                                      dtstamp.getInTimezone(cal.dtz.UTC).icalString);
     }
 }
 
 /**
  * Strips user specific data, e.g. categories and alarm settings and returns the stripped item.
+ *
+ * @param {calIItemBase} item_      The item to strip data from
+ * @return {calIItemBase}           The stripped item
  */
 function stripUserData(item_) {
     let item = item_.clone();
     let stamp = item.stampTime;
     let lastModified = item.lastModifiedTime;
     item.clearAlarms();
     item.alarmLastAck = null;
     item.setCategories(0, []);
@@ -1097,20 +1290,27 @@ function stripUserData(item_) {
     item.setProperty("DTSTAMP", stamp);
     item.setProperty("LAST-MODIFIED", lastModified); // need to be last to undirty the item
     return item;
 }
 
 /** local to this module file
  * Takes over relevant item information from iTIP item and sets received info.
  *
- * @param item         the stored calendar item to update
- * @param itipItemItem the received item
+ * @param {calIItemBase} item           The stored calendar item to update
+ * @param {calIItipItem} itipItemItem   The received item
+ * @return {calIItemBase}               A copy of the item with correct received info
  */
 function updateItem(item, itipItemItem) {
+    /**
+     * Migrates some user data from the old to new item
+     *
+     * @param {calIItemBase} newItem        The new item to copy to
+     * @param {calIItemBase} oldItem        The old item to copy from
+     */
     function updateUserData(newItem, oldItem) {
         // preserve user settings:
         newItem.generation = oldItem.generation;
         newItem.clearAlarms();
         for (let alarm of oldItem.getAlarms({})) {
             newItem.addAlarm(alarm);
         }
         newItem.alarmLastAck = oldItem.alarmLastAck;
@@ -1142,19 +1342,19 @@ function updateItem(item, itipItemItem) 
 
     return newItem;
 }
 
 /** local to this module file
  * Copies the provider-specified properties from the itip item to the passed
  * item. Special case property "METHOD" uses the itipItem's receivedMethod.
  *
- * @param itipItem      The itip item containing the receivedMethod.
- * @param itipItemItem  The calendar item inside the itip item.
- * @param item          The target item to copy to.
+ * @param {calIItipItem} itipItem      The itip item containing the receivedMethod.
+ * @param {calIItemBase} itipItemItem  The calendar item inside the itip item.
+ * @param {calIItemBase} item          The target item to copy to.
  */
 function copyProviderProperties(itipItem, itipItemItem, item) {
     // Copy over itip properties to the item if requested by the provider
     let copyProps = item.calendar.getProperty("itip.copyProperties") || [];
     for (let prop of copyProps) {
         if (prop == "METHOD") {
             // Special case, this copies over the received method
             item.setProperty("METHOD", itipItem.receivedMethod.toUpperCase());
@@ -1163,17 +1363,18 @@ function copyProviderProperties(itipItem
             item.setProperty(prop, itipItemItem.getProperty(prop));
         }
     }
 }
 
 /** local to this module file
  * Creates an organizer calIAttendee object based on the calendar's configured organizer id.
  *
- * @return calIAttendee object
+ * @param {calICalendar} aCalendar      The calendar to get the organizer id from
+ * @return {calIAttendee}               The organizer attendee
  */
 function createOrganizer(aCalendar) {
     let orgId = aCalendar.getProperty("organizerId");
     if (!orgId) {
         return null;
     }
     let organizer = cal.createAttendee();
     organizer.id = orgId;
@@ -1182,20 +1383,21 @@ function createOrganizer(aCalendar) {
     organizer.participationStatus = "ACCEPTED";
     organizer.isOrganizer = true;
     return organizer;
 }
 
 /** local to this module file
  * Sends an iTIP message using the passed item's calendar transport.
  *
- * @param {calIEvent} aItem           item to be sent
- * @param {String}    aMethod         iTIP method
- * @param {Array}     aRecipientsList array of calIAttendee objects the message should be sent to
- * @param {JSObject}  autoResponse    inout object whether the transport should ask before sending
+ * @param {calIEvent} aItem                 item to be sent
+ * @param {String} aMethod                  iTIP method
+ * @param {calIAttendee[]} aRecipientsList  array of calIAttendee objects the message should be sent to
+ * @param {Object} autoResponse             inout object whether the transport should ask before sending
+ * @return {Boolean}                        True, if the message could be sent
  */
 function sendMessage(aItem, aMethod, aRecipientsList, autoResponse) {
     let calendar = cal.wrapInstance(aItem.calendar, Components.interfaces.calISchedulingSupport);
     if (calendar) {
         if (calendar.QueryInterface(Components.interfaces.calISchedulingSupport)
                     .canNotify(aMethod, aItem)) {
             // provider will handle that, so we return - we leave it also to the provider to
             // deal with user canceled notifications (if possible), so set the return value
@@ -1252,35 +1454,38 @@ function sendMessage(aItem, aMethod, aRe
         return _sendItem(aRecipientsList, aItem);
     }
 }
 
 /** local to this module file
  * An operation listener that is used on calendar operations which checks and sends further iTIP
  * messages based on the calendar action.
  *
- * @param opListener operation listener to forward
- * @param oldItem the previous item before modification (if any)
+ * @param {Object} aOpListener          operation listener to forward
+ * @param {calIItemBase} aOldItem       The previous item before modification (if any)
+ * @param {?Object} aExtResponse        An object to provide additional parameters for sending itip
+ *                                        messages as response mode, comments or a subset of
+ *                                        recipients.
  */
 function ItipOpListener(aOpListener, aOldItem, aExtResponse=null) {
     this.mOpListener = aOpListener;
     this.mOldItem = aOldItem;
     this.mExtResponse = aExtResponse;
 }
 ItipOpListener.prototype = {
     QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
 
     mOpListener: null,
     mOldItem: null,
     mExtResponse: null,
 
     onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDetail) {
         cal.ASSERT(Components.isSuccessCode(aStatus), "error on iTIP processing");
         if (Components.isSuccessCode(aStatus)) {
-            cal.itip.checkAndSend(aOperationType, aDetail, this.mOldItem, this.mExtResponse);
+            calitip.checkAndSend(aOperationType, aDetail, this.mOldItem, this.mExtResponse);
         }
         if (this.mOpListener) {
             this.mOpListener.onOperationComplete(aCalendar,
                                                  aStatus,
                                                  aOperationType,
                                                  aId,
                                                  aDetail);
         }
@@ -1289,18 +1494,18 @@ ItipOpListener.prototype = {
     }
 };
 
 /** local to this module file
  * Add a parameter SCHEDULE-AGENT=CLIENT to the item before it is
  * created or updated so that the providers knows scheduling will
  * be handled by the client.
  *
- * @param item item about to be added or updated
- * @param calendar calendar into which the item is about to be added or updated
+ * @param {calIItemBase} item       item about to be added or updated
+ * @param {calICalendar} calendar   calendar into which the item is about to be added or updated
  */
 function addScheduleAgentClient(item, calendar) {
     if (calendar.getProperty("capabilities.autoschedule.supported") === true) {
         if (item.organizer) {
             item.organizer.setProperty("SCHEDULE-AGENT", "CLIENT");
         }
     }
 }
@@ -1308,47 +1513,48 @@ function addScheduleAgentClient(item, ca
 var ItipItemFinderFactory = {
     /**  Map to save finder instances for given ids */
     _findMap: {},
 
     /**
      * Create an item finder and track its progress. Be sure to clean up the
      * finder for this id at some point.
      *
-     * @param aId           The item id to search for
-     * @param aItipItem     The iTIP item used for processing
-     * @param aOptionsFunc  The options function used for processing the found item
+     * @param {String} aId              The item id to search for
+     * @param {calIIipItem} aItipItem   The iTIP item used for processing
+     * @param {Function} aOptionsFunc   The options function used for processing the found item
      */
     findItem: function(aId, aItipItem, aOptionsFunc) {
         this.cleanup(aId);
         let finder = new ItipItemFinder(aId, aItipItem, aOptionsFunc);
         this._findMap[aId] = finder;
         finder.findItem();
     },
 
     /**
      * Clean up tracking for the given id. This needs to be called once for
      * every time findItem is called.
      *
-     * @param aId           The item id to clean up for
+     * @param {String} aId           The item id to clean up for
      */
     cleanup: function(aId) {
         if (aId in this._findMap) {
             let finder = this._findMap[aId];
             finder.destroy();
             delete this._findMap[aId];
         }
     }
 };
 
 /** local to this module file
  * An operation listener triggered by cal.itip.processItipItem() for lookup of the sent iTIP item's UID.
  *
- * @param itipItem sent iTIP item
- * @param optionsFunc options func, see cal.itip.processItipItem()
+ * @param {String} aId              The search identifier for the item to find
+ * @param {calIItipItem} itipItem   Sent iTIP item
+ * @param {Function} optionsFunc    Options func, see cal.itip.processItipItem()
  */
 function ItipItemFinder(aId, itipItem, optionsFunc) {
     this.mItipItem = itipItem;
     this.mOptionsFunc = optionsFunc;
     this.mSearchId = aId;
 }
 
 ItipItemFinder.prototype = {
@@ -1502,28 +1708,28 @@ ItipItemFinder.prototype = {
                                         operations.push(action);
                                     }
                                     break;
                                 }
                                 case "PUBLISH":
                                     cal.ASSERT(itipItemItem.getAttendees({}).length == 0,
                                                "invalid number of attendees in PUBLISH!");
                                     if (item.calendar.getProperty("itip.disableRevisionChecks") ||
-                                        cal.itip.compare(itipItemItem, item) > 0) {
+                                        calitip.compare(itipItemItem, item) > 0) {
                                         let newItem = updateItem(item, itipItemItem);
                                         let action = function(opListener, partStat, extResponse) {
                                             return newItem.calendar.modifyItem(newItem, item, opListener);
                                         };
                                         actionMethod = method + ":UPDATE";
                                         operations.push(action);
                                     }
                                     break;
                                 case "REQUEST": {
                                     let newItem = updateItem(item, itipItemItem);
-                                    let att = cal.getInvitedAttendee(newItem);
+                                    let att = calitip.getInvitedAttendee(newItem);
                                     if (!att) { // fall back to using configured organizer
                                         att = createOrganizer(newItem.calendar);
                                         if (att) {
                                             att.isOrganizer = false;
                                         }
                                     }
                                     if (att) {
                                         let firstFoundItem = this.mFoundItems[0];
@@ -1532,41 +1738,41 @@ ItipItemFinder.prototype = {
 
                                         // If the the user hasn't responded to the invitation yet and we
                                         // are viewing the current representation of the item, show the
                                         // accept/decline buttons. This means newer events will show the
                                         // "Update" button and older events will show the "already
                                         // processed" text.
                                         if (foundAttendee.participationStatus == "NEEDS-ACTION" &&
                                             (item.calendar.getProperty("itip.disableRevisionChecks") ||
-                                             cal.itip.compare(itipItemItem, item) == 0)) {
+                                             calitip.compare(itipItemItem, item) == 0)) {
                                             actionMethod = "REQUEST:NEEDS-ACTION";
                                             operations.push((opListener, partStat, extResponse) => {
                                                 let changedItem = firstFoundItem.clone();
                                                 changedItem.removeAttendee(foundAttendee);
                                                 foundAttendee = foundAttendee.clone();
                                                 if (partStat) {
                                                     foundAttendee.participationStatus = partStat;
                                                 }
                                                 changedItem.addAttendee(foundAttendee);
 
                                                 return changedItem.calendar.modifyItem(
                                                     changedItem, firstFoundItem, new ItipOpListener(opListener, firstFoundItem, extResponse));
                                             });
                                         } else if (item.calendar.getProperty("itip.disableRevisionChecks") ||
-                                                   cal.itip.compare(itipItemItem, item) > 0) {
+                                                   calitip.compare(itipItemItem, item) > 0) {
                                             addScheduleAgentClient(newItem, item.calendar);
 
-                                            let isMinorUpdate = cal.itip.getSequence(newItem) ==
-                                                                cal.itip.getSequence(item);
+                                            let isMinorUpdate = calitip.getSequence(newItem) ==
+                                                                calitip.getSequence(item);
                                             actionMethod = (isMinorUpdate ? method + ":UPDATE-MINOR"
                                                                           : method + ":UPDATE");
                                             operations.push((opListener, partStat, extResponse) => {
                                                 if (!partStat) { // keep PARTSTAT
-                                                    let att_ = cal.getInvitedAttendee(item);
+                                                    let att_ = calitip.getInvitedAttendee(item);
                                                     partStat = att_ ? att_.participationStatus : "NEEDS-ACTION";
                                                 }
                                                 newItem.removeAttendee(att);
                                                 att = att.clone();
                                                 att.participationStatus = partStat;
                                                 newItem.addAttendee(att);
                                                 return newItem.calendar.modifyItem(
                                                     newItem, item, new ItipOpListener(opListener, item, extResponse));
@@ -1604,19 +1810,19 @@ ItipItemFinder.prototype = {
                                         // We accepts REPLYs also from previously uninvited
                                         // attendees, so we always have one for REPLY
                                         replyer = attendees[0];
                                     }
                                     let noCheck = item.calendar.getProperty(
                                         "itip.disableRevisionChecks");
                                     let revCheck = false;
                                     if (replyer && !noCheck) {
-                                        revCheck = cal.itip.compare(itipItemItem, replyer) > 0;
+                                        revCheck = calitip.compare(itipItemItem, replyer) > 0;
                                         if (revCheck && method == "COUNTER") {
-                                            revCheck = cal.itip.compareSequence(itipItemItem, item) == 0;
+                                            revCheck = calitip.compareSequence(itipItemItem, item) == 0;
                                         }
                                     }
 
                                     if (replyer && (noCheck || revCheck)) {
                                         let newItem = item.clone();
                                         newItem.removeAttendee(replyer);
                                         replyer = replyer.clone();
                                         setReceivedInfo(replyer, itipItemItem);
@@ -1704,17 +1910,17 @@ ItipItemFinder.prototype = {
                             let newItem = itipItemItem.clone();
                             setReceivedInfo(newItem, itipItemItem);
                             newItem.parentItem.calendar = this.mItipItem.targetCalendar;
                             addScheduleAgentClient(newItem, this.mItipItem.targetCalendar);
                             if (partStat) {
                                 if (partStat != "DECLINED") {
                                     cal.alarms.setDefaultValues(newItem);
                                 }
-                                let att = cal.getInvitedAttendee(newItem);
+                                let att = calitip.getInvitedAttendee(newItem);
                                 if (!att) { // fall back to using configured organizer
                                     att = createOrganizer(newItem.calendar);
                                     if (att) {
                                         att.isOrganizer = false;
                                         newItem.addAttendee(att);
                                     }
                                 }
                                 if (att) {
--- a/calendar/base/modules/calUtils.jsm
+++ b/calendar/base/modules/calUtils.jsm
@@ -181,280 +181,16 @@ var cal = {
 
     get threadingEnabled() {
         if (gCalThreadingEnabled === undefined) {
             gCalThreadingEnabled = !Preferences.get("calendar.threading.disabled", false);
         }
         return gCalThreadingEnabled;
     },
 
-    /**
-     * Returns a copy of an event that
-     * - has a relation set to the original event
-     * - has the same organizer but
-     * - has any attendee removed
-     * Intended to get a copy of a normal event invitation that behaves as if the PUBLISH method
-     * was chosen instead.
-     *
-     * @param aItem         original item
-     * @param aUid          (optional) UID to use for the new item
-     */
-    getPublishLikeItemCopy: function(aItem, aUid) {
-        // avoid changing aItem
-        let item = aItem.clone();
-        // reset to a new UUID if applicable
-        item.id = aUid || cal.getUUID();
-        // add a relation to the original item
-        let relation = cal.createRelation();
-        relation.relId = aItem.id;
-        relation.relType = "SIBLING";
-        item.addRelation(relation);
-        // remove attendees
-        item.removeAllAttendees();
-        if (!aItem.isMutable) {
-            item = item.makeImmutable();
-        }
-        return item;
-    },
-
-    /**
-     * Shortcut function to check whether an item is an invitation copy.
-     */
-    isInvitation: function(aItem) {
-        let isInvitation = false;
-        let calendar = cal.wrapInstance(aItem.calendar, Components.interfaces.calISchedulingSupport);
-        if (calendar) {
-            isInvitation = calendar.isInvitation(aItem);
-        }
-        return isInvitation;
-    },
-
-    /**
-     * Returns a basically checked recipient list - malformed elements will be removed
-     *
-     * @param   string aRecipients  a comma-seperated list of e-mail addresses
-     * @return  string              a comma-seperated list of e-mail addresses
-     */
-    validateRecipientList: function(aRecipients) {
-        let compFields = Components.classes["@mozilla.org/messengercompose/composefields;1"]
-                                   .createInstance(Components.interfaces.nsIMsgCompFields);
-        // Resolve the list considering also configured common names
-        let members = compFields.splitRecipients(aRecipients, false, {});
-        let list = [];
-        let prefix = "";
-        for (let member of members) {
-            if (prefix != "") {
-                // the previous member had no email address - this happens if a recipients CN
-                // contains a ',' or ';' (splitRecipients(..) behaves wrongly here and produces an
-                // additional member with only the first CN part of that recipient and no email
-                // address while the next has the second part of the CN and the according email
-                // address) - we still need to identify the original delimiter to append it to the
-                // prefix
-                let memberCnPart = member.match(/(.*) <.*>/);
-                if (memberCnPart) {
-                    let pattern = new RegExp(prefix + "([;,] *)" + memberCnPart[1]);
-                    let delimiter = aRecipients.match(pattern);
-                    if (delimiter) {
-                        prefix = prefix + delimiter[1];
-                    }
-                }
-            }
-            let parts = (prefix + member).match(/(.*)( <.*>)/);
-            if (parts) {
-                if (parts[2] == " <>") {
-                    // CN but no email address - we keep the CN part to prefix the next member's CN
-                    prefix = parts[1];
-                } else {
-                    // CN with email address
-                    let commonName = parts[1].trim();
-                    // in case of any special characters in the CN string, we make sure to enclose
-                    // it with dquotes - simple spaces don't require dquotes
-                    if (commonName.match(/[-[\]{}()*+?.,;\\^$|#\f\n\r\t\v]/)) {
-                        commonName = '"' + commonName.replace(/\\"|"/, "").trim() + '"';
-                    }
-                    list.push(commonName + parts[2]);
-                    prefix = "";
-                }
-            } else if (member.length) {
-                // email address only
-                list.push(member);
-                prefix = "";
-            }
-        }
-        return list.join(", ");
-    },
-
-    /**
-     * Shortcut function to check whether an item is an invitation copy and
-     * has a participation status of either NEEDS-ACTION or TENTATIVE.
-     *
-     * @param aItem either calIAttendee or calIItemBase
-     */
-    isOpenInvitation: function(aItem) {
-        let wrappedItem = cal.wrapInstance(aItem, Components.interfaces.calIAttendee);
-        if (!wrappedItem) {
-            aItem = cal.getInvitedAttendee(aItem);
-        }
-        if (aItem) {
-            switch (aItem.participationStatus) {
-                case "NEEDS-ACTION":
-                case "TENTATIVE":
-                    return true;
-            }
-        }
-        return false;
-    },
-
-    /**
-     * Prepends a mailto: prefix to an email address like string
-     *
-     * @param  {string}        the string to prepend the prefix if not already there
-     * @return {string}        the string with prefix
-     */
-    prependMailTo: function(aId) {
-        return aId.replace(/^(?:mailto:)?(.*)@/i, "mailto:$1@");
-    },
-
-    /**
-     * Removes an existing mailto: prefix from an attendee id
-     *
-     * @param  {string}       the string to remove the prefix from if any
-     * @return {string}       the string without prefix
-     */
-    removeMailTo: function(aId) {
-        return aId.replace(/^mailto:/i, "");
-    },
-
-    /**
-     * Resolves delegated-to/delegated-from calusers for a given attendee to also include the
-     * respective CNs if available in a given set of attendees
-     *
-     * @param aAttendee  {calIAttendee}  The attendee to resolve the delegation information for
-     * @param aAttendees {Array}         An array of calIAttendee objects to look up
-     * @return           {Object}        An object with string attributes for delegators and delegatees
-     */
-    resolveDelegation: function(aAttendee, aAttendees) {
-        let attendees = aAttendees || [aAttendee];
-
-        // this will be replaced by a direct property getter in calIAttendee
-        let delegators = [];
-        let delegatees = [];
-        let delegatorProp = aAttendee.getProperty("DELEGATED-FROM");
-        if (delegatorProp) {
-            delegators = typeof delegatorProp == "string" ? [delegatorProp] : delegatorProp;
-        }
-        let delegateeProp = aAttendee.getProperty("DELEGATED-TO");
-        if (delegateeProp) {
-            delegatees = typeof delegateeProp == "string" ? [delegateeProp] : delegateeProp;
-        }
-
-        for (let att of attendees) {
-            let resolveDelegation = function(e, i, a) {
-                if (e == att.id) {
-                    a[i] = att.toString();
-                }
-            };
-            delegators.forEach(resolveDelegation);
-            delegatees.forEach(resolveDelegation);
-        }
-        return {
-            delegatees: delegatees.join(", "),
-            delegators: delegators.join(", ")
-        };
-    },
-
-    /**
-     * Shortcut function to get the invited attendee of an item.
-     */
-    getInvitedAttendee: function(aItem, aCalendar) {
-        if (!aCalendar) {
-            aCalendar = aItem.calendar;
-        }
-        let invitedAttendee = null;
-        let calendar = cal.wrapInstance(aCalendar, Components.interfaces.calISchedulingSupport);
-        if (calendar) {
-            invitedAttendee = calendar.getInvitedAttendee(aItem);
-        }
-        return invitedAttendee;
-    },
-
-    /**
-     * Returns all attendees from given set of attendees matching based on the attendee id
-     * or a sent-by parameter compared to the specified email address
-     *
-     * @param  {Array}  aAttendees      An array of calIAttendee objects
-     * @param  {String} aEmailAddress   A string containing the email address for lookup
-     * @return {Array}                  Returns an array of matching attendees
-     */
-    getAttendeesBySender: function(aAttendees, aEmailAddress) {
-        let attendees = [];
-        // we extract the email address to make it work also for a raw header value
-        let compFields = Components.classes["@mozilla.org/messengercompose/composefields;1"]
-                                   .createInstance(Components.interfaces.nsIMsgCompFields);
-        let addresses = compFields.splitRecipients(aEmailAddress, true, {});
-        if (addresses.length == 1) {
-            let searchFor = cal.prependMailTo(addresses[0]);
-            aAttendees.forEach(aAttendee => {
-                if ([aAttendee.id, aAttendee.getProperty("SENT-BY")].includes(searchFor)) {
-                    attendees.push(aAttendee);
-                }
-            });
-        } else {
-            cal.WARN("No unique email address for lookup!");
-        }
-        return attendees;
-    },
-
-    /**
-     * Returns a wellformed email string like 'attendee@example.net',
-     * 'Common Name <attendee@example.net>' or '"Name, Common" <attendee@example.net>'
-     *
-     * @param  {calIAttendee}  aAttendee - the attendee to check
-     * @param  {boolean}       aIncludeCn - whether or not to return also the CN if available
-     * @return {string}        valid email string or an empty string in case of error
-     */
-    getAttendeeEmail: function(aAttendee, aIncludeCn) {
-        // If the recipient id is of type urn, we need to figure out the email address, otherwise
-        // we fall back to the attendee id
-        let email = aAttendee.id.match(/^urn:/i) ? aAttendee.getProperty("EMAIL") || "" : aAttendee.id;
-        // Strip leading "mailto:" if it exists.
-        email = email.replace(/^mailto:/i, "");
-        // We add the CN if requested and available
-        let commonName = aAttendee.commonName;
-        if (aIncludeCn && email.length > 0 && commonName && commonName.length > 0) {
-            if (commonName.match(/[,;]/)) {
-                commonName = '"' + commonName + '"';
-            }
-            commonName = commonName + " <" + email + ">";
-            if (cal.validateRecipientList(commonName) == commonName) {
-                email = commonName;
-            }
-        }
-        return email;
-    },
-
-    /**
-     * Provides a string to use in email "to" header for given attendees
-     *
-     * @param  {array}   aAttendees - array of calIAttendee's to check
-     * @return {string}  Valid string to use in a 'to' header of an email
-     */
-    getRecipientList: function(aAttendees) {
-        let cbEmail = function(aVal, aInd, aArr) {
-            let email = cal.getAttendeeEmail(aVal, true);
-            if (!email.length) {
-                cal.LOG("Dropping invalid recipient for email transport: " + aVal.toString());
-            }
-            return email;
-        };
-        return aAttendees.map(cbEmail)
-                         .filter(aVal => aVal.length > 0)
-                         .join(", ");
-    },
-
     // The below functions will move to some different place once the
     // unifinder tress are consolidated.
 
     compareNativeTime: function(a, b) {
         if (a < b) {
             return -1;
         } else if (a > b) {
             return 1;
@@ -747,21 +483,23 @@ var cal = {
      * @param obj    object
      * @param prop   property to be deleted on shutdown
      *               (if null, |object| will be deleted)
      */
     registerForShutdownCleanup: shutdownCleanup
 };
 
 // Sub-modules for calUtils
+XPCOMUtils.defineLazyModuleGetter(cal, "acl", "resource://calendar/modules/calACLUtils.jsm", "calacl");
+XPCOMUtils.defineLazyModuleGetter(cal, "category", "resource://calendar/modules/calCategoryUtils.jsm", "calcategory");
 XPCOMUtils.defineLazyModuleGetter(cal, "data", "resource://calendar/modules/calDataUtils.jsm", "caldata");
 XPCOMUtils.defineLazyModuleGetter(cal, "dtz", "resource://calendar/modules/calDateTimeUtils.jsm", "caldtz");
-XPCOMUtils.defineLazyModuleGetter(cal, "acl", "resource://calendar/modules/calACLUtils.jsm", "calacl");
-XPCOMUtils.defineLazyModuleGetter(cal, "category", "resource://calendar/modules/calCategoryUtils.jsm", "calcategory");
+XPCOMUtils.defineLazyModuleGetter(cal, "email", "resource://calendar/modules/calEmailUtils.jsm", "calemail");
 XPCOMUtils.defineLazyModuleGetter(cal, "item", "resource://calendar/modules/calItemUtils.jsm", "calitem");
+XPCOMUtils.defineLazyModuleGetter(cal, "itip", "resource://calendar/modules/calItipUtils.jsm", "calitip");
 XPCOMUtils.defineLazyModuleGetter(cal, "view", "resource://calendar/modules/calViewUtils.jsm", "calview");
 XPCOMUtils.defineLazyModuleGetter(cal, "window", "resource://calendar/modules/calWindowUtils.jsm", "calwindow");
 
 /**
  * Returns a function that provides access to the given service.
  *
  * @param cid           The contract id to create
  * @param iid           The interface id to create with
--- a/calendar/base/modules/calUtilsCompat.jsm
+++ b/calendar/base/modules/calUtilsCompat.jsm
@@ -48,28 +48,46 @@ var migrations = {
         jsDateToDateTime: "jsDateToDateTime",
         dateTimeToJsDate: "dateTimeToJsDate",
 
         // The following are now getters
         calendarDefaultTimezone: "defaultTimezone",
         floating: "floating",
         UTC: "UTC"
     },
+    email: {
+        sendMailTo: "sendMailTo",
+        calIterateEmailIdentities: "iterateIdentities",
+        prependMailTo: "prependMailTo",
+        removeMailTo: "removeMailTo",
+        getRecipientList: "createRecipientList",
+        getAttendeeEmail: "getAttendeeEmail",
+        validateRecipientList: "validateRecipientList",
+        attendeeMatchesAddresses: "attendeeMatchesAddresses"
+    },
     item: {
         // ItemDiff also belongs here, but is separately migrated in
         // calItemUtils.jsm
         isItemSupported: "isItemSupported",
         isEventCalendar: "isEventCalendar",
         isTaskCalendar: "isTaskCalendar",
         isEvent: "isEvent",
         isToDo: "isToDo",
         checkIfInRange: "checkIfInRange",
         setItemProperty: "setItemProperty",
         getEventDefaultTransparency: "getEventDefaultTransparency"
     },
+    itip: {
+        getPublishLikeItemCopy: "getPublishLikeItemCopy",
+        isInvitation: "isInvitation",
+        isOpenInvitation: "isOpenInvitation",
+        resolveDelegation: "resolveDelegation",
+        getInvitedAttendee: "getInvitedAttendee",
+        getAttendeesBySender: "getAttendeesBySender"
+    },
     view: {
         isMouseOverBox: "isMouseOverBox",
         calRadioGroupSelectItem: "radioGroupSelectItem",
         applyAttributeToMenuChildren: "applyAttributeToMenuChildren",
         removeChildElementsByAttribute: "removeChildElementsByAttribute",
         getParentNodeOrThis: "getParentNodeOrThis",
         getParentNodeOrThisByAttribute: "getParentNodeOrThisByAttribute",
         formatStringForCSSRule: "formatStringForCSSRule",
--- a/calendar/base/modules/moz.build
+++ b/calendar/base/modules/moz.build
@@ -6,16 +6,17 @@
 EXTRA_JS_MODULES += [
     'calACLUtils.jsm',
     'calAlarmUtils.jsm',
     'calAsyncUtils.jsm',
     'calAuthUtils.jsm',
     'calCategoryUtils.jsm',
     'calDataUtils.jsm',
     'calDateTimeUtils.jsm',
+    'calEmailUtils.jsm',
     'calExtract.jsm',
     'calHashedArray.jsm',
     'calItemUtils.jsm',
     'calIteratorUtils.jsm',
     'calItipUtils.jsm',
     'calPrintUtils.jsm',
     'calProviderUtils.jsm',
     'calRecurrenceUtils.jsm',
--- a/calendar/base/src/calTransactionManager.js
+++ b/calendar/base/src/calTransactionManager.js
@@ -1,14 +1,13 @@
 /* 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/. */
 
 ChromeUtils.import("resource://calendar/modules/calUtils.jsm");
-ChromeUtils.import("resource://calendar/modules/calItipUtils.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 function calTransactionManager() {
     this.wrappedJSObject = this;
     if (!this.transactionManager) {
         this.transactionManager =
             Components.classes["@mozilla.org/transactionmanager;1"]
                       .createInstance(Components.interfaces.nsITransactionManager);
--- a/calendar/base/src/calUtils.js
+++ b/calendar/base/src/calUtils.js
@@ -2,56 +2,27 @@
  * 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/. */
 
 /* This file contains commonly used functions in a centralized place so that
  * various components (and other js scopes) don't need to replicate them. Note
  * that loading this file twice in the same scope will throw errors.
  */
 
-/* exported attendeeMatchesAddresses, calTryWrappedJSObject,
- *          LOG, WARN, ERROR, showError, sendMailTo,
- *          applyAttributeToMenuChildren, isPropertyValueSame,
- *          calIterateEmailIdentities, calGetString, getUUID
+/* exported calTryWrappedJSObject, LOG, WARN, ERROR, showError,
+ *          applyAttributeToMenuChildren, isPropertyValueSame, calGetString,
+ *          getUUID
  */
 
 ChromeUtils.import("resource:///modules/mailServices.js");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/Preferences.jsm");
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 
-/**
- * Check if the attendee object matches one of the addresses in the list. This
- * is useful to determine whether the current user acts as a delegate.
- *
- * @param aAttendee     The reference attendee object
- * @param addresses     The list of addresses
- * @return              True if there is a match
- */
-function attendeeMatchesAddresses(anAttendee, addresses) {
-    let attId = anAttendee.id;
-    if (!attId.match(/^mailto:/i)) {
-        // Looks like its not a normal attendee, possibly urn:uuid:...
-        // Try getting the email through the EMAIL property.
-        let emailProp = anAttendee.getProperty("EMAIL");
-        if (emailProp) {
-            attId = emailProp;
-        }
-    }
-
-    attId = attId.toLowerCase().replace(/^mailto:/, "");
-    for (let address of addresses) {
-        if (attId == address.toLowerCase().replace(/^mailto:/, "")) {
-            return true;
-        }
-    }
-
-    return false;
-}
 
 /**
  * Other functions
  */
 
 /**
  * Gets the value of a string in a .properties file from the calendar bundle
  *
@@ -212,33 +183,16 @@ function ASSERT(aCondition, aMessage, aC
  *
  * @param aMsg The message to be shown
  * @param aWindow The window to show the message in, or null for any window.
  */
 function showError(aMsg, aWindow=null) {
     Services.prompt.alert(aWindow, cal.calGetString("calendar", "genericErrorTitle"), aMsg);
 }
 
-function sendMailTo(aRecipient, aSubject, aBody, aIdentity) {
-    let msgParams = Components.classes["@mozilla.org/messengercompose/composeparams;1"]
-                              .createInstance(Components.interfaces.nsIMsgComposeParams);
-    let composeFields = Components.classes["@mozilla.org/messengercompose/composefields;1"]
-                                  .createInstance(Components.interfaces.nsIMsgCompFields);
-
-    composeFields.to = aRecipient;
-    composeFields.subject = aSubject;
-    composeFields.body = aBody;
-
-    msgParams.type = Components.interfaces.nsIMsgCompType.New;
-    msgParams.format = Components.interfaces.nsIMsgCompFormat.Default;
-    msgParams.composeFields = composeFields;
-    msgParams.identity = aIdentity;
-
-    MailServices.compose.OpenComposeWindowWithParams(null, msgParams);
-}
 
 /**
  * TODO: The following UI-related functions need to move somewhere different,
  * i.e calendar-ui-utils.js
  */
 
 
 /**
@@ -261,26 +215,8 @@ function isPropertyValueSame(aObjects, a
     }
     return true;
 }
 
 /**
  * END TODO: The above UI-related functions need to move somewhere different,
  * i.e calendar-ui-utils.js
  */
-
-/**
- * Iterates all email identities and calls the passed function with identity and account.
- * If the called function returns false, iteration is stopped.
- */
-function calIterateEmailIdentities(func) {
-    let accounts = MailServices.accounts.accounts;
-    for (let i = 0; i < accounts.length; ++i) {
-        let account = accounts.queryElementAt(i, Components.interfaces.nsIMsgAccount);
-        let identities = account.identities;
-        for (let j = 0; j < identities.length; ++j) {
-            let identity = identities.queryElementAt(j, Components.interfaces.nsIMsgIdentity);
-            if (!func(identity, account)) {
-                break;
-            }
-        }
-    }
-}
--- a/calendar/lightning/content/imip-bar.js
+++ b/calendar/lightning/content/imip-bar.js
@@ -1,14 +1,13 @@
 /* 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/. */
 
 ChromeUtils.import("resource://calendar/modules/calUtils.jsm");
-ChromeUtils.import("resource://calendar/modules/calItipUtils.jsm");
 ChromeUtils.import("resource://calendar/modules/calXMLUtils.jsm");
 ChromeUtils.import("resource://calendar/modules/ltnInvitationUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Preferences.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 /**
  * This bar lives inside the message window.
  * Its lifetime is the lifetime of the main thunderbird message window.
--- a/calendar/providers/gdata/modules/calUtilsShim.jsm
+++ b/calendar/providers/gdata/modules/calUtilsShim.jsm
@@ -74,8 +74,16 @@ if (typeof cal.window == "undefined") {
 }
 
 if (typeof cal.category == "undefined") {
     cal.category = {
         stringToArray: function(aStr) { return cal.categoriesStringToArray(aStr); },
         arrayToString: function(aArr) { return cal.categoriesArrayToString(aArr); }
     };
 }
+
+if (typeof cal.itip == "undefined") {
+    ChromeUtils.import("resource://calendar/modules/calItipUtils.jsm");
+}
+
+if (typeof cal.itip.isInvitation == "undefined") {
+    cal.itip.isInvitation = function(aItem) { return cal.isInvitation(aItem); };
+}
--- a/calendar/test/unit/test_calitiputils.js
+++ b/calendar/test/unit/test_calitiputils.js
@@ -1,14 +1,13 @@
 /* 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/. */
 
 ChromeUtils.import("resource://calendar/modules/calUtils.jsm");
-ChromeUtils.import("resource://calendar/modules/calItipUtils.jsm");
 ChromeUtils.import("resource://testing-common/mailnews/mailTestUtils.js");
 
 // tests for calItipUtils.jsm
 
 function run_test() {
     getMessageSender_test();
     getSequence_test();
     getStamp_test();