/** * Email utility — outbound SMTP via nodemailer. * * Used for: * - Order confirmation emails (sent immediately after payment) * - Portal access link emails (JWT-signed links) * * All transactional emails go through Carbonio: co.carrierone.com:587 * (SMTP2GO is used only by Listmonk for mass-mail campaigns.) * Env vars: SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROM */ import nodemailer from "nodemailer"; // ─── SMTP transport ─────────────────────────────────────────────────────────── const SMTP_HOST = process.env.SMTP_HOST || "co.carrierone.com"; const SMTP_PORT = parseInt(process.env.SMTP_PORT || "587", 10); const SMTP_USER = process.env.SMTP_USER || ""; const SMTP_PASS = process.env.SMTP_PASS || ""; const SMTP_FROM = process.env.SMTP_FROM || "Performance West "; let _transporter: nodemailer.Transporter | null = null; function getTransporter(): nodemailer.Transporter { if (!_transporter) { _transporter = nodemailer.createTransport({ host: SMTP_HOST, port: SMTP_PORT, secure: false, // STARTTLS auth: { user: SMTP_USER, pass: SMTP_PASS }, }); } return _transporter; } // ─── HTML → plaintext (dependency-free) ───────────────────────────────────── // A multipart/alternative (which nodemailer builds when both html+text are // given) with ONLY an HTML part is malformed, and HTML-only mail is a spam // signal. When a caller doesn't supply `text`, derive a readable plaintext // fallback from the HTML so every message ships a proper text/plain part. export function htmlToText(html: string): string { if (!html) return ""; let s = html; // Drop non-content blocks entirely. s = s.replace(/<(script|style|head)[\s\S]*?<\/\1>/gi, ""); // text -> text (url) s = s.replace(/]*\bhref\s*=\s*["']?([^"'>\s]+)["']?[^>]*>([\s\S]*?)<\/a>/gi, (_m, url, txt) => { const t = txt.replace(/<[^>]+>/g, "").trim(); return t && !url.startsWith("mailto:") && t !== url ? `${t} (${url})` : (t || url); }); // List items -> "- item"; block/line breaks -> newlines. s = s.replace(/]*>/gi, "\n- "); s = s.replace(/<\/(p|div|tr|h[1-6]|li|ul|ol|table)>/gi, "\n"); s = s.replace(//gi, "\n"); // Strip remaining tags, unescape common entities, collapse whitespace. s = s.replace(/<[^>]+>/g, ""); s = s.replace(/ /gi, " ").replace(/&/gi, "&").replace(/</gi, "<") .replace(/>/gi, ">").replace(/"/gi, '"').replace(/'/gi, "'") .replace(/→/gi, "->").replace(/·/gi, "-").replace(/§/gi, "Section"); s = s.replace(/[ \t]+/g, " ").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n"); s = s.split("\n").map((line) => line.trim()).join("\n"); return s.trim(); } // ─── Generic send ───────────────────────────────────────────────────────────── export async function sendEmail(opts: { to: string; subject: string; html: string; text?: string; cc?: string }): Promise { const t = getTransporter(); await t.sendMail({ from: SMTP_FROM, to: opts.to, ...(opts.cc ? { cc: opts.cc } : {}), subject: opts.subject, html: opts.html, text: opts.text || htmlToText(opts.html), }); } // ─── Branded HTML wrapper ───────────────────────────────────────────────────── function htmlEmail(title: string, body: string): string { const logo = "https://performancewest.net/images/logo.png"; return ` ${title}
Performance West Telecom Compliance
 
${body}

Problem with your order? We're here to help.

Questions, a change, or something not right? Reach our team and we'll make it right, fast.

Get help with your order →

Or email info@performancewest.net · call 1-888-411-0383

Performance West

Performance West Inc. · performancewest.net · 1-888-411-0383

`; } // ─── Types ──────────────────────────────────────────────────────────────────── export interface OrderConfirmationParams { order_id: string; order_type: string; customer_email: string; customer_name: string; session_id: string; amount_cents?: number; service_name?: string; payment_method?: string; } // ─── Order confirmation email ───────────────────────────────────────────────── const ORDER_TIMELINES: Record = { canada_crtc: "6–10 weeks", formation: "3–5 business days", bundle: "5–10 business days", compliance: "1–10 business days (varies by service)", compliance_batch: "1–10 business days (varies by service)", }; const ORDER_LABELS: Record = { canada_crtc: "Canada CRTC Telecom Carrier Package", formation: "Business Formation", bundle: "Compliance Bundle", compliance: "Compliance Service", compliance_batch: "FCC Compliance Services", }; const ORDER_NEXT_STEPS: Record = { canada_crtc: [ "We will begin your BC corporation incorporation within 1–2 business days.", "You will receive an email to choose your .ca domain name once your corporation is registered.", "Your CRTC registration letter will be prepared after incorporation.", "Your complete corporate binder will be delivered by email when all steps are complete.", "A Canadian business banking referral will be sent upon delivery.", ], formation: [ "We will begin your state filing within 1 business day.", "You will receive your filed documents by email when processing is complete.", "If you ordered an EIN, we will obtain it from the IRS after filing.", ], bundle: [ "Our team will review your order within 1 business day.", "You will be contacted to schedule any required consultations.", "Completed deliverables will be emailed to you as they are ready.", ], compliance: [ "Our team will review your order within 1 business day.", "You will be contacted if we need any additional information.", "Your completed deliverable will be emailed when ready.", ], compliance_batch: [ "We will begin processing your services within 1 business day.", "Some services (CPNI, RMD) may require your review and approval before we submit to the FCC.", "You will receive a separate confirmation email for each filing as it is completed.", "If we need any additional information, we will contact you by email.", ], }; export async function sendOrderConfirmationEmail(params: OrderConfirmationParams): Promise { const { order_id, order_type, customer_email, customer_name, session_id, amount_cents, service_name: svcName, payment_method } = params; if (!customer_email) { console.warn("[email] sendOrderConfirmationEmail: no customer_email for", order_id); return; } if (!SMTP_USER || !SMTP_PASS) { console.warn("[email] SMTP not configured — skipping confirmation email for", order_id); return; } const firstName = customer_name.split(" ")[0] || customer_name; const label = svcName || ORDER_LABELS[order_type] || "Your Order"; const timeline = ORDER_TIMELINES[order_type] || "1–10 business days"; const nextSteps = ORDER_NEXT_STEPS[order_type] || []; const stepsHtml = nextSteps .map((s, i) => `

${i + 1}. ${s}

`) .join("\n"); const body = `

Order Confirmed

Hi ${firstName}, your payment has been received. Here is your order summary.

${amount_cents ? `` : ""}

Service

${svcName || label}

Order Number

${order_id}

Amount Paid

$${(amount_cents / 100).toFixed(2)}

${payment_method ? `

via ${payment_method}

` : ""}

Estimated Turnaround

${timeline}

${(order_type === "compliance" || order_type === "compliance_batch") ? `

FCC compliance fees are tax deductible as ordinary business expenses under IRC § 162. A formal receipt will be sent separately.

` : ""}

What happens next

${stepsHtml}

Set up your client portal

Track your orders, download documents, and manage your services.

Access Client Portal →

First time? Set your password here

Questions? Reply to this email or reach us at info@performancewest.net or call 1-888-411-0383.

`; const transporter = getTransporter(); await transporter.sendMail({ from: SMTP_FROM, to: customer_email, subject: `Order Confirmed — ${order_id} — ${label}`, html: htmlEmail("Order Confirmed — Performance West", body), text: [ `Hi ${firstName},`, ``, `Your payment has been received for order ${order_id}.`, `Service: ${label}`, `Estimated turnaround: ${timeline}`, ``, `What happens next:`, ...nextSteps.map((s, i) => `${i + 1}. ${s}`), ``, `Questions? Email info@performancewest.net or call 1-888-411-0383.`, ``, `Performance West Inc.`, ].join("\n"), }); console.log(`[email] Confirmation sent to ${customer_email} for ${order_id}`); } // ─── Portal access link email ───────────────────────────────────────────────── export interface PortalLinkParams { customer_email: string; customer_name: string; portal_url: string; // full signed URL order_id: string; link_purpose: string; // e.g. "domain selection", "manage your services" } export async function sendPortalLinkEmail(params: PortalLinkParams): Promise { const { customer_email, customer_name, portal_url, order_id, link_purpose } = params; if (!customer_email || !SMTP_USER || !SMTP_PASS) return; const firstName = customer_name.split(" ")[0] || customer_name; const body = `

Action Required

Hi ${firstName}, a step in your order requires your input.

Please click the button below to complete ${link_purpose} for order ${order_id}.

Continue →

This link expires in 72 hours. If you did not request this, please ignore this email.

`; const transporter = getTransporter(); await transporter.sendMail({ from: SMTP_FROM, to: customer_email, subject: `Action needed for order ${order_id} — ${link_purpose}`, html: htmlEmail("Action Needed — Performance West", body), text: `Hi ${firstName},\n\nPlease complete ${link_purpose} for order ${order_id}:\n\n${portal_url}\n\nThis link expires in 72 hours.\n\nPerformance West Inc.`, }); console.log(`[email] Portal link sent to ${customer_email} for ${order_id} (${link_purpose})`); }