Лучший способ проанализировать строку адресов электронной почты

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

    }


}

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

  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-анализируемыми полями. Так что да, это сложно, и есть много 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}