import prisma from '../lib/prisma.js'; import { encrypt, decrypt } from '../utils/encryption.js'; import { provisionEmail, provisionEmailWithMailbox, enableMailboxForExistingEmail, checkEmailExists, getProviderDomain, updateMailboxPassword, setEmailForwardTargets, getActiveProviderConfig, } from './emailProvider/emailProviderService.js'; import { generateSecurePassword } from '../utils/passwordGenerator.js'; import { ApiError } from '../utils/apiError.js'; // Locker, aber strikt genug gegen offensichtlichen Müll (CRLF, Spaces, // Komma). Wirklich validiert wird vom Provider beim Sync. const FORWARD_EMAIL_REGEX = /^[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}$/; // Pentest 71.1 (MEDIUM, 2026-06-08): RFC-reservierte und private/ // On-Prem-TLDs. Eine Weiterleitung an `attacker@plesk.internal` würde // am Provider DNS-Lookups gegen internes Netz auslösen oder bei mDNS- // Setup an einen lokalen Mailserver gehen. Wir blocken sie hart. const BLOCKED_TLDS = new Set([ 'local', 'internal', 'corp', 'lan', 'home', 'private', 'invalid', 'test', 'localhost', 'example', 'intranet', 'localdomain', 'arpa', ]); export function parseAdditionalForwards(raw: string | null | undefined): string[] { if (!raw) return []; try { const parsed = JSON.parse(raw); if (!Array.isArray(parsed)) return []; return parsed.filter((x): x is string => typeof x === 'string' && x.trim() !== ''); } catch { return []; } } export function serializeAdditionalForwards(list: string[]): string | null { const cleaned = list.map((s) => s.trim()).filter((s) => s !== ''); return cleaned.length === 0 ? null : JSON.stringify(cleaned); } /** * Liefert eine normalisierte Form der Adresse für den Dedup-Vergleich: * lowercase + Plus-Tag aus dem Local-Part rausgestrippt. * `billing+pentest@test.de` und `billing@test.de` haben so denselben * Schlüssel und treffen sich beim Dedup. Pentest 71.2. */ export function canonicalEmailKey(email: string): string { const trimmed = email.trim().toLowerCase(); const at = trimmed.lastIndexOf('@'); if (at < 1) return trimmed; const localPart = trimmed.slice(0, at); const domain = trimmed.slice(at + 1); const plus = localPart.indexOf('+'); const cleanedLocal = plus === -1 ? localPart : localPart.slice(0, plus); return `${cleanedLocal}@${domain}`; } export function assertValidForwardingEmail(value: unknown): string { if (typeof value !== 'string') { throw new ApiError(400, 'Ungültige Weiterleitungs-E-Mail-Adresse'); } const trimmed = value.trim(); if (trimmed.length === 0 || trimmed.length > 254) { throw new ApiError(400, 'Weiterleitungs-E-Mail-Adresse ist leer oder zu lang'); } if (!FORWARD_EMAIL_REGEX.test(trimmed)) { throw new ApiError(400, `Ungültiges E-Mail-Format: ${trimmed}`); } // 71.1: TLD aus dem Domain-Part rausziehen und gegen die Blocklist // halten. Domain liegt nach dem letzten @, TLD nach dem letzten Punkt. const domain = trimmed.slice(trimmed.lastIndexOf('@') + 1).toLowerCase(); const tld = domain.slice(domain.lastIndexOf('.') + 1); if (BLOCKED_TLDS.has(tld)) { throw new ApiError(400, `Top-Level-Domain "${tld}" ist nicht erlaubt (reservierte/private TLD).`); } return trimmed.toLowerCase(); } export async function getEmailsByCustomerId(customerId: number, includeInactive = false) { const where: Record = { customerId }; if (!includeInactive) { where.isActive = true; } return prisma.stressfreiEmail.findMany({ where, orderBy: { createdAt: 'desc' }, }); } // Mit Mailbox-Status für E-Mail-Client export async function getEmailsWithMailboxByCustomerId(customerId: number) { return prisma.stressfreiEmail.findMany({ where: { customerId, isActive: true, hasMailbox: true, }, select: { id: true, email: true, notes: true, hasMailbox: true, _count: { select: { cachedEmails: true, }, }, }, orderBy: { email: 'asc' }, }); } export async function getEmailById(id: number) { return prisma.stressfreiEmail.findUnique({ where: { id }, }); } // E-Mail mit Mailbox-Status laden export async function getEmailWithMailboxById(id: number) { return prisma.stressfreiEmail.findUnique({ where: { id }, select: { id: true, customerId: true, email: true, platform: true, notes: true, isActive: true, hasMailbox: true, emailPasswordEncrypted: true, createdAt: true, updatedAt: true, }, }); } export interface CreateEmailData { customerId: number; email: string; platform?: string; notes?: string; provisionAtProvider?: boolean; createMailbox?: boolean; } export async function createEmail(data: CreateEmailData) { const { provisionAtProvider, createMailbox, ...emailData } = data; // Duplikatscheck: pro Kunde darf eine Stressfrei-Adresse nur einmal // existieren. Case-insensitive, weil RFC-localpart-Großschreibung in // der Praxis nie semantischen Unterschied macht und der Provider eh // einheitlich lowercased. const normalized = data.email.trim().toLowerCase(); const conflict = await prisma.stressfreiEmail.findFirst({ where: { customerId: data.customerId, email: { equals: normalized }, }, select: { id: true, isActive: true }, }); if (conflict) { const hint = conflict.isActive ? 'Diese E-Mail-Adresse ist bei diesem Kunden bereits angelegt.' : 'Diese E-Mail-Adresse existiert bei diesem Kunden bereits (deaktiviert). Bitte reaktiviere den vorhandenen Eintrag, statt einen neuen anzulegen.'; throw new ApiError(409, hint); } // Wert in DB ist eh schon lowercase – wir setzen es einheitlich. emailData.email = normalized; // Falls beim Provider anlegen gewünscht if (provisionAtProvider) { // Kunde laden für Weiterleitung const customer = await prisma.customer.findUnique({ where: { id: data.customerId }, select: { email: true }, }); if (!customer?.email) { throw new Error('Kunde hat keine E-Mail-Adresse für Weiterleitung'); } // LocalPart extrahieren const localPart = data.email.split('@')[0]; if (createMailbox) { // Mit echter Mailbox anlegen. Immer ein frisches, sicheres Passwort // erzeugen. const password = generateSecurePassword(); const result = await provisionEmailWithMailbox(localPart, customer.email, password); if (!result.success) { throw new Error(result.error || 'Fehler beim Anlegen der Mailbox'); } // Passwort verbindlich am Postfach setzen. Beim Neu-Anlegen hat // createEmailWithMailbox es zwar schon gesetzt (der folgende Call ist // dann idempotent), ABER wenn die Adresse beim Provider bereits als // reine Weiterleitung existierte, kehrt provisionEmailWithMailbox früh // mit success zurück, ohne je ein Postfach-Passwort zu setzen. Ohne // diesen expliziten Schritt läge im CRM ein verschlüsseltes Passwort, // das der Provider gar nicht kennt → IMAP/SMTP-Login schlägt fehl. // Deshalb: nach jedem Mailbox-Anlegen das generierte Passwort // garantiert am Provider setzen, damit CRM und Provider übereinstimmen. const pwResult = await updateMailboxPassword(localPart, password); if (!pwResult.success) { throw new Error( pwResult.error || 'Mailbox angelegt, aber Postfach-Passwort konnte nicht gesetzt werden', ); } // Passwort verschlüsseln und speichern const passwordEncrypted = encrypt(password); return prisma.stressfreiEmail.create({ data: { ...emailData, isActive: true, hasMailbox: true, isProvisioned: true, provisionedAt: new Date(), emailPasswordEncrypted: passwordEncrypted, }, }); } else { // Nur Weiterleitung anlegen const result = await provisionEmail(localPart, customer.email); if (!result.success && !result.message?.includes('existiert bereits')) { throw new Error(result.error || 'Fehler beim Anlegen der E-Mail'); } } } return prisma.stressfreiEmail.create({ data: { ...emailData, isActive: true, hasMailbox: createMailbox || false, // Provisioned-Flag nur setzen wenn Provider-Aufruf gerade lief (oder // die Mail bei Plesk schon existierte und der „existiert bereits"-Pfad // gegriffen hat). isProvisioned: !!provisionAtProvider, provisionedAt: provisionAtProvider ? new Date() : null, }, }); } export async function updateEmail( id: number, data: { email?: string; platform?: string; notes?: string; isActive?: boolean; } ) { // Beim Umbenennen der E-Mail-Adresse erneut Kollisionscheck. Sonst // kann man eine zweite Adresse mit identischer Mail beim selben Kunden // anlegen (Umweg um den Create-Check). if (typeof data.email === 'string' && data.email.trim() !== '') { const normalized = data.email.trim().toLowerCase(); const current = await prisma.stressfreiEmail.findUnique({ where: { id }, select: { customerId: true, email: true }, }); if (!current) { throw new ApiError(404, 'StressfreiEmail nicht gefunden'); } if (normalized !== current.email.toLowerCase()) { const conflict = await prisma.stressfreiEmail.findFirst({ where: { customerId: current.customerId, email: { equals: normalized }, NOT: { id }, }, select: { id: true }, }); if (conflict) { throw new ApiError(409, 'Diese E-Mail-Adresse ist bei diesem Kunden bereits angelegt.'); } } data.email = normalized; } return prisma.stressfreiEmail.update({ where: { id }, data, }); } export async function deleteEmail(id: number) { return prisma.stressfreiEmail.delete({ where: { id } }); } /** * Komplette Liste zusätzlicher Weiterleitungs-E-Mails ersetzen und * direkt mit dem Provider synchronisieren. Aufrufer hat eine canonical * Liste – das Sub-Modal arbeitet auf Snapshot-Basis. * * Pentest 71.2: Dedup über `canonicalEmailKey` (Plus-Tags strippen), * damit `billing+tag@x.de` und `billing@x.de` als gleiches Ziel * erkannt werden – auch im Vergleich zur Stamm-E-Mail des Kunden. * * Pentest 71.4: DB-Update wird bei Provider-Sync-Fehler zurückgerollt, * damit Plesk und DB nicht auseinanderlaufen. * * Pentest 81.1: Self-Forward wird hart abgelehnt – würde sonst am * Provider einen Mail-Loop erzeugen (Stressfrei-Adresse leitet auf * sich selbst um → unendliche Weiterleitung). */ export async function setAdditionalForwards( id: number, emails: string[], ): Promise<{ success: boolean; forwardTargets?: string[]; error?: string }> { // Kunden-Stamm-Mail + eigene Email holen für Dedup gegen die fest // gesetzten Ziele bzw. die Stressfrei-Adresse selbst. const meta = await prisma.stressfreiEmail.findUnique({ where: { id }, select: { email: true, additionalForwardingEmails: true, customer: { select: { email: true } }, }, }); if (!meta) { throw new ApiError(404, 'StressfreiEmail nicht gefunden'); } const previousRaw = meta.additionalForwardingEmails; const customerEmailKey = meta.customer?.email ? canonicalEmailKey(meta.customer.email) : null; const selfKey = canonicalEmailKey(meta.email); // Input normalisieren + Duplikate raus. const seen = new Set(); if (customerEmailKey) seen.add(customerEmailKey); const cleaned: string[] = []; for (const raw of emails) { const ok = assertValidForwardingEmail(raw); const key = canonicalEmailKey(ok); // 81.1: Eintrag, der auf die Adresse selbst zeigt, würde einen // Mail-Loop am Provider erzeugen. Hart ablehnen mit klarer // Fehlermeldung, statt silent zu droppen – der User soll merken, // dass sein Eintrag bewusst nicht akzeptiert wurde. if (key === selfKey) { throw new ApiError( 400, `"${ok}" zeigt auf die Adresse selbst – Mail-Loop. Bitte eine andere Weiterleitungsadresse wählen.`, ); } if (!seen.has(key)) { seen.add(key); cleaned.push(ok); } } const nextRaw = serializeAdditionalForwards(cleaned); await prisma.stressfreiEmail.update({ where: { id }, data: { additionalForwardingEmails: nextRaw }, }); // Provider unmittelbar nachziehen, sonst läuft das Plesk-Mail-Konto // mit der alten Liste weiter. autoImport=false, weil unsere DB-Liste // hier die explizite User-Intent ist – kein Plesk-Member-Auto-Pull, // sonst landen gerade entfernte Adressen zurück in der Liste. const syncResult = await syncForwardingForEmail(id, { autoImportPleskMembers: false }); // 71.4: Rollback wenn Plesk den Sync abgelehnt hat. DB darf nicht // den optimistischen Stand zeigen, wenn der Provider noch auf dem // alten Stand ist. if (!syncResult.success && previousRaw !== nextRaw) { await prisma.stressfreiEmail.update({ where: { id }, data: { additionalForwardingEmails: previousRaw }, }).catch((rollbackErr) => { console.error( '[setAdditionalForwards] Rollback nach Provider-Fail fehlgeschlagen:', rollbackErr, ); }); } return syncResult; } // Mailbox nachträglich aktivieren (für existierende E-Mail-Weiterleitung) export async function enableMailbox(id: number): Promise<{ success: boolean; error?: string }> { const stressfreiEmail = await prisma.stressfreiEmail.findUnique({ where: { id }, }); if (!stressfreiEmail) { return { success: false, error: 'StressfreiEmail nicht gefunden' }; } if (stressfreiEmail.hasMailbox) { return { success: false, error: 'Mailbox ist bereits aktiviert' }; } const localPart = stressfreiEmail.email.split('@')[0]; const password = generateSecurePassword(); // Mailbox für existierende E-Mail aktivieren (nicht neu erstellen!) const result = await enableMailboxForExistingEmail(localPart, password); if (!result.success) { return { success: false, error: result.error || 'Fehler beim Aktivieren der Mailbox' }; } // Passwort verschlüsseln und speichern const passwordEncrypted = encrypt(password); await prisma.stressfreiEmail.update({ where: { id }, data: { hasMailbox: true, emailPasswordEncrypted: passwordEncrypted, }, }); return { success: true }; } // Mailbox-Status mit Provider synchronisieren export async function syncMailboxStatus(id: number): Promise<{ success: boolean; hasMailbox?: boolean; wasUpdated?: boolean; error?: string }> { const stressfreiEmail = await prisma.stressfreiEmail.findUnique({ where: { id }, select: { email: true, hasMailbox: true, isProvisioned: true, provisionedAt: true }, }); if (!stressfreiEmail) { return { success: false, error: 'StressfreiEmail nicht gefunden' }; } const localPart = stressfreiEmail.email.split('@')[0]; // Provider-Status prüfen const providerStatus = await checkEmailExists(localPart); // Self-Healing für `isProvisioned`: das Flag wurde in einer früheren Code- // Version beim Provisioning nie gesetzt → DB ist stellenweise inkonsistent // zum Provider. Wir reconciliieren bei jedem Status-Sync mit. const updates: Record = {}; if (!providerStatus.exists) { // Beim Provider nicht (mehr) vorhanden → DB-Flag entsprechend if (stressfreiEmail.isProvisioned) { updates.isProvisioned = false; } if (stressfreiEmail.hasMailbox) { updates.hasMailbox = false; } if (Object.keys(updates).length > 0) { await prisma.stressfreiEmail.update({ where: { id }, data: updates }); return { success: true, hasMailbox: false, wasUpdated: true }; } return { success: true, hasMailbox: false, wasUpdated: false }; } // Beim Provider vorhanden → isProvisioned auf true ziehen falls noch nicht if (!stressfreiEmail.isProvisioned) { updates.isProvisioned = true; if (!stressfreiEmail.provisionedAt) { updates.provisionedAt = new Date(); } } const providerHasMailbox = providerStatus.hasMailbox === true; if (stressfreiEmail.hasMailbox !== providerHasMailbox) { updates.hasMailbox = providerHasMailbox; } if (Object.keys(updates).length > 0) { await prisma.stressfreiEmail.update({ where: { id }, data: updates }); console.log(`Stressfrei-Status für ${stressfreiEmail.email} reconciled:`, updates); return { success: true, hasMailbox: providerHasMailbox, wasUpdated: true }; } return { success: true, hasMailbox: providerHasMailbox, wasUpdated: false }; } // Passwort für IMAP/SMTP-Zugang entschlüsseln (nur für autorisierte Nutzung) export async function getDecryptedPassword(id: number): Promise { const stressfreiEmail = await prisma.stressfreiEmail.findUnique({ where: { id }, select: { emailPasswordEncrypted: true }, }); if (!stressfreiEmail?.emailPasswordEncrypted) { return null; } try { return decrypt(stressfreiEmail.emailPasswordEncrypted); } catch { console.error('Fehler beim Entschlüsseln des Passworts'); return null; } } // Weiterleitungen einer Stressfrei-Adresse neu setzen (z.B. nach Änderung der // Stamm-E-Mail des Kunden). Ersetzt alle bestehenden Forwards durch // [aktuelle Kunden-E-Mail, defaultForwardEmail aus Provider-Config]. // // Wenn die Adresse `hasMailbox` ist: setzt zusätzlich das im CRM verschlüsselt // hinterlegte Passwort am Provider neu (Use-Case: Plesk-Restore, manueller // Eingriff im Plesk-UI etc. – CRM und Provider können sich entkoppeln, sodass // IMAP/SMTP-Logins im CRM nicht mehr passen). Self-Healing. // // Idempotent: das Plesk-CLI `set:` überschreibt die Adressliste komplett, kein // Duplikat-Risiko bei Mehrfachaufruf. Wenn die Operation erfolgreich war wird // das `isProvisioned`-Flag automatisch auf `true` gezogen (historische // Einträge, bei denen das Flag nie gesetzt wurde, werden so geheilt). export async function syncForwardingForEmail( id: number, options: { autoImportPleskMembers?: boolean } = {}, ): Promise<{ success: boolean; forwardTargets?: string[]; customerEmail?: string; passwordReset?: boolean; error?: string; }> { // Auto-Import übernimmt unbekannte Plesk-Members in unsere DB. Macht // beim Sync-Button Sinn (Bestands-Migration), aber NICHT beim // User-getriggerten Add/Remove – dort ist die DB-Liste die Wahrheit. // Sonst kreisen entfernte Adressen zurück in die Liste: // 1. User entfernt c → DB=[a,b], Plesk=[a,b,c] // 2. Auto-Import: "c ist in Plesk aber nicht in DB → in DB schreiben" // 3. → DB=[a,b,c], Diff sagt nichts zu löschen, Plesk bleibt [a,b,c]. const autoImport = options.autoImportPleskMembers ?? true; const stressfreiEmail = await prisma.stressfreiEmail.findUnique({ where: { id }, select: { email: true, customerId: true, isProvisioned: true, hasMailbox: true, emailPasswordEncrypted: true, additionalForwardingEmails: true, }, }); if (!stressfreiEmail) { return { success: false, error: 'StressfreiEmail nicht gefunden' }; } const customer = await prisma.customer.findUnique({ where: { id: stressfreiEmail.customerId }, select: { email: true }, }); if (!customer?.email) { return { success: false, error: 'Kunde hat keine Stamm-E-Mail-Adresse hinterlegt' }; } const config = await getActiveProviderConfig(); const forwardTargets: string[] = [customer.email]; if (config?.defaultForwardEmail) { forwardTargets.push(config.defaultForwardEmail); } // Zusätzliche Weiterleitungsziele (vom User im Modal gepflegt). Duplikate // gegen die Stamm-Mail oder den Default werden hier weggefiltert, damit // Plesk nicht mit Wiederholungen die Liste aufbläht. Pentest 71.2: // Vergleich über `canonicalEmailKey`, damit Plus-Tags nicht doppelt // zustellen. const seenKeys = new Set(forwardTargets.map(canonicalEmailKey)); for (const extra of parseAdditionalForwards(stressfreiEmail.additionalForwardingEmails)) { const key = canonicalEmailKey(extra); if (!seenKeys.has(key)) { seenKeys.add(key); forwardTargets.push(extra); } } const localPart = stressfreiEmail.email.split('@')[0]; // 0) Auto-Migration: Plesk hat zwei Verteil-Mechanismen (Mailgroup + // Forwarding). Alt-Anlagen liefen oft via Mailgroup – unser Sync // schreibt aber nur in die Forwarding-Liste, daher landeten neue // Adressen nirgendwo. Hier holen wir die aktuellen Mailgroup-Members // ab und ziehen alle, die wir nicht schon kennen, in unsere // additionalForwardingEmails-Liste rein. Der nachfolgende Plesk-Call // deaktiviert dann die Mailgroup und schreibt die volle Liste als // Forwarding. Verlustfrei – kein Empfänger fällt raus. // Pentest 83.2: Self-Forward auch beim Import blocken. Die // Stressfrei-Adresse selbst darf nicht aus Plesk in unsere DB // landen – sonst läuft sie nach dem Mailgroup→Forwarding-Umschalten // als Forwarding-Target auf sich selbst (Mail-Loop). seenKeys.add(canonicalEmailKey(stressfreiEmail.email)); if (autoImport) { try { const pleskState = await checkEmailExists(localPart); const existingMembers = [ ...(pleskState.mailgroupMembers ?? []), ...(pleskState.forwardingTargets ?? []), ]; const newImports: string[] = []; for (const member of existingMembers) { // Pentest 83.1: importierte Adressen aus Plesk müssen denselben // Filter passieren wie User-Eingaben (TLD-Blocklist, Format). // Sonst rutschen reservierte TLDs wie `.internal` ohne Check // in unsere DB, falls ein Plesk-Admin sie dort manuell gepflegt // hat. Ungültige werden silent gedroppt – Log informiert. let validated: string; try { validated = assertValidForwardingEmail(member); } catch (validationErr) { const reason = validationErr instanceof Error ? validationErr.message : 'unbekannt'; console.debug( `[syncForwardingForEmail] Plesk-Member "${member}" verworfen: ${reason}`, ); continue; } const key = canonicalEmailKey(validated); if (!seenKeys.has(key)) { seenKeys.add(key); forwardTargets.push(validated); newImports.push(validated); } } if (newImports.length > 0) { const mergedAdditional = [ ...parseAdditionalForwards(stressfreiEmail.additionalForwardingEmails), ...newImports, ]; await prisma.stressfreiEmail.update({ where: { id }, data: { additionalForwardingEmails: serializeAdditionalForwards(mergedAdditional) }, }); // Pentest 83.3: PII-Logs auf debug-Level statt log-Level. console.debug( `[syncForwardingForEmail] Importiert aus Plesk-Mailgroup für ${stressfreiEmail.email}:`, newImports, ); } } catch (importErr) { // Nicht hart fehlschlagen – im schlimmsten Fall fehlen ein paar // alte Empfänger, aber der eigentliche Sync soll trotzdem laufen. console.error('[syncForwardingForEmail] Mailgroup-Import fehlgeschlagen:', importErr); } } // 1) Forwards neu setzen (deaktiviert intern Mailgroup). const forwardResult = await setEmailForwardTargets(localPart, forwardTargets); if (!forwardResult.success) { // Wenn Plesk meldet „nicht gefunden", liefern wir eine sprechende Meldung // statt der rohen Provider-Nachricht. const err = forwardResult.error || 'Provider-Update fehlgeschlagen'; const friendly = /not\s*found|nicht\s*gefunden/i.test(err) ? 'E-Mail-Adresse beim Provider nicht gefunden – wurde sie dort gelöscht?' : err; return { success: false, error: friendly }; } // 2) Wenn Mailbox: Passwort aus CRM-Speicher entschlüsseln und am Provider // neu setzen (Self-Healing nach Provider-seitigen Änderungen). let passwordReset = false; if (stressfreiEmail.hasMailbox && stressfreiEmail.emailPasswordEncrypted) { try { const password = decrypt(stressfreiEmail.emailPasswordEncrypted); const pwResult = await updateMailboxPassword(localPart, password); if (!pwResult.success) { // Forwards waren schon erfolgreich – wir geben Forward-Erfolg + Passwort- // Fehler kombiniert zurück, statt die ganze Operation rot zu machen. return { success: false, forwardTargets, customerEmail: customer.email, error: 'Weiterleitungen aktualisiert, aber Passwort-Sync fehlgeschlagen: ' + (pwResult.error || 'unbekannt'), }; } passwordReset = true; } catch (e) { return { success: false, forwardTargets, customerEmail: customer.email, error: 'Weiterleitungen aktualisiert, aber Passwort konnte nicht entschlüsselt werden – ' + 'evtl. wurde der ENCRYPTION_KEY rotiert', }; } } // 3) Self-Healing: nach erfolgreichem Provider-Aufruf wissen wir definitiv, // dass die Adresse beim Provider existiert → Flag korrigieren. if (!stressfreiEmail.isProvisioned) { await prisma.stressfreiEmail.update({ where: { id }, data: { isProvisioned: true, provisionedAt: new Date() }, }); } return { success: true, forwardTargets, customerEmail: customer.email, passwordReset, }; } // Passwort neu generieren und beim Provider setzen export async function resetMailboxPassword(id: number): Promise<{ success: boolean; password?: string; error?: string }> { const stressfreiEmail = await prisma.stressfreiEmail.findUnique({ where: { id }, select: { email: true, hasMailbox: true }, }); if (!stressfreiEmail) { return { success: false, error: 'StressfreiEmail nicht gefunden' }; } if (!stressfreiEmail.hasMailbox) { return { success: false, error: 'Keine Mailbox für diese E-Mail-Adresse' }; } // Neues Passwort generieren const newPassword = generateSecurePassword(); const localPart = stressfreiEmail.email.split('@')[0]; // Passwort beim Provider ändern const providerResult = await updateMailboxPassword(localPart, newPassword); if (!providerResult.success) { return { success: false, error: providerResult.error || 'Fehler beim Aktualisieren des Passworts beim Provider' }; } // Passwort verschlüsseln und lokal speichern const passwordEncrypted = encrypt(newPassword); await prisma.stressfreiEmail.update({ where: { id }, data: { emailPasswordEncrypted: passwordEncrypted }, }); return { success: true, password: newPassword }; }