def test_write(self): mock_socket = get_mock_socket() socket_wrapper = SMTPSocket(mock_socket) test_data = b"abc123\r\n" socket_wrapper.write(test_data) mock_socket.send.assert_called_with(test_data)
def test_read(self): test_data = b"abc123\r\nxyz789\r\n" mock_socket = get_mock_socket() mock_socket.recv.configure_mock(return_value=test_data) socket_wrapper = SMTPSocket(mock_socket) result = socket_wrapper.read() self.assertEqual(result, test_data)
def _read_multiline_response(self, smtp_socket: SMTPSocket) -> bytes: line = smtp_socket.read_line() data = line while line[3] != 32: # space line = smtp_socket.read_line() if len(line) == 0: break data += line return data
def _send_response(self, smtp_socket: SMTPSocket, response: BaseResponse): if self._shared_state.esmtp_capable: self.__logger.info("%s Sending extended response to client with SMTP code %s", self._shared_state.transaction_id, response.get_code()) smtp_socket.write(response.get_extended_smtp_response().replace("<domain>", self.__server_name).encode()) else: self.__logger.info("%s Sending response to client with SMTP code %s", self._shared_state.transaction_id, response.get_code()) smtp_socket.write(response.get_smtp_response().replace("<domain>", self.__server_name).encode())
def test_read_line_limit(self): mock_socket = get_mock_socket() mock_socket.recv.configure_mock(return_value=b"abcd1234") socket_wrapper = SMTPSocket(mock_socket) try: socket_wrapper.read_line() except LineLengthExceededException as e: pass except Exception as e: self.fail("Expected exception not raised")
def _connect_to_remote_server(self, shared_state: SharedState) -> (socket, BaseResponse): config = self._load_config(self._default_config) sock = socket() self._logger.info("Connecting to {} port {}".format(config["host"], config["port"])) sock.connect((config["host"], config["port"])) shared_state.proxy["socket"] = sock smtp_socket = SMTPSocket(sock) shared_state.proxy["smtp_socket"] = smtp_socket # TODO: Handle UTF-8 data = smtp_socket.read_line().decode("US-ASCII") return sock, self._parse_and_generate_response(data, shared_state)
def test_buffer_is_empty(self): mock_socket = get_mock_socket() mock_socket.recv.configure_mock(return_value=b"abc123\r\nxyz789\r\n") socket_wrapper = SMTPSocket(mock_socket) socket_wrapper.read_line() self.assertFalse(socket_wrapper.buffer_is_empty()) socket_wrapper.read_line() self.assertTrue(socket_wrapper.buffer_is_empty())
def _read_data(self, smtp_socket: SMTPSocket) -> (str, bool, bool): # Reading SMTP data is done line-by-line to enforce data lengths and handle data termination # TODO: Enforce data line length line = smtp_socket.read_line() data_end = line.rstrip() == b"." # If a line begins with a period, remove it (RFC 5321 4.5.2) if len(line) and line[0] == b".": data_chunk = line[1:] return line, data_end
def _read_command(self, smtp_socket: SMTPSocket) -> (str, str): # TODO: Enforce line length line_bytes = smtp_socket.read_line() self._shared_state.last_command_has_standard_line_ending = line_bytes[-2:] == b"\r\n" try: # TODO: Handle non-ASCII encoding line = line_bytes.decode("US-ASCII").strip() # Split the command on the first space split_line = line.split(" ", maxsplit=1) command = split_line[0] if len(split_line) == 2: argument = split_line[1].strip() else: argument = "" except Exception as ex: command = None argument = None self.__logger.info("Unable to read incoming command") self.__logger.info(ex, exc_info=True) return command, argument
def test_read_line(self): mock_socket = get_mock_socket() mock_socket.recv.configure_mock(return_value=b"abc123\r\nxyz789\r\n") socket_wrapper = SMTPSocket(mock_socket) result = socket_wrapper.read_line() self.assertEqual(result, b"abc123\r\n") result = socket_wrapper.read_line() self.assertEqual(result, b"xyz789\r\n") mock_socket.recv.reset_mock() mock_socket.recv.configure_mock(return_value=b"abc123\nxyz789\r\n") result = socket_wrapper.read_line() self.assertEqual(result, b"abc123\n") result = socket_wrapper.read_line() self.assertEqual(result, b"xyz789\r\n")
def handle_client(self, sock: socket, remote_address, tls: TLS): smtp_socket = SMTPSocket(sock) # Load the worker and handler configurations command_handler_name = self._get_worker_config() self._handler_config = self._get_handler_config(command_handler_name) # Initialize the shared state self._shared_state = SharedState(remote_address, tls.enabled()) self.__logger.info("%s Starting SMTP session with %s:%s", self._shared_state.transaction_id, self._shared_state.remote_ip, self._shared_state.remote_port) # Initialize the main command loop with the __OPEN__ command command = "__OPEN__" argument = None # This is the main command loop; it can be told to handle a particular command or, if None, wait for the client self.__logger.debug("Entering main command loop") while True: if command is None: # Read the next command from the client try: command, argument = self._read_command(smtp_socket) except RemoteConnectionClosedException as e: self.__logger.warning("Connection closed unexpectedly by client") return self.__logger.debug("%s Received command \"%s\" with argument of \"%s\"", self._shared_state.transaction_id, command, argument) # Run the command response = None if command is None: response = SmtpResponse500() elif command == "__DATA__": response = self._handle_data(smtp_socket) else: response = self._handle_command(command, argument, smtp_socket.buffer_is_empty()) if response is None: self.__logger.warning("%s Command handlers for %s command did not provide a response; issuing " "451 response to client and closing connection", self._shared_state.transaction_id, command) response = SmtpResponse451() if response.get_action() == FORCE_CLOSE: self.__logger.info("%s Forcefully ending SMTP session with %s:%s as requested by command handler", self._shared_state.transaction_id, self._shared_state.remote_ip, self._shared_state.remote_port) return elif response.get_action() == STARTTLS: if tls.enabled(): self._send_response(smtp_socket, response) ssl_socket, response, server_name = tls.start(sock) if not response: self._shared_state.tls_enabled = True self.__server_name = server_name sock = ssl_socket smtp_socket = SMTPSocket(ssl_socket) self.__logger.info("TLS successfully initialized") command = None continue else: response = SmtpResponse500() self._send_response(smtp_socket, response) # Clear the command for the next loop iteration command = None # TODO: Handle all actions if response.get_action() == CLOSE: self.__logger.info("%s Ending SMTP session with %s:%s as requested by command handler", self._shared_state.transaction_id, self._shared_state.remote_ip, self._shared_state.remote_port) return elif response.get_action() == FORCE_CLOSE: return elif response.get_action() == CONTINUE: # Get the data in the next iteration command = "__DATA__"