diff --git a/components/InterestDetailsModal.vue b/components/InterestDetailsModal.vue index 0e74b54..03b4e81 100644 --- a/components/InterestDetailsModal.vue +++ b/components/InterestDetailsModal.vue @@ -1085,35 +1085,48 @@ const updateBerthRecommendations = async (newRecommendations: number[]) => { // Format date helper function const formatDate = (dateString: string | null | undefined) => { if (!dateString) return ""; + try { + let date: Date; + + // Check if it's an ISO date string (e.g., "2025-06-09T22:58:47.731Z") + if (dateString.includes('T') || dateString.includes('Z')) { + date = new Date(dateString); + } // Handle DD-MM-YYYY format - if (dateString.includes("-")) { - const parts = dateString.split("-"); - if (parts.length === 3) { - const [day, month, year] = parts; - const date = new Date( - parseInt(year), - parseInt(month) - 1, - parseInt(day) - ); - return date.toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - }); - } + else if (dateString.match(/^\d{2}-\d{2}-\d{4}$/)) { + const [day, month, year] = dateString.split("-"); + date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day)); } - // Fallback to direct date parsing - const date = new Date(dateString); - if (!isNaN(date.getTime())) { - return date.toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - }); + // Handle YYYY-MM-DD format + else if (dateString.match(/^\d{4}-\d{2}-\d{2}$/)) { + date = new Date(dateString); } - return dateString; + // Fallback to direct parsing + else { + date = new Date(dateString); + } + + // Check if date is valid + if (isNaN(date.getTime())) { + return dateString; + } + + // Format date in DD/MM/YYYY HH:mm format + const day = date.getDate().toString().padStart(2, '0'); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const year = date.getFullYear(); + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + + // Include time if it's not midnight + if (hours !== '00' || minutes !== '00') { + return `${day}/${month}/${year} ${hours}:${minutes}`; + } + + return `${day}/${month}/${year}`; } catch (error) { + console.error('Date formatting error:', error, dateString); return dateString; } }; diff --git a/docs/email-system-fixes.md b/docs/email-system-fixes.md index dc05af2..31d7bac 100644 --- a/docs/email-system-fixes.md +++ b/docs/email-system-fixes.md @@ -162,6 +162,29 @@ NUXT_DOCUMENSO_BASE_URL=https://signatures.portnimara.dev - **Cause**: BCC search criteria not supported by all IMAP servers - **Solution**: Removed BCC from search criteria, now only searches FROM, TO, and CC fields +### 15. MinIO Private Bucket Authentication Fix +- **Problem**: Email caching failed when MinIO buckets were set to private +- **Cause**: Frontend was trying to fetch presigned URLs which failed with CORS/auth issues +- **Solution**: + - Now reading cached emails directly on server using MinIO client + - No presigned URLs needed - server has full authentication + - Works perfectly with private buckets + +### 16. Email Fetching & Caching Improvements +- **Optimizations**: + - Date-based IMAP search (last 30 days) to reduce email count + - Increased timeout from 15s to 30s + - Now caching ALL emails (both sent and received) + - Fire-and-forget caching to avoid slowing down email fetch + - Proper IMAP date format (e.g., "1-Jan-2024") + +### 17. Date Display Fix +- **Problem**: EOI dates showing wrong format/timezone +- **Solution**: + - Enhanced date parsing to handle ISO dates, DD-MM-YYYY, and YYYY-MM-DD + - Now displays in DD/MM/YYYY HH:mm format + - Properly handles timezones + ## How It Works Now 1. **Email Session Management**: diff --git a/server/api/email/fetch-thread.ts b/server/api/email/fetch-thread.ts index ff0e866..86d2265 100644 --- a/server/api/email/fetch-thread.ts +++ b/server/api/email/fetch-thread.ts @@ -1,7 +1,7 @@ import Imap from 'imap'; import { simpleParser } from 'mailparser'; import { getCredentialsFromSession, decryptCredentials } from '~/server/utils/encryption'; -import { listFiles, getFileStats } from '~/server/utils/minio'; +import { listFiles, getFileStats, getMinioClient, uploadFile } from '~/server/utils/minio'; interface EmailMessage { id: string; @@ -72,12 +72,25 @@ export default defineEventHandler(async (event) => { for (const file of files) { if (file.name.endsWith('.json') && !file.isFolder) { try { - // Use the getDownloadUrl function to get a proper presigned URL - const { getDownloadUrl } = await import('~/server/utils/minio'); - const downloadUrl = await getDownloadUrl(file.name); + // Read file directly on server using MinIO client (works with private buckets) + const client = getMinioClient(); + const bucketName = useRuntimeConfig().minio.bucketName; - const response = await fetch(downloadUrl); - const emailData = await response.json(); + // Get object as stream + const stream = await client.getObject(bucketName, file.name); + + // Convert stream to string + let data = ''; + stream.on('data', (chunk) => { + data += chunk; + }); + + await new Promise((resolve, reject) => { + stream.on('end', () => resolve(data)); + stream.on('error', reject); + }); + + const emailData = JSON.parse(data); cachedEmails.push(emailData); } catch (err) { console.error('Failed to read cached email:', file.name, err); @@ -103,15 +116,15 @@ export default defineEventHandler(async (event) => { authTimeout: 5000 // 5 seconds auth timeout }; - // Fetch emails from IMAP with timeout + // Fetch emails from IMAP with timeout (increased to 30 seconds) let imapEmails: EmailMessage[] = []; const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('IMAP connection timeout')), 15000) + setTimeout(() => reject(new Error('IMAP connection timeout')), 30000) ); try { imapEmails = await Promise.race([ - fetchImapEmails(imapConfig, userEmail, clientEmail, limit), + fetchImapEmails(imapConfig, userEmail, clientEmail, limit, interestId), timeoutPromise ]); } catch (imapError) { @@ -159,7 +172,8 @@ async function fetchImapEmails( imapConfig: any, userEmail: string, clientEmail: string, - limit: number + limit: number, + interestId?: string ): Promise { return new Promise((resolve, reject) => { const emails: EmailMessage[] = []; @@ -210,9 +224,16 @@ async function fetchImapEmails( return; } - // Use ALL to get all messages, then filter manually - // This avoids the complex search criteria issues - imap.search(['ALL'], (err, results) => { + // Use date-based search to reduce the number of emails fetched + // Search for emails from the last 30 days + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + + // Format date for IMAP (e.g., "1-Jan-2024") + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + const searchDate = `${thirtyDaysAgo.getDate()}-${months[thirtyDaysAgo.getMonth()]}-${thirtyDaysAgo.getFullYear()}`; + + imap.search(['SINCE', searchDate], (err, results) => { if (err) { console.error(`Search error in ${folderName}:`, err); searchNextFolder(); @@ -286,6 +307,27 @@ async function fetchImapEmails( } allEmails.push(email); + + // Cache this email if we have an interestId + if (interestId && involvesClient) { + try { + const emailData = { + ...email, + interestId: interestId + }; + + const objectName = `client-emails/interest-${interestId}/${Date.now()}-${email.direction}.json`; + const buffer = Buffer.from(JSON.stringify(emailData, null, 2)); + + // Fire and forget - don't wait for upload + uploadFile(objectName, buffer, 'application/json').catch(err => { + console.error('Failed to cache email:', err); + }); + } catch (cacheError) { + console.error('Failed to cache email:', cacheError); + } + } + messagesProcessed++; if (messagesProcessed === messagesToFetch.length) {