Validate emails in Django forms

A Django validator and form and model fields that fail an email address that cannot receive email. They are built on the official Python client library, check each address once, and never stop a real person from signing up when the API cannot answer in time.

from django import forms
from email_validation_django import DeliverableEmailFormField

class SignupForm(forms.Form):
    email = DeliverableEmailFormField()

Install

The package is not on PyPI yet. Download it here and install it with pip:

  1. Download email_validation_django-0.1.0-py3-none-any.whl.

  2. Install it into your project's environment (pip installs Django and the Python client library from PyPI):

    pip install ./email_validation_django-0.1.0-py3-none-any.whl

    To keep it in your requirements, commit the file to your repository and list its path there.

  3. Create an API key on the API keys page with the permission "Validate addresses", and give it a daily credit cap to limit what a busy form can spend. Set it in your environment as EV_API_KEY; never put it in code. Each new address a visitor sends is a paid check, so also rate-limit the form's view per visitor (with a rate-limiting package or at your proxy).

It needs Django 4.2 or newer and Python 3.10 or newer. Nothing goes in INSTALLED_APPS.

Use it

from django import forms
from django.db import models
from email_validation_django import (
    DeliverableEmailField,
    DeliverableEmailFormField,
    DeliverableEmailValidator,
    validate_email_deliverable,
)

# a form field: forms.EmailField, then the check once the format is right
class SignupForm(forms.Form):
    email = DeliverableEmailFormField()
    work_email = DeliverableEmailFormField(fail_on=["invalid", "disposable"], fail_open=False, required=False)

# a model field: models.EmailField, checked in full_clean() and in model forms
class Customer(models.Model):
    email = DeliverableEmailField(unique=True)

# the validator on any field
email = forms.EmailField(validators=[validate_email_deliverable])
strict = DeliverableEmailValidator(fail_on=["invalid", "disposable"], messages={"disposable": "Please use your work address."})

A malformed address is refused by Django's own email check and never sent.

Settings

All optional, in settings.py:

EMAIL_VALIDATION = {
    # default: the EV_API_KEY environment variable
    "API_KEY": None,
    # default: EV_BASE_URL, else https://api.email-validation.org
    "BASE_URL": None,
    "FAIL_ON": ["invalid"],
    "BLOCK_DISPOSABLE": False,
    "FAIL_OPEN": True,
    "TIMEOUT": 5,
    "CLIENT_TIMEOUT": 8,
    "CACHE": "default",
    "CACHE_TTL": 3600,
}
  • Which statuses fail. Only invalid by default. Set "BLOCK_DISPOSABLE": True to fail disposable addresses as well. catch_all and unknown pass unless you add them: see what each status means.
  • When the check cannot finish (it timed out, the API could not be reached, it answered with a 5xx or 429 error, the account has no credits or no key is set), the address passes and a warning goes to the package's logger (named after the package) with the error code and request id, never the address. Set "FAIL_OPEN": False to fail the field with "We could not check this address right now" instead.
  • EU data residency. Set "BASE_URL": "https://api-eu.email-validation.org"; the same key works on both.

Speed and caching

The validator gives the API 5 seconds for the check (the API takes 5 to 15) and waits 8 seconds for the answer, without retrying: a slow mail server then answers unknown, which passes and is free.

Each result is kept in Django's cache for an hour, under a SHA-256 hash of the trimmed, lower-cased address, so a form sent again after fixing another field is not checked or charged again. Only the status is kept, never the address, and unknown results are not kept.

Messages

The messages are translatable (lazy gettext strings): translate them in your project's locale files, or replace one with the validator's messages argument. Each error has a code for your templates or API responses:

email_invalid  email_disposable  email_catch_all  email_unknown  email_unavailable

The messages never show the API's own error text.

Costs

Each completed check costs one credit. Results with the status unknown are free, and so are malformed addresses and the test addresses: try the field with accepted@sandbox.email-validation.org (passes) and no_mailbox@sandbox.email-validation.org (fails) at no cost. See credits and billing.

Updated · Maintained by the email-validation.org team