Bug 858337 - Implement JS header parsing in JSMime, r=irving, sr=Standard8

This commit is contained in:
Joshua Cranmer 2014-06-04 09:51:20 -05:00
Родитель c6bf838db2
Коммит 1d526917d7
12 изменённых файлов: 307 добавлений и 1777 удалений

Просмотреть файл

@ -238,7 +238,6 @@
#include "nsStreamConverter.h"
#include "nsMimeObjectClassAccess.h"
#include "nsMimeConverter.h"
#include "nsMsgHeaderParser.h"
///////////////////////////////////////////////////////////////////////////////
// mime emitter includes
@ -672,11 +671,9 @@ NS_DEFINE_NAMED_CID(NS_MSGDB_SERVICE_CID);
NS_GENERIC_FACTORY_CONSTRUCTOR(nsMimeObjectClassAccess)
NS_GENERIC_FACTORY_CONSTRUCTOR(nsMimeConverter)
NS_GENERIC_FACTORY_CONSTRUCTOR(nsStreamConverter)
NS_GENERIC_FACTORY_CONSTRUCTOR(nsMsgHeaderParser)
NS_DEFINE_NAMED_CID(NS_MIME_OBJECT_CLASS_ACCESS_CID);
NS_DEFINE_NAMED_CID(NS_MIME_CONVERTER_CID);
NS_DEFINE_NAMED_CID(NS_MSGHEADERPARSER_CID);
NS_DEFINE_NAMED_CID(NS_MAILNEWS_MIME_STREAM_CONVERTER_CID);
////////////////////////////////////////////////////////////////////////////////
@ -1004,7 +1001,6 @@ const mozilla::Module::CIDEntry kMailNewsCIDs[] = {
// Mime Entries
{ &kNS_MIME_OBJECT_CLASS_ACCESS_CID, false, NULL, nsMimeObjectClassAccessConstructor },
{ &kNS_MIME_CONVERTER_CID, false, NULL, nsMimeConverterConstructor },
{ &kNS_MSGHEADERPARSER_CID, false, NULL, nsMsgHeaderParserConstructor },
{ &kNS_MAILNEWS_MIME_STREAM_CONVERTER_CID, false, NULL, nsStreamConverterConstructor },
{ &kNS_HTML_MIME_EMITTER_CID, false, NULL, nsMimeHtmlDisplayEmitterConstructor},
{ &kNS_XML_MIME_EMITTER_CID, false, NULL, nsMimeXmlEmitterConstructor},
@ -1229,7 +1225,6 @@ const mozilla::Module::ContractIDEntry kMailNewsContracts[] = {
// Mime Entries
{ NS_MIME_OBJECT_CONTRACTID, &kNS_MIME_OBJECT_CLASS_ACCESS_CID },
{ NS_MIME_CONVERTER_CONTRACTID, &kNS_MIME_CONVERTER_CID },
{ NS_MAILNEWS_MIME_HEADER_PARSER_CONTRACTID, &kNS_MSGHEADERPARSER_CID },
{ NS_MAILNEWS_MIME_STREAM_CONVERTER_CONTRACTID, &kNS_MAILNEWS_MIME_STREAM_CONVERTER_CID },
{ NS_MAILNEWS_MIME_STREAM_CONVERTER_CONTRACTID1, &kNS_MAILNEWS_MIME_STREAM_CONVERTER_CID },
{ NS_MAILNEWS_MIME_STREAM_CONVERTER_CONTRACTID2, &kNS_MAILNEWS_MIME_STREAM_CONVERTER_CID },

Просмотреть файл

@ -139,6 +139,6 @@ function run_test() {
"test5@foo.invalid,test1@com.invalid,test2@com.invalid,test3@com.invalid");
// test bug 254519 rfc 2047 encoding
checkPopulate("=?iso-8859-1?Q?Sure=F6name=2C_Forename__Dr=2E?= <pb@bieringer.invalid>",
"\"Sure\u00F6name, Forename Dr.\" <pb@bieringer.invalid>");
checkPopulate("=?iso-8859-1?Q?Sure=F6name=2C_Forename_Dr=2E?= <pb@bieringer.invalid>",
"\"Sure\u00F6name, Forename Dr.\" <pb@bieringer.invalid>");
};

Просмотреть файл

@ -38,17 +38,17 @@ const splitRecipientsTests =
{ recipients: "A Group:Ed Jones <c@a.invalid>,joe@where.invalid,John <jdoe@one.invalid>;",
emailAddressOnly: false,
count: 3,
result: [ "A Group:Ed Jones <c@a.invalid>", "joe@where.invalid", "John <jdoe@one.invalid>" ]
result: [ "Ed Jones <c@a.invalid>", "joe@where.invalid", "John <jdoe@one.invalid>" ]
},
{ recipients: 'mygroup:;, empty:;, foo@foo.invalid, othergroup:bar@foo.invalid, bar2@foo.invalid;, y@y.invalid, empty:;',
emailAddressOnly: true,
count: 7,
result: [ "mygroup:;", "empty:;", "foo@foo.invalid", "othergroup:bar@foo.invalid", "bar2@foo.invalid", "y@y.invalid", "empty:;" ]
count: 4,
result: [ "foo@foo.invalid", "bar@foo.invalid", "bar2@foo.invalid", "y@y.invalid" ]
},
{ recipients: 'Undisclosed recipients:;;;;;;;;;;;;;;;;,,,,,,,,,,,,,,,,',
emailAddressOnly: true,
count: 1,
result: ["\"Undisclosed recipients:;\""]
count: 0,
result: []
},
{ recipients: 'a@xxx.invalid; b@xxx.invalid',
emailAddressOnly: true,
@ -73,22 +73,22 @@ const splitRecipientsTests =
{ recipients: "A (this: is, a comment;) <a.invalid>; g: (this: is, <a> comment;) C <c.invalid>, d.invalid;",
emailAddressOnly: false,
count: 3,
result: [ 'A (this: is, a comment;) <a.invalid>', 'g: (this: is, <a> comment;) C <c.invalid>', "d.invalid" ]
result: [ 'A (this: is, a comment;) <a.invalid>', '(this: is, <a> comment;) C <c.invalid>', "d.invalid <>" ]
},
{ recipients: 'Mary Smith <mary@x.invalid>, extra:;, group:jdoe@example.invalid; Who? <one@y.invalid>; <boss@nil.invalid>, "Giant; \\"Big\\" Box" <sysservices@example.invalid>, ',
emailAddressOnly: false,
count: 6,
result: [ "Mary Smith <mary@x.invalid>", "extra:;", "group:jdoe@example.invalid;", "Who? <one@y.invalid>", "boss@nil.invalid", 'Giant; \"Big\" Box <sysservices@example.invalid>' ]
count: 5,
result: [ "Mary Smith <mary@x.invalid>", "jdoe@example.invalid", "Who? <one@y.invalid>", "boss@nil.invalid", 'Giant; \"Big\" Box <sysservices@example.invalid>' ]
},
{ recipients: 'Undisclosed recipients: a@foo.invalid ;;extra:;',
emailAddressOnly: true,
count: 2,
result: [ '\"Undisclosed recipients: a\"@foo.invalid ;', 'extra:;' ]
count: 1,
result: [ 'a@foo.invalid' ]
},
{ recipients: 'Undisclosed recipients:;;extra:a@foo.invalid;',
emailAddressOnly: true,
count: 2,
result: [ '\"Undisclosed recipients:;\"', 'extra:a@foo.invalid;' ]
count: 1,
result: [ 'a@foo.invalid' ]
},
{ recipients: "",
emailAddressOnly: false,

Просмотреть файл

@ -35,9 +35,4 @@
#define NS_MIME_OBJECT_CONTRACTID \
"@mozilla.org/messenger/mimeobject;1"
// {932C53A5-F398-11d2-82B7-444553540002}
#define NS_MSGHEADERPARSER_CID \
{ 0x932c53a5, 0xf398, 0x11d2, \
{ 0x82, 0xb7, 0x44, 0x45, 0x53, 0x54, 0x0, 0x2 } }
#endif // nsMessageMimeCID_h__

Просмотреть файл

@ -2,6 +2,7 @@
* 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:///modules/jsmime.jsm");
Components.utils.import("resource:///modules/mimeParser.jsm");
Components.utils.import("resource:///modules/XPCOMUtils.jsm");
@ -36,5 +37,229 @@ MimeHeaders.prototype = {
}
};
var components = [MimeHeaders];
// These are prototypes for nsIMsgHeaderParser implementation
var Mailbox = {
toString: function () {
return this.name ? this.name + " <" + this.email + ">" : this.email;
}
};
var EmailGroup = {
toString: function () {
return this.name + ": " + [x.toString() for (x of this.group)].join(", ");
}
};
function MimeAddressParser() {
}
MimeAddressParser.prototype = {
classDescription: "Mime message header parser implementation",
classID: Components.ID("96bd8769-2d0e-4440-963d-22b97fb3ba77"),
contractID: "@mozilla.org/messenger/headerparser;1",
QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIMsgHeaderParser]),
parseEncodedHeader: function (aHeader, aCharset, aPreserveGroups, count) {
aHeader = aHeader || "";
let value = MimeParser.parseHeaderField(aHeader,
MimeParser.HEADER_ADDRESS | MimeParser.HEADER_OPTION_ALL_I18N, aCharset);
return this._fixArray(value, aPreserveGroups, count);
},
parseDecodedHeader: function (aHeader, aPreserveGroups, count) {
aHeader = aHeader || "";
let value = MimeParser.parseHeaderField(aHeader, MimeParser.HEADER_ADDRESS);
return this._fixArray(value, aPreserveGroups, count);
},
// A helper method for parse*Header that takes into account the desire to
// preserve group and also tweaks the output to support the prototypes for the
// XPIDL output.
_fixArray: function (addresses, preserveGroups, count) {
function resetPrototype(obj, prototype) {
let prototyped = Object.create(prototype);
for (var key in obj)
prototyped[key] = obj[key];
return prototyped;
}
let outputArray = [];
for (let element of addresses) {
if ('group' in element) {
// Fix up the prototypes of the group and the list members
element = resetPrototype(element, EmailGroup);
element.group = element.group.map(e => resetPrototype(e, Mailbox));
// Add to the output array
if (preserveGroups)
outputArray.push(element);
else
outputArray = outputArray.concat(element.group);
} else {
element = resetPrototype(element, Mailbox);
outputArray.push(element);
}
}
if (count)
count.value = outputArray.length;
return outputArray;
},
makeMimeHeader: function (addresses, length) {
// Don't output any necessary continuations, so make line length as large as
// possible first.
let options = {
softMargin: 900,
hardMargin: 900,
useASCII: false // We don't want RFC 2047 encoding here.
};
let handler = {
value: "",
deliverData: function (str) { this.value += str; },
deliverEOF: function () {}
};
let emitter = new jsmime.headeremitter.makeStreamingEmitter(handler,
options);
emitter.addAddresses(addresses);
emitter.finish(true);
return handler.value.replace(/\r\n( |$)/g, '');
},
extractFirstName: function (aHeader) {
let address = this.parseDecodedHeader(aHeader, false)[0];
return address.name || address.email;
},
removeDuplicateAddresses: function (aAddrs, aOtherAddrs) {
// This is actually a rather complicated algorithm, especially if we want to
// preserve group structure. Basically, we use a set to identify which
// headers we have seen and therefore want to remove. To work in several
// various forms of edge cases, we need to normalize the entries in that
// structure.
function normalize(email) {
// XXX: This algorithm doesn't work with IDN yet. It looks like we have to
// convert from IDN then do lower case, but I haven't confirmed yet.
return email.toLowerCase();
}
// The filtration function, which removes email addresses that are
// duplicates of those we have already seen.
function filterAccept(e) {
if ('email' in e) {
// If we've seen the address, don't keep this one; otherwise, add it to
// the list.
let key = normalize(e.email);
if (allAddresses.has(key))
return false;
allAddresses.add(key);
} else {
// Groups -> filter out all the member addresses.
e.group = e.group.filter(filterAccept);
}
return true;
}
// First, collect all of the emails to forcibly delete.
let allAddresses = Set();
for (let element of this.parseDecodedHeader(aOtherAddrs, false)) {
allAddresses.add(normalize(element.email));
}
// The actual data to filter
let filtered = this.parseDecodedHeader(aAddrs, true).filter(filterAccept);
return this.makeMimeHeader(filtered);
},
makeMailboxObject: function (aName, aEmail) {
let object = Object.create(Mailbox);
object.name = aName;
object.email = aEmail;
return object;
},
makeGroupObject: function (aName, aMembers) {
let object = Object.create(EmailGroup);
object.name = aName;
object.members = aMembers;
return object;
},
makeFromDisplayAddress: function (aDisplay, count) {
// The basic idea is to split on every comma, so long as there is a
// preceding @.
let output = [];
while (aDisplay.length) {
let at = aDisplay.indexOf('@');
let comma = aDisplay.indexOf(',', at + 1);
let addr;
if (comma > 0) {
addr = aDisplay.substr(0, comma);
aDisplay = aDisplay.substr(comma + 1);
} else {
addr = aDisplay;
aDisplay = "";
}
output.push(this._makeSingleAddress(addr.trimLeft()));
}
if (count)
count.value = output.length;
return output;
},
/// Construct a single email address from a name <local@domain> token.
_makeSingleAddress: function (aDisplayName) {
if (aDisplayName.contains('<')) {
let lbracket = aDisplayName.lastIndexOf('<');
let rbracket = aDisplayName.lastIndexOf('>');
// If there are multiple spaces between the display name and the bracket,
// strip off only a single space.
return this.makeMailboxObject(
lbracket == 0 ? '' : aDisplayName.slice(0, lbracket - 1),
aDisplayName.slice(lbracket + 1, rbracket));
} else {
return this.makeMailboxObject('', aDisplayName);
}
},
// What follows is the deprecated API that will be removed shortly.
parseHeadersWithArray: function (aHeader, aAddrs, aNames, aFullNames) {
let addrs = [], names = [], fullNames = [];
let allAddresses = this.parseEncodedHeader(aHeader, undefined, false);
// Don't index the dummy empty address.
if (aHeader.trim() == "")
allAddresses = [];
for (let address of allAddresses) {
addrs.push(address.email);
names.push(address.name || null);
fullNames.push(address.toString());
}
aAddrs.value = addrs;
aNames.value = names;
aFullNames.value = fullNames;
return allAddresses.length;
},
extractHeaderAddressMailboxes: function (aLine) {
return [addr.email for (addr of this.parseDecodedHeader(aLine))].join(", ");
},
extractHeaderAddressNames: function (aLine) {
return [addr.name || addr.email for
(addr of this.parseDecodedHeader(aLine))].join(", ");
},
extractHeaderAddressName: function (aLine) {
let addrs = [addr.name || addr.email for
(addr of this.parseDecodedHeader(aLine))];
return addrs.length == 0 ? "" : addrs[0];
},
makeMimeAddress: function (aName, aEmail) {
let object = this.makeMailboxObject(aName, aEmail);
return this.makeMimeHeader([object]);
},
};
var components = [MimeHeaders, MimeAddressParser];
var NSGetFactory = XPCOMUtils.generateNSGetFactory(components);

Просмотреть файл

@ -178,6 +178,13 @@ var MimeParser = {
// Parameters for parseHeaderField
/**
* Parse the header as if it were unstructured.
*
* This results in the same string if no other options are specified. If other
* options are specified, this causes the string to be modified appropriately.
*/
HEADER_UNSTRUCTURED: 0x00,
/**
* Parse the header as if it were in the form text; attr=val; attr=val.
*
@ -185,6 +192,28 @@ var MimeParser = {
* headers used by MIME as opposed to messages.
*/
HEADER_PARAMETER: 0x02,
/**
* Parse the header as if it were a sequence of mailboxes.
*/
HEADER_ADDRESS: 0x03,
/**
* This decodes parameter values according to RFC 2231.
*
* This flag means nothing if HEADER_PARAMETER is not specified.
*/
HEADER_OPTION_DECODE_2231: 0x10,
/**
* This decodes the inline encoded-words that are in RFC 2047.
*/
HEADER_OPTION_DECODE_2047: 0x20,
/**
* This converts the header from a raw string to proper Unicode.
*/
HEADER_OPTION_ALLOW_RAW: 0x40,
/// Convenience for all three of the above.
HEADER_OPTION_ALL_I18N: 0x70,
/**
* Parse a header field according to the specification given by flags.
@ -192,23 +221,36 @@ var MimeParser = {
* Permissible flags begin with one of the HEADER_* flags, which may be or'd
* with any of the HEADER_OPTION_* flags to modify the result appropriately.
*
* If a charset-aware option (HEADER_OPTION_DECODE_2231 or
* HEADER_OPTION_DECODE_2047) is used, the charset parameter, if present, is
* the default charset to assume if no charset is found. If this parameter is
* not present, UTF-8 will be assumed to be the default. Furthermore, if any
* of these options are used, resulting strings will be normalized to full
* Unicode.
* If the option HEADER_OPTION_ALLOW_RAW is passed, the charset parameter, if
* present, is the charset to fallback to if the header is not decodable as
* UTF-8 text. If HEADER_OPTION_ALLOW_RAW is passed but the charset parameter
* is not provided, then no fallback decoding will be done. If
* HEADER_OPTION_ALLOW_RAW is not passed, then no attempt will be made to
* convert charsets.
*
* @param text The value of a MIME or message header to parse.
* @param flags A set of flags that controls interpretation of the header.
* @param charset A default charset to assume if no information may be found.
*/
parseHeaderField: function MimeParser_parseHeaderField(text, flags, charset) {
// If we have a raw string, convert it to Unicode first
if (flags & MimeParser.HEADER_OPTION_ALLOW_RAW)
text = jsmime.headerparser.convert8BitHeader(text, charset);
// The low 4 bits indicate the type of the header we are parsing. All of the
// higher-order bits are flags.
switch (flags & 0x0f) {
case MimeParser.HEADER_UNSTRUCTURED:
if (flags & MimeParser.HEADER_OPTION_DECODE_2047)
text = jsmime.headerparser.decodeRFC2047Words(text);
return text;
case MimeParser.HEADER_PARAMETER:
return jsmime.headerparser.parseParameterHeader(text, false, false);
return jsmime.headerparser.parseParameterHeader(text,
(flags & MimeParser.HEADER_OPTION_DECODE_2047) != 0,
(flags & MimeParser.HEADER_OPTION_DECODE_2231) != 0);
case MimeParser.HEADER_ADDRESS:
return jsmime.headerparser.parseAddressingHeader(text,
(flags & MimeParser.HEADER_OPTION_DECODE_2047) != 0);
default:
throw "Illegal type of header field";
}

Просмотреть файл

@ -65,7 +65,6 @@ SOURCES += [
'mimeunty.cpp',
'nsMimeConverter.cpp',
'nsMimeObjectClassAccess.cpp',
'nsMsgHeaderParser.cpp',
'nsSimpleMimeConverterStub.cpp',
'nsStreamConverter.cpp',
]

Просмотреть файл

@ -1,2 +1,4 @@
component {d1258011-f391-44fd-992e-c6f4b461a42f} mimeJSComponents.js
component {96bd8769-2d0e-4440-963d-22b97fb3ba77} mimeJSComponents.js
contract @mozilla.org/messenger/mimeheaders;1 {d1258011-f391-44fd-992e-c6f4b461a42f}
contract @mozilla.org/messenger/headerparser;1 {96bd8769-2d0e-4440-963d-22b97fb3ba77}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -1,77 +0,0 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/. */
/********************************************************************************************************
Interface for parsing RFC-822 addresses.
*********************************************************************************************************/
#ifndef nsMSGRFCPARSER_h__
#define nsMSGRFCPARSER_h__
#include "msgCore.h"
#include "nsIMsgHeaderParser.h" /* include the interface we are going to support */
#include "nsIMimeConverter.h"
#include "comi18n.h"
#include "nsCOMArray.h"
#include "nsCOMPtr.h"
#include "nsStringGlue.h"
/*
* RFC-822 parser
*/
class nsMsgHeaderParser: public nsIMsgHeaderParser
{
public:
nsMsgHeaderParser();
virtual ~nsMsgHeaderParser();
/* this macro defines QueryInterface, AddRef and Release for this class */
NS_DECL_ISUPPORTS
NS_DECL_NSIMSGHEADERPARSER
/**
* Given a string which contains a list of Header addresses, parses it into
* their component names and mailboxes.
*
* @param aLine The header line to parse.
* @param aNames A string of the names in the header line. The names
* are separated by null-terminators.
* This param may be null if the caller does not want
* this part of the result.
* @param aAddresses A string of the addresses in the header line. The
* addresses are separated by null-terminators.
* This param may be null if the caller does not want
* this part of the result.
* @param aNumAddresses The number of addresses in the header. If this is
* negative, there has been an error parsing the
* header.
*/
static nsresult ParseHeaderAddresses(const char *aLine, char **aNames,
char **aAddresses, uint32_t *aNumAddresses);
static nsresult UnquotePhraseOrAddr(const char *line, bool preserveIntegrity,
char **result);
static nsresult UnquotePhraseOrAddrWString(const char16_t *line,
bool preserveIntegrity,
char16_t **result);
};
class MsgAddressObject MOZ_FINAL : public msgIAddressObject
{
public:
NS_DECL_ISUPPORTS
NS_DECL_MSGIADDRESSOBJECT
MsgAddressObject(const nsAString &aName, const nsAString &aEmail);
private:
nsString mName;
nsString mEmail;
};
#endif /* nsMSGRFCPARSER_h__ */

Просмотреть файл

@ -44,28 +44,20 @@ function run_test() {
// More complicated examples drawn from RFC 2822
["\"Joe Q. Public\" <john.q.public@example.com>,Test <\"abc!x.yz\"@foo.invalid>, Test <test@[xyz!]>,\"Giant; \\\"Big\\\" Box\" <sysservices@example.net>",
"john.q.public@example.com, \"abc!x.yz\"@foo.invalid, test@[xyz!], sysservices@example.net",
"\"Joe Q. Public\", Test, Test, \"Giant; \\\"Big\\\" Box\"",
"Joe Q. Public, Test, Test, Giant; \"Big\" Box",
// extractHeaderAddressName returns unquoted names, hence the difference.
"Joe Q. Public" ],
// Bug 549931
["Undisclosed recipients:;",
"\"Undisclosed recipients:;\"", // Mailboxes
"\"Undisclosed recipients:;\"", // Address Names
"Undisclosed recipients:;"] // Address Name
"", // Mailboxes
"", // Address Names
""] // Address Name
];
// this used to cause memory read overruns
let addresses = {}, names = {}, fullAddresses = {};
MailServices.headerParser.parseHeadersWithArray("\" \"@a a;b", addresses, names, fullAddresses);
// This checks that the mime header parser doesn't march past the end
// of strings with ":;" in them. The second ":;" is required to force the
// parser to keep going.
do_check_eq(MailServices.headerParser.extractHeaderAddressMailboxes(
"undisclosed-recipients:;\0:; foo <ghj@veryveryveryverylongveryveryveryveryinvalidaddress.invalid>"),
"undisclosed-recipients:;");
do_check_eq(MailServices.headerParser.extractHeaderAddressMailboxes("<a;a@invalid"), "");
// Test - empty strings

Просмотреть файл

@ -25,7 +25,7 @@ function run_test() {
expectedResult: "foo bar <test@foo.invalid>" },
{ addrs: "foo bar <test@foo.invalid>",
otherAddrs: "foo bar <test@foo.invalid>",
expectedResult: null },
expectedResult: "" },
{ addrs: "foo bar <test@foo.invalid>, abc@foo.invalid",
otherAddrs: "foo bar <test@foo.invalid>",
expectedResult: "abc@foo.invalid" },
@ -44,7 +44,17 @@ function run_test() {
expectedResult: "\u00F6foo <ghj@foo.invalid>" },
{ addrs: "foo\u00D0 bar <foo@bar.invalid>, \u00F6foo <ghj@foo.invalid>, foo\u00D0 bar <foo@bar.invalid>",
otherAddrs: "\u00F6foo <ghj@foo.invalid>",
expectedResult: "foo\u00D0 bar <foo@bar.invalid>" }
expectedResult: "foo\u00D0 bar <foo@bar.invalid>" },
// Test email groups
{ addrs: "A group: foo bar <foo@bar.invalid>, foo <ghj@foo.invalid>;",
otherAddrs: "foo <ghj@foo.invalid>",
expectedResult: "A group: foo bar <foo@bar.invalid>;" },
{ addrs: "A group: foo bar <foo@bar.invalid>, foo <ghj@foo.invalid>;",
otherAddrs: "foo bar <ghj@foo.invalid>",
expectedResult: "A group: foo bar <foo@bar.invalid>;" },
{ addrs: "A group: foo bar <foo@bar.invalid>;, foo <ghj@foo.invalid>",
otherAddrs: "foo <foo@bar.invalid>",
expectedResult: "A group: ; , foo <ghj@foo.invalid>" },
];
// Test - empty strings