Лучший способ разобрать строку адресов электронной почты - PullRequest
10 голосов
/ 16 января 2009

Итак, я работаю с некоторыми данными заголовка электронной почты, а для полей 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);
}

Приведенный выше код генерирует индивидуальные адреса, которые я могу обработать в дальнейшем.

Ответы [ 12 ]

5 голосов
/ 09 мая 2017

Существует внутренний класс 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
    }
4 голосов
/ 16 января 2009

С риском создания двух проблем вы можете создать регулярное выражение, соответствующее любому из ваших форматов электронной почты. Используйте "|" отделить форматы в этом одном регулярном выражении. Затем вы можете запустить его поверх входной строки и извлечь все совпадения.

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

У этого подхода есть несколько недостатков. Во-первых, он не проверяет строку. Если в строке есть символы, которые не соответствуют ни одному из выбранных вами форматов, эти символы просто игнорируются. Другое заключается в том, что принятые форматы все выражены в одном месте. Вы не можете добавлять новые форматы без изменения монолитного регулярного выражения.

4 голосов
/ 16 января 2009

На самом деле не существует простого решения для этого. Я бы порекомендовал сделать небольшой конечный автомат, который читает символ за символом и делает работу таким образом. Как вы сказали, разделение запятой не всегда будет работать.

Конечный автомат позволит вам охватить все возможности. Я уверен, что есть много других, которых вы еще не видели. Например: «Первый последний»

Поищите в RFC об этом, чтобы узнать, каковы все возможности. Извините, я не знаю номер. Вероятно, их много, потому что это то, что развивается.

3 голосов
/ 23 сентября 2016

Ваш второй пример электронной почты не является действительным адресом, так как содержит запятую, которая не находится в строке в кавычках. Чтобы быть действительным, оно должно выглядеть так: "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 голосов
/ 16 января 2009

Вот решение, которое я придумал для достижения этой цели:

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);
}
2 голосов
/ 16 января 2009

Нет общего простого решения для этого. Требуемый RFC RFC2822 , который описывает все возможные конфигурации адреса электронной почты. Лучшее, что вы получите, это будет правильно , это реализовать токенизатор на основе состояния, который следует правилам, указанным в RFC.

0 голосов
/ 09 января 2018

Вот что я придумал. Предполагается, что действительный адрес электронной почты должен содержать один и только один знак «@»:

    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 голосов
/ 05 сентября 2015

Я решил, что собираюсь нарисовать линию на песке с двумя ограничениями:

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

EDIT

Дальнейшие исследования показали, что мои предположения хороши. Прочтение спецификации 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'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);
}
0 голосов
/ 29 мая 2012

// На основании ответа Майкла Перри * // необходимо обработать 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;

    }


}
0 голосов
/ 29 января 2010

Я использую следующее регулярное выражение в Java для получения строки электронной почты с адреса электронной почты, совместимого с RFC:

[A-Za-z0-9]+[A-Za-z0-9._-]+@[A-Za-z0-9]+[A-Za-z0-9._-]+[.][A-Za-z0-9]{2,3}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...