// mailu's provisioner — the adapter that makes mailu a provider of the mesh `smtp` interface. // The reconcile loop, the contributions file, and reading the mesh's minted password are the sdk // harness's; this writes only the per-service half: how mailu creates and removes a consumer's // sending account (novox/hq ADR 0048/0076, gitea's package-registry provisioner is the sibling). // // The `smtp` interface: a consumer authenticates to submission (port 587, STARTTLS) as a real // mailbox this provisioner creates. The address is `@`: the local part is the // consumer's `account` contribution — the name it wants to send as — falling back to the mesh's // own login for a consumer that named none; the domain is the mail server's, which is this // module's fact, not the consumer's. // // **The password is the mesh's, not the provisioner's (ADR 0048).** The mesh mints it and hands // it to both ends; mailu sets exactly that password every run — so a rotation takes — and seals // nothing: the consumer already has its copy through the mesh's own channel. import { readFileSync } from "node:fs"; import { runProvisioner, type Provision } from "@novox/mesh-sdk/provisioner"; import { MailuClient } from "../client.js"; const mailu = MailuClient.fromEnv(); // The mail server's own domain: the operator's value, from the settings the mesh merges into this // module's config file (`settings set mailu` with {"domain": …}; novox/hq ADR 0112, ADR 0155). A // definition names no mesh, so it is never a literal in the manifest — and it used to be, as // MESH_MAILU_DOMAIN, which is still read for a mesh that has not re-registered the manifest. function domain(): string { const file = process.env.MESH_MAILU_CONFIG_FILE; if (file) { try { const config = JSON.parse(readFileSync(file, "utf8")) as { domain?: unknown }; if (typeof config.domain === "string" && config.domain.trim() !== "") return config.domain.trim(); } catch { // Unreadable or not JSON: fall through to the environment, and the error below names both. } } const named = (process.env.MESH_MAILU_DOMAIN ?? "").trim(); if (named === "") { throw new Error( "no mail domain is set, so a consumer's address cannot be composed — `settings set mailu ` " + 'with {"domain": ""}', ); } return named; } // The address one consumer sends as. The local part is refused rather than sanitised when it is // not a plain mailbox name — a rewritten name is an address nobody asked for. function addressOf(p: { as: string; values?: Readonly> }): string { const contributed = typeof p.values?.["account"] === "string" ? (p.values["account"] as string).trim() : ""; const local = contributed !== "" ? contributed : p.as; if (!/^[a-z0-9][a-z0-9._-]*$/.test(local)) { throw new Error(`${JSON.stringify(local)} is not a usable mailbox name`); } return `${local}@${domain()}`; } runProvisioner("smtp", { async create(p: Provision): Promise { const email = addressOf(p); // Create if absent, and set exactly the minted password either way so a rotation takes. // Mailu's create refuses a duplicate address, which is the signal to fall through to the // password set — the same found-then-apply shape gitea's ensureUser settled on. try { await mailu.createUser(email, p.password); } catch { await mailu.applyProvisioned(email, p.password); } }, async remove(p: { as: string }): Promise { // The withdrawal only knows the mesh login, never the contributed local part — so accounts // that contributed one are removed when the address matching the login is absent? No: the // harness hands remove only `as`, and an address composed from a contribution cannot be // recomputed from it. The account is therefore removed by its login-shaped address when one // exists, and left otherwise — a mailbox holding mail is the one thing a background loop // must not guess about (this module's own events file says the same). Withdrawal of a // named-account consumer is an operator action until the harness carries values here. await mailu.deleteUser(`${p.as}@${domain()}`).catch(() => {}); }, // Asked every minute by the harness: whether the backend still holds this consumer exactly as // the mesh gave it, so a login lost behind the provisioner's back is made again (novox/hq issue 120). async holds(p: Provision): Promise { return mailu.holdsUser(addressOf(p)); }, });