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'; 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; // 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 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 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; } ) { return prisma.stressfreiEmail.update({ where: { id }, data, }); } export async function deleteEmail(id: number) { return prisma.stressfreiEmail.delete({ where: { id } }); } // 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, ): Promise<{ success: boolean; forwardTargets?: string[]; customerEmail?: string; passwordReset?: boolean; error?: string; }> { const stressfreiEmail = await prisma.stressfreiEmail.findUnique({ where: { id }, select: { email: true, customerId: true, isProvisioned: true, hasMailbox: true, emailPasswordEncrypted: 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); } const localPart = stressfreiEmail.email.split('@')[0]; // 1) Forwards neu setzen. 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 }; }