287 lines
No EOL
10 KiB
JavaScript
287 lines
No EOL
10 KiB
JavaScript
/**
|
|
* Parse a MIME email and extract its components
|
|
* @param {string} mimeContent - The raw MIME content of the email
|
|
* @returns {Object} Parsed email with headers, text and HTML content
|
|
*/
|
|
function parseMimeEmail(mimeContent) {
|
|
console.log('Starting MIME email parsing...');
|
|
|
|
// Extract headers and body
|
|
const parts = mimeContent.split(/\r?\n\r?\n/, 2);
|
|
const headers = parseHeaders(parts[0]);
|
|
const body = parts.length > 1 ? parts.slice(1).join('\n\n') : '';
|
|
|
|
console.log('Headers parsed:', Object.keys(headers).length, 'headers found');
|
|
|
|
// Find boundary if multipart
|
|
const contentType = headers['Content-Type'] || '';
|
|
const boundaryMatch = contentType.match(/boundary="?([^";\r\n]+)"?/i);
|
|
const boundary = boundaryMatch ? boundaryMatch[1] : null;
|
|
|
|
console.log('Content-Type:', contentType);
|
|
console.log('Boundary detected:', boundary || 'None');
|
|
|
|
// Parse body parts if multipart
|
|
let textContent = '';
|
|
let htmlContent = '';
|
|
|
|
if (boundary) {
|
|
console.log('Processing multipart email with boundary:', boundary);
|
|
const bodyParts = body.split(new RegExp(`--${boundary}(?:--)?\r?\n?`));
|
|
console.log('Found', bodyParts.length, 'body parts');
|
|
|
|
// Process each part
|
|
bodyParts.forEach((part, index) => {
|
|
if (!part.trim()) {
|
|
console.log(`Part ${index}: Empty part, skipping`);
|
|
return;
|
|
}
|
|
|
|
console.log(`Processing part ${index}...`);
|
|
const partParts = part.split(/\r?\n\r?\n/, 2);
|
|
const partHeaders = parseHeaders(partParts[0]);
|
|
const partContent = partParts.length > 1 ? partParts.slice(1).join('\n\n') : '';
|
|
|
|
const partContentType = partHeaders['Content-Type'] || '';
|
|
console.log(`Part ${index} Content-Type:`, partContentType);
|
|
|
|
// Handle nested multipart
|
|
if (partContentType.includes('multipart/')) {
|
|
console.log(`Part ${index}: Found nested multipart content`);
|
|
const nestedBoundaryMatch = partContentType.match(/boundary="?([^";\r\n]+)"?/i);
|
|
if (nestedBoundaryMatch) {
|
|
const nestedBoundary = nestedBoundaryMatch[1];
|
|
console.log(`Part ${index}: Nested boundary:`, nestedBoundary);
|
|
const nestedParts = partContent.split(new RegExp(`--${nestedBoundary}(?:--)?\r?\n?`));
|
|
console.log(`Part ${index}: Found`, nestedParts.length, 'nested parts');
|
|
|
|
nestedParts.forEach((nestedPart, nestedIndex) => {
|
|
if (!nestedPart.trim()) {
|
|
console.log(`Part ${index}, Nested part ${nestedIndex}: Empty, skipping`);
|
|
return;
|
|
}
|
|
|
|
console.log(`Processing nested part ${nestedIndex} of part ${index}...`);
|
|
const nestedPartParts = nestedPart.split(/\r?\n\r?\n/, 2);
|
|
const nestedPartHeaders = parseHeaders(nestedPartParts[0]);
|
|
const nestedPartContent = nestedPartParts.length > 1 ? nestedPartParts.slice(1).join('\n\n') : '';
|
|
|
|
const nestedPartContentType = nestedPartHeaders['Content-Type'] || '';
|
|
console.log(`Nested part ${nestedIndex} Content-Type:`, nestedPartContentType);
|
|
|
|
if (nestedPartContentType.includes('text/plain')) {
|
|
console.log(`Nested part ${nestedIndex}: Found plain text content`);
|
|
textContent = decodeContent(nestedPartContent, nestedPartHeaders);
|
|
console.log('Plain text content length:', textContent.length);
|
|
} else if (nestedPartContentType.includes('text/html')) {
|
|
console.log(`Nested part ${nestedIndex}: Found HTML content`);
|
|
htmlContent = decodeContent(nestedPartContent, nestedPartHeaders);
|
|
console.log('HTML content length:', htmlContent.length);
|
|
} else {
|
|
console.log(`Nested part ${nestedIndex}: Unknown content type, skipping`);
|
|
}
|
|
});
|
|
} else {
|
|
console.log(`Part ${index}: No nested boundary found in multipart content`);
|
|
}
|
|
}
|
|
// Handle regular content types
|
|
else if (partContentType.includes('text/plain')) {
|
|
console.log(`Part ${index}: Found plain text content`);
|
|
textContent = decodeContent(partContent, partHeaders);
|
|
console.log('Plain text content length:', textContent.length);
|
|
} else if (partContentType.includes('text/html')) {
|
|
console.log(`Part ${index}: Found HTML content`);
|
|
htmlContent = decodeContent(partContent, partHeaders);
|
|
console.log('HTML content length:', htmlContent.length);
|
|
} else {
|
|
console.log(`Part ${index}: Unknown content type, skipping`);
|
|
}
|
|
});
|
|
} else {
|
|
// Not multipart, treat as plain text
|
|
console.log('Email is not multipart, treating body as plain text');
|
|
textContent = body;
|
|
console.log('Plain text content length:', textContent.length);
|
|
}
|
|
|
|
console.log('MIME parsing complete');
|
|
return {
|
|
headers,
|
|
textContent,
|
|
htmlContent,
|
|
raw: mimeContent
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Parse email headers into an object
|
|
* @param {string} headerText - Raw header text
|
|
* @returns {Object} Parsed headers
|
|
*/
|
|
function parseHeaders(headerText) {
|
|
console.log('Parsing headers...');
|
|
const headers = {};
|
|
let currentHeader = '';
|
|
let currentValue = '';
|
|
|
|
// Split by lines and process
|
|
const lines = headerText.split(/\r?\n/);
|
|
console.log('Header lines to process:', lines.length);
|
|
|
|
lines.forEach((line, index) => {
|
|
// If line starts with whitespace, it's a continuation
|
|
if (/^\s+/.test(line)) {
|
|
currentValue += ' ' + line.trim();
|
|
console.log(`Line ${index+1}: Continuation of header "${currentHeader}"`);
|
|
} else {
|
|
// Save previous header if exists
|
|
if (currentHeader) {
|
|
headers[currentHeader] = currentValue.trim();
|
|
console.log(`Saved header: ${currentHeader}`);
|
|
}
|
|
|
|
// Parse new header
|
|
const match = line.match(/^([^:]+):\s*(.*)/);
|
|
if (match) {
|
|
currentHeader = match[1];
|
|
currentValue = match[2];
|
|
console.log(`Line ${index+1}: New header "${currentHeader}"`);
|
|
} else {
|
|
console.log(`Line ${index+1}: Not a valid header line: "${line}"`);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Save the last header
|
|
if (currentHeader) {
|
|
headers[currentHeader] = currentValue.trim();
|
|
console.log(`Saved final header: ${currentHeader}`);
|
|
}
|
|
|
|
console.log('Headers parsing complete, found', Object.keys(headers).length, 'headers');
|
|
return headers;
|
|
}
|
|
|
|
/**
|
|
* Decode content based on Content-Transfer-Encoding
|
|
* @param {string} content - Raw content
|
|
* @param {Object} headers - Headers with encoding information
|
|
* @returns {string} Decoded content
|
|
*/
|
|
function decodeContent(content, headers) {
|
|
const encoding = headers['Content-Transfer-Encoding'] || '';
|
|
console.log('Decoding content with encoding:', encoding || 'none specified');
|
|
|
|
if (encoding.toLowerCase() === 'quoted-printable') {
|
|
console.log('Decoding quoted-printable content');
|
|
const decoded = decodeQuotedPrintable(content);
|
|
console.log('Decoded content length:', decoded.length);
|
|
return decoded;
|
|
} else if (encoding.toLowerCase() === 'base64') {
|
|
console.log('Decoding base64 content');
|
|
try {
|
|
const decoded = atob(content.replace(/\s+/g, ''));
|
|
console.log('Decoded content length:', decoded.length);
|
|
return decoded;
|
|
} catch (error) {
|
|
console.error('Base64 decoding failed:', error);
|
|
return content;
|
|
}
|
|
}
|
|
|
|
console.log('No decoding needed, returning raw content');
|
|
return content;
|
|
}
|
|
|
|
/**
|
|
* Decode quoted-printable content
|
|
* @param {string} content - Quoted-printable encoded content
|
|
* @returns {string} Decoded content
|
|
*/
|
|
function decodeQuotedPrintable(content) {
|
|
console.log('Starting quoted-printable decoding');
|
|
console.log('Input content length:', content.length);
|
|
|
|
// Remove soft line breaks
|
|
const withoutLineBreaks = content.replace(/=\r?\n/g, '');
|
|
console.log('After removing soft line breaks:', withoutLineBreaks.length);
|
|
|
|
// Decode hex characters
|
|
const decoded = withoutLineBreaks.replace(/=([0-9A-F]{2})/gi, (_, hex) => {
|
|
const char = String.fromCharCode(parseInt(hex, 16));
|
|
return char;
|
|
});
|
|
|
|
console.log('After decoding hex characters:', decoded.length);
|
|
return decoded;
|
|
}
|
|
|
|
/**
|
|
* Extract email thread from MIME content
|
|
* @param {string} mimeContent - Raw MIME content
|
|
* @returns {Object} Structured email data
|
|
*/
|
|
function extractEmailThread(mimeContent) {
|
|
console.log('Extracting email thread from MIME content...');
|
|
const parsed = parseMimeEmail(mimeContent);
|
|
|
|
// Get sender information
|
|
const from = parsed.headers['From'] || '';
|
|
console.log('From header:', from);
|
|
const fromMatch = from.match(/(.*?)\s*<([^>]+)>/);
|
|
const senderName = fromMatch ? fromMatch[1].trim() : from;
|
|
const senderEmail = fromMatch ? fromMatch[2] : from;
|
|
console.log('Sender parsed:', senderName, senderEmail);
|
|
|
|
// Get recipient information
|
|
const to = parsed.headers['To'] || '';
|
|
console.log('To header:', to);
|
|
const toMatch = to.match(/(.*?)\s*<([^>]+)>/);
|
|
const recipientName = toMatch ? toMatch[1].trim() : to;
|
|
const recipientEmail = toMatch ? toMatch[2] : to;
|
|
console.log('Recipient parsed:', recipientName, recipientEmail);
|
|
|
|
// Get subject
|
|
const subject = parsed.headers['Subject'] || '';
|
|
console.log('Subject:', subject);
|
|
|
|
// Get date
|
|
const date = parsed.headers['Date'] || '';
|
|
console.log('Date:', date);
|
|
|
|
// Prefer HTML content if available, otherwise use text
|
|
const content = parsed.htmlContent || parsed.textContent;
|
|
console.log('Using content type:', parsed.htmlContent ? 'HTML' : 'Plain text');
|
|
console.log('Content length:', content.length);
|
|
|
|
console.log('Email thread extraction complete');
|
|
return {
|
|
subject,
|
|
date,
|
|
sender: {
|
|
name: senderName,
|
|
email: senderEmail
|
|
},
|
|
recipient: {
|
|
name: recipientName,
|
|
email: recipientEmail
|
|
},
|
|
content,
|
|
cc: parsed.headers['Cc'] || '',
|
|
bcc: parsed.headers['Bcc'] || '',
|
|
messageId: parsed.headers['Message-ID'] || '',
|
|
inReplyTo: parsed.headers['In-Reply-To'] || '',
|
|
references: parsed.headers['References'] || '',
|
|
headers: parsed.headers,
|
|
raw: parsed.raw
|
|
};
|
|
}
|
|
|
|
// Export functions
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = {
|
|
parseMimeEmail,
|
|
extractEmailThread
|
|
};
|
|
}
|