1- from .exceptions_types import EmailSyntaxError
1+ from .exceptions_types import EmailSyntaxError , ValidatedEmail
22from .rfc_constants import EMAIL_MAX_LENGTH , LOCAL_PART_MAX_LENGTH , DOMAIN_MAX_LENGTH , \
33 DOT_ATOM_TEXT , DOT_ATOM_TEXT_INTL , ATEXT_RE , ATEXT_INTL_DOT_RE , ATEXT_HOSTNAME_INTL , QTEXT_INTL , \
44 DNS_LABEL_LENGTH_LIMIT , DOT_ATOM_TEXT_HOSTNAME , DOMAIN_NAME_REGEX , DOMAIN_LITERAL_CHARS
77import unicodedata
88import idna # implements IDNA 2008; Python's codec is only IDNA 2003
99import ipaddress
10- from typing import Optional
10+ from typing import Optional , Tuple , TypedDict , Union
1111
1212
13- def split_email (email ) :
13+ def split_email (email : str ) -> Tuple [ Optional [ str ], str , str , bool ] :
1414 # Return the display name, unescaped local part, and domain part
1515 # of the address, and whether the local part was quoted. If no
1616 # display name was present and angle brackets do not surround
@@ -46,7 +46,7 @@ def split_email(email):
4646 # We assume the input string is already stripped of leading and
4747 # trailing CFWS.
4848
49- def split_string_at_unquoted_special (text , specials ) :
49+ def split_string_at_unquoted_special (text : str , specials : Tuple [ str , ...]) -> Tuple [ str , str ] :
5050 # Split the string at the first character in specials (an @-sign
5151 # or left angle bracket) that does not occur within quotes.
5252 inside_quote = False
@@ -77,7 +77,7 @@ def split_string_at_unquoted_special(text, specials):
7777
7878 return left_part , right_part
7979
80- def unquote_quoted_string (text ) :
80+ def unquote_quoted_string (text : str ) -> Tuple [ str , bool ] :
8181 # Remove surrounding quotes and unescape escaped backslashes
8282 # and quotes. Escapes are parsed liberally. I think only
8383 # backslashes and quotes can be escaped but we'll allow anything
@@ -155,15 +155,15 @@ def unquote_quoted_string(text):
155155 return display_name , local_part , domain_part , is_quoted_local_part
156156
157157
158- def get_length_reason (addr , utf8 = False , limit = EMAIL_MAX_LENGTH ):
158+ def get_length_reason (addr : str , utf8 : bool = False , limit : int = EMAIL_MAX_LENGTH ) -> str :
159159 """Helper function to return an error message related to invalid length."""
160160 diff = len (addr ) - limit
161161 prefix = "at least " if utf8 else ""
162162 suffix = "s" if diff > 1 else ""
163163 return f"({ prefix } { diff } character{ suffix } too many)"
164164
165165
166- def safe_character_display (c ) :
166+ def safe_character_display (c : str ) -> str :
167167 # Return safely displayable characters in quotes.
168168 if c == '\\ ' :
169169 return f"\" { c } \" " # can't use repr because it escapes it
@@ -180,8 +180,14 @@ def safe_character_display(c):
180180 return unicodedata .name (c , h )
181181
182182
183+ class LocalPartValidationResult (TypedDict ):
184+ local_part : str
185+ ascii_local_part : Optional [str ]
186+ smtputf8 : bool
187+
188+
183189def validate_email_local_part (local : str , allow_smtputf8 : bool = True , allow_empty_local : bool = False ,
184- quoted_local_part : bool = False ):
190+ quoted_local_part : bool = False ) -> LocalPartValidationResult :
185191 """Validates the syntax of the local part of an email address."""
186192
187193 if len (local ) == 0 :
@@ -345,7 +351,7 @@ def validate_email_local_part(local: str, allow_smtputf8: bool = True, allow_emp
345351 raise EmailSyntaxError ("The email address contains invalid characters before the @-sign." )
346352
347353
348- def check_unsafe_chars (s , allow_space = False ):
354+ def check_unsafe_chars (s : str , allow_space : bool = False ) -> None :
349355 # Check for unsafe characters or characters that would make the string
350356 # invalid or non-sensible Unicode.
351357 bad_chars = set ()
@@ -397,7 +403,7 @@ def check_unsafe_chars(s, allow_space=False):
397403 + ", " .join (safe_character_display (c ) for c in sorted (bad_chars )) + "." )
398404
399405
400- def check_dot_atom (label , start_descr , end_descr , is_hostname ) :
406+ def check_dot_atom (label : str , start_descr : str , end_descr : str , is_hostname : bool ) -> None :
401407 # RFC 5322 3.2.3
402408 if label .endswith ("." ):
403409 raise EmailSyntaxError (end_descr .format ("period" ))
@@ -416,7 +422,12 @@ def check_dot_atom(label, start_descr, end_descr, is_hostname):
416422 raise EmailSyntaxError ("An email address cannot have a period and a hyphen next to each other." )
417423
418424
419- def validate_email_domain_name (domain , test_environment = False , globally_deliverable = True ):
425+ class DomainNameValidationResult (TypedDict ):
426+ ascii_domain : str
427+ domain : str
428+
429+
430+ def validate_email_domain_name (domain : str , test_environment : bool = False , globally_deliverable : bool = True ) -> DomainNameValidationResult :
420431 """Validates the syntax of the domain part of an email address."""
421432
422433 # Check for invalid characters before normalization.
@@ -580,7 +591,7 @@ def validate_email_domain_name(domain, test_environment=False, globally_delivera
580591 }
581592
582593
583- def validate_email_length (addrinfo ) :
594+ def validate_email_length (addrinfo : ValidatedEmail ) -> None :
584595 # If the email address has an ASCII representation, then we assume it may be
585596 # transmitted in ASCII (we can't assume SMTPUTF8 will be used on all hops to
586597 # the destination) and the length limit applies to ASCII characters (which is
@@ -621,11 +632,18 @@ def validate_email_length(addrinfo):
621632 raise EmailSyntaxError (f"The email address is too long { reason } ." )
622633
623634
624- def validate_email_domain_literal (domain_literal ):
635+ class DomainLiteralValidationResult (TypedDict ):
636+ domain_address : Union [ipaddress .IPv4Address , ipaddress .IPv6Address ]
637+ domain : str
638+
639+
640+ def validate_email_domain_literal (domain_literal : str ) -> DomainLiteralValidationResult :
625641 # This is obscure domain-literal syntax. Parse it and return
626642 # a compressed/normalized address.
627643 # RFC 5321 4.1.3 and RFC 5322 3.4.1.
628644
645+ addr : Union [ipaddress .IPv4Address , ipaddress .IPv6Address ]
646+
629647 # Try to parse the domain literal as an IPv4 address.
630648 # There is no tag for IPv4 addresses, so we can never
631649 # be sure if the user intends an IPv4 address.
0 commit comments