How to Add a New Website Module to holehe Following the Existing Module Pattern

Creating a new website module for holehe requires implementing a single async function with the signature async def <module_name>(email, client, out) in a Python file under holehe/modules/<category>/, with no registration step needed.

holehe automatically discovers every check routine by walking the package holehe.modules using the import_submodules helper in holehe/core.py. This means you can add a new website module simply by creating a properly structured Python file—the framework handles the rest. This guide walks you through the exact pattern used by existing modules like the Gmail check in holehe/modules/mails/google.py.

Understanding the Module Discovery System

In holehe/core.py at lines 37-47, the import_submodules function recursively imports all Python files beneath holehe.modules:

def import_submodules(package, recursive=True):
    """Import all submodules of a package."""
    results = {}
    for _, name, is_pkg in pkgutil.walk_packages(package.__path__, package.__name__ + '.'):
        results[name] = importlib.import_module(name)
        if recursive and is_pkg:
            results.update(import_submodules(sys.modules[name]))
    return results

Because of this automatic discovery, you never need to register or list your new module manually. Placing a valid .py file in the correct location is sufficient.

Required Function Signature

Every module must export exactly one async function with this signature:

async def <module_name>(email, client, out):
Parameter Type Purpose
email str The email address being verified
client httpx.AsyncClient Shared async HTTP client—always reuse this, never create your own
out list Shared result list to append your dictionary to

The Five-Step Module Template

Follow this structure based on existing modules like holehe/modules/mails/google.py:

1. Define Service Metadata

Set four standardized variables identifying the service:

name = "examplemail"
domain = "examplemail.com"
method = "register"  # or "password recovery"

frequent_rate_limit = False

2. Prepare Request Headers

Import random user-agents from holehe.localuseragent:

from holehe.localuseragent import *

headers = {
    "User-Agent": random.choice(ua["browsers"]["firefox"]),
    "Accept": "*/*",
    # Add service-specific headers as needed

}

3. Perform the HTTP Request

Use the provided client for all network operations:

try:
    response = await client.post(
        "https://api.examplemail.com/check",
        headers=headers,
        json={"email": email},
    )
except Exception:
    # Network-level failure → treat as rate-limited

    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

4. Parse the Response

Analyze status codes, JSON, or HTML to determine account existence:

data = response.json()

if "account_exists" in data:
    exists = data["account_exists"]
elif "error" in data and "rate" in data["error"].lower():
    # Service-side rate limit

    out.append({...rateLimit: True...})
    return
else:
    # Unknown response format

    exists = False

5. Append Standardized Result

The output dictionary must contain exactly these keys:

Key Type Description
name str Module name
domain str Service domain
method str Check method used
frequent_rate_limit bool Whether service commonly rate-limits
rateLimit bool Whether this request hit a limit
exists bool Whether email is registered
emailrecovery str or None Recovery email if exposed
phoneNumber str or None Phone number if exposed
others dict or None Additional metadata
out.append({
    "name": name,
    "domain": domain,
    "method": method,
    "frequent_rate_limit": frequent_rate_limit,
    "rateLimit": False,
    "exists": exists,
    "emailrecovery": data.get("recovery_email"),
    "phoneNumber": data.get("phone"),
    "others": None,
})

Complete Example: Adding an ExampleMail Module

Create holehe/modules/mails/examplemail.py:

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

async def examplemail(email, client, out):
    name = "examplemail"
    domain = "examplemail.com"
    method = "register"
    frequent_rate_limit = False

    headers = {
        "User-Agent": random.choice(ua["browsers"]["chrome"]),
        "Accept": "application/json",
        "X-Requested-With": "XMLHttpRequest",
    }

    try:
        r = await client.post(
            "https://api.examplemail.com/v1/users/check",
            headers=headers,
            json={"email": email},
            timeout=10,
        )
    except Exception:
        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

    try:
        data = r.json()
    except ValueError:
        # Invalid JSON response

        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

    # Check for rate limiting in response

    if r.status_code == 429 or data.get("status") == "rate_limited":
        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

    exists = data.get("user_exists", False)

    out.append({
        "name": name,
        "domain": domain,
        "method": method,
        "frequent_rate_limit": frequent_rate_limit,
        "rateLimit": False,
        "exists": exists,
        "emailrecovery": data.get("recovery_email"),
        "phoneNumber": data.get("verified_phone"),
        "others": {"username": data.get("suggested_username")} if "suggested_username" in data else None,
    })

Step-by-Step Checklist for Adding a Module

  • Choose an appropriate category folder under holehe/modules/ (e.g., mails, social, cms, forum, shopping)
  • Create a new .py file with a lowercase name matching your function name
  • Implement the async function with the exact signature async def <name>(email, client, out)
  • Always use the provided client parameter—never instantiate httpx.AsyncClient inside your module
  • Return results using the standard dictionary keys used by all other modules
  • Test your module with holehe test@example.com and verify output formatting

Key Files in the holehe Source Code

Path Role
holehe/core.py (lines 37-47) Contains import_submodules which auto-discovers all modules
holehe/modules/mails/google.py Reference implementation for email-based services
holehe/localuseragent.py Supplies ua dictionary with browser user-agent strings
holehe/__init__.py Package entry point—no modifications needed

Testing Your New Module

After creating your file, verify discovery and functionality:


# Test that the module appears in the module list

python -c "from holehe.core import import_submodules; import holehe.modules; print(list(import_submodules(holehe.modules).keys()))"

# Run a live test

holehe -C your-test-email@example.com

Expected output should show [+] examplemail.com (green, account exists) or [-] examplemail.com (magenta, not found).

Common Mistakes to Avoid

  • Creating a new HTTP client: Always use the client parameter to enable connection pooling and proper async handling
  • Missing keys in output: The result dictionary must contain all nine standardized keys or downstream formatting will break
  • Synchronous requests: The function must be async and use await on all network calls
  • Wrong file location: Files outside holehe/modules/ or without the correct function name won't be discovered

Summary

  • holehe discovers modules automatically via import_submodules in holehe/core.py—no registration required
  • Each module exports one async def function with signature (email, client, out)
  • Five required steps: define metadata, set headers, make request, parse response, append standardized result
  • Always reuse the provided httpx.AsyncClient and return all nine required dictionary keys
  • Place new files in holehe/modules/<category>/<module_name>.py following lowercase naming conventions

Frequently Asked Questions

What happens if I forget a key in the output dictionary?

Downstream code in holehe expects exactly nine keys in every result dictionary. Missing keys will cause KeyError exceptions during result formatting and display. Always include name, domain, method, frequent_rate_limit, rateLimit, exists, emailrecovery, phoneNumber, and others—use None for values you cannot populate.

Can I create a new category folder instead of using existing ones?

Yes. The import_submodules function recursively walks all subdirectories. Create holehe/modules/customservice/ and place your module there—holehe will discover it automatically. Use lowercase, no-spaces folder names for consistency with existing code.

Why must I use the provided client instead of creating my own?

The client parameter is a pre-configured httpx.AsyncClient that shares connection pools, follows redirects consistently, and respects timeout settings across all modules. Creating separate clients would exhaust file descriptors, lose cookie persistence, and break the concurrent execution model that makes holehe performant.

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 →