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]>
103 lines
3.5 KiB
TypeScript
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;
|
|
}
|
|
}
|