Merge pull request 'Capture automx's live answers as fixtures and add a read-only tool for its database rows, before R4d replaces it (issue 1630)' (#415) from feat/r4d-automx into main
This commit was merged in pull request #415.
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
// 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 the one query that reads it. Fixed text: the tool takes no query from its caller. */
|
||||
export const AUTOMX_TABLES: Record<string, string> = {
|
||||
provider: "SELECT id, name, short_name FROM provider ORDER BY id",
|
||||
server: "SELECT id, prio, name, port, type, socket_type, user_name, authentication FROM server ORDER BY id",
|
||||
domain: "SELECT id, name, provider_id, ldapserver_id FROM domain ORDER BY id",
|
||||
server_domain: "SELECT server_id, domain_id FROM server_domain ORDER BY domain_id, server_id",
|
||||
};
|
||||
|
||||
export type Row = Record<string, string | number | null>;
|
||||
|
||||
export type AutomxRows = {
|
||||
database: string;
|
||||
tables: Record<string, Row[]>;
|
||||
/** 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<string, string>;
|
||||
};
|
||||
|
||||
/** sqlite3 -json prints one JSON array, or nothing at all for a table without rows. */
|
||||
export function parseRows(output: string): Row[] {
|
||||
const text = output.trim();
|
||||
if (text === "") return [];
|
||||
const rows = JSON.parse(text) as unknown;
|
||||
if (!Array.isArray(rows)) throw new Error("sqlite3 did not print a list of rows");
|
||||
return rows as Row[];
|
||||
}
|
||||
|
||||
/** The command that reads one table: read-only, JSON, one fixed statement. */
|
||||
export function readCommand(sql: string): string[] {
|
||||
return ["sqlite3", "-readonly", "-json", AUTOMX_DATABASE, sql];
|
||||
}
|
||||
|
||||
/** 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<AutomxRows> {
|
||||
const answer: AutomxRows = { database: AUTOMX_DATABASE, tables: {}, empty_names: [], unreadable: {} };
|
||||
for (const [table, sql] of Object.entries(AUTOMX_TABLES)) {
|
||||
try {
|
||||
answer.tables[table] = parseRows(await exec(container, readCommand(sql)));
|
||||
} 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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>novox.be</domain><displayName>novox.be</displayName><displayShortName>novox.be</displayShortName><incomingServer type="imap"><hostname /><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.novox.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Length: 612
|
||||
Content-Type: application/xml
|
||||
Server: Werkzeug/3.1.9 Python/3.9.5
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
GET https://autoconfig.novox.be/mail/config-v1.1.xml?emailaddress=jochen@novox.be
|
||||
User-Agent: Thunderbird
|
||||
Accept: */*
|
||||
+1
@@ -0,0 +1 @@
|
||||
200 OK
|
||||
@@ -0,0 +1 @@
|
||||
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>novox.be</domain><displayName>novox.be</displayName><displayShortName>novox.be</displayShortName><incomingServer type="imap"><hostname /><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.novox.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Length: 612
|
||||
Content-Type: application/xml
|
||||
Server: Werkzeug/3.1.9 Python/3.9.5
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
GET https://automx.novox.be/mail/config-v1.1.xml?emailaddress=jochen@novox.be
|
||||
User-Agent: Thunderbird
|
||||
+1
@@ -0,0 +1 @@
|
||||
200 OK
|
||||
+1
@@ -0,0 +1 @@
|
||||
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>novox.be</domain><displayName>novox.be</displayName><displayShortName>novox.be</displayShortName><incomingServer type="imap"><hostname /><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.novox.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Length: 612
|
||||
Content-Type: application/xml
|
||||
Server: Werkzeug/3.1.9 Python/3.9.5
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
GET https://autodiscover.novox.be/mail/config-v1.1.xml?emailaddress=jochen@novox.be
|
||||
User-Agent: Thunderbird
|
||||
+1
@@ -0,0 +1 @@
|
||||
200 OK
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Server: Werkzeug/3.1.9 Python/3.9.5
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
GET https://autoconfig.novox.be/mail/config-v1.1.xml?emailaddress=nobody@unknown.example
|
||||
User-Agent: Thunderbird
|
||||
+1
@@ -0,0 +1 @@
|
||||
204 No Content
|
||||
+1
@@ -0,0 +1 @@
|
||||
Missing request argument "emailaddress"
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Length: 39
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Server: Werkzeug/3.1.9 Python/3.9.5
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
GET https://autoconfig.novox.be/mail/config-v1.1.xml
|
||||
User-Agent: Thunderbird
|
||||
+1
@@ -0,0 +1 @@
|
||||
400 Bad Request
|
||||
@@ -0,0 +1 @@
|
||||
<html><body>Show Thunderbird-style <a href="/mail/config-v1.1.xml?emailaddress=abc@example.com">XML configuration</a> for abc@example.com.</body></html>
|
||||
@@ -0,0 +1,3 @@
|
||||
Content-Length: 152
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Server: Werkzeug/3.1.9 Python/3.9.5
|
||||
@@ -0,0 +1,2 @@
|
||||
GET https://autoconfig.novox.be/
|
||||
User-Agent: Mozilla/5.0
|
||||
@@ -0,0 +1 @@
|
||||
200 OK
|
||||
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang=en>
|
||||
<title>404 Not Found</title>
|
||||
<h1>Not Found</h1>
|
||||
<p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Length: 207
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Server: Werkzeug/3.1.9 Python/3.9.5
|
||||
+1
@@ -0,0 +1 @@
|
||||
GET https://autoconfig.novox.be/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress=jochen@novox.be
|
||||
+1
@@ -0,0 +1 @@
|
||||
404 Not Found
|
||||
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang=en>
|
||||
<title>500 Internal Server Error</title>
|
||||
<h1>Internal Server Error</h1>
|
||||
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Length: 265
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Server: Werkzeug/3.1.9 Python/3.9.5
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
POST https://autodiscover.novox.be/autodiscover/autodiscover.xml
|
||||
User-Agent: Microsoft Office/16.0 (Windows NT 10.0; Microsoft Outlook 16.0.14326; Pro)
|
||||
Content-Type: text/xml
|
||||
Accept: */*
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
|
||||
<Request>
|
||||
<EMailAddress>jochen@novox.be</EMailAddress>
|
||||
<AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
|
||||
</Request>
|
||||
</Autodiscover>
|
||||
+1
@@ -0,0 +1 @@
|
||||
500 Internal Server Error
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Server: Werkzeug/3.1.9 Python/3.9.5
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
POST https://autodiscover.novox.be/autodiscover/autodiscover.xml
|
||||
User-Agent: Microsoft Office/16.0
|
||||
Content-Type: text/xml
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006"><Request><EMailAddress>nobody@unknown.example</EMailAddress><AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema></Request></Autodiscover>
|
||||
+1
@@ -0,0 +1 @@
|
||||
204 No Content
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang=en>
|
||||
<title>500 Internal Server Error</title>
|
||||
<h1>Internal Server Error</h1>
|
||||
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Length: 265
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Server: Werkzeug/3.1.9 Python/3.9.5
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
POST https://autodiscover.novox.be/autodiscover/autodiscover.xml
|
||||
User-Agent: Microsoft Office/16.0
|
||||
Content-Type: text/xml
|
||||
|
||||
this is not xml
|
||||
+1
@@ -0,0 +1 @@
|
||||
500 Internal Server Error
|
||||
+1
@@ -0,0 +1 @@
|
||||
Missing request argument "EMailAddress"
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Length: 39
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Server: Werkzeug/3.1.9 Python/3.9.5
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
POST https://autodiscover.novox.be/autodiscover/autodiscover.xml
|
||||
User-Agent: Microsoft Office/16.0
|
||||
Content-Type: text/xml
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006"><Request><AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema></Request></Autodiscover>
|
||||
+1
@@ -0,0 +1 @@
|
||||
400 Bad Request
|
||||
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang=en>
|
||||
<title>405 Method Not Allowed</title>
|
||||
<h1>Method Not Allowed</h1>
|
||||
<p>The method is not allowed for the requested URL.</p>
|
||||
@@ -0,0 +1,4 @@
|
||||
Allow: POST, OPTIONS
|
||||
Content-Length: 153
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Server: Werkzeug/3.1.9 Python/3.9.5
|
||||
@@ -0,0 +1,2 @@
|
||||
GET https://autodiscover.novox.be/autodiscover/autodiscover.xml
|
||||
User-Agent: Microsoft Office/16.0
|
||||
@@ -0,0 +1 @@
|
||||
405 Method Not Allowed
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang=en>
|
||||
<title>404 Not Found</title>
|
||||
<h1>Not Found</h1>
|
||||
<p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Length: 207
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Server: Werkzeug/3.1.9 Python/3.9.5
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
POST https://autodiscover.novox.be/Autodiscover/Autodiscover.xml
|
||||
User-Agent: Microsoft Office/16.0
|
||||
Content-Type: text/xml
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006"><Request><EMailAddress>jochen@novox.be</EMailAddress><AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema></Request></Autodiscover>
|
||||
+1
@@ -0,0 +1 @@
|
||||
404 Not Found
|
||||
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang=en>
|
||||
<title>Redirecting...</title>
|
||||
<h1>Redirecting...</h1>
|
||||
<p>You should be redirected automatically to the target URL: <a href="https://automx.novox.be/mobileconfig/?emailaddress=jochen@novox.be">https://automx.novox.be/mobileconfig/?emailaddress=jochen@novox.be</a>. If not, click the link.
|
||||
@@ -0,0 +1,4 @@
|
||||
Content-Length: 319
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Location: https://automx.novox.be/mobileconfig/?emailaddress=jochen@novox.be
|
||||
Server: Werkzeug/3.1.9 Python/3.9.5
|
||||
@@ -0,0 +1,3 @@
|
||||
GET https://automx.novox.be/mobileconfig?emailaddress=jochen@novox.be
|
||||
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) Safari
|
||||
Accept: */*
|
||||
@@ -0,0 +1 @@
|
||||
308 Permanent Redirect
|
||||
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang=en>
|
||||
<title>400 Bad Request</title>
|
||||
<h1>Bad Request</h1>
|
||||
<p>The browser (or proxy) sent a request that this server could not understand.</p>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Length: 167
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Server: Werkzeug/3.1.9 Python/3.9.5
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
GET https://automx.novox.be/mobileconfig/?emailaddress=jochen@novox.be
|
||||
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) Safari
|
||||
Accept: */*
|
||||
+1
@@ -0,0 +1 @@
|
||||
400 Bad Request
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Server: Werkzeug/3.1.9 Python/3.9.5
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
GET https://automx.novox.be/mobileconfig/?emailaddress=nobody@unknown.example
|
||||
User-Agent: Mozilla/5.0 (iPhone)
|
||||
+1
@@ -0,0 +1 @@
|
||||
204 No Content
|
||||
+1
@@ -0,0 +1 @@
|
||||
Missing request argument "emailaddress"
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Length: 39
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Server: Werkzeug/3.1.9 Python/3.9.5
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
GET https://automx.novox.be/mobileconfig/
|
||||
User-Agent: Mozilla/5.0 (iPhone)
|
||||
+1
@@ -0,0 +1 @@
|
||||
400 Bad Request
|
||||
@@ -0,0 +1,30 @@
|
||||
What mailu's autoconfiguration module (automx2 2021.6, container mailu-automx on novox, Werkzeug/3.1.9 Python/3.9.5)
|
||||
answers to the requests mail clients make (R4d step AMX-1, issue 1630, ADR 0480).
|
||||
|
||||
Captured 2026-10-21 between 13:58:25 and 13:58:52 (CEST; the Date headers said 11:58 GMT) with the mesh tool
|
||||
shanks/netcheck.netcheck_request, called from shanks, through the names clients use (autoconfig.novox.be,
|
||||
autodiscover.novox.be, automx.novox.be: HTTPS, the mesh's route), no ssh. A redirect is reported, never followed.
|
||||
Mail domain served: novox.be. Mailbox used: jochen@novox.be; unknown domain: nobody@unknown.example.
|
||||
|
||||
Each capture is four files with the same stem:
|
||||
<stem>.request method and URL, then the request headers sent, then (after a blank line) the body sent
|
||||
<stem>.status the status line
|
||||
<stem>.headers the response headers, sorted by name, without Date (it changes on every request)
|
||||
<stem>.body the response body, byte for byte (Content-Length in .headers equals its size)
|
||||
The files were written from the tool's answers by a script (not by retyping each by hand); the tool returns JSON, so
|
||||
a later capture that wants exact bytes needs a tool that writes the answer to a file.
|
||||
|
||||
What the answers show (read, not guessed):
|
||||
- GET /mail/config-v1.1.xml?emailaddress=... answers 200 application/xml on all three names; same body on each.
|
||||
- The incoming (IMAP) server has an EMPTY hostname (<hostname />), port 143, STARTTLS. The outgoing server is
|
||||
mail.novox.be:587 STARTTLS. Cause (from reading the module, not from the database): automx/files/add-domains:35
|
||||
reads AMX_IMAP_SERVER, files/mailu.env:58 sets AMX_IMAP_ADDRESS, so the IMAP name was never written.
|
||||
A client given this answer cannot find its IMAP server.
|
||||
- POST /autodiscover/autodiscover.xml with a valid request for the served domain answers 500 (the Outlook
|
||||
answer for a real mailbox does not work today). Unknown domain: 204 no body. Malformed body: 500.
|
||||
No EMailAddress element: 400 "Missing request argument "EMailAddress"". GET: 405 with Allow: POST, OPTIONS.
|
||||
The path is case sensitive (/Autodiscover/Autodiscover.xml is 404).
|
||||
- GET /mobileconfig?emailaddress=... redirects (308) to /mobileconfig/?emailaddress=...; for the served domain that
|
||||
answers 400 with no profile; unknown domain 204; no emailaddress 400.
|
||||
- GET / on autoconfig answers 200 with a small HTML page linking to an example address.
|
||||
- Not captured: the database rows (no mesh tool reads /data/db.sqlite; issue 1630 AC4).
|
||||
@@ -71,7 +71,8 @@
|
||||
"mailu_list_domains",
|
||||
"mailu_read_mail",
|
||||
"mailu_search_mail",
|
||||
"mailu_health"
|
||||
"mailu_health",
|
||||
"automx_rows"
|
||||
],
|
||||
"emits": [
|
||||
"user.created",
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// What automx_rows reads (novox/hq issue 1630): the rows of automx2's database as stored, through a read-only
|
||||
// sqlite3 inside its container. The database here is a real SQLite file made by the test with the tables
|
||||
// automx/files/setup-db creates and the rows add-domains writes — with the IMAP server's name empty, the case
|
||||
// the live instance is in. The fake container runs each command's statement against that database and prints
|
||||
// it the way `sqlite3 -json` does (one JSON array, nothing for no rows). Run against the compiled module.
|
||||
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
|
||||
import { readAutomx, readCommand, AUTOMX_DATABASE } from "../dist/automx.js";
|
||||
|
||||
function database(imapName: string): DatabaseSync {
|
||||
const db = new DatabaseSync(":memory:");
|
||||
db.exec(`
|
||||
CREATE TABLE provider(id INT PRIMARY KEY NOT NULL, name TEXT NOT NULL, short_name TEXT NOT NULL);
|
||||
CREATE TABLE server(id INT PRIMARY KEY NOT NULL, prio INT NOT NULL DEFAULT 10, name TEXT NOT NULL, port INT NOT NULL,
|
||||
type TEXT NOT NULL, socket_type TEXT NOT NULL, user_name TEXT NOT NULL, authentication TEXT NOT NULL);
|
||||
CREATE TABLE domain(id INT PRIMARY KEY NOT NULL, name TEXT NOT NULL, provider_id INT NOT NULL, ldapserver_id INT NULL);
|
||||
CREATE TABLE server_domain(server_id INT NOT NULL, domain_id INT NOT NULL);
|
||||
INSERT INTO provider VALUES (1, 'example.org', 'example.org');
|
||||
INSERT INTO server(id, port, type, name, socket_type, user_name, authentication)
|
||||
VALUES (2, 143, 'imap', '${imapName}', 'STARTTLS', '%EMAILADDRESS%', 'password-cleartext');
|
||||
INSERT INTO server(id, port, type, name, socket_type, user_name, authentication)
|
||||
VALUES (3, 587, 'smtp', 'mail.example.org', 'STARTTLS', '%EMAILADDRESS%', 'password-cleartext');
|
||||
INSERT INTO domain VALUES (5, 'example.org', 1, NULL);
|
||||
INSERT INTO server_domain VALUES (2, 5), (3, 5);
|
||||
`);
|
||||
return db;
|
||||
}
|
||||
|
||||
function container(db: DatabaseSync, asked: string[][] = []) {
|
||||
return async (_name: string, command: string[]) => {
|
||||
asked.push(command);
|
||||
const rows = db.prepare(command[command.length - 1]).all();
|
||||
return rows.length === 0 ? "" : JSON.stringify(rows) + "\n";
|
||||
};
|
||||
}
|
||||
|
||||
test("the rows come back as stored, and an empty server name is said", async () => {
|
||||
const answer = await readAutomx(container(database("")), "mailu-automx");
|
||||
assert.equal(answer.database, AUTOMX_DATABASE);
|
||||
assert.deepEqual(answer.unreadable, {});
|
||||
assert.deepEqual(answer.tables.provider, [{ id: 1, name: "example.org", short_name: "example.org" }]);
|
||||
assert.deepEqual(answer.tables.domain, [{ id: 5, name: "example.org", provider_id: 1, ldapserver_id: null }]);
|
||||
assert.deepEqual(answer.tables.server_domain, [{ server_id: 2, domain_id: 5 }, { server_id: 3, domain_id: 5 }]);
|
||||
assert.deepEqual(
|
||||
answer.tables.server.map((s) => [s.id, s.type, s.name, s.port]),
|
||||
[[2, "imap", "", 143], [3, "smtp", "mail.example.org", 587]],
|
||||
);
|
||||
assert.deepEqual(answer.empty_names, [{ id: 2, type: "imap" }]);
|
||||
});
|
||||
|
||||
test("a named IMAP server is not reported empty", async () => {
|
||||
const answer = await readAutomx(container(database("mail.example.org")), "mailu-automx");
|
||||
assert.deepEqual(answer.empty_names, []);
|
||||
});
|
||||
|
||||
test("every command opens the file read-only and runs one fixed SELECT", async () => {
|
||||
const asked: string[][] = [];
|
||||
await readAutomx(container(database("x"), asked), "mailu-automx");
|
||||
assert.equal(asked.length, 4);
|
||||
for (const c of asked) {
|
||||
assert.deepEqual(c.slice(0, 4), ["sqlite3", "-readonly", "-json", AUTOMX_DATABASE]);
|
||||
assert.match(c[4], /^SELECT /);
|
||||
assert.equal(c.length, 5);
|
||||
}
|
||||
assert.deepEqual(readCommand("SELECT 1").length, 5);
|
||||
});
|
||||
|
||||
test("a table that cannot be read is said and does not hide the others", async () => {
|
||||
const db = database("x");
|
||||
const exec = async (name: string, command: string[]) => {
|
||||
if (command[4].includes("FROM domain ")) throw Object.assign(new Error("Command failed: docker exec"), { stderr: "Error: unable to open database file\nmore" });
|
||||
return container(db)(name, command);
|
||||
};
|
||||
const answer = await readAutomx(exec, "mailu-automx");
|
||||
assert.deepEqual(answer.unreadable, { domain: "Error: unable to open database file" });
|
||||
assert.equal(answer.tables.provider.length, 1);
|
||||
assert.equal(answer.tables.domain, undefined);
|
||||
});
|
||||
@@ -135,6 +135,7 @@ test("the health tool is served without the admin API, on the smtp container it
|
||||
await import("../dist/tools/index.js");
|
||||
const mailu = collectTools({ MESH_MAILU_SMTP_CONTAINER: "mailu-smtp" }).find((m) => m.module === "mailu");
|
||||
const names = (mailu?.tools ?? []).map((t) => t.name);
|
||||
assert.deepEqual(names, ["mailu_health"]);
|
||||
assert.deepEqual(names, ["mailu_health", "automx_rows"]);
|
||||
assert.match(mailu!.tools[0].description, /inside mailu-smtp/);
|
||||
assert.match(mailu!.tools[1].description, /read-only, inside mailu-automx/);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools";
|
||||
import { MailuClient } from "../client.js";
|
||||
import { readAutomx } from "../automx.js";
|
||||
import { dockerExec, mailFlows, DEFERRED_BOUND_SECONDS, PUBLIC_NAME, SPREAD_DOMAINS } from "../health.js";
|
||||
|
||||
/**
|
||||
@@ -26,6 +27,23 @@ export function getHealthTool(env: NodeJS.ProcessEnv): ToolDefinition {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* What automx holds, as stored (novox/hq issue 1630): the rows behind the answers it gives mail clients. It needs
|
||||
* no admin API, only the automx container, so like the health tool it is served without the API.
|
||||
*/
|
||||
export function getAutomxTool(env: NodeJS.ProcessEnv): ToolDefinition {
|
||||
const container = env.MESH_MAILU_AUTOMX_CONTAINER || "mailu-automx";
|
||||
return {
|
||||
name: "automx_rows",
|
||||
description:
|
||||
`Read the autoconfiguration module's database, as stored, read-only, inside ${container}: its provider, ` +
|
||||
"server, domain and server_domain rows, and the servers whose name is empty (a mail client is then told " +
|
||||
"an empty host name). Writes nothing.",
|
||||
input: {},
|
||||
run: async () => readAutomx(dockerExec(), container),
|
||||
};
|
||||
}
|
||||
|
||||
export function getMailuTools(mailu: MailuClient): ToolDefinition[] {
|
||||
return [
|
||||
{
|
||||
@@ -173,5 +191,5 @@ registerModuleTools("mailu", (env) => {
|
||||
} catch {
|
||||
tools = [];
|
||||
}
|
||||
return [...tools, getHealthTool(env)];
|
||||
return [...tools, getHealthTool(env), getAutomxTool(env)];
|
||||
});
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["client.ts", "health.ts", "index.ts", "tools/index.ts"]
|
||||
"include": ["client.ts", "health.ts", "automx.ts", "index.ts", "tools/index.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user