email-validator 2.1.0.post1__tar.gz → 2.1.2__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.
- email_validator-2.1.2/LICENSE +27 -0
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/PKG-INFO +11 -7
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/README.md +8 -4
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/email_validator/__init__.py +0 -2
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/email_validator/deliverability.py +36 -10
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/email_validator/exceptions_types.py +2 -5
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/email_validator/rfc_constants.py +3 -3
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/email_validator/syntax.py +41 -35
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/email_validator/validate_email.py +2 -2
- email_validator-2.1.2/email_validator/version.py +1 -0
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/email_validator.egg-info/PKG-INFO +12 -8
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/pyproject.toml +1 -1
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/setup.cfg +2 -2
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/tests/mocked-dns-answers.json +66 -11
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/tests/mocked_dns_response.py +4 -3
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/tests/test_deliverability.py +25 -22
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/tests/test_syntax.py +31 -5
- email_validator-2.1.0.post1/LICENSE +0 -121
- email_validator-2.1.0.post1/email_validator/version.py +0 -1
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/MANIFEST.in +0 -0
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/email_validator/__main__.py +0 -0
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/email_validator/py.typed +0 -0
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/email_validator.egg-info/SOURCES.txt +0 -0
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/email_validator.egg-info/dependency_links.txt +0 -0
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/email_validator.egg-info/entry_points.txt +0 -0
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/email_validator.egg-info/requires.txt +0 -0
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/email_validator.egg-info/top_level.txt +0 -0
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/setup.py +0 -0
- {email_validator-2.1.0.post1 → email_validator-2.1.2}/tests/test_main.py +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
This is free and unencumbered software released into the public
|
|
2
|
+
domain.
|
|
3
|
+
|
|
4
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
5
|
+
distribute this software, either in source code form or as a
|
|
6
|
+
compiled binary, for any purpose, commercial or non-commercial,
|
|
7
|
+
and by any means.
|
|
8
|
+
|
|
9
|
+
In jurisdictions that recognize copyright laws, the author or
|
|
10
|
+
authors of this software dedicate any and all copyright
|
|
11
|
+
interest in the software to the public domain. We make this
|
|
12
|
+
dedication for the benefit of the public at large and to the
|
|
13
|
+
detriment of our heirs and successors. We intend this
|
|
14
|
+
dedication to be an overt act of relinquishment in perpetuity
|
|
15
|
+
of all present and future rights to this software under
|
|
16
|
+
copyright law.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
19
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
20
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
21
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
|
|
22
|
+
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
|
23
|
+
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
24
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
25
|
+
THE SOFTWARE.
|
|
26
|
+
|
|
27
|
+
For more information, please refer to <https://unlicense.org/>
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: email_validator
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.2
|
|
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
|
|
7
7
|
Author-email: jt@occams.info
|
|
8
|
-
License:
|
|
8
|
+
License: Unlicense
|
|
9
9
|
Keywords: email address validator
|
|
10
10
|
Classifier: Development Status :: 5 - Production/Stable
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: License ::
|
|
12
|
+
Classifier: License :: OSI Approved :: The Unlicense (Unlicense)
|
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.8
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.9
|
|
@@ -37,8 +37,8 @@ necessarily for composing an email message, see below).
|
|
|
37
37
|
|
|
38
38
|
Key features:
|
|
39
39
|
|
|
40
|
-
* Checks that an email address has the correct syntax ---
|
|
41
|
-
registration/login forms or
|
|
40
|
+
* Checks that an email address has the correct syntax --- great for
|
|
41
|
+
email-based registration/login forms or validing data.
|
|
42
42
|
* Gives friendly English error messages when validation fails that you
|
|
43
43
|
can display to end-users.
|
|
44
44
|
* Checks deliverability (optional): Does the domain name resolve?
|
|
@@ -233,7 +233,7 @@ local parts, a wider range of Unicode characters are allowed.
|
|
|
233
233
|
|
|
234
234
|
A surprisingly large number of Unicode characters are not safe to display,
|
|
235
235
|
especially when the email address is concatenated with other text, so this
|
|
236
|
-
library tries to protect you by not permitting
|
|
236
|
+
library tries to protect you by not permitting reserved, non-, private use,
|
|
237
237
|
formatting (which can be used to alter the display order of characters),
|
|
238
238
|
whitespace, and control characters, and combining characters
|
|
239
239
|
as the first character of the local part and the domain name (so that they
|
|
@@ -448,7 +448,6 @@ or likely to cause trouble:
|
|
|
448
448
|
No one uses these forms anymore, and I can't think of any reason why anyone
|
|
449
449
|
using this library would need to accept them.
|
|
450
450
|
|
|
451
|
-
|
|
452
451
|
Testing
|
|
453
452
|
-------
|
|
454
453
|
|
|
@@ -480,3 +479,8 @@ git tag v$(cat email_validator/version.py | sed "s/.* = //" | sed 's/"//g')
|
|
|
480
479
|
git push --tags
|
|
481
480
|
./release_to_pypi.sh
|
|
482
481
|
```
|
|
482
|
+
|
|
483
|
+
License
|
|
484
|
+
-------
|
|
485
|
+
|
|
486
|
+
This project is free of any copyright restrictions per the [Unlicense](https://unlicense.org/). (Prior to Feb. 4, 2024, the project was made available under the terms of the [CC0 1.0 Universal public domain dedication](http://creativecommons.org/publicdomain/zero/1.0/).) See [LICENSE](LICENSE) and [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
@@ -12,8 +12,8 @@ necessarily for composing an email message, see below).
|
|
|
12
12
|
|
|
13
13
|
Key features:
|
|
14
14
|
|
|
15
|
-
* Checks that an email address has the correct syntax ---
|
|
16
|
-
registration/login forms or
|
|
15
|
+
* Checks that an email address has the correct syntax --- great for
|
|
16
|
+
email-based registration/login forms or validing data.
|
|
17
17
|
* Gives friendly English error messages when validation fails that you
|
|
18
18
|
can display to end-users.
|
|
19
19
|
* Checks deliverability (optional): Does the domain name resolve?
|
|
@@ -208,7 +208,7 @@ local parts, a wider range of Unicode characters are allowed.
|
|
|
208
208
|
|
|
209
209
|
A surprisingly large number of Unicode characters are not safe to display,
|
|
210
210
|
especially when the email address is concatenated with other text, so this
|
|
211
|
-
library tries to protect you by not permitting
|
|
211
|
+
library tries to protect you by not permitting reserved, non-, private use,
|
|
212
212
|
formatting (which can be used to alter the display order of characters),
|
|
213
213
|
whitespace, and control characters, and combining characters
|
|
214
214
|
as the first character of the local part and the domain name (so that they
|
|
@@ -423,7 +423,6 @@ or likely to cause trouble:
|
|
|
423
423
|
No one uses these forms anymore, and I can't think of any reason why anyone
|
|
424
424
|
using this library would need to accept them.
|
|
425
425
|
|
|
426
|
-
|
|
427
426
|
Testing
|
|
428
427
|
-------
|
|
429
428
|
|
|
@@ -455,3 +454,8 @@ git tag v$(cat email_validator/version.py | sed "s/.* = //" | sed 's/"//g')
|
|
|
455
454
|
git push --tags
|
|
456
455
|
./release_to_pypi.sh
|
|
457
456
|
```
|
|
457
|
+
|
|
458
|
+
License
|
|
459
|
+
-------
|
|
460
|
+
|
|
461
|
+
This project is free of any copyright restrictions per the [Unlicense](https://unlicense.org/). (Prior to Feb. 4, 2024, the project was made available under the terms of the [CC0 1.0 Universal public domain dedication](http://creativecommons.org/publicdomain/zero/1.0/).) See [LICENSE](LICENSE) and [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
from typing import Optional, Any, Dict
|
|
2
2
|
|
|
3
|
+
import ipaddress
|
|
4
|
+
|
|
3
5
|
from .exceptions_types import EmailUndeliverableError
|
|
4
6
|
|
|
5
7
|
import dns.resolver
|
|
6
8
|
import dns.exception
|
|
7
9
|
|
|
8
10
|
|
|
9
|
-
def caching_resolver(*, timeout: Optional[int] = None, cache=None):
|
|
11
|
+
def caching_resolver(*, timeout: Optional[int] = None, cache=None, dns_resolver=None):
|
|
10
12
|
if timeout is None:
|
|
11
13
|
from . import DEFAULT_TIMEOUT
|
|
12
14
|
timeout = DEFAULT_TIMEOUT
|
|
13
|
-
resolver = dns.resolver.Resolver()
|
|
15
|
+
resolver = dns_resolver or dns.resolver.Resolver()
|
|
14
16
|
resolver.cache = cache or dns.resolver.LRUCache() # type: ignore
|
|
15
17
|
resolver.lifetime = timeout # type: ignore # timeout, in seconds
|
|
16
18
|
return resolver
|
|
@@ -57,10 +59,30 @@ def validate_email_deliverability(domain: str, domain_i18n: str, timeout: Option
|
|
|
57
59
|
deliverability_info["mx_fallback_type"] = None
|
|
58
60
|
|
|
59
61
|
except dns.resolver.NoAnswer:
|
|
60
|
-
# If there was no MX record, fall back to an A
|
|
62
|
+
# If there was no MX record, fall back to an A or AAA record
|
|
63
|
+
# (RFC 5321 Section 5). Check A first since it's more common.
|
|
64
|
+
|
|
65
|
+
# If the A/AAAA response has no Globally Reachable IP address,
|
|
66
|
+
# treat the response as if it were NoAnswer, i.e., the following
|
|
67
|
+
# address types are not allowed fallbacks: Private-Use, Loopback,
|
|
68
|
+
# Link-Local, and some other obscure ranges. See
|
|
69
|
+
# https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
|
|
70
|
+
# https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
|
|
71
|
+
# (Issue #134.)
|
|
72
|
+
def is_global_addr(ipaddr):
|
|
73
|
+
try:
|
|
74
|
+
ipaddr = ipaddress.ip_address(ipaddr)
|
|
75
|
+
except ValueError:
|
|
76
|
+
return False
|
|
77
|
+
return ipaddr.is_global
|
|
78
|
+
|
|
61
79
|
try:
|
|
62
80
|
response = dns_resolver.resolve(domain, "A")
|
|
63
|
-
|
|
81
|
+
|
|
82
|
+
if not any(is_global_addr(r.address) for r in response):
|
|
83
|
+
raise dns.resolver.NoAnswer # fall back to AAAA
|
|
84
|
+
|
|
85
|
+
deliverability_info["mx"] = [(0, domain)]
|
|
64
86
|
deliverability_info["mx_fallback_type"] = "A"
|
|
65
87
|
|
|
66
88
|
except dns.resolver.NoAnswer:
|
|
@@ -69,15 +91,19 @@ def validate_email_deliverability(domain: str, domain_i18n: str, timeout: Option
|
|
|
69
91
|
# (It's unclear if SMTP servers actually do this.)
|
|
70
92
|
try:
|
|
71
93
|
response = dns_resolver.resolve(domain, "AAAA")
|
|
72
|
-
|
|
94
|
+
|
|
95
|
+
if not any(is_global_addr(r.address) for r in response):
|
|
96
|
+
raise dns.resolver.NoAnswer
|
|
97
|
+
|
|
98
|
+
deliverability_info["mx"] = [(0, domain)]
|
|
73
99
|
deliverability_info["mx_fallback_type"] = "AAAA"
|
|
74
100
|
|
|
75
|
-
except dns.resolver.NoAnswer:
|
|
101
|
+
except dns.resolver.NoAnswer as e:
|
|
76
102
|
# If there was no MX, A, or AAAA record, then mail to
|
|
77
103
|
# this domain is not deliverable, although the domain
|
|
78
104
|
# name has other records (otherwise NXDOMAIN would
|
|
79
105
|
# have been raised).
|
|
80
|
-
raise EmailUndeliverableError(f"The domain name {domain_i18n} does not accept email.")
|
|
106
|
+
raise EmailUndeliverableError(f"The domain name {domain_i18n} does not accept email.") from e
|
|
81
107
|
|
|
82
108
|
# Check for a SPF (RFC 7208) reject-all record ("v=spf1 -all") which indicates
|
|
83
109
|
# no emails are sent from this domain (similar to a Null MX record
|
|
@@ -96,10 +122,10 @@ def validate_email_deliverability(domain: str, domain_i18n: str, timeout: Option
|
|
|
96
122
|
# No TXT records means there is no SPF policy, so we cannot take any action.
|
|
97
123
|
pass
|
|
98
124
|
|
|
99
|
-
except dns.resolver.NXDOMAIN:
|
|
125
|
+
except dns.resolver.NXDOMAIN as e:
|
|
100
126
|
# The domain name does not exist --- there are no records of any sort
|
|
101
127
|
# for the domain name.
|
|
102
|
-
raise EmailUndeliverableError(f"The domain name {domain_i18n} does not exist.")
|
|
128
|
+
raise EmailUndeliverableError(f"The domain name {domain_i18n} does not exist.") from e
|
|
103
129
|
|
|
104
130
|
except dns.resolver.NoNameservers:
|
|
105
131
|
# All nameservers failed to answer the query. This might be a problem
|
|
@@ -122,6 +148,6 @@ def validate_email_deliverability(domain: str, domain_i18n: str, timeout: Option
|
|
|
122
148
|
# Unhandled conditions should not propagate.
|
|
123
149
|
raise EmailUndeliverableError(
|
|
124
150
|
"There was an error while checking if the domain name in the email address is deliverable: " + str(e)
|
|
125
|
-
)
|
|
151
|
+
) from e
|
|
126
152
|
|
|
127
153
|
return deliverability_info
|
|
@@ -17,7 +17,7 @@ class EmailUndeliverableError(EmailNotValidError):
|
|
|
17
17
|
pass
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
class ValidatedEmail
|
|
20
|
+
class ValidatedEmail:
|
|
21
21
|
"""The validate_email function returns objects of this type holding the normalized form of the email address
|
|
22
22
|
and other information."""
|
|
23
23
|
|
|
@@ -80,7 +80,6 @@ class ValidatedEmail(object):
|
|
|
80
80
|
|
|
81
81
|
@property
|
|
82
82
|
def email(self):
|
|
83
|
-
import warnings
|
|
84
83
|
warnings.warn("ValidatedEmail.email is deprecated and will be removed, use ValidatedEmail.normalized instead", DeprecationWarning)
|
|
85
84
|
return self.normalized
|
|
86
85
|
|
|
@@ -126,9 +125,7 @@ class ValidatedEmail(object):
|
|
|
126
125
|
"""This helps producing the README."""
|
|
127
126
|
def as_constructor(self):
|
|
128
127
|
return "ValidatedEmail(" \
|
|
129
|
-
+ ",".join("\n {}={}"
|
|
130
|
-
key,
|
|
131
|
-
repr(getattr(self, key)))
|
|
128
|
+
+ ",".join(f"\n {key}={repr(getattr(self, key))}"
|
|
132
129
|
for key in ('normalized', 'local_part', 'domain',
|
|
133
130
|
'ascii_email', 'ascii_local_part', 'ascii_domain',
|
|
134
131
|
'smtputf8', 'mx', 'mx_fallback_type')
|
|
@@ -12,7 +12,7 @@ DOT_ATOM_TEXT = re.compile('[' + ATEXT + ']+(?:\\.[' + ATEXT + r']+)*\Z')
|
|
|
12
12
|
# addresses to also include three specific ranges of UTF8 defined in
|
|
13
13
|
# RFC 3629 section 4, which appear to be the Unicode code points from
|
|
14
14
|
# U+0080 to U+10FFFF.
|
|
15
|
-
ATEXT_INTL = ATEXT +
|
|
15
|
+
ATEXT_INTL = ATEXT + "\u0080-\U0010FFFF"
|
|
16
16
|
ATEXT_INTL_RE = re.compile('[.' + ATEXT_INTL + ']') # ATEXT_INTL plus dots
|
|
17
17
|
DOT_ATOM_TEXT_INTL = re.compile('[' + ATEXT_INTL + ']+(?:\\.[' + ATEXT_INTL + r']+)*\Z')
|
|
18
18
|
|
|
@@ -42,11 +42,11 @@ QTEXT_INTL = re.compile(r"[\u0020-\u007E\u0080-\U0010FFFF]")
|
|
|
42
42
|
EMAIL_MAX_LENGTH = 254
|
|
43
43
|
LOCAL_PART_MAX_LENGTH = 64
|
|
44
44
|
DNS_LABEL_LENGTH_LIMIT = 63 # in "octets", RFC 1035 2.3.1
|
|
45
|
-
DOMAIN_MAX_LENGTH =
|
|
45
|
+
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
|
|
46
46
|
|
|
47
47
|
# RFC 2142
|
|
48
48
|
CASE_INSENSITIVE_MAILBOX_NAMES = [
|
|
49
|
-
'info', '
|
|
49
|
+
'info', 'marketing', 'sales', 'support', # section 3
|
|
50
50
|
'abuse', 'noc', 'security', # section 4
|
|
51
51
|
'postmaster', 'hostmaster', 'usenet', 'news', 'webmaster', 'www', 'uucp', 'ftp', # section 5
|
|
52
52
|
]
|
|
@@ -26,7 +26,6 @@ def split_email(email):
|
|
|
26
26
|
# Since backslash-escaping is no longer needed because
|
|
27
27
|
# the quotes are removed, remove backslash-escaping
|
|
28
28
|
# to return in the normalized form.
|
|
29
|
-
import re
|
|
30
29
|
local_part = re.sub(r"\\(.)", "\\1", local_part)
|
|
31
30
|
|
|
32
31
|
return local_part, domain_part, True
|
|
@@ -72,14 +71,14 @@ def validate_email_local_part(local: str, allow_smtputf8: bool = True, allow_emp
|
|
|
72
71
|
if len(local) == 0:
|
|
73
72
|
if not allow_empty_local:
|
|
74
73
|
raise EmailSyntaxError("There must be something before the @-sign.")
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
74
|
+
|
|
75
|
+
# The caller allows an empty local part. Useful for validating certain
|
|
76
|
+
# Postfix aliases.
|
|
77
|
+
return {
|
|
78
|
+
"local_part": local,
|
|
79
|
+
"ascii_local_part": local,
|
|
80
|
+
"smtputf8": False,
|
|
81
|
+
}
|
|
83
82
|
|
|
84
83
|
# Check the length of the local part by counting characters.
|
|
85
84
|
# (RFC 5321 4.5.3.1.1)
|
|
@@ -124,11 +123,11 @@ def validate_email_local_part(local: str, allow_smtputf8: bool = True, allow_emp
|
|
|
124
123
|
# Check for invalid characters against the non-internationalized
|
|
125
124
|
# permitted character set.
|
|
126
125
|
# (RFC 5322 3.2.3)
|
|
127
|
-
bad_chars =
|
|
126
|
+
bad_chars = {
|
|
128
127
|
safe_character_display(c)
|
|
129
128
|
for c in local
|
|
130
129
|
if not ATEXT_RE.match(c)
|
|
131
|
-
|
|
130
|
+
}
|
|
132
131
|
if bad_chars:
|
|
133
132
|
raise EmailSyntaxError("Internationalized characters before the @-sign are not supported: " + ", ".join(sorted(bad_chars)) + ".")
|
|
134
133
|
|
|
@@ -148,20 +147,20 @@ def validate_email_local_part(local: str, allow_smtputf8: bool = True, allow_emp
|
|
|
148
147
|
# (RFC 5321 4.1.2. RFC 5322 lists additional permitted *obsolete*
|
|
149
148
|
# characters which are *not* allowed here. RFC 6531 section 3.3
|
|
150
149
|
# extends the range to UTF8 strings.)
|
|
151
|
-
bad_chars =
|
|
150
|
+
bad_chars = {
|
|
152
151
|
safe_character_display(c)
|
|
153
152
|
for c in local
|
|
154
153
|
if not QTEXT_INTL.match(c)
|
|
155
|
-
|
|
154
|
+
}
|
|
156
155
|
if bad_chars:
|
|
157
156
|
raise EmailSyntaxError("The email address contains invalid characters in quotes before the @-sign: " + ", ".join(sorted(bad_chars)) + ".")
|
|
158
157
|
|
|
159
158
|
# See if any characters are outside of the ASCII range.
|
|
160
|
-
bad_chars =
|
|
159
|
+
bad_chars = {
|
|
161
160
|
safe_character_display(c)
|
|
162
161
|
for c in local
|
|
163
162
|
if not (32 <= ord(c) <= 126)
|
|
164
|
-
|
|
163
|
+
}
|
|
165
164
|
if bad_chars:
|
|
166
165
|
requires_smtputf8 = True
|
|
167
166
|
|
|
@@ -191,8 +190,8 @@ def validate_email_local_part(local: str, allow_smtputf8: bool = True, allow_emp
|
|
|
191
190
|
# want to have an unhandled exception later.
|
|
192
191
|
try:
|
|
193
192
|
local.encode("utf8")
|
|
194
|
-
except ValueError:
|
|
195
|
-
raise EmailSyntaxError("The email address contains an invalid character.")
|
|
193
|
+
except ValueError as e:
|
|
194
|
+
raise EmailSyntaxError("The email address contains an invalid character.") from e
|
|
196
195
|
|
|
197
196
|
# If this address passes only by the quoted string form, re-quote it
|
|
198
197
|
# and backslash-escape quotes and backslashes (removing any unnecessary
|
|
@@ -213,11 +212,11 @@ def validate_email_local_part(local: str, allow_smtputf8: bool = True, allow_emp
|
|
|
213
212
|
|
|
214
213
|
# Check for invalid characters.
|
|
215
214
|
# (RFC 5322 3.2.3, plus RFC 6531 3.3)
|
|
216
|
-
bad_chars =
|
|
215
|
+
bad_chars = {
|
|
217
216
|
safe_character_display(c)
|
|
218
217
|
for c in local
|
|
219
218
|
if not ATEXT_INTL_RE.match(c)
|
|
220
|
-
|
|
219
|
+
}
|
|
221
220
|
if bad_chars:
|
|
222
221
|
raise EmailSyntaxError("The email address contains invalid characters before the @-sign: " + ", ".join(sorted(bad_chars)) + ".")
|
|
223
222
|
|
|
@@ -264,7 +263,7 @@ def check_unsafe_chars(s, allow_space=False):
|
|
|
264
263
|
# as far as I can tell, but they violate the spirit of the non-internationalized
|
|
265
264
|
# specification that email addresses do not contain line breaks when not quoted.
|
|
266
265
|
bad_chars.add(c)
|
|
267
|
-
elif category[0]
|
|
266
|
+
elif category[0] == "C":
|
|
268
267
|
# Control, format, surrogate, private use, and unassigned code points (C)
|
|
269
268
|
# are all unsafe in various ways. Control and format characters can affect
|
|
270
269
|
# text rendering if the email address is concatenated with other text.
|
|
@@ -306,11 +305,11 @@ def validate_email_domain_name(domain, test_environment=False, globally_delivera
|
|
|
306
305
|
|
|
307
306
|
# Check for invalid characters before normalization.
|
|
308
307
|
# (RFC 952 plus RFC 6531 section 3.3 for internationalized addresses)
|
|
309
|
-
bad_chars =
|
|
308
|
+
bad_chars = {
|
|
310
309
|
safe_character_display(c)
|
|
311
310
|
for c in domain
|
|
312
311
|
if not ATEXT_HOSTNAME_INTL.match(c)
|
|
313
|
-
|
|
312
|
+
}
|
|
314
313
|
if bad_chars:
|
|
315
314
|
raise EmailSyntaxError("The part after the @-sign contains invalid characters: " + ", ".join(sorted(bad_chars)) + ".")
|
|
316
315
|
|
|
@@ -322,15 +321,17 @@ def validate_email_domain_name(domain, test_environment=False, globally_delivera
|
|
|
322
321
|
|
|
323
322
|
# Perform UTS-46 normalization, which includes casefolding, NFC normalization,
|
|
324
323
|
# and converting all label separators (the period/full stop, fullwidth full stop,
|
|
325
|
-
# ideographic full stop, and halfwidth ideographic full stop) to
|
|
324
|
+
# ideographic full stop, and halfwidth ideographic full stop) to regular dots.
|
|
326
325
|
# It will also raise an exception if there is an invalid character in the input,
|
|
327
|
-
# such as "⒈" which is invalid because it would expand to include a
|
|
326
|
+
# such as "⒈" which is invalid because it would expand to include a dot.
|
|
327
|
+
# Since several characters are normalized to a dot, this has to come before
|
|
328
|
+
# checks related to dots, like check_dot_atom which comes next.
|
|
328
329
|
try:
|
|
329
330
|
domain = idna.uts46_remap(domain, std3_rules=False, transitional=False)
|
|
330
331
|
except idna.IDNAError as e:
|
|
331
|
-
raise EmailSyntaxError(f"The part after the @-sign contains invalid characters ({e}).")
|
|
332
|
+
raise EmailSyntaxError(f"The part after the @-sign contains invalid characters ({e}).") from e
|
|
332
333
|
|
|
333
|
-
# The domain part is made up
|
|
334
|
+
# The domain part is made up dot-separated "labels." Each label must
|
|
334
335
|
# have at least one character and cannot start or end with dashes, which
|
|
335
336
|
# means there are some surprising restrictions on periods and dashes.
|
|
336
337
|
# Check that before we do IDNA encoding because the IDNA library gives
|
|
@@ -362,6 +363,8 @@ def validate_email_domain_name(domain, test_environment=False, globally_delivera
|
|
|
362
363
|
# For ASCII-only domains, the transformation does nothing and is safe to
|
|
363
364
|
# apply. However, to ensure we don't rely on the idna library for basic
|
|
364
365
|
# syntax checks, we don't use it if it's not needed.
|
|
366
|
+
#
|
|
367
|
+
# uts46 is off here because it is handled above.
|
|
365
368
|
try:
|
|
366
369
|
ascii_domain = idna.encode(domain, uts46=False).decode("ascii")
|
|
367
370
|
except idna.IDNAError as e:
|
|
@@ -370,8 +373,11 @@ def validate_email_domain_name(domain, test_environment=False, globally_delivera
|
|
|
370
373
|
# the length check is applied to a string that is different from the
|
|
371
374
|
# one the user supplied. Also I'm not sure if the length check applies
|
|
372
375
|
# to the internationalized form, the IDNA ASCII form, or even both!
|
|
373
|
-
raise EmailSyntaxError("The email address is too long after the @-sign.")
|
|
374
|
-
|
|
376
|
+
raise EmailSyntaxError("The email address is too long after the @-sign.") from e
|
|
377
|
+
|
|
378
|
+
# Other errors seem to not be possible because the call to idna.uts46_remap
|
|
379
|
+
# would have already raised them.
|
|
380
|
+
raise EmailSyntaxError(f"The part after the @-sign contains invalid characters ({e}).") from e
|
|
375
381
|
|
|
376
382
|
# Check the syntax of the string returned by idna.encode.
|
|
377
383
|
# It should never fail.
|
|
@@ -433,15 +439,15 @@ def validate_email_domain_name(domain, test_environment=False, globally_delivera
|
|
|
433
439
|
try:
|
|
434
440
|
domain_i18n = idna.decode(ascii_domain.encode('ascii'))
|
|
435
441
|
except idna.IDNAError as e:
|
|
436
|
-
raise EmailSyntaxError(f"The part after the @-sign is not valid IDNA ({e}).")
|
|
442
|
+
raise EmailSyntaxError(f"The part after the @-sign is not valid IDNA ({e}).") from e
|
|
437
443
|
|
|
438
444
|
# Check for invalid characters after normalization. These
|
|
439
445
|
# should never arise. See the similar checks above.
|
|
440
|
-
bad_chars =
|
|
446
|
+
bad_chars = {
|
|
441
447
|
safe_character_display(c)
|
|
442
448
|
for c in domain
|
|
443
449
|
if not ATEXT_HOSTNAME_INTL.match(c)
|
|
444
|
-
|
|
450
|
+
}
|
|
445
451
|
if bad_chars:
|
|
446
452
|
raise EmailSyntaxError("The part after the @-sign contains invalid characters: " + ", ".join(sorted(bad_chars)) + ".")
|
|
447
453
|
check_unsafe_chars(domain)
|
|
@@ -511,7 +517,7 @@ def validate_email_domain_literal(domain_literal):
|
|
|
511
517
|
try:
|
|
512
518
|
addr = ipaddress.IPv4Address(domain_literal)
|
|
513
519
|
except ValueError as e:
|
|
514
|
-
raise EmailSyntaxError(f"The address in brackets after the @-sign is not valid: It is not an IPv4 address ({e}) or is missing an address literal tag.")
|
|
520
|
+
raise EmailSyntaxError(f"The address in brackets after the @-sign is not valid: It is not an IPv4 address ({e}) or is missing an address literal tag.") from e
|
|
515
521
|
|
|
516
522
|
# Return the IPv4Address object and the domain back unchanged.
|
|
517
523
|
return {
|
|
@@ -524,7 +530,7 @@ def validate_email_domain_literal(domain_literal):
|
|
|
524
530
|
try:
|
|
525
531
|
addr = ipaddress.IPv6Address(domain_literal[5:])
|
|
526
532
|
except ValueError as e:
|
|
527
|
-
raise EmailSyntaxError(f"The IPv6 address in brackets after the @-sign is not valid ({e}).")
|
|
533
|
+
raise EmailSyntaxError(f"The IPv6 address in brackets after the @-sign is not valid ({e}).") from e
|
|
528
534
|
|
|
529
535
|
# Return the IPv6Address object and construct a normalized
|
|
530
536
|
# domain literal.
|
|
@@ -544,11 +550,11 @@ def validate_email_domain_literal(domain_literal):
|
|
|
544
550
|
|
|
545
551
|
# Check for permitted ASCII characters. This actually doesn't matter
|
|
546
552
|
# since there will be an exception after anyway.
|
|
547
|
-
bad_chars =
|
|
553
|
+
bad_chars = {
|
|
548
554
|
safe_character_display(c)
|
|
549
555
|
for c in domain_literal
|
|
550
556
|
if not DOMAIN_LITERAL_CHARS.match(c)
|
|
551
|
-
|
|
557
|
+
}
|
|
552
558
|
if bad_chars:
|
|
553
559
|
raise EmailSyntaxError("The part after the @-sign contains invalid characters in brackets: " + ", ".join(sorted(bad_chars)) + ".")
|
|
554
560
|
|
|
@@ -49,8 +49,8 @@ def validate_email(
|
|
|
49
49
|
if not isinstance(email, str):
|
|
50
50
|
try:
|
|
51
51
|
email = email.decode("ascii")
|
|
52
|
-
except ValueError:
|
|
53
|
-
raise EmailSyntaxError("The email address is not valid ASCII.")
|
|
52
|
+
except ValueError as e:
|
|
53
|
+
raise EmailSyntaxError("The email address is not valid ASCII.") from e
|
|
54
54
|
|
|
55
55
|
# Split the address into the local part (before the @-sign)
|
|
56
56
|
# and the domain part (after the @-sign). Normally, there
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.1.2"
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
|
-
Name:
|
|
3
|
-
Version: 2.1.
|
|
2
|
+
Name: email_validator
|
|
3
|
+
Version: 2.1.2
|
|
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
|
|
7
7
|
Author-email: jt@occams.info
|
|
8
|
-
License:
|
|
8
|
+
License: Unlicense
|
|
9
9
|
Keywords: email address validator
|
|
10
10
|
Classifier: Development Status :: 5 - Production/Stable
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: License ::
|
|
12
|
+
Classifier: License :: OSI Approved :: The Unlicense (Unlicense)
|
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.8
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.9
|
|
@@ -37,8 +37,8 @@ necessarily for composing an email message, see below).
|
|
|
37
37
|
|
|
38
38
|
Key features:
|
|
39
39
|
|
|
40
|
-
* Checks that an email address has the correct syntax ---
|
|
41
|
-
registration/login forms or
|
|
40
|
+
* Checks that an email address has the correct syntax --- great for
|
|
41
|
+
email-based registration/login forms or validing data.
|
|
42
42
|
* Gives friendly English error messages when validation fails that you
|
|
43
43
|
can display to end-users.
|
|
44
44
|
* Checks deliverability (optional): Does the domain name resolve?
|
|
@@ -233,7 +233,7 @@ local parts, a wider range of Unicode characters are allowed.
|
|
|
233
233
|
|
|
234
234
|
A surprisingly large number of Unicode characters are not safe to display,
|
|
235
235
|
especially when the email address is concatenated with other text, so this
|
|
236
|
-
library tries to protect you by not permitting
|
|
236
|
+
library tries to protect you by not permitting reserved, non-, private use,
|
|
237
237
|
formatting (which can be used to alter the display order of characters),
|
|
238
238
|
whitespace, and control characters, and combining characters
|
|
239
239
|
as the first character of the local part and the domain name (so that they
|
|
@@ -448,7 +448,6 @@ or likely to cause trouble:
|
|
|
448
448
|
No one uses these forms anymore, and I can't think of any reason why anyone
|
|
449
449
|
using this library would need to accept them.
|
|
450
450
|
|
|
451
|
-
|
|
452
451
|
Testing
|
|
453
452
|
-------
|
|
454
453
|
|
|
@@ -480,3 +479,8 @@ git tag v$(cat email_validator/version.py | sed "s/.* = //" | sed 's/"//g')
|
|
|
480
479
|
git push --tags
|
|
481
480
|
./release_to_pypi.sh
|
|
482
481
|
```
|
|
482
|
+
|
|
483
|
+
License
|
|
484
|
+
-------
|
|
485
|
+
|
|
486
|
+
This project is free of any copyright restrictions per the [Unlicense](https://unlicense.org/). (Prior to Feb. 4, 2024, the project was made available under the terms of the [CC0 1.0 Universal public domain dedication](http://creativecommons.org/publicdomain/zero/1.0/).) See [LICENSE](LICENSE) and [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
@@ -7,12 +7,12 @@ long_description_content_type = text/markdown
|
|
|
7
7
|
url = https://github.com/JoshData/python-email-validator
|
|
8
8
|
author = Joshua Tauberer
|
|
9
9
|
author_email = jt@occams.info
|
|
10
|
-
license =
|
|
10
|
+
license = Unlicense
|
|
11
11
|
license_files = LICENSE
|
|
12
12
|
classifiers =
|
|
13
13
|
Development Status :: 5 - Production/Stable
|
|
14
14
|
Intended Audience :: Developers
|
|
15
|
-
License ::
|
|
15
|
+
License :: OSI Approved :: The Unlicense (Unlicense)
|
|
16
16
|
Programming Language :: Python :: 3
|
|
17
17
|
Programming Language :: Python :: 3.8
|
|
18
18
|
Programming Language :: Python :: 3.9
|
|
@@ -7,20 +7,41 @@
|
|
|
7
7
|
},
|
|
8
8
|
"answer": [
|
|
9
9
|
"10 alt1.gmail-smtp-in.l.google.com.",
|
|
10
|
-
"30 alt3.gmail-smtp-in.l.google.com.",
|
|
11
|
-
"5 gmail-smtp-in.l.google.com.",
|
|
12
10
|
"20 alt2.gmail-smtp-in.l.google.com.",
|
|
13
|
-
"
|
|
11
|
+
"30 alt3.gmail-smtp-in.l.google.com.",
|
|
12
|
+
"40 alt4.gmail-smtp-in.l.google.com.",
|
|
13
|
+
"5 gmail-smtp-in.l.google.com."
|
|
14
14
|
]
|
|
15
15
|
},
|
|
16
16
|
{
|
|
17
17
|
"query": {
|
|
18
|
-
"name": "
|
|
18
|
+
"name": "pages.github.com",
|
|
19
19
|
"type": "MX",
|
|
20
20
|
"class": "IN"
|
|
21
21
|
},
|
|
22
22
|
"answer": []
|
|
23
23
|
},
|
|
24
|
+
{
|
|
25
|
+
"query": {
|
|
26
|
+
"name": "pages.github.com",
|
|
27
|
+
"type": "A",
|
|
28
|
+
"class": "IN"
|
|
29
|
+
},
|
|
30
|
+
"answer": [
|
|
31
|
+
"185.199.108.153",
|
|
32
|
+
"185.199.109.153",
|
|
33
|
+
"185.199.110.153",
|
|
34
|
+
"185.199.111.153"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"query": {
|
|
39
|
+
"name": "pages.github.com",
|
|
40
|
+
"type": "TXT",
|
|
41
|
+
"class": "IN"
|
|
42
|
+
},
|
|
43
|
+
"answer": []
|
|
44
|
+
},
|
|
24
45
|
{
|
|
25
46
|
"query": {
|
|
26
47
|
"name": "xkxufoekjvjfjeodlfmdfjcu.com",
|
|
@@ -39,6 +60,32 @@
|
|
|
39
60
|
"0 ."
|
|
40
61
|
]
|
|
41
62
|
},
|
|
63
|
+
{
|
|
64
|
+
"query": {
|
|
65
|
+
"name": "g.mail.com",
|
|
66
|
+
"type": "MX",
|
|
67
|
+
"class": "IN"
|
|
68
|
+
},
|
|
69
|
+
"answer": []
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"query": {
|
|
73
|
+
"name": "g.mail.com",
|
|
74
|
+
"type": "A",
|
|
75
|
+
"class": "IN"
|
|
76
|
+
},
|
|
77
|
+
"answer": []
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"query": {
|
|
81
|
+
"name": "g.mail.com",
|
|
82
|
+
"type": "AAAA",
|
|
83
|
+
"class": "IN"
|
|
84
|
+
},
|
|
85
|
+
"answer": [
|
|
86
|
+
"::1"
|
|
87
|
+
]
|
|
88
|
+
},
|
|
42
89
|
{
|
|
43
90
|
"query": {
|
|
44
91
|
"name": "nellis.af.mil",
|
|
@@ -64,13 +111,13 @@
|
|
|
64
111
|
"class": "IN"
|
|
65
112
|
},
|
|
66
113
|
"answer": [
|
|
67
|
-
"\"
|
|
68
|
-
"\"
|
|
114
|
+
"\"MS=ms47108184\"",
|
|
115
|
+
"\"v=spf1 -all\""
|
|
69
116
|
]
|
|
70
117
|
},
|
|
71
118
|
{
|
|
72
119
|
"query": {
|
|
73
|
-
"name": "
|
|
120
|
+
"name": "justtxt.joshdata.me",
|
|
74
121
|
"type": "MX",
|
|
75
122
|
"class": "IN"
|
|
76
123
|
},
|
|
@@ -78,16 +125,24 @@
|
|
|
78
125
|
},
|
|
79
126
|
{
|
|
80
127
|
"query": {
|
|
81
|
-
"name": "
|
|
82
|
-
"type": "
|
|
128
|
+
"name": "justtxt.joshdata.me",
|
|
129
|
+
"type": "A",
|
|
83
130
|
"class": "IN"
|
|
84
131
|
},
|
|
85
132
|
"answer": []
|
|
86
133
|
},
|
|
87
134
|
{
|
|
88
135
|
"query": {
|
|
89
|
-
"name": "
|
|
90
|
-
"type": "
|
|
136
|
+
"name": "justtxt.joshdata.me",
|
|
137
|
+
"type": "AAAA",
|
|
138
|
+
"class": "IN"
|
|
139
|
+
},
|
|
140
|
+
"answer": []
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"query": {
|
|
144
|
+
"name": "mail.example",
|
|
145
|
+
"type": "ANY",
|
|
91
146
|
"class": "IN"
|
|
92
147
|
},
|
|
93
148
|
"answer": []
|
|
@@ -32,7 +32,8 @@ class MockedDnsResponseData:
|
|
|
32
32
|
|
|
33
33
|
# Return a new dns.resolver.Resolver configured for caching
|
|
34
34
|
# using the singleton instance.
|
|
35
|
-
|
|
35
|
+
dns_resolver = dns.resolver.Resolver(configure=BUILD_MOCKED_DNS_RESPONSE_DATA)
|
|
36
|
+
return caching_resolver(cache=MockedDnsResponseData.INSTANCE, dns_resolver=dns_resolver)
|
|
36
37
|
|
|
37
38
|
def __init__(self):
|
|
38
39
|
self.data = {}
|
|
@@ -72,10 +73,10 @@ class MockedDnsResponseData:
|
|
|
72
73
|
"type": dns.rdatatype.to_text(key[1]),
|
|
73
74
|
"class": dns.rdataclass.to_text(key[2]),
|
|
74
75
|
},
|
|
75
|
-
"answer": [
|
|
76
|
+
"answer": sorted([
|
|
76
77
|
rr.to_text()
|
|
77
78
|
for rr in value
|
|
78
|
-
]
|
|
79
|
+
])
|
|
79
80
|
}
|
|
80
81
|
for key, value in self.data.items()
|
|
81
82
|
]
|
|
@@ -10,30 +10,33 @@ from mocked_dns_response import MockedDnsResponseData, MockedDnsResponseDataClea
|
|
|
10
10
|
RESOLVER = MockedDnsResponseData.create_resolver()
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def test_deliverability_fails():
|
|
24
|
-
# No MX record.
|
|
25
|
-
domain = 'xkxufoekjvjfjeodlfmdfjcu.com'
|
|
26
|
-
with pytest.raises(EmailUndeliverableError, match=f'The domain name {domain} does not exist'):
|
|
27
|
-
validate_email_deliverability(domain, domain, dns_resolver=RESOLVER)
|
|
13
|
+
@pytest.mark.parametrize(
|
|
14
|
+
'domain,expected_response',
|
|
15
|
+
[
|
|
16
|
+
('gmail.com', {'mx': [(5, 'gmail-smtp-in.l.google.com'), (10, 'alt1.gmail-smtp-in.l.google.com'), (20, 'alt2.gmail-smtp-in.l.google.com'), (30, 'alt3.gmail-smtp-in.l.google.com'), (40, 'alt4.gmail-smtp-in.l.google.com')], 'mx_fallback_type': None}),
|
|
17
|
+
('pages.github.com', {'mx': [(0, 'pages.github.com')], 'mx_fallback_type': 'A'}),
|
|
18
|
+
],
|
|
19
|
+
)
|
|
20
|
+
def test_deliverability_found(domain, expected_response):
|
|
21
|
+
response = validate_email_deliverability(domain, domain, dns_resolver=RESOLVER)
|
|
22
|
+
assert response == expected_response
|
|
28
23
|
|
|
29
|
-
# Null MX record.
|
|
30
|
-
domain = 'example.com'
|
|
31
|
-
with pytest.raises(EmailUndeliverableError, match=f'The domain name {domain} does not accept email'):
|
|
32
|
-
validate_email_deliverability(domain, domain, dns_resolver=RESOLVER)
|
|
33
24
|
|
|
34
|
-
|
|
35
|
-
domain
|
|
36
|
-
|
|
25
|
+
@pytest.mark.parametrize(
|
|
26
|
+
'domain,error',
|
|
27
|
+
[
|
|
28
|
+
('xkxufoekjvjfjeodlfmdfjcu.com', 'The domain name {domain} does not exist'),
|
|
29
|
+
('example.com', 'The domain name {domain} does not accept email'), # Null MX record
|
|
30
|
+
('g.mail.com', 'The domain name {domain} does not accept email'), # No MX record but invalid AAAA record fallback (issue #134)
|
|
31
|
+
('nellis.af.mil', 'The domain name {domain} does not send email'), # No MX record, A record fallback, reject-all SPF record.
|
|
32
|
+
|
|
33
|
+
# No MX or A/AAAA records, but some other DNS records must
|
|
34
|
+
# exist such that the response is NOANSWER instead of NXDOMAIN.
|
|
35
|
+
('justtxt.joshdata.me', 'The domain name {domain} does not accept email'),
|
|
36
|
+
],
|
|
37
|
+
)
|
|
38
|
+
def test_deliverability_fails(domain, error):
|
|
39
|
+
with pytest.raises(EmailUndeliverableError, match=error.format(domain=domain)):
|
|
37
40
|
validate_email_deliverability(domain, domain, dns_resolver=RESOLVER)
|
|
38
41
|
|
|
39
42
|
|
|
@@ -68,15 +68,41 @@ from email_validator import EmailSyntaxError, \
|
|
|
68
68
|
ascii_email='jeff@xn--fiqq24b10vi0d.tw',
|
|
69
69
|
),
|
|
70
70
|
),
|
|
71
|
+
(
|
|
72
|
+
'"quoted local part"@example.org',
|
|
73
|
+
ValidatedEmail(
|
|
74
|
+
local_part='"quoted local part"',
|
|
75
|
+
ascii_local_part='"quoted local part"',
|
|
76
|
+
smtputf8=False,
|
|
77
|
+
ascii_domain='example.org',
|
|
78
|
+
domain='example.org',
|
|
79
|
+
normalized='"quoted local part"@example.org',
|
|
80
|
+
ascii_email='"quoted local part"@example.org'
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
(
|
|
84
|
+
'"de-quoted.local.part"@example.org',
|
|
85
|
+
ValidatedEmail(
|
|
86
|
+
local_part='de-quoted.local.part',
|
|
87
|
+
ascii_local_part='de-quoted.local.part',
|
|
88
|
+
smtputf8=False,
|
|
89
|
+
ascii_domain='example.org',
|
|
90
|
+
domain='example.org',
|
|
91
|
+
normalized='de-quoted.local.part@example.org',
|
|
92
|
+
ascii_email='de-quoted.local.part@example.org'
|
|
93
|
+
),
|
|
94
|
+
),
|
|
71
95
|
],
|
|
72
96
|
)
|
|
73
97
|
def test_email_valid(email_input, output):
|
|
74
98
|
# These addresses do not require SMTPUTF8. See test_email_valid_intl_local_part
|
|
75
99
|
# for addresses that are valid but require SMTPUTF8. Check that it passes with
|
|
76
100
|
# allow_smtput8 both on and off.
|
|
77
|
-
emailinfo = validate_email(email_input, check_deliverability=False, allow_smtputf8=False
|
|
101
|
+
emailinfo = validate_email(email_input, check_deliverability=False, allow_smtputf8=False,
|
|
102
|
+
allow_quoted_local=True)
|
|
78
103
|
assert emailinfo == output
|
|
79
|
-
assert validate_email(email_input, check_deliverability=False, allow_smtputf8=True
|
|
104
|
+
assert validate_email(email_input, check_deliverability=False, allow_smtputf8=True,
|
|
105
|
+
allow_quoted_local=True) == output
|
|
80
106
|
|
|
81
107
|
# Check that the old `email` attribute to access the normalized form still works
|
|
82
108
|
# if the DeprecationWarning is suppressed.
|
|
@@ -317,9 +343,9 @@ def test_domain_literal():
|
|
|
317
343
|
('obsolete."quoted".atom@example.com', 'The email address contains invalid characters before the @-sign: \'"\'.'),
|
|
318
344
|
('11111111112222222222333333333344444444445555555555666666666677777@example.com', 'The email address is too long before the @-sign (1 character too many).'),
|
|
319
345
|
('111111111122222222223333333333444444444455555555556666666666777777@example.com', 'The email address is too long before the @-sign (2 characters too many).'),
|
|
320
|
-
('
|
|
321
|
-
('me@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.
|
|
322
|
-
('me@中1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.
|
|
346
|
+
('meme@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.com', 'The email address is too long (4 characters too many).'),
|
|
347
|
+
('me@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.11111111112222222222333333333344444444445555555555.com', 'The email address is too long after the @-sign (1 character too many).'),
|
|
348
|
+
('me@中1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444.com', 'The email address is too long after the @-sign.'),
|
|
323
349
|
('my.long.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.11111111112222222222333333333344444.info', 'The email address is too long (2 characters too many).'),
|
|
324
350
|
('my.long.address@λ111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.11111111112222222222333333.info', 'The email address is too long (when converted to IDNA ASCII).'),
|
|
325
351
|
('my.long.address@λ111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444.info', 'The email address is too long (at least 1 character too many).'),
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
Creative Commons Legal Code
|
|
2
|
-
|
|
3
|
-
CC0 1.0 Universal
|
|
4
|
-
|
|
5
|
-
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
|
6
|
-
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
|
7
|
-
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
|
8
|
-
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
|
9
|
-
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
|
10
|
-
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
|
11
|
-
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
|
12
|
-
HEREUNDER.
|
|
13
|
-
|
|
14
|
-
Statement of Purpose
|
|
15
|
-
|
|
16
|
-
The laws of most jurisdictions throughout the world automatically confer
|
|
17
|
-
exclusive Copyright and Related Rights (defined below) upon the creator
|
|
18
|
-
and subsequent owner(s) (each and all, an "owner") of an original work of
|
|
19
|
-
authorship and/or a database (each, a "Work").
|
|
20
|
-
|
|
21
|
-
Certain owners wish to permanently relinquish those rights to a Work for
|
|
22
|
-
the purpose of contributing to a commons of creative, cultural and
|
|
23
|
-
scientific works ("Commons") that the public can reliably and without fear
|
|
24
|
-
of later claims of infringement build upon, modify, incorporate in other
|
|
25
|
-
works, reuse and redistribute as freely as possible in any form whatsoever
|
|
26
|
-
and for any purposes, including without limitation commercial purposes.
|
|
27
|
-
These owners may contribute to the Commons to promote the ideal of a free
|
|
28
|
-
culture and the further production of creative, cultural and scientific
|
|
29
|
-
works, or to gain reputation or greater distribution for their Work in
|
|
30
|
-
part through the use and efforts of others.
|
|
31
|
-
|
|
32
|
-
For these and/or other purposes and motivations, and without any
|
|
33
|
-
expectation of additional consideration or compensation, the person
|
|
34
|
-
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
|
35
|
-
is an owner of Copyright and Related Rights in the Work, voluntarily
|
|
36
|
-
elects to apply CC0 to the Work and publicly distribute the Work under its
|
|
37
|
-
terms, with knowledge of his or her Copyright and Related Rights in the
|
|
38
|
-
Work and the meaning and intended legal effect of CC0 on those rights.
|
|
39
|
-
|
|
40
|
-
1. Copyright and Related Rights. A Work made available under CC0 may be
|
|
41
|
-
protected by copyright and related or neighboring rights ("Copyright and
|
|
42
|
-
Related Rights"). Copyright and Related Rights include, but are not
|
|
43
|
-
limited to, the following:
|
|
44
|
-
|
|
45
|
-
i. the right to reproduce, adapt, distribute, perform, display,
|
|
46
|
-
communicate, and translate a Work;
|
|
47
|
-
ii. moral rights retained by the original author(s) and/or performer(s);
|
|
48
|
-
iii. publicity and privacy rights pertaining to a person's image or
|
|
49
|
-
likeness depicted in a Work;
|
|
50
|
-
iv. rights protecting against unfair competition in regards to a Work,
|
|
51
|
-
subject to the limitations in paragraph 4(a), below;
|
|
52
|
-
v. rights protecting the extraction, dissemination, use and reuse of data
|
|
53
|
-
in a Work;
|
|
54
|
-
vi. database rights (such as those arising under Directive 96/9/EC of the
|
|
55
|
-
European Parliament and of the Council of 11 March 1996 on the legal
|
|
56
|
-
protection of databases, and under any national implementation
|
|
57
|
-
thereof, including any amended or successor version of such
|
|
58
|
-
directive); and
|
|
59
|
-
vii. other similar, equivalent or corresponding rights throughout the
|
|
60
|
-
world based on applicable law or treaty, and any national
|
|
61
|
-
implementations thereof.
|
|
62
|
-
|
|
63
|
-
2. Waiver. To the greatest extent permitted by, but not in contravention
|
|
64
|
-
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
|
65
|
-
irrevocably and unconditionally waives, abandons, and surrenders all of
|
|
66
|
-
Affirmer's Copyright and Related Rights and associated claims and causes
|
|
67
|
-
of action, whether now known or unknown (including existing as well as
|
|
68
|
-
future claims and causes of action), in the Work (i) in all territories
|
|
69
|
-
worldwide, (ii) for the maximum duration provided by applicable law or
|
|
70
|
-
treaty (including future time extensions), (iii) in any current or future
|
|
71
|
-
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
|
72
|
-
including without limitation commercial, advertising or promotional
|
|
73
|
-
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
|
74
|
-
member of the public at large and to the detriment of Affirmer's heirs and
|
|
75
|
-
successors, fully intending that such Waiver shall not be subject to
|
|
76
|
-
revocation, rescission, cancellation, termination, or any other legal or
|
|
77
|
-
equitable action to disrupt the quiet enjoyment of the Work by the public
|
|
78
|
-
as contemplated by Affirmer's express Statement of Purpose.
|
|
79
|
-
|
|
80
|
-
3. Public License Fallback. Should any part of the Waiver for any reason
|
|
81
|
-
be judged legally invalid or ineffective under applicable law, then the
|
|
82
|
-
Waiver shall be preserved to the maximum extent permitted taking into
|
|
83
|
-
account Affirmer's express Statement of Purpose. In addition, to the
|
|
84
|
-
extent the Waiver is so judged Affirmer hereby grants to each affected
|
|
85
|
-
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
|
86
|
-
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
|
87
|
-
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
|
88
|
-
maximum duration provided by applicable law or treaty (including future
|
|
89
|
-
time extensions), (iii) in any current or future medium and for any number
|
|
90
|
-
of copies, and (iv) for any purpose whatsoever, including without
|
|
91
|
-
limitation commercial, advertising or promotional purposes (the
|
|
92
|
-
"License"). The License shall be deemed effective as of the date CC0 was
|
|
93
|
-
applied by Affirmer to the Work. Should any part of the License for any
|
|
94
|
-
reason be judged legally invalid or ineffective under applicable law, such
|
|
95
|
-
partial invalidity or ineffectiveness shall not invalidate the remainder
|
|
96
|
-
of the License, and in such case Affirmer hereby affirms that he or she
|
|
97
|
-
will not (i) exercise any of his or her remaining Copyright and Related
|
|
98
|
-
Rights in the Work or (ii) assert any associated claims and causes of
|
|
99
|
-
action with respect to the Work, in either case contrary to Affirmer's
|
|
100
|
-
express Statement of Purpose.
|
|
101
|
-
|
|
102
|
-
4. Limitations and Disclaimers.
|
|
103
|
-
|
|
104
|
-
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
|
105
|
-
surrendered, licensed or otherwise affected by this document.
|
|
106
|
-
b. Affirmer offers the Work as-is and makes no representations or
|
|
107
|
-
warranties of any kind concerning the Work, express, implied,
|
|
108
|
-
statutory or otherwise, including without limitation warranties of
|
|
109
|
-
title, merchantability, fitness for a particular purpose, non
|
|
110
|
-
infringement, or the absence of latent or other defects, accuracy, or
|
|
111
|
-
the present or absence of errors, whether or not discoverable, all to
|
|
112
|
-
the greatest extent permissible under applicable law.
|
|
113
|
-
c. Affirmer disclaims responsibility for clearing rights of other persons
|
|
114
|
-
that may apply to the Work or any use thereof, including without
|
|
115
|
-
limitation any person's Copyright and Related Rights in the Work.
|
|
116
|
-
Further, Affirmer disclaims responsibility for obtaining any necessary
|
|
117
|
-
consents, permissions or other rights required for any use of the
|
|
118
|
-
Work.
|
|
119
|
-
d. Affirmer understands and acknowledges that Creative Commons is not a
|
|
120
|
-
party to this document and has no duty or obligation with respect to
|
|
121
|
-
this CC0 or use of the Work.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.1.0.post1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{email_validator-2.1.0.post1 → email_validator-2.1.2}/email_validator.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{email_validator-2.1.0.post1 → email_validator-2.1.2}/email_validator.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
{email_validator-2.1.0.post1 → email_validator-2.1.2}/email_validator.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|