Merge pull request #232 from Coffey-Labs/docs/policy-example
Ship an example settings policy, and name the variables in .env.example
This commit is contained in:
@@ -69,3 +69,27 @@ APP_NAME=ihasmail
|
|||||||
# you have patched it, point this at your own tree. Shown on the sign-in page
|
# you have patched it, point this at your own tree. Shown on the sign-in page
|
||||||
# and in Settings > About.
|
# and in Settings > About.
|
||||||
SOURCE_URL=https://github.com/Coffey-Labs/ihasmail
|
SOURCE_URL=https://github.com/Coffey-Labs/ihasmail
|
||||||
|
|
||||||
|
# ---- Settings this installation decides (all optional) ----
|
||||||
|
#
|
||||||
|
# Seed what a new account starts on, lock what nobody may change, and turn
|
||||||
|
# something on once for accounts that already exist. Setting none of these --
|
||||||
|
# the default -- behaves exactly as ihasmail always has.
|
||||||
|
#
|
||||||
|
# A file is easier once there are `changes` in it. See the shipped
|
||||||
|
# settings-policy.example.json, and mount it read-only:
|
||||||
|
#
|
||||||
|
# -v /srv/ihasmail/policy.json:/etc/ihasmail/policy.json:ro
|
||||||
|
#
|
||||||
|
# SETTINGS_POLICY_FILE=/etc/ihasmail/policy.json
|
||||||
|
#
|
||||||
|
# Or inline, which is what an immutable deployment with no volume wants. These
|
||||||
|
# are ignored entirely when SETTINGS_POLICY_FILE is set, so a file and a stray
|
||||||
|
# variable cannot half-apply between them.
|
||||||
|
#
|
||||||
|
# SETTINGS_DEFAULTS={"externalSenderBanner":true}
|
||||||
|
# SETTINGS_ENFORCED={"externalRecipientConfirm":true}
|
||||||
|
# SETTINGS_CHANGES=[{"version":"20260902084513","settings":{"externalSenderBanner":true}}]
|
||||||
|
#
|
||||||
|
# Read once at startup: editing a policy means restarting the container.
|
||||||
|
# Docs: https://docs.ihasmail.org/configure/#settings-your-installation-decides
|
||||||
|
|||||||
@@ -171,6 +171,10 @@ docker run -d --name ihasmail \
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
[`settings-policy.example.json`](settings-policy.example.json) in this repo is
|
||||||
|
that file with every section explained in it — copy it and delete what you do
|
||||||
|
not want.
|
||||||
|
|
||||||
Mount it read-only: the server only ever reads it, and `:ro` keeps that true
|
Mount it read-only: the server only ever reads it, and `:ro` keeps that true
|
||||||
under `--read-only` as well.
|
under `--read-only` as well.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { test } from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The shipped example policy, checked against the rules the server enforces.
|
||||||
|
*
|
||||||
|
* An example that has drifted out of step with the parser is worse than no
|
||||||
|
* example: somebody copies it, the server refuses to start, and the first
|
||||||
|
* experience of the feature is a crash loop. This does not import the config
|
||||||
|
* module -- reading it has side effects and wants a whole environment -- so the
|
||||||
|
* rules it checks are restated here, and both are short enough that saying them
|
||||||
|
* twice is cheaper than the machinery to say them once.
|
||||||
|
*/
|
||||||
|
const EXAMPLE = fileURLToPath(new URL("../../settings-policy.example.json", import.meta.url));
|
||||||
|
|
||||||
|
test("the example policy is valid JSON", () => {
|
||||||
|
assert.doesNotThrow(() => JSON.parse(readFileSync(EXAMPLE, "utf8")));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the example policy has the three sections, in the shapes the server reads", () => {
|
||||||
|
const p = JSON.parse(readFileSync(EXAMPLE, "utf8")) as Record<string, unknown>;
|
||||||
|
for (const section of ["defaults", "enforced"]) {
|
||||||
|
const v = p[section];
|
||||||
|
assert.ok(v && typeof v === "object" && !Array.isArray(v), `${section} must be an object`);
|
||||||
|
}
|
||||||
|
assert.ok(Array.isArray(p.changes), "changes must be a list");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("every change in the example has a unique version and settings", () => {
|
||||||
|
const p = JSON.parse(readFileSync(EXAMPLE, "utf8")) as { changes: Array<{ version?: unknown; settings?: unknown }> };
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const [i, c] of p.changes.entries()) {
|
||||||
|
assert.equal(typeof c.version, "string", `changes[${i}] needs a string version`);
|
||||||
|
assert.ok((c.version as string).trim(), `changes[${i}] needs a non-empty version`);
|
||||||
|
assert.ok(!seen.has(c.version as string), `changes[${i}] repeats version ${String(c.version)}`);
|
||||||
|
seen.add(c.version as string);
|
||||||
|
assert.ok(c.settings && typeof c.settings === "object" && !Array.isArray(c.settings), `changes[${i}] needs a settings object`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the example's commentary cannot be mistaken for a section", () => {
|
||||||
|
/*
|
||||||
|
* JSON has no comments, so the example explains itself in `_`-prefixed keys.
|
||||||
|
* The server reads three names and ignores everything else, which is what
|
||||||
|
* makes that safe -- but only for as long as no comment key collides with a
|
||||||
|
* real one.
|
||||||
|
*/
|
||||||
|
const p = JSON.parse(readFileSync(EXAMPLE, "utf8")) as Record<string, unknown>;
|
||||||
|
const real = new Set(["defaults", "enforced", "changes"]);
|
||||||
|
for (const key of Object.keys(p)) {
|
||||||
|
assert.ok(real.has(key) || key.startsWith("_"), `unexpected top-level key ${key}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"_comment": [
|
||||||
|
"A settings policy: what this installation decides, rather than each reader.",
|
||||||
|
"Point at it with SETTINGS_POLICY_FILE=/etc/ihasmail/policy.json and mount it",
|
||||||
|
"read-only. Read once at startup, so editing it means restarting.",
|
||||||
|
"Delete the sections you do not want -- all three are optional, and an",
|
||||||
|
"installation that sets none of them behaves exactly as ihasmail always has.",
|
||||||
|
"Keys and values are the ones a settings export uses: configure one account",
|
||||||
|
"by hand, Settings > General > Export, and copy out what you care about.",
|
||||||
|
"Docs: https://docs.ihasmail.org/configure/#settings-your-installation-decides"
|
||||||
|
],
|
||||||
|
|
||||||
|
"_defaults_comment": [
|
||||||
|
"A starting point for accounts that have never had settings of their own.",
|
||||||
|
"The reader can change any of these afterwards. An account that already",
|
||||||
|
"exists never sees them -- use `changes` below to reach those."
|
||||||
|
],
|
||||||
|
"defaults": {
|
||||||
|
"externalSenderBanner": true,
|
||||||
|
"conversationMode": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"_enforced_comment": [
|
||||||
|
"Reapplied on every load, and the reader cannot change them at all. Their",
|
||||||
|
"controls stay visible in Settings and go dead with a line saying why.",
|
||||||
|
"Reset, an imported settings file, and a settings file synced from a device",
|
||||||
|
"that predates this policy all cannot get around them."
|
||||||
|
],
|
||||||
|
"enforced": {
|
||||||
|
"externalRecipientConfirm": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"_changes_comment": [
|
||||||
|
"Applied once each, to everybody, including accounts that already exist --",
|
||||||
|
"and the reader may change them back afterwards, which sticks.",
|
||||||
|
"",
|
||||||
|
"Each entry needs a `version` that is unique in this file. It is opaque: a",
|
||||||
|
"timestamp sorts and never repeats, but any unique string works. Every",
|
||||||
|
"account remembers the versions it has had, so a change runs exactly once",
|
||||||
|
"per person -- not once per browser.",
|
||||||
|
"",
|
||||||
|
"Note that a change DOES override a decision a reader has already made. That",
|
||||||
|
"is the point of it: it reaches people who are already here. If you want it",
|
||||||
|
"to stay on regardless of what they do next, that is `enforced`, not this."
|
||||||
|
],
|
||||||
|
"changes": [
|
||||||
|
{
|
||||||
|
"version": "20260902084513",
|
||||||
|
"settings": { "externalSenderBanner": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "20261014091500",
|
||||||
|
"settings": { "externalLinkWarning": true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user