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
- 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.

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:
- Added automatic auto-updater for updating built-in blacklists.

View File

@ -19,25 +19,19 @@ class BlacklistCheckTestCase(TestCase):
domainlist_check(user_part='pa2', domain_part='mailinator.com')
with self.assertRaises(DomainBlacklistedError):
validate_email_or_fail(
email_address='pa2@mailinator.com',
check_regex=False,
email_address='pa2@mailinator.com', check_regex=False,
use_blacklist=True)
with self.assertRaises(DomainBlacklistedError):
validate_email_or_fail(
email_address='pa2@mailinator.com',
check_regex=True,
email_address='pa2@mailinator.com', check_regex=True,
use_blacklist=True)
with self.assertLogs():
self.assertFalse(
validate_email(
email_address='pa2@mailinator.com',
check_regex=False,
self.assertFalse(expr=validate_email(
email_address='pa2@mailinator.com', check_regex=False,
use_blacklist=True))
with self.assertLogs():
self.assertFalse(
validate_email(
email_address='pa2@mailinator.com',
check_regex=True,
self.assertFalse(expr=validate_email(
email_address='pa2@mailinator.com', check_regex=True,
use_blacklist=True))
def test_blacklist_negative(self):

View File

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

View File

@ -57,7 +57,8 @@ class FormatValidity(TestCase):
for address in INVALID_EXAMPLES:
user_part, domain_part = address.rsplit('@', 1)
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),
def test_unparseable_email(self):

View File

@ -1,7 +1,8 @@
from typing import Iterable
class EmailValidationError(Exception):
"""
Base class for all exceptions indicating validation failure.
"""
'Base class for all exceptions indicating validation failure.'
message = 'Unknown error.'
def __str__(self):
@ -9,74 +10,55 @@ class EmailValidationError(Exception):
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.'
class DomainBlacklistedError(EmailValidationError):
"""
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.'
class DomainNotFoundError(EmailValidationError):
"""
Raised when the domain of the email address is blacklisted on
https://git.com/martenson/disposable-email-domains.
"""
'Raised when the domain is not found.'
message = 'Domain not found.'
class NoNameserverError(EmailValidationError):
"""
Raised when the domain of the email address is blacklisted on
https://git.com/martenson/disposable-email-domains.
"""
'Raised when the domain does not resolve by nameservers in time.'
message = 'No nameserver found for domain.'
class DNSTimeoutError(EmailValidationError):
"""
Raised when the domain of the email address is blacklisted on
https://git.com/martenson/disposable-email-domains.
"""
'Raised when the domain lookup times out.'
message = 'Domain lookup timed out.'
class DNSConfigurationError(EmailValidationError):
"""
Raised when the domain of the email address is blacklisted on
https://git.com/martenson/disposable-email-domains.
Raised when the DNS entries for this domain are falsely configured.
"""
message = 'Misconfigurated DNS entries for domain.'
class NoMXError(EmailValidationError):
"""
Raised when the domain of the email address is blacklisted on
https://git.com/martenson/disposable-email-domains.
"""
'Raised then the domain has no MX records configured.'
message = 'No MX record for domain found.'
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.'
class AddressNotDeliverableError(EmailValidationError):
"""
Raised when the domain of the email address is blacklisted on
https://git.com/martenson/disposable-email-domains.
"""
message = 'Non-deliverable email address:'
'Raised when a non-ambigious resulted lookup fails.'
message = 'Email address undeliverable:'
def __init__(self, error_messages):
self.message = '\n'.join([self.message] + error_messages)
def __init__(self, error_messages: Iterable):
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
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)]
if not len(result):
if not result:
raise NoValidMXError
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(
mx_records: list, smtp_timeout: int, helo_host: str, from_address: str,
email_address: str
@ -77,32 +106,15 @@ def _check_mx_records(
found_ambigious = False
for mx_record in mx_records:
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:
found_ambigious = True
continue
except SocketError as error:
error_messages.append(f'{mx_record}: {error}')
continue
if code == 250:
found_ambigious |= _check_one_mx(
smtp=smtp, error_messages=error_messages, mx_record=mx_record,
helo_host=helo_host, from_address=from_address,
email_address=email_address)
except StopIteration:
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
# an exceptin containing the collected error messages.
if found_ambigious:
return None
else:
# an exception containing the collected error messages.
if not found_ambigious:
raise AddressNotDeliverableError(error_messages)

View File

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

View File

@ -6,6 +6,8 @@ from .exceptions import AddressFormatError, EmailValidationError
from .mx_check import mx_check
from .regex_check import regex_check
logger = getLogger(name='validate_email')
def validate_email_or_fail(
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)
except EmailValidationError as error:
message = f'Validation for {email_address!r} failed: {error}'
getLogger('validate_email').info(message)
logger.warning(msg=message)
return False