Skip to content

maint: improves email module #233

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 45 additions & 36 deletions tests/test_email.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,54 @@
"""Test eMail."""
# -*- coding: utf-8 -*-

# standard
import pytest

# local
from validators import email, ValidationFailure


@pytest.mark.parametrize(('value', 'whitelist'), [
('[email protected]', None),
('[email protected]', None),
('email@[127.0.0.1]', None),
('[email protected]', None),
('[email protected]', None),
('[email protected].उदाहरण.परीक्षा', None),
('email@localhost', None),
('email@localdomain', ['localdomain']),
('"test@test"@example.com', None),
('"\\\011"@here.com', None),
])
def test_returns_true_on_valid_email(value, whitelist):
assert email(value, whitelist=whitelist)
@pytest.mark.parametrize(
("value",),
[
("[email protected]",),
("[email protected]",),
("[email protected]",),
("[email protected]",),
("[email protected]",),
("[email protected].उदाहरण.परीक्षा",),
("[email protected]",),
("[email protected]",),
('"\\\011"@here.com',),
],
)
def test_returns_true_on_valid_email(value: str):
"""Test returns true on valid email."""
assert email(value)


@pytest.mark.parametrize(('value',), [
(None,),
('',),
('abc',),
('abc@',),
('abc@bar',),
('a @x.cz',),
('[email protected]',),
('something@@somewhere.com',),
('[email protected]',),
('[email protected]',),
('[email protected]',),
('[email protected]',),
('[email protected]',),
(
'john56789.john56789.john56789.john56789.john56789.john56789.john5'
'@example.com',
),
# Quoted-string format (CR not allowed)
('"\\\012"@here.com',),
])
def test_returns_failed_validation_on_invalid_email(value):
@pytest.mark.parametrize(
("value",),
[
(None,),
("",),
("abc",),
("abc@",),
("abc@bar",),
("a @x.cz",),
("[email protected]",),
("something@@somewhere.com",),
("[email protected]",),
("[email protected]",),
("[email protected]",),
("[email protected]",),
("[email protected]",),
("john56789.john56789.john56789.john56789.john56789.john56789.john5@example.com",),
('"test@test"@example.com',),
# Quoted-string format (CR not allowed)
('"\\\012"@here.com',),
],
)
def test_returns_failed_validation_on_invalid_email(value: str):
"""Test returns failed validation on invalid email."""
assert isinstance(email(value), ValidationFailure)
96 changes: 41 additions & 55 deletions validators/email.py
Original file line number Diff line number Diff line change
@@ -1,75 +1,61 @@
"""eMail."""
# -*- coding: utf-8 -*-

# standard
import re

# local
from .utils import validator

user_regex = re.compile(
# dot-atom
r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+"
r"(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*$"
# quoted-string
r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|'
r"""\\[\001-\011\013\014\016-\177])*"$)""",
re.IGNORECASE
)
domain_regex = re.compile(
# domain
r'(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+'
r'(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?$)'
# literal form, ipv4 address (SMTP 4.1.3)
r'|^\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)'
r'(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$',
re.IGNORECASE)
domain_whitelist = ['localhost']
from .domain import domain


@validator
def email(value, whitelist=None):
"""
Validate an email address.
def email(value: str, /):
"""Validate an email address.

This validator is based on `Django's email validator`_. Returns
``True`` on success and :class:`~validators.utils.ValidationFailure`
when validation fails.
This was inspired from [Django's email validator][1].
Also ref: [RFC 1034][2], [RFC 5321][3] and [RFC 5322][4].

Examples::
[1]: https://github.com/django/django/blob/main/django/core/validators.py#L174
[2]: https://www.rfc-editor.org/rfc/rfc1034
[3]: https://www.rfc-editor.org/rfc/rfc5321
[4]: https://www.rfc-editor.org/rfc/rfc5322

Examples:
>>> email('[email protected]')
True

# Output: True
>>> email('bogus@@')
ValidationFailure(func=email, ...)

.. _Django's email validator:
https://github.com/django/django/blob/master/django/core/validators.py
# Output: ValidationFailure(email=email, args={'value': 'bogus@@'})

.. versionadded:: 0.1
Args:
value:
eMail string to validate.

:param value: value to validate
:param whitelist: domain names to whitelist
Returns:
(Literal[True]):
If `value` is a valid eMail.
(ValidationFailure):
If `value` is an invalid eMail.

:copyright: (c) Django Software Foundation and individual contributors.
:license: BSD
> *New in version 0.1.0*.
"""

if whitelist is None:
whitelist = domain_whitelist

if not value or '@' not in value:
if not value or value.count("@") != 1:
return False

user_part, domain_part = value.rsplit('@', 1)

if not user_regex.match(user_part):
return False
username_part, domain_part = value.rsplit("@", 1)

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

if domain_part not in whitelist and not domain_regex.match(domain_part):
# Try for possible IDN domain-part
try:
domain_part = domain_part.encode('idna').decode('ascii')
return domain_regex.match(domain_part)
except UnicodeError:
return False
return True
return (
bool(domain(domain_part))
if re.compile(
# dot-atom
r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*$"
# quoted-string
+ r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"$)',
re.IGNORECASE,
).match(username_part)
else False
)