Bug 1212075 - CNs with comma are not getting quoted by validateRecipientList. r=philipp a=aleth
authorMakeMyDay <makemyday@gmx-topmail.de>
Sun, 10 Jan 2016 14:07:02 +0100
changeset 26631 a8a7e424d921914726205c6387032da08d2ff6e4
parent 26630 4236037e0175a3d9c01ceced73b2898800921a0b
child 26632 1c4e397973ecceba2fce8ddc24f996ac1e7a385b
push id1850
push userclokep@gmail.com
push dateWed, 08 Mar 2017 19:29:12 +0000
treeherdercomm-esr52@028df196b2d9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersphilipp, aleth
bugs1212075
Bug 1212075 - CNs with comma are not getting quoted by validateRecipientList. r=philipp a=aleth
calendar/base/modules/calUtils.jsm
calendar/test/unit/test_calutils.js
calendar/test/unit/test_ltninvitationutils.js
--- a/calendar/base/modules/calUtils.jsm
+++ b/calendar/base/modules/calUtils.jsm
@@ -249,33 +249,67 @@ var cal = {
      * 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 display names
-        let result = compFields.splitRecipients(aRecipients, false, {});
-        // Malformed e-mail addresses with display name in list will result in "Display name <>".
-        // So, we need an additional check on the e-mail address itself and sort out malformed
-        // entries from the previous list (both objects have always the same length)
-        if (result.length > 0) {
-            let resultAddress = compFields.splitRecipients(aRecipients, true, {});
-            result = result.filter((v, idx) => !!resultAddress[idx]);
+        // 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 cn = 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 (cn.match(/[\-\[\]{}()*+?.,;\\\^$|#\f\n\r\t\v]/)) {
+                        cn = '"' + cn.replace(/\\"|"/, "").trim() + '"';
+                    }
+                    list.push(cn + parts[2]);
+                    prefix = "";
+                }
+            } else if (member.length) {
+                // email address only
+                list.push(member);
+                prefix = "";
+            }
         }
-        return result.join(",");
+        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 
+     * @param aItem either calIAttendee or calIItemBase
      */
     isOpenInvitation: function cal_isOpenInvitation(aItem) {
         let wrappedItem = cal.wrapInstance(aItem, Components.interfaces.calIAttendee);
         if (!wrappedItem) {
             aItem = cal.getInvitedAttendee(aItem);
         }
         if (aItem) {
             switch (aItem.participationStatus) {
@@ -359,17 +393,17 @@ var cal = {
         }
         return invitedAttendee;
     },
 
     /**
      * 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  {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.
--- a/calendar/test/unit/test_calutils.js
+++ b/calendar/test/unit/test_calutils.js
@@ -2,19 +2,20 @@
  * 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/. */
 
 Components.utils.import("resource://calendar/modules/calUtils.jsm");
 
 function run_test() {
     getAttendeeEmail_test();
     getRecipientList_test();
+    prependMailTo_test();
     removeMailTo_test();
-    prependMailTo_test();
     resolveDelegation_test();
+    validateRecipientList_test();
 }
 
 // tests for calUtils.jsm
 
 function getAttendeeEmail_test() {
     let data = [{
         input: {id: "mailto:first.last@example.net", cn: "Last, First", email: null, useCn: true},
         expected: "\"Last, First\" <first.last@example.net>"
@@ -102,33 +103,37 @@ function getRecipientList_test() {
                 attendee.commonName = att.cn;
             }
             attendees.push(attendee);
         }
         equal(cal.getRecipientList(attendees), test.expected, "(test #" + i + ")");
     }
 };
 
+function prependMailTo_test() {
+    let data = [{input: "mailto:first.last@example.net", expected: "mailto:first.last@example.net"},
+                {input: "MAILTO:first.last@example.net", expected: "mailto:first.last@example.net"},
+                {input: "first.last@example.net", expected: "mailto:first.last@example.net"},
+                {input: "first.last.example.net", expected: "first.last.example.net"}];
+    let i = 0;
+    for (let test of data) {
+        i++;
+        equal(cal.prependMailTo(test.input), test.expected, "(test #" + i + ")");
+    }
+};
+
 function removeMailTo_test() {
     let data = [{input: "mailto:first.last@example.net", expected: "first.last@example.net"},
                 {input: "MAILTO:first.last@example.net", expected: "first.last@example.net"},
                 {input: "first.last@example.net", expected: "first.last@example.net"},
                 {input: "first.last.example.net", expected: "first.last.example.net"}];
+    let i = 0;
     for (let test of data) {
-        equal(cal.removeMailTo(test.input), test.expected)
-    }
-};
-
-function prependMailTo_test() {
-    let data = [{input: "mailto:first.last@example.net", expected: "mailto:first.last@example.net"},
-                {input: "MAILTO:first.last@example.net", expected: "mailto:first.last@example.net"},
-                {input: "first.last@example.net", expected: "mailto:first.last@example.net"},
-                {input: "first.last.example.net", expected: "first.last.example.net"}];
-    for (let test of data) {
-        equal(cal.prependMailTo(test.input), test.expected)
+        i++;
+        equal(cal.removeMailTo(test.input), test.expected, "(test #" + i + ")");
     }
 };
 
 function resolveDelegation_test() {
     let data = [{
         input: {
             attendee:
                 'ATTENDEE;DELEGATED-FROM="mailto:attendee2@example.net";CN="Attendee 1":mailto:at' +
@@ -221,8 +226,54 @@ function resolveDelegation_test() {
         }
         let attendee = cal.createAttendee();
         attendee.icalString = test.input.attendee;
         let result = cal.resolveDelegation(attendee, attendees);
         equal(result.delegatees, test.expected.delegatees, "(test #" + i + " - delegatees)");
         equal(result.delegators, test.expected.delegators, "(test #" + i + " - delegators)");
     }
 }
+
+function validateRecipientList_test() {
+    let data = [{
+        input: "first.last@example.net",
+        expected: "first.last@example.net"
+    }, {
+        input: "first last <first.last@example.net>",
+        expected: "first last <first.last@example.net>"
+    }, {
+        input: "\"last, first\" <first.last@example.net>",
+        expected: "\"last, first\" <first.last@example.net>"
+    }, {
+        input: "last, first <first.last@example.net>",
+        expected: "\"last, first\" <first.last@example.net>"
+    }, {
+        input: "\"last; first\" <first.last@example.net>",
+        expected: "\"last; first\" <first.last@example.net>"
+    }, {
+        input: "first1.last1@example.net,first2.last2@example.net,first3.last2@example.net",
+        expected: "first1.last1@example.net, first2.last2@example.net, first3.last2@example.net"
+    }, {
+        input: "first1.last1@example.net, first2.last2@example.net, first3.last2@example.net",
+        expected: "first1.last1@example.net, first2.last2@example.net, first3.last2@example.net"
+    }, {
+        input: "first1.last1@example.net, first2 last2 <first2.last2@example.net>, \"last3, first" +
+               "3\" <first3.last2@example.net>",
+        expected: "first1.last1@example.net, first2 last2 <first2.last2@example.net>, \"last3, fi" +
+               "rst3\" <first3.last2@example.net>"
+    }, {
+        input: "first1.last1@example.net, last2; first2 <first2.last2@example.net>, \"last3; first" +
+               "3\" <first3.last2@example.net>",
+        expected: "first1.last1@example.net, \"last2; first2\" <first2.last2@example.net>, \"last" +
+               "3; first3\" <first3.last2@example.net>"
+    }, {
+        input: "first1 last2 <first1.last1@example.net>, last2, first2 <first2.last2@example.net>" +
+               ", \"last3, first3\" <first3.last2@example.net>",
+        expected: "first1 last2 <first1.last1@example.net>, \"last2, first2\" <first2.last2@examp" +
+                  "le.net>, \"last3, first3\" <first3.last2@example.net>"
+    }];
+    let i = 0;
+    for (let test of data) {
+        i++;
+        equal(cal.validateRecipientList(test.input), test.expected,
+              "(test #" + i + ")");
+    }
+};
--- a/calendar/test/unit/test_ltninvitationutils.js
+++ b/calendar/test/unit/test_ltninvitationutils.js
@@ -456,17 +456,16 @@ add_task(function* getHeaderSection_test
         expected: "MIME-version: 1.0\r\n" +
                   "Return-path: no-reply@example.net\r\n" +
                   "From: Invitation sender <sender@example.net>\r\n" +
                   "Organization: Example Net\r\n" +
                   "To: recipient@example.net\r\n" +
                   "Subject: Invitation: test subject\r\n" +
                   "Cc: cc@example.net\r\n" +
                   "Bcc: bcc@example.net\r\n"
-    /* TODO: re-enable test case when Bug 1212075 lands
     }, {
         input: {
             toList: "rec1@example.net, Recipient 2 <rec2@example.net>, \"Rec, 3\" <rec3@example.net>",
             subject: "Invitation: test subject",
             identity: {
                 fullName: "\"invitation, sender\"",
                 email: "sender@example.net",
                 replyTo: "no-reply@example.net",
@@ -476,17 +475,16 @@ add_task(function* getHeaderSection_test
         expected: "MIME-version: 1.0\r\n" +
                   "Return-path: no-reply@example.net\r\n" +
                   "From: \"invitation, sender\" <sender@example.net>\r\n" +
                   "Organization: Example Net\r\n" +
                   "To: rec1@example.net, Recipient 2 <rec2@example.net>,\r\n \"Rec, 3\" <rec3@example.net>\r\n" +
                   "Subject: Invitation: test subject\r\n" +
                   "Cc: cc1@example.net, Cc 2 <cc2@example.net>, \"Cc, 3\" <cc3@example.net>\r\n" +
                   "Bcc: bcc1@example.net, BCc 2 <bcc2@example.net>, \"Bcc, 3\"\r\n <bcc3@example.net>\r\n"
-    */
     }, {
         input: {
             toList: "recipient@example.net",
             subject: "Invitation: test subject",
             identity: {
                 email: "sender@example.net"}},
         expected: "MIME-version: 1.0\r\n" +
                   "From: sender@example.net\r\n" +