Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
line_length: 120
]
2 changes: 2 additions & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
erlang 27.0
elixir 1.18.0-otp-27
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@

Disposable is an Elixir library for checking if an email address is from a disposable email service. It provides a fast, memory-efficient way to validate email domains against a known list of disposable email providers. With over 169.000 domains in the list, Disposable is a reliable tool for preventing users from signing up with temporary email addresses.

## Note

- Requires Elixir 1.18 or later

## Features

- Fast in-memory checking of email domains
- Easy to use API
- Configurable disposable domains list
- Ability to reload domains without application restart
- Built-in list of 169.000+ disposable email domains

## Installation

Expand All @@ -19,7 +24,7 @@ The package can be installed by adding `disposable` to your list of dependencies
```elixir
def deps do
[
{:disposable, "~> 0.1.3"}
{:disposable, "~> 0.1.4"}
]
end
```
Expand Down
6 changes: 2 additions & 4 deletions lib/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@ defmodule Disposable.Application do
@impl true
def start(_type, _args) do
children = [
# List all child processes to be supervised
Disposable
{Disposable, []}
]

# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
# See https://hexdocs.pm/elixir/Supervisor.html for other strategies and supported options
opts = [strategy: :one_for_one, name: Disposable.Supervisor]
Supervisor.start_link(children, opts)
end
Expand Down
113 changes: 81 additions & 32 deletions lib/disposable.ex
Original file line number Diff line number Diff line change
@@ -1,72 +1,121 @@
defmodule Disposable do
use Agent
@moduledoc """
Provides functionality to check if an email address belongs to a disposable email service.
Uses an Agent process to maintain an in-memory cache of disposable domains for efficient lookups.
"""

use Agent
require Logger

@moduledoc """
Checks if an email address is from a disposable email service.
Uses an Agent to keep domains in memory for faster checking.
"""
@typedoc "A disposable email"
@type email :: String.t()
@typedoc "A disposable domain name"
@type domain :: String.t()

@doc """
Starts the Disposable domain cache Agent.

The Agent is registered under the current module name and initialized with
domains loaded from the configured file path.
"""
@spec start_link(keyword()) :: Agent.on_start()
def start_link(_opts) do
Agent.start_link(&load_domains/0, name: __MODULE__)
end

@doc """
Checks if an email address is from a disposable email service.
Checks if an email address belongs to a disposable email service.

Returns `false` if the email is invalid or if the Agent process is not running.

## Examples

iex> Disposable.check("test@example.com")
iex> Disposable.check("user@example.com")
false

iex> Disposable.check("test@alltempmail.com")
true

"""
def check(email) do
if Process.whereis(__MODULE__) do
domain = get_domain(email)
@spec check(email()) :: boolean() | {:error, :not_running} | {:error, :invalid_email} | no_return()
def check(email) when is_binary(email) do
with {:ok, pid} <- ensure_running(),
{:ok, domain} <- extract_domain(email) do
Agent.get(pid, &MapSet.member?(&1, domain))
else
{:error, :not_running} ->
Logger.error("Disposable email Agent is not running")
raise Disposable.Exception, message: "Disposable email Agent is not running"

if domain do
Agent.get(__MODULE__, &MapSet.member?(&1, domain))
else
{:error, :invalid_email} ->
false
end
else
Logger.error("Disposable E-mail Agent is not running.")
false
end
end

defp get_domain(email) do
@doc """
Reloads the disposable domains from the configured file into memory.

Useful for updating the domain list without restarting the application.
"""
@spec reload() :: :ok
def reload do
data = load_domains()
Agent.update(__MODULE__, fn _state -> data end)
end

@doc """
Load domains from a URL into memory.
"""
def load_url(url) do
case Disposable.Http.get(url) do
{:ok, 200, _headers, body} ->
data = to_string(body) |> String.split("\n")

parsed =
data
|> Stream.map(&String.trim/1)
|> MapSet.new()

Agent.update(__MODULE__, fn _state -> parsed end)
end
end

# Private Functions

@spec ensure_running() :: {:ok, pid()} | {:error, :not_running}
defp ensure_running do
case Process.whereis(__MODULE__) do
pid when is_pid(pid) -> {:ok, pid}
nil -> {:error, :not_running}
end
end

@spec extract_domain(email()) :: {:ok, domain()} | {:error, :invalid_email}
defp extract_domain(email) do
case String.split(email, "@") do
[_local_part, domain] -> String.downcase(domain)
_ -> nil
[_local_part, domain] -> {:ok, String.downcase(domain)}
_invalid -> {:error, :invalid_email}
end
end

@spec load_domains() :: MapSet.t()
defp load_domains do
domains_file()
|> File.stream!()
|> Stream.map(&String.trim/1)
|> MapSet.new()
end

@spec domains_file() :: String.t()
defp domains_file do
default_path = Application.app_dir(:disposable, "priv/domains.txt")

case Application.get_env(:disposable, :disposable_domains_file) do
nil -> default_path
path -> if File.exists?(path), do: path, else: default_path
end
Application.get_env(:disposable, :disposable_domains_file)
|> determine_file_path(default_path)
end

@doc """
Reloads the domains from the file into memory.
Useful for updating the list without restarting the application.
"""
def reload do
Agent.update(__MODULE__, fn _state -> load_domains() end)
end
@spec determine_file_path(String.t() | nil, String.t()) :: String.t()
defp determine_file_path(nil, default_path), do: default_path

defp determine_file_path(path, default_path),
do: if(File.exists?(path), do: path, else: default_path)
end
3 changes: 3 additions & 0 deletions lib/disposable/disposable_exception.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
defmodule Disposable.Exception do
defexception [:message]
end
23 changes: 23 additions & 0 deletions lib/disposable/http.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule Disposable.Http do
@default_timeout 15_000

def get(url, headers \\ [], timeout \\ @default_timeout) do
request_headers = Enum.map(headers, fn {k, v} -> {String.to_charlist(k), String.to_charlist(v)} end)

opts = [
timeout: timeout,
ssl: [verify: :verify_none],
autoredirect: false
]

case :httpc.request(:get, {String.to_charlist(url), request_headers}, opts, []) do
{:ok, {{_version, status_code, _reason}, response_headers, body}} ->
headers_map = Enum.into(response_headers, %{}, fn {k, v} -> {List.to_string(k), List.to_string(v)} end)

{:ok, status_code, headers_map, body}

{:error, reason} ->
{:error, reason}
end
end
end
4 changes: 2 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
defmodule Disposable.MixProject do
use Mix.Project

@version "0.1.3"
@version "0.1.4"
@source_url "https://github.com/marinac-dev/disposable"

def project do
[
app: :disposable,
version: @version,
elixir: "~> 1.16",
elixir: "~> 1.18",
start_permanent: Mix.env() == :prod,
deps: deps(),
description: description(),
Expand Down