c# - studio - La mejor forma de analizar cadena de direcciones de correo electrónico
leer correo outlook desde c# (12)
Así que estoy trabajando con algunos datos de cabecera de correo electrónico, y para los campos to :, from :, cc: y bcc: las direcciones de correo electrónico se pueden expresar de varias maneras diferentes:
First Last <[email protected]>
Last, First <[email protected]>
[email protected]
Y estas variaciones pueden aparecer en el mismo mensaje, en cualquier orden, todo en una secuencia separada por comas:
First, Last <[email protected]>, [email protected], First Last <[email protected]>
He estado tratando de encontrar una forma de analizar esta cadena en Nombre, Apellido, Correo electrónico para cada persona (omitiendo el nombre si solo se proporciona una dirección de correo electrónico).
¿Alguien puede sugerir la mejor manera de hacer esto?
Intenté dividir las comas, lo que funcionaría excepto en el segundo ejemplo, donde el apellido se coloca primero. Supongo que este método podría funcionar, si después de dividir, examino cada elemento y veo si contiene un ''@'' o ''<'' / ''>'', si no lo hace, se puede suponer que el siguiente elemento es el nombre de pila. ¿Es esta una buena manera de abordar esto? ¿He pasado por alto otro formato en el que podría estar la dirección?
ACTUALIZACIÓN: Tal vez debería aclarar un poco, básicamente, todo lo que quiero hacer es dividir la cadena que contiene las múltiples direcciones en cadenas individuales que contienen la dirección en cualquier formato en que fue enviada. Tengo mis propios métodos para validar y extraer la información desde una dirección, fue difícil para mí encontrar la mejor manera de separar cada dirección.
Aquí está la solución que se me ocurrió para lograr esto:
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);
}
El código anterior genera las direcciones individuales que puedo procesar más adelante en la línea.
A riesgo de crear dos problemas, puede crear una expresión regular que coincida con cualquiera de sus formatos de correo electrónico. Use "|" para separar los formatos dentro de este regex. Luego puede ejecutarlo sobre su cadena de entrada y sacar todas las coincidencias.
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);
}
}
Hay muchos puntos negativos en este enfoque. Una es que no valida la cadena. Si tiene caracteres en la cadena que no se ajustan a uno de sus formatos elegidos, entonces esos caracteres simplemente se ignoran. Otra es que los formatos aceptados se expresan en un solo lugar. No puede agregar nuevos formatos sin cambiar la expresión regular monolítica.
Aquí está la solución que se me ocurrió para lograr esto:
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);
}
Así es como lo haría:
- Puede intentar estandarizar los datos tanto como sea posible, es decir, deshacerse de cosas tales como los símbolos <y> y todas las comas después del ''.com''. Necesitará las comas que separan los nombres y apellidos.
- Después de deshacerse de los símbolos adicionales, ponga cada registro de correo electrónico agrupado en una lista como una cadena. Puede usar .com para determinar dónde dividir la cadena si es necesario.
- Después de tener la lista de direcciones de correo electrónico en la lista de cadenas, puede dividir aún más las direcciones de correo electrónico utilizando solo espacios en blanco como el delímetro.
- El último paso es determinar cuál es el primer nombre, cuál es el apellido, etc. Esto se haría al verificar los 3 componentes para: una coma, lo que indicaría que es el apellido; a . que indicaría la dirección real; y lo que queda es el primer nombre. Si no hay coma, el primer nombre es primero, el apellido es el segundo, etc.
No sé si esta es la solución más concisa, pero funcionaría y no requiere ninguna técnica de programación avanzada
No hay una solución genérica simple para esto. El RFC que desea es RFC2822 , que describe todas las configuraciones posibles de una dirección de correo electrónico. Lo mejor que obtendrá será la implementación de un tokenizador basado en estado que siga las reglas especificadas en el RFC.
Podrías usar expresiones regulares para tratar de separar esto, prueba este tipo:
^(?<name1>[a-zA-Z0-9]+?),? (?<name2>[a-zA-Z0-9]+?),? (?<address1>[a-zA-Z0-9.-_<>]+?)$
coincidirá: Last, First [email protected]
; Last, First <[email protected]>
; First last [email protected]
; First Last <[email protected]>
. Puede agregar otra coincidencia opcional en la expresión regular al final para recoger el último segmento de First, Last <[email protected]>, [email protected]
después de la dirección de correo electrónico encerrada en llaves angulares.
Espero que esto ayude un poco!
EDITAR:
y, por supuesto, puede agregar más caracteres a cada una de las secciones para aceptar citas, etc. para cualquier formato que se esté leyendo. Como mencionó sjbotha, esto podría ser difícil ya que la cadena que se envía no está necesariamente en un formato establecido.
Este enlace puede proporcionarle más información sobre cómo hacer coincidir Y validar direcciones de correo electrónico utilizando expresiones regulares.
Realmente no hay una solución fácil para esto. Yo recomendaría hacer una pequeña máquina de estado que lea char-by-char y haga el trabajo de esa manera. Como dijiste, dividir por comas no siempre funcionará.
Una máquina de estado le permitirá cubrir todas las posibilidades. Estoy seguro de que hay muchos otros que aún no has visto. Por ejemplo: "First Last"
Busque el RFC sobre esto para descubrir cuáles son todas las posibilidades. Lo siento, no sé el número. Probablemente haya múltiples ya que este es el tipo de cosas que evolucionan.
// Basado en la respuesta de Michael Perry * // necesita manejar [email protected], [email protected] y sintaxis relacionadas // también busca el nombre y apellido dentro de esas sintaxis de correo electrónico
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;
}
}
Utilizo la siguiente expresión regular en Java para obtener una cadena de correo electrónico desde una dirección de correo electrónico compatible con RFC:
[A-Za-z0-9]+[A-Za-z0-9._-]+@[A-Za-z0-9]+[A-Za-z0-9._-]+[.][A-Za-z0-9]{2,3}
Decidí que iba a trazar una línea en la arena con dos restricciones:
- Los encabezados To y Cc deben ser cadenas analizables de csv.
- Cualquier cosa que MailAddress no haya podido analizar, simplemente no me voy a preocupar por eso.
También decidí que solo me interesan las direcciones de correo electrónico y no mostrar el nombre, ya que el nombre para mostrar es tan problemático y difícil de definir, mientras que la dirección de correo electrónico que puedo validar. Entonces usé MailAddress para validar mi análisis sintáctico.
Traté los encabezados To y Cc como una cadena csv, y de nuevo, todo lo que no se puede analizar de esa manera no me preocupa.
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(('',''));
}
EDITAR
La investigación adicional me ha mostrado que mis suposiciones son buenas. Al leer la especificación, RFC 2822 muestra que los campos A, Cc y Bcc son campos analizables por csv. Así que sí, es difícil y hay muchas trampas, como con cualquier análisis csv, pero si tienes una forma confiable de analizar campos csv (que es TextFieldParser en el espacio de nombres Microsoft.VisualBasic.FileIO, y es lo que usé para esto) , entonces eres dorado.
Editar 2
Aparentemente no necesitan ser cadenas de CSV válidas ... las comillas realmente estropean las cosas. Entonces su analizador csv tiene que ser tolerante a fallas. Lo hice intentar analizar la cadena, si falló, quita todas las comillas y vuelve a intentarlo:
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};
}
}
}
}
}
}
}
Lo único que no manejará son las cuentas citadas en un correo electrónico, es decir, "Monkey Header" @ stupidemailaddresses.com.
Y esta es la prueba:
[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 valid@[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);
}
Su segundo ejemplo de correo electrónico no es una dirección válida, ya que contiene una coma que no está dentro de una cadena entre comillas. Para que sea válido, debe ser como: "Last, First"<[email protected]>
.
En cuanto al análisis sintáctico, si desea algo que sea bastante estricto, puede usar System.Net.Mail.MailAddressCollection
.
Si solo desea que su entrada se divida en cadenas de correo electrónico separadas, entonces el siguiente código debería funcionar. No es muy estricto, pero manejará las comas dentro de las cadenas entre comillas y arrojará una excepción si la entrada contiene una comilla no cerrada.
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;
}
Existe una clase interna System.Net.Mail.MailAddressParser
que tiene el método ParseMultipleAddresses
que hace exactamente lo que usted desea. Puede acceder a él directamente a través de la reflexión o llamando MailMessage.To.Add
método MailMessage.To.Add
, que acepta cadena de lista de correo electrónico.
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
}
Esto es lo que se me ocurrió. Supone que una dirección de correo electrónico válida debe tener un solo y un signo ''@'':
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();
}