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 { recordBillingEvent } from '../services/billing.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 }); } async function refreshQuotaForMailbox(emailAddress: string): Promise { const quota = await dms.getMailboxQuota(emailAddress); await pool.query( `UPDATE mailboxes SET used_bytes=$2, quota_bytes=$3, quota_percent=$4, message_count=$5, message_limit=$6, usage_scanned_at=now(), updated_at=now() WHERE email_address=$1`, [ emailAddress, quota.usedBytes, quota.quotaBytes, quota.quotaPercent, quota.messageCount, quota.messageLimit, ], ); } async function refreshQuotaForDomain(req: any, domain: string): Promise { ensureDomain(req, domain); const rows = (await pool.query( `SELECT email_address, domain FROM mailboxes WHERE node_name=$1 AND domain=$2 AND status='active' ORDER BY email_address`, [config.nodeName, domain], )).rows; let count = 0; for (const row of rows) { if (!canAccessDomain(req.user!, row.domain)) continue; try { await refreshQuotaForMailbox(row.email_address); count++; } catch (err) { console.warn(`Could not refresh quota for ${row.email_address}:`, err); } } return count; } mailboxesRouter.get('/', async (req, res) => { const domain = String(req.query.domain ?? '').toLowerCase(); const refreshQuota = String(req.query.refreshQuota ?? '').toLowerCase() === 'true'; const includeDeleted = String(req.query.includeDeleted ?? '').toLowerCase() === 'true'; if (domain) { ensureDomain(req, domain); if (refreshQuota) { const count = await refreshQuotaForDomain(req, domain); await audit(req.user!.email, 'mailbox.quota_refresh', 'domain', domain, { count }, req.ip); } } const params: unknown[] = [config.nodeName]; let where = 'WHERE node_name=$1'; if (!includeDeleted) where += ` AND status <> 'deleted'`; 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 refreshQuotaForMailbox(email).catch((err) => console.warn(`Could not refresh quota for ${email}:`, err)); await audit(req.user!.email, 'mailbox.create', 'mailbox', email, { domain }, req.ip); await recordBillingEvent('created', domain, email, req.user!.email); 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); await recordBillingEvent('deleted', domain, email, req.user!.email); 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('/:email/quota', async (req, res) => { const body = z.object({ quota_gb: z.number().int().min(1).max(1024) }).parse(req.body); const email = normalizeEmail(req.params.email); ensureDomain(req, domainFromEmail(email)); await dms.setQuota(email, body.quota_gb); await refreshQuotaForMailbox(email).catch((err) => console.warn(`Could not refresh quota for ${email}:`, err)); await audit(req.user!.email, 'mailbox.quota_set', 'mailbox', email, { quota_gb: body.quota_gb }, req.ip); res.json({ ok: true, email, quota_gb: body.quota_gb }); }); 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(), ooo_content_type: z.enum(['text', 'html']).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, ooo_content_type: body.ooo_content_type, 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); });