Skip to content

Commit

Permalink
Support email addresses with international domain names
Browse files Browse the repository at this point in the history
`colander.Email` uses a regex which does not allow domain names
beginning with `xn--` or Unicode characters. Either of these limitations
prevents users from registering with email addresses that use
international domain names.

This commit replaces the validator with an alternate pattern taken from
the HTML spec which is used by Chrome and which at least allows the
Punycode version of an internationalized email address to be used.

I opted not to just allow unicode chars because we first have to make
sure that the rest of our stack supports it. Alternatively we could
convert to Punycode automatically when deserializing the user's input.

Fixes #4662
  • Loading branch information
robertknight committed Sep 21, 2017
1 parent 25fc3ce commit 759e0b9
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 5 deletions.
26 changes: 21 additions & 5 deletions h/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,27 @@
import colander


class Email(colander.Email):
def __init__(self, *args, **kwargs):
if 'msg' not in kwargs:
kwargs['msg'] = "Invalid email address."
super(Email, self).__init__(*args, **kwargs)
# Regex for email addresses.
#
# Taken from Chromium and derived from the WhatWG HTML spec
# (4.10.7.1.5 E-Mail state).
#
# This was chosen because it is a widely used and relatively simple pattern.
# Unlike `colander.Email` it supports International Domain Names (IDNs) in
# Punycode form.
HTML5_EMAIL_REGEX = ("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@"
"[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$")


class Email(colander.Regex):
"""
Validator for email addresses.
This is a replacement for `colander.Email` which rejects certain valid email
addresses (see https://github.com/hypothesis/h/issues/4662).
"""
def __init__(self, msg='Invalid email address.'):
super(Email, self).__init__(HTML5_EMAIL_REGEX, msg=msg)


class Length(colander.Length):
Expand Down
35 changes: 35 additions & 0 deletions tests/h/validators_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import pytest

from mock import Mock

import colander

from h.validators import Email


class TestEmail(object):
@pytest.mark.parametrize('email', [
'jimsmith@foobar.com',
'international@xn--domain.com',
'jim.smith@gmail.com',
'jim.smith@foo.bar.com',
])
def test_accepts_valid_addresses(self, schema_node, email):
validator = Email()
validator(schema_node, email)

@pytest.mark.parametrize('email', [
' spaces @ spaces.com',
])
def test_rejects_invalid_addresses(self, schema_node, email):
validator = Email()

with pytest.raises(colander.Invalid):
validator(schema_node, email)

@pytest.fixture
def schema_node(self):
"""
Mock for `colander.SchemaNode` arg that validator callables require.
"""
return Mock(spec_set=[])

0 comments on commit 759e0b9

Please sign in to comment.