Skip to content

Commit 9c8c4b2

Browse files
committed
Add POP support
1 parent 0add215 commit 9c8c4b2

File tree

1 file changed

+81
-1
lines changed

1 file changed

+81
-1
lines changed

emailproxy.py

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
CENSOR_MESSAGE = b'[[ Credentials removed from proxy log ]]' # replaces credentials; must be a byte-type string
6767

6868
CONFIG_FILE_PATH = '%s/%s.config' % (os.path.dirname(os.path.realpath(__file__)), APP_SHORT_NAME)
69-
CONFIG_SERVER_MATCHER = re.compile(r'^(?P<type>(IMAP|SMTP))-(?P<port>\d+)')
69+
CONFIG_SERVER_MATCHER = re.compile(r'^(?P<type>(IMAP|SMTP|POP))-(?P<port>\d+)')
7070

7171
MAX_CONNECTIONS = 0 # maximum concurrent IMAP/SMTP connections; 0 = no limit; limit is per server
7272

@@ -807,6 +807,40 @@ def send_authentication_request(self):
807807
super().process_data(b'AUTH XOAUTH2\r\n')
808808

809809

810+
class POPOAuth2ClientConnection(OAuth2ClientConnection):
811+
"""The client side of the connection - watch for USER and PASS commands and replace with OAuth 2.0"""
812+
813+
class AUTH(enum.Enum):
814+
PENDING = 1
815+
AWAITING_PASS = 2
816+
AWAITING_AUTH = 3
817+
CREDENTIALS_SENT = 5
818+
819+
def __init__(self, connection, socket_map, connection_info, server_connection, proxy_parent, custom_configuration):
820+
super().__init__('POP', connection, socket_map, connection_info, server_connection, proxy_parent,
821+
custom_configuration)
822+
self.authentication_state = self.AUTH.PENDING
823+
824+
def process_data(self, byte_data, censor_server_log=False):
825+
str_data = byte_data.decode('utf-8', 'replace').rstrip('\r\n')
826+
str_data_lower = str_data.lower()
827+
828+
if self.authentication_state is self.AUTH.PENDING and str_data_lower.startswith('user'):
829+
self.server_connection.username = str_data[5:] # 5 = len('USER ')
830+
self.authentication_state = self.AUTH.AWAITING_PASS
831+
self.censor_next_log = True
832+
self.send(b'+OK\r\n') # request password
833+
834+
elif self.authentication_state is self.AUTH.AWAITING_PASS and str_data_lower.startswith('pass'):
835+
self.server_connection.password = str_data[5:] # 5 = len('PASS ')
836+
self.authentication_state = self.AUTH.AWAITING_AUTH
837+
super().process_data(b'AUTH XOAUTH2\r\n')
838+
839+
# some other command that we don't handle - pass directly to server
840+
else:
841+
super().process_data(byte_data)
842+
843+
810844
class OAuth2ServerConnection(asyncore.dispatcher_with_send):
811845
"""The base server-side connection, setting up STARTTLS if requested, subclassed for IMAP/SMTP server interaction"""
812846

@@ -1049,6 +1083,52 @@ def process_data(self, byte_data):
10491083
super().process_data(byte_data) # a server->client interaction we don't handle; ignore
10501084

10511085

1086+
class POPOAuth2ServerConnection(OAuth2ServerConnection):
1087+
"""The POP server side - submit credentials, then watch for +OK and ignore subsequent data"""
1088+
1089+
def __init__(self, socket_map, server_address, connection_info, proxy_parent, custom_configuration):
1090+
super().__init__('POP', socket_map, server_address, connection_info, proxy_parent, custom_configuration)
1091+
self.username = None
1092+
self.password = None
1093+
1094+
def process_data(self, byte_data):
1095+
str_data = byte_data.decode('utf-8', 'replace').rstrip('\r\n')
1096+
1097+
if self.client_connection.authentication_state is POPOAuth2ClientConnection.AUTH.AWAITING_AUTH and \
1098+
self.username is not None and self.password is not None:
1099+
if str_data.startswith('+'): # + = 'please send credentials'
1100+
(success, result) = OAuth2Helper.get_oauth2_credentials(self.username, self.password,
1101+
self.connection_info)
1102+
if success:
1103+
self.client_connection.authentication_state = POPOAuth2ClientConnection.AUTH.CREDENTIALS_SENT
1104+
self.send(b'%s\r\n' % OAuth2Helper.encode_oauth2_string(result), censor_log=True)
1105+
self.authenticated_username = self.username
1106+
1107+
self.username = None
1108+
self.password = None
1109+
if not success:
1110+
# a local authentication error occurred - send details to the client and exit
1111+
super().process_data(b'-ERR Authentication failed. %s\r\n' % result.encode('utf-8'))
1112+
self.client_connection.close()
1113+
1114+
else:
1115+
super().process_data(byte_data) # an error occurred - just send to the client and exit
1116+
self.client_connection.close()
1117+
1118+
elif self.client_connection.authentication_state is POPOAuth2ClientConnection.AUTH.CREDENTIALS_SENT:
1119+
if str_data.startswith('+OK'):
1120+
Log.info(self.proxy_type, self.connection_info,
1121+
'[ Successfully authenticated POP connection - removing proxy ]')
1122+
self.client_connection.authenticated = True
1123+
super().process_data(byte_data)
1124+
else:
1125+
super().process_data(byte_data) # an error occurred - just send to the client and exit
1126+
self.client_connection.close()
1127+
1128+
else:
1129+
super().process_data(byte_data) # a server->client interaction we don't handle; ignore
1130+
1131+
10521132
class OAuth2Proxy(asyncore.dispatcher):
10531133
"""Listen on local_address, creating an OAuth2ServerConnection + OAuth2ClientConnection for each new connection"""
10541134

0 commit comments

Comments
 (0)