diff --git a/kairon/api/app/routers/bot/channels.py b/kairon/api/app/routers/bot/channels.py index abf708d9e..3d6bf1af6 100644 --- a/kairon/api/app/routers/bot/channels.py +++ b/kairon/api/app/routers/bot/channels.py @@ -32,7 +32,6 @@ async def add_channel_config( ) return Response(message='Channel added', data=channel_endpoint) - @router.get("/params", response_model=Response) async def channels_params( current_user: User = Security(Authentication.get_current_user_and_bot, scopes=DESIGNER_ACCESS) diff --git a/kairon/shared/channels/mail/processor.py b/kairon/shared/channels/mail/processor.py index b7003232a..20a69e5a0 100644 --- a/kairon/shared/channels/mail/processor.py +++ b/kairon/shared/channels/mail/processor.py @@ -55,6 +55,26 @@ def get_mail_channel_state_data(bot:str): except Exception as e: raise AppException(str(e)) + @staticmethod + def check_email_config_exists(config_dict: dict) -> bool: + """ + Check if email configuration already exists + :param config_dict: dict - email configuration + :return status: bool True if configuration exists, False otherwise + """ + configs = ChatDataProcessor.get_all_channel_configs(ChannelTypes.MAIL, False) + email_account = config_dict['email_account'] + for config in configs: + if config['config']['email_account'] == email_account: + subjects1 = Utility.string_to_list(config['config'].get('subjects', '')) + subjects2 = Utility.string_to_list(config_dict.get('subjects', '')) + if (not subjects1) and (not subjects2): + return True + if len(set(subjects1).intersection(set(subjects2))) > 0: + return True + + return False + def login_imap(self): if self.mailbox: return @@ -109,6 +129,13 @@ def validate_imap_connection(bot): return False async def send_mail(self, to: str, subject: str, body: str, log_id: str): + """ + Send mail to a user + :param to: str - email address + :param subject: str - email subject + :param body: str - email body + :param log_id: str - log id + """ exception = None try: if body and len(body) > 0: @@ -129,7 +156,13 @@ async def send_mail(self, to: str, subject: str, body: str, log_id: str): mail_log.responses.append(exception) mail_log.save() - def process_mail(self, rasa_chat_response: dict, log_id: str): + def process_mail(self, rasa_chat_response: dict, log_id: str) -> str: + """ + Process mail response from Rasa and return formatted mail + :param rasa_chat_response: dict - Rasa chat response + :param log_id: str - log id + :return: str - formatted mail + """ slots = rasa_chat_response.get('slots', []) slots = {key.strip(): value.strip() for slot_str in slots for split_result in [slot_str.split(":", 1)] @@ -152,6 +185,10 @@ def process_mail(self, rasa_chat_response: dict, log_id: str): def get_log(bot_id: str, offset: int, limit: int) -> dict: """ Get logs for a bot + :param bot_id: str - bot id + :param offset: int - offset + :param limit: int - limit + :return: dict - logs and count """ try: count = MailResponseLog.objects(bot=bot_id).count() diff --git a/kairon/shared/chat/processor.py b/kairon/shared/chat/processor.py index c6c9693dc..17a34a5f8 100644 --- a/kairon/shared/chat/processor.py +++ b/kairon/shared/chat/processor.py @@ -27,6 +27,10 @@ def save_channel_config(configuration: Dict, bot: Text, user: Text): private_key = configuration['config'].get('private_key', None) if configuration['connector_type'] == ChannelTypes.BUSINESS_MESSAGES.value and private_key: configuration['config']['private_key'] = private_key.replace("\\n", "\n") + if configuration['connector_type'] == ChannelTypes.MAIL.value: + from kairon.shared.channels.mail.processor import MailProcessor + if MailProcessor.check_email_config_exists(configuration['config']): + raise AppException("Email configuration already exists for same email address and subject") try: filter_args = ChatDataProcessor.__attach_metadata_and_get_filter(configuration, bot) channel = Channels.objects(**filter_args).get() @@ -108,6 +112,21 @@ def get_channel_config(connector_type: Text, bot: Text, mask_characters=True, ** ChatDataProcessor.__prepare_config(config, mask_characters) return config + @staticmethod + def get_all_channel_configs(connector_type: str, mask_characters: bool = True, **kwargs): + """ + fetch all channel configs for connector type + :param connector_type: channel name + :param mask_characters: whether to mask the security keys default is True + :return: List + """ + for channel in Channels.objects(connector_type=connector_type).exclude("user", "timestamp"): + data = channel.to_mongo().to_dict() + data['_id'] = data['_id'].__str__() + data.pop("timestamp") + ChatDataProcessor.__prepare_config(data, mask_characters) + yield data + @staticmethod def __prepare_config(config: dict, mask_characters: bool): connector_type = config['connector_type'] diff --git a/tests/integration_test/services_test.py b/tests/integration_test/services_test.py index ac6971682..fef2a646b 100644 --- a/tests/integration_test/services_test.py +++ b/tests/integration_test/services_test.py @@ -24280,9 +24280,6 @@ def test_add_channel_config_error(): ) - - - def test_add_bot_with_template_name(monkeypatch): from kairon.shared.admin.data_objects import BotSecrets diff --git a/tests/unit_test/channels/mail_channel_test.py b/tests/unit_test/channels/mail_channel_test.py index c748d5a57..bfa2653c1 100644 --- a/tests/unit_test/channels/mail_channel_test.py +++ b/tests/unit_test/channels/mail_channel_test.py @@ -589,3 +589,43 @@ def test_get_log_exception(self): + @pytest.fixture + def config_dict(self): + return { + 'email_account': 'test@example.com', + 'subjects': 'subject1,subject2' + } + + @patch('kairon.shared.chat.processor.ChatDataProcessor.get_all_channel_configs') + def test_check_email_config_exists_no_existing_config(self,mock_get_all_channel_configs, config_dict): + mock_get_all_channel_configs.return_value = [] + result = MailProcessor.check_email_config_exists(config_dict) + assert result == False + + @patch('kairon.shared.chat.processor.ChatDataProcessor.get_all_channel_configs') + def test_check_email_config_exists_same_config_exists(self, mock_get_all_channel_configs, config_dict): + mock_get_all_channel_configs.return_value = [{ + 'config': config_dict + }] + result = MailProcessor.check_email_config_exists(config_dict) + assert result == True + + @patch('kairon.shared.chat.processor.ChatDataProcessor.get_all_channel_configs') + def test_check_email_config_exists_different_config_exists(self,mock_get_all_channel_configs, config_dict): + existing_config = config_dict.copy() + existing_config['subjects'] = 'subject3' + mock_get_all_channel_configs.return_value = [{ + 'config': existing_config + }] + result = MailProcessor.check_email_config_exists(config_dict) + assert result == False + + @patch('kairon.shared.chat.processor.ChatDataProcessor.get_all_channel_configs') + def test_check_email_config_exists_partial_subject_match(self, mock_get_all_channel_configs, config_dict): + existing_config = config_dict.copy() + existing_config['subjects'] = 'subject1,subject3' + mock_get_all_channel_configs.return_value = [{ + 'config': existing_config + }] + result = MailProcessor.check_email_config_exists(config_dict) + assert result == True diff --git a/tests/unit_test/chat/chat_test.py b/tests/unit_test/chat/chat_test.py index 9add9b72a..8e07c1858 100644 --- a/tests/unit_test/chat/chat_test.py +++ b/tests/unit_test/chat/chat_test.py @@ -937,6 +937,105 @@ async def test_mongotracker_save(self): assert data[0]['type'] == 'flattened' + def test_get_all_channel_configs(self): + configs = list(ChatDataProcessor.get_all_channel_configs('telegram')) + assert len(configs) == 1 + assert configs[0].get("connector_type") == "telegram" + assert str(configs[0]["config"].get("access_token")).__contains__("***") + + configs = list(ChatDataProcessor.get_all_channel_configs('telegram', mask_characters=False)) + assert len(configs) == 1 + assert configs[0].get("connector_type") == "telegram" + assert not str(configs[0]["config"].get("access_token")).__contains__("***") + + @patch('kairon.shared.channels.mail.scheduler.MailScheduler.request_epoch') + @patch('kairon.shared.chat.processor.ChatDataProcessor.get_all_channel_configs') + def test_mail_channel_save_duplicate_error(self, mock_get_all_channels, mock_request_epock, monkeypatch): + def __get_bot(*args, **kwargs): + return {"account": 1000} + + monkeypatch.setattr(AccountProcessor, "get_bot", __get_bot) + + mock_get_all_channels.return_value = [{ + 'connector_type': 'mail', + 'config': { + 'email_account': 'test@example.com', + 'subjects': 'subject1,subject2' + } + }] + + #error case + #same email and subject + with pytest.raises(AppException, match='Email configuration already exists for same email address and subject'): + ChatDataProcessor.save_channel_config({ + 'connector_type': 'mail', + 'config': { + 'email_account': 'test@example.com', + 'subjects': 'subject1,subject2', + 'email_password': 'test', + 'imap_server': 'imap.gmail.com', + 'smtp_server': 'smtp.gmail.com', + 'smtp_port': '587' + } + }, 'test', 'test') + + #same email partical subject overlap + with pytest.raises(AppException, match='Email configuration already exists for same email address and subject'): + ChatDataProcessor.save_channel_config({ + 'connector_type': 'mail', + 'config': { + 'email_account': 'test@example.com', + 'subjects': 'subject1', + 'email_password': 'test', + 'imap_server': 'imap.gmail.com', + 'smtp_server': 'smtp.gmail.com', + 'smtp_port': '587' + } + }, 'test', 'test') + + + #non error case + #different email and subject + ChatDataProcessor.save_channel_config({ + 'connector_type': 'mail', + 'config': { + 'email_account': 'test2@example.com', + 'subjects': '', + 'email_password': 'test', + 'imap_server': 'imap.gmail.com', + 'smtp_server': 'smtp.gmail.com', + 'smtp_port': '587' + } + }, 'test', 'test') + + #different email same subject + ChatDataProcessor.save_channel_config({ + 'connector_type': 'mail', + 'config': { + 'email_account': 'test3@example.com', + 'subjects': '', + 'email_password': 'subject1,subject2', + 'imap_server': 'imap.gmail.com', + 'smtp_server': 'smtp.gmail.com', + 'smtp_port': '587' + } + }, 'test', 'test') + + #same email different subject + ChatDataProcessor.save_channel_config({ + 'connector_type': 'mail', + 'config': { + 'email_account': 'test@example.com', + 'subjects': 'apple', + 'email_password': 'test', + 'imap_server': 'imap.gmail.com', + 'smtp_server': 'smtp.gmail.com', + 'smtp_port': '587' + } + }, 'test', 'test') + assert mock_request_epock.call_count == 3 + + Channels.objects(connector_type='mail').delete() @pytest.mark.asyncio