Skip to content

Commit

Permalink
Mail channel availability verification (#1727)
Browse files Browse the repository at this point in the history
* Mail channel availability verification

* Mail channel availability verification

* added  doc strings

---------

Co-authored-by: spandan_mondal <[email protected]>
  • Loading branch information
hasinaxp and spandan_mondal authored Jan 20, 2025
1 parent 238c76d commit 5b0c669
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 5 deletions.
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

0 comments on commit 5b0c669

Please sign in to comment.