Files
Wabenhain/src/lib/mail.ts
T
Nick SzaunigandClaude Opus 5.5 839bbff595 feat: sichtbare Rueckmeldung fuer alle Aktionen (Toast statt URL-Meldungen)
Viele Aktionen gaben keine Rueckmeldung (Retoure annehmen, Archiv,
Bewertungen, Adressen, Newsletter, Kundensperre ...) oder meldeten Erfolg,
obwohl die zugehoerige E-Mail nie rausging.

- lib/flash: Meldung als kurzlebiges Cookie; Toast im Root-Layout
  (FlashToast). Ersetzt ?message=/?error= in der URL — darueber liess sich
  bisher beliebiger Text als Systemmeldung im Wabenhain-Layout anzeigen
  (Phishing-Hebel). Server Components leiten ueber /api/hinweis mit festen
  Codes und festen Zielen weiter.
- fail()/succeed() zentral und asynchron; ein Quelltext-Test stellt sicher,
  dass jeder Aufruf mit await/return erfolgt (sonst liefe eine Action nach
  einem Fehler weiter).
- sendMail meldet, ob die Mail angenommen wurde; Aktionen mit Mail zeigen
  ehrlich an, wenn der Versand scheitert (z.B. "Link erneut senden").
- Jede Server Action meldet jetzt Erfolg oder den Grund des Abbruchs.
- SubmitFeedback: geklickter Knopf zeigt sofort einen Ladezustand und ist
  bis zur Antwort gegen Doppelklick gesperrt.
- 2FA: nach dem Einrichten zurueck ins Profil statt auf die Seite mit
  "Deaktivieren".

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-25 04:36:36 +02:00

103 lines
3.5 KiB
TypeScript

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<typeof nodemailer.createTransport> | 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<boolean> {
const transport = getTransport();
const from = process.env.SMTP_FROM || "Wabenhain <[email protected]>";
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;
}
}