Rebuilt from: notification email, session transcript. Opening date is approximate.
*The original description was not recovered.*
**Referenced by commits**
- coffey-labs/ihasmail@c31a653a0491 2026-09-02 Apply installation policy changes once each, per account
- coffey-labs/ihasmail@457ea53ca3d6 2026-09-02 Let an installation seed and lock user settings
<sub>Rebuilt from: notification email, session transcript. Opening date is approximate.</sub>
let me investigate, we may be able to feed this during container or npm start via a variable file. The changes coming out right now are part of a larger major feature push to catch up with other projects also. While still maintaining our stateless and immutable nature. It's fun finding the workarounds to maintain those requirements.
Date not recovered.
let me investigate, we may be able to feed this during container or npm start via a variable file. The changes coming out right now are part of a larger major feature push to catch up with other projects also. While still maintaining our stateless and immutable nature. It's fun finding the workarounds to maintain those requirements.
<sub>Date not recovered.</sub>
Are you deploying stalwart as a single instance per client, or are you using the enterprise license with multi-tenancy?
Definitely one Stalwart instance per client, not a multi-tenancy setup.
> Are you deploying stalwart as a single instance per client, or are you using the enterprise license with multi-tenancy?
Definitely one Stalwart instance per client, not a multi-tenancy setup.
We have an enterprise license, too, but that's mostly for testing purposes as the pricing is so cheap for a company of our size. We don't really require the features unlocked by it.
We have an enterprise license, too, but that's mostly for testing purposes as the pricing is so cheap for a company of our size. We don't really require the features unlocked by it.
All three of your sections are built and live as 2026.9.2+pr232. #230 for the first two, #231 for the third, #232 for the example file. Documented at docs.ihasmail.org/configure.
Your three-way split survived intact, because it was the right split:
Section
Applies to
Reader can change it
defaults
accounts that have never had settings of their own
yes, at any time
enforced
everyone, on every load
no — the control goes dead
changes
everyone, once each, existing accounts included
yes, afterwards, and it stays changed
There's a commented settings-policy.example.json in the repo to copy — every section explained in the file. Point at it with SETTINGS_POLICY_FILE=/etc/ihasmail/policy.json and mount it :ro, or set SETTINGS_DEFAULTS / SETTINGS_ENFORCED / SETTINGS_CHANGES inline where mounting is awkward. The file wins if both are present, so a file and a stray variable cannot half-apply between them. It is read once at startup — editing means restarting the container, deliberately no reload signal.
Writing one is easier than it looks. Every section takes the same names and values a settings export uses, so configure one account by hand the way you want everybody to start, Settings → General → Export, and copy out the keys you care about.
On your third section, built as specified. Version ids rather than a high-water mark, so a change you backdate is applied rather than silently skipped. One correction to something I nearly told you: the stamp lives in each account's own settings file, which is on the mail server rather than in the browser — so "exactly once" is exactly once per person, not per device. Your migration analogy maps cleanly.
Enforced controls stay visible and go dead with a line saying why. The lock is in the settings store rather than only on the controls, so an imported settings file, a settings file synced from a device that signed in before your policy existed, and "reset to defaults" all cannot get around it. Reset returns to your defaults, not ihasmail's.
Immutability holds. You did not ask, but you might have: this costs nothing there. I ran it against the real image — --read-only --tmpfs /tmp -e IMMUTABLE=1 -e SESSION_FILE= — and it starts and serves the policy both with a read-only file mount and with the environment variables alone. The only thing the feature writes is that applied-changes stamp, and it goes into the reader's own settings file on Stalwart like every other setting. Nothing lands on ihasmail's disk.
Three checks fail loudly at startup rather than quietly: malformed JSON, a change with no unique version, and a change with no settings. Keys this build does not have are dropped, so a policy written against a newer ihasmail cannot put a dead setting into everyone's synced file — and a changes entry whose keys are all unknown is dropped whole rather than recorded as applied, so it still runs after you upgrade.
Two things I would rather you heard from me:
A changes entry does override a decision somebody has already made. You confirmed that is intended and it is built that way, but it is the one case where this power feels like enforcement to the person on the receiving end. It is called out in the docs as the thing to be deliberate about when choosing between the two.
Readers are told when it happens — a notice saying how many settings the administrator changed, with a link into Settings. Your idea of listing exactly which ones, clickable, is better and I have not done it. Say the word if it matters to you and I will follow up.
Thanks for the shape of the original request. If you had just asked for "admin defaults" I would have built something worse.
All three of your sections are built and live as `2026.9.2+pr232`. #230 for the first two, #231 for the third, #232 for the example file. Documented at [docs.ihasmail.org/configure](https://docs.ihasmail.org/configure/#settings-your-installation-decides).
Your three-way split survived intact, because it was the right split:
| Section | Applies to | Reader can change it |
| --- | --- | --- |
| `defaults` | accounts that have never had settings of their own | yes, at any time |
| `enforced` | everyone, on every load | no — the control goes dead |
| `changes` | everyone, **once each**, existing accounts included | yes, afterwards, and it stays changed |
There's a commented [`settings-policy.example.json`](https://github.com/Coffey-Labs/ihasmail/blob/main/settings-policy.example.json) in the repo to copy — every section explained in the file. Point at it with `SETTINGS_POLICY_FILE=/etc/ihasmail/policy.json` and mount it `:ro`, or set `SETTINGS_DEFAULTS` / `SETTINGS_ENFORCED` / `SETTINGS_CHANGES` inline where mounting is awkward. The file wins if both are present, so a file and a stray variable cannot half-apply between them. It is read once at startup — editing means restarting the container, deliberately no reload signal.
**Writing one is easier than it looks.** Every section takes the same names and values a settings export uses, so configure one account by hand the way you want everybody to start, `Settings → General → Export`, and copy out the keys you care about.
**On your third section**, built as specified. Version ids rather than a high-water mark, so a change you backdate is applied rather than silently skipped. One correction to something I nearly told you: the stamp lives in each account's own settings file, which is on the mail server rather than in the browser — so "exactly once" is exactly once per *person*, not per device. Your migration analogy maps cleanly.
Enforced controls stay visible and go dead with a line saying why. The lock is in the settings store rather than only on the controls, so an imported settings file, a settings file synced from a device that signed in before your policy existed, and "reset to defaults" all cannot get around it. Reset returns to *your* defaults, not ihasmail's.
**Immutability holds.** You did not ask, but you might have: this costs nothing there. I ran it against the real image — `--read-only --tmpfs /tmp -e IMMUTABLE=1 -e SESSION_FILE=` — and it starts and serves the policy both with a read-only file mount and with the environment variables alone. The only thing the feature writes is that applied-changes stamp, and it goes into the reader's own settings file on Stalwart like every other setting. Nothing lands on ihasmail's disk.
Three checks fail loudly at startup rather than quietly: malformed JSON, a change with no unique `version`, and a change with no `settings`. Keys this build does not have are dropped, so a policy written against a newer ihasmail cannot put a dead setting into everyone's synced file — and a `changes` entry whose keys are *all* unknown is dropped whole rather than recorded as applied, so it still runs after you upgrade.
Two things I would rather you heard from me:
**A `changes` entry does override a decision somebody has already made.** You confirmed that is intended and it is built that way, but it is the one case where this power feels like enforcement to the person on the receiving end. It is called out in the docs as the thing to be deliberate about when choosing between the two.
**Readers are told when it happens** — a notice saying how many settings the administrator changed, with a link into Settings. Your idea of listing exactly *which* ones, clickable, is better and I have not done it. Say the word if it matters to you and I will follow up.
Thanks for the shape of the original request. If you had just asked for "admin defaults" I would have built something worse.
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.
The original description was not recovered.
Referenced by commits
Rebuilt from: notification email, session transcript. Opening date is approximate.
let me investigate, we may be able to feed this during container or npm start via a variable file. The changes coming out right now are part of a larger major feature push to catch up with other projects also. While still maintaining our stateless and immutable nature. It's fun finding the workarounds to maintain those requirements.
Date not recovered.
Are you deploying stalwart as a single instance per client, or are you using the enterprise license with multi-tenancy?
Date not recovered.
also feel free to email me some of these suggestions if you want to noodle it out. [email protected]
Date not recovered.
Definitely one Stalwart instance per client, not a multi-tenancy setup.
that's great, i am using an enterprise instance, but scaled down multi-tenancy for now to work this stuff out.
Date not recovered.
We have an enterprise license, too, but that's mostly for testing purposes as the pricing is so cheap for a company of our size. We don't really require the features unlocked by it.
All three of your sections are built and live as
2026.9.2+pr232. #230 for the first two, #231 for the third, #232 for the example file. Documented at docs.ihasmail.org/configure.Your three-way split survived intact, because it was the right split:
defaultsenforcedchangesThere's a commented
settings-policy.example.jsonin the repo to copy — every section explained in the file. Point at it withSETTINGS_POLICY_FILE=/etc/ihasmail/policy.jsonand mount it:ro, or setSETTINGS_DEFAULTS/SETTINGS_ENFORCED/SETTINGS_CHANGESinline where mounting is awkward. The file wins if both are present, so a file and a stray variable cannot half-apply between them. It is read once at startup — editing means restarting the container, deliberately no reload signal.Writing one is easier than it looks. Every section takes the same names and values a settings export uses, so configure one account by hand the way you want everybody to start,
Settings → General → Export, and copy out the keys you care about.On your third section, built as specified. Version ids rather than a high-water mark, so a change you backdate is applied rather than silently skipped. One correction to something I nearly told you: the stamp lives in each account's own settings file, which is on the mail server rather than in the browser — so "exactly once" is exactly once per person, not per device. Your migration analogy maps cleanly.
Enforced controls stay visible and go dead with a line saying why. The lock is in the settings store rather than only on the controls, so an imported settings file, a settings file synced from a device that signed in before your policy existed, and "reset to defaults" all cannot get around it. Reset returns to your defaults, not ihasmail's.
Immutability holds. You did not ask, but you might have: this costs nothing there. I ran it against the real image —
--read-only --tmpfs /tmp -e IMMUTABLE=1 -e SESSION_FILE=— and it starts and serves the policy both with a read-only file mount and with the environment variables alone. The only thing the feature writes is that applied-changes stamp, and it goes into the reader's own settings file on Stalwart like every other setting. Nothing lands on ihasmail's disk.Three checks fail loudly at startup rather than quietly: malformed JSON, a change with no unique
version, and a change with nosettings. Keys this build does not have are dropped, so a policy written against a newer ihasmail cannot put a dead setting into everyone's synced file — and achangesentry whose keys are all unknown is dropped whole rather than recorded as applied, so it still runs after you upgrade.Two things I would rather you heard from me:
A
changesentry does override a decision somebody has already made. You confirmed that is intended and it is built that way, but it is the one case where this power feels like enforcement to the person on the receiving end. It is called out in the docs as the thing to be deliberate about when choosing between the two.Readers are told when it happens — a notice saying how many settings the administrator changed, with a link into Settings. Your idea of listing exactly which ones, clickable, is better and I have not done it. Say the word if it matters to you and I will follow up.
Thanks for the shape of the original request. If you had just asked for "admin defaults" I would have built something worse.