Compare commits

...
1 Commits
Author SHA1 Message Date
jcoffey-dev f5dd4e5537 Use the server's renamed registry capability, urn:inbuxa:jmap:registry
ci / version (pull_request) Skipped
ci / node (pull_request) Successful in 2m47s
ci / publish (pull_request) Skipped
ci / docker-build (pull_request) In progress
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.
2026-09-22 19:00:19 -07:00
9 changed files with 17 additions and 17 deletions
+3 -3
View File
@@ -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 () => {
+1 -1
View File
@@ -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";
+3 -3
View File
@@ -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", () => {
+1 -1
View File
@@ -18,7 +18,7 @@ const PORT = 18799;
process.env.MOCK_PORT = String(PORT);
process.env.MOCK_USER = "[email protected]";
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";
+1 -1
View File
@@ -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.
+3 -3
View File
@@ -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: "[email protected]", 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: "[email protected]", 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));
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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.
*/
@@ -15,7 +15,7 @@ const HELPDESK = ["sysAccountGet", "sysAccountQuery", "sysAccountUpdate"];
function signIn(permissions: string[], username = "[email protected]") {
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,
});
}