rspace-online/modules/rinbox/agent-notify.ts

92 lines
2.9 KiB
TypeScript

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