gpt_chrome_autodrafter_v3/email_parser.js
2025-07-01 15:46:34 -07:00

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