How to Add a Custom Website Module to Holehe for Email Checking
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.
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 works as follows:
import_submodules("holehe.modules")recursively imports every.pyfile under the modules directoryget_functions()extracts callables whose names match their filename (e.g.,googlefromgoogle.py)launch_module()executes each function inside a Trio nursery with shared state- Each function receives:
email(target string),client(sharedhttpx.AsyncClient), andout(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:
async def example_site(email, client, out):
# implementation
email: The target email address as stringclient: Pre-configuredhttpx.AsyncClientwith proxy/cookie supportout: 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:
# 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"])fromholehe.localuseragentfor 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 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, inside launch_module() around line 166:
# 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 |
Clean example of production email-checking logic |
| Module discovery & execution | holehe/core.py |
Contains import_submodules(), get_functions(), launch_module(), and the data registry |
| Package structure | holehe/modules/__init__.py |
Ensures sub-packages are importable |
| Request fingerprinting | holehe/localuseragent.py |
Browser User-Agent strings for header rotation |
Study 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:
- Install in editable mode (if not already):
pip install -e . - Run Holehe against your target:
holehe -e test@example.com - Verify output: Your
domainvalue should appear in the results table - Check with
--onlyflag:holehe -e test@example.com --only example_siteto 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 - 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 theoutlist - Domain registration: Add the
module_name: domain.compair todatainholehe/core.pyfor correct display - Reference pattern: Copy the structure from
holehe/modules/mails/google.pyfor 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. 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.
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 →