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
+1
View File
@@ -55,6 +55,7 @@ import type { Catalog } from "@/lib/i18n";
*/
export const catalog: Catalog = {
strings: {
"Set for everyone here. You cannot change this.": "Für alle hier festgelegt. Sie können dies nicht ändern.",
"Export iCAL file": "iCAL-Datei exportieren",
"Could not export this calendar: {error}": "Dieser Kalender konnte nicht exportiert werden: {error}",
// ── Actions ────────────────────────────────────────────────────────
+1
View File
@@ -47,6 +47,7 @@ import type { Catalog } from "@/lib/i18n";
*/
export const catalog: Catalog = {
strings: {
"Set for everyone here. You cannot change this.": "Definido para todos aquí. No puedes cambiarlo.",
"Export iCAL file": "Exportar archivo iCAL",
"Could not export this calendar: {error}": "No se pudo exportar este calendario: {error}",
// ── Actions ────────────────────────────────────────────────────────
+1
View File
@@ -52,6 +52,7 @@ import type { Catalog } from "@/lib/i18n";
*/
export const catalog: Catalog = {
strings: {
"Set for everyone here. You cannot change this.": "Défini pour tout le monde ici. Vous ne pouvez pas le modifier.",
"Export iCAL file": "Exporter un fichier iCAL",
"Could not export this calendar: {error}": "Impossible dexporter ce calendrier : {error}",
// ── Actions ────────────────────────────────────────────────────────
+1
View File
@@ -46,6 +46,7 @@ import type { Catalog } from "@/lib/i18n";
*/
export const catalog: Catalog = {
strings: {
"Set for everyone here. You cannot change this.": "この環境全体で設定されています。変更できません。",
"Export iCAL file": "iCAL ファイルをエクスポート",
"Could not export this calendar: {error}": "このカレンダーをエクスポートできませんでした: {error}",
// ── Actions ────────────────────────────────────────────────────────
+1
View File
@@ -43,6 +43,7 @@ import type { Catalog } from "@/lib/i18n";
*/
export const catalog: Catalog = {
strings: {
"Set for everyone here. You cannot change this.": "Hier voor iedereen ingesteld. U kunt dit niet wijzigen.",
"Export iCAL file": "iCAL-bestand exporteren",
"Could not export this calendar: {error}": "Kon deze agenda niet exporteren: {error}",
// ── Actions ────────────────────────────────────────────────────────
+1
View File
@@ -50,6 +50,7 @@ import type { Catalog } from "@/lib/i18n";
*/
export const catalog: Catalog = {
strings: {
"Set for everyone here. You cannot change this.": "Definido para todos aqui. Você não pode alterar isto.",
"Export iCAL file": "Exportar arquivo iCAL",
"Could not export this calendar: {error}": "Não foi possível exportar esta agenda: {error}",
// ── Actions ────────────────────────────────────────────────────────
+1
View File
@@ -49,6 +49,7 @@ import type { Catalog } from "@/lib/i18n";
*/
export const catalog: Catalog = {
strings: {
"Set for everyone here. You cannot change this.": "Задано для всех здесь. Изменить нельзя.",
"Export iCAL file": "Экспортировать файл iCAL",
"Could not export this calendar: {error}": "Не удалось экспортировать этот календарь: {error}",
// ── Actions ────────────────────────────────────────────────────────
+1
View File
@@ -43,6 +43,7 @@ import type { Catalog } from "@/lib/i18n";
*/
export const catalog: Catalog = {
strings: {
"Set for everyone here. You cannot change this.": "Задано для всіх тут. Змінити не можна.",
"Export iCAL file": "Експортувати файл iCAL",
"Could not export this calendar: {error}": "Не вдалося експортувати цей календар: {error}",
// ── Actions ────────────────────────────────────────────────────────
+1
View File
@@ -45,6 +45,7 @@ import type { Catalog } from "@/lib/i18n";
*/
export const catalog: Catalog = {
strings: {
"Set for everyone here. You cannot change this.": "已为此处所有人设定,您无法更改。",
"Export iCAL file": "导出 iCAL 文件",
"Could not export this calendar: {error}": "无法导出此日历:{error}",
// ── Actions ────────────────────────────────────────────────────────