import Imap from 'imap'; import { simpleParser } from 'mailparser'; import { prisma } from './prisma'; import { emailParser } from './email-parser'; import { bookingAutomationService } from './booking-automation'; export interface ImapConfig { host: string; port: number; user: string; password: string; secure: boolean; } export class EmailSyncService { async syncLocationEmails(locationId: string): Promise<{ success: boolean; newEmails: number; newBookings: number; errors: string[]; }> { console.log('=== Starting email sync for location:', locationId); const location = await prisma.location.findUnique({ where: { id: locationId }, }); console.log('Location found:', location?.name, 'emailSyncEnabled:', location?.emailSyncEnabled); if (!location || !location.emailSyncEnabled) { return { success: false, newEmails: 0, newBookings: 0, errors: ['Location not found or sync disabled'] }; } if (!location.imapHost || !location.imapUser || !location.imapPassword) { console.log('IMAP config incomplete:', { host: !!location.imapHost, user: !!location.imapUser, password: !!location.imapPassword, }); return { success: false, newEmails: 0, newBookings: 0, errors: ['IMAP configuration incomplete'] }; } const config: ImapConfig = { host: location.imapHost, port: location.imapPort || 993, user: location.imapUser, password: location.imapPassword, secure: location.imapSecure ?? true, }; console.log('IMAP config:', { host: config.host, port: config.port, user: config.user, secure: config.secure }); return this.fetchNewEmails(config, location.slug, locationId); } private async fetchNewEmails( config: ImapConfig, locationSlug: string, locationId: string ): Promise<{ success: boolean; newEmails: number; newBookings: number; errors: string[]; }> { console.log('=== fetchNewEmails called for', locationSlug); return new Promise((resolve) => { const imap = new Imap({ user: config.user, password: config.password, host: config.host, port: config.port, tls: config.secure, tlsOptions: { rejectUnauthorized: false }, }); console.log('IMAP instance created'); let newEmails = 0; let newBookings = 0; const errors: string[] = []; imap.once('ready', () => { console.log('IMAP connection ready!'); imap.openBox('INBOX', false, (err, box) => { if (err) { errors.push(`Failed to open inbox: ${err.message}`); imap.end(); return; } // Fetch all emails from last 30 days (including seen/read ones for testing) const thirtyDaysAgo = new Date(); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); imap.search([['SINCE', thirtyDaysAgo]], async (err, results) => { if (err) { console.error('IMAP search error:', err); errors.push(`Search failed: ${err.message}`); imap.end(); return; } console.log('IMAP search results:', results?.length || 0, 'emails found'); if (!results || results.length === 0) { console.log('No emails found in search'); imap.end(); resolve({ success: true, newEmails: 0, newBookings: 0, errors }); return; } console.log('Fetching', results.length, 'emails...'); const fetch = imap.fetch(results, { bodies: '' }); fetch.on('message', (msg: any, seqno: number) => { console.log('Processing email', seqno); msg.on('body', (stream: any, info: any) => { simpleParser(stream, async (err: any, parsed: any) => { if (err) { console.error('Parse error for email', seqno, ':', err); errors.push(`Failed to parse email ${seqno}: ${err.message}`); return; } console.log('Parsed email:', { subject: parsed.subject, from: parsed.from?.text, messageId: parsed.messageId, }); try { // Save email to database const existingEmail = await prisma.email.findUnique({ where: { messageId: parsed.messageId }, }); if (existingEmail) { console.log('Email already exists, skipping:', parsed.messageId); return; // Already processed } console.log('Saving new email to database...'); const emailRecord = await prisma.email.create({ data: { locationSlug, from: parsed.from?.text || '', to: parsed.to?.text || '', subject: parsed.subject || '', textBody: parsed.text || '', htmlBody: parsed.html || '', messageId: parsed.messageId || undefined, direction: 'INBOUND', receivedAt: parsed.date || new Date(), }, }); console.log('✅ Email saved with ID:', emailRecord.id); newEmails++; // Try to parse as Ninjaforms booking console.log('Attempting to parse as Ninjaforms email...'); console.log('Email subject:', parsed.subject); console.log('Text body length:', parsed.text?.length || 0); console.log('HTML body length:', parsed.html?.length || 0); console.log('First 500 chars of content:', (parsed.html || parsed.text || '').substring(0, 500)); const parsedData = emailParser.parseFromNinjaFormsEmail( parsed.html || parsed.text || '', parsed.subject || '' ); console.log('Parse result:', parsedData ? '✅ Parsed successfully' : '❌ Not a Ninjaforms email'); if (parsedData) { console.log('Parsed data:', JSON.stringify(parsedData, null, 2)); } if (parsedData) { console.log('Creating booking from parsed data:', parsedData); // Create booking from parsed data const booking = await this.createBookingFromParsedData(parsedData, locationSlug); if (booking) { console.log('✅ Booking created:', booking.id); newBookings++; // Update email with booking ID await prisma.email.update({ where: { id: emailRecord.id }, data: { parsed: true, parsedData: parsedData as any, bookingId: booking.id, }, }); // Create notification await prisma.notification.create({ data: { type: 'NEW_BOOKING_EMAIL', title: 'Neue Buchung per E-Mail', message: `${parsedData.firstName || parsedData.companyName} - ${parsedData.eventDate}`, metadata: { bookingId: booking.id, emailId: emailRecord.id, }, }, }); console.log('✅ Notification created'); } else { console.log('❌ Booking creation failed'); } } } catch (error: any) { errors.push(`Failed to process email ${seqno}: ${error.message}`); } }); }); }); fetch.once('end', async () => { // Update last sync time await prisma.location.update({ where: { id: locationId }, data: { lastEmailSync: new Date() }, }); imap.end(); resolve({ success: true, newEmails, newBookings, errors }); }); }); }); }); imap.once('error', (err: any) => { errors.push(`IMAP connection error: ${err.message}`); resolve({ success: false, newEmails, newBookings, errors }); }); imap.connect(); }); } private parseDateString(dateString: string): Date | null { if (!dateString) return null; // Try German format: DD.MM.YYYY or D.M.YYYY const germanMatch = dateString.match(/(\d{1,2})\.(\d{1,2})\.(\d{4})/); if (germanMatch) { const [, day, month, year] = germanMatch; return new Date(parseInt(year), parseInt(month) - 1, parseInt(day)); } // Try ISO format or other standard formats const date = new Date(dateString); return isNaN(date.getTime()) ? null : date; } private async createBookingFromParsedData(data: any, locationSlug: string) { try { const location = await prisma.location.findUnique({ where: { slug: locationSlug }, }); if (!location) return null; // Parse German date format const eventDate = this.parseDateString(data.eventDate); if (!eventDate) { console.error('Invalid event date:', data.eventDate); return null; } const startOfDay = new Date(eventDate); startOfDay.setHours(0, 0, 0, 0); const endOfDay = new Date(eventDate); endOfDay.setHours(23, 59, 59, 999); const availablePhotobox = await prisma.photobox.findFirst({ where: { locationId: location.id, model: data.model, active: true, NOT: { bookings: { some: { eventDate: { gte: startOfDay, lte: endOfDay, }, status: { in: ['RESERVED', 'CONFIRMED'], }, }, }, }, }, }); if (!availablePhotobox) { console.warn('No photobox available for parsed booking'); return null; } const bookingNumber = this.generateBookingNumber(); const booking = await prisma.booking.create({ data: { bookingNumber, locationId: location.id, photoboxId: availablePhotobox.id, status: 'RESERVED', customerName: data.companyName || `${data.firstName || ''} ${data.lastName || ''}`.trim(), customerEmail: data.email, customerPhone: data.phone, customerAddress: data.street, customerCity: data.city, customerZip: data.zip, invoiceType: data.companyName ? 'BUSINESS' : 'PRIVATE', companyName: data.companyName, eventDate: eventDate, eventAddress: data.eventLocation || '', eventCity: data.city || '', eventZip: data.zip || '', eventLocation: data.eventLocation, setupTimeStart: eventDate, setupTimeLatest: eventDate, calculatedPrice: data.calculatedPrice || 0, notes: data.message, }, }); // 🤖 Automatische Post-Booking Aktionen console.log('📢 Starte automatische Aktionen...'); bookingAutomationService.runPostBookingActions(booking.id).catch(err => { console.error('⚠️ Automatische Aktionen fehlgeschlagen:', err); }); return booking; } catch (error) { console.error('Failed to create booking from parsed data:', error); return null; } } private generateBookingNumber(): string { const date = new Date(); const year = date.getFullYear().toString().slice(-2); const month = String(date.getMonth() + 1).padStart(2, '0'); const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0'); return `STM-${year}${month}-${random}`; } } export const emailSyncService = new EmailSyncService();