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
- Valide os campos: nome com tamanho máximo, e-mail com
filter_var, mensagem com mínimo e máximo de caracteres. - Descarte em silêncio o envio que preencheu o campo isca.
- Aplique limite de envios por IP numa janela de tempo.
- Grave a mensagem no banco com status
pendente. - Envie por SMTP autenticado, com biblioteca que monta cabeçalhos com segurança.
- Atualize o status para
enviadaoufalhoue registre o erro no log. - 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?
Referência: http://blog.makedesignbr.com.br/arquivos/343