Repository: Pylons/colander

Issue:
Email validator does not allow double hyphen
`lorem@i--ipsum.com` is a valid email address but the current email validator doesn't validate it.

Here's an email validator that I wrote with code that I stole from Django which works:
```python

class Email(object):
    """
    Email validator shamelessly stolen from Django
    """
    USER_REGEX = (
        r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*\Z"  # dot-atom
        r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"\Z)'  # quoted-string
    )
    # max length for domain name labels is 63 characters per RFC 1034
    DOMAIN_REGEX = r'((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+)(?:[A-Z0-9-]{2,63}(?<!-))\Z'

    def __init__(self, msg=None):
        if msg is None:
            msg = "Invalid email address"
        self.msg = msg
        self.user_regex = re.compile(self.USER_REGEX, re.IGNORECASE)
        self.domain_regex = re.compile(self.DOMAIN_REGEX, re.IGNORECASE)

    def __call__(self, node, value):
        if not value or '@' not in value:
            raise Invalid(node, self.msg)

        user_part, domain_part = value.rsplit('@', 1)

        if not self.user_regex.match(user_part):
            raise Invalid(node, self.msg)

        if not self.domain_regex.match(domain_part):
            # Try for possible IDN domain-part
            try:
                domain_part = domain_part.encode('idna').decode('ascii')
                if self.domain_regex.match(domain_part):
                    return
            except UnicodeError:
                pass
            raise Invalid(node, self.msg)
```

Discussion:

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 43c932c..1d64b84 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,12 @@
+Unreleased
+==========
+
+- The Email validator has been updated to use the same regular expression that
+  is used by the WhatWG HTML specification, thereby increasing the email
+  addresses that will validate correctly from web forms submitted. See
+  https://github.com/Pylons/colander/pull/324 and
+  https://github.com/Pylons/colander/issues/283
+
 1.6.0 (2019-01-31)
 ==================
 
diff --git a/colander/__init__.py b/colander/__init__.py
index 8bddfd6..8956997 100644
--- a/colander/__init__.py
+++ b/colander/__init__.py
@@ -377,19 +377,17 @@ class Regex(object):
             raise Invalid(node, self.msg)
 
 
-EMAIL_RE = r"""(?ix) # matches case insensitive with spaces and comments
-                     # ignored for the entire expression
-^ # matches the start of string
-[A-Z0-9._!#$%&'*+\-/=?^_`{|}~()]+ # matches multiples of the characters:
-                                # A-Z0-9._!#$%&'*+-/=?^_`{|}~() one or
-                                # more times
-@ # matches the @ sign
-[A-Z0-9]+ # matches multiples of the characters A-Z0-9
-([.-][A-Z0-9]+)* # matches one of . or - followed by at least one of A-Z0-9,
-                 # zero to unlimited times
-\.[A-Z]{2,22} # matches a period, followed by two to twenty-two of A-Z
-$ # matches the end of the string
-"""
+# Regex for email addresses.
+#
+# Stolen from the WhatWG HTML spec:
+# https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type=email)
+#
+# If it is good enough for browsers, it is good enough for us!
+EMAIL_RE = (
+    r"^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9]"
+    r"(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9]"
+    r"(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
+)
 
 
 class Email(Regex):


Test patch:
diff --git a/colander/tests/test_colander.py b/colander/tests/test_colander.py
index 12b904e..a6b85c2 100644
--- a/colander/tests/test_colander.py
+++ b/colander/tests/test_colander.py
@@ -480,6 +480,7 @@ class TestEmail(unittest.TestCase):
         self.assertEqual(validator(None, 'name@here1.info'), None)
         self.assertEqual(validator(None, 'foo@bar.baz.biz'), None)
         self.assertEqual(validator(None, "tip'oneill@house.gov"), None)
+        self.assertEqual(validator(None, "lorem@i--ipsum.com"), None)
 
     def test_empty_email(self):
         validator = self._makeOne()
@@ -491,9 +492,6 @@ class TestEmail(unittest.TestCase):
         from colander import Invalid
 
         self.assertRaises(Invalid, validator, None, 'me@here.')
-        self.assertRaises(
-            Invalid, validator, None, 'name@here.tldiswaytoolooooooooong'
-        )
         self.assertRaises(Invalid, validator, None, '@here.us')
         self.assertRaises(Invalid, validator, None, 'me@here..com')
         self.assertRaises(Invalid, validator, None, 'me@we-here-.com')