743 lines · 26.1 KB · Python
RawDownload
#! /usr/bin/env python '''SMTP/ESMTP client class. This should follow RFC 821 (SMTP), RFC 1869 (ESMTP), RFC 2554 (SMTPAuthentication) and RFC 2487 (Secure SMTP over TLS). Notes: Please remember, when doing ESMTP, that the names of the SMTP serviceextensions are NOT the same thing as the option keywords for the RCPTand MAIL commands! Example:   >>> import smtplib  >>> s=smtplib.SMTP("localhost")  >>> print s.help()  This is Sendmail version 8.8.4  Topics:      HELO    EHLO    MAIL    RCPT    DATA      RSET    NOOP    QUIT    HELP    VRFY      EXPN    VERB    ETRN    DSN  For more info use "HELP <topic>".  To report bugs in the implementation send email to      sendmail-bugs@sendmail.org.  For local information send email to Postmaster at your site.  End of HELP info  >>> s.putcmd("vrfy","someone@here")  >>> s.getreply()  (250, "Somebody OverHere <somebody@here.my.org>")  >>> s.quit()''' # Author: The Dragon De Monsyne <dragondm@integral.org># ESMTP support, test code and doc fixes added by#     Eric S. Raymond <esr@thyrsus.com># Better RFC 821 compliance (MAIL and RCPT, and CRLF in data)#     by Carey Evans <c.evans@clear.net.nz>, for picky mail servers.# RFC 2554 (authentication) support by Gerhard Haering <gerhard@bigfoot.de>.## This was modified from the Python 1.5 library HTTP lib. import socketimport reimport email.Utilsimport base64import hmacfrom email.base64MIME import encode as encode_base64from sys import stderr __all__ = ["SMTPException","SMTPServerDisconnected","SMTPResponseException",           "SMTPSenderRefused","SMTPRecipientsRefused","SMTPDataError",           "SMTPConnectError","SMTPHeloError","SMTPAuthenticationError",           "quoteaddr","quotedata","SMTP"] SMTP_PORT = 25CRLF="\r\n" OLDSTYLE_AUTH = re.compile(r"auth=(.*)", re.I) # Exception classes used by this module.class SMTPException(Exception):    """Base class for all exceptions raised by this module.""" class SMTPServerDisconnected(SMTPException):    """Not connected to any SMTP server.     This exception is raised when the server unexpectedly disconnects,    or when an attempt is made to use the SMTP instance before    connecting it to a server.    """ class SMTPResponseException(SMTPException):    """Base class for all exceptions that include an SMTP error code.     These exceptions are generated in some instances when the SMTP    server returns an error code.  The error code is stored in the    `smtp_code' attribute of the error, and the `smtp_error' attribute    is set to the error message.    """     def __init__(self, code, msg):        self.smtp_code = code        self.smtp_error = msg        self.args = (code, msg) class SMTPSenderRefused(SMTPResponseException):    """Sender address refused.     In addition to the attributes set by on all SMTPResponseException    exceptions, this sets `sender' to the string that the SMTP refused.    """     def __init__(self, code, msg, sender):        self.smtp_code = code        self.smtp_error = msg        self.sender = sender        self.args = (code, msg, sender) class SMTPRecipientsRefused(SMTPException):    """All recipient addresses refused.     The errors for each recipient are accessible through the attribute    'recipients', which is a dictionary of exactly the same sort as    SMTP.sendmail() returns.    """     def __init__(self, recipients):        self.recipients = recipients        self.args = ( recipients,)  class SMTPDataError(SMTPResponseException):    """The SMTP server didn't accept the data.""" class SMTPConnectError(SMTPResponseException):    """Error during connection establishment.""" class SMTPHeloError(SMTPResponseException):    """The server refused our HELO reply.""" class SMTPAuthenticationError(SMTPResponseException):    """Authentication error.     Most probably the server didn't accept the username/password    combination provided.    """ class SSLFakeSocket:    """A fake socket object that really wraps a SSLObject.     It only supports what is needed in smtplib.    """    def __init__(self, realsock, sslobj):        self.realsock = realsock        self.sslobj = sslobj     def send(self, str):        self.sslobj.write(str)        return len(str)     sendall = send     def close(self):        self.realsock.close() class SSLFakeFile:    """A fake file like object that really wraps a SSLObject.     It only supports what is needed in smtplib.    """    def __init__(self, sslobj):        self.sslobj = sslobj     def readline(self):        str = ""        chr = None        while chr != "\n":            chr = self.sslobj.read(1)            str += chr        return str     def close(self):        pass def quoteaddr(addr):    """Quote a subset of the email addresses defined by RFC 821.     Should be able to handle anything rfc822.parseaddr can handle.    """    m = (None, None)    try:        m = email.Utils.parseaddr(addr)[1]    except AttributeError:        pass    if m == (None, None): # Indicates parse failure or AttributeError        # something weird here.. punt -ddm        return "<%s>" % addr    elif m is None:        # the sender wants an empty return address        return "<>"    else:        return "<%s>" % m def quotedata(data):    """Quote data for email.     Double leading '.', and change Unix newline '\\n', or Mac '\\r' into    Internet CRLF end-of-line.    """    return re.sub(r'(?m)^\.', '..',        re.sub(r'(?:\r\n|\n|\r(?!\n))', CRLF, data))  class SMTP:    """This class manages a connection to an SMTP or ESMTP server.    SMTP Objects:        SMTP objects have the following attributes:            helo_resp                This is the message given by the server in response to the                most recent HELO command.             ehlo_resp                This is the message given by the server in response to the                most recent EHLO command. This is usually multiline.             does_esmtp                This is a True value _after you do an EHLO command_, if the                server supports ESMTP.             esmtp_features                This is a dictionary, which, if the server supports ESMTP,                will _after you do an EHLO command_, contain the names of the                SMTP service extensions this server supports, and their                parameters (if any).                 Note, all extension names are mapped to lower case in the                dictionary.         See each method's docstrings for details.  In general, there is a        method of the same name to perform each SMTP command.  There is also a        method called 'sendmail' that will do an entire mail transaction.        """    debuglevel = 0    file = None    helo_resp = None    ehlo_resp = None    does_esmtp = 0     def __init__(self, host = '', port = 0, local_hostname = None):        """Initialize a new instance.         If specified, `host' is the name of the remote host to which to        connect.  If specified, `port' specifies the port to which to connect.        By default, smtplib.SMTP_PORT is used.  An SMTPConnectError is raised        if the specified `host' doesn't respond correctly.  If specified,        `local_hostname` is used as the FQDN of the local host.  By default,        the local hostname is found using socket.getfqdn().         """        self.esmtp_features = {}        if host:            (code, msg) = self.connect(host, port)            if code != 220:                raise SMTPConnectError(code, msg)        if local_hostname is not None:            self.local_hostname = local_hostname        else:            # RFC 2821 says we should use the fqdn in the EHLO/HELO verb, and            # if that can't be calculated, that we should use a domain literal            # instead (essentially an encoded IP address like [A.B.C.D]).            fqdn = socket.getfqdn()            if '.' in fqdn:                self.local_hostname = fqdn            else:                # We can't find an fqdn hostname, so use a domain literal                addr = '127.0.0.1'                try:                    addr = socket.gethostbyname(socket.gethostname())                except socket.gaierror:                    pass                self.local_hostname = '[%s]' % addr     def set_debuglevel(self, debuglevel):        """Set the debug output level.         A non-false value results in debug messages for connection and for all        messages sent to and received from the server.         """        self.debuglevel = debuglevel     def connect(self, host='localhost', port = 0):        """Connect to a host on a given port.         If the hostname ends with a colon (`:') followed by a number, and        there is no port specified, that suffix will be stripped off and the        number interpreted as the port number to use.         Note: This method is automatically invoked by __init__, if a host is        specified during instantiation.         """        if not port and (host.find(':') == host.rfind(':')):            i = host.rfind(':')            if i >= 0:                host, port = host[:i], host[i+1:]                try: port = int(port)                except ValueError:                    raise socket.error, "nonnumeric port"        if not port: port = SMTP_PORT        if self.debuglevel > 0: print>>stderr, 'connect:', (host, port)        msg = "getaddrinfo returns an empty list"        self.sock = None        for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):            af, socktype, proto, canonname, sa = res            try:                self.sock = socket.socket(af, socktype, proto)                if self.debuglevel > 0: print>>stderr, 'connect:', sa                self.sock.connect(sa)            except socket.error, msg:                if self.debuglevel > 0: print>>stderr, 'connect fail:', msg                if self.sock:                    self.sock.close()                self.sock = None                continue            break        if not self.sock:            raise socket.error, msg        (code, msg) = self.getreply()        if self.debuglevel > 0: print>>stderr, "connect:", msg        return (code, msg)     def send(self, str):        """Send `str' to the server."""        if self.debuglevel > 0: print>>stderr, 'send:', repr(str)        if self.sock:            try:                self.sock.sendall(str)            except socket.error:                self.close()                raise SMTPServerDisconnected('Server not connected')        else:            raise SMTPServerDisconnected('please run connect() first')     def putcmd(self, cmd, args=""):        """Send a command to the server."""        if args == "":            str = '%s%s' % (cmd, CRLF)        else:            str = '%s %s%s' % (cmd, args, CRLF)        self.send(str)     def getreply(self):        """Get a reply from the server.         Returns a tuple consisting of:           - server response code (e.g. '250', or such, if all goes well)            Note: returns -1 if it can't read response code.           - server response string corresponding to response code (multiline            responses are converted to a single, multiline string).         Raises SMTPServerDisconnected if end-of-file is reached.        """        resp=[]        if self.file is None:            self.file = self.sock.makefile('rb')        while 1:            line = self.file.readline()            if line == '':                self.close()                raise SMTPServerDisconnected("Connection unexpectedly closed")            if self.debuglevel > 0: print>>stderr, 'reply:', repr(line)            resp.append(line[4:].strip())            code=line[:3]            # Check that the error code is syntactically correct.            # Don't attempt to read a continuation line if it is broken.            try:                errcode = int(code)            except ValueError:                errcode = -1                break            # Check if multiline response.            if line[3:4]!="-":                break         errmsg = "\n".join(resp)        if self.debuglevel > 0:            print>>stderr, 'reply: retcode (%s); Msg: %s' % (errcode,errmsg)        return errcode, errmsg     def docmd(self, cmd, args=""):        """Send a command, and return its response code."""        self.putcmd(cmd,args)        return self.getreply()     # std smtp commands    def helo(self, name=''):        """SMTP 'helo' command.        Hostname to send for this command defaults to the FQDN of the local        host.        """        self.putcmd("helo", name or self.local_hostname)        (code,msg)=self.getreply()        self.helo_resp=msg        return (code,msg)     def ehlo(self, name=''):        """ SMTP 'ehlo' command.        Hostname to send for this command defaults to the FQDN of the local        host.        """        self.esmtp_features = {}        self.putcmd("ehlo", name or self.local_hostname)        (code,msg)=self.getreply()        # According to RFC1869 some (badly written)        # MTA's will disconnect on an ehlo. Toss an exception if        # that happens -ddm        if code == -1 and len(msg) == 0:            self.close()            raise SMTPServerDisconnected("Server not connected")        self.ehlo_resp=msg        if code != 250:            return (code,msg)        self.does_esmtp=1        #parse the ehlo response -ddm        resp=self.ehlo_resp.split('\n')        del resp[0]        for each in resp:            # To be able to communicate with as many SMTP servers as possible,            # we have to take the old-style auth advertisement into account,            # because:            # 1) Else our SMTP feature parser gets confused.            # 2) There are some servers that only advertise the auth methods we            #    support using the old style.            auth_match = OLDSTYLE_AUTH.match(each)            if auth_match:                # This doesn't remove duplicates, but that's no problem                self.esmtp_features["auth"] = self.esmtp_features.get("auth", "") \                        + " " + auth_match.groups(0)[0]                continue             # RFC 1869 requires a space between ehlo keyword and parameters.            # It's actually stricter, in that only spaces are allowed between            # parameters, but were not going to check for that here.  Note            # that the space isn't present if there are no parameters.            m=re.match(r'(?P<feature>[A-Za-z0-9][A-Za-z0-9\-]*) ?',each)            if m:                feature=m.group("feature").lower()                params=m.string[m.end("feature"):].strip()                if feature == "auth":                    self.esmtp_features[feature] = self.esmtp_features.get(feature, "") \                            + " " + params                else:                    self.esmtp_features[feature]=params        return (code,msg)     def has_extn(self, opt):        """Does the server support a given SMTP service extension?"""        return opt.lower() in self.esmtp_features     def help(self, args=''):        """SMTP 'help' command.        Returns help text from server."""        self.putcmd("help", args)        return self.getreply()[1]     def rset(self):        """SMTP 'rset' command -- resets session."""        return self.docmd("rset")     def noop(self):        """SMTP 'noop' command -- doesn't do anything :>"""        return self.docmd("noop")     def mail(self,sender,options=[]):        """SMTP 'mail' command -- begins mail xfer session."""        optionlist = ''        if options and self.does_esmtp:            optionlist = ' ' + ' '.join(options)        self.putcmd("mail", "FROM:%s%s" % (quoteaddr(sender) ,optionlist))        return self.getreply()     def rcpt(self,recip,options=[]):        """SMTP 'rcpt' command -- indicates 1 recipient for this mail."""        optionlist = ''        if options and self.does_esmtp:            optionlist = ' ' + ' '.join(options)        self.putcmd("rcpt","TO:%s%s" % (quoteaddr(recip),optionlist))        return self.getreply()     def data(self,msg):        """SMTP 'DATA' command -- sends message data to server.         Automatically quotes lines beginning with a period per rfc821.        Raises SMTPDataError if there is an unexpected reply to the        DATA command; the return value from this method is the final        response code received when the all data is sent.        """        self.putcmd("data")        (code,repl)=self.getreply()        if self.debuglevel >0 : print>>stderr, "data:", (code,repl)        if code != 354:            raise SMTPDataError(code,repl)        else:            q = quotedata(msg)            if q[-2:] != CRLF:                q = q + CRLF            q = q + "." + CRLF            self.send(q)            (code,msg)=self.getreply()            if self.debuglevel >0 : print>>stderr, "data:", (code,msg)            return (code,msg)     def verify(self, address):        """SMTP 'verify' command -- checks for address validity."""        self.putcmd("vrfy", quoteaddr(address))

Lines 1–500 of 743.