mail page rest
This commit is contained in:
parent
0139845f18
commit
bf4c4400a5
@ -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, ' ')
|
||||
.replace(/ /g, ' ');
|
||||
.replace(/ /g, ' ')
|
||||
// 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user