Add login() method and SMTPAuthenticationError exception.  SF patch
#460112 by Gerhard Haering.

(With slight layout changes to conform to docstrings guidelines and to
prevent a line longer than 78 characters.  Also fixed some docstrings
that Gerhard didn't touch.)
diff --git a/Lib/smtplib.py b/Lib/smtplib.py
index 61b733e..d64fa20 100755
--- a/Lib/smtplib.py
+++ b/Lib/smtplib.py
@@ -2,7 +2,8 @@
 
 '''SMTP/ESMTP client class.
 
-This should follow RFC 821 (SMTP) and RFC 1869 (ESMTP).
+This should follow RFC 821 (SMTP), RFC 1869 (ESMTP) and RFC 2554 (SMTP
+Authentication).
 
 Notes:
 
@@ -36,6 +37,7 @@
 #     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.
 
@@ -43,11 +45,13 @@
 import re
 import rfc822
 import types
+import base64
+import hmac
 
 __all__ = ["SMTPException","SMTPServerDisconnected","SMTPResponseException",
            "SMTPSenderRefused","SMTPRecipientsRefused","SMTPDataError",
-           "SMTPConnectError","SMTPHeloError","quoteaddr","quotedata",
-           "SMTP"]
+           "SMTPConnectError","SMTPHeloError","SMTPAuthenticationError",
+           "quoteaddr","quotedata","SMTP"]
 
 SMTP_PORT = 25
 CRLF="\r\n"
@@ -80,6 +84,7 @@
 
 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.
     """
@@ -92,6 +97,7 @@
 
 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.
@@ -111,6 +117,12 @@
 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.
+    """
 
 def quoteaddr(addr):
     """Quote a subset of the email addresses defined by RFC 821.
@@ -416,6 +428,84 @@
         return self.getreply()
 
     # some useful methods
+
+    def login(self, user, password):
+        """Log in on an SMTP server that requires authentication.
+
+        The arguments are:
+            - user:     The user name to authenticate with.
+            - password: The password for the authentication.
+
+        If there has been no previous EHLO or HELO command this session, this
+        method tries ESMTP EHLO first.
+
+        This method will return normally if the authentication was successful.
+
+        This method may raise the following exceptions:
+
+         SMTPHeloError            The server didn't reply properly to
+                                  the helo greeting.
+         SMTPAuthenticationError  The server didn't accept the username/
+                                  password combination.
+         SMTPError                No suitable authentication method was
+                                  found.
+        """
+
+        def encode_cram_md5(challenge, user, password):
+            challenge = base64.decodestring(challenge)
+            response = user + " " + hmac.HMAC(password, challenge).hexdigest()
+            return base64.encodestring(response)[:-1]
+
+        def encode_plain(user, password):
+            return base64.encodestring("%s\0%s\0%s" %
+                                       (user, user, password))[:-1]
+
+        AUTH_PLAIN = "PLAIN"
+        AUTH_CRAM_MD5 = "CRAM-MD5"
+
+        if self.helo_resp is None and self.ehlo_resp is None:
+            if not (200 <= self.ehlo()[0] <= 299):
+                (code, resp) = self.helo()
+                if not (200 <= code <= 299):
+                    raise SMTPHeloError(code, resp)
+
+        if not self.has_extn("auth"):
+            raise SMTPException("SMTP AUTH extension not supported by server.")
+
+        # Authentication methods the server supports:
+        authlist = self.esmtp_features["auth"].split()
+
+        # List of authentication methods we support: from preferred to
+        # less preferred methods. Except for the purpose of testing the weaker
+        # ones, we prefer stronger methods like CRAM-MD5:
+        preferred_auths = [AUTH_CRAM_MD5, AUTH_PLAIN]
+        #preferred_auths = [AUTH_PLAIN, AUTH_CRAM_MD5]
+
+        # Determine the authentication method we'll use
+        authmethod = None
+        for method in preferred_auths:
+            if method in authlist:
+                authmethod = method
+                break
+        if self.debuglevel > 0: print "AuthMethod:", authmethod
+         
+        if authmethod == AUTH_CRAM_MD5:
+            (code, resp) = self.docmd("AUTH", AUTH_CRAM_MD5)
+            if code == 503:
+                # 503 == 'Error: already authenticated'
+                return (code, resp)
+            (code, resp) = self.docmd(encode_cram_md5(resp, user, password))
+        elif authmethod == AUTH_PLAIN:
+            (code, resp) = self.docmd("AUTH", 
+                AUTH_PLAIN + " " + encode_plain(user, password))
+        elif authmethod == None:
+            raise SMTPError("No suitable authentication method found.")
+        if code not in [235, 503]:
+            # 235 == 'Authentication successful'
+            # 503 == 'Error: already authenticated'
+            raise SMTPAuthenticationError(code, resp)
+        return (code, resp)
+
     def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
                  rcpt_options=[]):
         """This command performs an entire mail transaction.