How Are Modules Organized in Holehe: A Complete Guide to the Discovery Architecture

Holehe organizes its site-checking modules into a hierarchical holehe/modules package grouped by functional domain, with dynamic discovery via import_submodules in core.py eliminating the need for manual registration.

Holehe, the open-source email enumeration tool by megadose (megadose/holehe), uses a modular architecture that scales to hundreds of services without requiring a centralized registry. Understanding how modules are organized in holehe reveals a design pattern that prioritizes automatic discovery and logical categorization. The architecture separates site-specific logic from orchestration, allowing contributors to add new platforms by simply dropping Python files into categorized subdirectories.

Domain-Based Package Hierarchy

The holehe/modules directory acts as the root package, with subdirectories representing distinct functional categories. Each subdirectory contains individual Python files implementing checks for specific services.

Category Organization

Directory Example Modules Purpose
social_media/ twitter.py, instagram.py, facebook.py Check email usage on major social networks
software/ docker.py, adobe.py, lastpass.py Query SaaS and desktop software accounts
cms/ wordpress.py, gravatar.py, atlassian.py Verify presence on content management platforms
forum/ mybb.py, demonforums.py, blitzortung.py Test registration on web forums
shopping/ amazon.py, ebay.py, vivino.py Detect e-commerce accounts
payment/ venmo.py Look for payment service accounts
crm/ hubspot.py, zoho.py, amocrm.py Check customer relationship platforms
company/, music/, sport/, real_estate/, programing/, crowfunding/, medias/, jobs/, transport/ Various niche modules Additional specialized services

Each module file defines a single async function named after the service (e.g., async def twitter(email, client, out)) that performs the HTTP request and appends a result dictionary to the shared out list.

Dynamic Module Discovery in core.py

Rather than maintaining a static list of supported services, holehe/core.py implements automatic module discovery using Python's pkgutil module.

The import_submodules Function

The import_submodules function recursively walks the package tree to load every module dynamically:


# holehe/core.py

def import_submodules(package, recursive=True):
    """Get all the holehe submodules"""
    if isinstance(package, str):
        package = importlib.import_module(package)
    results = {}
    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 function returns a dictionary mapping full module paths (e.g., 'holehe.modules.social_media.twitter') to imported module objects.

Filtering to Callable Functions

The get_functions method transforms the imported modules into a list of callable check functions:


# holehe/core.py

def get_functions(modules, args=None):
    """Transform the modules objects to functions"""
    websites = []
    for module in modules:
        if len(module.split(".")) > 3:
            modu = modules[module]
            site = module.split(".")[-1]
            # optional filtering (e.g. --no-password-recovery)

            websites.append(modu.__dict__[site])
    return websites

The filter len(module.split(".")) > 3 ensures only modules nested within category directories (not the root modules package itself) are processed.

Standard Module Interface

Every module in the holehe/modules tree follows an identical asynchronous interface pattern.

Required Function Signature

Each module exports an async function matching the filename:


# holehe/modules/social_media/twitter.py (structure example)

from holehe.core import *
from holehe.localuseragent import *

async def twitter(email, client, out):
    name = "twitter"
    domain = "twitter.com"
    method = "register"
    try:
        resp = await client.get("https://api.twitter.com/...", params={"email": email})
        # ... logic to determine if account exists ...

        out.append({
            "name": name,
            "domain": domain,
            "method": method,
            "frequent_rate_limit": False,
            "rateLimit": False,
            "exists": True,  # or False based on response

            "emailrecovery": None,
            "phoneNumber": None,
            "others": None
        })
    except Exception:
        out.append({"name": name, "domain": domain, "method": method,
                   "frequent_rate_limit": False,
                   "rateLimit": True, "exists": False,
                   "emailrecovery": None, "phoneNumber": None, "others": None})

Output Dictionary Schema

The out parameter is a shared list that collects standardized result dictionaries containing:

  • name: Service identifier
  • domain: Target domain
  • method: Verification method used
  • exists: Boolean indicating if the email is registered
  • rateLimit: Boolean indicating if the service temporarily blocked the check
  • emailrecovery: Partial email address if recovery info is exposed
  • phoneNumber: Partial phone number if exposed
  • others: Additional metadata

Execution Flow and Error Handling

Once loaded, modules execute through the launch_module wrapper in core.py, which provides standardized exception handling and output normalization.

The launch_module Function


# holehe/core.py

async def launch_module(module, email, client, out):
    try:
        await module(email, client, out)
    except Exception:
        name = str(module).split('<function ')[1].split(' ')[0]
        out.append({
            "name": name,
            "domain": data[name],
            "rateLimit": False,
            "error": True,
            "exists": False,
            "emailrecovery": None,
            "phoneNumber": None,
            "others": None,
        })

This wrapper ensures that a crashing module does not interrupt the entire enumeration process. The client parameter is typically an httpx.AsyncClient instance shared across all modules to enable connection pooling.

Extending the Module Library

Adding support for a new service requires no changes to core.py or any registry file.

Creating a New Module

  1. Create a new Python file in the appropriate category directory (e.g., holehe/modules/social_media/example.py)
  2. Import the standard helpers: from holehe.core import * and from holehe.localuseragent import *
  3. Define the async function with the standard signature
  4. Implement the request logic and append results to out

The new module is automatically discovered on the next run because import_submodules recursively walks the directory tree at startup.

Summary

  • Hierarchical organization: Modules reside in holehe/modules grouped by functional domains like social_media, software, and shopping
  • Automatic discovery: The import_submodules function in core.py recursively imports all Python files without manual registration
  • Standardized interface: Each module exposes a single async function named after the service that accepts (email, client, out) parameters
  • Fault tolerance: launch_module wraps each execution to prevent individual service failures from crashing the entire enumeration
  • Simple extension: Adding new services requires only creating a Python file in the appropriate category directory

Frequently Asked Questions

How does holehe discover new modules automatically?

Holehe uses the import_submodules function in holehe/core.py which utilizes pkgutil.walk_packages to recursively traverse the holehe/modules directory tree. This imports every Python file found in category subdirectories, builds a dictionary of module references, and extracts the callable functions via get_functions. No central registry or configuration file needs updating when new modules are added.

What is the standard function signature for a holehe module?

Every module must define an async function with the signature async def servicename(email, client, out) where email is the target string, client is an httpx.AsyncClient for HTTP requests, and out is a list where the function appends a result dictionary. The function name must match the filename (e.g., twitter.py contains async def twitter).

How are modules categorized in holehe?

Modules are organized into subdirectories under holehe/modules based on the service's primary function. Categories include social_media, software, cms, forum, shopping, payment, crm, company, music, sport, and others. This grouping allows users to mentally map related services and helps contributors place new modules in logical locations.

Can I add custom modules to holehe without modifying core files?

Yes. Simply create a new Python file in any subdirectory of holehe/modules (or create a new category directory) following the standard async function pattern. The dynamic loader will discover and execute your module automatically on the next run. No changes to core.py, __init__.py, or any registry are required unless you need to add special handling logic.

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 →