Programação Por Equipe DevBras

Capa do artigo: Formulário de contato que chega na caixa de entrada e não na do spammer

Formulário de contato que chega na caixa de entrada e não na do spammer

Monte um formulário de contato em PHP que grava antes de enviar, usa SMTP autenticado, não deixa o visitante mexer em cabeçalho e segura robôs.

A pessoa preenche o formulário, clica em enviar e vê "mensagem enviada com sucesso". Você nunca recebe. Ou recebe cinquenta mensagens por hora de gente que não existe, oferecendo remédio em inglês. Os dois sintomas costumam sair do mesmo código: um mail() com os campos do formulário colados nos cabeçalhos. A resposta curta: grave a mensagem no banco antes de tentar enviar, mande por SMTP autenticado de um serviço de envio, com remetente do seu domínio e destinatário fixo no código, e use o e-mail do visitante só no Reply-To. Um campo isca e um limite por IP seguram a maior parte dos robôs.

Por que a mensagem some

A função mail() do PHP não envia e-mail. Ela entrega a mensagem para o programa de envio configurado no servidor e devolve true se ele aceitou. Se esse programa não existe, como em muito contêiner e hospedagem moderna, ou se ele tenta entregar e o destino recusa, você não fica sabendo. O "enviado com sucesso" na tela significa só que o PHP não reclamou.

Quando a mensagem sai, outro filtro entra em cena. Provedores como Gmail e Outlook conferem se o servidor que enviou tem autorização para falar em nome do domínio do remetente. Isso é o SPF, a assinatura DKIM e a política DMARC, configurados no DNS do domínio. Formulário que manda e-mail com From igual ao endereço do visitante, digamos fulano@gmail.com, a partir do seu servidor, está falsificando o remetente. O destino percebe e joga no spam ou recusa. Manter um servidor de e-mail próprio para resolver isso é um projeto à parte, e quase nunca vale a pena, como explica o guia sobre servidor de e-mail.

Por que o formulário vira retransmissor de spam

Cabeçalho de e-mail é texto separado por quebra de linha. Se o campo "assunto" ou "e-mail" do formulário vai direto para o cabeçalho, alguém envia um valor com quebra de linha seguida de Bcc: lista-de-mil-enderecos. O seu servidor passa a mandar spam para terceiros, assinado pelo seu domínio, e a reputação do domínio despenca. O mesmo vale para formulário que aceita o destinatário num campo escondido: quem lê o HTML troca o valor.

A regra é simples. Destinatário, remetente e assunto são fixos no servidor. O que o visitante digitou vai no corpo da mensagem e, o e-mail dele, validado, no Reply-To, para você responder com um clique.

Como montar o envio passo a passo

  1. Valide os campos: nome com tamanho máximo, e-mail com filter_var, mensagem com mínimo e máximo de caracteres.
  2. Descarte em silêncio o envio que preencheu o campo isca.
  3. Aplique limite de envios por IP numa janela de tempo.
  4. Grave a mensagem no banco com status pendente.
  5. Envie por SMTP autenticado, com biblioteca que monta cabeçalhos com segurança.
  6. Atualize o status para enviada ou falhou e registre o erro no log.
  7. Redirecione com 303 para uma página de confirmação, para um F5 não reenviar.

O exemplo usa a biblioteca PHPMailer, instalada com composer require phpmailer/phpmailer. Symfony Mailer resolve o mesmo problema, se o projeto já usa Symfony. Host, usuário e senha do SMTP vêm do serviço de envio que você contratar e ficam em variáveis de ambiente.

<?php
declare(strict_types=1);

use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception as MailerException;

require __DIR__ . '/vendor/autoload.php';
$pdo = require __DIR__ . '/conexao.php';

if (($_POST['website'] ?? '') !== '') {
    header('Location: /contato/obrigado', true, 303);
    exit;
}

$nome = trim((string) ($_POST['nome'] ?? ''));
$email = trim((string) ($_POST['email'] ?? ''));
$mensagem = trim((string) ($_POST['mensagem'] ?? ''));

$erros = [];
if ($nome === '' || mb_strlen($nome) > 100) {
    $erros[] = 'Informe seu nome.';
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    $erros[] = 'Informe um e-mail válido.';
}
if (mb_strlen($mensagem) < 10 || mb_strlen($mensagem) > 5000) {
    $erros[] = 'A mensagem precisa ter entre 10 e 5.000 caracteres.';
}
if ($erros) {
    http_response_code(422);
    exit(implode(' ', $erros));
}

$stmt = $pdo->prepare(
    "INSERT INTO mensagens_contato (nome, email, mensagem, ip, status, criado_em)
     VALUES (?, ?, ?, ?, 'pendente', NOW())"
);
$stmt->execute([$nome, $email, $mensagem, $_SERVER['REMOTE_ADDR']]);
$id = (int) $pdo->lastInsertId();

$mail = new PHPMailer(true);
try {
    $mail->isSMTP();
    $mail->Host = getenv('SMTP_HOST');
    $mail->SMTPAuth = true;
    $mail->Username = getenv('SMTP_USER');
    $mail->Password = getenv('SMTP_PASS');
    $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
    $mail->Port = 587;
    $mail->CharSet = 'UTF-8';

    $mail->setFrom('site@exemplo.com.br', 'Site Exemplo');
    $mail->addAddress('contato@exemplo.com.br');
    $mail->addReplyTo($email, $nome);
    $mail->Subject = 'Nova mensagem pelo formulário do site';
    $mail->Body = "Nome: $nome\nE-mail: $email\n\n$mensagem";
    $mail->send();

    $pdo->prepare("UPDATE mensagens_contato SET status = 'enviada' WHERE id = ?")->execute([$id]);
} catch (MailerException $e) {
    error_log('Contato ' . $id . ' não enviado: ' . $mail->ErrorInfo);
    $pdo->prepare("UPDATE mensagens_contato SET status = 'falhou' WHERE id = ?")->execute([$id]);
}

header('Location: /contato/obrigado', true, 303);
exit;

Repare que a confirmação aparece mesmo quando o envio falha. Isso é honesto aqui porque a mensagem está gravada: você vai receber, só que um pouco depois. Um job agendado procura as linhas com status falhou e tenta de novo, e um painel simples mostra as mensagens mesmo que o e-mail nunca chegue. Se você não grava no banco, aí sim a tela deve dizer que não deu certo e pedir outro canal.

O corpo vai como texto puro, que é o padrão do PHPMailer. Se você montar o corpo em HTML, escape tudo o que veio do visitante com htmlspecialchars, senão a mensagem pode carregar links e scripts disfarçados. O redirecionamento com status 303 faz o navegador abrir a página de obrigado com GET, e recarregar essa página não reenvia o formulário.

Como segurar robôs sem atrapalhar gente

O campo isca, chamado website no exemplo, fica no formulário, escondido por CSS e fora da ordem de tabulação, com autocomplete="off". Pessoas não veem e não preenchem. Robôs simples preenchem tudo. Quando ele chega com valor, o código finge sucesso e não grava nada, para o robô não aprender.

O limite por IP é a segunda camada: por exemplo, no máximo cinco mensagens por IP a cada hora, contando na própria tabela de mensagens. Se o spam continuar, um desafio como Cloudflare Turnstile ou reCAPTCHA resolve a maior parte do resto, ao custo de um script de terceiro na página. Comece pelo campo isca e pelo limite, que não incomodam ninguém.

No DNS do domínio do remetente, configure SPF e DKIM conforme as instruções do serviço de envio, e publique uma política DMARC. Sem isso, até o SMTP autenticado pode cair no spam da sua própria caixa.

Erros comuns

  • Confiar no retorno de mail(). Ele diz que o PHP entregou para o sistema, não que a mensagem chegou.
  • Usar o e-mail do visitante como From. É falsificação de remetente, e o provedor de destino joga no spam ou recusa.
  • Destinatário ou assunto vindos do formulário. O site vira retransmissor de spam com a reputação do seu domínio.
  • Enviar sem gravar. Qualquer falha do SMTP faz o contato de um cliente sumir sem rastro.
  • Mostrar "enviado" quando não foi e nada ficou gravado. O cliente espera resposta que nunca vem.
  • Colocar a senha do SMTP no código. Ela vai para o repositório, e quem a tiver manda e-mail em nome da empresa.

Checklist

  • Destinatário, remetente e assunto são fixos no servidor.
  • O e-mail do visitante vai no Reply-To, depois de validado.
  • Toda mensagem é gravada no banco antes do envio, com status.
  • O envio usa SMTP autenticado com TLS, e as credenciais estão em variáveis de ambiente.
  • SPF, DKIM e DMARC do domínio remetente estão publicados.
  • Existe campo isca e limite de envios por IP.
  • Falhas de envio vão para o log e são reenviadas por um job.
  • Depois do envio, o navegador é redirecionado com 303.

Mande agora uma mensagem de teste pelo seu formulário e outra com o e-mail sem arroba. A primeira chegou na caixa de entrada, não no spam? A segunda parou na validação? E, se o servidor de e-mail estivesse fora do ar hoje, onde a primeira estaria guardada?

Avalie este artigo

Referência: http://blog.makedesignbr.com.br/arquivos/343