'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 {
// First, parse the full email to get headers and body
const parsed = parseFullEmail(email.body);
// If we have HTML content, render it
if (parsed.html) {
return (
);
}
// If we have text content, render it
if (parsed.text) {
return (
{parsed.text.split('\n').map((line, i) => (
{line}
))}
);
}
// If we have attachments, display them
if (parsed.attachments && parsed.attachments.length > 0) {
return (
Attachments
{parsed.attachments.map((attachment, index) => (
{attachment.filename}
))}
);
}
// If we couldn't parse the content, try to decode and clean the raw body
const decodedBody = decodeMIME(email.body, 'quoted-printable', 'utf-8');
const cleanedContent = cleanHtml(decodedBody);
return (
{(() => {
// 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(/