Spaces:
Sleeping
Sleeping
improve email validation
Browse files- blossomtune_gradio/federation.py +2 -1
- blossomtune_gradio/util.py +35 -0
- pyproject.toml +1 -0
- tests/test_util.py +74 -0
- uv.lock +11 -0
blossomtune_gradio/federation.py
CHANGED
|
@@ -6,6 +6,7 @@ from datetime import datetime
|
|
| 6 |
|
| 7 |
from blossomtune_gradio import config as cfg
|
| 8 |
from blossomtune_gradio import mail
|
|
|
|
| 9 |
from blossomtune_gradio.settings import settings
|
| 10 |
|
| 11 |
|
|
@@ -45,7 +46,7 @@ def check_participant_status(pid_to_check: str, email: str, activation_code: str
|
|
| 45 |
if result is None:
|
| 46 |
if activation_code:
|
| 47 |
return (False, settings.get_text("activation_invalid_md"))
|
| 48 |
-
if not email
|
| 49 |
return (False, settings.get_text("invalid_email_md"))
|
| 50 |
|
| 51 |
with sqlite3.connect(cfg.DB_PATH) as conn:
|
|
|
|
| 6 |
|
| 7 |
from blossomtune_gradio import config as cfg
|
| 8 |
from blossomtune_gradio import mail
|
| 9 |
+
from blossomtune_gradio import util
|
| 10 |
from blossomtune_gradio.settings import settings
|
| 11 |
|
| 12 |
|
|
|
|
| 46 |
if result is None:
|
| 47 |
if activation_code:
|
| 48 |
return (False, settings.get_text("activation_invalid_md"))
|
| 49 |
+
if not util.validate_email(email):
|
| 50 |
return (False, settings.get_text("invalid_email_md"))
|
| 51 |
|
| 52 |
with sqlite3.connect(cfg.DB_PATH) as conn:
|
blossomtune_gradio/util.py
CHANGED
|
@@ -1,4 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
def strtobool(value: str) -> bool:
|
|
|
|
|
|
|
| 2 |
value = value.lower()
|
| 3 |
if value in ("y", "yes", "on", "1", "true", "t"):
|
| 4 |
return True
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
import dns.resolver
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def validate_email(email_address: str) -> bool:
|
| 6 |
+
"""
|
| 7 |
+
Validates an email address using regex for format and
|
| 8 |
+
DNS for domain's MX record.
|
| 9 |
+
|
| 10 |
+
Args:
|
| 11 |
+
email_address (str): The email address to validate.
|
| 12 |
+
|
| 13 |
+
Returns:
|
| 14 |
+
bool: True if the email is syntactically valid and
|
| 15 |
+
the domain has an MX record, False otherwise.
|
| 16 |
+
"""
|
| 17 |
+
# Regex validation for basic format
|
| 18 |
+
regex = r"[^@]+@[^@]+\.[^@]+"
|
| 19 |
+
if not re.match(regex, email_address):
|
| 20 |
+
return False
|
| 21 |
+
|
| 22 |
+
# DNS MX record validation
|
| 23 |
+
try:
|
| 24 |
+
domain = email_address.rsplit("@", 1)[-1]
|
| 25 |
+
dns.resolver.query(domain, "MX")
|
| 26 |
+
return True
|
| 27 |
+
except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN, IndexError):
|
| 28 |
+
return False
|
| 29 |
+
except Exception as e:
|
| 30 |
+
print(f"An unexpected error occurred: {e}")
|
| 31 |
+
return False
|
| 32 |
+
|
| 33 |
+
|
| 34 |
def strtobool(value: str) -> bool:
|
| 35 |
+
if not value:
|
| 36 |
+
return False
|
| 37 |
value = value.lower()
|
| 38 |
if value in ("y", "yes", "on", "1", "true", "t"):
|
| 39 |
return True
|
pyproject.toml
CHANGED
|
@@ -68,6 +68,7 @@ convention = "google" # Accepts: "google", "numpy", or "pep257".
|
|
| 68 |
|
| 69 |
[dependency-groups]
|
| 70 |
dev = [
|
|
|
|
| 71 |
"pytest>=8.4.1",
|
| 72 |
"pytest-mock>=3.15.1",
|
| 73 |
]
|
|
|
|
| 68 |
|
| 69 |
[dependency-groups]
|
| 70 |
dev = [
|
| 71 |
+
"dnspython>=2.8.0",
|
| 72 |
"pytest>=8.4.1",
|
| 73 |
"pytest-mock>=3.15.1",
|
| 74 |
]
|
tests/test_util.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
import dns.resolver
|
| 3 |
+
from unittest.mock import MagicMock
|
| 4 |
+
|
| 5 |
+
from blossomtune_gradio.util import validate_email, strtobool
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def test_validate_email_valid(monkeypatch):
|
| 9 |
+
"""Tests a syntactically valid email with an existing MX record."""
|
| 10 |
+
# Mock the dns.resolver.query to return a successful result.
|
| 11 |
+
mock_query = MagicMock()
|
| 12 |
+
monkeypatch.setattr(dns.resolver, "query", mock_query)
|
| 13 |
+
|
| 14 |
+
email = "test@google.com"
|
| 15 |
+
assert validate_email(email) is True
|
| 16 |
+
# Verify that the query function was called with the correct arguments.
|
| 17 |
+
mock_query.assert_called_once_with("google.com", "MX")
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def test_validate_email_invalid_format():
|
| 21 |
+
"""Tests an email with an invalid regex format."""
|
| 22 |
+
assert validate_email("invalid-email") is False
|
| 23 |
+
assert validate_email("user@.com") is False
|
| 24 |
+
assert validate_email("@domain.com") is False
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def test_validate_email_no_mx_record(monkeypatch):
|
| 28 |
+
"""Tests a domain that exists but has no MX record."""
|
| 29 |
+
# Mock the dns.resolver.query to raise a NoAnswer exception.
|
| 30 |
+
mock_query = MagicMock(side_effect=dns.resolver.NoAnswer)
|
| 31 |
+
monkeypatch.setattr(dns.resolver, "query", mock_query)
|
| 32 |
+
|
| 33 |
+
email = "user@example.com"
|
| 34 |
+
assert validate_email(email) is False
|
| 35 |
+
mock_query.assert_called_once_with("example.com", "MX")
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def test_validate_email_non_existent_domain(monkeypatch):
|
| 39 |
+
"""Tests a domain that does not exist."""
|
| 40 |
+
# Mock the dns.resolver.query to raise an NXDOMAIN exception.
|
| 41 |
+
mock_query = MagicMock(side_effect=dns.resolver.NXDOMAIN)
|
| 42 |
+
monkeypatch.setattr(dns.resolver, "query", mock_query)
|
| 43 |
+
|
| 44 |
+
email = "user@not-a-real-domain-123.com"
|
| 45 |
+
assert validate_email(email) is False
|
| 46 |
+
mock_query.assert_called_once_with("not-a-real-domain-123.com", "MX")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@pytest.mark.parametrize(
|
| 50 |
+
"value, expected",
|
| 51 |
+
[
|
| 52 |
+
("y", True),
|
| 53 |
+
("yes", True),
|
| 54 |
+
("on", True),
|
| 55 |
+
("1", True),
|
| 56 |
+
("true", True),
|
| 57 |
+
("t", True),
|
| 58 |
+
("Y", True),
|
| 59 |
+
("YES", True),
|
| 60 |
+
("On", True),
|
| 61 |
+
("0", False),
|
| 62 |
+
("n", False),
|
| 63 |
+
("off", False),
|
| 64 |
+
("false", False),
|
| 65 |
+
("f", False),
|
| 66 |
+
("no", False),
|
| 67 |
+
("anything else", False),
|
| 68 |
+
("", False),
|
| 69 |
+
(None, False), # Test with None to ensure it doesn't crash
|
| 70 |
+
],
|
| 71 |
+
)
|
| 72 |
+
def test_strtobool(value, expected):
|
| 73 |
+
"""Tests the strtobool function with various inputs."""
|
| 74 |
+
assert strtobool(value) == expected
|
uv.lock
CHANGED
|
@@ -241,6 +241,7 @@ dependencies = [
|
|
| 241 |
|
| 242 |
[package.dev-dependencies]
|
| 243 |
dev = [
|
|
|
|
| 244 |
{ name = "pytest" },
|
| 245 |
{ name = "pytest-mock" },
|
| 246 |
]
|
|
@@ -261,6 +262,7 @@ requires-dist = [
|
|
| 261 |
|
| 262 |
[package.metadata.requires-dev]
|
| 263 |
dev = [
|
|
|
|
| 264 |
{ name = "pytest", specifier = ">=8.4.1" },
|
| 265 |
{ name = "pytest-mock", specifier = ">=3.15.1" },
|
| 266 |
]
|
|
@@ -637,6 +639,15 @@ wheels = [
|
|
| 637 |
{ url = "https://files.pythonhosted.org/packages/c9/7a/cef76fd8438a42f96db64ddaa85280485a9c395e7df3db8158cfec1eee34/dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7", size = 116252, upload-time = "2024-01-27T23:42:14.239Z" },
|
| 638 |
]
|
| 639 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 640 |
[[package]]
|
| 641 |
name = "evaluate"
|
| 642 |
version = "0.4.5"
|
|
|
|
| 241 |
|
| 242 |
[package.dev-dependencies]
|
| 243 |
dev = [
|
| 244 |
+
{ name = "dnspython" },
|
| 245 |
{ name = "pytest" },
|
| 246 |
{ name = "pytest-mock" },
|
| 247 |
]
|
|
|
|
| 262 |
|
| 263 |
[package.metadata.requires-dev]
|
| 264 |
dev = [
|
| 265 |
+
{ name = "dnspython", specifier = ">=2.8.0" },
|
| 266 |
{ name = "pytest", specifier = ">=8.4.1" },
|
| 267 |
{ name = "pytest-mock", specifier = ">=3.15.1" },
|
| 268 |
]
|
|
|
|
| 639 |
{ url = "https://files.pythonhosted.org/packages/c9/7a/cef76fd8438a42f96db64ddaa85280485a9c395e7df3db8158cfec1eee34/dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7", size = 116252, upload-time = "2024-01-27T23:42:14.239Z" },
|
| 640 |
]
|
| 641 |
|
| 642 |
+
[[package]]
|
| 643 |
+
name = "dnspython"
|
| 644 |
+
version = "2.8.0"
|
| 645 |
+
source = { registry = "https://pypi.org/simple" }
|
| 646 |
+
sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" }
|
| 647 |
+
wheels = [
|
| 648 |
+
{ url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
|
| 649 |
+
]
|
| 650 |
+
|
| 651 |
[[package]]
|
| 652 |
name = "evaluate"
|
| 653 |
version = "0.4.5"
|