/** Send magic link invitations via email. */ import { magicLinkConfig } from "./config"; import { createMagicToken } from "./token"; export interface MagicLinkRecipient { name: string; email: string; } export interface SendMagicLinkOpts { space: string; type: "poll" | "rsvp"; targetId: string; /** Title of the poll session or event (for email subject) */ title: string; participants: MagicLinkRecipient[]; /** If false, only return URLs without sending emails */ sendEmail?: boolean; } export interface MagicLinkResult { name: string; email: string; url: string; sent: boolean; } export async function sendMagicLinks(opts: SendMagicLinkOpts): Promise { const results: MagicLinkResult[] = []; for (const participant of opts.participants) { const token = await createMagicToken(opts.space, opts.type, opts.targetId, participant.name); const url = `${magicLinkConfig.baseUrl}/respond/${token}`; let sent = false; if (opts.sendEmail !== false && participant.email) { try { const nodemailer = await import("nodemailer"); const transport = nodemailer.createTransport({ host: process.env.SMTP_HOST || "mailcowdockerized-postfix-mailcow-1", port: Number(process.env.SMTP_PORT) || 587, secure: false, auth: { user: process.env.SMTP_USER || "noreply@rmail.online", pass: process.env.SMTP_PASS, }, tls: { rejectUnauthorized: false }, }); const typeLabel = opts.type === "poll" ? "Poll" : "RSVP"; const subject = `${typeLabel}: ${opts.title}`; const html = buildInviteEmail(opts, participant, url); await transport.sendMail({ from: process.env.SMTP_FROM || `rSpace <${process.env.SMTP_USER || "noreply@rmail.online"}>`, to: participant.email, subject, html, }); sent = true; } catch (err) { console.error(`[MagicLink] Email send failed for ${participant.email}:`, err); } } results.push({ name: participant.name, email: participant.email, url, sent }); } return results; } function esc(str: string): string { return str.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } function buildInviteEmail(opts: SendMagicLinkOpts, participant: MagicLinkRecipient, url: string): string { const typeLabel = opts.type === "poll" ? "Poll" : "Event RSVP"; const actionLabel = opts.type === "poll" ? "Vote Now" : "RSVP Now"; return `

${esc(opts.title)}

Hi ${esc(participant.name)}, you've been invited to respond to this ${typeLabel.toLowerCase()}.

${actionLabel}

This link expires in ${magicLinkConfig.tokenExpiryDays} days. No account required.

Sent by rSpace · rspace.online

`; }