# `PhoenixKit.Mailer.SmtpTransport`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.14.0/lib/phoenix_kit/mailer/smtp_transport.ex#L1)

Builds the gen_smtp/Swoosh connection options for an SMTP integration.

Extracted so that *sending* and *"Test Connection"* are driven by literally the
same options — a check that connects differently from the sender is a check
that can lie in either direction. It is a pure function of the credentials map
and depends on nothing else in the tree (in particular not on
`PhoenixKit.Integrations`, which would otherwise close a
Integrations → Validators → Mailer → Integrations cycle).

## Operator settings

Everything below is derived from the `smtp` provider's setup fields. All of
them are optional; left blank, the transport behaves exactly as it did when
the port was the only signal:

  * `security` — `auto` (default) | `ssl` | `starttls` | `starttls_optional` |
    `none`. `auto` reproduces the historical rule: port 465 means implicit
    TLS, anything else means STARTTLS — required when credentials are present,
    opportunistic when they are not.
  * `verify_cert` — `verify_peer` (default) | `verify_none`.
  * `ca_cert` — PEM bundle for a private/self-signed CA. Replaces the system
    store for this connection.
  * `auth` — `if_available` (default) | `always` | `never`. Carried in the
    built options, so the Test Connection probe reads the operator's choice
    instead of guessing (it still upgrades `if_available` → `always` for the
    probe only — see `PhoenixKit.Integrations.Validators.smtp/1` — because a
    tolerated AUTH failure would let a wrong password pass the check).
  * `timeout` — seconds; maps to gen_smtp's `:timeout`.

## TLS

gen_smtp supplies **no** TLS options of its own, and OTP's `:ssl` now defaults
to `verify: :verify_peer` with no CA store. Left alone, that means:

  * implicit TLS (465, `ssl: true`) dies on connect with
    `{:options, :incompatible, [verify: :verify_peer, cacerts: :undefined]}`;
  * STARTTLS (`tls: :always`) fails the handshake with `:tls_failed`.

So the options below are load-bearing, not decoration. They ride on `sockopts`
for implicit TLS (gen_smtp hands those straight to `:ssl.connect/4`) and on
`tls_options` for STARTTLS.

Note that passing `tls_options` *replaces* gen_smtp's default
`[{versions, ['tlsv1', 'tlsv1.1', 'tlsv1.2']}]` wholesale (it merges with
`lists:ukeymerge/3`). That is deliberate: we take OTP's defaults, which drop
the long-dead TLS 1.0/1.1 and allow TLS 1.3.

## Certificate verification is not optional when credentials are on the wire

If no CA store can be found we refuse to build a config for a relay that
expects a password (`{:error, :no_ca_store}`) rather than silently falling back
to `verify: :verify_none` — an unauthenticated TLS peer can present any
certificate, terminate the connection and harvest the AUTH exchange. A relay
that takes no credentials has nothing to protect, so it degrades instead.

An operator who *chooses* `verify_none` (or `security: none`) gets what they
asked for: the point of those settings is the internal relay with a
self-signed certificate, or none at all. They are never reached by `auto`.

# `config`

```elixir
@spec config(map()) :: {:ok, keyword()} | {:error, term()}
```

Returns `{:ok, options}` for gen_smtp/`Swoosh.Adapters.SMTP`, or `{:error, reason}`.

Reasons: `{:invalid_smtp_port, term}`, `{:invalid_security, term}`,
`{:invalid_verify_cert, term}`, `{:invalid_auth, term}`,
`{:invalid_timeout, term}`, `:invalid_ca_cert`, `:no_ca_store`.

# `config`

```elixir
@spec config(map(), [binary()] | [tuple()]) :: {:ok, keyword()} | {:error, term()}
```

Same as `config/1`, with the trusted CA store supplied explicitly.

The options are a pure function of the credentials and the CA store; `config/1`
simply reads the store from the system. Passing it in makes the fail-closed
branch — no CA store, credentials on the wire — reachable from a test.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
