Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 86 additions & 9 deletions django/core/validators.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import ipaddress
import math
import re
import warnings
from pathlib import Path
from urllib.parse import urlsplit

from django.core.exceptions import ValidationError
from django.utils.deconstruct import deconstructible
from django.utils.deprecation import RemovedInDjango2029Warning, warn_about_external_use
from django.utils.http import MAX_URL_LENGTH
from django.utils.ipv6 import is_valid_ipv6_address
from django.utils.regex_helper import _lazy_re_compile
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext_lazy
from django.utils.warnings import django_file_prefixes

# These values, if given to validate(), will trigger the self.required check.
EMPTY_VALUES = (None, "", [], (), {})
Expand Down Expand Up @@ -233,6 +236,27 @@ class EmailValidator:
)
domain_allowlist = ["localhost"]

# RemovedInDjango2029Warning.
validate_domain_part_deprecated_msg = (
"EmailValidator.validate_domain_part() is deprecated. Migrate to "
"validate_domain(), raising ValidationError for invalid domains."
)
# RemovedInDjango2029Warning: tracks if a subclass overrides the deprecated
# `validate_domain_part()` hook (set in __init_subclass__()).
validate_domain_part_overridden = False

# RemovedInDjango2029Warning: warn once if a subclass overrides the
# deprecated `validate_domain_part()` hook.
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if "validate_domain_part" in cls.__dict__:
cls.validate_domain_part_overridden = True
warnings.warn(
cls.validate_domain_part_deprecated_msg,
RemovedInDjango2029Warning,
skip_file_prefixes=django_file_prefixes(),
)

def __init__(self, message=None, code=None, allowlist=None):
if message is not None:
self.message = message
Expand All @@ -242,22 +266,75 @@ def __init__(self, message=None, code=None, allowlist=None):
self.domain_allowlist = allowlist

def __call__(self, value):
# The maximum length of an email is 320 characters per RFC 3696
# section 3.
username, domain = self.parse_address(value)
self.validate(value, username, domain)

def validate(self, value, username, domain):
self.validate_username(username, value)

# RemovedInDjango2029Warning: honor a legacy validate_domain_part()
# override (the warning is issued in __init_subclass__()). The
# allowlist is checked here for the legacy path only, preserving that
# hook's contract of never being called with an allowlisted domain;
# validate_domain() enforces the allowlist itself. When the deprecation
# ends, remove this `if` branch entirely, leaving just:
# self.validate_domain(domain, value)
if self.validate_domain_part_overridden:
if domain not in self.domain_allowlist and not self.validate_domain_part(
domain
):
raise ValidationError(
self.message,
code=self.code,
params={"value": value, "domain": domain},
)
else:
self.validate_domain(domain, value)

def parse_address(self, value):
# The maximum length of an email is 320 chars per RFC 3696 section 3.
if not value or "@" not in value or len(value) > 320:
raise ValidationError(self.message, code=self.code, params={"value": value})
return value.rsplit("@", 1)

user_part, domain_part = value.rsplit("@", 1)
def validate_username(self, username, value):
if not self.user_regex.match(username):
raise ValidationError(
self.message,
code=self.code,
params={"value": value, "username": username},
)

if not self.user_regex.match(user_part):
raise ValidationError(self.message, code=self.code, params={"value": value})
def validate_domain(self, domain, value):
if domain in self.domain_allowlist:
return

if domain_part not in self.domain_allowlist and not self.validate_domain_part(
domain_part
):
raise ValidationError(self.message, code=self.code, params={"value": value})
if self.domain_regex.match(domain):
return

literal_match = self.literal_regex.match(domain)
if literal_match:
ip_address = literal_match[1]
try:
validate_ipv46_address(ip_address)
return
except ValidationError:
pass
raise ValidationError(
self.message,
code=self.code,
params={"value": value, "domain": domain},
)

# RemovedInDjango2029Warning.
def validate_domain_part(self, domain_part):
# Warn external callers, except when reached through this class' own
# validate() dispatch (i.e. a legacy override calling super()).
warn_about_external_use(
self.validate_domain_part_deprecated_msg,
RemovedInDjango2029Warning,
skip_frames=1,
)
if self.domain_regex.match(domain_part):
return True

Expand Down
3 changes: 3 additions & 0 deletions docs/internals/deprecation.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ details on these changes.
the current timezone in migrations when the ``tzinfo`` argument is omitted
and :setting:`USE_TZ` is ``True``.

* The undocumented ``EmailValidator.validate_domain_part()`` method will be
removed.

.. _deprecation-removed-in-2028:

2028
Expand Down
43 changes: 42 additions & 1 deletion docs/ref/validators.txt
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ to, or in lieu of custom ``field.clean()`` methods.
An :class:`EmailValidator` ensures that a value looks like an email
address, and raises a :exc:`~django.core.exceptions.ValidationError` with
:attr:`message` and :attr:`code` if it doesn't. Values longer than 320
characters are always considered invalid.
characters are always considered invalid. The raised error includes the
invalid ``username`` or ``domain`` part in its ``params``.

.. attribute:: message

Expand All @@ -159,6 +160,46 @@ to, or in lieu of custom ``field.clean()`` methods.
validation, so you'd need to add them to the ``allowlist`` as
necessary.

.. method:: parse_address(value)

.. versionadded:: 6.2

Returns a ``(username, domain)`` pair, where ``username`` is the
portion of ``value`` before the last ``@`` and ``domain`` is the
portion after (this is not a full :rfc:`5322` address parser though).
Raises a :exc:`~django.core.exceptions.ValidationError` if ``value`` is
empty, has no ``@``, or exceeds the maximum length. This method can be
overridden to customize how the address is parsed.

.. method:: validate(value, username, domain)

.. versionadded:: 6.2

Runs :meth:`validate_username` and :meth:`validate_domain` on the
parsed ``value`` (resulting from :meth:`parse_address`), raising a
:exc:`~django.core.exceptions.ValidationError` if either part is
invalid. This method can be overridden to customize validation
depending on both the ``username`` and ``domain`` parts.

.. method:: validate_domain(domain, value)

.. versionadded:: 6.2

Validates ``domain`` (the portion after the ``@``), raising a
:exc:`~django.core.exceptions.ValidationError` if it is invalid. A
``domain`` in the :attr:`allowlist` is always considered valid.
``value`` is the full address being validated. This method can be
overridden to customize domain validation.

.. method:: validate_username(username, value)

.. versionadded:: 6.2

Validates ``username`` (the portion before the ``@``), raising a
:exc:`~django.core.exceptions.ValidationError` if it is invalid.
``value`` is the full address being validated. This method can be
overridden to customize username validation.

``DomainNameValidator``
-----------------------

Expand Down
16 changes: 15 additions & 1 deletion docs/releases/6.2.txt
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,14 @@ Utilities
Validators
~~~~~~~~~~

* ...
* :class:`~django.core.validators.EmailValidator` now includes the invalid
``username`` or ``domain`` in the ``params`` dict of the
:exc:`~django.core.exceptions.ValidationError` it raises. The new
:meth:`~django.core.validators.EmailValidator.parse_address`,
:meth:`~django.core.validators.EmailValidator.validate`,
:meth:`~django.core.validators.EmailValidator.validate_domain`, and
:meth:`~django.core.validators.EmailValidator.validate_username` methods can
be overridden to customize how the address is parsed and validated.

.. _backwards-incompatible-6.2:

Expand Down Expand Up @@ -345,6 +352,13 @@ Miscellaneous
Features deprecated in 6.2
==========================

Validators
----------

* The undocumented ``EmailValidator.validate_domain_part()`` method is
deprecated in favor of the new
:meth:`~django.core.validators.EmailValidator.validate_domain` method.

Miscellaneous
-------------

Expand Down
Loading
Loading