mail page imap connection mime

This commit is contained in:
alma 2025-04-15 22:06:29 +02:00
parent 6af27f9065
commit bc1dab3b2b

View File

@ -41,62 +41,171 @@ interface Email {
category: string;
}
// Improved MIME decoding function for all emails
const decodeMimeContent = (content: string): string => {
// MIME Decoder Implementation
function decodeMIME(text: string, encoding?: string, charset: string = 'utf-8'): string {
if (!text) return '';
encoding = (encoding || '').toLowerCase();
charset = (charset || 'utf-8').toLowerCase();
try {
// Handle forwarded message headers specially
if (content.includes('-------- Forwarded message ----------')) {
const parts: string[] = content.split('-------- Forwarded message ----------');
if (parts.length > 1) {
// Clean the forwarded headers section
const headers = parts[1].split('\n')
.filter(line => line.trim())
.map(line => line.replace(/=\n/g, ''))
.map(line => line.replace(/=([0-9A-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))))
.join('\n');
// Clean the message body
const messageBody = parts[1].split('\n\n').slice(1).join('\n\n')
.replace(/^This is a multi-part message.*?charset="utf-8"/s, '')
.replace(/---InfomaniakPhpMail.*?Content-Transfer-Encoding:.*?\n/g, '')
.replace(/Content-Type:.*?\n/g, '')
.replace(/Content-Transfer-Encoding:.*?\n/g, '')
.replace(/=C2=A0/g, ' ') // non-breaking space
.replace(/=E2=80=(93|94)/g, '-') // dashes
.replace(/=\n/g, '') // soft line breaks
.replace(/=([0-9A-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
.replace(/\[IMG:(.*?)\]/g, '') // remove image placeholders
.replace(/\[ ?LINK: ([^\]]+?) ?\]/g, '$1') // clean up links
.replace(/\*(.*?)\*/g, '$1') // remove asterisk formatting
.replace(/\n{3,}/g, '\n\n') // reduce multiple line breaks
.replace(/^\s+|\s+$/gm, '') // trim each line
.trim();
return `-------- Forwarded message ----------\n${headers}\n\n${messageBody}`;
}
if (encoding === 'quoted-printable') {
return decodeQuotedPrintable(text, charset);
} else if (encoding === 'base64') {
return decodeBase64(text, charset);
} else {
return text;
}
// Regular email content cleaning
return content
.replace(/^This is a multi-part message.*?charset="utf-8"/s, '')
.replace(/---InfomaniakPhpMail.*?Content-Transfer-Encoding:.*?\n/g, '')
.replace(/Content-Type:.*?\n/g, '')
.replace(/Content-Transfer-Encoding:.*?\n/g, '')
.replace(/=C2=A0/g, ' ') // non-breaking space
.replace(/=E2=80=(93|94)/g, '-') // dashes
.replace(/=\n/g, '') // soft line breaks
.replace(/=([0-9A-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
.replace(/\[IMG:(.*?)\]/g, '') // remove image placeholders
.replace(/\[ ?LINK: ([^\]]+?) ?\]/g, '$1') // clean up links
.replace(/\*(.*?)\*/g, '$1') // remove asterisk formatting
.replace(/\n{3,}/g, '\n\n') // reduce multiple line breaks
.replace(/^\s+|\s+$/gm, '') // trim each line
.trim();
} catch (error) {
console.error('Error decoding MIME content:', error);
console.error('Error decoding MIME:', error);
return text;
}
}
function decodeQuotedPrintable(text: string, charset: string): string {
let decoded = text.replace(/=(?:\r\n|\n)/g, '');
decoded = decoded.replace(/=([0-9A-F]{2})/gi, (match, p1) => {
return String.fromCharCode(parseInt(p1, 16));
});
if (charset !== 'utf-8' && typeof window !== 'undefined' && typeof TextDecoder !== 'undefined') {
try {
const bytes = new Uint8Array(decoded.length);
for (let i = 0; i < decoded.length; i++) {
bytes[i] = decoded.charCodeAt(i);
}
return new TextDecoder(charset).decode(bytes);
} catch (e) {
console.error('Charset decoding error:', e);
return decoded;
}
}
return decoded;
}
function decodeBase64(text: string, charset: string): string {
const cleanText = text.replace(/\s/g, '');
try {
const binary = atob(cleanText);
if (charset !== 'utf-8' && typeof TextDecoder !== 'undefined') {
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return new TextDecoder(charset).decode(bytes);
}
return binary;
} catch (e) {
console.error('Base64 decoding error:', e);
return text;
}
}
function parseEmailHeaders(headers: string): { contentType: string; encoding: string; charset: string } {
const result = {
contentType: 'text/plain',
encoding: 'quoted-printable',
charset: 'utf-8'
};
const contentTypeMatch = headers.match(/Content-Type:\s*([^;]+)(?:;\s*charset=([^;]+))?/i);
if (contentTypeMatch) {
result.contentType = contentTypeMatch[1].trim().toLowerCase();
if (contentTypeMatch[2]) {
result.charset = contentTypeMatch[2].trim().replace(/"/g, '').toLowerCase();
}
}
const encodingMatch = headers.match(/Content-Transfer-Encoding:\s*([^\s]+)/i);
if (encodingMatch) {
result.encoding = encodingMatch[1].trim().toLowerCase();
}
return result;
}
function decodeEmail(emailRaw: string): { contentType: string; charset: string; encoding: string; decodedBody: string; headers: string } {
const parts = emailRaw.split(/\r?\n\r?\n/);
const headers = parts[0];
const body = parts.slice(1).join('\n\n');
const { contentType, encoding, charset } = parseEmailHeaders(headers);
const decodedBody = decodeMIME(body, encoding, charset);
return {
contentType,
charset,
encoding,
decodedBody,
headers
};
}
function processMultipartEmail(emailRaw: string, boundary: string): { text: string; html: string; attachments: Array<{ contentType: string; content: string }> } {
const result = {
text: '',
html: '',
attachments: []
};
const boundaryRegex = new RegExp(`--${boundary}\\r?\\n|--${boundary}--\\r?\\n?`, 'g');
const parts = emailRaw.split(boundaryRegex).filter(part => part.trim());
parts.forEach(part => {
const decoded = decodeEmail(part);
if (decoded.contentType === 'text/plain') {
result.text = decoded.decodedBody;
} else if (decoded.contentType === 'text/html') {
result.html = decoded.decodedBody;
} else if (decoded.contentType.startsWith('image/') || decoded.contentType.startsWith('application/')) {
result.attachments.push({
contentType: decoded.contentType,
content: decoded.decodedBody
});
}
});
return result;
}
// Replace the old decodeMimeContent function with a new implementation that uses the above functions
function decodeMimeContent(content: string): string {
if (!content) return '';
try {
// Check if the content includes headers
if (content.includes('Content-Type:') || content.includes('Content-Transfer-Encoding:')) {
// If it's a complete email with headers, use the full decoding process
const decoded = decodeEmail(content);
return decoded.decodedBody;
}
// If no headers are present, try to detect the encoding and decode accordingly
if (content.includes('=?UTF-8?B?') || content.includes('=?utf-8?B?')) {
// Base64 encoded content
return decodeMIME(content, 'base64', 'utf-8');
} else if (content.includes('=?UTF-8?Q?') || content.includes('=?utf-8?Q?') || content.includes('=20')) {
// Quoted-printable content
return decodeMIME(content, 'quoted-printable', 'utf-8');
}
// If no specific encoding is detected, try quoted-printable first
const qpDecoded = decodeMIME(content, 'quoted-printable', 'utf-8');
if (qpDecoded !== content) {
return qpDecoded;
}
// If quoted-printable didn't change anything, return the original content
return content;
} catch (error) {
console.error('Error decoding email content:', error);
return content;
}
};
}
export default function MailPage() {
// Single IMAP account for now