837 lines
27 KiB
TypeScript
837 lines
27 KiB
TypeScript
import * as plugins from '../../plugins.js';
|
|
import * as paths from '../../paths.js';
|
|
import { Email } from '../core/classes.email.js';
|
|
import type { UnifiedEmailServer } from '../routing/classes.unified.email.server.js';
|
|
import { logger } from '../../logger.js';
|
|
import {
|
|
SecurityLogger,
|
|
SecurityLogLevel,
|
|
SecurityEventType,
|
|
IPReputationChecker,
|
|
ReputationThreshold
|
|
} from '../../security/index.js';
|
|
|
|
export interface ISmtpServerOptions {
|
|
port: number;
|
|
key: string;
|
|
cert: string;
|
|
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 {
|
|
id: string;
|
|
state: SmtpState;
|
|
clientHostname: string;
|
|
mailFrom: string;
|
|
rcptTo: string[];
|
|
emailData: string;
|
|
useTLS: boolean;
|
|
connectionEnded: boolean;
|
|
remoteAddress: string;
|
|
secure: boolean;
|
|
authenticated: boolean;
|
|
envelope: {
|
|
mailFrom: {
|
|
address: string;
|
|
args: any;
|
|
};
|
|
rcptTo: Array<{
|
|
address: string;
|
|
args: any;
|
|
}>;
|
|
};
|
|
processingMode?: 'forward' | 'mta' | 'process';
|
|
}
|
|
|
|
export class SMTPServer {
|
|
public emailServerRef: UnifiedEmailServer;
|
|
private smtpServerOptions: ISmtpServerOptions;
|
|
private server: plugins.net.Server;
|
|
private sessions: Map<plugins.net.Socket | plugins.tls.TLSSocket, SmtpSession>;
|
|
private hostname: string;
|
|
|
|
constructor(emailServerRefArg: UnifiedEmailServer, optionsArg: ISmtpServerOptions) {
|
|
console.log('SMTPServer instance is being created...');
|
|
|
|
this.emailServerRef = emailServerRefArg;
|
|
this.smtpServerOptions = optionsArg;
|
|
this.sessions = new Map();
|
|
this.hostname = optionsArg.hostname || 'mail.lossless.one';
|
|
|
|
this.server = plugins.net.createServer((socket) => {
|
|
this.handleNewConnection(socket);
|
|
});
|
|
}
|
|
|
|
private async handleNewConnection(socket: plugins.net.Socket): Promise<void> {
|
|
const clientIp = socket.remoteAddress;
|
|
const clientPort = socket.remotePort;
|
|
console.log(`New connection from ${clientIp}:${clientPort}`);
|
|
|
|
// Initialize a new session
|
|
this.sessions.set(socket, {
|
|
id: `${socket.remoteAddress}:${socket.remotePort}`,
|
|
state: SmtpState.GREETING,
|
|
clientHostname: '',
|
|
mailFrom: '',
|
|
rcptTo: [],
|
|
emailData: '',
|
|
useTLS: false,
|
|
connectionEnded: false,
|
|
remoteAddress: socket.remoteAddress || '',
|
|
secure: false,
|
|
authenticated: false,
|
|
envelope: {
|
|
mailFrom: {
|
|
address: '',
|
|
args: {}
|
|
},
|
|
rcptTo: []
|
|
}
|
|
});
|
|
|
|
// Check IP reputation
|
|
try {
|
|
if (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 - reject the connection for security
|
|
// The email server has security settings for high-risk IPs
|
|
this.sendResponse(socket, `554 Transaction failed - IP is on spam blocklist`);
|
|
socket.destroy();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
} 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
|
|
}
|
|
});
|
|
|
|
// Send greeting
|
|
this.sendResponse(socket, `220 ${this.hostname} ESMTP Service Ready`);
|
|
|
|
socket.on('data', (data) => {
|
|
this.processData(socket, data);
|
|
});
|
|
|
|
socket.on('end', () => {
|
|
const clientIp = socket.remoteAddress;
|
|
const clientPort = socket.remotePort;
|
|
console.log(`Connection ended from ${clientIp}:${clientPort}`);
|
|
|
|
const session = this.sessions.get(socket);
|
|
if (session) {
|
|
session.connectionEnded = true;
|
|
|
|
// 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'
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
socket.on('error', (err) => {
|
|
const clientIp = socket.remoteAddress;
|
|
const clientPort = socket.remotePort;
|
|
console.error(`Socket error: ${err.message}`);
|
|
|
|
// 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'
|
|
}
|
|
});
|
|
|
|
this.sessions.delete(socket);
|
|
socket.destroy();
|
|
});
|
|
|
|
socket.on('close', () => {
|
|
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
|
|
}
|
|
});
|
|
|
|
this.sessions.delete(socket);
|
|
});
|
|
}
|
|
|
|
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}`);
|
|
socket.destroy();
|
|
}
|
|
}
|
|
|
|
private processData(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: Buffer): void {
|
|
const session = this.sessions.get(socket);
|
|
if (!session) {
|
|
console.error('No session found for socket. Closing connection.');
|
|
socket.destroy();
|
|
return;
|
|
}
|
|
|
|
// If we're in DATA_RECEIVING state, handle differently
|
|
if (session.state === SmtpState.DATA_RECEIVING) {
|
|
// Call async method but don't return the promise
|
|
this.processEmailData(socket, data.toString()).catch(err => {
|
|
console.error(`Error processing email data: ${err.message}`);
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Process normal SMTP commands
|
|
const lines = data.toString().split('\r\n').filter(line => line.length > 0);
|
|
for (const line of lines) {
|
|
console.log(`← ${line}`);
|
|
this.processCommand(socket, line);
|
|
}
|
|
}
|
|
|
|
private processCommand(socket: plugins.net.Socket | plugins.tls.TLSSocket, commandLine: string): void {
|
|
const session = this.sessions.get(socket);
|
|
if (!session || session.connectionEnded) return;
|
|
|
|
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');
|
|
|
|
// 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;
|
|
}
|
|
|
|
this.sendResponse(socket, '220 Ready to start TLS');
|
|
this.startTLS(socket);
|
|
}
|
|
|
|
private handleMailFrom(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): 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;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
const email = emailMatch[1];
|
|
if (!this.isValidEmail(email)) {
|
|
this.sendResponse(socket, '501 Invalid email address');
|
|
return;
|
|
}
|
|
|
|
session.mailFrom = email;
|
|
session.state = SmtpState.MAIL_FROM;
|
|
|
|
// Update envelope information
|
|
session.envelope.mailFrom = {
|
|
address: email,
|
|
args: {}
|
|
};
|
|
|
|
this.sendResponse(socket, '250 OK');
|
|
}
|
|
|
|
private handleRcptTo(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void {
|
|
const session = this.sessions.get(socket);
|
|
if (!session) return;
|
|
|
|
if (session.state !== SmtpState.MAIL_FROM && session.state !== SmtpState.RCPT_TO) {
|
|
this.sendResponse(socket, '503 Bad sequence of commands');
|
|
return;
|
|
}
|
|
|
|
// Extract email from RCPT TO:<user@example.com>
|
|
const emailMatch = args.match(/TO:<([^>]*)>/i);
|
|
if (!emailMatch) {
|
|
this.sendResponse(socket, '501 Syntax error in parameters or arguments');
|
|
return;
|
|
}
|
|
|
|
const email = emailMatch[1];
|
|
if (!this.isValidEmail(email)) {
|
|
this.sendResponse(socket, '501 Invalid email address');
|
|
return;
|
|
}
|
|
|
|
session.rcptTo.push(email);
|
|
session.state = SmtpState.RCPT_TO;
|
|
|
|
// Update envelope information
|
|
session.envelope.rcptTo.push({
|
|
address: email,
|
|
args: {}
|
|
});
|
|
|
|
this.sendResponse(socket, '250 OK');
|
|
}
|
|
|
|
private handleData(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
|
|
const session = this.sessions.get(socket);
|
|
if (!session) return;
|
|
|
|
if (session.state !== SmtpState.RCPT_TO) {
|
|
this.sendResponse(socket, '503 Bad sequence of commands');
|
|
return;
|
|
}
|
|
|
|
session.state = SmtpState.DATA_RECEIVING;
|
|
session.emailData = '';
|
|
this.sendResponse(socket, '354 End data with <CR><LF>.<CR><LF>');
|
|
}
|
|
|
|
private handleRset(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
|
|
const session = this.sessions.get(socket);
|
|
if (!session) return;
|
|
|
|
// Reset the session data but keep connection information
|
|
session.state = SmtpState.AFTER_EHLO;
|
|
session.mailFrom = '';
|
|
session.rcptTo = [];
|
|
session.emailData = '';
|
|
|
|
this.sendResponse(socket, '250 OK');
|
|
}
|
|
|
|
private handleQuit(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
|
|
const session = this.sessions.get(socket);
|
|
if (!session) return;
|
|
|
|
this.sendResponse(socket, '221 Goodbye');
|
|
|
|
// If we have collected email data, try to parse it before closing
|
|
if (session.state === SmtpState.FINISHED && session.emailData.length > 0) {
|
|
this.parseEmail(socket);
|
|
}
|
|
|
|
socket.end();
|
|
this.sessions.delete(socket);
|
|
}
|
|
|
|
private async processEmailData(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: string): Promise<void> {
|
|
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.');
|
|
return;
|
|
}
|
|
|
|
let mightBeSpam = false;
|
|
// Prepare headers for DKIM verification results
|
|
const customHeaders: Record<string, string> = {};
|
|
|
|
// Authentication results
|
|
let dkimResult = { domain: '', result: false };
|
|
let spfResult = { domain: '', result: false };
|
|
|
|
// Check security configuration
|
|
const securityConfig = { verifyDkim: true, verifySpf: true, verifyDmarc: true }; // Default security settings
|
|
|
|
// 1. Verify DKIM signature if enabled
|
|
if (securityConfig.verifyDkim) {
|
|
try {
|
|
// Mock DKIM verification for now - this is temporary during migration
|
|
const verificationResult = {
|
|
isValid: true,
|
|
domain: session.mailFrom.split('@')[1] || '',
|
|
selector: 'default',
|
|
status: 'pass',
|
|
errorMessage: ''
|
|
};
|
|
|
|
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';
|
|
}
|
|
}
|
|
|
|
// 2. Verify SPF if enabled
|
|
if (securityConfig.verifySpf) {
|
|
try {
|
|
// Get the client IP and hostname
|
|
const clientIp = socket.remoteAddress || '127.0.0.1';
|
|
const clientHostname = session.clientHostname || 'localhost';
|
|
|
|
// 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 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);
|
|
|
|
// 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})`;
|
|
}
|
|
}
|
|
|
|
// 3. Verify DMARC if enabled
|
|
if (securityConfig.verifyDmarc) {
|
|
try {
|
|
// Parse the email again
|
|
const parsedEmail = await plugins.mailparser.simpleParser(session.emailData);
|
|
|
|
// 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 - handled by email server in real implementation
|
|
const dmarcResult = {};
|
|
|
|
// Apply DMARC policy - assuming we would pass if either SPF or DKIM passes
|
|
const dmarcPassed = spfResult.result || dkimResult.result;
|
|
|
|
// 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.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})`;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const parsedEmail = await plugins.mailparser.simpleParser(session.emailData);
|
|
|
|
const email = new Email({
|
|
from: parsedEmail.from?.value[0].address || session.mailFrom,
|
|
to: session.rcptTo[0], // Use the first recipient
|
|
headers: customHeaders, // Add our custom headers with DKIM verification results
|
|
subject: parsedEmail.subject || '',
|
|
text: parsedEmail.html || parsedEmail.text || '',
|
|
attachments: parsedEmail.attachments?.map((attachment) => ({
|
|
filename: attachment.filename || '',
|
|
content: attachment.content,
|
|
contentType: attachment.contentType,
|
|
})) || [],
|
|
mightBeSpam: mightBeSpam,
|
|
});
|
|
|
|
console.log('Email received and parsed:', {
|
|
from: email.from,
|
|
to: email.to,
|
|
subject: email.subject,
|
|
attachments: email.attachments.length,
|
|
mightBeSpam: email.mightBeSpam
|
|
});
|
|
|
|
// 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
|
|
});
|
|
|
|
// Process or forward the email via unified email server
|
|
try {
|
|
await this.emailServerRef.processEmailByMode(email, {
|
|
id: session.id,
|
|
remoteAddress: session.remoteAddress,
|
|
clientHostname: session.clientHostname,
|
|
secure: session.useTLS,
|
|
authenticated: session.authenticated,
|
|
envelope: session.envelope,
|
|
processingMode: session.processingMode
|
|
}, session.processingMode || 'process');
|
|
} catch (err) {
|
|
console.error('Error in email server processing of incoming email:', err);
|
|
|
|
// 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
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error parsing email:', error);
|
|
|
|
// Log parsing errors
|
|
SecurityLogger.getInstance().logEvent({
|
|
level: SecurityLogLevel.ERROR,
|
|
type: SecurityEventType.EMAIL_VALIDATION,
|
|
message: `Error parsing incoming email`,
|
|
ipAddress: socket.remoteAddress,
|
|
details: {
|
|
error: error.message,
|
|
sender: session.mailFrom,
|
|
stack: error.stack
|
|
},
|
|
success: false
|
|
});
|
|
}
|
|
}
|
|
|
|
private startTLS(socket: plugins.net.Socket): void {
|
|
try {
|
|
const secureContext = plugins.tls.createSecureContext({
|
|
key: this.smtpServerOptions.key,
|
|
cert: this.smtpServerOptions.cert,
|
|
});
|
|
|
|
const tlsSocket = new plugins.tls.TLSSocket(socket, {
|
|
secureContext: secureContext,
|
|
isServer: true,
|
|
server: this.server
|
|
});
|
|
|
|
const originalSession = this.sessions.get(socket);
|
|
if (!originalSession) {
|
|
console.error('No session found when upgrading to TLS');
|
|
return;
|
|
}
|
|
|
|
// Transfer the session data to the new TLS socket
|
|
this.sessions.set(tlsSocket, {
|
|
...originalSession,
|
|
useTLS: true,
|
|
secure: true,
|
|
state: SmtpState.GREETING // Reset state to require a new EHLO
|
|
});
|
|
|
|
this.sessions.delete(socket);
|
|
|
|
tlsSocket.on('secure', () => {
|
|
console.log('TLS negotiation successful');
|
|
});
|
|
|
|
tlsSocket.on('data', (data: Buffer) => {
|
|
this.processData(tlsSocket, data);
|
|
});
|
|
|
|
tlsSocket.on('end', () => {
|
|
console.log('TLS socket ended');
|
|
const session = this.sessions.get(tlsSocket);
|
|
if (session) {
|
|
session.connectionEnded = true;
|
|
}
|
|
});
|
|
|
|
tlsSocket.on('error', (err) => {
|
|
console.error('TLS socket error:', err);
|
|
this.sessions.delete(tlsSocket);
|
|
tlsSocket.destroy();
|
|
});
|
|
|
|
tlsSocket.on('close', () => {
|
|
console.log('TLS socket closed');
|
|
this.sessions.delete(tlsSocket);
|
|
});
|
|
} catch (error) {
|
|
console.error('Error upgrading connection to TLS:', error);
|
|
socket.destroy();
|
|
}
|
|
}
|
|
|
|
private isValidEmail(email: string): boolean {
|
|
// Basic email validation - more comprehensive validation could be implemented
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
return emailRegex.test(email);
|
|
}
|
|
|
|
// These methods are defined elsewhere in the class, duplicates removed
|
|
} |