trihub-ernaehrung-backseatDevs/src/server/services/email-service.js

125 lines
4.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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 <buchung@example.test>";
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) =>
({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
})[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: `<html lang="de"><body><h1>Deine Bestellbestätigung</h1>${lines.map((line) => (line ? `<p>${escapeHtml(line)}</p>` : "")).join("")}</body></html>`,
});
} 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");
}
};
}