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

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 can import and execute them uniformly.


The Required Function Signature

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

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

The core engine validates this signature at runtime in launch_module. 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 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. 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:


# 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:

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 renders module output:

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:

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 Central engine: module discovery, launch_module, result printing
holehe/modules/social_media/discord.py Reference implementation of the contract
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 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.

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 →