Лучший способ проанализировать строку адресов электронной почты
поэтому я работаю с некоторыми данными заголовка электронной почты, а для полей 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>
Я пытался придумать способ разобрать эту строку на отдельные имя, фамилию, электронную почту для каждого человека (опуская имя, если только адрес электронной почты предусмотренный.)
кто-нибудь может предложить лучший способ сделать это?
Я попытался разделить запятые, которые будут работать, за исключением второго примера, где фамилия помещается первой. Я полагаю, что этот метод может работать, если после разделения я изучаю каждый элемент и вижу, содержит ли он " @ "или"", если это не так, то можно предположить, что следующий элемент является первым именем. Это хороший способ подойти к этому? Я пропустил другой формат, адрес может быть внутрь?
UPDATE: возможно, я должен немного прояснить, в основном все, что я хочу сделать, это разбить строку, содержащую несколько адресов, на отдельные строки, содержащие адрес в любом формате, в котором он был отправлен. У меня есть свои методы проверки и извлечения информации из адреса, мне было просто сложно найти лучший способ разделить каждый адрес.
вот решение я придумал для выполнения это:
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);
}
приведенный выше код генерирует отдельные адреса, которые я могу обработать дальше по строке.
12 ответов
на самом деле это не простое решение. Я бы рекомендовал сделать небольшую государственную машину, которая читает 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);
}
}
существует несколько нижних сторон этого подхода. Во-первых, он не проверяет строку. Если у вас есть какие-либо символы в строке, которые не соответствуют одному из выбранных вами форматов, эти символы просто игнорируются. Другое заключается в том, что все принятые форматы выражены в одном месте. Нельзя добавлять новые форматы без изменения монолитного регулярного выражения.
внутреннее 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
}
ваш 2-й пример электронной почты не является допустимым адресом, поскольку он содержит запятую, которая не находится в цитируемой строке. Чтобы быть действительным, это должно быть так: "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);
}
вот как я бы сделал это:
- вы можете попытаться стандартизировать данные как можно больше, т. е. избавиться от такие вещи, как символы и все запятые после '.ком.' Вы нужны запятые что разделяет первое и последнее имена.
- после избавления от дополнительных символов, поместите каждый сгруппированный адрес электронной почты запись в списке в виде строки. Вы можно использовать .com, чтобы определить, где разделить строку, если нужно.
- после того, как у вас есть список из адресов электронной почты в списке строк, вы затем можно дополнительно разделить электронную почту адреса, использующие только пробелы делиметр.
- последний шаг-определить, что такое первое имя, что такое
фамилия, и т. д. Это будет сделано
проверив 3 компонента для: a
запятая, которая бы указывала, что
это фамилия; a . что бы
указать фактический адрес; и
все что осталось-имя.
Если запятая отсутствует, то первая
имя первого, фамилия второй,
так далее.
Я не знаю, является ли это самым кратким решением, но оно будет работать и не требует каких-либо передовых методов программирования
вы можете использовать регулярные выражения, чтобы попытаться отделить это, попробуйте этого парня:
^(?<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
после адреса электронной почты, заключенного в угловые скобки.
надеюсь, это поможет!
EDIT:
и, конечно, вы можете добавить больше символов в каждый из разделов, чтобы принять котировки и т. д независимо от формата, в котором читается. Как упоминал sjbotha, это может быть сложно, поскольку строка, которая представляется, не обязательно в заданном формате.
этой ссылке может предоставить вам дополнительную информацию о сопоставлении и проверке адресов электронной почты с помощью регулярных выражений.
// на основе ответа Майкла Перри * // необходимо обрабатывать 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;
}
}
я решил, что собираюсь провести линию на песке при двух ограничениях:
- заголовки To и Cc должны быть CSV-синтаксическими строками.
- все, что 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-анализируемыми полями. Так что да, это сложно, и есть много gotchas, как с любым CSV-разбором, но если у вас есть надежный способ анализа полей csv (которые TextFieldParser в Microsoft.На языке VisualBasic.Пространство имен FileIO есть, и это то, что я использовал для этого), то вы Золотой.
Изменить 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};
}
}
}
}
}
}
}
это не будет обрабатывать процитированные учетные записи в электронной почте, т. е. "заголовок обезьяны" @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);
}
вот что я придумал. Предполагается, что действительный адрес электронной почты должен иметь один и только один знак"@":
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();
}
Я использую следующее регулярное выражение в Java для получения строки электронной почты из RFC-совместимого адреса электронной почты:
[A-Za-z0-9]+[A-Za-z0-9._-]+@[A-Za-z0-9]+[A-Za-z0-9._-]+[.][A-Za-z0-9]{2,3}