Commit Graph
3 Commits
Author SHA1 Message Date
jcoffey-dev 4787e8bf12 Point from the dashboard to Stalwart's own administration
A line under the cards says where the rest is: detailed metrics, the
delivery queue, logs and server settings are in Stalwart's own
administration. It links there when the operator sets STALWART_ADMIN_URL,
and stays plain text otherwise, because STALWART_URL is how this server
reaches Stalwart and is often an address no browser can open.

Several servers: a servers file entry may now be an object,
{"url": ..., "adminUrl": ...}, and a session routed to that server gets its
adminUrl. A routed domain without one gets no link rather than the default
server's, for the same reason routing never falls back. The URL is sent
only to a session that may administer.

The shipped example file stopped the server at startup: its "_comment"
key was read as a domain and refused as not a URL, while the test that
checks the example skipped it. Keys starting with an underscore are notes
now -- no mail domain starts with one -- and the example is also loaded
through the real parser in a test, so the two cannot disagree again.

Two new strings, in all nine catalogues.
2026-09-15 08:16:52 -07:00
jcoffey-dev 171c11fc92 Choose the Stalwart by the domain somebody signs in with
One ihasmail in front of several Stalwarts, from #238. STALWART_URL stays
required and stays the default, so an installation that sets nothing behaves
exactly as it always has -- the mapping only adds domains that go elsewhere.

An unlisted domain goes to the default. So does a bare username, which
Stalwart accepts and which has no domain to map at all.

A listed domain never falls back. If its server is unreachable that sign-in
fails rather than retrying against the default, because falling back would
authenticate somebody against a server their domain was deliberately routed
away from -- and if the same account name existed there, they would land in
another tenant's mailbox. The fallback is a decision about unmapped domains,
taken before any network call, not a recovery path.

Smaller than it sounds because only four places read config.stalwartUrl, all
in upstream.ts. The upstream session now records which server issued it, since
the relative URLs inside it only mean anything against that server, and every
route already holding a session gets the right upstream without a second
lookup. The client is untouched: it talks to one proxy and never learns there
is more than one server behind it, which is exactly why this is small and
several-servers-at-once is not.

The upstream is derived from the username rather than stored on the session,
so a mapping change takes effect on restart instead of being frozen into
sessions that outlive it.

Validated at boot the way the settings policy is: malformed JSON, a duplicate
domain once normalised, a missing file or a value that is not an http(s) URL
all stop the server. Domains are lower-cased and stripped of a trailing dot,
because that is how one arrives off a username and comparing them any other
way means a mapping that silently never matches. The servers themselves are
not contacted -- a mapping is a routing table, not a health check, and one
customer's outage must not stop ihasmail starting for the other four.

Eight tests on the routing, two on the shipped example, and the four refusals
checked by hand against a real config load.
2026-09-02 14:18:50 -07:00
jcoffey-dev a24b4c5538 Ship an example settings policy, and name the variables in .env.example
#231 added the policy but nothing to copy. The repo already answers this the
same way four times over -- Caddyfile.example, deploy.example.sh,
nginx.example.conf, .env.example -- and the new feature was the one thing
configurable here with no example beside it.

settings-policy.example.json carries all three sections with the reasoning in
it, including the part worth being deliberate about: a `changes` entry
overrides a decision a reader has already made, and if you want it to stay put
regardless that is `enforced` instead. JSON has no comments, so the commentary
is in `_`-prefixed keys, which is safe because the server reads three names and
ignores everything else.

A test asserts the shipped example stays valid against the rules the parser
enforces -- unique versions, settings objects, no comment key colliding with a
real section. An example that has drifted is worse than none: somebody copies
it, the server refuses to start, and the first experience of the feature is a
crash loop.

.env.example gains the four variables, commented out, with the file form and
the inline form and the note that the file wins over the variables.

Confirmed against the real image on the deploy host rather than reasoned
about: an immutable container -- --read-only, IMMUTABLE=1, SESSION_FILE=
empty -- starts and serves the policy both with a read-only file mount and
with the environment variables alone. The feature costs nothing in
immutability, because the only thing it writes is the applied-changes stamp,
and that goes in the reader's own settings file on Stalwart like every other
setting.
2026-09-02 11:20:34 -07:00