From f5dd4e553737391a3c6b6ae6c80970e1d7144930 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Tue, 22 Sep 2026 19:00:19 -0700 Subject: [PATCH] Use the server's renamed registry capability, urn:inbuxa:jmap:registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit inbuxa-server renames the identifiers that carried the upstream name (its SPEC.md §2.4). Upstream's capability for the registry (x:) objects is now urn:inbuxa:jmap:registry, beside the fork's own urn:inbuxa:jmap, which is unchanged. There's no alias, so this lands with the server change and deploys with it. The mock advertises the new name too. No user-visible strings change. --- server/src/account.test.ts | 6 +++--- server/src/account.ts | 2 +- server/src/accountinfo.test.ts | 6 +++--- server/src/login-guard.test.ts | 2 +- server/src/mock/config.ts | 2 +- server/src/mock/index.ts | 6 +++--- server/src/upstream.ts | 4 ++-- web/src/jmap/client.ts | 4 ++-- web/src/views/admin/__tests__/account-sheet.test.tsx | 2 +- 9 files changed, 17 insertions(+), 17 deletions(-) diff --git a/server/src/account.test.ts b/server/src/account.test.ts index 2db7f31..23d0ce3 100644 --- a/server/src/account.test.ts +++ b/server/src/account.test.ts @@ -47,7 +47,7 @@ after(() => { }); /** - * Stalwart advertises `urn:stalwart:jmap` only per-account, never in the + * Stalwart advertises `urn:inbuxa:jmap:registry` only per-account, never in the * session-level capabilities. Looking for it at the top level alone reported * every real 0.16 server as older than 0.16 — and now that the same check * decides whether a sign-in is allowed at all, that mistake would lock @@ -57,8 +57,8 @@ test("the session is accepted on a server that advertises the registry per-accou const res = await call("/api/auth/session"); assert.equal(res.status, 200); assert.equal(res.body.ihasmail.server.edition, "oss"); - assert.equal(res.body.capabilities["urn:stalwart:jmap"], undefined, "not where a client would first look"); - assert.ok("urn:stalwart:jmap" in res.body.primaryAccounts, "but here, as on a real server"); + assert.equal(res.body.capabilities["urn:inbuxa:jmap:registry"], undefined, "not where a client would first look"); + assert.ok("urn:inbuxa:jmap:registry" in res.body.primaryAccounts, "but here, as on a real server"); }); test("the registry reports an account with nothing set up yet", async () => { diff --git a/server/src/account.ts b/server/src/account.ts index 88e064b..5bf12e4 100644 --- a/server/src/account.ts +++ b/server/src/account.ts @@ -12,7 +12,7 @@ import { generateSecret, otpauthUrl, parseOtpauthUrl, verifyTotp } from "./totp. * the registry is known to be there. */ -const STALWART_CAP = "urn:stalwart:jmap"; +const STALWART_CAP = "urn:inbuxa:jmap:registry"; const JMAP_CORE = "urn:ietf:params:jmap:core"; /** Stalwart's id for a singleton object; the number it encodes spells this. */ const SINGLETON = "singleton"; diff --git a/server/src/accountinfo.test.ts b/server/src/accountinfo.test.ts index fbe32b0..0791368 100644 --- a/server/src/accountinfo.test.ts +++ b/server/src/accountinfo.test.ts @@ -44,7 +44,7 @@ test("locales that carry no language are dropped, not passed through", () => { test("a server without the registry is not asked for anything", async () => { // Sign-in refuses these, so getAccountInfo should never reach the wire for - // one - and must not, since a server that cannot parse `urn:stalwart:jmap` + // one - and must not, since a server that cannot parse `urn:inbuxa:jmap:registry` // fails the whole request rather than the one call. const session = { capabilities: { "urn:ietf:params:jmap:core": {}, "urn:ietf:params:jmap:mail": {} }, accounts: {}, primaryAccounts: {} }; const info = await getAccountInfo("session-unsupported", "Basic x", session as never); @@ -57,7 +57,7 @@ test("no capabilities at all is treated the same way", async () => { }); /** - * Where Stalwart actually advertises `urn:stalwart:jmap`. + * Where Stalwart actually advertises `urn:inbuxa:jmap:registry`. * * Not in the session-level `capabilities`: `Session::new` builds those from a * fixed list that has never carried this capability, in any 0.16.x. It is @@ -70,7 +70,7 @@ test("no capabilities at all is treated the same way", async () => { * This check now decides whether a sign-in is allowed at all, so getting it * wrong would lock every user out of a perfectly good server. */ -const STALWART = "urn:stalwart:jmap"; +const STALWART = "urn:inbuxa:jmap:registry"; const baseCaps = { "urn:ietf:params:jmap:core": {}, "urn:ietf:params:jmap:mail": {} }; test("a 0.16 server is recognized from primaryAccounts, where it advertises itself", () => { diff --git a/server/src/login-guard.test.ts b/server/src/login-guard.test.ts index 0e6e10d..1eff938 100644 --- a/server/src/login-guard.test.ts +++ b/server/src/login-guard.test.ts @@ -18,7 +18,7 @@ const PORT = 18799; process.env.MOCK_PORT = String(PORT); process.env.MOCK_USER = "demo@example.com"; process.env.MOCK_PASS = "demo-password"; -process.env.MOCK_NO_REGISTRY = "1"; // a server without urn:stalwart:jmap +process.env.MOCK_NO_REGISTRY = "1"; // a server without urn:inbuxa:jmap:registry process.env.MAIL_SERVER_URL = `http://127.0.0.1:${PORT}`; process.env.APP_SECRET = "test-secret-for-login-guard"; diff --git a/server/src/mock/config.ts b/server/src/mock/config.ts index 6004552..d60c8a4 100644 --- a/server/src/mock/config.ts +++ b/server/src/mock/config.ts @@ -5,7 +5,7 @@ export const PERMISSION_SNAPSHOT = (JSON.parse(readFileSync(new URL("../../../we export const PORT = Number(process.env.MOCK_PORT ?? 8788); /** - * Omit `urn:stalwart:jmap` from the session, so a sign-in can be tested + * Omit `urn:inbuxa:jmap:registry` from the session, so a sign-in can be tested * against a server ihasmail does not support. This is only that: the rest of * the mock still behaves like 0.16. Emulating 0.15 properly went with the * support for it. diff --git a/server/src/mock/index.ts b/server/src/mock/index.ts index 3d54b7d..729ebb1 100644 --- a/server/src/mock/index.ts +++ b/server/src/mock/index.ts @@ -60,8 +60,8 @@ const session = () => ({ * the only way this stays honest about what can be inferred from a * capability, which is nothing. */ - accounts: { [SHARED_ACCOUNT]: { name: "grace@example.org", isPersonal: false, isReadOnly: false, accountCapabilities: SHARED_CAPS }, [ACCOUNT]: { name: USER, isPersonal: true, isReadOnly: false, accountCapabilities: { "urn:ietf:params:jmap:mail": {}, "urn:ietf:params:jmap:submission": { maxDelayedSend: MAX_DELAYED_SEND, submissionExtensions: { FUTURERELEASE: [], SIZE: [], DSN: [], DELIVERYBY: [], "MT-PRIORITY": ["MIXER"], REQUIRETLS: [] } }, "urn:ietf:params:jmap:vacationresponse": {}, "urn:ietf:params:jmap:sieve": {}, "urn:ietf:params:jmap:calendars": {}, "urn:ietf:params:jmap:contacts": {}, "urn:ietf:params:jmap:principals": {}, "urn:ietf:params:jmap:quota": {}, "urn:ietf:params:jmap:filenode": {}, ...(NO_REGISTRY ? {} : { "urn:stalwart:jmap": {} }) } } }, - primaryAccounts: { ...Object.fromEntries(["mail", "submission", "vacationresponse", "sieve", "calendars", "contacts", "principals", "quota", "filenode", "blob"].map((c) => [`urn:ietf:params:jmap:${c}`, ACCOUNT])), ...(NO_REGISTRY ? {} : { "urn:stalwart:jmap": ACCOUNT }) }, + accounts: { [SHARED_ACCOUNT]: { name: "grace@example.org", isPersonal: false, isReadOnly: false, accountCapabilities: SHARED_CAPS }, [ACCOUNT]: { name: USER, isPersonal: true, isReadOnly: false, accountCapabilities: { "urn:ietf:params:jmap:mail": {}, "urn:ietf:params:jmap:submission": { maxDelayedSend: MAX_DELAYED_SEND, submissionExtensions: { FUTURERELEASE: [], SIZE: [], DSN: [], DELIVERYBY: [], "MT-PRIORITY": ["MIXER"], REQUIRETLS: [] } }, "urn:ietf:params:jmap:vacationresponse": {}, "urn:ietf:params:jmap:sieve": {}, "urn:ietf:params:jmap:calendars": {}, "urn:ietf:params:jmap:contacts": {}, "urn:ietf:params:jmap:principals": {}, "urn:ietf:params:jmap:quota": {}, "urn:ietf:params:jmap:filenode": {}, ...(NO_REGISTRY ? {} : { "urn:inbuxa:jmap:registry": {} }) } } }, + primaryAccounts: { ...Object.fromEntries(["mail", "submission", "vacationresponse", "sieve", "calendars", "contacts", "principals", "quota", "filenode", "blob"].map((c) => [`urn:ietf:params:jmap:${c}`, ACCOUNT])), ...(NO_REGISTRY ? {} : { "urn:inbuxa:jmap:registry": ACCOUNT }) }, username: USER, apiUrl: `http://127.0.0.1:${PORT}/jmap/`, downloadUrl: `http://127.0.0.1:${PORT}/jmap/download/{accountId}/{blobId}/{name}?accept={type}`, @@ -103,7 +103,7 @@ export const server = createServer(async (req, res) => { // call that wanted it - which is why an over-eager `using` is so damaging. // Stalwart decides this by parsing the urn, not by looking it up in the // session, so a capability it hands out per-account is still usable here: - // `urn:stalwart:jmap` never appears in the session-level capabilities and + // `urn:inbuxa:jmap:registry` never appears in the session-level capabilities and // the registry calls that name it work all the same. const known = new Set([...Object.keys(session().capabilities), ...Object.keys(session().accounts[ACCOUNT]?.accountCapabilities ?? {})]); const unknown = (body.using ?? []).find((u) => !known.has(u)); diff --git a/server/src/upstream.ts b/server/src/upstream.ts index cdf7067..e798535 100644 --- a/server/src/upstream.ts +++ b/server/src/upstream.ts @@ -178,14 +178,14 @@ export function forgetUpstreamSession(sessionId: string): void { /* Account locale */ /* ------------------------------------------------------------------ */ -const STALWART_CAP = "urn:stalwart:jmap"; +const STALWART_CAP = "urn:inbuxa:jmap:registry"; const JMAP_CORE = "urn:ietf:params:jmap:core"; /** * Whether this server has Stalwart's JMAP registry — the `x:` objects that * carry credentials, account settings and the newer FileNode shape. * - * `urn:stalwart:jmap` is the marker, but **not** in the session-level + * `urn:inbuxa:jmap:registry` is the marker, but **not** in the session-level * `capabilities`, which is where a JMAP client would naturally look. Stalwart * builds that list from a fixed set that has never included this capability; * it hands it out per-account instead, so it turns up in `primaryAccounts` and diff --git a/web/src/jmap/client.ts b/web/src/jmap/client.ts index 3d9abf9..7427f9c 100644 --- a/web/src/jmap/client.ts +++ b/web/src/jmap/client.ts @@ -20,7 +20,7 @@ export const CAP = { } as const; /** Stalwart's own capability, which carries its `x:` registry methods. */ -export const STALWART_CAP = "urn:stalwart:jmap"; +export const STALWART_CAP = "urn:inbuxa:jmap:registry"; /** INBUXA's own capability (contract C-1), on the signed-in account. */ export const INBUXA_CAP = "urn:inbuxa:jmap"; @@ -155,7 +155,7 @@ export class JmapClient { * Whether the server carries a capability at all, wherever it chose to * advertise it. * - * Stalwart hands `urn:stalwart:jmap` out per-account rather than putting it + * Stalwart hands `urn:inbuxa:jmap:registry` out per-account rather than putting it * in the session-level `capabilities`, so `hasCapability` alone reports every * real 0.16 server as though it were older. Look in all three places. */ diff --git a/web/src/views/admin/__tests__/account-sheet.test.tsx b/web/src/views/admin/__tests__/account-sheet.test.tsx index d7c122f..5b304bb 100644 --- a/web/src/views/admin/__tests__/account-sheet.test.tsx +++ b/web/src/views/admin/__tests__/account-sheet.test.tsx @@ -15,7 +15,7 @@ const HELPDESK = ["sysAccountGet", "sysAccountQuery", "sysAccountUpdate"]; function signIn(permissions: string[], username = "sam@example.com") { useSession.setState({ - session: { capabilities: {}, accounts: {}, primaryAccounts: { "urn:stalwart:jmap": "self" }, username, ihasmail: { permissions } } as unknown as JmapSession, + session: { capabilities: {}, accounts: {}, primaryAccounts: { "urn:inbuxa:jmap:registry": "self" }, username, ihasmail: { permissions } } as unknown as JmapSession, }); }