fix: repaired smtp configuration for port 587
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
837f5fd5bf
commit
e95c1d6254
6 changed files with 138 additions and 20 deletions
12
.env.example
12
.env.example
|
|
@ -41,3 +41,15 @@ ASSOCIATION_NAME="Sportsclub XYZ"
|
|||
# VEREINFACHT_API_KEY=your-api-key
|
||||
# VEREINFACHT_CLUB_ID=2
|
||||
# VEREINFACHT_APP_URL=https://app.verein.visuel.dev
|
||||
|
||||
# Optional: Mail / SMTP (transactional emails). If set, overrides Settings UI.
|
||||
# Export current UI settings to .env: mix mv.export_smtp_to_env
|
||||
# SMTP_HOST=smtp.example.com
|
||||
# SMTP_PORT=587
|
||||
# SMTP_USERNAME=user
|
||||
# SMTP_PASSWORD=secret
|
||||
# SMTP_PASSWORD_FILE=/run/secrets/smtp_password
|
||||
# SMTP_SSL=tls
|
||||
# SMTP_VERIFY_PEER=false
|
||||
# MAIL_FROM_EMAIL=noreply@example.com
|
||||
# MAIL_FROM_NAME=Mila
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- **SMTP configuration** – Repaired so that both port 587 (TLS/STARTTLS) and 465 (SSL) work correctly.
|
||||
|
||||
## [1.1.0] - 2026-03-13
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -263,20 +263,25 @@ if config_env() == :prod do
|
|||
|
||||
verify_mode = if smtp_verify_peer, do: :verify_peer, else: :verify_none
|
||||
|
||||
base_smtp_opts = [
|
||||
adapter: Swoosh.Adapters.SMTP,
|
||||
relay: String.trim(smtp_host_env),
|
||||
port: smtp_port_env,
|
||||
username: System.get_env("SMTP_USERNAME"),
|
||||
password: smtp_password_env,
|
||||
ssl: smtp_ssl_mode == "ssl",
|
||||
tls: if(smtp_ssl_mode == "tls", do: :always, else: :never),
|
||||
auth: :always,
|
||||
tls_options: [verify: verify_mode]
|
||||
]
|
||||
|
||||
# Port 465: pass verify in sockopts for initial ssl:connect. Port 587: do not (gen_tcp rejects it).
|
||||
smtp_opts =
|
||||
[
|
||||
adapter: Swoosh.Adapters.SMTP,
|
||||
relay: String.trim(smtp_host_env),
|
||||
port: smtp_port_env,
|
||||
username: System.get_env("SMTP_USERNAME"),
|
||||
password: smtp_password_env,
|
||||
ssl: smtp_ssl_mode == "ssl",
|
||||
tls: if(smtp_ssl_mode == "tls", do: :always, else: :never),
|
||||
auth: :always,
|
||||
# tls_options: STARTTLS (587); sockopts: direct SSL (465).
|
||||
tls_options: [verify: verify_mode],
|
||||
sockopts: [verify: verify_mode]
|
||||
]
|
||||
if smtp_ssl_mode == "ssl" do
|
||||
Keyword.put(base_smtp_opts, :sockopts, verify: verify_mode)
|
||||
else
|
||||
base_smtp_opts
|
||||
end
|
||||
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
||||
|
||||
config :mv, Mv.Mailer, smtp_opts
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ By default, TLS certificate verification is relaxed (`verify_none`) so self-sign
|
|||
- **ENV (prod):** Set `SMTP_VERIFY_PEER=true` (or `1`/`yes`) when configuring SMTP via environment variables in `config/runtime.exs`. This sets `config :mv, :smtp_verify_peer` and is used for both boot-time and per-send config.
|
||||
- **Default:** `false` (verify_none) for backward compatibility and internal/self-signed certs.
|
||||
|
||||
Both `tls_options` (STARTTLS, port 587) and `sockopts` (direct SSL, port 465) use the same verify mode. The logic is duplicated in `config/runtime.exs` (boot) and `Mv.Mailer.smtp_config/0` (Settings-only); keep in sync.
|
||||
Verify mode is set in `tls_options` for port 587 (STARTTLS). For port 465 (implicit SSL), the initial connection is `ssl:connect`, so we also pass `sockopts: [verify: verify_mode]` so the SSL handshake uses the same mode. For 587 we must not pass `verify` in sockopts—gen_tcp is used first and rejects it (ArgumentError). The logic is in `config/runtime.exs` (boot) and `Mv.Mailer.smtp_config/0` (Settings-only); keep in sync.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -117,7 +117,7 @@ Both `tls_options` (STARTTLS, port 587) and `sockopts` (direct SSL, port 465) us
|
|||
- [x] Password from file: `SMTP_PASSWORD_FILE` supported in `runtime.exs`.
|
||||
- [x] Mailer: Swoosh SMTP adapter configured from merged ENV + Settings when SMTP is configured.
|
||||
- [x] Per-request SMTP config via `Mv.Mailer.smtp_config/0` for Settings-only scenarios.
|
||||
- [x] TLS certificate validation relaxed for OTP 27 (tls_options + sockopts).
|
||||
- [x] TLS certificate validation relaxed for OTP 27 (tls_options for 587; sockopts with verify only for 465).
|
||||
- [x] Prod warning: clear message in Settings when SMTP is not configured.
|
||||
- [x] Test email: form with recipient field, translatable content, classified success/error messages.
|
||||
- [x] Join confirmation email: uses `Mailer.smtp_config/0` (same as test mail); on failure returns `{:error, :email_delivery_failed}`, error shown in JoinLive, logged for admin.
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ defmodule Mv.Mailer do
|
|||
do: :verify_peer,
|
||||
else: :verify_none
|
||||
|
||||
[
|
||||
base_opts = [
|
||||
adapter: Swoosh.Adapters.SMTP,
|
||||
relay: host,
|
||||
port: port,
|
||||
|
|
@ -120,11 +120,20 @@ defmodule Mv.Mailer do
|
|||
auth: :always,
|
||||
username: username,
|
||||
password: password,
|
||||
# tls_options: STARTTLS (587); sockopts: direct SSL (465). Verify from :smtp_verify_peer (ENV SMTP_VERIFY_PEER).
|
||||
tls_options: [verify: verify_mode],
|
||||
sockopts: [verify: verify_mode]
|
||||
# tls_options: used for STARTTLS (587). For 465, gen_smtp uses sockopts for initial ssl:connect.
|
||||
tls_options: [verify: verify_mode]
|
||||
]
|
||||
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
||||
|
||||
# Port 465: initial connection is ssl:connect; pass verify in sockopts so server cert is not required.
|
||||
# Port 587: initial connection is gen_tcp; sockopts must NOT contain verify (gen_tcp rejects it).
|
||||
opts =
|
||||
if ssl_mode == "ssl" do
|
||||
Keyword.put(base_opts, :sockopts, verify: verify_mode)
|
||||
else
|
||||
base_opts
|
||||
end
|
||||
|
||||
opts |> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
|
|
|||
89
test/mv/mailer_smtp_config_test.exs
Normal file
89
test/mv/mailer_smtp_config_test.exs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
defmodule Mv.MailerSmtpConfigTest do
|
||||
@moduledoc """
|
||||
Unit tests for Mv.Mailer.smtp_config/0.
|
||||
|
||||
Ensures both port 587 (STARTTLS) and 465 (implicit SSL) work:
|
||||
- 587: sockopts must NOT contain :verify (gen_tcp:connect would raise ArgumentError).
|
||||
- 465: sockopts MUST contain :verify so initial ssl:connect uses verify_none/verify_peer.
|
||||
Uses ENV to drive config; async: false.
|
||||
"""
|
||||
use Mv.DataCase, async: false
|
||||
|
||||
alias Mv.Mailer
|
||||
|
||||
defp set_smtp_env(key, value), do: System.put_env(key, value)
|
||||
|
||||
defp clear_smtp_env do
|
||||
System.delete_env("SMTP_HOST")
|
||||
System.delete_env("SMTP_PORT")
|
||||
System.delete_env("SMTP_USERNAME")
|
||||
System.delete_env("SMTP_PASSWORD")
|
||||
System.delete_env("SMTP_PASSWORD_FILE")
|
||||
System.delete_env("SMTP_SSL")
|
||||
end
|
||||
|
||||
describe "smtp_config/0" do
|
||||
test "port 587 (TLS): does not include :verify in sockopts so gen_tcp:connect does not crash" do
|
||||
set_smtp_env("SMTP_HOST", "smtp.example.com")
|
||||
set_smtp_env("SMTP_PORT", "587")
|
||||
set_smtp_env("SMTP_SSL", "tls")
|
||||
|
||||
config = Mailer.smtp_config()
|
||||
|
||||
assert config != [], "expected non-empty config when SMTP_HOST is set"
|
||||
|
||||
sockopts = Keyword.get(config, :sockopts, [])
|
||||
|
||||
refute Keyword.has_key?(sockopts, :verify),
|
||||
"for 587 gen_tcp is used first; sockopts must not contain :verify"
|
||||
after
|
||||
clear_smtp_env()
|
||||
end
|
||||
|
||||
test "port 465 (SSL): includes :verify in sockopts so initial ssl:connect accepts verify mode" do
|
||||
set_smtp_env("SMTP_HOST", "smtp.example.com")
|
||||
set_smtp_env("SMTP_PORT", "465")
|
||||
set_smtp_env("SMTP_SSL", "ssl")
|
||||
|
||||
config = Mailer.smtp_config()
|
||||
|
||||
assert config != []
|
||||
sockopts = Keyword.get(config, :sockopts, [])
|
||||
|
||||
assert Keyword.has_key?(sockopts, :verify),
|
||||
"for 465 initial connection is ssl:connect; sockopts must contain :verify"
|
||||
|
||||
assert Keyword.get(sockopts, :verify) in [:verify_none, :verify_peer]
|
||||
after
|
||||
clear_smtp_env()
|
||||
end
|
||||
|
||||
test "builds TLS mode for port 587 (STARTTLS)" do
|
||||
set_smtp_env("SMTP_HOST", "smtp.example.com")
|
||||
set_smtp_env("SMTP_PORT", "587")
|
||||
set_smtp_env("SMTP_SSL", "tls")
|
||||
|
||||
config = Mailer.smtp_config()
|
||||
|
||||
assert config != []
|
||||
assert Keyword.get(config, :tls) == :always
|
||||
assert Keyword.get(config, :ssl) == false
|
||||
after
|
||||
clear_smtp_env()
|
||||
end
|
||||
|
||||
test "builds SSL mode for port 465 (implicit SSL)" do
|
||||
set_smtp_env("SMTP_HOST", "smtp.example.com")
|
||||
set_smtp_env("SMTP_PORT", "465")
|
||||
set_smtp_env("SMTP_SSL", "ssl")
|
||||
|
||||
config = Mailer.smtp_config()
|
||||
|
||||
assert config != []
|
||||
assert Keyword.get(config, :ssl) == true
|
||||
assert Keyword.get(config, :tls) == :never
|
||||
after
|
||||
clear_smtp_env()
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue