mail page rest
This commit is contained in:
parent
037977b3fb
commit
0139845f18
@ -48,70 +48,22 @@ export async function GET() {
|
||||
await client.connect();
|
||||
const mailbox = await client.mailboxOpen('INBOX');
|
||||
|
||||
// Fetch messages with body content
|
||||
// Fetch only essential message data
|
||||
const messages = await client.fetch('1:20', {
|
||||
envelope: true,
|
||||
flags: true,
|
||||
bodyStructure: true,
|
||||
bodyParts: ['HEADER', 'TEXT']
|
||||
flags: true
|
||||
});
|
||||
|
||||
const result = [];
|
||||
for await (const message of messages) {
|
||||
// Get the body content
|
||||
let body = '';
|
||||
if (message.bodyParts && Array.isArray(message.bodyParts)) {
|
||||
for (const [partType, content] of message.bodyParts) {
|
||||
if (partType === 'text') {
|
||||
// Convert buffer to string and decode if needed
|
||||
const contentStr = content.toString('utf-8');
|
||||
// Check if content is base64 encoded
|
||||
if (contentStr.includes('Content-Transfer-Encoding: base64')) {
|
||||
const base64Content = contentStr.split('\r\n\r\n')[1];
|
||||
body = Buffer.from(base64Content, 'base64').toString('utf-8');
|
||||
} else {
|
||||
body = contentStr;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no body found, try to get it from the message structure
|
||||
if (!body && message.bodyStructure) {
|
||||
try {
|
||||
const fetch = await client.fetchOne(message.uid.toString(), {
|
||||
bodyStructure: true,
|
||||
bodyParts: ['TEXT']
|
||||
});
|
||||
if (fetch?.bodyParts) {
|
||||
for (const [partType, content] of fetch.bodyParts) {
|
||||
if (partType === 'text') {
|
||||
body = content.toString('utf-8');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching message content:', error);
|
||||
}
|
||||
}
|
||||
|
||||
result.push({
|
||||
id: message.uid.toString(),
|
||||
accountId: 1, // Default account ID
|
||||
from: message.envelope.from?.[0]?.address || '',
|
||||
fromName: message.envelope.from?.[0]?.name || message.envelope.from?.[0]?.address?.split('@')[0] || '',
|
||||
to: message.envelope.to?.[0]?.address || '',
|
||||
from: message.envelope.from[0].address,
|
||||
subject: message.envelope.subject || '(No subject)',
|
||||
body: body,
|
||||
date: message.envelope.date.toISOString(),
|
||||
read: message.flags.has('\\Seen'),
|
||||
starred: message.flags.has('\\Flagged'),
|
||||
folder: mailbox.path,
|
||||
cc: message.envelope.cc?.map(addr => addr.address).join(', '),
|
||||
bcc: message.envelope.bcc?.map(addr => addr.address).join(', '),
|
||||
flags: Array.from(message.flags)
|
||||
folder: mailbox.path
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -153,29 +153,9 @@ function processMultipartEmail(emailRaw: string, boundary: string, mainHeaders:
|
||||
const partContent = emailRaw.substring(startPos, endPos).trim();
|
||||
|
||||
if (partContent) {
|
||||
// Check if this is a nested multipart
|
||||
const nestedBoundaryMatch = partContent.match(/boundary="?([^"\r\n;]+)"?/i);
|
||||
if (nestedBoundaryMatch) {
|
||||
const nestedResult = processMultipartEmail(partContent, nestedBoundaryMatch[1]);
|
||||
result.text = result.text || nestedResult.text;
|
||||
result.html = result.html || nestedResult.html;
|
||||
result.attachments.push(...nestedResult.attachments);
|
||||
continue;
|
||||
}
|
||||
|
||||
const decoded = processSinglePartEmail(partContent);
|
||||
const contentDisposition = extractHeader(partContent, 'Content-Disposition');
|
||||
const isAttachment = contentDisposition.toLowerCase().includes('attachment');
|
||||
|
||||
if (isAttachment) {
|
||||
const filename = extractFilename(partContent) || 'attachment';
|
||||
result.attachments.push({
|
||||
filename,
|
||||
contentType: decoded.contentType,
|
||||
encoding: decoded.raw?.headers ? parseEmailHeaders(decoded.raw.headers).encoding : '7bit',
|
||||
content: decoded.raw?.body || ''
|
||||
});
|
||||
} else if (decoded.contentType.includes('text/plain')) {
|
||||
if (decoded.contentType.includes('text/plain')) {
|
||||
result.text = decoded.text || '';
|
||||
} else if (decoded.contentType.includes('text/html')) {
|
||||
result.html = cleanHtml(decoded.html || '');
|
||||
@ -243,37 +223,23 @@ function parseEmailHeaders(headers: string): { contentType: string; encoding: st
|
||||
charset: 'utf-8'
|
||||
};
|
||||
|
||||
// Extract content type and charset with better handling of quoted strings
|
||||
const contentTypeMatch = headers.match(/Content-Type:\s*([^;]+)(?:;\s*([^=]+)=([^;"\r\n]+|"[^"]*"))*/gi);
|
||||
// Extract content type and charset
|
||||
const contentTypeMatch = headers.match(/Content-Type:\s*([^;]+)(?:;\s*charset=([^;"\r\n]+)|(?:;\s*charset="([^"]+)"))?/i);
|
||||
if (contentTypeMatch) {
|
||||
const fullContentType = contentTypeMatch[0];
|
||||
const typeMatch = fullContentType.match(/Content-Type:\s*([^;]+)/i);
|
||||
if (typeMatch) {
|
||||
result.contentType = typeMatch[1].trim().toLowerCase();
|
||||
}
|
||||
|
||||
// Extract charset with better handling of quoted values
|
||||
const charsetMatch = fullContentType.match(/charset=([^;"\r\n]+|"[^"]*")/i);
|
||||
if (charsetMatch) {
|
||||
result.charset = charsetMatch[1].replace(/"/g, '').trim().toLowerCase();
|
||||
result.contentType = contentTypeMatch[1].trim().toLowerCase();
|
||||
if (contentTypeMatch[2]) {
|
||||
result.charset = contentTypeMatch[2].trim().toLowerCase();
|
||||
} else if (contentTypeMatch[3]) {
|
||||
result.charset = contentTypeMatch[3].trim().toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
// Extract content transfer encoding with better pattern matching
|
||||
// Extract content transfer encoding
|
||||
const encodingMatch = headers.match(/Content-Transfer-Encoding:\s*([^\s;\r\n]+)/i);
|
||||
if (encodingMatch) {
|
||||
result.encoding = encodingMatch[1].trim().toLowerCase();
|
||||
}
|
||||
|
||||
// Normalize charset names
|
||||
if (result.charset === 'iso-8859-1' || result.charset === 'latin1') {
|
||||
result.charset = 'iso-8859-1';
|
||||
} else if (result.charset === 'utf-8' || result.charset === 'utf8') {
|
||||
result.charset = 'utf-8';
|
||||
} else if (result.charset === 'windows-1252' || result.charset === 'cp1252') {
|
||||
result.charset = 'windows-1252';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -289,34 +255,17 @@ function decodeMIME(text: string, encoding?: string, charset: string = 'utf-8'):
|
||||
if (encoding === 'quoted-printable') {
|
||||
return decodeQuotedPrintable(text, charset);
|
||||
} else if (encoding === 'base64') {
|
||||
// Handle line breaks in base64 content
|
||||
const cleanText = text.replace(/[\r\n]/g, '');
|
||||
return decodeBase64(cleanText, charset);
|
||||
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, try to detect and handle
|
||||
if (text.match(/^[A-Za-z0-9+/=]+$/)) {
|
||||
// Looks like base64
|
||||
return decodeBase64(text, charset);
|
||||
} else if (text.includes('=') && text.match(/=[A-F0-9]{2}/)) {
|
||||
// Looks like quoted-printable
|
||||
return decodeQuotedPrintable(text, charset);
|
||||
} else {
|
||||
// Unknown encoding, return as is but still handle charset
|
||||
return convertCharset(text, charset);
|
||||
}
|
||||
// Unknown encoding, return as is but still handle charset
|
||||
return convertCharset(text, charset);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error decoding MIME:', error);
|
||||
// Try to recover by returning the original text with charset conversion
|
||||
try {
|
||||
return convertCharset(text, charset);
|
||||
} catch (e) {
|
||||
console.error('Error in fallback charset conversion:', e);
|
||||
return text;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
@ -328,14 +277,7 @@ function decodeBase64(text: string, charset: string): string {
|
||||
binaryString = atob(cleanText);
|
||||
} catch (e) {
|
||||
console.error('Base64 decoding error:', e);
|
||||
// Try to recover by removing invalid characters
|
||||
const validBase64 = cleanText.replace(/[^A-Za-z0-9+/=]/g, '');
|
||||
try {
|
||||
binaryString = atob(validBase64);
|
||||
} catch (e2) {
|
||||
console.error('Base64 recovery failed:', e2);
|
||||
return text;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
return convertCharset(binaryString, charset);
|
||||
@ -471,11 +413,51 @@ function decodeMimeContent(content: string): string {
|
||||
|
||||
// Add this helper function
|
||||
const renderEmailContent = (email: Email) => {
|
||||
const decodedContent = decodeMimeContent(email.body);
|
||||
if (email.body.includes('Content-Type: text/html')) {
|
||||
return <div dangerouslySetInnerHTML={{ __html: decodedContent }} />;
|
||||
try {
|
||||
const parsed = parseFullEmail(email.body);
|
||||
const content = parsed.text || parsed.html || email.body;
|
||||
const isHtml = parsed.html || content.includes('<');
|
||||
|
||||
if (isHtml) {
|
||||
// Sanitize HTML content
|
||||
const sanitizedHtml = content
|
||||
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||||
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
|
||||
.replace(/on\w+="[^"]*"/g, '')
|
||||
.replace(/on\w+='[^']*'/g, '')
|
||||
.replace(/javascript:/gi, '')
|
||||
.replace(/data:/gi, '')
|
||||
.replace(/<meta[^>]*>/gi, '')
|
||||
.replace(/<link[^>]*>/gi, '');
|
||||
|
||||
return (
|
||||
<div
|
||||
className="prose prose-sm max-w-none"
|
||||
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// Format plain text content
|
||||
const formattedText = content
|
||||
.replace(/\n/g, '<br>')
|
||||
.replace(/\t/g, ' ')
|
||||
.replace(/ /g, ' ');
|
||||
|
||||
return (
|
||||
<div
|
||||
className="prose prose-sm max-w-none whitespace-pre-wrap"
|
||||
dangerouslySetInnerHTML={{ __html: formattedText }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error rendering email content:', e);
|
||||
return (
|
||||
<div className="text-sm text-gray-500">
|
||||
Error rendering email content. Please try refreshing the page.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <div className="whitespace-pre-wrap">{decodedContent}</div>;
|
||||
};
|
||||
|
||||
// Add this helper function
|
||||
@ -528,7 +510,7 @@ const initialSidebarItems = [
|
||||
}
|
||||
];
|
||||
|
||||
export default function MailPage() {
|
||||
export default function CourrierPage() {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [accounts, setAccounts] = useState<Account[]>([
|
||||
@ -642,7 +624,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 || '',
|
||||
@ -908,20 +890,20 @@ export default function MailPage() {
|
||||
// Update the email count in the header to show filtered count
|
||||
const renderEmailListHeader = () => (
|
||||
<div className="border-b border-gray-100">
|
||||
<div className="px-4 py-2">
|
||||
<div className="px-4 py-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Search className="absolute left-2 top-2 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search in folder..."
|
||||
className="pl-8 h-9 bg-gray-50"
|
||||
className="pl-8 h-8 bg-gray-50"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-4 h-14">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center justify-between px-4 h-10">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={filteredEmails.length > 0 && selectedEmails.length === filteredEmails.length}
|
||||
onCheckedChange={toggleSelectAll}
|
||||
@ -1086,39 +1068,7 @@ export default function MailPage() {
|
||||
</div>
|
||||
|
||||
<div className="prose max-w-none">
|
||||
{(() => {
|
||||
try {
|
||||
const parsed = parseFullEmail(selectedEmail.body);
|
||||
return (
|
||||
<div>
|
||||
{/* Display HTML content if available, otherwise fallback to text */}
|
||||
<div dangerouslySetInnerHTML={{
|
||||
__html: parsed.html || parsed.text || decodeMimeContent(selectedEmail.body)
|
||||
}} />
|
||||
|
||||
{/* Display attachments if present */}
|
||||
{parsed.attachments && parsed.attachments.length > 0 && (
|
||||
<div className="mt-6 border-t border-gray-200 pt-6">
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-4">Attachments</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{parsed.attachments.map((attachment, index) => (
|
||||
<div key={index} className="flex items-center space-x-2 p-2 border rounded">
|
||||
<Paperclip className="h-4 w-4 text-gray-400" />
|
||||
<span className="text-sm text-gray-600 truncate">
|
||||
{attachment.filename}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Error parsing email:', e);
|
||||
return selectedEmail.body;
|
||||
}
|
||||
})()}
|
||||
{renderEmailContent(selectedEmail)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</>
|
||||
@ -1199,23 +1149,22 @@ export default function MailPage() {
|
||||
</h3>
|
||||
<div className="text-xs text-gray-500 truncate">
|
||||
{(() => {
|
||||
// Get clean preview of the actual message content
|
||||
let preview = '';
|
||||
try {
|
||||
// First try to parse the full email
|
||||
const parsed = parseFullEmail(email.body);
|
||||
|
||||
// Try to get content from parsed email
|
||||
if (parsed.html) {
|
||||
// Extract text from HTML
|
||||
// Get text content from parsed email
|
||||
let preview = '';
|
||||
if (parsed.text) {
|
||||
preview = parsed.text;
|
||||
} else if (parsed.html) {
|
||||
// If only HTML is available, extract text content
|
||||
preview = parsed.html
|
||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/ |‌|»|«|>/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
} else if (parsed.text) {
|
||||
preview = parsed.text;
|
||||
}
|
||||
|
||||
// If no preview from parsed content, try direct body
|
||||
@ -1227,7 +1176,7 @@ export default function MailPage() {
|
||||
.trim();
|
||||
}
|
||||
|
||||
// Remove email artifacts and clean up
|
||||
// Clean up the preview
|
||||
preview = preview
|
||||
.replace(/^>+/gm, '')
|
||||
.replace(/Content-Type:[^\n]+/g, '')
|
||||
@ -1250,12 +1199,11 @@ export default function MailPage() {
|
||||
preview += '...';
|
||||
}
|
||||
|
||||
return preview || 'No preview available';
|
||||
} catch (e) {
|
||||
console.error('Error generating preview:', e);
|
||||
preview = '(Error generating preview)';
|
||||
return 'Error loading preview';
|
||||
}
|
||||
|
||||
return preview || 'No preview available';
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
@ -1687,92 +1635,93 @@ export default function MailPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Main layout */}
|
||||
<div className="flex h-[calc(100vh-theme(spacing.12))] bg-gray-50 text-gray-900 overflow-hidden mt-12">
|
||||
{/* Sidebar */}
|
||||
<div className={`${sidebarOpen ? 'w-60' : 'w-16'} bg-white/95 backdrop-blur-sm border-r border-gray-100 flex flex-col transition-all duration-300 ease-in-out
|
||||
${mobileSidebarOpen ? 'fixed inset-y-0 left-0 z-40' : 'hidden'} md:block`}>
|
||||
{/* Courrier Title */}
|
||||
<div className="p-3 border-b border-gray-100">
|
||||
<div className="flex items-center gap-2">
|
||||
<Mail className="h-6 w-6 text-gray-600" />
|
||||
<span className="text-xl font-semibold text-gray-900">COURRIER</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Compose button and refresh button */}
|
||||
<div className="p-2 border-b border-gray-100 flex items-center gap-2">
|
||||
<Button
|
||||
className="flex-1 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center justify-center transition-all py-1.5 text-sm"
|
||||
onClick={() => {
|
||||
setShowCompose(true);
|
||||
setComposeTo('');
|
||||
setComposeCc('');
|
||||
setComposeBcc('');
|
||||
setComposeSubject('');
|
||||
setComposeBody('');
|
||||
setShowCc(false);
|
||||
setShowBcc(false);
|
||||
}}
|
||||
>
|
||||
<main className="w-full h-screen bg-black">
|
||||
<div className="w-full h-full px-4 pt-12 pb-4">
|
||||
<div className="flex h-full bg-background text-gray-900 overflow-hidden">
|
||||
{/* Sidebar */}
|
||||
<div className={`${sidebarOpen ? 'w-60' : 'w-16'} bg-white/95 backdrop-blur-sm border-r border-gray-100 flex flex-col transition-all duration-300 ease-in-out
|
||||
${mobileSidebarOpen ? 'fixed inset-y-0 left-0 z-40' : 'hidden'} md:block`}>
|
||||
{/* Courrier Title */}
|
||||
<div className="p-3 border-b border-gray-100">
|
||||
<div className="flex items-center gap-2">
|
||||
<PlusIcon className="h-3.5 w-3.5" />
|
||||
<span>Compose</span>
|
||||
<Mail className="h-6 w-6 text-gray-600" />
|
||||
<span className="text-xl font-semibold text-gray-900">COURRIER</span>
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleMailboxChange('INBOX')}
|
||||
className="text-gray-600 hover:text-gray-900 hover:bg-gray-100"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Accounts Section */}
|
||||
<div className="p-3 border-b border-gray-100">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-between mb-2 text-sm font-medium text-gray-500"
|
||||
onClick={() => setAccountsDropdownOpen(!accountsDropdownOpen)}
|
||||
>
|
||||
<span>Accounts</span>
|
||||
{accountsDropdownOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</Button>
|
||||
|
||||
{accountsDropdownOpen && (
|
||||
<div className="space-y-1 pl-2">
|
||||
{accounts.map(account => (
|
||||
<div key={account.id} className="relative group">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-between px-2 py-1.5 text-sm group"
|
||||
onClick={() => setSelectedAccount(account)}
|
||||
>
|
||||
<div className="flex flex-col items-start">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2.5 h-2.5 rounded-full ${account.color}`}></div>
|
||||
<span className="font-medium">{account.name}</span>
|
||||
{/* Compose button and refresh button */}
|
||||
<div className="p-2 border-b border-gray-100 flex items-center gap-2">
|
||||
<Button
|
||||
className="flex-1 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center justify-center transition-all py-1.5 text-sm"
|
||||
onClick={() => {
|
||||
setShowCompose(true);
|
||||
setComposeTo('');
|
||||
setComposeCc('');
|
||||
setComposeBcc('');
|
||||
setComposeSubject('');
|
||||
setComposeBody('');
|
||||
setShowCc(false);
|
||||
setShowBcc(false);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<PlusIcon className="h-3.5 w-3.5" />
|
||||
<span>Compose</span>
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleMailboxChange('INBOX')}
|
||||
className="text-gray-600 hover:text-gray-900 hover:bg-gray-100"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Accounts Section */}
|
||||
<div className="p-3 border-b border-gray-100">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-between mb-2 text-sm font-medium text-gray-500"
|
||||
onClick={() => setAccountsDropdownOpen(!accountsDropdownOpen)}
|
||||
>
|
||||
<span>Accounts</span>
|
||||
{accountsDropdownOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</Button>
|
||||
|
||||
{accountsDropdownOpen && (
|
||||
<div className="space-y-1 pl-2">
|
||||
{accounts.map(account => (
|
||||
<div key={account.id} className="relative group">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-between px-2 py-1.5 text-sm group"
|
||||
onClick={() => setSelectedAccount(account)}
|
||||
>
|
||||
<div className="flex flex-col items-start">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2.5 h-2.5 rounded-full ${account.color}`}></div>
|
||||
<span className="font-medium">{account.name}</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 ml-4">{account.email}</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 ml-4">{account.email}</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
{renderSidebarNav()}
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
{renderSidebarNav()}
|
||||
</div>
|
||||
|
||||
{/* Main content area */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Email list panel */}
|
||||
{renderEmailListWrapper()}
|
||||
{/* Main content area */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Email list panel */}
|
||||
{renderEmailListWrapper()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1937,6 +1886,6 @@ export default function MailPage() {
|
||||
</div>
|
||||
)}
|
||||
{renderDeleteConfirmDialog()}
|
||||
</>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user