Spaces:
Running
Running
from typing import Optional, Union | |
from .exceptions_types import EmailSyntaxError, ValidatedEmail | |
from .syntax import split_email, validate_email_local_part, validate_email_domain_name, validate_email_domain_literal, validate_email_length | |
from .rfc_constants import CASE_INSENSITIVE_MAILBOX_NAMES | |
def validate_email( | |
email: Union[str, bytes], | |
/, # prior arguments are positional-only | |
*, # subsequent arguments are keyword-only | |
allow_smtputf8: Optional[bool] = None, | |
allow_empty_local: bool = False, | |
allow_quoted_local: Optional[bool] = None, | |
allow_domain_literal: Optional[bool] = None, | |
check_deliverability: Optional[bool] = None, | |
test_environment: Optional[bool] = None, | |
globally_deliverable: Optional[bool] = None, | |
timeout: Optional[int] = None, | |
dns_resolver: Optional[object] = None | |
) -> ValidatedEmail: | |
""" | |
Given an email address, and some options, returns a ValidatedEmail instance | |
with information about the address if it is valid or, if the address is not | |
valid, raises an EmailNotValidError. This is the main function of the module. | |
""" | |
# Fill in default values of arguments. | |
from . import ALLOW_SMTPUTF8, ALLOW_QUOTED_LOCAL, ALLOW_DOMAIN_LITERAL, \ | |
GLOBALLY_DELIVERABLE, CHECK_DELIVERABILITY, TEST_ENVIRONMENT, DEFAULT_TIMEOUT | |
if allow_smtputf8 is None: | |
allow_smtputf8 = ALLOW_SMTPUTF8 | |
if allow_quoted_local is None: | |
allow_quoted_local = ALLOW_QUOTED_LOCAL | |
if allow_domain_literal is None: | |
allow_domain_literal = ALLOW_DOMAIN_LITERAL | |
if check_deliverability is None: | |
check_deliverability = CHECK_DELIVERABILITY | |
if test_environment is None: | |
test_environment = TEST_ENVIRONMENT | |
if globally_deliverable is None: | |
globally_deliverable = GLOBALLY_DELIVERABLE | |
if timeout is None and dns_resolver is None: | |
timeout = DEFAULT_TIMEOUT | |
# Allow email to be a str or bytes instance. If bytes, | |
# it must be ASCII because that's how the bytes work | |
# on the wire with SMTP. | |
if not isinstance(email, str): | |
try: | |
email = email.decode("ascii") | |
except ValueError as e: | |
raise EmailSyntaxError("The email address is not valid ASCII.") from e | |
# Split the address into the local part (before the @-sign) | |
# and the domain part (after the @-sign). Normally, there | |
# is only one @-sign. But the awkward "quoted string" local | |
# part form (RFC 5321 4.1.2) allows @-signs in the local | |
# part if the local part is quoted. | |
local_part, domain_part, is_quoted_local_part \ | |
= split_email(email) | |
# Collect return values in this instance. | |
ret = ValidatedEmail() | |
ret.original = email | |
# Validate the email address's local part syntax and get a normalized form. | |
# If the original address was quoted and the decoded local part is a valid | |
# unquoted local part, then we'll get back a normalized (unescaped) local | |
# part. | |
local_part_info = validate_email_local_part(local_part, | |
allow_smtputf8=allow_smtputf8, | |
allow_empty_local=allow_empty_local, | |
quoted_local_part=is_quoted_local_part) | |
ret.local_part = local_part_info["local_part"] | |
ret.ascii_local_part = local_part_info["ascii_local_part"] | |
ret.smtputf8 = local_part_info["smtputf8"] | |
# If a quoted local part isn't allowed but is present, now raise an exception. | |
# This is done after any exceptions raised by validate_email_local_part so | |
# that mandatory checks have highest precedence. | |
if is_quoted_local_part and not allow_quoted_local: | |
raise EmailSyntaxError("Quoting the part before the @-sign is not allowed here.") | |
# Some local parts are required to be case-insensitive, so we should normalize | |
# to lowercase. | |
# RFC 2142 | |
if ret.ascii_local_part is not None \ | |
and ret.ascii_local_part.lower() in CASE_INSENSITIVE_MAILBOX_NAMES \ | |
and ret.local_part is not None: | |
ret.ascii_local_part = ret.ascii_local_part.lower() | |
ret.local_part = ret.local_part.lower() | |
# Validate the email address's domain part syntax and get a normalized form. | |
is_domain_literal = False | |
if len(domain_part) == 0: | |
raise EmailSyntaxError("There must be something after the @-sign.") | |
elif domain_part.startswith("[") and domain_part.endswith("]"): | |
# Parse the address in the domain literal and get back a normalized domain. | |
domain_part_info = validate_email_domain_literal(domain_part[1:-1]) | |
if not allow_domain_literal: | |
raise EmailSyntaxError("A bracketed IP address after the @-sign is not allowed here.") | |
ret.domain = domain_part_info["domain"] | |
ret.ascii_domain = domain_part_info["domain"] # Domain literals are always ASCII. | |
ret.domain_address = domain_part_info["domain_address"] | |
is_domain_literal = True # Prevent deliverability checks. | |
else: | |
# Check the syntax of the domain and get back a normalized | |
# internationalized and ASCII form. | |
domain_part_info = validate_email_domain_name(domain_part, test_environment=test_environment, globally_deliverable=globally_deliverable) | |
ret.domain = domain_part_info["domain"] | |
ret.ascii_domain = domain_part_info["ascii_domain"] | |
# Construct the complete normalized form. | |
ret.normalized = ret.local_part + "@" + ret.domain | |
# If the email address has an ASCII form, add it. | |
if not ret.smtputf8: | |
if not ret.ascii_domain: | |
raise Exception("Missing ASCII domain.") | |
ret.ascii_email = (ret.ascii_local_part or "") + "@" + ret.ascii_domain | |
else: | |
ret.ascii_email = None | |
# Check the length of the address. | |
validate_email_length(ret) | |
if check_deliverability and not test_environment: | |
# Validate the email address's deliverability using DNS | |
# and update the returned ValidatedEmail object with metadata. | |
if is_domain_literal: | |
# There is nothing to check --- skip deliverability checks. | |
return ret | |
# Lazy load `deliverability` as it is slow to import (due to dns.resolver) | |
from .deliverability import validate_email_deliverability | |
deliverability_info = validate_email_deliverability( | |
ret.ascii_domain, ret.domain, timeout, dns_resolver | |
) | |
for key, value in deliverability_info.items(): | |
setattr(ret, key, value) | |
return ret | |