Files
mailadmin/backend/src/routes/mailboxes.ts
2026-04-28 17:59:51 -05:00

181 lines
6.7 KiB
TypeScript

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<void> {
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<number> {
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);
});