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:
@@ -5,6 +5,7 @@ import { domainOf } from "@/lib/address";
|
||||
import { Switch } from "@/ui/misc";
|
||||
import { X } from "lucide-react";
|
||||
import { t } from "@/lib/i18n";
|
||||
import { isEnforced } from "@/lib/settingsPolicy";
|
||||
|
||||
/**
|
||||
* Everything about what reaches a sender, and what asks before it happens.
|
||||
@@ -36,7 +37,7 @@ export function PrivacySettings() {
|
||||
<h2>{t("Remote content")}</h2>
|
||||
<div className="field">
|
||||
<label>{t("Remote images")}</label>
|
||||
<select className="select" value={s.imagePolicy} onChange={(e) => update({ imagePolicy: e.target.value as typeof s.imagePolicy })}>
|
||||
<select disabled={isEnforced("imagePolicy")} className="select" value={s.imagePolicy} onChange={(e) => update({ imagePolicy: e.target.value as typeof s.imagePolicy })}>
|
||||
<option value="ask">{t("Ask before showing (recommended)")}</option>
|
||||
<option value="contacts">{t("Show automatically from my contacts")}</option>
|
||||
<option value="always">{t("Always show")}</option>
|
||||
@@ -67,10 +68,10 @@ export function PrivacySettings() {
|
||||
)}
|
||||
|
||||
<h2>{t("Read receipts")}</h2>
|
||||
<Switch checked={s.requestReadReceipt} onChange={(v) => update({ requestReadReceipt: v })} label={t("Always request read receipts")} />
|
||||
<Switch locked={isEnforced("requestReadReceipt")} checked={s.requestReadReceipt} onChange={(v) => update({ requestReadReceipt: v })} label={t("Always request read receipts")} />
|
||||
<div className="field">
|
||||
<label>{t("When someone requests a read receipt")}</label>
|
||||
<select className="select" value={s.readReceiptPolicy} onChange={(e) => update({ readReceiptPolicy: e.target.value as ReadReceiptPolicy })}>
|
||||
<select disabled={isEnforced("readReceiptPolicy")} className="select" value={s.readReceiptPolicy} onChange={(e) => update({ readReceiptPolicy: e.target.value as ReadReceiptPolicy })}>
|
||||
<option value="ask">{t("Ask me on each message")}</option>
|
||||
<option value="never">{t("Never send one")}</option>
|
||||
</select>
|
||||
@@ -108,7 +109,7 @@ export function PrivacySettings() {
|
||||
|
||||
<div className="field">
|
||||
<label>{t("Ask before sending to a large group")}</label>
|
||||
<select className="select" value={String(s.replyAllThreshold)} onChange={(e) => update({ replyAllThreshold: Number(e.target.value) })}>
|
||||
<select disabled={isEnforced("replyAllThreshold")} className="select" value={String(s.replyAllThreshold)} onChange={(e) => update({ replyAllThreshold: Number(e.target.value) })}>
|
||||
<option value="0">{t("Never ask")}</option>
|
||||
<option value="5">{t("5 people or more")}</option>
|
||||
<option value="10">{t("10 people or more")}</option>
|
||||
@@ -136,7 +137,7 @@ export function PrivacySettings() {
|
||||
<h2>{t("Before it happens")}</h2>
|
||||
<div className="field">
|
||||
<label>{t("Undo send window")}</label>
|
||||
<select className="select" value={String(s.undoSendSeconds)} onChange={(e) => update({ undoSendSeconds: Number(e.target.value) })}>
|
||||
<select disabled={isEnforced("undoSendSeconds")} className="select" value={String(s.undoSendSeconds)} onChange={(e) => update({ undoSendSeconds: Number(e.target.value) })}>
|
||||
<option value="0">{t("Off")}</option>
|
||||
<option value="5">{t("5 seconds")}</option>
|
||||
<option value="8">{t("8 seconds")}</option>
|
||||
@@ -145,8 +146,8 @@ export function PrivacySettings() {
|
||||
</select>
|
||||
<p className="hint">{t("The message is held in this browser and has not been submitted yet, so taking it back costs nothing.")}</p>
|
||||
</div>
|
||||
<Switch checked={s.attachmentReminder} onChange={(v) => update({ attachmentReminder: v })} label={t("Attachment reminder")} hint={t("Warn when the message mentions an attachment but none is attached.")} />
|
||||
<Switch checked={s.confirmDelete} onChange={(v) => update({ confirmDelete: v })} label={t("Confirm before deleting")} />
|
||||
<Switch locked={isEnforced("attachmentReminder")} checked={s.attachmentReminder} onChange={(v) => update({ attachmentReminder: v })} label={t("Attachment reminder")} hint={t("Warn when the message mentions an attachment but none is attached.")} />
|
||||
<Switch locked={isEnforced("confirmDelete")} checked={s.confirmDelete} onChange={(v) => update({ confirmDelete: v })} label={t("Confirm before deleting")} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user