email-validator 2.2.0__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {email_validator-2.2.0/email_validator.egg-info → email_validator-2.3.0}/PKG-INFO +8 -7
  2. {email_validator-2.2.0 → email_validator-2.3.0}/README.md +4 -4
  3. {email_validator-2.2.0 → email_validator-2.3.0}/email_validator/__init__.py +4 -2
  4. {email_validator-2.2.0 → email_validator-2.3.0}/email_validator/__main__.py +5 -4
  5. {email_validator-2.2.0 → email_validator-2.3.0}/email_validator/deliverability.py +1 -1
  6. email_validator-2.3.0/email_validator/exceptions.py +13 -0
  7. {email_validator-2.2.0 → email_validator-2.3.0}/email_validator/rfc_constants.py +14 -2
  8. {email_validator-2.2.0 → email_validator-2.3.0}/email_validator/syntax.py +70 -9
  9. email_validator-2.2.0/email_validator/exceptions_types.py → email_validator-2.3.0/email_validator/types.py +0 -15
  10. {email_validator-2.2.0 → email_validator-2.3.0}/email_validator/validate_email.py +39 -10
  11. email_validator-2.3.0/email_validator/version.py +1 -0
  12. {email_validator-2.2.0 → email_validator-2.3.0/email_validator.egg-info}/PKG-INFO +8 -7
  13. {email_validator-2.2.0 → email_validator-2.3.0}/email_validator.egg-info/SOURCES.txt +2 -1
  14. {email_validator-2.2.0 → email_validator-2.3.0}/setup.cfg +1 -1
  15. {email_validator-2.2.0 → email_validator-2.3.0}/tests/test_syntax.py +38 -9
  16. email_validator-2.2.0/email_validator/version.py +0 -1
  17. {email_validator-2.2.0 → email_validator-2.3.0}/LICENSE +0 -0
  18. {email_validator-2.2.0 → email_validator-2.3.0}/MANIFEST.in +0 -0
  19. {email_validator-2.2.0 → email_validator-2.3.0}/email_validator/py.typed +0 -0
  20. {email_validator-2.2.0 → email_validator-2.3.0}/email_validator.egg-info/dependency_links.txt +0 -0
  21. {email_validator-2.2.0 → email_validator-2.3.0}/email_validator.egg-info/entry_points.txt +0 -0
  22. {email_validator-2.2.0 → email_validator-2.3.0}/email_validator.egg-info/requires.txt +0 -0
  23. {email_validator-2.2.0 → email_validator-2.3.0}/email_validator.egg-info/top_level.txt +0 -0
  24. {email_validator-2.2.0 → email_validator-2.3.0}/pyproject.toml +0 -0
  25. {email_validator-2.2.0 → email_validator-2.3.0}/setup.py +0 -0
  26. {email_validator-2.2.0 → email_validator-2.3.0}/tests/mocked-dns-answers.json +0 -0
  27. {email_validator-2.2.0 → email_validator-2.3.0}/tests/mocked_dns_response.py +0 -0
  28. {email_validator-2.2.0 → email_validator-2.3.0}/tests/test_deliverability.py +0 -0
  29. {email_validator-2.2.0 → email_validator-2.3.0}/tests/test_main.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
2
- Name: email_validator
3
- Version: 2.2.0
1
+ Metadata-Version: 2.4
2
+ Name: email-validator
3
+ Version: 2.3.0
4
4
  Summary: A robust email address syntax and deliverability validation library.
5
5
  Home-page: https://github.com/JoshData/python-email-validator
6
6
  Author: Joshua Tauberer
@@ -22,6 +22,7 @@ Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
23
  Requires-Dist: dnspython>=2.0.0
24
24
  Requires-Dist: idna>=2.0.0
25
+ Dynamic: license-file
25
26
 
26
27
  email-validator: Validate Email Addresses
27
28
  =========================================
@@ -37,7 +38,7 @@ users by their email address like on a registration form.
37
38
  Key features:
38
39
 
39
40
  * Checks that an email address has the correct syntax --- great for
40
- email-based registration/login forms or validing data.
41
+ email-based registration/login forms or validating data.
41
42
  * Gives friendly English error messages when validation fails that you
42
43
  can display to end-users.
43
44
  * Checks deliverability (optional): Does the domain name resolve?
@@ -168,13 +169,14 @@ The `validate_email` function also accepts the following keyword arguments
168
169
 
169
170
  `allow_quoted_local=False`: Set to `True` to allow obscure and potentially problematic email addresses in which the part of the address before the @-sign contains spaces, @-signs, or other surprising characters when the local part is surrounded in quotes (so-called quoted-string local parts). In the object returned by `validate_email`, the normalized local part removes any unnecessary backslash-escaping and even removes the surrounding quotes if the address would be valid without them. You can also set `email_validator.ALLOW_QUOTED_LOCAL` to `True` to turn this on for all calls by default.
170
171
 
171
- `allow_domain_literal=False`: Set to `True` to allow bracketed IPv4 and "IPv6:"-prefixd IPv6 addresses in the domain part of the email address. No deliverability checks are performed for these addresses. In the object returned by `validate_email`, the normalized domain will use the condensed IPv6 format, if applicable. The object's `domain_address` attribute will hold the parsed `ipaddress.IPv4Address` or `ipaddress.IPv6Address` object if applicable. You can also set `email_validator.ALLOW_DOMAIN_LITERAL` to `True` to turn this on for all calls by default.
172
+ `allow_domain_literal=False`: Set to `True` to allow bracketed IPv4 and "IPv6:"-prefixed IPv6 addresses in the domain part of the email address. No deliverability checks are performed for these addresses. In the object returned by `validate_email`, the normalized domain will use the condensed IPv6 format, if applicable. The object's `domain_address` attribute will hold the parsed `ipaddress.IPv4Address` or `ipaddress.IPv6Address` object if applicable. You can also set `email_validator.ALLOW_DOMAIN_LITERAL` to `True` to turn this on for all calls by default.
172
173
 
173
174
  `allow_display_name=False`: Set to `True` to allow a display name and bracketed address in the input string, like `My Name <me@example.org>`. It's implemented in the spirit but not the letter of RFC 5322 3.4, so it may be stricter or more relaxed than what you want. The display name, if present, is provided in the returned object's `display_name` field after being unquoted and unescaped. You can also set `email_validator.ALLOW_DISPLAY_NAME` to `True` to turn this on for all calls by default.
174
175
 
175
176
  `allow_empty_local=False`: Set to `True` to allow an empty local part (i.e.
176
177
  `@example.com`), e.g. for validating Postfix aliases.
177
178
 
179
+ `strict=False`: Set to `True` to perform additional syntax checks (currently only a local part length check). This should be used by mail service providers at address creation to ensure email addresses meet broad compatibility requirements.
178
180
 
179
181
  ### DNS timeout and cache
180
182
 
@@ -449,8 +451,7 @@ To release:
449
451
  * Update CHANGELOG.md.
450
452
  * Update the version number in `email_validator/version.py`.
451
453
  * Make & push a commit with the new version number and make sure tests pass.
452
- * Make & push a tag (see command below).
453
- * Make a release at https://github.com/JoshData/python-email-validator/releases/new.
454
+ * Make a release at https://github.com/JoshData/python-email-validator/releases/new creating a new tag (or use command below).
454
455
  * Publish a source and wheel distribution to pypi (see command below).
455
456
 
456
457
  ```sh
@@ -12,7 +12,7 @@ users by their email address like on a registration form.
12
12
  Key features:
13
13
 
14
14
  * Checks that an email address has the correct syntax --- great for
15
- email-based registration/login forms or validing data.
15
+ email-based registration/login forms or validating data.
16
16
  * Gives friendly English error messages when validation fails that you
17
17
  can display to end-users.
18
18
  * Checks deliverability (optional): Does the domain name resolve?
@@ -143,13 +143,14 @@ The `validate_email` function also accepts the following keyword arguments
143
143
 
144
144
  `allow_quoted_local=False`: Set to `True` to allow obscure and potentially problematic email addresses in which the part of the address before the @-sign contains spaces, @-signs, or other surprising characters when the local part is surrounded in quotes (so-called quoted-string local parts). In the object returned by `validate_email`, the normalized local part removes any unnecessary backslash-escaping and even removes the surrounding quotes if the address would be valid without them. You can also set `email_validator.ALLOW_QUOTED_LOCAL` to `True` to turn this on for all calls by default.
145
145
 
146
- `allow_domain_literal=False`: Set to `True` to allow bracketed IPv4 and "IPv6:"-prefixd IPv6 addresses in the domain part of the email address. No deliverability checks are performed for these addresses. In the object returned by `validate_email`, the normalized domain will use the condensed IPv6 format, if applicable. The object's `domain_address` attribute will hold the parsed `ipaddress.IPv4Address` or `ipaddress.IPv6Address` object if applicable. You can also set `email_validator.ALLOW_DOMAIN_LITERAL` to `True` to turn this on for all calls by default.
146
+ `allow_domain_literal=False`: Set to `True` to allow bracketed IPv4 and "IPv6:"-prefixed IPv6 addresses in the domain part of the email address. No deliverability checks are performed for these addresses. In the object returned by `validate_email`, the normalized domain will use the condensed IPv6 format, if applicable. The object's `domain_address` attribute will hold the parsed `ipaddress.IPv4Address` or `ipaddress.IPv6Address` object if applicable. You can also set `email_validator.ALLOW_DOMAIN_LITERAL` to `True` to turn this on for all calls by default.
147
147
 
148
148
  `allow_display_name=False`: Set to `True` to allow a display name and bracketed address in the input string, like `My Name <me@example.org>`. It's implemented in the spirit but not the letter of RFC 5322 3.4, so it may be stricter or more relaxed than what you want. The display name, if present, is provided in the returned object's `display_name` field after being unquoted and unescaped. You can also set `email_validator.ALLOW_DISPLAY_NAME` to `True` to turn this on for all calls by default.
149
149
 
150
150
  `allow_empty_local=False`: Set to `True` to allow an empty local part (i.e.
151
151
  `@example.com`), e.g. for validating Postfix aliases.
152
152
 
153
+ `strict=False`: Set to `True` to perform additional syntax checks (currently only a local part length check). This should be used by mail service providers at address creation to ensure email addresses meet broad compatibility requirements.
153
154
 
154
155
  ### DNS timeout and cache
155
156
 
@@ -424,8 +425,7 @@ To release:
424
425
  * Update CHANGELOG.md.
425
426
  * Update the version number in `email_validator/version.py`.
426
427
  * Make & push a commit with the new version number and make sure tests pass.
427
- * Make & push a tag (see command below).
428
- * Make a release at https://github.com/JoshData/python-email-validator/releases/new.
428
+ * Make a release at https://github.com/JoshData/python-email-validator/releases/new creating a new tag (or use command below).
429
429
  * Publish a source and wheel distribution to pypi (see command below).
430
430
 
431
431
  ```sh
@@ -1,8 +1,8 @@
1
1
  from typing import TYPE_CHECKING
2
2
 
3
3
  # Export the main method, helper methods, and the public data types.
4
- from .exceptions_types import ValidatedEmail, EmailNotValidError, \
5
- EmailSyntaxError, EmailUndeliverableError
4
+ from .exceptions import EmailNotValidError, EmailSyntaxError, EmailUndeliverableError
5
+ from .types import ValidatedEmail
6
6
  from .validate_email import validate_email
7
7
  from .version import __version__
8
8
 
@@ -27,9 +27,11 @@ else:
27
27
  # Default values for keyword arguments.
28
28
 
29
29
  ALLOW_SMTPUTF8 = True
30
+ ALLOW_EMPTY_LOCAL = False
30
31
  ALLOW_QUOTED_LOCAL = False
31
32
  ALLOW_DOMAIN_LITERAL = False
32
33
  ALLOW_DISPLAY_NAME = False
34
+ STRICT = False
33
35
  GLOBALLY_DELIVERABLE = True
34
36
  CHECK_DELIVERABILITY = True
35
37
  TEST_ENVIRONMENT = False
@@ -12,7 +12,7 @@
12
12
  # When using STDIN, no output will be given for valid email addresses.
13
13
  #
14
14
  # Keyword arguments to validate_email can be set in environment variables
15
- # of the same name but upprcase (see below).
15
+ # of the same name but uppercase (see below).
16
16
 
17
17
  import json
18
18
  import os
@@ -21,7 +21,7 @@ from typing import Any, Dict, Optional
21
21
 
22
22
  from .validate_email import validate_email, _Resolver
23
23
  from .deliverability import caching_resolver
24
- from .exceptions_types import EmailNotValidError
24
+ from .exceptions import EmailNotValidError
25
25
 
26
26
 
27
27
  def main(dns_resolver: Optional[_Resolver] = None) -> None:
@@ -29,7 +29,8 @@ def main(dns_resolver: Optional[_Resolver] = None) -> None:
29
29
 
30
30
  # Set options from environment variables.
31
31
  options: Dict[str, Any] = {}
32
- for varname in ('ALLOW_SMTPUTF8', 'ALLOW_QUOTED_LOCAL', 'ALLOW_DOMAIN_LITERAL',
32
+ for varname in ('ALLOW_SMTPUTF8', 'ALLOW_EMPTY_LOCAL', 'ALLOW_QUOTED_LOCAL', 'ALLOW_DOMAIN_LITERAL',
33
+ 'ALLOW_DISPLAY_NAME',
33
34
  'GLOBALLY_DELIVERABLE', 'CHECK_DELIVERABILITY', 'TEST_ENVIRONMENT'):
34
35
  if varname in os.environ:
35
36
  options[varname.lower()] = bool(os.environ[varname])
@@ -38,7 +39,7 @@ def main(dns_resolver: Optional[_Resolver] = None) -> None:
38
39
  options[varname.lower()] = float(os.environ[varname])
39
40
 
40
41
  if len(sys.argv) == 1:
41
- # Validate the email addresses pased line-by-line on STDIN.
42
+ # Validate the email addresses passed line-by-line on STDIN.
42
43
  dns_resolver = dns_resolver or caching_resolver()
43
44
  for line in sys.stdin:
44
45
  email = line.strip()
@@ -2,7 +2,7 @@ from typing import Any, List, Optional, Tuple, TypedDict
2
2
 
3
3
  import ipaddress
4
4
 
5
- from .exceptions_types import EmailUndeliverableError
5
+ from .exceptions import EmailUndeliverableError
6
6
 
7
7
  import dns.resolver
8
8
  import dns.exception
@@ -0,0 +1,13 @@
1
+ class EmailNotValidError(ValueError):
2
+ """Parent class of all exceptions raised by this module."""
3
+ pass
4
+
5
+
6
+ class EmailSyntaxError(EmailNotValidError):
7
+ """Exception raised when an email address fails validation because of its form."""
8
+ pass
9
+
10
+
11
+ class EmailUndeliverableError(EmailNotValidError):
12
+ """Exception raised when an email address fails validation because its domain name does not appear deliverable."""
13
+ pass
@@ -36,12 +36,24 @@ DOMAIN_LITERAL_CHARS = re.compile(r"[\u0021-\u00FA\u005E-\u007E]")
36
36
  QTEXT_INTL = re.compile(r"[\u0020-\u007E\u0080-\U0010FFFF]")
37
37
 
38
38
  # Length constants
39
+
39
40
  # RFC 3696 + errata 1003 + errata 1690 (https://www.rfc-editor.org/errata_search.php?rfc=3696&eid=1690)
40
- # explains the maximum length of an email address is 254 octets.
41
+ # explains the maximum length of an email address is 254 octets based on RFC 5321 4.5.3.1.3. A
42
+ # maximum local part length is also given at RFC 5321 4.5.3.1.1.
43
+ #
44
+ # But RFC 5321 4.5.3.1 says that these (and other) limits are in a sense suggestions, and longer
45
+ # local parts have been seen in the wild. Consequntely, the local part length is only checked
46
+ # in "strict" mode. Although the email address maximum length is also somewhat of a suggestion,
47
+ # I don't like the idea of having no length checks performed, so I'm leaving that to always be
48
+ # checked.
41
49
  EMAIL_MAX_LENGTH = 254
42
50
  LOCAL_PART_MAX_LENGTH = 64
51
+
52
+ # Although RFC 5321 4.5.3.1.2 gives a (suggested, see above) limit of 255 octets, RFC 1035 2.3.4 also
53
+ # imposes a length limit (255 octets). But per https://stackoverflow.com/questions/32290167/what-is-the-maximum-length-of-a-dns-name,
54
+ # two of those octets are taken up by the optional final dot and null root label.
43
55
  DNS_LABEL_LENGTH_LIMIT = 63 # in "octets", RFC 1035 2.3.1
44
- DOMAIN_MAX_LENGTH = 253 # in "octets" as transmitted, RFC 1035 2.3.4 and RFC 5321 4.5.3.1.2, and see https://stackoverflow.com/questions/32290167/what-is-the-maximum-length-of-a-dns-name
56
+ DOMAIN_MAX_LENGTH = 253 # in "octets" as transmitted
45
57
 
46
58
  # RFC 2142
47
59
  CASE_INSENSITIVE_MAILBOX_NAMES = [
@@ -1,4 +1,5 @@
1
- from .exceptions_types import EmailSyntaxError, ValidatedEmail
1
+ from .exceptions import EmailSyntaxError
2
+ from .types import ValidatedEmail
2
3
  from .rfc_constants import EMAIL_MAX_LENGTH, LOCAL_PART_MAX_LENGTH, DOMAIN_MAX_LENGTH, \
3
4
  DOT_ATOM_TEXT, DOT_ATOM_TEXT_INTL, ATEXT_RE, ATEXT_INTL_DOT_RE, ATEXT_HOSTNAME_INTL, QTEXT_INTL, \
4
5
  DNS_LABEL_LENGTH_LIMIT, DOT_ATOM_TEXT_HOSTNAME, DOMAIN_NAME_REGEX, DOMAIN_LITERAL_CHARS
@@ -57,7 +58,7 @@ def split_email(email: str) -> Tuple[Optional[str], str, str, bool]:
57
58
  for i, c in enumerate(text):
58
59
  # < plus U+0338 (Combining Long Solidus Overlay) normalizes to
59
60
  # ≮ U+226E (Not Less-Than), and it would be confusing to treat
60
- # the < as the start of "<email>" syntax in that case. Liekwise,
61
+ # the < as the start of "<email>" syntax in that case. Likewise,
61
62
  # if anything combines with an @ or ", we should probably not
62
63
  # treat it as a special character.
63
64
  if unicodedata.normalize("NFC", text[i:])[0] != c:
@@ -82,7 +83,28 @@ def split_email(email: str) -> Tuple[Optional[str], str, str, bool]:
82
83
  else:
83
84
  left_part += c
84
85
 
86
+ # No special symbol found. The special symbols always
87
+ # include an at-sign, so this always indicates a missing
88
+ # at-sign. The other symbol is optional.
85
89
  if len(left_part) == len(text):
90
+ # The full-width at-sign might occur in CJK contexts.
91
+ # We can't accept it because we only accept addresess
92
+ # that are actually valid. But if this is common we
93
+ # may want to consider accepting and normalizing full-
94
+ # width characters for the other special symbols (and
95
+ # full-width dot is already accepted in internationalized
96
+ # domains) with a new option.
97
+ # See https://news.ycombinator.com/item?id=42235268.
98
+ if "@" in text:
99
+ raise EmailSyntaxError("The email address has the \"full-width\" at-sign (@) character instead of a regular at-sign.")
100
+
101
+ # Check another near-homoglyph for good measure because
102
+ # homoglyphs in place of required characters could be
103
+ # very confusing. We may want to consider checking for
104
+ # homoglyphs anywhere we look for a special symbol.
105
+ if "﹫" in text:
106
+ raise EmailSyntaxError('The email address has the "small commercial at" character instead of a regular at-sign.')
107
+
86
108
  raise EmailSyntaxError("An email address must have an @-sign.")
87
109
 
88
110
  # The right part is whatever is left.
@@ -207,7 +229,7 @@ class LocalPartValidationResult(TypedDict):
207
229
 
208
230
 
209
231
  def validate_email_local_part(local: str, allow_smtputf8: bool = True, allow_empty_local: bool = False,
210
- quoted_local_part: bool = False) -> LocalPartValidationResult:
232
+ quoted_local_part: bool = False, strict: bool = False) -> LocalPartValidationResult:
211
233
  """Validates the syntax of the local part of an email address."""
212
234
 
213
235
  if len(local) == 0:
@@ -229,7 +251,7 @@ def validate_email_local_part(local: str, allow_smtputf8: bool = True, allow_emp
229
251
  # internationalized, then the UTF-8 encoding may be longer, but
230
252
  # that may not be relevant. We will check the total address length
231
253
  # instead.
232
- if len(local) > LOCAL_PART_MAX_LENGTH:
254
+ if strict and len(local) > LOCAL_PART_MAX_LENGTH:
233
255
  reason = get_length_reason(local, limit=LOCAL_PART_MAX_LENGTH)
234
256
  raise EmailSyntaxError(f"The email address is too long before the @-sign {reason}.")
235
257
 
@@ -280,8 +302,8 @@ def validate_email_local_part(local: str, allow_smtputf8: bool = True, allow_emp
280
302
  valid = "dot-atom"
281
303
  requires_smtputf8 = True
282
304
 
283
- # There are no syntactic restrictions on quoted local parts, so if
284
- # it was originally quoted, it is probably valid. More characters
305
+ # There are no dot-atom syntax restrictions on quoted local parts, so
306
+ # if it was originally quoted, it is probably valid. More characters
285
307
  # are allowed, like @-signs, spaces, and quotes, and there are no
286
308
  # restrictions on the placement of dots, as in dot-atom local parts.
287
309
  elif quoted_local_part:
@@ -438,6 +460,36 @@ def check_dot_atom(label: str, start_descr: str, end_descr: str, is_hostname: bo
438
460
  raise EmailSyntaxError("An email address cannot have a period and a hyphen next to each other.")
439
461
 
440
462
 
463
+ def uts46_valid_char(char: str) -> bool:
464
+ # By exhaustively searching for characters rejected by
465
+ # for c in (chr(i) for i in range(0x110000)):
466
+ # idna.uts46_remap(c, std3_rules=False, transitional=False)
467
+ # I found the following rules are pretty close.
468
+ c = ord(char)
469
+ if 0x80 <= c <= 0x9f:
470
+ # 8-bit ASCII range.
471
+ return False
472
+ elif ((0x2010 <= c <= 0x2060 and not (0x2024 <= c <= 0x2026) and not (0x2028 <= c <= 0x202E))
473
+ or c in (0x00AD, 0x2064, 0xFF0E)
474
+ or 0x200B <= c <= 0x200D
475
+ or 0x1BCA0 <= c <= 0x1BCA3):
476
+ # Characters that are permitted but fall into one of the
477
+ # tests below.
478
+ return True
479
+ elif unicodedata.category(chr(c)) in ("Cf", "Cn", "Co", "Cs", "Zs", "Zl", "Zp"):
480
+ # There are a bunch of Zs characters including regular space
481
+ # that are allowed by UTS46 but are not allowed in domain
482
+ # names anyway.
483
+ #
484
+ # There are some Cn (unassigned) characters that the idna
485
+ # package doesn't reject but we can, I think.
486
+ return False
487
+ elif "002E" in unicodedata.decomposition(chr(c)).split(" "):
488
+ # Characters that decompose into a sequence with a dot.
489
+ return False
490
+ return True
491
+
492
+
441
493
  class DomainNameValidationResult(TypedDict):
442
494
  ascii_domain: str
443
495
  domain: str
@@ -462,6 +514,15 @@ def validate_email_domain_name(domain: str, test_environment: bool = False, glob
462
514
  # they may not be valid, safe, or sensible Unicode strings.
463
515
  check_unsafe_chars(domain)
464
516
 
517
+ # Reject characters that would be rejected by UTS-46 normalization next but
518
+ # with an error message under our control.
519
+ bad_chars = {
520
+ safe_character_display(c) for c in domain
521
+ if not uts46_valid_char(c)
522
+ }
523
+ if bad_chars:
524
+ raise EmailSyntaxError("The part after the @-sign contains invalid characters: " + ", ".join(sorted(bad_chars)) + ".")
525
+
465
526
  # Perform UTS-46 normalization, which includes casefolding, NFC normalization,
466
527
  # and converting all label separators (the period/full stop, fullwidth full stop,
467
528
  # ideographic full stop, and halfwidth ideographic full stop) to regular dots.
@@ -607,12 +668,12 @@ def validate_email_domain_name(domain: str, test_environment: bool = False, glob
607
668
  # case for this.
608
669
  bad_chars = {
609
670
  safe_character_display(c)
610
- for c in domain
671
+ for c in domain_i18n
611
672
  if not ATEXT_HOSTNAME_INTL.match(c)
612
673
  }
613
674
  if bad_chars:
614
675
  raise EmailSyntaxError("The part after the @-sign contains invalid characters: " + ", ".join(sorted(bad_chars)) + ".")
615
- check_unsafe_chars(domain)
676
+ check_unsafe_chars(domain_i18n)
616
677
 
617
678
  # Check that it can be encoded back to IDNA ASCII. We have no test
618
679
  # case for this.
@@ -642,7 +703,7 @@ def validate_email_length(addrinfo: ValidatedEmail) -> None:
642
703
  # form is checked first because it is the original input.
643
704
  # 2) The normalized email address. We perform Unicode NFC normalization of
644
705
  # the local part, we normalize the domain to internationalized characters
645
- # (if originaly IDNA ASCII) which also includes Unicode normalization,
706
+ # (if originally IDNA ASCII) which also includes Unicode normalization,
646
707
  # and we may remove quotes in quoted local parts. We recommend that
647
708
  # callers use this string, so it must be valid.
648
709
  # 3) The email address with the IDNA ASCII representation of the domain
@@ -2,21 +2,6 @@ import warnings
2
2
  from typing import Any, Dict, List, Optional, Tuple, Union
3
3
 
4
4
 
5
- class EmailNotValidError(ValueError):
6
- """Parent class of all exceptions raised by this module."""
7
- pass
8
-
9
-
10
- class EmailSyntaxError(EmailNotValidError):
11
- """Exception raised when an email address fails validation because of its form."""
12
- pass
13
-
14
-
15
- class EmailUndeliverableError(EmailNotValidError):
16
- """Exception raised when an email address fails validation because its domain name does not appear deliverable."""
17
- pass
18
-
19
-
20
5
  class ValidatedEmail:
21
6
  """The validate_email function returns objects of this type holding the normalized form of the email address
22
7
  and other information."""
@@ -1,7 +1,8 @@
1
1
  from typing import Optional, Union, TYPE_CHECKING
2
2
  import unicodedata
3
3
 
4
- from .exceptions_types import EmailSyntaxError, ValidatedEmail
4
+ from .exceptions import EmailSyntaxError
5
+ from .types import ValidatedEmail
5
6
  from .syntax import split_email, validate_email_local_part, validate_email_domain_name, validate_email_domain_literal, validate_email_length
6
7
  from .rfc_constants import CASE_INSENSITIVE_MAILBOX_NAMES
7
8
 
@@ -17,10 +18,11 @@ def validate_email(
17
18
  /, # prior arguments are positional-only
18
19
  *, # subsequent arguments are keyword-only
19
20
  allow_smtputf8: Optional[bool] = None,
20
- allow_empty_local: bool = False,
21
+ allow_empty_local: Optional[bool] = None,
21
22
  allow_quoted_local: Optional[bool] = None,
22
23
  allow_domain_literal: Optional[bool] = None,
23
24
  allow_display_name: Optional[bool] = None,
25
+ strict: Optional[bool] = None,
24
26
  check_deliverability: Optional[bool] = None,
25
27
  test_environment: Optional[bool] = None,
26
28
  globally_deliverable: Optional[bool] = None,
@@ -34,16 +36,20 @@ def validate_email(
34
36
  """
35
37
 
36
38
  # Fill in default values of arguments.
37
- from . import ALLOW_SMTPUTF8, ALLOW_QUOTED_LOCAL, ALLOW_DOMAIN_LITERAL, ALLOW_DISPLAY_NAME, \
38
- GLOBALLY_DELIVERABLE, CHECK_DELIVERABILITY, TEST_ENVIRONMENT, DEFAULT_TIMEOUT
39
+ from . import ALLOW_SMTPUTF8, ALLOW_EMPTY_LOCAL, ALLOW_QUOTED_LOCAL, ALLOW_DOMAIN_LITERAL, ALLOW_DISPLAY_NAME, \
40
+ STRICT, GLOBALLY_DELIVERABLE, CHECK_DELIVERABILITY, TEST_ENVIRONMENT, DEFAULT_TIMEOUT
39
41
  if allow_smtputf8 is None:
40
42
  allow_smtputf8 = ALLOW_SMTPUTF8
43
+ if allow_empty_local is None:
44
+ allow_empty_local = ALLOW_EMPTY_LOCAL
41
45
  if allow_quoted_local is None:
42
46
  allow_quoted_local = ALLOW_QUOTED_LOCAL
43
47
  if allow_domain_literal is None:
44
48
  allow_domain_literal = ALLOW_DOMAIN_LITERAL
45
49
  if allow_display_name is None:
46
50
  allow_display_name = ALLOW_DISPLAY_NAME
51
+ if strict is None:
52
+ strict = STRICT
47
53
  if check_deliverability is None:
48
54
  check_deliverability = CHECK_DELIVERABILITY
49
55
  if test_environment is None:
@@ -53,14 +59,18 @@ def validate_email(
53
59
  if timeout is None and dns_resolver is None:
54
60
  timeout = DEFAULT_TIMEOUT
55
61
 
56
- # Allow email to be a str or bytes instance. If bytes,
57
- # it must be ASCII because that's how the bytes work
58
- # on the wire with SMTP.
59
- if not isinstance(email, str):
62
+ if isinstance(email, str):
63
+ pass
64
+ elif isinstance(email, bytes):
65
+ # Allow email to be a bytes instance as if it is what
66
+ # will be transmitted on the wire. But assume SMTPUTF8
67
+ # is unavailable, so it must be ASCII.
60
68
  try:
61
69
  email = email.decode("ascii")
62
70
  except ValueError as e:
63
71
  raise EmailSyntaxError("The email address is not valid ASCII.") from e
72
+ else:
73
+ raise TypeError("email must be str or bytes")
64
74
 
65
75
  # Split the address into the display name (or None), the local part
66
76
  # (before the @-sign), and the domain part (after the @-sign).
@@ -70,6 +80,14 @@ def validate_email(
70
80
  display_name, local_part, domain_part, is_quoted_local_part \
71
81
  = split_email(email)
72
82
 
83
+ if display_name:
84
+ # UTS #39 3.3 Email Security Profiles for Identifiers requires
85
+ # display names (incorrectly called "quoted-string-part" there)
86
+ # to be NFC normalized. Since these are not a part of what we
87
+ # are really validating, we won't check that the input was NFC
88
+ # normalized, but we'll normalize in output.
89
+ display_name = unicodedata.normalize("NFC", display_name)
90
+
73
91
  # Collect return values in this instance.
74
92
  ret = ValidatedEmail()
75
93
  ret.original = ((local_part if not is_quoted_local_part
@@ -84,7 +102,8 @@ def validate_email(
84
102
  local_part_info = validate_email_local_part(local_part,
85
103
  allow_smtputf8=allow_smtputf8,
86
104
  allow_empty_local=allow_empty_local,
87
- quoted_local_part=is_quoted_local_part)
105
+ quoted_local_part=is_quoted_local_part,
106
+ strict=strict)
88
107
  ret.local_part = local_part_info["local_part"]
89
108
  ret.ascii_local_part = local_part_info["ascii_local_part"]
90
109
  ret.smtputf8 = local_part_info["smtputf8"]
@@ -92,13 +111,23 @@ def validate_email(
92
111
  # RFC 6532 section 3.1 says that Unicode NFC normalization should be applied,
93
112
  # so we'll return the NFC-normalized local part. Since the caller may use that
94
113
  # string in place of the original string, ensure it is also valid.
114
+ #
115
+ # UTS #39 3.3 Email Security Profiles for Identifiers requires local parts
116
+ # to be NFKC normalized, which loses some information in characters that can
117
+ # be decomposed. We might want to consider applying NFKC normalization, but
118
+ # we can't make the change easily because it would break database lookups
119
+ # for any caller that put a normalized address from a previous version of
120
+ # this library. (UTS #39 seems to require that the *input* be NKFC normalized
121
+ # and has other requirements that are hard to check without additional Unicode
122
+ # data, and I don't know whether the rules really apply in the wild.)
95
123
  normalized_local_part = unicodedata.normalize("NFC", ret.local_part)
96
124
  if normalized_local_part != ret.local_part:
97
125
  try:
98
126
  validate_email_local_part(normalized_local_part,
99
127
  allow_smtputf8=allow_smtputf8,
100
128
  allow_empty_local=allow_empty_local,
101
- quoted_local_part=is_quoted_local_part)
129
+ quoted_local_part=is_quoted_local_part,
130
+ strict=strict)
102
131
  except EmailSyntaxError as e:
103
132
  raise EmailSyntaxError("After Unicode normalization: " + str(e)) from e
104
133
  ret.local_part = normalized_local_part
@@ -0,0 +1 @@
1
+ __version__ = "2.3.0"
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
2
- Name: email_validator
3
- Version: 2.2.0
1
+ Metadata-Version: 2.4
2
+ Name: email-validator
3
+ Version: 2.3.0
4
4
  Summary: A robust email address syntax and deliverability validation library.
5
5
  Home-page: https://github.com/JoshData/python-email-validator
6
6
  Author: Joshua Tauberer
@@ -22,6 +22,7 @@ Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
23
  Requires-Dist: dnspython>=2.0.0
24
24
  Requires-Dist: idna>=2.0.0
25
+ Dynamic: license-file
25
26
 
26
27
  email-validator: Validate Email Addresses
27
28
  =========================================
@@ -37,7 +38,7 @@ users by their email address like on a registration form.
37
38
  Key features:
38
39
 
39
40
  * Checks that an email address has the correct syntax --- great for
40
- email-based registration/login forms or validing data.
41
+ email-based registration/login forms or validating data.
41
42
  * Gives friendly English error messages when validation fails that you
42
43
  can display to end-users.
43
44
  * Checks deliverability (optional): Does the domain name resolve?
@@ -168,13 +169,14 @@ The `validate_email` function also accepts the following keyword arguments
168
169
 
169
170
  `allow_quoted_local=False`: Set to `True` to allow obscure and potentially problematic email addresses in which the part of the address before the @-sign contains spaces, @-signs, or other surprising characters when the local part is surrounded in quotes (so-called quoted-string local parts). In the object returned by `validate_email`, the normalized local part removes any unnecessary backslash-escaping and even removes the surrounding quotes if the address would be valid without them. You can also set `email_validator.ALLOW_QUOTED_LOCAL` to `True` to turn this on for all calls by default.
170
171
 
171
- `allow_domain_literal=False`: Set to `True` to allow bracketed IPv4 and "IPv6:"-prefixd IPv6 addresses in the domain part of the email address. No deliverability checks are performed for these addresses. In the object returned by `validate_email`, the normalized domain will use the condensed IPv6 format, if applicable. The object's `domain_address` attribute will hold the parsed `ipaddress.IPv4Address` or `ipaddress.IPv6Address` object if applicable. You can also set `email_validator.ALLOW_DOMAIN_LITERAL` to `True` to turn this on for all calls by default.
172
+ `allow_domain_literal=False`: Set to `True` to allow bracketed IPv4 and "IPv6:"-prefixed IPv6 addresses in the domain part of the email address. No deliverability checks are performed for these addresses. In the object returned by `validate_email`, the normalized domain will use the condensed IPv6 format, if applicable. The object's `domain_address` attribute will hold the parsed `ipaddress.IPv4Address` or `ipaddress.IPv6Address` object if applicable. You can also set `email_validator.ALLOW_DOMAIN_LITERAL` to `True` to turn this on for all calls by default.
172
173
 
173
174
  `allow_display_name=False`: Set to `True` to allow a display name and bracketed address in the input string, like `My Name <me@example.org>`. It's implemented in the spirit but not the letter of RFC 5322 3.4, so it may be stricter or more relaxed than what you want. The display name, if present, is provided in the returned object's `display_name` field after being unquoted and unescaped. You can also set `email_validator.ALLOW_DISPLAY_NAME` to `True` to turn this on for all calls by default.
174
175
 
175
176
  `allow_empty_local=False`: Set to `True` to allow an empty local part (i.e.
176
177
  `@example.com`), e.g. for validating Postfix aliases.
177
178
 
179
+ `strict=False`: Set to `True` to perform additional syntax checks (currently only a local part length check). This should be used by mail service providers at address creation to ensure email addresses meet broad compatibility requirements.
178
180
 
179
181
  ### DNS timeout and cache
180
182
 
@@ -449,8 +451,7 @@ To release:
449
451
  * Update CHANGELOG.md.
450
452
  * Update the version number in `email_validator/version.py`.
451
453
  * Make & push a commit with the new version number and make sure tests pass.
452
- * Make & push a tag (see command below).
453
- * Make a release at https://github.com/JoshData/python-email-validator/releases/new.
454
+ * Make a release at https://github.com/JoshData/python-email-validator/releases/new creating a new tag (or use command below).
454
455
  * Publish a source and wheel distribution to pypi (see command below).
455
456
 
456
457
  ```sh
@@ -7,10 +7,11 @@ setup.py
7
7
  email_validator/__init__.py
8
8
  email_validator/__main__.py
9
9
  email_validator/deliverability.py
10
- email_validator/exceptions_types.py
10
+ email_validator/exceptions.py
11
11
  email_validator/py.typed
12
12
  email_validator/rfc_constants.py
13
13
  email_validator/syntax.py
14
+ email_validator/types.py
14
15
  email_validator/validate_email.py
15
16
  email_validator/version.py
16
17
  email_validator.egg-info/PKG-INFO
@@ -1,5 +1,5 @@
1
1
  [metadata]
2
- name = email_validator
2
+ name = email-validator
3
3
  version = attr: email_validator.version.__version__
4
4
  description = A robust email address syntax and deliverability validation library.
5
5
  long_description = file: README.md
@@ -295,24 +295,35 @@ def test_email_valid(email_input: str, output: ValidatedEmail) -> None:
295
295
  ),
296
296
  ),
297
297
  (
298
- 's\u0323\u0307@nfc.tld',
298
+ '\"s\u0323\u0307\" <s\u0323\u0307@nfc.tld>',
299
299
  MakeValidatedEmail(
300
300
  local_part='\u1E69',
301
301
  smtputf8=True,
302
302
  ascii_domain='nfc.tld',
303
303
  domain='nfc.tld',
304
304
  normalized='\u1E69@nfc.tld',
305
+ display_name='\u1E69'
306
+ ),
307
+ ),
308
+ (
309
+ '@@fullwidth.at',
310
+ MakeValidatedEmail(
311
+ local_part='@',
312
+ smtputf8=True,
313
+ ascii_domain='fullwidth.at',
314
+ domain='fullwidth.at',
315
+ normalized='@@fullwidth.at',
305
316
  ),
306
317
  ),
307
318
  ],
308
319
  )
309
320
  def test_email_valid_intl_local_part(email_input: str, output: ValidatedEmail) -> None:
310
321
  # Check that it passes when allow_smtputf8 is True.
311
- assert validate_email(email_input, check_deliverability=False) == output
322
+ assert validate_email(email_input, check_deliverability=False, allow_display_name=True) == output
312
323
 
313
324
  # Check that it fails when allow_smtputf8 is False.
314
325
  with pytest.raises(EmailSyntaxError) as exc_info:
315
- validate_email(email_input, allow_smtputf8=False, check_deliverability=False)
326
+ validate_email(email_input, allow_smtputf8=False, check_deliverability=False, allow_display_name=True)
316
327
  assert "Internationalized characters before the @-sign are not supported: " in str(exc_info.value)
317
328
 
318
329
 
@@ -363,6 +374,8 @@ def test_domain_literal() -> None:
363
374
  'email_input,error_msg',
364
375
  [
365
376
  ('hello.world', 'An email address must have an @-sign.'),
377
+ ('hello@world', 'The email address has the "full-width" at-sign (@) character instead of a regular at-sign.'),
378
+ ('hello﹫world', 'The email address has the "small commercial at" character instead of a regular at-sign.'),
366
379
  ('my@localhost', 'The part after the @-sign is not valid. It should have a period.'),
367
380
  ('my@.leadingdot.com', 'An email address cannot have a period immediately after the @-sign.'),
368
381
  ('my@.leadingfwdot.com', 'An email address cannot have a period immediately after the @-sign.'),
@@ -389,9 +402,7 @@ def test_domain_literal() -> None:
389
402
  ('.leadingdot@domain.com', 'An email address cannot start with a period.'),
390
403
  ('twodots..here@domain.com', 'An email address cannot have two periods in a row.'),
391
404
  ('trailingdot.@domain.email', 'An email address cannot have a period immediately before the @-sign.'),
392
- ('me@⒈wouldbeinvalid.com',
393
- "The part after the @-sign contains invalid characters (Codepoint U+2488 not allowed "
394
- "at position 1 in '⒈wouldbeinvalid.com')."),
405
+ ('me@⒈wouldbeinvalid.com', "The part after the @-sign contains invalid characters: '⒈'."),
395
406
  ('me@\u037e.com', "The part after the @-sign contains invalid characters after Unicode normalization: ';'."),
396
407
  ('me@\u1fef.com', "The part after the @-sign contains invalid characters after Unicode normalization: '`'."),
397
408
  ('@example.com', 'There must be something before the @-sign.'),
@@ -404,9 +415,6 @@ def test_domain_literal() -> None:
404
415
  ('test@\n', 'The part after the @-sign contains invalid characters: U+000A.'),
405
416
  ('bad"quotes"@example.com', 'The email address contains invalid characters before the @-sign: \'"\'.'),
406
417
  ('obsolete."quoted".atom@example.com', 'The email address contains invalid characters before the @-sign: \'"\'.'),
407
- ('11111111112222222222333333333344444444445555555555666666666677777@example.com', 'The email address is too long before the @-sign (1 character too many).'),
408
- ('111111111122222222223333333333444444444455555555556666666666777777@example.com', 'The email address is too long before the @-sign (2 characters too many).'),
409
- ('\uFB2C111111122222222223333333333444444444455555555556666666666777777@example.com', 'After Unicode normalization: The email address is too long before the @-sign (2 characters too many).'),
410
418
  ('me@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.11111111112222222222333333333344444444445555555555.com', 'The email address is too long after the @-sign (1 character too many).'),
411
419
  ('me@中1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444.com', 'The email address is too long after the @-sign (1 byte too many after IDNA encoding).'),
412
420
  ('me@\uFB2C1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444.com', 'The email address is too long after the @-sign (5 bytes too many after IDNA encoding).'),
@@ -456,6 +464,22 @@ def test_email_invalid_syntax(email_input: str, error_msg: str) -> None:
456
464
  assert str(exc_info.value) == error_msg
457
465
 
458
466
 
467
+ @pytest.mark.parametrize(
468
+ 'email_input,error_msg',
469
+ [
470
+ ('11111111112222222222333333333344444444445555555555666666666677777@example.com', 'The email address is too long before the @-sign (1 character too many).'),
471
+ ('111111111122222222223333333333444444444455555555556666666666777777@example.com', 'The email address is too long before the @-sign (2 characters too many).'),
472
+ ('\uFB2C111111122222222223333333333444444444455555555556666666666777777@example.com', 'After Unicode normalization: The email address is too long before the @-sign (2 characters too many).'),
473
+ ])
474
+ def test_email_invalid_syntax_strict(email_input: str, error_msg: str) -> None:
475
+ # Since these all have syntax errors, deliverability
476
+ # checks do not arise.
477
+ validate_email(email_input, check_deliverability=False) # pass without strict
478
+ with pytest.raises(EmailSyntaxError) as exc_info:
479
+ validate_email(email_input, strict=True, check_deliverability=False)
480
+ assert str(exc_info.value) == error_msg
481
+
482
+
459
483
  @pytest.mark.parametrize(
460
484
  'email_input',
461
485
  [
@@ -717,6 +741,11 @@ def test_pyisemail_tests(email_input: str, status: str) -> None:
717
741
  validate_email(email_input, test_environment=True)
718
742
  validate_email(email_input, allow_quoted_local=True, allow_domain_literal=True, test_environment=True)
719
743
 
744
+ elif status == "ISEMAIL_RFC5322_LOCAL_TOOLONG":
745
+ # Requires strict.
746
+ with pytest.raises(EmailSyntaxError):
747
+ validate_email(email_input, strict=True, test_environment=True)
748
+
720
749
  elif status == "ISEMAIL_RFC5321_QUOTEDSTRING":
721
750
  # Quoted-literal local parts are only valid with an option.
722
751
  with pytest.raises(EmailSyntaxError):
@@ -1 +0,0 @@
1
- __version__ = "2.2.0"
File without changes