簡體   English   中英

解析電子郵件地址字符串的最佳方法

[英]Best way to parse string of email addresses

所以我正在處理一些電子郵件標題數據,對於to:,from:,cc:和bcc:fields,電子郵件地址可以用多種不同的方式表示:

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

上面的代碼生成了我可以進一步處理的各個地址。

有一個內部的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
    }

對此沒有一個簡單的解決方案。 我建議制作一個小型的狀態機來讀取char-by-char並以這種方式完成工作。 就像你說的,用逗號分割並不總是有效。

狀態機將允許您涵蓋所有可能性。 我相信還有很多其他你還沒見過的人。 例如:“First Last”

尋找關於此的RFC以發現所有可能性。 對不起,我不知道這個號碼。 可能有多種,因為這是一種發展的東西。

冒着創建兩個問題的風險,您可以創建一個匹配任何電子郵件格式的正則表達式。 使用“|” 分離這一個正則表達式中的格式。 然后,您可以在輸入字符串上運行它並拉出所有匹配項。

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

這種方法有幾個缺點。 一個是它不驗證字符串。 如果字符串中的任何字符不符合您選擇的格式,則只會忽略這些字符。 另一個是接受的格式都在一個地方表達。 如果不更改單片正則表達式,則無法添加新格式。

您的第二個電子郵件示例不是有效地址,因為它包含的逗號不在帶引號的字符串中。 要有效,它應該是: "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;
}

對此沒有通用的簡單解決方案。 您想要的RFC是RFC2822 ,它描述了電子郵件地址的所有可能配置。 你會得到,這將是正確的 ,最好是實現遵循RFC中指定的規則基於狀態的標記生成器。

以下是我想出的解決方案:

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

// 根據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;

    }


}

我決定在兩個限制條件下在沙灘上划一條線:

  1. To和Cc頭必須是csv可解析字符串。
  2. 任何MailAddress都無法解析,我只是不擔心它。

我還決定我只對電子郵件地址感興趣,而不是顯示名稱,因為顯示名稱是如此有問題且難以定義,而電子郵件地址我可以驗證。 所以我使用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);
}

您可以使用正則表達式嘗試將其分開,試試這個人:

^(?<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 last First, Last <name@domain.com>, name@domain.com

希望這有點幫助!

編輯:

當然,您可以為每個部分添加更多字符以接受引用等任何正在讀取的格式。正如sjbotha所提到的,這可能很難,因為提交的字符串不一定是設置格式。

此鏈接可以為您提供有關使用正則表達式匹配和驗證電子郵件地址的更多信息。

我將如何做到這一點:

  • 您可以嘗試盡可能地標准化數據,即刪除諸如<和>符號之類的內容以及“.com”之后的所有逗號。 您將需要用於分隔名字和姓氏的逗號。
  • 刪除額外符號后,將每個分組的電子郵件記錄作為字符串放在列表中。 如果需要,您可以使用.com來確定拆分字符串的位置。
  • 在字符串列表中有電子郵件地址列表后,您可以使用空格作為分隔符進一步拆分電子郵件地址。
  • 最后一步是確定名字是什么,姓氏是什么等等。這可以通過檢查3個組件來完成:逗號,表示它是姓氏; 一種 。 這表示實際地址; 剩下的就是名字。 如果沒有逗號,則第一個名稱是第一個,姓氏是第二個,等等。

    我不知道這是否是最簡潔的解決方案,但它可以工作,不需要任何高級編程技術

這就是我想出來的。 它假定有效的電子郵件地址必須包含一個且只有一個“@”符號:

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

簡潔明了的解決方案是使用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
}

我在Java中使用以下正則表達式從RFC兼容的電子郵件地址中獲取電子郵件字符串:

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM