4bnode
Version:
4bnode is a CLI-powered backend development platform with a built-in visual dashboard to generate, manage, and test Node.js/Express APIs faster.
154 lines (137 loc) • 6.64 kB
JavaScript
#!/usr/bin/env node
import fs from "fs";
import path from "path";
import os from "os";
import { createRequire } from "module";
import { pathToFileURL } from "url";
import { input, select, confirm, password } from "@inquirer/prompts";
import { getProjectRoot } from "./lib/project.js";
import { updateEnvFile } from "./lib/env.js";
import { showHeader, showTaskDone, showError, showInfo, showFileAction, installDeps } from "./lib/ui.js";
import {
MAIL_PROVIDERS,
buildMailerService,
isOfficialMailDomain,
OFFICIAL_MAIL_DOMAIN_PASSWORD,
OFFICIAL_MAIL_NOTIFY,
} from "./lib/codegen.js";
// The 4brains.in domain can't be configured directly — require the shared
// authorization password (exits on a wrong answer).
async function ensureOfficialAllowed(from) {
if (!isOfficialMailDomain(from)) return;
showInfo("4brains.in is an official email domain — you can't use it directly.");
const pw = (await password({ message: "Authorization password for 4brains.in:", mask: "*" })).trim();
if (pw !== OFFICIAL_MAIL_DOMAIN_PASSWORD) {
showError("Incorrect authorization password. 4brains.in was not configured.");
process.exit(1);
}
}
// Report an authorized 4brains.in configuration to the domain owner (best-effort;
// uses the project's just-installed mail package).
async function sendOfficialNotify(root, ctx) {
const require = createRequire(path.join(root, "package.json"));
let appName = "unknown";
try { appName = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).name || appName; } catch {}
let osUser = "unknown";
try { osUser = os.userInfo().username; } catch {}
const subject = "[4bnode] 4brains.in sender configured: " + ctx.from;
const text = [
"A 4brains.in sender was configured in a 4bnode app (via CLI).",
"",
"Email id (from): " + ctx.from,
"Provider: " + ctx.kind,
"Configured by: " + osUser + "@" + os.hostname(),
"App: " + appName,
"Time: " + new Date().toISOString(),
].join("\n");
if (ctx.kind === "resend") {
const { Resend } = await import(pathToFileURL(require.resolve("resend")).href);
const resend = new Resend(ctx.apiKey);
const { error } = await resend.emails.send({ from: ctx.from, to: OFFICIAL_MAIL_NOTIFY, subject, text });
if (error) throw new Error(error.message || "Resend error");
return;
}
const nodemailer = (await import(pathToFileURL(require.resolve("nodemailer")).href)).default;
const transporter = nodemailer.createTransport({
host: ctx.smtp.host,
port: Number(ctx.smtp.port) || 587,
secure: ctx.smtp.secure,
auth: { user: ctx.smtp.user, pass: ctx.smtp.pass },
});
await transporter.sendMail({ from: ctx.from, to: OFFICIAL_MAIL_NOTIFY, subject, text });
}
async function main() {
showHeader("add-email", "Set up transactional email");
const providerId = await select({
message: "Email provider:",
choices: Object.entries(MAIL_PROVIDERS).map(([id, p]) => ({ name: p.label, value: id })),
});
const preset = MAIL_PROVIDERS[providerId];
if (preset.hint) showInfo(preset.hint);
const root = getProjectRoot();
const envPath = path.join(root, ".env");
const dir = path.join(root, "src", "services");
const mailerPath = path.join(dir, "mailer.js");
updateEnvFile(envPath, "MAIL_PROVIDER", providerId);
const summary = [`Provider: ${preset.label}`];
let dep;
let notifyCtx = null; // set when an authorized 4brains.in sender is configured
if (preset.type === "resend") {
// IntraApp(Postmaster) → Resend API. Only an API key + from address.
const apiKey = (await password({ message: "Resend API key (re_...):", mask: "*" })).trim();
const from = (await input({ message: "From address:", default: "" })).trim();
await ensureOfficialAllowed(from);
if (isOfficialMailDomain(from)) updateEnvFile(envPath, "OFFICIAL_MAIL_AUTHORIZED", "1");
if (apiKey) updateEnvFile(envPath, "RESEND_API_KEY", apiKey);
if (from) updateEnvFile(envPath, "MAIL_FROM", from);
dep = "resend";
summary.push(from ? `From: ${from}` : "From: (set MAIL_FROM in .env)");
if (isOfficialMailDomain(from) && apiKey) notifyCtx = { kind: "resend", from, apiKey };
} else {
// Custom SMTP → nodemailer. TLS selects the port (465 vs 587).
const host = (await input({ message: "SMTP host:", default: preset.host || "" })).trim();
const secure = await confirm({ message: "Use TLS (port 465)? (No = port 587)", default: false });
const defPort = secure ? 465 : 587;
const port = (await input({ message: "SMTP port:", default: String(defPort) })).trim();
const user = (await input({ message: "SMTP user:", default: "" })).trim();
const pass = (await password({ message: "SMTP password:", mask: "*" })).trim();
const from = (await input({ message: "From address:", default: user })).trim();
await ensureOfficialAllowed(from || user);
if (isOfficialMailDomain(from || user)) updateEnvFile(envPath, "OFFICIAL_MAIL_AUTHORIZED", "1");
updateEnvFile(envPath, "SMTP_HOST", host);
updateEnvFile(envPath, "SMTP_PORT", port || String(defPort));
updateEnvFile(envPath, "SMTP_SECURE", secure ? "true" : "false");
updateEnvFile(envPath, "SMTP_USER", user);
if (pass) updateEnvFile(envPath, "SMTP_PASS", pass);
updateEnvFile(envPath, "SMTP_FROM", from || user);
dep = "nodemailer";
summary.push(`Host: ${host}:${port || defPort}`);
if (isOfficialMailDomain(from || user)) notifyCtx = { kind: "smtp", from: from || user, smtp: { host, port: port || defPort, secure, user, pass } };
}
console.log();
showFileAction("updated", ".env");
fs.mkdirSync(dir, { recursive: true });
// Rewrite the mailer only if missing or on the old single-backend version.
if (!fs.existsSync(mailerPath) || !fs.readFileSync(mailerPath, "utf8").includes("MAIL_PROVIDER")) {
fs.writeFileSync(mailerPath, buildMailerService(), "utf8");
showFileAction(fs.existsSync(mailerPath) ? "updated" : "created", "src/services/mailer.js");
} else {
showInfo("src/services/mailer.js already up to date — keeping it.");
}
await installDeps(dep);
if (notifyCtx) {
try {
await sendOfficialNotify(root, notifyCtx);
showInfo(`4brains.in configuration reported to ${OFFICIAL_MAIL_NOTIFY}.`);
} catch (e) {
showInfo(`Could not send the 4brains.in notification: ${e.message}`);
}
}
summary.push("Use sendMail({ to, subject, html }) from src/services/mailer.js");
showTaskDone("Email configured", summary);
}
main().catch((err) => {
if (err.name === "ExitPromptError") process.exit(0);
showError(err.message);
process.exit(1);
});