From c70abccef376e00bc120f9c5318f42515c0ea0e0 Mon Sep 17 00:00:00 2001 From: Timothy Warren Date: Wed, 9 Sep 2020 16:57:37 -0400 Subject: [PATCH 1/2] Add email address validation utilities Refs #4 --- keg_mail/content.py | 20 ++++++++++++++++++++ keg_mail/tests/test_content.py | 23 +++++++++++++++++++++++ setup.py | 2 ++ 3 files changed, 45 insertions(+) diff --git a/keg_mail/content.py b/keg_mail/content.py index 775b12c..ba55435 100644 --- a/keg_mail/content.py +++ b/keg_mail/content.py @@ -6,6 +6,9 @@ from flask_mail import Message from flask import current_app +import pymailcheck +import validator_collection +import validator_collection.errors import keg_mail.utils as utils @@ -124,6 +127,23 @@ def __init__(self, subject, content, type_=None): self.content = content self.type_ = type_ + @classmethod + def validate_address(cls, address): + """Returns `True` if the address looks like a valid e-mail address""" + try: + validator_collection.email(address) + return True + + except ValueError: + return False + + @classmethod + def address_might_be_invalid(cls, address): + if not cls.validate_address(address): + return True + + return bool(pymailcheck.suggest(address)) + def format(self, *args, **kwargs): select_text = lambda item: getattr(item, 'text', item) # noqa return Email( diff --git a/keg_mail/tests/test_content.py b/keg_mail/tests/test_content.py index f971ee3..0dd3ee5 100644 --- a/keg_mail/tests/test_content.py +++ b/keg_mail/tests/test_content.py @@ -52,6 +52,29 @@ def setup_method(self): self.content = content.EmailContent('{a}', '{a}') self.content_b = content.EmailContent('{b}', '{b}') + def test_validate_address(self): + validate_address = content.Email.validate_address + + assert validate_address('user@example.com') is True + assert validate_address('user@gmail.com') is True + + assert validate_address('') is False + assert validate_address('user@invalid') is False + assert validate_address('invalid') is False + + assert validate_address('user@example.con') is True + assert validate_address('user@gmaol.com') is True + + def test_address_might_be_invalid(self): + address_might_be_invalid = content.Email.address_might_be_invalid + + assert address_might_be_invalid('user@example.con') is True + assert address_might_be_invalid('user@gmaol.com') is True + + assert address_might_be_invalid('user@valid.com') is False + assert address_might_be_invalid('user@example.com') is False + assert address_might_be_invalid('user@gmail.com') is False + def test_equality(self): # Same assert (content.Email('a', self.content, 'a') == diff --git a/setup.py b/setup.py index dea377e..1bcd9bd 100644 --- a/setup.py +++ b/setup.py @@ -34,6 +34,8 @@ 'Keg', 'SQLAlchemy-Utils', 'arrow', + 'pymailcheck', + 'validator-collection', ], extras_require={ 'mailgun': [ From 1c9777dc28d8f2c3e04d35772664e09d710bd4bc Mon Sep 17 00:00:00 2001 From: Timothy Warren Date: Tue, 2 Feb 2021 14:33:11 -0500 Subject: [PATCH 2/2] Update validation per PR review * Extract suggestions method to allow developer access to suggestions * Expand test coverage * Remove unused import * Update the readme Refs GH-4 --- README.rst | 19 ++++++++++++++++++- keg_mail/content.py | 13 +++++++++++-- keg_mail/tests/test_content.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index f7d3ad6..508ece5 100644 --- a/README.rst +++ b/README.rst @@ -16,7 +16,7 @@ Usage $ pip install keg-mail -Initialize Keg-Mail in you application +Initialize Keg-Mail in your application .. code:: @@ -55,6 +55,23 @@ Define email content ) +Validate the recipient + +.. code:: + + import keg_mail.content + + address = "user@example.com" + if keg_mail.content.Email.address_might_be_invalid(address): + # the address might be invalid (it might be incomplete, or there + # may be a typo) + . . . + + else: + # address should be good, send the email + . . . + + Send the email .. code:: diff --git a/keg_mail/content.py b/keg_mail/content.py index ba55435..17a8cfb 100644 --- a/keg_mail/content.py +++ b/keg_mail/content.py @@ -8,7 +8,6 @@ from flask import current_app import pymailcheck import validator_collection -import validator_collection.errors import keg_mail.utils as utils @@ -139,10 +138,20 @@ def validate_address(cls, address): @classmethod def address_might_be_invalid(cls, address): + """Returns `True` if the address is or could be invalid, `False` otherwise""" if not cls.validate_address(address): return True - return bool(pymailcheck.suggest(address)) + return bool(cls.get_invalid_address_suggestions(address)) + + @classmethod + def get_invalid_address_suggestions(cls, address): + """Returns a suggested address if one exists, else `None`""" + suggestion = pymailcheck.suggest(address) + if suggestion is False: + return None + + return suggestion['full'] def format(self, *args, **kwargs): select_text = lambda item: getattr(item, 'text', item) # noqa diff --git a/keg_mail/tests/test_content.py b/keg_mail/tests/test_content.py index 0dd3ee5..60f4966 100644 --- a/keg_mail/tests/test_content.py +++ b/keg_mail/tests/test_content.py @@ -58,6 +58,9 @@ def test_validate_address(self): assert validate_address('user@example.com') is True assert validate_address('user@gmail.com') is True + # pet peeve, this address is valid, ensure it's seen that way + assert validate_address('user+foo@example.com') is True + assert validate_address('') is False assert validate_address('user@invalid') is False assert validate_address('invalid') is False @@ -68,13 +71,41 @@ def test_validate_address(self): def test_address_might_be_invalid(self): address_might_be_invalid = content.Email.address_might_be_invalid + assert address_might_be_invalid('') is True + assert address_might_be_invalid('user@invalid') is True + assert address_might_be_invalid('invalid') is True + assert address_might_be_invalid('user@example.con') is True assert address_might_be_invalid('user@gmaol.com') is True + # pet peeve, this address is valid, ensure it's seen that way + assert address_might_be_invalid("user+foo@example.com") is False + assert address_might_be_invalid('user@valid.com') is False assert address_might_be_invalid('user@example.com') is False assert address_might_be_invalid('user@gmail.com') is False + def test_get_invalid_address_suggestions(self): + get_invalid_address_suggestions = content.Email.get_invalid_address_suggestions + + # can't make a suggestion on an empty address + assert get_invalid_address_suggestions('') is None + + # no suggestions on a valid address + assert get_invalid_address_suggestions('user@example.com') is None + assert get_invalid_address_suggestions('user@gmail.com') is None + assert get_invalid_address_suggestions("user+foo@example.com") is None + + # suggestions... + assert get_invalid_address_suggestions('user@example.con') == 'user@example.com' + assert get_invalid_address_suggestions('user@gmai.com') == 'user@gmail.com' + + # the result for this address isn't deterministic + assert get_invalid_address_suggestions('user@gmaol.com') in ( + 'user@gmail.com', + 'user@aol.com' + ) + def test_equality(self): # Same assert (content.Email('a', self.content, 'a') ==