email-validator 2.1.1__tar.gz → 2.2.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 (30) hide show
  1. {email_validator-2.1.1/email_validator.egg-info → email_validator-2.2.0}/PKG-INFO +44 -65
  2. {email_validator-2.1.1 → email_validator-2.2.0}/README.md +44 -65
  3. {email_validator-2.1.1 → email_validator-2.2.0}/email_validator/__init__.py +9 -4
  4. {email_validator-2.1.1 → email_validator-2.2.0}/email_validator/__main__.py +4 -3
  5. {email_validator-2.1.1 → email_validator-2.2.0}/email_validator/deliverability.py +42 -10
  6. {email_validator-2.1.1 → email_validator-2.2.0}/email_validator/exceptions_types.py +16 -16
  7. {email_validator-2.1.1 → email_validator-2.2.0}/email_validator/rfc_constants.py +3 -4
  8. {email_validator-2.1.1 → email_validator-2.2.0}/email_validator/syntax.py +306 -108
  9. {email_validator-2.1.1 → email_validator-2.2.0}/email_validator/validate_email.py +52 -18
  10. email_validator-2.2.0/email_validator/version.py +1 -0
  11. {email_validator-2.1.1 → email_validator-2.2.0/email_validator.egg-info}/PKG-INFO +44 -65
  12. email_validator-2.2.0/pyproject.toml +17 -0
  13. {email_validator-2.1.1 → email_validator-2.2.0}/tests/mocked-dns-answers.json +55 -0
  14. {email_validator-2.1.1 → email_validator-2.2.0}/tests/mocked_dns_response.py +29 -23
  15. email_validator-2.2.0/tests/test_deliverability.py +86 -0
  16. {email_validator-2.1.1 → email_validator-2.2.0}/tests/test_main.py +6 -6
  17. {email_validator-2.1.1 → email_validator-2.2.0}/tests/test_syntax.py +125 -42
  18. email_validator-2.1.1/email_validator/version.py +0 -1
  19. email_validator-2.1.1/pyproject.toml +0 -4
  20. email_validator-2.1.1/tests/test_deliverability.py +0 -87
  21. {email_validator-2.1.1 → email_validator-2.2.0}/LICENSE +0 -0
  22. {email_validator-2.1.1 → email_validator-2.2.0}/MANIFEST.in +0 -0
  23. {email_validator-2.1.1 → email_validator-2.2.0}/email_validator/py.typed +0 -0
  24. {email_validator-2.1.1 → email_validator-2.2.0}/email_validator.egg-info/SOURCES.txt +0 -0
  25. {email_validator-2.1.1 → email_validator-2.2.0}/email_validator.egg-info/dependency_links.txt +0 -0
  26. {email_validator-2.1.1 → email_validator-2.2.0}/email_validator.egg-info/entry_points.txt +0 -0
  27. {email_validator-2.1.1 → email_validator-2.2.0}/email_validator.egg-info/requires.txt +0 -0
  28. {email_validator-2.1.1 → email_validator-2.2.0}/email_validator.egg-info/top_level.txt +0 -0
  29. {email_validator-2.1.1 → email_validator-2.2.0}/setup.cfg +0 -0
  30. {email_validator-2.1.1 → email_validator-2.2.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: email_validator
3
- Version: 2.1.1
3
+ Version: 2.2.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
@@ -32,8 +32,7 @@ Python 3.8+ by [Joshua Tauberer](https://joshdata.me).
32
32
  This library validates that a string is of the form `name@example.com`
33
33
  and optionally checks that the domain name is set up to receive email.
34
34
  This is the sort of validation you would want when you are identifying
35
- users by their email address like on a registration/login form (but not
36
- necessarily for composing an email message, see below).
35
+ users by their email address like on a registration form.
37
36
 
38
37
  Key features:
39
38
 
@@ -43,19 +42,21 @@ Key features:
43
42
  can display to end-users.
44
43
  * Checks deliverability (optional): Does the domain name resolve?
45
44
  (You can override the default DNS resolver to add query caching.)
46
- * Supports internationalized domain names and internationalized local parts.
47
- * Rejects addresses with unsafe Unicode characters, obsolete email address
48
- syntax that you'd find unexpected, special use domain names like
49
- `@localhost`, and domains without a dot by default. This is an
50
- opinionated library!
45
+ * Supports internationalized domain names (like `@ツ.life`),
46
+ internationalized local parts (like `ツ@example.com`),
47
+ and optionally parses display names (e.g. `"My Name" <me@example.com>`).
48
+ * Rejects addresses with invalid or unsafe Unicode characters,
49
+ obsolete email address syntax that you'd find unexpected,
50
+ special use domain names like `@localhost`,
51
+ and domains without a dot by default.
52
+ This is an opinionated library!
51
53
  * Normalizes email addresses (important for internationalized
52
54
  and quoted-string addresses! see below).
53
55
  * Python type annotations are used.
54
56
 
55
57
  This is an opinionated library. You should definitely also consider using
56
- the less-opinionated [pyIsEmail](https://github.com/michaelherold/pyIsEmail) and
57
- [flanker](https://github.com/mailgun/flanker) if they are better for your
58
- use case.
58
+ the less-opinionated [pyIsEmail](https://github.com/michaelherold/pyIsEmail)
59
+ if it works better for you.
59
60
 
60
61
  [![Build Status](https://github.com/JoshData/python-email-validator/actions/workflows/test_and_build.yaml/badge.svg)](https://github.com/JoshData/python-email-validator/actions/workflows/test_and_build.yaml)
61
62
 
@@ -169,6 +170,8 @@ The `validate_email` function also accepts the following keyword arguments
169
170
 
170
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.
171
172
 
173
+ `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
+
172
175
  `allow_empty_local=False`: Set to `True` to allow an empty local part (i.e.
173
176
  `@example.com`), e.g. for validating Postfix aliases.
174
177
 
@@ -207,8 +210,12 @@ Internationalized email addresses
207
210
  The email protocol SMTP and the domain name system DNS have historically
208
211
  only allowed English (ASCII) characters in email addresses and domain names,
209
212
  respectively. Each has adapted to internationalization in a separate
210
- way, creating two separate aspects to email address
211
- internationalization.
213
+ way, creating two separate aspects to email address internationalization.
214
+
215
+ (If your mail submission library doesn't support Unicode at all, then
216
+ immediately prior to mail submission you must replace the email address with
217
+ its ASCII-ized form. This library gives you back the ASCII-ized form in the
218
+ `ascii_email` field in the returned object.)
212
219
 
213
220
  ### Internationalized domain names (IDN)
214
221
 
@@ -231,6 +238,19 @@ email addresses, only English letters, numbers, and some punctuation
231
238
  (`._!#$%&'^``*+-=~/?{|}`) are allowed. In internationalized email address
232
239
  local parts, a wider range of Unicode characters are allowed.
233
240
 
241
+ Email addresses with these non-ASCII characters require that your mail
242
+ submission library and all the mail servers along the route to the destination,
243
+ including your own outbound mail server, all support the
244
+ [SMTPUTF8 (RFC 6531)](https://tools.ietf.org/html/rfc6531) extension.
245
+ Support for SMTPUTF8 varies. If you know ahead of time that SMTPUTF8 is not
246
+ supported by your mail submission stack, then you must filter out addresses that
247
+ require SMTPUTF8 using the `allow_smtputf8=False` keyword argument (see above).
248
+ This will cause the validation function to raise a `EmailSyntaxError` if
249
+ delivery would require SMTPUTF8. If you do not set `allow_smtputf8=False`,
250
+ you can also check the value of the `smtputf8` field in the returned object.
251
+
252
+ ### Unsafe Unicode characters are rejected
253
+
234
254
  A surprisingly large number of Unicode characters are not safe to display,
235
255
  especially when the email address is concatenated with other text, so this
236
256
  library tries to protect you by not permitting reserved, non-, private use,
@@ -241,48 +261,10 @@ cannot combine with something outside of the email address string or with
241
261
  the @-sign). See https://qntm.org/safe and https://trojansource.codes/
242
262
  for relevant prior work. (Other than whitespace, these are checks that
243
263
  you should be applying to nearly all user inputs in a security-sensitive
244
- context.)
245
-
246
- These character checks are performed after Unicode normalization (see below),
247
- so you are only fully protected if you replace all user-provided email addresses
248
- with the normalized email address string returned by this library. This does not
249
- guard against the well known problem that many Unicode characters look alike
250
- (or are identical), which can be used to fool humans reading displayed text.
251
-
252
- Email addresses with these non-ASCII characters require that your mail
253
- submission library and the mail servers along the route to the destination,
254
- including your own outbound mail server, all support the
255
- [SMTPUTF8 (RFC 6531)](https://tools.ietf.org/html/rfc6531) extension.
256
- Support for SMTPUTF8 varies. See the `allow_smtputf8` parameter.
257
-
258
- ### If you know ahead of time that SMTPUTF8 is not supported by your mail submission stack
264
+ context.) This does not guard against the well known problem that many
265
+ Unicode characters look alike, which can be used to fool humans reading
266
+ displayed text.
259
267
 
260
- By default all internationalized forms are accepted by the validator.
261
- But if you know ahead of time that SMTPUTF8 is not supported by your
262
- mail submission stack, then you must filter out addresses that require
263
- SMTPUTF8 using the `allow_smtputf8=False` keyword argument (see above).
264
- This will cause the validation function to raise a `EmailSyntaxError` if
265
- delivery would require SMTPUTF8. That's just in those cases where
266
- non-ASCII characters appear before the @-sign. If you do not set
267
- `allow_smtputf8=False`, you can also check the value of the `smtputf8`
268
- field in the returned object.
269
-
270
- If your mail submission library doesn't support Unicode at all --- even
271
- in the domain part of the address --- then immediately prior to mail
272
- submission you must replace the email address with its ASCII-ized form.
273
- This library gives you back the ASCII-ized form in the `ascii_email`
274
- field in the returned object, which you can get like this:
275
-
276
- ```python
277
- emailinfo = validate_email(email, allow_smtputf8=False)
278
- email = emailinfo.ascii_email
279
- ```
280
-
281
- The local part is left alone (if it has internationalized characters
282
- `allow_smtputf8=False` will force validation to fail) and the domain
283
- part is converted to [IDNA ASCII](https://tools.ietf.org/html/rfc5891).
284
- (You probably should not do this at account creation time so you don't
285
- change the user's login information without telling them.)
286
268
 
287
269
  Normalization
288
270
  -------------
@@ -297,7 +279,7 @@ address.
297
279
 
298
280
  For example, the CJK fullwidth Latin letters are considered semantically
299
281
  equivalent in domain names to their ASCII counterparts. This library
300
- normalizes them to their ASCII counterparts:
282
+ normalizes them to their ASCII counterparts (as required by IDNA):
301
283
 
302
284
  ```python
303
285
  emailinfo = validate_email("me@Domain.com")
@@ -310,9 +292,7 @@ Because an end-user might type their email address in different (but
310
292
  equivalent) un-normalized forms at different times, you ought to
311
293
  replace what they enter with the normalized form immediately prior to
312
294
  going into your database (during account creation), querying your database
313
- (during login), or sending outbound mail. Normalization may also change
314
- the length of an email address, and this may affect whether it is valid
315
- and acceptable by your SMTP provider.
295
+ (during login), or sending outbound mail.
316
296
 
317
297
  The normalizations include lowercasing the domain part of the email
318
298
  address (domain names are case-insensitive), [Unicode "NFC"
@@ -326,6 +306,11 @@ in the domain part, possibly other
326
306
  [UTS46](http://unicode.org/reports/tr46) mappings on the domain part,
327
307
  and conversion from Punycode to Unicode characters.
328
308
 
309
+ Normalization may change the characters in the email address and the
310
+ length of the email address, such that a string might be a valid address
311
+ before normalization but invalid after, or vice versa. This library only
312
+ permits addresses that are valid both before and after normalization.
313
+
329
314
  (See [RFC 6532 (internationalized email) section
330
315
  3.1](https://tools.ietf.org/html/rfc6532#section-3.1) and [RFC 5895
331
316
  (IDNA 2008) section 2](http://www.ietf.org/rfc/rfc5895.txt).)
@@ -340,13 +325,6 @@ they are unnecessary. For IPv6 domain literals, the IPv6 address is
340
325
  normalized to condensed form. [RFC 2142](https://datatracker.ietf.org/doc/html/rfc2142)
341
326
  also requires lowercase normalization for some specific mailbox names like `postmaster@`.
342
327
 
343
- ### Length checks
344
-
345
- This library checks that the length of the email address is not longer than
346
- the maximum length. The check is performed on the normalized form of the
347
- address, which might be different from a string provided by a user. If you
348
- send email to the original string and not the normalized address, the email
349
- might be rejected because the original address could be too long.
350
328
 
351
329
  Examples
352
330
  --------
@@ -420,6 +398,7 @@ are:
420
398
  | `domain` | The canonical internationalized Unicode form of the domain part of the email address. If the returned string contains non-ASCII characters, either the [SMTPUTF8](https://tools.ietf.org/html/rfc6531) feature of your mail relay will be required to transmit the message or else the email address's domain part must be converted to IDNA ASCII first: Use `ascii_domain` field instead. |
421
399
  | `ascii_domain` | The [IDNA](https://tools.ietf.org/html/rfc5891) [Punycode](https://www.rfc-editor.org/rfc/rfc3492.txt)-encoded form of the domain part of the given email address, as it would be transmitted on the wire. |
422
400
  | `domain_address` | If domain literals are allowed and if the email address contains one, an `ipaddress.IPv4Address` or `ipaddress.IPv6Address` object. |
401
+ | `display_name` | If no display name was present and angle brackets do not surround the address, this will be `None`; otherwise, it will be set to the display name, or the empty string if there were angle brackets but no display name. If the display name was quoted, it will be unquoted and unescaped. |
423
402
  | `smtputf8` | A boolean indicating that the [SMTPUTF8](https://tools.ietf.org/html/rfc6531) feature of your mail relay will be required to transmit messages to this address because the local part of the address has non-ASCII characters (the local part cannot be IDNA-encoded). If `allow_smtputf8=False` is passed as an argument, this flag will always be false because an exception is raised if it would have been true. |
424
403
  | `mx` | A list of (priority, domain) tuples of MX records specified in the DNS for the domain (see [RFC 5321 section 5](https://tools.ietf.org/html/rfc5321#section-5)). May be `None` if the deliverability check could not be completed because of a temporary issue like a timeout. |
425
404
  | `mx_fallback_type` | `None` if an `MX` record is found. If no MX records are actually specified in DNS and instead are inferred, through an obsolete mechanism, from A or AAAA records, the value is the type of DNS record used instead (`A` or `AAAA`). May be `None` if the deliverability check could not be completed because of a temporary issue like a timeout. |
@@ -7,8 +7,7 @@ Python 3.8+ by [Joshua Tauberer](https://joshdata.me).
7
7
  This library validates that a string is of the form `name@example.com`
8
8
  and optionally checks that the domain name is set up to receive email.
9
9
  This is the sort of validation you would want when you are identifying
10
- users by their email address like on a registration/login form (but not
11
- necessarily for composing an email message, see below).
10
+ users by their email address like on a registration form.
12
11
 
13
12
  Key features:
14
13
 
@@ -18,19 +17,21 @@ Key features:
18
17
  can display to end-users.
19
18
  * Checks deliverability (optional): Does the domain name resolve?
20
19
  (You can override the default DNS resolver to add query caching.)
21
- * Supports internationalized domain names and internationalized local parts.
22
- * Rejects addresses with unsafe Unicode characters, obsolete email address
23
- syntax that you'd find unexpected, special use domain names like
24
- `@localhost`, and domains without a dot by default. This is an
25
- opinionated library!
20
+ * Supports internationalized domain names (like `@ツ.life`),
21
+ internationalized local parts (like `ツ@example.com`),
22
+ and optionally parses display names (e.g. `"My Name" <me@example.com>`).
23
+ * Rejects addresses with invalid or unsafe Unicode characters,
24
+ obsolete email address syntax that you'd find unexpected,
25
+ special use domain names like `@localhost`,
26
+ and domains without a dot by default.
27
+ This is an opinionated library!
26
28
  * Normalizes email addresses (important for internationalized
27
29
  and quoted-string addresses! see below).
28
30
  * Python type annotations are used.
29
31
 
30
32
  This is an opinionated library. You should definitely also consider using
31
- the less-opinionated [pyIsEmail](https://github.com/michaelherold/pyIsEmail) and
32
- [flanker](https://github.com/mailgun/flanker) if they are better for your
33
- use case.
33
+ the less-opinionated [pyIsEmail](https://github.com/michaelherold/pyIsEmail)
34
+ if it works better for you.
34
35
 
35
36
  [![Build Status](https://github.com/JoshData/python-email-validator/actions/workflows/test_and_build.yaml/badge.svg)](https://github.com/JoshData/python-email-validator/actions/workflows/test_and_build.yaml)
36
37
 
@@ -144,6 +145,8 @@ The `validate_email` function also accepts the following keyword arguments
144
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
147
 
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
+
147
150
  `allow_empty_local=False`: Set to `True` to allow an empty local part (i.e.
148
151
  `@example.com`), e.g. for validating Postfix aliases.
149
152
 
@@ -182,8 +185,12 @@ Internationalized email addresses
182
185
  The email protocol SMTP and the domain name system DNS have historically
183
186
  only allowed English (ASCII) characters in email addresses and domain names,
184
187
  respectively. Each has adapted to internationalization in a separate
185
- way, creating two separate aspects to email address
186
- internationalization.
188
+ way, creating two separate aspects to email address internationalization.
189
+
190
+ (If your mail submission library doesn't support Unicode at all, then
191
+ immediately prior to mail submission you must replace the email address with
192
+ its ASCII-ized form. This library gives you back the ASCII-ized form in the
193
+ `ascii_email` field in the returned object.)
187
194
 
188
195
  ### Internationalized domain names (IDN)
189
196
 
@@ -206,6 +213,19 @@ email addresses, only English letters, numbers, and some punctuation
206
213
  (`._!#$%&'^``*+-=~/?{|}`) are allowed. In internationalized email address
207
214
  local parts, a wider range of Unicode characters are allowed.
208
215
 
216
+ Email addresses with these non-ASCII characters require that your mail
217
+ submission library and all the mail servers along the route to the destination,
218
+ including your own outbound mail server, all support the
219
+ [SMTPUTF8 (RFC 6531)](https://tools.ietf.org/html/rfc6531) extension.
220
+ Support for SMTPUTF8 varies. If you know ahead of time that SMTPUTF8 is not
221
+ supported by your mail submission stack, then you must filter out addresses that
222
+ require SMTPUTF8 using the `allow_smtputf8=False` keyword argument (see above).
223
+ This will cause the validation function to raise a `EmailSyntaxError` if
224
+ delivery would require SMTPUTF8. If you do not set `allow_smtputf8=False`,
225
+ you can also check the value of the `smtputf8` field in the returned object.
226
+
227
+ ### Unsafe Unicode characters are rejected
228
+
209
229
  A surprisingly large number of Unicode characters are not safe to display,
210
230
  especially when the email address is concatenated with other text, so this
211
231
  library tries to protect you by not permitting reserved, non-, private use,
@@ -216,48 +236,10 @@ cannot combine with something outside of the email address string or with
216
236
  the @-sign). See https://qntm.org/safe and https://trojansource.codes/
217
237
  for relevant prior work. (Other than whitespace, these are checks that
218
238
  you should be applying to nearly all user inputs in a security-sensitive
219
- context.)
220
-
221
- These character checks are performed after Unicode normalization (see below),
222
- so you are only fully protected if you replace all user-provided email addresses
223
- with the normalized email address string returned by this library. This does not
224
- guard against the well known problem that many Unicode characters look alike
225
- (or are identical), which can be used to fool humans reading displayed text.
226
-
227
- Email addresses with these non-ASCII characters require that your mail
228
- submission library and the mail servers along the route to the destination,
229
- including your own outbound mail server, all support the
230
- [SMTPUTF8 (RFC 6531)](https://tools.ietf.org/html/rfc6531) extension.
231
- Support for SMTPUTF8 varies. See the `allow_smtputf8` parameter.
232
-
233
- ### If you know ahead of time that SMTPUTF8 is not supported by your mail submission stack
239
+ context.) This does not guard against the well known problem that many
240
+ Unicode characters look alike, which can be used to fool humans reading
241
+ displayed text.
234
242
 
235
- By default all internationalized forms are accepted by the validator.
236
- But if you know ahead of time that SMTPUTF8 is not supported by your
237
- mail submission stack, then you must filter out addresses that require
238
- SMTPUTF8 using the `allow_smtputf8=False` keyword argument (see above).
239
- This will cause the validation function to raise a `EmailSyntaxError` if
240
- delivery would require SMTPUTF8. That's just in those cases where
241
- non-ASCII characters appear before the @-sign. If you do not set
242
- `allow_smtputf8=False`, you can also check the value of the `smtputf8`
243
- field in the returned object.
244
-
245
- If your mail submission library doesn't support Unicode at all --- even
246
- in the domain part of the address --- then immediately prior to mail
247
- submission you must replace the email address with its ASCII-ized form.
248
- This library gives you back the ASCII-ized form in the `ascii_email`
249
- field in the returned object, which you can get like this:
250
-
251
- ```python
252
- emailinfo = validate_email(email, allow_smtputf8=False)
253
- email = emailinfo.ascii_email
254
- ```
255
-
256
- The local part is left alone (if it has internationalized characters
257
- `allow_smtputf8=False` will force validation to fail) and the domain
258
- part is converted to [IDNA ASCII](https://tools.ietf.org/html/rfc5891).
259
- (You probably should not do this at account creation time so you don't
260
- change the user's login information without telling them.)
261
243
 
262
244
  Normalization
263
245
  -------------
@@ -272,7 +254,7 @@ address.
272
254
 
273
255
  For example, the CJK fullwidth Latin letters are considered semantically
274
256
  equivalent in domain names to their ASCII counterparts. This library
275
- normalizes them to their ASCII counterparts:
257
+ normalizes them to their ASCII counterparts (as required by IDNA):
276
258
 
277
259
  ```python
278
260
  emailinfo = validate_email("me@Domain.com")
@@ -285,9 +267,7 @@ Because an end-user might type their email address in different (but
285
267
  equivalent) un-normalized forms at different times, you ought to
286
268
  replace what they enter with the normalized form immediately prior to
287
269
  going into your database (during account creation), querying your database
288
- (during login), or sending outbound mail. Normalization may also change
289
- the length of an email address, and this may affect whether it is valid
290
- and acceptable by your SMTP provider.
270
+ (during login), or sending outbound mail.
291
271
 
292
272
  The normalizations include lowercasing the domain part of the email
293
273
  address (domain names are case-insensitive), [Unicode "NFC"
@@ -301,6 +281,11 @@ in the domain part, possibly other
301
281
  [UTS46](http://unicode.org/reports/tr46) mappings on the domain part,
302
282
  and conversion from Punycode to Unicode characters.
303
283
 
284
+ Normalization may change the characters in the email address and the
285
+ length of the email address, such that a string might be a valid address
286
+ before normalization but invalid after, or vice versa. This library only
287
+ permits addresses that are valid both before and after normalization.
288
+
304
289
  (See [RFC 6532 (internationalized email) section
305
290
  3.1](https://tools.ietf.org/html/rfc6532#section-3.1) and [RFC 5895
306
291
  (IDNA 2008) section 2](http://www.ietf.org/rfc/rfc5895.txt).)
@@ -315,13 +300,6 @@ they are unnecessary. For IPv6 domain literals, the IPv6 address is
315
300
  normalized to condensed form. [RFC 2142](https://datatracker.ietf.org/doc/html/rfc2142)
316
301
  also requires lowercase normalization for some specific mailbox names like `postmaster@`.
317
302
 
318
- ### Length checks
319
-
320
- This library checks that the length of the email address is not longer than
321
- the maximum length. The check is performed on the normalized form of the
322
- address, which might be different from a string provided by a user. If you
323
- send email to the original string and not the normalized address, the email
324
- might be rejected because the original address could be too long.
325
303
 
326
304
  Examples
327
305
  --------
@@ -395,6 +373,7 @@ are:
395
373
  | `domain` | The canonical internationalized Unicode form of the domain part of the email address. If the returned string contains non-ASCII characters, either the [SMTPUTF8](https://tools.ietf.org/html/rfc6531) feature of your mail relay will be required to transmit the message or else the email address's domain part must be converted to IDNA ASCII first: Use `ascii_domain` field instead. |
396
374
  | `ascii_domain` | The [IDNA](https://tools.ietf.org/html/rfc5891) [Punycode](https://www.rfc-editor.org/rfc/rfc3492.txt)-encoded form of the domain part of the given email address, as it would be transmitted on the wire. |
397
375
  | `domain_address` | If domain literals are allowed and if the email address contains one, an `ipaddress.IPv4Address` or `ipaddress.IPv6Address` object. |
376
+ | `display_name` | If no display name was present and angle brackets do not surround the address, this will be `None`; otherwise, it will be set to the display name, or the empty string if there were angle brackets but no display name. If the display name was quoted, it will be unquoted and unescaped. |
398
377
  | `smtputf8` | A boolean indicating that the [SMTPUTF8](https://tools.ietf.org/html/rfc6531) feature of your mail relay will be required to transmit messages to this address because the local part of the address has non-ASCII characters (the local part cannot be IDNA-encoded). If `allow_smtputf8=False` is passed as an argument, this flag will always be false because an exception is raised if it would have been true. |
399
378
  | `mx` | A list of (priority, domain) tuples of MX records specified in the DNS for the domain (see [RFC 5321 section 5](https://tools.ietf.org/html/rfc5321#section-5)). May be `None` if the deliverability check could not be completed because of a temporary issue like a timeout. |
400
379
  | `mx_fallback_type` | `None` if an `MX` record is found. If no MX records are actually specified in DNS and instead are inferred, through an obsolete mechanism, from A or AAAA records, the value is the type of DNS record used instead (`A` or `AAAA`). May be `None` if the deliverability check could not be completed because of a temporary issue like a timeout. |
@@ -458,4 +437,4 @@ git push --tags
458
437
  License
459
438
  -------
460
439
 
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).
440
+ 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,3 +1,5 @@
1
+ from typing import TYPE_CHECKING
2
+
1
3
  # Export the main method, helper methods, and the public data types.
2
4
  from .exceptions_types import ValidatedEmail, EmailNotValidError, \
3
5
  EmailSyntaxError, EmailUndeliverableError
@@ -9,12 +11,14 @@ __all__ = ["validate_email",
9
11
  "EmailSyntaxError", "EmailUndeliverableError",
10
12
  "caching_resolver", "__version__"]
11
13
 
12
-
13
- def caching_resolver(*args, **kwargs):
14
- # Lazy load `deliverability` as it is slow to import (due to dns.resolver)
14
+ if TYPE_CHECKING:
15
15
  from .deliverability import caching_resolver
16
+ else:
17
+ def caching_resolver(*args, **kwargs):
18
+ # Lazy load `deliverability` as it is slow to import (due to dns.resolver)
19
+ from .deliverability import caching_resolver
16
20
 
17
- return caching_resolver(*args, **kwargs)
21
+ return caching_resolver(*args, **kwargs)
18
22
 
19
23
 
20
24
  # These global attributes are a part of the library's API and can be
@@ -25,6 +29,7 @@ def caching_resolver(*args, **kwargs):
25
29
  ALLOW_SMTPUTF8 = True
26
30
  ALLOW_QUOTED_LOCAL = False
27
31
  ALLOW_DOMAIN_LITERAL = False
32
+ ALLOW_DISPLAY_NAME = False
28
33
  GLOBALLY_DELIVERABLE = True
29
34
  CHECK_DELIVERABILITY = True
30
35
  TEST_ENVIRONMENT = False
@@ -17,17 +17,18 @@
17
17
  import json
18
18
  import os
19
19
  import sys
20
+ from typing import Any, Dict, Optional
20
21
 
21
- from .validate_email import validate_email
22
+ from .validate_email import validate_email, _Resolver
22
23
  from .deliverability import caching_resolver
23
24
  from .exceptions_types import EmailNotValidError
24
25
 
25
26
 
26
- def main(dns_resolver=None):
27
+ def main(dns_resolver: Optional[_Resolver] = None) -> None:
27
28
  # The dns_resolver argument is for tests.
28
29
 
29
30
  # Set options from environment variables.
30
- options = {}
31
+ options: Dict[str, Any] = {}
31
32
  for varname in ('ALLOW_SMTPUTF8', 'ALLOW_QUOTED_LOCAL', 'ALLOW_DOMAIN_LITERAL',
32
33
  'GLOBALLY_DELIVERABLE', 'CHECK_DELIVERABILITY', 'TEST_ENVIRONMENT'):
33
34
  if varname in os.environ:
@@ -1,4 +1,6 @@
1
- from typing import Optional, Any, Dict
1
+ from typing import Any, List, Optional, Tuple, TypedDict
2
+
3
+ import ipaddress
2
4
 
3
5
  from .exceptions_types import EmailUndeliverableError
4
6
 
@@ -6,17 +8,24 @@ import dns.resolver
6
8
  import dns.exception
7
9
 
8
10
 
9
- def caching_resolver(*, timeout: Optional[int] = None, cache=None, dns_resolver=None):
11
+ def caching_resolver(*, timeout: Optional[int] = None, cache: Any = None, dns_resolver: Optional[dns.resolver.Resolver] = None) -> dns.resolver.Resolver:
10
12
  if timeout is None:
11
13
  from . import DEFAULT_TIMEOUT
12
14
  timeout = DEFAULT_TIMEOUT
13
15
  resolver = dns_resolver or dns.resolver.Resolver()
14
- resolver.cache = cache or dns.resolver.LRUCache() # type: ignore
15
- resolver.lifetime = timeout # type: ignore # timeout, in seconds
16
+ resolver.cache = cache or dns.resolver.LRUCache()
17
+ resolver.lifetime = timeout # timeout, in seconds
16
18
  return resolver
17
19
 
18
20
 
19
- def validate_email_deliverability(domain: str, domain_i18n: str, timeout: Optional[int] = None, dns_resolver=None):
21
+ DeliverabilityInfo = TypedDict("DeliverabilityInfo", {
22
+ "mx": List[Tuple[int, str]],
23
+ "mx_fallback_type": Optional[str],
24
+ "unknown-deliverability": str,
25
+ }, total=False)
26
+
27
+
28
+ def validate_email_deliverability(domain: str, domain_i18n: str, timeout: Optional[int] = None, dns_resolver: Optional[dns.resolver.Resolver] = None) -> DeliverabilityInfo:
20
29
  # Check that the domain resolves to an MX record. If there is no MX record,
21
30
  # try an A or AAAA record which is a deprecated fallback for deliverability.
22
31
  # Raises an EmailUndeliverableError on failure. On success, returns a dict
@@ -34,7 +43,7 @@ def validate_email_deliverability(domain: str, domain_i18n: str, timeout: Option
34
43
  elif timeout is not None:
35
44
  raise ValueError("It's not valid to pass both timeout and dns_resolver.")
36
45
 
37
- deliverability_info: Dict[str, Any] = {}
46
+ deliverability_info: DeliverabilityInfo = {}
38
47
 
39
48
  try:
40
49
  try:
@@ -57,10 +66,30 @@ def validate_email_deliverability(domain: str, domain_i18n: str, timeout: Option
57
66
  deliverability_info["mx_fallback_type"] = None
58
67
 
59
68
  except dns.resolver.NoAnswer:
60
- # If there was no MX record, fall back to an A record. (RFC 5321 Section 5)
69
+ # If there was no MX record, fall back to an A or AAA record
70
+ # (RFC 5321 Section 5). Check A first since it's more common.
71
+
72
+ # If the A/AAAA response has no Globally Reachable IP address,
73
+ # treat the response as if it were NoAnswer, i.e., the following
74
+ # address types are not allowed fallbacks: Private-Use, Loopback,
75
+ # Link-Local, and some other obscure ranges. See
76
+ # https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
77
+ # https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
78
+ # (Issue #134.)
79
+ def is_global_addr(address: Any) -> bool:
80
+ try:
81
+ ipaddr = ipaddress.ip_address(address)
82
+ except ValueError:
83
+ return False
84
+ return ipaddr.is_global
85
+
61
86
  try:
62
87
  response = dns_resolver.resolve(domain, "A")
63
- deliverability_info["mx"] = [(0, str(r)) for r in response]
88
+
89
+ if not any(is_global_addr(r.address) for r in response):
90
+ raise dns.resolver.NoAnswer # fall back to AAAA
91
+
92
+ deliverability_info["mx"] = [(0, domain)]
64
93
  deliverability_info["mx_fallback_type"] = "A"
65
94
 
66
95
  except dns.resolver.NoAnswer:
@@ -69,7 +98,11 @@ def validate_email_deliverability(domain: str, domain_i18n: str, timeout: Option
69
98
  # (It's unclear if SMTP servers actually do this.)
70
99
  try:
71
100
  response = dns_resolver.resolve(domain, "AAAA")
72
- deliverability_info["mx"] = [(0, str(r)) for r in response]
101
+
102
+ if not any(is_global_addr(r.address) for r in response):
103
+ raise dns.resolver.NoAnswer
104
+
105
+ deliverability_info["mx"] = [(0, domain)]
73
106
  deliverability_info["mx_fallback_type"] = "AAAA"
74
107
 
75
108
  except dns.resolver.NoAnswer as e:
@@ -89,7 +122,6 @@ def validate_email_deliverability(domain: str, domain_i18n: str, timeout: Option
89
122
  for rec in response:
90
123
  value = b"".join(rec.strings)
91
124
  if value.startswith(b"v=spf1 "):
92
- deliverability_info["spf"] = value.decode("ascii", errors='replace')
93
125
  if value == b"v=spf1 -all":
94
126
  raise EmailUndeliverableError(f"The domain name {domain_i18n} does not send email.")
95
127
  except dns.resolver.NoAnswer:
@@ -1,5 +1,5 @@
1
1
  import warnings
2
- from typing import Optional
2
+ from typing import Any, Dict, List, Optional, Tuple, Union
3
3
 
4
4
 
5
5
  class EmailNotValidError(ValueError):
@@ -24,7 +24,7 @@ class ValidatedEmail:
24
24
  """The email address that was passed to validate_email. (If passed as bytes, this will be a string.)"""
25
25
  original: str
26
26
 
27
- """The normalized email address, which should always be used in preferance to the original address.
27
+ """The normalized email address, which should always be used in preference to the original address.
28
28
  The normalized address converts an IDNA ASCII domain name to Unicode, if possible, and performs
29
29
  Unicode normalization on the local part and on the domain (if originally Unicode). It is the
30
30
  concatenation of the local_part and domain attributes, separated by an @-sign."""
@@ -56,22 +56,20 @@ class ValidatedEmail:
56
56
 
57
57
  """If a deliverability check is performed and if it succeeds, a list of (priority, domain)
58
58
  tuples of MX records specified in the DNS for the domain."""
59
- mx: list
59
+ mx: List[Tuple[int, str]]
60
60
 
61
61
  """If no MX records are actually specified in DNS and instead are inferred, through an obsolete
62
62
  mechanism, from A or AAAA records, the value is the type of DNS record used instead (`A` or `AAAA`)."""
63
- mx_fallback_type: str
63
+ mx_fallback_type: Optional[str]
64
64
 
65
- """Tests use this constructor."""
66
- def __init__(self, **kwargs):
67
- for k, v in kwargs.items():
68
- setattr(self, k, v)
65
+ """The display name in the original input text, unquoted and unescaped, or None."""
66
+ display_name: Optional[str]
69
67
 
70
- def __repr__(self):
68
+ def __repr__(self) -> str:
71
69
  return f"<ValidatedEmail {self.normalized}>"
72
70
 
73
71
  """For backwards compatibility, support old field names."""
74
- def __getattr__(self, key):
72
+ def __getattr__(self, key: str) -> str:
75
73
  if key == "original_email":
76
74
  return self.original
77
75
  if key == "email":
@@ -79,13 +77,13 @@ class ValidatedEmail:
79
77
  raise AttributeError(key)
80
78
 
81
79
  @property
82
- def email(self):
80
+ def email(self) -> str:
83
81
  warnings.warn("ValidatedEmail.email is deprecated and will be removed, use ValidatedEmail.normalized instead", DeprecationWarning)
84
82
  return self.normalized
85
83
 
86
84
  """For backwards compatibility, some fields are also exposed through a dict-like interface. Note
87
85
  that some of the names changed when they became attributes."""
88
- def __getitem__(self, key):
86
+ def __getitem__(self, key: str) -> Union[Optional[str], bool, List[Tuple[int, str]]]:
89
87
  warnings.warn("dict-like access to the return value of validate_email is deprecated and may not be supported in the future.", DeprecationWarning, stacklevel=2)
90
88
  if key == "email":
91
89
  return self.normalized
@@ -106,7 +104,7 @@ class ValidatedEmail:
106
104
  raise KeyError()
107
105
 
108
106
  """Tests use this."""
109
- def __eq__(self, other):
107
+ def __eq__(self, other: object) -> bool:
110
108
  if not isinstance(other, ValidatedEmail):
111
109
  return False
112
110
  return (
@@ -120,21 +118,23 @@ class ValidatedEmail:
120
118
  and repr(sorted(self.mx) if getattr(self, 'mx', None) else None)
121
119
  == repr(sorted(other.mx) if getattr(other, 'mx', None) else None)
122
120
  and getattr(self, 'mx_fallback_type', None) == getattr(other, 'mx_fallback_type', None)
121
+ and getattr(self, 'display_name', None) == getattr(other, 'display_name', None)
123
122
  )
124
123
 
125
124
  """This helps producing the README."""
126
- def as_constructor(self):
125
+ def as_constructor(self) -> str:
127
126
  return "ValidatedEmail(" \
128
127
  + ",".join(f"\n {key}={repr(getattr(self, key))}"
129
128
  for key in ('normalized', 'local_part', 'domain',
130
129
  'ascii_email', 'ascii_local_part', 'ascii_domain',
131
- 'smtputf8', 'mx', 'mx_fallback_type')
130
+ 'smtputf8', 'mx', 'mx_fallback_type',
131
+ 'display_name')
132
132
  if hasattr(self, key)
133
133
  ) \
134
134
  + ")"
135
135
 
136
136
  """Convenience method for accessing ValidatedEmail as a dict"""
137
- def as_dict(self):
137
+ def as_dict(self) -> Dict[str, Any]:
138
138
  d = self.__dict__
139
139
  if d.get('domain_address'):
140
140
  d['domain_address'] = repr(d['domain_address'])