Repository: ietf-tools/datatracker

Issue:
Improve validation for form that accepts pasted email feedback
### Describe the issue

When cut/pasting email feedback via the Chair Task "Enter email feedback" it *really* wants email headers.  I got an unknown server error.  When I hit BACK and then put a From and Subject line and a blank line as the first three lines of the text, it worked.


### Code of Conduct

- [X] I agree to follow the [IETF's Code of Conduct](https://github.com/ietf-tools/.github/blob/main/CODE_OF_CONDUCT.md)


Discussion:
Yes, what's intended is that the _full raw message_ go in.
I figured that out.  But maybe change the message to be "From: not found" or such.
Yes, the validation should let you know what happened rather than hand you a server crash.

Resolving patch:
diff --git a/ietf/nomcom/utils.py b/ietf/nomcom/utils.py
--- a/ietf/nomcom/utils.py
+++ b/ietf/nomcom/utils.py
@@ -12,6 +12,7 @@
 
 from collections import defaultdict
 from email import message_from_string, message_from_bytes
+from email.errors import HeaderParseError
 from email.header import decode_header
 from email.iterators import typed_subpart_iterator
 from email.utils import parseaddr
@@ -428,7 +429,11 @@ def make_nomineeposition_for_newperson(nomcom, candidate_name, candidate_email,
 def getheader(header_text, default="utf-8"):
     """Decode the specified header"""
 
-    tuples = decode_header(header_text)
+    try:
+        tuples = decode_header(header_text)
+    except TypeError:
+        return ""
+
     header_sections = [ text.decode(charset or default) if isinstance(text, bytes) else text for text, charset in tuples]
     return "".join(header_sections)
 
@@ -477,6 +482,9 @@ def parse_email(text):
     body = get_body(msg)
     subject = getheader(msg['Subject'])
     __, addr = parseaddr(msg['From'])
+    if not addr:
+        raise HeaderParseError
+
     return addr.lower(), subject, body
 
 
diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py
--- a/ietf/nomcom/views.py
+++ b/ietf/nomcom/views.py
@@ -20,6 +20,7 @@
 from django.urls import reverse
 from django.utils.encoding import force_bytes, force_text
 
+from email.errors import HeaderParseError
 
 from ietf.dbtemplate.models import DBTemplate
 from ietf.dbtemplate.views import group_template_edit, group_template_show
@@ -644,9 +645,12 @@ def private_feedback_email(request, year):
         form = FeedbackEmailForm(data=request.POST,
                                  nomcom=nomcom)
         if form.is_valid():
-            form.save()
-            form = FeedbackEmailForm(nomcom=nomcom)
-            messages.success(request, 'The feedback email has been registered.')
+            try:
+                form.save()
+                form = FeedbackEmailForm(nomcom=nomcom)
+                messages.success(request, 'The feedback email has been registered.')
+            except HeaderParseError:
+                messages.error(request, 'Missing email headers')
 
     return render(request, template,
                               {'form': form,


Test patch:
diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py
--- a/ietf/nomcom/tests.py
+++ b/ietf/nomcom/tests.py
@@ -98,6 +98,7 @@ def setUp(self):
         self.private_nominate_newperson_url = reverse('ietf.nomcom.views.private_nominate_newperson', kwargs={'year': self.year})
         self.add_questionnaire_url = reverse('ietf.nomcom.views.private_questionnaire', kwargs={'year': self.year})
         self.private_feedback_url = reverse('ietf.nomcom.views.private_feedback', kwargs={'year': self.year})
+        self.private_feedback_email_url = reverse('ietf.nomcom.views.private_feedback_email', kwargs={'year': self.year})
         self.positions_url = reverse('ietf.nomcom.views.list_positions', kwargs={'year': self.year})        
         self.edit_position_url = reverse('ietf.nomcom.views.edit_position', kwargs={'year': self.year})
 
@@ -1011,6 +1012,43 @@ def feedback_view(self, *args, **kwargs):
             nominee_position.save()
 
 
+    def test_private_feedback_email(self):
+        self.access_chair_url(self.private_feedback_email_url)
+
+        feedback_url = self.private_feedback_email_url
+        response = self.client.get(feedback_url)
+        self.assertEqual(response.status_code, 200)
+
+        nomcom = get_nomcom_by_year(self.year)
+        if not nomcom.public_key:
+            self.assertNotContains(response, "paste-email-feedback-form")
+
+        # save the cert file in tmp
+        with io.open(self.cert_file.name, 'r') as fd:
+            nomcom.public_key.save('cert', File(fd))
+
+        response = self.client.get(feedback_url)
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, "paste-email-feedback-form")
+
+        headers = \
+            "From: Zaphod Beeblebrox <president@galaxy>\n" \
+            "Subject: Ford Prefect\n\n"
+        body = \
+            "Hey, you sass that hoopy Ford Prefect?\n" \
+            "There's a frood who really knows where his towel is.\n"
+
+        test_data = {'email_text': body}
+        response = self.client.post(feedback_url, test_data)
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, 'Missing email headers')
+
+        test_data = {'email_text': headers + body}
+        response = self.client.post(feedback_url, test_data)
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, 'The feedback email has been registered.')
+
+
 class NomineePositionStateSaveTest(TestCase):
     """Tests for the NomineePosition save override method"""