KI-AGENT: Kontaktformular und Datenschutzseite ergänzt

This commit is contained in:
2026-05-22 15:03:56 +02:00
parent 0bd0120ec2
commit 76764eb4c3
8 changed files with 590 additions and 9 deletions

9
website/.env.example Normal file
View File

@@ -0,0 +1,9 @@
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=website@example.com
SMTP_PASSWORD=change-me
SMTP_FROM=website@example.com
SMTP_HELO=fedeo.de
SMTP_REJECT_UNAUTHORIZED=true
CONTACT_TO=f.federspiel@federspiel.tech

View File

@@ -676,21 +676,25 @@ footer {
padding: clamp(3rem, 7vw, 6rem) clamp(1rem, 4vw, 2rem) 5rem; padding: clamp(3rem, 7vw, 6rem) clamp(1rem, 4vw, 2rem) 5rem;
} }
.contact-page,
.audience-page { .audience-page {
margin: 0 auto; margin: 0 auto;
max-width: 1180px; max-width: 1180px;
padding: clamp(3rem, 7vw, 6rem) clamp(1rem, 4vw, 2rem) 5rem; padding: clamp(3rem, 7vw, 6rem) clamp(1rem, 4vw, 2rem) 5rem;
} }
.contact-heading,
.audience-heading { .audience-heading {
max-width: 58rem; max-width: 58rem;
margin-bottom: 2.5rem; margin-bottom: 2.5rem;
} }
.contact-heading h1,
.audience-heading h1 { .audience-heading h1 {
max-width: 12ch; max-width: 12ch;
} }
.contact-heading p,
.audience-heading p { .audience-heading p {
color: #51605c; color: #51605c;
font-size: 1.08rem; font-size: 1.08rem;
@@ -698,6 +702,103 @@ footer {
max-width: 46rem; max-width: 46rem;
} }
.contact-form {
background: rgba(255, 255, 255, 0.76);
border: 1px solid rgba(23, 33, 31, 0.1);
border-radius: 0.5rem;
display: grid;
gap: 1rem;
max-width: 820px;
padding: clamp(1.25rem, 4vw, 2rem);
}
.form-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.contact-form label {
color: #17211f;
display: grid;
font-size: 0.95rem;
font-weight: 800;
gap: 0.45rem;
}
.contact-form input,
.contact-form textarea {
background: #ffffff;
border: 1px solid rgba(23, 33, 31, 0.14);
border-radius: 0.45rem;
color: #17211f;
font: inherit;
font-weight: 500;
min-height: 3rem;
padding: 0.8rem 0.9rem;
width: 100%;
}
.contact-form textarea {
line-height: 1.55;
min-height: 12rem;
resize: vertical;
}
.contact-form input:focus,
.contact-form textarea:focus {
border-color: var(--accent);
box-shadow: 0 0 0 0.2rem rgba(105, 195, 80, 0.18);
outline: none;
}
.honeypot {
display: none;
}
.check-row {
align-items: start;
display: flex;
font-weight: 500;
gap: 0.7rem;
line-height: 1.55;
}
.check-row input {
flex: 0 0 auto;
margin-top: 0.25rem;
min-height: auto;
width: auto;
}
.check-row a,
.form-status.error {
color: var(--accent-dark);
font-weight: 800;
}
.form-actions {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.form-actions button {
border: 0;
cursor: pointer;
}
.form-actions button:disabled {
cursor: wait;
opacity: 0.72;
}
.form-status {
color: #51605c;
margin: 0;
}
.audience-grid { .audience-grid {
display: grid; display: grid;
gap: 1rem; gap: 1rem;
@@ -771,6 +872,10 @@ footer {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
.privacy-grid article {
align-content: start;
}
.legal-grid article { .legal-grid article {
background: rgba(255, 255, 255, 0.76); background: rgba(255, 255, 255, 0.76);
border: 1px solid rgba(23, 33, 31, 0.1); border: 1px solid rgba(23, 33, 31, 0.1);
@@ -842,6 +947,7 @@ footer p {
.highlights, .highlights,
.feature-grid, .feature-grid,
.form-grid,
.audience-grid, .audience-grid,
.audience-fit, .audience-fit,
.matrix-layout, .matrix-layout,
@@ -886,6 +992,7 @@ footer p {
font-size: 3rem; font-size: 3rem;
} }
.contact-heading h1,
.audience-heading h1 { .audience-heading h1 {
font-size: 2.55rem; font-size: 2.55rem;
max-width: none; max-width: none;

View File

@@ -0,0 +1,105 @@
<template>
<main>
<header class="site-header">
<NuxtLink class="brand" to="/" aria-label="FEDEO Startseite">
<img src="/Logo.png" alt="FEDEO" />
</NuxtLink>
<nav aria-label="Hauptnavigation">
<NuxtLink to="/zielgruppen">Zielgruppen</NuxtLink>
<NuxtLink to="/#funktionen">Funktionen</NuxtLink>
<NuxtLink to="/#open-source">Open Source</NuxtLink>
<NuxtLink to="/#selfhost">Selfhost</NuxtLink>
<NuxtLink to="/#matrix">Matrix</NuxtLink>
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
<a class="login-link" href="https://app.fedeo.de">Einloggen</a>
</nav>
</header>
<section class="legal-page">
<div class="legal-heading">
<p class="eyebrow">Federspiel Technology</p>
<h1>Datenschutzerklärung</h1>
<p>Stand: 22. Mai 2026</p>
</div>
<div class="legal-grid privacy-grid">
<article>
<h2>Verantwortlicher</h2>
<p>
Federspiel Technology UG (haftungsbeschränkt)<br>
Am Schwarzen Brack 14<br>
26452 Sande<br>
Deutschland
</p>
<p>
E-Mail: <a href="mailto:f.federspiel@federspiel.tech">f.federspiel@federspiel.tech</a>
</p>
</article>
<article>
<h2>Aufruf der Webseite</h2>
<p>
Beim Besuch dieser Webseite werden technisch notwendige Zugriffsdaten verarbeitet, damit die Seite ausgeliefert, gesichert und betrieben werden kann. Dazu können IP-Adresse, Datum, Uhrzeit, angeforderte Seite, Browserinformationen und Serverstatus gehören.
</p>
<p>Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO.</p>
</article>
<article>
<h2>Kontaktformular</h2>
<p>
Wenn du das Kontaktformular nutzt, verarbeiten wir deinen Namen, deine E-Mail-Adresse, optional dein Unternehmen und deine Nachricht, um deine Anfrage zu beantworten.
</p>
<p>Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO, bei allgemeinen Anfragen zusätzlich Art. 6 Abs. 1 lit. f DSGVO.</p>
</article>
<article>
<h2>E-Mail-Versand</h2>
<p>
Die Angaben aus dem Kontaktformular werden serverseitig per SMTP an die hinterlegte Kontaktadresse übermittelt. Die SMTP-Zugangsdaten werden ausschließlich als Umgebungsvariablen verarbeitet und nicht im Quellcode gespeichert.
</p>
</article>
<article>
<h2>Speicherdauer</h2>
<p>
Kontaktanfragen werden nur so lange aufbewahrt, wie es für die Bearbeitung und gesetzliche Aufbewahrungspflichten erforderlich ist. Server-Logdaten werden nach Maßgabe der technischen und sicherheitsbezogenen Erfordernisse gelöscht.
</p>
</article>
<article>
<h2>Empfänger</h2>
<p>
Eine Weitergabe personenbezogener Daten erfolgt nur, wenn sie für Betrieb, Sicherheit, Kommunikation oder gesetzliche Pflichten erforderlich ist.
</p>
</article>
<article>
<h2>Deine Rechte</h2>
<p>
Du hast nach Maßgabe der DSGVO Rechte auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung, Datenübertragbarkeit und Widerspruch. Außerdem kannst du dich bei einer Datenschutzaufsichtsbehörde beschweren.
</p>
</article>
<article>
<h2>Pflichtangaben</h2>
<p>
Die Nutzung des Kontaktformulars ist freiwillig. Ohne die erforderlichen Angaben können wir deine Anfrage jedoch nicht beantworten.
</p>
</article>
</div>
</section>
<footer>
<img src="/Logo.png" alt="FEDEO" />
<div>
<NuxtLink to="/zielgruppen">Zielgruppen</NuxtLink>
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
<NuxtLink to="/impressum">Impressum</NuxtLink>
<NuxtLink to="/datenschutz">Datenschutz</NuxtLink>
<a href="https://app.fedeo.de">Einloggen</a>
</div>
<p>Copyright © 2026 Federspiel Technology UG haftungsbeschränkt.</p>
</footer>
</main>
</template>

View File

@@ -11,7 +11,7 @@
<NuxtLink to="/#open-source">Open Source</NuxtLink> <NuxtLink to="/#open-source">Open Source</NuxtLink>
<NuxtLink to="/#selfhost">Selfhost</NuxtLink> <NuxtLink to="/#selfhost">Selfhost</NuxtLink>
<NuxtLink to="/#matrix">Matrix</NuxtLink> <NuxtLink to="/#matrix">Matrix</NuxtLink>
<NuxtLink to="/#kontakt">Kontakt</NuxtLink> <NuxtLink to="/kontakt">Kontakt</NuxtLink>
<a class="login-link" href="https://app.fedeo.de">Einloggen</a> <a class="login-link" href="https://app.fedeo.de">Einloggen</a>
</nav> </nav>
</header> </header>
@@ -99,8 +99,9 @@
<img src="/Logo.png" alt="FEDEO" /> <img src="/Logo.png" alt="FEDEO" />
<div> <div>
<NuxtLink to="/zielgruppen">Zielgruppen</NuxtLink> <NuxtLink to="/zielgruppen">Zielgruppen</NuxtLink>
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
<NuxtLink to="/impressum">Impressum</NuxtLink> <NuxtLink to="/impressum">Impressum</NuxtLink>
<a href="https://federspiel.tech/datenschutz">Datenschutz</a> <NuxtLink to="/datenschutz">Datenschutz</NuxtLink>
<a href="https://app.fedeo.de">Einloggen</a> <a href="https://app.fedeo.de">Einloggen</a>
</div> </div>
<p>Copyright © 2026 Federspiel Technology UG haftungsbeschränkt.</p> <p>Copyright © 2026 Federspiel Technology UG haftungsbeschränkt.</p>

View File

@@ -11,7 +11,7 @@
<a href="#open-source">Open Source</a> <a href="#open-source">Open Source</a>
<a href="#selfhost">Selfhost</a> <a href="#selfhost">Selfhost</a>
<a href="#matrix">Matrix</a> <a href="#matrix">Matrix</a>
<a href="#kontakt">Kontakt</a> <NuxtLink to="/kontakt">Kontakt</NuxtLink>
<a class="login-link" href="https://app.fedeo.de">Einloggen</a> <a class="login-link" href="https://app.fedeo.de">Einloggen</a>
</nav> </nav>
</header> </header>
@@ -24,7 +24,7 @@
FEDEO bündelt Projekte, Zeiten, Buchhaltung, Kommunikation, Geräte und Warenflüsse in einer Oberfläche, die den Arbeitsalltag kleiner und mittlerer Teams spürbar ruhiger macht. FEDEO bündelt Projekte, Zeiten, Buchhaltung, Kommunikation, Geräte und Warenflüsse in einer Oberfläche, die den Arbeitsalltag kleiner und mittlerer Teams spürbar ruhiger macht.
</p> </p>
<div class="hero-actions"> <div class="hero-actions">
<a class="primary-action" href="#kontakt">Demo anfragen</a> <NuxtLink class="primary-action" to="/kontakt">Demo anfragen</NuxtLink>
<a class="secondary-action" href="#funktionen">Funktionen ansehen</a> <a class="secondary-action" href="#funktionen">Funktionen ansehen</a>
</div> </div>
</div> </div>
@@ -215,15 +215,16 @@
Frag eine Demo an und wir zeigen dir, wie FEDEO zu deinen Abläufen passt. Frag eine Demo an und wir zeigen dir, wie FEDEO zu deinen Abläufen passt.
</p> </p>
</div> </div>
<a class="primary-action" href="https://fedeo.de/kontakt">Kontakt aufnehmen</a> <NuxtLink class="primary-action" to="/kontakt">Kontakt aufnehmen</NuxtLink>
</section> </section>
<footer> <footer>
<img src="/Logo.png" alt="FEDEO" /> <img src="/Logo.png" alt="FEDEO" />
<div> <div>
<NuxtLink to="/zielgruppen">Zielgruppen</NuxtLink> <NuxtLink to="/zielgruppen">Zielgruppen</NuxtLink>
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
<NuxtLink to="/impressum">Impressum</NuxtLink> <NuxtLink to="/impressum">Impressum</NuxtLink>
<a href="https://federspiel.tech/datenschutz">Datenschutz</a> <NuxtLink to="/datenschutz">Datenschutz</NuxtLink>
<a href="https://app.fedeo.de">Einloggen</a> <a href="https://app.fedeo.de">Einloggen</a>
</div> </div>
<p>Copyright © 2026 Federspiel Technology UG haftungsbeschränkt.</p> <p>Copyright © 2026 Federspiel Technology UG haftungsbeschränkt.</p>

View File

@@ -0,0 +1,124 @@
<template>
<main>
<header class="site-header">
<NuxtLink class="brand" to="/" aria-label="FEDEO Startseite">
<img src="/Logo.png" alt="FEDEO" />
</NuxtLink>
<nav aria-label="Hauptnavigation">
<NuxtLink to="/zielgruppen">Zielgruppen</NuxtLink>
<NuxtLink to="/#funktionen">Funktionen</NuxtLink>
<NuxtLink to="/#open-source">Open Source</NuxtLink>
<NuxtLink to="/#selfhost">Selfhost</NuxtLink>
<NuxtLink to="/#matrix">Matrix</NuxtLink>
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
<a class="login-link" href="https://app.fedeo.de">Einloggen</a>
</nav>
</header>
<section class="contact-page">
<div class="contact-heading">
<p class="eyebrow">Kontakt</p>
<h1>Sprich mit uns über FEDEO.</h1>
<p>
Ob Demo, Selfhosting, Integrationen oder ein konkreter Ablauf im Betrieb: Schreib uns kurz, worum es geht.
</p>
</div>
<form class="contact-form" @submit.prevent="submitForm">
<div class="form-grid">
<label>
Name
<input v-model="form.name" name="name" autocomplete="name" required>
</label>
<label>
E-Mail
<input v-model="form.email" name="email" autocomplete="email" required type="email">
</label>
</div>
<label>
Unternehmen
<input v-model="form.company" name="company" autocomplete="organization">
</label>
<label class="honeypot">
Website
<input v-model="form.website" name="website" tabindex="-1" autocomplete="off">
</label>
<label>
Nachricht
<textarea v-model="form.message" name="message" required rows="8"></textarea>
</label>
<label class="check-row">
<input v-model="form.privacy" type="checkbox" required>
<span>
Ich habe die <NuxtLink to="/datenschutz">Datenschutzerklärung</NuxtLink> gelesen und bin mit der Verarbeitung meiner Angaben zur Kontaktaufnahme einverstanden.
</span>
</label>
<div class="form-actions">
<button class="primary-action" :disabled="pending" type="submit">
{{ pending ? 'Wird gesendet' : 'Anfrage senden' }}
</button>
<p v-if="statusMessage" :class="['form-status', statusType]">{{ statusMessage }}</p>
</div>
</form>
</section>
<footer>
<img src="/Logo.png" alt="FEDEO" />
<div>
<NuxtLink to="/zielgruppen">Zielgruppen</NuxtLink>
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
<NuxtLink to="/impressum">Impressum</NuxtLink>
<NuxtLink to="/datenschutz">Datenschutz</NuxtLink>
<a href="https://app.fedeo.de">Einloggen</a>
</div>
<p>Copyright © 2026 Federspiel Technology UG haftungsbeschränkt.</p>
</footer>
</main>
</template>
<script setup lang="ts">
const form = reactive({
company: '',
email: '',
message: '',
name: '',
privacy: false,
website: ''
})
const pending = ref(false)
const statusMessage = ref('')
const statusType = ref<'error' | 'success'>('success')
const submitForm = async () => {
pending.value = true
statusMessage.value = ''
try {
await $fetch('/api/contact', {
body: form,
method: 'POST'
})
form.company = ''
form.email = ''
form.message = ''
form.name = ''
form.privacy = false
form.website = ''
statusType.value = 'success'
statusMessage.value = 'Danke, deine Nachricht wurde gesendet.'
} catch (error: any) {
statusType.value = 'error'
statusMessage.value = error?.statusMessage || 'Die Nachricht konnte gerade nicht gesendet werden.'
} finally {
pending.value = false
}
}
</script>

View File

@@ -11,7 +11,7 @@
<NuxtLink to="/#open-source">Open Source</NuxtLink> <NuxtLink to="/#open-source">Open Source</NuxtLink>
<NuxtLink to="/#selfhost">Selfhost</NuxtLink> <NuxtLink to="/#selfhost">Selfhost</NuxtLink>
<NuxtLink to="/#matrix">Matrix</NuxtLink> <NuxtLink to="/#matrix">Matrix</NuxtLink>
<NuxtLink to="/#kontakt">Kontakt</NuxtLink> <NuxtLink to="/kontakt">Kontakt</NuxtLink>
<a class="login-link" href="https://app.fedeo.de">Einloggen</a> <a class="login-link" href="https://app.fedeo.de">Einloggen</a>
</nav> </nav>
</header> </header>
@@ -56,15 +56,16 @@
<h2>Nicht sicher, ob FEDEO zu deinem Betrieb passt?</h2> <h2>Nicht sicher, ob FEDEO zu deinem Betrieb passt?</h2>
<p>In einer Demo lässt sich schnell prüfen, welche Abläufe FEDEO direkt abdecken kann.</p> <p>In einer Demo lässt sich schnell prüfen, welche Abläufe FEDEO direkt abdecken kann.</p>
</div> </div>
<a class="primary-action" href="https://fedeo.de/kontakt">Demo anfragen</a> <NuxtLink class="primary-action" to="/kontakt">Demo anfragen</NuxtLink>
</section> </section>
<footer> <footer>
<img src="/Logo.png" alt="FEDEO" /> <img src="/Logo.png" alt="FEDEO" />
<div> <div>
<NuxtLink to="/zielgruppen">Zielgruppen</NuxtLink> <NuxtLink to="/zielgruppen">Zielgruppen</NuxtLink>
<NuxtLink to="/kontakt">Kontakt</NuxtLink>
<NuxtLink to="/impressum">Impressum</NuxtLink> <NuxtLink to="/impressum">Impressum</NuxtLink>
<a href="https://federspiel.tech/datenschutz">Datenschutz</a> <NuxtLink to="/datenschutz">Datenschutz</NuxtLink>
<a href="https://app.fedeo.de">Einloggen</a> <a href="https://app.fedeo.de">Einloggen</a>
</div> </div>
<p>Copyright © 2026 Federspiel Technology UG haftungsbeschränkt.</p> <p>Copyright © 2026 Federspiel Technology UG haftungsbeschränkt.</p>

View File

@@ -0,0 +1,233 @@
import net from 'node:net'
import tls from 'node:tls'
import { Buffer } from 'node:buffer'
type ContactPayload = {
company?: string
email?: string
message?: string
name?: string
privacy?: boolean
website?: string
}
class SmtpClient {
private buffer = ''
private socket: net.Socket | tls.TLSSocket
private waiters: Array<() => void> = []
constructor(socket: net.Socket | tls.TLSSocket) {
this.socket = socket
this.attach(socket)
}
private attach(socket: net.Socket | tls.TLSSocket) {
socket.setEncoding('utf8')
socket.on('data', (chunk) => {
this.buffer += chunk
this.waiters.splice(0).forEach((resolve) => resolve())
})
}
private waitForData() {
return new Promise<void>((resolve, reject) => {
const onError = (error: Error) => {
cleanup()
reject(error)
}
const onClose = () => {
cleanup()
reject(new Error('SMTP-Verbindung wurde geschlossen.'))
}
const cleanup = () => {
this.socket.off('error', onError)
this.socket.off('close', onClose)
}
this.socket.once('error', onError)
this.socket.once('close', onClose)
this.waiters.push(() => {
cleanup()
resolve()
})
})
}
private async readLine() {
while (!this.buffer.includes('\r\n')) {
await this.waitForData()
}
const index = this.buffer.indexOf('\r\n')
const line = this.buffer.slice(0, index)
this.buffer = this.buffer.slice(index + 2)
return line
}
private async readResponse() {
const lines: string[] = []
while (true) {
const line = await this.readLine()
lines.push(line)
if (/^\d{3} /.test(line)) {
const code = Number(line.slice(0, 3))
return { code, message: lines.join('\n') }
}
}
}
async connect(expected = 220) {
const response = await this.readResponse()
this.expect(response, expected)
}
async command(command: string, expected: number | number[]) {
this.socket.write(`${command}\r\n`)
const response = await this.readResponse()
this.expect(response, expected)
return response
}
async data(content: string) {
this.socket.write(`${content.replace(/^\./gm, '..')}\r\n.\r\n`)
const response = await this.readResponse()
this.expect(response, 250)
}
async startTls(host: string) {
this.socket = tls.connect({
rejectUnauthorized: process.env.SMTP_REJECT_UNAUTHORIZED !== 'false',
servername: host,
socket: this.socket
})
this.buffer = ''
this.attach(this.socket)
await new Promise<void>((resolve, reject) => {
this.socket.once('secureConnect', resolve)
this.socket.once('error', reject)
})
}
close() {
this.socket.end()
}
private expect(response: { code: number, message: string }, expected: number | number[]) {
const expectedCodes = Array.isArray(expected) ? expected : [expected]
if (!expectedCodes.includes(response.code)) {
throw new Error(`SMTP-Fehler ${response.code}: ${response.message}`)
}
}
}
const cleanHeader = (value: string) => value.replace(/[\r\n]+/g, ' ').trim()
const isValidEmail = (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
const encodeHeader = (value: string) => {
const safeValue = cleanHeader(value)
return /^[\x20-\x7e]*$/.test(safeValue)
? safeValue
: `=?UTF-8?B?${Buffer.from(safeValue, 'utf8').toString('base64')}?=`
}
const createEmail = (payload: Required<Pick<ContactPayload, 'email' | 'message' | 'name'>> & Pick<ContactPayload, 'company'>, from: string, to: string) => {
const subject = `FEDEO Kontaktanfrage von ${payload.name}`
const lines = [
`Name: ${payload.name}`,
`E-Mail: ${payload.email}`,
`Unternehmen: ${payload.company || '-'}`,
'',
payload.message
]
return [
`From: ${encodeHeader('FEDEO Webseite')} <${cleanHeader(from)}>`,
`To: ${cleanHeader(to)}`,
`Reply-To: ${cleanHeader(payload.email)}`,
`Subject: ${encodeHeader(subject)}`,
'MIME-Version: 1.0',
'Content-Type: text/plain; charset=UTF-8',
'Content-Transfer-Encoding: 8bit',
'',
lines.join('\n')
].join('\r\n')
}
const sendMail = async (payload: Required<Pick<ContactPayload, 'email' | 'message' | 'name'>> & Pick<ContactPayload, 'company'>) => {
const host = process.env.SMTP_HOST
const user = process.env.SMTP_USER
const password = process.env.SMTP_PASSWORD
const to = process.env.CONTACT_TO
if (!host || !user || !password || !to) {
throw createError({
statusCode: 500,
statusMessage: 'Das Kontaktformular ist noch nicht vollständig konfiguriert.'
})
}
const secure = process.env.SMTP_SECURE === 'true'
const port = Number(process.env.SMTP_PORT || (secure ? 465 : 587))
const from = process.env.SMTP_FROM || user
const client = new SmtpClient(secure
? tls.connect({ host, port, rejectUnauthorized: process.env.SMTP_REJECT_UNAUTHORIZED !== 'false', servername: host })
: net.connect({ host, port })
)
try {
await client.connect()
await client.command(`EHLO ${process.env.SMTP_HELO || 'fedeo.de'}`, 250)
if (!secure) {
await client.command('STARTTLS', 220)
await client.startTls(host)
await client.command(`EHLO ${process.env.SMTP_HELO || 'fedeo.de'}`, 250)
}
await client.command('AUTH LOGIN', 334)
await client.command(Buffer.from(user).toString('base64'), 334)
await client.command(Buffer.from(password).toString('base64'), 235)
await client.command(`MAIL FROM:<${cleanHeader(from)}>`, 250)
await client.command(`RCPT TO:<${cleanHeader(to)}>`, [250, 251])
await client.command('DATA', 354)
await client.data(createEmail(payload, from, to))
await client.command('QUIT', 221)
} finally {
client.close()
}
}
export default defineEventHandler(async (event) => {
const body = await readBody<ContactPayload>(event)
const name = body.name?.trim() || ''
const email = body.email?.trim() || ''
const company = body.company?.trim() || ''
const message = body.message?.trim() || ''
if (body.website) {
return { ok: true }
}
if (!name || name.length > 120) {
throw createError({ statusCode: 400, statusMessage: 'Bitte gib deinen Namen an.' })
}
if (!email || !isValidEmail(email) || email.length > 180) {
throw createError({ statusCode: 400, statusMessage: 'Bitte gib eine gültige E-Mail-Adresse an.' })
}
if (!message || message.length < 10 || message.length > 5000) {
throw createError({ statusCode: 400, statusMessage: 'Bitte beschreibe dein Anliegen etwas genauer.' })
}
if (!body.privacy) {
throw createError({ statusCode: 400, statusMessage: 'Bitte bestätige die Datenschutzhinweise.' })
}
await sendMail({ company, email, message, name })
return { ok: true }
})