Build an Email Account Discovery Tool
Scan 120+ websites to find where an email is registered using Python.
Introduction
30 Second Summary
Think about how many websites you have signed up for over the years. Most people have accounts on dozens of services they have completely forgotten about, each one a potential security risk.
In this project, you will build a command-line tool that checks 120+ popular websites to find where your email is registered. It stores every result in a local database and generates a colour-coded HTML report so you can see your digital footprint at a glance.
What You'll Build
A Python email account discovery tool that scans websites using their public password-reset endpoints, saves results to a local SQLite database, and generates a styled HTML report.
By the end of this project, you'll have:
- A working Python script that checks 120+ websites to find where your email is registered.
- A local SQLite database that stores every scan result with timestamps for historical tracking.
- A styled HTML report that opens in your browser showing discovered accounts colour-coded by status.
- Secret Mission: Add a comparison feature that highlights new accounts discovered since your last scan.
Are there any prerequisites?
None. This guide walks you through installing everything you need from scratch, including Python and VS Code. All you need is a Windows computer and an email address you want to scan.
Before We Start
Before We Start...
Install Python and VS Code
Your email account discovery tool is built with Python. To write and run the code, you need two things: Python itself (the language your script runs on) and VS Code (the editor where you write your code).
In this step, you will install both tools on your Windows device and create a dedicated project folder on your Desktop.
In this step, get ready to:
- Install Python 3.13 and verify it works in your terminal.
- Install VS Code with the Python extension.
- Create a project folder and open it in VS Code.
Install Python 3.13
Python is the programming language your scanner script is written in. You need version 3.13 specifically because the scanning library (Holehe) has been tested and confirmed working with it.
- Open your web browser and go to the Python downloads page for Windows:
- https://www.python.org/downloads/windows/
- Scroll down to find the Python 3.13 section and click Download Windows installer (64-bit).
- Run the downloaded installer file.
- On the very first installer screen, CHECK the box at the bottom that says Add python.exe to PATH. This is critical.
- Click Install Now and wait for the installation to complete.
- Click Close when the installer finishes.
Why does the PATH checkbox matter?
Adding Python to your PATH lets you run python from any terminal window. Without it, your computer would not know where to find Python when you type the command.
Now verify that Python is installed correctly.
- Press the Windows key to open your search bar.
- Type Terminal and press Enter to open Windows Terminal.
- Check your Python version by running this command:
python --version
✔️ I see version 3.13
You should see something like Python 3.13.13 (the last number may differ). Python is ready to go.
ⓧ I see an older version
If you see a version lower than 3.13 (such as 3.11 or 3.12), you need to install Python 3.13 alongside your existing version.
- Go to https://www.python.org/downloads/windows/ in your browser.
- Find the Python 3.13 section and click Download Windows installer (64-bit).
- Run the installer and CHECK the Add python.exe to PATH box at the bottom.
- Click Install Now and wait for it to complete.
- Close and reopen Windows Terminal, then run python --version again. You should now see 3.13.
Still seeing the old version?
Windows may still point to the older Python. Try running py -3.13 --version to confirm 3.13 is installed. If that works, use py -3.13 instead of python throughout this project.
ⓧ Command not found
If you see an error or Windows tries to open the Microsoft Store, Python is not installed yet.
- Go to https://www.python.org/downloads/windows/ in your browser.
- Find the Python 3.13 section and click Download Windows installer (64-bit).
- Run the installer and CHECK the Add python.exe to PATH box at the bottom.
- Click Install Now and wait for it to complete.
- Close and reopen Windows Terminal, then run python --version again. You should now see Python 3.13.
Still not working after install?
If you missed the PATH checkbox during installation, uninstall Python from Settings > Apps > Installed apps, then re-run the installer with the PATH box checked.
help me fix Python not being recognized in my terminal after installation
Install VS Code and the Python extension
VS Code is a free code editor that gives you syntax highlighting, error detection, and a built-in terminal. The Python extension adds extra features like auto-complete and linting specifically for Python files.
- Open your browser and go to https://code.visualstudio.com/.
- Click the large Download for Windows button.
- Run the downloaded installer.
- Accept the default options and click through the wizard until installation completes.
- Press the Windows key to open your search bar.
- Type Visual Studio Code and press Enter to open it.
Now install the Python extension.
- In VS Code, click the Extensions icon in the left sidebar (it looks like four squares).
- Type Python in the search bar at the top of the Extensions panel.
- Find the extension named Python by Microsoft (it should be the first result).
- Click Install.
What does the Python extension do?
The Python extension adds IntelliSense (smart auto-complete), code linting (error highlighting), and the ability to run Python files directly from the editor. It makes writing Python in VS Code much smoother.
Create your project folder
Every project needs its own folder to keep files organised. You will create one on your Desktop so it is easy to find.
- Switch back to your Windows Terminal window.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir email-discovery
cd email-discovery
- Open this folder in VS Code by running:
code .
What does code . do?
The code command launches VS Code from your terminal. The dot (.) means "the current folder." Together, code . opens VS Code with your email-discovery folder as the workspace.
VS Code will open and may show a trust dialog asking if you trust the authors of the files in this folder. Click Yes, I trust the authors to continue.
- Confirm the folder opened correctly by checking the Explorer sidebar on the left. You should see the folder name EMAIL-DISCOVERY at the top (VS Code shows it in uppercase).
Don't see your folder in VS Code?
Make sure you ran code . from inside the email-discovery folder. If VS Code opened to the wrong location, click File > Open Folder and navigate to Desktop > email-discovery.
help me open my project folder in VS Code
Your development environment is ready. Next up, you will set up a virtual environment and install the Holehe scanning library.
Set Up the Project and Install Holehe
Your Python and VS Code setup is ready. Now it's time to prepare an isolated workspace for the tool that will scan where your email is registered.
Python projects use virtual environments to keep their packages separate from other projects on your machine. You'll create one now, install the scanning library, and run a test scan with your own email address.
In this step, get ready to:
- Create and activate a Python virtual environment.
- Install Holehe and verify it runs correctly.
- Run a test scan to see your email's digital footprint.
Create a virtual environment and requirements file
A virtual environment is a self-contained Python installation that keeps this project's packages separate from everything else on your machine. This prevents version conflicts between different projects.
- Open the integrated terminal in VS Code by pressing Ctrl+` (the backtick key, above Tab).
- Create the virtual environment by running this command:
python -m venv venv
What does this command do?
python -m venv venv tells Python to create a virtual environment in a folder called venv. This folder contains its own copy of Python and a space for installed packages.
- You should see a new venv folder appear in the VS Code Explorer sidebar on the left.
- Activate the virtual environment by running:
venv\Scripts\activate
Your terminal prompt now starts with (venv), confirming the virtual environment is active.
Don't see (venv) in your prompt?
If you see an error about execution policies, run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned and then try the activate command again.
Make sure you're in your project folder. The venv folder must be in the same directory where your terminal is pointing.
help me activate my virtual environment on Windows
Now you'll create the file that lists your project's dependencies.
- In VS Code, click File in the top menu bar.
- Click New File.
- Save the file immediately with Ctrl+S and name it requirements.txt.
- Paste the following content into the file:
holehe==1.61
requests
What are these packages?
holehe==1.61 installs Holehe, the email account discovery tool, pinned to a specific version for reliability.
requests is an HTTP library that Holehe needs on Windows. Without it, Holehe crashes with a missing module error.
- Save requirements.txt with Ctrl+S.
- Confirm you can see requirements.txt in the Explorer sidebar alongside the venv folder.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Your requirements.txt should contain exactly this:
holehe==1.61
requests
Install the dependencies and verify Holehe
- Install both packages by running this command:
pip install -r requirements.txt
You should see pip download and install holehe, requests, and their sub-dependencies. The output ends with a confirmation that packages were successfully installed.
Seeing an error during install?
Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt.
If you see a permissions error, try closing VS Code completely and reopening it. Then re-activate the environment with venv\Scripts\activate before running the install again.
help me fix a pip install error on Windows
- Verify Holehe is installed correctly by running:
holehe --help
You should see Holehe's usage information and available command-line options listed in your terminal.
'holehe' is not recognized?
Make sure your virtual environment is still active. Run venv\Scripts\activate again, then retry the command.
If it still fails, try reinstalling directly with pip install holehe==1.61 and then run holehe --help again.
help me get holehe working in my terminal
Run a test scan on your email
Holehe works by checking public password-reset and registration endpoints on 120+ websites. It does not send any notifications or alerts to the email address being scanned.
- Run a test scan on your own email address:
holehe [[YOUR_EMAIL="your-email@example.com"]]
What is this tool actually doing?
Holehe sends requests to the password-reset or registration pages of 120+ popular websites (Twitter, Instagram, GitHub, Amazon, Discord, Spotify, and more). If the site confirms the email exists, Holehe marks it as found.
This technique is called OSINT (Open Source Intelligence). It uses publicly available information to map your digital footprint.
The scan takes 2-5 minutes to check all sites. Watch for colour-coded results in your terminal as they appear.
- When the scan finishes, look at the results. Lines with [+] in green mean your email is registered on that site. Lines with [x] in orange mean the site rate-limited the check.
Seeing 'all rate limited' or no results?
This is a known issue with Holehe. Some networks and ISPs block the rapid requests that the tool makes to check each site.
Try running the scan again after a few minutes. If the problem persists, try from a different network (such as a mobile hotspot). Your results may be partial, but the tool is still working correctly.
help me troubleshoot holehe rate limiting
Your scanning tool is installed and you've seen it in action. Next, you'll build a Python script that runs Holehe programmatically and saves every result to a local database.
Build the Scanner Script with Database Storage
In the previous step, you installed Holehe and confirmed it can scan websites for your email. You saw raw terminal output, but it disappeared the moment you closed the window.
Now you will build a Python script that runs Holehe programmatically, stores every result in a local SQLite database, and generates a styled HTML report you can open in your browser. This means your scan history is permanent and searchable.
In this step, get ready to:
- Create the database setup function that stores scan history.
- Write functions that run Holehe, parse the output, and save results.
- Generate a colour-coded HTML report from the database.
Create scanner.py and set up the database
Your script needs a place to store scan results that survives after the terminal closes. SQLite is a database engine built into Python, so there is nothing extra to install. It saves everything in a single file on your computer.
- In the VS Code Explorer sidebar, right-click inside your project folder and select New File.
- Name the file scanner.py and press Enter.
- Add the imports and file path constants at the top of scanner.py by pasting this code:
import subprocess
import json
import sqlite3
import os
import sys
from datetime import datetime
# Database setup
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "email_scans.db")
REPORT_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "report.html")
COMPARE_REPORT_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "comparison_report.html")
What does this code do?
- subprocess lets Python run terminal commands (like holehe) from inside the script.
- sqlite3 is Python's built-in database library. No separate install needed.
- DB_PATH, REPORT_PATH, and COMPARE_REPORT_PATH build absolute file paths so the script always saves files next to itself, regardless of where you run it from.
- Below the constants, add the init_db() function that creates the database tables:
def init_db():
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS scans (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
scan_date TEXT NOT NULL,
total_sites INTEGER,
accounts_found INTEGER
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
scan_id INTEGER NOT NULL,
site_name TEXT NOT NULL,
exists_status INTEGER,
rate_limited INTEGER,
email_recovery TEXT,
phone_number TEXT,
FOREIGN KEY (scan_id) REFERENCES scans(id)
)
""")
conn.commit()
conn.close()
What does this code do?
This function creates two tables in the database if they do not already exist.
- scans stores one row per scan session (email, date, summary counts).
- results stores one row per website checked, linked to its parent scan via scan_id.
The CREATE TABLE IF NOT EXISTS pattern means the function is safe to call every time the script runs. It only creates the tables on the first run.
- Save scanner.py.
Seeing red squiggles in VS Code?
If VS Code highlights imports with errors, it may be using the wrong Python interpreter. Click the Python version in the bottom-left status bar and select the interpreter inside your venv folder.
help me fix Python interpreter issues in VS Code
Write the scanning and parsing functions
The scanner needs to do three things: run the holehe command, parse the text output into structured data, and save that data to the database.
- Below init_db() in scanner.py, add the run_scan() function that executes holehe as a subprocess:
def run_scan(email):
print(f"\nScanning {email} across 120+ websites...")
print("This may take 2-5 minutes. Please wait.\n")
try:
result = subprocess.run(
["holehe", "--only-used", email, "--no-color"],
capture_output=True,
text=True,
timeout=600
)
output = result.stdout
except subprocess.TimeoutExpired:
print("Scan timed out after 10 minutes.")
return None
except FileNotFoundError:
print("Error: holehe is not installed or not found in PATH.")
print("Make sure your virtual environment is activated.")
return None
return output
What does this code do?
- subprocess.run() launches holehe exactly like you typed it in the terminal, but captures the output in a variable instead of printing it.
- --only-used filters holehe's output to only show sites where your email exists. --no-color removes colour formatting so the text is easier to parse.
- The try/except blocks handle two failure modes: the scan taking longer than 10 minutes, or holehe not being installed.
- Below run_scan(), add the parse_results() function that turns holehe's text output into structured data:
def parse_results(output):
results = []
lines = output.strip().split("\n")
for line in lines:
line = line.strip()
if not line:
continue
if "[+]" in line:
site_name = line.split("[+]")[1].strip().split(" ")[0] if "[+]" in line else ""
results.append({
"site_name": site_name,
"exists": 1,
"rate_limited": 0,
"email_recovery": "",
"phone_number": ""
})
elif "[x]" in line:
site_name = line.split("[x]")[1].strip().split(" ")[0] if "[x]" in line else ""
results.append({
"site_name": site_name,
"exists": 0,
"rate_limited": 1,
"email_recovery": "",
"phone_number": ""
})
return results
What does this code do?
Holehe's CLI output uses markers to indicate status. A [+] line means the email was found on that site. An [x] line means the site rate-limited the check.
This function splits the output into individual lines and extracts the site name from each marker. It builds a list of dictionaries, one per site, ready for the database.
- Below parse_results(), add the save_to_db() function that writes parsed results into the SQLite database:
def save_to_db(email, results):
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
scan_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
accounts_found = sum(1 for r in results if r["exists"] == 1)
cursor.execute(
"INSERT INTO scans (email, scan_date, total_sites, accounts_found) VALUES (?, ?, ?, ?)",
(email, scan_date, len(results), accounts_found)
)
scan_id = cursor.lastrowid
for r in results:
cursor.execute(
"INSERT INTO results (scan_id, site_name, exists_status, rate_limited, email_recovery, phone_number) VALUES (?, ?, ?, ?, ?, ?)",
(scan_id, r["site_name"], r["exists"], r["rate_limited"], r["email_recovery"], r["phone_number"])
)
conn.commit()
conn.close()
return scan_id
What does this code do?
- First it creates a row in the scans table with the email, current timestamp, and summary counts.
- cursor.lastrowid grabs the auto-generated ID of that new scan row.
- Then it loops through every result and inserts a row into the results table, linking each site back to its parent scan.
- Save scanner.py.
Seeing indentation errors?
Python is very strict about whitespace. Make sure each function uses consistent 4-space indentation. In VS Code, check that the status bar at the bottom shows Spaces: 4.
help me fix indentation in my Python file
Write the report generation and main function
The final piece is an HTML report that makes your results visual. Instead of scrolling through terminal text, you will open a colour-coded page in your browser showing every account found.
- Below save_to_db() in scanner.py, add the generate_report() function:
def generate_report(scan_id):
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT email, scan_date, total_sites, accounts_found FROM scans WHERE id = ?", (scan_id,))
scan = cursor.fetchone()
cursor.execute("SELECT site_name, exists_status, rate_limited FROM results WHERE scan_id = ?", (scan_id,))
results = cursor.fetchall()
conn.close()
email, scan_date, total_sites, accounts_found = scan
found_sites = [r for r in results if r[1] == 1]
rate_limited_sites = [r for r in results if r[2] == 1]
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email Account Discovery Report</title>
<style>
body {{ font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background: #1a1a2e; color: #eee; }}
h1 {{ color: #00d4ff; }}
.summary {{ background: #16213e; padding: 20px; border-radius: 10px; margin: 20px 0; }}
.summary h2 {{ margin-top: 0; color: #00d4ff; }}
.stat {{ display: inline-block; margin-right: 30px; }}
.stat-number {{ font-size: 2em; font-weight: bold; color: #00d4ff; }}
.stat-label {{ font-size: 0.9em; color: #aaa; }}
.found {{ background: #0a3d0a; border-left: 4px solid #00ff00; padding: 10px; margin: 5px 0; border-radius: 4px; }}
.rate-limited {{ background: #3d2a0a; border-left: 4px solid #ff9900; padding: 10px; margin: 5px 0; border-radius: 4px; }}
.section-title {{ color: #00ff00; margin-top: 30px; }}
.warning {{ color: #ff9900; }}
</style>
</head>
<body>
<h1>Email Account Discovery Report</h1>
<div class="summary">
<h2>Scan Summary</h2>
<p><strong>Email:</strong> {email}</p>
<p><strong>Date:</strong> {scan_date}</p>
<div class="stat">
<div class="stat-number">{accounts_found}</div>
<div class="stat-label">Accounts Found</div>
</div>
<div class="stat">
<div class="stat-number">{len(rate_limited_sites)}</div>
<div class="stat-label">Rate Limited</div>
</div>
<div class="stat">
<div class="stat-number">{total_sites}</div>
<div class="stat-label">Total Checked</div>
</div>
</div>
<h2 class="section-title">Accounts Found ({accounts_found})</h2>
"""
for site in found_sites:
html += f' <div class="found">{site[0]}</div>\n'
if rate_limited_sites:
html += f'\n <h2 class="warning">Rate Limited ({len(rate_limited_sites)})</h2>\n'
html += ' <p style="color: #aaa;">These sites blocked the check. Try again later.</p>\n'
for site in rate_limited_sites:
html += f' <div class="rate-limited">{site[0]}</div>\n'
html += """
</body>
</html>"""
with open(REPORT_PATH, "w", encoding="utf-8") as f:
f.write(html)
print(f"\nReport saved to: {REPORT_PATH}")
What does this code do?
- It queries the database for the scan summary and all individual results matching the given scan_id.
- It builds an HTML page with dark-themed CSS styling. Found accounts get a green left border. Rate-limited sites get an orange left border.
- The report is saved as report.html in the same folder as your script.
- Below generate_report(), add the compare_scans() function that generates a comparison report between two scans:
def compare_scans():
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT id, email, scan_date FROM scans ORDER BY id DESC LIMIT 2")
scans = cursor.fetchall()
if len(scans) < 2:
print("Need at least 2 scans to compare. Run more scans first.")
conn.close()
return
latest_id, email, latest_date = scans[0]
previous_id, _, previous_date = scans[1]
cursor.execute("SELECT site_name FROM results WHERE scan_id = ? AND exists_status = 1", (latest_id,))
latest_sites = set(row[0] for row in cursor.fetchall())
cursor.execute("SELECT site_name FROM results WHERE scan_id = ? AND exists_status = 1", (previous_id,))
previous_sites = set(row[0] for row in cursor.fetchall())
conn.close()
new_accounts = latest_sites - previous_sites
removed_accounts = previous_sites - latest_sites
unchanged = latest_sites & previous_sites
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Scan Comparison Report</title>
<style>
body {{ font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background: #1a1a2e; color: #eee; }}
h1 {{ color: #00d4ff; }}
.summary {{ background: #16213e; padding: 20px; border-radius: 10px; margin: 20px 0; }}
.new {{ background: #3d0a0a; border-left: 4px solid #ff4444; padding: 10px; margin: 5px 0; border-radius: 4px; }}
.removed {{ background: #0a1a3d; border-left: 4px solid #4488ff; padding: 10px; margin: 5px 0; border-radius: 4px; }}
.unchanged {{ background: #1a1a1a; border-left: 4px solid #666; padding: 10px; margin: 5px 0; border-radius: 4px; }}
</style>
</head>
<body>
<h1>Scan Comparison Report</h1>
<div class="summary">
<p><strong>Email:</strong> {email}</p>
<p><strong>Previous scan:</strong> {previous_date}</p>
<p><strong>Latest scan:</strong> {latest_date}</p>
</div>
<h2 style="color: #ff4444;">New Accounts ({len(new_accounts)})</h2>
"""
if new_accounts:
for site in sorted(new_accounts):
html += f' <div class="new">{site}</div>\n'
else:
html += ' <p style="color: #aaa;">No new accounts found.</p>\n'
html += f'\n <h2 style="color: #4488ff;">Removed Accounts ({len(removed_accounts)})</h2>\n'
if removed_accounts:
for site in sorted(removed_accounts):
html += f' <div class="removed">{site}</div>\n'
else:
html += ' <p style="color: #aaa;">No accounts removed.</p>\n'
html += f'\n <h2 style="color: #666;">Unchanged ({len(unchanged)})</h2>\n'
for site in sorted(unchanged):
html += f' <div class="unchanged">{site}</div>\n'
html += """
</body>
</html>"""
with open(COMPARE_REPORT_PATH, "w", encoding="utf-8") as f:
f.write(html)
print(f"\nComparison report saved to: {COMPARE_REPORT_PATH}")
print(f" New accounts: {len(new_accounts)}")
print(f" Removed accounts: {len(removed_accounts)}")
print(f" Unchanged: {len(unchanged)}")
What does this code do?
This function compares the two most recent scans stored in your database. It uses Python set operations to compute the difference: new accounts (found now but not before), removed accounts (found before but not now), and unchanged accounts.
You will use this function later in the Secret Mission. For now it sits ready in your script.
- Finally, add the main() function at the bottom of scanner.py to tie everything together:
def main():
init_db()
if "--compare" in sys.argv:
compare_scans()
return
email = input("Enter the email address to scan: ").strip()
if not email or "@" not in email:
print("Please enter a valid email address.")
return
output = run_scan(email)
if output is None:
return
results = parse_results(output)
if not results:
print("No results were captured. The scan may have been fully rate-limited.")
print("Try again later or from a different network.")
return
scan_id = save_to_db(email, results)
accounts_found = sum(1 for r in results if r["exists"] == 1)
print(f"\nScan complete!")
print(f" Accounts found: {accounts_found}")
print(f" Sites checked: {len(results)}")
print(f" Results saved to database (scan ID: {scan_id})")
generate_report(scan_id)
print(f"\nOpen report.html in your browser to view results.")
if __name__ == "__main__":
main()
What does this code do?
- init_db() runs first to guarantee the database tables exist.
- If the user passes --compare as a command-line argument, it runs the comparison function instead of a new scan.
- Otherwise it prompts for an email, runs the scan, parses and saves results, then generates the HTML report.
- if __name__ == "__main__" ensures main() only runs when you execute the file directly, not when it is imported.
- Save scanner.py.
You will not run the script yet. The next step creates a launcher and walks through the first full scan.
File not saving?
If VS Code shows a dot on the scanner.py tab, the file has unsaved changes. Press Ctrl+S to save. If you see a permission error, make sure you are editing files inside your own project folder and not a system directory.
help me save my file in VS Code
✔️ Awesome, I've got everything!
Great. Double check you have saved scanner.py and that the dot on the tab has disappeared.
ⓧ I'd like to double check the full code
Here is the complete scanner.py file. Compare yours against it and fix any differences.
import subprocess
import json
import sqlite3
import os
import sys
from datetime import datetime
# Database setup
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "email_scans.db")
REPORT_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "report.html")
COMPARE_REPORT_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "comparison_report.html")
def init_db():
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS scans (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
scan_date TEXT NOT NULL,
total_sites INTEGER,
accounts_found INTEGER
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
scan_id INTEGER NOT NULL,
site_name TEXT NOT NULL,
exists_status INTEGER,
rate_limited INTEGER,
email_recovery TEXT,
phone_number TEXT,
FOREIGN KEY (scan_id) REFERENCES scans(id)
)
""")
conn.commit()
conn.close()
def run_scan(email):
print(f"\nScanning {email} across 120+ websites...")
print("This may take 2-5 minutes. Please wait.\n")
try:
result = subprocess.run(
["holehe", "--only-used", email, "--no-color"],
capture_output=True,
text=True,
timeout=600
)
output = result.stdout
except subprocess.TimeoutExpired:
print("Scan timed out after 10 minutes.")
return None
except FileNotFoundError:
print("Error: holehe is not installed or not found in PATH.")
print("Make sure your virtual environment is activated.")
return None
return output
def parse_results(output):
results = []
lines = output.strip().split("\n")
for line in lines:
line = line.strip()
if not line:
continue
if "[+]" in line:
site_name = line.split("[+]")[1].strip().split(" ")[0] if "[+]" in line else ""
results.append({
"site_name": site_name,
"exists": 1,
"rate_limited": 0,
"email_recovery": "",
"phone_number": ""
})
elif "[x]" in line:
site_name = line.split("[x]")[1].strip().split(" ")[0] if "[x]" in line else ""
results.append({
"site_name": site_name,
"exists": 0,
"rate_limited": 1,
"email_recovery": "",
"phone_number": ""
})
return results
def save_to_db(email, results):
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
scan_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
accounts_found = sum(1 for r in results if r["exists"] == 1)
cursor.execute(
"INSERT INTO scans (email, scan_date, total_sites, accounts_found) VALUES (?, ?, ?, ?)",
(email, scan_date, len(results), accounts_found)
)
scan_id = cursor.lastrowid
for r in results:
cursor.execute(
"INSERT INTO results (scan_id, site_name, exists_status, rate_limited, email_recovery, phone_number) VALUES (?, ?, ?, ?, ?, ?)",
(scan_id, r["site_name"], r["exists"], r["rate_limited"], r["email_recovery"], r["phone_number"])
)
conn.commit()
conn.close()
return scan_id
def generate_report(scan_id):
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT email, scan_date, total_sites, accounts_found FROM scans WHERE id = ?", (scan_id,))
scan = cursor.fetchone()
cursor.execute("SELECT site_name, exists_status, rate_limited FROM results WHERE scan_id = ?", (scan_id,))
results = cursor.fetchall()
conn.close()
email, scan_date, total_sites, accounts_found = scan
found_sites = [r for r in results if r[1] == 1]
rate_limited_sites = [r for r in results if r[2] == 1]
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email Account Discovery Report</title>
<style>
body {{ font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background: #1a1a2e; color: #eee; }}
h1 {{ color: #00d4ff; }}
.summary {{ background: #16213e; padding: 20px; border-radius: 10px; margin: 20px 0; }}
.summary h2 {{ margin-top: 0; color: #00d4ff; }}
.stat {{ display: inline-block; margin-right: 30px; }}
.stat-number {{ font-size: 2em; font-weight: bold; color: #00d4ff; }}
.stat-label {{ font-size: 0.9em; color: #aaa; }}
.found {{ background: #0a3d0a; border-left: 4px solid #00ff00; padding: 10px; margin: 5px 0; border-radius: 4px; }}
.rate-limited {{ background: #3d2a0a; border-left: 4px solid #ff9900; padding: 10px; margin: 5px 0; border-radius: 4px; }}
.section-title {{ color: #00ff00; margin-top: 30px; }}
.warning {{ color: #ff9900; }}
</style>
</head>
<body>
<h1>Email Account Discovery Report</h1>
<div class="summary">
<h2>Scan Summary</h2>
<p><strong>Email:</strong> {email}</p>
<p><strong>Date:</strong> {scan_date}</p>
<div class="stat">
<div class="stat-number">{accounts_found}</div>
<div class="stat-label">Accounts Found</div>
</div>
<div class="stat">
<div class="stat-number">{len(rate_limited_sites)}</div>
<div class="stat-label">Rate Limited</div>
</div>
<div class="stat">
<div class="stat-number">{total_sites}</div>
<div class="stat-label">Total Checked</div>
</div>
</div>
<h2 class="section-title">Accounts Found ({accounts_found})</h2>
"""
for site in found_sites:
html += f' <div class="found">{site[0]}</div>\n'
if rate_limited_sites:
html += f'\n <h2 class="warning">Rate Limited ({len(rate_limited_sites)})</h2>\n'
html += ' <p style="color: #aaa;">These sites blocked the check. Try again later.</p>\n'
for site in rate_limited_sites:
html += f' <div class="rate-limited">{site[0]}</div>\n'
html += """
</body>
</html>"""
with open(REPORT_PATH, "w", encoding="utf-8") as f:
f.write(html)
print(f"\nReport saved to: {REPORT_PATH}")
def compare_scans():
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT id, email, scan_date FROM scans ORDER BY id DESC LIMIT 2")
scans = cursor.fetchall()
if len(scans) < 2:
print("Need at least 2 scans to compare. Run more scans first.")
conn.close()
return
latest_id, email, latest_date = scans[0]
previous_id, _, previous_date = scans[1]
cursor.execute("SELECT site_name FROM results WHERE scan_id = ? AND exists_status = 1", (latest_id,))
latest_sites = set(row[0] for row in cursor.fetchall())
cursor.execute("SELECT site_name FROM results WHERE scan_id = ? AND exists_status = 1", (previous_id,))
previous_sites = set(row[0] for row in cursor.fetchall())
conn.close()
new_accounts = latest_sites - previous_sites
removed_accounts = previous_sites - latest_sites
unchanged = latest_sites & previous_sites
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Scan Comparison Report</title>
<style>
body {{ font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background: #1a1a2e; color: #eee; }}
h1 {{ color: #00d4ff; }}
.summary {{ background: #16213e; padding: 20px; border-radius: 10px; margin: 20px 0; }}
.new {{ background: #3d0a0a; border-left: 4px solid #ff4444; padding: 10px; margin: 5px 0; border-radius: 4px; }}
.removed {{ background: #0a1a3d; border-left: 4px solid #4488ff; padding: 10px; margin: 5px 0; border-radius: 4px; }}
.unchanged {{ background: #1a1a1a; border-left: 4px solid #666; padding: 10px; margin: 5px 0; border-radius: 4px; }}
</style>
</head>
<body>
<h1>Scan Comparison Report</h1>
<div class="summary">
<p><strong>Email:</strong> {email}</p>
<p><strong>Previous scan:</strong> {previous_date}</p>
<p><strong>Latest scan:</strong> {latest_date}</p>
</div>
<h2 style="color: #ff4444;">New Accounts ({len(new_accounts)})</h2>
"""
if new_accounts:
for site in sorted(new_accounts):
html += f' <div class="new">{site}</div>\n'
else:
html += ' <p style="color: #aaa;">No new accounts found.</p>\n'
html += f'\n <h2 style="color: #4488ff;">Removed Accounts ({len(removed_accounts)})</h2>\n'
if removed_accounts:
for site in sorted(removed_accounts):
html += f' <div class="removed">{site}</div>\n'
else:
html += ' <p style="color: #aaa;">No accounts removed.</p>\n'
html += f'\n <h2 style="color: #666;">Unchanged ({len(unchanged)})</h2>\n'
for site in sorted(unchanged):
html += f' <div class="unchanged">{site}</div>\n'
html += """
</body>
</html>"""
with open(COMPARE_REPORT_PATH, "w", encoding="utf-8") as f:
f.write(html)
print(f"\nComparison report saved to: {COMPARE_REPORT_PATH}")
print(f" New accounts: {len(new_accounts)}")
print(f" Removed accounts: {len(removed_accounts)}")
print(f" Unchanged: {len(unchanged)}")
def main():
init_db()
if "--compare" in sys.argv:
compare_scans()
return
email = input("Enter the email address to scan: ").strip()
if not email or "@" not in email:
print("Please enter a valid email address.")
return
output = run_scan(email)
if output is None:
return
results = parse_results(output)
if not results:
print("No results were captured. The scan may have been fully rate-limited.")
print("Try again later or from a different network.")
return
scan_id = save_to_db(email, results)
accounts_found = sum(1 for r in results if r["exists"] == 1)
print(f"\nScan complete!")
print(f" Accounts found: {accounts_found}")
print(f" Sites checked: {len(results)}")
print(f" Results saved to database (scan ID: {scan_id})")
generate_report(scan_id)
print(f"\nOpen report.html in your browser to view results.")
if __name__ == "__main__":
main()
Your scanner script is complete. Next up, you will create a one-click launcher and run your first full scan to see real results in the database and browser.
Create the Launcher and Run Your First Scan
Your scanner script is fully built and ready to go. The last piece is making it easy to run whenever you want to check your digital footprint.
In this step, you will create a batch file launcher so you can double-click to run a scan anytime. Then you will run your first full scan and explore the results.
In this step, get ready to:
- Create a one-click batch file launcher for your scanner.
- Run your first full scan and view the HTML report.
- Verify your scan history is stored in the database.
Create the batch file launcher
Right now, running your tool requires activating the virtual environment and typing the python command manually every time. A batch file automates this so you can double-click a single file from File Explorer to launch a scan.
- In VS Code, create a new file called run_scan.bat in your project folder (the same folder that contains scanner.py).
- Paste the following code into run_scan.bat:
@echo off
cd /d "%~dp0"
call venv\Scripts\activate
python scanner.py %*
pause
What does this code do?
- @echo off hides the commands themselves from the window so you only see the script's output.
- cd /d "%~dp0" changes directory to wherever the batch file lives, so it works regardless of where you double-click from.
- call venv\Scripts\activate activates your virtual environment automatically.
- python scanner.py %* runs your scanner and passes along any extra arguments (like --compare later).
- pause keeps the window open after the script finishes so you can read the results.
- Save run_scan.bat.
- Confirm the file exists by running:
ls
- You should see run_scan.bat listed alongside scanner.py, requirements.txt, and the venv folder.
Don't see run_scan.bat?
Make sure you created the file in the project root folder, not inside the venv folder or another subfolder. The file must sit next to scanner.py.
help me find where my run_scan.bat file was saved
✔️ Awesome, I've got everything!
Great. Make sure you have saved run_scan.bat before moving on.
ⓧ I'd like to double check the full code
Here is the complete run_scan.bat file:
@echo off
cd /d "%~dp0"
call venv\Scripts\activate
python scanner.py %*
pause
Run your first full scan
Everything is wired up. Time to put your tool to the test and discover where your email has been used to create accounts.
- Run the batch file from your VS Code terminal by running this command:
.\run_scan.bat
- When prompted with Enter the email address to scan:, type your own email address and press Enter.
How long does the scan take?
The scan checks 120+ websites one by one, which typically takes 2-5 minutes. You will see output appear as the scan progresses. Be patient and let it finish.
Holehe does not alert the email owner or send any messages. It only checks public password-reset and registration endpoints.
When the scan finishes, you should see output similar to this in your terminal:
Scan complete!
Accounts found: 12
Sites checked: 47
Results saved to database (scan ID: 1)
Report saved to: C:\...\report.html
Open report.html in your browser to view results.
What if I see fewer results than expected?
Many websites rate-limit password-reset checks. Your first scan might show fewer accounts than actually exist. This is normal. Running the scan again later or from a different network can yield different results.
Seeing an error instead of results?
- If you see "holehe is not installed or not found in PATH", your virtual environment did not activate properly. Close the terminal, open a new one, run venv\Scripts\activate manually, then try .\run_scan.bat again.
- If the scan timed out, your network may be slow. Try running it again.
- If you see "No results were captured", the scan was fully rate-limited. Wait a few minutes and try again, or try from a different network.
help me debug my email scan errors
View your report and check the database
Your scan created two things: an HTML report for visual browsing and a database record for historical tracking. Let's look at both.
- Open report.html in your browser by double-clicking it in File Explorer, or by right-clicking it in the VS Code Explorer sidebar and selecting Reveal in File Explorer, then double-clicking the file.
You should see a dark-themed report with a summary section at the top showing your email, the scan date, and three stats: accounts found, rate limited, and total checked. Below that, each discovered account is listed in green, and rate-limited sites appear in orange.
What do the colours mean?
- Green means the website confirmed your email is registered there.
- Orange means the website blocked the check (rate-limited). These sites may or may not have your email registered.
- Now verify your scan history is stored in the database by running this command in your VS Code terminal:
python -c "import sqlite3; conn = sqlite3.connect('email_scans.db'); rows = conn.execute('SELECT id, email, scan_date, accounts_found FROM scans').fetchall(); [print(row) for row in rows]; conn.close()"
You should see a row printed with your scan ID, email address, timestamp, and the number of accounts found.
Why store results in a database?
Every time you run a scan, the results are saved with a timestamp. Over time, this builds a history of your digital footprint. You can compare scans to see if new accounts appear or old ones get removed.
Database query returns nothing?
Make sure your scan completed successfully before querying. If the scan was fully rate-limited ("No results were captured"), nothing gets saved to the database. Run the scan again and wait for the "Scan complete" message before checking.
help me check my email_scans.db database
Secret mission
Add a "Compare Scans" Feature
Your scanner already has a hidden power. A comparison mode that highlights what changed between your last two scans. New accounts show up in red. Removed accounts in blue. Run a second scan and unlock it.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your files for future scans, pause by freeing up space, or delete everything entirely.
Resources you used:
- Your project folder containing scanner.py, requirements.txt, run_scan.bat, report.html, comparison_report.html, and email_scans.db.
- The venv virtual environment folder inside your project.
- Python 3.13 installed on your system.
- VS Code installed on your system.
Keep everything running
No action needed. Choose this if you plan to keep running scans on your email or other emails in the future.
- Your project folder stays where you created it.
- Python and VS Code remain installed for future projects.
- Double-click run_scan.bat anytime you want to run a new scan.
Pause - I'll come back to this later
Free up disk space by removing the virtual environment while keeping your code and scan history intact.
- Open Windows Terminal and navigate to your project folder.
- Delete the virtual environment by running:
rmdir /s /q venv
- Your code files, database, and reports stay in place.
- When you return, recreate the virtual environment by running python -m venv venv, activating it, and reinstalling with pip install -r requirements.txt.
Delete - I don't want to use this again
Remove all project files, the database, and optionally uninstall Python and VS Code.
- Open Windows Terminal and navigate to the parent folder that contains your project folder (for example, your Desktop).
- Delete the entire project folder by running the following command, replacing the folder name with whatever you named your project folder:
rmdir /s /q [[PROJECT_FOLDER="your project folder name"]]
This removes all project files including scanner.py, email_scans.db, the generated reports, and the venv folder.
- To uninstall Python, press the Windows key and search for Add or remove programs.
- Find Python 3.13 in the list, click it, and select Uninstall.
- To uninstall VS Code, find Microsoft Visual Studio Code in the same list, click it, and select Uninstall.
Nice Work!
Nice Work!
You just built a personal email account discovery tool from scratch using Python. You now have a reusable security scanner that reveals your digital footprint on demand.
You learned how to:
- Used Holehe to scan 120+ websites for email registration status using password-reset enumeration.
- Built a SQLite database to store scan history with timestamps for long-term tracking.
- Generated styled HTML reports that colour-code your discovered accounts by status and open directly in your browser.
- Added a comparison feature that highlights new and removed accounts between scans so you can track changes in your digital footprint over time.
Ready to quiz yourself?