Ctrl K
ComunidadGuías…

scripts/setup-mailbox.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env tsx
/**
 * `pnpm setup:mailbox` — seeds the D1 control-plane rows a mailbox needs to actually
 * receive mail (domain + mailbox + alias, optionally a catch-all routing rule).
 *
 * This is the step that turns "deployed" into "an inbox that receives": `resolveRoutingForRecipient`
 * stores mail for a recipient as soon as an active alias + active domain exist (src/db/d1.ts).
 *
 * SAFETY: dry-run by default (prints the mailbox id, the SQL, and the exact `wrangler d1
 * execute` command). Pass `--apply` to run it. The SQL uses conflict-safe inserts/upserts and
 * resolves the live domain row by name, so it is idempotent and safe to re-run.
 *
 * `mailbox_id` is random and assigned by the INSERT, with D1 as the only source of truth. A
 * re-run for an address that already exists is a no-op (UNIQUE(primary_address) + DO NOTHING),
 * and the alias/rule rows read the stored `mailbox_id` back out of `mailboxes` rather than
 * assuming the candidate id this run generated — so repeats converge instead of forking.
 *
 * Usage:
 *   pnpm setup:mailbox --domain example.com --address inbox@example.com            # local dry run
 *   pnpm setup:mailbox --domain example.com --address inbox@example.com --local --apply
 *   pnpm setup:mailbox --domain example.com --address inbox@example.com \
 *     --env dev --catch-all --apply                                                # remote
 */
import { execFileSync } from "node:child_process";
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { canonicalPrimaryAddress, generateMailboxId } from "../src/lib/mailbox-id";

type WranglerBlock = {
	d1_databases?: Array<{ binding: string; database_name: string }>;
};

type WranglerConfig = WranglerBlock & {
	env?: Record<string, WranglerBlock>;
};

type D1ExecuteJson = Array<{
	results?: Array<Record<string, unknown>>;
	success?: boolean;
	meta?: { duration?: number };
}>;

function parseArgs(argv: string[]): Record<string, string> {
	const args: Record<string, string> = {};
	for (let i = 0; i < argv.length; i += 1) {
		const arg = argv[i];
		if (!arg?.startsWith("--")) continue;
		const [rawKey, inlineValue] = arg.slice(2).split("=", 2);
		if (!rawKey) continue;
		if (inlineValue !== undefined) {
			args[rawKey] = inlineValue;
			continue;
		}
		const next = argv[i + 1];
		if (!next || next.startsWith("--")) {
			args[rawKey] = "true";
			continue;
		}
		args[rawKey] = next;
		i += 1;
	}
	return args;
}

function stripJsonc(input: string): string {
	return input.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
}

/** Reads a single key out of a dotenv-style file, ignoring comments. */
function readDotEnvValue(path: string, key: string): string | undefined {
	if (!existsSync(path)) return undefined;
	for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
		const trimmed = line.trim();
		if (!trimmed || trimmed.startsWith("#")) continue;
		const sep = trimmed.indexOf("=");
		if (sep !== -1 && trimmed.slice(0, sep).trim() === key) {
			return trimmed.slice(sep + 1).trim();
		}
	}
	return undefined;
}

/** SQL single-quoted string literal with quotes escaped. */
function q(value: string): string {
	return `'${value.replace(/'/g, "''")}'`;
}

function runD1Query(execArgs: string[], sqlCommand: string): D1ExecuteJson {
	return JSON.parse(
		execFileSync("pnpm", ["wrangler", ...execArgs, `--command=${sqlCommand}`, "--json"], {
			encoding: "utf8",
			stdio: ["ignore", "pipe", "inherit"],
		}),
	) as D1ExecuteJson;
}

const args = parseArgs(process.argv.slice(2));
const apply = args.apply === "true";
const local = args.local === "true";
const targetEnv = args.env;
const catchAll = args["catch-all"] === "true";
// Inbound is provisioned by default, and this flag is how you say you meant it.
// Seeding only the D1 rows produces a mailbox the portal lists and mail cannot
// reach: "exists" and "can receive" were separate facts, and nothing surfaced the
// gap -- the address simply bounced at the MX, which is the wrong place to learn.
const skipInbound = args["no-inbound"] === "true";
const envLabel = targetEnv ?? "production";

const domain = args.domain?.trim().toLowerCase();
const rawAddress = args.address?.trim();
if (!domain || !rawAddress) {
	console.error("setup:mailbox: --domain and --address are required.");
	console.error("Example: pnpm setup:mailbox --domain example.com --address inbox@example.com");
	process.exit(1);
}

// `domain` is narrowed to a string by the guard above, but that narrowing does not
// reach into functions declared later, which use the declared type.
const resolvedDomain: string = domain;

let address: string;
try {
	address = canonicalPrimaryAddress(rawAddress);
} catch {
	console.error(`setup:mailbox: invalid --address "${rawAddress}".`);
	process.exit(1);
}
if (!address.endsWith(`@${domain}`)) {
	console.error(`setup:mailbox: --address ${address} is not on --domain ${domain}.`);
	process.exit(1);
}

const displayName = args["display-name"]?.trim() || address.split("@")[0] || "Inbox";
const zoneId = args["zone-id"]?.trim() || "zone-placeholder";

// Resolve owner email: explicit flag > OWNER_BOOTSTRAP_EMAILS (first entry if single).
let ownerEmail = args.owner?.trim().toLowerCase() || undefined;
if (!ownerEmail) {
	const allowList =
		readDotEnvValue(".dev.vars", "OWNER_BOOTSTRAP_EMAILS") || process.env.OWNER_BOOTSTRAP_EMAILS;
	if (allowList) {
		const emails = allowList
			.split(",")
			.map((e) => e.trim())
			.filter((e) => e.length > 0);
		if (emails.length === 1) {
			ownerEmail = emails[0]?.toLowerCase();
		}
	}
}
const ownerSql = ownerEmail ? q(ownerEmail) : "NULL";

// Only used if this address has no mailbox yet; the SQL below discards it on conflict.
const candidateMailboxId = generateMailboxId();
const domainId = `dom_${domain.replace(/[^a-z0-9]+/g, "_")}`;
const now = new Date().toISOString();
const generatedConfigPath = `wrangler.generated.${envLabel}.json`;
const preferredConfigPath = existsSync(generatedConfigPath)
	? generatedConfigPath
	: "wrangler.jsonc";

const statements = [
	`INSERT INTO domains (id, domain, zone_id, status, created_at, updated_at)
VALUES (${q(domainId)}, ${q(domain)}, ${q(zoneId)}, 'active', ${q(now)}, ${q(now)})
ON CONFLICT(domain) DO NOTHING;`,
	`INSERT INTO mailboxes (mailbox_id, primary_address, display_name, status, owner_email, created_at, updated_at)
VALUES (${q(candidateMailboxId)}, ${q(address)}, ${q(displayName)}, 'active', ${ownerSql}, ${q(now)}, ${q(now)})
ON CONFLICT(primary_address) DO NOTHING;`,
	`INSERT INTO aliases (alias_address, mailbox_id, domain_id, status, created_at, updated_at)
SELECT ${q(address)}, m.mailbox_id, d.id, 'active', ${q(now)}, ${q(now)}
FROM mailboxes m
JOIN domains d ON d.domain = ${q(domain)}
WHERE m.primary_address = ${q(address)}
ON CONFLICT(alias_address) DO UPDATE SET
	mailbox_id = excluded.mailbox_id,
	domain_id = excluded.domain_id,
	status = excluded.status,
	updated_at = excluded.updated_at;`,
];
if (catchAll) {
	statements.push(
		`INSERT INTO routing_rules (id, domain_id, pattern, priority, action, mailbox_id, forward_to_json, reject_reason, enabled, created_at, updated_at)
SELECT ${q(`rule_${domainId}_catchall`)}, d.id, '*', 100, 'store', m.mailbox_id, '[]', NULL, 1, ${q(now)}, ${q(now)}
FROM mailboxes m
JOIN domains d ON d.domain = ${q(domain)}
WHERE m.primary_address = ${q(address)}
ON CONFLICT(id) DO UPDATE SET
	domain_id = excluded.domain_id,
	pattern = excluded.pattern,
	priority = excluded.priority,
	action = excluded.action,
	mailbox_id = excluded.mailbox_id,
	forward_to_json = excluded.forward_to_json,
	reject_reason = excluded.reject_reason,
	enabled = excluded.enabled,
	updated_at = excluded.updated_at;`,
	);
}
const sql =
	`-- Reccado mailbox seed for ${address}. Idempotent.\n` +
	`-- Domains are resolved by name and the mailbox_id by primary_address at execution time, so\n` +
	`-- existing rows are reused and ${candidateMailboxId} is only used if this address is new.\n` +
	`${statements.join("\n")}\n`;

// Resolve the D1 execute target.
let execArgs: string[];
let targetLabel: string;
if (local) {
	execArgs = ["d1", "execute", "INDEX_DB", "--local"];
	targetLabel = "local D1 (INDEX_DB binding)";
} else {
	const config = JSON.parse(
		stripJsonc(readFileSync(preferredConfigPath, "utf8")),
	) as WranglerConfig;
	const block = targetEnv ? config.env?.[targetEnv] : config;
	const d1Name = block?.d1_databases?.find((d) => d.binding === "INDEX_DB")?.database_name;
	if (!d1Name) {
		console.error(
			`setup:mailbox: could not resolve the INDEX_DB name for env "${envLabel}" from ${preferredConfigPath}.`,
		);
		process.exit(1);
	}
	execArgs = [
		"d1",
		"execute",
		d1Name,
		"--remote",
		...(preferredConfigPath === "wrangler.jsonc" ? [] : ["--config", preferredConfigPath]),
		...(targetEnv ? ["--env", targetEnv] : []),
	];
	targetLabel = `remote D1 "${d1Name}"${targetEnv ? ` (env ${targetEnv})` : ""}`;
}

let existingDomainId: string | undefined;
let existingMailboxId: string | undefined;
if (apply) {
	const [domainLookup, mailboxLookup] = runD1Query(
		execArgs,
		`SELECT id FROM domains WHERE domain = ${q(domain)} LIMIT 1;
SELECT mailbox_id FROM mailboxes WHERE primary_address = ${q(address)} LIMIT 1;`,
	);
	existingDomainId =
		typeof domainLookup?.results?.[0]?.id === "string"
			? (domainLookup.results[0].id as string)
			: undefined;
	existingMailboxId =
		typeof mailboxLookup?.results?.[0]?.mailbox_id === "string"
			? (mailboxLookup.results[0].mailbox_id as string)
			: undefined;
}

console.log(`\nReccado setup:mailbox\n`);
console.log(`  address:     ${address}`);
console.log(
	`  mailbox_id:  ${existingMailboxId ?? candidateMailboxId}${
		existingMailboxId ? " (existing row, reused)" : " (candidate; assigned only if new)"
	}`,
);
console.log(
	`  domain:      ${domain} (${existingDomainId ?? domainId}${existingDomainId ? ", existing row" : ", preferred id"})`,
);
console.log(
	`  catch-all:   ${catchAll ? "yes (routing_rule pattern '*')" : "no (exact alias only)"}`,
);
console.log(`  target:      ${targetLabel}`);
if (!local && preferredConfigPath !== "wrangler.jsonc") {
	console.log(`  config:      ${preferredConfigPath}`);
}
console.log(`  mode:        ${apply ? "APPLY" : "dry run (no changes)"}\n`);
console.log(sql);

if (!apply) {
	console.log(
		`Command that would run:\n  $ pnpm wrangler ${execArgs.join(" ")} --file <tmp.sql>\n`,
	);
	console.log(
		"Note: a dry run does not read D1, so it prints a fresh candidate id. If this address is\n" +
			"already seeded, --apply keeps the stored mailbox_id and changes nothing.\n",
	);
	provisionInbound(false);
	console.log("\nDry run only. Re-run with --apply to execute.\n");
	process.exit(0);
}

function inboundArgs(withApply: boolean): string[] {
	const out = ["setup:routing", "--domain", resolvedDomain, "--address", address];
	if (catchAll) out.push("--catch-all");
	if (targetEnv) out.push("--env", targetEnv);
	if (withApply) out.push("--apply");
	return out;
}

/**
 * The other half of "a mailbox exists": Cloudflare Email Routing enabled on the
 * zone and a rule delivering this address to the Worker. `setup:routing` owns
 * that, including the catch-all shape Wrangler rejects client-side, so this
 * delegates rather than reimplementing it.
 *
 * Local runs skip it: there is no zone behind a local D1.
 */
function provisionInbound(withApply: boolean): void {
	if (local) return;
	if (skipInbound) {
		console.log("\n▸ Inbound: SKIPPED (--no-inbound)");
		console.log(`  ${address} will be listed as a mailbox and will not receive mail.`);
		console.log("  Replies to anything it sends bounce at the MX. Deliberate for a");
		console.log("  send-only sender; a mistake anywhere else.");
		return;
	}
	console.log("\n▸ Inbound (Email Routing → Worker)");
	console.log(`  $ pnpm ${inboundArgs(withApply).join(" ")}`);
	if (!withApply) return;
	try {
		execFileSync("pnpm", inboundArgs(true), { stdio: "inherit" });
	} catch {
		// The mailbox rows are already committed, so failing here must not read as
		// "nothing happened". Name what exists and what does not.
		console.error(
			`\nsetup:mailbox: the D1 rows for ${address} are seeded, but inbound provisioning failed.`,
		);
		console.error(`  The mailbox is listed and cannot receive until you re-run:`);
		console.error(`    $ pnpm ${inboundArgs(true).join(" ")}`);
		process.exitCode = 1;
	}
}

const dir = mkdtempSync(join(tmpdir(), "reccado-setup-mailbox-"));
const sqlPath = join(dir, "seed.sql");
writeFileSync(sqlPath, sql, "utf8");
execFileSync("pnpm", ["wrangler", ...execArgs, `--file=${sqlPath}`], { stdio: "inherit" });

// Read the id back rather than reporting the candidate: on conflict the INSERT kept the stored one.
const [seeded] = runD1Query(
	execArgs,
	`SELECT mailbox_id FROM mailboxes WHERE primary_address = ${q(address)} LIMIT 1;`,
);
const seededMailboxId =
	typeof seeded?.results?.[0]?.mailbox_id === "string"
		? (seeded.results[0].mailbox_id as string)
		: candidateMailboxId;
console.log(`\nSeeded ${address} → ${seededMailboxId} into ${targetLabel}.`);
provisionInbound(true);