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>;
|
2025-03-15 13:45:29 +00:00
|
|
|
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;
|
2025-03-15 13:45:29 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2025-03-15 13:45:29 +00:00
|
|
|
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-03-15 13:45:29 +00:00
|
|
|
|
2025-05-21 10:00:06 +00:00
|
|
|
// Increment connection count
|
|
|
|
this.connectionCount++;
|
|
|
|
|
|
|
|
// Generate unique session ID
|
|
|
|
const sessionId = this.generateSessionId();
|
|
|
|
|
2025-03-15 13:45:29 +00:00
|
|
|
// Initialize a new session
|
|
|
|
this.sessions.set(socket, {
|
2025-05-21 10:00:06 +00:00
|
|
|
id: sessionId,
|
2025-03-15 13:45:29 +00:00
|
|
|
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
|
|
|
|
2025-03-15 13:45:29 +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
|
2025-03-15 13:45:29 +00:00
|
|
|
this.processData(socket, data);
|
2024-02-16 13:28:40 +01:00
|
|
|
});
|
|
|
|
|
2025-03-15 13:45:29 +00: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}`);
|
|
|
|
|
2025-03-15 13:45:29 +00:00
|
|
|
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-03-15 13:45:29 +00:00
|
|
|
}
|
2025-05-21 10:00:06 +00:00
|
|
|
|
|
|
|
// Clean up session
|
|
|
|
this.removeSession(socket);
|
2024-02-16 13:28:40 +01:00
|
|
|
});
|
|
|
|
|
2025-03-15 13:45:29 +00: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);
|
2025-03-15 13:45:29 +00:00
|
|
|
socket.destroy();
|
2024-02-16 13:28:40 +01:00
|
|
|
});
|
|
|
|
|
2025-03-15 13:45:29 +00: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
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-03-15 13:45:29 +00: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();
|
|
|
|
}
|
2025-03-15 13:45:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2025-05-07 17:41:04 +00:00
|
|
|
// 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;
|
2025-03-15 13:45:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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);
|
2025-03-15 13:45:29 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
*/
|
2025-03-15 13:45:29 +00:00
|
|
|
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-03-15 13:45:29 +00:00
|
|
|
|
2025-05-21 12:52:24 +00:00
|
|
|
// Check if hostname is provided (required by RFC 5321)
|
2025-03-15 13:45:29 +00:00
|
|
|
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-03-15 13:45:29 +00:00
|
|
|
|
2025-05-21 12:52:24 +00:00
|
|
|
// Update session with client hostname
|
2025-03-15 13:45:29 +00:00
|
|
|
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-03-15 13:45:29 +00:00
|
|
|
|
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-03-15 13:45:29 +00:00
|
|
|
}
|
|
|
|
|
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);
|
2025-03-15 13:45:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
}
|
2025-03-15 13:45:29 +00:00
|
|
|
|
|
|
|
this.sendResponse(socket, '220 Ready to start TLS');
|
|
|
|
this.startTLS(socket);
|
2024-02-16 13:28:40 +01:00
|
|
|
}
|
|
|
|
|
2025-03-15 13:45:29 +00: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
|
|
|
|
2025-03-15 13:45:29 +00:00
|
|
|
if (session.state !== SmtpState.AFTER_EHLO) {
|
|
|
|
this.sendResponse(socket, '503 Bad sequence of commands');
|
|
|
|
return;
|
|
|
|
}
|
2024-02-16 13:28:40 +01:00
|
|
|
|
2025-03-15 13:45:29 +00: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 });
|
2025-03-15 13:45:29 +00:00
|
|
|
this.sendResponse(socket, '501 Syntax error in parameters or arguments');
|
|
|
|
return;
|
|
|
|
}
|
2024-02-16 13:28:40 +01:00
|
|
|
|
2025-03-15 13:45:29 +00: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 });
|
2025-03-15 13:45:29 +00:00
|
|
|
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
|
2025-03-15 13:45:29 +00:00
|
|
|
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
|
|
|
};
|
|
|
|
|
2025-03-15 13:45:29 +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 });
|
2025-03-15 13:45:29 +00:00
|
|
|
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 });
|
2025-03-15 13:45:29 +00:00
|
|
|
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
|
|
|
|
});
|
|
|
|
|
2025-03-15 13:45:29 +00:00
|
|
|
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
|
|
|
});
|
|
|
|
|
2025-03-15 13:45:29 +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
|
2025-03-15 13:45:29 +00:00
|
|
|
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
|
|
|
|
});
|
|
|
|
|
2025-03-15 13:45:29 +00:00
|
|
|
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
|
|
|
|
});
|
|
|
|
|
2025-03-15 13:45:29 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2025-03-15 13:45:29 +00:00
|
|
|
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
|
2025-03-15 13:45:29 +00:00
|
|
|
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> {
|
2025-03-15 13:45:29 +00:00
|
|
|
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 = [];
|
|
|
|
}
|
|
|
|
|
2025-03-15 13:45:29 +00:00
|
|
|
// 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;
|
2025-03-15 13:45:29 +00:00
|
|
|
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
|
|
|
|
});
|
|
|
|
|
2025-03-15 13:45:29 +00:00
|
|
|
// 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
|
2025-03-15 13:45:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
2025-05-07 17:41:04 +00:00
|
|
|
// 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 17:41:04 +00:00
|
|
|
|
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 17:41:04 +00:00
|
|
|
}
|
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 17:41:04 +00:00
|
|
|
|
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 17:41:04 +00:00
|
|
|
}
|
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 17:41:04 +00:00
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2025-03-15 13:45:29 +00: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
|
2025-05-07 17:41:04 +00:00
|
|
|
headers: customHeaders, // Add our custom headers with DKIM verification results
|
2025-03-15 13:45:29 +00:00
|
|
|
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,
|
|
|
|
})) || [],
|
2025-03-15 13:45:29 +00:00
|
|
|
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-03-15 13:45:29 +00:00
|
|
|
|
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
|
|
|
}
|
2025-03-15 13:45:29 +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-03-15 13:45:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-21 12:52:24 +00:00
|
|
|
/**
|
|
|
|
* Upgrade a plain socket to TLS using STARTTLS
|
|
|
|
* @private
|
|
|
|
*/
|
2025-03-15 13:45:29 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2025-03-15 13:45:29 +00:00
|
|
|
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
|
2025-03-15 13:45:29 +00:00
|
|
|
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-03-15 13:45:29 +00:00
|
|
|
});
|
|
|
|
|
2025-05-21 12:52:24 +00:00
|
|
|
// Get the original session before upgrading
|
2025-03-15 13:45:29 +00:00
|
|
|
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');
|
2025-03-15 13:45:29 +00:00
|
|
|
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-03-15 13:45:29 +00:00
|
|
|
});
|
|
|
|
|
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
|
2025-03-15 13:45:29 +00:00
|
|
|
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-03-15 13:45:29 +00:00
|
|
|
});
|
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-03-15 13:45:29 +00:00
|
|
|
}
|
2025-05-21 12:52:24 +00:00
|
|
|
|
|
|
|
// Clean up
|
2025-03-15 13:45:29 +00:00
|
|
|
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');
|
2025-03-15 13:45:29 +00:00
|
|
|
socket.destroy();
|
|
|
|
}
|
2024-02-16 13:28:40 +01:00
|
|
|
}
|
|
|
|
|
2025-03-15 13:45:29 +00:00
|
|
|
private isValidEmail(email: string): boolean {
|
|
|
|
// Basic email validation - more comprehensive validation could be implemented
|
|
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
|
return emailRegex.test(email);
|
|
|
|
}
|
|
|
|
}
|