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

13

我正在处理一些电子邮件头数据,对于收件人(to:)、发件人(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>

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

有人能建议最好的方法吗?

我已经尝试了逗号分隔,这个方法本来可以工作,但在第二个示例中,姓氏被放在了首位。我想这种方法可能可行,如果我在拆分后检查每个元素并查看它是否包含“@”或 “<”/“>”,如果不包含,则可以假定下一个元素是名字。这是一种好的方法吗?还有其他格式的地址我忽略了吗?


更新:也许我应该稍微澄清一下,基本上我要做的就是将包含多个地址的字符串拆分成包含以任何格式发送的地址的单个字符串。我有自己的方法来验证和从地址中提取信息,只是对我来说难以找到最好的方法来分隔每个地址。

以下是我想出来的解决方案:

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);
}

上述代码生成了各个地址,我可以在后续操作中进一步处理这些地址。


你能控制头部数据以何种方式传递给你吗? - Ian Jacobs
我使用了这个方法,非常有帮助,只想指出一个我必须进行的微调。在检查循环是否已经到达字符串结尾时,我必须将逗号索引设置为字符串长度,或者更具体地说,大于2。如果(commaIdx < 2)检查会在输入字符串是没有逗号的单个电子邮件地址时向List<>添加重复项。 - A. Wilson
可能是如何解析格式为“名称<电子邮件>”的字符串的重复问题。 - Michael Freidgeim
13个回答

7

有一个内部的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
这段代码对上面给出的示例无效 - 它会抛出 System.FormatException: 指定的字符串不符合电子邮件地址所需的格式。可能是因为“Last, First name@domain.com”地址没有在名称周围加引号。 - Simon Green
这段代码完美地处理了符合RFC 2822格式的电子邮件地址!我很惊讶它没有被标记为最佳答案 - 因为它利用了框架自带的功能。你可以查看MailAddressParser源代码来了解实现细节。 - Vikhram

4

您的第二个电子邮件示例不是有效地址,因为它包含一个不在引号字符串内的逗号。要有效,它应该像这样:"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;
}

2
对我来说, System.Net.Mail.MailAddressCollection 的答案是迄今为止最好的... - Simon Green
1
我必须再次发表评论,因为我们也遇到了这样的情况:我们必须处理因人名未引用而无效的电子邮件地址。MailAddressCollection不支持它们。我们在MsgReader.Mime.Message.Headers.To字段中遇到了类似这样的字符串。我不知道这些无效地址是如何进入其中的,但是它们确实存在,因此我们必须为其做好准备。 - Simon Green

4

针对这个问题,实际上并没有简单的解决方案。我建议制作一个小型状态机,逐个字符读取并进行处理。正如你所说的,通过逗号分隔不总是奏效。

状态机可以让你涵盖所有可能性。我相信还有许多其他可能性,你尚未看到。例如:"First Last"。

搜索RFC以发现所有可能性。对不起,我不知道编号。可能有多个,因为这是发展的事物。


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);
    }
}

这种方法有几个缺点。其中一个是它不能验证字符串的有效性。如果您的字符串中有任何不符合所选格式之一的字符,则这些字符将被忽略。另一个缺点是接受的格式都在一个地方表达。您无法添加新的格式而不更改庞大的正则表达式。


我发现这非常有帮助。我将完整的代码作为答案包含在内,以便格式正确。感谢Michael Perry。 - Vince

2
清晰简明的解决方案是使用 MailAddressCollection,它与 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
}

2

这个问题没有通用简单的解决方案。你需要的RFC是RFC2822,其中描述了电子邮件地址的所有可能配置。你能得到的最好的答案是实现一个基于状态的分词器,遵循RFC中指定的规则,以确保正确性。


不需要验证电子邮件,只需提取重要信息,无论其格式如何。 - Jason Miesionczek
不要阅读RFC,它很混乱,你的代码会变得过于复杂,并支持99.9%的电子邮件程序无法处理的事情。例如,根据RFC,您可以使用带有空格、双引号甚至控制字符的电子邮件地址(这是在本地名称/邮箱部分而不是显示名称中)。在我看来,RFC很混乱。 - eselk
相反,所有的邮件服务器都能处理所有特殊字符。如果不能,那就是个bug。RFC并不混乱 - 它们正是有效电子邮件地址的定义。仅因为一些客户端存在bug,并不意味着你永远不会遇到奇怪的地址。 - Panagiotis Kanavos

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);
}

我知道现在有点晚了,但是应该是“if (commaIdx > atIdx && atIdx > lastComma)”而不是“if (commaIdx > atIdx && atIdx > 0)”,对吗? - user2298337
晚了吗?这个问题已经超过10年了 :) - Jason Miesionczek

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();
    }

0

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

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

将匹配:Last, First test@test.comLast, First <test@test.com>First last test@test.comFirst Last <test@test.com>。您可以在正则表达式的末尾添加另一个可选匹配项,以捕获尖括号中的电子邮件地址后面的First, Last <name@domain.com>, name@domain.com的最后一段。

希望这能有所帮助!

编辑:

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

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


1
这个正则表达式无法验证所有可能的电子邮件地址格式。 - Scott Dorman
正确。再读一遍我的帖子,注意我没有说它会验证地址,只是匹配它。要匹配“所有可能”的电子邮件地址的正则表达式(根据规格)非常长和复杂。由于他的问题不是关于验证电子邮件,而是解析一个字符串,这可能会起到相当不错的作用。 - Anders

0

以下是我会怎么做:

  • 您可以尽可能地规范化数据,即摆脱<和>符号以及“.com.”后面的所有逗号。您需要分隔名字的逗号。
  • 在去除额外符号后,将每个分组的电子邮件记录作为字符串放入列表中。如果需要,您可以使用.com来确定在哪里拆分字符串。
  • 在将电子邮件地址列表放入字符串列表中后,您可以再使用空格作为分隔符进一步拆分电子邮件地址。
  • 最后一步是确定名字、姓氏等。这可以通过检查三个组成部分来完成:逗号表示姓氏;点表示实际地址;其余则是名字。如果没有逗号,则第一个是名字,第二个是姓氏等。

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

问题在于'.com'。可能会出现任何顶级域名/国家代码。 - Jason Miesionczek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接