[DRAFT]Web Security7 min read

$ The Arms Race of Email Protection: Why We Hide Addresses and How Scrapers Adapt

A deep dive into email harvesting, obfuscation techniques, and the evolving cat-and-mouse game between website owners and spam infrastructure in 2025.

Cover image for: The Arms Race of Email Protection: Why We Hide Addresses and How Scrapers Adapt
// cover_image.render()

For decades, web developers have played a cat-and-mouse game with spammers: hide your email address creatively enough that humans can find it, but bots cannot. It’s 2025, and this arms race continues—but the rules have changed dramatically. Let’s explore why we hide emails, how attackers have adapted, and what actually works today.

Part 1: The Problem — Email Harvesting at Scale

What Is Email Harvesting?

Email harvesting is the automated collection of email addresses from public sources for unsolicited commercial purposes (spam), phishing attacks, or resale to data brokers. The process is surprisingly straightforward:

  1. Discovery: Crawlers scan websites, forums, GitHub repos, WHOIS records, and social media
  2. Extraction: Pattern matching finds anything resembling [email protected]
  3. Validation: Addresses are verified using SMTP checks or “email append” services
  4. Monetization: Sold to spammers ($0.10-$1 per verified address), used for phishing, or fed into targeted advertising networks

Who’s Harvesting?

Casual Scrapers (Low Sophistication)

  • Simple regex-based crawlers looking for [\w\.-]+@[\w\.-]+\.\w+
  • Scrape public pages, don’t execute JavaScript
  • Stopped by basic obfuscation (HTML entities, [at] replacements)

Professional Operations (Medium Sophistication)

  • Use headless browsers (Puppeteer, Playwright) to execute JavaScript
  • Can solve simple CAPTCHAs using third-party services
  • Bypass basic JavaScript obfuscation by evaluating DOM state
  • Often employ residential proxies to avoid IP-based rate limiting

State-of-the-Art (2025) (High Sophistication)

  • LLM-powered parsing that understands context: “reach me at john dot smith at company dot com”
  • OCR for image-based email addresses
  • Browser automation frameworks that mimic human behavior (mouse movements, realistic timing)
  • Can defeat most honeypots and behavioral CAPTCHAs

Where Emails Get Discovered

Your email address leaks from more places than you think:

# Common discovery vectors
- Public website contact pages (highest volume)
- GitHub commit history (git log --all --format='%aE' | sort -u)
- WHOIS records (legacy domains without privacy protection)
- Public forums, mailing list archives
- LinkedIn, Twitter bios
- Academic paper author sections (DBLP, ArXiv)
- Conference speaker lists, press releases
- Cached versions (Wayback Machine, Google Cache)
- PDF metadata (Document Properties > Author)

Real-world volume: A typical scraping operation can harvest 100K-1M email addresses per day from public web sources. A single exposed GitHub organization can leak thousands of employee emails.

Part 2: Threat Model — Who Are You Protecting Against?

Static Personal Blog (Your Use Case)

Threat Level: Medium Primary Risk: Generic spam campaigns, phishing attempts Secondary Risk: Targeted harassment (if you become notable)

You’re probably not worried about nation-state actors, but you do want to avoid:

  • Your inbox becoming unusable (100+ spam emails/day)
  • Sophisticated phishing attempts that reference your blog content
  • Email ending up in data broker databases permanently

High-Value Targets

Journalists, activists, executives, or anyone handling sensitive information face:

  • Spear phishing campaigns using harvested emails
  • Social engineering attacks combining email with other OSINT
  • Persistent adversaries willing to defeat sophisticated protections

Part 3: Obfuscation Techniques — A Taxonomy

3.1 Text-Based Obfuscation

Human-Readable Formats

<!-- The classic -->
name [at] domain [dot] com

<!-- Unicode substitution -->
name@domain․com  <!-- using fullwidth @ and one-dot leader -->

<!-- ROT13 encoding -->
[email protected]  <!-- decodes to [email protected] -->

<!-- Zero-width characters -->
na​me@dom​ain.com  <!-- contains zero-width spaces -->

Effectiveness (2025): ⭐☆☆☆☆ Why it fails: LLMs trivially parse these variations. GPT-4 class models understand context like “reach out at john [at] company [dot] com” perfectly. Even traditional regex scrapers now include common obfuscation patterns.

3.2 HTML Entity Encoding

<!-- Character entities -->
<a href="mailto:&#110;&#97;&#109;&#101;&#64;&#100;&#111;&#109;&#97;&#105;&#110;&#46;&#99;&#111;&#109;">
  Contact me
</a>

<!-- Hex entities -->
&#x6e;&#x61;&#x6d;&#x65;&#x40;&#x64;&#x6f;&#x6d;&#x61;&#x69;&#x6e;&#x2e;&#x63;&#x6f;&#x6d;

<!-- Mixing styles -->
nam&#101;&#x40;domain&#46;com

Effectiveness (2025): ⭐⭐☆☆☆ Pros: Stops naive text scrapers Cons: Any HTML parser (built into every scraping library) decodes these instantly. Still useful as a first layer.

3.3 JavaScript Reconstruction

Client-Side Assembly

<span class="email-user">name</span>
<span class="email-at">@</span>
<span class="email-domain">domain.com</span>

<script>
document.addEventListener('DOMContentLoaded', () => {
  const parts = {
    user: document.querySelector('.email-user').textContent,
    at: document.querySelector('.email-at').textContent,
    domain: document.querySelector('.email-domain').textContent
  };

  const email = parts.user + parts.at + parts.domain;
  const link = document.createElement('a');
  link.href = 'mailto:' + email;
  link.textContent = email;

  document.querySelector('.email-container').replaceWith(link);
});
</script>

More Advanced: Encrypted Email

// ROT13 cipher
function rot13(str) {
  return str.replace(/[a-zA-Z]/g, c =>
    String.fromCharCode((c <= 'Z' ? 90 : 122) >= (c = c.charCodeAt(0) + 13) ? c : c - 26)
  );
}

// Store encrypted, decrypt on click
const encrypted = "[email protected]";
document.getElementById('email-link').addEventListener('click', e => {
  e.preventDefault();
  window.location.href = 'mailto:' + rot13(encrypted);
});

Effectiveness (2025): ⭐⭐⭐☆☆ Pros: Defeats simple crawlers; invisible to search engines Cons:

  • Headless browsers execute JavaScript naturally
  • Accessibility nightmare (screen readers fail)
  • Requires JavaScript enabled (5-10% of users have it disabled)
  • Can still be extracted from page state after rendering

3.4 Visual Obfuscation

CSS-Based Hiding

<style>
.email-reversed { unicode-bidi: bidi-override; direction: rtl; }
.email-parts::after { content: '@'; }
</style>

<!-- Displays correctly but source is reversed -->
<span class="email-reversed">moc.niamod@eman</span>

<!-- CSS-generated @ symbol -->
<span>name<span class="email-parts"></span>domain.com</span>

Image-Based

<!-- Email as image -->
<img src="data:image/svg+xml;base64,..." alt="Contact email" />

<!-- Canvas rendering -->
<canvas id="email-canvas"></canvas>
<script>
const canvas = document.getElementById('email-canvas');
const ctx = canvas.getContext('2d');
ctx.font = '14px monospace';
ctx.fillText('[email protected]', 10, 20);
</script>

Effectiveness (2025): ⭐⭐⭐⭐☆ Pros: OCR requires significantly more effort; defeats text-based scrapers entirely Cons:

  • Screen reader accessibility: zero
  • Can’t copy-paste on mobile
  • Modern OCR (Tesseract, cloud APIs) handles clean text at 99%+ accuracy
  • LLMs with vision capabilities (GPT-4V, Claude 3) can read images

3.5 Contact Forms (The Nuclear Option)

<!-- Instead of email, provide a form -->
<form action="/api/contact" method="POST">
  <input type="text" name="name" required>
  <input type="email" name="reply_to" required>
  <textarea name="message" required></textarea>

  <!-- Honeypot field (hidden from humans) -->
  <input type="text" name="website" style="display:none" tabindex="-1" autocomplete="off">

  <!-- Rate limiting via session tokens -->
  <input type="hidden" name="csrf_token" value="{{csrf_token}}">

  <button type="submit">Send</button>
</form>

Backend Protection Layers

// Rate limiting (per IP)
const rateLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 3, // 3 submissions per window
  message: 'Too many submissions, please try again later'
});

// Honeypot check
if (req.body.website !== '') {
  return res.status(400).send('Bot detected');
}

// CAPTCHA verification
const captchaValid = await verifyCaptcha(req.body.captcha_response);
if (!captchaValid) {
  return res.status(400).send('CAPTCHA verification failed');
}

// Email validation + MX record check
const emailValid = await validateEmailWithMX(req.body.reply_to);

Effectiveness (2025): ⭐⭐⭐⭐⭐ Pros: Email never exposed publicly; full control over spam filtering Cons:

  • Requires server-side infrastructure (not suitable for static sites)
  • Friction: people just want to click mailto: links
  • CAPTCHA fatigue (users hate them)
  • Maintenance overhead (forms break, need monitoring)

3.6 Email Aliases and Forwarding

Plus Addressing

# Gmail/Outlook support + addressing
[email protected]      # Auto-forwards to [email protected]
[email protected]   # Easy to filter/track

# Set up filter rules:
# If TO: [email protected] → Label: "Blog Inquiries"

Catch-All Domains

# Configure catch-all on your domain
[email protected] forwards to [email protected]

# Use context-specific addresses
[email protected]
[email protected]
[email protected]

# If one gets compromised, disable that specific alias

Disposable Forwarding Services

  • SimpleLogin / AnonAddy: Generate unlimited aliases
  • Firefox Relay / Apple Hide My Email: Built into browsers/OS
  • ProtonMail: Includes alias management

Effectiveness (2025): ⭐⭐⭐⭐⭐ Pros: Real email still hidden; can revoke compromised aliases without changing primary address Cons: Requires DNS configuration; some services look “spammy” to recipients

Part 4: Pros, Cons, and Failure Modes

Accessibility Hell

<!-- Screen reader nightmare -->
<span aria-hidden="true">name[at]domain[dot]com</span>
<span class="sr-only">Email hidden to prevent spam</span>

The W3C’s WCAG guidelines are clear: if information is visible to sighted users, it must be available to assistive technology. Most obfuscation breaks this.

Real-world impact: 15% of users have some form of disability. Email obfuscation that breaks screen readers is actively discriminatory.

Mobile Pain Points

Try copying an email address that’s:

  • Split across multiple DOM nodes (triple-tap selection fails)
  • Rendered with CSS tricks (direction: rtl)
  • Embedded in an image (can’t copy at all)

User behavior data: Mobile users are 3x more likely to abandon contact if they can’t easily copy/paste your email.

SEO Tradeoffs

<!-- Search engines can't index this -->
<script>document.write('name' + '@' + 'domain.com')</script>

<!-- But they CAN index this -->
<a href="mailto:[email protected]">Contact</a>

Google explicitly states they don’t execute JavaScript for email indexing. If you want “contact emails” showing up in search results (sometimes desirable!), obfuscation works against you.

The Modern Scraper Advantage

Why most obfuscation fails (2025 edition):

# Headless browser scraping (2025)
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto('https://target-blog.com/contact')

    # Wait for JavaScript to execute
    page.wait_for_load_state('networkidle')

    # Extract all mailto: links after JS reconstruction
    emails = page.eval_on_selector_all(
        'a[href^="mailto:"]',
        'elements => elements.map(el => el.href.replace("mailto:", ""))'
    )

    browser.close()
    return emails

LLM-powered extraction:

# Using GPT-4 to understand obfuscation
import openai

page_content = """
Feel free to reach out to john [at] example [dot] com
or call me at (555) 123-4567
"""

response = openai.ChatCompletion.create(
    model="gpt-4",
    messages=[{
        "role": "user",
        "content": f"Extract all email addresses from this text, " +
                   f"regardless of obfuscation:\n\n{page_content}"
    }]
)

# Returns: ["[email protected]"]

Privacy and Compliance

GDPR Considerations: Email addresses are PII (Personally Identifiable Information). Exposing them publicly without consent could be problematic, especially in EU context.

Data Broker Pipeline: Once harvested, your email enters a permanent database:

  1. Sold to data broker (Epsilon, Acxiom, Oracle BlueKai)
  2. Enriched with other data (social profiles, purchase history)
  3. Sold/licensed to advertisers, insurance companies, political campaigns
  4. Nearly impossible to remove (California CCPA provides some rights)

Part 5: Recommendations for 2025

The Pragmatic Approach: Accept Some Risk

Philosophy: Perfect protection requires too much UX sacrifice. Instead, layer simple defenses and use disposable addresses.

Good / Better / Best Options

Good: HTML Entity Encoding + Alias

<!-- In your footer/contact page -->
<a href="mailto:&#98;&#108;&#111;&#103;&#64;&#121;&#111;&#117;&#114;&#100;&#111;&#109;&#97;&#105;&#110;&#46;&#99;&#111;&#109;">
  [email protected]
</a>

Setup:

  1. Create [email protected] as catch-all or alias
  2. Forward to real email
  3. If spam gets bad, disable alias and create new one

Pros: Simple, accessible, no JavaScript required Cons: Stops only unsophisticated scrapers (~60% of harvesters)

Better: Cloudflare Email Obfuscation + Alias

<!-- Cloudflare automatically protects this -->
<a href="mailto:[email protected]" data-cf-protected="true">
  Get in touch
</a>

Cloudflare’s Email Address Obfuscation (free feature):

  • Automatically encodes emails during HTML delivery
  • Uses JavaScript reconstruction
  • Includes anti-bot fingerprinting

Pros: Zero maintenance; handles edge cases; ~85% effective Cons: Requires Cloudflare; JS dependency

Best: Contact Form with Aggressive Anti-Spam

// Astro API endpoint: src/pages/api/contact.ts
import type { APIRoute } from 'astro';
import { Resend } from 'resend';

const resend = new Resend(import.meta.env.RESEND_API_KEY);

export const POST: APIRoute = async ({ request, clientAddress }) => {
  const data = await request.formData();
  const name = data.get('name');
  const email = data.get('email');
  const message = data.get('message');
  const honeypot = data.get('website'); // Should be empty

  // Honeypot check
  if (honeypot) {
    return new Response('Error', { status: 400 });
  }

  // Rate limiting (implement with Redis/KV)
  const rateKey = `rate_limit:${clientAddress}`;
  // ... check rate limit ...

  // Send email via transactional service
  await resend.emails.send({
    from: '[email protected]',
    to: '[email protected]',
    replyTo: email,
    subject: `Blog contact from ${name}`,
    text: message
  });

  return new Response('Message sent!', { status: 200 });
};

Pros: Zero email exposure; full spam control; professional Cons: Requires server or serverless functions; maintenance overhead

My Recommendation for Your Blog

Given that you’re running Astro on Vercel with a personal blog:

Tier 1 Protection (Implement Now):


<!-- Use a dedicated blog email alias -->
<a href="mailto:[email protected]">[email protected]</a>

Set up [email protected] as an alias forwarding to your real email. If it gets compromised, change it to contact@ or hello@.

Tier 2 Protection (If Spam Gets Bad):


<!-- Add Cloudflare Email Obfuscation -->
<!-- Automatically enabled when proxying through Cloudflare -->
<a href="mailto:[email protected]" class="email-link">
  Get in touch
</a>

Tier 3 Protection (Nuclear Option): Build a simple contact form using Vercel Functions + Resend/SendGrid. Store it as /src/pages/contact.astro alongside your current contact page.

What NOT to Do (2025 Edition)

Don’t: Use pure JavaScript obfuscation without progressive enhancement ✅ Do: Provide mailto: fallback wrapped in <noscript>

Don’t: Put email in images without alt text ✅ Do: Include proper ARIA labels and copy-to-clipboard button

Don’t: Use aggressive CAPTCHAs (hCaptcha, reCAPTCHA v2) ✅ Do: Use invisible/score-based protection (reCAPTCHA v3, Cloudflare Turnstile)

Don’t: Rely solely on client-side validation ✅ Do: Layer defenses (honeypots + rate limiting + email validation)

Part 6: The Future (2026 and Beyond)

The Scraper Evolution

Next-gen threats:

  • Multimodal LLMs: Vision + language models that can understand context from images, videos, even audio
  • Behavioral fingerprinting: Bots that perfectly mimic human mouse movement, typing patterns
  • Distributed scraping: Residential proxy networks with millions of IPs
  • AI-powered CAPTCHA solving: Services offering 99%+ success rates

The Defense Evolution

Emerging protections:

  • Zero-knowledge proofs: Prove you’re human without revealing identity
  • Web3 contact systems: Decentralized identity + encrypted messaging
  • Disposable everything: Email addresses that expire after first contact
  • AI-powered spam filtering: Models trained on your specific communication patterns

The Realistic Middle Ground

Email obfuscation is fundamentally a losing battle against determined attackers. The goal isn’t perfect protection—it’s raising the cost of harvesting your address enough that bulk scrapers skip you for easier targets.

The 2025 pragmatist’s checklist:

  • ✅ Use email aliases you can rotate
  • ✅ Implement one layer of protection (entities or Cloudflare)
  • ✅ Keep accessibility in mind
  • ✅ Have aggressive spam filtering on the receiving end
  • ✅ Accept that some spam is inevitable
  • ✅ Don’t sacrifice UX for marginal security gains

Conclusion: Choose Your Own Adventure

For a personal blog in 2025, the sweet spot is:

  1. Use a dedicated alias ([email protected])
  2. Add basic obfuscation (HTML entities or Cloudflare)
  3. Deploy aggressive spam filtering (Gmail’s AI is excellent; Proton offers privacy)
  4. Accept some spam as the cost of being accessible

The arms race continues, but with smart layering and disposable identities, you can stay one step ahead without sacrificing the human connections that make having a blog worthwhile.


What’s your email protection strategy? Have you found techniques that work well without frustrating real humans? I’m at [email protected]—yes, I practice what I preach with an alias. 😉

//WAS THIS HELPFUL?