save attachment from email in customer data and - or contracts
This commit is contained in:
@@ -8,6 +8,12 @@ import { fetchAttachment, appendToSent, ImapCredentials } from '../services/imap
|
||||
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 { PrismaClient, DocumentType } from '@prisma/client';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// ==================== E-MAIL LIST ====================
|
||||
|
||||
@@ -803,3 +809,464 @@ export async function permanentDeleteEmail(req: Request, res: Response): Promise
|
||||
} 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;
|
||||
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,
|
||||
cancellationLetterPath: true,
|
||||
cancellationConfirmationPath: true,
|
||||
cancellationLetterOptionsPath: true,
|
||||
cancellationConfirmationOptionsPath: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (contract) {
|
||||
const contractTargets = getContractTargets();
|
||||
response.contract = {
|
||||
id: contract.id,
|
||||
contractNumber: contract.contractNumber,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user