Beautifying code, updating AUTHORS+CHANGELOG
This commit is contained in:
parent
4cb98ee2ba
commit
9b174a591f
9 changed files with 87 additions and 86 deletions
1
AUTHORS
1
AUTHORS
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -19,26 +19,20 @@ 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,
|
||||
use_blacklist=True)
|
||||
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,
|
||||
use_blacklist=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,
|
||||
use_blacklist=True))
|
||||
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,
|
||||
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.'
|
||||
|
|
|
@ -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, ())
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue