Repository: pypi/warehouse

Issue:
Valid `Author-email` and `Maintainer-email` fields are rejected
Per https://packaging.python.org/specifications/core-metadata/#author-email-optional:

> A string containing the author’s e-mail address. It can contain a name and e-mail address in the legal forms for a RFC-822 `From:` header
>
> Example:
>
> `Author-email: "C. Schultz" <cschultz@example.com>`

However if the provided `Author-email` field is set to the example value, the resulting distribution is rejected:

```
HTTPError: 400 Client Error: author_email: Invalid email address. for url: http://warehouse.local/legacy/
```

Because we're validating that this field is an email address, not a valid RFC-822 style header.


Discussion:
I am willing to take a shot at this. Would it be fine if I ask my possibly dumb questions here? 
@pradyunsg no question is a dumb question :) 
Yes, If you have any questions, please ask here - we will do our best to help you.
Just wanted to point out to any potential new contributors that @pradyunsg hasn't submitted a PR for this issue (yet!) so it's still up for grabs.
I am not sure that the definition of the format matches the data model of the Metadata PEP here.  There are distinct fields for author and author email, so are we sure that all RFC 822 formats should be accepted?  Would this break the various Metadata file parsers?

Same question for multiple authors.
> I am not sure that the definition of the format matches the data model of the Metadata PEP here. There are distinct fields for author and author email, so are we sure that all RFC 822 formats should be accepted?

@merwok If I follow you correctly, you're pointing out that a RFC 822 style email may contain the author's name, which seems redundant when there is an `Author` field as well. I don't think this is a problem.

> Would this break the various Metadata file parsers?

I don't think this would break anything in Warehouse. The `pkginfo` project already interprets these fields via the `email` stdlib module, so I don't think it would break anything there either. To my knowledge, there isn't anything that depends on these fields being a single valid email address, because this restriction was a relatively new addition and there are a fair amount of packages that don't even have valid RFC 822 style values.

To be clear, this should be considered a regression: there are packages on PyPI already that are using this format for these fields, which were uploaded to pypi-legacy, and would fail if they were uploaded to Warehouse today.
> @pradyunsg hasn't submitted a PR for this issue (yet!)

Yeah... I've been swamped by some other work, so, I've not been able to get to this.
Hmm, so my thinking when I added the new validation was that currently people would be just complete garbage in that field sometimes. I think validating data here is still the right thing to do (because having nonsense values is bad, even if previous releases were able to upload with that). I don't think that it's a bad idea to allow the RFC 822 email addresses here though, though it raises the question of how we should display it. Right now we generate a link that looks like``<a href="mailto:THEEMAILADDRESS">THEAUTHORNAME</a>``, but what do we do if we have ``Author-Name``, and an email address like ``Foo <foo@example.com>``, do we just extract the email address? Change the display?
@dstufft I think that anything that is a valid RFC 822 value _should_ be a valid value for a a `mailto` link, e.g. <a href="mailto: Foo <foo@example.com>">a link like this</a> would work just fine.

Heh, welp. I guess I forgot that worked.

Resolving patch:
diff --git a/warehouse/forklift/legacy.py b/warehouse/forklift/legacy.py
--- a/warehouse/forklift/legacy.py
+++ b/warehouse/forklift/legacy.py
@@ -10,6 +10,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import email
 import hashlib
 import hmac
 import os.path
@@ -255,6 +256,14 @@ def _validate_project_url_list(form, field):
         _validate_project_url(datum)
 
 
+def _validate_rfc822_email_field(form, field):
+    email_validator = wtforms.validators.Email(message='Invalid email address')
+    addresses = email.utils.getaddresses([field.data])
+
+    for real_name, address in addresses:
+        email_validator(form, type('field', (), {'data': address}))
+
+
 def _construct_dependencies(form, types):
     for name, kind in types.items():
         for item in getattr(form, name).data:
@@ -338,7 +347,7 @@ class MetadataForm(forms.Form):
         description="Author-email",
         validators=[
             wtforms.validators.Optional(),
-            wtforms.validators.Email(),
+            _validate_rfc822_email_field,
         ],
     )
     maintainer = wtforms.StringField(
@@ -349,7 +358,7 @@ class MetadataForm(forms.Form):
         description="Maintainer-email",
         validators=[
             wtforms.validators.Optional(),
-            wtforms.validators.Email(),
+            _validate_rfc822_email_field,
         ],
     )
     license = wtforms.StringField(


Test patch:
diff --git a/tests/unit/forklift/test_legacy.py b/tests/unit/forklift/test_legacy.py
--- a/tests/unit/forklift/test_legacy.py
+++ b/tests/unit/forklift/test_legacy.py
@@ -266,6 +266,37 @@ def test_validate_project_url_list(self, monkeypatch):
 
         assert validator.calls == [pretend.call(datum) for datum in data]
 
+    @pytest.mark.parametrize(
+        'data',
+        [
+            (''),
+            ('foo@bar.com'),
+            ('foo@bar.com,'),
+            ('foo@bar.com, biz@baz.com'),
+            ('"C. Schultz" <cschultz@example.com>'),
+            ('"C. Schultz" <cschultz@example.com>, snoopy@peanuts.com'),
+        ]
+    )
+    def test_validate_rfc822_email_field(self, data):
+        form, field = pretend.stub(), pretend.stub(data=data)
+        legacy._validate_rfc822_email_field(form, field)
+
+    @pytest.mark.parametrize(
+        'data',
+        [
+            ('foo'),
+            ('foo@'),
+            ('@bar.com'),
+            ('foo@bar'),
+            ('foo AT bar DOT com'),
+            ('foo@bar.com, foo'),
+        ]
+    )
+    def test_validate_rfc822_email_field_raises(self, data):
+        form, field = pretend.stub(), pretend.stub(data=data)
+        with pytest.raises(ValidationError):
+            legacy._validate_rfc822_email_field(form, field)
+
 
 def test_construct_dependencies():
     types = {