Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mail channel availability verification #1727

Merged
merged 3 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion kairon/api/app/routers/bot/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
39 changes: 38 additions & 1 deletion kairon/shared/channels/mail/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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)]
Expand All @@ -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()
Expand Down
19 changes: 19 additions & 0 deletions kairon/shared/chat/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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']
Expand Down
3 changes: 0 additions & 3 deletions tests/integration_test/services_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
40 changes: 40 additions & 0 deletions tests/unit_test/channels/mail_channel_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,3 +589,43 @@ def test_get_log_exception(self):



@pytest.fixture
def config_dict(self):
return {
'email_account': '[email protected]',
'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
99 changes: 99 additions & 0 deletions tests/unit_test/chat/chat_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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': '[email protected]',
'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': '[email protected]',
'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': '[email protected]',
'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': '[email protected]',
'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': '[email protected]',
'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': '[email protected]',
'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
Expand Down
Loading