diff --git a/app/api/emails/route.ts b/app/api/emails/route.ts
index 5d86b2f3..8f3a3960 100644
--- a/app/api/emails/route.ts
+++ b/app/api/emails/route.ts
@@ -44,7 +44,7 @@ export async function GET(req: NextRequest) {
date: new Date().toISOString(),
isUnread: true
}],
- mailUrl: `${nextcloudUrl}/apps/mail/box/unified`
+ mailUrl: `${nextcloudUrl}/apps/courrier/box/unified`
});
} catch (error) {
console.error('Error:', error);
diff --git a/app/courrier/login/page.tsx b/app/courrier/login/page.tsx
index 3782f9bd..8726f4ba 100644
--- a/app/courrier/login/page.tsx
+++ b/app/courrier/login/page.tsx
@@ -22,7 +22,7 @@ export default function MailLoginPage() {
setLoading(true);
try {
- const response = await fetch('/api/mail/login', {
+ const response = await fetch('/api/courrier/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx
index 8a33f9cc..f632e967 100644
--- a/app/courrier/page.tsx
+++ b/app/courrier/page.tsx
@@ -429,13 +429,13 @@ export default function CourrierPage() {
const checkCredentials = async () => {
try {
console.log('Checking for stored credentials...');
- const response = await fetch('/api/mail');
+ const response = await fetch('/api/courrier');
if (!response.ok) {
const errorData = await response.json();
console.log('API response error:', errorData);
if (errorData.error === 'No stored credentials found') {
console.log('No credentials found, redirecting to login...');
- router.push('/mail/login');
+ router.push('/courrier/login');
return;
}
throw new Error(errorData.error || 'Failed to check credentials');
@@ -463,7 +463,7 @@ export default function CourrierPage() {
}
setError(null);
- const response = await fetch(`/api/mail?folder=${currentView}&page=${page}&limit=${emailsPerPage}`);
+ const response = await fetch(`/api/courrier?folder=${currentView}&page=${page}&limit=${emailsPerPage}`);
if (!response.ok) {
throw new Error('Failed to load emails');
}
@@ -557,7 +557,7 @@ export default function CourrierPage() {
setSelectedEmail(email);
// Fetch the full email content
- const response = await fetch(`/api/mail/${emailId}`);
+ const response = await fetch(`/api/courrier/${emailId}`);
if (!response.ok) {
throw new Error('Failed to fetch full email content');
}
@@ -575,7 +575,7 @@ export default function CourrierPage() {
// Try to mark as read in the background
try {
- const markReadResponse = await fetch(`/api/mail/mark-read`, {
+ const markReadResponse = await fetch(`/api/courrier/mark-read`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -630,7 +630,7 @@ export default function CourrierPage() {
}
try {
- const response = await fetch('/api/mail/bulk-actions', {
+ const response = await fetch('/api/courrier/bulk-actions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -673,7 +673,7 @@ export default function CourrierPage() {
// Add handleDeleteConfirm function
const handleDeleteConfirm = async () => {
try {
- const response = await fetch('/api/mail/bulk-actions', {
+ const response = await fetch('/api/courrier/bulk-actions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -1221,7 +1221,7 @@ export default function CourrierPage() {
}
try {
- const response = await fetch('/api/mail/send', {
+ const response = await fetch('/api/courrier/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -1271,7 +1271,7 @@ export default function CourrierPage() {
if (!email) return;
try {
- const response = await fetch('/api/mail/toggle-star', {
+ const response = await fetch('/api/courrier/toggle-star', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -1369,7 +1369,7 @@ export default function CourrierPage() {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
- const response = await fetch(`/api/mail?folder=${encodeURIComponent(newMailbox)}&page=1&limit=${emailsPerPage}`, {
+ const response = await fetch(`/api/courrier?folder=${encodeURIComponent(newMailbox)}&page=1&limit=${emailsPerPage}`, {
signal: controller.signal
});
diff --git a/app/mail/login/page.tsx b/app/mail/login/page.tsx
deleted file mode 100644
index 3782f9bd..00000000
--- a/app/mail/login/page.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-import { useRouter } from 'next/navigation';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { Button } from '@/components/ui/button';
-import { Input } from '@/components/ui/input';
-import { Label } from '@/components/ui/label';
-
-export default function MailLoginPage() {
- const router = useRouter();
- const [email, setEmail] = useState('');
- const [password, setPassword] = useState('');
- const [host, setHost] = useState('mail.infomaniak.com');
- const [port, setPort] = useState('993');
- const [error, setError] = useState('');
- const [loading, setLoading] = useState(false);
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- setError('');
- setLoading(true);
-
- try {
- const response = await fetch('/api/mail/login', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- email,
- password,
- host,
- port,
- }),
- });
-
- const data = await response.json();
-
- if (!response.ok) {
- throw new Error(data.error || 'Failed to connect to email server');
- }
-
- // Redirect to mail page
- router.push('/mail');
- } catch (err) {
- setError(err instanceof Error ? err.message : 'An error occurred');
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
-
- Email Configuration
-
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/app/mail/page.tsx b/app/mail/page.tsx
deleted file mode 100644
index 8c1b3e97..00000000
--- a/app/mail/page.tsx
+++ /dev/null
@@ -1,1778 +0,0 @@
-'use client';
-
-import { useEffect, useState, useMemo, useCallback } from 'react';
-import { useRouter } from 'next/navigation';
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
-import { Input } from '@/components/ui/input';
-import { Button } from '@/components/ui/button';
-import { Label } from '@/components/ui/label';
-import { Textarea } from '@/components/ui/textarea';
-import { Checkbox } from '@/components/ui/checkbox';
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
-} from "@/components/ui/alert-dialog";
-import { Avatar, AvatarFallback } from '@/components/ui/avatar';
-import {
- MoreVertical, Settings, Plus as PlusIcon, Trash2, Edit, Mail,
- Inbox, Send, Star, Trash, Plus, ChevronLeft, ChevronRight,
- Search, ChevronDown, Folder, ChevronUp, Reply, Forward, ReplyAll,
- MoreHorizontal, FolderOpen, X, Paperclip, MessageSquare, Copy, EyeOff,
- AlertOctagon, Archive, RefreshCw
-} from 'lucide-react';
-import { ScrollArea } from '@/components/ui/scroll-area';
-import {
- decodeQuotedPrintable,
- decodeBase64,
- convertCharset,
- cleanHtml,
- parseEmailHeaders,
- extractBoundary,
- extractFilename,
- extractHeader
-} from '@/lib/infomaniak-mime-decoder';
-
-interface Account {
- id: number;
- name: string;
- email: string;
- color: string;
- folders?: string[];
-}
-
-interface Email {
- id: number;
- accountId: number;
- from: string;
- fromName?: string;
- to: string;
- subject: string;
- body: string;
- date: string;
- read: boolean;
- starred: boolean;
- folder: string;
- cc?: string;
- bcc?: string;
- flags?: string[];
-}
-
-interface Attachment {
- name: string;
- type: string;
- content: string;
- encoding: string;
-}
-
-interface EmailAttachment {
- filename: string;
- contentType: string;
- encoding: string;
- content: string;
-}
-
-interface ParsedEmail {
- text: string | null;
- html: string | null;
- attachments: Array<{
- filename: string;
- contentType: string;
- encoding: string;
- content: string;
- }>;
-}
-
-interface EmailMessage {
- subject: string;
- from: string;
- to: string;
- date: string;
- contentType: string;
- text: string | null;
- html: string | null;
- attachments: EmailAttachment[];
- raw: {
- headers: string;
- body: string;
- };
-}
-
-function parseFullEmail(content: string): ParsedEmail {
- try {
- // First, try to parse the email headers
- const headers = parseEmailHeaders(content);
-
- // If it's a multipart email, process each part
- if (headers.contentType?.includes('multipart')) {
- const boundary = extractBoundary(headers.contentType);
- if (!boundary) {
- throw new Error('No boundary found in multipart content');
- }
-
- const parts = content.split(boundary);
- const result: ParsedEmail = {
- text: null,
- html: null,
- attachments: []
- };
-
- for (const part of parts) {
- if (!part.trim()) continue;
-
- const partHeaders = parseEmailHeaders(part);
- const partContent = part.split('\r\n\r\n')[1] || '';
-
- // Handle HTML content
- if (partHeaders.contentType?.includes('text/html')) {
- const decoded = decodeMIME(
- partContent,
- partHeaders.encoding || '7bit',
- partHeaders.charset || 'utf-8'
- );
- result.html = cleanHtml(decoded);
- }
- // Handle plain text content
- else if (partHeaders.contentType?.includes('text/plain')) {
- const decoded = decodeMIME(
- partContent,
- partHeaders.encoding || '7bit',
- partHeaders.charset || 'utf-8'
- );
- result.text = decoded;
- }
- // Handle attachments
- else if (partHeaders.contentType && !partHeaders.contentType.includes('text/')) {
- const filename = extractFilename(partHeaders.contentType) || 'attachment';
- result.attachments.push({
- filename,
- contentType: partHeaders.contentType,
- encoding: partHeaders.encoding || '7bit',
- content: partContent
- });
- }
- }
-
- return result;
- }
-
- // If it's not multipart, handle as a single part
- const body = content.split('\r\n\r\n')[1] || '';
- const decoded = decodeMIME(
- body,
- headers.encoding || '7bit',
- headers.charset || 'utf-8'
- );
-
- if (headers.contentType?.includes('text/html')) {
- return {
- html: cleanHtml(decoded),
- text: null,
- attachments: []
- };
- }
-
- return {
- html: null,
- text: decoded,
- attachments: []
- };
- } catch (e) {
- console.error('Error parsing email:', e);
- return {
- html: null,
- text: content,
- attachments: []
- };
- }
-}
-
-function processMultipartEmail(emailRaw: string, boundary: string, mainHeaders: string): ParsedEmail {
- const parts = emailRaw.split(new RegExp(`--${boundary}(?:--)?\\s*`, 'm'));
- const result: ParsedEmail = {
- text: '',
- html: '',
- attachments: []
- };
-
- for (const part of parts) {
- if (!part.trim()) continue;
-
- const [partHeaders, ...bodyParts] = part.split(/\r?\n\r?\n/);
- const partBody = bodyParts.join('\n\n');
- const partInfo = parseEmailHeaders(partHeaders);
-
- if (partInfo.contentType.startsWith('text/')) {
- let decodedContent = '';
-
- if (partInfo.encoding === 'quoted-printable') {
- decodedContent = decodeQuotedPrintable(partBody, partInfo.charset);
- } else if (partInfo.encoding === 'base64') {
- decodedContent = decodeBase64(partBody, partInfo.charset);
- } else {
- decodedContent = partBody;
- }
-
- if (partInfo.contentType.includes('html')) {
- decodedContent = cleanHtml(decodedContent);
- result.html = decodedContent;
- } else {
- result.text = decodedContent;
- }
- } else {
- // Handle attachment
- const filename = extractFilename(partHeaders);
- result.attachments.push({
- filename,
- contentType: partInfo.contentType,
- encoding: partInfo.encoding,
- content: partBody
- });
- }
- }
-
- return result;
-}
-
-function decodeMIME(text: string, encoding?: string, charset: string = 'utf-8'): string {
- if (!text) return '';
-
- // Normalize encoding and charset
- encoding = (encoding || '').toLowerCase();
- charset = (charset || 'utf-8').toLowerCase();
-
- try {
- // Handle different encoding types
- if (encoding === 'quoted-printable') {
- return decodeQuotedPrintable(text, charset);
- } else if (encoding === 'base64') {
- return decodeBase64(text, charset);
- } else if (encoding === '7bit' || encoding === '8bit' || encoding === 'binary') {
- // For these encodings, we still need to handle the character set
- return convertCharset(text, charset);
- } else {
- // Unknown encoding, return as is but still handle charset
- return convertCharset(text, charset);
- }
- } catch (error) {
- console.error('Error decoding MIME:', error);
- return text;
- }
-}
-
-function decodeMimeContent(content: string): string {
- if (!content) return '';
-
- // 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;
- }
- }
-
- // If not multipart or no boundary found, clean the content directly
- return cleanHtml(content);
-}
-
-function renderEmailContent(email: Email) {
- try {
- // Parse the full email content
- const parsed = parseFullEmail(email.body);
-
- // If we have HTML content, render it
- if (parsed.html) {
- return (
-
- );
- }
-
- // If we have text content, render it with proper formatting
- if (parsed.text) {
- return (
-
- {parsed.text.split('\n').map((line, i) => (
-
{line}
- ))}
-
- );
- }
-
- // If we have attachments but no content, show a message
- if (parsed.attachments.length > 0) {
- return (
-
- This email contains {parsed.attachments.length} attachment{parsed.attachments.length > 1 ? 's' : ''}.
-
- );
- }
-
- // If we couldn't parse the content, try to clean and display it
- const cleanedContent = cleanHtml(email.body);
- return (
-
- {cleanedContent.split('\n').map((line, i) => (
-
{line}
- ))}
-
- );
- } catch (e) {
- console.error('Error rendering email content:', e);
- return (
-
- Error rendering email content. Please try again later.
-
- );
- }
-}
-
-// 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
-type MailFolder = string;
-
-// Map IMAP folders to sidebar items with icons
-const getFolderIcon = (folder: string) => {
- switch (folder.toLowerCase()) {
- case 'inbox':
- return Inbox;
- case 'sent':
- return Send;
- case 'drafts':
- return Edit;
- case 'trash':
- return Trash;
- case 'spam':
- return AlertOctagon;
- case 'archive':
- case 'archives':
- return Archive;
- default:
- return Folder;
- }
-};
-
-// Initial sidebar items - only INBOX
-const initialSidebarItems = [
- {
- view: 'INBOX' as MailFolder,
- label: 'Inbox',
- icon: Inbox,
- folder: 'INBOX'
- }
-];
-
-export default function CourrierPage() {
- const router = useRouter();
- const [loading, setLoading] = useState(true);
- const [accounts, setAccounts] = useState([
- { id: 0, name: 'All', email: '', color: 'bg-gray-500' },
- { id: 1, name: 'Mail', email: 'alma@governance-labs.org', color: 'bg-blue-500' }
- ]);
- const [selectedAccount, setSelectedAccount] = useState(null);
- const [currentView, setCurrentView] = useState('INBOX');
- const [showCompose, setShowCompose] = useState(false);
- const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
- const [selectedEmails, setSelectedEmails] = useState([]);
- const [showBulkActions, setShowBulkActions] = useState(false);
- const [showBcc, setShowBcc] = useState(false);
- const [emails, setEmails] = useState([]);
- const [error, setError] = useState(null);
- const [composeSubject, setComposeSubject] = useState('');
- const [composeTo, setComposeTo] = useState('');
- const [composeCc, setComposeCc] = useState('');
- const [composeBcc, setComposeBcc] = useState('');
- const [composeBody, setComposeBody] = useState('');
- const [selectedEmail, setSelectedEmail] = useState(null);
- const [sidebarOpen, setSidebarOpen] = useState(true);
- const [foldersOpen, setFoldersOpen] = useState(true);
- const [showSettings, setShowSettings] = useState(false);
- const [searchQuery, setSearchQuery] = useState('');
- const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
- const [composeOpen, setComposeOpen] = useState(false);
- const [accountsDropdownOpen, setAccountsDropdownOpen] = useState(false);
- const [foldersDropdownOpen, setFoldersDropdownOpen] = useState(false);
- const [showAccountActions, setShowAccountActions] = useState(null);
- const [showEmailActions, setShowEmailActions] = useState(false);
- const [deleteType, setDeleteType] = useState<'email' | 'emails' | 'account'>('email');
- const [itemToDelete, setItemToDelete] = useState(null);
- const [showCc, setShowCc] = useState(false);
- const [contentLoading, setContentLoading] = useState(false);
- const [attachments, setAttachments] = useState([]);
- const [folders, setFolders] = useState([]);
- const [unreadCount, setUnreadCount] = useState(0);
- const [availableFolders, setAvailableFolders] = useState([]);
- const [sidebarItems, setSidebarItems] = useState(initialSidebarItems);
- const [page, setPage] = useState(1);
- const [hasMore, setHasMore] = useState(true);
- const [isLoadingMore, setIsLoadingMore] = useState(false);
- const emailsPerPage = 24;
-
- // Debug logging for email distribution
- useEffect(() => {
- const emailsByFolder = emails.reduce((acc, email) => {
- acc[email.folder] = (acc[email.folder] || 0) + 1;
- return acc;
- }, {} as Record);
-
- console.log('Emails by folder:', emailsByFolder);
- console.log('Current view:', currentView);
- }, [emails, currentView]);
-
- // Move getSelectedEmail inside the component
- const getSelectedEmail = () => {
- return emails.find(email => email.id === selectedEmail?.id);
- };
-
- // Check for stored credentials
- useEffect(() => {
- const checkCredentials = async () => {
- try {
- console.log('Checking for stored credentials...');
- const response = await fetch('/api/mail');
- if (!response.ok) {
- const errorData = await response.json();
- console.log('API response error:', errorData);
- if (errorData.error === 'No stored credentials found') {
- console.log('No credentials found, redirecting to login...');
- router.push('/mail/login');
- return;
- }
- throw new Error(errorData.error || 'Failed to check credentials');
- }
- console.log('Credentials verified, loading emails...');
- setLoading(false);
- loadEmails();
- } catch (err) {
- console.error('Error checking credentials:', err);
- setError(err instanceof Error ? err.message : 'Failed to check credentials');
- setLoading(false);
- }
- };
-
- checkCredentials();
- }, [router]);
-
- // Update the loadEmails function
- const loadEmails = async (isLoadMore = false) => {
- try {
- if (isLoadMore) {
- setIsLoadingMore(true);
- } else {
- setLoading(true);
- }
- setError(null);
-
- const response = await fetch(`/api/mail?folder=${currentView}&page=${page}&limit=${emailsPerPage}`);
- if (!response.ok) {
- throw new Error('Failed to load emails');
- }
-
- const data = await response.json();
-
- // Get available folders from the API response
- if (data.folders) {
- setAvailableFolders(data.folders);
- }
-
- // Process emails keeping exact folder names
- const processedEmails = (data.emails || []).map((email: any) => ({
- id: Number(email.id),
- accountId: 1,
- from: email.from || '',
- fromName: email.from?.split('@')[0] || '',
- to: email.to || '',
- subject: email.subject || '(No subject)',
- body: email.body || '',
- date: email.date || new Date().toISOString(),
- read: email.read || false,
- starred: email.starred || false,
- folder: email.folder || 'INBOX',
- cc: email.cc,
- bcc: email.bcc,
- flags: email.flags || []
- }));
-
- // Only update unread count if we're in the Inbox folder
- if (currentView === 'INBOX') {
- const unreadInboxEmails = processedEmails.filter(
- (email: Email) => !email.read && email.folder === 'INBOX'
- ).length;
- setUnreadCount(unreadInboxEmails);
- }
-
- // Update emails state based on whether we're loading more
- if (isLoadMore) {
- setEmails(prev => [...prev, ...processedEmails]);
- } else {
- setEmails(processedEmails);
- }
-
- // Update hasMore state based on the number of emails received
- setHasMore(processedEmails.length === emailsPerPage);
-
- } catch (err) {
- console.error('Error loading emails:', err);
- setError(err instanceof Error ? err.message : 'Failed to load emails');
- } finally {
- setLoading(false);
- setIsLoadingMore(false);
- }
- };
-
- // Add an effect to reload emails when the view changes
- useEffect(() => {
- setPage(1); // Reset page when view changes
- setHasMore(true);
- loadEmails();
- }, [currentView]);
-
- // Format date for display
- const formatDate = (dateString: string) => {
- const date = new Date(dateString);
- const now = new Date();
-
- if (date.toDateString() === now.toDateString()) {
- return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
- } else {
- return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
- }
- };
-
- // Get account color
- const getAccountColor = (accountId: number) => {
- const account = accounts.find(acc => acc.id === accountId);
- return account ? account.color : 'bg-gray-500';
- };
-
- // Update handleEmailSelect to set selectedEmail correctly
- const handleEmailSelect = (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);
- });
- }
- }
- };
-
- // Add these improved handlers
- const handleEmailCheckbox = (e: React.ChangeEvent, emailId: number) => {
- e.stopPropagation();
- if (e.target.checked) {
- setSelectedEmails([...selectedEmails, emailId.toString()]);
- } else {
- setSelectedEmails(selectedEmails.filter(id => id !== emailId.toString()));
- }
- };
-
- // Handles marking an individual email as read/unread
- const handleMarkAsRead = (emailId: string, isRead: boolean) => {
- setEmails(emails.map(email =>
- email.id.toString() === emailId ? { ...email, read: isRead } : email
- ));
- };
-
- // Handles bulk actions for selected emails
- const handleBulkAction = async (action: 'delete' | 'mark-read' | 'mark-unread' | 'archive') => {
- if (action === 'delete') {
- setDeleteType('emails');
- setShowDeleteConfirm(true);
- return;
- }
-
- try {
- const response = await fetch('/api/mail/bulk-actions', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- emailIds: selectedEmails,
- action: action
- }),
- });
-
- if (!response.ok) {
- throw new Error('Failed to perform bulk action');
- }
-
- // Update local state based on the action
- setEmails(emails.map(email => {
- if (selectedEmails.includes(email.id.toString())) {
- switch (action) {
- case 'mark-read':
- return { ...email, read: true };
- case 'mark-unread':
- return { ...email, read: false };
- case 'archive':
- return { ...email, folder: 'Archive' };
- default:
- return email;
- }
- }
- return email;
- }));
-
- // Clear selection after successful action
- setSelectedEmails([]);
- } catch (error) {
- console.error('Error performing bulk action:', error);
- alert('Failed to perform bulk action. Please try again.');
- }
- };
-
- // Add handleDeleteConfirm function
- const handleDeleteConfirm = async () => {
- try {
- const response = await fetch('/api/mail/bulk-actions', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- emailIds: selectedEmails,
- action: 'delete'
- }),
- });
-
- if (!response.ok) {
- throw new Error('Failed to delete emails');
- }
-
- // Remove deleted emails from state
- setEmails(emails.filter(email => !selectedEmails.includes(email.id.toString())));
- setSelectedEmails([]);
- } catch (error) {
- console.error('Error deleting emails:', error);
- alert('Failed to delete emails. Please try again.');
- } finally {
- setShowDeleteConfirm(false);
- }
- };
-
- // Add infinite scroll handler
- const handleScroll = useCallback((e: React.UIEvent) => {
- const target = e.currentTarget;
- if (
- target.scrollHeight - target.scrollTop === target.clientHeight &&
- !isLoadingMore &&
- hasMore
- ) {
- setPage(prev => prev + 1);
- loadEmails(true);
- }
- }, [isLoadingMore, hasMore]);
-
- // Sort emails by date (most recent first)
- const sortedEmails = useMemo(() => {
- return [...emails].sort((a, b) => {
- return new Date(b.date).getTime() - new Date(a.date).getTime();
- });
- }, [emails]);
-
- const toggleSelectAll = () => {
- if (selectedEmails.length === emails.length) {
- setSelectedEmails([]);
- } else {
- setSelectedEmails(emails.map(email => email.id.toString()));
- }
- };
-
- // Add filtered emails based on search query
- const filteredEmails = useMemo(() => {
- if (!searchQuery) return emails;
-
- const query = searchQuery.toLowerCase();
- return emails.filter(email =>
- email.subject.toLowerCase().includes(query) ||
- email.from.toLowerCase().includes(query) ||
- email.to.toLowerCase().includes(query) ||
- email.body.toLowerCase().includes(query)
- );
- }, [emails, searchQuery]);
-
- // Update the email list to use filtered emails
- const renderEmailList = () => (
-
- {renderEmailListHeader()}
- {renderBulkActionsToolbar()}
-
-
- {loading ? (
-
- ) : filteredEmails.length === 0 ? (
-
-
-
- {searchQuery ? 'No emails match your search' : 'No emails in this folder'}
-
-
- ) : (
-
- {filteredEmails.map((email) => renderEmailListItem(email))}
- {isLoadingMore && (
-
- )}
-
- )}
-
-
- );
-
- // Update the email count in the header to show filtered count
- const renderEmailListHeader = () => (
-
-
-
-
- setSearchQuery(e.target.value)}
- />
-
-
-
-
- 0 && selectedEmails.length === filteredEmails.length}
- onCheckedChange={toggleSelectAll}
- className="mt-0.5"
- />
- Inbox
-
-
- {searchQuery ? `${filteredEmails.length} of ${emails.length} emails` : `${emails.length} emails`}
-
-
-
- );
-
- // Update the bulk actions toolbar to include confirmation dialog
- const renderBulkActionsToolbar = () => {
- if (selectedEmails.length === 0) return null;
-
- return (
-
-
-
- {selectedEmails.length} selected
-
-
-
-
-
-
-
-
- );
- };
-
- // Keep only one renderEmailListWrapper function that includes both panels
- const renderEmailListWrapper = () => (
-
- {/* Email list panel */}
- {renderEmailList()}
-
- {/* Preview panel - will automatically take remaining space */}
-
- {selectedEmail ? (
- <>
- {/* Email actions header */}
-
-
-
-
-
-
- {selectedEmail.subject}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Scrollable content area */}
-
-
-
-
- {selectedEmail.fromName?.charAt(0) || selectedEmail.from.charAt(0)}
-
-
-
-
- {selectedEmail.fromName || selectedEmail.from}
-
-
- to {selectedEmail.to}
-
-
-
- {formatDate(selectedEmail.date)}
-
-
-
-
- {renderEmailContent(selectedEmail)}
-
-
- >
- ) : (
-
-
-
Select an email to view its contents
-
- )}
-
-
- );
-
- // Update sidebar items when available folders change
- useEffect(() => {
- if (availableFolders.length > 0) {
- const newItems = [
- ...initialSidebarItems,
- ...availableFolders
- .filter(folder => !['INBOX'].includes(folder)) // Exclude folders already in initial items
- .map(folder => ({
- view: folder as MailFolder,
- label: folder.charAt(0).toUpperCase() + folder.slice(1).toLowerCase(),
- icon: getFolderIcon(folder),
- folder: folder
- }))
- ];
- setSidebarItems(newItems);
- }
- }, [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;
- })()
- )}
-
-
-
-
- {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(/