import { requireAuth } from '~/server/utils/auth'; import { getCredentialsFromSession, decryptCredentials } from '~/server/utils/encryption'; import { getCachedEmails, syncEmailsWithRetry, getSyncMetadata } from '~/server/utils/email-sync'; interface EmailMessage { id: string; from: string; to: string | string[]; subject: string; body: string; html?: string; timestamp: string; direction: 'sent' | 'received'; threadId?: string; attachments?: any[]; } interface EmailThread { id: string; subject: string; emailCount: number; latestTimestamp: string; emails: EmailMessage[]; } export default defineEventHandler(async (event) => { // Check authentication (x-tag header OR Keycloak session) await requireAuth(event); try { const body = await readBody(event); const { clientEmail, interestId, sessionId } = body; if (!clientEmail || !sessionId || !interestId) { throw createError({ statusCode: 400, statusMessage: "Client email, interestId and sessionId are required" }); } // Get encrypted credentials from session const encryptedCredentials = getCredentialsFromSession(sessionId); if (!encryptedCredentials) { return { success: true, emails: [], threads: [] }; } // Decrypt credentials let userEmail: string; let password: string; try { const decrypted = decryptCredentials(encryptedCredentials); userEmail = decrypted.email; password = decrypted.password; } catch (decryptError) { console.error('[Email V2] Failed to decrypt credentials:', decryptError); return { success: true, emails: [], threads: [] }; } // First, get emails from MinIO cache (instant response) const cachedEmails = await getCachedEmails(interestId); // Get sync metadata const metadata = await getSyncMetadata(interestId); // Trigger background sync if not currently syncing and last sync was over 5 minutes ago const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000); const lastSync = new Date(metadata.lastSyncTime); if (metadata.syncStatus !== 'syncing' && lastSync < fiveMinutesAgo) { // Fire and forget - don't wait for sync to complete syncEmailsWithRetry(sessionId, userEmail, clientEmail, interestId).catch(err => { console.error('[Email V2] Background sync failed:', err); }); } // Process cached emails const emails: EmailMessage[] = cachedEmails.map(email => ({ id: email.id || email.messageId || `${Date.now()}-${Math.random()}`, from: email.from || '', to: email.to || '', subject: email.subject || '', body: email.body || email.text || '', html: email.html, timestamp: email.timestamp || new Date().toISOString(), direction: email.direction || (email.from?.toLowerCase().includes(userEmail.toLowerCase()) ? 'sent' : 'received'), threadId: email.threadId, attachments: email.attachments })); // Filter to only include emails involving the client const filteredEmails = emails.filter(email => { const fromEmail = email.from.toLowerCase(); const toEmails = Array.isArray(email.to) ? email.to.join(' ').toLowerCase() : email.to.toLowerCase(); return fromEmail.includes(clientEmail.toLowerCase()) || toEmails.includes(clientEmail.toLowerCase()) || fromEmail.includes(userEmail.toLowerCase()) || toEmails.includes(userEmail.toLowerCase()); }); // Sort by timestamp (newest first) filteredEmails.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() ); // Group into threads const threads = groupIntoThreads(filteredEmails); return { success: true, emails: filteredEmails, threads: threads, syncStatus: metadata.syncStatus, lastSync: metadata.lastSyncTime, totalEmails: metadata.totalEmails }; } catch (error) { console.error('[Email V2] Failed to fetch email thread:', error); if (error instanceof Error) { throw createError({ statusCode: 500, statusMessage: `Failed to fetch emails: ${error.message}` }); } else { throw createError({ statusCode: 500, statusMessage: "An unexpected error occurred", }); } } }); // Group emails into threads based on subject and references function groupIntoThreads(emails: EmailMessage[]): EmailThread[] { const threads = new Map(); const emailById = new Map(); // First pass: index all emails by ID emails.forEach(email => { emailById.set(email.id, email); }); // Second pass: group emails into threads emails.forEach(email => { // Normalize subject by removing Re:, Fwd:, etc. const normalizedSubject = email.subject .replace(/^(Re:|Fwd:|Fw:|RE:|FW:|FWD:)\s*/gi, '') .replace(/\s+/g, ' ') .trim() .toLowerCase(); // Check if this email belongs to an existing thread let threadFound = false; // First, check if it has a threadId (in-reply-to header) if (email.threadId) { // Look for the parent email const parentEmail = emailById.get(email.threadId); if (parentEmail) { // Find which thread the parent belongs to for (const [threadId, threadEmails] of threads.entries()) { if (threadEmails.some(e => e.id === parentEmail.id)) { threadEmails.push(email); threadFound = true; break; } } } } // If not found by threadId, try to match by subject if (!threadFound) { for (const [threadId, threadEmails] of threads.entries()) { const threadSubject = threadEmails[0].subject .replace(/^(Re:|Fwd:|Fw:|RE:|FW:|FWD:)\s*/gi, '') .replace(/\s+/g, ' ') .trim() .toLowerCase(); if (threadSubject === normalizedSubject) { threadEmails.push(email); threadFound = true; break; } } } // If still not found, create a new thread if (!threadFound) { threads.set(email.id, [email]); } }); // Convert to array format and sort emails within each thread return Array.from(threads.entries()) .map(([threadId, threadEmails]) => { // Sort emails within thread by timestamp (oldest first for chronological order) threadEmails.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() ); return { id: threadId, subject: threadEmails[0].subject, emailCount: threadEmails.length, latestTimestamp: threadEmails[threadEmails.length - 1].timestamp, emails: threadEmails }; }) // Sort threads by latest activity (newest first) .sort((a, b) => new Date(b.latestTimestamp).getTime() - new Date(a.latestTimestamp).getTime()); }