diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index 1616e3cc..c67f2475 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -27,7 +27,6 @@ import { AlertOctagon, Archive, RefreshCw } from 'lucide-react'; import { ScrollArea } from '@/components/ui/scroll-area'; -import { useSession } from 'next-auth/react'; interface Account { id: number; @@ -41,7 +40,7 @@ interface Email { id: number; accountId: number; from: string; - fromName: string; + fromName?: string; to: string; subject: string; body: string; @@ -61,31 +60,6 @@ interface Attachment { encoding: string; } -interface ParsedEmailContent { - text: string | null; - html: string | null; - attachments: Array<{ - filename: string; - contentType: string; - encoding: string; - content: string; - }>; -} - -interface ParsedEmailMetadata { - subject: string; - from: string; - to: string; - date: string; - contentType: string; - text: string | null; - html: string | null; - raw: { - headers: string; - body: string; - }; -} - // Improved MIME Decoder Implementation for Infomaniak function extractBoundary(headers: string): string | null { const boundaryMatch = headers.match(/boundary="?([^"\r\n;]+)"?/i) || @@ -122,144 +96,113 @@ function decodeQuotedPrintable(text: string, charset: string): string { } } -function parseFullEmail(emailRaw: string): ParsedEmailContent { - console.log('=== parseFullEmail Debug ==='); - console.log('Input email length:', emailRaw.length); - console.log('First 200 chars:', emailRaw.substring(0, 200)); - - // Split headers and body - const headerBodySplit = emailRaw.split(/\r?\n\r?\n/); - const headers = headerBodySplit[0]; - const body = headerBodySplit.slice(1).join('\n\n'); - - // Parse content type from headers - const contentTypeMatch = headers.match(/Content-Type:\s*([^;]+)/i); - const contentType = contentTypeMatch ? contentTypeMatch[1].trim().toLowerCase() : 'text/plain'; - - // Initialize result - const result: ParsedEmailContent = { - text: null, - html: null, - attachments: [] - }; - - // Handle multipart content - if (contentType.includes('multipart')) { - const boundaryMatch = emailRaw.match(/boundary="?([^"\r\n;]+)"?/i) || - emailRaw.match(/boundary=([^\r\n;]+)/i); +function parseFullEmail(emailRaw: string) { + // Check if this is a multipart message by looking for boundary definition + const boundaryMatch = emailRaw.match(/boundary="?([^"\r\n;]+)"?/i) || + emailRaw.match(/boundary=([^\r\n;]+)/i); + + if (boundaryMatch) { + const boundary = boundaryMatch[1].trim(); - if (boundaryMatch) { - const boundary = boundaryMatch[1].trim(); - const parts = emailRaw.split(new RegExp(`--${boundary}(?:--)?(\\r?\\n|$)`)); + // Check if there's a preamble before the first boundary + let mainHeaders = ''; + let mainContent = emailRaw; + + // Extract the headers before the first boundary if they exist + const firstBoundaryPos = emailRaw.indexOf('--' + boundary); + if (firstBoundaryPos > 0) { + const headerSeparatorPos = emailRaw.indexOf('\r\n\r\n'); + if (headerSeparatorPos > 0 && headerSeparatorPos < firstBoundaryPos) { + mainHeaders = emailRaw.substring(0, headerSeparatorPos); + } + } + + return processMultipartEmail(emailRaw, boundary, mainHeaders); + } else { + // This is a single part message + return processSinglePartEmail(emailRaw); + } +} + +function processMultipartEmail(emailRaw: string, boundary: string, mainHeaders: string = ''): { + text: string; + html: string; + attachments: { filename: string; contentType: string; encoding: string; content: string; }[]; + headers?: string; +} { + const result = { + text: '', + html: '', + attachments: [] as { filename: string; contentType: string; encoding: string; content: string; }[], + headers: mainHeaders + }; + + // Split by boundary (more robust pattern) + const boundaryRegex = new RegExp(`--${boundary}(?:--)?(\\r?\\n|$)`, 'g'); + + // Get all boundary positions + const matches = Array.from(emailRaw.matchAll(boundaryRegex)); + const boundaryPositions = matches.map(match => match.index!); + + // Extract content between boundaries + for (let i = 0; i < boundaryPositions.length - 1; i++) { + const startPos = boundaryPositions[i] + matches[i][0].length; + const endPos = boundaryPositions[i + 1]; + + if (endPos > startPos) { + const partContent = emailRaw.substring(startPos, endPos).trim(); - for (const part of parts) { - if (!part.trim()) continue; + if (partContent) { + const decoded = processSinglePartEmail(partContent); - const partHeaderBodySplit = part.split(/\r?\n\r?\n/); - const partHeaders = partHeaderBodySplit[0]; - const partBody = partHeaderBodySplit.slice(1).join('\n\n'); - - const partContentTypeMatch = partHeaders.match(/Content-Type:\s*([^;]+)/i); - const partContentType = partContentTypeMatch ? partContentTypeMatch[1].trim().toLowerCase() : 'text/plain'; - - if (partContentType.includes('text/plain')) { - result.text = decodeEmailBody(partBody, partContentType); - } else if (partContentType.includes('text/html')) { - result.html = decodeEmailBody(partBody, partContentType); - } else if (partContentType.startsWith('image/') || partContentType.startsWith('application/')) { - const filenameMatch = partHeaders.match(/filename="?([^"\r\n;]+)"?/i); - const filename = filenameMatch ? filenameMatch[1] : 'attachment'; - + if (decoded.contentType.includes('text/plain')) { + result.text = decoded.text || ''; + } else if (decoded.contentType.includes('text/html')) { + result.html = cleanHtml(decoded.html || ''); + } else if ( + decoded.contentType.startsWith('image/') || + decoded.contentType.startsWith('application/') + ) { + const filename = extractFilename(partContent); result.attachments.push({ filename, - contentType: partContentType, - encoding: 'base64', - content: partBody + contentType: decoded.contentType, + encoding: decoded.raw?.headers ? parseEmailHeaders(decoded.raw.headers).encoding : '7bit', + content: decoded.raw?.body || '' }); } } } - } else { - // Single part content - if (contentType.includes('text/html')) { - result.html = decodeEmailBody(body, contentType); - } else { - result.text = decodeEmailBody(body, contentType); - } } - - // If no content was found, try to extract content directly - if (!result.text && !result.html) { - // Try to extract HTML content - const htmlMatch = emailRaw.match(/]*>[\s\S]*?<\/html>/i); - if (htmlMatch) { - result.html = decodeEmailBody(htmlMatch[0], 'text/html'); - } else { - // Try to extract plain text - const textContent = emailRaw - .replace(/<[^>]+>/g, '') - .replace(/ /g, ' ') - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/\r\n/g, '\n') - .replace(/=\n/g, '') - .replace(/=3D/g, '=') - .replace(/=09/g, '\t') - .trim(); - - if (textContent) { - result.text = textContent; - } - } - } - + return result; } -function decodeEmailBody(content: string, contentType: string): string { - try { - // Remove email client-specific markers - content = content.replace(/\r\n/g, '\n') - .replace(/=\n/g, '') - .replace(/=3D/g, '=') - .replace(/=09/g, '\t'); - - // If it's HTML content - if (contentType.includes('text/html')) { - return extractTextFromHtml(content); +function processSinglePartEmail(rawEmail: string) { + // Split headers and body + const headerBodySplit = rawEmail.split(/\r?\n\r?\n/); + const headers = headerBodySplit[0]; + const body = headerBodySplit.slice(1).join('\n\n'); + + // Parse headers to get content type, encoding, etc. + const emailInfo = parseEmailHeaders(headers); + + // Decode the body based on its encoding + const decodedBody = decodeMIME(body, emailInfo.encoding, emailInfo.charset); + + return { + subject: extractHeader(headers, 'Subject'), + from: extractHeader(headers, 'From'), + to: extractHeader(headers, 'To'), + date: extractHeader(headers, 'Date'), + contentType: emailInfo.contentType, + text: emailInfo.contentType.includes('html') ? null : decodedBody, + html: emailInfo.contentType.includes('html') ? decodedBody : null, + raw: { + headers, + body } - - return content; - } catch (error) { - console.error('Error decoding email body:', error); - return content; - } -} - -function extractTextFromHtml(html: string): string { - // Remove scripts and style tags - html = html.replace(/]*>[\s\S]*?<\/script>/gi, '') - .replace(/]*>[\s\S]*?<\/style>/gi, ''); - - // Convert
and

to newlines - html = html.replace(/]*>/gi, '\n') - .replace(/]*>/gi, '\n') - .replace(/<\/p>/gi, '\n'); - - // Remove all other HTML tags - html = html.replace(/<[^>]+>/g, ''); - - // Decode HTML entities - html = html.replace(/ /g, ' ') - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"'); - - // Clean up whitespace - return html.replace(/\n\s*\n/g, '\n\n').trim(); + }; } function extractHeader(headers: string, headerName: string): string { @@ -437,7 +380,7 @@ function cleanHtml(html: string): string { function decodeMimeContent(content: string): string { if (!content) return ''; - // Check if this is a multipart message + // Check if this is an Infomaniak multipart message if (content.includes('Content-Type: multipart/')) { const boundary = content.match(/boundary="([^"]+)"/)?.[1]; if (boundary) { @@ -468,96 +411,29 @@ function decodeMimeContent(content: string): string { return cleanHtml(content); } -function renderEmailContent(email: Email) { - console.log('=== renderEmailContent Debug ==='); - console.log('Email ID:', email.id); - console.log('Subject:', email.subject); - console.log('Body length:', email.body.length); - console.log('First 100 chars:', email.body.substring(0, 100)); - - try { - // First try to parse the full email - const parsed = parseFullEmail(email.body); - console.log('Parsed content:', { - hasText: !!parsed.text, - hasHtml: !!parsed.html, - hasAttachments: parsed.attachments.length > 0 - }); - - // Determine content and type - let content = ''; - let isHtml = false; - - if (parsed.html) { - // Use our existing MIME decoding for HTML content - content = decodeMIME(parsed.html, 'quoted-printable', 'utf-8'); - isHtml = true; - } else if (parsed.text) { - // Use our existing MIME decoding for plain text content - content = decodeMIME(parsed.text, 'quoted-printable', 'utf-8'); - isHtml = false; - } else { - // Try to extract content directly from body using our existing functions - const htmlMatch = email.body.match(/]*>[\s\S]*?<\/html>/i); - if (htmlMatch) { - content = decodeMIME(htmlMatch[0], 'quoted-printable', 'utf-8'); - isHtml = true; - } else { - // Use our existing text extraction function - content = extractTextFromHtml(email.body); - isHtml = false; - } - } - - if (!content) { - console.log('No content available after all attempts'); - return

No content available
; - } - - // Handle attachments - const attachmentElements = parsed.attachments.map((attachment, index) => ( -
-
- - {attachment.filename} -
-
- )); - - return ( -
- {isHtml ? ( -
]*>[\s\S]*?<\/style>/gi, '') - .replace(/]*>[\s\S]*?<\/script>/gi, '') - .replace(/]*>/gi, '') - .replace(/]*>/gi, '') - .replace(/]*>/gi, '') - .replace(/]*>[\s\S]*?<\/title>/gi, '') - .replace(/]*>[\s\S]*?<\/head>/gi, '') - .replace(/]*>/gi, '') - .replace(/<\/body>/gi, '') - .replace(/]*>/gi, '') - .replace(/<\/html>/gi, '') - }} - /> - ) : ( -
- {content.split('\n').map((line, i) => ( -

{line}

- ))} -
- )} - {attachmentElements} -
- ); - } catch (e) { - console.error('Error parsing email:', e); - return
Error displaying email content
; +// Add this helper function +const renderEmailContent = (email: Email) => { + const decodedContent = decodeMimeContent(email.body); + if (email.body.includes('Content-Type: text/html')) { + return
; } + return
{decodedContent}
; +}; + +// Add this helper function +const decodeEmailContent = (content: string, charset: string = 'utf-8') => { + return convertCharset(content, charset); +}; + +function cleanEmailContent(content: string): string { + // Remove or fix malformed URLs + return content.replace(/=3D"(http[^"]+)"/g, (match, url) => { + try { + return `"${decodeURIComponent(url)}"`; + } catch { + return ''; + } + }); } // Define the exact folder names from IMAP @@ -594,9 +470,8 @@ const initialSidebarItems = [ } ]; -export default function CourrierPage() { +export default function MailPage() { const router = useRouter(); - const { data: session } = useSession(); const [loading, setLoading] = useState(true); const [accounts, setAccounts] = useState([ { id: 0, name: 'All', email: '', color: 'bg-gray-500' }, @@ -639,46 +514,7 @@ export default function CourrierPage() { const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [isLoadingMore, setIsLoadingMore] = useState(false); - const [isLoadingInitial, setIsLoadingInitial] = useState(true); - const [isLoadingSearch, setIsLoadingSearch] = useState(false); - const [isLoadingCompose, setIsLoadingCompose] = useState(false); - const [isLoadingReply, setIsLoadingReply] = useState(false); - const [isLoadingForward, setIsLoadingForward] = useState(false); - const [isLoadingDelete, setIsLoadingDelete] = useState(false); - const [isLoadingMove, setIsLoadingMove] = useState(false); - const [isLoadingStar, setIsLoadingStar] = useState(false); - const [isLoadingUnstar, setIsLoadingUnstar] = useState(false); - const [isLoadingMarkRead, setIsLoadingMarkRead] = useState(false); - const [isLoadingMarkUnread, setIsLoadingMarkUnread] = useState(false); - const [isLoadingRefresh, setIsLoadingRefresh] = useState(false); - const emailsPerPage = 20; - const [isSearching, setIsSearching] = useState(false); - const [searchResults, setSearchResults] = useState([]); - const [showSearchResults, setShowSearchResults] = useState(false); - const [isComposing, setIsComposing] = useState(false); - const [composeEmail, setComposeEmail] = useState({ - to: '', - subject: '', - body: '', - }); - const [isSending, setIsSending] = useState(false); - const [isReplying, setIsReplying] = useState(false); - const [isForwarding, setIsForwarding] = useState(false); - const [replyToEmail, setReplyToEmail] = useState(null); - const [forwardEmail, setForwardEmail] = useState(null); - const [replyBody, setReplyBody] = useState(''); - const [forwardBody, setForwardBody] = useState(''); - const [replyAttachments, setReplyAttachments] = useState([]); - const [forwardAttachments, setForwardAttachments] = useState([]); - const [isSendingReply, setIsSendingReply] = useState(false); - const [isSendingForward, setIsSendingForward] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); - const [isMoving, setIsMoving] = useState(false); - const [isStarring, setIsStarring] = useState(false); - const [isUnstarring, setIsUnstarring] = useState(false); - const [isMarkingRead, setIsMarkingRead] = useState(false); - const [isMarkingUnread, setIsMarkingUnread] = useState(false); - const [isRefreshing, setIsRefreshing] = useState(false); + const emailsPerPage = 24; // Debug logging for email distribution useEffect(() => { @@ -748,7 +584,7 @@ export default function CourrierPage() { } // Process emails keeping exact folder names - const processedEmails = (data.emails || []).map((email: any) => ({ + const processedEmails = data.emails.map((email: any) => ({ id: Number(email.id), accountId: 1, from: email.from || '', @@ -818,61 +654,25 @@ export default function CourrierPage() { }; // Update handleEmailSelect to set selectedEmail correctly - const handleEmailSelect = async (emailId: number) => { + const handleEmailSelect = (emailId: number) => { const email = emails.find(e => e.id === emailId); - if (!email) { - console.error('Email not found in list'); - return; - } - - // Set the selected email first to show preview immediately - setSelectedEmail(email); - - // Fetch the full email content - const response = await fetch(`/api/mail/${emailId}`); - if (!response.ok) { - throw new Error('Failed to fetch full email content'); - } - - const fullEmail = await response.json(); - - // Update the email in the list and selected email with full content - setEmails(prevEmails => prevEmails.map(email => - email.id === emailId - ? { ...email, body: fullEmail.body } - : email - )); - - setSelectedEmail(prev => prev ? { ...prev, body: fullEmail.body } : prev); - - // Try to mark as read in the background - try { - const markReadResponse = await fetch(`/api/mail/mark-read`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${session?.user?.id}` // Add session token - }, - body: JSON.stringify({ - emailId, - isRead: true, - }), - }); - - if (markReadResponse.ok) { - // Only update the emails list if the API call was successful - setEmails((prevEmails: Email[]) => - prevEmails.map((email: Email): Email => - email.id === emailId - ? { ...email, read: true } - : email - ) - ); - } else { - console.error('Failed to mark email as read:', await markReadResponse.text()); + if (email) { + setSelectedEmail(email); + if (!email.read) { + // Mark as read in state + setEmails(emails.map(e => + e.id === emailId ? { ...e, read: true } : e + )); + + // Update read status on server + fetch('/api/mail/mark-read', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ emailId }) + }).catch(error => { + console.error('Error marking email as read:', error); + }); } - } catch (error) { - console.error('Error marking email as read:', error); } }; @@ -1050,20 +850,20 @@ export default function CourrierPage() { // Update the email count in the header to show filtered count const renderEmailListHeader = () => (
-
+
- + setSearchQuery(e.target.value)} />
-
-
+
+
0 && selectedEmails.length === filteredEmails.length} onCheckedChange={toggleSelectAll} @@ -1228,7 +1028,39 @@ export default function CourrierPage() {
- {renderEmailContent(selectedEmail)} + {(() => { + try { + const parsed = parseFullEmail(selectedEmail.body); + return ( +
+ {/* Display HTML content if available, otherwise fallback to text */} +
+ + {/* Display attachments if present */} + {parsed.attachments && parsed.attachments.length > 0 && ( +
+

Attachments

+
+ {parsed.attachments.map((attachment, index) => ( +
+ + + {attachment.filename} + +
+ ))} +
+
+ )} +
+ ); + } catch (e) { + console.error('Error parsing email:', e); + return selectedEmail.body; + } + })()}
@@ -1261,142 +1093,111 @@ export default function CourrierPage() { }, [availableFolders]); // Update the email list item to match header checkbox alignment - const renderEmailListItem = (email: Email) => { - console.log('=== Email List Item Debug ==='); - console.log('Email ID:', email.id); - console.log('Subject:', email.subject); - console.log('Body length:', email.body.length); - console.log('First 100 chars of body:', email.body.substring(0, 100)); - - const preview = generateEmailPreview(email); - console.log('Generated preview:', preview); - - return ( -
handleEmailSelect(email.id)} - > - { - const e = { target: { checked }, stopPropagation: () => {} } as React.ChangeEvent; - handleEmailCheckbox(e, email.id); - }} - onClick={(e) => e.stopPropagation()} - className="mt-0.5" - /> -
-
-
- - {currentView === 'Sent' ? email.to : ( - (() => { - const fromMatch = email.from.match(/^([^<]+)\s*<([^>]+)>$/); - return fromMatch ? fromMatch[1].trim() : email.from; - })() - )} - -
-
- - {formatDate(email.date)} - - -
+ const renderEmailListItem = (email: Email) => ( +
handleEmailSelect(email.id)} + > + { + const e = { target: { checked }, stopPropagation: () => {} } as React.ChangeEvent; + handleEmailCheckbox(e, email.id); + }} + onClick={(e) => e.stopPropagation()} + className="mt-0.5" + /> +
+
+
+ + {currentView === 'Sent' ? email.to : ( + (() => { + const fromMatch = email.from.match(/^([^<]+)\s*<([^>]+)>$/); + return fromMatch ? fromMatch[1].trim() : email.from; + })() + )} +
-

- {email.subject || '(No subject)'} -

-
- {preview} +
+ + {formatDate(email.date)} + +
+

+ {email.subject || '(No subject)'} +

+
+ {(() => { + // Get clean preview of the actual message content + let preview = ''; + try { + const parsed = parseFullEmail(email.body); + + // Try to get content from parsed email + preview = (parsed.text || parsed.html || '') + .replace(/]*>[\s\S]*?<\/style>/gi, '') + .replace(/]*>[\s\S]*?<\/script>/gi, '') + .replace(/<[^>]+>/g, '') + .replace(/ |‌|»|«|>/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + + // If no preview from parsed content, try direct body + if (!preview) { + preview = email.body + .replace(/<[^>]+>/g, '') + .replace(/ |‌|»|«|>/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + } + + // Remove email artifacts and clean up + preview = preview + .replace(/^>+/gm, '') + .replace(/Content-Type:[^\n]+/g, '') + .replace(/Content-Transfer-Encoding:[^\n]+/g, '') + .replace(/--[a-zA-Z0-9]+(-[a-zA-Z0-9]+)?/g, '') + .replace(/boundary=[^\n]+/g, '') + .replace(/charset=[^\n]+/g, '') + .replace(/[\r\n]+/g, ' ') + .trim(); + + // Take first 100 characters + preview = preview.substring(0, 100); + + // Try to end at a complete word + if (preview.length === 100) { + const lastSpace = preview.lastIndexOf(' '); + if (lastSpace > 80) { + preview = preview.substring(0, lastSpace); + } + preview += '...'; + } + + } catch (e) { + console.error('Error generating preview:', e); + preview = ''; + } + + return preview || 'No preview available'; + })()} +
- ); - }; - - const generateEmailPreview = (email: Email): string => { - console.log('=== generateEmailPreview Debug ==='); - console.log('Email ID:', email.id); - console.log('Subject:', email.subject); - console.log('Body length:', email.body.length); - console.log('First 200 chars of body:', email.body.substring(0, 200)); - - try { - const parsed = parseFullEmail(email.body); - console.log('Parsed content:', { - hasText: !!parsed.text, - hasHtml: !!parsed.html, - textPreview: parsed.text?.substring(0, 100) || 'No text', - htmlPreview: parsed.html?.substring(0, 100) || 'No HTML' - }); - - let preview = ''; - if (parsed.text) { - preview = parsed.text; - console.log('Using text content for preview'); - } else if (parsed.html) { - preview = parsed.html - .replace(/]*>[\s\S]*?<\/style>/gi, '') - .replace(/]*>[\s\S]*?<\/script>/gi, '') - .replace(/<[^>]+>/g, ' ') - .replace(/\s+/g, ' ') - .trim(); - console.log('Using HTML content for preview'); - } - - if (!preview) { - console.log('No preview from parsed content, using raw body'); - preview = email.body - .replace(/<[^>]+>/g, ' ') - .replace(/ |‌|»|«|>/g, ' ') - .replace(/\s+/g, ' ') - .trim(); - } - - console.log('Final preview before cleaning:', preview.substring(0, 100) + '...'); - - // Clean up the preview - preview = preview - .replace(/^>+/gm, '') - .replace(/Content-Type:[^\n]+/g, '') - .replace(/Content-Transfer-Encoding:[^\n]+/g, '') - .replace(/--[a-zA-Z0-9]+(-[a-zA-Z0-9]+)?/g, '') - .replace(/boundary=[^\n]+/g, '') - .replace(/charset=[^\n]+/g, '') - .replace(/[\r\n]+/g, ' ') - .trim(); - - // Take first 100 characters - preview = preview.substring(0, 100); - - // Try to end at a complete word - if (preview.length === 100) { - const lastSpace = preview.lastIndexOf(' '); - if (lastSpace > 80) { - preview = preview.substring(0, lastSpace); - } - preview += '...'; - } - - console.log('Final preview:', preview); - return preview; - - } catch (e) { - console.error('Error generating preview:', e); - return 'No preview available'; - } - }; +
+ ); // Render the sidebar navigation const renderSidebarNav = () => ( @@ -2075,4 +1876,4 @@ export default function CourrierPage() { {renderDeleteConfirmDialog()} ); -} +} \ No newline at end of file diff --git a/lib/email-parser.ts b/lib/email-parser.ts index 6a3d0bf3..7bbfb69c 100644 --- a/lib/email-parser.ts +++ b/lib/email-parser.ts @@ -90,4 +90,4 @@ function extractTextFromHtml(html: string): string { // Clean up whitespace return html.replace(/\n\s*\n/g, '\n\n').trim(); -} \ No newline at end of file +} diff --git a/lib/imap.ts b/lib/imap.ts index 33e98119..ce14e059 100644 --- a/lib/imap.ts +++ b/lib/imap.ts @@ -1,36 +1,26 @@ import { ImapFlow } from 'imapflow'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/app/api/auth/[...nextauth]/route'; -import { prisma } from '@/lib/prisma'; + +let client: ImapFlow | null = null; export async function getImapClient() { + if (client) return client; + const session = await getServerSession(authOptions); - if (!session?.user?.id) { + if (!session?.user?.email) { throw new Error('No authenticated user'); } - const credentials = await prisma.mailCredentials.findUnique({ - where: { - userId: session.user.id - } - }); - - if (!credentials) { - throw new Error('No mail credentials found. Please configure your email account.'); - } - - const client = new ImapFlow({ - host: credentials.host, - port: credentials.port, + client = new ImapFlow({ + host: process.env.IMAP_HOST || 'imap.gmail.com', + port: parseInt(process.env.IMAP_PORT || '993', 10), secure: true, auth: { - user: credentials.email, - pass: credentials.password, + user: session.user.email, + pass: session.user.accessToken }, - logger: false, - tls: { - rejectUnauthorized: false - } + logger: false }); await client.connect(); @@ -44,7 +34,7 @@ export async function moveEmails(emailIds: number[], targetFolder: string) { for (const id of emailIds) { const message = await imap.fetchOne(id.toString(), { uid: true }); if (message) { - await imap.messageMove(message.uid.toString(), targetFolder); + await imap.messageMove(message.uid, targetFolder); } } } finally { @@ -59,14 +49,10 @@ export async function markAsRead(emailIds: number[], isRead: boolean) { for (const id of emailIds) { const message = await imap.fetchOne(id.toString(), { uid: true }); if (message) { - if (isRead) { - await imap.messageFlagsAdd(message.uid.toString(), ['\\Seen'], { uid: true }); - } else { - await imap.messageFlagsRemove(message.uid.toString(), ['\\Seen'], { uid: true }); - } + await imap.messageFlagsAdd(message.uid, isRead ? ['\\Seen'] : [], { uid: true }); } } } finally { lock.release(); } -} \ No newline at end of file +}