import * as plugins from '../plugins.js'; import { Readable } from 'node:stream'; import type { ISmtpConfig, ISmtpAuthConfig } from './classes.smtp.config.js'; import { EventEmitter } from 'node:events'; /** * Connection session information */ export interface ISmtpSession { id: string; remoteAddress: string; remotePort: number; clientHostname?: string; secure: boolean; transmissionType?: 'SMTP' | 'ESMTP'; user?: { username: string; [key: string]: any; }; envelope?: { mailFrom: { address: string; args: any; }; rcptTo: Array<{ address: string; args: any; }>; }; } /** * Authentication data */ export interface IAuthData { method: string; username: string; password: string; } /** * SMTP Server class for receiving emails */ export class SmtpServer extends EventEmitter { private config: ISmtpConfig; private server: any; // Will be SMTPServer from smtp-server once we add the dependency private incomingConnections: Map = new Map(); /** * Create a new SMTP server * @param config SMTP server configuration */ constructor(config: ISmtpConfig) { super(); this.config = config; } /** * Initialize and start the SMTP server */ public async start(): Promise { try { // This is a placeholder for the actual server creation // In the real implementation, we would use the smtp-server package console.log(`Starting SMTP server on ports ${this.config.ports.join(', ')}`); // Setup TLS options if provided const tlsOptions = this.config.tls ? { key: this.config.tls.keyPath ? await plugins.fs.promises.readFile(this.config.tls.keyPath, 'utf8') : undefined, cert: this.config.tls.certPath ? await plugins.fs.promises.readFile(this.config.tls.certPath, 'utf8') : undefined, ca: this.config.tls.caPath ? await plugins.fs.promises.readFile(this.config.tls.caPath, 'utf8') : undefined, minVersion: this.config.tls.minVersion || 'TLSv1.2', ciphers: this.config.tls.ciphers } : undefined; // Create the server // Note: In the actual implementation, this would use SMTPServer from smtp-server this.server = { // Placeholder for server instance async close() { console.log('SMTP server closed'); } }; // Set up event handlers this.setupEventHandlers(); // Listen on all specified ports for (const port of this.config.ports) { // In actual implementation, this would call server.listen(port) console.log(`SMTP server listening on port ${port}`); } this.emit('started'); } catch (error) { console.error('Failed to start SMTP server:', error); throw error; } } /** * Stop the SMTP server */ public async stop(): Promise { try { if (this.server) { // Close the server await this.server.close(); this.server = null; // Clear connection tracking this.incomingConnections.clear(); this.emit('stopped'); } } catch (error) { console.error('Error stopping SMTP server:', error); throw error; } } /** * Set up event handlers for the SMTP server */ private setupEventHandlers(): void { // These would be connected to actual server events in implementation // Connection handler this.onConnect((session, callback) => { // Store connection in tracking map this.incomingConnections.set(session.id, session); // Check if connection is allowed based on IP if (!this.isIpAllowed(session.remoteAddress)) { return callback(new Error('Connection refused')); } // Accept the connection callback(); }); // Authentication handler this.onAuth((auth, session, callback) => { // Skip auth check if not required if (!this.config.auth?.required) { return callback(null, { user: auth.username }); } // Check authentication if (this.authenticateUser(auth)) { return callback(null, { user: auth.username }); } // Authentication failed callback(new Error('Invalid credentials')); }); // Sender validation this.onMailFrom((address, session, callback) => { // Validate sender address if needed // Accept the sender callback(); }); // Recipient validation this.onRcptTo((address, session, callback) => { // Validate recipient address // Check if we handle this domain if (!this.isDomainHandled(address.address.split('@')[1])) { return callback(new Error('Domain not handled by this server')); } // Accept the recipient callback(); }); // Message data handler this.onData((stream, session, callback) => { // Process the incoming message this.processMessageData(stream, session) .then(() => callback()) .catch(err => callback(err)); }); } /** * Process incoming message data * @param stream Message data stream * @param session SMTP session */ private async processMessageData(stream: Readable, session: ISmtpSession): Promise { return new Promise((resolve, reject) => { // Collect the message data let messageData = ''; let messageSize = 0; stream.on('data', (chunk) => { messageData += chunk; messageSize += chunk.length; // Check size limits if (this.config.maxMessageSize && messageSize > this.config.maxMessageSize) { stream.unpipe(); return reject(new Error('Message size exceeds limit')); } }); stream.on('end', async () => { try { // Parse the email using mailparser const parsedMail = await this.parseEmail(messageData); // Emit message received event this.emit('message', { session, mail: parsedMail, rawData: messageData }); resolve(); } catch (error) { reject(error); } }); stream.on('error', (error) => { reject(error); }); }); } /** * Parse raw email data using mailparser * @param rawData Raw email data */ private async parseEmail(rawData: string): Promise { // Use mailparser to parse the email // We return 'any' here which will be treated as ExtendedParsedMail by consumers return plugins.mailparser.simpleParser(rawData); } /** * Check if an IP address is allowed to connect * @param ip IP address */ private isIpAllowed(ip: string): boolean { // Default to allowing all IPs if no restrictions const defaultAllowed = ['0.0.0.0/0']; // Check domain configs for IP restrictions for (const domainConfig of this.config.domainConfigs) { if (domainConfig.allowedIPs && domainConfig.allowedIPs.length > 0) { // Check if IP matches any of the allowed IPs for (const allowedIp of domainConfig.allowedIPs) { if (this.ipMatchesRange(ip, allowedIp)) { return true; } } } } // Check against default allowed IPs for (const allowedIp of defaultAllowed) { if (this.ipMatchesRange(ip, allowedIp)) { return true; } } return false; } /** * Check if an IP matches a range * @param ip IP address to check * @param range IP range in CIDR notation */ private ipMatchesRange(ip: string, range: string): boolean { try { // Use the 'ip' package to check if IP is in range return plugins.ip.cidrSubnet(range).contains(ip); } catch (error) { console.error(`Invalid IP range: ${range}`, error); return false; } } /** * Check if a domain is handled by this server * @param domain Domain to check */ private isDomainHandled(domain: string): boolean { // Check if the domain is configured in any domain config for (const domainConfig of this.config.domainConfigs) { for (const configDomain of domainConfig.domains) { if (this.domainMatches(domain, configDomain)) { return true; } } } return false; } /** * Check if a domain matches a pattern (including wildcards) * @param domain Domain to check * @param pattern Pattern to match against */ private domainMatches(domain: string, pattern: string): boolean { domain = domain.toLowerCase(); pattern = pattern.toLowerCase(); // Exact match if (domain === pattern) { return true; } // Wildcard match (*.example.com) if (pattern.startsWith('*.')) { const suffix = pattern.slice(2); return domain.endsWith(suffix) && domain.length > suffix.length; } return false; } /** * Authenticate a user * @param auth Authentication data */ private authenticateUser(auth: IAuthData): boolean { // Skip if no auth config if (!this.config.auth) { return true; } // Check if auth method is supported if (this.config.auth.methods && !this.config.auth.methods.includes(auth.method as any)) { return false; } // Check static user credentials if (this.config.auth.users) { const user = this.config.auth.users.find(u => u.username === auth.username && u.password === auth.password); if (user) { return true; } } // LDAP authentication would go here return false; } /** * Event handler for connection * @param handler Function to handle connection */ public onConnect(handler: (session: ISmtpSession, callback: (err?: Error) => void) => void): void { // In actual implementation, this would connect to the server's 'connection' event this.on('connect', handler); } /** * Event handler for authentication * @param handler Function to handle authentication */ public onAuth(handler: (auth: IAuthData, session: ISmtpSession, callback: (err?: Error, user?: any) => void) => void): void { // In actual implementation, this would connect to the server's 'auth' event this.on('auth', handler); } /** * Event handler for MAIL FROM command * @param handler Function to handle MAIL FROM */ public onMailFrom(handler: (address: { address: string; args: any }, session: ISmtpSession, callback: (err?: Error) => void) => void): void { // In actual implementation, this would connect to the server's 'mail' event this.on('mail', handler); } /** * Event handler for RCPT TO command * @param handler Function to handle RCPT TO */ public onRcptTo(handler: (address: { address: string; args: any }, session: ISmtpSession, callback: (err?: Error) => void) => void): void { // In actual implementation, this would connect to the server's 'rcpt' event this.on('rcpt', handler); } /** * Event handler for DATA command * @param handler Function to handle DATA */ public onData(handler: (stream: Readable, session: ISmtpSession, callback: (err?: Error) => void) => void): void { // In actual implementation, this would connect to the server's 'data' event this.on('dataReady', handler); } /** * Update the server configuration * @param config New configuration */ public updateConfig(config: Partial): void { this.config = { ...this.config, ...config }; // In a real implementation, this might require restarting the server this.emit('configUpdated', this.config); } /** * Get server statistics */ public getStats(): any { return { connections: this.incomingConnections.size, // Additional stats would be included here }; } }