AGPL source offer and name cleanup
Every build writes the exact source it was built from, uncommitted work and new files included, as source.tar.gz next to the app, named after that tree. Docker builds, which have no git, pack the build context and name it by a hash of its files. The sign-in page and Settings > About link to it instead of a repository that can drift. What users, operators and packagers see no longer names the upstream server: - interface text, in all nine catalogues, with a token-session line for Security; - server messages; - the settings, now MAIL_SERVER_URL, MAIL_SERVERS_FILE, ADMIN_URL and MAIL_SERVER_FOLLOW_ADVERTISED_URLS, and mail-servers.example.json; - the Tenants notice, which is gone; - the README, CONTRIBUTING and SECURITY. ihasmail's own FEATURES, KNOWN-ISSUES and ROADMAP stay with public ihasmail, and INBUXA.md is folded into the README.
This commit is contained in:
@@ -12,7 +12,7 @@ const PORT = 18797;
|
||||
process.env.MOCK_PORT = String(PORT);
|
||||
process.env.MOCK_USER = "[email protected]";
|
||||
process.env.MOCK_PASS = "demo-password";
|
||||
process.env.STALWART_URL = `http://127.0.0.1:${PORT}`;
|
||||
process.env.MAIL_SERVER_URL = `http://127.0.0.1:${PORT}`;
|
||||
process.env.APP_SECRET = "test-secret-for-account-flows";
|
||||
|
||||
const mock = await import("./mock/index.js");
|
||||
|
||||
@@ -72,7 +72,7 @@ async function jmap(ctx: Ctx, methodCalls: Invocation[]): Promise<{ methodRespon
|
||||
signal: AbortSignal.timeout(config.upstreamTimeout),
|
||||
});
|
||||
if (res.status === 401 || res.status === 403) throw new UpstreamError("Invalid credentials", 401);
|
||||
if (!res.ok) throw new UpstreamError(`Stalwart rejected the request (${res.status})`, 502);
|
||||
if (!res.ok) throw new UpstreamError(`The mail server rejected the request (${res.status})`, 502);
|
||||
return (await res.json()) as { methodResponses?: [string, unknown, string][] };
|
||||
}
|
||||
|
||||
|
||||
@@ -14,16 +14,16 @@ writeFileSync(
|
||||
"Linked.Test.": { url: "https://mail.linked.test", adminUrl: "https://admin.linked.test/" },
|
||||
}),
|
||||
);
|
||||
process.env.STALWART_URL = "https://default.example";
|
||||
process.env.STALWART_ADMIN_URL = "https://admin.default.example/";
|
||||
process.env.STALWART_SERVERS_FILE = file;
|
||||
process.env.MAIL_SERVER_URL = "https://default.example";
|
||||
process.env.ADMIN_URL = "https://admin.default.example/";
|
||||
process.env.MAIL_SERVERS_FILE = file;
|
||||
|
||||
const { adminPrefixFrom, adminUrlFor, advertisedOrigin, upstreamFor } = await import("./upstream.js");
|
||||
const { config, parseStalwartServers } = await import("./config.js");
|
||||
|
||||
/**
|
||||
* Where the dashboard's "Open Stalwart admin" points. STALWART_URL is how this
|
||||
* server reaches Stalwart; STALWART_ADMIN_URL is where a browser opens its
|
||||
* Where the dashboard's "Open Stalwart admin" points. MAIL_SERVER_URL is how this
|
||||
* server reaches Stalwart; ADMIN_URL is where a browser opens its
|
||||
* administration, and follows the same domain routing.
|
||||
*/
|
||||
test("a servers file entry may name its administration as well as its server, and a note is not a domain", () => {
|
||||
@@ -67,7 +67,7 @@ test("the origin is the one Stalwart advertises, even when it is reached on a pr
|
||||
});
|
||||
|
||||
test("the shipped example loads through the parser that reads it", () => {
|
||||
const example = new URL("../../stalwart-servers.example.json", import.meta.url);
|
||||
const example = new URL("../../mail-servers.example.json", import.meta.url);
|
||||
const parsed = parseStalwartServers(JSON.parse(readFileSync(example, "utf8")), "example");
|
||||
assert.ok(Object.keys(parsed.urls).length > 0);
|
||||
assert.ok(!("_comment" in parsed.urls));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
process.env.STALWART_URL = "http://127.0.0.1:1";
|
||||
process.env.MAIL_SERVER_URL = "http://127.0.0.1:1";
|
||||
const { createApp } = await import("./app.js");
|
||||
|
||||
test("CSRF guard rejects API POSTs without the custom header", async () => {
|
||||
@@ -111,7 +111,7 @@ test("only a PDF blob may be framed, and only by us", async () => {
|
||||
/*
|
||||
* #239: retrying through an outage must not lock somebody out of the recovery.
|
||||
*
|
||||
* STALWART_URL at the top of this file is 127.0.0.1:1 — nothing listens there,
|
||||
* MAIL_SERVER_URL at the top of this file is 127.0.0.1:1 — nothing listens there,
|
||||
* so every sign-in here is the outage case. Before the fix, the eleventh of
|
||||
* these came back 429 and stayed 429 for fifteen minutes, outliving whatever
|
||||
* had actually been wrong.
|
||||
|
||||
+3
-3
@@ -521,7 +521,7 @@ export function createApp(basePath = config.basePath): Hono<Env> {
|
||||
{
|
||||
error: "unsupported_server",
|
||||
message:
|
||||
"Your credentials are fine, but this mail server is older than Stalwart 0.16, which ihasmail needs. Upgrade the server, or run the release tagged stalwart-0.15-support.",
|
||||
"Your credentials are fine, but this mail server isn't one this webmail supports.",
|
||||
},
|
||||
501,
|
||||
);
|
||||
@@ -564,7 +564,7 @@ export function createApp(basePath = config.basePath): Hono<Env> {
|
||||
{
|
||||
error: "totp_unsupported",
|
||||
message:
|
||||
"This mail server does not accept two-factor codes from webmail. Sign in with an app password instead — create one in Stalwart's own settings, under app passwords. Your password and code are probably fine.",
|
||||
"This mail server does not accept two-factor codes from this form. Sign in with an app password instead. Your password and code are probably fine.",
|
||||
},
|
||||
401,
|
||||
);
|
||||
@@ -629,7 +629,7 @@ export function createApp(basePath = config.basePath): Hono<Env> {
|
||||
const accountCtx = async (c: Context<Env>) => {
|
||||
const session = c.get("session");
|
||||
// The account's own server. Without it, the first fetch after the cached
|
||||
// session expires goes to STALWART_URL -- which, for a domain mapped
|
||||
// session expires goes to MAIL_SERVER_URL -- which, for a domain mapped
|
||||
// elsewhere, either refuses the password or knows a different account by
|
||||
// the same name (#238).
|
||||
const upstream = await getUpstreamSession(session.id, session.authorization, upstreamFor(session.username));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
process.env.STALWART_URL = "http://127.0.0.1:1";
|
||||
process.env.MAIL_SERVER_URL = "http://127.0.0.1:1";
|
||||
const { createApp } = await import("./app.js");
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,7 +19,7 @@ writeFileSync(join(root, "assets", "app.js"), script);
|
||||
writeFileSync(join(root, "index.html"), `<!doctype html><title>t</title>${"<p>hello</p>".repeat(400)}`);
|
||||
|
||||
process.env.STATIC_DIR = root;
|
||||
process.env.STALWART_URL = "http://127.0.0.1:1";
|
||||
process.env.MAIL_SERVER_URL = "http://127.0.0.1:1";
|
||||
const { createApp } = await import("./app.js");
|
||||
|
||||
test("an asset is gzipped when the client asks for it", async () => {
|
||||
|
||||
+15
-15
@@ -57,7 +57,7 @@ if (!appSecret || appSecret === "change-me") {
|
||||
);
|
||||
}
|
||||
|
||||
const stalwartUrl = env("STALWART_URL", "https://mail.example.com").replace(/\/+$/, "");
|
||||
const stalwartUrl = env("MAIL_SERVER_URL", "https://mail.example.com").replace(/\/+$/, "");
|
||||
|
||||
/**
|
||||
* Declares that this instance is running as an immutable container: read-only
|
||||
@@ -190,7 +190,7 @@ function readSettingsPolicy(): { defaults: Record<string, unknown>; enforced: Re
|
||||
/**
|
||||
* Which Stalwart a domain signs in to.
|
||||
*
|
||||
* `STALWART_URL` stays required and stays the default; this only adds domains
|
||||
* `MAIL_SERVER_URL` stays required and stays the default; this only adds domains
|
||||
* that go somewhere else (#238). An installation that sets nothing behaves
|
||||
* exactly as it always has.
|
||||
*
|
||||
@@ -203,15 +203,15 @@ function readSettingsPolicy(): { defaults: Record<string, unknown>; enforced: Re
|
||||
* one is unreachable is a sign-in question, answered in #239.
|
||||
*/
|
||||
function readStalwartServers(): { urls: Record<string, string>; adminUrls: Record<string, string> } {
|
||||
const file = process.env.STALWART_SERVERS_FILE;
|
||||
const file = process.env.MAIL_SERVERS_FILE;
|
||||
if (!file) return { urls: {}, adminUrls: {} };
|
||||
if (!existsSync(file)) throw new Error(`STALWART_SERVERS_FILE does not exist: ${file}`);
|
||||
if (!existsSync(file)) throw new Error(`MAIL_SERVERS_FILE does not exist: ${file}`);
|
||||
|
||||
let raw: unknown;
|
||||
try {
|
||||
raw = JSON.parse(readFileSync(file, "utf8"));
|
||||
} catch (err) {
|
||||
throw new Error(`Invalid STALWART_SERVERS_FILE (${file}): ${(err as Error).message}`);
|
||||
throw new Error(`Invalid MAIL_SERVERS_FILE (${file}): ${(err as Error).message}`);
|
||||
}
|
||||
return parseStalwartServers(raw, file);
|
||||
}
|
||||
@@ -219,7 +219,7 @@ function readStalwartServers(): { urls: Record<string, string>; adminUrls: Recor
|
||||
/** The servers file's contents, checked. Exported so the shipped example is tested by the parser that reads it. */
|
||||
export function parseStalwartServers(raw: unknown, file: string): { urls: Record<string, string>; adminUrls: Record<string, string> } {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
||||
throw new Error(`Invalid STALWART_SERVERS_FILE (${file}): expected an object of domain to URL`);
|
||||
throw new Error(`Invalid MAIL_SERVERS_FILE (${file}): expected an object of domain to URL`);
|
||||
}
|
||||
|
||||
const out: Record<string, string> = {};
|
||||
@@ -233,16 +233,16 @@ export function parseStalwartServers(raw: unknown, file: string): { urls: Record
|
||||
taken off a username will arrive and comparing them any other way means
|
||||
a mapping that silently never matches. */
|
||||
const domain = rawDomain.trim().toLowerCase().replace(/\.$/, "");
|
||||
if (!domain) throw new Error(`Invalid STALWART_SERVERS_FILE (${file}): a domain key is empty`);
|
||||
if (domain in out) throw new Error(`Invalid STALWART_SERVERS_FILE (${file}): "${domain}" appears twice once normalized`);
|
||||
if (!domain) throw new Error(`Invalid MAIL_SERVERS_FILE (${file}): a domain key is empty`);
|
||||
if (domain in out) throw new Error(`Invalid MAIL_SERVERS_FILE (${file}): "${domain}" appears twice once normalized`);
|
||||
/* A domain's value is its server's URL, or an object that also names where
|
||||
that server's own administration is: `{"url": …, "adminUrl": …}`. */
|
||||
const value = rawValue && typeof rawValue === "object" && !Array.isArray(rawValue) ? (rawValue as Record<string, unknown>) : { url: rawValue };
|
||||
if (typeof value.url !== "string") throw new Error(`Invalid STALWART_SERVERS_FILE (${file}): "${domain}" is not a URL`);
|
||||
out[domain] = httpUrl(value.url, `STALWART_SERVERS_FILE (${file}): "${domain}"`);
|
||||
if (typeof value.url !== "string") throw new Error(`Invalid MAIL_SERVERS_FILE (${file}): "${domain}" is not a URL`);
|
||||
out[domain] = httpUrl(value.url, `MAIL_SERVERS_FILE (${file}): "${domain}"`);
|
||||
if (value.adminUrl !== undefined) {
|
||||
if (typeof value.adminUrl !== "string") throw new Error(`Invalid STALWART_SERVERS_FILE (${file}): "${domain}" adminUrl is not a URL`);
|
||||
adminUrls[domain] = httpUrl(value.adminUrl, `STALWART_SERVERS_FILE (${file}): "${domain}" adminUrl`);
|
||||
if (typeof value.adminUrl !== "string") throw new Error(`Invalid MAIL_SERVERS_FILE (${file}): "${domain}" adminUrl is not a URL`);
|
||||
adminUrls[domain] = httpUrl(value.adminUrl, `MAIL_SERVERS_FILE (${file}): "${domain}" adminUrl`);
|
||||
}
|
||||
}
|
||||
return { urls: out, adminUrls };
|
||||
@@ -314,12 +314,12 @@ export const config = {
|
||||
stalwartServers: stalwartServers.urls,
|
||||
/**
|
||||
* Where an administrator reaches Stalwart's own administration, for the
|
||||
* pointer on ihasmail's dashboard. Optional, and separate from STALWART_URL,
|
||||
* pointer on ihasmail's dashboard. Optional, and separate from MAIL_SERVER_URL,
|
||||
* which is how *this server* reaches Stalwart -- often an address no browser
|
||||
* can open. Unset, the dashboard names Stalwart's administration without a
|
||||
* link. A domain routed elsewhere takes its server's `adminUrl` instead.
|
||||
*/
|
||||
stalwartAdminUrl: process.env.STALWART_ADMIN_URL ? httpUrl(process.env.STALWART_ADMIN_URL, "STALWART_ADMIN_URL") : "",
|
||||
stalwartAdminUrl: process.env.ADMIN_URL ? httpUrl(process.env.ADMIN_URL, "ADMIN_URL") : "",
|
||||
stalwartAdminUrls: stalwartServers.adminUrls,
|
||||
/**
|
||||
* Say that an Enterprise-only section is Enterprise-only even on an
|
||||
@@ -385,7 +385,7 @@ export const config = {
|
||||
/* See relayPushRaw(): pipe the push stream socket-to-socket instead of through fetch(). */
|
||||
rawPushRelay: process.env.RAW_PUSH_RELAY !== "0",
|
||||
/* See absoluteUpstream(): follow Stalwart's advertised origin instead of pinning to ours. */
|
||||
followAdvertisedUrls: process.env.STALWART_FOLLOW_ADVERTISED_URLS === "1",
|
||||
followAdvertisedUrls: process.env.MAIL_SERVER_FOLLOW_ADVERTISED_URLS === "1",
|
||||
};
|
||||
|
||||
export type Config = typeof config;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
process.env.STALWART_URL = "https://default.example";
|
||||
process.env.MAIL_SERVER_URL = "https://default.example";
|
||||
|
||||
const { upstreamFor } = await import("./upstream.js");
|
||||
const { config } = await import("./config.js");
|
||||
@@ -9,7 +9,7 @@ const { config } = await import("./config.js");
|
||||
/**
|
||||
* Which Stalwart a username goes to (#238).
|
||||
*
|
||||
* `STALWART_URL` is required and is the default. The mapping only adds domains
|
||||
* `MAIL_SERVER_URL` is required and is the default. The mapping only adds domains
|
||||
* that go elsewhere, so an installation with no mapping behaves exactly as it
|
||||
* always has -- which is what these first cases pin.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
process.env.STALWART_URL = "http://127.0.0.1:1";
|
||||
process.env.MAIL_SERVER_URL = "http://127.0.0.1:1";
|
||||
process.env.APP_SECRET = "test-secret-for-ics-proxy";
|
||||
|
||||
const { safeFetch, safeFetchStatus } = await import("./imageproxy.js");
|
||||
|
||||
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
|
||||
import { createServer, request as httpRequest, type IncomingMessage, type Server } from "node:http";
|
||||
import { AddressInfo } from "node:net";
|
||||
|
||||
process.env.STALWART_URL = "http://127.0.0.1:1";
|
||||
process.env.MAIL_SERVER_URL = "http://127.0.0.1:1";
|
||||
process.env.APP_SECRET = "test-secret-for-image-proxy";
|
||||
|
||||
const { fetchPinned, isPrivateAddress } = await import("./imageproxy.js");
|
||||
|
||||
@@ -19,7 +19,7 @@ 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.STALWART_URL = `http://127.0.0.1:${PORT}`;
|
||||
process.env.MAIL_SERVER_URL = `http://127.0.0.1:${PORT}`;
|
||||
process.env.APP_SECRET = "test-secret-for-login-guard";
|
||||
|
||||
const mock = await import("./mock/index.js");
|
||||
@@ -48,13 +48,12 @@ test("a server without the registry is refused, with good credentials", async ()
|
||||
assert.equal(res.body.error, "unsupported_server");
|
||||
});
|
||||
|
||||
test("the message says the credentials were fine, and names the way out", async () => {
|
||||
test("the message says the credentials were fine, and that the server isn't supported", async () => {
|
||||
const { body } = await login({ username: "[email protected]", password: "demo-password" });
|
||||
// Someone hitting this has typed a correct password. Saying so is the
|
||||
// difference between "upgrade your server" and "try your password again".
|
||||
// difference between "wrong server" and "try your password again".
|
||||
assert.match(body.message, /credentials are fine/i);
|
||||
assert.match(body.message, /0\.16/);
|
||||
assert.match(body.message, /stalwart-0\.15-support/, "the tag to build from if they cannot upgrade");
|
||||
assert.match(body.message, /isn't one this webmail supports/);
|
||||
});
|
||||
|
||||
test("no session is minted for a server we cannot talk to", async () => {
|
||||
|
||||
@@ -226,7 +226,7 @@ export function createDirectory(opts: Options) {
|
||||
push(4, "Counter", "queue.report-queued", h % 4 === 1 ? 2 : 0);
|
||||
}
|
||||
}
|
||||
const applications: Obj[] = [{ id: "app1", description: "Stalwart Web Interface", enabled: true, urlPrefix: { "/admin": true, "/account": true } }];
|
||||
const applications: Obj[] = [{ id: "app1", description: "Web Interface", enabled: true, urlPrefix: { "/admin": true, "/account": true } }];
|
||||
|
||||
/** Tenants: a name, limits, and whatever names them in its memberTenantId. */
|
||||
const tenants: Obj[] = [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* A tiny in-memory JMAP server that mimics the subset of Stalwart that ihasmail
|
||||
* uses. For local development and demos only: `npm run mock` then point the
|
||||
* server at it with STALWART_URL=http://127.0.0.1:8788 (user: demo / pass: demo).
|
||||
* server at it with MAIL_SERVER_URL=http://127.0.0.1:8788 (user: demo / pass: demo).
|
||||
*/
|
||||
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
||||
import { parseOtpauthUrl, verifyTotp } from "../totp.js";
|
||||
@@ -205,7 +205,7 @@ export const server = createServer(async (req, res) => {
|
||||
res.end(JSON.stringify({ error: "not found" }));
|
||||
}).listen(PORT, "127.0.0.1", () => {
|
||||
console.log(`[mock-stalwart] listening on http://127.0.0.1:${PORT} (login: ${USER} / ${PASS})`);
|
||||
console.log(`[mock-stalwart] run the app with: STALWART_URL=http://127.0.0.1:${PORT} npm run dev`);
|
||||
console.log(`[mock-stalwart] run the app with: MAIL_SERVER_URL=http://127.0.0.1:${PORT} npm run dev`);
|
||||
});
|
||||
|
||||
// Periodically inject a new inbox email to demo push
|
||||
|
||||
@@ -11,7 +11,7 @@ const PORT = 18811;
|
||||
process.env.MOCK_PORT = String(PORT);
|
||||
process.env.MOCK_USER = "[email protected]";
|
||||
process.env.MOCK_PASS = "demo-password";
|
||||
process.env.STALWART_URL = `http://127.0.0.1:${PORT}`;
|
||||
process.env.MAIL_SERVER_URL = `http://127.0.0.1:${PORT}`;
|
||||
process.env.APP_SECRET = "test-secret-for-oauth";
|
||||
process.env.OAUTH_CLIENT_SECRET = "mock-oauth-secret";
|
||||
process.env.PUBLIC_URL = "https://webmail.example.test";
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ export function oauthEnabled(): boolean {
|
||||
/**
|
||||
* Whether every account is on the same server. Then sign-in needs no address
|
||||
* first: the server's page asks for the username itself. With several servers
|
||||
* (STALWART_SERVERS_FILE), the domain picks the server, so the address comes
|
||||
* (MAIL_SERVERS_FILE), the domain picks the server, so the address comes
|
||||
* first.
|
||||
*/
|
||||
export function singleServer(): boolean {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { EventEmitter } from "node:events";
|
||||
process.env.STALWART_URL = "http://127.0.0.1:1";
|
||||
process.env.MAIL_SERVER_URL = "http://127.0.0.1:1";
|
||||
process.env.PUSH_URL = "https://ihasmail.example";
|
||||
const push = await import("./push.js");
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ const PORT = 18813;
|
||||
process.env.MOCK_PORT = String(PORT);
|
||||
process.env.MOCK_USER = "[email protected]";
|
||||
process.env.MOCK_PASS = "demo-password";
|
||||
process.env.STALWART_URL = `http://127.0.0.1:${PORT}`;
|
||||
process.env.MAIL_SERVER_URL = `http://127.0.0.1:${PORT}`;
|
||||
process.env.APP_SECRET = "test-secret-for-request-limits";
|
||||
|
||||
const mock = await import("./mock/index.js");
|
||||
|
||||
@@ -59,7 +59,7 @@ test("the example's commentary cannot be mistaken for a section", () => {
|
||||
* reason: an example that no longer loads is worse than no example, because
|
||||
* the first experience of the feature is a server that refuses to start.
|
||||
*/
|
||||
const SERVERS = fileURLToPath(new URL("../../stalwart-servers.example.json", import.meta.url));
|
||||
const SERVERS = fileURLToPath(new URL("../../mail-servers.example.json", import.meta.url));
|
||||
|
||||
test("the example server mapping is valid JSON", () => {
|
||||
assert.doesNotThrow(() => JSON.parse(readFileSync(SERVERS, "utf8")));
|
||||
|
||||
@@ -29,7 +29,7 @@ writeFileSync(join(root, "sw.js"), "/* worker */\n");
|
||||
writeFileSync(join(root, "index.html"), "<!doctype html><title>t</title>");
|
||||
|
||||
process.env.STATIC_DIR = root;
|
||||
process.env.STALWART_URL = "http://127.0.0.1:1";
|
||||
process.env.MAIL_SERVER_URL = "http://127.0.0.1:1";
|
||||
const { createApp } = await import("./app.js");
|
||||
const app = createApp();
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ writeFileSync(join(root, "index.html"), "<!doctype html><title>t</title>");
|
||||
writeFileSync(join(root, "img.png"), "not really a png");
|
||||
|
||||
process.env.STATIC_DIR = root;
|
||||
process.env.STALWART_URL = "http://127.0.0.1:1";
|
||||
process.env.MAIL_SERVER_URL = "http://127.0.0.1:1";
|
||||
const { createApp } = await import("./app.js");
|
||||
|
||||
const cacheControl = async (path: string) => {
|
||||
|
||||
@@ -37,7 +37,7 @@ const SESSION_CACHE_MS = 5 * 60_000;
|
||||
/**
|
||||
* The Stalwart a username belongs to.
|
||||
*
|
||||
* `STALWART_URL` is the default and is always the answer for a domain nobody
|
||||
* `MAIL_SERVER_URL` is the default and is always the answer for a domain nobody
|
||||
* mapped -- and for a bare username, which Stalwart accepts and which has no
|
||||
* domain to map (#238).
|
||||
*
|
||||
@@ -59,7 +59,7 @@ export function upstreamFor(username: string): string {
|
||||
* Where the administrator signed in as `username` opens Stalwart's own
|
||||
* administration.
|
||||
*
|
||||
* What the operator configured wins -- STALWART_ADMIN_URL for the default
|
||||
* What the operator configured wins -- ADMIN_URL for the default
|
||||
* server, a servers file entry's `adminUrl` for a routed domain -- and what was
|
||||
* found on the account's own server (`detected`) is used otherwise. Routing is
|
||||
* the same as `upstreamFor`: a routed domain is never pointed at the default
|
||||
@@ -93,7 +93,7 @@ export function adminPrefixFrom(responses: [string, Record<string, unknown>, str
|
||||
/**
|
||||
* The public origin a Stalwart session belongs to: the host it advertises in
|
||||
* its own URLs, which is the address people reach it at even when this server
|
||||
* talks to it on a private one (STALWART_URL=http://127.0.0.1:…). A relative
|
||||
* talks to it on a private one (MAIL_SERVER_URL=http://127.0.0.1:…). A relative
|
||||
* URL falls back to the configured base.
|
||||
*/
|
||||
export function advertisedOrigin(session: Pick<UpstreamSession, "apiUrl" | "baseUrl">): string | null {
|
||||
@@ -427,7 +427,7 @@ export function localizeSession(s: UpstreamSession, extras: Record<string, unkno
|
||||
};
|
||||
}
|
||||
|
||||
/** Resolve a possibly-relative upstream URL template against STALWART_URL. */
|
||||
/** Resolve a possibly-relative upstream URL template against MAIL_SERVER_URL. */
|
||||
/**
|
||||
* Resolve a URL Stalwart handed us against the server we were configured to
|
||||
* talk to.
|
||||
@@ -435,7 +435,7 @@ export function localizeSession(s: UpstreamSession, extras: Record<string, unkno
|
||||
* Stalwart advertises absolute URLs in its session -- apiUrl, eventSourceUrl
|
||||
* and the rest -- built from its public hostname, which is always https. A
|
||||
* proxy that follows them takes every upstream call, and every held push
|
||||
* stream, out through the public route even when STALWART_URL names a private
|
||||
* stream, out through the public route even when MAIL_SERVER_URL names a private
|
||||
* plain-HTTP hop on the same network. Measured, that TLS leg is ~80 KiB of
|
||||
* native OpenSSL state per signed-in tab: 60% of what a tab costs, and the
|
||||
* whole difference between 1,665 and 3,680 tabs in 256 MiB.
|
||||
@@ -443,7 +443,7 @@ export function localizeSession(s: UpstreamSession, extras: Record<string, unkno
|
||||
* So by default only the path and query are taken from the advertised URL;
|
||||
* scheme, host and port come from the configured base. That is what a proxy
|
||||
* should have done all along -- the operator named the route on purpose.
|
||||
* STALWART_FOLLOW_ADVERTISED_URLS=1 restores the old behavior for a setup
|
||||
* MAIL_SERVER_FOLLOW_ADVERTISED_URLS=1 restores the old behavior for a setup
|
||||
* that genuinely needs to reach Stalwart at a different origin than the one
|
||||
* it was given.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user