import nodemailer from "nodemailer"; export interface MailAttachment { filename: string; content: Buffer; contentType: string; } interface SendMailInput { to: string; subject: string; html: string; text: string; /** * Optionale Anhaenge, aktuell die Rechnung zur Bestellbestaetigung. * Bewusst als Buffer und nicht als Pfad: der Aufrufer hat das Dokument * ohnehin schon im Speicher, und so haengt der Mailversand nicht daran, * dass das Ablageverzeichnis gerade erreichbar ist. */ attachments?: MailAttachment[]; } /** Maskiert eine E-Mail-Adresse fuer Log-Ausgaben, z.B. "ma***@example.com". */ function maskEmail(email: string): string { const [local, domain] = email.split("@"); if (!domain) return "***"; return `${local.slice(0, 2)}***@${domain}`; } let cachedTransport: ReturnType | null = null; // Nur einmal geprueft (nicht bei jedem sendMail-Aufruf) — macht ein in // Produktion fehlendes SMTP-Setup im Server-Log sichtbar, statt dass // Bestellbestaetigungen/Passwort-Resets fortlaufend lautlos verschwinden. let warnedAboutMissingSmtpInProduction = false; function getTransport() { if (cachedTransport) return cachedTransport; const host = process.env.SMTP_HOST; const port = Number(process.env.SMTP_PORT ?? 587); const user = process.env.SMTP_USER; const pass = process.env.SMTP_PASSWORD; if (!host || !user || !pass) { if (process.env.NODE_ENV === "production" && !warnedAboutMissingSmtpInProduction) { warnedAboutMissingSmtpInProduction = true; console.warn( "SMTP_HOST/SMTP_USER/SMTP_PASSWORD sind in Produktion nicht gesetzt — Bestellbestaetigungen, Passwort-Resets und weitere Transaktions-E-Mails werden nur geloggt, nicht versendet." ); } return null; } cachedTransport = nodemailer.createTransport({ host, port, secure: port === 465, auth: { user, pass }, }); return cachedTransport; } /** * Versendet eine E-Mail ueber den in SMTP_HOST/SMTP_USER/SMTP_PASSWORD * konfigurierten Mailserver. Ohne Konfiguration wird die Mail NICHT * gesendet, sondern nur geloggt — analog zum Stripe-Fallback ohne Keys, * damit lokale Entwicklung und Deployments ohne Mail-Setup nicht abstuerzen. * Wirft absichtlich keinen Fehler: ein Mail-Ausfall darf eine Bestellung * oder Registrierung nicht verhindern. * * Liefert true, wenn der Mailserver die Nachricht angenommen hat, sonst * false (nicht konfiguriert oder Versand fehlgeschlagen). Aktionen mit * sichtbarer Rueckmeldung ("Link erneut gesendet", "Kundschaft informiert") * duerfen Erfolg nur melden, wenn hier true zurueckkam — sonst wartet die * Kundschaft auf eine Mail, die nie kommt. */ export async function sendMail(input: SendMailInput): Promise { const transport = getTransport(); const from = process.env.SMTP_FROM || "Wabenhain "; if (!transport) { console.warn( `[mail] SMTP nicht konfiguriert — E-Mail an ${maskEmail(input.to)} ("${input.subject}") wurde nicht gesendet.` ); return false; } try { await transport.sendMail({ from, to: input.to, subject: input.subject, html: input.html, text: input.text, ...(input.attachments?.length ? { attachments: input.attachments } : {}), }); return true; } catch (error: unknown) { const message = error instanceof Error ? error.message : "Unbekannter Fehler"; console.error(`[mail] Versand an ${maskEmail(input.to)} fehlgeschlagen: ${message}`); return false; } }