How the Holehe Register Check Module Detects Email Usage Across Websites

Holehe’s Register Check module determines if an email address is already registered on a service by sending non-intrusive registration validation requests and interpreting specific error codes that indicate account existence, all without creating actual accounts.

The open-source OSINT tool Holehe (maintained by megadose/holehe) identifies where an email address appears online by leveraging a specialized Register Check system. This mechanism uses dynamically loaded, site-specific modules to probe registration validation endpoints. Each module follows a standardized three-step pattern to distinguish between "account exists," "available," and "rate-limited" states through careful analysis of API responses.

Core Architecture and Module Discovery

The Register Check system relies on dynamic module loading and a centralized dispatcher to manage concurrent execution across hundreds of supported websites.

Dynamic Module Loading via import_submodules

Holehe discovers available site checks through automatic package introspection. In holehe/core.py (lines 37-47), the import_submodules() function walks the holehe/modules/* directory tree, imports every Python file, and exposes their public functions. The get_functions() helper then filters these imports to extract only async functions representing individual site checks.

Each Register Check module must define three constant fields used by the core logic:

name    = "tumblr"      # Human-readable identifier

domain  = "tumblr.com"  # Displayed domain

method  = "register"    # Flags this as a register-check module

Only modules with method == "register" are selected for execution when running a Register Check workflow.

The Launch Dispatcher

The launch_module() function in holehe/core.py (lines 66-73) serves as the execution wrapper. It receives the site-specific async function, the target email, an httpx.AsyncClient instance, and a shared results list. This dispatcher executes the module within a try-catch block, converting unexpected exceptions into generic rate-limit entries to prevent individual module failures from crashing the entire scan.

The Three-Step Register Check Implementation Pattern

Every Register Check module follows an identical operational pattern to test for account existence without completing actual registration.

Step 1: Token and Session Acquisition

Modules begin by preparing the HTTP session. This typically involves a GET request to the site's registration page to obtain anti-CSRF tokens, session cookies, or bearer tokens required for subsequent API calls.

Step 2: Registration Payload Submission

The module constructs and sends a POST request to the service’s registration validation endpoint. The payload includes the target email address paired with a dummy password and random username. This mimics a legitimate registration attempt while providing the necessary data to trigger validation logic on the server.

Step 3: Response Interpretation and Error Mapping

The critical logic resides in parsing the API response. Services return specific error codes or messages when an email already exists in their database:

  • Account exists: Specific error indicators (e.g., Tumblr error code 2, Xing message "We already know this e-mail address.") map to exists: True
  • Available/Invalid: Password policy failures or validation errors map to exists: False
  • Rate limits: HTTP 429 responses or network failures map to rateLimit: True

Anatomy of a Register Check Module: The Tumblr Example

The Tumblr implementation in holehe/modules/social_media/tumblr.py demonstrates the complete workflow. The module extracts a bearer token and CSRF header, submits a validation payload, and interprets the JSON response:

async def tumblr(email, client, out):
    # Constants required by core.py

    name = "tumblr"
    domain = "tumblr.com"
    method = "register"
    
    # Step 1: Obtain authentication tokens

    getBearer = await client.get("https://www.tumblr.com/", headers={...})
    bearer = f"Bearer {extract_token(getBearer)}"
    csrf = getBearer.headers["X-Csrf"]
    
    # Step 2: Submit validation payload

    data = json.dumps({
        "password": random_string(),
        "email": email,
        "tumblelog": random_string()
    })
    
    post = await client.post(
        "https://www.tumblr.com/api/v2/register/account/validate",
        headers={"Authorization": bearer, "X-CSRF": csrf, ...},
        data=data
    )
    
    # Step 3: Interpret error codes

    error = post.json()["response"]["code"]
    if error == 2:  # "User already exists"

        out.append({"name": name, "domain": domain, "method": method,
                    "exists": True, "rateLimit": False})
    elif error == 1030:  # Password too short

        out.append({"name": name, "domain": domain, "method": method,
                    "exists": False, "rateLimit": False})
    else:
        raise Exception  # Triggers rate-limit handling in core

This skeleton appears consistently across modules like xing.py, github.py, and lastpass.py, varying only in endpoint URLs, required headers, and specific error signatures.

Concurrent Execution and Result Aggregation

When invoked via the CLI (holehe <email>), the core orchestration logic performs four distinct operations:

  1. Loads all modules using import_submodules() to discover available checks
  2. Filters by method, selecting only those with method == "register"
  3. Executes concurrently using a Trio nursery, running launch_module for each site in parallel
  4. Aggregates results, sorting them into a color-coded table:
    • [+] – Email exists (confirmed via register-check validation)
    • [-] – Email not found
    • [x] – Rate limit or temporary block encountered

Summary

  • Dynamic discovery: Holehe automatically loads Register Check modules from holehe/modules/* using import_submodules() in holehe/core.py
  • Standardized interface: Every module exposes name, domain, and method = "register" constants for identification
  • Non-intrusive validation: The Register Check sends dummy registration data to validation endpoints without creating accounts
  • Error-code analysis: Modules parse specific API error responses to distinguish existing accounts (e.g., Tumblr error 2) from available emails
  • Concurrent execution: The launch_module dispatcher manages parallel execution via Trio, handling exceptions as rate-limits to ensure stability

Frequently Asked Questions

How does Holehe avoid creating actual accounts during the Register Check?

Holehe exploits the validation phase of registration workflows rather than completion endpoints. By sending registration data to validate or check endpoints (such as Tumblr’s /api/v2/register/account/validate), the tool triggers the server-side email lookup without submitting the final account creation request. The service returns error codes indicating whether the email exists, allowing Holehe to determine registration status without completing sign-up.

What differentiates a Register Check module from other Holehe modules?

Register Check modules specifically set method = "register" and target registration validation endpoints. Unlike recovery-based or login-based checks, these modules intentionally submit malformed or dummy registration payloads (random passwords, invalid usernames) to force validation errors. This contrasts with password reset flows or OAuth checks used by other module types in the Holehe ecosystem.

How can I add a custom Register Check module for a new service?

Create a Python file in holehe/modules/ (e.g., holehe/modules/social_media/myservice.py) containing an async function that accepts (email, client, out) parameters. Define the required constants (name, domain, method = "register"), implement the three-step pattern (token acquisition, validation POST, error interpretation), and append a dictionary with exists and rateLimit booleans to the out list. Holehe automatically discovers the module on next execution via import_submodules().

Why does Holehe mark some results as rate-limited [x] instead of exists [+] or not exists [-]?

When a module encounters HTTP 429 responses, CAPTCHA challenges, or unhandled exceptions (network timeouts, JSON parsing errors), the launch_module wrapper in holehe/core.py catches these and marks the result as rate-limited. This distinguishes temporary blocks from definitive account existence checks, ensuring the user knows when to retry rather than accepting a false negative.

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 →