refactor(mail): one definition of the standard folders, and reconcile the drift
The console created Reports, the CLI that makes the first administrator did not, so that account had no folder for DMARC and TLS-RPT reports. The list now lives in one SQL function, provision_default_folders (migration 0014), which both call. The same migration reconciles what the two paths had left wrong in existing mailboxes: folder counters and the mailbox's used bytes are recomputed from the rows (only where they differ, so it is safe to run again). create-admin can now be tested: fail() throws instead of exiting, and the command runs only when invoked as one. The new test fails on the old code (no Reports).
This commit is contained in:
1 parent
caf52c0225
commit
a8941a6191
6 files changed
+310
-37
No files matched your search
+22
-25
@@ -13,22 +13,19 @@
|
||||
* node dist/cli.js reset-mfa <email>
|
||||
*/
|
||||
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { query, queryOne, closePool } from "./db.js";
|
||||
import { hashPassword } from "./crypto.js";
|
||||
|
||||
const SYSTEM_ALIASES = ["postmaster", "abuse", "dmarc", "tlsrpt"];
|
||||
const STANDARD_FOLDERS: Array<[string, string]> = [
|
||||
["INBOX", "inbox"],
|
||||
["Sent", "sent"],
|
||||
["Drafts", "drafts"],
|
||||
["Trash", "trash"],
|
||||
["Junk", "junk"],
|
||||
["Archive", "archive"],
|
||||
];
|
||||
|
||||
/** A refusal the operator should read, as opposed to a bug. */
|
||||
export class CliError extends Error {}
|
||||
|
||||
// Thrown rather than exiting here, so the commands can be exercised from a test;
|
||||
// main() prints it as "error: ..." and exits 1, exactly as this used to.
|
||||
function fail(message: string): never {
|
||||
console.error(`error: ${message}`);
|
||||
process.exit(1);
|
||||
throw new CliError(message);
|
||||
}
|
||||
|
||||
function parseAddress(email: string): { localPart: string; domain: string } {
|
||||
@@ -37,7 +34,7 @@ function parseAddress(email: string): { localPart: string; domain: string } {
|
||||
return { localPart: email.slice(0, at).toLowerCase(), domain: email.slice(at + 1).toLowerCase() };
|
||||
}
|
||||
|
||||
async function createAdmin(email: string, password: string, force: boolean): Promise<void> {
|
||||
export async function createAdmin(email: string, password: string, force: boolean): Promise<void> {
|
||||
if (password.length < 12) fail("the password must be at least 12 characters");
|
||||
const { localPart, domain } = parseAddress(email);
|
||||
|
||||
@@ -69,13 +66,10 @@ async function createAdmin(email: string, password: string, force: boolean): Pro
|
||||
);
|
||||
|
||||
// Delivery needs these: a mailbox with no INBOX cannot receive at all, and
|
||||
// one with no Junk used to fail the spam path outright.
|
||||
for (const [name, specialUse] of STANDARD_FOLDERS) {
|
||||
await query(
|
||||
`INSERT INTO folders (mailbox_id, name, special_use) VALUES ($1, $2, $3)`,
|
||||
[mailbox!.id, name, specialUse],
|
||||
);
|
||||
}
|
||||
// one with no Junk used to fail the spam path outright. The list lives in the
|
||||
// database (migration 0014) so the console and this command cannot disagree
|
||||
// about it, as they once did over the Reports folder.
|
||||
await query(`SELECT provision_default_folders($1)`, [mailbox!.id]);
|
||||
|
||||
// PLAN.md section 7.4b: the published DNS records name these, and reports
|
||||
// bounce if they do not resolve.
|
||||
@@ -174,10 +168,13 @@ async function main(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => closePool())
|
||||
.catch(async (err) => {
|
||||
console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
await closePool().catch(() => undefined);
|
||||
process.exit(1);
|
||||
});
|
||||
// Only when run as a command, not when a test imports the functions above
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
main()
|
||||
.then(() => closePool())
|
||||
.catch(async (err) => {
|
||||
console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
await closePool().catch(() => undefined);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user