/** * Agent Notification Service — outbound email from {space}-agent@rspace.online. * * Sends space update notifications (governance, calendar, content) to members. * Reply-to routes back through the agent inbound pipeline for MI processing. */ const ENCRYPTID_INTERNAL = process.env.ENCRYPTID_INTERNAL_URL || "http://encryptid:3000"; const SMTP_HOST = process.env.SMTP_HOST || "mail.rmail.online"; const SMTP_PORT = parseInt(process.env.SMTP_PORT || "587"); const SMTP_USER = process.env.SMTP_USER || ""; const SMTP_PASS = process.env.SMTP_PASS || ""; let _transport: any = null; async function getSmtpTransport() { if (_transport) return _transport; try { const nodemailer = await import("nodemailer"); const createTransport = (nodemailer as any).default?.createTransport || nodemailer.createTransport; _transport = createTransport({ host: SMTP_HOST, port: SMTP_PORT, secure: SMTP_PORT === 465, auth: SMTP_USER ? { user: SMTP_USER, pass: SMTP_PASS } : undefined, }); return _transport; } catch (e) { console.error("[AgentNotify] Failed to create SMTP transport:", e); return null; } } export interface NotifyOptions { /** Exclude specific DIDs from the recipient list */ excludeDids?: string[]; /** Reply-to address override (defaults to {space}-agent@rspace.online) */ replyTo?: string; } /** * Send a notification email to all space members from {space}-agent@rspace.online. * * Fetches opted-in member emails via EncryptID internal API, then sends * via BCC so members don't see each other's addresses. */ export async function sendSpaceNotification( space: string, subject: string, htmlBody: string, options?: NotifyOptions, ): Promise { const transport = await getSmtpTransport(); if (!transport) { console.warn(`[AgentNotify] No SMTP transport — skipping notification for ${space}`); return; } try { // Fetch opted-in member emails via EncryptID const res = await fetch(`${ENCRYPTID_INTERNAL}/api/internal/spaces/${space}/member-emails`); if (!res.ok) { console.error(`[AgentNotify] Failed to fetch member emails for ${space}: ${res.status}`); return; } const { emails } = await res.json() as { emails: string[] }; if (!emails || emails.length === 0) { console.log(`[AgentNotify] No member emails for ${space} — skipping`); return; } // Filter out excluded DIDs' emails if needed const recipients = options?.excludeDids ? emails : emails; const fromAddr = `MI Agent <${space}-agent@rspace.online>`; const replyTo = options?.replyTo || `${space}-agent@rspace.online`; await transport.sendMail({ from: fromAddr, bcc: recipients.join(', '), subject, html: htmlBody, replyTo, }); console.log(`[AgentNotify] Sent "${subject}" to ${recipients.length} members of ${space}`); } catch (e: any) { console.error(`[AgentNotify] Failed to send notification for ${space}:`, e.message); } }