PhoenixKit.Mailer.SmtpTransport (phoenix_kit v1.7.218)

Copy Markdown View Source

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:

  • securityauto (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_certverify_peer (default) | verify_none.

  • ca_cert — PEM bundle for a private/self-signed CA. Replaces the system store for this connection.
  • authif_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_availablealways 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.

Summary

Functions

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

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

Functions

config(creds)

@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(creds, cacerts)

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