This commit is contained in:
Philipp Kunz 2025-05-21 21:29:04 +00:00
parent ca111f4783
commit 2564d0874b
2 changed files with 264 additions and 19 deletions

View File

@ -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, ' ');
}

View File

@ -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)}`);
}
}
}
}