@@ -571,12 +571,60 @@ def ehlo_or_helo_if_needed(self):
571571 if not (200 <= code <= 299 ):
572572 raise SMTPHeloError (code , resp )
573573
574+ def auth (self , mechanism , authobject ):
575+ """Authentication command - requires response processing.
576+
577+ 'mechanism' specifies which authentication mechanism is to
578+ be used - the valid values are those listed in the 'auth'
579+ element of 'esmtp_features'.
580+
581+ 'authobject' must be a callable object taking a single argument:
582+
583+ data = authobject(challenge)
584+
585+ It will be called to process the server's challenge response; the
586+ challenge argument it is passed will be a bytes. It should return
587+ bytes data that will be base64 encoded and sent to the server.
588+ """
589+
590+ mechanism = mechanism .upper ()
591+ (code , resp ) = self .docmd ("AUTH" , mechanism )
592+ # Server replies with 334 (challenge) or 535 (not supported)
593+ if code == 334 :
594+ challenge = base64 .decodebytes (resp )
595+ response = encode_base64 (
596+ authobject (challenge ).encode ('ascii' ), eol = '' )
597+ (code , resp ) = self .docmd (response )
598+ if code in (235 , 503 ):
599+ return (code , resp )
600+ raise SMTPAuthenticationError (code , resp )
601+
602+ def auth_cram_md5 (self , challenge ):
603+ """ Authobject to use with CRAM-MD5 authentication. Requires self.user
604+ and self.password to be set."""
605+ return self .user + " " + hmac .HMAC (
606+ self .password .encode ('ascii' ), challenge , 'md5' ).hexdigest ()
607+
608+ def auth_plain (self , challenge ):
609+ """ Authobject to use with PLAIN authentication. Requires self.user and
610+ self.password to be set."""
611+ return "\0 %s\0 %s" % (self .user , self .password )
612+
613+ def auth_login (self , challenge ):
614+ """ Authobject to use with LOGIN authentication. Requires self.user and
615+ self.password to be set."""
616+ (code , resp ) = self .docmd (
617+ encode_base64 (self .user .encode ('ascii' ), eol = '' ))
618+ if code == 334 :
619+ return self .password
620+ raise SMTPAuthenticationError (code , resp )
621+
574622 def login (self , user , password ):
575623 """Log in on an SMTP server that requires authentication.
576624
577625 The arguments are:
578- - user: The user name to authenticate with.
579- - password: The password for the authentication.
626+ - user: The user name to authenticate with.
627+ - password: The password for the authentication.
580628
581629 If there has been no previous EHLO or HELO command this session, this
582630 method tries ESMTP EHLO first.
@@ -593,63 +641,40 @@ def login(self, user, password):
593641 found.
594642 """
595643
596- def encode_cram_md5 (challenge , user , password ):
597- challenge = base64 .decodebytes (challenge )
598- response = user + " " + hmac .HMAC (password .encode ('ascii' ),
599- challenge , 'md5' ).hexdigest ()
600- return encode_base64 (response .encode ('ascii' ), eol = '' )
601-
602- def encode_plain (user , password ):
603- s = "\0 %s\0 %s" % (user , password )
604- return encode_base64 (s .encode ('ascii' ), eol = '' )
605-
606- AUTH_PLAIN = "PLAIN"
607- AUTH_CRAM_MD5 = "CRAM-MD5"
608- AUTH_LOGIN = "LOGIN"
609-
610644 self .ehlo_or_helo_if_needed ()
611-
612645 if not self .has_extn ("auth" ):
613646 raise SMTPException ("SMTP AUTH extension not supported by server." )
614647
615648 # Authentication methods the server claims to support
616649 advertised_authlist = self .esmtp_features ["auth" ].split ()
617650
618- # List of authentication methods we support: from preferred to
619- # less preferred methods. Except for the purpose of testing the weaker
620- # ones, we prefer stronger methods like CRAM-MD5:
621- preferred_auths = [AUTH_CRAM_MD5 , AUTH_PLAIN , AUTH_LOGIN ]
651+ # Authentication methods we can handle in our preferred order:
652+ preferred_auths = ['CRAM-MD5' , 'PLAIN' , 'LOGIN' ]
622653
623- # We try the authentication methods the server advertises, but only the
624- # ones *we* support. And in our preferred order.
625- authlist = [auth for auth in preferred_auths if auth in advertised_authlist ]
654+ # We try the supported authentications in our preferred order, if
655+ # the server supports them.
656+ authlist = [auth for auth in preferred_auths
657+ if auth in advertised_authlist ]
626658 if not authlist :
627659 raise SMTPException ("No suitable authentication method found." )
628660
629661 # Some servers advertise authentication methods they don't really
630662 # support, so if authentication fails, we continue until we've tried
631663 # all methods.
664+ self .user , self .password = user , password
632665 for authmethod in authlist :
633- if authmethod == AUTH_CRAM_MD5 :
634- (code , resp ) = self .docmd ("AUTH" , AUTH_CRAM_MD5 )
635- if code == 334 :
636- (code , resp ) = self .docmd (encode_cram_md5 (resp , user , password ))
637- elif authmethod == AUTH_PLAIN :
638- (code , resp ) = self .docmd ("AUTH" ,
639- AUTH_PLAIN + " " + encode_plain (user , password ))
640- elif authmethod == AUTH_LOGIN :
641- (code , resp ) = self .docmd ("AUTH" ,
642- "%s %s" % (AUTH_LOGIN , encode_base64 (user .encode ('ascii' ), eol = '' )))
643- if code == 334 :
644- (code , resp ) = self .docmd (encode_base64 (password .encode ('ascii' ), eol = '' ))
645-
646- # 235 == 'Authentication successful'
647- # 503 == 'Error: already authenticated'
648- if code in (235 , 503 ):
649- return (code , resp )
650-
651- # We could not login sucessfully. Return result of last attempt.
652- raise SMTPAuthenticationError (code , resp )
666+ method_name = 'auth_' + authmethod .lower ().replace ('-' , '_' )
667+ try :
668+ (code , resp ) = self .auth (authmethod , getattr (self , method_name ))
669+ # 235 == 'Authentication successful'
670+ # 503 == 'Error: already authenticated'
671+ if code in (235 , 503 ):
672+ return (code , resp )
673+ except SMTPAuthenticationError as e :
674+ last_exception = e
675+
676+ # We could not login successfully. Return result of last attempt.
677+ raise last_exception
653678
654679 def starttls (self , keyfile = None , certfile = None , context = None ):
655680 """Puts the connection to the SMTP server into TLS mode.
0 commit comments