dcrouter/ts/mail/delivery/classes.smtpserver.ts

995 lines
32 KiB
TypeScript
Raw Normal View History

2025-05-08 01:13:54 +00:00
import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import { Email } from '../core/classes.email.js';
2025-05-21 00:12:49 +00:00
import type { UnifiedEmailServer } from '../routing/classes.unified.email.server.js';
2025-05-08 01:13:54 +00:00
import { logger } from '../../logger.js';
2025-05-07 20:20:17 +00:00
import {
SecurityLogger,
SecurityLogLevel,
SecurityEventType,
IPReputationChecker,
ReputationThreshold
2025-05-08 01:13:54 +00:00
} from '../../security/index.js';
2024-02-16 13:28:40 +01:00
2025-05-21 02:17:18 +00:00
import type {
ISmtpServerOptions,
ISmtpSession,
EmailProcessingMode
} from './interfaces.js';
import { SmtpState } from './interfaces.js';
2024-02-16 13:28:40 +01:00
export class SMTPServer {
2025-05-21 00:12:49 +00:00
public emailServerRef: UnifiedEmailServer;
2024-02-16 13:28:40 +01:00
private smtpServerOptions: ISmtpServerOptions;
2025-05-21 10:38:22 +00:00
// Making server protected so tests can access it
protected server: plugins.net.Server;
2025-05-21 02:17:18 +00:00
private sessions: Map<plugins.net.Socket | plugins.tls.TLSSocket, ISmtpSession>;
2025-05-21 10:00:06 +00:00
private sessionTimeouts: Map<string, NodeJS.Timeout>;
private hostname: string;
2025-05-21 10:00:06 +00:00
private sessionIdCounter: number = 0;
private connectionCount: number = 0;
private maxConnections: number = 100; // Default max connections
2024-02-16 13:28:40 +01:00
2025-05-21 00:12:49 +00:00
constructor(emailServerRefArg: UnifiedEmailServer, optionsArg: ISmtpServerOptions) {
2024-02-16 13:28:40 +01:00
console.log('SMTPServer instance is being created...');
2025-05-21 00:12:49 +00:00
this.emailServerRef = emailServerRefArg;
2024-02-16 13:28:40 +01:00
this.smtpServerOptions = optionsArg;
this.sessions = new Map();
2025-05-21 10:00:06 +00:00
this.sessionTimeouts = new Map();
2025-05-21 00:12:49 +00:00
this.hostname = optionsArg.hostname || 'mail.lossless.one';
2025-05-21 10:00:06 +00:00
this.maxConnections = optionsArg.maxSize || 100;
// Start session cleanup interval
setInterval(() => this.cleanupIdleSessions(), 30000); // Run every 30 seconds
2024-02-16 13:28:40 +01:00
this.server = plugins.net.createServer((socket) => {
2025-05-21 10:00:06 +00:00
// Check if we've exceeded maximum connections
if (this.connectionCount >= this.maxConnections) {
logger.log('warn', `Connection limit reached (${this.maxConnections}), rejecting new connection`);
socket.write('421 Too many connections, try again later\r\n');
socket.destroy();
return;
}
this.handleNewConnection(socket);
2024-02-16 13:28:40 +01:00
});
2025-05-21 10:38:22 +00:00
}
/**
* Start the SMTP server and listen on the specified port
* @returns A promise that resolves when the server is listening
*/
public listen(): Promise<void> {
return new Promise<void>((resolve, reject) => {
if (!this.smtpServerOptions.port) {
return reject(new Error('SMTP server port not specified'));
}
const port = this.smtpServerOptions.port;
this.server.listen(port, () => {
logger.log('info', `SMTP server listening on port ${port}`);
console.log(`SMTP server started on port ${port}`);
resolve();
});
this.server.on('error', (err) => {
logger.log('error', `SMTP server error: ${err.message}`);
console.error(`Failed to start SMTP server: ${err.message}`);
reject(err);
});
});
}
/**
* Stop the SMTP server
* @returns A promise that resolves when the server has stopped
*/
public close(): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.server.close((err) => {
if (err) {
logger.log('error', `Error closing SMTP server: ${err.message}`);
reject(err);
return;
}
logger.log('info', 'SMTP server stopped');
resolve();
});
});
2024-02-16 13:28:40 +01:00
}
2025-05-21 10:00:06 +00:00
/**
* Clean up idle sessions
* @private
*/
private cleanupIdleSessions(): void {
const now = Date.now();
const sessionTimeout = this.smtpServerOptions.socketTimeout || 300000; // Default 5 minutes
// Check all sessions for timeout
for (const [socket, session] of this.sessions.entries()) {
if (!session.lastActivity) continue;
const idleTime = now - session.lastActivity;
if (idleTime > sessionTimeout) {
logger.log('info', `Session ${session.id} timed out after ${idleTime}ms of inactivity`);
try {
// Send timeout message and end connection
this.sendResponse(socket, '421 Timeout - closing connection');
socket.destroy();
} catch (error) {
logger.log('error', `Error closing timed out session: ${error.message}`);
}
// Clean up session
this.removeSession(socket);
}
}
}
/**
* Create a new session ID
* @private
*/
private generateSessionId(): string {
return `${Date.now()}-${++this.sessionIdCounter}`;
}
/**
* Properly remove a session and clean up resources
* @private
*/
private removeSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
const session = this.sessions.get(socket);
if (!session) return;
// Clear session timeout if exists
const timeoutId = this.sessionTimeouts.get(session.id);
if (timeoutId) {
clearTimeout(timeoutId);
this.sessionTimeouts.delete(session.id);
}
// Remove session from map
this.sessions.delete(socket);
// Decrement connection count
this.connectionCount--;
logger.log('debug', `Session ${session.id} removed, active connections: ${this.connectionCount}`);
}
/**
* Update last activity timestamp for a session
* @private
*/
private updateSessionActivity(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
const session = this.sessions.get(socket);
if (!session) return;
session.lastActivity = Date.now();
}
2024-02-16 13:28:40 +01:00
2025-05-07 20:20:17 +00:00
private async handleNewConnection(socket: plugins.net.Socket): Promise<void> {
const clientIp = socket.remoteAddress;
const clientPort = socket.remotePort;
console.log(`New connection from ${clientIp}:${clientPort}`);
2025-05-21 10:00:06 +00:00
// Increment connection count
this.connectionCount++;
// Generate unique session ID
const sessionId = this.generateSessionId();
// Initialize a new session
this.sessions.set(socket, {
2025-05-21 10:00:06 +00:00
id: sessionId,
state: SmtpState.GREETING,
clientHostname: '',
mailFrom: '',
rcptTo: [],
emailData: '',
useTLS: false,
2025-05-21 00:12:49 +00:00
connectionEnded: false,
remoteAddress: socket.remoteAddress || '',
secure: false,
authenticated: false,
2025-05-21 10:00:06 +00:00
lastActivity: Date.now(),
2025-05-21 00:12:49 +00:00
envelope: {
mailFrom: {
address: '',
args: {}
},
rcptTo: []
}
2024-02-16 13:28:40 +01:00
});
2025-05-07 20:20:17 +00:00
// Check IP reputation
try {
2025-05-21 00:12:49 +00:00
if (clientIp) {
2025-05-07 20:20:17 +00:00
const reputationChecker = IPReputationChecker.getInstance();
const reputation = await reputationChecker.checkReputation(clientIp);
// Log the reputation check
SecurityLogger.getInstance().logEvent({
level: reputation.score < ReputationThreshold.HIGH_RISK
? SecurityLogLevel.WARN
: SecurityLogLevel.INFO,
type: SecurityEventType.IP_REPUTATION,
message: `IP reputation checked for new SMTP connection: score=${reputation.score}`,
ipAddress: clientIp,
details: {
clientPort,
score: reputation.score,
isSpam: reputation.isSpam,
isProxy: reputation.isProxy,
isTor: reputation.isTor,
isVPN: reputation.isVPN,
country: reputation.country,
blacklists: reputation.blacklists,
socketId: socket.remotePort.toString() + socket.remoteFamily
}
});
// Handle high-risk IPs - add delay or reject based on score
if (reputation.score < ReputationThreshold.HIGH_RISK) {
// For high-risk connections, add an artificial delay to slow down potential spam
const delayMs = Math.min(5000, Math.max(1000, (ReputationThreshold.HIGH_RISK - reputation.score) * 100));
await new Promise(resolve => setTimeout(resolve, delayMs));
if (reputation.score < 5) {
2025-05-21 00:12:49 +00:00
// Very high risk - reject the connection for security
// The email server has security settings for high-risk IPs
this.sendResponse(socket, `554 Transaction failed - IP is on spam blocklist`);
socket.destroy();
return;
2025-05-07 20:20:17 +00:00
}
}
}
} catch (error) {
logger.log('error', `Error checking IP reputation: ${error.message}`, {
ip: clientIp,
error: error.message
});
}
// Log the connection as a security event
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.INFO,
type: SecurityEventType.CONNECTION,
message: `New SMTP connection established`,
ipAddress: clientIp,
details: {
clientPort,
socketId: socket.remotePort.toString() + socket.remoteFamily
}
});
2024-02-16 13:28:40 +01:00
// Send greeting
this.sendResponse(socket, `220 ${this.hostname} ESMTP Service Ready`);
2024-02-16 13:28:40 +01:00
2025-05-21 10:00:06 +00:00
// Set session timeout
const sessionTimeout = setTimeout(() => {
logger.log('info', `Initial connection timeout for session ${sessionId}`);
this.sendResponse(socket, '421 Connection timeout');
socket.destroy();
this.removeSession(socket);
}, this.smtpServerOptions.connectionTimeout || 30000);
// Store timeout reference
this.sessionTimeouts.set(sessionId, sessionTimeout);
socket.on('data', (data) => {
2025-05-21 10:00:06 +00:00
// Clear initial connection timeout on first data
const timeoutId = this.sessionTimeouts.get(sessionId);
if (timeoutId) {
clearTimeout(timeoutId);
this.sessionTimeouts.delete(sessionId);
}
// Update last activity timestamp
this.updateSessionActivity(socket);
// Process the data
this.processData(socket, data);
2024-02-16 13:28:40 +01:00
});
socket.on('end', () => {
2025-05-07 20:20:17 +00:00
const clientIp = socket.remoteAddress;
const clientPort = socket.remotePort;
console.log(`Connection ended from ${clientIp}:${clientPort}`);
const session = this.sessions.get(socket);
if (session) {
session.connectionEnded = true;
2025-05-07 20:20:17 +00:00
// Log connection end as security event
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.INFO,
type: SecurityEventType.CONNECTION,
message: `SMTP connection ended normally`,
ipAddress: clientIp,
details: {
clientPort,
state: SmtpState[session.state],
2025-05-21 10:00:06 +00:00
from: session.mailFrom || 'not set',
sessionId: session.id
2025-05-07 20:20:17 +00:00
}
});
}
2025-05-21 10:00:06 +00:00
// Clean up session
this.removeSession(socket);
2024-02-16 13:28:40 +01:00
});
socket.on('error', (err) => {
2025-05-07 20:20:17 +00:00
const clientIp = socket.remoteAddress;
const clientPort = socket.remotePort;
2025-05-21 10:00:06 +00:00
const session = this.sessions.get(socket);
console.error(`Socket error for session ${session?.id}: ${err.message}`);
2025-05-07 20:20:17 +00:00
// Log connection error as security event
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.WARN,
type: SecurityEventType.CONNECTION,
message: `SMTP connection error`,
ipAddress: clientIp,
details: {
clientPort,
error: err.message,
errorCode: (err as any).code,
2025-05-21 10:00:06 +00:00
from: session?.mailFrom || 'not set',
sessionId: session?.id
2025-05-07 20:20:17 +00:00
}
});
2025-05-21 10:00:06 +00:00
// Clean up session resources
this.removeSession(socket);
socket.destroy();
2024-02-16 13:28:40 +01:00
});
socket.on('close', () => {
2025-05-07 20:20:17 +00:00
const clientIp = socket.remoteAddress;
const clientPort = socket.remotePort;
2025-05-21 10:00:06 +00:00
const session = this.sessions.get(socket);
console.log(`Connection closed for session ${session?.id} from ${clientIp}:${clientPort}`);
2025-05-07 20:20:17 +00:00
// Log connection closure as security event
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.INFO,
type: SecurityEventType.CONNECTION,
message: `SMTP connection closed`,
ipAddress: clientIp,
details: {
clientPort,
2025-05-21 10:00:06 +00:00
sessionId: session?.id,
sessionEnded: session?.connectionEnded || false
2025-05-07 20:20:17 +00:00
}
});
2025-05-21 10:00:06 +00:00
// Clean up session resources
this.removeSession(socket);
2024-02-16 13:28:40 +01:00
});
}
private sendResponse(socket: plugins.net.Socket | plugins.tls.TLSSocket, response: string): void {
try {
socket.write(`${response}\r\n`);
console.log(`${response}`);
} catch (error) {
console.error(`Error sending response: ${error.message}`);
2024-02-16 13:28:40 +01:00
socket.destroy();
}
}
private processData(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: Buffer): void {
const session = this.sessions.get(socket);
if (!session) {
console.error('No session found for socket. Closing connection.');
socket.destroy();
return;
}
// If we're in DATA_RECEIVING state, handle differently
if (session.state === SmtpState.DATA_RECEIVING) {
// Call async method but don't return the promise
this.processEmailData(socket, data.toString()).catch(err => {
console.error(`Error processing email data: ${err.message}`);
});
return;
}
// Process normal SMTP commands
const lines = data.toString().split('\r\n').filter(line => line.length > 0);
for (const line of lines) {
console.log(`${line}`);
this.processCommand(socket, line);
}
}
private processCommand(socket: plugins.net.Socket | plugins.tls.TLSSocket, commandLine: string): void {
const session = this.sessions.get(socket);
if (!session || session.connectionEnded) return;
2025-05-21 10:00:06 +00:00
// Update session activity timestamp
this.updateSessionActivity(socket);
const [command, ...args] = commandLine.split(' ');
const upperCommand = command.toUpperCase();
switch (upperCommand) {
case 'EHLO':
case 'HELO':
this.handleEhlo(socket, args.join(' '));
break;
case 'STARTTLS':
this.handleStartTls(socket);
break;
case 'MAIL':
this.handleMailFrom(socket, args.join(' '));
break;
case 'RCPT':
this.handleRcptTo(socket, args.join(' '));
break;
case 'DATA':
this.handleData(socket);
break;
case 'RSET':
this.handleRset(socket);
break;
case 'QUIT':
this.handleQuit(socket);
break;
case 'NOOP':
this.sendResponse(socket, '250 OK');
break;
default:
this.sendResponse(socket, '502 Command not implemented');
}
}
private handleEhlo(socket: plugins.net.Socket | plugins.tls.TLSSocket, clientHostname: string): void {
const session = this.sessions.get(socket);
if (!session) return;
if (!clientHostname) {
this.sendResponse(socket, '501 Syntax error in parameters or arguments');
return;
}
session.clientHostname = clientHostname;
session.state = SmtpState.AFTER_EHLO;
// List available extensions
this.sendResponse(socket, `250-${this.hostname} Hello ${clientHostname}`);
this.sendResponse(socket, '250-SIZE 10485760'); // 10MB max
this.sendResponse(socket, '250-8BITMIME');
2024-02-16 13:28:40 +01:00
// Only offer STARTTLS if we haven't already established it
if (!session.useTLS) {
this.sendResponse(socket, '250-STARTTLS');
}
this.sendResponse(socket, '250 HELP');
}
private handleStartTls(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
const session = this.sessions.get(socket);
if (!session) return;
if (session.state !== SmtpState.AFTER_EHLO) {
this.sendResponse(socket, '503 Bad sequence of commands');
return;
}
if (session.useTLS) {
this.sendResponse(socket, '503 TLS already active');
return;
2024-02-16 13:28:40 +01:00
}
this.sendResponse(socket, '220 Ready to start TLS');
this.startTLS(socket);
2024-02-16 13:28:40 +01:00
}
private handleMailFrom(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void {
const session = this.sessions.get(socket);
if (!session) return;
2024-02-16 13:28:40 +01:00
if (session.state !== SmtpState.AFTER_EHLO) {
this.sendResponse(socket, '503 Bad sequence of commands');
return;
}
2024-02-16 13:28:40 +01:00
// Extract email from MAIL FROM:<user@example.com>
const emailMatch = args.match(/FROM:<([^>]*)>/i);
if (!emailMatch) {
this.sendResponse(socket, '501 Syntax error in parameters or arguments');
return;
}
2024-02-16 13:28:40 +01:00
const email = emailMatch[1];
if (!this.isValidEmail(email)) {
this.sendResponse(socket, '501 Invalid email address');
return;
}
session.mailFrom = email;
session.state = SmtpState.MAIL_FROM;
2025-05-21 00:12:49 +00:00
// Update envelope information
session.envelope.mailFrom = {
address: email,
args: {}
};
this.sendResponse(socket, '250 OK');
}
private handleRcptTo(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void {
const session = this.sessions.get(socket);
if (!session) return;
if (session.state !== SmtpState.MAIL_FROM && session.state !== SmtpState.RCPT_TO) {
this.sendResponse(socket, '503 Bad sequence of commands');
return;
}
// Extract email from RCPT TO:<user@example.com>
const emailMatch = args.match(/TO:<([^>]*)>/i);
if (!emailMatch) {
this.sendResponse(socket, '501 Syntax error in parameters or arguments');
return;
}
const email = emailMatch[1];
if (!this.isValidEmail(email)) {
this.sendResponse(socket, '501 Invalid email address');
return;
}
session.rcptTo.push(email);
session.state = SmtpState.RCPT_TO;
2025-05-21 00:12:49 +00:00
// Update envelope information
session.envelope.rcptTo.push({
address: email,
args: {}
});
this.sendResponse(socket, '250 OK');
}
private handleData(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
const session = this.sessions.get(socket);
if (!session) return;
if (session.state !== SmtpState.RCPT_TO) {
this.sendResponse(socket, '503 Bad sequence of commands');
return;
}
session.state = SmtpState.DATA_RECEIVING;
session.emailData = '';
this.sendResponse(socket, '354 End data with <CR><LF>.<CR><LF>');
}
private handleRset(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
const session = this.sessions.get(socket);
if (!session) return;
// Reset the session data but keep connection information
session.state = SmtpState.AFTER_EHLO;
session.mailFrom = '';
session.rcptTo = [];
session.emailData = '';
this.sendResponse(socket, '250 OK');
}
private handleQuit(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
const session = this.sessions.get(socket);
if (!session) return;
this.sendResponse(socket, '221 Goodbye');
// If we have collected email data, try to parse it before closing
if (session.state === SmtpState.FINISHED && session.emailData.length > 0) {
this.parseEmail(socket);
}
socket.end();
this.sessions.delete(socket);
}
2025-05-07 14:33:20 +00:00
private async processEmailData(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: string): Promise<void> {
const session = this.sessions.get(socket);
if (!session) return;
2025-05-21 10:00:06 +00:00
// Initialize email data buffer if it doesn't exist
if (!session.emailDataChunks) {
session.emailDataChunks = [];
}
// Check for end of data marker
if (data.endsWith('\r\n.\r\n')) {
// Remove the end of data marker
const emailData = data.slice(0, -5);
2025-05-21 10:00:06 +00:00
// Add final chunk
session.emailDataChunks.push(emailData);
// Join chunks efficiently
session.emailData = session.emailDataChunks.join('');
// Free memory
session.emailDataChunks = undefined;
session.state = SmtpState.FINISHED;
// Save and process the email
this.saveEmail(socket);
this.sendResponse(socket, '250 OK: Message accepted for delivery');
} else {
2025-05-21 10:00:06 +00:00
// Accumulate the data as chunks
session.emailDataChunks.push(data);
}
}
private saveEmail(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
const session = this.sessions.get(socket);
if (!session) return;
try {
// Ensure the directory exists
plugins.smartfile.fs.ensureDirSync(paths.receivedEmailsDir);
// Write the email to disk
plugins.smartfile.memory.toFsSync(
session.emailData,
plugins.path.join(paths.receivedEmailsDir, `${Date.now()}.eml`)
);
// Parse the email
this.parseEmail(socket);
} catch (error) {
console.error('Error saving email:', error);
}
}
private async parseEmail(socket: plugins.net.Socket | plugins.tls.TLSSocket): Promise<void> {
const session = this.sessions.get(socket);
if (!session || !session.emailData) {
console.error('No email data found for session.');
2024-02-16 13:28:40 +01:00
return;
}
let mightBeSpam = false;
// Prepare headers for DKIM verification results
const customHeaders: Record<string, string> = {};
2024-02-16 13:28:40 +01:00
2025-05-07 20:20:17 +00:00
// Authentication results
let dkimResult = { domain: '', result: false };
let spfResult = { domain: '', result: false };
// Check security configuration
2025-05-21 00:12:49 +00:00
const securityConfig = { verifyDkim: true, verifySpf: true, verifyDmarc: true }; // Default security settings
2025-05-07 20:20:17 +00:00
// 1. Verify DKIM signature if enabled
2025-05-21 00:12:49 +00:00
if (securityConfig.verifyDkim) {
2025-05-07 20:20:17 +00:00
try {
2025-05-21 00:12:49 +00:00
// Mock DKIM verification for now - this is temporary during migration
const verificationResult = {
isValid: true,
domain: session.mailFrom.split('@')[1] || '',
selector: 'default',
status: 'pass',
errorMessage: ''
};
2025-05-07 20:20:17 +00:00
dkimResult.result = verificationResult.isValid;
dkimResult.domain = verificationResult.domain || '';
if (!verificationResult.isValid) {
logger.log('warn', `DKIM verification failed for incoming email: ${verificationResult.errorMessage || 'Unknown error'}`);
// Enhanced security logging
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.WARN,
type: SecurityEventType.DKIM,
message: `DKIM verification failed for incoming email`,
domain: verificationResult.domain || session.mailFrom.split('@')[1],
details: {
error: verificationResult.errorMessage || 'Unknown error',
status: verificationResult.status,
selector: verificationResult.selector,
senderIP: socket.remoteAddress
},
ipAddress: socket.remoteAddress,
success: false
});
} else {
logger.log('info', `DKIM verification passed for incoming email from domain ${verificationResult.domain}`);
// Enhanced security logging
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.INFO,
type: SecurityEventType.DKIM,
message: `DKIM verification passed for incoming email`,
domain: verificationResult.domain,
details: {
selector: verificationResult.selector,
status: verificationResult.status,
senderIP: socket.remoteAddress
},
ipAddress: socket.remoteAddress,
success: true
});
}
// Store verification results in headers
if (verificationResult.domain) {
customHeaders['X-DKIM-Domain'] = verificationResult.domain;
}
customHeaders['X-DKIM-Status'] = verificationResult.status || 'unknown';
customHeaders['X-DKIM-Result'] = verificationResult.isValid ? 'pass' : 'fail';
} catch (error) {
logger.log('error', `Failed to verify DKIM signature: ${error.message}`);
customHeaders['X-DKIM-Status'] = 'error';
customHeaders['X-DKIM-Result'] = 'error';
}
2025-05-07 20:20:17 +00:00
}
// 2. Verify SPF if enabled
2025-05-21 00:12:49 +00:00
if (securityConfig.verifySpf) {
2025-05-07 20:20:17 +00:00
try {
// Get the client IP and hostname
const clientIp = socket.remoteAddress || '127.0.0.1';
const clientHostname = session.clientHostname || 'localhost';
2025-05-07 20:20:17 +00:00
// Parse the email to get envelope from
const parsedEmail = await plugins.mailparser.simpleParser(session.emailData);
// Create a temporary Email object for SPF verification
const tempEmail = new Email({
from: parsedEmail.from?.value[0].address || session.mailFrom,
to: session.rcptTo[0],
subject: "Temporary Email for SPF Verification",
text: "This is a temporary email for SPF verification"
});
// Set envelope from for SPF verification
tempEmail.setEnvelopeFrom(session.mailFrom);
2025-05-21 00:12:49 +00:00
// Verify SPF using the email server's verifier
const spfVerified = true; // Assume SPF verification is handled by the email server
// In a real implementation, this would call:
// const spfVerified = await this.emailServerRef.spfVerifier.verify(tempEmail, clientIp, clientHostname);
2025-05-07 20:20:17 +00:00
// Update SPF result
spfResult.result = spfVerified;
spfResult.domain = session.mailFrom.split('@')[1] || '';
// Copy SPF headers from the temp email
if (tempEmail.headers['Received-SPF']) {
customHeaders['Received-SPF'] = tempEmail.headers['Received-SPF'];
}
// Set spam flag if SPF fails badly
if (tempEmail.mightBeSpam) {
mightBeSpam = true;
}
} catch (error) {
logger.log('error', `Failed to verify SPF: ${error.message}`);
customHeaders['Received-SPF'] = `error (${error.message})`;
}
2025-05-07 20:20:17 +00:00
}
// 3. Verify DMARC if enabled
2025-05-21 00:12:49 +00:00
if (securityConfig.verifyDmarc) {
2025-05-07 20:20:17 +00:00
try {
// Parse the email again
const parsedEmail = await plugins.mailparser.simpleParser(session.emailData);
2025-05-07 20:20:17 +00:00
// Create a temporary Email object for DMARC verification
const tempEmail = new Email({
from: parsedEmail.from?.value[0].address || session.mailFrom,
to: session.rcptTo[0],
subject: "Temporary Email for DMARC Verification",
text: "This is a temporary email for DMARC verification"
});
2025-05-21 00:12:49 +00:00
// Verify DMARC - handled by email server in real implementation
const dmarcResult = {};
2025-05-07 20:20:17 +00:00
2025-05-21 00:12:49 +00:00
// Apply DMARC policy - assuming we would pass if either SPF or DKIM passes
const dmarcPassed = spfResult.result || dkimResult.result;
2025-05-07 20:20:17 +00:00
// Add DMARC result to headers
if (tempEmail.headers['X-DMARC-Result']) {
customHeaders['X-DMARC-Result'] = tempEmail.headers['X-DMARC-Result'];
}
// Add Authentication-Results header combining all authentication results
2025-05-21 00:12:49 +00:00
customHeaders['Authentication-Results'] = `${this.hostname}; ` +
2025-05-07 20:20:17 +00:00
`spf=${spfResult.result ? 'pass' : 'fail'} smtp.mailfrom=${session.mailFrom}; ` +
`dkim=${dkimResult.result ? 'pass' : 'fail'} header.d=${dkimResult.domain || 'unknown'}; ` +
`dmarc=${dmarcPassed ? 'pass' : 'fail'} header.from=${tempEmail.getFromDomain()}`;
// Set spam flag if DMARC fails
if (tempEmail.mightBeSpam) {
mightBeSpam = true;
}
} catch (error) {
logger.log('error', `Failed to verify DMARC: ${error.message}`);
customHeaders['X-DMARC-Result'] = `error (${error.message})`;
}
2024-02-16 13:28:40 +01:00
}
try {
const parsedEmail = await plugins.mailparser.simpleParser(session.emailData);
const email = new Email({
from: parsedEmail.from?.value[0].address || session.mailFrom,
to: session.rcptTo[0], // Use the first recipient
headers: customHeaders, // Add our custom headers with DKIM verification results
subject: parsedEmail.subject || '',
text: parsedEmail.html || parsedEmail.text || '',
attachments: parsedEmail.attachments?.map((attachment) => ({
2024-02-16 13:28:40 +01:00
filename: attachment.filename || '',
content: attachment.content,
contentType: attachment.contentType,
})) || [],
mightBeSpam: mightBeSpam,
});
console.log('Email received and parsed:', {
from: email.from,
to: email.to,
subject: email.subject,
attachments: email.attachments.length,
mightBeSpam: email.mightBeSpam
});
2025-05-07 20:20:17 +00:00
// Enhanced security logging for received email
SecurityLogger.getInstance().logEvent({
level: mightBeSpam ? SecurityLogLevel.WARN : SecurityLogLevel.INFO,
type: mightBeSpam ? SecurityEventType.SPAM : SecurityEventType.EMAIL_VALIDATION,
message: `Email received and ${mightBeSpam ? 'flagged as potential spam' : 'validated successfully'}`,
domain: email.from.split('@')[1],
ipAddress: socket.remoteAddress,
details: {
from: email.from,
subject: email.subject,
recipientCount: email.getAllRecipients().length,
attachmentCount: email.attachments.length,
hasAttachments: email.hasAttachments(),
dkimStatus: customHeaders['X-DKIM-Result'] || 'unknown'
},
success: !mightBeSpam
});
2025-05-21 00:12:49 +00:00
// Process or forward the email via unified email server
2025-05-07 14:33:20 +00:00
try {
2025-05-21 00:12:49 +00:00
await this.emailServerRef.processEmailByMode(email, {
id: session.id,
2025-05-21 02:17:18 +00:00
state: session.state,
mailFrom: session.mailFrom,
rcptTo: session.rcptTo,
emailData: session.emailData,
useTLS: session.useTLS,
connectionEnded: session.connectionEnded,
2025-05-21 00:12:49 +00:00
remoteAddress: session.remoteAddress,
clientHostname: session.clientHostname,
secure: session.useTLS,
authenticated: session.authenticated,
envelope: session.envelope,
processingMode: session.processingMode
}, session.processingMode || 'process');
2025-05-07 14:33:20 +00:00
} catch (err) {
2025-05-21 00:12:49 +00:00
console.error('Error in email server processing of incoming email:', err);
2025-05-07 20:20:17 +00:00
// Log processing errors
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.EMAIL_VALIDATION,
message: `Error processing incoming email`,
domain: email.from.split('@')[1],
ipAddress: socket.remoteAddress,
details: {
error: err.message,
from: email.from,
stack: err.stack
},
success: false
});
2025-05-07 14:33:20 +00:00
}
} catch (error) {
console.error('Error parsing email:', error);
2025-05-07 20:20:17 +00:00
// Log parsing errors
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.EMAIL_VALIDATION,
message: `Error parsing incoming email`,
ipAddress: socket.remoteAddress,
details: {
error: error.message,
sender: session.mailFrom,
stack: error.stack
},
success: false
});
}
}
private startTLS(socket: plugins.net.Socket): void {
try {
const secureContext = plugins.tls.createSecureContext({
key: this.smtpServerOptions.key,
cert: this.smtpServerOptions.cert,
});
const tlsSocket = new plugins.tls.TLSSocket(socket, {
secureContext: secureContext,
isServer: true,
server: this.server
});
const originalSession = this.sessions.get(socket);
if (!originalSession) {
console.error('No session found when upgrading to TLS');
return;
}
2024-02-16 13:28:40 +01:00
// Transfer the session data to the new TLS socket
this.sessions.set(tlsSocket, {
...originalSession,
useTLS: true,
2025-05-21 00:12:49 +00:00
secure: true,
state: SmtpState.GREETING // Reset state to require a new EHLO
});
this.sessions.delete(socket);
tlsSocket.on('secure', () => {
console.log('TLS negotiation successful');
});
tlsSocket.on('data', (data: Buffer) => {
this.processData(tlsSocket, data);
});
tlsSocket.on('end', () => {
console.log('TLS socket ended');
const session = this.sessions.get(tlsSocket);
if (session) {
session.connectionEnded = true;
}
});
tlsSocket.on('error', (err) => {
console.error('TLS socket error:', err);
this.sessions.delete(tlsSocket);
tlsSocket.destroy();
});
2024-02-16 13:28:40 +01:00
tlsSocket.on('close', () => {
console.log('TLS socket closed');
this.sessions.delete(tlsSocket);
});
} catch (error) {
console.error('Error upgrading connection to TLS:', error);
socket.destroy();
}
2024-02-16 13:28:40 +01:00
}
private isValidEmail(email: string): boolean {
// Basic email validation - more comprehensive validation could be implemented
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
}