Repository: getsentry/sentry

Issue:
Sentry API 400s with "Please enter a valid email address" when commit emails contain [square brackets]
## Important Details

**How are you running Sentry?**
-> Saas (sentry.io)

## Description

For one of our projects, we are unable to upload releases to Sentry's API via `sentry-cli` due to an email validation error. Sentry is failing to let some RFC2822 compliant email addresses through.

Specifically, our team uses Dependabot to update dependencies. We also use squash commits on GitHub. This results in commits having an email address like `27856297+dependabot-preview[bot]@users.noreply.github.com`. A validation is failing on these emails.

## Steps to Reproduce

1. Create a git repo that has a commit with author.email set to `27856297+dependabot-preview[bot]@users.noreply.github.com` (in particular, the square brackets `[bot]`). Note this is a RFC2822 valid email address (and also not really in our team's control).

2. Run e.g. `SENTRY_LOG_LEVEL=debug sentry-cli releases -o $ORGANIZATION set-commits --auto $CIRCLE_SHA1`

```
[... snip (sorry, these are private repos) ...]
  DEBUG   2020-07-29 18:27:34.311394218 +00:00 response status: 400
  DEBUG   2020-07-29 18:27:34.311418182 +00:00 body: {"commits":{"author_email":["Enter a valid email address."]}}
  DEBUG   2020-07-29 18:27:34.312391739 +00:00 error: running update nagger
error: API request failed
  caused by: sentry reported an error: request failure (http status: 400)
  Object({"commits": Object({"author_email": Array([String("Enter a valid email address.")])})})
  DEBUG   2020-07-29 18:27:34.313350041 +00:00 client close; no transport to shut down  (from sentry)
```

### What you expected to happen

Sentry's API should have accepted my commits with the quirky (but not invalid!) email addresses.

### Possible Solution

Unfortunately this one isn't going to be easy - I verified it's Django's `EmailValidator` that is causing this:

```
root@77f2244ff101:/# python3
Python 3.8.5 (default, Jul 22 2020, 12:28:11)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from django.core import validators
>>> validators.validate_email('good@gmail.com')
>>> validators.validate_email('12345+user@github.com')
>>> validators.validate_email('27856297+dependabot-preview[bot]@users.noreply.github.com')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.8/site-packages/django/core/validators.py", line 197, in __call__
    raise ValidationError(self.message, code=self.code)
django.core.exceptions.ValidationError: <exception str() failed>
>>>
```

I don't have great ideas. Getting something fixed upstream isn't going to be quick or easy. 

Perhaps `sentry-cli` could be modified to clean up these email addresses before sending them to the API? (not the best, I know). 

From our perspective - sure we could re-write our Git repo history, but this will keep happening whenever we merge these Dependabot PRs.


Discussion:

Resolving patch:
diff --git a/src/sentry/api/serializers/rest_framework/commit.py b/src/sentry/api/serializers/rest_framework/commit.py
--- a/src/sentry/api/serializers/rest_framework/commit.py
+++ b/src/sentry/api/serializers/rest_framework/commit.py
@@ -24,8 +24,8 @@ class CommitSerializer(serializers.Serializer):
     author_name = serializers.CharField(
         max_length=128, required=False, allow_null=True, allow_blank=True
     )
-    author_email = serializers.EmailField(
-        max_length=75, required=False, allow_null=True, allow_blank=True
+    author_email = serializers.CharField(
+        max_length=75, required=False, allow_null=True, allow_blank=True,
     )
     timestamp = serializers.DateTimeField(required=False, allow_null=True)
     patch_set = ListField(
diff --git a/src/sentry/migrations/0120_commit_author_charfield.py b/src/sentry/migrations/0120_commit_author_charfield.py
new file mode 100644
--- /dev/null
+++ b/src/sentry/migrations/0120_commit_author_charfield.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.29 on 2020-11-02 20:25
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    # This flag is used to mark that a migration shouldn't be automatically run in
+    # production. We set this to True for operations that we think are risky and want
+    # someone from ops to run manually and monitor.
+    # General advice is that if in doubt, mark your migration as `is_dangerous`.
+    # Some things you should always mark as dangerous:
+    # - Large data migrations. Typically we want these to be run manually by ops so that
+    #   they can be monitored. Since data migrations will now hold a transaction open
+    #   this is even more important.
+    # - Adding columns to highly active tables, even ones that are NULL.
+    is_dangerous = False
+
+    # This flag is used to decide whether to run this migration in a transaction or not.
+    # By default we prefer to run in a transaction, but for migrations where you want
+    # to `CREATE INDEX CONCURRENTLY` this needs to be set to False. Typically you'll
+    # want to create an index concurrently when adding one to an existing table.
+    atomic = True
+
+
+    dependencies = [
+        ('sentry', '0119_fix_set_none'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='commitauthor',
+            name='email',
+            field=models.CharField(max_length=75),
+        ),
+    ]
diff --git a/src/sentry/models/commitauthor.py b/src/sentry/models/commitauthor.py
--- a/src/sentry/models/commitauthor.py
+++ b/src/sentry/models/commitauthor.py
@@ -21,7 +21,7 @@ class CommitAuthor(Model):
 
     organization_id = BoundedPositiveIntegerField(db_index=True)
     name = models.CharField(max_length=128, null=True)
-    email = models.EmailField(max_length=75)
+    email = models.CharField(max_length=75)
     external_id = models.CharField(max_length=164, null=True)
 
     objects = CommitAuthorManager()


Test patch:
diff --git a/tests/sentry/api/endpoints/test_organization_release_details.py b/tests/sentry/api/endpoints/test_organization_release_details.py
--- a/tests/sentry/api/endpoints/test_organization_release_details.py
+++ b/tests/sentry/api/endpoints/test_organization_release_details.py
@@ -603,3 +603,9 @@ def test_ref_limited_by_max_version_length(self):
         assert serializer.is_valid()
         serializer = OrganizationReleaseSerializer(data={"ref": "a" * (MAX_VERSION_LENGTH + 1)})
         assert not serializer.is_valid()
+
+    def test_author_email_patch(self):
+        serializer = OrganizationReleaseSerializer(
+            data={"commits": [{"id": "a", "author_email": "email[test]@example.org"}]}
+        )
+        assert serializer.is_valid()