§08 · Deploy & operate

Email · SMTP & Mailgun

Configure outbound email so password resets and invitations actually send. Pick SMTP or Mailgun; without one, both flows fail silently.

Exo sends two kinds of transactional email: self-serve password resets and team invitations. The backend still boots with no email provider configured, but both flows then go nowhere. The user sees no error and no message arrives. Configure a provider before you invite anyone or rely on password login.

What email is used for

  • Password reset. The forgot-password flow emails a reset link that expires in 1 hour. Accounts on an external identity provider get an explainer instead, telling them to sign in through their IdP.
  • Invitations. Inviting a user by email sends an accept link that expires in 7 days.

Choosing a provider

Exo picks a provider at boot, in order. SMTP wins if it is configured; otherwise Mailgun; otherwise email is off.

  1. SMTP, if EXO_SMTP_HOST and EXO_SMTP_FROM are set and EXO_SMTP_PORT is a valid port.
  2. Mailgun, if EXO_MAILGUN_API_KEY and EXO_MAILGUN_DOMAIN are set.
  3. Disabled, if neither is. The backend logs a warning and the email flows no-op.

The startup log tells you which one is live:

backend startup log· text
1mail: provider=smtp
2mail: provider=mailgun domain=mg.example.com
3WARNING: EXO_SMTP_* not configured ... self-serve password reset emails disabled until EXO_SMTP_HOST and EXO_SMTP_FROM are set

SMTP

Point Exo at any SMTP server. Set the host, a from address, and a port, and add credentials if your relay needs them.

VariableRequiredDefaultPurposeExample
EXO_SMTP_HOSTto enable(none)SMTP server hostnamesmtp.acme.com
EXO_SMTP_FROMto enable(none)From address (see "From address" below)Exo <no-reply@acme.com>
EXO_SMTP_PORTno587must be > 0; an invalid value disables SMTP587
EXO_SMTP_USERNAMEno(none)sets PLAIN auth when presentapikey
EXO_SMTP_PASSWORDno(none)paired with the usernameSG.…
EXO_SMTP_TLSnostarttlsstarttls | tls | nonestarttls

EXO_SMTP_TLS takes one of three values: starttls (the default, the usual choice on port 587), tls for implicit TLS (typically port 465), or none for an unencrypted connection, which is for local testing only. Leave the username blank to connect without auth, for a relay that gates on IP instead.

terminal· bash
kubectl -n exo create secret generic exo-smtp \
--from-literal=EXO_SMTP_HOST="smtp.example.com" \
--from-literal=EXO_SMTP_PORT="587" \
--from-literal=EXO_SMTP_FROM="Exo <no-reply@example.com>" \
--from-literal=EXO_SMTP_USERNAME="apikey" \
--from-literal=EXO_SMTP_PASSWORD="..." \
--from-literal=EXO_SMTP_TLS="starttls"

Mailgun

If SMTP is not set, Exo falls back to Mailgun. It sends over Mailgun's HTTP API, so you do not need outbound SMTP ports open. Set your API key and sending domain.

VariableRequiredDefaultPurposeExample
EXO_MAILGUN_API_KEYto enable(none)Mailgun private API keykey-3ax6…
EXO_MAILGUN_DOMAINyesexo.mailer.adaptive.devyour Mailgun sending domainmg.acme.com
EXO_MAILGUN_BASE_URLnohttps://api.mailgun.netuse https://api.eu.mailgun.net for the EU regionhttps://api.mailgun.net
EXO_MAILGUN_FROMnoExo <no-reply@DOMAIN>From addressExo <no-reply@mg.acme.com>

Links in reset and invite emails are built from EXO_BASE_URL. Set it to your public URL or the links will point at localhost. Behind a proxy, see networking.

For the from address, a bare address like no-reply@example.com gets Exo as its display name so inboxes show the product. Supply your own Name <addr> to override it. The Mailgun default already includes a name.

Verify delivery

After you deploy, confirm the startup log shows the provider you expect (mail: provider=smtp or provider=mailgun, not the warning). Then run a real flow: invite a test user, or use forgot-password on a local-auth account, and confirm the message arrives. Reset links last 1 hour; invites last 7 days.