Лучший способ разобрать строку адресов электронной почты
Итак, я работаю с некоторыми данными заголовка электронной почты, а для полей from:, cc:, и bcc: адрес электронной почты может быть выражен несколькими способами:
First Last <[email protected]>
Last, First <[email protected]>
[email protected]
И эти вариации могут отображаться в одном и том же сообщении в любом порядке в одной строке, разделенной запятой:
First, Last <[email protected]>, [email protected], First Last <[email protected]>
Я пытаюсь разобраться с этой строкой в отдельном имени, фамилии, электронной почте для каждого человека (без имени, если предоставляется только адрес электронной почты).
Может кто-нибудь предложить лучший способ сделать это?
Я попытался разделить запятые, которые будут работать, кроме как во втором примере, где первое место помещается первым. Я полагаю, что этот метод мог бы работать, если после я split я исследую каждый элемент и вижу, содержит ли он '@' или '<'/' > ', если это не так, тогда можно предположить, что следующий элемент первое имя. Это хороший способ приблизиться к этому? Упустил ли я другой формат, который может быть в адресе?
UPDATE: Возможно, я должен немного разъяснить, в основном все, что я хочу сделать, это разбить строку, содержащую несколько адресов, на отдельные строки, содержащие адрес в любом формате, в котором он был отправлен. У меня есть свои собственные методы проверки и извлекая информацию из адреса, мне было просто сложно найти лучший способ разделить каждый адрес.
Вот решение, которое я придумал, чтобы выполнить это:
String str = "Last, First <[email protected]>, [email protected], First Last <[email protected]>, \"First Last\" <[email protected]>";
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);
}
Приведенный выше код генерирует отдельные адреса, которые я могу обрабатывать дальше по строке.
Ответы
Ответ 1
Вот решение, которое я придумал, чтобы выполнить это:
String str = "Last, First <[email protected]>, [email protected], First Last <[email protected]>, \"First Last\" <[email protected]>";
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);
}
Ответ 2
Существует внутренний класс 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
}
Ответ 3
На самом деле нет простого решения. Я бы рекомендовал сделать небольшую машину состояний, которая читает char -by- char и выполняет эту работу таким образом. Как вы сказали, разделение запятой не всегда будет работать.
Конечный автомат позволит вам охватить все возможности. Я уверен, что есть еще много других, которых вы еще не видели. Например: "Первый последний"
Ищите 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 <[email protected]>, [email protected], First Last <[email protected]>";
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);
}
}
С этим подходом существует несколько сторон. Во-первых, он не проверяет строку. Если у вас есть символы в строке, которые не соответствуют одному из ваших выбранных форматов, то эти символы просто игнорируются. Другим является то, что принятые форматы все выражаются в одном месте. Вы не можете добавлять новые форматы без изменения монолитного регулярного выражения.
Ответ 5
Ваш второй пример электронной почты не является допустимым адресом, так как он содержит запятую, которая не входит в строку с кавычками. Чтобы быть действительным, он должен выглядеть следующим образом: "Last, First"<[email protected]>
.
Что касается разбора, если вы хотите что-то довольно строгое, вы можете использовать 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;
}
Ответ 6
Нет такого простого простого решения. RFC, который вы хотите, RFC2822, который описывает все возможные конфигурации адреса электронной почты. Лучшее, что вы собираетесь получить, будет правильным - реализовать токенизатор на основе состояния, который следует правилам, указанным в RFC.
Ответ 7
Вы можете использовать регулярные выражения, чтобы попытаться отделить это, попробуйте этого парня:
^(?<name1>[a-zA-Z0-9]+?),? (?<name2>[a-zA-Z0-9]+?),? (?<address1>[a-zA-Z0-9.-_<>]+?)$
будет соответствовать: Last, First [email protected]
; Last, First <[email protected]>
; First last [email protected]
; First Last <[email protected]>
. Вы можете добавить еще одно необязательное совпадение в регулярном выражении в конце, чтобы получить последний сегмент First, Last <[email protected]>, [email protected]
после адреса электронной почты, заключенного в угловые фигурные скобки.
Надеюсь, что это поможет немного!
EDIT:
и, конечно, вы можете добавить больше символов в каждый из разделов, чтобы принимать котировки и т.д. для любого формата, который читается. Как упоминалось в sjbotha, это может быть сложно, поскольку строка, которая отправлена, необязательно находится в установленном формате.
Эта ссылка может предоставить вам дополнительную информацию о сопоставлении и проверке адресов электронной почты с использованием регулярных выражений.
Ответ 8
Вот как я это сделаю:
- Вы можете попытаться стандартизировать данные
насколько это возможно, то есть избавиться от
такие как < и > символы
и все запятые после
'.com'. Вам понадобятся запятые
которые отделяют первый и последний
имена.
- После того, как вы избавитесь от лишних символов, поместите каждую сгруппированную электронную почту
запись в списке в виде строки. Вы
может использовать .com, чтобы определить, где
если нужно, разделите строку.
- После того, как у вас есть список адресов электронной почты в списке строк, вы
затем может разделить электронную почту
адреса, используя только пробелы, как
делиметр.
- Последний шаг - определить, что такое имя, что такое
фамилию и т.д. Это было бы сделано
путем проверки трех компонентов для: a
запятой, что указывает на то, что это
это фамилия; a. которые бы
указать фактический адрес; а также
все, что осталось, - это имя.
Если нет запятой, тогда первая
имя первое, фамилия вторая,
и т.п.
Я не знаю, является ли это наиболее сжатым решением, но оно будет работать и не требует каких-либо передовых методов программирования.
Ответ 9
// На основании ответа Майкла Перри *
// нужно обрабатывать [email protected], [email protected] и связанные с ними синтаксисы
// также ищет имя и фамилию в этих синтаксисах электронной почты
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;
// [email protected], [email protected] 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;
}
}
Ответ 10
Я решил, что я собираюсь провести линию на песке с двумя ограничениями:
- Заголовки 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((','));
}
ИЗМЕНИТЬ
Дальнейшие исследования показали мне, что мои предположения хороши. Чтение через spec RFC 2822 в значительной степени показывает, что поля To, Cc и Bcc являются полями csv-parseable. Так что да, это сложно, и есть много ошибок, как при любом синтаксическом анализе 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 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"" <[email protected]>" + "," +
@"<[email protected]>, [email protected], [email protected]" + "," +
@"<[email protected]>, [email protected]" + "," +
@"""""Yes, this is valid""""@[emails are hard to parse!]" + "," +
@"First, Last <[email protected]>, [email protected], First Last <[email protected]>"
;
static string[] results1;
static string[] expectedResults1;
Establish context = () =>
{
expectedResults1 = new string[]
{
@"Lastname",
@"Firstname <[email protected]>",
@"<[email protected]>",
@"[email protected]",
@"[email protected]",
@"<[email protected]>",
@"[email protected]",
@"Yes",
@"this is [email protected][emails are hard to parse!]",
@"First",
@"Last <[email protected]>",
@"[email protected]",
@"First Last <[email protected]>"
};
};
Because of = () =>
{
results1 = CSVProcessor.GetFieldsFromString(recipientHeaderToParse1);
};
It should_parse_the_email_parts_properly = () => results1.ShouldBeLike(expectedResults1);
}
Ответ 11
Вот что я придумал. Предполагается, что действительный адрес электронной почты должен содержать один и только один знак "@":
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();
}
Ответ 12
Чистое и краткое решение заключается в использовании MailAddressCollection:
var collection = new MailAddressCollection();
collection.Add(addresses);
Этот подход анализирует список адресов, разделенных двоеточием ,
, и проверяет его в соответствии с RFC. Выдает FormatException
в случае, если адреса недействительны. Как и предлагалось в других публикациях, если вам нужно иметь дело с недействительными адресами, вы должны предварительно обработать или проанализировать значение самостоятельно, в противном случае рекомендуем использовать то, что предлагает .NET, без использования отражения.
Пример:
var collection = new MailAddressCollection();
collection.Add("Joe Doe <[email protected]>, [email protected]");
foreach (var addr in collection)
{
// addr.DisplayName, addr.User, addr.Host
}
Ответ 13
Я использую следующее регулярное выражение в Java, чтобы получить строку электронной почты с RFC-совместимого адреса электронной почты:
[A-Za-z0-9]+[A-Za-z0-9._-][email protected][A-Za-z0-9]+[A-Za-z0-9._-]+[.][A-Za-z0-9]{2,3}