import express from "express" import fs from "fs/promises" import rateLimit from "express-rate-limit" import Docker from "dockerode" import validator from "validator" import PasswordValidator from "password-validator" import { accounts, cacheInfo } from "./db/schema" import { eq } from "drizzle-orm" import type { Request, Response } from "express" import { drizzle } from 'drizzle-orm/bun-sqlite'; interface RateLimitOptions { windowMs: number limit: number message?: string } interface RateLimitConfig { [endpoint: string]: RateLimitOptions } interface Account { email: string used: string capacity: string percentage: string } const docker = new Docker({ socketPath: "/var/run/docker.sock" }) const app = express() const db = drizzle(process.env.DB_FILE_NAME!); const container = docker.getContainer("mailserver"); app.use(express.json()) let rateLimitConfig: RateLimitConfig = {} ;(async () => { try { const data = await fs.readFile("/app/ratelimit.json", "utf8") rateLimitConfig = JSON.parse(data) } catch (err) { console.error("Error loading rate limit config:", err) } })() const createLimiter = (options: RateLimitOptions) => rateLimit({ windowMs: options.windowMs, limit: options.limit, message: options.message || "Too many requests, please try again later.", }) Object.entries(rateLimitConfig).forEach(([route, options]) => { app.use(route, createLimiter(options)) }) const validateEmail = (email: string): boolean => validator.isEmail(email) const passwordSchema = new PasswordValidator() passwordSchema.is().min(8).is().max(64).has().letters().has().digits().has().not().spaces() const listAccountsFromDocker = async (): Promise => { return new Promise((resolve, reject) => { const container = docker.getContainer("mailserver") container.exec( { Cmd: ["setup", "email", "list"], AttachStdout: true, AttachStderr: true, }, (err, exec) => { if (err || !exec) return reject(err || new Error("Exec is undefined")) exec.start({}, (err, stream) => { if (err || !stream) return reject(err || new Error("Exec stream is undefined")) let output = "" stream.on("data", (chunk: Buffer) => (output += chunk.toString())) stream.on("end", () => { const regex = /\*\s*(\S+)\s*\(\s*([^\/]+?)\s*\/\s*([^)]+?)\s*\)\s*\[(\d+)%]/g const accounts: Account[] = [...output.matchAll(regex)].map((match) => ({ email: match[1], used: match[2].trim() === "~" ? "Unlimited" : match[2].trim(), capacity: match[3].trim() === "~" ? "Unlimited" : match[3].trim(), percentage: match[4], })) resolve(accounts) }) }) }, ) }) } const updateAccountsCache = async () => { const dockerAccounts = await listAccountsFromDocker(); await db.delete(accounts); if (dockerAccounts.length > 0) { await db.insert(accounts).values(dockerAccounts); } await db .insert(cacheInfo) .values({ lastUpdated: Date.now() }) .onConflictDoUpdate({ target: cacheInfo.id, set: { lastUpdated: Date.now() } }); }; app.get("/accounts/list", async (_req, res) => { try { const cacheData = await db.select().from(cacheInfo).limit(1) const lastUpdated = cacheData[0]?.lastUpdated || 0 const currentTime = Date.now() if (currentTime - lastUpdated > 30 * 60 * 1000) { // 30 minutes await updateAccountsCache() } const accountsList = await db.select().from(accounts) res.json({ accounts: accountsList }) } catch (err) { res.status(500).json({ error: (err as Error).message }) } }) app.post("/accounts/user", async (req: Request, res: Response): Promise => { const { email } = req.body; if (!validateEmail(email)) { res.status(400).json({ error: "Invalid email format" }); return; } try { const account = await db .select() .from(accounts) .where(eq(accounts.email, email)) .limit(1); if (account.length > 0) { res.json({ account: account[0] }); } else { res.status(404).json({ error: "Account not found" }); } } catch (err) { res.status(500).json({ error: (err as Error).message }); } }); // TODO: The wait upon account creation needs to be more adaptive app.post("/accounts/add", async (req: Request, res: Response): Promise => { const { email, password, migrate } = req.body; let failureReason = ""; if (!validateEmail(email)) { failureReason = "Invalid email format"; console.log(`Error adding account: ${failureReason}`); res.status(400).json({ error: failureReason }); return; } let finalPassword = password; if (migrate) { try { const data = await fs.readFile("/app/migrate.txt", "utf8"); const line = data.split("\n").find(l => l.trim() === email); if (!line) { failureReason = "Account not eligible"; console.log(`Error adding account (migrate): ${failureReason}`); res.status(500).json({ error: "Backend error" }); return; } else { const newData = data.replace(line, ""); await fs.writeFile("/app/migrate.txt", newData); const exec = await container.exec({ Cmd: ["setup", "email", "update", email, finalPassword], AttachStdout: true, AttachStderr: true, }); const stream = await new Promise((resolve, reject) => { exec.start({}, (err, stream) => { if (err || !stream) { reject(err || new Error("Exec stream is undefined")); } else { resolve(stream); } }); }); let output = ""; stream.on("data", (chunk: Buffer) => { output += chunk.toString(); }); await new Promise((resolve) => stream.on("end", resolve)); console.log("Docker output (migrate update):\n", output); // detect errors if (/ERROR/i.test(output)) { failureReason = "Migration failed"; console.log(`Error during migration: ${failureReason}`); res.status(500).json({ error: failureReason }); return; } else { // force refresh await updateAccountsCache(); const addedAccount = await db .select() .from(accounts) .where(eq(accounts.email, email)) .limit(1); if (addedAccount.length > 0) { console.log(`Added account (via migrate): ${email}`); res.json({ success: true }); return; } else { failureReason = "Migration failed"; console.log(`Failed to migrate account: ${failureReason}`); res.status(500).json({ error: failureReason }); return; } } } } catch (error) { failureReason = "Backend error"; console.log(`Error adding account (migrate branch): ${failureReason}`, error); res.status(500).json({ error: failureReason }); return; } } else { try { const account = await db .select() .from(accounts) .where(eq(accounts.email, email)) .limit(1); if (account.length > 0) { console.log(`Account already exists: ${email}`); res.status(400).json({ error: "Account already exists" }); return; } } catch (err) { console.log(`Error checking user: ${err}`); res.status(500).json({ error: "Internal server error" }); return; } } if (!finalPassword || !passwordSchema.validate(finalPassword)) { failureReason = "Invalid or weak password"; console.log(`Failed to add account: ${failureReason}`); res.status(400).json({ error: failureReason }); return; } // Non-migrate account creation route try { const exec = await container.exec({ Cmd: ["setup", "email", "add", email, finalPassword], AttachStdout: true, AttachStderr: true, }); const stream = await new Promise((resolve, reject) => { exec.start({}, (err, stream) => { if (err || !stream) { failureReason = "Failed to start Docker exec"; console.log(`Failed to add account: ${failureReason}`, err); reject(err || new Error("Exec stream is undefined")); } else { resolve(stream); } }); }); let output = ""; stream.on("data", (chunk: Buffer) => { output += chunk.toString(); }); await new Promise((resolve) => stream.on("end", resolve)); // poll db let accountFound = false; for (let attempt = 0; attempt < 15; attempt++) { await updateAccountsCache(); const addedAccount = await db .select() .from(accounts) .where(eq(accounts.email, email)) .limit(1); if (addedAccount.length > 0) { accountFound = true; break; } await new Promise((resolve) => setTimeout(resolve, 1000)); } if (accountFound) { console.log(`Added account: ${email}`); res.json({ success: true }); } else { failureReason = "Timed out waiting for account creation"; console.log(`Failed to add account: ${failureReason}`); res.status(500).json({ error: "Check timed out waiting for account creation", failureReason, }); } } catch (error) { failureReason = "Error executing Docker command"; console.log(`Failed to add account: ${failureReason}`, error); res.status(500).json({ error: "Backend error" }); } }); const PORT = 3000 app.listen(PORT, () => console.log(`API listening on port ${PORT}`))