diff --git a/core/mail/tests/v2/test_mails.py b/core/mail/tests/v2/test_mails.py index f58918c..8200c34 100644 --- a/core/mail/tests/v2/test_mails.py +++ b/core/mail/tests/v2/test_mails.py @@ -3,6 +3,7 @@ from unittest import mock from django.test import TestCase, Client +from core.settings import MAIL_DOMAIN from inventory.models import Event from mail.models import Email from mail.protocol import LMTPHandler @@ -133,3 +134,41 @@ class LMTPHandlerTestCase(TestCase): # TODO replace with less hacky test self.assertEqual(IssueThread.objects.all()[0].name, 'test') self.assertEqual(IssueThread.objects.all()[0].state, 'new') self.assertEqual(IssueThread.objects.all()[0].assigned_to, None) + + def test_mail_reply(self): + issue_thread = IssueThread.objects.create( + name="test", + ) + mail1 = Email.objects.create( + subject='test subject', + body='test', + sender='test1@test', + recipient='test2@' + MAIL_DOMAIN, + issue_thread=issue_thread, + ) + mail1_reply = Email.objects.create( + subject='Re: test subject', + body='Thank you for your message.', + sender='test2@' + MAIL_DOMAIN, + recipient='test1@test', + in_reply_to=mail1.reference, + issue_thread=issue_thread, + ) + import aiosmtplib + aiosmtplib.send = make_mocked_coro() + client = Client() + response = client.post(f'/api/2/tickets/{issue_thread.id}/reply/', { + 'message': 'test' + }) + self.assertEqual(response.status_code, 201) + self.assertEqual(len(Email.objects.all()), 3) + self.assertEqual(len(IssueThread.objects.all()), 1) + aiosmtplib.send.assert_called_once() + self.assertEqual(Email.objects.all()[2].subject, 'Re: test subject') + self.assertEqual(Email.objects.all()[2].sender, 'test2@' + MAIL_DOMAIN) + self.assertEqual(Email.objects.all()[2].recipient, 'test1@test') + self.assertEqual(Email.objects.all()[2].body, 'test') + self.assertEqual(Email.objects.all()[2].issue_thread, issue_thread) + self.assertTrue(Email.objects.all()[2].reference.startswith("<")) + self.assertTrue(Email.objects.all()[2].reference.endswith("@localhost>")) + self.assertEqual(Email.objects.all()[2].in_reply_to, mail1.reference) diff --git a/core/tickets/api_v2.py b/core/tickets/api_v2.py index 441d417..b1c202a 100644 --- a/core/tickets/api_v2.py +++ b/core/tickets/api_v2.py @@ -1,6 +1,15 @@ -from rest_framework import routers, viewsets, serializers +import logging -from tickets.models import IssueThread, Comment, StateChange +from django.urls import path +from rest_framework.decorators import api_view, permission_classes, authentication_classes +from rest_framework import routers, viewsets, serializers, status +from rest_framework.response import Response +from asgiref.sync import async_to_sync + +from core.settings import MAIL_DOMAIN +from mail.models import Email +from mail.protocol import send_smtp, make_reply, collect_references +from tickets.models import IssueThread class IssueSerializer(serializers.ModelSerializer): @@ -48,7 +57,32 @@ class IssueViewSet(viewsets.ModelViewSet): authentication_classes = [] +@api_view(['POST']) +@permission_classes([]) +@authentication_classes([]) +def reply(request, pk): + issue = IssueThread.objects.get(pk=pk) + # email = issue.reply(request.data['body']) # TODO evaluate if this is a useful abstraction + references = collect_references(issue) + most_recent = Email.objects.filter(issue_thread=issue, recipient__endswith='@' + MAIL_DOMAIN).order_by( + '-timestamp').first() + mail = Email.objects.create( + issue_thread=issue, + sender=most_recent.recipient, + recipient=most_recent.sender, + subject=f'Re: {most_recent.subject}', + body=request.data['message'], + in_reply_to=most_recent.reference, + ) + log = logging.getLogger('mail.log') + async_to_sync(send_smtp)(make_reply(mail, references), log) + + return Response({'status': 'ok'}, status=status.HTTP_201_CREATED) + + router = routers.SimpleRouter() router.register(r'tickets', IssueViewSet, basename='issues') -urlpatterns = router.urls +urlpatterns = router.urls + [ + path('tickets//reply/', reply, name='reply'), +] diff --git a/web/src/components/Timeline.vue b/web/src/components/Timeline.vue index 1410136..71b8bd9 100644 --- a/web/src/components/Timeline.vue +++ b/web/src/components/Timeline.vue @@ -24,7 +24,12 @@
- +
+ + +
@@ -45,6 +50,16 @@ export default { default: () => [] } }, + emits: ['sendMail'], + data: () => ({ + newMail: "" + }), + methods: { + sendMail() { + this.$emit('sendMail', this.newMail); + this.newMail = ""; + } + } }; @@ -105,7 +120,6 @@ img { border-radius: 6px; height: 48px; padding: 0 16px; - width: 100%; &::placeholder { color: var(--gray-dark); diff --git a/web/src/store/index.js b/web/src/store/index.js index 3ad6b1c..16a6b4d 100644 --- a/web/src/store/index.js +++ b/web/src/store/index.js @@ -157,6 +157,10 @@ const store = new Vuex.Store({ const {data} = await axios.get('/2/tickets/'); commit('replaceTickets', data); }, + async sendMail({commit, dispatch}, {id, message}) { + const {data} = await axios.post(`/2/tickets/${id}/reply/`, {message}); + await dispatch('loadTickets'); + } } }); diff --git a/web/src/views/Ticket.vue b/web/src/views/Ticket.vue index dd6e0fb..c4e064a 100644 --- a/web/src/views/Ticket.vue +++ b/web/src/views/Ticket.vue @@ -6,7 +6,7 @@

Ticket #{{ ticket.id }} - {{ ticket.name }}

- +