defmodule Channels.Email do @moduledoc """ Delivers notifications via email using SMTP. Sends a plain-text email through any SMTP relay. Uses `Swoosh.Adapters.SMTP` with per-delivery configuration so each channel instance can target a different mail server without application-level reconfiguration. ## Params * `:relay` — SMTP server hostname (required) * `:port` — SMTP port; defaults to `587` * `:username` — SMTP auth username (optional) * `:password` — SMTP auth password (optional) * `:tls` — TLS mode: `"always"`, `"never"`, or `"if_available"` (default) * `:from` — sender email address (required) * `:to` — recipient email address (required) ## Example Via Mailpit (local test server, no auth): %{relay: "mailpit.apps.svc.cluster.local", port: "1025", tls: "never", from: "raven@example.com", to: "admin@example.com"} If `:raven_core, :base_url` is configured, the body includes a link back to the monitor's page on this instance's web UI. """ use CodeNameRaven.Channel, category: :email import Swoosh.Email alias CodeNameRaven.Channel.Notification defmodule Mailer do @moduledoc false use Swoosh.Mailer, otp_app: :raven_integration_email end @impl true def display_name, do: "Email" @impl true def params_template do %{ relay: "", port: "587", username: "", password: "", tls: "if_available", from: "", to: "" } end @impl true def params_schema do [ relay: [type: :string, required: true], port: [type: :string, default: "587"], username: [type: :string], password: [type: :string], tls: [type: {:in, ["always", "never", "if_available"]}, default: "if_available"], from: [type: :string, required: true], to: [type: :string, required: true] ] end @impl true def deliver(%Notification{} = n, params) do relay = str(params[:relay] || params["relay"]) from = str(params[:from] || params["from"]) to = str(params[:to] || params["to"]) with :ok <- require_param(relay, "relay"), :ok <- require_param(from, "from"), :ok <- require_param(to, "to") do port = parse_int(params[:port] || params["port"], 587) username = str(params[:username] || params["username"]) password = str(params[:password] || params["password"]) tls = parse_tls(params[:tls] || params["tls"]) email = new() |> from(from) |> to(to) |> subject(format_subject(n)) |> text_body(format_body(n)) smtp_config = [adapter: Swoosh.Adapters.SMTP, relay: relay, port: port, tls: tls] |> add_auth(username, password) case Mailer.deliver(email, smtp_config) do {:ok, _} -> :ok {:error, reason} -> {:error, inspect(reason)} end end end # --------------------------------------------------------------------------- # Formatting — public, directly exercised by CodeNameRaven.Channel.FormattingTest # --------------------------------------------------------------------------- @doc false def format_subject(%Notification{} = n) do "[Raven] #{n.monitor_name || n.monitor_id} is #{n.status}" end @doc false def format_body(%Notification{} = n) do [ "Monitor: #{n.monitor_name || n.monitor_id}", "Status: #{n.status}", previous_status_line(n), down_duration_line(n), "At: #{DateTime.to_iso8601(n.occurred_at)}", view_link_line(n) ] |> Enum.reject(&is_nil/1) |> Enum.join("\n") end defp previous_status_line(%Notification{previous_status: nil}), do: nil defp previous_status_line(%Notification{previous_status: prev}), do: "Previous status: #{prev}" defp down_duration_line(%Notification{} = n) do case Notification.format_duration_down(n) do nil -> nil duration -> "Down for: #{duration}" end end defp view_link_line(%Notification{monitor_id: monitor_id}) do case Application.get_env(:raven_core, :base_url) do base_url when is_binary(base_url) and base_url != "" -> "View: #{base_url}/monitors/#{monitor_id}" _ -> nil end end # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- defp add_auth(config, nil, _password), do: config defp add_auth(config, username, password), do: config ++ [username: username, password: password || ""] defp require_param(nil, name), do: {:error, "#{name} is required"} defp require_param(_, _), do: :ok defp parse_tls("always"), do: :always defp parse_tls("never"), do: :never defp parse_tls(_), do: :if_available defp str(nil), do: nil defp str(""), do: nil defp str(v), do: v defp parse_int(nil, default), do: default defp parse_int(v, _) when is_integer(v), do: v defp parse_int(v, default) when is_binary(v) do case Integer.parse(v) do {n, _} -> n :error -> default end end defp parse_int(_, default), do: default end