import nodemailer from "nodemailer"; import { orderSummaryRows } from "../../shared/order-summary.js"; import { paymentNotice } from "../../shared/booking-copy.js"; export function createEmailService( env = process.env, makeTransport = nodemailer.createTransport, ) { const localHosts = ["127.0.0.1", "localhost", "::1", "mailpit"]; const host = env.SMTP_HOST || "127.0.0.1"; const local = localHosts.includes(host); if (!local && env.SMTP_ALLOW_EXTERNAL !== "true") { throw new Error( "Externes SMTP ist gesperrt. Erst nach ausdrücklicher Freigabe SMTP_ALLOW_EXTERNAL=true setzen.", ); } const port = Number(env.SMTP_PORT || (local ? 1025 : 587)); if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("Ungültiger SMTP_PORT"); if (Boolean(env.SMTP_USER) !== Boolean(env.SMTP_PASS)) throw new Error("SMTP_USER und SMTP_PASS gemeinsam setzen"); const transport = makeTransport({ host, port, secure: env.SMTP_SECURE === "true", requireTLS: !local, auth: env.SMTP_USER ? { user: env.SMTP_USER, pass: env.SMTP_PASS } : undefined, connectionTimeout: 10000, greetingTimeout: 10000, socketTimeout: 20000, disableFileAccess: true, disableUrlAccess: true, }); const from = env.SMTP_FROM || "Tri-Hub "; return async function sendConfirmation(booking) { const order = booking.bookedPackage; const phone = booking.phone || booking.customer?.phone; const lines = [ `Hallo ${booking.name},`, "", "vielen Dank für deine Bestellung bei Tri-Hub. Hier findest du die Zusammenfassung deiner Buchung.", `Buchungsnummer: ${booking.bookingId}`, ...(booking.createdAt ? [ `Bestelldatum: ${new Intl.DateTimeFormat("de-DE", { dateStyle: "long", timeStyle: "short", timeZone: "Europe/Berlin" }).format(new Date(booking.createdAt))} (Europe/Berlin)`, ] : []), `Kunde: ${booking.name}`, `E-Mail: ${booking.email}`, ...(phone ? [`Telefon: ${phone}`] : []), ...(booking.customer?.notes ? [`Deine Anmerkungen: ${booking.customer.notes}`] : []), "", `Paket: ${order?.title ?? booking.packageName}`, ...(order ? [ order.summaryForBooking, order.subtitle, "", ...orderSummaryRows(order).map( ([label, value]) => `${label}: ${value}`, ), "", "Enthaltene Leistungen:", ...order.includedFeatures.map( (feature) => `• ${feature}`, ), "", "So geht es weiter:", ...(order.processSteps || []).map( ({ step, text }) => `${step}: ${text}`, ), ] : []), "", paymentNotice, "", "Bitte bewahre diese Bestätigung auf und gib bei Rückfragen deine Buchungsnummer an.", "Dein Tri-Hub-Team", ]; const escapeHtml = (value) => String(value).replace( /[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", })[char], ); let info; try { info = await transport.sendMail({ from, to: { address: booking.email, name: booking.name }, subject: `Bestellbestätigung ${booking.bookingId} – Tri-Hub`, text: lines.join("\n"), html: `

Deine Bestellbestätigung

${lines.map((line) => (line ? `

${escapeHtml(line)}

` : "")).join("")}`, }); } catch (error) { // Bei Verbindungsabbruch kann SMTP die Nachricht schon angenommen // haben. Nur ausdrückliche Ablehnungen gelten sicher als Fehler. const explicitRejection = ["EAUTH", "EENVELOPE", "ECONNECTION", "EDNS"].includes( error.code, ) || Number(error.responseCode) >= 400; error.deliveryUnknown = !explicitRejection; throw error; } if ( !info.accepted?.some( (address) => address.toLowerCase() === booking.email.toLowerCase(), ) ) { throw new Error("Empfänger vom Mailserver nicht angenommen"); } }; }