From 21ccc83d69a3a4d87ccfaddae242e4670c52f1be Mon Sep 17 00:00:00 2001 From: finnertysea <26181241+finnertysea@users.noreply.github.com> Date: Fri, 17 Mar 2023 13:12:03 -0700 Subject: [PATCH 1/8] #1075 - make view agnostic to format of incoming due date --- helpdesk/views/staff.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 7d33f6a9..d54bfd94 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -10,7 +10,7 @@ from ..lib import format_time_spent from ..templated_email import send_templated_mail from collections import defaultdict from copy import deepcopy -from datetime import date, datetime, timedelta +from datetime import date, datetime, time, timedelta from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.decorators import user_passes_test @@ -24,7 +24,7 @@ from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonRespons from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse, reverse_lazy from django.utils import timezone -from django.utils.dateparse import parse_datetime +from django.utils.dateparse import parse_date, parse_datetime from django.utils.html import escape from django.utils.timezone import make_aware from django.utils.translation import gettext as _ @@ -521,7 +521,8 @@ def get_due_date_from_request_or_ticket( due_date = request.POST.get('due_date', None) or None if due_date is not None: - due_date = make_aware(parse_datetime(due_date)) + parsed_date = parse_datetime(due_date) or datetime.combine(parse_date(due_date), time()) + due_date = make_aware(parsed_date) else: due_date_year = int(request.POST.get('due_date_year', 0)) due_date_month = int(request.POST.get('due_date_month', 0)) From ae2f24b299ed7a96595ef8675228c6c9f4825f57 Mon Sep 17 00:00:00 2001 From: Christopher Broderick Date: Sat, 25 Mar 2023 13:04:38 +0000 Subject: [PATCH 2/8] Add test to verify that attachments as multipart are stored. --- helpdesk/tests/test_get_email.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/helpdesk/tests/test_get_email.py b/helpdesk/tests/test_get_email.py index 0fe945ff..804b89e1 100644 --- a/helpdesk/tests/test_get_email.py +++ b/helpdesk/tests/test_get_email.py @@ -12,7 +12,8 @@ import helpdesk.email from helpdesk.email import extract_part_data, object_from_message from helpdesk.exceptions import DeleteIgnoredTicketException, IgnoreTicketException from helpdesk.management.commands.get_email import Command -from helpdesk.models import FollowUp, FollowUpAttachment, IgnoreEmail, Queue, Ticket, TicketCC +from helpdesk.models import FollowUp, FollowUpAttachment, IgnoreEmail, Queue, Ticket, TicketCC,\ + Attachment from helpdesk.tests import utils import itertools import logging @@ -231,6 +232,29 @@ class GetEmailCommonTests(TestCase): followup = ticket.followup_set.get() self.assertEqual(1, followup.followupattachment_set.count()) + def test_email_with_multipart_as_attachment(self): + """ + Is a multipart attachment to an email correctly saved as an attachment + """ + att_filename = 'email_attachment.eml' + message, _, _ = utils.generate_multipart_email(type_list=['plain', 'html']) + email_attachment, _, _ = utils.generate_multipart_email(type_list=['plain', 'html']) + att_content = email_attachment.as_string() + message.attach(utils.generate_file_mime_part(filename=att_filename, content=att_content)) + + object_from_message(message.as_string(), self.queue_public, self.logger) + + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(f'[test-1] {message.get("subject")} (Opened)', mail.outbox[0].subject) + + ticket = Ticket.objects.get() + followup = ticket.followup_set.get() + att_retrieved: Attachment = followup.followupattachment_set.get() + self.assertTrue(att_retrieved.filename.endswith(att_filename), "Filename of attached multipart not detected: %s" % (att_retrieved.filename)) + with att_retrieved.file.open('r') as f: + retrieved_content = f.read() + self.assertEquals(att_content, retrieved_content, "Retrieved attachment content different to original :\n\n%s\n\n%s" % (att_content, retrieved_content)) + class GetEmailParametricTemplate(object): """TestCase that checks basic email functionality across methods and socks configs.""" From cdbd5319311c4cd1139e92ecf1bb1cf216a6e9d9 Mon Sep 17 00:00:00 2001 From: Christopher Broderick Date: Sat, 25 Mar 2023 13:06:03 +0000 Subject: [PATCH 3/8] Ensure multipart attachments are saved --- helpdesk/email.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helpdesk/email.py b/helpdesk/email.py index f27dc969..fdc5f8db 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -687,8 +687,8 @@ def extract_part_data( name = f"part-{counter}{ext}" else: name = f"part-{counter}_{name}" - - files.append(SimpleUploadedFile(name, part.get_payload(decode=True), mimetypes.guess_type(name)[0])) + payload = decodeUnknown(part.get_charset(), part.as_bytes()) if part.is_multipart() else part.get_payload(decode=True) + files.append(SimpleUploadedFile(name, payload, mimetypes.guess_type(name)[0])) logger.debug("Found MIME attachment %s", name) return part_body, part_full_body From 85aeb8e79ebc65b9d602d01e4df3bed4aea00f84 Mon Sep 17 00:00:00 2001 From: Christopher Broderick Date: Sat, 25 Mar 2023 13:06:40 +0000 Subject: [PATCH 4/8] Allow specifiying the content for a multipart --- helpdesk/tests/utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/helpdesk/tests/utils.py b/helpdesk/tests/utils.py index f2822732..f93d715a 100644 --- a/helpdesk/tests/utils.py +++ b/helpdesk/tests/utils.py @@ -141,14 +141,15 @@ def generate_email_address( # format email address for RFC 2822 and return return email.utils.formataddr((real_name, email_address)), email_address, first_name, last_name -def generate_file_mime_part(locale: str="en_US",filename: str = None) -> Message: +def generate_file_mime_part(locale: str="en_US",filename: str = None, content: str = None) -> Message: """ :param locale: change this to generate locale specific file name and attachment content :param filename: pass a file name if you want to specify a specific name otherwise a random name will be generated + :param content: pass a string value if you want have specific content otherwise a random string will be generated """ part = MIMEBase('application', 'octet-stream') - part.set_payload(get_fake("text", locale=locale, min_length=1024)) + part.set_payload(get_fake("text", locale=locale, min_length=1024) if content is None else content) encoders.encode_base64(part) if not filename: filename = get_fake("word", locale=locale, min_length=8) + ".txt" @@ -227,7 +228,7 @@ def generate_mime_part(locale: str="en_US", return msg def generate_multipart_email(locale: str="en_US", - type_list: typing.List[str]=["plain", "html", "attachment"], + type_list: typing.List[str]=["plain", "html", "image"], use_short_email: bool=False ) -> typing.Tuple[Message, typing.Tuple[str, str], typing.Tuple[str, str]]: """ From 9e71fc8467f4c8958a9d1913d61799245c84ea43 Mon Sep 17 00:00:00 2001 From: Christopher Broderick Date: Sat, 25 Mar 2023 13:21:27 +0000 Subject: [PATCH 5/8] Remove redundant import --- helpdesk/views/staff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 7d33f6a9..34a8a5ac 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -10,7 +10,7 @@ from ..lib import format_time_spent from ..templated_email import send_templated_mail from collections import defaultdict from copy import deepcopy -from datetime import date, datetime, timedelta +from datetime import datetime, timedelta from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.decorators import user_passes_test From 4cd66d7e0cf5a430f02fa35ea7f1ae57561710ac Mon Sep 17 00:00:00 2001 From: Christopher Broderick Date: Sat, 25 Mar 2023 13:21:38 +0000 Subject: [PATCH 6/8] Fix import format fail --- helpdesk/email.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helpdesk/email.py b/helpdesk/email.py index fdc5f8db..58a666f9 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -687,7 +687,8 @@ def extract_part_data( name = f"part-{counter}{ext}" else: name = f"part-{counter}_{name}" - payload = decodeUnknown(part.get_charset(), part.as_bytes()) if part.is_multipart() else part.get_payload(decode=True) + payload = decodeUnknown(part.get_charset(), part.as_bytes( + )) if part.is_multipart() else part.get_payload(decode=True) files.append(SimpleUploadedFile(name, payload, mimetypes.guess_type(name)[0])) logger.debug("Found MIME attachment %s", name) return part_body, part_full_body From 8cb3d433114dbc298b93b18f6622df4441b6100b Mon Sep 17 00:00:00 2001 From: Christopher Broderick Date: Sat, 25 Mar 2023 13:25:22 +0000 Subject: [PATCH 7/8] Use simpler conversion to string --- helpdesk/email.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/helpdesk/email.py b/helpdesk/email.py index 58a666f9..15e27f54 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -687,8 +687,7 @@ def extract_part_data( name = f"part-{counter}{ext}" else: name = f"part-{counter}_{name}" - payload = decodeUnknown(part.get_charset(), part.as_bytes( - )) if part.is_multipart() else part.get_payload(decode=True) + payload = part.as_string() if part.is_multipart() else part.get_payload(decode=True) files.append(SimpleUploadedFile(name, payload, mimetypes.guess_type(name)[0])) logger.debug("Found MIME attachment %s", name) return part_body, part_full_body From 6c968626b6765572fffd4aa56f0b4a9b74415375 Mon Sep 17 00:00:00 2001 From: Christopher Broderick Date: Sat, 25 Mar 2023 13:28:13 +0000 Subject: [PATCH 8/8] Fix format failure --- helpdesk/tests/test_get_email.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/helpdesk/tests/test_get_email.py b/helpdesk/tests/test_get_email.py index 804b89e1..0b223beb 100644 --- a/helpdesk/tests/test_get_email.py +++ b/helpdesk/tests/test_get_email.py @@ -12,8 +12,7 @@ import helpdesk.email from helpdesk.email import extract_part_data, object_from_message from helpdesk.exceptions import DeleteIgnoredTicketException, IgnoreTicketException from helpdesk.management.commands.get_email import Command -from helpdesk.models import FollowUp, FollowUpAttachment, IgnoreEmail, Queue, Ticket, TicketCC,\ - Attachment +from helpdesk.models import Attachment, FollowUp, FollowUpAttachment, IgnoreEmail, Queue, Ticket, TicketCC from helpdesk.tests import utils import itertools import logging