121 lines
3.7 KiB
TypeScript
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}`);
|
|
}
|