# TODO: Accept multiple addresses for email verification # TODO: Handle errors according to status code. # TODO: Retry on some responses. import binascii import os from collections import namedtuple from email.utils import parseaddr import pprint import smtplib import socks from dns import resolver from socks_smtp import SocksSMTP as SMTP blocked_keywords = ["spamhaus", "proofpoint", "cloudmark", "banned", "blacklisted", "blocked", "block list", "denied"] proxy = { 'socks4': socks.SOCKS4, 'socks5': socks.SOCKS5, 'http': socks.HTTP # does not guareentee it will work with HTTP } class UnknownProxyError(Exception): def __init__(self, proxy_type): self.msg = f"The proxy type {proxy_type} is not known\n Try one of socks4, socks5 or http" class EmailFormatError(Exception): def __init__(self, msg): self.msg = msg class SMTPRecepientException(Exception): # don't cover def __init__(self, code, response): self.code = code self.response = response #### # SMTP RCPT error handlers #### def handle_550(response): if any([keyword.encode() in response for keyword in blocked_keywords]): return dict(message="Blocked by mail server", deliverable=False, host_exists=True) else: return dict(deliverable=False, host_exists=True) # https://www.greenend.org.uk/rjk/tech/smtpreplies.html#RCPT # https://sendgrid.com/blog/smtp-server-response-codes-explained/ # Most of these errors return a dict that should be merged with 'lookup' afterwards handle_error = { # 250 and 251 are not errors 550: handle_550, 551: lambda _: dict(deliverable=False, host_exists=True), 552: lambda _: dict(deliverable=True, host_exists=True, full_inbox=True), 553: lambda _: dict(deliverable=False, host_exists=True), 450: lambda _: dict(deliverable=False, host_exists=True), 451: lambda _: dict(deliverable=False, message="Local error processing, try again later."), 452: lambda _: dict(deliverable=True, full_inbox=True), # Syntax errors # 500 (command not recognised) # 501 (parameter/argument not recognised) # 503 (bad command sequence) 521: lambda _: dict(deliverable=False, host_exists=False), 421: lambda _: dict(deliverable=False, host_exists=True, message="Service not available, try again later."), 441: lambda _: dict(deliverable=True, full_inbox=True, host_exists=True) } handle_unrecognised = lambda a: dict(message=f"Unrecognised error: {a}", deliverable=False) # create a namedtuple to hold the email address Address = namedtuple("Address", ["name", "addr", "username", "domain"]) class Verifier: def __init__(self, source_addr, proxy_type = None, proxy_addr = None, proxy_port = None, proxy_username = None, proxy_password = None): """ Initializes the Verifier object with proxy settings. :param proxy_type: One of `SOCKS4`, `SOCKS5` or `HTTP`. :param proxy_addr: Address of the proxy. :param proxy_port: Port of the proxy. :param proxy_username: Username to authenticate with. :param proxy_password: Password for the user. (Only when username is provided) """ if proxy_type: try: self.proxy_type = proxy[proxy_type.lower()] except KeyError as e: raise UnknownProxyError(proxy_type) else: self.proxy_type = None self.source_addr = source_addr self.proxy_addr = proxy_addr self.proxy_port = proxy_port self.proxy_username = proxy_username self.proxy_password = proxy_password def _parse_address(self, email) -> Address: """ Parses the email address provided and splits it into username and domain. Returns a named tuple Address """ name, addr = parseaddr(email) if not addr: raise EmailFormatError(f"email does not contain address: {email}") try: domain = addr.split('@')[-1] username = addr.split('@')[:-1][0] except IndexError: raise EmailFormatError(f"address provided is invalid: {email}") return Address(name, addr, username, domain) def _random_email(self, domain): """ This method generates a random email by using the os.urandom for the domain provided in the parameter. """ return f'{binascii.hexlify(os.urandom(20)).decode()}@{domain}' def _can_deliver(self, exchange : str, address : str): """ Checks the deliverablity of an email to the given mail_exchange. Creates a connection using the SMTP and tries to add the email to a recipients. :param exchange: The exchange url for the domain of email :param address: The email address to check for deliverablity :returns: A 3-tuple of host_exists, deliverable and catch_all """ host_exists = False with SMTP(exchange[1], proxy_type=self.proxy_type, proxy_addr=self.proxy_addr, proxy_port=self.proxy_port, proxy_username=self.proxy_username, proxy_password=self.proxy_password) as smtp: host_exists = True smtp.helo() smtp.mail(self.source_addr) test_resp = smtp.rcpt(address.addr) catch_all_resp = smtp.rcpt(self._random_email(address.domain)) if test_resp[0] == 250: deliverable = True if catch_all_resp[0] == 250: catch_all = True else: catch_all = False elif test_resp[0] >= 400: raise SMTPRecepientException(*test_resp) return host_exists, deliverable, catch_all def verify(self, email): """ method that performs the verification on the passed email address. """ lookup = { 'address': None, 'valid_format': False, 'deliverable': False, 'full_inbox': False, 'host_exists': False, 'catch_all': False, } try: lookup['address'] = self._parse_address(email) lookup['valid_format'] = True except EmailFormatError: lookup['address'] = f"{email}" return lookup # look for mx record and create a list of mail exchanges try: mx_record = resolver.query(lookup['address'].domain, 'MX') mail_exchangers = [exchange.to_text().split() for exchange in mx_record] lookup['host_exists'] = True except (resolver.NoAnswer, resolver.NXDOMAIN, resolver.NoNameservers): lookup['host_exists'] = False return lookup for exchange in mail_exchangers: try: host_exists, deliverable, catch_all = self._can_deliver(exchange, lookup['address']) if deliverable: lookup['host_exists'] = host_exists lookup['deliverable'] = deliverable lookup['catch_all'] = catch_all break except SMTPRecepientException as err: # Error handlers return a dict that is then merged with 'lookup' kwargs = handle_error.get(err.code, handle_unrecognised)(err.response) # This expression merges the lookup dict with kwargs lookup = {**lookup, **kwargs} except smtplib.SMTPServerDisconnected as err: lookup['message'] = "Internal Error" except smtplib.SMTPConnectError as err: lookup['message'] = "Internal Error. Maybe blacklisted" return lookup if __name__ == "__main__": v = Verifier(source_addr='user@example.com') email = input('Enter email to verify: ') l = v.verify(email) pprint.pprint(l)