Files
unredact/scripts/build_emails.py
T
Alex-GilbertandClaude Opus 4.6 0d88d6c0e7 feat: add build script to fetch and merge Epstein-related email addresses
Fetches from 3 data sources (HuggingFace parquet, rhowardstone entities,
Epstein Exposed API) plus 8 court-verified addresses. Merges, deduplicates,
validates, and writes 644 unique emails to unredact/data/emails.txt.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-02-16 13:29:18 -06:00

221 lines
7.5 KiB
Python

"""Build the emails.txt word list from multiple Epstein-related data sources.
Usage:
python scripts/build_emails.py [--data-dir PATH]
Downloads and merges email addresses from:
1. HuggingFace dataset (notesbymuneeb/epstein-emails) — parquet
2. Epstein Exposed API (paginated persons endpoint)
3. rhowardstone extracted_entities_filtered.json
4. Hardcoded court-verified addresses
Outputs deduplicated, sorted, one-per-line to unredact/data/emails.txt.
"""
import argparse
import json
import re
import time
import urllib.request
from pathlib import Path
OUTPUT_PATH = Path(__file__).parent.parent / "unredact" / "data" / "emails.txt"
EMAIL_RE = re.compile(r"[\w.+-]+@[\w.-]+\.\w{2,}")
COURT_VERIFIED = [
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
]
# ── Source 1: HuggingFace parquet ──────────────────────────────────────────
HUGGINGFACE_URL = (
"https://huggingface.co/datasets/notesbymuneeb/epstein-emails"
"/resolve/main/epstein_email_threads.parquet"
)
def fetch_huggingface(data_dir: Path) -> set[str]:
"""Download the HuggingFace parquet file and extract email addresses."""
import pandas as pd
parquet_path = data_dir / "epstein-emails.parquet"
if not parquet_path.exists():
print(f"Downloading {HUGGINGFACE_URL}...")
urllib.request.urlretrieve(HUGGINGFACE_URL, parquet_path)
print(f" -> saved to {parquet_path}")
df = pd.read_parquet(parquet_path)
emails: set[str] = set()
# Scan sender, recipients, and messages columns for email addresses
for col in ("sender", "recipients", "messages"):
if col not in df.columns:
continue
for value in df[col].dropna():
text = str(value)
emails.update(EMAIL_RE.findall(text))
print(f" HuggingFace: found {len(emails)} unique emails")
return emails
# ── Source 2: Epstein Exposed API ──────────────────────────────────────────
EPSTEIN_EXPOSED_API = "https://epsteinexposed.com/api/v1/persons"
def fetch_epstein_exposed() -> set[str]:
"""Paginate the Epstein Exposed persons API and extract emails."""
emails: set[str] = set()
page = 1
while True:
url = f"{EPSTEIN_EXPOSED_API}?per_page=100&page={page}"
print(f" Epstein Exposed API: fetching page {page}...")
try:
req = urllib.request.Request(url, headers={"User-Agent": "unredact-build/1.0"})
with urllib.request.urlopen(req, timeout=30) as resp:
data = json.loads(resp.read().decode())
except (urllib.error.HTTPError, urllib.error.URLError, json.JSONDecodeError) as exc:
print(f" Epstein Exposed API: stopping at page {page} ({exc})")
break
# Handle both list response and paginated object response
if isinstance(data, list):
persons = data
elif isinstance(data, dict):
persons = data.get("data", data.get("results", data.get("persons", [])))
else:
break
if not persons:
break
for person in persons:
# Scan the entire person record for email addresses
text = json.dumps(person)
emails.update(EMAIL_RE.findall(text))
page += 1
time.sleep(1) # rate limit: 60 req/min
print(f" Epstein Exposed: found {len(emails)} unique emails")
return emails
# ── Source 3: rhowardstone extracted entities ──────────────────────────────
RHOWARDSTONE_URL = (
"https://raw.githubusercontent.com/rhowardstone/Epstein-research-data"
"/main/extracted_entities_filtered.json"
)
def fetch_rhowardstone(data_dir: Path) -> set[str]:
"""Download extracted_entities_filtered.json and extract email entities."""
cache_path = data_dir / "extracted_entities_filtered.json"
if not cache_path.exists():
print(f" Downloading {RHOWARDSTONE_URL}...")
urllib.request.urlretrieve(RHOWARDSTONE_URL, cache_path)
print(f" -> saved to {cache_path}")
data = json.loads(cache_path.read_text())
emails: set[str] = set()
# Collect all entity lists to scan
entity_lists: list[list] = []
if isinstance(data, list):
entity_lists.append(data)
elif isinstance(data, dict):
# Top-level keys like "emails", "names", etc. each hold a list
for key, value in data.items():
if isinstance(value, list):
entity_lists.append(value)
for items in entity_lists:
for item in items:
if not isinstance(item, dict):
continue
entity_type = str(item.get("entity_type", item.get("type", ""))).lower()
if "email" in entity_type:
value = item.get("entity_value", item.get("value", item.get("entity", "")))
found = EMAIL_RE.findall(str(value))
emails.update(found)
print(f" rhowardstone: found {len(emails)} unique emails")
return emails
# ── Source 4: Court-verified ───────────────────────────────────────────────
def get_court_verified() -> set[str]:
"""Return hardcoded court-verified email addresses."""
emails = set(COURT_VERIFIED)
print(f" Court-verified: {len(emails)} emails")
return emails
# ── Merge and validate ─────────────────────────────────────────────────────
def merge_and_validate(all_emails: set[str]) -> list[str]:
"""Lowercase, deduplicate, validate, and sort email addresses."""
valid = set()
for email in all_emails:
email = email.lower().strip()
if EMAIL_RE.fullmatch(email):
valid.add(email)
return sorted(valid)
# ── Main ───────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="Build emails.txt from Epstein data sources")
parser.add_argument("--data-dir", type=Path, default=Path(__file__).parent / ".cache",
help="Directory to cache downloaded data files")
args = parser.parse_args()
args.data_dir.mkdir(parents=True, exist_ok=True)
all_emails: set[str] = set()
# Source 1: HuggingFace
print("Source 1: HuggingFace dataset")
all_emails.update(fetch_huggingface(args.data_dir))
# Source 2: Epstein Exposed API
print("Source 2: Epstein Exposed API")
all_emails.update(fetch_epstein_exposed())
# Source 3: rhowardstone
print("Source 3: rhowardstone extracted entities")
all_emails.update(fetch_rhowardstone(args.data_dir))
# Source 4: Court-verified
print("Source 4: Court-verified addresses")
all_emails.update(get_court_verified())
# Merge, validate, deduplicate
emails = merge_and_validate(all_emails)
print(f"\nTotal: {len(emails)} unique valid emails")
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
OUTPUT_PATH.write_text("\n".join(emails) + "\n")
print(f"Wrote {OUTPUT_PATH}")
if __name__ == "__main__":
main()