/** * 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 { 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 { 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 { 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 { 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}`); }