If your email address appears as plain text anywhere in your site's HTML, assume it will be harvested. Obfuscation tricks like name [at] domain slow the dumbest bots and annoy every human. What actually works is not publishing a harvestable address at all: use a contact form with a honeypot, and when you do want to publish an address, make it a dedicated per-project one you can retire, not your personal inbox.

How harvesting actually works

Email harvesters are simple crawlers. They fetch pages the same way a search engine does, then run regular expressions over the HTML looking for anything shaped like something@domain.tld. They don't need to be clever, because most sites hand them exactly what they want: a mailto:link. That's the easiest possible catch: the address sits in an href, in a standardized format, on an attribute crawlers already parse. You could not design a better beacon.

The part people underestimate is what happens next. A harvested address isn't spammed once; it gets compiled into lists, and the lists get sold, merged, and resold. An address that leaked years ago still collects spam today. There is no un-leak: once an address is in circulation, your options are aggressive filtering or abandoning it.

To keep this honest: a brand-new site with zero inbound links might not get crawled for a while. But any page that other pages link to will be visited, and it only takes one harvester one visit.

The classic tricks, and why they mostly fail

Text obfuscation: writing name [at] domain [dot] com. This pattern is decades old, and harvesters ship regexes for the common mutations: [at], (at), AT, extra spaces, all of it. Meanwhile, every real visitor has to reconstruct your address by hand and hope they typed it right. You've traded a guaranteed cost for humans against a filter that only catches the laziest bots.

JavaScript encoding: assembling the address at runtime from encoded pieces. This is genuinely better: a harvester that only fetches raw HTML sees nothing. But headless browsers are cheap now, and serious harvesters render pages before scanning them. HTML entity encoding (he…) is worse still: every parser normalizes entities before matching, so it stops nothing. And the costs land on your side: the address breaks with JavaScript disabled, and depending on the implementation, copy-paste and screen readers break too.

Images of your address: the worst trade of the three. Screen readers get nothing (and if you add the address as alt text, you've defeated the point). Nobody can copy it. It renders badly on high-DPI screens. And OCR is cheap enough that determined harvesters read images anyway.

The common thread: obfuscation is an arms race where the attacker's cost is a one-line regex update and your cost is every legitimate visitor's experience. It slows harvesting; it doesn't stop it. And it does nothing if the address leaks from anywhere else: a git commit, a newsletter, someone else's contact list.

What actually works

Two moves, and they're complementary.

1. A contact form with a honeypot

A form puts no address in your HTML at all, so there's nothing to harvest. Submissions post to an endpoint and land in an inbox. Forms do attract their own kind of spam (bots that stuff junk into any form they find), and the cheapest effective counter is a honeypot: a hidden field humans never see. Bots auto-fill every input, so a submission with the honeypot filled gets dropped.

<form id="contact">
  <input name="name" placeholder="Your name" />
  <input name="email" type="email" placeholder="you@example.com" required />
  <!-- Honeypot: humans never see it, bots fill it. -->
  <input name="_gotcha" style="position:absolute;left:-9999px" tabindex="-1" autocomplete="off" aria-hidden="true" />
  <textarea name="message" placeholder="What can I help with?" required></textarea>
  <button type="submit">Send</button>
</form>

<script>
  document.getElementById("contact").addEventListener("submit", async (e) => {
    e.preventDefault();
    await fetch("https://mailcontact.app/api/forms/your-form-id", {
      method: "POST",
      body: new FormData(e.target),
    });
    e.target.reset();
  });
</script>

mailcontact's form endpoint accepts plain FormData or JSON from any origin and answers with JSON, so submitting via fetch keeps visitors on the page (a configurable redirect for native form posts is coming with the launch). The honeypot wiring is covered in the spam protection docs.

2. A dedicated address per project

Some people won't use a form. They want to write from their own mail client, forward you a thread, keep a record in their sent folder. That's fine. Just don't give them your personal address. Every mailcontact project comes with its own yourproject@mailcontact.app address that works immediately, with no DNS records and no mail server to run.

The point is blast radius. If a per-project address gets harvested, the spam lands in that one project's inbox, not in the mailbox your bank writes to. And because the address is disposable, you can retire it and switch to a fresh one. That's the actual fix: not making the address unreadable, but making it not worth protecting.

The practical setup

  • Make a contact form your primary channel, honeypot enabled. If you want a head start, the contact form generator produces the markup for you, and there are stack-specific guides for static sites and GitHub Pages, where publishing your address in the HTML is often the only thing people think they can do.
  • Publish your project's address in plain text for people who prefer email. Yes, plain text. It's fine, because the address is disposable.
  • Both sources feed the same project inbox, with notifications in Discord or email so you're not polling a dashboard.
  • Your personal address never appears in any HTML, anywhere.

None of this makes spam zero. Honeypots catch dumb bots, not humans paid to fill in forms, and a published address will eventually collect some junk. The difference is that the problem becomes containable: you rotate one project address or tighten one form, instead of living with a decade of spam on the address you can't change.

If you're setting up a form on a static site from scratch, this guide walks through the options.