nauthera-server sends a small set of transactional messages through an SMTP relay you provide. Without a relay it sends nothing, and the features that need email quietly do without it.
| Message | Sent when | Links to |
|---|---|---|
| Password reset | A user asks for one on /forgot-password | /reset-password |
| Address verification | POST /v1/auth/verify-email/resend is called for an unverified address | /verify-email |
| New-sign-in alert | A user signs in from a browser or network they have not used before | /forgot-password |
| Relay test | An administrator sends one from the dashboard's email page | — |
The flows behind the first three are described in Sign-in Experience. Every link points at the organization's own issuer.
Relays
Each organization's email goes through one relay, chosen in this order:
- the organization's own relay, from
mail.organizations, by organization slug; - otherwise the deployment's relay,
mail.smtp; - otherwise none: the organization cannot send email.
Relays are configuration only. They are not stored in the database, and neither the admin API, the dashboard nor the operator can create or change one.
The deployment's relay
mail:
smtp:
host: smtp.example.com # empty = no deployment relay (the default)
port: 587
tls: starttls # starttls | implicit | none
allow_plaintext_auth: false
username: nauthera
password: "" # secret: use NAUTHERA_SERVER_MAIL_SMTP_PASSWORD_FILE
from:
address: no-reply@example.com # required when a host is set
name: Nauthera
reply_to: "" # optional
local_name: "" # EHLO name; empty sends "localhost"
timeout: 30s # per stage: dial, TLS handshake, each commandSet the username and password together, or neither for a relay that authorizes by
network. mail.timeout applies to organizations' relays too. Relays may be in-cluster
addresses. With the Helm chart, set these keys under config.mail and supply the
password through secrets.smtpPassword (an inline value, or existingSecret and
existingSecretKey).
Per-organization relays
An organization listed in mail.organizations sends through its own relay, as its own
sender. Its credentials are never configuration: smtp.username_file and
smtp.password_file name files the server reads at startup, where a missing file stops
it, and again at every delivery, so a rotated Secret takes effect without a restart.
With the Helm chart, use mailOrganizations. The chart mounts each entry's Secret under
secrets.mountPath/smtp/<slug>/ and renders mail.organizations pointing at those
files, so only file paths reach the ConfigMap:
mailOrganizations:
- slug: acme
smtp:
host: smtp.acme.example
port: 587
tls: starttls # starttls | implicit | none
allowPlaintextAuth: false # only with tls: none and credentials
from:
address: no-reply@acme.example
name: Acme
replyTo: support@acme.example
credentials: # omit for a relay without AUTH
existingSecret: acme-smtp
usernameKey: username # default
passwordKey: password # defaultTLS
Every relay connection is encrypted unless you explicitly say otherwise:
starttls(port 587, RFC 3207) upgrades the connection before anything butEHLOis sent. A relay that does not offer STARTTLS is refused, never used in the clear.implicit(port 465, RFC 8314) opens TLS before the SMTP greeting.
Neither falls back to plaintext. The relay's certificate is verified against the configured host, and TLS 1.2 is the minimum.
none is the explicit exception, for a relay that offers no TLS. Messages, reset links
included, then cross the network unencrypted. Credentials are sent over it only with
allow_plaintext_auth: true (in the chart, smtp.allowPlaintextAuth); without it, a
none relay with credentials is a startup error. The server logs a warning for every
none relay at each startup. allow_plaintext_auth with any other mode is refused.
The outbox
Nothing touches SMTP while a user waits. A message is rendered, sealed under the
key-encryption key that also seals organization signing keys (keys.encryption_key),
and written to the database. A reset or verification request is answered first and the
message queued after the response.
Every replica runs a delivery worker. A worker claims due messages with an expiring lease, so two replicas never hold the same message, and a message held by a replica that stopped is claimed again when its lease runs out. Messages for one organization share an SMTP session.
- Retries. A temporary failure (a
4xxreply, a timeout, a dropped connection) is retried on an exponential schedule with jitter, starting at 30 seconds and capped at 2 hours. A permanent failure (a5xxreply, a refused certificate, a relay without TLS) or a tenth failed attempt fails the message for good. - Duplicates. Delivery is once, except when a replica hands a message to the relay
and stops before it records the send. The message keeps its
Message-IDacross attempts, so a relay or mail client can tell the copies apart. - Retention. A message's body is dropped once it is sent or has failed for good, and the remaining row is deleted after seven days.
Each delivery attempt is counted in nauthera_mail_deliveries_total, labelled result
sent, retried or failed. See Observability.
Branding and languages
Every message is one layout, sent as both HTML and plain text. It carries the
organization's display name (its slug when it has none) and, from the organization's
dashboard theme, its logo when that is an https URL and its accent colour when that is a
#rrggbb value. Per-client sign-in branding does
not apply to email.
Messages are translated into English, German and French, the sign-in app's languages.
The language is the first supported one in the Accept-Language header of the request
that caused the message, or English. A test message uses the dashboard's language.
The dashboard's email page
The admin dashboard shows the organization's relay, read
only: whether it is the organization's own or the deployment's, its host, port, TLS mode
and sender, and whether credentials are configured, never the credentials themselves. An
organization without a relay sees how to configure one. The same is available from
GET /v1/admin/orgs/{slug}/mail.
Send a test message sends the relay test to the signed-in administrator's own
address, never to an address the request names. It is sent at once, not queued, and the
page reports what the relay answered: delivered, or the stage that failed, the SMTP reply
code and whether retrying could help. It is POST /v1/admin/orgs/{slug}/mail/test, needs
nauthera.orgs:write, and is recorded as organization.mail.tested. It answers 409
when the organization has no relay, when the relay's credentials cannot be read, or when
the administrator's account has no address.
Where a sign-in came from
An optional city database in the MaxMind DB format (GeoLite2-City or GeoIP2-City) lets a new-sign-in alert name the city and country of the sign-in's address, in the alert's language when the database has it. Without one, the alert shows the address alone.
geoip:
database: /var/lib/geoip/GeoLite2-City.mmdb # empty = no location (the default)The file is read at the first lookup and again whenever it changes, so refreshing it
needs no restart. Replace it by renaming the new file into place, as geoipupdate does,
not by rewriting it. A missing or unreadable file is logged and locates nothing; it never
fails a sign-in.
With the Helm chart, set geoip.volume to the volume that holds the file (for example a
PersistentVolumeClaim geoipupdate writes to) and geoip.fileName to its name. The chart
mounts it read-only at geoip.mountPath (/etc/nauthera/geoip) and renders
geoip.database.
geoip:
volume:
persistentVolumeClaim:
claimName: geoip
fileName: GeoLite2-City.mmdb