├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── emailahoy ├── __init__.py └── email.py ├── pep8.sh ├── requirements.txt ├── setup.py └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | MANIFEST 3 | dist/ 4 | build/ 5 | *.egg-info/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.5" 4 | - "2.6" 5 | - "2.7" 6 | script: python test.py 7 | 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.6 2 | 3 | Bugfixes: 4 | 5 | - Fixed Unittest 6 | 7 | ## 0.0.5 8 | 9 | Enhancement: 10 | 11 | - Enabled Unittest 12 | 13 | 14 | ## 0.0.4 15 | 16 | Enhancement: 17 | 18 | - Added Unittest 19 | 20 | 21 | ## 0.0.3 22 | 23 | Enhancement: 24 | 25 | - Added Travis CI support 26 | 27 | 28 | ## 0.0.2 29 | 30 | Enhancement: 31 | 32 | - Removed dependency on external packages (pydns) 33 | 34 | 35 | ## 0.0.1 36 | 37 | Features: 38 | 39 | - Initial Release 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © Val Neekman ([Neekware Inc.](http://neekware.com)) [ info@neekware.com, [@vneekman](https://twitter.com/vneekman) ] 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of this project nor the names of its contributors may be 16 | used to endorse or promote products derived from this software without 17 | specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Python Email Ahoy 2 | ==================== 3 | 4 | **A Python email utility that verifies existence of an email address** 5 | 6 | 7 | [![build-status-image-fury]][fury] 8 | 9 | 10 | Overview 11 | ======== 12 | 13 | A Python email utility that verifies existence of an email address. 14 | 15 | How to install 16 | ================== 17 | 18 | 1. easy_install python-emailahoy 19 | 2. pip install python-emailahoy 20 | 3. git clone http://github.com/un33k/python-emailahoy 21 | a. cd python-emailahoy 22 | b. run python setup.py 23 | 4. wget https://github.com/un33k/python-emailahoy/zipball/master 24 | a. unzip the downloaded file 25 | b. cd into python-emailahoy-* directory 26 | c. run python setup.py 27 | 28 | How to use 29 | ================= 30 | 31 | ``Use the class for more control & more granular return status`` 32 | 33 | ```python 34 | from emailahoy import VerifyEmail 35 | e = VerifyEmail() 36 | status = e.verify_email_smtp( 37 | email='test@example.com', 38 | from_host='mydomain.com', 39 | from_email='verify@mydomain.com' 40 | ) 41 | 42 | if e.was_found(status): 43 | print >> sys.stderr, "Found:", status 44 | elif e.not_found(status): 45 | print >> sys.stderr, "Not Found:", status 46 | else: 47 | print >> sys.stderr, "Unverifiable:", status 48 | ``` 49 | 50 | ``Use the shorthand function for quick check`` 51 | 52 | ```python 53 | if verify_email_address('test@example.com'): 54 | print >> sys.stderr, "Found" 55 | else: 56 | print >> sys.stderr, "Don't care" 57 | ``` 58 | 59 | ``Note:`` 60 | 61 | 1. Not all email servers will return the correct status 62 | 2. Checking an invalid email address returns within 1 second 63 | 3. Checking a valid email address returns within 4 seconds or more 64 | 65 | 66 | Running the tests 67 | ================= 68 | 69 | To run the tests against the current environment: 70 | 71 | python test.py 72 | 73 | License 74 | ==================== 75 | 76 | Released under a ([BSD](LICENSE.md)) license. 77 | 78 | 79 | [build-status-image-fury]: https://badge.fury.io/py/python-emailahoy.png 80 | [fury]: http://badge.fury.io/py/python-emailahoy 81 | 82 | 83 | -------------------------------------------------------------------------------- /emailahoy/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __version__ = '0.0.6' 4 | 5 | import re 6 | import sys 7 | import socket 8 | import popen2 9 | import smtplib as _smtp 10 | 11 | DEBUG = False 12 | 13 | __all__ = ['VerifyEmail', 'verify_email_address', 'query_mx'] 14 | 15 | MX_RE = re.compile('mail\sexchanger\s=\s(\d+)\s(.*)\.') 16 | EMAIL_RE = re.compile('([\w\-\.+]+@\w[\w\-]+\.+[\w\-]+)') 17 | NOT_FOUND_KEYWORDS = [ 18 | "does not exist", 19 | "doesn't exist", 20 | "doesn't have", 21 | "doesn't handle", 22 | "unknown user", 23 | "user unknown", 24 | "rejected", 25 | "disabled", 26 | "discontinued", 27 | "unavailable", 28 | "unknown", 29 | "invalid", 30 | "typos", 31 | "unnecessary spaces", 32 | ] 33 | 34 | UNVERIFIABLE_KEYWORDS = [ 35 | "block", 36 | "block list", 37 | "spam", 38 | "spammer", 39 | "isp", 40 | "weren't sent", 41 | "not accepted", 42 | ] 43 | 44 | 45 | def query_mx(host): 46 | """ Returns all MX records of a given domain name """ 47 | 48 | mail_exchangers = [] 49 | addr = {} 50 | fout, fin = popen2.popen2('which nslookup') 51 | cmd = fout.readline().strip() 52 | if cmd <> '': 53 | fout, fin = popen2.popen2('%s -query=mx %s' % (cmd, host)) 54 | line = fout.readline() 55 | while line <> '': 56 | mx = MX_RE.search(line.lower()) 57 | if mx: 58 | mail_exchangers.append((eval(mx.group(1)), mx.group(2))) 59 | line = fout.readline() 60 | 61 | if mail_exchangers: 62 | mail_exchangers.sort() 63 | return mail_exchangers 64 | 65 | 66 | class VerifyEmail(object): 67 | """ Verify if email exists """ 68 | 69 | EMAIL_FOUND = 1 70 | EMAIL_NOT_FOUND = 2 71 | UNABLE_TO_VERIFY = 3 72 | 73 | 74 | def connect(self, hostname, timeout=10): 75 | """ Returns a server connection or None given a hostname """ 76 | 77 | try: 78 | socket.gethostbyname(hostname) 79 | server = _smtp.SMTP(timeout=timeout) 80 | code, resp = server.connect(hostname) 81 | if code == 220: 82 | return server 83 | except: 84 | pass 85 | return None 86 | 87 | 88 | def unverifiable(self, resp): 89 | """ Return true if email is not verifiable """ 90 | return any(a in resp.lower() for a in UNVERIFIABLE_KEYWORDS) 91 | 92 | 93 | def nonexistent(self, resp): 94 | """ Return true if email is not verifiable """ 95 | return any(a in resp.lower() for a in NOT_FOUND_KEYWORDS) 96 | 97 | 98 | def verify( 99 | self, 100 | email, 101 | from_host='example.com', 102 | from_email='verify@example.com' 103 | ): 104 | """ verifies whether an email address does exist """ 105 | 106 | if not EMAIL_RE.search(email): 107 | return self.EMAIL_NOT_FOUND 108 | 109 | try: 110 | hostname = email.strip().split('@')[1] 111 | socket.gethostbyname(hostname) 112 | mail_exchangers = query_mx(hostname) 113 | except: 114 | return self.UNABLE_TO_VERIFY 115 | 116 | for mx in mail_exchangers: 117 | mx_name = mx[1] 118 | server = self.connect(mx_name) 119 | if not server: 120 | continue 121 | if DEBUG: 122 | server.set_debuglevel(1) 123 | try: 124 | code, resp = server.helo(mx_name) 125 | if code != 250: 126 | if not self.unverifiable(resp): 127 | return self.UNABLE_TO_VERIFY 128 | continue 129 | except: 130 | pass 131 | try: 132 | code, resp = server.mail(from_email) 133 | if code != 250: 134 | if not self.unverifiable(resp): 135 | return self.UNABLE_TO_VERIFY 136 | continue 137 | except: 138 | pass 139 | try: 140 | code, resp = server.rcpt(email) 141 | if code != 250: 142 | if self.nonexistent(resp): 143 | return self.EMAIL_NOT_FOUND 144 | elif self.unverifiable(resp): 145 | return self.UNABLE_TO_VERIFY 146 | else: 147 | continue 148 | except: 149 | pass 150 | try: 151 | code, resp = server.data('Ahoy. Are you there?{0}.{0}'.format(_smtp.CRLF)) 152 | if code != 250: 153 | if self.nonexistent(resp): 154 | return self.EMAIL_NOT_FOUND 155 | elif self.unverifiable(resp): 156 | return self.UNABLE_TO_VERIFY 157 | elif code == 250: 158 | return self.EMAIL_FOUND 159 | except: 160 | pass 161 | 162 | return self.UNABLE_TO_VERIFY 163 | 164 | 165 | # given an email it returns True if it can tell it exist or False 166 | def verify_email_address( 167 | email, 168 | from_host='example.com', 169 | from_email='verify@example.com' 170 | ): 171 | """ A quick email verification function """ 172 | e = VerifyEmail() 173 | status = e.verify(email, from_host, from_email) 174 | if status == e.EMAIL_NOT_FOUND: 175 | return False 176 | return True 177 | 178 | if __name__ == "__main__": 179 | # if verify_email_address('un33kvu@gmail.com', 'djanguru.djanguru.net', 'verify@djanguru.net'): 180 | # if verify_email_address('un33ksssddsdsd333vu@gmail.com', 'djanguru.net', 'verify@djanguru.net'): 181 | # if verify_email_address('un33kvu@yahoo.com', 'djanguru.net', 'verify@djanguru.net'): 182 | # if verify_email_address('un33ksssddsdsd333vu@yahoo.com', 'djanguru.net', 'verify@djanguru.net'): 183 | # if verify_email_address('un33ksssddsdsd333vu@cnn.com', 'djanguru.net', 'verify@djanguru.net'): 184 | # if verify_email_address('vman@outsourcefactor.com', 'djanguru.net', 'verify@djanguru.net'): 185 | if verify_email_address('asfsadfasfsdf@hotmail.com', 'djanguru.net', 'verify@djanguru.net'): 186 | print "found" 187 | -------------------------------------------------------------------------------- /emailahoy/email.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __version__ = '0.0.6' 4 | 5 | from email import * 6 | 7 | -------------------------------------------------------------------------------- /pep8.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo -e "\nRunning: (pep8 --show-source --show-pep8 --select=errors --testsuite=.)\n\n" 4 | pep8 --show-source --show-pep8 --select=errors --testsuite=./ 5 | echo -e "\n\n" 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # -*- coding: utf-8 -*- 4 | from setuptools import setup 5 | import re 6 | import os 7 | import sys 8 | 9 | 10 | name = 'python-emailahoy' 11 | package = 'emailahoy' 12 | description = 'A Python email utility that verifies existence of an email address' 13 | url = 'https://github.com/un33k/python-emailahoy' 14 | author = 'Val Neekman' 15 | author_email = 'info@neekware.com' 16 | license = 'BSD' 17 | install_requires = [] 18 | classifiers = [ 19 | 'Development Status :: 3 - Alpha', 20 | 'Intended Audience :: Developers', 21 | 'License :: OSI Approved :: BSD License', 22 | 'Operating System :: POSIX', 23 | 'Programming Language :: Python', 24 | 'Topic :: Software Development :: Libraries :: Python Modules', 25 | 'Topic :: Communications :: Email', 26 | ] 27 | 28 | def read(fname): 29 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 30 | 31 | def get_version(package): 32 | """ 33 | Return package version as listed in `__version__` in `init.py`. 34 | """ 35 | init_py = open(os.path.join(package, '__init__.py')).read() 36 | return re.search("^__version__ = ['\"]([^'\"]+)['\"]", init_py, re.MULTILINE).group(1) 37 | 38 | 39 | def get_packages(package): 40 | """ 41 | Return root package and all sub-packages. 42 | """ 43 | return [dirpath 44 | for dirpath, dirnames, filenames in os.walk(package) 45 | if os.path.exists(os.path.join(dirpath, '__init__.py'))] 46 | 47 | 48 | def get_package_data(package): 49 | """ 50 | Return all files under the root package, that are not in a 51 | package themselves. 52 | """ 53 | walk = [(dirpath.replace(package + os.sep, '', 1), filenames) 54 | for dirpath, dirnames, filenames in os.walk(package) 55 | if not os.path.exists(os.path.join(dirpath, '__init__.py'))] 56 | 57 | filepaths = [] 58 | for base, filenames in walk: 59 | filepaths.extend([os.path.join(base, filename) 60 | for filename in filenames]) 61 | return {package: filepaths} 62 | 63 | 64 | if sys.argv[-1] == 'publish': 65 | os.system("python setup.py sdist upload") 66 | args = {'version': get_version(package)} 67 | print "You probably want to also tag the version now:" 68 | print " git tag -a %(version)s -m 'version %(version)s'" % args 69 | print " git push --tags" 70 | sys.exit() 71 | 72 | 73 | setup( 74 | name=name, 75 | version=get_version(package), 76 | url=url, 77 | license=license, 78 | description=description, 79 | long_description = read('README.md'), 80 | author=author, 81 | author_email=author_email, 82 | packages=get_packages(package), 83 | package_data=get_package_data(package), 84 | install_requires=install_requires, 85 | classifiers=classifiers 86 | ) 87 | 88 | 89 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | from emailahoy import VerifyEmail 5 | from emailahoy import query_mx 6 | from emailahoy import verify_email_address 7 | import sys 8 | import logging 9 | 10 | class TestEmailVerificationFunctions(unittest.TestCase): 11 | 12 | def setUp(self): 13 | """ Instantiate the class """ 14 | self.e = VerifyEmail() 15 | self.log= logging.getLogger( "TestEmailVerificationFunctions" ) 16 | 17 | 18 | def test_class_based_invalid_email(self): 19 | """ Test the existence of an invalid email address (class based)""" 20 | 21 | email = 'non-existing-email@cnn.com' 22 | self.log.debug("Testing classy invalid email address (%s)" % email) 23 | 24 | status = self.e.verify( 25 | email=email, 26 | from_host='neekware.com', 27 | from_email='info@neekware.com' 28 | ) 29 | 30 | self.log.debug(status) 31 | self.assertEquals(self.e.EMAIL_NOT_FOUND, status) 32 | 33 | def test_class_based_valid_email(self): 34 | """ Test the existence of a valid email address (class based)""" 35 | 36 | email = 'vinnie@cnn.com' 37 | self.log.debug("Testing classy valid email address (%s)" % email) 38 | status = self.e.verify( 39 | email=email, 40 | from_host='neekware.com', 41 | from_email='info@neekware.com' 42 | ) 43 | 44 | self.log.debug(status) 45 | self.assertEquals(self.e.EMAIL_FOUND, status) 46 | 47 | def test_function_based_invalid_email(self): 48 | """ Test the existence of an invalid email address (function based)""" 49 | 50 | email = 'non-existing-email@cnn.com' 51 | self.log.debug("Testing invalid email address (%s)" % email) 52 | 53 | found = verify_email_address( 54 | email=email, 55 | from_host='neekware.com', 56 | from_email='info@neekware.com' 57 | ) 58 | 59 | # email doesn't exists 60 | self.assertEquals(found, False) 61 | 62 | 63 | def test_function_based_valid_email(self): 64 | """ Test the existence of a valid email address (function based)""" 65 | 66 | email = 'vinnie@cnn.com' 67 | self.log.debug("Testing valid email address (%s)" % email) 68 | 69 | found = verify_email_address( 70 | email=email, 71 | from_host='neekware.com', 72 | from_email='info@neekware.com' 73 | ) 74 | # email exists 75 | self.assertEquals(found, True) 76 | 77 | 78 | def test_mx_query_invalid_domain(self): 79 | """ Query mx of an invalid domain name """ 80 | 81 | domain = 'invalid_domain_address.com' 82 | self.log.debug("Testing MX Query for invalid domain (%s)" % domain) 83 | mx = query_mx(domain) 84 | self.assertEquals(len(mx), 0) 85 | 86 | 87 | def test_mx_query_valid_domain(self): 88 | """ Query mx of a valid domain name """ 89 | 90 | domain = 'cnn.com' 91 | self.log.debug("Testing MX Query for valid domain (%s)" % domain) 92 | 93 | mx = query_mx(domain) 94 | self.assertNotEqual(len(mx), 0) 95 | 96 | 97 | if __name__ == '__main__': 98 | logging.basicConfig( stream=sys.stderr ) 99 | logging.getLogger( "TestEmailVerificationFunctions" ).setLevel( logging.DEBUG ) 100 | unittest.main() 101 | 102 | 103 | --------------------------------------------------------------------------------