181 lines
6.7 KiB
TypeScript
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);
|
|
});
|