# `PhoenixKit.Mailer.SmtpTransport`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.213/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).

## 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.

# `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}`, `: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*
