From 0321a400e0674198b2e4fdbe2dd0a25d254677c3 Mon Sep 17 00:00:00 2001 From: Jovial Joe Jayarson Date: Fri, 3 Mar 2023 11:41:44 +0530 Subject: [PATCH] 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 --- tests/test_email.py | 81 +++++++++++++++++++++----------------- validators/email.py | 96 +++++++++++++++++++-------------------------- 2 files changed, 86 insertions(+), 91 deletions(-) diff --git a/tests/test_email.py b/tests/test_email.py index 0b7f4e27..6d980bc6 100644 --- a/tests/test_email.py +++ b/tests/test_email.py @@ -1,45 +1,54 @@ +"""Test eMail.""" # -*- coding: utf-8 -*- + +# standard import pytest +# local from validators import email, ValidationFailure -@pytest.mark.parametrize(('value', 'whitelist'), [ - ('email@here.com', None), - ('weirder-email@here.and.there.com', None), - ('email@[127.0.0.1]', None), - ('example@valid-----hyphens.com', None), - ('example@valid-with-hyphens.com', None), - ('test@domain.with.idn.tld.उदाहरण.परीक्षा', 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@here.com",), + ("weirder-email@here.and.there.com",), + ("email@127.local.home.arpa",), + ("example@valid-----hyphens.com",), + ("example@valid-with-hyphens.com",), + ("test@domain.with.idn.tld.उदाहरण.परीक्षा",), + ("email@localhost.in",), + ("email@localdomain.org",), + ('"\\\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',), - ('abc@.com',), - ('something@@somewhere.com',), - ('email@127.0.0.1',), - ('example@invalid-.com',), - ('example@-invalid.com',), - ('example@inv-.alid-.com',), - ('example@inv-.-alid.com',), - ( - '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",), + ("abc@.com",), + ("something@@somewhere.com",), + ("email@127.0.0.1",), + ("example@invalid-.com",), + ("example@-invalid.com",), + ("example@inv-.alid-.com",), + ("example@inv-.-alid.com",), + ("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) diff --git a/validators/email.py b/validators/email.py index 229c8e46..47f25c75 100644 --- a/validators/email.py +++ b/validators/email.py @@ -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('someone@example.com') - 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 + )