解析电子邮件地址字符串的最佳方法

时间:2009-01-16 18:54:14

标签: c# .net parsing

所以我正在处理一些电子邮件标题数据,而对于:,from:,cc:和bcc:字段,电子邮件地址可以用多种方式表示:

First Last <name@domain.com>
Last, First <name@domain.com>
name@domain.com

这些变体可以以任何顺序出现在同一条消息中,所有这些变体都以逗号分隔的字符串形式出现:

First, Last <name@domain.com>, name@domain.com, First Last <name@domain.com>

我一直试图想出一种方法将这个字符串解析为每个人的单独的名字,姓氏,电子邮件(如果只提供了一个电子邮件地址,则省略名称)。

有人可以提出最佳方法吗?

我试图在逗号上拆分,除了在第一个放置姓氏的第二个例子中之外,它会起作用。我想这个方法可以工作,如果我拆分后,我检查每个元素,看看它是否包含'@'或'&lt;'/'&gt;',如果没有,那么可以假设下一个元素是第一个名字。这是解决这个问题的好方法吗?我是否忽略了地址可能存在的另一种格式?


更新:也许我应该澄清一点,基本上我要做的就是将包含多个地址的字符串拆分成包含地址的单个字符串,无论发送的格式是什么。我有自己的方法来验证和从地址中提取信息,我找出分离每个地址的最佳方法真是太棘手了。

这是我想出的解决方案:

String str = "Last, First <name@domain.com>, name@domain.com, First Last <name@domain.com>, \"First Last\" <name@domain.com>";

List<string> addresses = new List<string>();
int atIdx = 0;
int commaIdx = 0;
int lastComma = 0;
for (int c = 0; c < str.Length; c++)
{
    if (str[c] == '@')
        atIdx = c;

    if (str[c] == ',')
        commaIdx = c;

    if (commaIdx > atIdx && atIdx > 0)
    {
        string temp = str.Substring(lastComma, commaIdx - lastComma);
        addresses.Add(temp);
        lastComma = commaIdx;
        atIdx = commaIdx;
    }

    if (c == str.Length -1)
    {
        string temp = str.Substring(lastComma, str.Legth - lastComma);
        addresses.Add(temp);
    }
}

if (commaIdx < 2)
{
    // if we get here we can assume either there was no comma, or there was only one comma as part of the last, first combo
    addresses.Add(str);
}

上面的代码生成了我可以进一步处理的各个地址。

13 个答案:

答案 0 :(得分:5)

有一个内部System.Net.Mail.MailAddressParser类,其方法ParseMultipleAddresses完全符合您的要求。您可以通过反射或调用MailMessage.To.Add方法直接访问它,该方法接受电子邮件列表字符串。

private static IEnumerable<MailAddress> ParseAddress(string addresses)
{
    var mailAddressParserClass = Type.GetType("System.Net.Mail.MailAddressParser");
    var parseMultipleAddressesMethod = mailAddressParserClass.GetMethod("ParseMultipleAddresses", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
    return (IList<MailAddress>)parseMultipleAddressesMethod.Invoke(null, new object[0]);
}


    private static IEnumerable<MailAddress> ParseAddress(string addresses)
    {
        MailMessage message = new MailMessage();
        message.To.Add(addresses);
        return new List<MailAddress>(message.To); //new List, because we don't want to hold reference on Disposable object
    }

答案 1 :(得分:4)

这并不是一个简单的解决方案。我建议制作一个小型的状态机来读取char-by-char并以这种方式完成工作。就像你说的那样,用逗号分割并不总是有效。

状态机将允许您涵盖所有可能性。我相信还有很多其他你还没见过的人。例如:“First Last”

寻找关于此的RFC以发现所有可能性。对不起,我不知道这个号码。可能有多种,因为这是一种发展的东西。

答案 2 :(得分:4)

冒着创建两个问题的风险,您可以创建一个与您的任何电子邮件格式匹配的正则表达式。使用“|”分离这一个正则表达式中的格式。然后你可以在你的输入字符串上运行它并拉出所有的匹配。

public class Address
{
    private string _first;
    private string _last;
    private string _name;
    private string _domain;

    public Address(string first, string last, string name, string domain)
    {
        _first = first;
        _last = last;
        _name = name;
        _domain = domain;
    }

    public string First
    {
        get { return _first; }
    }

    public string Last
    {
        get { return _last; }
    }

    public string Name
    {
        get { return _name; }
    }

    public string Domain
    {
        get { return _domain; }
    }
}

[TestFixture]
public class RegexEmailTest
{
    [Test]
    public void TestThreeEmailAddresses()
    {
        Regex emailAddress = new Regex(
            @"((?<last>\w*), (?<first>\w*) <(?<name>\w*)@(?<domain>\w*\.\w*)>)|" +
            @"((?<first>\w*) (?<last>\w*) <(?<name>\w*)@(?<domain>\w*\.\w*)>)|" +
            @"((?<name>\w*)@(?<domain>\w*\.\w*))");
        string input = "First, Last <name@domain.com>, name@domain.com, First Last <name@domain.com>";

        MatchCollection matches = emailAddress.Matches(input);
        List<Address> addresses =
            (from Match match in matches
             select new Address(
                 match.Groups["first"].Value,
                 match.Groups["last"].Value,
                 match.Groups["name"].Value,
                 match.Groups["domain"].Value)).ToList();
        Assert.AreEqual(3, addresses.Count);

        Assert.AreEqual("Last", addresses[0].First);
        Assert.AreEqual("First", addresses[0].Last);
        Assert.AreEqual("name", addresses[0].Name);
        Assert.AreEqual("domain.com", addresses[0].Domain);

        Assert.AreEqual("", addresses[1].First);
        Assert.AreEqual("", addresses[1].Last);
        Assert.AreEqual("name", addresses[1].Name);
        Assert.AreEqual("domain.com", addresses[1].Domain);

        Assert.AreEqual("First", addresses[2].First);
        Assert.AreEqual("Last", addresses[2].Last);
        Assert.AreEqual("name", addresses[2].Name);
        Assert.AreEqual("domain.com", addresses[2].Domain);
    }
}

这种方法有几个缺点。一个是它不验证字符串。如果字符串中的任何字符不符合您选择的格式,则只会忽略这些字符。另一个是接受的格式都在一个地方表达。如果不更改单片正则表达式,则无法添加新格式。

答案 3 :(得分:3)

您的第二封电子邮件示例不是有效地址,因为它包含的逗号不在带引号的字符串中。为了有效,它应该是:"Last, First"<name@domain.com>

至于解析,如果你想要一些非常严格的东西,你可以使用System.Net.Mail.MailAddressCollection

如果您只想将输入拆分为单独的电子邮件字符串,则以下代码应该有效。它不是很严格,但会在带引号的字符串中处理逗号,并在输入包含未闭合的引号时抛出异常。

public List<string> SplitAddresses(string addresses)
{
    var result = new List<string>();

    var startIndex = 0;
    var currentIndex = 0;
    var inQuotedString = false;

    while (currentIndex < addresses.Length)
    {
        if (addresses[currentIndex] == QUOTE)
        {
            inQuotedString = !inQuotedString;
        }
        // Split if a comma is found, unless inside a quoted string
        else if (addresses[currentIndex] == COMMA && !inQuotedString)
        {
            var address = GetAndCleanSubstring(addresses, startIndex, currentIndex);
            if (address.Length > 0)
            {
                result.Add(address);
            }
            startIndex = currentIndex + 1;
        }
        currentIndex++;
    }

    if (currentIndex > startIndex)
    {
        var address = GetAndCleanSubstring(addresses, startIndex, currentIndex);
        if (address.Length > 0)
        {
            result.Add(address);
        }
    }

    if (inQuotedString)
        throw new FormatException("Unclosed quote in email addresses");

    return result;
}

private string GetAndCleanSubstring(string addresses, int startIndex, int currentIndex)
{
    var address = addresses.Substring(startIndex, currentIndex - startIndex);
    address = address.Trim();
    return address;
}

答案 4 :(得分:2)

这没有通用的简单解决方案。您想要的RFC是RFC2822,它描述了电子邮件地址的所有可能配置。您将获得的最佳结果正确是实现遵循RFC中指定规则的基于状态的标记生成器。

答案 5 :(得分:2)

这是我想出的解决方案:

String str = "Last, First <name@domain.com>, name@domain.com, First Last <name@domain.com>, \"First Last\" <name@domain.com>";

List<string> addresses = new List<string>();
int atIdx = 0;
int commaIdx = 0;
int lastComma = 0;
for (int c = 0; c < str.Length; c++)
{
if (str[c] == '@')
    atIdx = c;

if (str[c] == ',')
    commaIdx = c;

if (commaIdx > atIdx && atIdx > 0)
{
    string temp = str.Substring(lastComma, commaIdx - lastComma);
    addresses.Add(temp);
    lastComma = commaIdx;
    atIdx = commaIdx;
}

if (c == str.Length -1)
{
    string temp = str.Substring(lastComma, str.Legth - lastComma);
    addresses.Add(temp);
}
}

if (commaIdx < 2)
{
    // if we get here we can assume either there was no comma, or there was only one comma as part of the last, first combo
    addresses.Add(str);
}

答案 6 :(得分:0)

您可以使用正则表达式尝试将其分开,试试这个人:

^(?<name1>[a-zA-Z0-9]+?),? (?<name2>[a-zA-Z0-9]+?),? (?<address1>[a-zA-Z0-9.-_<>]+?)$

将匹配:Last, First test@test.com; Last, First <test@test.com>; First last test@test.com; First Last <test@test.com>。您可以在结尾处的正则表达式中添加另一个可选匹配项,以便在包含在斜角括号中的电子邮件地址后选取First, Last <name@domain.com>, name@domain.com的最后一段。

希望这有点帮助!

编辑:

当然,您可以为每个部分添加更多字符以接受引用等任何正在读取的格式。正如sjbotha所提到的,这可能很难,因为提交的字符串不一定是设置格式。

This link可以为您提供有关使用正则表达式匹配和验证电子邮件地址的更多信息。

答案 7 :(得分:0)

我将如何做到这一点:

  • 您可以尝试标准化数据 尽可能地摆脱 诸如&lt;和&gt;符号 和之后的所有逗号 '.COM'。你需要逗号 将第一个和最后一个分开 名。
  • 在删除额外符号后,将每个分组的电子邮件放入 以字符串形式记录在列表中。您 可以使用.com来确定在哪里 如果需要,拆分字符串。
  • 在您拥有字符串列表中的电子邮件地址列表之后 然后可以进一步拆分电子邮件 仅使用空格的地址 除界。
  • 最后一步是确定名字是什么,是什么 姓氏等等。这将完成 通过检查3个组件:a 逗号,这表明它 是姓;一个 。这将 表明实际地址;和 剩下的就是名字。 如果没有逗号,那么第一个 名字是第一,姓氏是第二, 等等

    我不知道这是否是最简洁的解决方案,但它可以工作,不需要任何高级编程技术

答案 8 :(得分:0)

// 根据Michael Perry的回答 * //需要处理first.last @ domain.com,first_last @ domain.com和相关语法 //还会查找这些电子邮件语法中的名字和姓氏

public class ParsedEmail
{
    private string _first;
    private string _last;
    private string _name;
    private string _domain;

    public ParsedEmail(string first, string last, string name, string domain)
    {
        _name = name;
        _domain = domain;

        // first.last@domain.com, first_last@domain.com etc. syntax
        char[] chars = { '.', '_', '+', '-' };
        var pos = _name.IndexOfAny(chars);

        if (string.IsNullOrWhiteSpace(_first) && string.IsNullOrWhiteSpace(_last) && pos > -1)
        {
            _first = _name.Substring(0, pos);
            _last = _name.Substring(pos+1);
        }
    }

    public string First
    {
        get { return _first; }
    }

    public string Last
    {
        get { return _last; }
    }

    public string Name
    {
        get { return _name; }
    }

    public string Domain
    {
        get { return _domain; }
    }

    public string Email
    {
        get
        {
            return Name + "@" + Domain;
        }
    }

    public override string ToString()
    {
        return Email;
    }

    public static IEnumerable<ParsedEmail> SplitEmailList(string delimList)
    {
        delimList = delimList.Replace("\"", string.Empty);

        Regex re = new Regex(
                    @"((?<last>\w*), (?<first>\w*) <(?<name>[a-zA-Z_0-9\.\+\-]+)@(?<domain>\w*\.\w*)>)|" +
                    @"((?<first>\w*) (?<last>\w*) <(?<name>[a-zA-Z_0-9\.\+\-]+)@(?<domain>\w*\.\w*)>)|" +
                    @"((?<name>[a-zA-Z_0-9\.\+\-]+)@(?<domain>\w*\.\w*))");


        MatchCollection matches = re.Matches(delimList);

        var parsedEmails =
                   (from Match match in matches
                    select new ParsedEmail(
                            match.Groups["first"].Value,
                            match.Groups["last"].Value,
                            match.Groups["name"].Value,
                            match.Groups["domain"].Value)).ToList();

        return parsedEmails;

    }


}

答案 9 :(得分:0)

我决定在两个限制条件下画一条线:

  1. To和Cc标头必须是csv可解析字符串。
  2. 任何MailAddress都无法解析,我只是不会担心它。
  3. 我还决定我只对电子邮件地址感兴趣,而不是显示名称,因为显示名称是如此有问题且难以定义,而电子邮件地址我可以验证。所以我使用MailAddress来验证我的解析。

    我将To和Cc标题视为csv字符串,再次,任何不可解析的东西我都不担心。

    private string GetProperlyFormattedEmailString(string emailString)
        {
            var emailStringParts = CSVProcessor.GetFieldsFromString(emailString);
    
            string emailStringProcessed = "";
    
            foreach (var part in emailStringParts)
            {
                try
                {
                    var address = new MailAddress(part);
                    emailStringProcessed += address.Address + ",";
                }
                catch (Exception)
                {
                    //wasn't an email address
                    throw;
                }
            }
    
            return emailStringProcessed.TrimEnd((','));
        }
    

    修改

    进一步的研究表明我的假设是好的。阅读规范RFC 2822几乎表明To,Cc和Bcc字段是csv-parseable字段。所以是的,它很难,并且有很多陷阱,就像任何csv解析一样,但是如果你有一个可靠的方法来解析csv字段(在Microsoft.VisualBasic.FileIO命名空间中TextFieldParser,那就是我用于此),那么你是金。

    修改2

    显然他们不需要是有效的CSV字符串...引号真的搞砸了。所以你的csv解析器必须是容错的。我尝试解析字符串,如果失败,它会删除所有引号并再次尝试:

    public static string[] GetFieldsFromString(string csvString)
        {
            using (var stringAsReader = new StringReader(csvString))
            {
                using (var textFieldParser = new TextFieldParser(stringAsReader))
                {
                    SetUpTextFieldParser(textFieldParser, FieldType.Delimited, new[] {","}, false, true);
    
                    try
                    {
                        return textFieldParser.ReadFields();
                    }
                    catch (MalformedLineException ex1)
                    {
                        //assume it's not parseable due to double quotes, so we strip them all out and take what we have
                        var sanitizedString = csvString.Replace("\"", "");
    
                        using (var sanitizedStringAsReader = new StringReader(sanitizedString))
                        {
                            using (var textFieldParser2 = new TextFieldParser(sanitizedStringAsReader))
                            {
                                SetUpTextFieldParser(textFieldParser2, FieldType.Delimited, new[] {","}, false, true);
    
                                try
                                {
                                    return textFieldParser2.ReadFields().Select(part => part.Trim()).ToArray();
                                }
                                catch (MalformedLineException ex2)
                                {
                                    return new string[] {csvString};
                                }
                            }
                        }
                    }
                }
            }
        }
    

    它无法处理的一件事是在电子邮件中引用帐户,即“Monkey Header”@ stupidemailaddresses.com。

    这是测试:

    [Subject(typeof(CSVProcessor))]
    public class when_processing_an_email_recipient_header
    {
        static string recipientHeaderToParse1 = @"""Lastname, Firstname"" <firstname_lastname@domain.com>" + "," +
                                               @"<testto@domain.com>, testto1@domain.com, testto2@domain.com" + "," +
                                               @"<testcc@domain.com>, test3@domain.com" + "," +
                                               @"""""Yes, this is valid""""@[emails are hard to parse!]" + "," +
                                               @"First, Last <name@domain.com>, name@domain.com, First Last <name@domain.com>"
                                               ;
    
        static string[] results1;
        static string[] expectedResults1;
    
        Establish context = () =>
        {
            expectedResults1 = new string[]
            {
                @"Lastname",
                @"Firstname <firstname_lastname@domain.com>",
                @"<testto@domain.com>",
                @"testto1@domain.com",
                @"testto2@domain.com",
                @"<testcc@domain.com>",
                @"test3@domain.com",
                @"Yes",
                @"this is valid@[emails are hard to parse!]",
                @"First",
                @"Last <name@domain.com>",
                @"name@domain.com",
                @"First Last <name@domain.com>"
            };
        };
    
        Because of = () =>
        {
            results1 = CSVProcessor.GetFieldsFromString(recipientHeaderToParse1);
        };
    
        It should_parse_the_email_parts_properly = () => results1.ShouldBeLike(expectedResults1);
    }
    

答案 10 :(得分:0)

这就是我想出的。它假定有效的电子邮件地址必须只有一个“@”符号:

    public List<MailAddress> ParseAddresses(string field)
    {
        var tokens = field.Split(',');
        var addresses = new List<string>();

        var tokenBuffer = new List<string>();

        foreach (var token in tokens)
        {
            tokenBuffer.Add(token);

            if (token.IndexOf("@", StringComparison.Ordinal) > -1)
            {
                addresses.Add( string.Join( ",", tokenBuffer));
                tokenBuffer.Clear();
            }
        }

        return addresses.Select(t => new MailAddress(t)).ToList();
    }

答案 11 :(得分:0)

干净简洁的解决方案是使用MailAddressCollection

var collection = new MailAddressCollection();
collection.Add(addresses);

此方法解析用冒号,分隔的地址列表,并根据RFC对其进行验证。如果地址无效,它将抛出FormatException。正如其他帖子中所建议的那样,如果您需要处理无效的地址,则必须自己处理或解析值,否则建议使用.NET提供的内容而不使用反射。

示例:

var collection = new MailAddressCollection();
collection.Add("Joe Doe <doe@example.com>, postmaster@example.com");

foreach (var addr in collection)
{
  // addr.DisplayName, addr.User, addr.Host
}

答案 12 :(得分:-2)

我在Java中使用以下正则表达式从符合RFC的电子邮件地址中获取电子邮件字符串:

[A-Za-z0-9]+[A-Za-z0-9._-]+@[A-Za-z0-9]+[A-Za-z0-9._-]+[.][A-Za-z0-9]{2,3}