Skip to main content

OpenSSF Scorecard

django-email-validators

no more invalid or disposable emails in your database.

Installation

  • Run pip install django-email-validators
  • Add django_email_validators to settings.INSTALLED_APPS
  • Restart your application server

Usage

Validators

  • 🗑️ validate_email_non_disposable
  • 🌐 validate_email_mx
  • ✍️ validate_email_provider_typo
  • 👤 validate_email_unique
    • ⚫ validate_email_unique_dot_insensitive
    • ➕ validate_email_unique_subaddress_insensitive

validate_email_non_disposable

Validates that the email is not from a disposable email provider (fast, offline check).

Accepts an optional check_mx argument (default: False): when enabled, the domain MX hostnames (and their domain suffixes) are also checked against the disposable providers blocklists. Disposable services rotate their facade domains faster than blocklists, but the MX records keep pointing to the provider's (blocklisted) mail server, so this catches fresh domains not yet blocklisted (e.g. kjkpc.net -> MX prd-smtp.10minutemail.com -> 10minutemail.com). Note: with check_mx=True a network request is performed, so it may be slow (MX lookups are cached in-process per domain).

validate_email_mx

Validates that the email domain has valid MX records (slow, requires network access).

validate_email_provider_typo

Validates that the email domain is not a likely typo of a common email provider. Checks a one-character diff against 80+ common providers and verifies the domain has no valid MX records (prevents false positives).

Examples that will be caught:

  • user@gmai.com -> suggests user@gmail.com
  • user@gmail.co -> suggests user@gmail.com
  • user@yahooo.com -> suggests user@yahoo.com

validate_email_unique

Validates that the email is unique in the database, preventing multiple accounts that map to the same inbox:

  • dot_insensitive (default: True): on dot-insensitive providers (e.g. Gmail) dots in the local part are ignored when comparing, so us.er@gmail.com and user@gmail.com are treated as the same inbox.
  • subaddress_insensitive (default: True): the +tag subaddress (RFC 5233) is ignored when comparing, on any domain, so user+tag@example.com and user@example.com are treated as the same inbox. Emails with + remain valid and are stored as entered: only the uniqueness check changes.

With both options disabled it performs a plain case-insensitive uniqueness check.

Accepts an optional exclude_pk argument to exclude the current user when updating an existing account, and an optional field argument (default: "email") to specify the model field name.

Examples that will be caught:

  • user@gmail.com already exists → us.er@gmail.com is rejected
  • user@example.com already exists → user+tag@example.com is rejected (and vice versa)
  • user@gmail.com already exists → us.er+tag@gmail.com is rejected

Since this validator requires access to the model instance (to exclude it on update), it cannot be used directly in a field's validators=[...]. Call it explicitly in a form or serializer:

from django_email_validators import validate_email_unique

# Form example
class UserForm(forms.ModelForm):
    def clean_email(self):
        email = self.cleaned_data["email"]
        validate_email_unique(
            email,
            exclude_pk=self.instance.pk,  # exclude the current user on update
            field="email",  # model field name (default: "email")
            message=None,  # custom error message (default: localized message)
            dot_insensitive=True,  # ignore dots on dot-insensitive providers
            subaddress_insensitive=True,  # ignore the "+tag" subaddress
        )
        return email

Or via validate_unique on the model:

class User(models.Model):
    email = models.EmailField()

    def validate_unique(self, exclude=None):
        super().validate_unique(exclude=exclude)
        validate_email_unique(
            self.email,
            exclude_pk=self.pk,  # exclude the current instance on update
            field="email",  # model field name (default: "email")
            message=None,  # custom error message (default: localized message)
            dot_insensitive=True,  # ignore dots on dot-insensitive providers
            subaddress_insensitive=True,  # ignore the "+tag" subaddress
        )

validate_email_unique_dot_insensitive

Validates that the email is unique in the database, accounting only for dot-insensitive providers (e.g. Gmail treats dots in the local part as insignificant), while the +tag subaddress is significant.

Equivalent to validate_email_unique(dot_insensitive=True, subaddress_insensitive=False), it accepts the same exclude_pk, field and message arguments.

Examples that will be caught:

  • user@gmail.com already exists → us.er@gmail.com is rejected

Examples that will pass:

  • user@example.com already exists → us.er@example.com passes (non dot-insensitive domain)
  • user@gmail.com already exists → user+tag@gmail.com passes (subaddress is significant)

validate_email_unique_subaddress_insensitive

Validates that the email is unique in the database, ignoring only the +tag subaddress (RFC 5233), on any domain, while dots in the local part are always significant.

Equivalent to validate_email_unique(dot_insensitive=False, subaddress_insensitive=True), it accepts the same exclude_pk, field and message arguments.

Examples that will be caught:

  • user@example.com already exists → user+tag@example.com is rejected (and vice versa)

Examples that will pass:

  • user@gmail.com already exists → us.er@gmail.com passes (dots are significant)

Usage

Note: validate_email_unique requires access to the model instance and cannot be used in validators=[...]. See the dedicated section above for usage examples.

from django.db import models
from django_email_validators import (
    validate_email_non_disposable,
    validate_email_mx,
    validate_email_provider_typo,
)

class User(models.Model):
    email = models.EmailField(
        validators=[
            validate_email_non_disposable,
            validate_email_mx,
            validate_email_provider_typo,
        ]
    )

Lookup

  • 🔍 get_user_queryset_by_email
  • 🔍 get_user_object_by_email
  • 🔍 get_queryset_by_email
  • 🔍 get_object_by_email

Retrieve the user account(s) matching an email address, using the same matching rules as validate_email_unique (case-insensitive, dot_insensitive and subaddress_insensitive, both True by default). Unlike the validators, these functions never raise ValidationError for a match: they return the matching records.

All accept the same options: field (default: "email"), exclude_pk, dot_insensitive and subaddress_insensitive.

get_user_queryset_by_email

Returns the queryset of matching users (0..N records). Accepts an optional base queryset (default: all users). Raises ValueError if the field does not exist on the user model.

get_user_object_by_email

Returns the first matching user or None. Unlike Manager.get, it never raises for missing matches.

get_queryset_by_email / get_object_by_email

Generic versions that work with any model: the base queryset is required as second argument.

from django_email_validators import get_queryset_by_email

subscribers = get_queryset_by_email("us.er+tag@gmail.com", Subscriber.objects.all())
from django_email_validators import get_user_object_by_email

user = get_user_object_by_email("us.er+tag@gmail.com")
# -> the user registered as "user@gmail.com", or None if not found

This is useful for enumeration-safe signup/recovery flows: retrieve the existing account matching the submitted email (including dot/subaddress variants) without revealing its existence in the response:

from django_email_validators import get_user_object_by_email

def signup(request):
    email = request.POST["email"]
    user = get_user_object_by_email(email)
    if user:
        # account already exists: notify the account owner by email
        send_account_exists_email(user)
    else:
        create_account_and_send_confirmation(email)
    # same response in both cases: no account enumeration
    return render(request, "signup_check_your_email.html")

Extending the providers list for typo check

You can extend the list of common email providers used by validate_email_provider_typo by adding your own list in Django settings:

EMAIL_VALIDATORS_EXTEND_COMMON_PROVIDERS = [
    'hey.com',
]

Extending the dot-insensitive domains list

You can extend the list of dot-insensitive domains used by validate_email_unique by adding your own list in Django settings:

EMAIL_VALIDATORS_EXTEND_DOT_INSENSITIVE_DOMAINS = [
    'fastmail.com',
]

Testing

# clone repository
git clone https://github.com/fabiocaccamo/django-email-validators.git && cd django-email-validators

# create virtualenv and activate it
python -m venv venv && . venv/bin/activate

# upgrade pip
python -m pip install --upgrade pip

# install requirements
pip install -r requirements.txt -r requirements-test.txt

# install pre-commit to run formatters and linters
pre-commit install --install-hooks

# run tests
tox
# or
pytest

License

Released under MIT License.


Supporting

  • :star: Star this project on GitHub
  • :octocat: Follow me on GitHub
  • :blue_heart: Follow me on Bluesky
  • :moneybag: Sponsor me on Github

See also

  • django-admin-interface - the default admin interface made customizable by the admin itself. popup windows replaced by modals. 🧙 ⚡

  • django-cache-cleaner - clear the entire cache or individual caches easily using the admin panel or management command. 🧹

  • django-colorfield - simple color field for models with a nice color-picker in the admin. 🎨

  • django-extra-settings - config and manage typed extra settings using just the django admin. ⚙️

  • django-maintenance-mode - shows a 503 error page when maintenance-mode is on. 🚧 🛠️

  • django-redirects - redirects with full control. ↪️

  • django-treenode - probably the best abstract model / admin for your tree based stuff. 🌳

  • python-benedict - dict subclass with keylist/keypath support, I/O shortcuts (base64, csv, json, pickle, plist, query-string, toml, xml, yaml) and many utilities. 📘

  • python-codicefiscale - encode/decode Italian fiscal codes - codifica/decodifica del Codice Fiscale. 🇮🇹 💳

  • python-fontbro - friendly font operations. 🧢

  • python-fsutil - file-system utilities for lazy devs. 🧟‍♂️

Metadata

Release files for django-email-validators 0.7.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-email-validators 0.7.0
File Size Uploaded
django_email_validators-0.7.0.tar.gz 27.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-email-validators 0.7.0
File Interpreter ABI Platform
django_email_validators-0.7.0-py3-none-any.whl Python 3 none any Details

Total release size: 47.8 kB

Release files / django_email_validators-0.7.0.tar.gz

Download URL django_email_validators-0.7.0.tar.gz
Size 27.2 kB
Tags Source
SHA-256 checksum
How to use checksums
8cf7dfc1dc79a8e09eb96cb3b7a2626d1a0c214d9a5fffb7e4af6e553b1cb32f
BLAKE2b-256 checksum
How to use checksums
e3bb1114efdecb29c4ba5b950d9a7c0e41bbddd55db9ea9403698259d16d3140
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 4, 2026.

Transparency log

Release files / django_email_validators-0.7.0-py3-none-any.whl

Download URL django_email_validators-0.7.0-py3-none-any.whl
Size 20.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ee7ddad8beb25f0f9afd124719d3a08f69eac1b888b91bf71f09df64695a2a3d
BLAKE2b-256 checksum
How to use checksums
d5b4319dc7597956ba91a8fd8a7c1550af79f843cec41c5eba8e48b845f79ecc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 4, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.7.0 This release

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page