diff --git a/.gitignore b/.gitignore index 98f6c47..e03d964 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.pyc +validate_email/lib build dist MANIFEST diff --git a/CHANGELOG.txt b/CHANGELOG.txt index eff1110..c2a6ec9 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,6 @@ +0.1.12: +- Blacklist/whitelist domains checking is now independent of regex checking. + 0.1.11: - Handling IDNA errors diff --git a/requirements.txt b/requirements.txt index 2f9b1a8..de23800 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,2 @@ dnspython==1.16.0 -entrypoints==0.3 -flake8==3.7.7 idna==2.8 -isort==4.3.21 -mccabe==0.6.1 -pycodestyle==2.5.0 -pyflakes==2.1.1 diff --git a/setup.py b/setup.py index 20f4ba7..5b1d097 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from urllib.request import urlopen from setuptools import find_packages, setup from setuptools.command.build_py import build_py -blacklist_url = ( +BLACKLIST_URL = ( 'https://raw.githubusercontent.com/martenson/disposable-email-domains/' 'master/disposable_email_blocklist.conf') @@ -15,10 +15,10 @@ class PostBuildPyCommand(build_py): def run(self): if self.dry_run: return super().run() - with urlopen(url=blacklist_url) as fd: + with urlopen(url=BLACKLIST_URL) as fd: content = fd.read().decode('utf-8') target_dir = join(self.build_lib, 'validate_email/lib') - self.mkpath(target_dir) + self.mkpath(name=target_dir) with open(join(target_dir, 'blacklist.txt'), 'w') as fd: fd.write(content) super().run() @@ -26,7 +26,7 @@ class PostBuildPyCommand(build_py): setup( name='py3-validate-email', - version='0.1.11', + version='0.1.12', packages=find_packages(exclude=['tests']), install_requires=['dnspython>=1.16.0', 'idna>=2.8'], author='László Károlyi', diff --git a/tests/test_blacklist_check.py b/tests/test_blacklist_check.py new file mode 100644 index 0000000..b6cbc7d --- /dev/null +++ b/tests/test_blacklist_check.py @@ -0,0 +1,59 @@ +from os import makedirs +from os.path import dirname, join +from unittest.case import TestCase +from urllib.request import urlopen + +from validate_email import validate_email +from validate_email.domainlist_check import domainlist_check + +BLACKLIST_URL = ( + 'https://raw.githubusercontent.com/martenson/disposable-email-domains/' + 'master/disposable_email_blocklist.conf') + + +class DlBlacklist(object): + 'Emulating downloading of blacklists on post-build command.' + + def __init__(self): + from validate_email import domainlist_check + self.build_lib = dirname(dirname(domainlist_check.__file__)) + + def mkpath(self, name: str): + 'Emulate mkpath.' + makedirs(name=name, exist_ok=True) + + def run(self): + 'Deploy function identical to the one in setup.py.' + with urlopen(url=BLACKLIST_URL) as fd: + content = fd.read().decode('utf-8') + target_dir = join(self.build_lib, 'validate_email/lib') + self.mkpath(name=target_dir) + with open(join(target_dir, 'blacklist.txt'), 'w') as fd: + fd.write(content) + + +class BlacklistCheckTestCase(TestCase): + 'Testing if the included blacklist filtering works.' + + def test_blacklist_positive(self): + 'Disallows blacklist item: mailinator.com.' + dl = DlBlacklist() + dl.run() + self.assertFalse(expr=domainlist_check( + email_address='pa2@mailinator.com')) + self.assertFalse(expr=validate_email( + email_address='pa2@mailinator.com', check_regex=False, + use_blacklist=True)) + self.assertFalse(expr=validate_email( + email_address='pa2@mailinator.com', check_regex=True, + use_blacklist=True)) + + def test_blacklist_negative(self): + 'Allows a domain not in the blacklist.' + self.assertTrue(expr=domainlist_check( + email_address='pa2@some-random-domain-thats-not-blacklisted.com')) + + def test_erroneous_email(self): + 'Will reject emails in erroneous format.' + self.assertFalse(expr=domainlist_check( + email_address='pa2-mailinator.com')) diff --git a/validate_email/domainlist_check.py b/validate_email/domainlist_check.py new file mode 100644 index 0000000..1c5cc4e --- /dev/null +++ b/validate_email/domainlist_check.py @@ -0,0 +1,46 @@ +from os.path import dirname, join +from typing import Optional + +SetOrNone = Optional[set] + + +class DomainListValidator(object): + 'Check the provided email against domain lists.' + domain_whitelist = frozenset() + domain_blacklist = frozenset() + + def __init__( + self, whitelist: SetOrNone = None, blacklist: SetOrNone = None): + if whitelist: + self.domain_whitelist = set(x.lower() for x in whitelist) + if blacklist: + self.domain_blacklist = set(x.lower() for x in blacklist) + else: + self._load_builtin_blacklist() + + def _load_builtin_blacklist(self): + 'Load our built-in blacklist.' + path = join(dirname(__file__), 'lib', 'blacklist.txt') + try: + with open(path) as fd: + lines = fd.readlines() + except FileNotFoundError: + return + self.domain_blacklist = \ + set(x.strip().lower() for x in lines if x.strip()) + + def __call__(self, email_address: str) -> bool: + 'Do the checking here.' + if not email_address or '@' not in email_address: + return False + + user_part, domain_part = email_address.rsplit('@', 1) + + if domain_part in self.domain_whitelist: + return True + if domain_part in self.domain_blacklist: + return False + return True + + +domainlist_check = DomainListValidator() diff --git a/validate_email/regex_check.py b/validate_email/regex_check.py index 24ffee4..bbdb2ed 100644 --- a/validate_email/regex_check.py +++ b/validate_email/regex_check.py @@ -1,5 +1,4 @@ from ipaddress import IPv4Address, IPv6Address -from os.path import dirname, join from typing import Optional from .constants import HOST_REGEX, LITERAL_REGEX, USER_REGEX @@ -32,44 +31,18 @@ def _validate_ipv46_address(value: str) -> bool: return _validate_ipv6_address(value) -class EmailValidator(object): +class RegexValidator(object): 'Slightly adjusted email regex checker from the Django project.' - domain_whitelist = frozenset('localhost') - domain_blacklist = frozenset() - def __init__( - self, whitelist: SetOrNone = None, blacklist: SetOrNone = None): - self.domain_whitelist = set(whitelist) \ - if whitelist else self.domain_whitelist - self._load_blacklist(blacklist=blacklist) - - def _load_blacklist(self, blacklist: SetOrNone = None): - 'Load our blacklist.' - self.domain_blacklist = set(blacklist) \ - if blacklist else self.domain_blacklist - path = join(dirname(__file__), 'lib', 'blacklist.txt') - try: - with open(path) as fd: - lines = fd.readlines() - except FileNotFoundError: - return - self.domain_blacklist = self.domain_blacklist.union( - x.strip() for x in lines) - - def __call__(self, value: str, use_blacklist: bool = True) -> bool: - if not value or '@' not in value: + def __call__(self, email_address: str, use_blacklist: bool = True) -> bool: + if not email_address or '@' not in email_address: return False - user_part, domain_part = value.rsplit('@', 1) + user_part, domain_part = email_address.rsplit('@', 1) if not USER_REGEX.match(user_part): return False - if domain_part in self.domain_whitelist: - return True - if domain_part in self.domain_blacklist: - return False - if not self.validate_domain_part(domain_part): # Try for possible IDN domain-part try: @@ -93,4 +66,4 @@ class EmailValidator(object): return False -regex_check = EmailValidator() +regex_check = RegexValidator() diff --git a/validate_email/validate_email.py b/validate_email/validate_email.py index f6d65ab..0743aba 100644 --- a/validate_email/validate_email.py +++ b/validate_email/validate_email.py @@ -1,5 +1,6 @@ from typing import Optional +from .domainlist_check import domainlist_check from .mx_check import mx_check from .regex_check import regex_check @@ -15,8 +16,9 @@ def validate_email( Return `None` if the result is ambigious. """ - if check_regex and not regex_check( - value=email_address, use_blacklist=use_blacklist): + if check_regex and not regex_check(email_address=email_address): + return False + if use_blacklist and not domainlist_check(email_address=email_address): return False if not check_mx: return True