Beautifying code, updating AUTHORS+CHANGELOG

This commit is contained in:
László Károlyi 2020-04-10 12:53:10 +02:00
parent 4cb98ee2ba
commit 9b174a591f
Signed by: karolyi
GPG Key ID: 2DCAF25E55735BFE
9 changed files with 87 additions and 86 deletions

View File

@ -1,3 +1,4 @@
- April 2020: Extending with logging and raising errors by @reinhard-mueller
- March 2019: extending and upgrading with blacklists by László Károlyi <laszlo@karolyi.hu - March 2019: extending and upgrading with blacklists by László Károlyi <laszlo@karolyi.hu
- validate_email was extended and updated for use with Python 3 by Ben Baert <ben_b@gmx.com> in May 2018. - validate_email was extended and updated for use with Python 3 by Ben Baert <ben_b@gmx.com> in May 2018.
- validate_email was created by Syrus Akbary <me@syrusakbary.com> in April 2012. - validate_email was created by Syrus Akbary <me@syrusakbary.com> in April 2012.

View File

@ -1,3 +1,9 @@
0.2.1:
- Added a validate_email_or_fail function that will raise an exception
(base class validate_email.exceptions.EmailValidationError) when the
passed email check fails, while logging a warning with the validation
result.
0.2.0: 0.2.0:
- Added automatic auto-updater for updating built-in blacklists. - Added automatic auto-updater for updating built-in blacklists.

View File

@ -19,26 +19,20 @@ class BlacklistCheckTestCase(TestCase):
domainlist_check(user_part='pa2', domain_part='mailinator.com') domainlist_check(user_part='pa2', domain_part='mailinator.com')
with self.assertRaises(DomainBlacklistedError): with self.assertRaises(DomainBlacklistedError):
validate_email_or_fail( validate_email_or_fail(
email_address='pa2@mailinator.com', email_address='pa2@mailinator.com', check_regex=False,
check_regex=False, use_blacklist=True)
use_blacklist=True)
with self.assertRaises(DomainBlacklistedError): with self.assertRaises(DomainBlacklistedError):
validate_email_or_fail( validate_email_or_fail(
email_address='pa2@mailinator.com', email_address='pa2@mailinator.com', check_regex=True,
check_regex=True, use_blacklist=True)
use_blacklist=True)
with self.assertLogs(): with self.assertLogs():
self.assertFalse( self.assertFalse(expr=validate_email(
validate_email( email_address='pa2@mailinator.com', check_regex=False,
email_address='pa2@mailinator.com', use_blacklist=True))
check_regex=False,
use_blacklist=True))
with self.assertLogs(): with self.assertLogs():
self.assertFalse( self.assertFalse(expr=validate_email(
validate_email( email_address='pa2@mailinator.com', check_regex=True,
email_address='pa2@mailinator.com', use_blacklist=True))
check_regex=True,
use_blacklist=True))
def test_blacklist_negative(self): def test_blacklist_negative(self):
'Allows a domain not in the blacklist.' 'Allows a domain not in the blacklist.'

View File

@ -65,16 +65,18 @@ class GetMxRecordsTestCase(TestCase):
'Fails when an MX hostname is "."' 'Fails when an MX hostname is "."'
TEST_QUERY.return_value = [ TEST_QUERY.return_value = [
SimpleNamespace(exchange=DnsNameStub(value='.'))] SimpleNamespace(exchange=DnsNameStub(value='.'))]
with self.assertRaises(NoValidMXError): with self.assertRaises(NoValidMXError) as exc:
_get_mx_records(domain='testdomain1', timeout=10) _get_mx_records(domain='testdomain1', timeout=10)
self.assertTupleEqual(exc.exception.args, ())
@patch.object(target=mx_module, attribute='query', new=TEST_QUERY) @patch.object(target=mx_module, attribute='query', new=TEST_QUERY)
def test_fails_with_null_hostnames(self): def test_fails_with_null_hostnames(self):
'Fails when an MX hostname is invalid.' 'Fails when an MX hostname is invalid.'
TEST_QUERY.return_value = [ TEST_QUERY.return_value = [
SimpleNamespace(exchange=DnsNameStub(value='asdqwe'))] SimpleNamespace(exchange=DnsNameStub(value='asdqwe'))]
with self.assertRaises(NoValidMXError): with self.assertRaises(NoValidMXError) as exc:
_get_mx_records(domain='testdomain2', timeout=10) _get_mx_records(domain='testdomain2', timeout=10)
self.assertTupleEqual(exc.exception.args, ())
@patch.object(target=mx_module, attribute='query', new=TEST_QUERY) @patch.object(target=mx_module, attribute='query', new=TEST_QUERY)
def test_filters_out_invalid_hostnames(self): def test_filters_out_invalid_hostnames(self):
@ -92,12 +94,13 @@ class GetMxRecordsTestCase(TestCase):
def test_raises_exception_on_dns_timeout(self): def test_raises_exception_on_dns_timeout(self):
'Raises exception on DNS timeout.' 'Raises exception on DNS timeout.'
TEST_QUERY.side_effect = Timeout() TEST_QUERY.side_effect = Timeout()
with self.assertRaises(DNSTimeoutError): with self.assertRaises(DNSTimeoutError) as exc:
_get_mx_records(domain='testdomain3', timeout=10) _get_mx_records(domain='testdomain3', timeout=10)
self.assertTupleEqual(exc.exception.args, ())
def test_returns_false_on_idna_failure(self): def test_returns_false_on_idna_failure(self):
'Returns `False` on IDNA failure.' 'Returns `False` on IDNA failure.'
with self.assertRaises(AddressFormatError): with self.assertRaises(AddressFormatError) as exc:
mx_module.mx_check( mx_module.mx_check(
email_address='test@♥web.de', email_address='test@♥web.de', from_address='mail@example.com')
from_address='mail@example.com') self.assertTupleEqual(exc.exception.args, ())

View File

@ -57,7 +57,8 @@ class FormatValidity(TestCase):
for address in INVALID_EXAMPLES: for address in INVALID_EXAMPLES:
user_part, domain_part = address.rsplit('@', 1) user_part, domain_part = address.rsplit('@', 1)
with self.assertRaises( with self.assertRaises(
AddressFormatError, msg=f'Test failed for {address}'): expected_exception=AddressFormatError,
msg=f'Test failed for {address}'):
regex_check(user_part=user_part, domain_part=domain_part), regex_check(user_part=user_part, domain_part=domain_part),
def test_unparseable_email(self): def test_unparseable_email(self):

View File

@ -1,7 +1,8 @@
from typing import Iterable
class EmailValidationError(Exception): class EmailValidationError(Exception):
""" 'Base class for all exceptions indicating validation failure.'
Base class for all exceptions indicating validation failure.
"""
message = 'Unknown error.' message = 'Unknown error.'
def __str__(self): def __str__(self):
@ -9,74 +10,55 @@ class EmailValidationError(Exception):
class AddressFormatError(EmailValidationError): class AddressFormatError(EmailValidationError):
""" 'Raised when the email address has an invalid format.'
Raised when the email address has an invalid format.
"""
message = 'Invalid email address.' message = 'Invalid email address.'
class DomainBlacklistedError(EmailValidationError): class DomainBlacklistedError(EmailValidationError):
""" """
Raised when the domain of the email address is blacklisted on Raised when the domain of the email address is blacklisted on
https://git.com/martenson/disposable-email-domains. https://github.com/martenson/disposable-email-domains.
""" """
message = 'Domain blacklisted.' message = 'Domain blacklisted.'
class DomainNotFoundError(EmailValidationError): class DomainNotFoundError(EmailValidationError):
""" 'Raised when the domain is not found.'
Raised when the domain of the email address is blacklisted on
https://git.com/martenson/disposable-email-domains.
"""
message = 'Domain not found.' message = 'Domain not found.'
class NoNameserverError(EmailValidationError): class NoNameserverError(EmailValidationError):
""" 'Raised when the domain does not resolve by nameservers in time.'
Raised when the domain of the email address is blacklisted on
https://git.com/martenson/disposable-email-domains.
"""
message = 'No nameserver found for domain.' message = 'No nameserver found for domain.'
class DNSTimeoutError(EmailValidationError): class DNSTimeoutError(EmailValidationError):
""" 'Raised when the domain lookup times out.'
Raised when the domain of the email address is blacklisted on
https://git.com/martenson/disposable-email-domains.
"""
message = 'Domain lookup timed out.' message = 'Domain lookup timed out.'
class DNSConfigurationError(EmailValidationError): class DNSConfigurationError(EmailValidationError):
""" """
Raised when the domain of the email address is blacklisted on Raised when the DNS entries for this domain are falsely configured.
https://git.com/martenson/disposable-email-domains.
""" """
message = 'Misconfigurated DNS entries for domain.' message = 'Misconfigurated DNS entries for domain.'
class NoMXError(EmailValidationError): class NoMXError(EmailValidationError):
""" 'Raised then the domain has no MX records configured.'
Raised when the domain of the email address is blacklisted on
https://git.com/martenson/disposable-email-domains.
"""
message = 'No MX record for domain found.' message = 'No MX record for domain found.'
class NoValidMXError(EmailValidationError): class NoValidMXError(EmailValidationError):
"""
Raised when the domain of the email address is blacklisted on
https://git.com/martenson/disposable-email-domains.
"""
message = 'No valid MX record for domain found.' message = 'No valid MX record for domain found.'
class AddressNotDeliverableError(EmailValidationError): class AddressNotDeliverableError(EmailValidationError):
""" 'Raised when a non-ambigious resulted lookup fails.'
Raised when the domain of the email address is blacklisted on message = 'Email address undeliverable:'
https://git.com/martenson/disposable-email-domains.
"""
message = 'Non-deliverable email address:'
def __init__(self, error_messages): def __init__(self, error_messages: Iterable):
self.message = '\n'.join([self.message] + error_messages) self.error_messages = error_messages
def __str__(self) -> str:
return '\n'.join([self.message] + self.error_messages)

View File

@ -61,11 +61,40 @@ def _get_mx_records(domain: str, timeout: int) -> list:
dns_str = record.exchange.to_text() # type: str dns_str = record.exchange.to_text() # type: str
to_check[dns_str] = dns_str[:-1] if dns_str.endswith('.') else dns_str to_check[dns_str] = dns_str[:-1] if dns_str.endswith('.') else dns_str
result = [k for k, v in to_check.items() if HOST_REGEX.search(string=v)] result = [k for k, v in to_check.items() if HOST_REGEX.search(string=v)]
if not len(result): if not result:
raise NoValidMXError raise NoValidMXError
return result return result
def _check_one_mx(
smtp: SMTP, error_messages: list, mx_record: str, helo_host: str,
from_address: str, email_address: str) -> bool:
"""
Check one MX server, return the `is_ambigious` boolean or raise
`StopIteration` if this MX accepts the email.
"""
try:
smtp.connect(host=mx_record)
smtp.helo(name=helo_host)
smtp.mail(sender=from_address)
code, message = smtp.rcpt(recip=email_address)
smtp.quit()
except SMTPServerDisconnected:
return True
except SocketError as error:
error_messages.append(f'{mx_record}: {error}')
return False
if code == 250:
raise StopIteration
elif 400 <= code <= 499:
# Ambigious return code, can be graylist, temporary problems,
# quota or mailsystem error
return True
message = message.decode(errors='ignore')
error_messages.append(f'{mx_record}: {code} {message}')
return False
def _check_mx_records( def _check_mx_records(
mx_records: list, smtp_timeout: int, helo_host: str, from_address: str, mx_records: list, smtp_timeout: int, helo_host: str, from_address: str,
email_address: str email_address: str
@ -77,32 +106,15 @@ def _check_mx_records(
found_ambigious = False found_ambigious = False
for mx_record in mx_records: for mx_record in mx_records:
try: try:
smtp.connect(host=mx_record) found_ambigious |= _check_one_mx(
smtp.helo(name=helo_host) smtp=smtp, error_messages=error_messages, mx_record=mx_record,
smtp.mail(sender=from_address) helo_host=helo_host, from_address=from_address,
code, message = smtp.rcpt(recip=email_address) email_address=email_address)
smtp.quit() except StopIteration:
except SMTPServerDisconnected:
found_ambigious = True
continue
except SocketError as error:
error_messages.append(f'{mx_record}: {error}')
continue
if code == 250:
return True return True
elif 400 <= code <= 499:
# Ambigious return code, can be graylist, temporary
# problems, quota or mailsystem error
found_ambigious = True
else:
message = message.decode(errors='ignore')
error_messages.append(f'{mx_record}: {code} {message}')
# If any of the mx servers behaved ambigious, return None, otherwise raise # If any of the mx servers behaved ambigious, return None, otherwise raise
# an exceptin containing the collected error messages. # an exception containing the collected error messages.
if found_ambigious: if not found_ambigious:
return None
else:
raise AddressNotDeliverableError(error_messages) raise AddressNotDeliverableError(error_messages)

View File

@ -53,7 +53,7 @@ class RegexValidator(object):
raise AddressFormatError raise AddressFormatError
return True return True
def validate_domain_part(self, domain_part): def validate_domain_part(self, domain_part: str):
if HOST_REGEX.match(domain_part): if HOST_REGEX.match(domain_part):
return True return True

View File

@ -6,6 +6,8 @@ from .exceptions import AddressFormatError, EmailValidationError
from .mx_check import mx_check from .mx_check import mx_check
from .regex_check import regex_check from .regex_check import regex_check
logger = getLogger(name='validate_email')
def validate_email_or_fail( def validate_email_or_fail(
email_address: str, check_regex: bool = True, check_mx: bool = True, email_address: str, check_regex: bool = True, check_mx: bool = True,
@ -43,5 +45,5 @@ def validate_email(email_address: str, *args, **kwargs):
return validate_email_or_fail(email_address, *args, **kwargs) return validate_email_or_fail(email_address, *args, **kwargs)
except EmailValidationError as error: except EmailValidationError as error:
message = f'Validation for {email_address!r} failed: {error}' message = f'Validation for {email_address!r} failed: {error}'
getLogger('validate_email').info(message) logger.warning(msg=message)
return False return False