Packages

Elixir client for the Listmonk open-source email platform API

Current section

Files

Jump to
listmonk_client lib listmonk client.ex
Raw

lib/listmonk/client.ex

defmodule Listmonk.Client do
@moduledoc """
HTTP client for interacting with the Listmonk API.
This module handles all HTTP communication with the Listmonk server,
including authentication, request building, and response parsing.
"""
alias Listmonk.{Config, Error}
@user_agent "Listmonk-Elixir-Client/0.2.0"
@doc """
Checks if the Listmonk instance is healthy.
## Examples
iex> Listmonk.Client.healthy?()
{:ok, true}
"""
@spec healthy?(Config.t() | nil) :: {:ok, boolean()} | {:error, term()}
def healthy?(config \\ nil) do
with {:ok, config} <- resolve_config(config),
{:ok, response} <- get("/api/health", config) do
{:ok, get_in(response, ["data"]) == true}
end
end
@doc """
Checks if the Listmonk instance is healthy. Raises on error.
"""
@spec healthy!(Config.t() | nil) :: boolean()
def healthy!(config \\ nil) do
case healthy?(config) do
{:ok, result} -> result
{:error, error} -> raise error
end
end
@doc """
Makes a GET request to the Listmonk API.
## Examples
iex> Listmonk.Client.get("/api/lists", config)
{:ok, %{"data" => %{"results" => [...]}}}
"""
@spec get(String.t(), Config.t() | nil, keyword()) :: {:ok, map()} | {:error, Error.t()}
def get(path, config, opts \\ []) do
request(:get, path, config, opts)
end
@doc """
Makes a GET request to the Listmonk API. Raises on error.
"""
@spec get!(String.t(), Config.t() | nil, keyword()) :: map()
def get!(path, config, opts \\ []) do
case get(path, config, opts) do
{:ok, response} -> response
{:error, error} -> raise error
end
end
@doc """
Makes a POST request to the Listmonk API.
## Examples
iex> Listmonk.Client.post("/api/subscribers", config, json: %{email: "test@example.com"})
{:ok, %{"data" => %{...}}}
"""
@spec post(String.t(), Config.t() | nil, keyword()) :: {:ok, map()} | {:error, Error.t()}
def post(path, config, opts \\ []) do
request(:post, path, config, opts)
end
@doc """
Makes a POST request to the Listmonk API. Raises on error.
"""
@spec post!(String.t(), Config.t() | nil, keyword()) :: map()
def post!(path, config, opts \\ []) do
case post(path, config, opts) do
{:ok, response} -> response
{:error, error} -> raise error
end
end
@doc """
Makes a PUT request to the Listmonk API.
## Examples
iex> Listmonk.Client.put("/api/subscribers/1", config, json: %{name: "Updated"})
{:ok, %{"data" => %{...}}}
"""
@spec put(String.t(), Config.t() | nil, keyword()) :: {:ok, map()} | {:error, Error.t()}
def put(path, config, opts \\ []) do
request(:put, path, config, opts)
end
@doc """
Makes a PUT request to the Listmonk API. Raises on error.
"""
@spec put!(String.t(), Config.t() | nil, keyword()) :: map()
def put!(path, config, opts \\ []) do
case put(path, config, opts) do
{:ok, response} -> response
{:error, error} -> raise error
end
end
@doc """
Makes a DELETE request to the Listmonk API.
## Examples
iex> Listmonk.Client.delete("/api/subscribers/1", config)
{:ok, %{"data" => true}}
"""
@spec delete(String.t(), Config.t() | nil, keyword()) :: {:ok, map()} | {:error, Error.t()}
def delete(path, config, opts \\ []) do
request(:delete, path, config, opts)
end
@doc """
Makes a DELETE request to the Listmonk API. Raises on error.
"""
@spec delete!(String.t(), Config.t() | nil, keyword()) :: map()
def delete!(path, config, opts \\ []) do
case delete(path, config, opts) do
{:ok, response} -> response
{:error, error} -> raise error
end
end
# Private functions
defp request(method, path, config, opts) do
with {:ok, config} <- resolve_config(config),
:ok <- Config.validate(config) do
url = build_url(config.url, path)
req_opts =
[
method: method,
url: url,
auth: {:basic, "#{config.username}:#{config.password}"},
headers: [{"user-agent", @user_agent}],
receive_timeout: 30_000
]
|> Keyword.merge(opts)
case Req.request(req_opts) do
{:ok, %Req.Response{status: status} = response} when status in 200..299 ->
parse_response(response)
{:ok, %Req.Response{} = response} ->
{:error, Error.from_response(response)}
{:error, exception} ->
{:error, Error.new("Request failed: #{Exception.message(exception)}")}
end
end
end
defp resolve_config(nil), do: {:ok, Config.resolve(nil)}
defp resolve_config(%Config{} = config), do: {:ok, Config.resolve(config)}
defp resolve_config(_) do
{:error, Error.new("Invalid configuration provided")}
end
defp build_url(base_url, path) do
base_url = String.trim_trailing(base_url, "/")
path = if String.starts_with?(path, "/"), do: path, else: "/#{path}"
base_url <> path
end
defp parse_response(%Req.Response{body: body}) when is_map(body) do
{:ok, body}
end
defp parse_response(%Req.Response{body: body}) when is_binary(body) do
case Jason.decode(body) do
{:ok, decoded} -> {:ok, decoded}
{:error, _} -> {:ok, %{"data" => body}}
end
end
defp parse_response(%Req.Response{body: body}) do
{:ok, %{"data" => body}}
end
end