/** * Email sending utility for the PieCed portal. * * Uses nodemailer with SMTP credentials from environment variables * (populated via ExternalSecret from OpenBao at pieced/portal/smtp). * * Env vars (from portal-smtp K8s secret): * SMTP_HOST — e.g. smtp.gmail.com * SMTP_PORT — e.g. 587 (default) * SMTP_USER — e.g. noreply@pieced.ch * SMTP_PASS — App Password * SMTP_FROM — e.g. "PieCed " * ADMIN_NOTIFICATION_EMAIL — e.g. admin@pieced.ch (optional) */ import nodemailer from "nodemailer"; let _transporter: nodemailer.Transporter | null = null; function getTransporter(): nodemailer.Transporter { if (!_transporter) { const host = process.env.SMTP_HOST; const user = process.env.SMTP_USER; const pass = process.env.SMTP_PASS; if (!host || !user || !pass) { throw new Error("SMTP_HOST, SMTP_USER, and SMTP_PASS must be set"); } _transporter = nodemailer.createTransport({ host, port: parseInt(process.env.SMTP_PORT || "587", 10), secure: process.env.SMTP_SECURE === "true", auth: { user, pass }, }); } return _transporter; } function getFrom(): string { return ( process.env.SMTP_FROM || `PieCed <${process.env.SMTP_USER}>` ); } /** * Escape HTML entities to prevent injection in HTML emails. */ function escapeHtml(str: string): string { return str .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } export async function sendApprovalEmail( to: string, contactName: string, companyName: string ): Promise { const safeName = escapeHtml(contactName); const safeCompany = escapeHtml(companyName); try { await getTransporter().sendMail({ from: getFrom(), to, subject: `Your PieCed AI assistant is being set up — ${companyName}`, text: [ `Hello ${contactName},`, "", `Great news! Your onboarding request for ${companyName} has been approved.`, "", "Your AI assistant instance is now being provisioned. This usually takes a few minutes.", "You can check the status in your dashboard at https://app.pieced.ch", "", "Once your instance is ready, you'll see it on your dashboard and can start configuring it.", "", "Best regards,", "PieCed IT", ].join("\n"), html: `

Your AI assistant is being set up

Hello ${safeName},

Great news! Your onboarding request for ${safeCompany} has been approved.

Your AI assistant instance is now being provisioned. This usually takes a few minutes.

Go to Dashboard

Once your instance is ready, you'll see it on your dashboard and can start configuring it.


PieCed IT — Hosted on-premises in Switzerland

`, }); } catch (err) { console.error("Failed to send approval email:", err); } } export async function sendRejectionEmail( to: string, contactName: string, companyName: string, adminNotes?: string ): Promise { const safeName = escapeHtml(contactName); const safeCompany = escapeHtml(companyName); const safeNotes = adminNotes ? escapeHtml(adminNotes) : ""; try { const notesBlock = adminNotes ? `\nNote from our team:\n${adminNotes}\n` : ""; const notesHtml = safeNotes ? `

Note from our team:

${safeNotes}

` : ""; await getTransporter().sendMail({ from: getFrom(), to, subject: `Update on your PieCed onboarding request — ${companyName}`, text: [ `Hello ${contactName},`, "", `Thank you for your interest in PieCed IT. Unfortunately, we were unable to approve your onboarding request for ${companyName} at this time.`, notesBlock, "If you have questions or would like to discuss this further, please reply to this email.", "", "Best regards,", "PieCed IT", ].join("\n"), html: `

Update on your onboarding request

Hello ${safeName},

Thank you for your interest in PieCed IT. Unfortunately, we were unable to approve your onboarding request for ${safeCompany} at this time.

${notesHtml}

If you have questions or would like to discuss this further, please reply to this email.


PieCed IT — Hosted on-premises in Switzerland

`, }); } catch (err) { console.error("Failed to send rejection email:", err); } } export async function sendAdminNotificationEmail( companyName: string, contactName: string, contactEmail: string ): Promise { const adminEmail = process.env.ADMIN_NOTIFICATION_EMAIL; if (!adminEmail) return; const safeCompany = escapeHtml(companyName); const safeName = escapeHtml(contactName); const safeEmail = escapeHtml(contactEmail); try { await getTransporter().sendMail({ from: getFrom(), to: adminEmail, subject: `New onboarding request: ${companyName}`, text: [ `A new onboarding request has been submitted.`, "", `Company: ${companyName}`, `Contact: ${contactName} (${contactEmail})`, "", `Review it at https://app.pieced.ch/admin`, ].join("\n"), html: `

New onboarding request

A new onboarding request has been submitted.

Company:${safeCompany}
Contact:${safeName} (${safeEmail})

Review Request


PieCed IT — Hosted on-premises in Switzerland

`, }); } catch (err) { console.error("Failed to send admin notification email:", err); } }