Skip to content

Commit

Permalink
Email tweaks (#988)
Browse files Browse the repository at this point in the history
* small tweaks to email validation

* more test examples

* change and better error messages

* remove duplicate tests

* linting

* tweak from @hramezani
  • Loading branch information
samuelcolvin committed Sep 8, 2022
1 parent c475a67 commit ee52496
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 38 deletions.
3 changes: 3 additions & 0 deletions changes/988-samuelcolvin.md
@@ -0,0 +1,3 @@
Changes to email validation: whitespace is stripped from names, e.g. `' Fred Smith <fred@example.com>'`,
unicode NFC normalization is applied to the local part, the domain part is cleaned to the internationalized form
by round-tripping through IDNA ASCII, better error messages are provided when validation fails
5 changes: 4 additions & 1 deletion pydantic/errors.py
Expand Up @@ -186,7 +186,10 @@ class DictError(PydanticTypeError):


class EmailError(PydanticValueError):
msg_template = 'value is not a valid email address'
msg_template = 'value is not a valid email address: {reason}'

def __init__(self, reason: str):
super().__init__(reason=reason)


class UrlError(PydanticValueError):
Expand Down
20 changes: 7 additions & 13 deletions pydantic/networks.py
Expand Up @@ -700,19 +700,17 @@ def validate(cls, value: NetworkType) -> Union[IPv4Network, IPv6Network]:
raise errors.IPvAnyNetworkError()


pretty_email_regex = re.compile(r'([\w ]*?) *<(.*)> *')
pretty_email_regex = re.compile(r' *([\w ]*?) *<(.+?)> *')


def validate_email(value: Union[str]) -> Tuple[str, str]:
"""
Brutally simple email address validation. Note unlike most email address validation
Email address validation using https://pypi.org/project/email-validator/
Notes:
* raw ip address (literal) domain parts are not allowed.
* "John Doe <local_part@domain.com>" style "pretty" email addresses are processed
* the local part check is extremely basic. This raises the possibility of unicode spoofing, but no better
solution is really possible.
* spaces are striped from the beginning and end of addresses but no error is raised
See RFC 5322 but treat it with suspicion, there seems to exist no universally acknowledged test for a valid email!
"""
if email_validator is None:
import_email_validator()
Expand All @@ -725,12 +723,8 @@ def validate_email(value: Union[str]) -> Tuple[str, str]:
email = value.strip()

try:
email_validator.validate_email(email, check_deliverability=False)
parts = email_validator.validate_email(email, check_deliverability=False)
except email_validator.EmailNotValidError as e:
raise errors.EmailError() from e

at_index = email.index('@')
local_part = email[:at_index] # RFC 5321, local part must be case-sensitive.
global_part = email[at_index:].lower()
raise errors.EmailError(*e.args) from e

return name or local_part, local_part + global_part
return name or parts['local'], parts['email']
57 changes: 35 additions & 22 deletions tests/test_networks.py
Expand Up @@ -6,6 +6,7 @@
AnyUrl,
BaseModel,
CockroachDsn,
EmailError,
EmailStr,
FileUrl,
HttpUrl,
Expand Down Expand Up @@ -746,6 +747,8 @@ class Model(BaseModel):
('foo.bar@example.com ', 'foo.bar', 'foo.bar@example.com'),
('foo BAR <foobar@example.com >', 'foo BAR', 'foobar@example.com'),
('FOO bar <foobar@example.com> ', 'FOO bar', 'foobar@example.com'),
(' Whatever <foobar@example.com>', 'Whatever', 'foobar@example.com'),
('Whatever < foobar@example.com>', 'Whatever', 'foobar@example.com'),
('<FOOBAR@example.com> ', 'FOOBAR', 'FOOBAR@example.com'),
('ñoñó@example.com', 'ñoñó', 'ñoñó@example.com'),
('我買@example.com', '我買', '我買@example.com'),
Expand All @@ -759,6 +762,11 @@ class Model(BaseModel):
('foo.bar@example.com', 'foo.bar', 'foo.bar@example.com'),
('foo.bar@exam-ple.com ', 'foo.bar', 'foo.bar@exam-ple.com'),
('ιωάννης@εεττ.gr', 'ιωάννης', 'ιωάννης@εεττ.gr'),
('foobar@аррӏе.com', 'foobar', 'foobar@аррӏе.com'),
('foobar@xn--80ak6aa92e.com', 'foobar', 'foobar@аррӏе.com'),
('аррӏе@example.com', 'аррӏе', 'аррӏе@example.com'),
('xn--80ak6aa92e@example.com', 'xn--80ak6aa92e', 'xn--80ak6aa92e@example.com'),
('葉士豪@臺網中心.tw', '葉士豪', '葉士豪@臺網中心.tw'),
],
)
def test_address_valid(value, name, email):
Expand All @@ -767,31 +775,36 @@ def test_address_valid(value, name, email):

@pytest.mark.skipif(not email_validator, reason='email_validator not installed')
@pytest.mark.parametrize(
'value',
'value,reason',
[
'f oo.bar@example.com ',
'foo.bar@exam\nple.com ',
'foobar',
'foobar <foobar@example.com',
'@example.com',
'foobar@.example.com',
'foobar@.com',
'foo bar@example.com',
'foo@bar@example.com',
'\n@example.com',
'\r@example.com',
'\f@example.com',
' @example.com',
'\u0020@example.com',
'\u001f@example.com',
'"@example.com',
'\"@example.com',
',@example.com',
'foobar <foobar<@example.com>',
('@example.com', 'There must be something before the @-sign.'),
('f oo.bar@example.com', 'The email address contains invalid characters before the @-sign'),
('foobar', 'The email address is not valid. It must have exactly one @-sign.'),
('foobar@localhost', 'The domain name localhost is not valid. It should have a period.'),
('foobar@127.0.0.1', 'The domain name 127.0.0.1 is not valid. It is not within a valid top-level domain.'),
('foo.bar@exam\nple.com ', None),
('foobar <foobar@example.com', None),
('foobar@.example.com', None),
('foobar@.com', None),
('foo bar@example.com', None),
('foo@bar@example.com', None),
('\n@example.com', None),
('\r@example.com', None),
('\f@example.com', None),
(' @example.com', None),
('\u0020@example.com', None),
('\u001f@example.com', None),
('"@example.com', None),
('\"@example.com', None),
(',@example.com', None),
('foobar <foobar<@example.com>', None),
('foobar <foobar@example.com>>', None),
('foobar <<foobar<@example.com>', None),
('foobar <>', None),
],
)
def test_address_invalid(value):
with pytest.raises(ValueError):
def test_address_invalid(value, reason):
with pytest.raises(EmailError, match=f'value is not a valid email address: {reason or ""}'):
validate_email(value)


Expand Down
20 changes: 18 additions & 2 deletions tests/test_types.py
Expand Up @@ -1137,8 +1137,24 @@ class MoreStringsModel(BaseModel):
'type': 'value_error.any_str.min_length',
'ctx': {'limit_value': 5},
},
{'loc': ('str_email',), 'msg': 'value is not a valid email address', 'type': 'value_error.email'},
{'loc': ('name_email',), 'msg': 'value is not a valid email address', 'type': 'value_error.email'},
{
'loc': ('str_email',),
'msg': (
'value is not a valid email address: The email address contains invalid characters before the @-sign: '
'LESS-THAN SIGN.'
),
'type': 'value_error.email',
'ctx': {'reason': 'The email address contains invalid characters before the @-sign: LESS-THAN SIGN.'},
},
{
'loc': ('name_email',),
'msg': (
'value is not a valid email address: '
'The email address contains invalid characters before the @-sign: SPACE.'
),
'type': 'value_error.email',
'ctx': {'reason': 'The email address contains invalid characters before the @-sign: SPACE.'},
},
]


Expand Down

0 comments on commit ee52496

Please sign in to comment.