Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Email tweaks #988

Merged
merged 8 commits into from Sep 8, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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='value is not a valid email address: ' + (reason or '')):
samuelcolvin marked this conversation as resolved.
Show resolved Hide resolved
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