Para validar una dirección de correo en C#, la mayoría de los desarrolladores
echan mano de System.Net.Mail.MailAddress y siguen adelante. Es una primera
jugada razonable —el tipo viene integrado en el framework y rechaza basura sin
una expresión regular artesanal—, pero solo responde a la primera de las tres
preguntas que plantea la validación real. ¿Tiene la dirección la forma correcta?
¿Puede su dominio recibir correo? ¿Existe realmente el buzón? La biblioteca de
clases base de .NET responde a la primera con soltura, necesita un conocido
paquete NuGet para la segunda y deja la tercera como un problema de red que
conviene delegar. Esta guía construye la validación de correo en C# como tres
capas, con código async/await idiomático para cada una, y muestra exactamente
dónde se detiene MailAddress.
La respuesta corta#
Usa MailAddress.TryCreate para la sintaxis, el paquete DnsClient.NET para la
consulta MX y una API de verificación para la comprobación del buzón por SMTP:
lo más barato primero, cortocircuitando en cuanto una capa es decisiva. No abras
sockets SMTP sin más desde tu aplicación para sondear buzones por tu cuenta: el
puerto 25 saliente está bloqueado en la mayoría de los hosts, y la respuesta
depende de la reputación de la IP de envío y del greylisting que no quieres
reimplementar. Cada capa descarta direcciones de forma más barata que la
anterior; solo la última puede dar por buena una dirección. Si lo único que
quieres es un veredicto para una sola dirección ahora mismo, pégala en el
verificador de correo gratuito y sáltate el código: el
resto de esta guía es para integrar la comprobación en una aplicación. Para los
conceptos que hay detrás de cada etapa, consulta
qué es la verificación de correo.
Capa 1: sintaxis con MailAddress.TryCreate#
MailAddress es la herramienta adecuada para la primera capa. Prefiere la
sobrecarga TryCreate añadida en .NET 5: devuelve un bool en lugar de lanzar
una FormatException con entradas incorrectas, así que puedes usarla como filtro
sin envolver cada llamada en un try/catch.
using System.Net.Mail;
public static bool IsValidSyntax(string email)
{
return !string.IsNullOrWhiteSpace(email)
&& email.Length <= 320
&& MailAddress.TryCreate(email, out var parsed)
&& parsed.Address == email;
}
IsValidSyntax("jane@example.com"); // true
IsValidSyntax("not-an-email"); // false
IsValidSyntax("Jane <jane@example.com>"); // false
Esa última comparación importa. MailAddress es un analizador de correo, no un
validador estricto: acepta encantado formas con nombre para mostrar como
Jane <jane@example.com> y expone la parte de la dirección en parsed.Address.
Comparar parsed.Address con la entrada original rechaza esas formas y los
espacios en blanco sueltos, que es justo lo que quieres de un filtro de sintaxis
de sí o no sobre la entrada del usuario. La guarda de 320 caracteres es por si
acaso: nada más largo puede ser una dirección real, y sale más barato rechazar
pronto que pasar una cadena enorme a cualquier cosa más adelante.
En .NET Framework o cualquier destino anterior a .NET 5, TryCreate no existe,
así que recurre al constructor y captura la excepción:
try { _ = new MailAddress(email); /* syntax ok */ }
catch (FormatException) { /* reject */ }
Si ya usas anotaciones de datos en un modelo, el
System.ComponentModel.DataAnnotations.EmailAddressAttribute ([EmailAddress])
hace el mismo trabajo de forma declarativa y es una opción estupenda dentro de la
validación de modelos de ASP.NET Core. Elijas lo que elijas, trata un aprobado
como "vale la pena comprobarlo bien", nunca como "válido". Una comprobación de
sintaxis no sabe nada de DNS ni de buzones: es la misma razón por la que
la validación de correo con expresiones regulares falla:
la forma y la entregabilidad son preguntas distintas, una es un hecho sobre la
cadena y la otra un hecho sobre internet.
Capa 2: ¿puede el dominio recibir correo?#
Un dominio sin ruta de correo no puede aceptar correo para nadie, así que esta
única consulta elimina dominios muertos, nombres de empresa mal escritos y TLD
inventados antes de que toques la red de verdad. El inconveniente: .NET no
tiene un resolvedor de MX integrado. System.Net.Dns resuelve registros A y
AAAA, pero no MX, así que la respuesta idiomática es DnsClient.NET, un paquete
NuGet muy usado y bien mantenido al que recurre la mayoría de los proyectos .NET
cuando necesitan DNS más allá de las consultas de host. Instálalo y consulta el
tipo de registro MX:
using System.Linq;
using DnsClient; // dotnet add package DnsClient
// LookupClient is thread-safe and caches responses — create one and reuse it.
private static readonly LookupClient Dns = new();
public static async Task<bool> HasMailRouteAsync(string domain)
{
try
{
var response = await Dns.QueryAsync(domain, QueryType.MX);
return response.Answers.MxRecords().Any();
}
catch (DnsResponseException)
{
// No reachable resolver, SERVFAIL, and similar — treat as "check later".
return false;
}
}
await HasMailRouteAsync("gmail.com"); // true
await HasMailRouteAsync("company-that-folded.com"); // false
Dos modismos que vale la pena copiar. Crea el LookupClient una sola vez y
reutilízalo: es seguro para hilos y cachea las respuestas, así que una instancia
por petición no hace más que tirar esa caché a la basura. Y separa el dominio de
la dirección por el último @ con una expresión de rango, para no analizar mal
nunca una dirección que (legalmente) contenga más de uno:
var domain = email[(email.LastIndexOf('@') + 1)..];
Algunos dominios aceptan correo en un registro A sin MX (un MX implícito). Si
quieres contemplar ese caso límite, recurre a QueryType.A cuando el conjunto MX
esté vacío; pero para la inmensa mayoría de las direcciones reales, una
comprobación MX es el filtro adecuado. Una advertencia para un flujo de registro:
una consulta DNS se bloquea sobre un resolvedor, así que un hipo transitorio de
DNS no debería rechazar de forma tajante a un cliente real. Trata una consulta
fallida como "comprobar más tarde", no como "no válida".
Capa 3: ¿existe realmente el buzón?#
Las capas uno y dos solo pueden descartar una dirección. Un dominio puede
publicar registros MX perfectos y aun así no tener buzón en la dirección que
tienes entre manos: noreply-9f2x@gmail.com es sintaxis válida en un dominio con
una ruta de correo activa, y sigue siendo un buzón que nunca se creó. Confirmar
que un buzón concreto existe implica la conversación de entrega SMTP: conectarse
al host de correo, emitir RCPT TO, leer la respuesta y desconectarse antes de
enviar nada. Cómo funciona la verificación de correo
recorre esa canalización completa, incluidos los dominios catch-all que aceptan
cualquier dirección y burlan un sondeo ingenuo.
En principio puedes programar esto en C# con un TcpClient y comandos SMTP en
crudo. En la práctica no deberías ejecutarlo desde tu servidor de aplicación. La
mayoría de los proveedores de nube bloquean el puerto 25 saliente, así que un
sondeo que funciona en tu portátil falla sin ruido en producción. La respuesta
depende de la reputación de la IP desde la que te conectas, no solo de la
dirección que estás comprobando. Y los servidores receptores aplican greylisting
y limitan la tasa a remitentes desconocidos, así que sondear con cualquier volumen
hace que te aplacen o te metan en una lista de bloqueo. Esta es la capa que vale
la pena delegar en una infraestructura construida para ello.
Llamar a una API de verificación desde C##
El endpoint de verificación de Qualisend ejecuta toda la canalización —sintaxis,
DNS y el sondeo del buzón por SMTP— desde una infraestructura con reputación
gestionada, y devuelve un veredicto en tiempo real. HttpClient más
System.Text.Json es todo lo que necesitas; modela el sobre de la respuesta como
records y deja que el generador de origen de JSON o el deserializador por
reflexión los rellenen:
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
public record VerifyResponse(
[property: JsonPropertyName("result")] VerifyResult Result);
public record VerifyResult(
[property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("score")] int Score,
[property: JsonPropertyName("reason")] string? Reason,
[property: JsonPropertyName("sub_flags")] IReadOnlyDictionary<string, bool> SubFlags);
// One HttpClient for the whole app — don't new one up per request.
private static readonly HttpClient Http = new()
{
BaseAddress = new Uri("https://api.qualisend.com/v1/"),
};
public static async Task<VerifyResult> VerifyAsync(string email)
{
using var request = new HttpRequestMessage(HttpMethod.Post, "verify")
{
Content = JsonContent.Create(new { email }),
};
request.Headers.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("QUALISEND_API_KEY"));
using var response = await Http.SendAsync(request);
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadFromJsonAsync<VerifyResponse>();
return body!.Result; // status: deliverable | risky | undeliverable | unknown
}
POST https://api.qualisend.com/v1/verify es una forma de ejemplo: consulta la
referencia de la API en /developers para conocer el endpoint
exacto, el formato de clave con ámbito y todos los campos. Lee la clave desde una
variable de entorno en vez de codificar a fuego YOUR_API_KEY, y reutiliza un
único HttpClient para evitar el agotamiento de sockets. La respuesta llega
dentro de un sobre result:
{
"result": {
"status": "deliverable",
"score": 95,
"reason": null,
"sub_flags": { "role": false, "disposable": false, "free": false, "catch_all": false }
}
}
status es tu veredicto principal: deliverable, risky, undeliverable o
unknown. score gradúa la confianza, reason explica un resultado negativo y
sub_flags desglosa señales como role, disposable, free y catch-all para que
apliques tu propia política (rechazar undeliverable, retener risky para
revisión, marcar disposable en el registro).
Juntar las capas para validar una dirección de correo en C##
Lo más barato primero, para en cuanto tengas una respuesta:
public static async Task<VerifyResult> ValidateEmailAsync(string email)
{
if (!IsValidSyntax(email))
return new VerifyResult("undeliverable", 0, "invalid_email",
new Dictionary<string, bool>());
var domain = email[(email.LastIndexOf('@') + 1)..];
if (!await HasMailRouteAsync(domain))
return new VerifyResult("undeliverable", 0, "invalid_domain",
new Dictionary<string, bool>());
return await VerifyAsync(email); // deliverable | risky | undeliverable | unknown
}
Las dos capas locales no cuestan nada y atrapan al instante la mayor parte de la basura; la capa de la API se ejecuta solo sobre direcciones que merecen el viaje de ida y vuelta por la red. Ese orden es todo el truco: la misma forma que encontrarás en las versiones de esta guía para Node.js, Python y PHP, porque lo que hace que la validación funcione es la disposición en capas, no el lenguaje.
Preguntas frecuentes#
¿Basta con MailAddress o EmailAddressAttribute para validar un correo en C#?#
Para la sintaxis, sí: ambos son comprobaciones sólidas de primera capa y una
apuesta mejor que una expresión regular artesanal, y MailAddress.TryCreate te
da un booleano que no lanza excepciones. Pero validan la forma, no la
entregabilidad: ninguno resuelve DNS ni contacta con un servidor de correo, así
que un aprobado significa "parece un correo", no "se entregará". Combina la
comprobación de sintaxis con una consulta MX y una comprobación del buzón por
SMTP antes de confiar en la dirección.
¿Tiene .NET una forma integrada de consultar registros MX?#
No. System.Net.Dns resuelve registros de host A y AAAA, pero no admite MX, por
eso el enfoque estándar es el paquete NuGet DnsClient.NET. Consulta
QueryType.MX, reutiliza un único LookupClient y trata un conjunto de
respuestas vacío —o una DnsResponseException— como ausencia de ruta de correo
en lugar de dejar que lance una excepción.
¿Puedo comprobar si un buzón existe en C# sin una API?#
Solo en parte. DnsClient.NET confirma que el dominio acepta correo, lo que
descarta dominios muertos de forma barata. Confirmar el buzón en sí implica una
conversación SMTP que puedes intentar con un TcpClient pero que no deberías
ejecutar desde tu servidor de aplicación: el puerto 25 saliente está bloqueado de
forma generalizada y el resultado depende de la reputación de tu IP de envío. Esa
es la capa que existe para que la gestione un servicio de verificación, y vale la
pena comparar proveedores antes de
construirla tú mismo.
¿Debo validar correos en el registro o al limpiar una lista?#
Ambos, con distinta profundidad. Ejecuta la sintaxis y la consulta MX de forma síncrona en el registro —son lo bastante rápidas como para bloquear la petición y dar respuesta al instante— y actúa allí también según el veredicto inmediato de la API; la guía de registro serverless muestra el patrón de principio a fin. Reserva la verificación más profunda y por lotes para limpiar una lista existente, donde la latencia no importa y la exhaustividad sí.
¿Listo para añadir la capa SMTP? La referencia de la API tiene la
forma exacta de /verify, las claves con ámbito y ejemplos para copiar y pegar, o
suelta una dirección en el
verificador de correo gratuito para ver cómo una cadena
aprobada por MailAddress vuelve como undeliverable.