import Imap from 'imap'; import { simpleParser } from 'mailparser'; import { getCredentialsFromSession, decryptCredentials } from '~/server/utils/encryption'; import { listFiles, getFileStats } from '~/server/utils/minio'; interface EmailMessage { id: string; from: string; to: string | string[]; subject: string; body: string; html?: string; timestamp: string; direction: 'sent' | 'received'; threadId?: string; } export default defineEventHandler(async (event) => { const xTagHeader = getRequestHeader(event, "x-tag"); if (!xTagHeader || (xTagHeader !== "094ut234" && xTagHeader !== "pjnvü1230")) { throw createError({ statusCode: 401, statusMessage: "unauthenticated" }); } try { const body = await readBody(event); const { clientEmail, interestId, sessionId, limit = 50 } = body; if (!clientEmail || !sessionId) { throw createError({ statusCode: 400, statusMessage: "Client email and sessionId are required" }); } // Get encrypted credentials from session const encryptedCredentials = getCredentialsFromSession(sessionId); if (!encryptedCredentials) { throw createError({ statusCode: 401, statusMessage: "Email credentials not found. Please reconnect." }); } // Decrypt credentials const { email: userEmail, password } = decryptCredentials(encryptedCredentials); // First, get emails from MinIO cache if available const cachedEmails: EmailMessage[] = []; if (interestId) { try { const files = await listFiles(`client-emails/interest-${interestId}/`, true) as any[]; for (const file of files) { if (file.name.endsWith('.json') && !file.isFolder) { try { const response = await fetch(`${process.env.NUXT_MINIO_ENDPOINT || 'http://localhost:9000'}/${useRuntimeConfig().minio.bucketName}/${file.name}`); const emailData = await response.json(); cachedEmails.push(emailData); } catch (err) { console.error('Failed to read cached email:', err); } } } } catch (err) { console.error('Failed to list cached emails:', err); } } // Configure IMAP const imapConfig = { user: userEmail, password: password, host: process.env.NUXT_EMAIL_IMAP_HOST || 'mail.portnimara.com', port: parseInt(process.env.NUXT_EMAIL_IMAP_PORT || '993'), tls: true, tlsOptions: { rejectUnauthorized: false } }; // Fetch emails from IMAP const imapEmails: EmailMessage[] = await new Promise((resolve, reject) => { const emails: EmailMessage[] = []; const imap = new Imap(imapConfig); imap.once('ready', () => { // Search for emails to/from the client imap.openBox('INBOX', true, (err, box) => { if (err) { reject(err); return; } const searchCriteria = [ 'OR', ['FROM', clientEmail], ['TO', clientEmail] ]; imap.search(searchCriteria, (err, results) => { if (err) { reject(err); return; } if (!results || results.length === 0) { imap.end(); resolve(emails); return; } // Limit results const messagesToFetch = results.slice(-limit); const fetch = imap.fetch(messagesToFetch, { bodies: '', struct: true, envelope: true }); fetch.on('message', (msg, seqno) => { msg.on('body', (stream, info) => { simpleParser(stream as any, async (err: any, parsed: any) => { if (err) { console.error('Parse error:', err); return; } const email: EmailMessage = { id: parsed.messageId || `${Date.now()}-${seqno}`, from: parsed.from?.text || '', to: Array.isArray(parsed.to) ? parsed.to.map((addr: any) => addr.text).join(', ') : parsed.to?.text || '', subject: parsed.subject || '', body: parsed.text || '', html: parsed.html || undefined, timestamp: parsed.date?.toISOString() || new Date().toISOString(), direction: parsed.from?.text.includes(userEmail) ? 'sent' : 'received' }; // Extract thread ID from headers if available if (parsed.headers.has('in-reply-to')) { email.threadId = parsed.headers.get('in-reply-to') as string; } emails.push(email); }); }); }); fetch.once('error', (err) => { console.error('Fetch error:', err); reject(err); }); fetch.once('end', () => { imap.end(); }); }); }); // Also check Sent folder imap.openBox('[Gmail]/Sent Mail', true, (err, box) => { if (err) { // Try common sent folder names ['Sent', 'Sent Items', 'Sent Messages'].forEach(folderName => { imap.openBox(folderName, true, (err, box) => { if (!err) { // Search in sent folder imap.search([['TO', clientEmail]], (err, results) => { if (!err && results && results.length > 0) { // Process sent emails similarly } }); } }); }); } }); }); imap.once('error', (err: any) => { reject(err); }); imap.once('end', () => { resolve(emails); }); imap.connect(); }); // Combine cached and IMAP emails, remove duplicates const allEmails = [...cachedEmails, ...imapEmails]; const uniqueEmails = Array.from( new Map(allEmails.map(email => [email.id, email])).values() ); // Sort by timestamp uniqueEmails.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() ); // Group into threads const threads = groupIntoThreads(uniqueEmails); return { success: true, emails: uniqueEmails, threads: threads }; } catch (error) { console.error('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[]): any[] { const threads = new Map(); emails.forEach(email => { // Normalize subject by removing Re:, Fwd:, etc. const normalizedSubject = email.subject .replace(/^(Re:|Fwd:|Fw:)\s*/gi, '') .trim(); // Find existing thread or create new one let threadFound = false; for (const [threadId, threadEmails] of threads.entries()) { const threadSubject = threadEmails[0].subject .replace(/^(Re:|Fwd:|Fw:)\s*/gi, '') .trim(); if (threadSubject === normalizedSubject) { threadEmails.push(email); threadFound = true; break; } } if (!threadFound) { threads.set(email.id, [email]); } }); // Convert to array format return Array.from(threads.entries()).map(([threadId, emails]) => ({ id: threadId, subject: emails[0].subject, emailCount: emails.length, latestTimestamp: emails[emails.length - 1].timestamp, emails: emails })); }