# Holehe Module Interface Contract: How to Build a Valid Email Checker

> Understand the Holehe module interface contract. Learn how to build a valid email checker with this asynchronous function signature and standardized result dictionary.

- Repository: [Palenath/holehe](https://github.com/megadose/holehe)
- Tags: api-reference
- Published: 2026-09-01

---

**A Holehe module is a self-contained asynchronous function with a strict three-parameter signature that appends a standardized result dictionary to a mutable list rather than returning a value.**

Holehe is an email reconnaissance framework by Tristan Granier (`megadose/holehe`). Its modular architecture lets developers add support for new services without touching core logic. Every module must follow a precise **interface contract** so that the discovery mechanism in [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py) can import and execute them uniformly.

---

## The Required Function Signature

Every Holehe module must expose exactly one top-level asynchronous function with this signature:

```python
async def <service_name>(email: str, client: httpx.AsyncClient, out: list) -> None

```

The core engine validates this signature at runtime in [`launch_module`](https://github.com/megadose/holehe/blob/master/holehe/core.py#L66-L73). The three positional arguments are:

- **`email`** – The target email address as a string.
- **`client`** – A pre-configured `httpx.AsyncClient` for all HTTP operations.
- **`out`** – A mutable list that **must** receive the result dictionary via `out.append()`.

The function must **not** return anything. The only permitted side effect is mutating `out`.

---

## The Result Dictionary Schema

Each module must append a dictionary with exactly these keys to the `out` list:

| Key | Type | Description |
|-----|------|-------------|
| `name` | `str` | Internal module name (typically the service identifier). |
| `domain` | `str` | Base domain of the service (e.g., `"discord.com"`). |
| `method` | `str \| None` | Optional HTTP method description (`"register"`, `"login"`). |
| `frequent_rate_limit` | `bool` | Whether the service commonly triggers rate limits. |
| `rateLimit` | `bool` | `True` if this specific request was throttled. |
| `exists` | `bool` | `True` if the email is confirmed to exist on the service. |
| `emailrecovery` | `str \| None` | Recovered secondary email, if any. |
| `phoneNumber` | `str \| None` | Recovered phone number, if any. |
| `others` | `dict \| None` | Arbitrary extra data (name, creation date, etc.). |

All keys are required. Values may be `None` where indicated. See the [Discord module](https://github.com/megadose/holehe/blob/master/holehe/modules/social_media/discord.py#L5-L80) for a production implementation of this schema.

---

## Error Handling Requirements

Modules must catch exceptions internally and still append a valid result. The standard pattern sets `rateLimit=True`, `exists=False`, and `None` for optional fields. This ensures the core engine continues processing other services without interruption.

---

## Module Discovery and Loading

The core engine discovers modules dynamically via `import_submodules("holehe.modules")` in [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py). It extracts callables with `get_functions()`. Any Python file placed under `holehe/modules/…/*.py` that satisfies the interface contract is automatically included in scans.

---

## Minimal Custom Module Example

Create [`holehe/modules/example.py`](https://github.com/megadose/holehe/blob/main/holehe/modules/example.py):

```python

# holehe/modules/example.py

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

async def example(email, client, out):
    name = "example"
    domain = "example.com"
    response = await client.head(f"https://api.example.com/users/{email}")
    exists = response.status_code == 200
    out.append({
        "name": name,
        "domain": domain,
        "method": None,
        "frequent_rate_limit": False,
        "rateLimit": False,
        "exists": exists,
        "emailrecovery": None,
        "phoneNumber": None,
        "others": None,
    })

```

This satisfies the **Holehe module interface contract**: async function, three parameters, list mutation, complete dictionary.

---

## Executing a Module Programmatically

Use `launch_module` directly for testing or custom orchestration:

```python
import trio
import httpx
from holehe.core import import_submodules, launch_module

async def demo():
    email = "test@example.com"
    client = httpx.AsyncClient(timeout=10)
    out = []
    
    modules = import_submodules("holehe.modules")
    discord_mod = modules["holehe.modules.social_media.discord"].discord
    await launch_module(discord_mod, email, client, out)
    
    await client.aclose()
    print(out)  # [{'name': 'discord', 'domain': 'discord.com', ...}]

trio.run(demo)

```

The core uses this same mechanism when running full scans.

---

## How Results Are Consumed

The `print_result` function in [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py) renders module output:

```python
for results in data:
    if results["exists"]:
        print(f"[+] {results['domain']} {results.get('emailrecovery') or ''}")
    elif results["rateLimit"]:
        print(f"[x] {results['domain']} (rate limited)")
    else:
        print(f"[-] {results['domain']}")

```

This demonstrates why the schema must be strict—downstream consumers depend on these exact keys.

---

## Common Utilities and Imports

Most modules begin with these imports for convenience:

```python
from holehe.core import *
from holehe.localuseragent import *

```

These provide helper functions and random User-Agent rotation. Strictly speaking, only the function signature and result schema are mandatory; the imports are conventional.

---

## Key Source Files

| File | Purpose |
|------|---------|
| [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py) | Central engine: module discovery, `launch_module`, result printing |
| [`holehe/modules/social_media/discord.py`](https://github.com/megadose/holehe/blob/main/holehe/modules/social_media/discord.py) | Reference implementation of the contract |
| [`holehe/localuseragent.py`](https://github.com/megadose/holehe/blob/main/holehe/localuseragent.py) | User-Agent utilities |

---

## Summary

- **Holehe module interface contract** requires an `async def` function with exactly three parameters: `email`, `client`, `out`.
- **Result delivery** happens exclusively through `out.append()` with a complete nine-key dictionary.
- **No return values** are permitted; side-effect-only design enables uniform async execution.
- **Error resilience** is mandatory: catch exceptions and append valid result dictionaries.
- **Automatic discovery** occurs for any `.py` file under `holehe/modules/` obeying the contract.

---

## Frequently Asked Questions

### What happens if my module returns a value instead of appending to `out`?

The return value is discarded. The core engine in [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py) only examines the `out` list after `await launch_module()` completes. Appending to `out` is the sole mechanism for communicating results.

### Can I use `aiohttp` instead of `httpx.AsyncClient` for HTTP requests?

No. The core passes a pre-configured `httpx.AsyncClient` instance, and the contract assumes this client type. Using a different HTTP library would break connection pooling, timeout handling, and instrumentation that the core manages.

### Is the `method` field in the result dictionary required?

No. While the key must be present, its value may be `None`. The field is optional for informational purposes and does not affect core processing logic.

### How do I test my module before submitting it to the repository?

Import `launch_module` from `holehe.core`, create an `httpx.AsyncClient` with appropriate timeouts, and invoke your function directly with a test email and empty list. Verify the appended dictionary contains all required keys with correct types.