Skip to content

Commit 0321a40

Browse files
committed
maint: improves email module
- Uses type hints, improve relevant docs - `email` now has coupling with `domain` module - moves `whitelist` parameter for future enhancements - Regards [RFC 1034](https://www.rfc-editor.org/rfc/rfc1034), [RFC 5321](https://www.rfc-editor.org/rfc/rfc5321) and [RFC 5322](https://www.rfc-editor.org/rfc/rfc5322) - Updates corresponding tests **Related Items** *Issues* - Closes #22 - Closes #64 - Closes #115 - Closes #153 - Closes #197 *PRs* - Closes #134
1 parent a5071c6 commit 0321a40

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 protected]', None),
9-
('[email protected]', None),
10-
('email@[127.0.0.1]', None),
11-
('[email protected]', None),
12-
('[email protected]', None),
13-
('[email protected].उदाहरण.परीक्षा', 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+
15+
16+
17+
18+
19+
("[email protected].उदाहरण.परीक्षा",),
20+
21+
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-
31-
('something@@somewhere.com',),
32-
33-
34-
35-
36-
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+
40+
("something@@somewhere.com",),
41+
42+
43+
44+
45+
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('[email protected]')
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)