Say every Administration refusal in the reader's language

Stalwart explains a refused change in English, and several of its words
reached the page as they were: "Invalid domain name" for a reserved TLD,
"Invalid email address" for a catch-all, a grant refusal, and ihasmail's own
proxy messages. Every registry error type now has its own message, and a
value one of the registry's string validators refused is recognised by the
validator's wording and explained again. A domain clash or a missing domain
is worded for a domain rather than an account.

The one exception is kept on purpose: a password policy's reason follows a
translated sentence, because the rule is the server's and dropping it would
leave no way to find out why.

The mock now refuses a reserved TLD and a catch-all without a domain the way
the live server did. KNOWN-ISSUES records the fix, and that the last two live
cases -- an administrator-set password and the outranking guard -- held.

15 new strings in all nine catalogues, 3 retired; strings falling back to
English stay at 16.
This commit is contained in:
2026-09-13 17:18:15 -07:00
parent 822314e8b7
commit 15b1838e21
16 changed files with 270 additions and 50 deletions
+5 -1
View File
@@ -341,7 +341,9 @@ export function createDirectory(opts: Options) {
demand("sysDomainCreate");
const o = raw as Obj;
const name = String(o.name ?? "");
if (!/^([a-z0-9-]+\.)+[a-z0-9-]{2,}$/.test(name)) { notCreated[cid] = setError("invalidProperties", "Invalid domain name.", ["name"]); continue; }
// Live on 2026-09-13: a reserved TLD is refused by the registry's
// domain validator, as invalidPatch with the validator's own words.
if (!/^([a-z0-9-]+\.)+[a-z0-9-]{2,}$/.test(name) || /\.(example|test|invalid|localhost)$/.test(name)) { notCreated[cid] = setError("invalidPatch", "Invalid domain name", ["name"]); continue; }
if (taken(name)) { notCreated[cid] = setError("primaryKeyViolation", "A domain with this name already exists.", ["name"]); continue; }
const id = `d${counter++}`;
domains.push(domain(id, name, { ...o, id, createdAt: new Date().toISOString().replace(/\.\d{3}Z$/, "Z") }));
@@ -355,6 +357,8 @@ export function createDirectory(opts: Options) {
if (!target) { notUpdated[id] = setError("notFound", "Domain not found."); continue; }
const next = structuredClone(target);
for (const [path, value] of Object.entries(raw as Obj)) setPointer(next, path, value);
// Live on 2026-09-13: a catch-all that is not a whole address.
if (typeof next.catchAllAddress === "string" && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(next.catchAllAddress)) { notUpdated[id] = setError("invalidPatch", "Invalid email address", ["catchAllAddress"]); continue; }
const clash = Object.keys((next.aliases as Obj) ?? {}).find((alias) => alias === next.name || taken(alias, id));
if (clash) { notUpdated[id] = setError("primaryKeyViolation", `The name ${clash} is already in use.`, ["aliases"]); continue; }
Object.assign(target, next);