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 toexists: 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:
- Loads all modules using
import_submodules()to discover available checks - Filters by method, selecting only those with
method == "register" - Executes concurrently using a Trio nursery, running
launch_modulefor each site in parallel - 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/*usingimport_submodules()inholehe/core.py - Standardized interface: Every module exposes
name,domain, andmethod = "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_moduledispatcher 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →