This commit is contained in:
2025-05-21 14:28:33 +00:00
parent 38811dbf23
commit 10ab09894b
8 changed files with 652 additions and 162 deletions

View File

@ -132,9 +132,20 @@ export class DataHandler implements IDataHandler {
return;
}
// Check for end of data marker
const lastChunk = session.emailDataChunks[session.emailDataChunks.length - 1] || '';
if (SMTP_PATTERNS.END_DATA.test(lastChunk)) {
// Check for end of data marker - combine all chunks to ensure we don't miss it if split across chunks
const combinedData = session.emailDataChunks.join('');
// More permissive check for the end-of-data marker
// Check for various formats: \r\n.\r\n, \n.\r\n, \r\n.\n, \n.\n, or just . or .\r\n at the end
if (combinedData.endsWith('\r\n.\r\n') ||
combinedData.endsWith('\n.\r\n') ||
combinedData.endsWith('\r\n.\n') ||
combinedData.endsWith('\n.\n') ||
data === '.\r\n' ||
data === '.') {
SmtpLogger.debug(`End of data marker found for session ${session.id}`, { sessionId: session.id });
// End of data marker found
await this.handleEndOfData(socket, session);
}
@ -149,8 +160,13 @@ export class DataHandler implements IDataHandler {
// Combine all chunks and remove end of data marker
session.emailData = (session.emailDataChunks || []).join('');
// Remove trailing end-of-data marker: \r\n.\r\n
session.emailData = session.emailData.replace(/\r\n\.\r\n$/, '');
// Remove trailing end-of-data marker: various formats
session.emailData = session.emailData
.replace(/\r\n\.\r\n$/, '')
.replace(/\n\.\r\n$/, '')
.replace(/\r\n\.\n$/, '')
.replace(/\n\.\n$/, '')
.replace(/\.$/, ''); // Handle a lone dot at the end
// Remove dot-stuffing (RFC 5321, section 4.5.2)
session.emailData = session.emailData.replace(/\r\n\.\./g, '\r\n.');
@ -286,16 +302,77 @@ export class DataHandler implements IDataHandler {
* @returns Promise that resolves with the parsed Email object
*/
public async parseEmail(session: ISmtpSession): Promise<Email> {
// Create an email with minimal required options
// Parse raw email text to extract headers
const rawData = session.emailData;
const headerEndIndex = rawData.indexOf('\r\n\r\n');
if (headerEndIndex === -1) {
// No headers/body separation, create basic email
return new Email({
from: session.envelope.mailFrom.address,
to: session.envelope.rcptTo.map(r => r.address),
subject: 'Received via SMTP',
text: rawData
});
}
// Extract headers and body
const headersText = rawData.substring(0, headerEndIndex);
const bodyText = rawData.substring(headerEndIndex + 4); // Skip the \r\n\r\n separator
// Parse headers
const headers: Record<string, string> = {};
const headerLines = headersText.split('\r\n');
let currentHeader = '';
for (const line of headerLines) {
// Check if this is a continuation of a previous header
if (line.startsWith(' ') || line.startsWith('\t')) {
if (currentHeader) {
headers[currentHeader] += ' ' + line.trim();
}
continue;
}
// This is a new header
const separatorIndex = line.indexOf(':');
if (separatorIndex !== -1) {
const name = line.substring(0, separatorIndex).trim().toLowerCase();
const value = line.substring(separatorIndex + 1).trim();
headers[name] = value;
currentHeader = name;
}
}
// Extract common headers
const subject = headers['subject'] || 'No Subject';
const from = headers['from'] || session.envelope.mailFrom.address;
const to = headers['to'] || session.envelope.rcptTo.map(r => r.address).join(', ');
const messageId = headers['message-id'] || `<${Date.now()}.${Math.random().toString(36).substring(2)}@${this.options.hostname}>`;
// Create email object
const email = new Email({
from: session.envelope.mailFrom.address,
to: session.envelope.rcptTo.map(r => r.address),
subject: 'Received via SMTP',
text: session.emailData
from: from,
to: to.split(',').map(addr => addr.trim()),
subject: subject,
text: bodyText,
messageId: messageId,
// Add original session envelope data for accurate routing
originalMailFrom: session.envelope.mailFrom.address,
originalRcptTo: session.envelope.rcptTo.map(r => r.address)
});
// Note: In a real implementation, we would parse the raw email data
// to extract headers, content, etc., but that's beyond the scope of this refactoring
// Add received header
const timestamp = new Date().toUTCString();
const receivedHeader = `from ${session.clientHostname || 'unknown'} (${session.remoteAddress}) by ${this.options.hostname} with ESMTP id ${session.id}; ${timestamp}`;
email.addHeader('Received', receivedHeader);
// Add all original headers
for (const [name, value] of Object.entries(headers)) {
if (!['from', 'to', 'subject', 'message-id'].includes(name)) {
email.addHeader(name, value);
}
}
return email;
}