Refs #207 — the first two of the three tiers the reporter laid out. Tier three is deliberately absent; see the bottom.
A school wanting "warn about outside senders" on for three thousand pupils cannot ask three thousand pupils. The reporter is right that this is a company policy rather than a preference.
Two powers
defaults seed an account that has never had settings of its own. The reader can change any of them afterwards — 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 exactly that: a control that is simply missing reads as a bug to somebody who has used ihasmail without a policy.
The lock is in the store, not on the controls
There is one door — update — and putting the lock there means these are covered by construction rather than by remembering:
an imported settings file (Settings → Import),
a settings file synced from a device that signed in before the policy existed,
reset, which goes back to the installation's answer rather than ihasmail's,
and any control someone adds later and forgets to check.
The controls are disabled too, because that is what the reader sees — but the store is what makes it true.
Configuration
-e SETTINGS_DEFAULTS='{"externalSenderBanner":true}'\
-e SETTINGS_ENFORCED='{"externalRecipientConfirm":true}'# or
-e SETTINGS_POLICY_FILE=/etc/ihasmail/policy.json
Both forms, because ihasmail's own production runs read-only with no volume — an installation that cannot mount a file can still set a variable. Same names and values a settings export uses, so exporting a configured account is the quickest way to write one.
Two deliberate strictnesses: keys this build does not have are dropped (the same rule importJson already applies — a policy written against a newer ihasmail must not put a setting nothing reads into everyone's synced settings file), and malformed JSON stops the server at startup rather than quietly doing nothing, since a policy that silently did not apply is indistinguishable from the feature not working.
Tests
Twelve: unknown keys dropped from both sections, defaults seeding only an account with none, defaults still being changeable afterwards, and enforcement surviving an update, a partial update alongside free settings, a hydrate from another device, a reset, and an import.
npm run typecheck, npm test (968 web + 122 server), npm run build, i18n:check (no new stale keys) pass. One new string in all nine catalogues.
Not in this PR
Tier three — enforcing a setting once while still letting readers change it afterwards. It needs a decision that hasn't been made yet, and it is the only tier that stores anything new. Being put to the reporter on #207.
Refs #207 — the first two of the three tiers the reporter laid out. Tier three is deliberately absent; see the bottom.
A school wanting "warn about outside senders" on for three thousand pupils cannot ask three thousand pupils. The reporter is right that this is a company policy rather than a preference.
## Two powers
- **`defaults`** seed an account that has never had settings of its own. The reader can change any of them afterwards — 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 exactly that: a control that is simply missing reads as a bug to somebody who has used ihasmail without a policy.
## The lock is in the store, not on the controls
There is one door — `update` — and putting the lock there means these are covered by construction rather than by remembering:
- an imported settings file (`Settings → Import`),
- a settings file synced from a device that signed in before the policy existed,
- `reset`, which goes back to the *installation's* answer rather than ihasmail's,
- and any control someone adds later and forgets to check.
The controls are disabled too, because that is what the reader sees — but the store is what makes it true.
## Configuration
```bash
-e SETTINGS_DEFAULTS='{"externalSenderBanner":true}' \
-e SETTINGS_ENFORCED='{"externalRecipientConfirm":true}'
# or
-e SETTINGS_POLICY_FILE=/etc/ihasmail/policy.json
```
Both forms, because ihasmail's own production runs read-only with no volume — an installation that cannot mount a file can still set a variable. Same names and values a settings export uses, so exporting a configured account is the quickest way to write one.
Two deliberate strictnesses: **keys this build does not have are dropped** (the same rule `importJson` already applies — a policy written against a newer ihasmail must not put a setting nothing reads into everyone's synced settings file), and **malformed JSON stops the server at startup** rather than quietly doing nothing, since a policy that silently did not apply is indistinguishable from the feature not working.
## Tests
Twelve: unknown keys dropped from both sections, defaults seeding only an account with none, defaults still being changeable afterwards, and enforcement surviving an update, a partial update alongside free settings, a hydrate from another device, a reset, and an import.
`npm run typecheck`, `npm test` (968 web + 122 server), `npm run build`, `i18n:check` (no new stale keys) pass. One new string in all nine catalogues.
## Not in this PR
**Tier three** — enforcing a setting once while still letting readers change it afterwards. It needs a decision that hasn't been made yet, and it is the only tier that stores anything new. Being put to the reporter on #207.
**Merged** 2026-09-02 as coffey-labs/ihasmail@6821d6a93ffc
<sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Refs #207 — the first two of the three tiers the reporter laid out. Tier three is deliberately absent; see the bottom.
A school wanting "warn about outside senders" on for three thousand pupils cannot ask three thousand pupils. The reporter is right that this is a company policy rather than a preference.
Two powers
defaultsseed an account that has never had settings of its own. The reader can change any of them afterwards — a starting point, not a rule.enforcedare 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 exactly that: a control that is simply missing reads as a bug to somebody who has used ihasmail without a policy.
The lock is in the store, not on the controls
There is one door —
update— and putting the lock there means these are covered by construction rather than by remembering:Settings → Import),reset, which goes back to the installation's answer rather than ihasmail's,The controls are disabled too, because that is what the reader sees — but the store is what makes it true.
Configuration
Both forms, because ihasmail's own production runs read-only with no volume — an installation that cannot mount a file can still set a variable. Same names and values a settings export uses, so exporting a configured account is the quickest way to write one.
Two deliberate strictnesses: keys this build does not have are dropped (the same rule
importJsonalready applies — a policy written against a newer ihasmail must not put a setting nothing reads into everyone's synced settings file), and malformed JSON stops the server at startup rather than quietly doing nothing, since a policy that silently did not apply is indistinguishable from the feature not working.Tests
Twelve: unknown keys dropped from both sections, defaults seeding only an account with none, defaults still being changeable afterwards, and enforcement surviving an update, a partial update alongside free settings, a hydrate from another device, a reset, and an import.
npm run typecheck,npm test(968 web + 122 server),npm run build,i18n:check(no new stale keys) pass. One new string in all nine catalogues.Not in this PR
Tier three — enforcing a setting once while still letting readers change it afterwards. It needs a decision that hasn't been made yet, and it is the only tier that stores anything new. Being put to the reporter on #207.
Merged 2026-09-02 as coffey-labs/ihasmail@6821d6a93f
Rebuilt from: git history, session transcript.