Find Stalwart's administration instead of asking for it

The dashboard's link to Stalwart's own administration needed
STALWART_ADMIN_URL, which an operator had to know to set. Everything it
holds can be read from the server:

- the origin is the host Stalwart advertises in its own session URLs, the
  one people reach it at even when ihasmail talks to it on a private
  address;
- the prefix is where its web interface application is installed. The
  production server's x:Application reads "Stalwart Web Interface",
  enabled, urlPrefix {"/admin", "/account"}, and /admin is also what
  Stalwart writes at first boot.

So, for an account that administers, the account info fetch now asks the
account's own server for its applications and links to origin + /admin/.
An installation whose web interface is disabled or moved gets no link; an
administrator who may not read applications gets Stalwart's default
/admin. It is cached with the rest of the account info.

STALWART_ADMIN_URL and a servers file entry's adminUrl still win, for an
administration that lives somewhere else. A routed domain without one now
takes what its own server said, never the default server's.
This commit is contained in:
2026-09-15 10:01:18 -07:00
parent 53d9cd37fb
commit 2e2724c55c
7 changed files with 136 additions and 27 deletions
+25 -2
View File
@@ -18,7 +18,7 @@ process.env.STALWART_URL = "https://default.example";
process.env.STALWART_ADMIN_URL = "https://admin.default.example/";
process.env.STALWART_SERVERS_FILE = file;
const { adminUrlFor, upstreamFor } = await import("./upstream.js");
const { adminPrefixFrom, adminUrlFor, advertisedOrigin, upstreamFor } = await import("./upstream.js");
const { config, parseStalwartServers } = await import("./config.js");
/**
@@ -39,10 +39,33 @@ test("an unmapped domain and a bare username open the default administration", (
test("a routed domain opens its own server's administration, and never the default's", () => {
assert.equal(adminUrlFor("[email protected]"), "https://admin.linked.test");
// Routed away, with no adminUrl of its own: no link rather than the wrong server.
// Routed away, with no adminUrl of its own and nothing found: no link rather than the wrong server.
assert.equal(adminUrlFor("[email protected]"), null);
});
test("what the operator configured wins over what was found, and what was found fills the gap", () => {
assert.equal(adminUrlFor("[email protected]", "https://found.example/admin/"), "https://admin.default.example");
assert.equal(adminUrlFor("[email protected]", "https://found.example/admin/"), "https://admin.linked.test");
// The routed domain without an adminUrl takes what its own server said.
assert.equal(adminUrlFor("[email protected]", "https://mail.plain.test/admin/"), "https://mail.plain.test/admin/");
});
/** Finding the administration on the server itself, as production's answered on 2026-09-15. */
test("the web interface's prefix is read from the applications Stalwart has installed", () => {
const got = (list: unknown[]) => adminPrefixFrom([["x:Application/query", { ids: ["a"] }, "q"], ["x:Application/get", { list }, "g"]]);
assert.equal(got([{ enabled: true, description: "Stalwart Web Interface", urlPrefix: { "/admin": true, "/account": true } }]), "/admin");
assert.equal(got([{ enabled: false, urlPrefix: { "/admin": true } }]), null);
assert.equal(got([{ enabled: true, urlPrefix: { "/console": true } }]), null);
assert.equal(got([]), null);
// May not read applications: not an answer, so Stalwart's own default.
assert.equal(adminPrefixFrom([["error", { type: "forbidden" }, "q"], ["error", { type: "forbidden" }, "g"]]), "/admin");
});
test("the origin is the one Stalwart advertises, even when it is reached on a private address", () => {
assert.equal(advertisedOrigin({ apiUrl: "https://mail.example.com/jmap/", baseUrl: "http://127.0.0.1:8080" }), "https://mail.example.com");
assert.equal(advertisedOrigin({ apiUrl: "/jmap/", baseUrl: "https://mail.example.com" }), "https://mail.example.com");
});
test("the shipped example loads through the parser that reads it", () => {
const example = new URL("../../stalwart-servers.example.json", import.meta.url);
const parsed = parseStalwartServers(JSON.parse(readFileSync(example, "utf8")), "example");