// What the autoconfiguration module (automx2) holds, as stored: its provider, server, domain and server_domain // rows, read from its SQLite file inside its container with the sqlite3 program the image carries, opened // read-only. Nothing is written and nothing is interpreted: the rows are the evidence for what automx answers // to mail clients (novox/hq issue 1630, ADR 0480), and the reason the answer may be empty. import type { Exec } from "./health.js"; export const AUTOMX_DATABASE = "/data/db.sqlite"; /** The tables read, each with its columns and its ordering. Fixed text: the tool takes no query from its caller. */ export const AUTOMX_TABLES: Record = { provider: { columns: ["id", "name", "short_name"], order: "id" }, server: { columns: ["id", "prio", "name", "port", "type", "socket_type", "user_name", "authentication"], order: "id" }, domain: { columns: ["id", "name", "provider_id", "ldapserver_id"], order: "id" }, server_domain: { columns: ["server_id", "domain_id"], order: "domain_id, server_id" }, }; /** * The one statement that reads a table. The sqlite3 in the mailu-automx image does not know `-json` (it answers * "sqlite3: Error: unknown option: -json", issue 1630), so each column is printed as `:`, which * the oldest sqlite3 prints in its default list mode: no quoting, no separator or newline inside a cell, and a NULL * (`null:`) apart from an empty text (`text:`). */ export function selectOf(table: string): string { const t = AUTOMX_TABLES[table]; return `SELECT ${t.columns.map((c) => `typeof(${c}) || ':' || hex(${c})`).join(", ")} FROM ${table} ORDER BY ${t.order}`; } export type Row = Record; export type AutomxRows = { database: string; tables: Record; /** servers whose name is empty: the client is told an empty host name for them */ empty_names: { id: number; type: string }[]; /** tables that could not be read, each with what sqlite3 or docker said on one line */ unreadable: Record; }; /** One cell as selectOf prints it: `:`; text is the bytes read as UTF-8, a number is read from its text. */ function cellOf(cell: string): string | number | null { const at = cell.indexOf(":"); if (at < 0 || cell.length - at - 1 > 0 && !/^([0-9A-Fa-f]{2})+$/.test(cell.slice(at + 1))) throw new Error(`sqlite3 printed ${JSON.stringify(cell.slice(0, 40))}, not a cell`); const type = cell.slice(0, at); const bytes = Buffer.from(cell.slice(at + 1), "hex").toString("utf8"); switch (type) { case "null": return null; case "integer": case "real": return Number(bytes); default: return bytes; } } /** sqlite3 prints one line per row, cells apart by `|`, and nothing at all for a table without rows. */ export function parseRows(table: string, output: string): Row[] { const columns = AUTOMX_TABLES[table].columns; const rows: Row[] = []; for (const line of output.split("\n")) { if (line.trim() === "") continue; const cells = line.replace(/\r$/, "").split("|"); if (cells.length !== columns.length) throw new Error(`sqlite3 printed ${cells.length} cells for ${columns.length} columns`); rows.push(Object.fromEntries(columns.map((c, i) => [c, cellOf(cells[i])]))); } return rows; } /** The command that reads one table: read-only, one fixed statement. */ export function readCommand(table: string): string[] { return ["sqlite3", "-readonly", AUTOMX_DATABASE, selectOf(table)]; } /** Read every table; one that fails is said in `unreadable` and does not hide the others. */ export async function readAutomx(exec: Exec, container: string): Promise { const answer: AutomxRows = { database: AUTOMX_DATABASE, tables: {}, empty_names: [], unreadable: {} }; for (const table of Object.keys(AUTOMX_TABLES)) { try { answer.tables[table] = parseRows(table, await exec(container, readCommand(table))); } catch (e) { const err = e as { stderr?: string; message?: string }; const text = (err?.stderr ?? "").trim() || (err?.message ?? String(e)).trim(); answer.unreadable[table] = text.split("\n").map((l) => l.trim()).filter(Boolean)[0] ?? "it failed and said nothing"; } } for (const s of answer.tables.server ?? []) { if (String(s.name ?? "").trim() === "") answer.empty_names.push({ id: Number(s.id), type: String(s.type) }); } return answer; }