// ==================== CACHED EMAIL CONTROLLER ==================== import { Request, Response } from 'express'; import * as cachedEmailService from '../services/cachedEmail.service.js'; import * as stressfreiEmailService from '../services/stressfreiEmail.service.js'; import * as invoiceService from '../services/invoice.service.js'; import { sendEmail, SmtpCredentials, SendEmailParams, EmailAttachment } from '../services/smtpService.js'; import { fetchAttachment, appendToSent, ImapCredentials } from '../services/imapService.js'; import { getImapSmtpSettings } from '../services/emailProvider/emailProviderService.js'; import { decrypt } from '../utils/encryption.js'; import { ApiResponse } from '../types/index.js'; import { getCustomerTargets, getContractTargets, getIdentityDocumentTargets, getBankCardTargets, documentTargets } from '../config/documentTargets.config.js'; import { generateEmailPdf } from '../services/pdfService.js'; import { PrismaClient, DocumentType } from '@prisma/client'; import path from 'path'; import fs from 'fs'; const prisma = new PrismaClient(); // ==================== E-MAIL LIST ==================== // E-Mails für einen Kunden abrufen export async function getEmailsForCustomer(req: Request, res: Response): Promise { try { const customerId = parseInt(req.params.customerId); const stressfreiEmailId = req.query.accountId ? parseInt(req.query.accountId as string) : undefined; const folder = req.query.folder as string | undefined; // INBOX oder SENT const limit = req.query.limit ? parseInt(req.query.limit as string) : 50; const offset = req.query.offset ? parseInt(req.query.offset as string) : 0; const emails = await cachedEmailService.getCachedEmails({ customerId, stressfreiEmailId, folder, limit, offset, includeBody: false, }); res.json({ success: true, data: emails } as ApiResponse); } catch (error) { console.error('getEmailsForCustomer error:', error); res.status(500).json({ success: false, error: 'Fehler beim Laden der E-Mails', } as ApiResponse); } } // E-Mails für einen Vertrag abrufen export async function getEmailsForContract(req: Request, res: Response): Promise { try { const contractId = parseInt(req.params.contractId); const folder = req.query.folder as string | undefined; // INBOX oder SENT const limit = req.query.limit ? parseInt(req.query.limit as string) : 50; const offset = req.query.offset ? parseInt(req.query.offset as string) : 0; const emails = await cachedEmailService.getCachedEmails({ contractId, folder, limit, offset, includeBody: false, }); res.json({ success: true, data: emails } as ApiResponse); } catch (error) { console.error('getEmailsForContract error:', error); res.status(500).json({ success: false, error: 'Fehler beim Laden der Vertrags-E-Mails', } as ApiResponse); } } // ==================== SINGLE EMAIL ==================== // Einzelne E-Mail abrufen (mit Body) export async function getEmail(req: Request, res: Response): Promise { try { const id = parseInt(req.params.id); const email = await cachedEmailService.getCachedEmailById(id); if (!email) { res.status(404).json({ success: false, error: 'E-Mail nicht gefunden', } as ApiResponse); return; } // Als gelesen markieren await cachedEmailService.markEmailAsRead(id); res.json({ success: true, data: { ...email, isRead: true } } as ApiResponse); } catch (error) { console.error('getEmail error:', error); res.status(500).json({ success: false, error: 'Fehler beim Laden der E-Mail', } as ApiResponse); } } // E-Mail als gelesen/ungelesen markieren export async function markAsRead(req: Request, res: Response): Promise { try { const id = parseInt(req.params.id); const { isRead } = req.body; if (isRead) { await cachedEmailService.markEmailAsRead(id); } else { await cachedEmailService.markEmailAsUnread(id); } res.json({ success: true } as ApiResponse); } catch (error) { console.error('markAsRead error:', error); res.status(500).json({ success: false, error: 'Fehler beim Markieren der E-Mail', } as ApiResponse); } } // E-Mail Stern umschalten export async function toggleStar(req: Request, res: Response): Promise { try { const id = parseInt(req.params.id); const isStarred = await cachedEmailService.toggleEmailStar(id); res.json({ success: true, data: { isStarred } } as ApiResponse); } catch (error) { console.error('toggleStar error:', error); res.status(500).json({ success: false, error: 'Fehler beim Ändern des Sterns', } as ApiResponse); } } // ==================== CONTRACT ASSIGNMENT ==================== // E-Mail einem Vertrag zuordnen export async function assignToContract(req: Request, res: Response): Promise { try { const emailId = parseInt(req.params.id); const { contractId } = req.body; const userId = (req as any).userId; // Falls Auth-Middleware userId setzt const email = await cachedEmailService.assignEmailToContract(emailId, contractId, userId); res.json({ success: true, data: email } as ApiResponse); } catch (error) { console.error('assignToContract error:', error); res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Fehler beim Zuordnen der E-Mail', } as ApiResponse); } } // Vertragszuordnung aufheben export async function unassignFromContract(req: Request, res: Response): Promise { try { const emailId = parseInt(req.params.id); const email = await cachedEmailService.unassignEmailFromContract(emailId); res.json({ success: true, data: email } as ApiResponse); } catch (error) { console.error('unassignFromContract error:', error); res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Fehler beim Aufheben der Zuordnung', } as ApiResponse); } } // E-Mail-Anzahl pro Ordner für ein Konto export async function getFolderCounts(req: Request, res: Response): Promise { try { const stressfreiEmailId = parseInt(req.params.id); const counts = await cachedEmailService.getFolderCountsForAccount(stressfreiEmailId); res.json({ success: true, data: counts } as ApiResponse); } catch (error) { console.error('getFolderCounts error:', error); res.status(500).json({ success: false, error: 'Fehler beim Laden der Ordner-Anzahlen', } as ApiResponse); } } // E-Mail-Anzahl pro Ordner für einen Vertrag export async function getContractFolderCounts(req: Request, res: Response): Promise { try { const contractId = parseInt(req.params.contractId); const counts = await cachedEmailService.getFolderCountsForContract(contractId); res.json({ success: true, data: counts } as ApiResponse); } catch (error) { console.error('getContractFolderCounts error:', error); res.status(500).json({ success: false, error: 'Fehler beim Laden der Ordner-Anzahlen', } as ApiResponse); } } // ==================== SYNC & SEND ==================== // E-Mails für ein Konto synchronisieren (INBOX + SENT) export async function syncAccount(req: Request, res: Response): Promise { try { const stressfreiEmailId = parseInt(req.params.id); const fullSync = req.query.full === 'true'; // Synchronisiert sowohl INBOX als auch SENT const result = await cachedEmailService.syncAllFoldersForAccount(stressfreiEmailId, { fullSync }); if (!result.success) { res.status(400).json({ success: false, error: result.error, } as ApiResponse); return; } res.json({ success: true, data: { newEmails: result.newEmails, totalEmails: result.totalEmails, }, } as ApiResponse); } catch (error) { console.error('syncAccount error:', error); res.status(500).json({ success: false, error: 'Fehler beim Synchronisieren der E-Mails', } as ApiResponse); } } // E-Mail senden export async function sendEmailFromAccount(req: Request, res: Response): Promise { try { const stressfreiEmailId = parseInt(req.params.id); const { to, cc, subject, text, html, inReplyTo, references, attachments, contractId } = req.body; // StressfreiEmail laden const stressfreiEmail = await stressfreiEmailService.getEmailWithMailboxById(stressfreiEmailId); if (!stressfreiEmail) { res.status(404).json({ success: false, error: 'E-Mail-Konto nicht gefunden', } as ApiResponse); return; } if (!stressfreiEmail.hasMailbox) { res.status(400).json({ success: false, error: 'Dieses Konto hat keine Mailbox für den Versand', } as ApiResponse); return; } // Passwort entschlüsseln const password = await stressfreiEmailService.getDecryptedPassword(stressfreiEmailId); if (!password) { res.status(400).json({ success: false, error: 'Passwort für E-Mail-Versand nicht verfügbar', } as ApiResponse); return; } // SMTP-Einstellungen vom Provider const settings = await getImapSmtpSettings(); if (!settings) { res.status(400).json({ success: false, error: 'Keine SMTP-Einstellungen konfiguriert', } as ApiResponse); return; } // SMTP-Credentials const credentials: SmtpCredentials = { host: settings.smtpServer, port: settings.smtpPort, user: stressfreiEmail.email, password, encryption: settings.smtpEncryption, allowSelfSignedCerts: settings.allowSelfSignedCerts, }; // E-Mail-Parameter const emailParams: SendEmailParams = { to, cc, subject, text, html, inReplyTo, references, attachments: attachments as EmailAttachment[] | undefined, }; // E-Mail senden const result = await sendEmail(credentials, stressfreiEmail.email, emailParams); if (!result.success) { res.status(400).json({ success: false, error: result.error, } as ApiResponse); return; } // Gesendete E-Mail im IMAP Sent-Ordner speichern (für Attachment-Download) let sentUid: number | undefined; if (result.rawEmail) { try { // IMAP-Credentials für Sent-Ordner const imapCredentials: ImapCredentials = { host: settings.imapServer, port: settings.imapPort, user: stressfreiEmail.email, password, encryption: settings.imapEncryption, allowSelfSignedCerts: settings.allowSelfSignedCerts, }; const appendResult = await appendToSent(imapCredentials, { rawEmail: result.rawEmail, }); if (appendResult.success && appendResult.uid) { sentUid = appendResult.uid; console.log(`[SMTP] Email stored in Sent folder with UID ${sentUid}`); } } catch (appendError) { // Nicht kritisch - E-Mail wurde trotzdem gesendet console.error('Error appending to IMAP Sent folder:', appendError); } } // Gesendete E-Mail im Cache speichern try { // Anhangsnamen extrahieren falls vorhanden const attachmentNames = attachments?.map((a: EmailAttachment) => a.filename) || []; await cachedEmailService.createSentEmail(stressfreiEmailId, { to, cc, subject, text, html, messageId: result.messageId || `sent-${Date.now()}@opencrm.local`, contractId: contractId ? parseInt(contractId) : undefined, attachmentNames: attachmentNames.length > 0 ? attachmentNames : undefined, uid: sentUid, // UID vom IMAP Sent-Ordner für Attachment-Download }); } catch (saveError) { // Fehler beim Speichern nicht kritisch - E-Mail wurde trotzdem gesendet console.error('Error saving sent email to cache:', saveError); } res.json({ success: true, data: { messageId: result.messageId }, } as ApiResponse); } catch (error) { console.error('sendEmailFromAccount error:', error); res.status(500).json({ success: false, error: 'Fehler beim Senden der E-Mail', } as ApiResponse); } } // ==================== ATTACHMENTS ==================== // Anhang-Liste einer E-Mail abrufen export async function getAttachments(req: Request, res: Response): Promise { try { const emailId = parseInt(req.params.emailId); // E-Mail aus Cache laden const email = await cachedEmailService.getCachedEmailById(emailId); if (!email) { res.status(404).json({ success: false, error: 'E-Mail nicht gefunden', } as ApiResponse); return; } // Anhänge aus attachmentNames parsen (JSON Array) const attachmentNames: string[] = email.attachmentNames ? JSON.parse(email.attachmentNames) : []; res.json({ success: true, data: attachmentNames.map((name) => ({ filename: name })), } as ApiResponse); } catch (error) { console.error('getAttachments error:', error); res.status(500).json({ success: false, error: 'Fehler beim Laden der Anhänge', } as ApiResponse); } } // Einzelnen Anhang herunterladen export async function downloadAttachment(req: Request, res: Response): Promise { try { const emailId = parseInt(req.params.emailId); const filename = decodeURIComponent(req.params.filename); // E-Mail aus Cache laden const email = await cachedEmailService.getCachedEmailById(emailId); if (!email) { res.status(404).json({ success: false, error: 'E-Mail nicht gefunden', } as ApiResponse); return; } // Für gesendete E-Mails: Prüfen ob UID vorhanden (im IMAP Sent gespeichert) if (email.folder === 'SENT' && email.uid === 0) { res.status(400).json({ success: false, error: 'Anhang nicht verfügbar - E-Mail wurde vor der IMAP-Speicherung gesendet', } as ApiResponse); return; } // StressfreiEmail laden um Zugangsdaten zu bekommen const stressfreiEmail = await stressfreiEmailService.getEmailWithMailboxById(email.stressfreiEmailId); if (!stressfreiEmail || !stressfreiEmail.emailPasswordEncrypted) { res.status(400).json({ success: false, error: 'Keine Mailbox-Zugangsdaten verfügbar', } as ApiResponse); return; } // IMAP-Einstellungen laden const settings = await getImapSmtpSettings(); if (!settings) { res.status(400).json({ success: false, error: 'Keine E-Mail-Provider-Einstellungen gefunden', } as ApiResponse); return; } // Passwort entschlüsseln const password = decrypt(stressfreiEmail.emailPasswordEncrypted); // IMAP-Credentials const credentials: ImapCredentials = { host: settings.imapServer, port: settings.imapPort, user: stressfreiEmail.email, password, encryption: settings.imapEncryption, allowSelfSignedCerts: settings.allowSelfSignedCerts, }; // Ordner basierend auf E-Mail-Typ bestimmen (INBOX oder Sent) const imapFolder = email.folder === 'SENT' ? 'Sent' : 'INBOX'; // Anhang per IMAP abrufen const attachment = await fetchAttachment(credentials, email.uid, filename, imapFolder); if (!attachment) { res.status(404).json({ success: false, error: 'Anhang nicht gefunden', } as ApiResponse); return; } // Datei senden - inline (öffnen) oder attachment (download) const disposition = req.query.view === 'true' ? 'inline' : 'attachment'; res.setHeader('Content-Type', attachment.contentType); res.setHeader('Content-Disposition', `${disposition}; filename="${encodeURIComponent(attachment.filename)}"`); res.setHeader('Content-Length', attachment.size); res.send(attachment.content); } catch (error) { console.error('downloadAttachment error:', error); res.status(500).json({ success: false, error: 'Fehler beim Herunterladen des Anhangs', } as ApiResponse); } } // ==================== MAILBOX ACCOUNTS ==================== // Mailbox-Konten eines Kunden abrufen export async function getMailboxAccounts(req: Request, res: Response): Promise { try { const customerId = parseInt(req.params.customerId); const accounts = await cachedEmailService.getMailboxAccountsForCustomer(customerId); res.json({ success: true, data: accounts } as ApiResponse); } catch (error) { console.error('getMailboxAccounts error:', error); res.status(500).json({ success: false, error: 'Fehler beim Laden der E-Mail-Konten', } as ApiResponse); } } // Mailbox nachträglich aktivieren export async function enableMailbox(req: Request, res: Response): Promise { try { const id = parseInt(req.params.id); const result = await stressfreiEmailService.enableMailbox(id); if (!result.success) { res.status(400).json({ success: false, error: result.error, } as ApiResponse); return; } res.json({ success: true } as ApiResponse); } catch (error) { console.error('enableMailbox error:', error); res.status(500).json({ success: false, error: 'Fehler beim Aktivieren der Mailbox', } as ApiResponse); } } // Mailbox-Status mit Provider synchronisieren export async function syncMailboxStatus(req: Request, res: Response): Promise { try { const id = parseInt(req.params.id); const result = await stressfreiEmailService.syncMailboxStatus(id); if (!result.success) { res.status(400).json({ success: false, error: result.error, } as ApiResponse); return; } res.json({ success: true, data: { hasMailbox: result.hasMailbox, wasUpdated: result.wasUpdated, }, } as ApiResponse); } catch (error) { console.error('syncMailboxStatus error:', error); res.status(500).json({ success: false, error: 'Fehler beim Synchronisieren des Mailbox-Status', } as ApiResponse); } } // E-Mail-Thread abrufen export async function getThread(req: Request, res: Response): Promise { try { const id = parseInt(req.params.id); const thread = await cachedEmailService.getEmailThread(id); res.json({ success: true, data: thread } as ApiResponse); } catch (error) { console.error('getThread error:', error); res.status(500).json({ success: false, error: 'Fehler beim Laden des E-Mail-Threads', } as ApiResponse); } } // Mailbox-Zugangsdaten abrufen (IMAP/SMTP) export async function getMailboxCredentials(req: Request, res: Response): Promise { try { const id = parseInt(req.params.id); // StressfreiEmail laden const stressfreiEmail = await stressfreiEmailService.getEmailWithMailboxById(id); if (!stressfreiEmail) { res.status(404).json({ success: false, error: 'E-Mail-Konto nicht gefunden', } as ApiResponse); return; } if (!stressfreiEmail.hasMailbox) { res.status(400).json({ success: false, error: 'Keine Mailbox für diese E-Mail-Adresse', } as ApiResponse); return; } // Passwort entschlüsseln const password = await stressfreiEmailService.getDecryptedPassword(id); if (!password) { res.status(500).json({ success: false, error: 'Passwort konnte nicht entschlüsselt werden', } as ApiResponse); return; } // IMAP/SMTP-Einstellungen laden const settings = await getImapSmtpSettings(); res.json({ success: true, data: { email: stressfreiEmail.email, password, imap: settings ? { server: settings.imapServer, port: settings.imapPort, encryption: settings.imapEncryption, } : null, smtp: settings ? { server: settings.smtpServer, port: settings.smtpPort, encryption: settings.smtpEncryption, } : null, }, } as ApiResponse); } catch (error) { console.error('getMailboxCredentials error:', error); res.status(500).json({ success: false, error: 'Fehler beim Laden der Mailbox-Zugangsdaten', } as ApiResponse); } } // Ungelesene E-Mails zählen export async function getUnreadCount(req: Request, res: Response): Promise { try { const customerId = req.query.customerId ? parseInt(req.query.customerId as string) : undefined; const contractId = req.query.contractId ? parseInt(req.query.contractId as string) : undefined; let count = 0; if (customerId) { count = await cachedEmailService.getUnreadCountForCustomer(customerId); } else if (contractId) { count = await cachedEmailService.getUnreadCountForContract(contractId); } res.json({ success: true, data: { count } } as ApiResponse); } catch (error) { console.error('getUnreadCount error:', error); res.status(500).json({ success: false, error: 'Fehler beim Zählen der ungelesenen E-Mails', } as ApiResponse); } } // E-Mail in Papierkorb verschieben (nur Admin) export async function deleteEmail(req: Request, res: Response): Promise { try { const id = parseInt(req.params.id); // Prüfen ob E-Mail existiert const email = await cachedEmailService.getCachedEmailById(id); if (!email) { res.status(404).json({ success: false, error: 'E-Mail nicht gefunden', } as ApiResponse); return; } const result = await cachedEmailService.moveEmailToTrash(id); if (!result.success) { res.status(400).json({ success: false, error: result.error, } as ApiResponse); return; } res.json({ success: true, message: 'E-Mail in Papierkorb verschoben' } as ApiResponse); } catch (error) { console.error('deleteEmail error:', error); res.status(500).json({ success: false, error: 'Fehler beim Löschen der E-Mail', } as ApiResponse); } } // ==================== TRASH OPERATIONS ==================== // Papierkorb-E-Mails für einen Kunden abrufen export async function getTrashEmails(req: Request, res: Response): Promise { try { const customerId = parseInt(req.params.customerId); const emails = await cachedEmailService.getTrashEmails(customerId); res.json({ success: true, data: emails } as ApiResponse); } catch (error) { console.error('getTrashEmails error:', error); res.status(500).json({ success: false, error: 'Fehler beim Laden der Papierkorb-E-Mails', } as ApiResponse); } } // Papierkorb-Anzahl für einen Kunden export async function getTrashCount(req: Request, res: Response): Promise { try { const customerId = parseInt(req.params.customerId); const count = await cachedEmailService.getTrashCount(customerId); res.json({ success: true, data: { count } } as ApiResponse); } catch (error) { console.error('getTrashCount error:', error); res.status(500).json({ success: false, error: 'Fehler beim Zählen der Papierkorb-E-Mails', } as ApiResponse); } } // E-Mail aus Papierkorb wiederherstellen export async function restoreEmail(req: Request, res: Response): Promise { try { const id = parseInt(req.params.id); const result = await cachedEmailService.restoreEmailFromTrash(id); if (!result.success) { res.status(400).json({ success: false, error: result.error, } as ApiResponse); return; } res.json({ success: true, message: 'E-Mail wiederhergestellt' } as ApiResponse); } catch (error) { console.error('restoreEmail error:', error); res.status(500).json({ success: false, error: 'Fehler beim Wiederherstellen der E-Mail', } as ApiResponse); } } // E-Mail endgültig löschen (aus Papierkorb) export async function permanentDeleteEmail(req: Request, res: Response): Promise { try { const id = parseInt(req.params.id); const result = await cachedEmailService.permanentDeleteEmail(id); if (!result.success) { res.status(400).json({ success: false, error: result.error, } as ApiResponse); return; } res.json({ success: true, message: 'E-Mail endgültig gelöscht' } as ApiResponse); } catch (error) { console.error('permanentDeleteEmail error:', error); res.status(500).json({ success: false, error: 'Fehler beim endgültigen Löschen der E-Mail', } as ApiResponse); } } // ==================== ATTACHMENT TARGETS ==================== // Verfügbare Dokumenten-Ziele für E-Mail-Anhänge abrufen export async function getAttachmentTargets(req: Request, res: Response): Promise { try { const emailId = parseInt(req.params.id); // E-Mail mit StressfreiEmail laden const email = await cachedEmailService.getCachedEmailById(emailId); if (!email) { res.status(404).json({ success: false, error: 'E-Mail nicht gefunden', } as ApiResponse); return; } // StressfreiEmail laden um an den Kunden zu kommen const stressfreiEmail = await prisma.stressfreiEmail.findUnique({ where: { id: email.stressfreiEmailId }, include: { customer: { include: { identityDocuments: { where: { isActive: true }, select: { id: true, type: true, documentNumber: true, documentPath: true }, }, bankCards: { where: { isActive: true }, select: { id: true, iban: true, bankName: true, documentPath: true }, }, }, }, }, }); if (!stressfreiEmail) { res.status(404).json({ success: false, error: 'E-Mail-Konto nicht gefunden', } as ApiResponse); return; } const customer = stressfreiEmail.customer; const customerType = customer.type as 'PRIVATE' | 'BUSINESS'; // Ergebnis-Struktur interface TargetSlot { key: string; label: string; field: string; hasDocument: boolean; currentPath?: string; } interface EntityWithSlots { id: number; label: string; slots: TargetSlot[]; } interface AttachmentTargetsResponse { customer: { id: number; name: string; type: string; slots: TargetSlot[]; }; identityDocuments: EntityWithSlots[]; bankCards: EntityWithSlots[]; contract?: { id: number; contractNumber: string; type: string; energyDetailsId?: number; slots: TargetSlot[]; }; } // Kunden-Dokumentziele (gefiltert nach Kundentyp) const customerTargets = getCustomerTargets(customerType); const customerSlots: TargetSlot[] = customerTargets.map(target => ({ key: target.key, label: target.label, field: target.field, hasDocument: !!(customer as any)[target.field], currentPath: (customer as any)[target.field] || undefined, })); // Ausweis-Dokumentziele const identityTargets = getIdentityDocumentTargets(); const identityDocuments: EntityWithSlots[] = customer.identityDocuments.map((doc: { id: number; type: DocumentType; documentNumber: string; documentPath: string | null }) => { const docTypeLabels: Record = { ID_CARD: 'Personalausweis', PASSPORT: 'Reisepass', DRIVERS_LICENSE: 'Führerschein', OTHER: 'Sonstiges', }; return { id: doc.id, label: `${docTypeLabels[doc.type] || doc.type}: ${doc.documentNumber}`, slots: identityTargets.map(target => ({ key: target.key, label: target.label, field: target.field, hasDocument: !!(doc as Record)[target.field], currentPath: ((doc as Record)[target.field] as string | undefined) || undefined, })), }; }); // Bankkarten-Dokumentziele const bankCardTargets = getBankCardTargets(); const bankCards: EntityWithSlots[] = customer.bankCards.map((card: { id: number; iban: string; bankName: string | null; documentPath: string | null }) => ({ id: card.id, label: `${card.bankName || 'Bank'}: ${card.iban.slice(-4)}`, slots: bankCardTargets.map(target => ({ key: target.key, label: target.label, field: target.field, hasDocument: !!(card as Record)[target.field], currentPath: ((card as Record)[target.field] as string | undefined) || undefined, })), })); // Basis-Antwort const response: AttachmentTargetsResponse = { customer: { id: customer.id, name: customer.companyName || `${customer.firstName} ${customer.lastName}`, type: customerType, slots: customerSlots, }, identityDocuments, bankCards, }; // Vertrag hinzufügen, falls E-Mail einem Vertrag zugeordnet ist if (email.contractId) { const contract = await prisma.contract.findUnique({ where: { id: email.contractId }, select: { id: true, contractNumber: true, type: true, cancellationLetterPath: true, cancellationConfirmationPath: true, cancellationLetterOptionsPath: true, cancellationConfirmationOptionsPath: true, energyDetails: { select: { id: true }, }, }, }); if (contract) { const contractTargets = getContractTargets(); response.contract = { id: contract.id, contractNumber: contract.contractNumber, type: contract.type, energyDetailsId: contract.energyDetails?.id, slots: contractTargets.map(target => ({ key: target.key, label: target.label, field: target.field, hasDocument: !!(contract as any)[target.field], currentPath: (contract as any)[target.field] || undefined, })), }; } } res.json({ success: true, data: response } as ApiResponse); } catch (error) { console.error('getAttachmentTargets error:', error); res.status(500).json({ success: false, error: 'Fehler beim Laden der Dokumenten-Ziele', } as ApiResponse); } } // E-Mail-Anhang in ein Dokumentenfeld speichern export async function saveAttachmentTo(req: Request, res: Response): Promise { try { const emailId = parseInt(req.params.id); const filename = decodeURIComponent(req.params.filename); const { entityType, entityId, targetKey } = req.body; console.log('[saveAttachmentTo] Request:', { emailId, filename, entityType, entityId, targetKey }); // Validierung if (!entityType || !targetKey) { res.status(400).json({ success: false, error: 'entityType und targetKey sind erforderlich', } as ApiResponse); return; } // E-Mail aus Cache laden const email = await cachedEmailService.getCachedEmailById(emailId); if (!email) { res.status(404).json({ success: false, error: 'E-Mail nicht gefunden', } as ApiResponse); return; } // Für gesendete E-Mails: Prüfen ob UID vorhanden (im IMAP Sent gespeichert) if (email.folder === 'SENT' && email.uid === 0) { res.status(400).json({ success: false, error: 'Anhang nicht verfügbar - E-Mail wurde vor der IMAP-Speicherung gesendet', } as ApiResponse); return; } // StressfreiEmail laden um Zugangsdaten zu bekommen const stressfreiEmail = await stressfreiEmailService.getEmailWithMailboxById(email.stressfreiEmailId); if (!stressfreiEmail || !stressfreiEmail.emailPasswordEncrypted) { res.status(400).json({ success: false, error: 'Keine Mailbox-Zugangsdaten verfügbar', } as ApiResponse); return; } // IMAP-Einstellungen laden const settings = await getImapSmtpSettings(); if (!settings) { res.status(400).json({ success: false, error: 'Keine E-Mail-Provider-Einstellungen gefunden', } as ApiResponse); return; } // Passwort entschlüsseln const password = decrypt(stressfreiEmail.emailPasswordEncrypted); // IMAP-Credentials const credentials: ImapCredentials = { host: settings.imapServer, port: settings.imapPort, user: stressfreiEmail.email, password, encryption: settings.imapEncryption, allowSelfSignedCerts: settings.allowSelfSignedCerts, }; // Ordner basierend auf E-Mail-Typ bestimmen (INBOX oder Sent) const imapFolder = email.folder === 'SENT' ? 'Sent' : 'INBOX'; // Anhang per IMAP abrufen const attachment = await fetchAttachment(credentials, email.uid, filename, imapFolder); if (!attachment) { res.status(404).json({ success: false, error: 'Anhang nicht gefunden', } as ApiResponse); return; } // Ziel-Konfiguration finden let targetConfig; let targetDir: string; let targetField: string; console.log('[saveAttachmentTo] Looking for target config:', { entityType, targetKey }); if (entityType === 'customer') { targetConfig = documentTargets.customer.find(t => t.key === targetKey); } else if (entityType === 'identityDocument') { targetConfig = documentTargets.identityDocument.find(t => t.key === targetKey); } else if (entityType === 'bankCard') { targetConfig = documentTargets.bankCard.find(t => t.key === targetKey); } else if (entityType === 'contract') { targetConfig = documentTargets.contract.find(t => t.key === targetKey); } console.log('[saveAttachmentTo] Found targetConfig:', targetConfig); if (!targetConfig) { res.status(400).json({ success: false, error: `Unbekanntes Dokumentziel: ${entityType}/${targetKey}`, } as ApiResponse); return; } targetDir = targetConfig.directory; targetField = targetConfig.field; // Uploads-Verzeichnis erstellen const uploadsDir = path.join(process.cwd(), 'uploads', targetDir); if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir, { recursive: true }); } // Eindeutigen Dateinamen generieren const ext = path.extname(filename) || '.pdf'; const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); const newFilename = `${uniqueSuffix}${ext}`; const filePath = path.join(uploadsDir, newFilename); const relativePath = `/uploads/${targetDir}/${newFilename}`; // Datei speichern fs.writeFileSync(filePath, attachment.content); // Alte Datei löschen und DB aktualisieren if (entityType === 'customer') { // Customer ID aus StressfreiEmail laden console.log('[saveAttachmentTo] Looking up customer for stressfreiEmail.id:', stressfreiEmail.id); const customer = await prisma.customer.findFirst({ where: { stressfreiEmails: { some: { id: stressfreiEmail.id } }, }, }); console.log('[saveAttachmentTo] Found customer:', customer?.id); if (!customer) { fs.unlinkSync(filePath); res.status(404).json({ success: false, error: 'Kunde nicht gefunden', } as ApiResponse); return; } // Alte Datei löschen const oldPath = (customer as any)[targetField]; if (oldPath) { const oldFullPath = path.join(process.cwd(), oldPath); if (fs.existsSync(oldFullPath)) { fs.unlinkSync(oldFullPath); } } await prisma.customer.update({ where: { id: customer.id }, data: { [targetField]: relativePath }, }); } else if (entityType === 'identityDocument') { if (!entityId) { fs.unlinkSync(filePath); res.status(400).json({ success: false, error: 'entityId ist für identityDocument erforderlich', } as ApiResponse); return; } const doc = await prisma.identityDocument.findUnique({ where: { id: entityId } }); if (!doc) { fs.unlinkSync(filePath); res.status(404).json({ success: false, error: 'Ausweis nicht gefunden', } as ApiResponse); return; } // Alte Datei löschen const oldPath = (doc as any)[targetField]; if (oldPath) { const oldFullPath = path.join(process.cwd(), oldPath); if (fs.existsSync(oldFullPath)) { fs.unlinkSync(oldFullPath); } } await prisma.identityDocument.update({ where: { id: entityId }, data: { [targetField]: relativePath }, }); } else if (entityType === 'bankCard') { if (!entityId) { fs.unlinkSync(filePath); res.status(400).json({ success: false, error: 'entityId ist für bankCard erforderlich', } as ApiResponse); return; } const card = await prisma.bankCard.findUnique({ where: { id: entityId } }); if (!card) { fs.unlinkSync(filePath); res.status(404).json({ success: false, error: 'Bankkarte nicht gefunden', } as ApiResponse); return; } // Alte Datei löschen const oldPath = (card as any)[targetField]; if (oldPath) { const oldFullPath = path.join(process.cwd(), oldPath); if (fs.existsSync(oldFullPath)) { fs.unlinkSync(oldFullPath); } } await prisma.bankCard.update({ where: { id: entityId }, data: { [targetField]: relativePath }, }); } else if (entityType === 'contract') { // Contract-ID kommt aus der E-Mail-Zuordnung if (!email.contractId) { fs.unlinkSync(filePath); res.status(400).json({ success: false, error: 'E-Mail ist keinem Vertrag zugeordnet', } as ApiResponse); return; } const contract = await prisma.contract.findUnique({ where: { id: email.contractId } }); if (!contract) { fs.unlinkSync(filePath); res.status(404).json({ success: false, error: 'Vertrag nicht gefunden', } as ApiResponse); return; } // Alte Datei löschen const oldPath = (contract as any)[targetField]; if (oldPath) { const oldFullPath = path.join(process.cwd(), oldPath); if (fs.existsSync(oldFullPath)) { fs.unlinkSync(oldFullPath); } } await prisma.contract.update({ where: { id: email.contractId }, data: { [targetField]: relativePath }, }); } res.json({ success: true, data: { path: relativePath, filename: newFilename, originalName: filename, size: attachment.size, }, } as ApiResponse); } catch (error) { console.error('saveAttachmentTo error:', error); // Detailliertere Fehlermeldung für Debugging const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler'; res.status(500).json({ success: false, error: `Fehler beim Speichern des Anhangs: ${errorMessage}`, } as ApiResponse); } } // ==================== SAVE EMAIL AS PDF ==================== // E-Mail als PDF exportieren und in Dokumentenfeld speichern export async function saveEmailAsPdf(req: Request, res: Response): Promise { try { const emailId = parseInt(req.params.id); const { entityType, entityId, targetKey } = req.body; console.log('[saveEmailAsPdf] Request:', { emailId, entityType, entityId, targetKey }); // Validierung if (!entityType || !targetKey) { res.status(400).json({ success: false, error: 'entityType und targetKey sind erforderlich', } as ApiResponse); return; } // E-Mail aus Cache laden (mit Body) const email = await cachedEmailService.getCachedEmailById(emailId); if (!email) { res.status(404).json({ success: false, error: 'E-Mail nicht gefunden', } as ApiResponse); return; } // StressfreiEmail laden um an den Kunden zu kommen const stressfreiEmail = await prisma.stressfreiEmail.findUnique({ where: { id: email.stressfreiEmailId }, include: { customer: true }, }); if (!stressfreiEmail) { res.status(404).json({ success: false, error: 'E-Mail-Konto nicht gefunden', } as ApiResponse); return; } // Empfänger-Adressen parsen (JSON Array) let toAddresses: string[] = []; let ccAddresses: string[] = []; try { toAddresses = JSON.parse(email.toAddresses); } catch { toAddresses = [email.toAddresses]; } try { if (email.ccAddresses) ccAddresses = JSON.parse(email.ccAddresses); } catch { /* ignore */ } // PDF generieren const pdfBuffer = await generateEmailPdf({ from: email.fromAddress, to: toAddresses.join(', '), cc: ccAddresses.length > 0 ? ccAddresses.join(', ') : undefined, subject: email.subject || '(Kein Betreff)', date: email.receivedAt, bodyText: email.textBody || undefined, bodyHtml: email.htmlBody || undefined, }); // Ziel-Konfiguration finden let targetConfig; let targetDir: string; let targetField: string; console.log('[saveEmailAsPdf] Looking for target config:', { entityType, targetKey }); if (entityType === 'customer') { targetConfig = documentTargets.customer.find(t => t.key === targetKey); } else if (entityType === 'identityDocument') { targetConfig = documentTargets.identityDocument.find(t => t.key === targetKey); } else if (entityType === 'bankCard') { targetConfig = documentTargets.bankCard.find(t => t.key === targetKey); } else if (entityType === 'contract') { targetConfig = documentTargets.contract.find(t => t.key === targetKey); } console.log('[saveEmailAsPdf] Found targetConfig:', targetConfig); if (!targetConfig) { res.status(400).json({ success: false, error: `Unbekanntes Dokumentziel: ${entityType}/${targetKey}`, } as ApiResponse); return; } targetDir = targetConfig.directory; targetField = targetConfig.field; // Uploads-Verzeichnis erstellen const uploadsDir = path.join(process.cwd(), 'uploads', targetDir); if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir, { recursive: true }); } // Eindeutigen Dateinamen generieren const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); const newFilename = `email-${uniqueSuffix}.pdf`; const filePath = path.join(uploadsDir, newFilename); const relativePath = `/uploads/${targetDir}/${newFilename}`; // PDF speichern fs.writeFileSync(filePath, pdfBuffer); // Alte Datei löschen und DB aktualisieren if (entityType === 'customer') { const customer = stressfreiEmail.customer; // Alte Datei löschen const oldPath = (customer as any)[targetField]; if (oldPath) { const oldFullPath = path.join(process.cwd(), oldPath); if (fs.existsSync(oldFullPath)) { fs.unlinkSync(oldFullPath); } } await prisma.customer.update({ where: { id: customer.id }, data: { [targetField]: relativePath }, }); } else if (entityType === 'identityDocument') { if (!entityId) { fs.unlinkSync(filePath); res.status(400).json({ success: false, error: 'entityId ist für identityDocument erforderlich', } as ApiResponse); return; } const doc = await prisma.identityDocument.findUnique({ where: { id: entityId } }); if (!doc) { fs.unlinkSync(filePath); res.status(404).json({ success: false, error: 'Ausweis nicht gefunden', } as ApiResponse); return; } // Alte Datei löschen const oldPath = (doc as any)[targetField]; if (oldPath) { const oldFullPath = path.join(process.cwd(), oldPath); if (fs.existsSync(oldFullPath)) { fs.unlinkSync(oldFullPath); } } await prisma.identityDocument.update({ where: { id: entityId }, data: { [targetField]: relativePath }, }); } else if (entityType === 'bankCard') { if (!entityId) { fs.unlinkSync(filePath); res.status(400).json({ success: false, error: 'entityId ist für bankCard erforderlich', } as ApiResponse); return; } const card = await prisma.bankCard.findUnique({ where: { id: entityId } }); if (!card) { fs.unlinkSync(filePath); res.status(404).json({ success: false, error: 'Bankkarte nicht gefunden', } as ApiResponse); return; } // Alte Datei löschen const oldPath = (card as any)[targetField]; if (oldPath) { const oldFullPath = path.join(process.cwd(), oldPath); if (fs.existsSync(oldFullPath)) { fs.unlinkSync(oldFullPath); } } await prisma.bankCard.update({ where: { id: entityId }, data: { [targetField]: relativePath }, }); } else if (entityType === 'contract') { // Contract-ID kommt aus der E-Mail-Zuordnung oder direkt const contractId = email.contractId; if (!contractId) { fs.unlinkSync(filePath); res.status(400).json({ success: false, error: 'E-Mail ist keinem Vertrag zugeordnet', } as ApiResponse); return; } const contract = await prisma.contract.findUnique({ where: { id: contractId } }); if (!contract) { fs.unlinkSync(filePath); res.status(404).json({ success: false, error: 'Vertrag nicht gefunden', } as ApiResponse); return; } // Alte Datei löschen const oldPath = (contract as any)[targetField]; if (oldPath) { const oldFullPath = path.join(process.cwd(), oldPath); if (fs.existsSync(oldFullPath)) { fs.unlinkSync(oldFullPath); } } await prisma.contract.update({ where: { id: contractId }, data: { [targetField]: relativePath }, }); } res.json({ success: true, data: { path: relativePath, filename: newFilename, size: pdfBuffer.length, }, } as ApiResponse); } catch (error) { console.error('saveEmailAsPdf error:', error); const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler'; res.status(500).json({ success: false, error: `Fehler beim Erstellen der PDF: ${errorMessage}`, } as ApiResponse); } } // ==================== SAVE EMAIL AS INVOICE ==================== // E-Mail als PDF exportieren und als Rechnung speichern export async function saveEmailAsInvoice(req: Request, res: Response): Promise { try { const emailId = parseInt(req.params.id); const { invoiceDate, invoiceType, notes } = req.body; console.log('[saveEmailAsInvoice] Request:', { emailId, invoiceDate, invoiceType, notes }); // Validierung if (!invoiceDate || !invoiceType) { res.status(400).json({ success: false, error: 'invoiceDate und invoiceType sind erforderlich', } as ApiResponse); return; } // Validiere invoiceType if (!['INTERIM', 'FINAL', 'NOT_AVAILABLE'].includes(invoiceType)) { res.status(400).json({ success: false, error: 'Ungültiger Rechnungstyp', } as ApiResponse); return; } // E-Mail aus Cache laden const email = await cachedEmailService.getCachedEmailById(emailId); if (!email) { res.status(404).json({ success: false, error: 'E-Mail nicht gefunden', } as ApiResponse); return; } // Prüfen ob E-Mail einem Vertrag zugeordnet ist if (!email.contractId) { res.status(400).json({ success: false, error: 'E-Mail ist keinem Vertrag zugeordnet', } as ApiResponse); return; } // Vertrag laden und prüfen ob es ein Energievertrag ist const contract = await prisma.contract.findUnique({ where: { id: email.contractId }, include: { energyDetails: true }, }); if (!contract) { res.status(404).json({ success: false, error: 'Vertrag nicht gefunden', } as ApiResponse); return; } if (!['ELECTRICITY', 'GAS'].includes(contract.type)) { res.status(400).json({ success: false, error: 'Nur für Strom- und Gas-Verträge verfügbar', } as ApiResponse); return; } if (!contract.energyDetails) { res.status(400).json({ success: false, error: 'Keine Energie-Details für diesen Vertrag vorhanden', } as ApiResponse); return; } // Empfänger-Adressen parsen (JSON Array) let toAddresses: string[] = []; let ccAddresses: string[] = []; try { toAddresses = JSON.parse(email.toAddresses); } catch { toAddresses = [email.toAddresses]; } try { if (email.ccAddresses) ccAddresses = JSON.parse(email.ccAddresses); } catch { /* ignore */ } // PDF generieren const pdfBuffer = await generateEmailPdf({ from: email.fromAddress, to: toAddresses.join(', '), cc: ccAddresses.length > 0 ? ccAddresses.join(', ') : undefined, subject: email.subject || '(Kein Betreff)', date: email.receivedAt, bodyText: email.textBody || undefined, bodyHtml: email.htmlBody || undefined, }); // Uploads-Verzeichnis erstellen const uploadsDir = path.join(process.cwd(), 'uploads', 'invoices'); if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir, { recursive: true }); } // Eindeutigen Dateinamen generieren const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); const newFilename = `invoice-email-${uniqueSuffix}.pdf`; const filePath = path.join(uploadsDir, newFilename); const relativePath = `/uploads/invoices/${newFilename}`; // PDF speichern fs.writeFileSync(filePath, pdfBuffer); // Invoice in DB erstellen const invoice = await invoiceService.addInvoice(contract.energyDetails.id, { invoiceDate: new Date(invoiceDate), invoiceType, documentPath: relativePath, notes: notes || undefined, }); res.json({ success: true, data: invoice, } as ApiResponse); } catch (error) { console.error('saveEmailAsInvoice error:', error); const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler'; res.status(500).json({ success: false, error: `Fehler beim Erstellen der Rechnung: ${errorMessage}`, } as ApiResponse); } } // ==================== SAVE ATTACHMENT AS INVOICE ==================== // E-Mail-Anhang als Rechnung speichern export async function saveAttachmentAsInvoice(req: Request, res: Response): Promise { try { const emailId = parseInt(req.params.id); const filename = decodeURIComponent(req.params.filename); const { invoiceDate, invoiceType, notes } = req.body; console.log('[saveAttachmentAsInvoice] Request:', { emailId, filename, invoiceDate, invoiceType, notes }); // Validierung if (!invoiceDate || !invoiceType) { res.status(400).json({ success: false, error: 'invoiceDate und invoiceType sind erforderlich', } as ApiResponse); return; } // Validiere invoiceType if (!['INTERIM', 'FINAL', 'NOT_AVAILABLE'].includes(invoiceType)) { res.status(400).json({ success: false, error: 'Ungültiger Rechnungstyp', } as ApiResponse); return; } // E-Mail aus Cache laden const email = await cachedEmailService.getCachedEmailById(emailId); if (!email) { res.status(404).json({ success: false, error: 'E-Mail nicht gefunden', } as ApiResponse); return; } // Prüfen ob E-Mail einem Vertrag zugeordnet ist if (!email.contractId) { res.status(400).json({ success: false, error: 'E-Mail ist keinem Vertrag zugeordnet', } as ApiResponse); return; } // Vertrag laden und prüfen ob es ein Energievertrag ist const contract = await prisma.contract.findUnique({ where: { id: email.contractId }, include: { energyDetails: true }, }); if (!contract) { res.status(404).json({ success: false, error: 'Vertrag nicht gefunden', } as ApiResponse); return; } if (!['ELECTRICITY', 'GAS'].includes(contract.type)) { res.status(400).json({ success: false, error: 'Nur für Strom- und Gas-Verträge verfügbar', } as ApiResponse); return; } if (!contract.energyDetails) { res.status(400).json({ success: false, error: 'Keine Energie-Details für diesen Vertrag vorhanden', } as ApiResponse); return; } // Für gesendete E-Mails: Prüfen ob UID vorhanden if (email.folder === 'SENT' && email.uid === 0) { res.status(400).json({ success: false, error: 'Anhang nicht verfügbar - E-Mail wurde vor der IMAP-Speicherung gesendet', } as ApiResponse); return; } // StressfreiEmail laden um Zugangsdaten zu bekommen const stressfreiEmail = await stressfreiEmailService.getEmailWithMailboxById(email.stressfreiEmailId); if (!stressfreiEmail || !stressfreiEmail.emailPasswordEncrypted) { res.status(400).json({ success: false, error: 'Keine Mailbox-Zugangsdaten verfügbar', } as ApiResponse); return; } // IMAP-Einstellungen laden const settings = await getImapSmtpSettings(); if (!settings) { res.status(400).json({ success: false, error: 'Keine E-Mail-Provider-Einstellungen gefunden', } as ApiResponse); return; } // Passwort entschlüsseln const password = decrypt(stressfreiEmail.emailPasswordEncrypted); // IMAP-Credentials zusammenstellen const credentials: ImapCredentials = { host: settings.imapServer, port: settings.imapPort, user: stressfreiEmail.email, password, encryption: settings.imapEncryption, allowSelfSignedCerts: settings.allowSelfSignedCerts, }; // IMAP-Ordner bestimmen const imapFolder = email.folder === 'SENT' ? 'Sent' : 'INBOX'; // Anhang vom IMAP-Server laden const attachment = await fetchAttachment(credentials, email.uid, filename, imapFolder); if (!attachment) { res.status(404).json({ success: false, error: 'Anhang nicht gefunden oder nicht mehr verfügbar', } as ApiResponse); return; } // Uploads-Verzeichnis erstellen const uploadsDir = path.join(process.cwd(), 'uploads', 'invoices'); if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir, { recursive: true }); } // Dateiendung extrahieren const ext = path.extname(filename) || '.pdf'; const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); const newFilename = `invoice-attachment-${uniqueSuffix}${ext}`; const filePath = path.join(uploadsDir, newFilename); const relativePath = `/uploads/invoices/${newFilename}`; // Datei speichern fs.writeFileSync(filePath, attachment.content); // Invoice in DB erstellen const invoice = await invoiceService.addInvoice(contract.energyDetails.id, { invoiceDate: new Date(invoiceDate), invoiceType, documentPath: relativePath, notes: notes || undefined, }); res.json({ success: true, data: invoice, } as ApiResponse); } catch (error) { console.error('saveAttachmentAsInvoice error:', error); const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler'; res.status(500).json({ success: false, error: `Fehler beim Erstellen der Rechnung: ${errorMessage}`, } as ApiResponse); } }