Skip to content

Commit 6a6bfed

Browse files
authored
Merge pull request #233 from joe733/workshop
maint: improves `email` module
2 parents a5071c6 + 0321a40 commit 6a6bfed

File tree

2 files changed

+86
-91
lines changed

2 files changed

+86
-91
lines changed

tests/test_email.py

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,54 @@
1+
"""Test eMail."""
12
# -*- coding: utf-8 -*-
3+
4+
# standard
25
import pytest
36

7+
# local
48
from validators import email, ValidationFailure
59

610

7-
@pytest.mark.parametrize(('value', 'whitelist'), [
8-
('email@here.com', None),
9-
('weirder-email@here.and.there.com', None),
10-
('email@[127.0.0.1]', None),
11-
('example@valid-----hyphens.com', None),
12-
('example@valid-with-hyphens.com', None),
13-
('test@domain.with.idn.tld.उदाहरण.परीक्षा', None),
14-
('email@localhost', None),
15-
('email@localdomain', ['localdomain']),
16-
('"test@test"@example.com', None),
17-
('"\\\011"@here.com', None),
18-
])
19-
def test_returns_true_on_valid_email(value, whitelist):
20-
assert email(value, whitelist=whitelist)
11+
@pytest.mark.parametrize(
12+
("value",),
13+
[
14+
("email@here.com",),
15+
("weirder-email@here.and.there.com",),
16+
("email@127.local.home.arpa",),
17+
("example@valid-----hyphens.com",),
18+
("example@valid-with-hyphens.com",),
19+
("test@domain.with.idn.tld.उदाहरण.परीक्षा",),
20+
("email@localhost.in",),
21+
("email@localdomain.org",),
22+
('"\\\011"@here.com',),
23+
],
24+
)
25+
def test_returns_true_on_valid_email(value: str):
26+
"""Test returns true on valid email."""
27+
assert email(value)
2128

2229

23-
@pytest.mark.parametrize(('value',), [
24-
(None,),
25-
('',),
26-
('abc',),
27-
('abc@',),
28-
('abc@bar',),
29-
('a @x.cz',),
30-
('abc@.com',),
31-
('something@@somewhere.com',),
32-
('email@127.0.0.1',),
33-
('example@invalid-.com',),
34-
('example@-invalid.com',),
35-
('example@inv-.alid-.com',),
36-
('example@inv-.-alid.com',),
37-
(
38-
'john56789.john56789.john56789.john56789.john56789.john56789.john5'
39-
'@example.com',
40-
),
41-
# Quoted-string format (CR not allowed)
42-
('"\\\012"@here.com',),
43-
])
44-
def test_returns_failed_validation_on_invalid_email(value):
30+
@pytest.mark.parametrize(
31+
("value",),
32+
[
33+
(None,),
34+
("",),
35+
("abc",),
36+
("abc@",),
37+
("abc@bar",),
38+
("a @x.cz",),
39+
("abc@.com",),
40+
("something@@somewhere.com",),
41+
("email@127.0.0.1",),
42+
("example@invalid-.com",),
43+
("example@-invalid.com",),
44+
("example@inv-.alid-.com",),
45+
("example@inv-.-alid.com",),
46+
("john56789.john56789.john56789.john56789.john56789.john56789.john5@example.com",),
47+
('"test@test"@example.com',),
48+
# Quoted-string format (CR not allowed)
49+
('"\\\012"@here.com',),
50+
],
51+
)
52+
def test_returns_failed_validation_on_invalid_email(value: str):
53+
"""Test returns failed validation on invalid email."""
4554
assert isinstance(email(value), ValidationFailure)

validators/email.py

Lines changed: 41 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,61 @@
1+
"""eMail."""
2+
# -*- coding: utf-8 -*-
3+
4+
# standard
15
import re
26

7+
# local
38
from .utils import validator
4-
5-
user_regex = re.compile(
6-
# dot-atom
7-
r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+"
8-
r"(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*$"
9-
# quoted-string
10-
r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|'
11-
r"""\\[\001-\011\013\014\016-\177])*"$)""",
12-
re.IGNORECASE
13-
)
14-
domain_regex = re.compile(
15-
# domain
16-
r'(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+'
17-
r'(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?$)'
18-
# literal form, ipv4 address (SMTP 4.1.3)
19-
r'|^\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)'
20-
r'(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$',
21-
re.IGNORECASE)
22-
domain_whitelist = ['localhost']
9+
from .domain import domain
2310

2411

2512
@validator
26-
def email(value, whitelist=None):
27-
"""
28-
Validate an email address.
13+
def email(value: str, /):
14+
"""Validate an email address.
2915
30-
This validator is based on `Django's email validator`_. Returns
31-
``True`` on success and :class:`~validators.utils.ValidationFailure`
32-
when validation fails.
16+
This was inspired from [Django's email validator][1].
17+
Also ref: [RFC 1034][2], [RFC 5321][3] and [RFC 5322][4].
3318
34-
Examples::
19+
[1]: https://github.com/django/django/blob/main/django/core/validators.py#L174
20+
[2]: https://www.rfc-editor.org/rfc/rfc1034
21+
[3]: https://www.rfc-editor.org/rfc/rfc5321
22+
[4]: https://www.rfc-editor.org/rfc/rfc5322
3523
24+
Examples:
3625
>>> email('someone@example.com')
37-
True
38-
26+
# Output: True
3927
>>> email('bogus@@')
40-
ValidationFailure(func=email, ...)
41-
42-
.. _Django's email validator:
43-
https://github.com/django/django/blob/master/django/core/validators.py
28+
# Output: ValidationFailure(email=email, args={'value': 'bogus@@'})
4429
45-
.. versionadded:: 0.1
30+
Args:
31+
value:
32+
eMail string to validate.
4633
47-
:param value: value to validate
48-
:param whitelist: domain names to whitelist
34+
Returns:
35+
(Literal[True]):
36+
If `value` is a valid eMail.
37+
(ValidationFailure):
38+
If `value` is an invalid eMail.
4939
50-
:copyright: (c) Django Software Foundation and individual contributors.
51-
:license: BSD
40+
> *New in version 0.1.0*.
5241
"""
53-
54-
if whitelist is None:
55-
whitelist = domain_whitelist
56-
57-
if not value or '@' not in value:
42+
if not value or value.count("@") != 1:
5843
return False
5944

60-
user_part, domain_part = value.rsplit('@', 1)
61-
62-
if not user_regex.match(user_part):
63-
return False
45+
username_part, domain_part = value.rsplit("@", 1)
6446

65-
if len(user_part.encode("utf-8")) > 64:
47+
if len(username_part) > 64 or len(domain_part) > 253:
48+
# ref: RFC 1034 and 5231
6649
return False
6750

68-
if domain_part not in whitelist and not domain_regex.match(domain_part):
69-
# Try for possible IDN domain-part
70-
try:
71-
domain_part = domain_part.encode('idna').decode('ascii')
72-
return domain_regex.match(domain_part)
73-
except UnicodeError:
74-
return False
75-
return True
51+
return (
52+
bool(domain(domain_part))
53+
if re.compile(
54+
# dot-atom
55+
r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*$"
56+
# quoted-string
57+
+ r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"$)',
58+
re.IGNORECASE,
59+
).match(username_part)
60+
else False
61+
)

0 commit comments

Comments
 (0)