From 1311ffb815d0e6e6323a376d645d7469d6234062 Mon Sep 17 00:00:00 2001 From: alma Date: Mon, 21 Apr 2025 14:42:46 +0200 Subject: [PATCH] mail page rest --- app/courrier/page.tsx | 745 ++++++++++++++++++++++++++---------------- lib/imap.ts | 28 +- 2 files changed, 493 insertions(+), 280 deletions(-) diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index c67f2475..1616e3cc 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -27,6 +27,7 @@ import { AlertOctagon, Archive, RefreshCw } from 'lucide-react'; import { ScrollArea } from '@/components/ui/scroll-area'; +import { useSession } from 'next-auth/react'; interface Account { id: number; @@ -40,7 +41,7 @@ interface Email { id: number; accountId: number; from: string; - fromName?: string; + fromName: string; to: string; subject: string; body: string; @@ -60,6 +61,31 @@ 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) || @@ -96,113 +122,144 @@ function decodeQuotedPrintable(text: string, charset: string): string { } } -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(); - - // 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 parseFullEmail(emailRaw: string): ParsedEmailContent { + console.log('=== parseFullEmail Debug ==='); + console.log('Input email length:', emailRaw.length); + console.log('First 200 chars:', emailRaw.substring(0, 200)); -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 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: [] }; - - // 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]; + + // Handle multipart content + if (contentType.includes('multipart')) { + const boundaryMatch = emailRaw.match(/boundary="?([^"\r\n;]+)"?/i) || + emailRaw.match(/boundary=([^\r\n;]+)/i); - if (endPos > startPos) { - const partContent = emailRaw.substring(startPos, endPos).trim(); + if (boundaryMatch) { + const boundary = boundaryMatch[1].trim(); + const parts = emailRaw.split(new RegExp(`--${boundary}(?:--)?(\\r?\\n|$)`)); - if (partContent) { - const decoded = processSinglePartEmail(partContent); + for (const part of parts) { + if (!part.trim()) continue; - 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); + 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'; + result.attachments.push({ filename, - contentType: decoded.contentType, - encoding: decoded.raw?.headers ? parseEmailHeaders(decoded.raw.headers).encoding : '7bit', - content: decoded.raw?.body || '' + contentType: partContentType, + encoding: 'base64', + content: partBody }); } } } + } 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 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 +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); } - }; + + 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 { @@ -380,7 +437,7 @@ function cleanHtml(html: string): string { function decodeMimeContent(content: string): string { if (!content) return ''; - // Check if this is an Infomaniak multipart message + // Check if this is a multipart message if (content.includes('Content-Type: multipart/')) { const boundary = content.match(/boundary="([^"]+)"/)?.[1]; if (boundary) { @@ -411,29 +468,96 @@ function decodeMimeContent(content: string): string { return cleanHtml(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}
; -}; +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)); -// Add this helper function -const decodeEmailContent = (content: string, charset: string = 'utf-8') => { - return convertCharset(content, charset); -}; + 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 + }); -function cleanEmailContent(content: string): string { - // Remove or fix malformed URLs - return content.replace(/=3D"(http[^"]+)"/g, (match, url) => { - try { - return `"${decodeURIComponent(url)}"`; - } catch { - return ''; + // 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
; + } } // Define the exact folder names from IMAP @@ -470,8 +594,9 @@ const initialSidebarItems = [ } ]; -export default function MailPage() { +export default function CourrierPage() { 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' }, @@ -514,7 +639,46 @@ export default function MailPage() { const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [isLoadingMore, setIsLoadingMore] = useState(false); - const emailsPerPage = 24; + 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); // Debug logging for email distribution useEffect(() => { @@ -584,7 +748,7 @@ export default function MailPage() { } // 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 || '', @@ -654,25 +818,61 @@ export default function MailPage() { }; // Update handleEmailSelect to set selectedEmail correctly - const handleEmailSelect = (emailId: number) => { + const handleEmailSelect = async (emailId: number) => { const email = emails.find(e => e.id === emailId); - 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); - }); + 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()); } + } catch (error) { + console.error('Error marking email as read:', error); } }; @@ -850,20 +1050,20 @@ export default function MailPage() { // Update the email count in the header to show filtered count const renderEmailListHeader = () => (
-
+
- + setSearchQuery(e.target.value)} />
-
-
+
+
0 && selectedEmails.length === filteredEmails.length} onCheckedChange={toggleSelectAll} @@ -1028,39 +1228,7 @@ export default function MailPage() {
- {(() => { - 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; - } - })()} + {renderEmailContent(selectedEmail)}
@@ -1093,111 +1261,142 @@ export default function MailPage() { }, [availableFolders]); // Update the email list item to match header checkbox alignment - 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; - })() - )} - + 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)} + + +
-
- - {formatDate(email.date)} - - +

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

+
+ {preview}
-

- {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 = () => ( @@ -1876,4 +2075,4 @@ export default function MailPage() { {renderDeleteConfirmDialog()} ); -} \ No newline at end of file +} diff --git a/lib/imap.ts b/lib/imap.ts index ce14e059..708afceb 100644 --- a/lib/imap.ts +++ b/lib/imap.ts @@ -1,6 +1,7 @@ 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; @@ -8,19 +9,32 @@ export async function getImapClient() { if (client) return client; const session = await getServerSession(authOptions); - if (!session?.user?.email) { + if (!session?.user?.id) { 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.'); + } + client = new ImapFlow({ - host: process.env.IMAP_HOST || 'imap.gmail.com', - port: parseInt(process.env.IMAP_PORT || '993', 10), + host: credentials.host, + port: credentials.port, secure: true, auth: { - user: session.user.email, - pass: session.user.accessToken + user: credentials.email, + pass: credentials.password, }, - logger: false + logger: false, + tls: { + rejectUnauthorized: false + } }); await client.connect(); @@ -55,4 +69,4 @@ export async function markAsRead(emailIds: number[], isRead: boolean) { } finally { lock.release(); } -} +} \ No newline at end of file