initial commit

This commit is contained in:
2026-04-26 13:47:35 -05:00
commit 844c63dd85
27 changed files with 1241 additions and 0 deletions

View File

@@ -0,0 +1,113 @@
import { Router } from 'express';
import { z } from 'zod';
import { pool } from '../db.js';
import { config } from '../config.js';
import { requireAuth, canAccessDomain } from '../middleware/auth.js';
import { DmsService } from '../services/dms.js';
import { SyncService } from '../services/sync.js';
import { DynamoRulesService } from '../services/dynamodb.js';
import { audit } from '../services/audit.js';
import { domainFromEmail, localPartFromEmail, normalizeEmail } from '../utils/email.js';
export const mailboxesRouter = Router();
mailboxesRouter.use(requireAuth);
const dms = new DmsService();
const sync = new SyncService(dms);
const dynamo = new DynamoRulesService();
function ensureDomain(req: any, domain: string): void {
if (!canAccessDomain(req.user, domain)) throw Object.assign(new Error('Forbidden'), { status: 403 });
}
mailboxesRouter.get('/', async (req, res) => {
const domain = String(req.query.domain ?? '').toLowerCase();
if (domain) ensureDomain(req, domain);
const params: unknown[] = [config.nodeName];
let where = 'WHERE node_name=$1';
if (domain) { params.push(domain); where += ` AND domain=$${params.length}`; }
const result = await pool.query(
`SELECT * FROM mailboxes ${where} ORDER BY domain, local_part`,
params,
);
res.json(result.rows.filter((r) => canAccessDomain(req.user!, r.domain)));
});
mailboxesRouter.post('/', async (req, res) => {
const body = z.object({ email: z.string().email(), password: z.string().min(8) }).parse(req.body);
const email = normalizeEmail(body.email);
const domain = domainFromEmail(email);
ensureDomain(req, domain);
await dms.addMailbox(email, body.password);
await sync.syncFromDms();
await audit(req.user!.email, 'mailbox.create', 'mailbox', email, { domain }, req.ip);
res.status(201).json({ email, domain, local_part: localPartFromEmail(email) });
});
mailboxesRouter.delete('/:email', async (req, res) => {
const email = normalizeEmail(req.params.email);
const domain = domainFromEmail(email);
ensureDomain(req, domain);
await dms.deleteMailbox(email);
await dms.syncSesDomain(domain).catch(() => undefined);
await pool.query(`UPDATE mailboxes SET status='deleted', deleted_at=now(), updated_at=now() WHERE email_address=$1`, [email]);
await audit(req.user!.email, 'mailbox.delete', 'mailbox', email, { domain }, req.ip);
res.json({ ok: true });
});
mailboxesRouter.post('/:email/password', async (req, res) => {
const body = z.object({ password: z.string().min(8) }).parse(req.body);
const email = normalizeEmail(req.params.email);
ensureDomain(req, domainFromEmail(email));
await dms.updatePassword(email, body.password);
await audit(req.user!.email, 'mailbox.password_update', 'mailbox', email, {}, req.ip);
res.json({ ok: true });
});
mailboxesRouter.post('/usage/rescan', async (req, res) => {
const domain = String(req.body?.domain ?? '').toLowerCase();
if (domain) ensureDomain(req, domain);
const params: unknown[] = [config.nodeName];
let where = `WHERE node_name=$1 AND status='active'`;
if (domain) { params.push(domain); where += ` AND domain=$${params.length}`; }
const rows = (await pool.query(`SELECT email_address, domain FROM mailboxes ${where}`, params)).rows;
let count = 0;
for (const row of rows) {
if (!canAccessDomain(req.user!, row.domain)) continue;
const bytes = await dms.getMailboxUsageBytes(row.email_address);
await pool.query(`UPDATE mailboxes SET used_bytes=$2, usage_scanned_at=now(), updated_at=now() WHERE email_address=$1`, [row.email_address, bytes]);
count++;
}
await audit(req.user!.email, 'mailbox.usage_rescan', 'domain', domain || '*', { count }, req.ip);
res.json({ count });
});
mailboxesRouter.get('/:email/rules', async (req, res) => {
const email = normalizeEmail(req.params.email);
ensureDomain(req, domainFromEmail(email));
res.json(await dynamo.getRules(email));
});
mailboxesRouter.put('/:email/rules', async (req, res) => {
const email = normalizeEmail(req.params.email);
ensureDomain(req, domainFromEmail(email));
const body = z.object({ ooo_active: z.boolean().optional(), ooo_message: z.string().optional(), forwards: z.array(z.string().email()).optional() }).parse(req.body);
const saved = await dynamo.putRules({ email_address: email, ooo_active: body.ooo_active, ooo_message: body.ooo_message, forwards: body.forwards });
await audit(req.user!.email, 'mailbox.rules_update', 'mailbox', email, saved, req.ip);
res.json(saved);
});
mailboxesRouter.get('/:email/blocklist', async (req, res) => {
const email = normalizeEmail(req.params.email);
ensureDomain(req, domainFromEmail(email));
res.json(await dynamo.getBlocklist(email));
});
mailboxesRouter.put('/:email/blocklist', async (req, res) => {
const email = normalizeEmail(req.params.email);
ensureDomain(req, domainFromEmail(email));
const body = z.object({ blocked_patterns: z.array(z.string()) }).parse(req.body);
const saved = await dynamo.putBlocklist(email, body.blocked_patterns);
await audit(req.user!.email, 'mailbox.blocklist_update', 'mailbox', email, saved, req.ip);
res.json(saved);
});