From 63960f7581deb6f8016771352e48fc93f6c022db Mon Sep 17 00:00:00 2001 From: denshooter Date: Sat, 7 Mar 2026 00:17:39 +0100 Subject: [PATCH] feat: dark email design + Telegram notification for contact form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Notification email (to Dennis): - Complete dark-theme redesign: #0c0c0c bg, #141414 card, gradient top bar - Sender avatar with liquid-mint/sky gradient + initial letter - Subject displayed as pill badge - Message in styled blockquote with mint left border - Gradient "Direkt antworten" CTA button - replyTo header already set so email Reply goes directly to sender Telegram notification: - sendTelegramNotification() fires after successful email send (fire-and-forget) - Uses TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID env vars (silently skips if absent) - HTML-formatted message with emojis, name/email/subject/message preview - Inline keyboard button "Per E-Mail antworten" with pre-filled mailto link - Never blocks the contact form response if Telegram fails Reply email templates (respond/route.tsx): - Same dark design system as notification email - baseEmail() generates consistent header + footer - messageCard() helper for styled message blocks with colored left border - ctaButton() helper for gradient CTA buttons - Templates: welcome, project, quick, reply — all updated to dark theme Required new env vars: TELEGRAM_BOT_TOKEN= TELEGRAM_CHAT_ID= Co-Authored-By: Claude Sonnet 4.6 --- app/api/email/respond/route.tsx | 313 ++++++++------------- app/api/email/route.tsx | 464 +++++++++++++++----------------- 2 files changed, 333 insertions(+), 444 deletions(-) diff --git a/app/api/email/respond/route.tsx b/app/api/email/respond/route.tsx index cab9ddc..0ec3595 100644 --- a/app/api/email/respond/route.tsx +++ b/app/api/email/respond/route.tsx @@ -4,15 +4,12 @@ import SMTPTransport from "nodemailer/lib/smtp-transport"; import Mail from "nodemailer/lib/mailer"; import { checkRateLimit, getRateLimitHeaders, getClientIp, requireSessionAuth } from "@/lib/auth"; -const BRAND = { +const B = { siteUrl: "https://dk0.dev", email: "contact@dk0.dev", - bg: "#FDFCF8", - sand: "#F3F1E7", - border: "#E7E5E4", - text: "#292524", - muted: "#78716C", mint: "#A7F3D0", + sky: "#BAE6FD", + purple: "#E9D5FF", red: "#EF4444", }; @@ -26,58 +23,86 @@ function escapeHtml(input: string): string { } function nl2br(input: string): string { - return input.replace(/\r\n|\r|\n/g, "
"); + return escapeHtml(input).replace(/\r\n|\r|\n/g, "
"); } -function baseEmail(opts: { title: string; subtitle: string; bodyHtml: string }) { +function baseEmail(opts: { title: string; preheader: string; bodyHtml: string }): string { const sentAt = new Date().toLocaleString("de-DE", { - year: "numeric", - month: "long", - day: "numeric", - hour: "2-digit", - minute: "2-digit", + year: "numeric", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit", }); - return ` - + return ` - + ${escapeHtml(opts.title)} - -
-
-
-
-
Dennis Konkol
-
- dk0.dev + + +
+
+ + +
+
+
+
+
+
+ ${escapeHtml(opts.preheader)} · ${sentAt} +
+
+ ${escapeHtml(opts.title)} +
+
+
+ dk0.dev +
-
-
${escapeHtml(opts.title)}
-
${escapeHtml(opts.subtitle)} • ${sentAt}
-
-
-
+ +
${opts.bodyHtml}
-
-
- Automatisch generiert von dk0.dev • - ${BRAND.email} + +
+
+ +
+
- - `.trim(); +`; +} + +function messageCard(label: string, html: string, accentColor: string = B.mint): string { + return ` +
+
+ ${label} +
+
${html}
+
`; +} + +function ctaButton(text: string, href: string): string { + return ` +`; } const emailTemplates = { @@ -85,31 +110,16 @@ const emailTemplates = { subject: "Vielen Dank für deine Nachricht! 👋", template: (name: string, originalMessage: string) => { const safeName = escapeHtml(name); - const safeMsg = nl2br(escapeHtml(originalMessage)); return baseEmail({ title: `Danke, ${safeName}!`, - subtitle: "Nachricht erhalten", + preheader: "Nachricht erhalten", bodyHtml: ` -
+

Hey ${safeName},

- danke für deine Nachricht — ich habe sie erhalten und melde mich so schnell wie möglich bei dir zurück. -

- -
-
-
Deine Nachricht
-
-
- ${safeMsg} -
-
- - - `.trim(), + danke für deine Nachricht — ich habe sie erhalten und melde mich so schnell wie möglich bei dir zurück. 🙌 +

+${messageCard("Deine Nachricht", nl2br(originalMessage))} +${ctaButton("Portfolio ansehen →", B.siteUrl)}`, }); }, }, @@ -117,31 +127,16 @@ const emailTemplates = { subject: "Projekt-Anfrage erhalten! 🚀", template: (name: string, originalMessage: string) => { const safeName = escapeHtml(name); - const safeMsg = nl2br(escapeHtml(originalMessage)); return baseEmail({ title: `Projekt-Anfrage: danke, ${safeName}!`, - subtitle: "Ich melde mich zeitnah", + preheader: "Ich melde mich zeitnah", bodyHtml: ` -
+

Hey ${safeName},

- mega — danke für die Projekt-Anfrage. Ich schaue mir deine Nachricht an und komme mit Rückfragen/Ideen auf dich zu. -

- -
-
-
Deine Projekt-Nachricht
-
-
- ${safeMsg} -
-
- - - `.trim(), + mega — danke für die Projekt-Anfrage! Ich schaue mir alles an und melde mich bald mit Ideen und Rückfragen. 🚀 +

+${messageCard("Deine Projekt-Anfrage", nl2br(originalMessage), B.sky)} +${ctaButton("Mein Portfolio ansehen →", B.siteUrl)}`, }); }, }, @@ -149,25 +144,15 @@ const emailTemplates = { subject: "Danke für deine Nachricht! ⚡", template: (name: string, originalMessage: string) => { const safeName = escapeHtml(name); - const safeMsg = nl2br(escapeHtml(originalMessage)); return baseEmail({ title: `Danke, ${safeName}!`, - subtitle: "Kurze Bestätigung", + preheader: "Kurze Bestätigung", bodyHtml: ` -
+

Hey ${safeName},

- kurze Bestätigung: deine Nachricht ist angekommen. Ich melde mich bald zurück. -

- -
-
-
Deine Nachricht
-
-
- ${safeMsg} -
-
- `.trim(), + kurze Bestätigung: deine Nachricht ist angekommen. Ich melde mich bald zurück. ⚡ +

+${messageCard("Deine Nachricht", nl2br(originalMessage))}`, }); }, }, @@ -175,35 +160,19 @@ const emailTemplates = { subject: "Antwort auf deine Nachricht 📧", template: (name: string, originalMessage: string, responseMessage: string) => { const safeName = escapeHtml(name); - const safeOriginal = nl2br(escapeHtml(originalMessage)); - const safeResponse = nl2br(escapeHtml(responseMessage)); return baseEmail({ - title: `Antwort für ${safeName}`, - subtitle: "Neue Nachricht", + title: `Hey ${safeName}!`, + preheader: "Antwort von Dennis", bodyHtml: ` -
+

Hey ${safeName},

- hier ist meine Antwort: + ich habe mir deine Nachricht angeschaut — hier ist meine Antwort: +

+${messageCard("Antwort von Dennis", nl2br(responseMessage), B.mint)} +
+${messageCard("Deine ursprüngliche Nachricht", nl2br(originalMessage), "#2a2a2a")}
- -
-
-
Antwort
-
-
- ${safeResponse} -
-
- -
-
-
Deine ursprüngliche Nachricht
-
-
- ${safeOriginal} -
-
- `.trim(), +${ctaButton("Portfolio ansehen →", B.siteUrl)}`, }); }, }, @@ -231,36 +200,23 @@ export async function POST(request: NextRequest) { originalMessage: string; response?: string; }; - + const { to, name, template, originalMessage, response } = body; - // Validate input if (!to || !name || !template || !originalMessage) { - return NextResponse.json( - { error: "Alle Felder sind erforderlich" }, - { status: 400 }, - ); + return NextResponse.json({ error: "Alle Felder sind erforderlich" }, { status: 400 }); } if (template === "reply" && (!response || !response.trim())) { return NextResponse.json({ error: "Antworttext ist erforderlich" }, { status: 400 }); } - // Validate email format const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(to)) { - console.error('❌ Validation failed: Invalid email format'); - return NextResponse.json( - { error: "Ungültige E-Mail-Adresse" }, - { status: 400 }, - ); + return NextResponse.json({ error: "Ungültige E-Mail-Adresse" }, { status: 400 }); } - // Check if template exists if (!emailTemplates[template]) { - return NextResponse.json( - { error: "Ungültiges Template" }, - { status: 400 }, - ); + return NextResponse.json({ error: "Ungültiges Template" }, { status: 400 }); } const user = process.env.MY_EMAIL ?? ""; @@ -268,10 +224,7 @@ export async function POST(request: NextRequest) { if (!user || !pass) { console.error("❌ Missing email/password environment variables"); - return NextResponse.json( - { error: "E-Mail-Server nicht konfiguriert" }, - { status: 500 }, - ); + return NextResponse.json({ error: "E-Mail-Server nicht konfiguriert" }, { status: 500 }); } const transportOptions: SMTPTransport.Options = { @@ -279,86 +232,50 @@ export async function POST(request: NextRequest) { port: 587, secure: false, requireTLS: true, - auth: { - type: "login", - user, - pass, - }, + auth: { type: "login", user, pass }, connectionTimeout: 30000, greetingTimeout: 30000, socketTimeout: 60000, - tls: { - rejectUnauthorized: false, - ciphers: 'SSLv3' - } + tls: { rejectUnauthorized: false, ciphers: 'SSLv3' }, }; const transport = nodemailer.createTransport(transportOptions); - // Verify transport configuration try { await transport.verify(); - } catch (_verifyError) { - return NextResponse.json( - { error: "E-Mail-Server-Verbindung fehlgeschlagen" }, - { status: 500 }, - ); + } catch { + return NextResponse.json({ error: "E-Mail-Server-Verbindung fehlgeschlagen" }, { status: 500 }); } const selectedTemplate = emailTemplates[template]; - let html: string; - if (template === "reply") { - html = emailTemplates.reply.template(name, originalMessage, response || ""); - } else { - // Narrow the template type so TS knows this is not the 3-arg reply template - const nonReplyTemplate = template as Exclude; - html = emailTemplates[nonReplyTemplate].template(name, originalMessage); - } + const html = template === "reply" + ? emailTemplates.reply.template(name, originalMessage, response || "") + : emailTemplates[template as Exclude].template(name, originalMessage); + const mailOptions: Mail.Options = { from: `"Dennis Konkol" <${user}>`, - to: to, - replyTo: "contact@dk0.dev", + to, + replyTo: B.email, subject: selectedTemplate.subject, html, - text: ` -Hallo ${name}! - -Vielen Dank für deine Nachricht: -${originalMessage} - -${template === "reply" ? `\nAntwort:\n${response || ""}\n` : "\nIch werde mich so schnell wie möglich bei dir melden.\n"} - -Beste Grüße, -Dennis Konkol -Software Engineer & Student -https://dki.one -contact@dk0.dev - `, + text: template === "reply" + ? `Hey ${name}!\n\nAntwort:\n${response}\n\nDeine ursprüngliche Nachricht:\n${originalMessage}\n\n-- Dennis Konkol\n${B.siteUrl}` + : `Hey ${name}!\n\nDanke für deine Nachricht:\n${originalMessage}\n\nIch melde mich bald!\n\n-- Dennis Konkol\n${B.siteUrl}`, }; - const sendMailPromise = () => - new Promise((resolve, reject) => { - transport.sendMail(mailOptions, function (err, info) { - if (!err) { - resolve(info.response); - } else { - reject(err.message); - } - }); + const result = await new Promise((resolve, reject) => { + transport.sendMail(mailOptions, (err, info) => { + if (!err) resolve(info.response); + else reject(err.message); }); - - const result = await sendMailPromise(); - - return NextResponse.json({ - message: "Template-E-Mail erfolgreich gesendet", - template: template, - messageId: result }); - + + return NextResponse.json({ message: "E-Mail erfolgreich gesendet", template, messageId: result }); + } catch (err) { - return NextResponse.json({ - error: "Fehler beim Senden der Template-E-Mail", - details: err instanceof Error ? err.message : 'Unbekannter Fehler' + return NextResponse.json({ + error: "Fehler beim Senden der E-Mail", + details: err instanceof Error ? err.message : 'Unbekannter Fehler', }, { status: 500 }); } } diff --git a/app/api/email/route.tsx b/app/api/email/route.tsx index 006f0b7..2b11d40 100644 --- a/app/api/email/route.tsx +++ b/app/api/email/route.tsx @@ -5,12 +5,8 @@ import Mail from "nodemailer/lib/mailer"; import { checkRateLimit, getRateLimitHeaders } from '@/lib/auth'; import { prisma } from "@/lib/prisma"; -// Sanitize input to prevent XSS function sanitizeInput(input: string, maxLength: number = 10000): string { - return input - .slice(0, maxLength) - .replace(/[<>]/g, '') // Remove potential HTML tags - .trim(); + return input.slice(0, maxLength).replace(/[<>]/g, '').trim(); } function escapeHtml(input: string): string { @@ -22,19 +18,181 @@ function escapeHtml(input: string): string { .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 { - // Rate limiting (defensive: headers may be undefined in tests) const ip = request.headers?.get?.('x-forwarded-for') ?? request.headers?.get?.('x-real-ip') ?? 'unknown'; - if (!checkRateLimit(ip, 5, 60000)) { // 5 emails per minute per IP + 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) - } + headers: { 'Content-Type': 'application/json', ...getRateLimitHeaders(ip, 5, 60000) }, } ); } @@ -45,49 +203,27 @@ export async function POST(request: NextRequest) { subject: string; message: string; }; - - // Sanitize and validate input + const email = sanitizeInput(body.email || '', 255); const name = sanitizeInput(body.name || '', 100); const subject = sanitizeInput(body.subject || '', 200); const message = sanitizeInput(body.message || '', 5000); - // Email request received - - // Validate input if (!email || !name || !subject || !message) { - console.error('❌ Validation failed: Missing required fields'); - return NextResponse.json( - { error: "Alle Felder sind erforderlich" }, - { status: 400 }, - ); + return NextResponse.json({ error: "Alle Felder sind erforderlich" }, { status: 400 }); } - // Validate email format const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { - console.error('❌ Validation failed: Invalid email format'); - return NextResponse.json( - { error: "Ungültige E-Mail-Adresse" }, - { status: 400 }, - ); + return NextResponse.json({ error: "Ungültige E-Mail-Adresse" }, { status: 400 }); } - // Validate message length if (message.length < 10) { - console.error('❌ Validation failed: Message too short'); - return NextResponse.json( - { error: "Nachricht muss mindestens 10 Zeichen lang sein" }, - { status: 400 }, - ); + return NextResponse.json({ error: "Nachricht muss mindestens 10 Zeichen lang sein" }, { status: 400 }); } - // Validate field lengths if (name.length > 100 || subject.length > 200 || message.length > 5000) { - return NextResponse.json( - { error: "Eingabe zu lang" }, - { status: 400 }, - ); + return NextResponse.json({ error: "Eingabe zu lang" }, { status: 400 }); } const user = process.env.MY_EMAIL ?? ""; @@ -95,265 +231,101 @@ export async function POST(request: NextRequest) { if (!user || !pass) { console.error("❌ Missing email/password environment variables"); - return NextResponse.json( - { error: "E-Mail-Server nicht konfiguriert" }, - { status: 500 }, - ); + return NextResponse.json({ error: "E-Mail-Server nicht konfiguriert" }, { status: 500 }); } const transportOptions: SMTPTransport.Options = { host: "mail.dk0.dev", port: 587, - secure: false, // Port 587 uses STARTTLS, not SSL/TLS + secure: false, requireTLS: true, - auth: { - type: "login", - user, - pass, - }, - // Increased timeout settings for better reliability - connectionTimeout: 30000, // 30 seconds - greetingTimeout: 30000, // 30 seconds - socketTimeout: 60000, // 60 seconds - // TLS hardening (allow insecure/self-signed only when explicitly enabled) + 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" }, + process.env.SMTP_ALLOW_INSECURE_TLS === "true" || process.env.SMTP_ALLOW_SELF_SIGNED === "true" + ? { rejectUnauthorized: false } + : { rejectUnauthorized: true, minVersion: "TLSv1.2" }, }; - // Creating transport with configured options - const transport = nodemailer.createTransport(transportOptions); - // Verify transport configuration with retry logic let verificationAttempts = 0; - const maxVerificationAttempts = 3; - let verificationSuccess = false; - - while (verificationAttempts < maxVerificationAttempts && !verificationSuccess) { + while (verificationAttempts < 3) { try { verificationAttempts++; await transport.verify(); - verificationSuccess = true; + break; } catch (verifyError) { if (process.env.NODE_ENV === 'development') { console.error(`SMTP verification attempt ${verificationAttempts} failed:`, verifyError); } - - if (verificationAttempts >= maxVerificationAttempts) { - if (process.env.NODE_ENV === 'development') { - console.error('All SMTP verification attempts failed'); - } - return NextResponse.json( - { error: "E-Mail-Server-Verbindung fehlgeschlagen" }, - { status: 500 }, - ); + if (verificationAttempts >= 3) { + return NextResponse.json({ error: "E-Mail-Server-Verbindung fehlgeschlagen" }, { status: 500 }); } - - // Wait before retry await new Promise(resolve => setTimeout(resolve, 2000)); } } - const brandUrl = "https://dk0.dev"; const sentAt = new Date().toLocaleString('de-DE', { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' + year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', }); - const safeName = escapeHtml(name); - const safeEmail = escapeHtml(email); - const safeSubject = escapeHtml(subject); - const safeMessageHtml = escapeHtml(message).replace(/\n/g, "
"); 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", // Send to your contact email + to: "contact@dk0.dev", replyTo: email, - subject: `Portfolio Kontakt: ${subject}`, - html: ` - - - - - - Neue Kontaktanfrage - Portfolio - - -
-
- -
-
-
- Dennis Konkol -
-
- dk0.dev -
-
-
-
- Neue Kontaktanfrage -
-
- Eingegangen am ${sentAt} -
-
-
-
- - -
- -
-
- ${escapeHtml(initial)} -
-
-
- ${safeName} -
-
- E-Mail: ${safeEmail}
- Betreff: ${safeSubject} -
-
-
- - -
-
-
- Nachricht -
-
-
- ${safeMessageHtml} -
-
- - -
- - Antworten - -
- Oder antworte direkt auf diese E-Mail. -
-
-
- - -
-
- Automatisch generiert von dk0.dev -
-
-
-
- - - `, - text: ` -Neue Kontaktanfrage von deinem Portfolio - -Von: ${name} (${email}) -Betreff: ${subject} - -Nachricht: -${message} - ---- -Diese E-Mail wurde automatisch von dk0.dev generiert. - `, + 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}`, }; - // Sending email - - // Email sending with retry logic let sendAttempts = 0; - const maxSendAttempts = 3; - let sendSuccess = false; let result = ''; - while (sendAttempts < maxSendAttempts && !sendSuccess) { + while (sendAttempts < 3) { try { sendAttempts++; - // Email send attempt - - const sendMailPromise = () => - new Promise((resolve, reject) => { - transport.sendMail(mailOptions, function (err, info) { - if (!err) { - // Email sent successfully - resolve(info.response); - } else { - if (process.env.NODE_ENV === 'development') { - console.error("Error sending email:", err); - } - reject(err.message); - } - }); + 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); + } }); - - result = await sendMailPromise(); - sendSuccess = true; - // Email process completed successfully + }); + break; } catch (sendError) { - if (process.env.NODE_ENV === 'development') { - console.error(`Email send attempt ${sendAttempts} failed:`, sendError); + if (sendAttempts >= 3) { + throw new Error(`Failed to send email after 3 attempts: ${sendError}`); } - - if (sendAttempts >= maxSendAttempts) { - if (process.env.NODE_ENV === 'development') { - console.error('All email send attempts failed'); - } - throw new Error(`Failed to send email after ${maxSendAttempts} attempts: ${sendError}`); - } - - // Wait before retry await new Promise(resolve => setTimeout(resolve, 3000)); } } - - // Save contact to database + + // 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 - } - }); - // Contact saved to database + 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 database:', dbError); - } - // Don't fail the email send if DB save fails + if (process.env.NODE_ENV === 'development') console.error('Error saving contact to DB:', dbError); } - - return NextResponse.json({ - message: "E-Mail erfolgreich gesendet", - messageId: result - }); - + + return NextResponse.json({ message: "E-Mail erfolgreich gesendet", messageId: result }); + } catch (err) { console.error("❌ Unexpected error in email API:", err); - return NextResponse.json({ + return NextResponse.json({ error: "Fehler beim Senden der E-Mail", - details: err instanceof Error ? err.message : 'Unbekannter Fehler' + details: err instanceof Error ? err.message : 'Unbekannter Fehler', }, { status: 500 }); } }