views:

35

answers:

3

The MailAddress class doesn't provide a way to parse a string with multiple emails. The MailAddressCollection class does, but it only accepts CSV and does not allow commas inside of quotes. I am looking for a text processor to create a collection of emails from user input without these restrictions.

The processor should take comma- or semicolon-separated values in any of these formats:

"First Middle Last" <[email protected]>
First Middle Last <[email protected]>
[email protected]
"Last, First" <[email protected]>
A: 

Unfortunately, I haven't found an easy way to keep the commas in the quotations, but here's the simplest answer:

private static readonly Regex ValueInsideQuotes = new Regex("\"(.+?)\"");

/// <summary>
/// Extracts email addresses in the following formats:
/// "Tom W. Smith" &lt;[email protected]&gt;
/// Tom W. Smith &lt;[email protected]&gt;
/// [email protected]
/// Multiple emails can be separated by a comma or semicolon. If a comma is inside of quotes, it will be removed.
/// </summary>
/// <param name="value">Collection of emails in the accepted formats.</param>
/// <returns>
/// A collection of <see cref="System.Net.Mail.MailAddress"/>es.
/// </returns>
/// <exception cref="ArgumentException">Thrown if the value is null, empty, or just whitespace.</exception>
/// <exception cref="FormatException">Thrown if any of the emails is invalid.</exception>
public static IList<MailAddress> ExtractEmailAddresses(this string value)
{
    if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("The arg cannot be null, empty, or just whitespace.", "value");

    // Remove commas inside of quotes
    value = ValueInsideQuotes.Replace(value, m => m.Value.Replace(",", string.Empty));
    value = value.Replace(';', ',');
    var mailAddressCollection = new MailAddressCollection { value };
    return mailAddressCollection;
}

Here are some tests that this method passes:

[TestMethod]
public void ExtractEmails_SingleEmail_Matches()
{
    string value = "[email protected]";
    var expected = new List<MailAddress>
        {
            new MailAddress("[email protected]"),
        };

    var actual = value.ExtractEmailAddresses();

    CollectionAssert.AreEqual(expected, actual.ToList());
}

[TestMethod()]
public void ExtractEmails_JustEmailCSV_Matches()
{
    string value = "[email protected]; [email protected]";
    var expected = new List<MailAddress>
        {
            new MailAddress("[email protected]"),
            new MailAddress("[email protected]"),
        };

    var actual = value.ExtractEmailAddresses();

    CollectionAssert.AreEqual(expected, actual.ToList());
}

[TestMethod]
public void ExtractEmails_MultipleWordNameThenEmailSemicolonSV_Matches()
{
    string value = "a a a <[email protected]>; a a a <[email protected]>";
    var expected = new List<MailAddress>
        {
            new MailAddress("a a a <[email protected]>"),
            new MailAddress("a a a <[email protected]>"),
        };

    var actual = value.ExtractEmailAddresses();

    CollectionAssert.AreEqual(expected, actual.ToList());
}

[TestMethod]
public void ExtractEmails_JustEmailsSemicolonSV_Matches()
{
    string value = "[email protected]; [email protected]";
    var expected = new List<MailAddress>
        {
            new MailAddress("[email protected]"),
            new MailAddress("[email protected]"),
        };

    var actual = value.ExtractEmailAddresses();

    CollectionAssert.AreEqual(expected, actual.ToList());
}

[TestMethod]
public void ExtractEmails_NameInQuotesWithCommaThenEmailsCSV_MatchesWithCommaRemoved()
{
    string value = "\"a, a\" <[email protected]>; \"a, a\" <[email protected]>";
    var expected = new List<MailAddress>
        {
            new MailAddress("\"a a\" <[email protected]>"),
            new MailAddress("\"a a\" <[email protected]>"),
        };

    var actual = value.ExtractEmailAddresses();

    CollectionAssert.AreEqual(expected, actual.ToList());
}

[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ExtractEmails_EmptyString_Throws()
{
    string value = string.Empty;

    var actual = value.ExtractEmailAddresses();
}

[TestMethod]
[ExpectedException(typeof(FormatException))]
public void ExtractEmails_NonEmailValue_Throws()
{
    string value = "a";

    var actual = value.ExtractEmailAddresses();
}
Pat
A: 

The open source library DotNetOpenMail (old) has an EmailAddress class that can parse almost all legal forms of email addresses, and an EmailAddressCollection. You could start there.

edosoft
A: 

After asking a related question, I became aware of a better method:

/// <summary>
/// Extracts email addresses in the following formats:
/// "Tom W. Smith" &lt;[email protected]&gt;
/// "Smith, Tom" &lt;[email protected]&gt;
/// Tom W. Smith &lt;[email protected]&gt;
/// [email protected]
/// Multiple emails can be separated by a comma or semicolon.
/// Watch out for <see cref="FormatException"/>s when enumerating.
/// </summary>
/// <param name="value">Collection of emails in the accepted formats.</param>
/// <returns>
/// A collection of <see cref="System.Net.Mail.MailAddress"/>es.
/// </returns>
/// <exception cref="ArgumentException">Thrown if the value is null, empty, or just whitespace.</exception>
public static IEnumerable<MailAddress> ExtractEmailAddresses(this string value)
{
    if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("The arg cannot be null, empty, or just whitespace.", "value");

    // Remove commas inside of quotes
    value = value.Replace(';', ',');
    var emails = value.SplitWhilePreservingQuotedValues(',');
    var mailAddresses = emails.Select(email => new MailAddress(email));
    return mailAddresses;
}

/// <summary>
/// Splits the string while preserving quoted values (i.e. instances of the delimiter character inside of quotes will not be split apart).
/// Trims leading and trailing whitespace from the individual string values.
/// Does not include empty values.
/// </summary>
/// <param name="value">The string to be split.</param>
/// <param name="delimiter">The delimiter to use to split the string, e.g. ',' for CSV.</param>
/// <returns>A collection of individual strings parsed from the original value.</returns>
public static IEnumerable<string> SplitWhilePreservingQuotedValues(this string value, char delimiter)
{
    Regex csvPreservingQuotedStrings = new Regex(string.Format("(\"[^\"]*\"|[^{0}])+", delimiter));
    var values =
        csvPreservingQuotedStrings.Matches(value)
        .Cast<Match>()
        .Select(m => m.Value.Trim())
        .Where(v => !string.IsNullOrWhiteSpace(v));
    return values;
}

This method passes the following tests:

[TestMethod]
public void ExtractEmails_SingleEmail_Matches()
{
    string value = "[email protected]";
    var expected = new List<MailAddress>
        {
            new MailAddress("[email protected]"),
        };

    var actual = value.ExtractEmailAddresses();

    CollectionAssert.AreEqual(expected, actual.ToList());
}

[TestMethod()]
public void ExtractEmails_JustEmailCSV_Matches()
{
    string value = "[email protected]; [email protected]";
    var expected = new List<MailAddress>
        {
            new MailAddress("[email protected]"),
            new MailAddress("[email protected]"),
        };

    var actual = value.ExtractEmailAddresses();

    CollectionAssert.AreEqual(expected, actual.ToList());
}

[TestMethod]
public void ExtractEmails_MultipleWordNameThenEmailSemicolonSV_Matches()
{
    string value = "a a a <[email protected]>; a a a <[email protected]>";
    var expected = new List<MailAddress>
        {
            new MailAddress("a a a <[email protected]>"),
            new MailAddress("a a a <[email protected]>"),
        };

    var actual = value.ExtractEmailAddresses();

    CollectionAssert.AreEqual(expected, actual.ToList());
}

[TestMethod]
public void ExtractEmails_JustEmailsSemicolonSV_Matches()
{
    string value = "[email protected]; [email protected]";
    var expected = new List<MailAddress>
        {
            new MailAddress("[email protected]"),
            new MailAddress("[email protected]"),
        };

    var actual = value.ExtractEmailAddresses();

    CollectionAssert.AreEqual(expected, actual.ToList());
}

[TestMethod]
public void ExtractEmails_NameInQuotesWithCommaThenEmailsCSV_Matches()
{
    string value = "\"a, a\" <[email protected]>; \"a, a\" <[email protected]>";
    var expected = new List<MailAddress>
        {
            new MailAddress("\"a, a\" <[email protected]>"),
            new MailAddress("\"a, a\" <[email protected]>"),
        };

    var actual = value.ExtractEmailAddresses();

    CollectionAssert.AreEqual(expected, actual.ToList());
}

[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ExtractEmails_EmptyString_Throws()
{
    string value = string.Empty;

    var actual = value.ExtractEmailAddresses();
}

[TestMethod]
[ExpectedException(typeof(FormatException))]
public void ExtractEmails_NonEmailValue_ThrowsOnEnumeration()
{
    string value = "a";

    var actual = value.ExtractEmailAddresses();

    actual.ToList();
}
Pat