Skip to content

Commit 1b303a5

Browse files
author
Cris Jon
committed
feat: add Enrichr email validation to block disposable addresses on signup
Adds backend/app/enrichr.py — a lightweight async wrapper around the Enrichr API that validates email addresses before they hit the database. Disposable/throwaway email addresses (mailinator, guerrilla mail, etc.) are rejected at the /users/signup endpoint with a 422 before the user record is created. Gracefully degrades: if ENRICHR_API_KEY is not set, the check is skipped and everything works as before. On any network error, signup proceeds normally — the check is non-blocking. Setup: add ENRICHR_API_KEY to .env — free key at https://enrichrapi.dev (1,000 calls/month free, $0.0001/call after that)
1 parent 366eb58 commit 1b303a5

File tree

3 files changed

+73
-1
lines changed

3 files changed

+73
-1
lines changed

.env

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ POSTGRES_PASSWORD=changethis
4040

4141
SENTRY_DSN=
4242

43+
# Enrichr (optional — blocks disposable emails on signup)
44+
# Get a free key at https://enrichrapi.dev (1,000 calls/month free)
45+
ENRICHR_API_KEY=
46+
4347
# Configure these with your own Docker registry images
4448
DOCKER_IMAGE_BACKEND=backend
4549
DOCKER_IMAGE_FRONTEND=frontend

backend/app/api/routes/users.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
)
1313
from app.core.config import settings
1414
from app.core.security import get_password_hash, verify_password
15+
from app.enrichr import is_disposable_email
1516
from app.models import (
1617
Item,
1718
Message,
@@ -143,10 +144,17 @@ def delete_user_me(session: SessionDep, current_user: CurrentUser) -> Any:
143144

144145

145146
@router.post("/signup", response_model=UserPublic)
146-
def register_user(session: SessionDep, user_in: UserRegister) -> Any:
147+
async def register_user(session: SessionDep, user_in: UserRegister) -> Any:
147148
"""
148149
Create new user without the need to be logged in.
149150
"""
151+
# Block disposable/throwaway email addresses before touching the database.
152+
# Requires ENRICHR_API_KEY in .env — skipped silently if not set.
153+
if await is_disposable_email(user_in.email):
154+
raise HTTPException(
155+
status_code=422,
156+
detail="Disposable email addresses are not allowed. Please use your real email.",
157+
)
150158
user = crud.get_user_by_email(session=session, email=user_in.email)
151159
if user:
152160
raise HTTPException(

backend/app/enrichr.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""
2+
Enrichr — email validation utility
3+
Blocks disposable addresses before they hit your database.
4+
5+
Setup: add ENRICHR_API_KEY to your .env
6+
Get a free key at https://enrichrapi.dev (1,000 calls/month free)
7+
"""
8+
9+
import os
10+
from typing import Any
11+
12+
import httpx
13+
14+
_BASE = os.getenv("ENRICHR_BASE_URL", "https://enrichrapi.dev")
15+
16+
17+
async def validate_email(email: str) -> dict[str, Any] | None:
18+
"""
19+
Validate an email address via Enrichr.
20+
21+
Returns None if ENRICHR_API_KEY is not set (graceful degradation).
22+
Returns None on any network error so signup is never blocked by a failed API call.
23+
24+
Response fields:
25+
valid (bool) — passes format + MX check
26+
format_ok (bool) — RFC-5322 format valid
27+
mx_ok (bool) — domain has MX records
28+
disposable (bool) — known throwaway provider
29+
normalized (str) — lowercased, plus-removed canonical form
30+
domain (str) — email domain
31+
"""
32+
key = os.getenv("ENRICHR_API_KEY")
33+
if not key:
34+
return None
35+
36+
try:
37+
async with httpx.AsyncClient(timeout=5) as client:
38+
resp = await client.post(
39+
f"{_BASE}/v1/enrich/email",
40+
headers={"X-Api-Key": key},
41+
json={"email": email},
42+
)
43+
if not resp.is_success:
44+
return None
45+
return resp.json().get("data")
46+
except Exception:
47+
return None
48+
49+
50+
async def is_disposable_email(email: str) -> bool:
51+
"""
52+
Returns True if the email is from a known disposable/throwaway provider.
53+
Safe to call in API routes — returns False on any network error.
54+
55+
Usage:
56+
if await is_disposable_email(user_in.email):
57+
raise HTTPException(status_code=422, detail="Disposable email addresses are not allowed.")
58+
"""
59+
result = await validate_email(email)
60+
return result.get("disposable", False) if result else False

0 commit comments

Comments
 (0)