How to Add a Custom Website Module to Holehe for Email Checking

To add a custom website module to Holehe, create an async Python function in the correct sub-package under holehe/modules, follow the standardized result dictionary format, and register the domain mapping in holehe/core.py.

Holehe discovers registered email addresses across online services through a modular architecture. Each module is a self-contained async function that performs one check and reports back through a standardized interface. Extending the tool requires understanding this pattern and replicating it for your target service.

Understanding Holehe's Module Architecture

Holehe loads modules dynamically from the holehe.modules package tree. The discovery and execution flow in holehe/core.py works as follows:

  • import_submodules("holehe.modules") recursively imports every .py file under the modules directory
  • get_functions() extracts callables whose names match their filename (e.g., google from google.py)
  • launch_module() executes each function inside a Trio nursery with shared state
  • Each function receives: email (target string), client (shared httpx.AsyncClient), and out (result list)

Your custom module must follow this exact signature and append a dictionary with specific keys to the out list.

Creating a Custom Module: File Structure

Place new modules under an appropriate category sub-directory. For email services, use holehe/modules/mails/. The filename becomes the module identifier.


holehe/
└── modules/
    └── mails/
        ├── __init__.py
        ├── google.py          # Built-in example

        └── example_site.py    # Your custom module

Required Module Signature

Every module must define this exact async function signature:

async def example_site(email, client, out):
    # implementation
  • email: The target email address as string
  • client: Pre-configured httpx.AsyncClient with proxy/cookie support
  • out: List to which you append exactly one result dictionary

Writing the Module Implementation

This complete example demonstrates the pattern used by built-in modules like google.py:


# File: holehe/modules/mails/example_site.py

# -------------------------------------------------

from holehe.core import *
from holehe.localuseragent import *

async def example_site(email, client, out):
    """
    Checks whether an email is registered on ExampleSite.
    """
    # Metadata identifiers

    name = "example_site"               # Must match function name

    domain = "example.com"              # Human-readable service domain

    method = "login"                    # Technique used (login, registration, etc.)

    frequent_rate_limit = False         # Does this service often rate-limit?

    # -----------------------------------------------------------------

    # 1️⃣  Prepare request headers – mimic a real browser.

    # -----------------------------------------------------------------

    headers = {
        "User-Agent": random.choice(ua["browsers"]["firefox"]),
        "Accept": "*/*",
        "Accept-Language": "en-US,en;q=0.5",
        "Content-Type": "application/x-www-form-urlencoded",
    }

    # -----------------------------------------------------------------

    # 2️⃣  Perform the HTTP request appropriate for the service.

    # -----------------------------------------------------------------

    try:
        resp = await client.post(
            "https://api.example.com/check",
            headers=headers,
            data={"email": email},
        )
    except Exception:
        # Network failure → report as rate-limited with unknown existence

        out.append({
            "name": name,
            "domain": domain,
            "method": method,
            "frequent_rate_limit": frequent_rate_limit,
            "rateLimit": True,
            "exists": False,
            "emailrecovery": None,
            "phoneNumber": None,
            "others": None,
        })
        return

    # -----------------------------------------------------------------

    # 3️⃣  Parse response – adjust logic to service's API behavior.

    # -----------------------------------------------------------------

    json_resp = resp.json()
    if json_resp.get("registered") is True:
        exists = True
        rate_limit = False
    elif json_resp.get("error") == "rate_limited":
        exists = False
        rate_limit = True
    else:
        exists = False
        rate_limit = False

    # -----------------------------------------------------------------

    # 4️⃣  Append standardized result to output list.

    # -----------------------------------------------------------------

    out.append({
        "name": name,
        "domain": domain,
        "method": method,
        "frequent_rate_limit": frequent_rate_limit,
        "rateLimit": rate_limit,
        "exists": exists,
        "emailrecovery": None,      # Optional: extract if service leaks recovery email

        "phoneNumber": None,        # Optional: extract if service leaks phone

        "others": None,             # Optional: additional leaked information

    })

Key Implementation Requirements

  • Use random.choice(ua["browsers"]["firefox"]) from holehe.localuseragent for realistic headers
  • Always append to out — even on exceptions — or the module hangs the execution pool
  • Handle all exceptions gracefully; network errors are common against rate-limiting services
  • Never return values directly — the out.append() pattern is mandatory for Trio concurrency

Registering the Domain Mapping

Holehe's print_result() function relies on a data dictionary in holehe/core.py to map module names to display domains. Without this entry, your module runs but its domain appears incorrectly in output.

Add your module's domain mapping in holehe/core.py, inside launch_module() around line 166:


# File: holehe/core.py – data dictionary in launch_module()

# -------------------------------------------------------

data = {
    "google": "google.com",
    "microsoft": "outlook.com",
    # ... existing entries ...

    "example_site": "example.com",   # ← Add this line

}

This mapping ensures print_result() displays example.com when reporting your module's findings.

Locating Critical Source Files

Purpose File Path Description
Reference implementation holehe/modules/mails/google.py Clean example of production email-checking logic
Module discovery & execution holehe/core.py Contains import_submodules(), get_functions(), launch_module(), and the data registry
Package structure holehe/modules/__init__.py Ensures sub-packages are importable
Request fingerprinting holehe/localuseragent.py Browser User-Agent strings for header rotation

Study google.py as the canonical reference — it demonstrates proper header construction, response handling, and result formatting for a major service.

Testing Your Custom Module

After creating your file and updating core.py:

  1. Install in editable mode (if not already): pip install -e .
  2. Run Holehe against your target: holehe -e test@example.com
  3. Verify output: Your domain value should appear in the results table
  4. Check with --only flag: holehe -e test@example.com --only example_site to isolate your module

If your module doesn't appear, confirm:

  • The filename matches the function name exactly
  • The file is in a sub-directory with an __init__.py
  • No syntax errors during import (run python -c "from holehe.modules.mails import example_site")

Summary

  • Custom modules are async functions in holehe/modules/ sub-packages with a fixed three-parameter signature
  • Mandatory output: Append a dictionary with nine specific keys (name, domain, method, frequent_rate_limit, rateLimit, exists, emailrecovery, phoneNumber, others) to the out list
  • Domain registration: Add the module_name: domain.com pair to data in holehe/core.py for correct display
  • Reference pattern: Copy the structure from holehe/modules/mails/google.py for reliable results
  • Dynamic loading: No additional registration needed — Holehe discovers your module automatically on next execution

Frequently Asked Questions

What happens if my module doesn't append to the out list?

The Trio nursery in launch_module() waits for all concurrent tasks. If your function returns without appending, the execution hangs indefinitely. Always call out.append() before returning, including in exception handlers.

Can I use synchronous requests instead of httpx.AsyncClient?

No. Holehe's concurrency depends on Trio and async I/O throughout. Using blocking calls like requests would deadlock the event loop. Stick to client.get(), client.post(), and other httpx.AsyncClient methods.

Where should I place a module for a non-email service?

Use an appropriate category directory under holehe/modules/: mails/ for email providers, social/ for social networks, shopping/ for e-commerce, or create a new category with an __init__.py. The discovery mechanism recursively searches all sub-packages.

How do I extract leaked information like recovery emails?

Populate the optional keys in your result dictionary when the service's response contains PII. For example, if a password reset endpoint returns a masked phone +1 ***-***-1234, set "phoneNumber": "+1 ***-***-1234" and "others": {"masked": true}. This data appears in Holehe's detailed output and JSON exports.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →