a good email setup

eb57556 · gmorell · 2026-04-30 01:12

14 files +52 -18

Files changed

modified config/config.exs
+4 −1
@@ -53,7 +53,10 @@ config :diogramos, DiogramosWeb.Endpoint,
53 53 #
54 54 # For production it's recommended to configure a different adapter
55 55 # at the `config/runtime.exs`.
56 −config :diogramos, Diogramos.Mailer, adapter: Swoosh.Adapters.Local
56 +config :diogramos, Diogramos.Mailer,
57 + adapter: Swoosh.Adapters.Local,
58 + from_name: "Diogramos",
59 + from_address: "noreply@localhost"
57 60
58 61 # Configure esbuild (the version is required)
59 62 config :esbuild,
modified config/runtime.exs
+25 −8
@@ -105,20 +105,33 @@ if config_env() == :prod do
105 105
106 106 # Mailer adapter selection:
107 107 #
108 − # * MAILGUN_API_KEY + MAILGUN_DOMAIN → Swoosh.Adapters.Mailgun
109 − # * otherwise → Swoosh.Adapters.Logger
108 + # * SMTP_RELAY set → Swoosh.Adapters.SMTP (gen_smtp)
109 + # * otherwise → Swoosh.Adapters.Logger
110 110 #
111 111 # The Logger adapter doesn't actually send mail — it logs the
112 112 # rendered email at :info — but it gets us off Swoosh.Adapters.Local,
113 113 # which is what the login page checks for to show the dev-mailbox
114 − # banner. Wire up a real provider when you're ready to receive
115 − # confirmation links again.
114 + # banner.
115 + smtp_tri = fn var, default ->
116 + case System.get_env(var, default) do
117 + v when v in ~w(always if_available never) -> String.to_existing_atom(v)
118 + other -> raise "#{var} must be one of always|if_available|never, got: #{inspect(other)}"
119 + end
120 + end
121 +
116 122 mailer_opts =
117 − if System.get_env("MAILGUN_API_KEY") && System.get_env("MAILGUN_DOMAIN") do
123 + if relay = System.get_env("SMTP_RELAY") do
118 124 [
119 − adapter: Swoosh.Adapters.Mailgun,
120 − api_key: System.get_env("MAILGUN_API_KEY"),
121 − domain: System.get_env("MAILGUN_DOMAIN")
125 + adapter: Swoosh.Adapters.SMTP,
126 + relay: relay,
127 + port: String.to_integer(System.get_env("SMTP_PORT", "587")),
128 + username: System.get_env("SMTP_USERNAME"),
129 + password: System.get_env("SMTP_PASSWORD"),
130 + tls: smtp_tri.("SMTP_TLS", "if_available"),
131 + ssl: System.get_env("SMTP_SSL", "false") == "true",
132 + auth: smtp_tri.("SMTP_AUTH", "if_available"),
133 + retries: 1,
134 + no_mx_lookups: false
122 135 ]
123 136 else
124 137 [adapter: Swoosh.Adapters.Logger, level: :info]
@@ -126,6 +139,10 @@ if config_env() == :prod do
126 139
127 140 config :diogramos, Diogramos.Mailer, mailer_opts
128 141
142 + config :diogramos, Diogramos.Mailer,
143 + from_name: System.get_env("MAIL_FROM_NAME", "Diogramos"),
144 + from_address: System.get_env("MAIL_FROM_ADDRESS") || "noreply@#{host}"
145 +
129 146 config :diogramos, DiogramosWeb.Endpoint,
130 147 url: [host: host, port: 443, scheme: "https"],
131 148 # Accept both http and https origins for the configured host. The
modified lib/diogramos/accounts/user_notifier.ex
+5 −1
@@ -6,10 +6,14 @@ defmodule Diogramos.Accounts.UserNotifier do
6 6
7 7 # Delivers the email using the application mailer.
8 8 defp deliver(recipient, subject, body) do
9 + mailer_config = Application.get_env(:diogramos, Mailer, [])
10 + from_name = Keyword.get(mailer_config, :from_name, "Diogramos")
11 + from_address = Keyword.get(mailer_config, :from_address, "noreply@localhost")
12 +
9 13 email =
10 14 new()
11 15 |> to(recipient)
12 − |> from({"Diogramos", "contact@example.com"})
16 + |> from({from_name, from_address})
13 17 |> subject(subject)
14 18 |> text_body(body)
15 19
modified lib/diogramos_web/live/canvas_live/edit.ex
+14 −7
@@ -1899,7 +1899,7 @@ defmodule DiogramosWeb.CanvasLive.Edit do
1899 1899 if enabled == [] do
1900 1900 ~H""
1901 1901 else
1902 − assigns = assign(assigns, :placements, link_placements(element, enabled))
1902 + assigns = assign(assigns, :placements, link_placements(element, enabled, :editor))
1903 1903
1904 1904 ~H"""
1905 1905 <%= for %{link: link, x: x, y: y, size: size, href: href, icon: icon} <- @placements do %>
@@ -1937,7 +1937,7 @@ defmodule DiogramosWeb.CanvasLive.Edit do
1937 1937 bottom-right corner of the element's bbox toward the left so the
1938 1938 "primary" (first) link is closest to the corner.
1939 1939 """
1940 − def link_placements(element, enabled_links) do
1940 + def link_placements(element, enabled_links, context \\ :embed) do
1941 1941 {origin_x, origin_y, size} = link_overlay_geometry(element)
1942 1942 gap = 2
1943 1943
@@ -1946,7 +1946,7 @@ defmodule DiogramosWeb.CanvasLive.Edit do
1946 1946 |> Enum.map(fn {link, i} ->
1947 1947 %{
1948 1948 link: link,
1949 − href: link_href(link),
1949 + href: link_href(link, context),
1950 1950 icon: link_icon_id(link["icon"]),
1951 1951 x: origin_x - i * (size + gap),
1952 1952 y: origin_y,
@@ -1978,11 +1978,18 @@ defmodule DiogramosWeb.CanvasLive.Edit do
1978 1978 def link_icon_id(name), do: "dx-link-icon-#{name || "link"}"
1979 1979
1980 1980 @doc false
1981 − def link_href(%{"kind" => "canvas", "target" => slug}) when is_binary(slug) and slug != "",
1982 − do: "/c-embed/#{slug}"
1981 + def link_href(link, context \\ :embed)
1983 1982
1984 − def link_href(%{"target" => target}) when is_binary(target), do: target
1985 − def link_href(_), do: "#"
1983 + def link_href(%{"kind" => "canvas", "target" => slug}, :editor)
1984 + when is_binary(slug) and slug != "",
1985 + do: "/c/#{slug}"
1986 +
1987 + def link_href(%{"kind" => "canvas", "target" => slug}, _context)
1988 + when is_binary(slug) and slug != "",
1989 + do: "/c-embed/#{slug}"
1990 +
1991 + def link_href(%{"target" => target}, _context) when is_binary(target), do: target
1992 + def link_href(_, _context), do: "#"
1986 1993
1987 1994 attr :element, :map, required: true
1988 1995 attr :element_id, :string, required: true
modified lib/diogramos_web/live/canvas_live/embed.ex
+1 −1
@@ -493,7 +493,7 @@ defmodule DiogramosWeb.CanvasLive.Embed do
493 493 assign(
494 494 assigns,
495 495 :placements,
496 − DiogramosWeb.CanvasLive.Edit.link_placements(element, enabled)
496 + DiogramosWeb.CanvasLive.Edit.link_placements(element, enabled, :embed)
497 497 )
498 498
499 499 ~H"""
modified mix.exs
+1 −0
@@ -60,6 +60,7 @@ defmodule Diogramos.MixProject do
60 60 compile: false,
61 61 depth: 1},
62 62 {:swoosh, "~> 1.16"},
63 + {:gen_smtp, "~> 1.2"},
63 64 {:req, "~> 0.5"},
64 65 {:telemetry_metrics, "~> 1.0"},
65 66 {:telemetry_poller, "~> 1.0"},
modified mix.lock
+2 −0
@@ -16,6 +16,7 @@
16 16 "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
17 17 "finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"},
18 18 "fine": {:hex, :fine, "0.1.6", "4bf7151493443c454aac9f2fa2f34f5fefd0346a83fb5586a016c4a135c63247", [:mix], [], "hexpm", "5638eb4495488e885ebec167fa57973e5c35e1a50c344eb7666c90ec1c4e3b12"},
19 + "gen_smtp": {:hex, :gen_smtp, "1.3.0", "62c3d91f0dcf6ce9db71bcb6881d7ad0d1d834c7f38c13fa8e952f4104a8442e", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "0b73fbf069864ecbce02fe653b16d3f35fd889d0fdd4e14527675565c39d84e6"},
19 20 "gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"},
20 21 "hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"},
21 22 "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]},
@@ -46,6 +47,7 @@
46 47 "plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"},
47 48 "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
48 49 "postgrex": {:hex, :postgrex, "0.22.0", "fb027b58b6eab1f6de5396a2abcdaaeb168f9ed4eccbb594e6ac393b02078cbd", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a68c4261e299597909e03e6f8ff5a13876f5caadaddd0d23af0d0a61afcc5d84"},
50 + "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
49 51 "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},
50 52 "sentry": {:hex, :sentry, "10.10.0", "d058b635f3796947545c8057a42996f6dbefd12152da947209b56d16af41b161", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_ownership, "~> 0.3.0 or ~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.20 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "7c7ddd3cfdd63fcee53b1e28f9a653037e6927b2b1dbd300b7aeee9687c7a8f6"},
51 53 "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},

Parents: 7faf5f7