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

1777 lines
60 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 12:52:24 +00:00
// Secure server for TLS connections
protected secureServer?: plugins.tls.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
2025-05-21 12:52:24 +00:00
private cleanupInterval?: NodeJS.Timeout; // Reference to the cleanup interval for proper cleanup
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 12:52:24 +00:00
this.maxConnections = optionsArg.maxConnections || 100; // Use maxConnections instead of maxSize for clarity
2025-05-21 10:00:06 +00:00
2025-05-21 12:52:24 +00:00
// Log enhanced server configuration
const socketTimeout = optionsArg.socketTimeout || 300000;
const connectionTimeout = optionsArg.connectionTimeout || 30000;
const cleanupFrequency = optionsArg.cleanupInterval || 5000;
logger.log('info', 'SMTP server configuration', {
hostname: this.hostname,
maxConnections: this.maxConnections,
socketTimeout: socketTimeout,
connectionTimeout: connectionTimeout,
cleanupInterval: cleanupFrequency,
tlsEnabled: !!(optionsArg.key && optionsArg.cert),
starttlsEnabled: !!(optionsArg.key && optionsArg.cert),
securePort: optionsArg.securePort
});
// Start session cleanup interval - run more frequently to ensure timely timeout detection
// Default to 5 seconds for production use, but can be as low as 1 second for testing
logger.log('info', `Setting up session cleanup interval to run every ${cleanupFrequency}ms`);
const cleanupInterval = setInterval(() => this.cleanupIdleSessions(), cleanupFrequency);
// Ensure cleanup interval is cleared if server is stopped
this.cleanupInterval = cleanupInterval;
2024-02-16 13:28:40 +01:00
2025-05-21 12:52:24 +00:00
// Create a plain TCP server for non-TLS connections
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 12:52:24 +00:00
// Set up secure TLS server if TLS is configured
if (optionsArg.key && optionsArg.cert) {
logger.log('info', 'Setting up secure TLS SMTP server');
try {
// Create a secure context for TLS
const secureContext = plugins.tls.createSecureContext({
key: optionsArg.key,
cert: optionsArg.cert,
ca: optionsArg.ca
});
// Create a secure TLS server
this.secureServer = plugins.tls.createServer({
key: optionsArg.key,
cert: optionsArg.cert,
ca: optionsArg.ca,
secureContext: secureContext
}, (tlsSocket) => {
// Check if we've exceeded maximum connections
if (this.connectionCount >= this.maxConnections) {
logger.log('warn', `Connection limit reached (${this.maxConnections}), rejecting new TLS connection`);
tlsSocket.write('421 Too many connections, try again later\r\n');
tlsSocket.destroy();
return;
}
// Handle the new secure connection
this.handleNewSecureConnection(tlsSocket);
});
// Log errors from secure server
this.secureServer.on('error', (err) => {
logger.log('error', `Secure SMTP server error: ${err.message}`, { stack: err.stack });
});
} catch (error) {
logger.log('error', `Failed to initialize TLS server: ${error.message}`, { stack: error.stack });
}
}
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;
2025-05-21 12:52:24 +00:00
const securePort = this.smtpServerOptions.securePort || port; // Default to same port
2025-05-21 10:38:22 +00:00
2025-05-21 12:52:24 +00:00
// Store promises for both servers
const startPromises: Promise<void>[] = [];
// Start the plain TCP server
const plainServerPromise = new Promise<void>((plainResolve, plainReject) => {
// Set up error handler
this.server.on('error', (err) => {
logger.log('error', `SMTP server error: ${err.message}`, { stack: err.stack });
console.error(`Failed to start SMTP server: ${err.message}`);
plainReject(err);
});
// Start listening
this.server.listen(port, () => {
logger.log('info', `SMTP server listening on port ${port}`);
console.log(`SMTP server started on port ${port}`);
plainResolve();
});
2025-05-21 10:38:22 +00:00
});
2025-05-21 12:52:24 +00:00
startPromises.push(plainServerPromise);
// Start the secure TLS server if configured
if (this.secureServer && this.smtpServerOptions.key && this.smtpServerOptions.cert) {
const secureServerPromise = new Promise<void>((secureResolve, secureReject) => {
// Set up error handler
this.secureServer!.on('error', (err) => {
logger.log('error', `Secure SMTP server error: ${err.message}`, { stack: err.stack });
console.error(`Failed to start secure SMTP server: ${err.message}`);
secureReject(err);
});
// Decide whether to use a separate port for secure connections
if (securePort !== port && securePort > 0) {
// Use separate port for secure (implicit TLS) connections
this.secureServer!.listen(securePort, () => {
logger.log('info', `Secure SMTP server listening on port ${securePort}`);
console.log(`Secure SMTP server started on port ${securePort}`);
secureResolve();
});
} else {
// Use the same port for both plain and secure connections
// This means server needs to autodetect whether client is using TLS or not
this.secureServer!.listen(port, () => {
logger.log('info', `Secure SMTP server listening on same port ${port}`);
console.log(`Secure SMTP server started on port ${port}`);
secureResolve();
});
}
});
startPromises.push(secureServerPromise);
}
// Wait for all servers to start
Promise.all(startPromises)
.then(() => resolve())
.catch(err => reject(err));
2025-05-21 10:38:22 +00:00
});
}
/**
* Stop the SMTP server
* @returns A promise that resolves when the server has stopped
*/
public close(): Promise<void> {
return new Promise<void>((resolve, reject) => {
2025-05-21 12:52:24 +00:00
// Store promises for closing both servers
const closePromises: Promise<void>[] = [];
let errors: Error[] = [];
// Close the main server
const closeMainServer = new Promise<void>((closeResolve, closeReject) => {
this.server.close((err) => {
if (err) {
logger.log('error', `Error closing SMTP server: ${err.message}`);
errors.push(err);
closeReject(err);
} else {
logger.log('info', 'SMTP server stopped');
closeResolve();
}
});
2025-05-21 10:38:22 +00:00
});
2025-05-21 12:52:24 +00:00
closePromises.push(closeMainServer);
// Close the secure server if it exists
if (this.secureServer) {
const closeSecureServer = new Promise<void>((closeResolve, closeReject) => {
this.secureServer!.close((err) => {
if (err) {
logger.log('error', `Error closing secure SMTP server: ${err.message}`);
errors.push(err);
closeReject(err);
} else {
logger.log('info', 'Secure SMTP server stopped');
closeResolve();
}
});
});
closePromises.push(closeSecureServer);
}
// Clean up any active connections
for (const [socket, session] of this.sessions.entries()) {
try {
// Send a notification that server is shutting down
this.sendResponse(socket, '421 Server shutting down');
socket.destroy();
} catch (error) {
logger.log('error', `Error closing session: ${error.message}`);
}
}
// Clear cleanup interval
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = undefined;
}
// Clear all sessions and timeouts
this.sessions.clear();
for (const timeoutId of this.sessionTimeouts.values()) {
clearTimeout(timeoutId);
}
this.sessionTimeouts.clear();
// Wait for all servers to close
Promise.allSettled(closePromises)
.then(() => {
if (errors.length > 0) {
reject(new Error(`Errors while closing SMTP servers: ${errors.map(e => e.message).join(', ')}`));
} else {
resolve();
}
});
2025-05-21 10:38:22 +00:00
});
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
2025-05-21 12:52:24 +00:00
// Log that cleanup is running
logger.log('debug', `Running idle session cleanup, checking ${this.sessions.size} active sessions`);
2025-05-21 10:00:06 +00:00
// Check all sessions for timeout
for (const [socket, session] of this.sessions.entries()) {
2025-05-21 12:52:24 +00:00
if (!session.lastActivity) {
// Initialize lastActivity if not set
session.lastActivity = now;
continue;
}
2025-05-21 10:00:06 +00:00
const idleTime = now - session.lastActivity;
2025-05-21 12:52:24 +00:00
logger.log('debug', `Session ${session.id} idle time: ${idleTime}ms, timeout threshold: ${sessionTimeout}ms`);
2025-05-21 10:00:06 +00:00
if (idleTime > sessionTimeout) {
2025-05-21 12:52:24 +00:00
logger.log('info', `Session ${session.id} timed out after ${idleTime}ms of inactivity`, {
remoteAddress: socket.remoteAddress,
remotePort: socket.remotePort,
state: session.state
});
2025-05-21 10:00:06 +00:00
try {
// Send timeout message and end connection
this.sendResponse(socket, '421 Timeout - closing connection');
2025-05-21 12:52:24 +00:00
// Log security event for timeout
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.INFO,
type: SecurityEventType.CONNECTION,
message: `SMTP connection terminated due to timeout`,
ipAddress: socket.remoteAddress,
details: {
idleTime,
sessionId: session.id,
state: session.state
}
});
// Destroy socket - force timeout even if client doesn't respond
2025-05-21 10:00:06 +00:00
socket.destroy();
} catch (error) {
2025-05-21 12:52:24 +00:00
logger.log('error', `Error closing timed out session: ${error.message}`, {
sessionId: session.id,
error: error.message,
stack: error instanceof Error ? error.stack : undefined
});
2025-05-21 10:00:06 +00:00
}
// 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}`);
}
2025-05-21 12:52:24 +00:00
/**
* Handle a new secure TLS connection
* @private
*/
private handleNewSecureConnection(tlsSocket: plugins.tls.TLSSocket): void {
const clientIp = tlsSocket.remoteAddress;
const clientPort = tlsSocket.remotePort;
console.log(`New secure TLS connection from ${clientIp}:${clientPort}`);
// Log TLS details for debugging
logger.log('info', 'New secure TLS connection established', {
ip: clientIp,
port: clientPort,
protocol: tlsSocket.getProtocol(),
cipher: tlsSocket.getCipher()?.name,
authorized: tlsSocket.authorized
});
// Increment connection count
this.connectionCount++;
// Generate unique session ID
const sessionId = this.generateSessionId();
// Initialize a new session with secure flag set to true
this.sessions.set(tlsSocket, {
id: sessionId,
state: SmtpState.GREETING,
clientHostname: '',
mailFrom: '',
rcptTo: [],
emailData: '',
useTLS: true, // This is a secure connection from the start
connectionEnded: false,
remoteAddress: tlsSocket.remoteAddress || '',
secure: true, // Flag to indicate this is a secure connection
authenticated: false, // Not authenticated yet
lastActivity: Date.now(),
envelope: {
mailFrom: {
address: '',
args: {}
},
rcptTo: []
}
});
// Process IP reputation check
this.checkIpReputation(tlsSocket, clientIp, clientPort)
.then(shouldContinue => {
if (!shouldContinue) {
tlsSocket.destroy();
return;
}
// Send greeting
this.sendResponse(tlsSocket, `220 ${this.hostname} ESMTP Service Ready`);
// Set session timeout
const sessionTimeout = setTimeout(() => {
logger.log('info', `Initial connection timeout for secure session ${sessionId}`);
this.sendResponse(tlsSocket, '421 Connection timeout');
tlsSocket.destroy();
this.removeSession(tlsSocket);
}, this.smtpServerOptions.connectionTimeout || 30000);
// Store timeout reference
this.sessionTimeouts.set(sessionId, sessionTimeout);
// Set up event handlers
this.setupSocketEventHandlers(tlsSocket, sessionId);
})
.catch(error => {
logger.log('error', `Error during IP reputation check: ${error.message}`, {
stack: error.stack,
ip: clientIp
});
tlsSocket.destroy();
});
}
2025-05-21 10:00:06 +00:00
/**
* 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-21 12:52:24 +00:00
/**
* Handle a new plain connection
* @private
*/
private handleNewConnection(socket: plugins.net.Socket): void {
2025-05-07 20:20:17 +00:00
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
2025-05-21 12:52:24 +00:00
// Process IP reputation check
this.checkIpReputation(socket, clientIp, clientPort)
.then(shouldContinue => {
if (!shouldContinue) {
socket.destroy();
return;
}
2025-05-07 20:20:17 +00:00
2025-05-21 12:52:24 +00:00
// Send greeting
this.sendResponse(socket, `220 ${this.hostname} ESMTP Service Ready`);
// 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);
// Set up event handlers
this.setupSocketEventHandlers(socket, sessionId);
})
.catch(error => {
logger.log('error', `Error during IP reputation check: ${error.message}`, {
stack: error.stack,
ip: clientIp
2025-05-07 20:20:17 +00:00
});
2025-05-21 12:52:24 +00:00
socket.destroy();
});
}
/**
* Check IP reputation for a new connection
* @private
*/
private async checkIpReputation(
socket: plugins.net.Socket | plugins.tls.TLSSocket,
clientIp?: string,
clientPort?: number
): Promise<boolean> {
if (!clientIp) {
return true; // No IP to check
}
try {
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));
2025-05-07 20:20:17 +00:00
2025-05-21 12:52:24 +00:00
if (reputation.score < 5) {
// 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`);
return false;
2025-05-07 20:20:17 +00:00
}
}
2025-05-21 12:52:24 +00:00
// 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,
secure: socket instanceof plugins.tls.TLSSocket
}
});
return true;
2025-05-07 20:20:17 +00:00
} catch (error) {
logger.log('error', `Error checking IP reputation: ${error.message}`, {
ip: clientIp,
2025-05-21 12:52:24 +00:00
error: error.message,
stack: error.stack
2025-05-07 20:20:17 +00:00
});
2025-05-21 12:52:24 +00:00
return true; // Continue even if reputation check fails
2025-05-07 20:20:17 +00:00
}
2025-05-21 12:52:24 +00:00
}
/**
* Set up socket event handlers
* @private
*/
private setupSocketEventHandlers(socket: plugins.net.Socket | plugins.tls.TLSSocket, sessionId: string): void {
// Set socket timeout to detect connection issues
const socketTimeout = this.smtpServerOptions.socketTimeout || 300000; // Default 5 minutes
socket.setTimeout(socketTimeout);
2025-05-07 20:20:17 +00:00
2025-05-21 12:52:24 +00:00
// Add timeout event handler
socket.on('timeout', () => {
const session = this.sessions.get(socket);
if (!session) return;
logger.log('info', `Socket timeout event triggered for session ${session.id}`, {
remoteAddress: socket.remoteAddress,
remotePort: socket.remotePort
});
try {
// Send timeout message and end connection
this.sendResponse(socket, '421 Timeout - closing connection');
// Log security event for timeout
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.INFO,
type: SecurityEventType.CONNECTION,
message: `SMTP connection timeout detected by socket`,
ipAddress: socket.remoteAddress,
details: {
sessionId: session.id,
state: session.state,
lastActivity: new Date(session.lastActivity || Date.now()).toISOString()
}
});
// Force close the connection
socket.end();
socket.destroy();
} catch (error) {
logger.log('error', `Error handling socket timeout: ${error.message}`);
2025-05-07 20:20:17 +00:00
}
2025-05-21 12:52:24 +00:00
// Clean up session
2025-05-21 10:00:06 +00:00
this.removeSession(socket);
2025-05-21 12:52:24 +00:00
});
2025-05-21 10:00:06 +00:00
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',
2025-05-21 12:52:24 +00:00
sessionId: session.id,
secure: session.secure
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',
2025-05-21 12:52:24 +00:00
sessionId: session?.id,
secure: session?.secure
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,
2025-05-21 12:52:24 +00:00
sessionEnded: session?.connectionEnded || false,
secure: session?.secure
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');
}
}
2025-05-21 12:52:24 +00:00
/**
* Handle EHLO/HELO command
* @private
*/
private handleEhlo(socket: plugins.net.Socket | plugins.tls.TLSSocket, clientHostname: string): void {
const session = this.sessions.get(socket);
2025-05-21 12:52:24 +00:00
if (!session) {
logger.log('error', 'No session found when handling EHLO');
return;
}
2025-05-21 12:52:24 +00:00
// Check if hostname is provided (required by RFC 5321)
if (!clientHostname) {
this.sendResponse(socket, '501 Syntax error in parameters or arguments');
return;
}
2025-05-21 12:52:24 +00:00
// Check for invalid characters in hostname (not a domain per RFC 5321)
if (clientHostname.includes('@') || clientHostname.includes('<')) {
this.sendResponse(socket, '501 Invalid domain name');
return;
}
2025-05-21 12:52:24 +00:00
// Update session with client hostname
session.clientHostname = clientHostname;
session.state = SmtpState.AFTER_EHLO;
2025-05-21 12:52:24 +00:00
logger.log('debug', `EHLO received from client: ${clientHostname}`, {
sessionId: session.id,
remoteAddress: socket.remoteAddress
});
2025-05-21 12:52:24 +00:00
// Format extensions according to RFC 5321 section 4.1.1.1
let extensions: string[] = [];
// Add welcome message (first line)
extensions.push(`250-${this.hostname} Hello ${clientHostname}`);
// Add SIZE extension with max message size
const maxSize = this.smtpServerOptions.size || 10485760; // 10MB default
extensions.push(`250-SIZE ${maxSize}`);
// Add 8BITMIME (RFC 6152)
extensions.push('250-8BITMIME');
// Add STARTTLS (RFC 3207) if TLS is configured and not already in TLS mode
if (!session.useTLS && this.smtpServerOptions.key && this.smtpServerOptions.cert) {
extensions.push('250-STARTTLS');
}
// Add any additional extensions here
// Add HELP as the last extension
extensions.push('250 HELP');
// Server needs to respond with exactly ONE response that has multiple lines
// Each line except the last has a dash after the code (250-),
// and the last line has a space (250 )
2024-02-16 13:28:40 +01:00
2025-05-21 12:52:24 +00:00
// Format response as a single multiline response properly
for (let i = 0; i < extensions.length; i++) {
// All lines except last should have dash
if (i < extensions.length - 1) {
// Ensure the line starts with "250-"
const line = extensions[i];
if (!line.startsWith('250-')) {
extensions[i] = '250-' + line.substring(4);
}
} else {
// Last line should have space
const line = extensions[i];
if (!line.startsWith('250 ')) {
extensions[i] = '250 ' + line.substring(4);
}
}
}
2025-05-21 12:52:24 +00:00
// Combine all lines with CRLF and send as one response
const multilineResponse = extensions.join('\r\n');
socket.write(multilineResponse + '\r\n');
console.log(`${multilineResponse.replace(/\r\n/g, '\n→ ')}`);
// Update activity timestamp
this.updateSessionActivity(socket);
}
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) {
2025-05-21 12:52:24 +00:00
logger.log('debug', `Invalid MAIL FROM syntax: ${args}`, { sessionId: session.id });
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)) {
2025-05-21 12:52:24 +00:00
logger.log('debug', `Invalid email address in MAIL FROM: ${email}`, { sessionId: session.id });
this.sendResponse(socket, '501 Invalid email address');
return;
}
2025-05-21 12:52:24 +00:00
// Parse any ESMTP parameters (e.g., SIZE=1234)
const argsObj: Record<string, string> = {};
const paramRegex = /\s+([A-Za-z0-9][A-Za-z0-9\-]*)(?:=([^\s]+))?/g;
let paramMatch;
while ((paramMatch = paramRegex.exec(args)) !== null) {
const [, name, value = ''] = paramMatch;
argsObj[name.toUpperCase()] = value;
// Handle SIZE parameter validation
if (name.toUpperCase() === 'SIZE' && value) {
const size = parseInt(value, 10);
const maxSize = this.smtpServerOptions.size || 10485760; // 10MB default
if (isNaN(size)) {
logger.log('debug', `Invalid SIZE parameter: ${value}`, { sessionId: session.id });
this.sendResponse(socket, '501 Invalid SIZE parameter');
return;
}
if (size > maxSize) {
logger.log('debug', `Message size too large: ${size} > ${maxSize}`, { sessionId: session.id });
this.sendResponse(socket, `552 Message size exceeds fixed maximum message size (${maxSize})`);
return;
}
}
}
logger.log('info', `MAIL FROM accepted: ${email}`, {
sessionId: session.id,
params: argsObj,
remoteAddress: socket.remoteAddress
});
// Update session state
session.mailFrom = email;
session.state = SmtpState.MAIL_FROM;
2025-05-21 00:12:49 +00:00
2025-05-21 12:52:24 +00:00
// Update envelope information with all parameters
2025-05-21 00:12:49 +00:00
session.envelope.mailFrom = {
address: email,
2025-05-21 12:52:24 +00:00
args: argsObj
2025-05-21 00:12:49 +00:00
};
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) {
2025-05-21 12:52:24 +00:00
logger.log('debug', `Invalid RCPT TO syntax: ${args}`, { sessionId: session.id });
this.sendResponse(socket, '501 Syntax error in parameters or arguments');
return;
}
const email = emailMatch[1];
if (!this.isValidEmail(email)) {
2025-05-21 12:52:24 +00:00
logger.log('debug', `Invalid email address in RCPT TO: ${email}`, { sessionId: session.id });
this.sendResponse(socket, '501 Invalid email address');
return;
}
2025-05-21 12:52:24 +00:00
// Parse any ESMTP parameters
const argsObj: Record<string, string> = {};
const paramRegex = /\s+([A-Za-z0-9][A-Za-z0-9\-]*)(?:=([^\s]+))?/g;
let paramMatch;
while ((paramMatch = paramRegex.exec(args)) !== null) {
const [, name, value = ''] = paramMatch;
argsObj[name.toUpperCase()] = value;
}
// Check recipient limit if configured
const maxRecipients = this.smtpServerOptions.maxRecipients || 100;
if (session.rcptTo.length >= maxRecipients) {
logger.log('debug', `Too many recipients: ${session.rcptTo.length + 1} > ${maxRecipients}`, {
sessionId: session.id,
remoteAddress: socket.remoteAddress
});
this.sendResponse(socket, `452 Too many recipients, maximum allowed is ${maxRecipients}`);
return;
}
logger.log('info', `RCPT TO accepted: ${email}`, {
sessionId: session.id,
rcptCount: session.rcptTo.length + 1,
remoteAddress: socket.remoteAddress
});
session.rcptTo.push(email);
session.state = SmtpState.RCPT_TO;
2025-05-21 00:12:49 +00:00
2025-05-21 12:52:24 +00:00
// Update envelope information with all parameters
2025-05-21 00:12:49 +00:00
session.envelope.rcptTo.push({
address: email,
2025-05-21 12:52:24 +00:00
args: argsObj
2025-05-21 00:12:49 +00:00
});
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;
}
2025-05-21 12:52:24 +00:00
// Ensure we have at least one recipient
if (session.rcptTo.length === 0) {
logger.log('debug', 'DATA command received but no recipients specified', {
sessionId: session.id,
remoteAddress: socket.remoteAddress
});
this.sendResponse(socket, '503 Need RCPT TO before DATA');
return;
}
// Reset data buffers
session.emailData = '';
2025-05-21 12:52:24 +00:00
if (session.emailDataChunks) {
session.emailDataChunks = [];
}
// Update state and send response
session.state = SmtpState.DATA_RECEIVING;
// Set a timeout for the DATA command to prevent hanging connections
const dataTimeout = setTimeout(() => {
if (session.state === SmtpState.DATA_RECEIVING) {
logger.log('warn', 'DATA command timed out', {
sessionId: session.id,
remoteAddress: socket.remoteAddress
});
this.sendResponse(socket, '421 Data reception timeout');
socket.destroy();
}
}, this.smtpServerOptions.dataTimeout || 60000); // 1 minute default timeout for DATA
// Store the timeout ID in the session for cleanup
session.dataTimeoutId = dataTimeout;
// Log the DATA command
logger.log('info', 'DATA command accepted, expecting message content', {
sessionId: session.id,
remoteAddress: socket.remoteAddress,
recipientCount: session.rcptTo.length
});
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 = '';
2025-05-21 12:52:24 +00:00
// Clear data buffers and timeouts
if (session.emailDataChunks) {
session.emailDataChunks = [];
}
// Clear any existing data timeout
if (session.dataTimeoutId) {
clearTimeout(session.dataTimeoutId);
session.dataTimeoutId = undefined;
}
// Update envelope
session.envelope = {
mailFrom: {
address: '',
args: {}
},
rcptTo: []
};
// Log the RSET command
logger.log('debug', 'RSET command executed', {
sessionId: session.id,
remoteAddress: socket.remoteAddress
});
this.sendResponse(socket, '250 OK');
}
private handleQuit(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
const session = this.sessions.get(socket);
if (!session) return;
2025-05-21 12:52:24 +00:00
// Clear any existing data timeout
if (session.dataTimeoutId) {
clearTimeout(session.dataTimeoutId);
session.dataTimeoutId = undefined;
}
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);
}
2025-05-21 12:52:24 +00:00
// Log the QUIT command
logger.log('debug', 'QUIT command executed', {
sessionId: session.id,
remoteAddress: socket.remoteAddress,
state: session.state
});
// Close the connection
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 12:52:24 +00:00
if (session.state !== SmtpState.DATA_RECEIVING) {
logger.log('warn', 'Received data but not in DATA_RECEIVING state', {
sessionId: session.id,
state: session.state,
remoteAddress: socket.remoteAddress
});
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('');
2025-05-21 12:52:24 +00:00
// Check size limits
const dataSize = Buffer.byteLength(session.emailData);
const maxSize = this.smtpServerOptions.size || 10485760; // 10MB default
if (dataSize > maxSize) {
logger.log('warn', `Message size exceeds limit: ${dataSize} > ${maxSize}`, {
sessionId: session.id,
size: dataSize,
limit: maxSize,
remoteAddress: socket.remoteAddress
});
this.sendResponse(socket, `552 Message size exceeds fixed maximum message size (${maxSize})`);
// Reset data state
session.emailData = '';
session.emailDataChunks = [];
session.state = SmtpState.AFTER_EHLO; // Reset to after EHLO state to allow new transaction
// Clear data timeout
if (session.dataTimeoutId) {
clearTimeout(session.dataTimeoutId);
session.dataTimeoutId = undefined;
}
return;
}
// Clear data timeout
if (session.dataTimeoutId) {
clearTimeout(session.dataTimeoutId);
session.dataTimeoutId = undefined;
}
// Free memory for chunks
2025-05-21 10:00:06 +00:00
session.emailDataChunks = undefined;
session.state = SmtpState.FINISHED;
2025-05-21 12:52:24 +00:00
// Log successful data reception
logger.log('info', 'Email data received successfully', {
sessionId: session.id,
size: dataSize,
sender: session.mailFrom,
recipients: session.rcptTo.length,
remoteAddress: socket.remoteAddress
});
// 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);
2025-05-21 12:52:24 +00:00
// Check for excessive data size during accumulation
// This is a rough check on accumulated chunk lengths to detect huge emails early
const currentSize = session.emailDataChunks.reduce((sum, chunk) => sum + Buffer.byteLength(chunk), 0);
const maxSize = this.smtpServerOptions.size || 10485760; // 10MB default
if (currentSize > maxSize) {
logger.log('warn', `Accumulated message size exceeds limit: ${currentSize} > ${maxSize}`, {
sessionId: session.id,
size: currentSize,
limit: maxSize,
remoteAddress: socket.remoteAddress
});
this.sendResponse(socket, `552 Message size exceeds fixed maximum message size (${maxSize})`);
// Reset data state
session.emailData = '';
session.emailDataChunks = [];
session.state = SmtpState.AFTER_EHLO; // Reset to after EHLO state
// Clear data timeout
if (session.dataTimeoutId) {
clearTimeout(session.dataTimeoutId);
session.dataTimeoutId = undefined;
}
return;
}
// Reset data timeout - this allows more time for large emails that come in multiple chunks
if (session.dataTimeoutId) {
clearTimeout(session.dataTimeoutId);
}
session.dataTimeoutId = setTimeout(() => {
if (session.state === SmtpState.DATA_RECEIVING) {
logger.log('warn', 'DATA command timed out during data reception', {
sessionId: session.id,
remoteAddress: socket.remoteAddress
});
this.sendResponse(socket, '421 Data reception timeout');
socket.destroy();
}
}, this.smtpServerOptions.dataTimeout || 60000); // 1 minute default timeout for 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
});
}
}
2025-05-21 12:52:24 +00:00
/**
* Upgrade a plain socket to TLS using STARTTLS
* @private
*/
private startTLS(socket: plugins.net.Socket): void {
2025-05-21 12:52:24 +00:00
if (!this.smtpServerOptions.key || !this.smtpServerOptions.cert) {
logger.log('error', 'Cannot upgrade to TLS: No key or certificate provided');
this.sendResponse(socket, '454 TLS not available due to temporary reason');
return;
}
try {
2025-05-21 12:52:24 +00:00
logger.log('info', 'Starting TLS negotiation', {
remoteAddress: socket.remoteAddress,
remotePort: socket.remotePort
});
// Create a secure context for TLS
const secureContext = plugins.tls.createSecureContext({
key: this.smtpServerOptions.key,
cert: this.smtpServerOptions.cert,
2025-05-21 12:52:24 +00:00
ca: this.smtpServerOptions.ca,
requestCert: false,
rejectUnauthorized: false // Don't require client cert
});
2025-05-21 12:52:24 +00:00
// Get the original session before upgrading
const originalSession = this.sessions.get(socket);
if (!originalSession) {
2025-05-21 12:52:24 +00:00
logger.log('error', 'No session found when upgrading to TLS');
this.sendResponse(socket, '454 TLS not available: Internal error');
return;
}
2025-05-21 12:52:24 +00:00
// Log the TLS upgrade attempt
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.INFO,
type: SecurityEventType.TLS_NEGOTIATION,
message: `STARTTLS negotiation initiated`,
ipAddress: socket.remoteAddress,
details: {
sessionId: originalSession.id,
clientHostname: originalSession.clientHostname
}
});
2025-05-21 12:52:24 +00:00
// Remove existing listeners before upgrade to avoid data corruption
socket.removeAllListeners('data');
socket.removeAllListeners('end');
socket.removeAllListeners('close');
socket.removeAllListeners('error');
// Store the session ID before deleting from map
const sessionId = originalSession.id;
// Remove the old session
this.sessions.delete(socket);
2025-05-21 12:52:24 +00:00
// Prepare options for TLS Socket
const options: plugins.tls.TLSSocketOptions = {
secureContext: secureContext,
isServer: true,
server: this.server,
requestCert: false,
rejectUnauthorized: false
};
// Create a new TLS socket from the plain socket
const tlsSocket = new plugins.tls.TLSSocket(socket, options);
// Wait for secure event before sending/receiving data
tlsSocket.once('secure', () => {
// Create a new session for the TLS socket
this.sessions.set(tlsSocket, {
...originalSession,
id: sessionId, // Keep same ID to maintain timeouts
useTLS: true,
secure: true,
state: SmtpState.GREETING, // Reset state to require a new EHLO
lastActivity: Date.now() // Reset activity timer
});
// Set up all event handlers for the TLS socket
this.setupSocketEventHandlers(tlsSocket, sessionId);
console.log(`TLS negotiation successful: ${tlsSocket.getProtocol()} with cipher ${tlsSocket.getCipher()?.name}`);
// Log successful TLS upgrade as security event
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.INFO,
type: SecurityEventType.TLS_NEGOTIATION,
message: `STARTTLS negotiation successful`,
ipAddress: tlsSocket.remoteAddress,
details: {
sessionId: sessionId,
protocol: tlsSocket.getProtocol(),
cipher: tlsSocket.getCipher()?.name,
authorized: tlsSocket.authorized
}
});
// Enhanced logging
logger.log('info', 'TLS connection established', {
protocol: tlsSocket.getProtocol(),
cipher: tlsSocket.getCipher()?.name,
remoteAddress: tlsSocket.remoteAddress,
clientHostname: originalSession.clientHostname
});
});
2025-05-21 12:52:24 +00:00
// Handle error during TLS negotiation
tlsSocket.once('error', (error) => {
console.error('Error during TLS negotiation:', error);
logger.log('error', `Error during TLS negotiation: ${error.message}`, {
remoteAddress: socket.remoteAddress,
stack: error.stack
});
// Log TLS failure as security event
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.TLS_NEGOTIATION,
message: `STARTTLS negotiation failed`,
ipAddress: socket.remoteAddress,
details: {
error: error.message,
stack: error.stack
}
});
try {
this.sendResponse(socket, '454 TLS negotiation failed');
} catch (err) {
// Socket may be closed already
}
2025-05-21 12:52:24 +00:00
// Clean up
tlsSocket.destroy();
});
} catch (error) {
console.error('Error upgrading connection to TLS:', error);
2025-05-21 12:52:24 +00:00
logger.log('error', `Error upgrading connection to TLS: ${error.message}`, {
remoteAddress: socket.remoteAddress,
stack: error.stack
});
// Log TLS failure as security event
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.TLS_NEGOTIATION,
message: `STARTTLS negotiation failed`,
ipAddress: socket.remoteAddress,
details: {
error: error.message,
stack: error.stack
}
});
// Send error response to client
this.sendResponse(socket, '454 TLS negotiation failed');
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);
}
}