diff --git a/django/core/validators.py b/django/core/validators.py index 251b5d8856..cd9dba1ee8 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -78,30 +78,53 @@ def validate_integer(value): raise ValidationError('') -class EmailValidator(RegexValidator): +class EmailValidator(object): + message = _('Enter a valid e-mail address.') + code = 'invalid' + user_regex = re.compile( + r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*$" # dot-atom + r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"$)', # quoted-string + re.IGNORECASE) + domain_regex = re.compile( + r'(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?$)' # domain + # literal form, ipv4 address (SMTP 4.1.3) + r'|^\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$', + re.IGNORECASE) + domain_whitelist = ['localhost'] + + def __init__(self, message=None, code=None, whitelist=None): + if message is not None: + self.message = message + if code is not None: + self.code = code + if whitelist is not None: + self.domain_whitelist = whitelist def __call__(self, value): - try: - super(EmailValidator, self).__call__(value) - except ValidationError as e: - # Trivial case failed. Try for possible IDN domain-part - if value and '@' in value: - parts = value.split('@') - try: - parts[-1] = parts[-1].encode('idna').decode('ascii') - except UnicodeError: - raise e - super(EmailValidator, self).__call__('@'.join(parts)) - else: - raise + value = force_text(value) -email_re = re.compile( - r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom - # quoted-string, see also http://tools.ietf.org/html/rfc2822#section-3.2.5 - r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"' - r')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)$)' # domain - r'|\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$', re.IGNORECASE) # literal form, ipv4 address (SMTP 4.1.3) -validate_email = EmailValidator(email_re, _('Enter a valid email address.'), 'invalid') + if not value or '@' not in value: + raise ValidationError(self.message, code=self.code) + + user_part, domain_part = value.split('@', 1) + + if not self.user_regex.match(user_part): + raise ValidationError(self.message, code=self.code) + + if (not domain_part in self.domain_whitelist and + not self.domain_regex.match(domain_part)): + # Try for possible IDN domain-part + try: + domain_part = domain_part.encode('idna').decode('ascii') + if not self.domain_regex.match(domain_part): + raise ValidationError(self.message, code=self.code) + else: + return + except UnicodeError: + pass + raise ValidationError(self.message, code=self.code) + +validate_email = EmailValidator() slug_re = re.compile(r'^[-a-zA-Z0-9_]+$') validate_slug = RegexValidator(slug_re, _("Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."), 'invalid') diff --git a/docs/ref/validators.txt b/docs/ref/validators.txt index 8da134a42d..92e257ca85 100644 --- a/docs/ref/validators.txt +++ b/docs/ref/validators.txt @@ -96,7 +96,7 @@ to, or in lieu of custom ``field.clean()`` methods. ------------------ .. data:: validate_email - A :class:`RegexValidator` instance that ensures a value looks like an + An ``EmailValidator`` instance that ensures a value looks like an email address. ``validate_slug`` diff --git a/tests/modeltests/validators/tests.py b/tests/modeltests/validators/tests.py index 0174a606df..5b562a87e6 100644 --- a/tests/modeltests/validators/tests.py +++ b/tests/modeltests/validators/tests.py @@ -29,6 +29,8 @@ TEST_DATA = ( (validate_email, 'example@valid-----hyphens.com', None), (validate_email, 'example@valid-with-hyphens.com', None), (validate_email, 'test@domain.with.idn.tld.उदाहरण.परीक्षा', None), + (validate_email, 'email@localhost', None), + (EmailValidator(whitelist=['localdomain']), 'email@localdomain', None), (validate_email, None, ValidationError), (validate_email, '', ValidationError),