neiam
/waxx
waxx
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
37cd9a6 · gmorell · 2026-05-21 13:53
Files changed
modified
config/runtime.exs
+121
−0
@@ -97,6 +97,127 @@ if config_env() == :prod do
| 97 | 97 | ] |
| 98 | 98 | ] |
| 99 | 99 | |
| 100 | + # Mailer adapter selection: | |
| 101 | + # | |
| 102 | + # * SMTP_RELAY set → Swoosh.Adapters.SMTP (via gen_smtp) | |
| 103 | + # * otherwise → Swoosh.Adapters.Logger | |
| 104 | + # | |
| 105 | + # The Logger adapter doesn't actually send mail — it logs the | |
| 106 | + # rendered email at :info — but it gets us off Swoosh.Adapters.Local | |
| 107 | + # (the dev mailbox), so the magic-link flow at least leaves a trail | |
| 108 | + # in the pod log when SMTP isn't configured. | |
| 109 | + smtp_tri = fn var, default -> | |
| 110 | + case System.get_env(var, default) do | |
| 111 | + v when v in ~w(always if_available never) -> String.to_existing_atom(v) | |
| 112 | + other -> raise "#{var} must be one of always|if_available|never, got: #{inspect(other)}" | |
| 113 | + end | |
| 114 | + end | |
| 115 | + | |
| 116 | + # Eager-load the OS CA bundle into OTP's `public_key` cache. Without | |
| 117 | + # this, `:public_key.cacerts_get/0` returns `:undefined`, and | |
| 118 | + # `gen_smtp`'s internal default `tls_options` injects | |
| 119 | + # `cacerts: :undefined` — which `:ssl` then rejects as incompatible | |
| 120 | + # with `verify: :verify_peer`, clobbering our explicit `cacertfile`. | |
| 121 | + case :public_key.cacerts_load() do | |
| 122 | + :ok -> | |
| 123 | + :ok | |
| 124 | + | |
| 125 | + {:error, reason} -> | |
| 126 | + require Logger | |
| 127 | + Logger.warning("public_key:cacerts_load failed: #{inspect(reason)} — TLS verify may fail") | |
| 128 | + end | |
| 129 | + | |
| 130 | + # CA bundle for TLS verification. We pass both `cacertfile` (path) | |
| 131 | + # and rely on the cacerts_load above for `cacerts_get/0` to work, | |
| 132 | + # belt-and-braces. Override `SMTP_CACERTFILE` if your image puts the | |
| 133 | + # bundle elsewhere; set `SMTP_TLS_VERIFY=verify_none` for self-signed | |
| 134 | + # / private relays where you'd rather skip cert validation. | |
| 135 | + smtp_cacertfile = | |
| 136 | + System.get_env("SMTP_CACERTFILE", "/etc/ssl/certs/ca-certificates.crt") | |
| 137 | + | |
| 138 | + smtp_verify = | |
| 139 | + case System.get_env("SMTP_TLS_VERIFY", "verify_peer") do | |
| 140 | + v when v in ~w(verify_peer verify_none) -> String.to_existing_atom(v) | |
| 141 | + other -> raise "SMTP_TLS_VERIFY must be verify_peer or verify_none, got: #{inspect(other)}" | |
| 142 | + end | |
| 143 | + | |
| 144 | + # Resolve the CA store NOW (after `cacerts_load`) rather than letting | |
| 145 | + # the downstream stack call `cacerts_get/0` lazily. gen_smtp / ssl | |
| 146 | + # has a habit of evaluating it in a context where it still returns | |
| 147 | + # `:undefined`, which then collides with our `cacertfile`. Passing | |
| 148 | + # `cacerts: <list>` explicitly is unambiguous — `:ssl` honors it | |
| 149 | + # and never re-derives. | |
| 150 | + smtp_cacerts = | |
| 151 | + case :public_key.cacerts_get() do | |
| 152 | + certs when is_list(certs) and certs != [] -> certs | |
| 153 | + _ -> nil | |
| 154 | + end | |
| 155 | + | |
| 156 | + mailer_opts = | |
| 157 | + if relay = System.get_env("SMTP_RELAY") do | |
| 158 | + tls_options = | |
| 159 | + [ | |
| 160 | + verify: smtp_verify, | |
| 161 | + server_name_indication: String.to_charlist(relay), | |
| 162 | + depth: 99 | |
| 163 | + ] ++ | |
| 164 | + cond do | |
| 165 | + smtp_verify != :verify_peer -> | |
| 166 | + [] | |
| 167 | + | |
| 168 | + is_list(smtp_cacerts) -> | |
| 169 | + [cacerts: smtp_cacerts] | |
| 170 | + | |
| 171 | + true -> | |
| 172 | + [cacertfile: smtp_cacertfile] | |
| 173 | + end | |
| 174 | + | |
| 175 | + # Pin modern TLS at both the top level (read by gen_smtp before | |
| 176 | + # it calls :ssl.connect) and inside tls_options (read by :ssl | |
| 177 | + # itself). Matches the working pattern from gitgud. | |
| 178 | + tls_versions = [:"tlsv1.2", :"tlsv1.3"] | |
| 179 | + | |
| 180 | + # gen_smtp 1.2 has two distinct connection paths with different | |
| 181 | + # option keys: | |
| 182 | + # | |
| 183 | + # * `ssl: true` (port 465 implicit TLS) reads its TLS opts | |
| 184 | + # from `sockopts`. `tls_options` is ignored on this path. | |
| 185 | + # * `tls: :always|:if_available` (STARTTLS upgrade) reads from | |
| 186 | + # `tls_options` — only used during do_STARTTLS. | |
| 187 | + # | |
| 188 | + # Without sockopts on the implicit-TLS path, OTP 28's `:ssl` | |
| 189 | + # falls through to its own defaults (verify_peer + | |
| 190 | + # `public_key.cacerts_get()` which returns `:undefined`) and | |
| 191 | + # bails with `{options, incompatible, [verify: verify_peer, | |
| 192 | + # cacerts: undefined]}`. Putting the same opts on both keys | |
| 193 | + # covers both relay setups. | |
| 194 | + ssl_opts = [{:versions, tls_versions} | tls_options] | |
| 195 | + | |
| 196 | + [ | |
| 197 | + adapter: Swoosh.Adapters.SMTP, | |
| 198 | + relay: relay, | |
| 199 | + port: String.to_integer(System.get_env("SMTP_PORT", "587")), | |
| 200 | + username: System.get_env("SMTP_USERNAME"), | |
| 201 | + password: System.get_env("SMTP_PASSWORD"), | |
| 202 | + tls: smtp_tri.("SMTP_TLS", "if_available"), | |
| 203 | + ssl: System.get_env("SMTP_SSL", "false") == "true", | |
| 204 | + auth: smtp_tri.("SMTP_AUTH", "if_available"), | |
| 205 | + allowed_tls_versions: tls_versions, | |
| 206 | + sockopts: ssl_opts, | |
| 207 | + tls_options: ssl_opts, | |
| 208 | + retries: 1, | |
| 209 | + no_mx_lookups: false | |
| 210 | + ] | |
| 211 | + else | |
| 212 | + [adapter: Swoosh.Adapters.Logger, level: :info] | |
| 213 | + end | |
| 214 | + | |
| 215 | + config :waxx, Waxx.Mailer, mailer_opts | |
| 216 | + | |
| 217 | + config :waxx, Waxx.Mailer, | |
| 218 | + from_name: System.get_env("MAIL_FROM_NAME", "Waxx"), | |
| 219 | + from_address: System.get_env("MAIL_FROM_ADDRESS") || "noreply@#{host}" | |
| 220 | + | |
| 100 | 221 | config :waxx, WaxxWeb.Endpoint, |
| 101 | 222 | url: [host: host, port: 443, scheme: "https"], |
| 102 | 223 | # Accept both http and https origins for the configured host. The |
modified
mix.exs
+1
−0
@@ -60,6 +60,7 @@ defmodule Waxx.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
@@ -15,6 +15,7 @@
| 15 | 15 | "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, |
| 16 | 16 | "finch": {:hex, :finch, "0.22.0", "5c48fa6f9706a78eb9036cacb67b8b996b4e66d111c543f4c29bb0f879a6806b", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [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", "b94e83c47780fc6813f746a1f1a34ee65cda42da4c5ea26a68f0acc4498e23dc"}, |
| 17 | 17 | "fine": {:hex, :fine, "0.1.6", "4bf7151493443c454aac9f2fa2f34f5fefd0346a83fb5586a016c4a135c63247", [:mix], [], "hexpm", "5638eb4495488e885ebec167fa57973e5c35e1a50c344eb7666c90ec1c4e3b12"}, |
| 18 | + "gen_smtp": {:hex, :gen_smtp, "1.3.0", "62c3d91f0dcf6ce9db71bcb6881d7ad0d1d834c7f38c13fa8e952f4104a8442e", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "0b73fbf069864ecbce02fe653b16d3f35fd889d0fdd4e14527675565c39d84e6"}, | |
| 18 | 19 | "gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"}, |
| 19 | 20 | "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"}, |
| 20 | 21 | "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]}, |
@@ -42,6 +43,7 @@
| 42 | 43 | "plug": {:hex, :plug, "1.19.2", "e4950525b22c6789dfb38a3f95d47171ba159da3fc5a33be9643b43d5e8adb98", [: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", "b6fce20a56af5e60fa5dfecf3f907bb98ec981be43c79a3809a499bc3d133de0"}, |
| 43 | 44 | "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, |
| 44 | 45 | "postgrex": {:hex, :postgrex, "0.22.2", "4aec14df2a72722aee92492566edbeeb44e233ecb86b1915d03136297ef1385d", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.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", "8946382ddb06294f56026ac4278b3cc212bac8a2c82ed68b4087819ed1abc53b"}, |
| 46 | + "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, | |
| 45 | 47 | "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"}, |
| 46 | 48 | "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"}, |
| 47 | 49 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, |
Parents: 6670896