Files
2026-05-20 18:58:23 +00:00

141 lines
4.9 KiB
TypeScript

// /opt/erp-system/app/api/cron/imap/route.ts
import { NextResponse } from 'next/server';
import prisma from '../../../../lib/prisma';
import { ImapFlow } from 'imapflow';
import { simpleParser } from 'mailparser';
import { writeFile } from 'fs/promises';
import { join } from 'path';
export const dynamic = 'force-dynamic';
const UPLOAD_DIR = join(process.cwd(), 'uploads');
export async function GET() {
try {
const settings = await prisma.systemSettings.findFirst({ where: { id: 1 } });
if (!settings || !settings.imapHost || !settings.imapUser || !settings.imapPass) {
return NextResponse.json({ error: 'IMAP Zugangsdaten fehlen.' }, { status: 400 });
}
const client = new ImapFlow({
host: settings.imapHost,
port: settings.imapPort,
secure: settings.imapPort === 993,
auth: { user: settings.imapUser, pass: settings.imapPass },
logger: false
});
await client.connect();
let lock = await client.getMailboxLock('INBOX');
let processedCounter = 0;
try {
for await (let message of client.fetch({ seen: false }, { source: true, uid: true })) {
const parsed = await simpleParser(message.source);
const fromEmail = parsed.from?.value[0]?.address;
const fromName = parsed.from?.value[0]?.name || 'Unbekannt';
const subject = parsed.subject || 'Kein Betreff';
const textContent = parsed.text || parsed.textAsHtml || '(Kein Inhalt)';
if (!fromEmail) continue;
// 1. Kunden-Matching (Haupt-Email, alternative E-Mails oder Mitarbeiter)
let targetCustomerId = null;
const directCustomer = await prisma.customer.findFirst({
where: {
OR: [
{ email: fromEmail },
{ additionalEmails: { has: fromEmail } }
]
}
});
if (directCustomer) {
targetCustomerId = directCustomer.id;
} else {
const contact = await prisma.customerContact.findFirst({
where: { email: fromEmail }
});
if (contact) {
targetCustomerId = contact.customerId;
}
}
// Falls komplett unbekannt, neuen Kunden anlegen
if (!targetCustomerId) {
const newCustomer = await prisma.customer.create({
data: {
email: fromEmail,
firstName: fromName,
lastName: '(Auto-Erfasst)',
companyName: 'Aus E-Mail Anfrage'
}
});
targetCustomerId = newCustomer.id;
}
// 2. Ticket Zuordnung
const ticketMatch = subject.match(/Ticket #(\d+)/i);
let ticketId = ticketMatch ? parseInt(ticketMatch[1]) : null;
let activeTicketId = null;
if (ticketId) {
const existingTicket = await prisma.ticket.findUnique({ where: { id: ticketId } });
if (existingTicket) {
activeTicketId = existingTicket.id;
await prisma.ticketMessage.create({
data: { content: textContent, isFromCustomer: true, ticketId: existingTicket.id }
});
if (existingTicket.status !== 'OPEN' && existingTicket.status !== 'IN_PROGRESS') {
await prisma.ticket.update({ where: { id: existingTicket.id }, data: { status: 'OPEN' } });
}
}
}
if (!activeTicketId) {
const newTicket = await prisma.ticket.create({
data: { title: subject, description: textContent, customerId: targetCustomerId, status: 'OPEN' }
});
activeTicketId = newTicket.id;
}
// 3. Dateianhänge verarbeiten
if (parsed.attachments && parsed.attachments.length > 0) {
for (const att of parsed.attachments) {
// Buffer in Datei schreiben
const safeOriginalName = (att.filename || 'unbekannt.dat').replace(/[^a-zA-Z0-9.-]/g, '_');
const savedName = `${Date.now()}-${safeOriginalName}`;
const filepath = join(UPLOAD_DIR, savedName);
await writeFile(filepath, att.content);
// DB Eintrag
await prisma.attachment.create({
data: {
fileName: att.filename || 'unbekannt.dat',
savedName: savedName,
fileSize: att.size || 0,
fileType: att.contentType || 'application/octet-stream',
ticketId: activeTicketId
}
});
}
}
await client.messageFlagsAdd(message.uid, ['\\Seen'], { uid: true });
processedCounter++;
}
} finally {
lock.release();
}
await client.logout();
return NextResponse.json({ success: true, processedTickets: processedCounter });
} catch (error: any) {
console.error('IMAP Error:', error);
return NextResponse.json({ error: 'Verarbeitungsfehler', details: error.message }, { status: 500 });
}
}