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

> Learn to add a custom website module to Holehe for enhanced email checking. Follow our guide to create Python functions, format results, and register domains.

- Repository: [Palenath/holehe](https://github.com/megadose/holehe)
- Tags: how-to-guide
- Published: 2026-08-20

---

**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`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/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:

```python
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`](https://github.com/megadose/holehe/blob/main/google.py):

```python

# 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`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/holehe/core.py), inside `launch_module()` around line 166:

```python

# 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`](https://github.com/megadose/holehe/blob/main/holehe/modules/mails/google.py) | Clean example of production email-checking logic |
| **Module discovery & execution** | [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py) | Contains `import_submodules()`, `get_functions()`, `launch_module()`, and the `data` registry |
| **Package structure** | [`holehe/modules/__init__.py`](https://github.com/megadose/holehe/blob/main/holehe/modules/__init__.py) | Ensures sub-packages are importable |
| **Request fingerprinting** | [`holehe/localuseragent.py`](https://github.com/megadose/holehe/blob/main/holehe/localuseragent.py) | Browser User-Agent strings for header rotation |

Study [`google.py`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/__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`](https://github.com/megadose/holehe/blob/main/holehe/core.py) for correct display
- **Reference pattern**: Copy the structure from [`holehe/modules/mails/google.py`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/__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.