import { type NextRequest, NextResponse } from "next/server"; import nodemailer from "nodemailer"; import SMTPTransport from "nodemailer/lib/smtp-transport"; import Mail from "nodemailer/lib/mailer"; import { checkRateLimit, getRateLimitHeaders } from '@/lib/auth'; import { prisma } from "@/lib/prisma"; function sanitizeInput(input: string, maxLength: number = 10000): string { return input.slice(0, maxLength).replace(/[<>]/g, '').trim(); } function escapeHtml(input: string): string { return input .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function escapeHtmlTg(input: string): string { return input.replace(/&/g, "&").replace(//g, ">"); } async function sendTelegramNotification(data: { name: string; email: string; subject: string; message: string; sentAt: string; }): Promise { const token = process.env.TELEGRAM_BOT_TOKEN; const chatId = process.env.TELEGRAM_CHAT_ID; if (!token || !chatId) return; const preview = data.message.length > 400 ? data.message.slice(0, 400) + "…" : data.message; const text = [ "🔔 Neue Kontaktanfrage", "", `👤 Name: ${escapeHtmlTg(data.name)}`, `📧 Email: ${escapeHtmlTg(data.email)}`, `📌 Betreff: ${escapeHtmlTg(data.subject)}`, "", "💬 Nachricht:", `${escapeHtmlTg(preview)}`, "", `🕐 ${escapeHtmlTg(data.sentAt)}`, ].join("\n"); try { await fetch(`https://api.telegram.org/bot${token}/sendMessage`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ chat_id: chatId, text, parse_mode: "HTML", reply_markup: { inline_keyboard: [[ { text: "📧 Per E-Mail antworten", url: `mailto:${data.email}?subject=${encodeURIComponent("Re: " + data.subject)}`, }, ]], }, }), signal: AbortSignal.timeout(5000), }); } catch { // Never fail the contact form because of Telegram } } function buildNotificationEmail(opts: { name: string; email: string; subject: string; messageHtml: string; initial: string; replyHref: string; sentAt: string; }): string { const { name, email, subject, messageHtml, initial, replyHref, sentAt } = opts; return ` Neue Kontaktanfrage
dk0.dev · Portfolio Kontakt
Neue Kontaktanfrage
${escapeHtml(sentAt)}
dk0.dev
${escapeHtml(initial)}
${escapeHtml(name)}
${escapeHtml(email)}
${escapeHtml(subject)}
Nachricht
${messageHtml}
Direkt antworten →
Oder einfach auf diese E-Mail antworten — Reply-To ist bereits gesetzt.
Automatisch generiert · dk0.dev
contact@dk0.dev
`; } export async function POST(request: NextRequest) { try { const ip = request.headers?.get?.('x-forwarded-for') ?? request.headers?.get?.('x-real-ip') ?? 'unknown'; if (!checkRateLimit(ip, 5, 60000)) { return NextResponse.json( { error: 'Zu viele Anfragen. Bitte versuchen Sie es später erneut.' }, { status: 429, headers: { 'Content-Type': 'application/json', ...getRateLimitHeaders(ip, 5, 60000) }, } ); } const body = (await request.json()) as { email: string; name: string; subject: string; message: string; }; const email = sanitizeInput(body.email || '', 255); const name = sanitizeInput(body.name || '', 100); const subject = sanitizeInput(body.subject || '', 200); const message = sanitizeInput(body.message || '', 5000); if (!email || !name || !subject || !message) { return NextResponse.json({ error: "Alle Felder sind erforderlich" }, { status: 400 }); } const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { return NextResponse.json({ error: "Ungültige E-Mail-Adresse" }, { status: 400 }); } if (message.length < 10) { return NextResponse.json({ error: "Nachricht muss mindestens 10 Zeichen lang sein" }, { status: 400 }); } if (name.length > 100 || subject.length > 200 || message.length > 5000) { return NextResponse.json({ error: "Eingabe zu lang" }, { status: 400 }); } const user = process.env.MY_EMAIL ?? ""; const pass = process.env.MY_PASSWORD ?? ""; if (!user || !pass) { console.error("❌ Missing email/password environment variables"); return NextResponse.json({ error: "E-Mail-Server nicht konfiguriert" }, { status: 500 }); } const transportOptions: SMTPTransport.Options = { host: "mail.dk0.dev", port: 587, secure: false, requireTLS: true, auth: { type: "login", user, pass }, connectionTimeout: 30000, greetingTimeout: 30000, socketTimeout: 60000, tls: process.env.SMTP_ALLOW_INSECURE_TLS === "true" || process.env.SMTP_ALLOW_SELF_SIGNED === "true" ? { rejectUnauthorized: false } : { rejectUnauthorized: true, minVersion: "TLSv1.2" }, }; const transport = nodemailer.createTransport(transportOptions); let verificationAttempts = 0; while (verificationAttempts < 3) { try { verificationAttempts++; await transport.verify(); break; } catch (verifyError) { if (process.env.NODE_ENV === 'development') { console.error(`SMTP verification attempt ${verificationAttempts} failed:`, verifyError); } if (verificationAttempts >= 3) { return NextResponse.json({ error: "E-Mail-Server-Verbindung fehlgeschlagen" }, { status: 500 }); } await new Promise(resolve => setTimeout(resolve, 2000)); } } const sentAt = new Date().toLocaleString('de-DE', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', }); const initial = (name.trim()[0] || "?").toUpperCase(); const replyHref = `mailto:${email}?subject=${encodeURIComponent(`Re: ${subject}`)}`; const messageHtml = escapeHtml(message).replace(/\n/g, "
"); const mailOptions: Mail.Options = { from: `"Portfolio Contact" <${user}>`, to: "contact@dk0.dev", replyTo: email, subject: `📬 Neue Anfrage: ${subject}`, html: buildNotificationEmail({ name, email, subject, messageHtml, initial, replyHref, sentAt }), text: `Neue Kontaktanfrage\n\nVon: ${name} (${email})\nBetreff: ${subject}\n\n${message}\n\n---\nEingegangen: ${sentAt}`, }; let sendAttempts = 0; let result = ''; while (sendAttempts < 3) { try { sendAttempts++; result = await new Promise((resolve, reject) => { transport.sendMail(mailOptions, (err, info) => { if (!err) resolve(info.response); else { if (process.env.NODE_ENV === 'development') console.error("Error sending email:", err); reject(err.message); } }); }); break; } catch (sendError) { if (sendAttempts >= 3) { throw new Error(`Failed to send email after 3 attempts: ${sendError}`); } await new Promise(resolve => setTimeout(resolve, 3000)); } } // Telegram notification — fire and forget, never blocks the response sendTelegramNotification({ name, email, subject, message, sentAt }).catch(() => {}); // Save to DB try { await prisma.contact.create({ data: { name, email, subject, message, responded: false } }); } catch (dbError) { if (process.env.NODE_ENV === 'development') console.error('Error saving contact to DB:', dbError); } return NextResponse.json({ message: "E-Mail erfolgreich gesendet", messageId: result }); } catch (err) { console.error("❌ Unexpected error in email API:", err); return NextResponse.json({ error: "Fehler beim Senden der E-Mail", details: err instanceof Error ? err.message : 'Unbekannter Fehler', }, { status: 500 }); } }