nodemailer to resend

This commit is contained in:
Rajpreet0 2026-07-28 01:12:20 +02:00
parent 378e16b5c7
commit 3fa4da6f9a
15 changed files with 141 additions and 126 deletions

71
package-lock.json generated
View file

@ -35,7 +35,6 @@
"next": "16.2.6",
"next-intl": "^4.13.2",
"next-themes": "^0.4.6",
"nodemailer": "^8.0.5",
"prisma": "^6.19.0",
"radix-ui": "^1.4.3",
"react": "19.2.0",
@ -43,6 +42,7 @@
"react-dom": "19.2.0",
"react-icons": "^5.5.0",
"react-phone-number-input": "^3.4.14",
"resend": "^6.18.0",
"sonner": "^2.0.7",
"stripe": "^22.1.1",
"tailwind-merge": "^3.3.1",
@ -57,7 +57,6 @@
"@testing-library/user-event": "^14.6.1",
"@types/dompurify": "^3.0.5",
"@types/node": "^20",
"@types/nodemailer": "^7.0.4",
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^5.1.0",
@ -4003,6 +4002,12 @@
"integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==",
"license": "MIT"
},
"node_modules/@stablelib/base64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
"license": "MIT"
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
@ -5219,16 +5224,6 @@
"undici-types": "~6.21.0"
}
},
"node_modules/@types/nodemailer": {
"version": "7.0.12",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.12.tgz",
"integrity": "sha512-80vKwiIsVSyFA1rRovH59jNPLBOuc6dRZIHEu40gXTkBkZnQv8vog1xSGEb9j5q/tdMAs5ivvDR2pLTU0hGHXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/react": {
"version": "19.2.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
@ -8014,6 +8009,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-sha256": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
"license": "Unlicense"
},
"node_modules/fast-string-truncated-width": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz",
@ -10265,15 +10266,6 @@
"node": ">=18"
}
},
"node_modules/nodemailer": {
"version": "8.0.11",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.11.tgz",
"integrity": "sha512-nrO/pDAUKl+wXX+lx16tDLbnm0fW6sK/x8mgohaCpg+CdCEl482bD4tCuAZk2DyliruiNTIZxRCoWkDqJEnAiA==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/nypm": {
"version": "0.6.7",
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.7.tgz",
@ -10653,6 +10645,12 @@
"node": ">= 0.4"
}
},
"node_modules/postal-mime": {
"version": "2.7.5",
"resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.5.tgz",
"integrity": "sha512-GNEXKvWFQnbgO5NlrGzVa0FmWzBZ24PersAWErttSg1Hjpf0ATxTwS5DOMGaOpTG6bUh5cTr7xi0jAD942wCJA==",
"license": "MIT-0"
},
"node_modules/postcss": {
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
@ -11296,6 +11294,27 @@
"node": ">=0.10.0"
}
},
"node_modules/resend": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/resend/-/resend-6.18.0.tgz",
"integrity": "sha512-EjxZ9AVzywJgOlUoIJe9ytBWVrfbUtJbjeoLnRSvpU1sv97Hh9DSwhw+k8kiujrG4Rg4bzTBsjlmwWWuoOxSug==",
"license": "MIT",
"dependencies": {
"postal-mime": "2.7.5",
"standardwebhooks": "1.0.0"
},
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@react-email/render": "*"
},
"peerDependenciesMeta": {
"@react-email/render": {
"optional": true
}
}
},
"node_modules/resolve": {
"version": "2.0.0-next.7",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz",
@ -11833,6 +11852,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/standardwebhooks": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
"integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
"license": "MIT",
"dependencies": {
"@stablelib/base64": "^1.0.0",
"fast-sha256": "^1.3.0"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",

View file

@ -43,7 +43,6 @@
"next": "16.2.6",
"next-intl": "^4.13.2",
"next-themes": "^0.4.6",
"nodemailer": "^8.0.5",
"prisma": "^6.19.0",
"radix-ui": "^1.4.3",
"react": "19.2.0",
@ -51,6 +50,7 @@
"react-dom": "19.2.0",
"react-icons": "^5.5.0",
"react-phone-number-input": "^3.4.14",
"resend": "^6.18.0",
"sonner": "^2.0.7",
"stripe": "^22.1.1",
"tailwind-merge": "^3.3.1",
@ -65,7 +65,6 @@
"@testing-library/user-event": "^14.6.1",
"@types/dompurify": "^3.0.5",
"@types/node": "^20",
"@types/nodemailer": "^7.0.4",
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^5.1.0",

View file

@ -3,7 +3,7 @@ import Stripe from "stripe";
import { stripe, getWebhookSecret } from "@/src/lib/stripe";
import { prisma } from "@/src/lib/prisma";
import { revalidatePath, revalidateTag } from "next/cache";
import { sendMail } from "@/src/lib/mail/nodemailer";
import { sendMail } from "@/src/lib/mail/resend";
import { generateBookingConfirmationEmail } from "@/src/lib/mail/templates/booking-confirmation";
import { generateBookingRefundEmail } from "@/src/lib/mail/templates/booking-refund";

View file

@ -2458,7 +2458,7 @@ export default function LandingPage() {
<li>
<span className="hover:text-white cursor-pointer transition-colors">Support</span>
</li>
<li>info@since3.de</li>
<li>info@kumasy.de</li>
</ul>
</div>
</div>

View file

@ -1,5 +1,5 @@
import "dotenv/config";
import { sendMail, verifyMailConnection } from "@/src/lib/mail/nodemailer";
import { sendMail, verifyMailConnection } from "@/src/lib/mail/resend";
async function testEmail() {
// Verbindung testen
@ -18,7 +18,7 @@ async function testEmail() {
console.log("Sende Test-Mail...");
const result = await sendMail({
to: process.env.SMTP_TEST_MAIL || "",
subject: "Test-Email von S3 Kuma",
subject: "Test-Email von Kumasy",
html: "<h1>Test erfolgreich!</h1><p>Der E-Mail-Versand funktioniert.</p>",
});

View file

@ -1,87 +0,0 @@
/**
* Nodemailer Configuration
*
* Diese Datei konfiguriert Nodemailer für den E-Mail-Versand.
* Unterstützt verschiedene SMTP-Anbieter wie Gmail, Outlook, SendGrid, etc.
*/
import nodemailer from "nodemailer";
/**
* Erstellt einen Nodemailer Transporter
*
* Konfiguration über Umgebungsvariablen:
* - SMTP_HOST: SMTP Server Host (z.B. smtp.gmail.com)
* - SMTP_PORT: SMTP Server Port (z.B. 587)
* - SMTP_USER: SMTP Benutzername/E-Mail
* - SMTP_PASSWORD: SMTP Passwort/App-Passwort
* - SMTP_FROM: Absender E-Mail-Adresse
*/
export const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT || 587),
secure: Number(process.env.SMTP_PORT) === 465, // true für Port 465, false für andere Ports
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD,
},
});
/**
* Sendet eine E-Mail
*
* @param to - Empfänger E-Mail-Adresse
* @param subject - E-Mail Betreff
* @param html - HTML-Inhalt der E-Mail
* @param text - Plain-Text Alternative (optional)
* @returns Promise mit dem Ergebnis des Versands
*/
export async function sendMail({
to,
subject,
html,
text,
}: {
to: string;
subject: string;
html: string;
text?: string;
}) {
try {
const info = await transporter.sendMail({
from: process.env.SMTP_FROM || process.env.SMTP_USER,
to,
subject,
html,
text: text || stripHtml(html), // Fallback: HTML zu Text konvertieren
});
console.log("E-Mail erfolgreich gesendet:", info.messageId);
return { success: true, messageId: info.messageId };
} catch (error) {
console.error("Fehler beim E-Mail-Versand:", error);
return { success: false, error };
}
}
/**
* Einfache Funktion zum Entfernen von HTML-Tags (für Text-Alternative)
*/
function stripHtml(html: string): string {
return html.replace(/<[^>]*>/g, "");
}
/**
* Verifiziert die SMTP-Verbindung
* Nützlich zum Testen der Konfiguration
*/
export async function verifyMailConnection() {
try {
await transporter.verify();
console.log("SMTP-Verbindung erfolgreich verifiziert");
return true;
} catch (error) {
console.error("SMTP-Verbindung fehlgeschlagen:", error);
return false;
}
}

74
src/lib/mail/resend.ts Normal file
View file

@ -0,0 +1,74 @@
/**
* Resend Konfiguration
*
* Diese Datei konfiguriert Resend für den E-Mail-Versand.
*/
import { Resend } from "resend";
export const resend = new Resend(process.env.RESEND_API_KEY);
/**
* Sendet eine E-Mail
*
* @param to - Empfänger E-Mail-Adresse
* @param subject - E-Mail Betreff
* @param html - HTML-Inhalt der E-Mail
* @param text - Plain-Text Alternative (optional)
* @returns Promise mit dem Ergebnis des Versands
*/
export async function sendMail({
to,
subject,
html,
text,
}: {
to: string;
subject: string;
html: string;
text?: string;
}) {
try {
const { data, error } = await resend.emails.send({
from: process.env.RESEND_FROM || "",
to,
subject,
html,
text: text || stripHtml(html), // Fallback: HTML zu Text konvertieren
});
if (error) {
console.error("Fehler beim E-Mail-Versand:", error);
return { success: false, error };
}
console.log("E-Mail erfolgreich gesendet:", data?.id);
return { success: true, messageId: data?.id };
} catch (error) {
console.error("Fehler beim E-Mail-Versand:", error);
return { success: false, error };
}
}
/**
* Einfache Funktion zum Entfernen von HTML-Tags (für Text-Alternative)
*/
function stripHtml(html: string): string {
return html.replace(/<[^>]*>/g, "");
}
/**
* Prüft, ob die Resend-Konfiguration vollständig ist.
* Resend ist HTTP-basiert, es gibt keine dauerhafte Verbindung wie bei SMTP zu verifizieren.
*/
export async function verifyMailConnection() {
const isConfigured = Boolean(process.env.RESEND_API_KEY && process.env.RESEND_FROM);
if (!isConfigured) {
console.error("Resend-Konfiguration unvollständig: RESEND_API_KEY oder RESEND_FROM fehlt");
return false;
}
console.log("Resend-Konfiguration vorhanden");
return true;
}

View file

@ -47,7 +47,7 @@ const AuthSidebarComponent: React.FC<AuthSidebarComponentProps> = ({
<CardFooter className="pointer-events-auto p-0 pt-4 mt-auto">
<p className="text-yellow text-sm lg:text-base break-words">
Bei Fragen:{" "}
<a href={`mailto:info@since3.de`} className="hover:underline">
<a href={`mailto:${footer}`} className="hover:underline">
{footer}
</a>
</p>

View file

@ -212,7 +212,7 @@ const EmployeeOnboardingView = () => {
<div className="hidden lg:block fixed left-0 top-0 h-screen w-[40%] p-2 z-10">
<AuthSidebarComponent
title="Werde Teil unseres Teams"
footer="info@since3.de"
footer="info@kumasy.de"
isSteps
step={step}
totalSteps={2}

View file

@ -58,7 +58,7 @@ const ForgotPasswordView = () => {
<AuthSidebarComponent
title="Passwort zurücksetzen"
description="Keine Sorge, wir helfen Ihnen dabei, wieder Zugang zu Ihrem Konto zu erhalten."
footer="info@since3.de"
footer="info@kumasy.de"
/>
</div>
</div>

View file

@ -44,7 +44,7 @@ const LoginView = () => {
<AuthSidebarComponent
title={t("sidebarTitle")}
description={t("sidebarDescription")}
footer="info@since3.de"
footer="info@kumasy.de"
/>
</div>
</div>

View file

@ -140,7 +140,7 @@ const RegisterAdmin = () => {
<div className="hidden lg:block fixed left-0 top-0 h-screen w-[40%] p-2 z-10">
<AuthSidebarComponent
title="Werde zur besten Version deiner selbst"
footer="info@since3.de"
footer="info@kumasy.de"
isSteps
step={step}
totalSteps={3}

View file

@ -102,7 +102,7 @@ const RegisterUserView = () => {
<div className="hidden lg:block fixed left-0 top-0 h-screen w-[40%] p-2 z-10">
<AuthSidebarComponent
title="Werde zur besten Version deiner selbst"
footer="info@since3.de"
footer="info@kumasy.de"
isSteps
step={step}
totalSteps={2}

View file

@ -93,7 +93,7 @@ const ResetPasswordView = () => {
<AuthSidebarComponent
title="Neues Passwort setzen"
description="Wählen Sie ein sicheres Passwort für Ihr Konto."
footer="info@since3.de"
footer="info@kumasy.de"
/>
</div>
</div>

View file

@ -19,7 +19,7 @@ function invalidateUserDataCache(email: string) {
(revalidateTag as any)(`user-data-${email}`);
}
import { randomBytes } from "crypto";
import { sendMail } from "@/src/lib/mail/nodemailer";
import { sendMail } from "@/src/lib/mail/resend";
import { generateOnboardingEmail } from "@/src/lib/mail/templates/employee-onboarding";
import { generateOnboardingCompleteEmail } from "@/src/lib/mail/templates/onboarding-complete";
@ -161,7 +161,7 @@ export async function createEmployee(data: EmployeeFormData, status: "draft" | "
const { subject, html, text } = generateOnboardingEmail({
email: validatedData.email,
onboardingUrl,
companyName: process.env.COMPANY_NAME || "S3 Kuma",
companyName: process.env.COMPANY_NAME || "Kumasy",
});
// Send email asynchronously (fire and forget)
@ -522,7 +522,7 @@ export async function updateEmployee(
const { subject, html, text } = generateOnboardingEmail({
email: validatedData.email,
onboardingUrl,
companyName: process.env.COMPANY_NAME || "S3 Kuma",
companyName: process.env.COMPANY_NAME || "Kumasy",
});
// Send email asynchronously (fire and forget)
@ -876,7 +876,7 @@ export async function completeEmployeeOnboarding(
employeeFirstName: data.firstName,
employeeLastName: data.lastName,
employeeEmail: employee.email,
companyName: process.env.COMPANY_NAME || "S3 Kuma",
companyName: process.env.COMPANY_NAME || "Kumasy",
});
sendMail({ to: manager.email, subject, html, text }).catch((error) => {
@ -946,7 +946,7 @@ export async function resendOnboardingEmail(employeeId: string) {
const { subject, html, text } = generateOnboardingEmail({
email: employee.email,
onboardingUrl,
companyName: process.env.COMPANY_NAME || "S3 Kuma",
companyName: process.env.COMPANY_NAME || "Kumasy",
});
const mailResult = await sendMail({ to: employee.email, subject, html, text });