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

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