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-configuredhttpx.AsyncClientfor all HTTP operations.out– A mutable list that must receive the result dictionary viaout.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 deffunction 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
.pyfile underholehe/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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →