How to Add a New Website Module to holehe Following the Existing Module Pattern
Creating a new website module for holehe requires implementing a single async function with the signature async def <module_name>(email, client, out) in a Python file under holehe/modules/<category>/, with no registration step needed.
holehe automatically discovers every check routine by walking the package holehe.modules using the import_submodules helper in holehe/core.py. This means you can add a new website module simply by creating a properly structured Python file—the framework handles the rest. This guide walks you through the exact pattern used by existing modules like the Gmail check in holehe/modules/mails/google.py.
Understanding the Module Discovery System
In holehe/core.py at lines 37-47, the import_submodules function recursively imports all Python files beneath holehe.modules:
def import_submodules(package, recursive=True):
"""Import all submodules of a package."""
results = {}
for _, name, is_pkg in pkgutil.walk_packages(package.__path__, package.__name__ + '.'):
results[name] = importlib.import_module(name)
if recursive and is_pkg:
results.update(import_submodules(sys.modules[name]))
return results
Because of this automatic discovery, you never need to register or list your new module manually. Placing a valid .py file in the correct location is sufficient.
Required Function Signature
Every module must export exactly one async function with this signature:
async def <module_name>(email, client, out):
| Parameter | Type | Purpose |
|---|---|---|
email |
str |
The email address being verified |
client |
httpx.AsyncClient |
Shared async HTTP client—always reuse this, never create your own |
out |
list |
Shared result list to append your dictionary to |
The Five-Step Module Template
Follow this structure based on existing modules like holehe/modules/mails/google.py:
1. Define Service Metadata
Set four standardized variables identifying the service:
name = "examplemail"
domain = "examplemail.com"
method = "register" # or "password recovery"
frequent_rate_limit = False
2. Prepare Request Headers
Import random user-agents from holehe.localuseragent:
from holehe.localuseragent import *
headers = {
"User-Agent": random.choice(ua["browsers"]["firefox"]),
"Accept": "*/*",
# Add service-specific headers as needed
}
3. Perform the HTTP Request
Use the provided client for all network operations:
try:
response = await client.post(
"https://api.examplemail.com/check",
headers=headers,
json={"email": email},
)
except Exception:
# Network-level failure → treat as rate-limited
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
4. Parse the Response
Analyze status codes, JSON, or HTML to determine account existence:
data = response.json()
if "account_exists" in data:
exists = data["account_exists"]
elif "error" in data and "rate" in data["error"].lower():
# Service-side rate limit
out.append({...rateLimit: True...})
return
else:
# Unknown response format
exists = False
5. Append Standardized Result
The output dictionary must contain exactly these keys:
| Key | Type | Description |
|---|---|---|
name |
str |
Module name |
domain |
str |
Service domain |
method |
str |
Check method used |
frequent_rate_limit |
bool |
Whether service commonly rate-limits |
rateLimit |
bool |
Whether this request hit a limit |
exists |
bool |
Whether email is registered |
emailrecovery |
str or None |
Recovery email if exposed |
phoneNumber |
str or None |
Phone number if exposed |
others |
dict or None |
Additional metadata |
out.append({
"name": name,
"domain": domain,
"method": method,
"frequent_rate_limit": frequent_rate_limit,
"rateLimit": False,
"exists": exists,
"emailrecovery": data.get("recovery_email"),
"phoneNumber": data.get("phone"),
"others": None,
})
Complete Example: Adding an ExampleMail Module
Create holehe/modules/mails/examplemail.py:
from holehe.core import *
from holehe.localuseragent import *
async def examplemail(email, client, out):
name = "examplemail"
domain = "examplemail.com"
method = "register"
frequent_rate_limit = False
headers = {
"User-Agent": random.choice(ua["browsers"]["chrome"]),
"Accept": "application/json",
"X-Requested-With": "XMLHttpRequest",
}
try:
r = await client.post(
"https://api.examplemail.com/v1/users/check",
headers=headers,
json={"email": email},
timeout=10,
)
except Exception:
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
try:
data = r.json()
except ValueError:
# Invalid JSON response
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
# Check for rate limiting in response
if r.status_code == 429 or data.get("status") == "rate_limited":
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
exists = data.get("user_exists", False)
out.append({
"name": name,
"domain": domain,
"method": method,
"frequent_rate_limit": frequent_rate_limit,
"rateLimit": False,
"exists": exists,
"emailrecovery": data.get("recovery_email"),
"phoneNumber": data.get("verified_phone"),
"others": {"username": data.get("suggested_username")} if "suggested_username" in data else None,
})
Step-by-Step Checklist for Adding a Module
- Choose an appropriate category folder under
holehe/modules/(e.g.,mails,social,cms,forum,shopping) - Create a new
.pyfile with a lowercase name matching your function name - Implement the async function with the exact signature
async def <name>(email, client, out) - Always use the provided
clientparameter—never instantiatehttpx.AsyncClientinside your module - Return results using the standard dictionary keys used by all other modules
- Test your module with
holehe test@example.comand verify output formatting
Key Files in the holehe Source Code
| Path | Role |
|---|---|
holehe/core.py (lines 37-47) |
Contains import_submodules which auto-discovers all modules |
holehe/modules/mails/google.py |
Reference implementation for email-based services |
holehe/localuseragent.py |
Supplies ua dictionary with browser user-agent strings |
holehe/__init__.py |
Package entry point—no modifications needed |
Testing Your New Module
After creating your file, verify discovery and functionality:
# Test that the module appears in the module list
python -c "from holehe.core import import_submodules; import holehe.modules; print(list(import_submodules(holehe.modules).keys()))"
# Run a live test
holehe -C your-test-email@example.com
Expected output should show [+] examplemail.com (green, account exists) or [-] examplemail.com (magenta, not found).
Common Mistakes to Avoid
- Creating a new HTTP client: Always use the
clientparameter to enable connection pooling and proper async handling - Missing keys in output: The result dictionary must contain all nine standardized keys or downstream formatting will break
- Synchronous requests: The function must be
asyncand useawaiton all network calls - Wrong file location: Files outside
holehe/modules/or without the correct function name won't be discovered
Summary
- holehe discovers modules automatically via
import_submodulesinholehe/core.py—no registration required - Each module exports one
async deffunction with signature(email, client, out) - Five required steps: define metadata, set headers, make request, parse response, append standardized result
- Always reuse the provided
httpx.AsyncClientand return all nine required dictionary keys - Place new files in
holehe/modules/<category>/<module_name>.pyfollowing lowercase naming conventions
Frequently Asked Questions
What happens if I forget a key in the output dictionary?
Downstream code in holehe expects exactly nine keys in every result dictionary. Missing keys will cause KeyError exceptions during result formatting and display. Always include name, domain, method, frequent_rate_limit, rateLimit, exists, emailrecovery, phoneNumber, and others—use None for values you cannot populate.
Can I create a new category folder instead of using existing ones?
Yes. The import_submodules function recursively walks all subdirectories. Create holehe/modules/customservice/ and place your module there—holehe will discover it automatically. Use lowercase, no-spaces folder names for consistency with existing code.
Why must I use the provided client instead of creating my own?
The client parameter is a pre-configured httpx.AsyncClient that shares connection pools, follows redirects consistently, and respects timeout settings across all modules. Creating separate clients would exhaust file descriptors, lose cookie persistence, and break the concurrent execution model that makes holehe performant.
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 →