Detecção de formulário de contato soa como problema resolvido até você apontar isso para mil sites de empresas. As falhas não são exóticas. O formulário está lá e o parser não enxerga. O parser vê nove campos e escreve a mensagem no errado. A trava que deveria nos parar em um formulário protegido lê uma propriedade que não existe, recebe undefined de volta, e libera tudo. Chamamos de porta uma rota de contato pública utilizável, e é isto que achar uma envolve de verdade.
01A detecção de formulário de contato começa pela plataforma
A boa notícia primeiro: formulários de contato não são uma cauda longa. Entre as empresas que lemos, cinco coisas respondem pela maior parte do que aparece. HubSpot, ActiveCampaign, WPForms, Typeform, e um formulário HTML puro postando de volta para o site em que ele mora. Identifique a plataforma primeiro, depois rode o extrator escrito para ela. Uma passada genérica que coleta todo input da página erra pelo menos três das cinco, e erra sem levantar nada.
- HubSpot: a página carrega um portal id e um form guid, e nada do que você envia vai para a página que você está olhando. O envio é um POST JSON para um endpoint regional chaveado pelo portal, e a região faz parte do endereço em vez de ser um header, então um portal europeu enviado para o host padrão não é aceito por ninguém. A lista de campos vem da definição do formulário, não do DOM.
- ActiveCampaign: o embed é uma tag de script, e a marcação vive dentro de uma string JavaScript, com escape duplo. Sinais de menor e maior chegam como escapes unicode e toda aspa vem com barra invertida. Um parser de HTML apontado para aquela página não acha formulário nenhum, porque naquele momento não existe um. Desfaça o escape da string, parseie o resultado, e extraia do resultado.
- WPForms: os nomes são posicionais. Um input se chama wpforms[fields][3] e o 3 é um id de linha no banco WordPress de outra pessoa, então o nome não carrega significado. Quem carrega é o texto do label e o atributo de tipo do campo.
- Typeform: não existe formulário na página. Existe um fluxo hospedado descrito como blocos JSON, e a leitura honesta é que essa não é uma porta que possamos usar.
- HTML puro: um action, um method, e normalmente um token oculto que precisa ser devolvido junto com o cookie de sessão que o emitiu. Buscar e enviar precisam compartilhar o mesmo pote de cookies ou o post é rejeitado como falsificação, que é o comportamento correto da parte do site.
Cada um desses é um contrato de envio, e não uma peculiaridade de renderização. O extrator de um não é uma versão levemente ajustada do extrator de outro, e fingir que é produz um detector que reporta sucesso em quase toda página e uma taxa de entrega que não concorda com ele.
02O mapeamento de campos é onde fica difícil
Digamos que o formulário foi achado e parseado. Você agora tem entre quatro e onze inputs e uma mensagem para colocar. Qual caixa é a mensagem?
Pontuamos cada input por nome, id, placeholder, texto do label associado, aria-label, tipo de elemento e maxlength, e depois pegamos a melhor pontuação acima de um limiar. Um textarea é o sinal isolado mais forte e ainda assim não é decisivo: muitos formulários usam textarea para endereço, e alguns usam um input de linha única com maxlength de 500 para a mensagem. Assunto e mensagem são o par que mais dá errado. Os dois são texto, ficam um do lado do outro, e trocá-los dá ao destinatário um assunto de quatro parágrafos.
Depois há os campos que precisam ser deixados em paz. Um honeypot é um input escondido de uma pessoa e deixado visível para um script ingênuo, e preenchê-lo é exatamente como o site aprende que você é um script. Marcamos um input como honeypot em qualquer um destes casos: escondido por display ou visibility, posicionado fora da tela, opacidade zero, tabindex negativo, aria-hidden, ou um nome do conjunto de sempre (url, website, hp, _gotcha, ou comment quando já existe um campo de comentário). Um input marcado é enviado exatamente como o encontramos. Isso não é contornar nada. É preencher o formulário do jeito que o navegador na frente de uma pessoa preencheria.
Abaixo do limiar não chutamos. Não há porta, a leitura estaciona com o motivo escrito, e ela aparece no registro de recusas como recusada em vez de como tentativa silenciosa. Postar um parágrafo no campo rotulado "Como você ficou sabendo da gente" é pior que não postar nada, porque gasta a atenção de um estranho dentro de um formulário ao qual ele não pode responder.
03Dois bugs que ficaram quietos exatamente do jeito errado
O primeiro é a pior coisa que já subimos. O detector devolve um resultado aninhado e o achado do CAPTCHA fica em result.form.captcha. A trava lia result.captcha. Essa propriedade é undefined, undefined é falsy, e a trava concluiu portanto que nenhum formulário em lugar nenhum tinha CAPTCHA. Nada lançou erro. Nada logou. Formulários protegidos passaram por quase uma tarde inteira, e isso só apareceu porque uma checagem manual contra um formulário que sabíamos ser protegido voltou limpa e ninguém acreditou no resultado.
A correção é uma regra, e não um caminho de propriedade corrigido. A trava agora exige um false explícito para prosseguir. Qualquer outra coisa estaciona o job: undefined, null, um objeto ausente, uma string, um valor truthy. Afirmamos o formato do resultado do detector na fronteira em vez de ler através dele com otimismo, e existe um teste que entrega à trava um resultado com a flag movida um nível mais fundo e espera um estacionamento. Ele falha contra o código antigo, que é a única definição útil de teste de regressão.
O segundo bug é menor e custou mais horas. O casamento de campos permitia casar por sufixo, então um input chamado contact[email] e um input chamado contact[secondary_email] satisfaziam ambos uma regra que procurava um campo de e-mail, e o vencedor era o que viesse por último na ordem do documento. Em um formulário em que o endereço secundário era opcional e sem validação, mapeamos um campo obrigatório sobre o opcional. O post deu certo, o site devolveu uma página de agradecimento, e a mensagem caiu em um campo que ninguém lê.
Três regras resolvem isso agora, aplicadas em ordem. Um casamento exato de nome vence um casamento por sufixo e encerra a busca. Obrigatório vence opcional, lido do atributo required, do aria-required, e da definição de campo da plataforma quando a plataforma fornece uma. Empates vão para a posição anterior no documento. O mapeamento resultante é escrito no registro de auditoria, então a pessoa que aprova o rascunho consegue ver em qual input cada valor vai cair antes de aprovar, e não depois.
04Onde paramos
Nunca resolvemos nem contornamos um CAPTCHA. Nem com um serviço de solver, nem com um navegador ajustado para parecer mais humano, nem caçando o endpoint legado desprotegido que fica atrás do protegido, que é o mesmo ato com modos melhores. Quando o detector reporta um CAPTCHA, o job vai para needs_review e fica ali até uma pessoa abrir. Na maior parte das vezes ninguém abre, e isso é um resultado em vez de uma falha.
Um CAPTCHA em um formulário de contato é uma preferência declarada. O dono disse, em quase o único vocabulário disponível para dizer isso a um software, que quer uma pessoa do outro lado. Estamos no ramo de pedir a estranhos que considerem comprar alguma coisa, e essa conversa não abre bem demonstrando que as preferências deles não nos vinculam.
Algumas outras coisas não se qualificam como porta. Um formulário atrás de login não é público. Um formulário de ticket de suporte é o público errado, porque é uma fila mantida para clientes pagantes e uma mensagem de vendas ali custa a tarde de alguém. Um cadastro de newsletter não é rota de contato. Cada um desses é registrado como leitura recusada com motivo escrito, o mesmo tratamento que uma empresa recebe ao reprovar em um filtro de encaixe.
A entrega hoje passa pelo formulário de contato público da empresa alvo e por mais nada. O envio de e-mail está planejado para o quarto trimestre de 2026 e não está construído, então não há rota alternativa quando falta uma porta e não há segundo canal quando o primeiro está protegido. Essa ausência é o que mantém a regra honesta. Se existisse uma alternativa fácil, estacionar o job não custaria nada, e uma regra que não custa nada não diz nada.
Nada disso é engenhoso. É um detector de plataforma, uma função de pontuação, duas travas que falham fechando, e um motivo escrito anexado a toda leitura sobre a qual não agimos. A versão engenhosa deste sistema reportaria uma contagem maior de envios realizados e seria pior.