"""$URL: svn://svn/repos/trunk/sitereaper/lib/email_validator.py $
$Id: email_validator.py 20952 2003-03-05 16:51:11Z dbinger $

Verify email addresses.  Does pretty much the right thing: look for MX
hosts, and connect to them in turn until we get through to one, and do
MAIL FROM, RCPT TO, QUIT.  Doesn't distinguish between temporary and
permanent errors, and doesn't try another MX once we have connected to
one.
"""

from popen2 import Popen3
from smtplib import SMTP
from socket import error as SocketError
from timeoutsocket import Timeout
from sitereaper.utils import warn

class SMTPError (Exception):
    def __init__ (self, code, msg):
        self.code = code
        self.msg = msg

    def __str__ (self):
        return "%d %s" % (self.code, self.msg)


class EmailValidator:

    def __init__ (self, sender=None):

        self.sender = sender or "<>"

        # DNS cache: dict mapping domain name to list of
        # (priority, mx_host) tuples, sorted by priority.
        self.mx_hosts = {}

        # Cache results of previous validations; maps whole email
        # address to boolean -- true means address OK, false means not.
        self.addr_result = {}

        # Record error message for bad addresses.
        self.addr_error = {}


    def get_mx_hosts (self, domain):
        hosts = self.mx_hosts.get(domain)
        if hosts is None:               # not in cache
            dnsmx = Popen3(["dnsmx", domain])
            dnsmx.tochild.close()
            hosts = []
            for line in dnsmx.fromchild:
                (pri, host) = line.strip().split()
                pri = int(pri)
                hosts.append((pri, host))
            dnsmx.fromchild.close()

            status = dnsmx.wait()
            if status != 0:
                warn("dnsmx exited with status %d" % status)
                return []

            hosts.sort()
            self.mx_hosts[domain] = hosts

        return [host for (pri, host) in hosts]


    def check (self, addr):
        if self.addr_result.has_key(addr):
            return self.addr_result[addr]

        (local_part, domain) = addr.split("@")
        mx_hosts = self.get_mx_hosts(domain)
        if not mx_hosts:
            self.addr_error[addr] = "no MX hosts for domain %s" % domain
            self.addr_result[addr] = 0
            return 0

        host_err = {}

        connected = 0
        for host in mx_hosts:
            try:
                smtp = SMTP(host)
            except (SocketError, Timeout), err:
                warn("error connecting to %s: %s" % (host, err[-1]))
                host_err[host] = err[-1]
                continue

            connected = 1
            try:
                (code, msg) = smtp.ehlo()
                if code != 250:
                    raise SMTPError(code, msg)
                (code, msg) = smtp.mail(self.sender)
                if code != 250:
                    raise SMTPError(code, msg)
                (code, msg) = smtp.rcpt(addr)
                if code not in (250, 251):
                    raise SMTPError(code, msg)
                smtp.quit()
            except SMTPError, err:
                self.addr_error[addr] = str(err)
                self.addr_result[addr] = 0    # failure
                break
            else:
                self.addr_result[addr] = 1    # success
                break

        if not connected:
            # tried all MX hosts, couldn't connect to any
            err = ["%s: %s" % (host, host_err[host])
                   for host in mx_hosts]
            err = "; ".join(err)
            self.addr_error[addr] = err
            self.addr_result[addr] = 0

        return self.addr_result[addr]

    def get_error (self, addr):
        return self.addr_error.get(addr)


if __name__ == "__main__":
    import os, sys
    from timeoutsocket import setDefaultSocketTimeout

    prog = os.path.basename(sys.argv[0])
    args = sys.argv[1:]
    if len(args) < 2:
        sys.exit("usage: %s sender recip [...]" % prog)

    setDefaultSocketTimeout(15)

    sender = args.pop(0)
    checker = EmailValidator(sender)
    for addr in args:
        print "** checking %s" % addr
        r = checker.check(addr)
        print "result: %r (error: %r)" % (r, checker.get_error(addr))
        print
