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:
@@ -77,7 +77,7 @@ test("a domain in use is kept, and names what uses it", () => {
|
||||
|
||||
test("an unused domain goes once its keys do", () => {
|
||||
const dir = make("admin");
|
||||
const created = dir.handlers["x:Domain/set"]!({ create: { n: { name: "fresh.example" } } }) as { created: Record<string, { id: string }> };
|
||||
const created = dir.handlers["x:Domain/set"]!({ create: { n: { name: "fresh.example.net" } } }) as { created: Record<string, { id: string }> };
|
||||
const id = created.created.n!.id;
|
||||
const keys = dir.handlers["x:DkimSignature/query"]!({ filter: { domainId: id } }) as { ids: string[] };
|
||||
assert.equal(keys.ids.length, 1, "automatic DKIM makes a key straight away");
|
||||
@@ -100,3 +100,12 @@ test("a filter on a name the registry does not index is refused, as the live ser
|
||||
assert.throws(() => dir.handlers["x:Account/query"]!({ filter: { type: "User" } }), (e: Refused) => e.type === "unsupportedFilter" && e.message === "type");
|
||||
assert.doesNotThrow(() => dir.handlers["x:Account/query"]!({ filter: { "@type": "Group", domainId: "d1", text: "x" } }));
|
||||
});
|
||||
|
||||
test("the domain validators refuse what the live server refused, in its words", () => {
|
||||
const dir = make("admin");
|
||||
const set = dir.handlers["x:Domain/set"]!;
|
||||
const created = set({ create: { n: { name: "admin-test.example" } } }) as { notCreated?: Record<string, { type: string; description: string }> };
|
||||
assert.deepEqual([created.notCreated?.n?.type, created.notCreated?.n?.description], ["invalidPatch", "Invalid domain name"]);
|
||||
const updated = set({ update: { d2: { catchAllAddress: "postmaster" } } }) as { notUpdated?: Record<string, { type: string; description: string }> };
|
||||
assert.deepEqual([updated.notUpdated?.d2?.type, updated.notUpdated?.d2?.description], ["invalidPatch", "Invalid email address"]);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user