Enhance email handling by adding buffer serialization for attachments in QueueManager, sanitizing attachments in SMTPClient, and implementing email loop detection in IncomingSMTPServer.

This commit is contained in:
2026-01-02 21:56:19 -03:00
parent 4e650e8592
commit 68bfededa9
3 changed files with 66 additions and 7 deletions

View File

@@ -40,13 +40,39 @@ export class QueueManager {
{
store: new SQLiteStore({
path: this.dbPath,
// Store email
serialize: (data) => JSON.stringify(data),
deserialize: (text) => JSON.parse(text),
// Buffer handling
serialize: (data) => {
// Convert attachment buffers to base64
const serializable = { ...data };
if (serializable.attachments) {
serializable.attachments = serializable.attachments.map((att) => ({
...att,
content: att.content && Buffer.isBuffer(att.content)
? att.content.toString('base64')
: att.content,
_isBuffer: Buffer.isBuffer(att.content),
}));
}
return JSON.stringify(serializable);
},
deserialize: (text) => {
const data = JSON.parse(text);
// Convert base64 back to Buffers
if (data.attachments) {
data.attachments = data.attachments.map((att) => ({
...att,
content: att._isBuffer && typeof att.content === 'string'
? Buffer.from(att.content, 'base64')
: att.content,
}));
// Clean up
data.attachments.forEach((att) => delete att._isBuffer);
}
return data;
},
}),
// Retry
// Retry configuration
maxRetries: queueConfig.maxRetries,
retryDelay: queueConfig.retryDelay,
concurrent: 1,
// Retry exponential backoff
afterProcessDelay: 100,

View File

@@ -50,6 +50,24 @@ export class SMTPClient {
const transport = this.getTransport(provider);
try {
// Validate and sanitize attachments
const sanitizedAttachments = (emailData.attachments || []).map((att) => {
// Ensure content is a Buffer or string
if (att.content && typeof att.content === 'object' && !Buffer.isBuffer(att.content)) {
this.logger.warn('Invalid attachment content type, skipping', {
filename: att.filename,
type: typeof att.content,
});
return null;
}
return {
filename: att.filename || 'attachment',
content: att.content,
contentType: att.contentType || 'application/octet-stream',
encoding: att.encoding || 'base64',
};
}).filter(Boolean); // Remove null entries
// Prepare email
const mailOptions = {
from: provider.from,
@@ -58,7 +76,7 @@ export class SMTPClient {
text: emailData.text,
html: emailData.html,
headers: emailData.headers || {},
attachments: emailData.attachments || [],
attachments: sanitizedAttachments,
// Reply-to original sender
replyTo: emailData.envelope.from,

View File

@@ -119,7 +119,8 @@ export class IncomingSMTPServer {
content: att.content,
contentType: att.contentType,
contentDisposition: att.contentDisposition,
size: att.size,
contentId: att.contentId,
encoding: 'base64',
})) || [],
messageId: parsed.messageId,
date: parsed.date,
@@ -135,6 +136,20 @@ export class IncomingSMTPServer {
throw new Error('Missing recipient address');
}
// Check for email loops (sender sending to themselves)
const loopRecipients = emailData.envelope.to.filter(
(recipient) => recipient.toLowerCase() === emailData.envelope.from.toLowerCase()
);
if (loopRecipients.length > 0) {
this.logger.warn('Email loop detected - dropping email', {
from: emailData.envelope.from,
to: emailData.envelope.to,
loopRecipients,
});
throw new Error('Email loop detected: sender cannot send to themselves');
}
this.logger.debug('Email parsed successfully', {
from: emailData.envelope.from,
to: emailData.envelope.to,