mailu: the manifest matches the machine, provides smtp, and carries automx

Five gaps between the draft and what actually runs, each verified live
before being written down:

- front published bare 80 — the machine port Traefik holds; now the
  predecessor's own mappings (7080:80, 7443:443) plus the 110/143/995
  parity ports the draft dropped. Pruning legacy protocols is its own
  deliberate change, not a cutover side effect.
- TLS_FLAVOR said cert, which nothing supplies; live is letsencrypt —
  mailu runs its own certbot, state already on disk, HTTP-01 answered
  through a path-scoped route contribution (priority above the web one).
- the web route said http:7080, the redirect-loop shape; it now says
  what the hand-authored file always knew: https 7443, insecure.
- automx was absent entirely: the autoconfig responder is now a second
  artifact (its Containerfile moved in from the predecessor's images
  dir, base declared per ADR 0097), a container on a real data dir —
  the anonymous-volume loss of 2026-08-10 stays fixed — and the three
  public names are route contributions.
- and the reason this moved ahead of de-spiegel: mailu now provides
  smtp. A consumer contributes the account it sends as; the provisioner
  creates <account>@<domain> via the admin API and applies the minted
  password every reconcile (ADR 0048). The domain is served on the
  binding so a consumer composes its own login from mesh facts.

route-adapter learns to say no: a contribution over https, scoped to a
path, or carrying a policy is skipped aloud rather than written into a
file shape that cannot say it — plain http into a TLS listener was the
concrete wrong file this prevents. The hand-authored files keep covering
those routes until the mesh's own proxy takes over, exactly as today.
This commit is contained in:
2026-09-25 22:39:29 +02:00
parent 3875987656
commit ed5d1386ce
12 changed files with 473 additions and 9 deletions
+18
View File
@@ -151,6 +151,24 @@ export function routesFrom(document: unknown, machine: string): { routes: Route[
skipped.push(`${from} asked for ${JSON.stringify(name)}, which is not a name this can write`);
continue;
}
// What this adapter's one file shape cannot say, it skips aloud rather than approximating:
// a backend over its own TLS (the file would send plain http into a TLS listener), a
// path-scoped or refusing or redirecting rule (the file routes whole hosts). The mesh's own
// proxy serves all of these the day it takes over; until then the predecessor's hand-authored
// files keep covering them, exactly as they do today.
const scheme = typeof entry.values?.["scheme"] === "string" ? (entry.values["scheme"] as string).trim().toLowerCase() : "";
if (scheme !== "" && scheme !== "http") {
skipped.push(`${from} asked for route ${name} over ${scheme}, which this file shape cannot say`);
continue;
}
if (typeof entry.values?.["path"] === "string" && (entry.values["path"] as string).trim() !== "") {
skipped.push(`${from} asked for route ${name} scoped to a path, which this file shape cannot say`);
continue;
}
if (entry.values?.["deny"] === true || typeof entry.values?.["redirect"] === "string") {
skipped.push(`${from} asked for route ${name} with a policy this file shape cannot say`);
continue;
}
const port = asPort(entry.values?.["port"]);
if (port === undefined) {
skipped.push(`${from} asked for route ${name} and gave no usable port`);