1
- from .exceptions_types import EmailSyntaxError
1
+ from .exceptions_types import EmailSyntaxError , ValidatedEmail
2
2
from .rfc_constants import EMAIL_MAX_LENGTH , LOCAL_PART_MAX_LENGTH , DOMAIN_MAX_LENGTH , \
3
3
DOT_ATOM_TEXT , DOT_ATOM_TEXT_INTL , ATEXT_RE , ATEXT_INTL_DOT_RE , ATEXT_HOSTNAME_INTL , QTEXT_INTL , \
4
4
DNS_LABEL_LENGTH_LIMIT , DOT_ATOM_TEXT_HOSTNAME , DOMAIN_NAME_REGEX , DOMAIN_LITERAL_CHARS
7
7
import unicodedata
8
8
import idna # implements IDNA 2008; Python's codec is only IDNA 2003
9
9
import ipaddress
10
- from typing import Optional
10
+ from typing import Optional , Tuple , TypedDict , Union
11
11
12
12
13
- def split_email (email ) :
13
+ def split_email (email : str ) -> Tuple [ Optional [ str ], str , str , bool ] :
14
14
# Return the display name, unescaped local part, and domain part
15
15
# of the address, and whether the local part was quoted. If no
16
16
# display name was present and angle brackets do not surround
@@ -46,7 +46,7 @@ def split_email(email):
46
46
# We assume the input string is already stripped of leading and
47
47
# trailing CFWS.
48
48
49
- def split_string_at_unquoted_special (text , specials ) :
49
+ def split_string_at_unquoted_special (text : str , specials : Tuple [ str , ...]) -> Tuple [ str , str ] :
50
50
# Split the string at the first character in specials (an @-sign
51
51
# or left angle bracket) that does not occur within quotes.
52
52
inside_quote = False
@@ -77,7 +77,7 @@ def split_string_at_unquoted_special(text, specials):
77
77
78
78
return left_part , right_part
79
79
80
- def unquote_quoted_string (text ) :
80
+ def unquote_quoted_string (text : str ) -> Tuple [ str , bool ] :
81
81
# Remove surrounding quotes and unescape escaped backslashes
82
82
# and quotes. Escapes are parsed liberally. I think only
83
83
# backslashes and quotes can be escaped but we'll allow anything
@@ -155,15 +155,15 @@ def unquote_quoted_string(text):
155
155
return display_name , local_part , domain_part , is_quoted_local_part
156
156
157
157
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 :
159
159
"""Helper function to return an error message related to invalid length."""
160
160
diff = len (addr ) - limit
161
161
prefix = "at least " if utf8 else ""
162
162
suffix = "s" if diff > 1 else ""
163
163
return f"({ prefix } { diff } character{ suffix } too many)"
164
164
165
165
166
- def safe_character_display (c ) :
166
+ def safe_character_display (c : str ) -> str :
167
167
# Return safely displayable characters in quotes.
168
168
if c == '\\ ' :
169
169
return f"\" { c } \" " # can't use repr because it escapes it
@@ -180,8 +180,14 @@ def safe_character_display(c):
180
180
return unicodedata .name (c , h )
181
181
182
182
183
+ class LocalPartValidationResult (TypedDict ):
184
+ local_part : str
185
+ ascii_local_part : Optional [str ]
186
+ smtputf8 : bool
187
+
188
+
183
189
def 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 :
185
191
"""Validates the syntax of the local part of an email address."""
186
192
187
193
if len (local ) == 0 :
@@ -345,7 +351,7 @@ def validate_email_local_part(local: str, allow_smtputf8: bool = True, allow_emp
345
351
raise EmailSyntaxError ("The email address contains invalid characters before the @-sign." )
346
352
347
353
348
- def check_unsafe_chars (s , allow_space = False ):
354
+ def check_unsafe_chars (s : str , allow_space : bool = False ) -> None :
349
355
# Check for unsafe characters or characters that would make the string
350
356
# invalid or non-sensible Unicode.
351
357
bad_chars = set ()
@@ -397,7 +403,7 @@ def check_unsafe_chars(s, allow_space=False):
397
403
+ ", " .join (safe_character_display (c ) for c in sorted (bad_chars )) + "." )
398
404
399
405
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 :
401
407
# RFC 5322 3.2.3
402
408
if label .endswith ("." ):
403
409
raise EmailSyntaxError (end_descr .format ("period" ))
@@ -416,7 +422,12 @@ def check_dot_atom(label, start_descr, end_descr, is_hostname):
416
422
raise EmailSyntaxError ("An email address cannot have a period and a hyphen next to each other." )
417
423
418
424
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 :
420
431
"""Validates the syntax of the domain part of an email address."""
421
432
422
433
# Check for invalid characters before normalization.
@@ -580,7 +591,7 @@ def validate_email_domain_name(domain, test_environment=False, globally_delivera
580
591
}
581
592
582
593
583
- def validate_email_length (addrinfo ) :
594
+ def validate_email_length (addrinfo : ValidatedEmail ) -> None :
584
595
# If the email address has an ASCII representation, then we assume it may be
585
596
# transmitted in ASCII (we can't assume SMTPUTF8 will be used on all hops to
586
597
# the destination) and the length limit applies to ASCII characters (which is
@@ -621,11 +632,18 @@ def validate_email_length(addrinfo):
621
632
raise EmailSyntaxError (f"The email address is too long { reason } ." )
622
633
623
634
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 :
625
641
# This is obscure domain-literal syntax. Parse it and return
626
642
# a compressed/normalized address.
627
643
# RFC 5321 4.1.3 and RFC 5322 3.4.1.
628
644
645
+ addr : Union [ipaddress .IPv4Address , ipaddress .IPv6Address ]
646
+
629
647
# Try to parse the domain literal as an IPv4 address.
630
648
# There is no tag for IPv4 addresses, so we can never
631
649
# be sure if the user intends an IPv4 address.
0 commit comments