125 lines
4.9 KiB
JavaScript
125 lines
4.9 KiB
JavaScript
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) =>
|
||
({
|
||
"&": "&",
|
||
"<": "<",
|
||
">": ">",
|
||
'"': """,
|
||
"'": "'",
|
||
})[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");
|
||
}
|
||
};
|
||
}
|