mail page rest
This commit is contained in:
parent
5d5275183a
commit
3702908cdf
@ -28,6 +28,16 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
|
import {
|
||||||
|
decodeQuotedPrintable,
|
||||||
|
decodeBase64,
|
||||||
|
convertCharset,
|
||||||
|
cleanHtml,
|
||||||
|
parseEmailHeaders,
|
||||||
|
extractBoundary,
|
||||||
|
extractFilename,
|
||||||
|
extractHeader
|
||||||
|
} from '@/lib/infomaniak-mime-decoder';
|
||||||
|
|
||||||
interface Account {
|
interface Account {
|
||||||
id: number;
|
id: number;
|
||||||
@ -62,14 +72,8 @@ interface Attachment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ParsedEmailContent {
|
interface ParsedEmailContent {
|
||||||
text: string | null;
|
headers: string;
|
||||||
html: string | null;
|
body: string;
|
||||||
attachments: Array<{
|
|
||||||
filename: string;
|
|
||||||
contentType: string;
|
|
||||||
encoding: string;
|
|
||||||
content: string;
|
|
||||||
}>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ParsedEmailMetadata {
|
interface ParsedEmailMetadata {
|
||||||
@ -86,156 +90,69 @@ interface ParsedEmailMetadata {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Improved MIME Decoder Implementation for Infomaniak
|
function parseFullEmail(emailContent: string): ParsedEmailContent {
|
||||||
function extractBoundary(headers: string): string | null {
|
if (!emailContent) return { headers: '', body: '' };
|
||||||
const boundaryMatch = headers.match(/boundary="?([^"\r\n;]+)"?/i) ||
|
|
||||||
headers.match(/boundary=([^\r\n;]+)/i);
|
|
||||||
|
|
||||||
return boundaryMatch ? boundaryMatch[1].trim() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeQuotedPrintable(text: string, charset: string): string {
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// Replace soft line breaks (=\r\n or =\n or =\r)
|
|
||||||
let decoded = text.replace(/=(?:\r\n|\n|\r)/g, '');
|
|
||||||
|
|
||||||
// Replace quoted-printable encoded characters (including non-ASCII characters)
|
|
||||||
decoded = decoded.replace(/=([0-9A-F]{2})/gi, (match, p1) => {
|
|
||||||
return String.fromCharCode(parseInt(p1, 16));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle character encoding
|
|
||||||
try {
|
|
||||||
// For browsers with TextDecoder support
|
|
||||||
if (typeof TextDecoder !== 'undefined') {
|
|
||||||
// Convert string to array of byte values
|
|
||||||
const bytes = new Uint8Array(Array.from(decoded).map(c => c.charCodeAt(0)));
|
|
||||||
return new TextDecoder(charset).decode(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback for older browsers or when charset handling is not critical
|
|
||||||
return decoded;
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Charset conversion error:', e);
|
|
||||||
return decoded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
|
|
||||||
// Split headers and body
|
// Split headers and body
|
||||||
const headerBodySplit = emailRaw.split(/\r?\n\r?\n/);
|
const headerEnd = emailContent.indexOf('\r\n\r\n');
|
||||||
const headers = headerBodySplit[0];
|
if (headerEnd === -1) return { headers: '', body: emailContent };
|
||||||
const body = headerBodySplit.slice(1).join('\n\n');
|
|
||||||
|
|
||||||
// Parse content type from headers
|
const headers = emailContent.substring(0, headerEnd);
|
||||||
const contentTypeMatch = headers.match(/Content-Type:\s*([^;]+)/i);
|
const body = emailContent.substring(headerEnd + 4);
|
||||||
const contentType = contentTypeMatch ? contentTypeMatch[1].trim().toLowerCase() : 'text/plain';
|
|
||||||
|
|
||||||
// Initialize result
|
// Parse headers
|
||||||
const result: ParsedEmailContent = {
|
const headerInfo = parseEmailHeaders(headers);
|
||||||
text: null,
|
const boundary = extractBoundary(headers);
|
||||||
html: null,
|
|
||||||
attachments: []
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle multipart content
|
// Handle multipart content
|
||||||
if (contentType.includes('multipart')) {
|
if (boundary && headerInfo.contentType.startsWith('multipart/')) {
|
||||||
const boundaryMatch = emailRaw.match(/boundary="?([^"\r\n;]+)"?/i) ||
|
const parts = body.split(`--${boundary}`);
|
||||||
emailRaw.match(/boundary=([^\r\n;]+)/i);
|
const processedParts = parts
|
||||||
|
.filter(part => part.trim() && !part.includes('--'))
|
||||||
if (boundaryMatch) {
|
.map(part => {
|
||||||
const boundary = boundaryMatch[1].trim();
|
const partHeaderEnd = part.indexOf('\r\n\r\n');
|
||||||
const parts = emailRaw.split(new RegExp(`--${boundary}(?:--)?(\\r?\\n|$)`));
|
if (partHeaderEnd === -1) return part;
|
||||||
|
|
||||||
for (const part of parts) {
|
const partHeaders = part.substring(0, partHeaderEnd);
|
||||||
if (!part.trim()) continue;
|
const partBody = part.substring(partHeaderEnd + 4);
|
||||||
|
const partInfo = parseEmailHeaders(partHeaders);
|
||||||
const partHeaderBodySplit = part.split(/\r?\n\r?\n/);
|
|
||||||
const partHeaders = partHeaderBodySplit[0];
|
let decodedContent = partBody;
|
||||||
const partBody = partHeaderBodySplit.slice(1).join('\n\n');
|
if (partInfo.encoding === 'quoted-printable') {
|
||||||
|
decodedContent = decodeQuotedPrintable(partBody, partInfo.charset);
|
||||||
const partContentTypeMatch = partHeaders.match(/Content-Type:\s*([^;]+)/i);
|
} else if (partInfo.encoding === 'base64') {
|
||||||
const partContentType = partContentTypeMatch ? partContentTypeMatch[1].trim().toLowerCase() : 'text/plain';
|
decodedContent = decodeBase64(partBody, partInfo.charset);
|
||||||
|
|
||||||
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: partContentType,
|
|
||||||
encoding: 'base64',
|
|
||||||
content: partBody
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
if (partInfo.contentType.includes('text/html')) {
|
||||||
} else {
|
decodedContent = cleanHtml(decodedContent);
|
||||||
// Single part content
|
}
|
||||||
if (contentType.includes('text/html')) {
|
|
||||||
result.html = decodeEmailBody(body, contentType);
|
return decodedContent;
|
||||||
} else {
|
});
|
||||||
result.text = decodeEmailBody(body, contentType);
|
|
||||||
}
|
return {
|
||||||
|
headers,
|
||||||
|
body: processedParts.join('\n\n')
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no content was found, try to extract content directly
|
// Handle single part content
|
||||||
if (!result.text && !result.html) {
|
let decodedBody = body;
|
||||||
// Try to extract HTML content
|
if (headerInfo.encoding === 'quoted-printable') {
|
||||||
const htmlMatch = emailRaw.match(/<html[^>]*>[\s\S]*?<\/html>/i);
|
decodedBody = decodeQuotedPrintable(body, headerInfo.charset);
|
||||||
if (htmlMatch) {
|
} else if (headerInfo.encoding === 'base64') {
|
||||||
result.html = decodeEmailBody(htmlMatch[0], 'text/html');
|
decodedBody = decodeBase64(body, headerInfo.charset);
|
||||||
} else {
|
|
||||||
// Try to extract plain text
|
|
||||||
const textContent = emailRaw
|
|
||||||
.replace(/<[^>]+>/g, '')
|
|
||||||
.replace(/ /g, ' ')
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/\r\n/g, '\n')
|
|
||||||
.replace(/=\n/g, '')
|
|
||||||
.replace(/=3D/g, '=')
|
|
||||||
.replace(/=09/g, '\t')
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
if (textContent) {
|
|
||||||
result.text = textContent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
if (headerInfo.contentType.includes('text/html')) {
|
||||||
}
|
decodedBody = cleanHtml(decodedBody);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
headers,
|
||||||
|
body: decodedBody
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractTextFromHtml(html: string): string {
|
function extractTextFromHtml(html: string): string {
|
||||||
@ -262,44 +179,6 @@ function extractTextFromHtml(html: string): string {
|
|||||||
return html.replace(/\n\s*\n/g, '\n\n').trim();
|
return html.replace(/\n\s*\n/g, '\n\n').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractHeader(headers: string, headerName: string): string {
|
|
||||||
const regex = new RegExp(`^${headerName}:\\s*(.+?)(?:\\r?\\n(?!\\s)|$)`, 'im');
|
|
||||||
const match = headers.match(regex);
|
|
||||||
return match ? match[1].trim() : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractFilename(headers: string): string {
|
|
||||||
const filenameMatch = headers.match(/filename="?([^"\r\n;]+)"?/i);
|
|
||||||
return filenameMatch ? filenameMatch[1].trim() : 'attachment';
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseEmailHeaders(headers: string): { contentType: string; encoding: string; charset: string } {
|
|
||||||
const result = {
|
|
||||||
contentType: 'text/plain',
|
|
||||||
encoding: '7bit',
|
|
||||||
charset: 'utf-8'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract content type and charset
|
|
||||||
const contentTypeMatch = headers.match(/Content-Type:\s*([^;]+)(?:;\s*charset=([^;"\r\n]+)|(?:;\s*charset="([^"]+)"))?/i);
|
|
||||||
if (contentTypeMatch) {
|
|
||||||
result.contentType = contentTypeMatch[1].trim().toLowerCase();
|
|
||||||
if (contentTypeMatch[2]) {
|
|
||||||
result.charset = contentTypeMatch[2].trim().toLowerCase();
|
|
||||||
} else if (contentTypeMatch[3]) {
|
|
||||||
result.charset = contentTypeMatch[3].trim().toLowerCase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract content transfer encoding
|
|
||||||
const encodingMatch = headers.match(/Content-Transfer-Encoding:\s*([^\s;\r\n]+)/i);
|
|
||||||
if (encodingMatch) {
|
|
||||||
result.encoding = encodingMatch[1].trim().toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeMIME(text: string, encoding?: string, charset: string = 'utf-8'): string {
|
function decodeMIME(text: string, encoding?: string, charset: string = 'utf-8'): string {
|
||||||
if (!text) return '';
|
if (!text) return '';
|
||||||
|
|
||||||
@ -326,114 +205,11 @@ function decodeMIME(text: string, encoding?: string, charset: string = 'utf-8'):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeBase64(text: string, charset: string): string {
|
|
||||||
const cleanText = text.replace(/\s/g, '');
|
|
||||||
|
|
||||||
let binaryString;
|
|
||||||
try {
|
|
||||||
binaryString = atob(cleanText);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Base64 decoding error:', e);
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
return convertCharset(binaryString, charset);
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertCharset(text: string, fromCharset: string): string {
|
|
||||||
try {
|
|
||||||
if (typeof TextDecoder !== 'undefined') {
|
|
||||||
const bytes = new Uint8Array(text.length);
|
|
||||||
for (let i = 0; i < text.length; i++) {
|
|
||||||
bytes[i] = text.charCodeAt(i) & 0xFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
let normalizedCharset = fromCharset.toLowerCase();
|
|
||||||
|
|
||||||
// Normalize charset names
|
|
||||||
if (normalizedCharset === 'iso-8859-1' || normalizedCharset === 'latin1') {
|
|
||||||
normalizedCharset = 'iso-8859-1';
|
|
||||||
} else if (normalizedCharset === 'windows-1252' || normalizedCharset === 'cp1252') {
|
|
||||||
normalizedCharset = 'windows-1252';
|
|
||||||
}
|
|
||||||
|
|
||||||
const decoder = new TextDecoder(normalizedCharset);
|
|
||||||
return decoder.decode(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback for older browsers or unsupported charsets
|
|
||||||
if (fromCharset.toLowerCase() === 'iso-8859-1' || fromCharset.toLowerCase() === 'windows-1252') {
|
|
||||||
return text
|
|
||||||
.replace(/\xC3\xA0/g, 'à')
|
|
||||||
.replace(/\xC3\xA2/g, 'â')
|
|
||||||
.replace(/\xC3\xA9/g, 'é')
|
|
||||||
.replace(/\xC3\xA8/g, 'è')
|
|
||||||
.replace(/\xC3\xAA/g, 'ê')
|
|
||||||
.replace(/\xC3\xAB/g, 'ë')
|
|
||||||
.replace(/\xC3\xB4/g, 'ô')
|
|
||||||
.replace(/\xC3\xB9/g, 'ù')
|
|
||||||
.replace(/\xC3\xBB/g, 'û')
|
|
||||||
.replace(/\xC3\x80/g, 'À')
|
|
||||||
.replace(/\xC3\x89/g, 'É')
|
|
||||||
.replace(/\xC3\x87/g, 'Ç')
|
|
||||||
// Clean up HTML entities
|
|
||||||
.replace(/ç/g, 'ç')
|
|
||||||
.replace(/é/g, 'é')
|
|
||||||
.replace(/è/g, 'ë')
|
|
||||||
.replace(/ê/g, 'ª')
|
|
||||||
.replace(/ë/g, '«')
|
|
||||||
.replace(/û/g, '»')
|
|
||||||
.replace(/ /g, ' ')
|
|
||||||
.replace(/\xA0/g, ' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
return text;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Character set conversion error:', e, 'charset:', fromCharset);
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractHtmlBody(htmlContent: string): string {
|
function extractHtmlBody(htmlContent: string): string {
|
||||||
const bodyMatch = htmlContent.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
const bodyMatch = htmlContent.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
||||||
return bodyMatch ? bodyMatch[1] : htmlContent;
|
return bodyMatch ? bodyMatch[1] : htmlContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanHtml(html: string): string {
|
|
||||||
if (!html) return '';
|
|
||||||
|
|
||||||
return html
|
|
||||||
// Fix common Infomaniak-specific character encodings
|
|
||||||
.replace(/=C2=A0/g, ' ') // non-breaking space
|
|
||||||
.replace(/=E2=80=93/g, '\u2013') // en dash
|
|
||||||
.replace(/=E2=80=94/g, '\u2014') // em dash
|
|
||||||
.replace(/=E2=80=98/g, '\u2018') // left single quote
|
|
||||||
.replace(/=E2=80=99/g, '\u2019') // right single quote
|
|
||||||
.replace(/=E2=80=9C/g, '\u201C') // left double quote
|
|
||||||
.replace(/=E2=80=9D/g, '\u201D') // right double quote
|
|
||||||
.replace(/=C3=A0/g, 'à')
|
|
||||||
.replace(/=C3=A2/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=xBB/g, 'û')
|
|
||||||
.replace(/=C3=80/g, 'À')
|
|
||||||
.replace(/=C3=89/g, 'É')
|
|
||||||
.replace(/=C3=87/g, 'Ç')
|
|
||||||
// Clean up HTML entities
|
|
||||||
.replace(/ç/g, 'ç')
|
|
||||||
.replace(/é/g, 'é')
|
|
||||||
.replace(/è/g, 'ë')
|
|
||||||
.replace(/ê/g, 'ª')
|
|
||||||
.replace(/ë/g, '«')
|
|
||||||
.replace(/û/g, '»')
|
|
||||||
.replace(/ /g, ' ')
|
|
||||||
.replace(/\xA0/g, ' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeMimeContent(content: string): string {
|
function decodeMimeContent(content: string): string {
|
||||||
if (!content) return '';
|
if (!content) return '';
|
||||||
|
|
||||||
@ -479,22 +255,22 @@ function renderEmailContent(email: Email) {
|
|||||||
// First try to parse the full email
|
// First try to parse the full email
|
||||||
const parsed = parseFullEmail(email.body);
|
const parsed = parseFullEmail(email.body);
|
||||||
console.log('Parsed content:', {
|
console.log('Parsed content:', {
|
||||||
hasText: !!parsed.text,
|
hasText: !!parsed.body,
|
||||||
hasHtml: !!parsed.html,
|
hasHtml: !!parsed.headers,
|
||||||
hasAttachments: parsed.attachments.length > 0
|
hasAttachments: parsed.headers.length > 0
|
||||||
});
|
});
|
||||||
|
|
||||||
// Determine content and type
|
// Determine content and type
|
||||||
let content = '';
|
let content = '';
|
||||||
let isHtml = false;
|
let isHtml = false;
|
||||||
|
|
||||||
if (parsed.html) {
|
if (parsed.headers) {
|
||||||
// Use our existing MIME decoding for HTML content
|
// Use our existing MIME decoding for HTML content
|
||||||
content = decodeMIME(parsed.html, 'quoted-printable', 'utf-8');
|
content = decodeMIME(parsed.headers, 'quoted-printable', 'utf-8');
|
||||||
isHtml = true;
|
isHtml = true;
|
||||||
} else if (parsed.text) {
|
} else if (parsed.body) {
|
||||||
// Use our existing MIME decoding for plain text content
|
// Use our existing MIME decoding for plain text content
|
||||||
content = decodeMIME(parsed.text, 'quoted-printable', 'utf-8');
|
content = decodeMIME(parsed.body, 'quoted-printable', 'utf-8');
|
||||||
isHtml = false;
|
isHtml = false;
|
||||||
} else {
|
} else {
|
||||||
// Try to extract content directly from body using our existing functions
|
// Try to extract content directly from body using our existing functions
|
||||||
@ -515,11 +291,11 @@ function renderEmailContent(email: Email) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle attachments
|
// Handle attachments
|
||||||
const attachmentElements = parsed.attachments.map((attachment, index) => (
|
const attachmentElements = parsed.headers.split('\n').filter(header => header.startsWith('Content-Type:')).map((header, index) => (
|
||||||
<div key={index} className="mt-4 p-4 border rounded-lg bg-gray-50">
|
<div key={index} className="mt-4 p-4 border rounded-lg bg-gray-50">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Paperclip className="h-5 w-5 text-gray-400 mr-2" />
|
<Paperclip className="h-5 w-5 text-gray-400 mr-2" />
|
||||||
<span className="text-sm text-gray-600">{attachment.filename}</span>
|
<span className="text-sm text-gray-600">{header.split(': ')[1]}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
@ -1335,18 +1111,18 @@ export default function CourrierPage() {
|
|||||||
try {
|
try {
|
||||||
const parsed = parseFullEmail(email.body);
|
const parsed = parseFullEmail(email.body);
|
||||||
console.log('Parsed content:', {
|
console.log('Parsed content:', {
|
||||||
hasText: !!parsed.text,
|
hasText: !!parsed.body,
|
||||||
hasHtml: !!parsed.html,
|
hasHtml: !!parsed.headers,
|
||||||
textPreview: parsed.text?.substring(0, 100) || 'No text',
|
textPreview: parsed.body?.substring(0, 100) || 'No text',
|
||||||
htmlPreview: parsed.html?.substring(0, 100) || 'No HTML'
|
htmlPreview: parsed.headers?.substring(0, 100) || 'No HTML'
|
||||||
});
|
});
|
||||||
|
|
||||||
let preview = '';
|
let preview = '';
|
||||||
if (parsed.text) {
|
if (parsed.body) {
|
||||||
preview = parsed.text;
|
preview = parsed.body;
|
||||||
console.log('Using text content for preview');
|
console.log('Using text content for preview');
|
||||||
} else if (parsed.html) {
|
} else if (parsed.headers) {
|
||||||
preview = parsed.html
|
preview = parsed.headers
|
||||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
||||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
||||||
.replace(/<[^>]+>/g, ' ')
|
.replace(/<[^>]+>/g, ' ')
|
||||||
@ -1613,64 +1389,33 @@ export default function CourrierPage() {
|
|||||||
const getReplyBody = () => {
|
const getReplyBody = () => {
|
||||||
if (!selectedEmail?.body) return '';
|
if (!selectedEmail?.body) return '';
|
||||||
|
|
||||||
try {
|
const parsed = parseFullEmail(selectedEmail.body);
|
||||||
// Parse the full email content
|
if (!parsed) return '';
|
||||||
const parsed = parseFullEmail(selectedEmail.body);
|
|
||||||
let originalContent = '';
|
|
||||||
|
|
||||||
// Get the content from either HTML or text part
|
|
||||||
if (parsed.html) {
|
|
||||||
// Use MIME decoding for HTML content
|
|
||||||
originalContent = decodeMIME(parsed.html, 'quoted-printable', 'utf-8');
|
|
||||||
|
|
||||||
// Convert HTML to plain text for the reply
|
|
||||||
originalContent = originalContent
|
|
||||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
||||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
||||||
.replace(/<br\s*\/?>/gi, '\n')
|
|
||||||
.replace(/<div[^>]*>/gi, '\n')
|
|
||||||
.replace(/<\/div>/gi, '')
|
|
||||||
.replace(/<p[^>]*>/gi, '\n')
|
|
||||||
.replace(/<\/p>/gi, '')
|
|
||||||
.replace(/<[^>]+>/g, '')
|
|
||||||
.replace(/ |‌|»|«|>/g, match => {
|
|
||||||
switch (match) {
|
|
||||||
case ' ': return ' ';
|
|
||||||
case '‌': return '';
|
|
||||||
case '»': return '»';
|
|
||||||
case '«': return '«';
|
|
||||||
case '>': return '>';
|
|
||||||
case '<': return '<';
|
|
||||||
case '&': return '&';
|
|
||||||
default: return match;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.replace(/^\s+$/gm, '')
|
|
||||||
.replace(/\n{3,}/g, '\n\n')
|
|
||||||
.trim();
|
|
||||||
} else if (parsed.text) {
|
|
||||||
// Use MIME decoding for plain text content
|
|
||||||
originalContent = decodeMIME(parsed.text, 'quoted-printable', 'utf-8').trim();
|
|
||||||
} else {
|
|
||||||
// Fallback to raw body if parsing fails, but still try to decode it
|
|
||||||
originalContent = decodeMIME(
|
|
||||||
selectedEmail.body.replace(/<[^>]+>/g, ''),
|
|
||||||
'quoted-printable',
|
|
||||||
'utf-8'
|
|
||||||
).trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format the reply with proper indentation
|
const body = parsed.body;
|
||||||
const formattedContent = originalContent
|
|
||||||
.split('\n')
|
|
||||||
.map(line => `> ${line}`)
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
return `\n\n${formattedContent}\n\n`;
|
// Convert HTML to plain text if needed
|
||||||
} catch (error) {
|
const plainText = body
|
||||||
console.error('Error preparing reply body:', error);
|
.replace(/<br\s*\/?>/gi, '\n')
|
||||||
return '';
|
.replace(/<div[^>]*>/gi, '\n')
|
||||||
}
|
.replace(/<\/div>/gi, '')
|
||||||
|
.replace(/<p[^>]*>/gi, '\n')
|
||||||
|
.replace(/<\/p>/gi, '')
|
||||||
|
.replace(/ /g, ' ')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/<[^>]+>/g, '')
|
||||||
|
.replace(/^\s+$/gm, '')
|
||||||
|
.replace(/\n{3,}/g, '\n\n')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
// Add reply prefix to each line
|
||||||
|
return plainText
|
||||||
|
.split('\n')
|
||||||
|
.map(line => `> ${line}`)
|
||||||
|
.join('\n');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Prepare the reply email
|
// Prepare the reply email
|
||||||
|
|||||||
@ -27,6 +27,16 @@ import {
|
|||||||
AlertOctagon, Archive, RefreshCw
|
AlertOctagon, Archive, RefreshCw
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import {
|
||||||
|
decodeQuotedPrintable,
|
||||||
|
decodeBase64,
|
||||||
|
convertCharset,
|
||||||
|
cleanHtml,
|
||||||
|
parseEmailHeaders,
|
||||||
|
extractBoundary,
|
||||||
|
extractFilename,
|
||||||
|
extractHeader
|
||||||
|
} from '@/lib/infomaniak-mime-decoder';
|
||||||
|
|
||||||
interface Account {
|
interface Account {
|
||||||
id: number;
|
id: number;
|
||||||
@ -60,43 +70,36 @@ interface Attachment {
|
|||||||
encoding: string;
|
encoding: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Improved MIME Decoder Implementation for Infomaniak
|
interface EmailAttachment {
|
||||||
function extractBoundary(headers: string): string | null {
|
filename: string;
|
||||||
const boundaryMatch = headers.match(/boundary="?([^"\r\n;]+)"?/i) ||
|
contentType: string;
|
||||||
headers.match(/boundary=([^\r\n;]+)/i);
|
encoding: string;
|
||||||
|
content: string;
|
||||||
return boundaryMatch ? boundaryMatch[1].trim() : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeQuotedPrintable(text: string, charset: string): string {
|
interface ParsedEmail {
|
||||||
if (!text) return '';
|
text: string;
|
||||||
|
html: string;
|
||||||
// Replace soft line breaks (=\r\n or =\n or =\r)
|
attachments: EmailAttachment[];
|
||||||
let decoded = text.replace(/=(?:\r\n|\n|\r)/g, '');
|
headers?: string;
|
||||||
|
|
||||||
// Replace quoted-printable encoded characters (including non-ASCII characters)
|
|
||||||
decoded = decoded.replace(/=([0-9A-F]{2})/gi, (match, p1) => {
|
|
||||||
return String.fromCharCode(parseInt(p1, 16));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle character encoding
|
|
||||||
try {
|
|
||||||
// For browsers with TextDecoder support
|
|
||||||
if (typeof TextDecoder !== 'undefined') {
|
|
||||||
// Convert string to array of byte values
|
|
||||||
const bytes = new Uint8Array(Array.from(decoded).map(c => c.charCodeAt(0)));
|
|
||||||
return new TextDecoder(charset).decode(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback for older browsers or when charset handling is not critical
|
|
||||||
return decoded;
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Charset conversion error:', e);
|
|
||||||
return decoded;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFullEmail(emailRaw: 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(emailRaw: string): ParsedEmail | EmailMessage {
|
||||||
// Check if this is a multipart message by looking for boundary definition
|
// Check if this is a multipart message by looking for boundary definition
|
||||||
const boundaryMatch = emailRaw.match(/boundary="?([^"\r\n;]+)"?/i) ||
|
const boundaryMatch = emailRaw.match(/boundary="?([^"\r\n;]+)"?/i) ||
|
||||||
emailRaw.match(/boundary=([^\r\n;]+)/i);
|
emailRaw.match(/boundary=([^\r\n;]+)/i);
|
||||||
@ -119,127 +122,72 @@ function parseFullEmail(emailRaw: string) {
|
|||||||
|
|
||||||
return processMultipartEmail(emailRaw, boundary, mainHeaders);
|
return processMultipartEmail(emailRaw, boundary, mainHeaders);
|
||||||
} else {
|
} else {
|
||||||
// This is a single part message
|
// Split headers and body
|
||||||
return processSinglePartEmail(emailRaw);
|
const [headers, body] = emailRaw.split(/\r?\n\r?\n/, 2);
|
||||||
|
|
||||||
|
// If no boundary is found, treat as a single part message
|
||||||
|
const emailInfo = parseEmailHeaders(headers);
|
||||||
|
return {
|
||||||
|
subject: extractHeader(headers, 'Subject'),
|
||||||
|
from: extractHeader(headers, 'From'),
|
||||||
|
to: extractHeader(headers, 'To'),
|
||||||
|
date: extractHeader(headers, 'Date'),
|
||||||
|
contentType: emailInfo.contentType,
|
||||||
|
text: emailInfo.contentType.includes('text/plain') ? body : null,
|
||||||
|
html: emailInfo.contentType.includes('text/html') ? body : null,
|
||||||
|
attachments: [], // Add empty attachments array for single part messages
|
||||||
|
raw: {
|
||||||
|
headers,
|
||||||
|
body
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function processMultipartEmail(emailRaw: string, boundary: string, mainHeaders: string = ''): {
|
function processMultipartEmail(emailRaw: string, boundary: string, mainHeaders: string): ParsedEmail {
|
||||||
text: string;
|
const parts = emailRaw.split(new RegExp(`--${boundary}(?:--)?\\s*`, 'm'));
|
||||||
html: string;
|
const result: ParsedEmail = {
|
||||||
attachments: { filename: string; contentType: string; encoding: string; content: string; }[];
|
|
||||||
headers?: string;
|
|
||||||
} {
|
|
||||||
const result = {
|
|
||||||
text: '',
|
text: '',
|
||||||
html: '',
|
html: '',
|
||||||
attachments: [] as { filename: string; contentType: string; encoding: string; content: string; }[],
|
attachments: []
|
||||||
headers: mainHeaders
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Split by boundary (more robust pattern)
|
for (const part of parts) {
|
||||||
const boundaryRegex = new RegExp(`--${boundary}(?:--)?(\\r?\\n|$)`, 'g');
|
if (!part.trim()) continue;
|
||||||
|
|
||||||
// Get all boundary positions
|
const [partHeaders, ...bodyParts] = part.split(/\r?\n\r?\n/);
|
||||||
const matches = Array.from(emailRaw.matchAll(boundaryRegex));
|
const partBody = bodyParts.join('\n\n');
|
||||||
const boundaryPositions = matches.map(match => match.index!);
|
const partInfo = parseEmailHeaders(partHeaders);
|
||||||
|
|
||||||
// Extract content between boundaries
|
if (partInfo.contentType.startsWith('text/')) {
|
||||||
for (let i = 0; i < boundaryPositions.length - 1; i++) {
|
let decodedContent = '';
|
||||||
const startPos = boundaryPositions[i] + matches[i][0].length;
|
|
||||||
const endPos = boundaryPositions[i + 1];
|
|
||||||
|
|
||||||
if (endPos > startPos) {
|
|
||||||
const partContent = emailRaw.substring(startPos, endPos).trim();
|
|
||||||
|
|
||||||
if (partContent) {
|
if (partInfo.encoding === 'quoted-printable') {
|
||||||
const decoded = processSinglePartEmail(partContent);
|
decodedContent = decodeQuotedPrintable(partBody, partInfo.charset);
|
||||||
|
} else if (partInfo.encoding === 'base64') {
|
||||||
if (decoded.contentType.includes('text/plain')) {
|
decodedContent = decodeBase64(partBody, partInfo.charset);
|
||||||
result.text = decoded.text || '';
|
} else {
|
||||||
} else if (decoded.contentType.includes('text/html')) {
|
decodedContent = partBody;
|
||||||
result.html = cleanHtml(decoded.html || '');
|
|
||||||
} else if (
|
|
||||||
decoded.contentType.startsWith('image/') ||
|
|
||||||
decoded.contentType.startsWith('application/')
|
|
||||||
) {
|
|
||||||
const filename = extractFilename(partContent);
|
|
||||||
result.attachments.push({
|
|
||||||
filename,
|
|
||||||
contentType: decoded.contentType,
|
|
||||||
encoding: decoded.raw?.headers ? parseEmailHeaders(decoded.raw.headers).encoding : '7bit',
|
|
||||||
content: decoded.raw?.body || ''
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 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 extractHeader(headers: string, headerName: string): string {
|
|
||||||
const regex = new RegExp(`^${headerName}:\\s*(.+?)(?:\\r?\\n(?!\\s)|$)`, 'im');
|
|
||||||
const match = headers.match(regex);
|
|
||||||
return match ? match[1].trim() : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractFilename(headers: string): string {
|
|
||||||
const filenameMatch = headers.match(/filename="?([^"\r\n;]+)"?/i);
|
|
||||||
return filenameMatch ? filenameMatch[1].trim() : 'attachment';
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseEmailHeaders(headers: string): { contentType: string; encoding: string; charset: string } {
|
|
||||||
const result = {
|
|
||||||
contentType: 'text/plain',
|
|
||||||
encoding: '7bit',
|
|
||||||
charset: 'utf-8'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract content type and charset
|
|
||||||
const contentTypeMatch = headers.match(/Content-Type:\s*([^;]+)(?:;\s*charset=([^;"\r\n]+)|(?:;\s*charset="([^"]+)"))?/i);
|
|
||||||
if (contentTypeMatch) {
|
|
||||||
result.contentType = contentTypeMatch[1].trim().toLowerCase();
|
|
||||||
if (contentTypeMatch[2]) {
|
|
||||||
result.charset = contentTypeMatch[2].trim().toLowerCase();
|
|
||||||
} else if (contentTypeMatch[3]) {
|
|
||||||
result.charset = contentTypeMatch[3].trim().toLowerCase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract content transfer encoding
|
|
||||||
const encodingMatch = headers.match(/Content-Transfer-Encoding:\s*([^\s;\r\n]+)/i);
|
|
||||||
if (encodingMatch) {
|
|
||||||
result.encoding = encodingMatch[1].trim().toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,114 +217,6 @@ function decodeMIME(text: string, encoding?: string, charset: string = 'utf-8'):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeBase64(text: string, charset: string): string {
|
|
||||||
const cleanText = text.replace(/\s/g, '');
|
|
||||||
|
|
||||||
let binaryString;
|
|
||||||
try {
|
|
||||||
binaryString = atob(cleanText);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Base64 decoding error:', e);
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
return convertCharset(binaryString, charset);
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertCharset(text: string, fromCharset: string): string {
|
|
||||||
try {
|
|
||||||
if (typeof TextDecoder !== 'undefined') {
|
|
||||||
const bytes = new Uint8Array(text.length);
|
|
||||||
for (let i = 0; i < text.length; i++) {
|
|
||||||
bytes[i] = text.charCodeAt(i) & 0xFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
let normalizedCharset = fromCharset.toLowerCase();
|
|
||||||
|
|
||||||
// Normalize charset names
|
|
||||||
if (normalizedCharset === 'iso-8859-1' || normalizedCharset === 'latin1') {
|
|
||||||
normalizedCharset = 'iso-8859-1';
|
|
||||||
} else if (normalizedCharset === 'windows-1252' || normalizedCharset === 'cp1252') {
|
|
||||||
normalizedCharset = 'windows-1252';
|
|
||||||
}
|
|
||||||
|
|
||||||
const decoder = new TextDecoder(normalizedCharset);
|
|
||||||
return decoder.decode(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback for older browsers or unsupported charsets
|
|
||||||
if (fromCharset.toLowerCase() === 'iso-8859-1' || fromCharset.toLowerCase() === 'windows-1252') {
|
|
||||||
return text
|
|
||||||
.replace(/\xC3\xA0/g, 'à')
|
|
||||||
.replace(/\xC3\xA2/g, 'â')
|
|
||||||
.replace(/\xC3\xA9/g, 'é')
|
|
||||||
.replace(/\xC3\xA8/g, 'è')
|
|
||||||
.replace(/\xC3\xAA/g, 'ê')
|
|
||||||
.replace(/\xC3\xAB/g, 'ë')
|
|
||||||
.replace(/\xC3\xB4/g, 'ô')
|
|
||||||
.replace(/\xC3\xB9/g, 'ù')
|
|
||||||
.replace(/\xC3\xBB/g, 'û')
|
|
||||||
.replace(/\xC3\x80/g, 'À')
|
|
||||||
.replace(/\xC3\x89/g, 'É')
|
|
||||||
.replace(/\xC3\x87/g, 'Ç')
|
|
||||||
// Clean up HTML entities
|
|
||||||
.replace(/ç/g, 'ç')
|
|
||||||
.replace(/é/g, 'é')
|
|
||||||
.replace(/è/g, 'ë')
|
|
||||||
.replace(/ê/g, 'ª')
|
|
||||||
.replace(/ë/g, '«')
|
|
||||||
.replace(/û/g, '»')
|
|
||||||
.replace(/ /g, ' ')
|
|
||||||
.replace(/\xA0/g, ' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
return text;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Character set conversion error:', e, 'charset:', fromCharset);
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractHtmlBody(htmlContent: string): string {
|
|
||||||
const bodyMatch = htmlContent.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
|
||||||
return bodyMatch ? bodyMatch[1] : htmlContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanHtml(html: string): string {
|
|
||||||
if (!html) return '';
|
|
||||||
|
|
||||||
return html
|
|
||||||
// Fix common Infomaniak-specific character encodings
|
|
||||||
.replace(/=C2=A0/g, ' ') // non-breaking space
|
|
||||||
.replace(/=E2=80=93/g, '\u2013') // en dash
|
|
||||||
.replace(/=E2=80=94/g, '\u2014') // em dash
|
|
||||||
.replace(/=E2=80=98/g, '\u2018') // left single quote
|
|
||||||
.replace(/=E2=80=99/g, '\u2019') // right single quote
|
|
||||||
.replace(/=E2=80=9C/g, '\u201C') // left double quote
|
|
||||||
.replace(/=E2=80=9D/g, '\u201D') // right double quote
|
|
||||||
.replace(/=C3=A0/g, 'à')
|
|
||||||
.replace(/=C3=A2/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=xBB/g, 'û')
|
|
||||||
.replace(/=C3=80/g, 'À')
|
|
||||||
.replace(/=C3=89/g, 'É')
|
|
||||||
.replace(/=C3=87/g, 'Ç')
|
|
||||||
// Clean up HTML entities
|
|
||||||
.replace(/ç/g, 'ç')
|
|
||||||
.replace(/é/g, 'é')
|
|
||||||
.replace(/è/g, 'ë')
|
|
||||||
.replace(/ê/g, 'ª')
|
|
||||||
.replace(/ë/g, '«')
|
|
||||||
.replace(/û/g, '»')
|
|
||||||
.replace(/ /g, ' ')
|
|
||||||
.replace(/\xA0/g, ' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeMimeContent(content: string): string {
|
function decodeMimeContent(content: string): string {
|
||||||
if (!content) return '';
|
if (!content) return '';
|
||||||
|
|
||||||
|
|||||||
174
lib/infomaniak-mime-decoder.ts
Normal file
174
lib/infomaniak-mime-decoder.ts
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
// Infomaniak-specific MIME decoder functions
|
||||||
|
|
||||||
|
export function decodeQuotedPrintable(text: string, charset: string): string {
|
||||||
|
if (!text) return '';
|
||||||
|
|
||||||
|
// Replace soft line breaks (=\r\n or =\n or =\r)
|
||||||
|
let decoded = text.replace(/=(?:\r\n|\n|\r)/g, '');
|
||||||
|
|
||||||
|
// Replace quoted-printable encoded characters (including non-ASCII characters)
|
||||||
|
decoded = decoded.replace(/=([0-9A-F]{2})/gi, (match, p1) => {
|
||||||
|
return String.fromCharCode(parseInt(p1, 16));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle character encoding
|
||||||
|
try {
|
||||||
|
// For browsers with TextDecoder support
|
||||||
|
if (typeof TextDecoder !== 'undefined') {
|
||||||
|
// Convert string to array of byte values
|
||||||
|
const bytes = new Uint8Array(Array.from(decoded).map(c => c.charCodeAt(0)));
|
||||||
|
return new TextDecoder(charset).decode(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for older browsers or when charset handling is not critical
|
||||||
|
return decoded;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Charset conversion error:', e);
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeBase64(text: string, charset: string): string {
|
||||||
|
if (!text) return '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Remove any whitespace and line breaks
|
||||||
|
const cleanText = text.replace(/\s+/g, '');
|
||||||
|
|
||||||
|
// Decode base64
|
||||||
|
const binary = atob(cleanText);
|
||||||
|
|
||||||
|
// Convert to bytes
|
||||||
|
const bytes = new Uint8Array(binary.length);
|
||||||
|
for (let i = 0; i < binary.length; i++) {
|
||||||
|
bytes[i] = binary.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode using specified charset
|
||||||
|
if (typeof TextDecoder !== 'undefined') {
|
||||||
|
return new TextDecoder(charset).decode(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback
|
||||||
|
return binary;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Base64 decoding error:', e);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertCharset(text: string, charset: string): string {
|
||||||
|
if (!text) return '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// For browsers with TextDecoder support
|
||||||
|
if (typeof TextDecoder !== 'undefined') {
|
||||||
|
// Convert string to array of byte values
|
||||||
|
const bytes = new Uint8Array(Array.from(text).map(c => c.charCodeAt(0)));
|
||||||
|
return new TextDecoder(charset).decode(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for older browsers
|
||||||
|
return text;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Charset conversion error:', e);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cleanHtml(html: string): string {
|
||||||
|
if (!html) return '';
|
||||||
|
|
||||||
|
// Remove or fix malformed URLs
|
||||||
|
html = html.replace(/=3D"(http[^"]+)"/g, (match, url) => {
|
||||||
|
try {
|
||||||
|
return `"${decodeURIComponent(url)}"`;
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove any remaining quoted-printable artifacts
|
||||||
|
html = html.replace(/=([0-9A-F]{2})/gi, (match, p1) => {
|
||||||
|
return String.fromCharCode(parseInt(p1, 16));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up any remaining HTML issues
|
||||||
|
html = html
|
||||||
|
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
||||||
|
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
||||||
|
.replace(/<meta[^>]*>/gi, '')
|
||||||
|
.replace(/<link[^>]*>/gi, '')
|
||||||
|
.replace(/<base[^>]*>/gi, '')
|
||||||
|
.replace(/<title[^>]*>[\s\S]*?<\/title>/gi, '')
|
||||||
|
.replace(/<head[^>]*>[\s\S]*?<\/head>/gi, '')
|
||||||
|
.replace(/<body[^>]*>/gi, '')
|
||||||
|
.replace(/<\/body>/gi, '')
|
||||||
|
.replace(/<html[^>]*>/gi, '')
|
||||||
|
.replace(/<\/html>/gi, '')
|
||||||
|
.replace(/<br\s*\/?>/gi, '\n')
|
||||||
|
.replace(/<div[^>]*>/gi, '\n')
|
||||||
|
.replace(/<\/div>/gi, '')
|
||||||
|
.replace(/<p[^>]*>/gi, '\n')
|
||||||
|
.replace(/<\/p>/gi, '')
|
||||||
|
.replace(/ /g, ' ')
|
||||||
|
.replace(/‌/g, '')
|
||||||
|
.replace(/»/g, '»')
|
||||||
|
.replace(/«/g, '«')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/^\s+$/gm, '')
|
||||||
|
.replace(/\n{3,}/g, '\n\n')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseEmailHeaders(headers: string): { contentType: string; encoding: string; charset: string } {
|
||||||
|
const result = {
|
||||||
|
contentType: 'text/plain',
|
||||||
|
encoding: '7bit',
|
||||||
|
charset: 'utf-8'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract content type and charset
|
||||||
|
const contentTypeMatch = headers.match(/Content-Type:\s*([^;]+)(?:;\s*charset=([^;"\r\n]+)|(?:;\s*charset="([^"]+)"))?/i);
|
||||||
|
if (contentTypeMatch) {
|
||||||
|
result.contentType = contentTypeMatch[1].trim().toLowerCase();
|
||||||
|
if (contentTypeMatch[2]) {
|
||||||
|
result.charset = contentTypeMatch[2].trim().toLowerCase();
|
||||||
|
} else if (contentTypeMatch[3]) {
|
||||||
|
result.charset = contentTypeMatch[3].trim().toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract content transfer encoding
|
||||||
|
const encodingMatch = headers.match(/Content-Transfer-Encoding:\s*([^\s;\r\n]+)/i);
|
||||||
|
if (encodingMatch) {
|
||||||
|
result.encoding = encodingMatch[1].trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractBoundary(headers: string): string | null {
|
||||||
|
const boundaryMatch = headers.match(/boundary="?([^"\r\n;]+)"?/i) ||
|
||||||
|
headers.match(/boundary=([^\r\n;]+)/i);
|
||||||
|
|
||||||
|
return boundaryMatch ? boundaryMatch[1].trim() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractFilename(headers: string): string {
|
||||||
|
const filenameMatch = headers.match(/filename="?([^"\r\n;]+)"?/i) ||
|
||||||
|
headers.match(/name="?([^"\r\n;]+)"?/i);
|
||||||
|
|
||||||
|
return filenameMatch ? filenameMatch[1] : 'attachment';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractHeader(headers: string, headerName: string): string {
|
||||||
|
const regex = new RegExp(`^${headerName}:\\s*(.*)$`, 'im');
|
||||||
|
const match = headers.match(regex);
|
||||||
|
return match ? match[1].trim() : '';
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user