66__author__ = 'Simon Robinson'
77__copyright__ = 'Copyright (c) 2024 Simon Robinson'
88__license__ = 'Apache 2.0'
9- __version__ = '2024-01-20 ' # ISO 8601 (YYYY-MM-DD)
9+ __version__ = '2024-02-15 ' # ISO 8601 (YYYY-MM-DD)
1010__package_version__ = '.' .join ([str (int (i )) for i in __version__ .split ('-' )]) # for pyproject.toml usage only
1111
1212import abc
@@ -1416,11 +1416,17 @@ class IMAPOAuth2ClientConnection(OAuth2ClientConnection):
14161416
14171417 def __init__ (self , connection_socket , socket_map , proxy_parent , custom_configuration ):
14181418 super ().__init__ ('IMAP' , connection_socket , socket_map , proxy_parent , custom_configuration )
1419+ (self .authentication_tag , self .authentication_command , self .awaiting_credentials ,
1420+ self .login_literal_length_awaited , self .login_literal_username ) = self .reset_login_state ()
1421+
1422+ def reset_login_state (self ):
14191423 self .authentication_tag = None
14201424 self .authentication_command = None
14211425 self .awaiting_credentials = False
14221426 self .login_literal_length_awaited = 0
14231427 self .login_literal_username = None
1428+ return (self .authentication_tag , self .authentication_command , self .awaiting_credentials ,
1429+ self .login_literal_length_awaited , self .login_literal_username ) # avoid defining outside init complaint
14241430
14251431 def process_data (self , byte_data , censor_server_log = False ):
14261432 str_data = byte_data .decode ('utf-8' , 'replace' ).rstrip ('\r \n ' )
@@ -1525,11 +1531,11 @@ def authenticate_connection(self, username, password, command='login'):
15251531 if self .server_connection :
15261532 self .server_connection .authenticated_username = username
15271533
1528- else :
1529- error_message = '%s NO %s %s\r \n ' % (self .authentication_tag , command .upper (), result )
1534+ error_authentication_tag = self .authentication_tag
1535+ self .reset_login_state ()
1536+ if not success :
1537+ error_message = '%s NO %s %s\r \n ' % (error_authentication_tag , command .upper (), result )
15301538 self .send (error_message .encode ('utf-8' ))
1531- self .send (b'* BYE Autologout; authentication failed\r \n ' )
1532- self .close ()
15331539
15341540
15351541class POPOAuth2ClientConnection (OAuth2ClientConnection ):
@@ -1605,8 +1611,10 @@ def send_authentication_request(self):
16051611 self .connection_state = self .STATE .XOAUTH2_AWAITING_CONFIRMATION
16061612 super ().process_data (b'AUTH XOAUTH2\r \n ' )
16071613 else :
1614+ self .server_connection .username = None
1615+ self .server_connection .password = None
1616+ self .connection_state = self .STATE .PENDING
16081617 self .send (b'-ERR Authentication failed.\r \n ' )
1609- self .close ()
16101618
16111619
16121620class SMTPOAuth2ClientConnection (OAuth2ClientConnection ):
@@ -1690,8 +1698,10 @@ def send_authentication_request(self):
16901698 self .connection_state = self .STATE .XOAUTH2_AWAITING_CONFIRMATION
16911699 super ().process_data (b'AUTH XOAUTH2\r \n ' )
16921700 else :
1701+ self .server_connection .username = None
1702+ self .server_connection .password = None
1703+ self .connection_state = self .STATE .PENDING
16931704 self .send (b'535 5.7.8 Authentication credentials invalid.\r \n ' )
1694- self .close ()
16951705
16961706
16971707class OAuth2ServerConnection (SSLAsyncoreDispatcher ):
@@ -1890,6 +1900,7 @@ def __init__(self, connection_socket, socket_map, proxy_parent, custom_configura
18901900 self .capa = []
18911901 self .username = None
18921902 self .password = None
1903+ self .auth_error_result = None
18931904
18941905 def process_data (self , byte_data ):
18951906 # note: there is no reason why POP STARTTLS (https://tools.ietf.org/html/rfc2595) couldn't be supported here
@@ -1914,6 +1925,7 @@ def process_data(self, byte_data):
19141925 if capa_lower == 'user' :
19151926 has_user = True
19161927 super ().process_data (b'%s\r \n ' % capa .encode ('utf-8' ))
1928+ self .capa = []
19171929
19181930 if not has_sasl :
19191931 super ().process_data (b'SASL PLAIN\r \n ' )
@@ -1930,16 +1942,25 @@ def process_data(self, byte_data):
19301942 if str_data .startswith ('+' ) and self .username and self .password : # '+ ' = 'please send credentials'
19311943 success , result = OAuth2Helper .get_oauth2_credentials (self .username , self .password )
19321944 if success :
1933- self .client_connection .connection_state = POPOAuth2ClientConnection .STATE .XOAUTH2_CREDENTIALS_SENT
1945+ # because get_oauth2_credentials blocks, the client could have disconnected, and may no-longer exist
1946+ if self .client_connection :
1947+ self .client_connection .connection_state = (
1948+ POPOAuth2ClientConnection .STATE .XOAUTH2_CREDENTIALS_SENT )
19341949 self .send (b'%s\r \n ' % OAuth2Helper .encode_oauth2_string (result ), censor_log = True )
19351950 self .authenticated_username = self .username
19361951
19371952 self .username = None
19381953 self .password = None
19391954 if not success :
1940- # a local authentication error occurred - send details to the client and exit
1941- super ().process_data (b'-ERR Authentication failed. %s\r \n ' % result .encode ('utf-8' ))
1942- self .close ()
1955+ # a local authentication error occurred - cancel then (on confirmation) send details to the client
1956+ self .send (b'*\r \n ' ) # RFC 5034, Section 4
1957+ self .auth_error_result = result
1958+
1959+ elif str_data .startswith ('-ERR' ) and not self .username and not self .password :
1960+ self .client_connection .connection_state = POPOAuth2ClientConnection .STATE .PENDING
1961+ error_message = self .auth_error_result if self .auth_error_result else ''
1962+ self .auth_error_result = None
1963+ super ().process_data (b'-ERR Authentication failed. %s\r \n ' % error_message .encode ('utf-8' ))
19431964
19441965 else :
19451966 super ().process_data (byte_data ) # an error occurred - just send to the client and exit
@@ -1980,6 +2001,7 @@ def __init__(self, connection_socket, socket_map, proxy_parent, custom_configura
19802001
19812002 self .username = None
19822003 self .password = None
2004+ self .auth_error_result = None
19832005
19842006 def process_data (self , byte_data ):
19852007 # SMTP setup/authentication involves a little more back-and-forth than IMAP/POP as the default is STARTTLS...
@@ -2017,6 +2039,7 @@ def process_data(self, byte_data):
20172039 're-sending greeting ]' )
20182040 self .client_connection .connection_state = SMTPOAuth2ClientConnection .STATE .EHLO_AWAITING_RESPONSE
20192041 self .send (self .ehlo ) # re-send original EHLO/HELO to server (includes domain, so can't just be generic)
2042+ self .ehlo = None
20202043 else :
20212044 super ().process_data (byte_data ) # an error occurred - just send to the client and exit
20222045 self .close ()
@@ -2026,17 +2049,28 @@ def process_data(self, byte_data):
20262049 if str_data .startswith ('334' ) and self .username and self .password : # '334 ' = 'please send credentials'
20272050 success , result = OAuth2Helper .get_oauth2_credentials (self .username , self .password )
20282051 if success :
2029- self .client_connection .connection_state = SMTPOAuth2ClientConnection .STATE .XOAUTH2_CREDENTIALS_SENT
2052+ # because get_oauth2_credentials blocks, the client could have disconnected, and may no-longer exist
2053+ if self .client_connection :
2054+ self .client_connection .connection_state = (
2055+ SMTPOAuth2ClientConnection .STATE .XOAUTH2_CREDENTIALS_SENT )
20302056 self .authenticated_username = self .username
20312057 self .send (b'%s\r \n ' % OAuth2Helper .encode_oauth2_string (result ), censor_log = True )
20322058
20332059 self .username = None
20342060 self .password = None
20352061 if not success :
2036- # a local authentication error occurred - send details to the client and exit
2062+ # a local authentication error occurred - cancel then (on confirmation) send details to the client
2063+ self .send (b'*\r \n ' ) # RFC 4954, Section 4
2064+ self .auth_error_result = result
2065+
2066+ # note that RFC 4954 says that the server must respond with '501', but some (e.g., Office 365) return '535'
2067+ elif str_data .startswith ('5' ) and not self .username and not self .password :
2068+ if len (str_data ) >= 4 and str_data [3 ] == ' ' : # responses may be multiline - wait for last part
2069+ self .client_connection .connection_state = SMTPOAuth2ClientConnection .STATE .PENDING
2070+ error_message = self .auth_error_result if self .auth_error_result else ''
2071+ self .auth_error_result = None
20372072 super ().process_data (
2038- b'535 5.7.8 Authentication credentials invalid. %s\r \n ' % result .encode ('utf-8' ))
2039- self .close ()
2073+ b'535 5.7.8 Authentication credentials invalid. %s\r \n ' % error_message .encode ('utf-8' ))
20402074
20412075 else :
20422076 super ().process_data (byte_data ) # an error occurred - just send to the client and exit
0 commit comments