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

所以我正在使用一些电子邮件标题数据,并且对于:,from:,cc:和bcc:字段,电子邮件地址可以用多种不同的方式表示:

First Last  Last, First  name@domain.com 

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

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

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

有人可以建议最好的方法吗?

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


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

这是我想出的解决方案:

 String str = "Last, First , name@domain.com, First Last , \"First Last\" "; List addresses = new List(); int atIdx = 0; int commaIdx = 0; int lastComma = 0; for (int c = 0; c  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); } 

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

对此没有一个简单的解决方案。 我建议制作一个小型的状态机来读取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( @"((?\w*), (?\w*) <(?\w*)@(?\w*\.\w*)>)|" + @"((?\w*) (?\w*) <(?\w*)@(?\w*\.\w*)>)|" + @"((?\w*)@(?\w*\.\w*))"); string input = "First, Last , name@domain.com, First Last "; MatchCollection matches = emailAddress.Matches(input); List
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); } }

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

有一个内部的System.Net.Mail.MailAddressParser类,它有一个方法ParseMultipleAddresses ,可以完全按照你的意愿执行。 您可以通过reflection或通过调用MailMessage.To.Add方法直接访问它,该方法接受电子邮件列表字符串。

 private static IEnumerable 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)parseMultipleAddressesMethod.Invoke(null, new object[0]); } private static IEnumerable ParseAddress(string addresses) { MailMessage message = new MailMessage(); message.To.Add(addresses); return new List(message.To); //new List, because we don't want to hold reference on Disposable object } 

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

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

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

 public List SplitAddresses(string addresses) { var result = new List(); 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, First Last , \"First Last\" "; List addresses = new List(); 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); } 

我是这样做的:

  • 您可以尝试尽可能标准化数据,即除去<和>符号以及’.com’之后的所有逗号。 您将需要用于分隔名字和姓氏的逗号。
  • 在删除额外符号后,将每个分组的电子邮件记录作为字符串放在列表中。 如果需要,您可以使用.com来确定拆分字符串的位置。
  • 在字符串列表中有电子邮件地址列表后,您可以使用空格作为分隔符进一步拆分电子邮件地址。
  • 最后一步是确定名字是什么,姓氏是什么等等。这可以通过检查3个组件来完成:逗号,表示它是姓氏; 一个 。 这表示实际地址; 剩下的就是名字。 如果没有逗号,则第一个名称是第一个,姓氏是第二个,等等。

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

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

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

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

希望这有点帮助!

编辑:

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

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

//基于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 SplitEmailList(string delimList) { delimList = delimList.Replace("\"", string.Empty); Regex re = new Regex( @"((?\w*), (?\w*) <(?[a-zA-Z_0-9\.\+\-]+)@(?\w*\.\w*)>)|" + @"((?\w*) (?\w*) <(?[a-zA-Z_0-9\.\+\-]+)@(?\w*\.\w*)>)|" + @"((?[a-zA-Z_0-9\.\+\-]+)@(?\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无法解析,我只是不会担心它。

我还决定我只对电子邮件地址感兴趣,而不是显示名称,因为显示名称是如此有问题且难以定义,而电子邮件地址我可以validation。 所以我使用MailAddress来validation我的解析。

我把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"" " + "," + @", testto1@domain.com, testto2@domain.com" + "," + @", test3@domain.com" + "," + @"""""Yes, this is valid""""@[emails are hard to parse!]" + "," + @"First, Last , name@domain.com, First Last " ; static string[] results1; static string[] expectedResults1; Establish context = () => { expectedResults1 = new string[] { @"Lastname", @"Firstname ", @"", @"testto1@domain.com", @"testto2@domain.com", @"", @"test3@domain.com", @"Yes", @"this is valid@[emails are hard to parse!]", @"First", @"Last ", @"name@domain.com", @"First Last " }; }; Because of = () => { results1 = CSVProcessor.GetFieldsFromString(recipientHeaderToParse1); }; It should_parse_the_email_parts_properly = () => results1.ShouldBeLike(expectedResults1); } 

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

  public List ParseAddresses(string field) { var tokens = field.Split(','); var addresses = new List(); var tokenBuffer = new List(); 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(); } 

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

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