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 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)}
Direkt antworten →
Oder einfach auf diese E-Mail antworten — Reply-To ist bereits gesetzt.
`;
}
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));
}
}
// 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 });
}
}