Performs an authentication attemps using the CRAM-MD5 mechanism.

        Protocol:

            1. Send 'AUTH CRAM-MD5' to server ;
            2. If the server replies with a 334 return code, we can go on:

                1) The challenge (sent by the server) is base64-decoded ;
                2) The decoded challenge is hashed using HMAC-MD5 and the user
                   password as key (shared secret) ;
                3) The hashed challenge is converted to a string of lowercase
                   hexadecimal digits ;
                4) The username and a space character are prepended to the hex
                   digits ;
                5) The concatenation is base64-encoded and sent to the server.
                6) If the server replies with a return code of 235, user is
                   authenticated.

        Args:
            username (str): Identifier of the user trying to authenticate.
            password (str): Password for the user.

        Raises:
            ConnectionResetError: If the connection with the server is
                unexpectedely lost.
            SMTPAuthenticationError: If the authentication attempt fails.

        Returns:
            (int, str): A (code, message) 2-tuple containing the server
                response.

async def _auth_cram_md5(self, username, password):
        """
        Performs an authentication attemps using the CRAM-MD5 mechanism.

        Protocol:

            1. Send 'AUTH CRAM-MD5' to server ;
            2. If the server replies with a 334 return code, we can go on:

                1) The challenge (sent by the server) is base64-decoded ;
                2) The decoded challenge is hashed using HMAC-MD5 and the user
                   password as key (shared secret) ;
                3) The hashed challenge is converted to a string of lowercase
                   hexadecimal digits ;
                4) The username and a space character are prepended to the hex
                   digits ;
                5) The concatenation is base64-encoded and sent to the server.
                6) If the server replies with a return code of 235, user is
                   authenticated.

        Args:
            username (str): Identifier of the user trying to authenticate.
            password (str): Password for the user.

        Raises:
            ConnectionResetError: If the connection with the server is
                unexpectedely lost.
            SMTPAuthenticationError: If the authentication attempt fails.

        Returns:
            (int, str): A (code, message) 2-tuple containing the server
                response.
        """
        mechanism = "CRAM-MD5"

        code, message = await self.do_cmd("AUTH", mechanism, success=(334,))

        decoded_challenge = base64.b64decode(message)

        challenge_hash = hmac.new(
            key=password.encode("utf-8"), msg=decoded_challenge, digestmod="md5"
        )

        hex_hash = challenge_hash.hexdigest()
        response = "{} {}".format(username, hex_hash)
        encoded_response = SMTP.b64enc(response)

        try:
            code, message = await self.do_cmd(encoded_response, success=(235, 503))
        except SMTPCommandFailedError as e:
            raise SMTPAuthenticationError(e.code, e.message, mechanism)

        return code, message