92 lines
2.9 KiB
TypeScript
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);
|
|
}
|
|
}
|