How to block disposable email addresses View Source

The file lib/your_app/validations/forbidden_email_providers.txt will contain one provider (= what follows the @ character) by line to reject.

Example:

foo
bar.com

In the following implementation, the pattern "foo" wil be considered, comparatively to a regular expression (but we directly use pattern matching) to: ^foo.* on the hostname part of the email address.

Create lib/your_app/validations/email_provider_validation.ex with the following code:

# lib/your_app/validations/email_provider_validation.ex
defmodule YourApp.EmailProviderValidation do
  import Ecto.Changeset
  #import YourApp.Gettext

  @external_resource providers_path = Path.join([__DIR__, "forbidden_email_providers.txt"])
  for line <- File.stream!(providers_path, [], :line) do
    provider =
      line
      |> String.trim_trailing()
      |> String.downcase()

    defp valid_email_provider?(unquote(provider) <> _rest), do: false
  end

  defp valid_email_provider?(_provider), do: true

  def validate_email_provider(%Ecto.Changeset{valid?: true} = changeset, field)
    when is_atom(field)
  do
    validate_change changeset, field, {:format, nil}, fn _, value ->
      [_head, provider] =
        value
        |> String.downcase()
        |> String.split("@", parts: 2)
      if valid_email_provider?(provider) do
        []
      else
        [{field, {"%{provider} is not allowed", provider: provider, validation: :format}}] # better if you translate it with (d)gettext
      end
    end
  end

  def validate_email_provider(changeset = %Ecto.Changeset{}, _field), do: changeset
end

Then edit lib/your_app/user.ex to add to the end of the functions validate_create_registration/2 and validate_update_registration/2, the following line: |> YourApp.EmailProviderValidation.validate_email_provider(:email)

You can also write this functionnality as a plugin by implemenenting the validate_create_registration/2 and validate_update_registration/2 callbacks instead of modifying your lib/your_app/user.ex.

If so:

# lib/your_app/haytni/refuse_disposable_email_plugin.ex
defmodule YourApp.RefuseDisposableEmailPlugin do
  use Haytni.Plugin

  @impl Haytni.Plugin
  def validate_create_registration(changeset = %Ecto.Changeset{}, _module, _config) do
    changeset
    |> YourApp.EmailProviderValidation(:email)
  end

  @impl Haytni.Plugin
  def validate_update_registration(changeset = %Ecto.Changeset{}, _module, _config) do
    changeset
    |> YourApp.EmailProviderValidation(:email)
  end
end

And register YourApp.RefuseDisposableEmailPlugin to your Haytni stack in your lib/your_app/haytni.ex:

# lib/your_app/haytni.ex
defmodule YourApp.Haytni do
  use Haytni, otp_app: :your_app

  # ...

  stack YourApp.RefuseDisposableEmailPlugin
end

Note: this implementation does not check that the email address has a valid format, you need to check this point before (with Ecto.Changeset.validate_format(changeset, :email, ~R/^[^@\s]+@[^@\s]+$/) for example). Haytni already does it in RegisterablePlugin so, if you use it as a plugin, just call yours after RegisterablePlugin.