mail page rest
This commit is contained in:
parent
d8cab53806
commit
037977b3fb
@ -153,9 +153,29 @@ function processMultipartEmail(emailRaw: string, boundary: string, mainHeaders:
|
|||||||
const partContent = emailRaw.substring(startPos, endPos).trim();
|
const partContent = emailRaw.substring(startPos, endPos).trim();
|
||||||
|
|
||||||
if (partContent) {
|
if (partContent) {
|
||||||
const decoded = processSinglePartEmail(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;
|
||||||
|
}
|
||||||
|
|
||||||
if (decoded.contentType.includes('text/plain')) {
|
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')) {
|
||||||
result.text = decoded.text || '';
|
result.text = decoded.text || '';
|
||||||
} else if (decoded.contentType.includes('text/html')) {
|
} else if (decoded.contentType.includes('text/html')) {
|
||||||
result.html = cleanHtml(decoded.html || '');
|
result.html = cleanHtml(decoded.html || '');
|
||||||
@ -223,23 +243,37 @@ function parseEmailHeaders(headers: string): { contentType: string; encoding: st
|
|||||||
charset: 'utf-8'
|
charset: 'utf-8'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Extract content type and charset
|
// Extract content type and charset with better handling of quoted strings
|
||||||
const contentTypeMatch = headers.match(/Content-Type:\s*([^;]+)(?:;\s*charset=([^;"\r\n]+)|(?:;\s*charset="([^"]+)"))?/i);
|
const contentTypeMatch = headers.match(/Content-Type:\s*([^;]+)(?:;\s*([^=]+)=([^;"\r\n]+|"[^"]*"))*/gi);
|
||||||
if (contentTypeMatch) {
|
if (contentTypeMatch) {
|
||||||
result.contentType = contentTypeMatch[1].trim().toLowerCase();
|
const fullContentType = contentTypeMatch[0];
|
||||||
if (contentTypeMatch[2]) {
|
const typeMatch = fullContentType.match(/Content-Type:\s*([^;]+)/i);
|
||||||
result.charset = contentTypeMatch[2].trim().toLowerCase();
|
if (typeMatch) {
|
||||||
} else if (contentTypeMatch[3]) {
|
result.contentType = typeMatch[1].trim().toLowerCase();
|
||||||
result.charset = contentTypeMatch[3].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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract content transfer encoding
|
// Extract content transfer encoding with better pattern matching
|
||||||
const encodingMatch = headers.match(/Content-Transfer-Encoding:\s*([^\s;\r\n]+)/i);
|
const encodingMatch = headers.match(/Content-Transfer-Encoding:\s*([^\s;\r\n]+)/i);
|
||||||
if (encodingMatch) {
|
if (encodingMatch) {
|
||||||
result.encoding = encodingMatch[1].trim().toLowerCase();
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,17 +289,34 @@ function decodeMIME(text: string, encoding?: string, charset: string = 'utf-8'):
|
|||||||
if (encoding === 'quoted-printable') {
|
if (encoding === 'quoted-printable') {
|
||||||
return decodeQuotedPrintable(text, charset);
|
return decodeQuotedPrintable(text, charset);
|
||||||
} else if (encoding === 'base64') {
|
} else if (encoding === 'base64') {
|
||||||
return decodeBase64(text, charset);
|
// Handle line breaks in base64 content
|
||||||
|
const cleanText = text.replace(/[\r\n]/g, '');
|
||||||
|
return decodeBase64(cleanText, charset);
|
||||||
} else if (encoding === '7bit' || encoding === '8bit' || encoding === 'binary') {
|
} else if (encoding === '7bit' || encoding === '8bit' || encoding === 'binary') {
|
||||||
// For these encodings, we still need to handle the character set
|
// For these encodings, we still need to handle the character set
|
||||||
return convertCharset(text, charset);
|
return convertCharset(text, charset);
|
||||||
} else {
|
} else {
|
||||||
// Unknown encoding, return as is but still handle charset
|
// Unknown encoding, try to detect and handle
|
||||||
return convertCharset(text, charset);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error decoding MIME:', error);
|
console.error('Error decoding MIME:', error);
|
||||||
return text;
|
// 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,7 +328,14 @@ function decodeBase64(text: string, charset: string): string {
|
|||||||
binaryString = atob(cleanText);
|
binaryString = atob(cleanText);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Base64 decoding error:', e);
|
console.error('Base64 decoding error:', e);
|
||||||
return text;
|
// 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 convertCharset(binaryString, charset);
|
return convertCharset(binaryString, charset);
|
||||||
@ -380,154 +438,44 @@ function cleanHtml(html: string): string {
|
|||||||
function decodeMimeContent(content: string): string {
|
function decodeMimeContent(content: string): string {
|
||||||
if (!content) return '';
|
if (!content) return '';
|
||||||
|
|
||||||
try {
|
// Check if this is an Infomaniak multipart message
|
||||||
// First, try to extract the content type and encoding
|
if (content.includes('Content-Type: multipart/')) {
|
||||||
const contentTypeMatch = content.match(/Content-Type:\s*([^;\r\n]+)/i);
|
const boundary = content.match(/boundary="([^"]+)"/)?.[1];
|
||||||
const encodingMatch = content.match(/Content-Transfer-Encoding:\s*([^\r\n]+)/i);
|
if (boundary) {
|
||||||
const charsetMatch = content.match(/charset="?([^"\r\n;]+)"?/i);
|
const parts = content.split('--' + boundary);
|
||||||
|
let htmlContent = '';
|
||||||
const contentType = contentTypeMatch ? contentTypeMatch[1].toLowerCase() : 'text/plain';
|
let textContent = '';
|
||||||
const encoding = encodingMatch ? encodingMatch[1].toLowerCase() : '7bit';
|
|
||||||
const charset = charsetMatch ? charsetMatch[1].toLowerCase() : 'utf-8';
|
|
||||||
|
|
||||||
// Handle multipart messages
|
parts.forEach(part => {
|
||||||
if (contentType.includes('multipart/')) {
|
if (part.includes('Content-Type: text/html')) {
|
||||||
const boundaryMatch = content.match(/boundary="?([^"\r\n;]+)"?/i);
|
const match = part.match(/\r?\n\r?\n([\s\S]+?)(?=\r?\n--)/);
|
||||||
if (boundaryMatch) {
|
if (match) {
|
||||||
const boundary = boundaryMatch[1];
|
htmlContent = cleanHtml(match[1]);
|
||||||
const parts = content.split('--' + boundary);
|
|
||||||
|
|
||||||
let htmlContent = '';
|
|
||||||
let textContent = '';
|
|
||||||
|
|
||||||
for (const part of parts) {
|
|
||||||
if (!part.trim()) continue;
|
|
||||||
|
|
||||||
const partContentType = part.match(/Content-Type:\s*([^;\r\n]+)/i)?.[1]?.toLowerCase() || '';
|
|
||||||
const partEncoding = part.match(/Content-Transfer-Encoding:\s*([^\r\n]+)/i)?.[1]?.toLowerCase() || '7bit';
|
|
||||||
const partCharset = part.match(/charset="?([^"\r\n;]+)"?/i)?.[1]?.toLowerCase() || 'utf-8';
|
|
||||||
|
|
||||||
// Extract the actual content (after headers)
|
|
||||||
const contentMatch = part.match(/\r?\n\r?\n([\s\S]+?)(?=\r?\n--)/);
|
|
||||||
if (!contentMatch) continue;
|
|
||||||
|
|
||||||
let partContent = contentMatch[1];
|
|
||||||
|
|
||||||
// Decode based on encoding
|
|
||||||
if (partEncoding === 'quoted-printable') {
|
|
||||||
partContent = decodeQuotedPrintable(partContent, partCharset);
|
|
||||||
} else if (partEncoding === 'base64') {
|
|
||||||
partContent = decodeBase64(partContent, partCharset);
|
|
||||||
}
|
}
|
||||||
|
} else if (part.includes('Content-Type: text/plain')) {
|
||||||
if (partContentType.includes('text/html')) {
|
const match = part.match(/\r?\n\r?\n([\s\S]+?)(?=\r?\n--)/);
|
||||||
htmlContent = partContent;
|
if (match) {
|
||||||
} else if (partContentType.includes('text/plain')) {
|
textContent = cleanHtml(match[1]);
|
||||||
textContent = partContent;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
// Prefer HTML content if available
|
|
||||||
return htmlContent || textContent || content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle single part messages
|
// Prefer HTML content if available
|
||||||
let decodedContent = content;
|
return htmlContent || textContent;
|
||||||
|
|
||||||
// Find the actual content (after headers)
|
|
||||||
const contentMatch = content.match(/\r?\n\r?\n([\s\S]+)/);
|
|
||||||
if (contentMatch) {
|
|
||||||
decodedContent = contentMatch[1];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode based on encoding
|
|
||||||
if (encoding === 'quoted-printable') {
|
|
||||||
decodedContent = decodeQuotedPrintable(decodedContent, charset);
|
|
||||||
} else if (encoding === 'base64') {
|
|
||||||
decodedContent = decodeBase64(decodedContent, charset);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up the content
|
|
||||||
return cleanHtml(decodedContent);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error decoding MIME content:', e);
|
|
||||||
return content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If not multipart or no boundary found, clean the content directly
|
||||||
|
return cleanHtml(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add this helper function
|
// Add this helper function
|
||||||
const renderEmailContent = (email: Email) => {
|
const renderEmailContent = (email: Email) => {
|
||||||
try {
|
const decodedContent = decodeMimeContent(email.body);
|
||||||
// First try to decode the MIME content
|
if (email.body.includes('Content-Type: text/html')) {
|
||||||
const decodedContent = decodeMimeContent(email.body);
|
return <div dangerouslySetInnerHTML={{ __html: decodedContent }} />;
|
||||||
|
|
||||||
// Check if the content is HTML
|
|
||||||
const isHtml = decodedContent.includes('<') &&
|
|
||||||
(decodedContent.includes('<html') ||
|
|
||||||
decodedContent.includes('<body') ||
|
|
||||||
decodedContent.includes('<div'));
|
|
||||||
|
|
||||||
if (isHtml) {
|
|
||||||
// Extract the body content if it exists
|
|
||||||
const bodyMatch = decodedContent.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
|
||||||
const content = bodyMatch ? bodyMatch[1] : decodedContent;
|
|
||||||
|
|
||||||
// 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, '')
|
|
||||||
// Fix common encoding issues
|
|
||||||
.replace(/=C2=A0/g, ' ')
|
|
||||||
.replace(/=E2=80=93/g, '\u2013')
|
|
||||||
.replace(/=E2=80=94/g, '\u2014')
|
|
||||||
.replace(/=E2=80=98/g, '\u2018')
|
|
||||||
.replace(/=E2=80=99/g, '\u2019')
|
|
||||||
.replace(/=E2=80=9C/g, '\u201C')
|
|
||||||
.replace(/=E2=80=9D/g, '\u201D');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="prose prose-sm max-w-none"
|
|
||||||
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Format plain text content
|
|
||||||
const formattedText = decodedContent
|
|
||||||
.replace(/\n/g, '<br>')
|
|
||||||
.replace(/\t/g, ' ')
|
|
||||||
.replace(/ /g, ' ')
|
|
||||||
// Fix common encoding issues
|
|
||||||
.replace(/=C2=A0/g, ' ')
|
|
||||||
.replace(/=E2=80=93/g, '\u2013')
|
|
||||||
.replace(/=E2=80=94/g, '\u2014')
|
|
||||||
.replace(/=E2=80=98/g, '\u2018')
|
|
||||||
.replace(/=E2=80=99/g, '\u2019')
|
|
||||||
.replace(/=E2=80=9C/g, '\u201C')
|
|
||||||
.replace(/=E2=80=9D/g, '\u201D');
|
|
||||||
|
|
||||||
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
|
// Add this helper function
|
||||||
@ -580,7 +528,7 @@ const initialSidebarItems = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function CourrierPage() {
|
export default function MailPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [accounts, setAccounts] = useState<Account[]>([
|
const [accounts, setAccounts] = useState<Account[]>([
|
||||||
@ -694,7 +642,7 @@ export default function CourrierPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process emails keeping exact folder names
|
// Process emails keeping exact folder names
|
||||||
const processedEmails = (data.emails || []).map((email: any) => ({
|
const processedEmails = data.emails.map((email: any) => ({
|
||||||
id: Number(email.id),
|
id: Number(email.id),
|
||||||
accountId: 1,
|
accountId: 1,
|
||||||
from: email.from || '',
|
from: email.from || '',
|
||||||
@ -960,20 +908,20 @@ export default function CourrierPage() {
|
|||||||
// Update the email count in the header to show filtered count
|
// Update the email count in the header to show filtered count
|
||||||
const renderEmailListHeader = () => (
|
const renderEmailListHeader = () => (
|
||||||
<div className="border-b border-gray-100">
|
<div className="border-b border-gray-100">
|
||||||
<div className="px-4 py-1">
|
<div className="px-4 py-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-2 top-2 h-4 w-4 text-gray-400" />
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-400" />
|
||||||
<Input
|
<Input
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="Search in folder..."
|
placeholder="Search in folder..."
|
||||||
className="pl-8 h-8 bg-gray-50"
|
className="pl-8 h-9 bg-gray-50"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between px-4 h-10">
|
<div className="flex items-center justify-between px-4 h-14">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-3">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={filteredEmails.length > 0 && selectedEmails.length === filteredEmails.length}
|
checked={filteredEmails.length > 0 && selectedEmails.length === filteredEmails.length}
|
||||||
onCheckedChange={toggleSelectAll}
|
onCheckedChange={toggleSelectAll}
|
||||||
@ -1138,7 +1086,39 @@ export default function CourrierPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="prose max-w-none">
|
<div className="prose max-w-none">
|
||||||
{renderEmailContent(selectedEmail)}
|
{(() => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</>
|
</>
|
||||||
@ -1219,32 +1199,43 @@ export default function CourrierPage() {
|
|||||||
</h3>
|
</h3>
|
||||||
<div className="text-xs text-gray-500 truncate">
|
<div className="text-xs text-gray-500 truncate">
|
||||||
{(() => {
|
{(() => {
|
||||||
|
// Get clean preview of the actual message content
|
||||||
|
let preview = '';
|
||||||
try {
|
try {
|
||||||
// First decode the MIME content
|
const parsed = parseFullEmail(email.body);
|
||||||
const decodedContent = decodeMimeContent(email.body);
|
|
||||||
|
|
||||||
// Extract preview text
|
// Try to get content from parsed email
|
||||||
let preview = decodedContent
|
if (parsed.html) {
|
||||||
// Remove HTML tags
|
// Extract text from HTML
|
||||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
preview = parsed.html
|
||||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
||||||
.replace(/<[^>]+>/g, ' ')
|
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
||||||
// Remove email headers
|
.replace(/<[^>]+>/g, ' ')
|
||||||
.replace(/^(From|To|Sent|Subject|Date|Cc|Bcc):.*$/gim, '')
|
.replace(/ |‌|»|«|>/g, ' ')
|
||||||
// Remove quoted text
|
.replace(/\s+/g, ' ')
|
||||||
.replace(/^>.*$/gm, '')
|
.trim();
|
||||||
// Remove multiple spaces
|
} else if (parsed.text) {
|
||||||
.replace(/\s+/g, ' ')
|
preview = parsed.text;
|
||||||
// Remove special characters
|
}
|
||||||
.replace(/ |‌|»|«|>/g, ' ')
|
|
||||||
// Fix common encoding issues
|
// If no preview from parsed content, try direct body
|
||||||
.replace(/=C2=A0/g, ' ')
|
if (!preview) {
|
||||||
.replace(/=E2=80=93/g, '\u2013')
|
preview = email.body
|
||||||
.replace(/=E2=80=94/g, '\u2014')
|
.replace(/<[^>]+>/g, ' ')
|
||||||
.replace(/=E2=80=98/g, '\u2018')
|
.replace(/ |‌|»|«|>/g, ' ')
|
||||||
.replace(/=E2=80=99/g, '\u2019')
|
.replace(/\s+/g, ' ')
|
||||||
.replace(/=E2=80=9C/g, '\u201C')
|
.trim();
|
||||||
.replace(/=E2=80=9D/g, '\u201D')
|
}
|
||||||
|
|
||||||
|
// Remove email artifacts and clean up
|
||||||
|
preview = preview
|
||||||
|
.replace(/^>+/gm, '')
|
||||||
|
.replace(/Content-Type:[^\n]+/g, '')
|
||||||
|
.replace(/Content-Transfer-Encoding:[^\n]+/g, '')
|
||||||
|
.replace(/--[a-zA-Z0-9]+(-[a-zA-Z0-9]+)?/g, '')
|
||||||
|
.replace(/boundary=[^\n]+/g, '')
|
||||||
|
.replace(/charset=[^\n]+/g, '')
|
||||||
|
.replace(/[\r\n]+/g, ' ')
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
// Take first 100 characters
|
// Take first 100 characters
|
||||||
@ -1259,11 +1250,12 @@ export default function CourrierPage() {
|
|||||||
preview += '...';
|
preview += '...';
|
||||||
}
|
}
|
||||||
|
|
||||||
return preview || 'No preview available';
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error generating preview:', e);
|
console.error('Error generating preview:', e);
|
||||||
return 'Error loading preview';
|
preview = '(Error generating preview)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return preview || 'No preview available';
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1695,93 +1687,92 @@ export default function CourrierPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="w-full h-screen bg-black">
|
<>
|
||||||
<div className="w-full h-full px-4 pt-12 pb-4">
|
{/* Main layout */}
|
||||||
<div className="flex h-full bg-background text-gray-900 overflow-hidden">
|
<div className="flex h-[calc(100vh-theme(spacing.12))] bg-gray-50 text-gray-900 overflow-hidden mt-12">
|
||||||
{/* Sidebar */}
|
{/* 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
|
<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`}>
|
${mobileSidebarOpen ? 'fixed inset-y-0 left-0 z-40' : 'hidden'} md:block`}>
|
||||||
{/* Courrier Title */}
|
{/* Courrier Title */}
|
||||||
<div className="p-3 border-b border-gray-100">
|
<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">
|
<div className="flex items-center gap-2">
|
||||||
<Mail className="h-6 w-6 text-gray-600" />
|
<PlusIcon className="h-3.5 w-3.5" />
|
||||||
<span className="text-xl font-semibold text-gray-900">COURRIER</span>
|
<span>Compose</span>
|
||||||
</div>
|
</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 */}
|
{/* Accounts Section */}
|
||||||
<div className="p-2 border-b border-gray-100 flex items-center gap-2">
|
<div className="p-3 border-b border-gray-100">
|
||||||
<Button
|
<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"
|
variant="ghost"
|
||||||
onClick={() => {
|
className="w-full justify-between mb-2 text-sm font-medium text-gray-500"
|
||||||
setShowCompose(true);
|
onClick={() => setAccountsDropdownOpen(!accountsDropdownOpen)}
|
||||||
setComposeTo('');
|
>
|
||||||
setComposeCc('');
|
<span>Accounts</span>
|
||||||
setComposeBcc('');
|
{accountsDropdownOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||||
setComposeSubject('');
|
</Button>
|
||||||
setComposeBody('');
|
|
||||||
setShowCc(false);
|
{accountsDropdownOpen && (
|
||||||
setShowBcc(false);
|
<div className="space-y-1 pl-2">
|
||||||
}}
|
{accounts.map(account => (
|
||||||
>
|
<div key={account.id} className="relative group">
|
||||||
<div className="flex items-center gap-2">
|
<Button
|
||||||
<PlusIcon className="h-3.5 w-3.5" />
|
variant="ghost"
|
||||||
<span>Compose</span>
|
className="w-full justify-between px-2 py-1.5 text-sm group"
|
||||||
</div>
|
onClick={() => setSelectedAccount(account)}
|
||||||
</Button>
|
>
|
||||||
<Button
|
<div className="flex flex-col items-start">
|
||||||
variant="ghost"
|
<div className="flex items-center gap-2">
|
||||||
size="icon"
|
<div className={`w-2.5 h-2.5 rounded-full ${account.color}`}></div>
|
||||||
onClick={() => handleMailboxChange('INBOX')}
|
<span className="font-medium">{account.name}</span>
|
||||||
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>
|
</div>
|
||||||
</Button>
|
<span className="text-xs text-gray-500 ml-4">{account.email}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
{/* Navigation */}
|
|
||||||
{renderSidebarNav()}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main content area */}
|
{/* Navigation */}
|
||||||
<div className="flex-1 flex overflow-hidden">
|
{renderSidebarNav()}
|
||||||
{/* Email list panel */}
|
</div>
|
||||||
{renderEmailListWrapper()}
|
|
||||||
</div>
|
{/* Main content area */}
|
||||||
|
<div className="flex-1 flex overflow-hidden">
|
||||||
|
{/* Email list panel */}
|
||||||
|
{renderEmailListWrapper()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1946,6 +1937,6 @@ export default function CourrierPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{renderDeleteConfirmDialog()}
|
{renderDeleteConfirmDialog()}
|
||||||
</main>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user