update
This commit is contained in:
@@ -151,6 +151,7 @@ export class Email {
|
|||||||
if (!input) return '';
|
if (!input) return '';
|
||||||
|
|
||||||
// Remove CR and LF characters to prevent header injection
|
// Remove CR and LF characters to prevent header injection
|
||||||
|
// But preserve all other special characters including Unicode
|
||||||
return input.replace(/\r|\n/g, ' ');
|
return input.replace(/\r|\n/g, ' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -410,8 +410,21 @@ export class DataHandler implements IDataHandler {
|
|||||||
let to: string[] = [];
|
let to: string[] = [];
|
||||||
|
|
||||||
// Try to get recipients from parsed email
|
// Try to get recipients from parsed email
|
||||||
if (parsed.to?.value) {
|
if (parsed.to) {
|
||||||
to = parsed.to.value.map(addr => addr.address);
|
// 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
|
// 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);
|
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
|
// Create email object using the parsed content
|
||||||
const email = new Email({
|
const email = new Email({
|
||||||
@@ -438,13 +453,66 @@ export class DataHandler implements IDataHandler {
|
|||||||
|
|
||||||
// Add attachments if any
|
// Add attachments if any
|
||||||
if (parsed.attachments && parsed.attachments.length > 0) {
|
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) {
|
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({
|
email.attachments.push({
|
||||||
filename: attachment.filename || 'attachment',
|
filename: filename,
|
||||||
content: attachment.content,
|
content: attachment.content,
|
||||||
contentType: attachment.contentType || 'application/octet-stream',
|
contentType: contentType,
|
||||||
contentId: attachment.contentId
|
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) {
|
if (separatorIndex !== -1) {
|
||||||
const name = line.substring(0, separatorIndex).trim().toLowerCase();
|
const name = line.substring(0, separatorIndex).trim().toLowerCase();
|
||||||
const value = line.substring(separatorIndex + 1).trim();
|
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;
|
headers[name] = value;
|
||||||
currentHeader = name;
|
currentHeader = name;
|
||||||
}
|
}
|
||||||
@@ -604,18 +685,22 @@ export class DataHandler implements IDataHandler {
|
|||||||
// Split the body by boundary
|
// Split the body by boundary
|
||||||
const parts = bodyText.split(`--${boundary}`);
|
const parts = bodyText.split(`--${boundary}`);
|
||||||
|
|
||||||
|
SmtpLogger.debug(`Handling multipart content with ${parts.length - 1} parts (boundary: ${boundary})`);
|
||||||
|
|
||||||
// Process each part
|
// Process each part
|
||||||
for (let i = 1; i < parts.length; i++) {
|
for (let i = 1; i < parts.length; i++) {
|
||||||
const part = parts[i];
|
const part = parts[i];
|
||||||
|
|
||||||
// Skip the end boundary marker
|
// Skip the end boundary marker
|
||||||
if (part.startsWith('--')) {
|
if (part.startsWith('--')) {
|
||||||
|
SmtpLogger.debug(`Found end boundary marker in part ${i}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the headers and content
|
// Find the headers and content
|
||||||
const partHeaderEndIndex = part.indexOf('\r\n\r\n');
|
const partHeaderEndIndex = part.indexOf('\r\n\r\n');
|
||||||
if (partHeaderEndIndex === -1) {
|
if (partHeaderEndIndex === -1) {
|
||||||
|
SmtpLogger.debug(`No header/body separator found in part ${i}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -649,28 +734,187 @@ export class DataHandler implements IDataHandler {
|
|||||||
// Get content type
|
// Get content type
|
||||||
const contentType = partHeaders['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
|
// Handle text/plain parts
|
||||||
if (contentType.includes('text/plain')) {
|
if (contentType.includes('text/plain')) {
|
||||||
|
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();
|
email.text = partContent.trim();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle text/html parts
|
// Handle text/html parts
|
||||||
if (contentType.includes('text/html')) {
|
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)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle attachments
|
email.html = decodedContent.trim();
|
||||||
if (partHeaders['content-disposition'] && partHeaders['content-disposition'].includes('attachment')) {
|
} catch (error) {
|
||||||
// Extract filename
|
SmtpLogger.warn(`Error processing text/html part: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
const filenameMatch = partHeaders['content-disposition'].match(/filename="?([^";\r\n]+)"?/i);
|
email.html = partContent.trim();
|
||||||
const filename = filenameMatch && filenameMatch[1] ? filenameMatch[1] : 'attachment';
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add attachment
|
// 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({
|
email.attachments.push({
|
||||||
filename,
|
filename,
|
||||||
content: Buffer.from(partContent),
|
content,
|
||||||
contentType: contentType || 'application/octet-stream'
|
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)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user