1836 lines
56 KiB
TypeScript
1836 lines
56 KiB
TypeScript
// ==================== 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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<string, string> = {
|
|
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<string, unknown>)[target.field],
|
|
currentPath: ((doc as Record<string, unknown>)[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<string, unknown>)[target.field],
|
|
currentPath: ((card as Record<string, unknown>)[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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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);
|
|
}
|
|
}
|