update
This commit is contained in:
parent
ca111f4783
commit
2564d0874b
@ -151,6 +151,7 @@ export class Email {
|
||||
if (!input) return '';
|
||||
|
||||
// Remove CR and LF characters to prevent header injection
|
||||
// But preserve all other special characters including Unicode
|
||||
return input.replace(/\r|\n/g, ' ');
|
||||
}
|
||||
|
||||
|
@ -410,8 +410,21 @@ export class DataHandler implements IDataHandler {
|
||||
let to: string[] = [];
|
||||
|
||||
// Try to get recipients from parsed email
|
||||
if (parsed.to?.value) {
|
||||
to = parsed.to.value.map(addr => addr.address);
|
||||
if (parsed.to) {
|
||||
// Handle both array and single object cases
|
||||
if (Array.isArray(parsed.to)) {
|
||||
to = parsed.to.map(addr => typeof addr === 'object' && addr !== null && 'address' in addr ? String(addr.address) : '');
|
||||
} else if (typeof parsed.to === 'object' && parsed.to !== null) {
|
||||
// Handle object with value property (array or single address object)
|
||||
if ('value' in parsed.to && Array.isArray(parsed.to.value)) {
|
||||
to = parsed.to.value.map(addr => typeof addr === 'object' && addr !== null && 'address' in addr ? String(addr.address) : '');
|
||||
} else if ('address' in parsed.to) {
|
||||
to = [String(parsed.to.address)];
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out empty strings
|
||||
to = to.filter(Boolean);
|
||||
}
|
||||
|
||||
// If no recipients found, fall back to envelope
|
||||
@ -419,7 +432,9 @@ export class DataHandler implements IDataHandler {
|
||||
to = session.envelope.rcptTo.map(r => r.address);
|
||||
}
|
||||
|
||||
const subject = parsed.subject || 'No Subject';
|
||||
// Handle subject with special care for character encoding
|
||||
const subject = parsed.subject || headers['subject'] || 'No Subject';
|
||||
SmtpLogger.debug(`Parsed email subject: ${subject}`, { subject });
|
||||
|
||||
// Create email object using the parsed content
|
||||
const email = new Email({
|
||||
@ -438,13 +453,66 @@ export class DataHandler implements IDataHandler {
|
||||
|
||||
// Add attachments if any
|
||||
if (parsed.attachments && parsed.attachments.length > 0) {
|
||||
SmtpLogger.debug(`Found ${parsed.attachments.length} attachments in email`, {
|
||||
sessionId: session.id,
|
||||
attachmentCount: parsed.attachments.length
|
||||
});
|
||||
|
||||
for (const attachment of parsed.attachments) {
|
||||
// Enhanced attachment logging for debugging
|
||||
SmtpLogger.debug(`Processing attachment: ${attachment.filename}`, {
|
||||
filename: attachment.filename,
|
||||
contentType: attachment.contentType,
|
||||
size: attachment.content?.length,
|
||||
contentId: attachment.contentId || 'none',
|
||||
contentDisposition: attachment.contentDisposition || 'none'
|
||||
});
|
||||
|
||||
// Ensure we have valid content
|
||||
if (!attachment.content || !Buffer.isBuffer(attachment.content)) {
|
||||
SmtpLogger.warn(`Attachment ${attachment.filename} has invalid content, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fix up content type if missing but can be inferred from filename
|
||||
let contentType = attachment.contentType || 'application/octet-stream';
|
||||
const filename = attachment.filename || 'attachment';
|
||||
|
||||
if (!contentType || contentType === 'application/octet-stream') {
|
||||
if (filename.endsWith('.pdf')) {
|
||||
contentType = 'application/pdf';
|
||||
} else if (filename.endsWith('.jpg') || filename.endsWith('.jpeg')) {
|
||||
contentType = 'image/jpeg';
|
||||
} else if (filename.endsWith('.png')) {
|
||||
contentType = 'image/png';
|
||||
} else if (filename.endsWith('.gif')) {
|
||||
contentType = 'image/gif';
|
||||
} else if (filename.endsWith('.txt')) {
|
||||
contentType = 'text/plain';
|
||||
}
|
||||
}
|
||||
|
||||
email.attachments.push({
|
||||
filename: attachment.filename || 'attachment',
|
||||
filename: filename,
|
||||
content: attachment.content,
|
||||
contentType: attachment.contentType || 'application/octet-stream',
|
||||
contentType: contentType,
|
||||
contentId: attachment.contentId
|
||||
});
|
||||
|
||||
SmtpLogger.debug(`Added attachment to email: ${filename}, type: ${contentType}, size: ${attachment.content.length} bytes`);
|
||||
}
|
||||
} else {
|
||||
SmtpLogger.debug(`No attachments found in email via parser`, { sessionId: session.id });
|
||||
|
||||
// Additional check for attachments that might be missed by the parser
|
||||
// Look for Content-Disposition headers in the raw data
|
||||
const rawData = session.emailData;
|
||||
const hasAttachmentDisposition = rawData.includes('Content-Disposition: attachment');
|
||||
|
||||
if (hasAttachmentDisposition) {
|
||||
SmtpLogger.debug(`Found potential attachments in raw data, will handle in multipart processing`, {
|
||||
sessionId: session.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -530,6 +598,19 @@ export class DataHandler implements IDataHandler {
|
||||
if (separatorIndex !== -1) {
|
||||
const name = line.substring(0, separatorIndex).trim().toLowerCase();
|
||||
const value = line.substring(separatorIndex + 1).trim();
|
||||
|
||||
// Special handling for MIME-encoded headers (especially Subject)
|
||||
if (name === 'subject' && value.includes('=?')) {
|
||||
try {
|
||||
// Use plugins.mailparser to decode the MIME-encoded subject
|
||||
// This is a simplified approach - in a real system, you'd use a full MIME decoder
|
||||
// For now, just log it for debugging
|
||||
SmtpLogger.debug(`Found encoded subject: ${value}`, { encodedSubject: value });
|
||||
} catch (error) {
|
||||
SmtpLogger.warn(`Failed to decode MIME-encoded subject: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
headers[name] = value;
|
||||
currentHeader = name;
|
||||
}
|
||||
@ -604,18 +685,22 @@ export class DataHandler implements IDataHandler {
|
||||
// Split the body by boundary
|
||||
const parts = bodyText.split(`--${boundary}`);
|
||||
|
||||
SmtpLogger.debug(`Handling multipart content with ${parts.length - 1} parts (boundary: ${boundary})`);
|
||||
|
||||
// Process each part
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
|
||||
// Skip the end boundary marker
|
||||
if (part.startsWith('--')) {
|
||||
SmtpLogger.debug(`Found end boundary marker in part ${i}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the headers and content
|
||||
const partHeaderEndIndex = part.indexOf('\r\n\r\n');
|
||||
if (partHeaderEndIndex === -1) {
|
||||
SmtpLogger.debug(`No header/body separator found in part ${i}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -649,28 +734,187 @@ export class DataHandler implements IDataHandler {
|
||||
// Get content type
|
||||
const contentType = partHeaders['content-type'] || '';
|
||||
|
||||
// Get encoding
|
||||
const encoding = partHeaders['content-transfer-encoding'] || '7bit';
|
||||
|
||||
// Get disposition
|
||||
const disposition = partHeaders['content-disposition'] || '';
|
||||
|
||||
// Log part information
|
||||
SmtpLogger.debug(`Processing MIME part ${i}: type=${contentType}, encoding=${encoding}, disposition=${disposition}`);
|
||||
|
||||
// Handle text/plain parts
|
||||
if (contentType.includes('text/plain')) {
|
||||
email.text = partContent.trim();
|
||||
try {
|
||||
// Decode content based on encoding
|
||||
let decodedContent = partContent;
|
||||
|
||||
if (encoding.toLowerCase() === 'base64') {
|
||||
// Remove line breaks from base64 content before decoding
|
||||
const cleanBase64 = partContent.replace(/[\r\n]/g, '');
|
||||
try {
|
||||
decodedContent = Buffer.from(cleanBase64, 'base64').toString('utf8');
|
||||
} catch (error) {
|
||||
SmtpLogger.warn(`Failed to decode base64 text content: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
} else if (encoding.toLowerCase() === 'quoted-printable') {
|
||||
try {
|
||||
// Basic quoted-printable decoding
|
||||
decodedContent = partContent.replace(/=([0-9A-F]{2})/gi, (match, hex) => {
|
||||
return String.fromCharCode(parseInt(hex, 16));
|
||||
});
|
||||
} catch (error) {
|
||||
SmtpLogger.warn(`Failed to decode quoted-printable content: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
email.text = decodedContent.trim();
|
||||
} catch (error) {
|
||||
SmtpLogger.warn(`Error processing text/plain part: ${error instanceof Error ? error.message : String(error)}`);
|
||||
email.text = partContent.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle text/html parts
|
||||
if (contentType.includes('text/html')) {
|
||||
email.html = partContent.trim();
|
||||
try {
|
||||
// Decode content based on encoding
|
||||
let decodedContent = partContent;
|
||||
|
||||
if (encoding.toLowerCase() === 'base64') {
|
||||
// Remove line breaks from base64 content before decoding
|
||||
const cleanBase64 = partContent.replace(/[\r\n]/g, '');
|
||||
try {
|
||||
decodedContent = Buffer.from(cleanBase64, 'base64').toString('utf8');
|
||||
} catch (error) {
|
||||
SmtpLogger.warn(`Failed to decode base64 HTML content: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
} else if (encoding.toLowerCase() === 'quoted-printable') {
|
||||
try {
|
||||
// Basic quoted-printable decoding
|
||||
decodedContent = partContent.replace(/=([0-9A-F]{2})/gi, (match, hex) => {
|
||||
return String.fromCharCode(parseInt(hex, 16));
|
||||
});
|
||||
} catch (error) {
|
||||
SmtpLogger.warn(`Failed to decode quoted-printable HTML content: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
email.html = decodedContent.trim();
|
||||
} catch (error) {
|
||||
SmtpLogger.warn(`Error processing text/html part: ${error instanceof Error ? error.message : String(error)}`);
|
||||
email.html = partContent.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle attachments
|
||||
if (partHeaders['content-disposition'] && partHeaders['content-disposition'].includes('attachment')) {
|
||||
// Extract filename
|
||||
const filenameMatch = partHeaders['content-disposition'].match(/filename="?([^";\r\n]+)"?/i);
|
||||
const filename = filenameMatch && filenameMatch[1] ? filenameMatch[1] : 'attachment';
|
||||
|
||||
// Add attachment
|
||||
email.attachments.push({
|
||||
filename,
|
||||
content: Buffer.from(partContent),
|
||||
contentType: contentType || 'application/octet-stream'
|
||||
});
|
||||
// Handle attachments - detect attachments by content disposition or by content-type
|
||||
const isAttachment =
|
||||
(disposition && disposition.toLowerCase().includes('attachment')) ||
|
||||
(!contentType.includes('text/plain') && !contentType.includes('text/html'));
|
||||
|
||||
if (isAttachment) {
|
||||
try {
|
||||
// Extract filename from Content-Disposition or generate one based on content type
|
||||
let filename = 'attachment';
|
||||
|
||||
if (disposition) {
|
||||
const filenameMatch = disposition.match(/filename="?([^";\r\n]+)"?/i);
|
||||
if (filenameMatch && filenameMatch[1]) {
|
||||
filename = filenameMatch[1].trim();
|
||||
}
|
||||
} else if (contentType) {
|
||||
// If no filename but we have content type, generate a name with appropriate extension
|
||||
const mainType = contentType.split(';')[0].trim().toLowerCase();
|
||||
|
||||
if (mainType === 'application/pdf') {
|
||||
filename = `attachment_${Date.now()}.pdf`;
|
||||
} else if (mainType === 'image/jpeg' || mainType === 'image/jpg') {
|
||||
filename = `image_${Date.now()}.jpg`;
|
||||
} else if (mainType === 'image/png') {
|
||||
filename = `image_${Date.now()}.png`;
|
||||
} else if (mainType === 'image/gif') {
|
||||
filename = `image_${Date.now()}.gif`;
|
||||
} else {
|
||||
filename = `attachment_${Date.now()}.bin`;
|
||||
}
|
||||
}
|
||||
|
||||
// Decode content based on encoding
|
||||
let content: Buffer;
|
||||
|
||||
if (encoding.toLowerCase() === 'base64') {
|
||||
try {
|
||||
// Remove line breaks from base64 content before decoding
|
||||
const cleanBase64 = partContent.replace(/[\r\n]/g, '');
|
||||
content = Buffer.from(cleanBase64, 'base64');
|
||||
SmtpLogger.debug(`Successfully decoded base64 attachment: ${filename}, size: ${content.length} bytes`);
|
||||
} catch (error) {
|
||||
SmtpLogger.warn(`Failed to decode base64 attachment: ${error instanceof Error ? error.message : String(error)}`);
|
||||
content = Buffer.from(partContent);
|
||||
}
|
||||
} else if (encoding.toLowerCase() === 'quoted-printable') {
|
||||
try {
|
||||
// Basic quoted-printable decoding
|
||||
const decodedContent = partContent.replace(/=([0-9A-F]{2})/gi, (match, hex) => {
|
||||
return String.fromCharCode(parseInt(hex, 16));
|
||||
});
|
||||
content = Buffer.from(decodedContent);
|
||||
} catch (error) {
|
||||
SmtpLogger.warn(`Failed to decode quoted-printable attachment: ${error instanceof Error ? error.message : String(error)}`);
|
||||
content = Buffer.from(partContent);
|
||||
}
|
||||
} else {
|
||||
// Default for 7bit, 8bit, or binary encoding - no decoding needed
|
||||
content = Buffer.from(partContent);
|
||||
}
|
||||
|
||||
// Determine content type - use the one from headers or infer from filename
|
||||
let finalContentType = contentType;
|
||||
|
||||
if (!finalContentType || finalContentType === 'application/octet-stream') {
|
||||
if (filename.endsWith('.pdf')) {
|
||||
finalContentType = 'application/pdf';
|
||||
} else if (filename.endsWith('.jpg') || filename.endsWith('.jpeg')) {
|
||||
finalContentType = 'image/jpeg';
|
||||
} else if (filename.endsWith('.png')) {
|
||||
finalContentType = 'image/png';
|
||||
} else if (filename.endsWith('.gif')) {
|
||||
finalContentType = 'image/gif';
|
||||
} else if (filename.endsWith('.txt')) {
|
||||
finalContentType = 'text/plain';
|
||||
} else if (filename.endsWith('.html')) {
|
||||
finalContentType = 'text/html';
|
||||
}
|
||||
}
|
||||
|
||||
// Add attachment to email
|
||||
email.attachments.push({
|
||||
filename,
|
||||
content,
|
||||
contentType: finalContentType || 'application/octet-stream'
|
||||
});
|
||||
|
||||
SmtpLogger.debug(`Added attachment: ${filename}, type: ${finalContentType}, size: ${content.length} bytes`);
|
||||
} catch (error) {
|
||||
SmtpLogger.error(`Failed to process attachment: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for nested multipart content
|
||||
if (contentType.includes('multipart/')) {
|
||||
try {
|
||||
// Extract boundary
|
||||
const nestedBoundaryMatch = contentType.match(/boundary="?([^";\r\n]+)"?/i);
|
||||
if (nestedBoundaryMatch && nestedBoundaryMatch[1]) {
|
||||
const nestedBoundary = nestedBoundaryMatch[1].trim();
|
||||
SmtpLogger.debug(`Found nested multipart content with boundary: ${nestedBoundary}`);
|
||||
|
||||
// Process nested multipart
|
||||
this.handleMultipartContent(email, partContent, nestedBoundary);
|
||||
}
|
||||
} catch (error) {
|
||||
SmtpLogger.warn(`Error processing nested multipart content: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user