/** * 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 }; }