import { env } from "@kk/env/server"; import nodemailer from "nodemailer"; // Singleton transport — created once per module, reused across all email sends. // Re-creating it on every call causes EventEmitter listener accumulation in // long-lived Cloudflare Worker processes, triggering memory leak warnings. let _transport: nodemailer.Transporter | null | undefined; function getTransport(): nodemailer.Transporter | null { if (_transport !== undefined) return _transport; if (!env.SMTP_HOST || !env.SMTP_USER || !env.SMTP_PASS) { _transport = null; return null; } _transport = nodemailer.createTransport({ host: env.SMTP_HOST, port: env.SMTP_PORT, secure: env.SMTP_PORT === 465, auth: { user: env.SMTP_USER, pass: env.SMTP_PASS, }, }); return _transport; } const from = env.SMTP_FROM ?? "Kunstenkamp "; const baseUrl = env.BETTER_AUTH_URL ?? "https://kunstenkamp.be"; function registrationConfirmationHtml(params: { firstName: string; manageUrl: string; wantsToPerform: boolean; artForm?: string | null; giftAmount?: number; drinkCardValue?: number; }) { const role = params.wantsToPerform ? `Optreden${params.artForm ? ` — ${params.artForm}` : ""}` : "Toeschouwer"; const giftCents = params.giftAmount ?? 0; // drinkCardValue is stored in euros (e.g., 5), giftAmount in cents (e.g., 5000) const drinkCardCents = (params.drinkCardValue ?? 0) * 100; const hasPayment = drinkCardCents > 0 || giftCents > 0; const formatEuro = (cents: number) => `€${(cents / 100).toFixed(cents % 100 === 0 ? 0 : 2).replace(".", ",")}`; const paymentSummary = hasPayment ? `

Betaling

${drinkCardCents > 0 ? `

Drinkkaart: ${formatEuro(drinkCardCents)}

` : ""} ${giftCents > 0 ? `

Vrijwillige gift: ${formatEuro(giftCents)}

` : ""}

Totaal: ${formatEuro(drinkCardCents + giftCents)}

` : ""; return ` Bevestiging inschrijving

Kunstenkamp

Je inschrijving is bevestigd!

Hoi ${params.firstName},

We hebben je inschrijving voor Open Mic Night — vrijdag 18 april 2026 in goede orde ontvangen.

Jouw rol

${role}

${paymentSummary}

Wil je je gegevens later nog aanpassen of je inschrijving annuleren? Gebruik dan de knop hieronder. De link is uniek voor jou — deel hem niet.

Beheer mijn inschrijving

Of kopieer deze link: ${params.manageUrl}

Vragen? Mail ons op info@kunstenkamp.be
Kunstenkamp vzw — Een initiatief voor en door kunstenaars.

`; } function updateConfirmationHtml(params: { firstName: string; manageUrl: string; wantsToPerform: boolean; artForm?: string | null; giftAmount?: number; drinkCardValue?: number; }) { const role = params.wantsToPerform ? `Optreden${params.artForm ? ` — ${params.artForm}` : ""}` : "Toeschouwer"; const giftCents = params.giftAmount ?? 0; // drinkCardValue is stored in euros (e.g., 5), giftAmount in cents (e.g., 5000) const drinkCardCents = (params.drinkCardValue ?? 0) * 100; const hasPayment = drinkCardCents > 0 || giftCents > 0; const formatEuro = (cents: number) => `€${(cents / 100).toFixed(cents % 100 === 0 ? 0 : 2).replace(".", ",")}`; const paymentSummary = hasPayment ? `

Betaling

${drinkCardCents > 0 ? `

Drinkkaart: ${formatEuro(drinkCardCents)}

` : ""} ${giftCents > 0 ? `

Vrijwillige gift: ${formatEuro(giftCents)}

` : ""}

Totaal: ${formatEuro(drinkCardCents + giftCents)}

` : ""; return ` Inschrijving bijgewerkt

Kunstenkamp

Inschrijving bijgewerkt

Hoi ${params.firstName},

Je inschrijving voor Open Mic Night — vrijdag 18 april 2026 is succesvol bijgewerkt.

Jouw rol

${role}

${paymentSummary}
Bekijk mijn inschrijving

Vragen? Mail ons op info@kunstenkamp.be

`; } function cancellationHtml(params: { firstName: string }) { return ` Inschrijving geannuleerd

Kunstenkamp

Inschrijving geannuleerd

Hoi ${params.firstName},

Je inschrijving voor Open Mic Night — vrijdag 18 april 2026 is geannuleerd.

Van gedachten veranderd? Je kunt je altijd opnieuw inschrijven via kunstenkamp.be.

Vragen? Mail ons op info@kunstenkamp.be

`; } export async function sendConfirmationEmail(params: { to: string; firstName: string; managementToken: string; wantsToPerform: boolean; artForm?: string | null; giftAmount?: number; drinkCardValue?: number; }) { const transport = getTransport(); if (!transport) { console.warn("SMTP not configured — skipping confirmation email"); return; } const manageUrl = `${baseUrl}/manage/${params.managementToken}`; await transport.sendMail({ from, to: params.to, subject: "Bevestiging inschrijving — Open Mic Night", html: registrationConfirmationHtml({ firstName: params.firstName, manageUrl, wantsToPerform: params.wantsToPerform, artForm: params.artForm, giftAmount: params.giftAmount, drinkCardValue: params.drinkCardValue, }), }); } export async function sendUpdateEmail(params: { to: string; firstName: string; managementToken: string; wantsToPerform: boolean; artForm?: string | null; giftAmount?: number; drinkCardValue?: number; }) { const transport = getTransport(); if (!transport) { console.warn("SMTP not configured — skipping update email"); return; } const manageUrl = `${baseUrl}/manage/${params.managementToken}`; await transport.sendMail({ from, to: params.to, subject: "Inschrijving bijgewerkt — Open Mic Night", html: updateConfirmationHtml({ firstName: params.firstName, manageUrl, wantsToPerform: params.wantsToPerform, artForm: params.artForm, giftAmount: params.giftAmount, drinkCardValue: params.drinkCardValue, }), }); } export async function sendCancellationEmail(params: { to: string; firstName: string; }) { const transport = getTransport(); if (!transport) { console.warn("SMTP not configured — skipping cancellation email"); return; } await transport.sendMail({ from, to: params.to, subject: "Inschrijving geannuleerd — Open Mic Night", html: cancellationHtml({ firstName: params.firstName }), }); }