Add email notification system for contact form submissions

Implement a new mailer module to send email notifications upon contact form submission, integrating with SMTP settings and formatting the email content.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 23852c00-4779-460a-9e0c-d09fee4b6c92
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 3a6f3b4b-2588-4a9e-9f58-fbe9c763e594
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/23852c00-4779-460a-9e0c-d09fee4b6c92/koutl3W
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sebastjanartic 2026-03-06 14:11:34 +00:00
parent 1d4ab71636
commit 67ce78ae3b
3 changed files with 83 additions and 14 deletions

View File

@ -1,11 +1 @@
[
{
"id": 1772805974560,
"name": "sebastjan",
"email": "sebastjan.artic@folx.tv",
"subject": "Allgemeine Anfrage",
"message": "hallo ",
"createdAt": "2026-03-06T14:06:14.560Z",
"read": false
}
]
[]

75
server/mailer.ts Normal file
View File

@ -0,0 +1,75 @@
import nodemailer from "nodemailer";
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST || "mail.folx.tv",
port: parseInt(process.env.SMTP_PORT || "465"),
secure: true,
auth: {
user: process.env.SMTP_USER || "web@folx.tv",
pass: process.env.SMTP_PASSWORD || "",
},
});
interface ContactMessage {
name: string;
email: string;
subject: string;
message: string;
}
export async function sendContactEmail(data: ContactMessage): Promise<boolean> {
const subjectLine = data.subject
? `[FOLX TV Kontakt] ${data.subject} von ${data.name}`
: `[FOLX TV Kontakt] Nachricht von ${data.name}`;
const htmlBody = `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background: #1a1a2e; padding: 20px; border-radius: 8px 8px 0 0;">
<h2 style="color: #fff; margin: 0; font-size: 18px;">Neue Kontaktanfrage</h2>
</div>
<div style="background: #f9f9f9; padding: 24px; border: 1px solid #e0e0e0; border-top: none; border-radius: 0 0 8px 8px;">
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 12px; font-weight: bold; color: #555; width: 100px; vertical-align: top;">Name:</td>
<td style="padding: 8px 12px; color: #222;">${escapeHtml(data.name)}</td>
</tr>
<tr>
<td style="padding: 8px 12px; font-weight: bold; color: #555; vertical-align: top;">E-Mail:</td>
<td style="padding: 8px 12px; color: #222;"><a href="mailto:${escapeHtml(data.email)}" style="color: #1a73e8;">${escapeHtml(data.email)}</a></td>
</tr>
<tr>
<td style="padding: 8px 12px; font-weight: bold; color: #555; vertical-align: top;">Betreff:</td>
<td style="padding: 8px 12px; color: #222;">${escapeHtml(data.subject || "Keine Angabe")}</td>
</tr>
</table>
<hr style="border: none; border-top: 1px solid #e0e0e0; margin: 16px 0;" />
<div style="padding: 8px 12px; color: #222; white-space: pre-wrap; line-height: 1.6;">${escapeHtml(data.message)}</div>
</div>
<p style="text-align: center; color: #999; font-size: 11px; margin-top: 16px;">Gesendet über das Kontaktformular auf folx.tv</p>
</div>
`;
try {
await transporter.sendMail({
from: `"FOLX TV Website" <${process.env.SMTP_FROM || "web@folx.tv"}>`,
to: process.env.CONTACT_TO || "office@folx.tv",
replyTo: data.email,
subject: subjectLine,
html: htmlBody,
text: `Neue Kontaktanfrage\n\nName: ${data.name}\nE-Mail: ${data.email}\nBetreff: ${data.subject || "Keine Angabe"}\n\nNachricht:\n${data.message}`,
});
console.log(`[mailer] Contact email sent successfully (from: ${data.email})`);
return true;
} catch (err: any) {
console.error(`[mailer] Failed to send email:`, err.message);
return false;
}
}
function escapeHtml(str: string): string {
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}

View File

@ -8,6 +8,7 @@ import { analyzeAllArticleImages, getCachedFocalPoints } from "./focal-point";
import { optimizeImage } from "./image-optimizer";
import { getAuthUrl, exchangeCodeForTokens, isConnected, fetchGalleryFromDropbox, getValidAccessToken, migrateGalleryToCloudinary } from "./dropbox";
import { listCloudinaryGalleryDetailed, deleteFromCloudinary } from "./cloudinary";
import { sendContactEmail } from "./mailer";
import multer from "multer";
import path from "path";
import fs from "fs";
@ -473,7 +474,7 @@ export async function registerRoutes(
if (fs.existsSync(contactPath)) {
messages = JSON.parse(fs.readFileSync(contactPath, "utf-8"));
}
messages.push({
const entry = {
id: Date.now(),
name,
email,
@ -481,9 +482,12 @@ export async function registerRoutes(
message,
createdAt: new Date().toISOString(),
read: false,
});
};
messages.push(entry);
fs.writeFileSync(contactPath, JSON.stringify(messages, null, 2));
console.log(`[contact] New message from ${name} <${email}>: ${subject || "No subject"}`);
const emailSent = await sendContactEmail({ name, email, subject: subject || "", message });
console.log(`[contact] New message from ${name} <${email}>: ${subject || "No subject"} (email ${emailSent ? "sent" : "failed"})`);
res.json({ ok: true });
} catch (err: any) {
res.status(500).json({ message: err.message });