diff --git a/app/mail/page.tsx b/app/mail/page.tsx index 0c5f4aa..99b5205 100644 --- a/app/mail/page.tsx +++ b/app/mail/page.tsx @@ -28,17 +28,18 @@ interface Account { } interface Email { - id: number; - accountId: number; + id: string; + accountId: string; from: string; - fromName: string; + fromName?: string; to: string; subject: string; body: string; - date: string; + preview: string; + category: string; + date: Date; read: boolean; starred: boolean; - category: string; } // Improved MIME Decoder Implementation for Infomaniak @@ -319,6 +320,27 @@ function cleanHtml(html: string): string { if (!html) return ''; return html + // Fix common Infomaniak-specific character encodings + .replace(/=C2=A0/g, ' ') // non-breaking space + .replace(/=E2=80=93/g, '\u2013') // en dash + .replace(/=E2=80=94/g, '\u2014') // em dash + .replace(/=E2=80=98/g, '\u2018') // left single quote + .replace(/=E2=80=99/g, '\u2019') // right single quote + .replace(/=E2=80=9C/g, '\u201C') // left double quote + .replace(/=E2=80=9D/g, '\u201D') // right double quote + .replace(/=C3=A0/g, 'à') + .replace(/=C3=A2/g, 'â') + .replace(/=C3=A9/g, 'é') + .replace(/=C3=A8/g, 'è') + .replace(/=C3=AA/g, 'ê') + .replace(/=C3=AB/g, 'ë') + .replace(/=C3=B4/g, 'ô') + .replace(/=C3=B9/g, 'ù') + .replace(/=C3=BB/g, 'û') + .replace(/=C3=80/g, 'À') + .replace(/=C3=89/g, 'É') + .replace(/=C3=87/g, 'Ç') + // Clean up HTML entities .replace(/ç/g, 'ç') .replace(/é/g, 'é') .replace(/è/g, 'ë') @@ -329,48 +351,38 @@ function cleanHtml(html: string): string { .replace(/\xA0/g, ' '); } -// Update the decodeMimeContent function to use the new implementation function decodeMimeContent(content: string): string { if (!content) return ''; - try { - // Handle the special case with InfomaniakPhpMail boundary - if (content.includes('---InfomaniakPhpMail')) { - const boundaryMatch = content.match(/---InfomaniakPhpMail[\w\d]+/); - if (boundaryMatch) { - const boundary = boundaryMatch[0]; - const result = processMultipartEmail(content, boundary); - return result.html || result.text || content; - } + // Check if this is an Infomaniak multipart message + if (content.includes('Content-Type: multipart/')) { + const boundary = content.match(/boundary="([^"]+)"/)?.[1]; + if (boundary) { + const parts = content.split('--' + boundary); + let htmlContent = ''; + let textContent = ''; + + parts.forEach(part => { + if (part.includes('Content-Type: text/html')) { + const match = part.match(/\r?\n\r?\n([\s\S]+?)(?=\r?\n--)/); + if (match) { + htmlContent = cleanHtml(match[1]); + } + } else if (part.includes('Content-Type: text/plain')) { + const match = part.match(/\r?\n\r?\n([\s\S]+?)(?=\r?\n--)/); + if (match) { + textContent = cleanHtml(match[1]); + } + } + }); + + // Prefer HTML content if available + return htmlContent || textContent; } - - // Regular email parsing - const result = parseFullEmail(content); - if ('html' in result && result.html) { - return extractHtmlBody(result.html); - } else if ('text' in result && result.text) { - return result.text; - } - - // If parsing fails, try simple decoding - if (content.includes('Content-Type:') || content.includes('Content-Transfer-Encoding:')) { - const simpleDecoded = processSinglePartEmail(content); - return simpleDecoded.text || simpleDecoded.html || content; - } - - // Try to detect encoding and decode accordingly - if (content.includes('=?UTF-8?B?') || content.includes('=?utf-8?B?')) { - return decodeMIME(content, 'base64', 'utf-8'); - } else if (content.includes('=?UTF-8?Q?') || content.includes('=?utf-8?Q?') || content.includes('=20')) { - return decodeMIME(content, 'quoted-printable', 'utf-8'); - } - - // If nothing else worked, return the original content - return content; - } catch (error) { - console.error('Error decoding email content:', error); - return content; } + + // If not multipart or no boundary found, clean the content directly + return cleanHtml(content); } export default function MailPage() { @@ -383,7 +395,7 @@ export default function MailPage() { // State declarations const [selectedAccount, setSelectedAccount] = useState(1); const [currentView, setCurrentView] = useState('inbox'); - const [selectedEmail, setSelectedEmail] = useState(null); + const [selectedEmail, setSelectedEmail] = useState(null); const [sidebarOpen, setSidebarOpen] = useState(true); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const [composeOpen, setComposeOpen] = useState(false); @@ -391,7 +403,7 @@ export default function MailPage() { const [foldersDropdownOpen, setFoldersDropdownOpen] = useState(false); const [showAccountActions, setShowAccountActions] = useState(null); const [showEmailActions, setShowEmailActions] = useState(false); - const [selectedEmails, setSelectedEmails] = useState([]); + const [selectedEmails, setSelectedEmails] = useState([]); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [deleteType, setDeleteType] = useState<'email' | 'emails' | 'account'>('email'); const [itemToDelete, setItemToDelete] = useState(null); @@ -399,37 +411,29 @@ export default function MailPage() { const [showCc, setShowCc] = useState(false); const [showBcc, setShowBcc] = useState(false); const [emails, setEmails] = useState([]); - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [composeSubject, setComposeSubject] = useState(''); const [composeRecipient, setComposeRecipient] = useState(''); const [composeContent, setComposeContent] = useState(''); // Fetch emails from IMAP API - useEffect(() => { - async function fetchEmails() { - try { - setError(null); - setLoading(true); - const res = await fetch('/api/mail'); - if (!res.ok) { - throw new Error('Failed to fetch emails'); - } - const data = await res.json(); - if (data.error) { - throw new Error(data.error); - } - setEmails(data.messages || []); - } catch (error) { - console.error('Error fetching emails:', error); - setError('Unable to load emails. Please try again later.'); - } finally { - setLoading(false); + const loadEmails = async () => { + setLoading(true); + setError(null); + try { + const response = await fetch('/api/mail'); + if (!response.ok) { + throw new Error('Failed to fetch emails'); } + const data = await response.json(); + setEmails(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setLoading(false); } - - fetchEmails(); - }, [currentView]); + }; // Mock folders data const folders = [ @@ -445,11 +449,23 @@ export default function MailPage() { ...accounts ]; - // Filter emails based on selected account and view - const filteredEmails = emails.filter(email => - (selectedAccount === 0 || email.accountId === selectedAccount) && - (currentView === 'starred' ? email.starred : email.category === currentView) - ); + // Filter emails based on current view + const filteredEmails = emails.filter(email => { + if (selectedAccount !== 'all' && email.accountId !== selectedAccount) { + return false; + } + + switch (currentView) { + case 'starred': + return email.starred; + case 'sent': + return email.category === 'sent'; + case 'trash': + return email.category === 'trash'; + default: + return email.category === 'inbox'; + } + }); // Format date for display const formatDate = (dateString: string) => { @@ -470,17 +486,12 @@ export default function MailPage() { }; // Update email click handler to work without mark-read endpoint - const handleEmailClick = (emailId: number) => { - // Since the mark-read endpoint is not available, just update the UI - const updatedEmails = emails.map(email => - email.id === emailId ? { ...email, read: true } : email - ); - setEmails(updatedEmails); - setSelectedEmail(emailId); + const handleEmailSelect = (email: Email) => { + setSelectedEmail(selectedEmail?.id === email.id ? null : email); }; // Toggle starred status - const toggleStarred = async (emailId: number, e: React.MouseEvent) => { + const toggleStarred = async (emailId: string, e: React.MouseEvent) => { e.stopPropagation(); // Toggle star in IMAP @@ -501,7 +512,7 @@ export default function MailPage() { }; // Handle bulk selection - const toggleEmailSelection = (emailId: number, e: React.MouseEvent) => { + const toggleEmailSelection = (emailId: string, e: React.MouseEvent) => { e.stopPropagation(); setSelectedEmails(prev => prev.includes(emailId) @@ -608,6 +619,66 @@ export default function MailPage() { setComposeContent(content); }; + const handleEmailCheckbox = (e: React.ChangeEvent, emailId: string) => { + e.stopPropagation(); + if (e.target.checked) { + setSelectedEmails([...selectedEmails, emailId]); + } else { + setSelectedEmails(selectedEmails.filter(id => id !== emailId)); + } + }; + + // Update the mock data with all required properties + const mockEmails: Email[] = [ + { + id: "1", + accountId: "1", + from: "john@example.com", + fromName: "John Doe", + to: "me@example.com", + subject: "Hello", + body: "This is a test email", + preview: "This is a test email", + category: "inbox", + date: new Date(), + read: false, + starred: false + } + ]; + + // Update the email action handlers to use string IDs + const handleMarkAsRead = (emailId: string, isRead: boolean) => { + setEmails(emails.map(email => + email.id === emailId ? { ...email, read: isRead } : email + )); + }; + + const handleDeleteEmail = (emailId: string) => { + setEmails(emails.filter(email => email.id !== emailId)); + setSelectedEmails(selectedEmails.filter(id => id !== emailId)); + }; + + // Update the bulk action handlers + const handleBulkAction = (action: 'delete' | 'mark-read' | 'mark-unread') => { + selectedEmails.forEach(emailId => { + const email = emails.find(e => e.id === emailId); + if (email) { + switch (action) { + case 'delete': + handleDeleteEmail(emailId); + break; + case 'mark-read': + handleMarkAsRead(emailId, true); + break; + case 'mark-unread': + handleMarkAsRead(emailId, false); + break; + } + } + }); + setSelectedEmails([]); + }; + if (loading) { return (
@@ -898,347 +969,82 @@ export default function MailPage() { )} {/* Email List */} - {filteredEmails.length > 0 ? ( -
    - {filteredEmails.map(email => ( -
  • handleEmailClick(email.id)} - > -
    -
    -
    - toggleEmailSelection(email.id, e)} - /> -
    -
    - {email.fromName} -
    -
    -
    {formatDate(email.date)}
    -
    -
    -

    {email.subject}

    - -
    -

    - {decodeMimeContent(email.body)} -

    -
    -
  • - ))} -
- ) : ( -
- -

No emails in this folder

-
- )} -
- - {/* Email detail view */} -
- {selectedEmail ? ( -
- {/* Email actions header */} -
-
- - - +
+
+ {loading ? ( +
+
+ Loading emails...
-
- + Try Again +
-
- - {/* Email content */} -
-
- {getSelectedEmail() && ( - <> -
-
-

{getSelectedEmail()?.subject}

- -
- -
- - - {getSelectedEmail()?.fromName.charAt(0)} - - -
-
{getSelectedEmail()?.fromName}
-
- {getSelectedEmail()?.from} - - {new Date(getSelectedEmail()!.date).toLocaleString([], { dateStyle: 'medium', timeStyle: 'short' })} + ) : emails.length === 0 ? ( +
+
No emails found
+
+ ) : ( +
    + {filteredEmails.map((email) => ( +
  • handleEmailSelect(email)} + > +
    +
    +
    + handleEmailCheckbox(e, email.id)} + onClick={(e) => e.stopPropagation()} + className="h-4 w-4 text-blue-600" + /> +
    +

    + {email.fromName || email.from} +

    +

    {email.subject}

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

    {decodeMimeContent(getSelectedEmail()?.body || '')}

    -
    - - )} -
-
+ + ))} + + )}
- ) : ( -
-
- -

No email selected

-

Choose an email from the list to read its contents

-
-
- )} +
- - {/* Compose email modal */} - {composeOpen && ( -
- - - New Message - - - -
-
- - -
-
-
- -
- {!showCc && ( - - )} - {!showBcc && ( - - )} -
-
- setComposeRecipient(e.target.value)} - className="bg-white border-gray-200 py-1.5" - /> -
- {showCc && ( -
-
- - -
- -
- )} - {showBcc && ( -
-
- - -
- -
- )} -
- - setComposeSubject(e.target.value)} - className="bg-white border-gray-200 py-1.5" - /> -
-
- -
- -