Example #1
0
    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)
Example #2
0
    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)
Example #3
0
 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
Example #4
0
 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())
Example #5
0
    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")
Example #6
0
 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)
Example #7
0
    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())
Example #8
0
    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
Example #9
0
    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
Example #10
0
    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")
Example #11
0
    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__"