mrs83 commited on
Commit
4a4c0cf
·
1 Parent(s): e8143a3

improve email validation

Browse files
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 or "@" not in 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"