Packages

Compose, deliver and test your emails easily in Elixir. Supports SMTP, Sendgrid, Mandrill, Postmark, Mailgun and many more out of the box. Preview your emails in the browser. Test your email sending code.

Security advisory: This version has known vulnerabilities. View advisories

Current section

Files

Jump to
swoosh lib swoosh adapters sandbox storage.ex
Raw

lib/swoosh/adapters/sandbox/storage.ex

defmodule Swoosh.Adapters.Sandbox.Storage do
@moduledoc false
use GenServer
@table :swoosh_sandbox_emails
# Public API
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def checkout do
GenServer.call(__MODULE__, {:checkout, self()})
end
def checkin do
GenServer.call(__MODULE__, {:checkin, self()})
end
def allow(owner_pid, allowed_pid) do
GenServer.call(__MODULE__, {:allow, owner_pid, allowed_pid})
end
def set_shared(pid) do
GenServer.call(__MODULE__, {:set_shared, pid})
end
def get_shared do
GenServer.call(__MODULE__, :get_shared)
end
def find_owner(callers) do
GenServer.call(__MODULE__, {:find_owner, callers})
end
def push(owner_pid, email) do
GenServer.call(__MODULE__, {:push, owner_pid, email})
end
def push_many(owner_pid, emails) do
GenServer.call(__MODULE__, {:push_many, owner_pid, emails})
end
def all(owner_pid) do
case :ets.lookup(@table, owner_pid) do
[{^owner_pid, emails}] -> emails
[] -> []
end
end
def flush(owner_pid) do
GenServer.call(__MODULE__, {:flush, owner_pid})
end
# GenServer callbacks
@impl true
def init(_opts) do
:ets.new(@table, [:set, :named_table, :public, {:read_concurrency, true}])
{:ok, %{owners: MapSet.new(), allowed: %{}, shared: nil, monitors: %{}}}
end
@impl true
def handle_call({:checkout, pid}, _from, state) do
if MapSet.member?(state.owners, pid) do
{:reply, {:error, :already_checked_out}, state}
else
ref = Process.monitor(pid)
:ets.insert(@table, {pid, []})
new_state = %{
state
| owners: MapSet.put(state.owners, pid),
monitors: Map.put(state.monitors, ref, pid)
}
{:reply, :ok, new_state}
end
end
def handle_call({:checkin, pid}, _from, state) do
{:reply, :ok, do_checkin(pid, state)}
end
def handle_call({:allow, owner_pid, allowed_pid}, _from, state) do
if MapSet.member?(state.owners, owner_pid) do
{:reply, :ok, %{state | allowed: Map.put(state.allowed, allowed_pid, owner_pid)}}
else
{:reply, {:error, :not_checked_out}, state}
end
end
def handle_call({:set_shared, pid}, _from, state) do
{:reply, :ok, %{state | shared: pid}}
end
def handle_call(:get_shared, _from, state) do
{:reply, state.shared, state}
end
def handle_call({:find_owner, callers}, _from, state) do
result =
Enum.find_value(callers, fn pid ->
cond do
MapSet.member?(state.owners, pid) -> {:ok, pid}
owner = Map.get(state.allowed, pid) -> {:ok, owner}
true -> nil
end
end)
{:reply, result || :no_owner, state}
end
def handle_call({:push, owner_pid, email}, _from, state) do
existing =
case :ets.lookup(@table, owner_pid) do
[{^owner_pid, emails}] -> emails
[] -> []
end
:ets.insert(@table, {owner_pid, [email | existing]})
send(owner_pid, {:email, email})
{:reply, :ok, state}
end
def handle_call({:push_many, owner_pid, emails}, _from, state) do
existing =
case :ets.lookup(@table, owner_pid) do
[{^owner_pid, existing}] -> existing
[] -> []
end
:ets.insert(@table, {owner_pid, Enum.reverse(emails) ++ existing})
send(owner_pid, {:emails, emails})
{:reply, :ok, state}
end
def handle_call({:flush, owner_pid}, _from, state) do
emails =
case :ets.lookup(@table, owner_pid) do
[{^owner_pid, emails}] -> emails
[] -> []
end
:ets.insert(@table, {owner_pid, []})
{:reply, emails, state}
end
@impl true
def handle_info({:DOWN, ref, :process, pid, _}, state) do
if Map.has_key?(state.monitors, ref) do
{:noreply, do_checkin(pid, state)}
else
{:noreply, state}
end
end
# Internal helpers
defp do_checkin(pid, state) do
:ets.delete(@table, pid)
{to_demonitor, monitors} =
Enum.split_with(state.monitors, fn {_ref, p} -> p == pid end)
Enum.each(to_demonitor, fn {ref, _} -> Process.demonitor(ref, [:flush]) end)
allowed =
state.allowed
|> Enum.reject(fn {allowed_pid, owner_pid} ->
owner_pid == pid or allowed_pid == pid
end)
|> Map.new()
%{
state
| owners: MapSet.delete(state.owners, pid),
allowed: allowed,
shared: if(state.shared == pid, do: nil, else: state.shared),
monitors: Map.new(monitors)
}
end
end