Let an installation seed and lock user settings

The first two thirds of #207. A school wanting "warn about outside senders"
on for three thousand pupils cannot ask three thousand pupils, and the
reporter is right that this is a company policy rather than a preference.

Two powers, and the difference between them is the whole request. `defaults`
seed an account that has never had settings of its own and can be changed
afterwards like anything else -- a starting point, not a rule. `enforced` are
reapplied on every load and cannot be changed at all.

Enforced controls stay visible and go dead, with a line saying why. The issue
asked for that by name: a control that is simply missing reads as a bug to
somebody who has used ihasmail without a policy.

The lock is in the settings store rather than only on the controls. There is
one door -- `update` -- and putting it there means an imported settings file,
a settings file synced from a device that predates the policy, and a control
somebody adds later and forgets to check are all covered by construction.
Reset goes back to the installation's answer rather than to ihasmail's, so it
cannot be a way around a policy either.

Configured by environment variable or by a file, because ihasmail's own
production runs read-only with no volume: an installation that cannot mount a
file can still set a variable. Keys this build does not have are dropped, the
same rule an imported settings file already gets -- a policy written against a
newer ihasmail must not put a setting nothing reads into everybody's synced
settings file. Malformed JSON stops the server rather than quietly doing
nothing, since a policy that silently did not apply is indistinguishable from
the feature not working.

Tier three -- enforcing a setting once while still letting readers change it
afterwards -- is not here. It needs a decision the reporter and I have not
made yet, and it is the only part that stores anything new.

Refs #207.
This commit is contained in:
2026-09-02 10:49:55 -07:00
parent a01e1874d8
commit 457ea53ca3
22 changed files with 396 additions and 45 deletions
+40
View File
@@ -117,6 +117,46 @@ nowhere to live across a restart. Removing it means moving the session upstream
into a token Stalwart itself issues and can revoke, which is what the OAuth work
in [ROADMAP.md](ROADMAP.md) is for.
### Settings the installation decides
A deployment can seed and lock user settings, which is what a school wanting
"warn about outside senders" on for three thousand pupils needs — asking three
thousand pupils is not a plan.
```bash
-e SETTINGS_DEFAULTS='{"externalSenderBanner":true}' \
-e SETTINGS_ENFORCED='{"externalRecipientConfirm":true}'
```
Or, where mounting a file is easier than quoting JSON in a unit file:
```bash
-e SETTINGS_POLICY_FILE=/etc/ihasmail/policy.json
```
```json
{
"defaults": { "externalSenderBanner": true },
"enforced": { "externalRecipientConfirm": true }
}
```
The two are different powers. **`defaults`** seed an account that has never had
settings of its own; the reader can change any of them afterwards, and they are
a starting point rather than a rule. **`enforced`** are reapplied on every load
and cannot be changed at all — their controls stay visible in Settings and go
dead with a line saying why, because a control that is simply missing reads as a
bug to anyone who has used ihasmail without a policy.
Both are given in the same names and values a settings export uses, so
`Settings → General → Export` on a configured account is the quickest way to
write one. Keys this build does not have are ignored rather than stored, and
malformed JSON stops the server at startup rather than silently doing nothing.
Enforcement is applied in the settings store rather than only on the controls,
so an imported settings file or a settings file synced from a device that
predates the policy cannot get around it.
## Architecture
```