#231 added the settings 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 configurable thing here with no example beside it.
What's added
settings-policy.example.json at the repo root, with all three sections and the reasoning in the file. 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. That is safe because the server reads three names and ignores everything else — and there is a test asserting no comment key ever collides with a real section.
.env.example gains the four variables, commented out, with both forms and the note that the file wins over the inline ones.
A test guarding the example. It checks the shipped file against the rules the parser enforces: unique versions, settings objects, valid JSON, no stray top-level keys. An example that has drifted out of step is worse than no example — somebody copies it, the server refuses to start, and the first experience of the feature is a crash loop.
Immutability, confirmed rather than reasoned about
The question came up, so I ran it against the real image on the deploy host. An immutable container — --read-only --tmpfs /tmp -e IMMUTABLE=1 -e SESSION_FILE= — starts and serves the policy correctly in both forms:
with a read-only file mount (-v policy.json:/etc/ihasmail/policy.json:ro), and
with the environment variables alone and no mount at all.
The feature costs nothing in immutability. assertImmutable checks two things — SESSION_FILE empty and the install root not writable — and a read-only mount elsewhere touches neither. The only thing this feature writes is the applied-changes stamp, and that goes into the reader's own settings file on Stalwart, like every other setting.
One caveat that predates this and is unchanged by it: assertImmutable probes the install root only, so a policy file mounted read-write somewhere else would not trip it. Nothing previously gave a reason to mount anything; now something does, which is why both the README and the docs say :ro.
npm test (977 web + 126 server — four of those new), npm run build pass.
#231 added the settings 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 configurable thing here with no example beside it.
## What's added
**`settings-policy.example.json`** at the repo root, with all three sections and the reasoning in the file. 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. That is safe because the server reads three names and ignores everything else — and there is a test asserting no comment key ever collides with a real section.
**`.env.example`** gains the four variables, commented out, with both forms and the note that the file wins over the inline ones.
**A test guarding the example.** It checks the shipped file against the rules the parser enforces: unique versions, `settings` objects, valid JSON, no stray top-level keys. An example that has drifted out of step is worse than no example — somebody copies it, the server refuses to start, and the first experience of the feature is a crash loop.
## Immutability, confirmed rather than reasoned about
The question came up, so I ran it against the real image on the deploy host. An immutable container — `--read-only --tmpfs /tmp -e IMMUTABLE=1 -e SESSION_FILE=` — starts and serves the policy correctly in **both** forms:
- with a read-only file mount (`-v policy.json:/etc/ihasmail/policy.json:ro`), and
- with the environment variables alone and no mount at all.
The feature costs nothing in immutability. `assertImmutable` checks two things — `SESSION_FILE` empty and the install root not writable — and a read-only mount elsewhere touches neither. The only thing this feature writes is the applied-changes stamp, and that goes into the reader's own settings file on Stalwart, like every other setting.
One caveat that predates this and is unchanged by it: `assertImmutable` probes the install root only, so a policy file mounted read-*write* somewhere else would not trip it. Nothing previously gave a reason to mount anything; now something does, which is why both the README and the docs say `:ro`.
`npm test` (977 web + 126 server — four of those new), `npm run build` pass.
**Merged** 2026-09-02 as coffey-labs/ihasmail@0d5975d42756
<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.
#231 added the settings 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 configurable thing here with no example beside it.What's added
settings-policy.example.jsonat the repo root, with all three sections and the reasoning in the file. Including the part worth being deliberate about: achangesentry overrides a decision a reader has already made, and if you want it to stay put regardless, that isenforcedinstead.JSON has no comments, so the commentary is in
_-prefixed keys. That is safe because the server reads three names and ignores everything else — and there is a test asserting no comment key ever collides with a real section..env.examplegains the four variables, commented out, with both forms and the note that the file wins over the inline ones.A test guarding the example. It checks the shipped file against the rules the parser enforces: unique versions,
settingsobjects, valid JSON, no stray top-level keys. An example that has drifted out of step is worse than no example — somebody copies it, the server refuses to start, and the first experience of the feature is a crash loop.Immutability, confirmed rather than reasoned about
The question came up, so I ran it against the real image on the deploy host. An immutable container —
--read-only --tmpfs /tmp -e IMMUTABLE=1 -e SESSION_FILE=— starts and serves the policy correctly in both forms:-v policy.json:/etc/ihasmail/policy.json:ro), andThe feature costs nothing in immutability.
assertImmutablechecks two things —SESSION_FILEempty and the install root not writable — and a read-only mount elsewhere touches neither. The only thing this feature writes is the applied-changes stamp, and that goes into the reader's own settings file on Stalwart, like every other setting.One caveat that predates this and is unchanged by it:
assertImmutableprobes the install root only, so a policy file mounted read-write somewhere else would not trip it. Nothing previously gave a reason to mount anything; now something does, which is why both the README and the docs say:ro.npm test(977 web + 126 server — four of those new),npm run buildpass.Merged 2026-09-02 as coffey-labs/ihasmail@0d5975d427
Rebuilt from: git history, session transcript.