6
6
__author__ = 'Simon Robinson'
7
7
__copyright__ = 'Copyright (c) 2024 Simon Robinson'
8
8
__license__ = 'Apache 2.0'
9
- __version__ = '2024-01-20 ' # ISO 8601 (YYYY-MM-DD)
9
+ __version__ = '2024-02-15 ' # ISO 8601 (YYYY-MM-DD)
10
10
__package_version__ = '.' .join ([str (int (i )) for i in __version__ .split ('-' )]) # for pyproject.toml usage only
11
11
12
12
import abc
@@ -1416,11 +1416,17 @@ class IMAPOAuth2ClientConnection(OAuth2ClientConnection):
1416
1416
1417
1417
def __init__ (self , connection_socket , socket_map , proxy_parent , custom_configuration ):
1418
1418
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 ):
1419
1423
self .authentication_tag = None
1420
1424
self .authentication_command = None
1421
1425
self .awaiting_credentials = False
1422
1426
self .login_literal_length_awaited = 0
1423
1427
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
1424
1430
1425
1431
def process_data (self , byte_data , censor_server_log = False ):
1426
1432
str_data = byte_data .decode ('utf-8' , 'replace' ).rstrip ('\r \n ' )
@@ -1525,11 +1531,11 @@ def authenticate_connection(self, username, password, command='login'):
1525
1531
if self .server_connection :
1526
1532
self .server_connection .authenticated_username = username
1527
1533
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 )
1530
1538
self .send (error_message .encode ('utf-8' ))
1531
- self .send (b'* BYE Autologout; authentication failed\r \n ' )
1532
- self .close ()
1533
1539
1534
1540
1535
1541
class POPOAuth2ClientConnection (OAuth2ClientConnection ):
@@ -1605,8 +1611,10 @@ def send_authentication_request(self):
1605
1611
self .connection_state = self .STATE .XOAUTH2_AWAITING_CONFIRMATION
1606
1612
super ().process_data (b'AUTH XOAUTH2\r \n ' )
1607
1613
else :
1614
+ self .server_connection .username = None
1615
+ self .server_connection .password = None
1616
+ self .connection_state = self .STATE .PENDING
1608
1617
self .send (b'-ERR Authentication failed.\r \n ' )
1609
- self .close ()
1610
1618
1611
1619
1612
1620
class SMTPOAuth2ClientConnection (OAuth2ClientConnection ):
@@ -1690,8 +1698,10 @@ def send_authentication_request(self):
1690
1698
self .connection_state = self .STATE .XOAUTH2_AWAITING_CONFIRMATION
1691
1699
super ().process_data (b'AUTH XOAUTH2\r \n ' )
1692
1700
else :
1701
+ self .server_connection .username = None
1702
+ self .server_connection .password = None
1703
+ self .connection_state = self .STATE .PENDING
1693
1704
self .send (b'535 5.7.8 Authentication credentials invalid.\r \n ' )
1694
- self .close ()
1695
1705
1696
1706
1697
1707
class OAuth2ServerConnection (SSLAsyncoreDispatcher ):
@@ -1890,6 +1900,7 @@ def __init__(self, connection_socket, socket_map, proxy_parent, custom_configura
1890
1900
self .capa = []
1891
1901
self .username = None
1892
1902
self .password = None
1903
+ self .auth_error_result = None
1893
1904
1894
1905
def process_data (self , byte_data ):
1895
1906
# 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):
1914
1925
if capa_lower == 'user' :
1915
1926
has_user = True
1916
1927
super ().process_data (b'%s\r \n ' % capa .encode ('utf-8' ))
1928
+ self .capa = []
1917
1929
1918
1930
if not has_sasl :
1919
1931
super ().process_data (b'SASL PLAIN\r \n ' )
@@ -1930,16 +1942,25 @@ def process_data(self, byte_data):
1930
1942
if str_data .startswith ('+' ) and self .username and self .password : # '+ ' = 'please send credentials'
1931
1943
success , result = OAuth2Helper .get_oauth2_credentials (self .username , self .password )
1932
1944
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 )
1934
1949
self .send (b'%s\r \n ' % OAuth2Helper .encode_oauth2_string (result ), censor_log = True )
1935
1950
self .authenticated_username = self .username
1936
1951
1937
1952
self .username = None
1938
1953
self .password = None
1939
1954
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' ))
1943
1964
1944
1965
else :
1945
1966
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
1980
2001
1981
2002
self .username = None
1982
2003
self .password = None
2004
+ self .auth_error_result = None
1983
2005
1984
2006
def process_data (self , byte_data ):
1985
2007
# 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):
2017
2039
're-sending greeting ]' )
2018
2040
self .client_connection .connection_state = SMTPOAuth2ClientConnection .STATE .EHLO_AWAITING_RESPONSE
2019
2041
self .send (self .ehlo ) # re-send original EHLO/HELO to server (includes domain, so can't just be generic)
2042
+ self .ehlo = None
2020
2043
else :
2021
2044
super ().process_data (byte_data ) # an error occurred - just send to the client and exit
2022
2045
self .close ()
@@ -2026,17 +2049,28 @@ def process_data(self, byte_data):
2026
2049
if str_data .startswith ('334' ) and self .username and self .password : # '334 ' = 'please send credentials'
2027
2050
success , result = OAuth2Helper .get_oauth2_credentials (self .username , self .password )
2028
2051
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 )
2030
2056
self .authenticated_username = self .username
2031
2057
self .send (b'%s\r \n ' % OAuth2Helper .encode_oauth2_string (result ), censor_log = True )
2032
2058
2033
2059
self .username = None
2034
2060
self .password = None
2035
2061
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
2037
2072
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' ))
2040
2074
2041
2075
else :
2042
2076
super ().process_data (byte_data ) # an error occurred - just send to the client and exit
0 commit comments