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-03-15 16:21:37 +00:00
|
|
|
import type { MtaService } from './classes.mta.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
|
|
|
|
|
|
|
export interface ISmtpServerOptions {
|
|
|
|
port: number;
|
|
|
|
key: string;
|
|
|
|
cert: string;
|
2025-03-15 13:45:29 +00:00
|
|
|
hostname?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
// SMTP Session States
|
|
|
|
enum SmtpState {
|
|
|
|
GREETING,
|
|
|
|
AFTER_EHLO,
|
|
|
|
MAIL_FROM,
|
|
|
|
RCPT_TO,
|
|
|
|
DATA,
|
|
|
|
DATA_RECEIVING,
|
|
|
|
FINISHED
|
|
|
|
}
|
|
|
|
|
|
|
|
// Structure to store session information
|
|
|
|
interface SmtpSession {
|
|
|
|
state: SmtpState;
|
|
|
|
clientHostname: string;
|
|
|
|
mailFrom: string;
|
|
|
|
rcptTo: string[];
|
|
|
|
emailData: string;
|
|
|
|
useTLS: boolean;
|
|
|
|
connectionEnded: boolean;
|
2024-02-16 13:28:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export class SMTPServer {
|
2024-02-16 20:42:26 +01:00
|
|
|
public mtaRef: MtaService;
|
2024-02-16 13:28:40 +01:00
|
|
|
private smtpServerOptions: ISmtpServerOptions;
|
|
|
|
private server: plugins.net.Server;
|
2025-03-15 13:45:29 +00:00
|
|
|
private sessions: Map<plugins.net.Socket | plugins.tls.TLSSocket, SmtpSession>;
|
|
|
|
private hostname: string;
|
2024-02-16 13:28:40 +01:00
|
|
|
|
2024-02-16 20:42:26 +01:00
|
|
|
constructor(mtaRefArg: MtaService, optionsArg: ISmtpServerOptions) {
|
2024-02-16 13:28:40 +01:00
|
|
|
console.log('SMTPServer instance is being created...');
|
|
|
|
|
|
|
|
this.mtaRef = mtaRefArg;
|
|
|
|
this.smtpServerOptions = optionsArg;
|
2025-03-15 13:45:29 +00:00
|
|
|
this.sessions = new Map();
|
|
|
|
this.hostname = optionsArg.hostname || 'mta.lossless.one';
|
2024-02-16 13:28:40 +01:00
|
|
|
|
|
|
|
this.server = plugins.net.createServer((socket) => {
|
2025-03-15 13:45:29 +00:00
|
|
|
this.handleNewConnection(socket);
|
2024-02-16 13:28:40 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-05-07 20:20:17 +00:00
|
|
|
private async handleNewConnection(socket: plugins.net.Socket): Promise<void> {
|
|
|
|
const clientIp = socket.remoteAddress;
|
|
|
|
const clientPort = socket.remotePort;
|
|
|
|
console.log(`New connection from ${clientIp}:${clientPort}`);
|
2025-03-15 13:45:29 +00:00
|
|
|
|
|
|
|
// Initialize a new session
|
|
|
|
this.sessions.set(socket, {
|
|
|
|
state: SmtpState.GREETING,
|
|
|
|
clientHostname: '',
|
|
|
|
mailFrom: '',
|
|
|
|
rcptTo: [],
|
|
|
|
emailData: '',
|
|
|
|
useTLS: false,
|
|
|
|
connectionEnded: false
|
2024-02-16 13:28:40 +01:00
|
|
|
});
|
2025-05-07 20:20:17 +00:00
|
|
|
|
|
|
|
// Check IP reputation
|
|
|
|
try {
|
|
|
|
if (this.mtaRef.config.security?.checkIPReputation !== false && clientIp) {
|
|
|
|
const reputationChecker = IPReputationChecker.getInstance();
|
|
|
|
const reputation = await reputationChecker.checkReputation(clientIp);
|
|
|
|
|
|
|
|
// Log the reputation check
|
|
|
|
SecurityLogger.getInstance().logEvent({
|
|
|
|
level: reputation.score < ReputationThreshold.HIGH_RISK
|
|
|
|
? SecurityLogLevel.WARN
|
|
|
|
: SecurityLogLevel.INFO,
|
|
|
|
type: SecurityEventType.IP_REPUTATION,
|
|
|
|
message: `IP reputation checked for new SMTP connection: score=${reputation.score}`,
|
|
|
|
ipAddress: clientIp,
|
|
|
|
details: {
|
|
|
|
clientPort,
|
|
|
|
score: reputation.score,
|
|
|
|
isSpam: reputation.isSpam,
|
|
|
|
isProxy: reputation.isProxy,
|
|
|
|
isTor: reputation.isTor,
|
|
|
|
isVPN: reputation.isVPN,
|
|
|
|
country: reputation.country,
|
|
|
|
blacklists: reputation.blacklists,
|
|
|
|
socketId: socket.remotePort.toString() + socket.remoteFamily
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Handle high-risk IPs - add delay or reject based on score
|
|
|
|
if (reputation.score < ReputationThreshold.HIGH_RISK) {
|
|
|
|
// For high-risk connections, add an artificial delay to slow down potential spam
|
|
|
|
const delayMs = Math.min(5000, Math.max(1000, (ReputationThreshold.HIGH_RISK - reputation.score) * 100));
|
|
|
|
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
|
|
|
|
|
|
if (reputation.score < 5) {
|
|
|
|
// Very high risk - can optionally reject the connection
|
|
|
|
if (this.mtaRef.config.security?.rejectHighRiskIPs) {
|
|
|
|
this.sendResponse(socket, `554 Transaction failed - IP is on spam blocklist`);
|
|
|
|
socket.destroy();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
logger.log('error', `Error checking IP reputation: ${error.message}`, {
|
|
|
|
ip: clientIp,
|
|
|
|
error: error.message
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Log the connection as a security event
|
|
|
|
SecurityLogger.getInstance().logEvent({
|
|
|
|
level: SecurityLogLevel.INFO,
|
|
|
|
type: SecurityEventType.CONNECTION,
|
|
|
|
message: `New SMTP connection established`,
|
|
|
|
ipAddress: clientIp,
|
|
|
|
details: {
|
|
|
|
clientPort,
|
|
|
|
socketId: socket.remotePort.toString() + socket.remoteFamily
|
|
|
|
}
|
|
|
|
});
|
2024-02-16 13:28:40 +01:00
|
|
|
|
2025-03-15 13:45:29 +00:00
|
|
|
// Send greeting
|
|
|
|
this.sendResponse(socket, `220 ${this.hostname} ESMTP Service Ready`);
|
2024-02-16 13:28:40 +01:00
|
|
|
|
2025-03-15 13:45:29 +00:00
|
|
|
socket.on('data', (data) => {
|
|
|
|
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],
|
|
|
|
from: session.mailFrom || 'not set'
|
|
|
|
}
|
|
|
|
});
|
2025-03-15 13:45:29 +00:00
|
|
|
}
|
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-03-15 13:45:29 +00:00
|
|
|
console.error(`Socket error: ${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,
|
|
|
|
from: this.sessions.get(socket)?.mailFrom || 'not set'
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2025-03-15 13:45:29 +00:00
|
|
|
this.sessions.delete(socket);
|
|
|
|
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;
|
|
|
|
console.log(`Connection closed from ${clientIp}:${clientPort}`);
|
|
|
|
|
|
|
|
// Log connection closure as security event
|
|
|
|
SecurityLogger.getInstance().logEvent({
|
|
|
|
level: SecurityLogLevel.INFO,
|
|
|
|
type: SecurityEventType.CONNECTION,
|
|
|
|
message: `SMTP connection closed`,
|
|
|
|
ipAddress: clientIp,
|
|
|
|
details: {
|
|
|
|
clientPort,
|
|
|
|
sessionEnded: this.sessions.get(socket)?.connectionEnded || false
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2025-03-15 13:45:29 +00:00
|
|
|
this.sessions.delete(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;
|
|
|
|
|
|
|
|
const [command, ...args] = commandLine.split(' ');
|
|
|
|
const upperCommand = command.toUpperCase();
|
|
|
|
|
|
|
|
switch (upperCommand) {
|
|
|
|
case 'EHLO':
|
|
|
|
case 'HELO':
|
|
|
|
this.handleEhlo(socket, args.join(' '));
|
|
|
|
break;
|
|
|
|
case 'STARTTLS':
|
|
|
|
this.handleStartTls(socket);
|
|
|
|
break;
|
|
|
|
case 'MAIL':
|
|
|
|
this.handleMailFrom(socket, args.join(' '));
|
|
|
|
break;
|
|
|
|
case 'RCPT':
|
|
|
|
this.handleRcptTo(socket, args.join(' '));
|
|
|
|
break;
|
|
|
|
case 'DATA':
|
|
|
|
this.handleData(socket);
|
|
|
|
break;
|
|
|
|
case 'RSET':
|
|
|
|
this.handleRset(socket);
|
|
|
|
break;
|
|
|
|
case 'QUIT':
|
|
|
|
this.handleQuit(socket);
|
|
|
|
break;
|
|
|
|
case 'NOOP':
|
|
|
|
this.sendResponse(socket, '250 OK');
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
this.sendResponse(socket, '502 Command not implemented');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private handleEhlo(socket: plugins.net.Socket | plugins.tls.TLSSocket, clientHostname: string): void {
|
|
|
|
const session = this.sessions.get(socket);
|
|
|
|
if (!session) return;
|
|
|
|
|
|
|
|
if (!clientHostname) {
|
|
|
|
this.sendResponse(socket, '501 Syntax error in parameters or arguments');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
session.clientHostname = clientHostname;
|
|
|
|
session.state = SmtpState.AFTER_EHLO;
|
|
|
|
|
|
|
|
// List available extensions
|
|
|
|
this.sendResponse(socket, `250-${this.hostname} Hello ${clientHostname}`);
|
|
|
|
this.sendResponse(socket, '250-SIZE 10485760'); // 10MB max
|
|
|
|
this.sendResponse(socket, '250-8BITMIME');
|
2024-02-16 13:28:40 +01:00
|
|
|
|
2025-03-15 13:45:29 +00:00
|
|
|
// Only offer STARTTLS if we haven't already established it
|
|
|
|
if (!session.useTLS) {
|
|
|
|
this.sendResponse(socket, '250-STARTTLS');
|
|
|
|
}
|
|
|
|
|
|
|
|
this.sendResponse(socket, '250 HELP');
|
|
|
|
}
|
|
|
|
|
|
|
|
private handleStartTls(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
|
|
|
|
const session = this.sessions.get(socket);
|
|
|
|
if (!session) return;
|
|
|
|
|
|
|
|
if (session.state !== SmtpState.AFTER_EHLO) {
|
|
|
|
this.sendResponse(socket, '503 Bad sequence of commands');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (session.useTLS) {
|
|
|
|
this.sendResponse(socket, '503 TLS already active');
|
|
|
|
return;
|
2024-02-16 13:28:40 +01:00
|
|
|
}
|
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) {
|
|
|
|
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)) {
|
|
|
|
this.sendResponse(socket, '501 Invalid email address');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
session.mailFrom = email;
|
|
|
|
session.state = SmtpState.MAIL_FROM;
|
|
|
|
this.sendResponse(socket, '250 OK');
|
|
|
|
}
|
|
|
|
|
|
|
|
private handleRcptTo(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void {
|
|
|
|
const session = this.sessions.get(socket);
|
|
|
|
if (!session) return;
|
|
|
|
|
|
|
|
if (session.state !== SmtpState.MAIL_FROM && session.state !== SmtpState.RCPT_TO) {
|
|
|
|
this.sendResponse(socket, '503 Bad sequence of commands');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Extract email from RCPT TO:<user@example.com>
|
|
|
|
const emailMatch = args.match(/TO:<([^>]*)>/i);
|
|
|
|
if (!emailMatch) {
|
|
|
|
this.sendResponse(socket, '501 Syntax error in parameters or arguments');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const email = emailMatch[1];
|
|
|
|
if (!this.isValidEmail(email)) {
|
|
|
|
this.sendResponse(socket, '501 Invalid email address');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
session.rcptTo.push(email);
|
|
|
|
session.state = SmtpState.RCPT_TO;
|
|
|
|
this.sendResponse(socket, '250 OK');
|
|
|
|
}
|
|
|
|
|
|
|
|
private handleData(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
|
|
|
|
const session = this.sessions.get(socket);
|
|
|
|
if (!session) return;
|
|
|
|
|
|
|
|
if (session.state !== SmtpState.RCPT_TO) {
|
|
|
|
this.sendResponse(socket, '503 Bad sequence of commands');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
session.state = SmtpState.DATA_RECEIVING;
|
|
|
|
session.emailData = '';
|
|
|
|
this.sendResponse(socket, '354 End data with <CR><LF>.<CR><LF>');
|
|
|
|
}
|
|
|
|
|
|
|
|
private handleRset(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
|
|
|
|
const session = this.sessions.get(socket);
|
|
|
|
if (!session) return;
|
|
|
|
|
|
|
|
// Reset the session data but keep connection information
|
|
|
|
session.state = SmtpState.AFTER_EHLO;
|
|
|
|
session.mailFrom = '';
|
|
|
|
session.rcptTo = [];
|
|
|
|
session.emailData = '';
|
|
|
|
|
|
|
|
this.sendResponse(socket, '250 OK');
|
|
|
|
}
|
|
|
|
|
|
|
|
private handleQuit(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
|
|
|
|
const session = this.sessions.get(socket);
|
|
|
|
if (!session) return;
|
|
|
|
|
|
|
|
this.sendResponse(socket, '221 Goodbye');
|
|
|
|
|
|
|
|
// If we have collected email data, try to parse it before closing
|
|
|
|
if (session.state === SmtpState.FINISHED && session.emailData.length > 0) {
|
|
|
|
this.parseEmail(socket);
|
|
|
|
}
|
|
|
|
|
|
|
|
socket.end();
|
|
|
|
this.sessions.delete(socket);
|
|
|
|
}
|
|
|
|
|
2025-05-07 14:33:20 +00:00
|
|
|
private async processEmailData(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: string): Promise<void> {
|
2025-03-15 13:45:29 +00:00
|
|
|
const session = this.sessions.get(socket);
|
|
|
|
if (!session) return;
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
session.emailData += emailData;
|
|
|
|
session.state = SmtpState.FINISHED;
|
|
|
|
|
|
|
|
// Save and process the email
|
|
|
|
this.saveEmail(socket);
|
|
|
|
this.sendResponse(socket, '250 OK: Message accepted for delivery');
|
|
|
|
} else {
|
|
|
|
// Accumulate the data
|
|
|
|
session.emailData += 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;
|
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
|
|
|
|
const securityConfig = this.mtaRef.config.security || {};
|
|
|
|
|
|
|
|
// 1. Verify DKIM signature if enabled
|
|
|
|
if (securityConfig.verifyDkim !== false) {
|
|
|
|
try {
|
|
|
|
const verificationResult = await this.mtaRef.dkimVerifier.verify(session.emailData, {
|
|
|
|
useCache: true,
|
|
|
|
returnDetails: false
|
|
|
|
});
|
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
|
|
|
|
if (securityConfig.verifySpf !== false) {
|
|
|
|
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);
|
|
|
|
|
|
|
|
// Verify SPF
|
|
|
|
const spfVerified = await this.mtaRef.spfVerifier.verifyAndApply(
|
|
|
|
tempEmail,
|
|
|
|
clientIp,
|
|
|
|
clientHostname
|
|
|
|
);
|
|
|
|
|
|
|
|
// 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
|
|
|
|
if (securityConfig.verifyDmarc !== false) {
|
|
|
|
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"
|
|
|
|
});
|
|
|
|
|
|
|
|
// Verify DMARC
|
|
|
|
const dmarcResult = await this.mtaRef.dmarcVerifier.verify(
|
|
|
|
tempEmail,
|
|
|
|
spfResult,
|
|
|
|
dkimResult
|
|
|
|
);
|
|
|
|
|
|
|
|
// Apply DMARC policy
|
|
|
|
const dmarcPassed = this.mtaRef.dmarcVerifier.applyPolicy(tempEmail, dmarcResult);
|
|
|
|
|
|
|
|
// 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
|
|
|
|
customHeaders['Authentication-Results'] = `${this.mtaRef.config.smtp.hostname}; ` +
|
|
|
|
`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-07 14:33:20 +00:00
|
|
|
// Process or forward the email via MTA service
|
|
|
|
try {
|
|
|
|
await this.mtaRef.processIncomingEmail(email);
|
|
|
|
} catch (err) {
|
|
|
|
console.error('Error in MTA 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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private startTLS(socket: plugins.net.Socket): void {
|
|
|
|
try {
|
|
|
|
const secureContext = plugins.tls.createSecureContext({
|
|
|
|
key: this.smtpServerOptions.key,
|
|
|
|
cert: this.smtpServerOptions.cert,
|
|
|
|
});
|
|
|
|
|
|
|
|
const tlsSocket = new plugins.tls.TLSSocket(socket, {
|
|
|
|
secureContext: secureContext,
|
|
|
|
isServer: true,
|
|
|
|
server: this.server
|
|
|
|
});
|
|
|
|
|
|
|
|
const originalSession = this.sessions.get(socket);
|
|
|
|
if (!originalSession) {
|
|
|
|
console.error('No session found when upgrading to TLS');
|
|
|
|
return;
|
|
|
|
}
|
2024-02-16 13:28:40 +01:00
|
|
|
|
2025-03-15 13:45:29 +00:00
|
|
|
// Transfer the session data to the new TLS socket
|
|
|
|
this.sessions.set(tlsSocket, {
|
|
|
|
...originalSession,
|
|
|
|
useTLS: true,
|
|
|
|
state: SmtpState.GREETING // Reset state to require a new EHLO
|
|
|
|
});
|
|
|
|
|
|
|
|
this.sessions.delete(socket);
|
|
|
|
|
|
|
|
tlsSocket.on('secure', () => {
|
|
|
|
console.log('TLS negotiation successful');
|
|
|
|
});
|
|
|
|
|
|
|
|
tlsSocket.on('data', (data: Buffer) => {
|
|
|
|
this.processData(tlsSocket, data);
|
|
|
|
});
|
|
|
|
|
|
|
|
tlsSocket.on('end', () => {
|
|
|
|
console.log('TLS socket ended');
|
|
|
|
const session = this.sessions.get(tlsSocket);
|
|
|
|
if (session) {
|
|
|
|
session.connectionEnded = true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
tlsSocket.on('error', (err) => {
|
|
|
|
console.error('TLS socket error:', err);
|
|
|
|
this.sessions.delete(tlsSocket);
|
|
|
|
tlsSocket.destroy();
|
|
|
|
});
|
2024-02-16 13:28:40 +01:00
|
|
|
|
2025-03-15 13:45:29 +00:00
|
|
|
tlsSocket.on('close', () => {
|
|
|
|
console.log('TLS socket closed');
|
|
|
|
this.sessions.delete(tlsSocket);
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Error upgrading connection to TLS:', error);
|
|
|
|
socket.destroy();
|
|
|
|
}
|
2024-02-16 13:28:40 +01:00
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
public start(): void {
|
2024-02-16 13:28:40 +01:00
|
|
|
this.server.listen(this.smtpServerOptions.port, () => {
|
|
|
|
console.log(`SMTP Server is now running on port ${this.smtpServerOptions.port}`);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-03-15 13:45:29 +00:00
|
|
|
public stop(): void {
|
2024-02-16 13:28:40 +01:00
|
|
|
this.server.getConnections((err, count) => {
|
|
|
|
if (err) throw err;
|
|
|
|
console.log('Number of active connections: ', count);
|
|
|
|
});
|
2025-03-15 13:45:29 +00:00
|
|
|
|
2024-02-16 13:28:40 +01:00
|
|
|
this.server.close(() => {
|
|
|
|
console.log('SMTP Server is now stopped');
|
|
|
|
});
|
|
|
|
}
|
2025-03-15 13:45:29 +00:00
|
|
|
}
|