How to Add a New Service Module to Holehe: A Step-by-Step Guide
To add a new service module to Holehe, create a Python file in the appropriate category under holehe/modules/, implement an async function matching the filename, and return a standardized result dictionary—no manual registration is required.
Holehe discovers and executes all service checks through dynamic module importing. The framework automatically walks the holehe.modules package tree, extracts functions from each file, and runs them against target emails. This architecture means you can extend Holehe's capabilities without modifying core code.
How Holehe Discovers Service Modules
Understanding the discovery mechanism helps you implement modules correctly. The process happens in three stages, all defined in holehe/core.py.
Dynamic Import via import_submodules
The import_submodules function (lines 37-47) recursively walks the holehe.modules package and imports every Python file it finds:
# From holehe/core.py
def import_submodules(package, recursive=True):
"""Import all submodules of a module, recursively."""
results = {}
# Walks package.__path__ and imports each submodule
for loader, name, is_pkg in pkgutil.walk_packages(package.__path__):
full_name = package.__name__ + '.' + name
results[full_name] = importlib.import_module(full_name)
if recursive and is_pkg:
results.update(import_submodules(full_name))
return results
This means any .py file you add under holehe/modules/ or its subdirectories gets loaded automatically.
Function Extraction via get_functions
After import, get_functions (lines 50-63) extracts the callable that matches the module's filename:
# From holehe/core.py
def get_functions(modules):
"""Extract functions matching module names from imported modules."""
functions = []
for module_name, module in modules.items():
# Extracts 'twitter' from 'holehe.modules.social_media.twitter'
name = module_name.split('.')[-1]
if hasattr(module, name):
functions.append(getattr(module, name))
return functions
Critical requirement: The function name must exactly match the filename (without .py). A file named reddit.py must define async def reddit(...).
Async Execution via launch_module
The collected functions are invoked with a fixed signature in launch_module (lines 66-71):
# Expected signature: (email, client, out)
await function(email, client, out)
email: The target email stringclient: Anhttpx.AsyncClientinstance for HTTP requestsout: A list to which you append your result dictionary
Step-by-Step: Creating a New Service Module
Follow these six steps to add a service check to Holehe.
1. Choose the Appropriate Category
Review the existing structure under holehe/modules/:
holehe/modules/
├── social_media/
├── shopping/
├── forum/
├── productivity/
└── ...
Select the category that best fits your target service. For a new social platform, use social_media/.
2. Create the Module File
Add a new file with a lowercase, no-space name matching the service:
touch holehe/modules/social_media/example.py
The filename example.py dictates that your function must be named example.
3. Implement the Async Function
Define async def example(email, client, out) with the exact signature expected by launch_module. Import required dependencies from Holehe's core:
# holehe/modules/social_media/example.py
from holehe.core import *
from holehe.localuseragent import *
4. Query the Service Endpoint
Use the provided httpx.AsyncClient to make requests. Pattern your implementation after existing modules like holehe/modules/social_media/twitter.py.
5. Build and Append the Result Dictionary
Your function must append a dictionary with all required keys to the out list. Missing keys will cause the output printer to fail.
6. Verify Automatic Discovery
Run Holehe against any email. Your new service appears in the output without any configuration changes:
holehe test@example.com
Required Result Dictionary Structure
Every service module must return a dictionary with exactly these keys:
| Key | Type | Description |
|---|---|---|
name |
str |
Service identifier displayed in output |
domain |
str |
Service domain (e.g., "twitter.com") |
method |
str |
Detection method used (e.g., "register", "old_profile") |
frequent_rate_limit |
bool |
Whether the service commonly rate-limits requests |
rateLimit |
bool |
True if this specific request hit a rate limit |
exists |
bool |
True if the email is registered on the service |
emailrecovery |
str or None |
Recovery email if exposed by the service |
phoneNumber |
str or None |
Phone number if exposed by the service |
others |
any |
Additional metadata the service returns |
Complete Example: Adding a Dummy Service
This implementation demonstrates the full pattern. Replace the URL and parsing logic with your target service's actual API:
# File: holehe/modules/social_media/example.py
from holehe.core import *
from holehe.localuseragent import *
async def example(email, client, out):
name = "example"
domain = "example.com"
method = "register"
frequent_rate_limit = False
try:
headers = {
"User-Agent": random.choice(ua["browsers"]["chrome"]),
"Accept": "application/json"
}
resp = await client.get(
"https://api.example.com/v1/users/check_email",
headers=headers,
params={"email": email},
timeout=10
)
data = resp.json()
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": {"user_id": data.get("id")}
})
except Exception as e:
# On any exception, report rate limit and 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
})
Key Implementation Guidelines
- Function naming: Must match filename exactly (
twitter.py→def twitter()). Case-sensitive. - Exception handling: Always wrap requests in
try/except. On failure, setrateLimit=Trueandexists=False. - No manual imports: Never add
import examplestatements anywhere. The dynamic loader handles registration. - Async required: All service functions must be async—the framework uses
awaitto invoke them.
Reference: Core Files in Holehe
| File | Lines | Purpose |
|---|---|---|
holehe/core.py |
37-47 | import_submodules() — recursively imports all modules under holehe.modules |
holehe/core.py |
50-63 | get_functions() — extracts functions matching module names |
holehe/core.py |
66-71 | launch_module() — executes each function with (email, client, out) signature |
holehe/modules/social_media/twitter.py |
— | Canonical reference implementation showing real-world patterns |
Summary
- Holehe uses dynamic import in
holehe/core.pyto discover all modules underholehe.modulesautomatically - Create new service files in the appropriate category directory with matching function and filename names
- Implement
async def servicename(email, client, out)using the fixed three-parameter signature - Return a complete result dictionary with all nine required keys to avoid output errors
- No registration step exists—save the file and run Holehe to test immediately
Frequently Asked Questions
What happens if my function name doesn't match the filename?
Holehe will not detect your service. The get_functions implementation in holehe/core.py (lines 50-63) specifically looks for an attribute matching the final segment of the module path. A file named reddit.py must contain async def reddit(...), or it will be silently skipped during discovery.
Can I add a new category directory, or must I use existing ones?
You can create new category directories. The import_submodules function recursively walks all subpackages under holehe.modules. Simply create a new folder (e.g., holehe/modules/gaming/) with an __init__.py file and add your service modules there. The dynamic loader will find them automatically.
How should I handle services that require authentication or API keys?
Holehe's architecture passes the same httpx.AsyncClient to all modules, so you can configure custom headers or authentication in your module. However, there's no built-in credential management system. Store API keys as module-level constants or use environment variables, following the pattern in existing modules that require special headers.
Why does my new module show rateLimit=True even when the service isn't rate-limiting?
This indicates an uncaught exception in your implementation. The standard error-handling pattern sets rateLimit=True and exists=False for any exception, as shown in the twitter.py reference and the example above. Check your request URL, response parsing, and exception handling logic to identify the actual failure.
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 →