mail page rest

This commit is contained in:
alma 2025-04-21 12:53:41 +02:00
parent 0139845f18
commit bf4c4400a5

View File

@ -60,6 +60,32 @@ interface Attachment {
encoding: string;
}
interface ParsedEmailContent {
text: string;
html: string;
attachments: {
filename: string;
contentType: string;
encoding: string;
content: string;
}[];
headers?: 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) ||
@ -414,12 +440,12 @@ function decodeMimeContent(content: string): string {
// Add this helper function
const renderEmailContent = (email: Email) => {
try {
const parsed = parseFullEmail(email.body);
const content = parsed.text || parsed.html || email.body;
const isHtml = parsed.html || content.includes('<');
const parsed = parseFullEmail(email.body) as ParsedEmailContent | ParsedEmailMetadata;
const content = 'text' in parsed ? parsed.text : ('html' in parsed ? parsed.html || '' : email.body);
const isHtml = 'html' in parsed ? !!parsed.html : content.includes('<');
if (isHtml) {
// Sanitize HTML content
// Enhanced HTML sanitization
const sanitizedHtml = content
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
@ -428,26 +454,90 @@ const renderEmailContent = (email: Email) => {
.replace(/javascript:/gi, '')
.replace(/data:/gi, '')
.replace(/<meta[^>]*>/gi, '')
.replace(/<link[^>]*>/gi, '');
.replace(/<link[^>]*>/gi, '')
// Fix common email client quirks
.replace(/=3D/g, '=')
.replace(/=20/g, ' ')
.replace(/=E2=80=99/g, "'")
.replace(/=E2=80=9C/g, '"')
.replace(/=E2=80=9D/g, '"')
.replace(/=E2=80=93/g, '')
.replace(/=E2=80=94/g, '—')
.replace(/=C2=A0/g, ' ')
.replace(/=C3=A0/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, 'û');
return (
<div
className="prose prose-sm max-w-none"
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
/>
<div className="prose prose-sm max-w-none">
{'attachments' in parsed && parsed.attachments && parsed.attachments.length > 0 && (
<div className="mb-4 p-2 bg-gray-50 rounded">
<h4 className="text-sm font-medium mb-2">Attachments:</h4>
<div className="space-y-1">
{parsed.attachments.map((attachment, index: number) => (
<div key={index} className="flex items-center gap-2 text-sm">
<Paperclip className="h-4 w-4 text-gray-500" />
<span>{attachment.filename}</span>
<span className="text-gray-500 text-xs">
({attachment.contentType})
</span>
</div>
))}
</div>
</div>
)}
<div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />
</div>
);
} else {
// Format plain text content
// Enhanced plain text formatting
const formattedText = content
.replace(/\n/g, '<br>')
.replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;')
.replace(/ /g, '&nbsp;&nbsp;');
.replace(/ /g, '&nbsp;&nbsp;')
// Fix common email client quirks
.replace(/=3D/g, '=')
.replace(/=20/g, ' ')
.replace(/=E2=80=99/g, "'")
.replace(/=E2=80=9C/g, '"')
.replace(/=E2=80=9D/g, '"')
.replace(/=E2=80=93/g, '')
.replace(/=E2=80=94/g, '—')
.replace(/=C2=A0/g, ' ')
.replace(/=C3=A0/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, 'û');
return (
<div
className="prose prose-sm max-w-none whitespace-pre-wrap"
dangerouslySetInnerHTML={{ __html: formattedText }}
/>
<div className="prose prose-sm max-w-none whitespace-pre-wrap">
{'attachments' in parsed && parsed.attachments && parsed.attachments.length > 0 && (
<div className="mb-4 p-2 bg-gray-50 rounded">
<h4 className="text-sm font-medium mb-2">Attachments:</h4>
<div className="space-y-1">
{parsed.attachments.map((attachment, index: number) => (
<div key={index} className="flex items-center gap-2 text-sm">
<Paperclip className="h-4 w-4 text-gray-500" />
<span>{attachment.filename}</span>
<span className="text-gray-500 text-xs">
({attachment.contentType})
</span>
</div>
))}
</div>
</div>
)}
<div dangerouslySetInnerHTML={{ __html: formattedText }} />
</div>
);
}
} catch (e) {
@ -510,7 +600,7 @@ const initialSidebarItems = [
}
];
export default function CourrierPage() {
export default function MailPage() {
const router = useRouter();
const [loading, setLoading] = useState(true);
const [accounts, setAccounts] = useState<Account[]>([
@ -1155,9 +1245,9 @@ export default function CourrierPage() {
// Get text content from parsed email
let preview = '';
if (parsed.text) {
if ('text' in parsed && parsed.text) {
preview = parsed.text;
} else if (parsed.html) {
} else if ('html' in parsed && parsed.html) {
// If only HTML is available, extract text content
preview = parsed.html
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
@ -1178,13 +1268,29 @@ export default function CourrierPage() {
// Clean up the preview
preview = preview
.replace(/^>+/gm, '')
.replace(/Content-Type:[^\n]+/g, '')
.replace(/^>+/gm, '') // Remove quoted text markers
.replace(/Content-Type:[^\n]+/g, '') // Remove MIME headers
.replace(/Content-Transfer-Encoding:[^\n]+/g, '')
.replace(/--[a-zA-Z0-9]+(-[a-zA-Z0-9]+)?/g, '')
.replace(/--[a-zA-Z0-9]+(-[a-zA-Z0-9]+)?/g, '') // Remove MIME boundaries
.replace(/boundary=[^\n]+/g, '')
.replace(/charset=[^\n]+/g, '')
.replace(/[\r\n]+/g, ' ')
.replace(/=3D/g, '=') // Fix common email client quirks
.replace(/=20/g, ' ')
.replace(/=E2=80=99/g, "'")
.replace(/=E2=80=9C/g, '"')
.replace(/=E2=80=9D/g, '"')
.replace(/=E2=80=93/g, '')
.replace(/=E2=80=94/g, '—')
.replace(/=C2=A0/g, ' ')
.replace(/=C3=A0/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, 'û')
.trim();
// Take first 100 characters
@ -1635,93 +1741,92 @@ export default function CourrierPage() {
}
return (
<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">
<>
{/* 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);
}}
>
<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>
<PlusIcon className="h-3.5 w-3.5" />
<span>Compose</span>
</div>
</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>
{/* 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>
{/* 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>
</Button>
</div>
))}
</div>
)}
</div>
{/* Navigation */}
{renderSidebarNav()}
<span className="text-xs text-gray-500 ml-4">{account.email}</span>
</div>
</Button>
</div>
))}
</div>
)}
</div>
{/* Main content area */}
<div className="flex-1 flex overflow-hidden">
{/* Email list panel */}
{renderEmailListWrapper()}
</div>
{/* Navigation */}
{renderSidebarNav()}
</div>
{/* Main content area */}
<div className="flex-1 flex overflow-hidden">
{/* Email list panel */}
{renderEmailListWrapper()}
</div>
</div>
@ -1886,6 +1991,6 @@ export default function CourrierPage() {
</div>
)}
{renderDeleteConfirmDialog()}
</main>
</>
);
}