rspace-online/src/encryptid/space-alias-service.ts

121 lines
3.7 KiB
TypeScript

/**
* Space Email Alias Service — Mailcow forwarding alias management per space.
*
* Provisions {space}@rspace.online aliases that forward to opted-in members'
* personal emails. Admins/moderators are opted in by default.
*/
import {
isMailcowConfigured,
createAlias,
deleteAlias,
updateAlias,
findAliasId,
} from './mailcow.js';
import {
listSpaceMembers,
getSpaceEmailAlias,
setSpaceEmailAlias,
deleteSpaceEmailAlias,
upsertSpaceEmailForwarding,
getOptedInDids,
getProfileEmailsByDids,
} from './db.js';
const TAG = '[SpaceAlias]';
/** Compute comma-separated goto from opted-in members with emails */
async function computeGoto(spaceSlug: string): Promise<string> {
const dids = await getOptedInDids(spaceSlug);
if (dids.length === 0) return `noreply+${spaceSlug}@rspace.online`;
const emailMap = await getProfileEmailsByDids(dids);
const emails = [...emailMap.values()].filter(Boolean);
if (emails.length === 0) return `noreply+${spaceSlug}@rspace.online`;
return emails.join(',');
}
/**
* Provision a Mailcow forwarding alias for a space.
* Seeds opt-in rows for all current members (admin/mod = opted in).
* Idempotent — skips if alias already exists.
*/
export async function provisionSpaceAlias(spaceSlug: string): Promise<void> {
if (!isMailcowConfigured()) {
console.warn(`${TAG} Mailcow not configured, skipping alias for ${spaceSlug}`);
return;
}
// Check if we already have it in DB
const existing = await getSpaceEmailAlias(spaceSlug);
if (existing) {
console.log(`${TAG} Alias already exists for ${spaceSlug} (id: ${existing.mailcowId})`);
return;
}
// Check if Mailcow already has it (e.g. manually created)
const address = `${spaceSlug}@rspace.online`;
const existingMailcowId = await findAliasId(address);
if (existingMailcowId) {
await setSpaceEmailAlias(spaceSlug, existingMailcowId);
console.log(`${TAG} Adopted existing Mailcow alias for ${spaceSlug} (id: ${existingMailcowId})`);
}
// Seed opt-in rows for all members
const members = await listSpaceMembers(spaceSlug);
for (const m of members) {
const optIn = m.role === 'admin' || m.role === 'moderator';
await upsertSpaceEmailForwarding(spaceSlug, m.userDID, optIn);
}
const goto = await computeGoto(spaceSlug);
if (existingMailcowId) {
// Update the existing alias with current goto
await updateAlias(existingMailcowId, goto);
return;
}
// Create new alias
const mailcowId = await createAlias(spaceSlug, goto);
await setSpaceEmailAlias(spaceSlug, mailcowId);
console.log(`${TAG} Provisioned alias for ${spaceSlug}${goto} (id: ${mailcowId})`);
}
/**
* Recompute and update the forwarding destinations for a space alias.
* If the alias doesn't exist yet, delegates to provisionSpaceAlias.
*/
export async function syncSpaceAlias(spaceSlug: string): Promise<void> {
if (!isMailcowConfigured()) return;
const alias = await getSpaceEmailAlias(spaceSlug);
if (!alias) {
await provisionSpaceAlias(spaceSlug);
return;
}
const goto = await computeGoto(spaceSlug);
await updateAlias(alias.mailcowId, goto);
console.log(`${TAG} Synced alias for ${spaceSlug}${goto}`);
}
/**
* Remove the Mailcow alias and DB records for a space.
*/
export async function deprovisionSpaceAlias(spaceSlug: string): Promise<void> {
if (!isMailcowConfigured()) return;
const alias = await getSpaceEmailAlias(spaceSlug);
if (alias) {
try {
await deleteAlias(alias.mailcowId);
} catch (e) {
console.error(`${TAG} Failed to delete Mailcow alias for ${spaceSlug}:`, e);
}
await deleteSpaceEmailAlias(spaceSlug);
}
console.log(`${TAG} Deprovisioned alias for ${spaceSlug}`);
}