[Python-checkins] CVS: python/dist/src/Lib smtplib.py,1.40,1.41

Guido van Rossum gvanrossum@users.sourceforge.net
Tue, 11 Sep 2001 08:57:48 -0700


Update of /cvsroot/python/python/dist/src/Lib
In directory usw-pr-cvs1:/tmp/cvs-serv541

Modified Files:
	smtplib.py 
Log Message:
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.)


Index: smtplib.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/smtplib.py,v
retrieving revision 1.40
retrieving revision 1.41
diff -C2 -d -r1.40 -r1.41
*** smtplib.py	2001/08/13 14:41:39	1.40
--- smtplib.py	2001/09/11 15:57:46	1.41
***************
*** 3,7 ****
  '''SMTP/ESMTP client class.
  
! This should follow RFC 821 (SMTP) and RFC 1869 (ESMTP).
  
  Notes:
--- 3,8 ----
  '''SMTP/ESMTP client class.
  
! This should follow RFC 821 (SMTP), RFC 1869 (ESMTP) and RFC 2554 (SMTP
! Authentication).
  
  Notes:
***************
*** 37,40 ****
--- 38,42 ----
  # 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.
***************
*** 44,52 ****
  import rfc822
  import types
  
  __all__ = ["SMTPException","SMTPServerDisconnected","SMTPResponseException",
             "SMTPSenderRefused","SMTPRecipientsRefused","SMTPDataError",
!            "SMTPConnectError","SMTPHeloError","quoteaddr","quotedata",
!            "SMTP"]
  
  SMTP_PORT = 25
--- 46,56 ----
  import rfc822
  import types
+ import base64
+ import hmac
  
  __all__ = ["SMTPException","SMTPServerDisconnected","SMTPResponseException",
             "SMTPSenderRefused","SMTPRecipientsRefused","SMTPDataError",
!            "SMTPConnectError","SMTPHeloError","SMTPAuthenticationError",
!            "quoteaddr","quotedata","SMTP"]
  
  SMTP_PORT = 25
***************
*** 81,84 ****
--- 85,89 ----
  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.
***************
*** 93,96 ****
--- 98,102 ----
  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
***************
*** 112,115 ****
--- 118,127 ----
      """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):
***************
*** 417,420 ****
--- 429,510 ----
  
      # 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=[]):