31 lines
1.0 KiB
Python
31 lines
1.0 KiB
Python
|
|
"""Lightweight email validator that tolerates internal/lab TLDs (.local, .corp, …).
|
||
|
|
|
||
|
|
`pydantic.EmailStr` relies on `email-validator` with `globally_deliverable=True`,
|
||
|
|
which rejects RFC 6761 special-use domains. Red-team and corporate intranet
|
||
|
|
deployments routinely use such suffixes — we accept any RFC-shape email and
|
||
|
|
defer deliverability checks to the operator.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import re
|
||
|
|
from typing import Annotated
|
||
|
|
|
||
|
|
from pydantic import AfterValidator
|
||
|
|
|
||
|
|
# Permissive RFC-shape pattern: local-part 1..64 chars, domain has at least one
|
||
|
|
# dot, each label is 1..63 chars of letters/digits/hyphens, total ≤ 254.
|
||
|
|
_EMAIL_RE = re.compile(
|
||
|
|
r"^(?=.{1,254}$)[A-Za-z0-9._%+\-]{1,64}@[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?)+$"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _validate_email(value: str) -> str:
|
||
|
|
v = value.strip()
|
||
|
|
if not _EMAIL_RE.match(v):
|
||
|
|
raise ValueError("not a valid email address")
|
||
|
|
return v.lower()
|
||
|
|
|
||
|
|
|
||
|
|
Email = Annotated[str, AfterValidator(_validate_email)]
|