Como Validar Email em Rust

Aprenda a validar endereços de email em Rust com regex, validação manual e a crate validator. Guia completo com múltiplas abordagens e código executável.

Validar endereços de email é uma das tarefas mais comuns em aplicações web e CLIs. Existem diferentes níveis de validação, desde uma verificação simples de formato até conformidade com a RFC 5322. Nesta receita, você vai aprender três abordagens: regex, validação manual e a crate validator.

Dependências

[package]
name = "receita-validar-email"
version = "0.1.0"
edition = "2021"

[dependencies]
regex = "1"
validator = { version = "0.19", features = ["derive"] }

Código Completo

use regex::Regex;
use std::sync::LazyLock;
use validator::Validate;

// =============================================
// 1. Validação simples com regex
// =============================================
static RE_EMAIL: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(
        r"(?i)^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$"
    ).unwrap()
});

fn validar_email_regex(email: &str) -> bool {
    RE_EMAIL.is_match(email.trim())
}

// =============================================
// 2. Validação manual (sem dependências)
// =============================================
#[derive(Debug)]
enum EmailErro {
    Vazio,
    SemArroba,
    MultiploArrobas,
    UsuarioVazio,
    DominioVazio,
    DominioSemPonto,
    CaracterInvalido(char),
    MuitoLongo,
}

impl std::fmt::Display for EmailErro {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            EmailErro::Vazio => write!(f, "Email vazio"),
            EmailErro::SemArroba => write!(f, "Email não contém @"),
            EmailErro::MultiploArrobas => write!(f, "Email contém mais de um @"),
            EmailErro::UsuarioVazio => write!(f, "Parte do usuário está vazia"),
            EmailErro::DominioVazio => write!(f, "Domínio está vazio"),
            EmailErro::DominioSemPonto => write!(f, "Domínio não contém ponto"),
            EmailErro::CaracterInvalido(c) => write!(f, "Caractere inválido: '{}'", c),
            EmailErro::MuitoLongo => write!(f, "Email excede 254 caracteres"),
        }
    }
}

fn validar_email_manual(email: &str) -> Result<(), EmailErro> {
    let email = email.trim();

    if email.is_empty() {
        return Err(EmailErro::Vazio);
    }

    if email.len() > 254 {
        return Err(EmailErro::MuitoLongo);
    }

    // Verificar quantidade de @
    let arrobas = email.chars().filter(|&c| c == '@').count();
    if arrobas == 0 {
        return Err(EmailErro::SemArroba);
    }
    if arrobas > 1 {
        return Err(EmailErro::MultiploArrobas);
    }

    // Separar usuário e domínio
    let partes: Vec<&str> = email.splitn(2, '@').collect();
    let usuario = partes[0];
    let dominio = partes[1];

    if usuario.is_empty() {
        return Err(EmailErro::UsuarioVazio);
    }

    if dominio.is_empty() {
        return Err(EmailErro::DominioVazio);
    }

    if !dominio.contains('.') {
        return Err(EmailErro::DominioSemPonto);
    }

    // Verificar caracteres no usuário
    let chars_validos = |c: char| {
        c.is_alphanumeric() || "._%+-".contains(c)
    };
    if let Some(invalido) = usuario.chars().find(|c| !chars_validos(*c)) {
        return Err(EmailErro::CaracterInvalido(invalido));
    }

    // Verificar caracteres no domínio
    let chars_dominio = |c: char| {
        c.is_alphanumeric() || ".-".contains(c)
    };
    if let Some(invalido) = dominio.chars().find(|c| !chars_dominio(*c)) {
        return Err(EmailErro::CaracterInvalido(invalido));
    }

    // Verificar TLD (pelo menos 2 caracteres)
    let tld = dominio.rsplit('.').next().unwrap_or("");
    if tld.len() < 2 {
        return Err(EmailErro::DominioSemPonto);
    }

    Ok(())
}

// =============================================
// 3. Validação com a crate validator
// =============================================
#[derive(Debug, Validate)]
struct Cadastro {
    #[validate(email(message = "Email inválido"))]
    email: String,

    #[validate(length(min = 3, max = 50, message = "Nome deve ter 3-50 caracteres"))]
    nome: String,
}

fn main() {
    let emails_teste = vec![
        "usuario@exemplo.com",
        "nome.sobrenome@empresa.com.br",
        "usuario+tag@gmail.com",
        "invalido@",
        "@sem-usuario.com",
        "sem-arroba.com",
        "dois@@arrobas.com",
        "",
        "a@b.c",
        "usuario@dominio.com",
        " espacos@email.com ",
    ];

    // =============================================
    // Testar regex
    // =============================================
    println!("=== Validação com Regex ===");
    for email in &emails_teste {
        let valido = validar_email_regex(email);
        let icone = if valido { "OK" } else { "FALHA" };
        println!("  [{}] '{}'", icone, email);
    }

    // =============================================
    // Testar validação manual
    // =============================================
    println!("\n=== Validação Manual (com detalhes) ===");
    for email in &emails_teste {
        match validar_email_manual(email) {
            Ok(()) => println!("  [OK]    '{}'", email),
            Err(e) => println!("  [FALHA] '{}' -> {}", email, e),
        }
    }

    // =============================================
    // Testar crate validator
    // =============================================
    println!("\n=== Validação com crate validator ===");
    let cadastros = vec![
        Cadastro {
            email: "usuario@exemplo.com".into(),
            nome: "Ana Silva".into(),
        },
        Cadastro {
            email: "invalido@".into(),
            nome: "Bia".into(),
        },
        Cadastro {
            email: "valido@email.com".into(),
            nome: "AB".into(), // muito curto
        },
    ];

    for cadastro in &cadastros {
        match cadastro.validate() {
            Ok(()) => println!("  [OK]    {:?}", cadastro),
            Err(erros) => println!("  [FALHA] {:?} -> {}", cadastro, erros),
        }
    }

    // =============================================
    // Exemplo prático: processar lista de emails
    // =============================================
    println!("\n=== Processamento de Lista ===");
    let entrada = vec![
        "ana@empresa.com",
        "invalido",
        "bia@gmail.com",
        "",
        "carlos@outlook.com",
        "sem-arroba",
    ];

    let (validos, invalidos): (Vec<&str>, Vec<&str>) = entrada
        .into_iter()
        .partition(|email| validar_email_regex(email));

    println!("Válidos ({}):", validos.len());
    for email in &validos {
        println!("  {}", email);
    }
    println!("Inválidos ({}):", invalidos.len());
    for email in &invalidos {
        println!("  '{}'", email);
    }

    // =============================================
    // Normalizar emails
    // =============================================
    println!("\n=== Normalização ===");
    fn normalizar_email(email: &str) -> Option<String> {
        let email = email.trim().to_lowercase();
        if validar_email_regex(&email) {
            Some(email)
        } else {
            None
        }
    }

    let emails = vec!["  USUARIO@GMAIL.COM  ", "Ana@Empresa.COM", "invalido"];
    for email in &emails {
        match normalizar_email(email) {
            Some(normalizado) => println!("  '{}' -> '{}'", email, normalizado),
            None => println!("  '{}' -> INVÁLIDO", email),
        }
    }
}

Saída do Programa

=== Validação com Regex ===
  [OK] 'usuario@exemplo.com'
  [OK] 'nome.sobrenome@empresa.com.br'
  [OK] 'usuario+tag@gmail.com'
  [FALHA] 'invalido@'
  [FALHA] '@sem-usuario.com'
  [FALHA] 'sem-arroba.com'
  [FALHA] 'dois@@arrobas.com'
  [FALHA] ''
  [FALHA] 'a@b.c'
  [OK] 'usuario@dominio.com'
  [OK] ' espacos@email.com '

=== Validação Manual (com detalhes) ===
  [OK]    'usuario@exemplo.com'
  [OK]    'nome.sobrenome@empresa.com.br'
  [OK]    'usuario+tag@gmail.com'
  [FALHA] 'invalido@' -> Domínio está vazio
  [FALHA] '@sem-usuario.com' -> Parte do usuário está vazia
  [FALHA] 'sem-arroba.com' -> Email não contém @
  [FALHA] 'dois@@arrobas.com' -> Email contém mais de um @
  [FALHA] '' -> Email vazio
  [FALHA] 'a@b.c' -> Domínio não contém ponto
  [OK]    'usuario@dominio.com'
  [OK]    ' espacos@email.com '

=== Validação com crate validator ===
  [OK]    Cadastro { email: "usuario@exemplo.com", nome: "Ana Silva" }
  [FALHA] Cadastro { email: "invalido@", nome: "Bia" } -> email: Email inválido
  [FALHA] Cadastro { email: "valido@email.com", nome: "AB" } -> nome: Nome deve ter 3-50 caracteres

=== Processamento de Lista ===
Válidos (3):
  ana@empresa.com
  bia@gmail.com
  carlos@outlook.com
Inválidos (3):
  'invalido'
  ''
  'sem-arroba'

=== Normalização ===
  '  USUARIO@GMAIL.COM  ' -> 'usuario@gmail.com'
  'Ana@Empresa.COM' -> 'ana@empresa.com'
  'invalido' -> INVÁLIDO

Quando Usar Cada Abordagem

AbordagemVantagensDesvantagens
RegexRápida, padrão conhecidoRegex complexa, mensagens genéricas
ManualMensagens detalhadas, zero depsMais código, pode ter falhas
validatorIntegra com structs, derive macroDependência extra

Recomendação: Para aplicações web, use a crate validator integrada com suas structs de request. Para validações pontuais, regex é suficiente. A validação manual é útil quando você precisa de mensagens de erro detalhadas em português.

Veja Também