email-validator


Nameemail-validator JSON
Version 2.1.1 PyPI version JSON
download
home_pagehttps://github.com/JoshData/python-email-validator
SummaryA robust email address syntax and deliverability validation library.
upload_time2024-02-26 22:09:59
maintainer
docs_urlNone
authorJoshua Tauberer
requires_python>=3.8
licenseUnlicense
keywords email address validator
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            email-validator: Validate Email Addresses
=========================================

A robust email address syntax and deliverability validation library for
Python 3.8+ by [Joshua Tauberer](https://joshdata.me).

This library validates that a string is of the form `name@example.com`
and optionally checks that the domain name is set up to receive email.
This is the sort of validation you would want when you are identifying
users by their email address like on a registration/login form (but not
necessarily for composing an email message, see below).

Key features:

* Checks that an email address has the correct syntax --- great for
  email-based registration/login forms or validing data.
* Gives friendly English error messages when validation fails that you
  can display to end-users.
* Checks deliverability (optional): Does the domain name resolve?
  (You can override the default DNS resolver to add query caching.)
* Supports internationalized domain names and internationalized local parts.
* Rejects addresses with unsafe Unicode characters, obsolete email address
  syntax that you'd find unexpected, special use domain names like
  `@localhost`, and domains without a dot by default. This is an
  opinionated library!
* Normalizes email addresses (important for internationalized
  and quoted-string addresses! see below).
* Python type annotations are used.

This is an opinionated library. You should definitely also consider using
the less-opinionated [pyIsEmail](https://github.com/michaelherold/pyIsEmail) and
[flanker](https://github.com/mailgun/flanker) if they are better for your
use case.

[![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)

View the [CHANGELOG / Release Notes](CHANGELOG.md) for the version history of changes in the library. Occasionally this README is ahead of the latest published package --- see the CHANGELOG for details.

---

Installation
------------

This package [is on PyPI](https://pypi.org/project/email-validator/), so:

```sh
pip install email-validator
```

(You might need to use `pip3` depending on your local environment.)

Quick Start
-----------

If you're validating a user's email address before creating a user
account in your application, you might do this:

```python
from email_validator import validate_email, EmailNotValidError

email = "my+address@example.org"

try:

  # Check that the email address is valid. Turn on check_deliverability
  # for first-time validations like on account creation pages (but not
  # login pages).
  emailinfo = validate_email(email, check_deliverability=False)

  # After this point, use only the normalized form of the email address,
  # especially before going to a database query.
  email = emailinfo.normalized

except EmailNotValidError as e:

  # The exception message is human-readable explanation of why it's
  # not a valid (or deliverable) email address.
  print(str(e))
```

This validates the address and gives you its normalized form. You should
**put the normalized form in your database** and always normalize before
checking if an address is in your database. When using this in a login form,
set `check_deliverability` to `False` to avoid unnecessary DNS queries.

Usage
-----

### Overview

The module provides a function `validate_email(email_address)` which
takes an email address and:

- Raises a `EmailNotValidError` with a helpful, human-readable error
  message explaining why the email address is not valid, or
- Returns an object with a normalized form of the email address (which
  you should use!) and other information about it.

When an email address is not valid, `validate_email` raises either an
`EmailSyntaxError` if the form of the address is invalid or an
`EmailUndeliverableError` if the domain name fails DNS checks. Both
exception classes are subclasses of `EmailNotValidError`, which in turn
is a subclass of `ValueError`.

But when an email address is valid, an object is returned containing
a normalized form of the email address (which you should use!) and
other information.

The validator doesn't, by default, permit obsoleted forms of email addresses
that no one uses anymore even though they are still valid and deliverable, since
they will probably give you grief if you're using email for login. (See
later in the document about how to allow some obsolete forms.)

The validator optionally checks that the domain name in the email address has
a DNS MX record indicating that it can receive email. (Except a Null MX record.
If there is no MX record, a fallback A/AAAA-record is permitted, unless
a reject-all SPF record is present.) DNS is slow and sometimes unavailable or
unreliable, so consider whether these checks are useful for your use case and
turn them off if they aren't.
There is nothing to be gained by trying to actually contact an SMTP server, so
that's not done here. For privacy, security, and practicality reasons, servers
are good at not giving away whether an address is
deliverable or not: email addresses that appear to accept mail at first
can bounce mail after a delay, and bounced mail may indicate a temporary
failure of a good email address (sometimes an intentional failure, like
greylisting).

### Options

The `validate_email` function also accepts the following keyword arguments
(defaults are as shown below):

`check_deliverability=True`: If true, DNS queries are made to check that the domain name in the email address (the part after the @-sign) can receive mail, as described above. Set to `False` to skip this DNS-based check. It is recommended to pass `False` when performing validation for login pages (but not account creation pages) since re-validation of a previously validated domain in your database by querying DNS at every login is probably undesirable. You can also set `email_validator.CHECK_DELIVERABILITY` to `False` to turn this off for all calls by default.

`dns_resolver=None`: Pass an instance of [dns.resolver.Resolver](https://dnspython.readthedocs.io/en/latest/resolver-class.html) to control the DNS resolver including setting a timeout and [a cache](https://dnspython.readthedocs.io/en/latest/resolver-caching.html). The `caching_resolver` function shown below is a helper function to construct a dns.resolver.Resolver with a [LRUCache](https://dnspython.readthedocs.io/en/latest/resolver-caching.html#dns.resolver.LRUCache). Reuse the same resolver instance across calls to `validate_email` to make use of the cache.

`test_environment=False`: If `True`, DNS-based deliverability checks are disabled and  `test` and `**.test` domain names are permitted (see below). You can also set `email_validator.TEST_ENVIRONMENT` to `True` to turn it on for all calls by default.

`allow_smtputf8=True`: Set to `False` to prohibit internationalized addresses that would
    require the
    [SMTPUTF8](https://tools.ietf.org/html/rfc6531) extension. You can also set `email_validator.ALLOW_SMTPUTF8` to `False` to turn it off for all calls by default.

`allow_quoted_local=False`: Set to `True` to allow obscure and potentially problematic email addresses in which the part of the address before the @-sign contains spaces, @-signs, or other surprising characters when the local part is surrounded in quotes (so-called quoted-string local parts). In the object returned by `validate_email`, the normalized local part removes any unnecessary backslash-escaping and even removes the surrounding quotes if the address would be valid without them. You can also set `email_validator.ALLOW_QUOTED_LOCAL` to `True` to turn this on for all calls by default.

`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.

`allow_empty_local=False`: Set to `True` to allow an empty local part (i.e.
    `@example.com`), e.g. for validating Postfix aliases.
    

### DNS timeout and cache

When validating many email addresses or to control the timeout (the default is 15 seconds), create a caching [dns.resolver.Resolver](https://dnspython.readthedocs.io/en/latest/resolver-class.html) to reuse in each call. The `caching_resolver` function returns one easily for you:

```python
from email_validator import validate_email, caching_resolver

resolver = caching_resolver(timeout=10)

while True:
  validate_email(email, dns_resolver=resolver)
```

### Test addresses

This library rejects email addresses that use the [Special Use Domain Names](https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtml) `invalid`, `localhost`, `test`, and some others by raising `EmailSyntaxError`. This is to protect your system from abuse: You probably don't want a user to be able to cause an email to be sent to `localhost` (although they might be able to still do so via a malicious MX record). However, in your non-production test environments you may want to use `@test` or `@myname.test` email addresses. There are three ways you can allow this:

1. Add `test_environment=True` to the call to `validate_email` (see above).
2. Set `email_validator.TEST_ENVIRONMENT` to `True` globally.
3. Remove the special-use domain name that you want to use from `email_validator.SPECIAL_USE_DOMAIN_NAMES`, e.g.:

```python
import email_validator
email_validator.SPECIAL_USE_DOMAIN_NAMES.remove("test")
```

It is tempting to use `@example.com/net/org` in tests. They are *not* in this library's `SPECIAL_USE_DOMAIN_NAMES` list so you can, but shouldn't, use them. These domains are reserved to IANA for use in documentation so there is no risk of accidentally emailing someone at those domains. But beware that this library will nevertheless reject these domain names if DNS-based deliverability checks are not disabled because these domains do not resolve to domains that accept email. In tests, consider using your own domain name or `@test` or `@myname.test` instead.

Internationalized email addresses
---------------------------------

The email protocol SMTP and the domain name system DNS have historically
only allowed English (ASCII) characters in email addresses and domain names,
respectively. Each has adapted to internationalization in a separate
way, creating two separate aspects to email address
internationalization.

### Internationalized domain names (IDN)

The first is [internationalized domain names (RFC
5891)](https://tools.ietf.org/html/rfc5891), a.k.a IDNA 2008. The DNS
system has not been updated with Unicode support. Instead, internationalized
domain names are converted into a special IDNA ASCII "[Punycode](https://www.rfc-editor.org/rfc/rfc3492.txt)"
form starting with `xn--`. When an email address has non-ASCII
characters in its domain part, the domain part is replaced with its IDNA
ASCII equivalent form in the process of mail transmission. Your mail
submission library probably does this for you transparently. ([Compliance
around the web is not very good though](http://archives.miloush.net/michkap/archive/2012/02/27/10273315.html).) This library conforms to IDNA 2008
using the [idna](https://github.com/kjd/idna) module by Kim Davies.

### Internationalized local parts

The second sort of internationalization is internationalization in the
*local* part of the address (before the @-sign). In non-internationalized
email addresses, only English letters, numbers, and some punctuation
(`._!#$%&'^``*+-=~/?{|}`) are allowed. In internationalized email address
local parts, a wider range of Unicode characters are allowed.

A surprisingly large number of Unicode characters are not safe to display,
especially when the email address is concatenated with other text, so this
library tries to protect you by not permitting reserved, non-, private use,
formatting (which can be used to alter the display order of characters),
whitespace, and control characters, and combining characters
as the first character of the local part and the domain name (so that they
cannot combine with something outside of the email address string or with
the @-sign). See https://qntm.org/safe and https://trojansource.codes/
for relevant prior work. (Other than whitespace, these are checks that
you should be applying to nearly all user inputs in a security-sensitive
context.)

These character checks are performed after Unicode normalization (see below),
so you are only fully protected if you replace all user-provided email addresses
with the normalized email address string returned by this library. This does not
guard against the well known problem that many Unicode characters look alike
(or are identical), which can be used to fool humans reading displayed text.

Email addresses with these non-ASCII characters require that your mail
submission library and the mail servers along the route to the destination,
including your own outbound mail server, all support the
[SMTPUTF8 (RFC 6531)](https://tools.ietf.org/html/rfc6531) extension.
Support for SMTPUTF8 varies. See the `allow_smtputf8` parameter.

### If you know ahead of time that SMTPUTF8 is not supported by your mail submission stack

By default all internationalized forms are accepted by the validator.
But if you know ahead of time that SMTPUTF8 is not supported by your
mail submission stack, then you must filter out addresses that require
SMTPUTF8 using the `allow_smtputf8=False` keyword argument (see above).
This will cause the validation function to raise a `EmailSyntaxError` if
delivery would require SMTPUTF8. That's just in those cases where
non-ASCII characters appear before the @-sign. If you do not set
`allow_smtputf8=False`, you can also check the value of the `smtputf8`
field in the returned object.

If your mail submission library doesn't support Unicode at all --- even
in the domain part of the address --- then immediately prior to mail
submission you must replace the email address with its ASCII-ized form.
This library gives you back the ASCII-ized form in the `ascii_email`
field in the returned object, which you can get like this:

```python
emailinfo = validate_email(email, allow_smtputf8=False)
email = emailinfo.ascii_email
```

The local part is left alone (if it has internationalized characters
`allow_smtputf8=False` will force validation to fail) and the domain
part is converted to [IDNA ASCII](https://tools.ietf.org/html/rfc5891).
(You probably should not do this at account creation time so you don't
change the user's login information without telling them.)

Normalization
-------------

### Unicode Normalization

The use of Unicode in email addresses introduced a normalization
problem. Different Unicode strings can look identical and have the same
semantic meaning to the user. The `normalized` field returned on successful
validation provides the correctly normalized form of the given email
address.

For example, the CJK fullwidth Latin letters are considered semantically
equivalent in domain names to their ASCII counterparts. This library
normalizes them to their ASCII counterparts:

```python
emailinfo = validate_email("me@Domain.com")
print(emailinfo.normalized)
print(emailinfo.ascii_email)
# prints "me@domain.com" twice
```

Because an end-user might type their email address in different (but
equivalent) un-normalized forms at different times, you ought to
replace what they enter with the normalized form immediately prior to
going into your database (during account creation), querying your database
(during login), or sending outbound mail. Normalization may also change
the length of an email address, and this may affect whether it is valid
and acceptable by your SMTP provider.

The normalizations include lowercasing the domain part of the email
address (domain names are case-insensitive), [Unicode "NFC"
normalization](https://en.wikipedia.org/wiki/Unicode_equivalence) of the
whole address (which turns characters plus [combining
characters](https://en.wikipedia.org/wiki/Combining_character) into
precomposed characters where possible, replacement of [fullwidth and
halfwidth
characters](https://en.wikipedia.org/wiki/Halfwidth_and_fullwidth_forms)
in the domain part, possibly other
[UTS46](http://unicode.org/reports/tr46) mappings on the domain part,
and conversion from Punycode to Unicode characters.

(See [RFC 6532 (internationalized email) section
3.1](https://tools.ietf.org/html/rfc6532#section-3.1) and [RFC 5895
(IDNA 2008) section 2](http://www.ietf.org/rfc/rfc5895.txt).)

### Other Normalization

Normalization is also applied to quoted-string local parts and domain
literal IPv6 addresses if you have allowed them by the `allow_quoted_local`
and `allow_domain_literal` options. In quoted-string local parts, unnecessary
backslash escaping is removed and even the surrounding quotes are removed if
they are unnecessary. For IPv6 domain literals, the IPv6 address is
normalized to condensed form. [RFC 2142](https://datatracker.ietf.org/doc/html/rfc2142)
also requires lowercase normalization for some specific mailbox names like `postmaster@`.

### Length checks

This library checks that the length of the email address is not longer than
the maximum length. The check is performed on the normalized form of the
address, which might be different from a string provided by a user. If you
send email to the original string and not the normalized address, the email
might be rejected because the original address could be too long.

Examples
--------

For the email address `test@joshdata.me`, the returned object is:

```python
ValidatedEmail(
  normalized='test@joshdata.me',
  local_part='test',
  domain='joshdata.me',
  ascii_email='test@joshdata.me',
  ascii_local_part='test',
  ascii_domain='joshdata.me',
  smtputf8=False)
```

For the fictitious but valid address `example@ツ.ⓁⒾⒻⒺ`, which has an
internationalized domain but ASCII local part, the returned object is:

```python
ValidatedEmail(
  normalized='example@ツ.life',
  local_part='example',
  domain='ツ.life',
  ascii_email='example@xn--bdk.life',
  ascii_local_part='example',
  ascii_domain='xn--bdk.life',
  smtputf8=False)

```

Note that `normalized` and other fields provide a normalized form of the
email address, domain name, and (in other cases) local part (see earlier
discussion of normalization), which you should use in your database.

Calling `validate_email` with the ASCII form of the above email address,
`example@xn--bdk.life`, returns the exact same information (i.e., the
`normalized` field always will contain Unicode characters, not Punycode).

For the fictitious address `ツ-test@joshdata.me`, which has an
internationalized local part, the returned object is:

```python
ValidatedEmail(
  normalized='ツ-test@joshdata.me',
  local_part='ツ-test',
  domain='joshdata.me',
  ascii_email=None,
  ascii_local_part=None,
  ascii_domain='joshdata.me',
  smtputf8=True)
```

Now `smtputf8` is `True` and `ascii_email` is `None` because the local
part of the address is internationalized. The `local_part` and `normalized` fields
return the normalized form of the address.

Return value
------------

When an email address passes validation, the fields in the returned object
are:

| Field | Value |
| -----:|-------|
| `normalized` | The normalized form of the email address that you should put in your database. This combines the `local_part` and `domain` fields (see below). |
| `ascii_email` | If set, an ASCII-only form of the normalized email address by replacing the domain part with [IDNA](https://tools.ietf.org/html/rfc5891) [Punycode](https://www.rfc-editor.org/rfc/rfc3492.txt). This field will be present when an ASCII-only form of the email address exists (including if the email address is already ASCII). If the local part of the email address contains internationalized characters, `ascii_email` will be `None`. If set, it merely combines `ascii_local_part` and `ascii_domain`. |
| `local_part` | The normalized local part of the given email address (before the @-sign). Normalization includes Unicode NFC normalization and removing unnecessary quoted-string quotes and backslashes. If `allow_quoted_local` is True and the surrounding quotes are necessary, the quotes _will_ be present in this field. |
| `ascii_local_part` | If set, the local part, which is composed of ASCII characters only. |
| `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. |
| `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. |
| `domain_address` | If domain literals are allowed and if the email address contains one, an `ipaddress.IPv4Address` or `ipaddress.IPv6Address` object. |
| `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. |
| `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. |
| `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. |
| `spf` | Any SPF record found while checking deliverability. Only set if the SPF record is queried. |

Assumptions
-----------

By design, this validator does not pass all email addresses that
strictly conform to the standards. Many email address forms are obsolete
or likely to cause trouble:

* The validator assumes the email address is intended to be
  usable on the public Internet. The domain part
  of the email address must be a resolvable domain name
  (see the deliverability checks described above).
  Most [Special Use Domain Names](https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtml)
  and their subdomains, as well as
  domain names without a `.`, are rejected as a syntax error
  (except see the `test_environment` parameter above).
* Obsolete email syntaxes are rejected:
  The unusual ["(comment)" syntax](https://github.com/JoshData/python-email-validator/issues/77)
  is rejected. Extremely old obsolete syntaxes are
  rejected. Quoted-string local parts and domain-literal addresses
  are rejected by default, but there are options to allow them (see above).
  No one uses these forms anymore, and I can't think of any reason why anyone
  using this library would need to accept them.

Testing
-------

Tests can be run using

```sh
pip install -r test_requirements.txt 
make test
```

Tests run with mocked DNS responses. When adding or changing tests, temporarily turn on the `BUILD_MOCKED_DNS_RESPONSE_DATA` flag in `tests/mocked_dns_responses.py` to re-build the database of mocked responses from live queries.

For Project Maintainers
-----------------------

The package is distributed as a universal wheel and as a source package.

To release:

* Update CHANGELOG.md.
* Update the version number in `email_validator/version.py`.
* Make & push a commit with the new version number and make sure tests pass.
* Make & push a tag (see command below).
* Make a release at https://github.com/JoshData/python-email-validator/releases/new.
* Publish a source and wheel distribution to pypi (see command below).

```sh
git tag v$(cat email_validator/version.py  | sed "s/.* = //" | sed 's/"//g')
git push --tags
./release_to_pypi.sh
```

License
-------

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).

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/JoshData/python-email-validator",
    "name": "email-validator",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": "",
    "keywords": "email address validator",
    "author": "Joshua Tauberer",
    "author_email": "jt@occams.info",
    "download_url": "https://files.pythonhosted.org/packages/63/82/2914bff80ebee8c027802a664ad4b4caad502cd594e358f76aff395b5e56/email_validator-2.1.1.tar.gz",
    "platform": null,
    "description": "email-validator: Validate Email Addresses\n=========================================\n\nA robust email address syntax and deliverability validation library for\nPython 3.8+ by [Joshua Tauberer](https://joshdata.me).\n\nThis library validates that a string is of the form `name@example.com`\nand optionally checks that the domain name is set up to receive email.\nThis is the sort of validation you would want when you are identifying\nusers by their email address like on a registration/login form (but not\nnecessarily for composing an email message, see below).\n\nKey features:\n\n* Checks that an email address has the correct syntax --- great for\n  email-based registration/login forms or validing data.\n* Gives friendly English error messages when validation fails that you\n  can display to end-users.\n* Checks deliverability (optional): Does the domain name resolve?\n  (You can override the default DNS resolver to add query caching.)\n* Supports internationalized domain names and internationalized local parts.\n* Rejects addresses with unsafe Unicode characters, obsolete email address\n  syntax that you'd find unexpected, special use domain names like\n  `@localhost`, and domains without a dot by default. This is an\n  opinionated library!\n* Normalizes email addresses (important for internationalized\n  and quoted-string addresses! see below).\n* Python type annotations are used.\n\nThis is an opinionated library. You should definitely also consider using\nthe less-opinionated [pyIsEmail](https://github.com/michaelherold/pyIsEmail) and\n[flanker](https://github.com/mailgun/flanker) if they are better for your\nuse case.\n\n[![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)\n\nView the [CHANGELOG / Release Notes](CHANGELOG.md) for the version history of changes in the library. Occasionally this README is ahead of the latest published package --- see the CHANGELOG for details.\n\n---\n\nInstallation\n------------\n\nThis package [is on PyPI](https://pypi.org/project/email-validator/), so:\n\n```sh\npip install email-validator\n```\n\n(You might need to use `pip3` depending on your local environment.)\n\nQuick Start\n-----------\n\nIf you're validating a user's email address before creating a user\naccount in your application, you might do this:\n\n```python\nfrom email_validator import validate_email, EmailNotValidError\n\nemail = \"my+address@example.org\"\n\ntry:\n\n  # Check that the email address is valid. Turn on check_deliverability\n  # for first-time validations like on account creation pages (but not\n  # login pages).\n  emailinfo = validate_email(email, check_deliverability=False)\n\n  # After this point, use only the normalized form of the email address,\n  # especially before going to a database query.\n  email = emailinfo.normalized\n\nexcept EmailNotValidError as e:\n\n  # The exception message is human-readable explanation of why it's\n  # not a valid (or deliverable) email address.\n  print(str(e))\n```\n\nThis validates the address and gives you its normalized form. You should\n**put the normalized form in your database** and always normalize before\nchecking if an address is in your database. When using this in a login form,\nset `check_deliverability` to `False` to avoid unnecessary DNS queries.\n\nUsage\n-----\n\n### Overview\n\nThe module provides a function `validate_email(email_address)` which\ntakes an email address and:\n\n- Raises a `EmailNotValidError` with a helpful, human-readable error\n  message explaining why the email address is not valid, or\n- Returns an object with a normalized form of the email address (which\n  you should use!) and other information about it.\n\nWhen an email address is not valid, `validate_email` raises either an\n`EmailSyntaxError` if the form of the address is invalid or an\n`EmailUndeliverableError` if the domain name fails DNS checks. Both\nexception classes are subclasses of `EmailNotValidError`, which in turn\nis a subclass of `ValueError`.\n\nBut when an email address is valid, an object is returned containing\na normalized form of the email address (which you should use!) and\nother information.\n\nThe validator doesn't, by default, permit obsoleted forms of email addresses\nthat no one uses anymore even though they are still valid and deliverable, since\nthey will probably give you grief if you're using email for login. (See\nlater in the document about how to allow some obsolete forms.)\n\nThe validator optionally checks that the domain name in the email address has\na DNS MX record indicating that it can receive email. (Except a Null MX record.\nIf there is no MX record, a fallback A/AAAA-record is permitted, unless\na reject-all SPF record is present.) DNS is slow and sometimes unavailable or\nunreliable, so consider whether these checks are useful for your use case and\nturn them off if they aren't.\nThere is nothing to be gained by trying to actually contact an SMTP server, so\nthat's not done here. For privacy, security, and practicality reasons, servers\nare good at not giving away whether an address is\ndeliverable or not: email addresses that appear to accept mail at first\ncan bounce mail after a delay, and bounced mail may indicate a temporary\nfailure of a good email address (sometimes an intentional failure, like\ngreylisting).\n\n### Options\n\nThe `validate_email` function also accepts the following keyword arguments\n(defaults are as shown below):\n\n`check_deliverability=True`: If true, DNS queries are made to check that the domain name in the email address (the part after the @-sign) can receive mail, as described above. Set to `False` to skip this DNS-based check. It is recommended to pass `False` when performing validation for login pages (but not account creation pages) since re-validation of a previously validated domain in your database by querying DNS at every login is probably undesirable. You can also set `email_validator.CHECK_DELIVERABILITY` to `False` to turn this off for all calls by default.\n\n`dns_resolver=None`: Pass an instance of [dns.resolver.Resolver](https://dnspython.readthedocs.io/en/latest/resolver-class.html) to control the DNS resolver including setting a timeout and [a cache](https://dnspython.readthedocs.io/en/latest/resolver-caching.html). The `caching_resolver` function shown below is a helper function to construct a dns.resolver.Resolver with a [LRUCache](https://dnspython.readthedocs.io/en/latest/resolver-caching.html#dns.resolver.LRUCache). Reuse the same resolver instance across calls to `validate_email` to make use of the cache.\n\n`test_environment=False`: If `True`, DNS-based deliverability checks are disabled and  `test` and `**.test` domain names are permitted (see below). You can also set `email_validator.TEST_ENVIRONMENT` to `True` to turn it on for all calls by default.\n\n`allow_smtputf8=True`: Set to `False` to prohibit internationalized addresses that would\n    require the\n    [SMTPUTF8](https://tools.ietf.org/html/rfc6531) extension. You can also set `email_validator.ALLOW_SMTPUTF8` to `False` to turn it off for all calls by default.\n\n`allow_quoted_local=False`: Set to `True` to allow obscure and potentially problematic email addresses in which the part of the address before the @-sign contains spaces, @-signs, or other surprising characters when the local part is surrounded in quotes (so-called quoted-string local parts). In the object returned by `validate_email`, the normalized local part removes any unnecessary backslash-escaping and even removes the surrounding quotes if the address would be valid without them. You can also set `email_validator.ALLOW_QUOTED_LOCAL` to `True` to turn this on for all calls by default.\n\n`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.\n\n`allow_empty_local=False`: Set to `True` to allow an empty local part (i.e.\n    `@example.com`), e.g. for validating Postfix aliases.\n    \n\n### DNS timeout and cache\n\nWhen validating many email addresses or to control the timeout (the default is 15 seconds), create a caching [dns.resolver.Resolver](https://dnspython.readthedocs.io/en/latest/resolver-class.html) to reuse in each call. The `caching_resolver` function returns one easily for you:\n\n```python\nfrom email_validator import validate_email, caching_resolver\n\nresolver = caching_resolver(timeout=10)\n\nwhile True:\n  validate_email(email, dns_resolver=resolver)\n```\n\n### Test addresses\n\nThis library rejects email addresses that use the [Special Use Domain Names](https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtml) `invalid`, `localhost`, `test`, and some others by raising `EmailSyntaxError`. This is to protect your system from abuse: You probably don't want a user to be able to cause an email to be sent to `localhost` (although they might be able to still do so via a malicious MX record). However, in your non-production test environments you may want to use `@test` or `@myname.test` email addresses. There are three ways you can allow this:\n\n1. Add `test_environment=True` to the call to `validate_email` (see above).\n2. Set `email_validator.TEST_ENVIRONMENT` to `True` globally.\n3. Remove the special-use domain name that you want to use from `email_validator.SPECIAL_USE_DOMAIN_NAMES`, e.g.:\n\n```python\nimport email_validator\nemail_validator.SPECIAL_USE_DOMAIN_NAMES.remove(\"test\")\n```\n\nIt is tempting to use `@example.com/net/org` in tests. They are *not* in this library's `SPECIAL_USE_DOMAIN_NAMES` list so you can, but shouldn't, use them. These domains are reserved to IANA for use in documentation so there is no risk of accidentally emailing someone at those domains. But beware that this library will nevertheless reject these domain names if DNS-based deliverability checks are not disabled because these domains do not resolve to domains that accept email. In tests, consider using your own domain name or `@test` or `@myname.test` instead.\n\nInternationalized email addresses\n---------------------------------\n\nThe email protocol SMTP and the domain name system DNS have historically\nonly allowed English (ASCII) characters in email addresses and domain names,\nrespectively. Each has adapted to internationalization in a separate\nway, creating two separate aspects to email address\ninternationalization.\n\n### Internationalized domain names (IDN)\n\nThe first is [internationalized domain names (RFC\n5891)](https://tools.ietf.org/html/rfc5891), a.k.a IDNA 2008. The DNS\nsystem has not been updated with Unicode support. Instead, internationalized\ndomain names are converted into a special IDNA ASCII \"[Punycode](https://www.rfc-editor.org/rfc/rfc3492.txt)\"\nform starting with `xn--`. When an email address has non-ASCII\ncharacters in its domain part, the domain part is replaced with its IDNA\nASCII equivalent form in the process of mail transmission. Your mail\nsubmission library probably does this for you transparently. ([Compliance\naround the web is not very good though](http://archives.miloush.net/michkap/archive/2012/02/27/10273315.html).) This library conforms to IDNA 2008\nusing the [idna](https://github.com/kjd/idna) module by Kim Davies.\n\n### Internationalized local parts\n\nThe second sort of internationalization is internationalization in the\n*local* part of the address (before the @-sign). In non-internationalized\nemail addresses, only English letters, numbers, and some punctuation\n(`._!#$%&'^``*+-=~/?{|}`) are allowed. In internationalized email address\nlocal parts, a wider range of Unicode characters are allowed.\n\nA surprisingly large number of Unicode characters are not safe to display,\nespecially when the email address is concatenated with other text, so this\nlibrary tries to protect you by not permitting reserved, non-, private use,\nformatting (which can be used to alter the display order of characters),\nwhitespace, and control characters, and combining characters\nas the first character of the local part and the domain name (so that they\ncannot combine with something outside of the email address string or with\nthe @-sign). See https://qntm.org/safe and https://trojansource.codes/\nfor relevant prior work. (Other than whitespace, these are checks that\nyou should be applying to nearly all user inputs in a security-sensitive\ncontext.)\n\nThese character checks are performed after Unicode normalization (see below),\nso you are only fully protected if you replace all user-provided email addresses\nwith the normalized email address string returned by this library. This does not\nguard against the well known problem that many Unicode characters look alike\n(or are identical), which can be used to fool humans reading displayed text.\n\nEmail addresses with these non-ASCII characters require that your mail\nsubmission library and the mail servers along the route to the destination,\nincluding your own outbound mail server, all support the\n[SMTPUTF8 (RFC 6531)](https://tools.ietf.org/html/rfc6531) extension.\nSupport for SMTPUTF8 varies. See the `allow_smtputf8` parameter.\n\n### If you know ahead of time that SMTPUTF8 is not supported by your mail submission stack\n\nBy default all internationalized forms are accepted by the validator.\nBut if you know ahead of time that SMTPUTF8 is not supported by your\nmail submission stack, then you must filter out addresses that require\nSMTPUTF8 using the `allow_smtputf8=False` keyword argument (see above).\nThis will cause the validation function to raise a `EmailSyntaxError` if\ndelivery would require SMTPUTF8. That's just in those cases where\nnon-ASCII characters appear before the @-sign. If you do not set\n`allow_smtputf8=False`, you can also check the value of the `smtputf8`\nfield in the returned object.\n\nIf your mail submission library doesn't support Unicode at all --- even\nin the domain part of the address --- then immediately prior to mail\nsubmission you must replace the email address with its ASCII-ized form.\nThis library gives you back the ASCII-ized form in the `ascii_email`\nfield in the returned object, which you can get like this:\n\n```python\nemailinfo = validate_email(email, allow_smtputf8=False)\nemail = emailinfo.ascii_email\n```\n\nThe local part is left alone (if it has internationalized characters\n`allow_smtputf8=False` will force validation to fail) and the domain\npart is converted to [IDNA ASCII](https://tools.ietf.org/html/rfc5891).\n(You probably should not do this at account creation time so you don't\nchange the user's login information without telling them.)\n\nNormalization\n-------------\n\n### Unicode Normalization\n\nThe use of Unicode in email addresses introduced a normalization\nproblem. Different Unicode strings can look identical and have the same\nsemantic meaning to the user. The `normalized` field returned on successful\nvalidation provides the correctly normalized form of the given email\naddress.\n\nFor example, the CJK fullwidth Latin letters are considered semantically\nequivalent in domain names to their ASCII counterparts. This library\nnormalizes them to their ASCII counterparts:\n\n```python\nemailinfo = validate_email(\"me@\uff24\uff4f\uff4d\uff41\uff49\uff4e.com\")\nprint(emailinfo.normalized)\nprint(emailinfo.ascii_email)\n# prints \"me@domain.com\" twice\n```\n\nBecause an end-user might type their email address in different (but\nequivalent) un-normalized forms at different times, you ought to\nreplace what they enter with the normalized form immediately prior to\ngoing into your database (during account creation), querying your database\n(during login), or sending outbound mail. Normalization may also change\nthe length of an email address, and this may affect whether it is valid\nand acceptable by your SMTP provider.\n\nThe normalizations include lowercasing the domain part of the email\naddress (domain names are case-insensitive), [Unicode \"NFC\"\nnormalization](https://en.wikipedia.org/wiki/Unicode_equivalence) of the\nwhole address (which turns characters plus [combining\ncharacters](https://en.wikipedia.org/wiki/Combining_character) into\nprecomposed characters where possible, replacement of [fullwidth and\nhalfwidth\ncharacters](https://en.wikipedia.org/wiki/Halfwidth_and_fullwidth_forms)\nin the domain part, possibly other\n[UTS46](http://unicode.org/reports/tr46) mappings on the domain part,\nand conversion from Punycode to Unicode characters.\n\n(See [RFC 6532 (internationalized email) section\n3.1](https://tools.ietf.org/html/rfc6532#section-3.1) and [RFC 5895\n(IDNA 2008) section 2](http://www.ietf.org/rfc/rfc5895.txt).)\n\n### Other Normalization\n\nNormalization is also applied to quoted-string local parts and domain\nliteral IPv6 addresses if you have allowed them by the `allow_quoted_local`\nand `allow_domain_literal` options. In quoted-string local parts, unnecessary\nbackslash escaping is removed and even the surrounding quotes are removed if\nthey are unnecessary. For IPv6 domain literals, the IPv6 address is\nnormalized to condensed form. [RFC 2142](https://datatracker.ietf.org/doc/html/rfc2142)\nalso requires lowercase normalization for some specific mailbox names like `postmaster@`.\n\n### Length checks\n\nThis library checks that the length of the email address is not longer than\nthe maximum length. The check is performed on the normalized form of the\naddress, which might be different from a string provided by a user. If you\nsend email to the original string and not the normalized address, the email\nmight be rejected because the original address could be too long.\n\nExamples\n--------\n\nFor the email address `test@joshdata.me`, the returned object is:\n\n```python\nValidatedEmail(\n  normalized='test@joshdata.me',\n  local_part='test',\n  domain='joshdata.me',\n  ascii_email='test@joshdata.me',\n  ascii_local_part='test',\n  ascii_domain='joshdata.me',\n  smtputf8=False)\n```\n\nFor the fictitious but valid address `example@\u30c4.\u24c1\u24be\u24bb\u24ba`, which has an\ninternationalized domain but ASCII local part, the returned object is:\n\n```python\nValidatedEmail(\n  normalized='example@\u30c4.life',\n  local_part='example',\n  domain='\u30c4.life',\n  ascii_email='example@xn--bdk.life',\n  ascii_local_part='example',\n  ascii_domain='xn--bdk.life',\n  smtputf8=False)\n\n```\n\nNote that `normalized` and other fields provide a normalized form of the\nemail address, domain name, and (in other cases) local part (see earlier\ndiscussion of normalization), which you should use in your database.\n\nCalling `validate_email` with the ASCII form of the above email address,\n`example@xn--bdk.life`, returns the exact same information (i.e., the\n`normalized` field always will contain Unicode characters, not Punycode).\n\nFor the fictitious address `\u30c4-test@joshdata.me`, which has an\ninternationalized local part, the returned object is:\n\n```python\nValidatedEmail(\n  normalized='\u30c4-test@joshdata.me',\n  local_part='\u30c4-test',\n  domain='joshdata.me',\n  ascii_email=None,\n  ascii_local_part=None,\n  ascii_domain='joshdata.me',\n  smtputf8=True)\n```\n\nNow `smtputf8` is `True` and `ascii_email` is `None` because the local\npart of the address is internationalized. The `local_part` and `normalized` fields\nreturn the normalized form of the address.\n\nReturn value\n------------\n\nWhen an email address passes validation, the fields in the returned object\nare:\n\n| Field | Value |\n| -----:|-------|\n| `normalized` | The normalized form of the email address that you should put in your database. This combines the `local_part` and `domain` fields (see below). |\n| `ascii_email` | If set, an ASCII-only form of the normalized email address by replacing the domain part with [IDNA](https://tools.ietf.org/html/rfc5891) [Punycode](https://www.rfc-editor.org/rfc/rfc3492.txt). This field will be present when an ASCII-only form of the email address exists (including if the email address is already ASCII). If the local part of the email address contains internationalized characters, `ascii_email` will be `None`. If set, it merely combines `ascii_local_part` and `ascii_domain`. |\n| `local_part` | The normalized local part of the given email address (before the @-sign). Normalization includes Unicode NFC normalization and removing unnecessary quoted-string quotes and backslashes. If `allow_quoted_local` is True and the surrounding quotes are necessary, the quotes _will_ be present in this field. |\n| `ascii_local_part` | If set, the local part, which is composed of ASCII characters only. |\n| `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. |\n| `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. |\n| `domain_address` | If domain literals are allowed and if the email address contains one, an `ipaddress.IPv4Address` or `ipaddress.IPv6Address` object. |\n| `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. |\n| `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. |\n| `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. |\n| `spf` | Any SPF record found while checking deliverability. Only set if the SPF record is queried. |\n\nAssumptions\n-----------\n\nBy design, this validator does not pass all email addresses that\nstrictly conform to the standards. Many email address forms are obsolete\nor likely to cause trouble:\n\n* The validator assumes the email address is intended to be\n  usable on the public Internet. The domain part\n  of the email address must be a resolvable domain name\n  (see the deliverability checks described above).\n  Most [Special Use Domain Names](https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtml)\n  and their subdomains, as well as\n  domain names without a `.`, are rejected as a syntax error\n  (except see the `test_environment` parameter above).\n* Obsolete email syntaxes are rejected:\n  The unusual [\"(comment)\" syntax](https://github.com/JoshData/python-email-validator/issues/77)\n  is rejected. Extremely old obsolete syntaxes are\n  rejected. Quoted-string local parts and domain-literal addresses\n  are rejected by default, but there are options to allow them (see above).\n  No one uses these forms anymore, and I can't think of any reason why anyone\n  using this library would need to accept them.\n\nTesting\n-------\n\nTests can be run using\n\n```sh\npip install -r test_requirements.txt \nmake test\n```\n\nTests run with mocked DNS responses. When adding or changing tests, temporarily turn on the `BUILD_MOCKED_DNS_RESPONSE_DATA` flag in `tests/mocked_dns_responses.py` to re-build the database of mocked responses from live queries.\n\nFor Project Maintainers\n-----------------------\n\nThe package is distributed as a universal wheel and as a source package.\n\nTo release:\n\n* Update CHANGELOG.md.\n* Update the version number in `email_validator/version.py`.\n* Make & push a commit with the new version number and make sure tests pass.\n* Make & push a tag (see command below).\n* Make a release at https://github.com/JoshData/python-email-validator/releases/new.\n* Publish a source and wheel distribution to pypi (see command below).\n\n```sh\ngit tag v$(cat email_validator/version.py  | sed \"s/.* = //\" | sed 's/\"//g')\ngit push --tags\n./release_to_pypi.sh\n```\n\nLicense\n-------\n\nThis 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).\n",
    "bugtrack_url": null,
    "license": "Unlicense",
    "summary": "A robust email address syntax and deliverability validation library.",
    "version": "2.1.1",
    "project_urls": {
        "Homepage": "https://github.com/JoshData/python-email-validator"
    },
    "split_keywords": [
        "email",
        "address",
        "validator"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "e460b02cb0f5ee0be88bd4fbfdd9cc91e43ec2dfcc47fe064e7c70587ff58a94",
                "md5": "c939a0b160ab3960bb2ad04e2390cb93",
                "sha256": "97d882d174e2a65732fb43bfce81a3a834cbc1bde8bf419e30ef5ea976370a05"
            },
            "downloads": -1,
            "filename": "email_validator-2.1.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "c939a0b160ab3960bb2ad04e2390cb93",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 30334,
            "upload_time": "2024-02-26T22:09:57",
            "upload_time_iso_8601": "2024-02-26T22:09:57.951235Z",
            "url": "https://files.pythonhosted.org/packages/e4/60/b02cb0f5ee0be88bd4fbfdd9cc91e43ec2dfcc47fe064e7c70587ff58a94/email_validator-2.1.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "63822914bff80ebee8c027802a664ad4b4caad502cd594e358f76aff395b5e56",
                "md5": "a836759afeb7efdc5d493fbdf8a1bae6",
                "sha256": "200a70680ba08904be6d1eef729205cc0d687634399a5924d842533efb824b84"
            },
            "downloads": -1,
            "filename": "email_validator-2.1.1.tar.gz",
            "has_sig": false,
            "md5_digest": "a836759afeb7efdc5d493fbdf8a1bae6",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 44709,
            "upload_time": "2024-02-26T22:09:59",
            "upload_time_iso_8601": "2024-02-26T22:09:59.844802Z",
            "url": "https://files.pythonhosted.org/packages/63/82/2914bff80ebee8c027802a664ad4b4caad502cd594e358f76aff395b5e56/email_validator-2.1.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-02-26 22:09:59",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "JoshData",
    "github_project": "python-email-validator",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "test_requirements": [
        {
            "name": "coverage",
            "specs": [
                [
                    "==",
                    "7.4.4"
                ]
            ]
        },
        {
            "name": "dnspython",
            "specs": [
                [
                    "==",
                    "2.6.1"
                ]
            ]
        },
        {
            "name": "exceptiongroup",
            "specs": [
                [
                    "==",
                    "1.2.0"
                ]
            ]
        },
        {
            "name": "flake8",
            "specs": [
                [
                    "==",
                    "7.0.0"
                ]
            ]
        },
        {
            "name": "idna",
            "specs": [
                [
                    "==",
                    "3.7"
                ]
            ]
        },
        {
            "name": "iniconfig",
            "specs": [
                [
                    "==",
                    "2.0.0"
                ]
            ]
        },
        {
            "name": "mccabe",
            "specs": [
                [
                    "==",
                    "0.7.0"
                ]
            ]
        },
        {
            "name": "mypy",
            "specs": [
                [
                    "==",
                    "1.9.0"
                ]
            ]
        },
        {
            "name": "mypy-extensions",
            "specs": [
                [
                    "==",
                    "1.0.0"
                ]
            ]
        },
        {
            "name": "packaging",
            "specs": [
                [
                    "==",
                    "24.0"
                ]
            ]
        },
        {
            "name": "pluggy",
            "specs": [
                [
                    "==",
                    "1.4.0"
                ]
            ]
        },
        {
            "name": "pycodestyle",
            "specs": [
                [
                    "==",
                    "2.11.1"
                ]
            ]
        },
        {
            "name": "pyflakes",
            "specs": [
                [
                    "==",
                    "3.2.0"
                ]
            ]
        },
        {
            "name": "pytest",
            "specs": [
                [
                    "==",
                    "8.1.1"
                ]
            ]
        },
        {
            "name": "pytest-cov",
            "specs": [
                [
                    "==",
                    "5.0.0"
                ]
            ]
        },
        {
            "name": "tomli",
            "specs": [
                [
                    "==",
                    "2.0.1"
                ]
            ]
        },
        {
            "name": "typing_extensions",
            "specs": [
                [
                    "==",
                    "4.11.0"
                ]
            ]
        }
    ],
    "lcname": "email-validator"
}
        
Elapsed time: 0.25789s