August 9, 2026
One DNS Query Maps a Company’s Entire SaaS Stack
Part 3 — The complete email OSINT workflow, in order: validation, dorking, the pivot, verification discipline, the legal line, and a…

By Devansh Patel
11 min read
- 1 Part 3 — The complete email OSINT workflow, in order: validation, dorking, the pivot, verification discipline, the legal line, and a pipeline that runs the whole chain.
- 2 Step 1: The thirty seconds before you start
- 3 Step 2: The Gmail gap in every guide
- 4 Step 3: Dorking, across five engines
- 5 The git commit goldmine
Part 3 — The complete email OSINT workflow, in order: validation, dorking, the pivot, verification discipline, the legal line, and a pipeline that runs the whole chain.
Most people investigating an email address start in exactly the wrong place.
They paste it into a tool. The tool takes four minutes. It returns something ambiguous. They paste it into a second tool. Another four minutes. Somewhere around minute twenty they discover the domain has no MX records and has never been capable of receiving mail at all.
Thirty seconds of DNS at the start would have caught that — and along the way it would have handed them the target organisation's entire third-party SaaS stack, for free, from a single query.
Part 3, the final part of this series. In parts 1 and 2 we've discussed the theory behind why email leaks data, and the deepest sources available: Google's internal identifiers and infostealer logs. Here's how: the order of operations, the verification discipline that makes findings defensible, the legal line, and a script to automate the chain.
You don't need the earlier parts to use this one. But if you ever treat a "no accounts found" result as a finding, go read Part 1 first — that result is very often a lie.
Step 1: The thirty seconds before you start
Validate the address before you burn an hour on a domain that can't receive mail.
TARGET="target@example.com"
DOMAIN="${TARGET#*@}"
dig +short MX $DOMAIN
dig +short TXT $DOMAIN | grep -i spf
dig +short TXT _dmarc.$DOMAIN
dig +short TXT default._domainkey.$DOMAINTARGET="target@example.com"
DOMAIN="${TARGET#*@}"
dig +short MX $DOMAIN
dig +short TXT $DOMAIN | grep -i spf
dig +short TXT _dmarc.$DOMAIN
dig +short TXT default._domainkey.$DOMAINFour orders. Here's what each actually gives you:
MX — tells you who the mail provider is, which determines your whole toolset. aspmx.l.google.com means Google Workspace which means the Gaia ID pivots from Part 2 are live. protection.outlook.com sends you somewhere totally different. No MX at all means stop, the address is undeliverable and anything you find is historical at best.
SPF — this is the sleeper. Almost no one uses it this way. SPF records are there to list every third party that is allowed to send mail on behalf of that domain. Mailchimp . SendGrid. — Salesforce . Zendesk. Freshdesk Working day.
One DNS query just enumerated the organisation's SaaS vendors. Each entry is a new pivot — a new login portal, a new potential breach exposure, a new place employees have accounts.
DMARC and DKIM — security posture. A corporate domain with no DMARC record is a finding in its own right, and it tells you something about the organisation's maturity before you've looked at a single employee.
Then check for disposable domains before investing effort:
curl -s https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/master/disposable_email_blocklist.conf | grep -x "$DOMAIN"curl -s https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/master/disposable_email_blocklist.conf | grep -x "$DOMAIN"On SMTP probing: older guides recommend telnet mail.target.com 25 and reading the RCPT TO response code. In 2026, treat a 250 as very weak evidence. Catch-all configurations accept everything, greylisting delays responses, VRFY is disabled almost universally, and repeated probes get you rate-limited and logged. Not useless — just no longer the confirmation people treat it as.
Step 2: The Gmail gap in every guide
This one costs people results constantly, and I have never seen it covered properly in a beginner article.
Gmail ignores dots. Gmail ignores everything after a +. Gmail treats googlemail.com as an alias. Every one of these delivers to the same inbox:
johnsmith@gmail.com
john.smith@gmail.com
j.o.h.n.s.m.i.t.h@gmail.com
johnsmith+netflix@gmail.com
johnsmith@googlemail.comjohnsmith@gmail.com
john.smith@gmail.com
j.o.h.n.s.m.i.t.h@gmail.com
johnsmith+netflix@gmail.com
johnsmith@googlemail.comYour target was logged at multiple sites with different variants — people are inconsistent about their own dots. Your dorks search one string. You miss all the other stuff.
You don't need every mathematical permutation, that explodes combinatorially and most results are nonsense. You want the few that fall on plausible name boundaries:
def plausible_gmail_variants(local, first=None, last=None):
"""Realistic Gmail dot-variants, not all 2^(n-1) of them."""
v = {local, local.replace(".", "")}
if first and last:
f, l = first.lower(), last.lower()
v.update({
f"{f}.{l}", f"{f}{l}", f"{f[0]}.{l}", f"{f[0]}{l}",
f"{f}.{l[0]}", f"{f}_{l}", f"{l}.{f}", f"{l}{f}",
})
return sorted(v)
for variant in plausible_gmail_variants("johnsmith", "john", "smith"):
print(f'"{variant}@gmail.com"')def plausible_gmail_variants(local, first=None, last=None):
"""Realistic Gmail dot-variants, not all 2^(n-1) of them."""
v = {local, local.replace(".", "")}
if first and last:
f, l = first.lower(), last.lower()
v.update({
f"{f}.{l}", f"{f}{l}", f"{f[0]}.{l}", f"{f[0]}{l}",
f"{f}.{l[0]}", f"{f}_{l}", f"{l}.{f}", f"{l}{f}",
})
return sorted(v)
for variant in plausible_gmail_variants("johnsmith", "john", "smith"):
print(f'"{variant}@gmail.com"')Feed all variants into your dorks and your enumeration.
The +tag variations are a perk. If you ever find one in a breach dump — johnsmith+linkedin@gmail.com — the tag tells you which service they signed up for. Plus addressing is people tagging their own data for you.
Step 3: Dorking, across five engines
The basics still work:
"target@gmail.com"
intext:"target@gmail.com"
"target@gmail.com" filetype:pdf
"target@gmail.com" (filetype:xlsx OR filetype:docx OR filetype:csv OR filetype:txt)
site:pastebin.com "target@gmail.com"
site:github.com "target@gmail.com"
"targetusername" "gmail.com""target@gmail.com"
intext:"target@gmail.com"
"target@gmail.com" filetype:pdf
"target@gmail.com" (filetype:xlsx OR filetype:docx OR filetype:csv OR filetype:txt)
site:pastebin.com "target@gmail.com"
site:github.com "target@gmail.com"
"targetusername" "gmail.com"Documents are the highest yield class. CVs, delegate lists, purchase order records, academic author blocks, society minutes, tender documents People will upload a PDF of their entire contact block and never think about it again. That PDF outlasts every privacy setting they ever tinkered with.
Same set of questions on Google, Bing, DuckDuckGo, Yandex and Mojeek. There is way less index overlap than people think. Yandex in particular returns things Google has dropped or deranked — and it's much better at face matching if you get that far with reverse image search.
The git commit goldmine
Developers leak emails constantly, and git metadata is permanent by design — commit objects are immutable, and rewriting history doesn't touch forks or archives.
# Every public commit by an author email
curl -s -H "Authorization: token $GH_TOKEN" \
"https://api.github.com/search/commits?q=author-email:target@gmail.com" \
| jq '.items[] | {repo: .repository.full_name, date: .commit.author.date}'# Every public commit by an author email
curl -s -H "Authorization: token $GH_TOKEN" \
"https://api.github.com/search/commits?q=author-email:target@gmail.com" \
| jq '.items[] | {repo: .repository.full_name, date: .commit.author.date}'Reverse direction — pull every contributor email out of a repo's full history:
git clone --mirror https://github.com/org/repo.git && cd repo.git
git log --all --format='%aN <%aE>' | sort -ugit clone --mirror https://github.com/org/repo.git && cd repo.git
git log --all --format='%aN <%aE>' | sort -uEven if someone scrubbed their email from their current profile, it's sitting in commit objects from 2019 with their real name attached.
Step 4: The pivot to username
This is the step beginner guides skip, and it's the one that multiplies your surface area.
Strip the local part. Hunt it as a handle:
pipx install maigret
maigret targethandle --html --top-sites 500 --timeout 15pipx install maigret
maigret targethandle --html --top-sites 500 --timeout 15Or with user-scanner from Part 1:
user-scanner -u targethandleuser-scanner -u targethandleFor structured extraction from any profile you find, socid-extractor pulls the internal IDs — Gaia ID, Facebook UID, Yandex Public ID, Instagram pk. Those are the stable identifiers that survive every rename, and they're what you should be anchoring on.
The chain runs:
email → username → profiles → display name → other emails/phones → public records → new emails → repeat
Each loop widens the net. This is the actual craft, and it's why "which tool should I use" is the wrong question. The tools are interchangeable. The chain is the skill.
Step 5: Verification discipline
Here is where most investigations go quietly, catastrophically wrong.
A username match is correlation. It is not identity.
rahul_sharma on GitHub and rahul_sharma on Reddit might be the same person. They might be two of several hundred thousand people with that name. If you write "the target's Reddit account" in a report on the strength of a handle match alone, you have not made a finding. You have fabricated one.
You need at least 2 independent signals before you can merge 2 accounts into 1 person:
Signal type Example Power Internal platform identifier Same Gaia ID across services Conclusive Unique recycled media Same profile picture, reverse image verified Powerful self-reference One account explicitly links to the other Unless spoofed Strong Temporal correlation Posting hours group in the same timezone on both Moderate Stylometric Same unique phrasing, misspellings, punctuation style Moderate Handle match only Same username weak — never enough
And record your negatives. The finding is "Checked LinkedIn 3 ways and found nothing." It stops you and everyone after you from running into the same dead end again, and that's what allows an investigation to survive scrutiny six months later when someone asks how you came to a conclusion.
Confidence should be a field in your notes, not a feeling in your head.
Step 6: OPSEC — your investigation has a footprint too
Non-negotiable baseline:
Different person. Ideally separate VM, minimum dedicated browser profile.
Not your typical ride. Never a session logged into your real accounts. Old sockpuppets. Even a google account created 20 minutes ago and used for lookups right away is a flag in itself. They matter, but only if you warm them for weeks.
Network separation. VPN or residential proxy. This is what user-scanner's built-in proxy rotation with pre-scan health checks is for.
Know your modules by heart. Some enumeration modules actually send password-reset emails to the target. Give it a whirl on an address you know you own, and scan by category, not just blasting everything out at once.
Assume query logging. Every hosted tool logs your searches against your account. Every self-hosted tool that authenticates to a platform writes your identity into that platform's logs. Assume all of it is recorded and attributable.
Document as you go. Timestamp, query, result, confidence. Not optional if the work might ever be examined.
Step 7: The legal line — and yes, this matters in India
Most OSINT guides write "be ethical" and move on. That's not useful. Here's the actual framework.
India — the DPDP Act
The Digital Personal Data Protection Act, 2023 is now operational. MeitY notified the Digital Personal Data Protection Rules, 2025 on 14 November 2025, following a ten-month wait after the draft release, with phased compliance timelines.
The provision every Indian practitioner needs to know is Section 3(c)(ii): the Act excludes from its scope personal data that a Data Principal has made, or caused to be made, publicly available. Legal commentators have noted this exclusion is clear and unqualified in its operation — where it applies, the Act imposes no statutory obligations and the data principal's rights don't arise.
There's also a personal or domestic use exemption, mirroring GDPR.
Do not over-read either. Three cautions:
1. The exemption attaches to data the person made public themselves. A breach dump is not "publicly available data" in this sense — nobody chose to publish it. Availing the exemption requires verifying the source of the data, and analysts have specifically flagged that businesses cannot indiscriminately scrape public sources and claim cover.
2. The boundaries are genuinely unsettled. What counts as "publicly available" is legally blurry, and the DPDP Act sets a high bar for consent where it does apply — implied consent is not valid grounds for processing.
3. Penalties are severe. Up to ₹250 crore for serious violations, with extraterritorial reach.
And critically: the DPDP Act is not the only law in the room. Stalking, harassment, and unauthorised access provisions under the IT Act and the BNS operate entirely independently of whether the data was public.
Europe — GDPR
No "publicly available" carve-out equivalent to India's. Processing personal data of EU residents requires a lawful basis. Legitimate interest can cover fraud investigation and security research, but it demands a documented balancing test — and Article 14 notification obligations apply when you collect data about someone from third-party sources.
The line that actually matters
Every technique in this series is legal to run against yourself, against assets you're authorised to test, and in most jurisdictions against publicly published information for legitimate investigative purposes.
None of it is legal as a way to track an ex-partner, dox someone you're arguing with, or profile a private individual out of curiosity.
The techniques are identical in both cases. The only thing separating an investigator from a stalker is authorisation and purpose. There is no technical distinction — which is exactly why this discipline runs on documented scope rather than good intentions.
If you're learning: run every single thing in this series against your own address first. You'll learn the tooling, and you'll be genuinely unsettled by what surfaces about you. That's the point.
The full pipeline
Everything above, wired together. Save as email_recon.py.
#!/usr/bin/env python3
"""
Email OSINT orchestrator — validation, dorks, enumeration, stealer logs.
Run only against addresses you own or are authorised to investigate.
Deps: pip install user-scanner httpx
(dig from dnsutils / bind-utils)
"""
import asyncio
import json
import subprocess
import sys
from datetime import datetime, timezone
import httpx
HUDSON = "https://cavalier.hudsonrock.com/api/json/v2/osint-tools/search-by-email"
def dig(record: str, domain: str) -> list[str]:
try:
out = subprocess.run(
["dig", "+short", record, domain],
capture_output=True, text=True, timeout=10,
)
return [l.strip() for l in out.stdout.splitlines() if l.strip()]
except Exception as e:
return [f"error: {e}"]
def classify_provider(mx: list[str]) -> str:
joined = " ".join(mx).lower()
if "google" in joined or "googlemail" in joined:
return "Google Workspace / Gmail — Gaia ID pivots available"
if "outlook" in joined or "protection.outlook" in joined:
return "Microsoft 365"
if "zoho" in joined:
return "Zoho"
if "protonmail" in joined or "proton.me" in joined:
return "Proton — privacy-conscious target, expect a thin footprint"
return "Other / self-hosted" if mx else "No MX — undeliverable"
def validate_domain(domain: str) -> dict:
mx = dig("MX", domain)
txt = dig("TXT", domain)
return {
"mx": mx,
"deliverable": bool(mx),
"spf": [t for t in txt if "spf1" in t.lower()],
"dmarc": dig("TXT", f"_dmarc.{domain}"),
"provider": classify_provider(mx),
}
def gmail_variants(local: str, first=None, last=None) -> list[str]:
v = {local, local.replace(".", "")}
if first and last:
f, l = first.lower(), last.lower()
v.update({
f"{f}.{l}", f"{f}{l}", f"{f[0]}.{l}", f"{f[0]}{l}",
f"{f}.{l[0]}", f"{l}.{f}", f"{l}{f}",
})
return sorted(v)
def build_dorks(email: str) -> list[str]:
local, domain = email.split("@")
return [
f'"{email}"',
f'intext:"{email}"',
f'"{email}" filetype:pdf',
f'"{email}" (filetype:xlsx OR filetype:docx OR filetype:csv OR filetype:txt)',
f'site:pastebin.com "{email}"',
f'site:github.com "{email}"',
f'"{local}" "{domain}"',
]
async def hudson_rock(email: str) -> dict:
try:
async with httpx.AsyncClient(timeout=25) as c:
r = await c.get(HUDSON, params={"email": email})
r.raise_for_status()
return r.json()
except Exception as e:
return {"error": str(e)}
def run_scanner(email: str) -> dict:
"""Delegate enumeration to user-scanner."""
try:
out = subprocess.run(
["user-scanner", "-e", email, "--json"],
capture_output=True, text=True, timeout=400,
)
try:
return json.loads(out.stdout)
except json.JSONDecodeError:
return {"raw": out.stdout[-4000:]}
except FileNotFoundError:
return {"error": "user-scanner not installed: pip install user-scanner"}
except Exception as e:
return {"error": str(e)}
async def main(email: str, first=None, last=None):
local, domain = email.split("@")
print(f"[*] Target: {email}")
report = {
"target": email,
"timestamp": datetime.now(timezone.utc).isoformat(),
"dns": validate_domain(domain),
}
if not report["dns"]["deliverable"]:
print("[!] No MX records — domain does not accept mail. Stopping.")
report["verdict"] = "undeliverable"
print(json.dumps(report, indent=2))
return
print(f"[+] Provider: {report['dns']['provider']}")
if report["dns"]["spf"]:
print("[+] SPF present — check entries for third-party SaaS pivots")
if domain.lower() in ("gmail.com", "googlemail.com"):
report["gmail_variants"] = gmail_variants(local, first, last)
print(f"[+] {len(report['gmail_variants'])} Gmail variants to search")
report["dorks"] = build_dorks(email)
print("[*] Querying Hudson Rock (infostealer logs)...")
report["stealer_logs"] = await hudson_rock(email)
print("[*] Running account enumeration (this takes a minute)...")
report["enumeration"] = run_scanner(email)
fname = f"recon_{local}_{int(datetime.now().timestamp())}.json"
with open(fname, "w") as f:
json.dump(report, f, indent=2)
print(f"\n[+] Report written to {fname}")
print("\n--- DORKS: run manually across Google, Bing, DuckDuckGo, Yandex, Mojeek ---")
for d in report["dorks"]:
print(f" {d}")
print("\n[!] Reminder: a negative result from any single tool proves nothing.")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python3 email_recon.py <email> [firstname] [lastname]")
sys.exit(1)
args = sys.argv[1:]
asyncio.run(main(args[0], *(args[1:3] if len(args) > 1 else [])))
python3 email_recon.py yourname@gmail.com your first#!/usr/bin/env python3
"""
Email OSINT orchestrator — validation, dorks, enumeration, stealer logs.
Run only against addresses you own or are authorised to investigate.
Deps: pip install user-scanner httpx
(dig from dnsutils / bind-utils)
"""
import asyncio
import json
import subprocess
import sys
from datetime import datetime, timezone
import httpx
HUDSON = "https://cavalier.hudsonrock.com/api/json/v2/osint-tools/search-by-email"
def dig(record: str, domain: str) -> list[str]:
try:
out = subprocess.run(
["dig", "+short", record, domain],
capture_output=True, text=True, timeout=10,
)
return [l.strip() for l in out.stdout.splitlines() if l.strip()]
except Exception as e:
return [f"error: {e}"]
def classify_provider(mx: list[str]) -> str:
joined = " ".join(mx).lower()
if "google" in joined or "googlemail" in joined:
return "Google Workspace / Gmail — Gaia ID pivots available"
if "outlook" in joined or "protection.outlook" in joined:
return "Microsoft 365"
if "zoho" in joined:
return "Zoho"
if "protonmail" in joined or "proton.me" in joined:
return "Proton — privacy-conscious target, expect a thin footprint"
return "Other / self-hosted" if mx else "No MX — undeliverable"
def validate_domain(domain: str) -> dict:
mx = dig("MX", domain)
txt = dig("TXT", domain)
return {
"mx": mx,
"deliverable": bool(mx),
"spf": [t for t in txt if "spf1" in t.lower()],
"dmarc": dig("TXT", f"_dmarc.{domain}"),
"provider": classify_provider(mx),
}
def gmail_variants(local: str, first=None, last=None) -> list[str]:
v = {local, local.replace(".", "")}
if first and last:
f, l = first.lower(), last.lower()
v.update({
f"{f}.{l}", f"{f}{l}", f"{f[0]}.{l}", f"{f[0]}{l}",
f"{f}.{l[0]}", f"{l}.{f}", f"{l}{f}",
})
return sorted(v)
def build_dorks(email: str) -> list[str]:
local, domain = email.split("@")
return [
f'"{email}"',
f'intext:"{email}"',
f'"{email}" filetype:pdf',
f'"{email}" (filetype:xlsx OR filetype:docx OR filetype:csv OR filetype:txt)',
f'site:pastebin.com "{email}"',
f'site:github.com "{email}"',
f'"{local}" "{domain}"',
]
async def hudson_rock(email: str) -> dict:
try:
async with httpx.AsyncClient(timeout=25) as c:
r = await c.get(HUDSON, params={"email": email})
r.raise_for_status()
return r.json()
except Exception as e:
return {"error": str(e)}
def run_scanner(email: str) -> dict:
"""Delegate enumeration to user-scanner."""
try:
out = subprocess.run(
["user-scanner", "-e", email, "--json"],
capture_output=True, text=True, timeout=400,
)
try:
return json.loads(out.stdout)
except json.JSONDecodeError:
return {"raw": out.stdout[-4000:]}
except FileNotFoundError:
return {"error": "user-scanner not installed: pip install user-scanner"}
except Exception as e:
return {"error": str(e)}
async def main(email: str, first=None, last=None):
local, domain = email.split("@")
print(f"[*] Target: {email}")
report = {
"target": email,
"timestamp": datetime.now(timezone.utc).isoformat(),
"dns": validate_domain(domain),
}
if not report["dns"]["deliverable"]:
print("[!] No MX records — domain does not accept mail. Stopping.")
report["verdict"] = "undeliverable"
print(json.dumps(report, indent=2))
return
print(f"[+] Provider: {report['dns']['provider']}")
if report["dns"]["spf"]:
print("[+] SPF present — check entries for third-party SaaS pivots")
if domain.lower() in ("gmail.com", "googlemail.com"):
report["gmail_variants"] = gmail_variants(local, first, last)
print(f"[+] {len(report['gmail_variants'])} Gmail variants to search")
report["dorks"] = build_dorks(email)
print("[*] Querying Hudson Rock (infostealer logs)...")
report["stealer_logs"] = await hudson_rock(email)
print("[*] Running account enumeration (this takes a minute)...")
report["enumeration"] = run_scanner(email)
fname = f"recon_{local}_{int(datetime.now().timestamp())}.json"
with open(fname, "w") as f:
json.dump(report, f, indent=2)
print(f"\n[+] Report written to {fname}")
print("\n--- DORKS: run manually across Google, Bing, DuckDuckGo, Yandex, Mojeek ---")
for d in report["dorks"]:
print(f" {d}")
print("\n[!] Reminder: a negative result from any single tool proves nothing.")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python3 email_recon.py <email> [firstname] [lastname]")
sys.exit(1)
args = sys.argv[1:]
asyncio.run(main(args[0], *(args[1:3] if len(args) > 1 else [])))
python3 email_recon.py yourname@gmail.com your firstThe 2026 stack, condensed
Save this. It's the part you'll come back to.
Job Tool Cost Status Domain validation dig Free Always works Account enumeration user-scanner Free / OSS Actively maintained ~~Account enumeration~~ ~~holehe~~ — Unmaintained — false negatives Google account pivot GHunt Free / OSS Works, surface shrinking Hosted Google lookup Epieos Free tier, paid from ~€19–30/mo Fast triage Username pivot Maigret Free / OSS Actively maintained Internal ID extraction socid-extractor Free / OSS Engine behind Maigret Infostealer logs Hudson Rock Free API The 2026 essential Breach data HIBP API from ~$4.39/mo Stealer logs = Pro tier only Broad leak/paste search IntelX Free tier Deep archive LinkedIn resolution SignalHire 5 free credits/mo Cross-check results Reverse image Yandex > Google Lens > TinEye Free Yandex wins on faces
The free stack alone — user-scanner, Hudson Rock, GHunt, Maigret, dorking — covers the overwhelming majority of real investigative work. Paid tools buy speed and convenience, not fundamentally different intelligence.
The whole series in seven lines
- A negative result is not a finding unless you've verified the tool is current.
- holehe is done. Switch to user-scanner.
- Stealer logs beat breach data for depth, and Hudson Rock's tier is free.
- Convert unstable identifiers into stable ones and anchor on those.
- A handle match is correlation, not identity. Two independent signals, minimum.
- Your investigation has a footprint. Sock puppets, separate profiles, know your loud modules.
- Technique doesn't distinguish an investigator from a stalker. Authorisation and purpose do.
Run it on yourself
Copy the script. Point it at your own address.
python3 email_recon.py YOUR_EMAIL@gmail.compython3 email_recon.py YOUR_EMAIL@gmail.comThen read the JSON it produces as if you were investigating a stranger. That's the exercise — not "did it work," but "what would someone do with this?"
Tell me in the responses what surprised you most in your own report. The forgotten accounts are usually the funny part. The stealer log hits are usually the part that changes someone's week.
That's the series. If it saved you time — or stopped you recommending a dead tool to someone — a clap helps it reach the people still running holehe and calling it done. Highlight the parts you'll come back to; I use highlights to decide what to write next.
Missed the earlier parts? Part 1 covers why email enumeration works at the protocol level and the tool that's been silently broken for years. Part 2 covers Google's Gaia ID and the infostealer log ecosystem.
Everything here is for authorised security research, defensive footprint assessment, and education. Run it on yourself. Run it on assets you're permitted to test.