Repository: wtforms/wtforms
Pull request #294 (2016-08-27 18:41:32)

improve Email validator
Uses the same `user_part` regex from [Django](https://github.com/django/django/blob/12b4280444b58c94197255655e284e4103fe00a9/django/core/validators.py#L170).

Related to #172, #169, and #117.

The new tests added here previously would fail, now they pass.


alanhamlett: Want to bring this up again and see if it can get merged?

Patch:
diff --git a/wtforms/validators.py b/wtforms/validators.py
index 5b2cd71..af0e338 100644
--- a/wtforms/validators.py
+++ b/wtforms/validators.py
@@ -264,7 +264,7 @@ class Regexp(object):
         return match
 
 
-class Email(Regexp):
+class Email(object):
     """
     Validates an email address. Note that this uses a very primitive regular
     expression and should only be used in instances where you later verify by
@@ -273,19 +273,34 @@ class Email(Regexp):
     :param message:
         Error message to raise in case of a validation error.
     """
+
+    user_regex = re.compile(
+        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
+        re.IGNORECASE)
+
     def __init__(self, message=None):
+        self.message = message
         self.validate_hostname = HostnameValidation(
             require_tld=True,
         )
-        super(Email, self).__init__(r'^.+@([^.@][^@]+)$', re.IGNORECASE, message)
 
     def __call__(self, form, field):
+        value = field.data
+
         message = self.message
         if message is None:
             message = field.gettext('Invalid email address.')
 
-        match = super(Email, self).__call__(form, field, message)
-        if not self.validate_hostname(match.group(1)):
+        if not value or '@' not in value:
+            raise ValidationError(message)
+
+        user_part, domain_part = value.rsplit('@', 1)
+
+        if not self.user_regex.match(user_part):
+            raise ValidationError(message)
+
+        if not self.validate_hostname(domain_part):
             raise ValidationError(message)
 
 
@@ -506,7 +521,10 @@ class HostnameValidation(object):
                 return True
 
         # Encode out IDNA hostnames. This makes further validation easier.
-        hostname = hostname.encode('idna')
+        try:
+            hostname = hostname.encode('idna')
+        except UnicodeError:
+            pass
 
         # Turn back into a string in Python 3x
         if not isinstance(hostname, string_types):


Test patch:
diff --git a/tests/validators.py b/tests/validators.py
index 31b4b32..b137457 100644
--- a/tests/validators.py
+++ b/tests/validators.py
@@ -34,6 +34,9 @@ class ValidatorsTest(TestCase):
         self.assertRaises(ValidationError, email(), self.form, DummyField('foo@bar'))
         self.assertRaises(ValidationError, email(), self.form, DummyField('foo@bar.ab12'))
         self.assertRaises(ValidationError, email(), self.form, DummyField('foo@.bar.ab'))
+        self.assertRaises(ValidationError, email(), self.form, DummyField('foo.@bar.co'))
+        self.assertRaises(ValidationError, email(), self.form, DummyField('foo@foo@bar.co'))
+        self.assertRaises(ValidationError, email(), self.form, DummyField('fo o@bar.co'))
 
         # Test IDNA domains
         self.assertEqual(email()(self.form, DummyField(u'foo@bücher.中国')), None)