mail page rest
This commit is contained in:
parent
4b15a9a5be
commit
0de5f61bce
@ -61,15 +61,14 @@ interface Attachment {
|
||||
}
|
||||
|
||||
interface ParsedEmailContent {
|
||||
text: string;
|
||||
html: string;
|
||||
attachments: {
|
||||
text: string | null;
|
||||
html: string | null;
|
||||
attachments: Array<{
|
||||
filename: string;
|
||||
contentType: string;
|
||||
encoding: string;
|
||||
content: string;
|
||||
}[];
|
||||
headers?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface ParsedEmailMetadata {
|
||||
@ -122,119 +121,115 @@ function decodeQuotedPrintable(text: string, charset: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function parseFullEmail(emailRaw: string) {
|
||||
function parseFullEmail(emailRaw: string): ParsedEmailContent {
|
||||
console.log('=== parseFullEmail Debug ===');
|
||||
console.log('Input email length:', emailRaw.length);
|
||||
console.log('First 200 chars:', emailRaw.substring(0, 200));
|
||||
|
||||
// Check if this is a multipart message by looking for boundary definition
|
||||
const boundaryMatch = emailRaw.match(/boundary="?([^"\r\n;]+)"?/i) ||
|
||||
emailRaw.match(/boundary=([^\r\n;]+)/i);
|
||||
|
||||
console.log('Boundary found:', boundaryMatch ? boundaryMatch[1] : 'No boundary');
|
||||
|
||||
if (boundaryMatch) {
|
||||
const boundary = boundaryMatch[1].trim();
|
||||
|
||||
// Check if there's a preamble before the first boundary
|
||||
let mainHeaders = '';
|
||||
let mainContent = emailRaw;
|
||||
|
||||
// Extract the headers before the first boundary if they exist
|
||||
const firstBoundaryPos = emailRaw.indexOf('--' + boundary);
|
||||
if (firstBoundaryPos > 0) {
|
||||
const headerSeparatorPos = emailRaw.indexOf('\r\n\r\n');
|
||||
if (headerSeparatorPos > 0 && headerSeparatorPos < firstBoundaryPos) {
|
||||
mainHeaders = emailRaw.substring(0, headerSeparatorPos);
|
||||
}
|
||||
}
|
||||
|
||||
return processMultipartEmail(emailRaw, boundary, mainHeaders);
|
||||
} else {
|
||||
// This is a single part message
|
||||
return processSinglePartEmail(emailRaw);
|
||||
}
|
||||
}
|
||||
|
||||
function processMultipartEmail(emailRaw: string, boundary: string, mainHeaders: string = ''): {
|
||||
text: string;
|
||||
html: string;
|
||||
attachments: { filename: string; contentType: string; encoding: string; content: string; }[];
|
||||
headers?: string;
|
||||
} {
|
||||
const result = {
|
||||
text: '',
|
||||
html: '',
|
||||
attachments: [] as { filename: string; contentType: string; encoding: string; content: string; }[],
|
||||
headers: mainHeaders
|
||||
// Split headers and body
|
||||
const headerBodySplit = emailRaw.split(/\r?\n\r?\n/);
|
||||
const headers = headerBodySplit[0];
|
||||
const body = headerBodySplit.slice(1).join('\n\n');
|
||||
|
||||
// Parse content type from headers
|
||||
const contentTypeMatch = headers.match(/Content-Type:\s*([^;]+)/i);
|
||||
const contentType = contentTypeMatch ? contentTypeMatch[1].trim().toLowerCase() : 'text/plain';
|
||||
|
||||
// Initialize result
|
||||
const result: ParsedEmailContent = {
|
||||
text: null,
|
||||
html: null,
|
||||
attachments: []
|
||||
};
|
||||
|
||||
// Split by boundary (more robust pattern)
|
||||
const boundaryRegex = new RegExp(`--${boundary}(?:--)?(\\r?\\n|$)`, 'g');
|
||||
|
||||
// Get all boundary positions
|
||||
const matches = Array.from(emailRaw.matchAll(boundaryRegex));
|
||||
const boundaryPositions = matches.map(match => match.index!);
|
||||
|
||||
// Extract content between boundaries
|
||||
for (let i = 0; i < boundaryPositions.length - 1; i++) {
|
||||
const startPos = boundaryPositions[i] + matches[i][0].length;
|
||||
const endPos = boundaryPositions[i + 1];
|
||||
|
||||
if (endPos > startPos) {
|
||||
const partContent = emailRaw.substring(startPos, endPos).trim();
|
||||
|
||||
// Handle multipart content
|
||||
if (contentType.includes('multipart')) {
|
||||
const boundaryMatch = emailRaw.match(/boundary="?([^"\r\n;]+)"?/i);
|
||||
if (boundaryMatch) {
|
||||
const boundary = boundaryMatch[1].trim();
|
||||
const parts = emailRaw.split(new RegExp(`--${boundary}(?:--)?(\\r?\\n|$)`));
|
||||
|
||||
if (partContent) {
|
||||
const decoded = processSinglePartEmail(partContent);
|
||||
for (const part of parts) {
|
||||
if (!part.trim()) continue;
|
||||
|
||||
if (decoded.contentType.includes('text/plain')) {
|
||||
result.text = decoded.text || '';
|
||||
} else if (decoded.contentType.includes('text/html')) {
|
||||
result.html = cleanHtml(decoded.html || '');
|
||||
} else if (
|
||||
decoded.contentType.startsWith('image/') ||
|
||||
decoded.contentType.startsWith('application/')
|
||||
) {
|
||||
const filename = extractFilename(partContent);
|
||||
const partHeaderBodySplit = part.split(/\r?\n\r?\n/);
|
||||
const partHeaders = partHeaderBodySplit[0];
|
||||
const partBody = partHeaderBodySplit.slice(1).join('\n\n');
|
||||
|
||||
const partContentTypeMatch = partHeaders.match(/Content-Type:\s*([^;]+)/i);
|
||||
const partContentType = partContentTypeMatch ? partContentTypeMatch[1].trim().toLowerCase() : 'text/plain';
|
||||
|
||||
if (partContentType.includes('text/plain')) {
|
||||
result.text = decodeEmailBody(partBody, partContentType);
|
||||
} else if (partContentType.includes('text/html')) {
|
||||
result.html = decodeEmailBody(partBody, partContentType);
|
||||
} else if (partContentType.startsWith('image/') || partContentType.startsWith('application/')) {
|
||||
const filenameMatch = partHeaders.match(/filename="?([^"\r\n;]+)"?/i);
|
||||
const filename = filenameMatch ? filenameMatch[1] : 'attachment';
|
||||
|
||||
result.attachments.push({
|
||||
filename,
|
||||
contentType: decoded.contentType,
|
||||
encoding: decoded.raw?.headers ? parseEmailHeaders(decoded.raw.headers).encoding : '7bit',
|
||||
content: decoded.raw?.body || ''
|
||||
contentType: partContentType,
|
||||
encoding: 'base64',
|
||||
content: partBody
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Single part content
|
||||
if (contentType.includes('text/html')) {
|
||||
result.html = decodeEmailBody(body, contentType);
|
||||
} else {
|
||||
result.text = decodeEmailBody(body, contentType);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function processSinglePartEmail(rawEmail: string) {
|
||||
// Split headers and body
|
||||
const headerBodySplit = rawEmail.split(/\r?\n\r?\n/);
|
||||
const headers = headerBodySplit[0];
|
||||
const body = headerBodySplit.slice(1).join('\n\n');
|
||||
|
||||
// Parse headers to get content type, encoding, etc.
|
||||
const emailInfo = parseEmailHeaders(headers);
|
||||
|
||||
// Decode the body based on its encoding
|
||||
const decodedBody = decodeMIME(body, emailInfo.encoding, emailInfo.charset);
|
||||
|
||||
return {
|
||||
subject: extractHeader(headers, 'Subject'),
|
||||
from: extractHeader(headers, 'From'),
|
||||
to: extractHeader(headers, 'To'),
|
||||
date: extractHeader(headers, 'Date'),
|
||||
contentType: emailInfo.contentType,
|
||||
text: emailInfo.contentType.includes('html') ? null : decodedBody,
|
||||
html: emailInfo.contentType.includes('html') ? decodedBody : null,
|
||||
raw: {
|
||||
headers,
|
||||
body
|
||||
function decodeEmailBody(content: string, contentType: string): string {
|
||||
try {
|
||||
// Remove email client-specific markers
|
||||
content = content.replace(/\r\n/g, '\n')
|
||||
.replace(/=\n/g, '')
|
||||
.replace(/=3D/g, '=')
|
||||
.replace(/=09/g, '\t');
|
||||
|
||||
// If it's HTML content
|
||||
if (contentType.includes('text/html')) {
|
||||
return extractTextFromHtml(content);
|
||||
}
|
||||
};
|
||||
|
||||
return content;
|
||||
} catch (error) {
|
||||
console.error('Error decoding email body:', error);
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
function extractTextFromHtml(html: string): string {
|
||||
// Remove scripts and style tags
|
||||
html = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
|
||||
|
||||
// Convert <br> and <p> to newlines
|
||||
html = html.replace(/<br[^>]*>/gi, '\n')
|
||||
.replace(/<p[^>]*>/gi, '\n')
|
||||
.replace(/<\/p>/gi, '\n');
|
||||
|
||||
// Remove all other HTML tags
|
||||
html = html.replace(/<[^>]+>/g, '');
|
||||
|
||||
// Decode HTML entities
|
||||
html = html.replace(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
|
||||
// Clean up whitespace
|
||||
return html.replace(/\n\s*\n/g, '\n\n').trim();
|
||||
}
|
||||
|
||||
function extractHeader(headers: string, headerName: string): string {
|
||||
@ -454,54 +449,26 @@ const renderEmailContent = (email: Email) => {
|
||||
try {
|
||||
const parsed = parseFullEmail(email.body);
|
||||
console.log('Parsed content:', {
|
||||
hasText: 'text' in parsed ? !!parsed.text : false,
|
||||
hasHtml: 'html' in parsed ? !!parsed.html : false,
|
||||
textPreview: 'text' in parsed ? parsed.text?.substring(0, 100) : 'No text',
|
||||
htmlPreview: 'html' in parsed ? parsed.html?.substring(0, 100) : 'No HTML'
|
||||
hasText: !!parsed.text,
|
||||
hasHtml: !!parsed.html,
|
||||
textPreview: parsed.text?.substring(0, 100) || 'No text',
|
||||
htmlPreview: parsed.html?.substring(0, 100) || 'No HTML'
|
||||
});
|
||||
|
||||
const isHtml = 'html' in parsed ? !!parsed.html : email.body.includes('<');
|
||||
const content = 'text' in parsed ? parsed.text : ('html' in parsed ? parsed.html || '' : email.body);
|
||||
|
||||
const content = parsed.html || parsed.text || email.body;
|
||||
const isHtml = !!parsed.html;
|
||||
|
||||
console.log('Selected content type:', isHtml ? 'HTML' : 'Plain text');
|
||||
console.log('Content preview:', content.substring(0, 100) + '...');
|
||||
|
||||
if (isHtml) {
|
||||
// Enhanced HTML sanitization
|
||||
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 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">
|
||||
{'attachments' in parsed && parsed.attachments && parsed.attachments.length > 0 && (
|
||||
{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) => (
|
||||
{parsed.attachments.map((attachment, index) => (
|
||||
<div key={index} className="flex items-center gap-2 text-sm">
|
||||
<Paperclip className="h-4 w-4 text-gray-500" />
|
||||
<span>{attachment.filename}</span>
|
||||
@ -513,40 +480,17 @@ const renderEmailContent = (email: Email) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: content }} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// Enhanced plain text formatting
|
||||
const formattedText = content
|
||||
.replace(/\n/g, '<br>')
|
||||
.replace(/\t/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">
|
||||
{'attachments' in parsed && parsed.attachments && parsed.attachments.length > 0 && (
|
||||
{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) => (
|
||||
{parsed.attachments.map((attachment, index) => (
|
||||
<div key={index} className="flex items-center gap-2 text-sm">
|
||||
<Paperclip className="h-4 w-4 text-gray-500" />
|
||||
<span>{attachment.filename}</span>
|
||||
@ -558,7 +502,7 @@ const renderEmailContent = (email: Email) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div dangerouslySetInnerHTML={{ __html: formattedText }} />
|
||||
<div>{content}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1287,17 +1231,17 @@ export default function MailPage() {
|
||||
try {
|
||||
const parsed = parseFullEmail(email.body);
|
||||
console.log('Parsed content:', {
|
||||
hasText: 'text' in parsed ? !!parsed.text : false,
|
||||
hasHtml: 'html' in parsed ? !!parsed.html : false,
|
||||
textPreview: 'text' in parsed ? parsed.text?.substring(0, 100) : 'No text',
|
||||
htmlPreview: 'html' in parsed ? parsed.html?.substring(0, 100) : 'No HTML'
|
||||
hasText: !!parsed.text,
|
||||
hasHtml: !!parsed.html,
|
||||
textPreview: parsed.text?.substring(0, 100) || 'No text',
|
||||
htmlPreview: parsed.html?.substring(0, 100) || 'No HTML'
|
||||
});
|
||||
|
||||
let preview = '';
|
||||
if ('text' in parsed && parsed.text) {
|
||||
if (parsed.text) {
|
||||
preview = parsed.text;
|
||||
console.log('Using text content for preview');
|
||||
} else if ('html' in parsed && parsed.html) {
|
||||
} else if (parsed.html) {
|
||||
preview = parsed.html
|
||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
||||
@ -1327,22 +1271,6 @@ export default function MailPage() {
|
||||
.replace(/boundary=[^\n]+/g, '')
|
||||
.replace(/charset=[^\n]+/g, '')
|
||||
.replace(/[\r\n]+/g, ' ')
|
||||
.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, 'û')
|
||||
.trim();
|
||||
|
||||
// Take first 100 characters
|
||||
@ -1357,11 +1285,10 @@ export default function MailPage() {
|
||||
preview += '...';
|
||||
}
|
||||
|
||||
console.log('Final preview:', preview);
|
||||
return preview || 'No preview available';
|
||||
return preview;
|
||||
} catch (e) {
|
||||
console.error('Error in generateEmailPreview:', e);
|
||||
return 'Error generating preview';
|
||||
console.error('Error generating preview:', e);
|
||||
return '(No preview available)';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user