From 5220ee08575e478462c4ec93b964244fe375b7ec Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 10 Feb 2026 22:00:44 +0000 Subject: [PATCH] feat(mailer-smtp): implement in-process SMTP server and management IPC integration --- changelog.md | 12 + dist_ts/00_commitinfo_data.js | 2 +- .../routing/classes.unified.email.server.d.ts | 11 + .../routing/classes.unified.email.server.js | 278 +++-- .../security/classes.rustsecuritybridge.d.ts | 92 +- .../security/classes.rustsecuritybridge.js | 61 +- rust/Cargo.lock | 18 + rust/crates/mailer-bin/Cargo.toml | 1 + rust/crates/mailer-bin/src/main.rs | 460 +++++++- rust/crates/mailer-smtp/Cargo.toml | 8 + rust/crates/mailer-smtp/src/command.rs | 421 +++++++ rust/crates/mailer-smtp/src/config.rs | 86 ++ rust/crates/mailer-smtp/src/connection.rs | 1023 +++++++++++++++++ rust/crates/mailer-smtp/src/data.rs | 289 +++++ rust/crates/mailer-smtp/src/lib.rs | 43 +- rust/crates/mailer-smtp/src/rate_limiter.rs | 198 ++++ rust/crates/mailer-smtp/src/response.rs | 284 +++++ rust/crates/mailer-smtp/src/server.rs | 308 +++++ rust/crates/mailer-smtp/src/session.rs | 206 ++++ rust/crates/mailer-smtp/src/state.rs | 219 ++++ rust/crates/mailer-smtp/src/validation.rs | 169 +++ ts/00_commitinfo_data.ts | 2 +- .../routing/classes.unified.email.server.ts | 306 +++-- ts/security/classes.rustsecuritybridge.ts | 167 +++ 24 files changed, 4390 insertions(+), 274 deletions(-) create mode 100644 rust/crates/mailer-smtp/src/command.rs create mode 100644 rust/crates/mailer-smtp/src/config.rs create mode 100644 rust/crates/mailer-smtp/src/connection.rs create mode 100644 rust/crates/mailer-smtp/src/data.rs create mode 100644 rust/crates/mailer-smtp/src/rate_limiter.rs create mode 100644 rust/crates/mailer-smtp/src/response.rs create mode 100644 rust/crates/mailer-smtp/src/server.rs create mode 100644 rust/crates/mailer-smtp/src/session.rs create mode 100644 rust/crates/mailer-smtp/src/state.rs create mode 100644 rust/crates/mailer-smtp/src/validation.rs diff --git a/changelog.md b/changelog.md index 62d6b1c..1fb7bd3 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,17 @@ # Changelog +## 2026-02-10 - 2.2.0 - feat(mailer-smtp) +implement in-process SMTP server and management IPC integration + +- Add full SMTP protocol engine crate (mailer-smtp) with modules: command, config, connection, data, response, session, state, validation, rate_limiter and server +- Introduce SmtpServerConfig, DataAccumulator (DATA phase handling, dot-unstuffing, size limits) and SmtpResponse builder with EHLO capability construction +- Add in-process RateLimiter using DashMap and runtime-configurable RateLimitConfig +- Add TCP/TLS server start/stop API (start_server) with TlsAcceptor building from PEM and SmtpServerHandle for shutdown and status +- Integrate callback registry and oneshot-based correlation callbacks in mailer-bin management mode for email processing/auth results and JSON IPC parsing for SmtpServerConfig +- TypeScript bridge and routing updates: new IPC commands/types (startSmtpServer, stopSmtpServer, emailProcessingResult, authResult, configureRateLimits) and event handlers (emailReceived, authRequest) +- Update Cargo manifests and lockfile to add dependencies (dashmap, regex, rustls, rustls-pemfile, rustls-pki-types, uuid, serde_json, base64, etc.) +- Add comprehensive unit tests for new modules (config, data, response, session, state, rate_limiter, validation) + ## 2026-02-10 - 2.1.0 - feat(security) migrate content scanning and bounce detection to Rust security bridge; add scanContent IPC command and Rust content scanner with tests; update TS RustSecurityBridge and callers, and adjust CI package references diff --git a/dist_ts/00_commitinfo_data.js b/dist_ts/00_commitinfo_data.js index 1bdc242..47b8c58 100644 --- a/dist_ts/00_commitinfo_data.js +++ b/dist_ts/00_commitinfo_data.js @@ -3,7 +3,7 @@ */ export const commitinfo = { name: '@push.rocks/smartmta', - version: '2.0.1', + version: '2.1.0', description: 'A high-performance, enterprise-grade Mail Transfer Agent (MTA) built from scratch in TypeScript with Rust acceleration.' }; //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSxzQkFBc0I7SUFDNUIsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLHlIQUF5SDtDQUN2SSxDQUFBIn0= \ No newline at end of file diff --git a/dist_ts/mail/routing/classes.unified.email.server.d.ts b/dist_ts/mail/routing/classes.unified.email.server.d.ts index 0d2a331..5144ff9 100644 --- a/dist_ts/mail/routing/classes.unified.email.server.d.ts +++ b/dist_ts/mail/routing/classes.unified.email.server.d.ts @@ -164,6 +164,17 @@ export declare class UnifiedEmailServer extends EventEmitter { * Stop the unified email server */ stop(): Promise; + /** + * Handle an emailReceived event from the Rust SMTP server. + * Decodes the email data, processes it through the routing system, + * and sends back the result via the correlation-ID callback. + */ + private handleRustEmailReceived; + /** + * Handle an authRequest event from the Rust SMTP server. + * Validates credentials and sends back the result. + */ + private handleRustAuthRequest; /** * Verify inbound email security (DKIM/SPF/DMARC) using the Rust bridge. * Falls back gracefully if the bridge is not running. diff --git a/dist_ts/mail/routing/classes.unified.email.server.js b/dist_ts/mail/routing/classes.unified.email.server.js index c4b0545..4b0a574 100644 --- a/dist_ts/mail/routing/classes.unified.email.server.js +++ b/dist_ts/mail/routing/classes.unified.email.server.js @@ -204,125 +204,77 @@ export class UnifiedEmailServer extends EventEmitter { // Ensure we have the necessary TLS options const hasTlsConfig = this.options.tls?.keyPath && this.options.tls?.certPath; // Prepare the certificate and key if available - let key; - let cert; + let tlsCertPem; + let tlsKeyPem; if (hasTlsConfig) { try { - key = plugins.fs.readFileSync(this.options.tls.keyPath, 'utf8'); - cert = plugins.fs.readFileSync(this.options.tls.certPath, 'utf8'); + tlsKeyPem = plugins.fs.readFileSync(this.options.tls.keyPath, 'utf8'); + tlsCertPem = plugins.fs.readFileSync(this.options.tls.certPath, 'utf8'); logger.log('info', 'TLS certificates loaded successfully'); } catch (error) { logger.log('warn', `Failed to load TLS certificates: ${error.message}`); } } - // Create a SMTP server for each port - for (const port of this.options.ports) { - // Create a reference object to hold the MTA service during setup - const mtaRef = { - config: { - smtp: { - hostname: this.options.hostname - }, - security: { - checkIPReputation: false, - verifyDkim: true, - verifySpf: true, - verifyDmarc: true - } - }, - // Security verification delegated to the Rust bridge - dkimVerifier: { - verify: async (rawMessage) => { - try { - const results = await this.rustBridge.verifyDkim(rawMessage); - const first = results[0]; - return { isValid: first?.is_valid ?? false, domain: first?.domain ?? '' }; - } - catch (err) { - logger.log('warn', `Rust DKIM verification failed: ${err.message}`); - return { isValid: false, domain: '' }; - } - } - }, - spfVerifier: { - verifyAndApply: async (session) => { - if (!session?.remoteAddress || session.remoteAddress === '127.0.0.1') { - return true; // localhost — skip SPF - } - try { - const result = await this.rustBridge.checkSpf({ - ip: session.remoteAddress, - heloDomain: session.clientHostname || '', - hostname: this.options.hostname, - mailFrom: session.envelope?.mailFrom?.address || session.mailFrom || '', - }); - return result.result === 'pass' || result.result === 'none' || result.result === 'neutral'; - } - catch (err) { - logger.log('warn', `Rust SPF check failed: ${err.message}`); - return true; // Accept on error to avoid blocking mail - } - } - }, - dmarcVerifier: { - verify: async () => ({}), - applyPolicy: () => true - }, - processIncomingEmail: async (email) => { - // Process email using the new route-based system - await this.processEmailByMode(email, { - id: 'session-' + Math.random().toString(36).substring(2), - state: SmtpState.FINISHED, - mailFrom: email.from, - rcptTo: email.to, - emailData: email.toRFC822String(), // Use the proper method to get the full email content - useTLS: false, - connectionEnded: true, - remoteAddress: '127.0.0.1', - clientHostname: '', - secure: false, - authenticated: false, - envelope: { - mailFrom: { address: email.from, args: {} }, - rcptTo: email.to.map(recipient => ({ address: recipient, args: {} })) - } - }); - return true; - } - }; - // Create server options - const serverOptions = { - port, - hostname: this.options.hostname, - key, - cert - }; - // Create and start the SMTP server - const smtpServer = createSmtpServer(mtaRef, serverOptions); - this.servers.push(smtpServer); - // Start the server - await new Promise((resolve, reject) => { - try { - // Leave this empty for now, smtpServer.start() is handled by the SMTPServer class internally - // The server is started when it's created - logger.log('info', `UnifiedEmailServer listening on port ${port}`); - // Event handlers are managed internally by the SmtpServer class - // No need to access the private server property - resolve(); - } - catch (err) { - if (err.code === 'EADDRINUSE') { - logger.log('error', `Port ${port} is already in use`); - reject(new Error(`Port ${port} is already in use`)); - } - else { - logger.log('error', `Error starting server on port ${port}: ${err.message}`); - reject(err); - } - } - }); + // --- Start Rust SMTP server --- + // Register event handlers for email reception and auth + this.rustBridge.onEmailReceived(async (data) => { + try { + await this.handleRustEmailReceived(data); + } + catch (err) { + logger.log('error', `Error handling email from Rust SMTP: ${err.message}`); + // Send rejection back to Rust + await this.rustBridge.sendEmailProcessingResult({ + correlationId: data.correlationId, + accepted: false, + smtpCode: 451, + smtpMessage: 'Internal processing error', + }); + } + }); + this.rustBridge.onAuthRequest(async (data) => { + try { + await this.handleRustAuthRequest(data); + } + catch (err) { + logger.log('error', `Error handling auth from Rust SMTP: ${err.message}`); + await this.rustBridge.sendAuthResult({ + correlationId: data.correlationId, + success: false, + message: 'Internal auth error', + }); + } + }); + // Determine which ports need STARTTLS and which need implicit TLS + const smtpPorts = this.options.ports.filter(p => p !== 465); + const securePort = this.options.ports.find(p => p === 465); + const started = await this.rustBridge.startSmtpServer({ + hostname: this.options.hostname, + ports: smtpPorts, + securePort: securePort, + tlsCertPem, + tlsKeyPem, + maxMessageSize: this.options.maxMessageSize || 10 * 1024 * 1024, + maxConnections: this.options.maxConnections || this.options.maxClients || 100, + maxRecipients: 100, + connectionTimeoutSecs: this.options.connectionTimeout ? Math.floor(this.options.connectionTimeout / 1000) : 30, + dataTimeoutSecs: 60, + authEnabled: !!this.options.auth?.required || !!(this.options.auth?.users?.length), + maxAuthFailures: 3, + socketTimeoutSecs: this.options.socketTimeout ? Math.floor(this.options.socketTimeout / 1000) : 300, + processingTimeoutSecs: 30, + rateLimits: this.options.rateLimits ? { + maxConnectionsPerIp: this.options.rateLimits.global?.maxConnectionsPerIP || 50, + maxMessagesPerSender: this.options.rateLimits.global?.maxMessagesPerMinute || 100, + maxAuthFailuresPerIp: this.options.rateLimits.global?.maxAuthFailuresPerIP || 5, + windowSecs: 60, + } : undefined, + }); + if (!started) { + throw new Error('Failed to start Rust SMTP server'); } + logger.log('info', `Rust SMTP server listening on ports: ${smtpPorts.join(', ')}${securePort ? ` + ${securePort} (TLS)` : ''}`); logger.log('info', 'UnifiedEmailServer started successfully'); this.emit('started'); } @@ -378,6 +330,14 @@ export class UnifiedEmailServer extends EventEmitter { async stop() { logger.log('info', 'Stopping UnifiedEmailServer'); try { + // Stop the Rust SMTP server first + try { + await this.rustBridge.stopSmtpServer(); + logger.log('info', 'Rust SMTP server stopped'); + } + catch (err) { + logger.log('warn', `Error stopping Rust SMTP server: ${err.message}`); + } // Clear the servers array - servers will be garbage collected this.servers = []; // Stop Rust security bridge @@ -411,6 +371,102 @@ export class UnifiedEmailServer extends EventEmitter { throw error; } } + // ----------------------------------------------------------------------- + // Rust SMTP server event handlers + // ----------------------------------------------------------------------- + /** + * Handle an emailReceived event from the Rust SMTP server. + * Decodes the email data, processes it through the routing system, + * and sends back the result via the correlation-ID callback. + */ + async handleRustEmailReceived(data) { + const { correlationId, mailFrom, rcptTo, remoteAddr, clientHostname, secure, authenticatedUser } = data; + logger.log('info', `Rust SMTP received email from=${mailFrom} to=${rcptTo.join(',')} remote=${remoteAddr}`); + try { + // Decode the email data + let rawMessageBuffer; + if (data.data.type === 'inline' && data.data.base64) { + rawMessageBuffer = Buffer.from(data.data.base64, 'base64'); + } + else if (data.data.type === 'file' && data.data.path) { + rawMessageBuffer = plugins.fs.readFileSync(data.data.path); + // Clean up temp file + try { + plugins.fs.unlinkSync(data.data.path); + } + catch { + // Ignore cleanup errors + } + } + else { + throw new Error('Invalid email data transport'); + } + // Build a session-like object for processEmailByMode + const session = { + id: data.sessionId || 'rust-' + Math.random().toString(36).substring(2), + state: SmtpState.FINISHED, + mailFrom: mailFrom, + rcptTo: rcptTo, + emailData: rawMessageBuffer.toString('utf8'), + useTLS: secure, + connectionEnded: false, + remoteAddress: remoteAddr, + clientHostname: clientHostname || '', + secure: secure, + authenticated: !!authenticatedUser, + envelope: { + mailFrom: { address: mailFrom, args: {} }, + rcptTo: rcptTo.map(addr => ({ address: addr, args: {} })), + }, + }; + if (authenticatedUser) { + session.user = { username: authenticatedUser }; + } + // Process the email through the routing system + await this.processEmailByMode(rawMessageBuffer, session); + // Send acceptance back to Rust + await this.rustBridge.sendEmailProcessingResult({ + correlationId, + accepted: true, + smtpCode: 250, + smtpMessage: '2.0.0 Message accepted for delivery', + }); + } + catch (err) { + logger.log('error', `Failed to process email from Rust SMTP: ${err.message}`); + await this.rustBridge.sendEmailProcessingResult({ + correlationId, + accepted: false, + smtpCode: 550, + smtpMessage: `5.0.0 Processing failed: ${err.message}`, + }); + } + } + /** + * Handle an authRequest event from the Rust SMTP server. + * Validates credentials and sends back the result. + */ + async handleRustAuthRequest(data) { + const { correlationId, username, password, remoteAddr } = data; + logger.log('info', `Rust SMTP auth request for user=${username} from=${remoteAddr}`); + // Check against configured users + const users = this.options.auth?.users || []; + const matched = users.find(u => u.username === username && u.password === password); + if (matched) { + await this.rustBridge.sendAuthResult({ + correlationId, + success: true, + }); + } + else { + logger.log('warn', `Auth failed for user=${username} from=${remoteAddr}`); + await this.rustBridge.sendAuthResult({ + correlationId, + success: false, + message: 'Invalid credentials', + }); + } + } /** * Verify inbound email security (DKIM/SPF/DMARC) using the Rust bridge. * Falls back gracefully if the bridge is not running. @@ -1507,4 +1563,4 @@ export class UnifiedEmailServer extends EventEmitter { return this.rateLimiter; } } -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy51bmlmaWVkLmVtYWlsLnNlcnZlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3RzL21haWwvcm91dGluZy9jbGFzc2VzLnVuaWZpZWQuZW1haWwuc2VydmVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sa0JBQWtCLENBQUM7QUFDNUMsT0FBTyxLQUFLLEtBQUssTUFBTSxnQkFBZ0IsQ0FBQztBQUN4QyxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sUUFBUSxDQUFDO0FBQ3RDLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUN6QyxPQUFPLEVBQ0wsY0FBYyxFQUNkLGdCQUFnQixFQUNoQixpQkFBaUIsRUFDbEIsTUFBTSx5QkFBeUIsQ0FBQztBQUNqQyxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sb0NBQW9DLENBQUM7QUFDakUsT0FBTyxFQUFFLG1CQUFtQixFQUFFLE1BQU0sK0NBQStDLENBQUM7QUFDcEYsT0FBTyxFQUFFLGtCQUFrQixFQUFFLE1BQU0sOENBQThDLENBQUM7QUE4QmxGLE9BQU8sRUFBRSxXQUFXLEVBQUUsTUFBTSwyQkFBMkIsQ0FBQztBQUV4RCxPQUFPLEVBQUUsS0FBSyxFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFDakQsT0FBTyxFQUFFLGNBQWMsRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBQzlELE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQztBQUN0RCxPQUFPLEVBQUUsYUFBYSxFQUFFLFVBQVUsRUFBRSxjQUFjLEVBQUUsTUFBTSxrQ0FBa0MsQ0FBQztBQUM3RixPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxpQ0FBaUMsQ0FBQztBQUNuRSxPQUFPLEVBQUUsc0JBQXNCLEVBQUUsTUFBTSx5Q0FBeUMsQ0FBQztBQUVqRixPQUFPLEVBQUUsdUJBQXVCLEVBQWtDLE1BQU0sd0NBQXdDLENBQUM7QUFD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 \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy51bmlmaWVkLmVtYWlsLnNlcnZlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3RzL21haWwvcm91dGluZy9jbGFzc2VzLnVuaWZpZWQuZW1haWwuc2VydmVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sa0JBQWtCLENBQUM7QUFDNUMsT0FBTyxLQUFLLEtBQUssTUFBTSxnQkFBZ0IsQ0FBQztBQUN4QyxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sUUFBUSxDQUFDO0FBQ3RDLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUN6QyxPQUFPLEVBQ0wsY0FBYyxFQUNkLGdCQUFnQixFQUNoQixpQkFBaUIsRUFDbEIsTUFBTSx5QkFBeUIsQ0FBQztBQUNqQyxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sb0NBQW9DLENBQUM7QUFDakUsT0FBTyxFQUFFLG1CQUFtQixFQUFFLE1BQU0sK0NBQStDLENBQUM7QUFDcEYsT0FBTyxFQUFFLGtCQUFrQixFQUFFLE1BQU0sOENBQThDLENBQUM7QUErQmxGLE9BQU8sRUFBRSxXQUFXLEVBQUUsTUFBTSwyQkFBMkIsQ0FBQztBQUV4RCxPQUFPLEVBQUUsS0FBSyxFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFDakQsT0FBTyxFQUFFLGNBQWMsRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBQzlELE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQztBQUN0RCxPQUFPLEVBQUUsYUFBYSxFQUFFLFVBQVUsRUFBRSxjQUFjLEVBQUUsTUFBTSxrQ0FBa0MsQ0FBQztBQUM3RixPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxpQ0FBaUMsQ0FBQztBQUNuRSxPQUFPLEVBQUUsc0JBQXNCLEVBQUUsTUFBTSx5Q0FBeUMsQ0FBQztBQUVqRixPQUFPLEVBQUUsdUJBQXVCLEVBQWtDLE1BQU0sd0NBQXdDLENBQUM7QUFD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 \ No newline at end of file diff --git a/dist_ts/security/classes.rustsecuritybridge.d.ts b/dist_ts/security/classes.rustsecuritybridge.d.ts index 9975a10..e008e4d 100644 --- a/dist_ts/security/classes.rustsecuritybridge.d.ts +++ b/dist_ts/security/classes.rustsecuritybridge.d.ts @@ -60,6 +60,53 @@ interface IVersionInfo { security: string; smtp: string; } +interface ISmtpServerConfig { + hostname: string; + ports: number[]; + securePort?: number; + tlsCertPem?: string; + tlsKeyPem?: string; + maxMessageSize?: number; + maxConnections?: number; + maxRecipients?: number; + connectionTimeoutSecs?: number; + dataTimeoutSecs?: number; + authEnabled?: boolean; + maxAuthFailures?: number; + socketTimeoutSecs?: number; + processingTimeoutSecs?: number; + rateLimits?: IRateLimitConfig; +} +interface IRateLimitConfig { + maxConnectionsPerIp?: number; + maxMessagesPerSender?: number; + maxAuthFailuresPerIp?: number; + windowSecs?: number; +} +interface IEmailData { + type: 'inline' | 'file'; + base64?: string; + path?: string; +} +interface IEmailReceivedEvent { + correlationId: string; + sessionId: string; + mailFrom: string; + rcptTo: string[]; + data: IEmailData; + remoteAddr: string; + clientHostname: string | null; + secure: boolean; + authenticatedUser: string | null; + securityResults: any | null; +} +interface IAuthRequestEvent { + correlationId: string; + sessionId: string; + username: string; + password: string; + remoteAddr: string; +} /** * Bridge between TypeScript and the Rust `mailer-bin` binary. * @@ -135,5 +182,48 @@ export declare class RustSecurityBridge { hostname?: string; mailFrom: string; }): Promise; + /** + * Start the Rust SMTP server. + * The server will listen on the configured ports and emit events for + * emailReceived and authRequest that must be handled by the caller. + */ + startSmtpServer(config: ISmtpServerConfig): Promise; + /** Stop the Rust SMTP server. */ + stopSmtpServer(): Promise; + /** + * Send the result of email processing back to the Rust SMTP server. + * This resolves a pending correlation-ID callback, allowing the Rust + * server to send the SMTP response to the client. + */ + sendEmailProcessingResult(opts: { + correlationId: string; + accepted: boolean; + smtpCode?: number; + smtpMessage?: string; + }): Promise; + /** + * Send the result of authentication validation back to the Rust SMTP server. + */ + sendAuthResult(opts: { + correlationId: string; + success: boolean; + message?: string; + }): Promise; + /** Update rate limit configuration at runtime. */ + configureRateLimits(config: IRateLimitConfig): Promise; + /** + * Register a handler for emailReceived events from the Rust SMTP server. + * These events fire when a complete email has been received and needs processing. + */ + onEmailReceived(handler: (data: IEmailReceivedEvent) => void): void; + /** + * Register a handler for authRequest events from the Rust SMTP server. + * The handler must call sendAuthResult() with the correlationId. + */ + onAuthRequest(handler: (data: IAuthRequestEvent) => void): void; + /** Remove an emailReceived event handler. */ + offEmailReceived(handler: (data: IEmailReceivedEvent) => void): void; + /** Remove an authRequest event handler. */ + offAuthRequest(handler: (data: IAuthRequestEvent) => void): void; } -export type { IDkimVerificationResult, ISpfResult, IDmarcResult, IEmailSecurityResult, IValidationResult, IBounceDetection, IContentScanResult, IReputationResult as IRustReputationResult, IVersionInfo, }; +export type { IDkimVerificationResult, ISpfResult, IDmarcResult, IEmailSecurityResult, IValidationResult, IBounceDetection, IContentScanResult, IReputationResult as IRustReputationResult, IVersionInfo, ISmtpServerConfig, IRateLimitConfig, IEmailData, IEmailReceivedEvent, IAuthRequestEvent, }; diff --git a/dist_ts/security/classes.rustsecuritybridge.js b/dist_ts/security/classes.rustsecuritybridge.js index 122385e..349a922 100644 --- a/dist_ts/security/classes.rustsecuritybridge.js +++ b/dist_ts/security/classes.rustsecuritybridge.js @@ -141,5 +141,64 @@ export class RustSecurityBridge { async verifyEmail(opts) { return this.bridge.sendCommand('verifyEmail', opts); } + // ----------------------------------------------------------------------- + // SMTP Server lifecycle + // ----------------------------------------------------------------------- + /** + * Start the Rust SMTP server. + * The server will listen on the configured ports and emit events for + * emailReceived and authRequest that must be handled by the caller. + */ + async startSmtpServer(config) { + const result = await this.bridge.sendCommand('startSmtpServer', config); + return result?.started === true; + } + /** Stop the Rust SMTP server. */ + async stopSmtpServer() { + await this.bridge.sendCommand('stopSmtpServer', {}); + } + /** + * Send the result of email processing back to the Rust SMTP server. + * This resolves a pending correlation-ID callback, allowing the Rust + * server to send the SMTP response to the client. + */ + async sendEmailProcessingResult(opts) { + await this.bridge.sendCommand('emailProcessingResult', opts); + } + /** + * Send the result of authentication validation back to the Rust SMTP server. + */ + async sendAuthResult(opts) { + await this.bridge.sendCommand('authResult', opts); + } + /** Update rate limit configuration at runtime. */ + async configureRateLimits(config) { + await this.bridge.sendCommand('configureRateLimits', config); + } + // ----------------------------------------------------------------------- + // Event registration — delegates to the underlying bridge EventEmitter + // ----------------------------------------------------------------------- + /** + * Register a handler for emailReceived events from the Rust SMTP server. + * These events fire when a complete email has been received and needs processing. + */ + onEmailReceived(handler) { + this.bridge.on('management:emailReceived', handler); + } + /** + * Register a handler for authRequest events from the Rust SMTP server. + * The handler must call sendAuthResult() with the correlationId. + */ + onAuthRequest(handler) { + this.bridge.on('management:authRequest', handler); + } + /** Remove an emailReceived event handler. */ + offEmailReceived(handler) { + this.bridge.off('management:emailReceived', handler); + } + /** Remove an authRequest event handler. */ + offAuthRequest(handler) { + this.bridge.off('management:authRequest', handler); + } } -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5ydXN0c2VjdXJpdHlicmlkZ2UuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9zZWN1cml0eS9jbGFzc2VzLnJ1c3RzZWN1cml0eWJyaWRnZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGVBQWUsQ0FBQztBQUN6QyxPQUFPLEtBQUssS0FBSyxNQUFNLGFBQWEsQ0FBQztBQUNyQyxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sY0FBYyxDQUFDO0FBa0l0Qyw4RUFBOEU7QUFDOUUscUVBQXFFO0FBQ3JFLDhFQUE4RTtBQUU5RTs7Ozs7R0FLRztBQUNILE1BQU0sT0FBTyxrQkFBa0I7SUFDckIsTUFBTSxDQUFDLFFBQVEsR0FBOEIsSUFBSSxDQUFDO0lBRWxELE1BQU0sQ0FBcUU7SUFDM0UsUUFBUSxHQUFHLEtBQUssQ0FBQztJQUV6QjtRQUNFLElBQUksQ0FBQyxNQUFNLEdBQUcsSUFBSSxPQUFPLENBQUMsU0FBUyxDQUFDLFVBQVUsQ0FBa0I7WUFDOUQsVUFBVSxFQUFFLFlBQVk7WUFDeEIsT0FBTyxFQUFFLENBQUMsY0FBYyxDQUFDO1lBQ3pCLGdCQUFnQixFQUFFLE1BQU07WUFDeEIsY0FBYyxFQUFFLE1BQU07WUFDdEIsVUFBVSxFQUFFO2dCQUNWLE9BQU8sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxVQUFVLEVBQUUsV0FBVyxFQUFFLFlBQVksQ0FBQztnQkFDOUQsT0FBTyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLFVBQVUsRUFBRSxNQUFNLEVBQUUsUUFBUSxFQUFFLFNBQVMsRUFBRSxZQUFZLENBQUM7Z0JBQzlFLE9BQU8sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxVQUFVLEVBQUUsTUFBTSxFQUFFLFFBQVEsRUFBRSxPQUFPLEVBQUUsWUFBWSxDQUFDO2FBQzdFO1lBQ0QsZ0JBQWdCLEVBQUUsS0FBSztT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 \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5ydXN0c2VjdXJpdHlicmlkZ2UuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9zZWN1cml0eS9jbGFzc2VzLnJ1c3RzZWN1cml0eWJyaWRnZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGVBQWUsQ0FBQztBQUN6QyxPQUFPLEtBQUssS0FBSyxNQUFNLGFBQWEsQ0FBQztBQUNyQyxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sY0FBYyxDQUFDO0FBcU50Qyw4RUFBOEU7QUFDOUUscUVBQXFFO0FBQ3JFLDhFQUE4RTtBQUU5RTs7Ozs7R0FLRztBQUNILE1BQU0sT0FBTyxrQkFBa0I7SUFDckIsTUFBTSxDQUFDLFFBQVEsR0FBOEIsSUFBSSxDQUFDO0lBRWxELE1BQU0sQ0FBcUU7SUFDM0UsUUFBUSxHQUFHLEtBQUssQ0FBQztJQUV6QjtRQUNFLElBQUksQ0FBQyxNQUFNLEdBQUcsSUFBSSxPQUFPLENBQUMsU0FBUyxDQUFDLFVBQVUsQ0FBa0I7WUFDOUQsVUFBVSxFQUFFLFlBQVk7WUFDeEIsT0FBTyxFQUFFLENBQUMsY0FBYyxDQUFDO1lBQ3pCLGdCQUFnQixFQUFFLE1BQU07WUFDeEIsY0FBYyxFQUFFLE1BQU07WUFDdEIsVUFBVSxFQUFFO2dCQUNWLE9BQU8sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxVQUFVLEVBQUUsV0FBVyxFQUFFLFlBQVksQ0FBQztnQkFDOUQsT0FBTyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLFVBQVUsRUFBRSxNQUFNLEVBQUUsUUFBUSxFQUFFLFNBQVMsRUFBRSxZQUFZLENBQUM7Z0JBQzlFLE9BQU8sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxVQUFVLEVBQUUsTUFBTSxFQUFFLFFBQVEsRUFBRSxPQUFPLEVBQUUsWUFBWSxDQUFDO2FBQzdFO1lBQ0QsZ0JBQWdCLEVBQUUsS0FBSztT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 \ No newline at end of file diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 70a1b7a..42b669c 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1005,6 +1005,7 @@ name = "mailer-bin" version = "0.1.0" dependencies = [ "clap", + "dashmap", "hickory-resolver 0.25.2", "mailer-core", "mailer-security", @@ -1068,15 +1069,23 @@ dependencies = [ name = "mailer-smtp" version = "0.1.0" dependencies = [ + "base64", "bytes", "dashmap", "hickory-resolver 0.25.2", "mailer-core", + "mailer-security", + "regex", + "rustls", + "rustls-pemfile", + "rustls-pki-types", "serde", + "serde_json", "thiserror", "tokio", "tokio-rustls", "tracing", + "uuid", ] [[package]] @@ -1491,6 +1500,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.14.0" diff --git a/rust/crates/mailer-bin/Cargo.toml b/rust/crates/mailer-bin/Cargo.toml index a878ede..d212549 100644 --- a/rust/crates/mailer-bin/Cargo.toml +++ b/rust/crates/mailer-bin/Cargo.toml @@ -18,3 +18,4 @@ serde.workspace = true serde_json.workspace = true clap.workspace = true hickory-resolver.workspace = true +dashmap.workspace = true diff --git a/rust/crates/mailer-bin/src/main.rs b/rust/crates/mailer-bin/src/main.rs index ce3abd4..18cb8d7 100644 --- a/rust/crates/mailer-bin/src/main.rs +++ b/rust/crates/mailer-bin/src/main.rs @@ -6,9 +6,16 @@ //! integration with `@push.rocks/smartrust` from TypeScript use clap::{Parser, Subcommand}; +use dashmap::DashMap; use serde::{Deserialize, Serialize}; use std::io::{self, BufRead, Write}; use std::net::IpAddr; +use std::sync::Arc; +use tokio::sync::oneshot; + +use mailer_smtp::connection::{ + AuthResult, CallbackRegistry, ConnectionEvent, EmailProcessingResult, +}; /// mailer-bin: Rust-powered email security tools #[derive(Parser)] @@ -105,6 +112,43 @@ struct IpcEvent { data: serde_json::Value, } +// --- Pending callbacks for correlation-ID based reverse calls --- + +/// Stores oneshot senders for pending email processing and auth callbacks. +struct PendingCallbacks { + email: DashMap>, + auth: DashMap>, +} + +impl PendingCallbacks { + fn new() -> Self { + Self { + email: DashMap::new(), + auth: DashMap::new(), + } + } +} + +impl CallbackRegistry for PendingCallbacks { + fn register_email_callback( + &self, + correlation_id: &str, + ) -> oneshot::Receiver { + let (tx, rx) = oneshot::channel(); + self.email.insert(correlation_id.to_string(), tx); + rx + } + + fn register_auth_callback( + &self, + correlation_id: &str, + ) -> oneshot::Receiver { + let (tx, rx) = oneshot::channel(); + self.auth.insert(correlation_id.to_string(), tx); + rx + } +} + fn main() { let cli = Cli::parse(); @@ -278,7 +322,17 @@ fn main() { use std::io::Read; +/// Shared state for the management mode. +struct ManagementState { + callbacks: Arc, + smtp_handle: Option, + smtp_event_rx: Option>, +} + /// Run in management/IPC mode for smartrust bridge. +/// +/// This mode supports both request/response IPC (existing commands) and +/// long-running SMTP server with event-based callbacks. fn run_management_mode() { // Signal readiness let ready_event = IpcEvent { @@ -294,39 +348,153 @@ fn run_management_mode() { let rt = tokio::runtime::Runtime::new().unwrap(); - let stdin = io::stdin(); - for line in stdin.lock().lines() { - let line = match line { - Ok(l) => l, - Err(_) => break, - }; + let callbacks = Arc::new(PendingCallbacks::new()); + let mut state = ManagementState { + callbacks: callbacks.clone(), + smtp_handle: None, + smtp_event_rx: None, + }; - if line.trim().is_empty() { - continue; - } + // We need to read stdin in a separate thread (blocking I/O) + // and process commands + SMTP events in the tokio runtime. + let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::channel::(256); - let req: IpcRequest = match serde_json::from_str(&line) { - Ok(r) => r, - Err(e) => { - let resp = IpcResponse { - id: "unknown".to_string(), - success: false, - result: None, - error: Some(format!("Invalid request: {}", e)), - }; - println!("{}", serde_json::to_string(&resp).unwrap()); - io::stdout().flush().unwrap(); - continue; + // Spawn stdin reader thread + std::thread::spawn(move || { + let stdin = io::stdin(); + for line in stdin.lock().lines() { + match line { + Ok(l) if !l.trim().is_empty() => { + if cmd_tx.blocking_send(l).is_err() { + break; + } + } + Ok(_) => continue, + Err(_) => break, } - }; + } + }); - let response = rt.block_on(handle_ipc_request(&req)); - println!("{}", serde_json::to_string(&response).unwrap()); - io::stdout().flush().unwrap(); + rt.block_on(async { + loop { + // Select between stdin commands and SMTP server events + tokio::select! { + cmd = cmd_rx.recv() => { + match cmd { + Some(line) => { + let req: IpcRequest = match serde_json::from_str(&line) { + Ok(r) => r, + Err(e) => { + let resp = IpcResponse { + id: "unknown".to_string(), + success: false, + result: None, + error: Some(format!("Invalid request: {}", e)), + }; + emit_line(&serde_json::to_string(&resp).unwrap()); + continue; + } + }; + + let response = handle_ipc_request(&req, &mut state).await; + emit_line(&serde_json::to_string(&response).unwrap()); + } + None => { + // stdin closed — shut down + if let Some(handle) = state.smtp_handle.take() { + handle.shutdown().await; + } + break; + } + } + } + event = async { + if let Some(rx) = &mut state.smtp_event_rx { + rx.recv().await + } else { + // No SMTP server running — wait forever (yields to other branch) + std::future::pending::>().await + } + } => { + if let Some(event) = event { + handle_smtp_event(event); + } + } + } + } + }); +} + +/// Emit a line to stdout and flush. +fn emit_line(line: &str) { + let stdout = io::stdout(); + let mut handle = stdout.lock(); + let _ = writeln!(handle, "{}", line); + let _ = handle.flush(); +} + +/// Emit an IPC event to stdout. +fn emit_event(event_name: &str, data: serde_json::Value) { + let event = IpcEvent { + event: event_name.to_string(), + data, + }; + emit_line(&serde_json::to_string(&event).unwrap()); +} + +/// Handle a connection event from the SMTP server. +fn handle_smtp_event(event: ConnectionEvent) { + match event { + ConnectionEvent::EmailReceived { + correlation_id, + session_id, + mail_from, + rcpt_to, + data, + remote_addr, + client_hostname, + secure, + authenticated_user, + security_results, + } => { + emit_event( + "emailReceived", + serde_json::json!({ + "correlationId": correlation_id, + "sessionId": session_id, + "mailFrom": mail_from, + "rcptTo": rcpt_to, + "data": data, + "remoteAddr": remote_addr, + "clientHostname": client_hostname, + "secure": secure, + "authenticatedUser": authenticated_user, + "securityResults": security_results, + }), + ); + } + ConnectionEvent::AuthRequest { + correlation_id, + session_id, + username, + password, + remote_addr, + } => { + emit_event( + "authRequest", + serde_json::json!({ + "correlationId": correlation_id, + "sessionId": session_id, + "username": username, + "password": password, + "remoteAddr": remote_addr, + }), + ); + } } } -async fn handle_ipc_request(req: &IpcRequest) -> IpcResponse { +async fn handle_ipc_request(req: &IpcRequest, state: &mut ManagementState) -> IpcResponse { match req.method.as_str() { "ping" => IpcResponse { id: req.id.clone(), @@ -636,6 +804,35 @@ async fn handle_ipc_request(req: &IpcRequest) -> IpcResponse { } } + // --- SMTP Server lifecycle commands --- + + "startSmtpServer" => { + handle_start_smtp_server(req, state).await + } + + "stopSmtpServer" => { + handle_stop_smtp_server(req, state).await + } + + "emailProcessingResult" => { + handle_email_processing_result(req, state) + } + + "authResult" => { + handle_auth_result(req, state) + } + + "configureRateLimits" => { + // Rate limit configuration is set at startSmtpServer time. + // This command allows runtime updates, but for now we acknowledge it. + IpcResponse { + id: req.id.clone(), + success: true, + result: Some(serde_json::json!({"configured": true})), + error: None, + } + } + _ => IpcResponse { id: req.id.clone(), success: false, @@ -644,3 +841,214 @@ async fn handle_ipc_request(req: &IpcRequest) -> IpcResponse { }, } } + +/// Handle startSmtpServer IPC command. +async fn handle_start_smtp_server(req: &IpcRequest, state: &mut ManagementState) -> IpcResponse { + // Stop existing server if running + if let Some(handle) = state.smtp_handle.take() { + handle.shutdown().await; + } + + // Parse config from params + let config = match parse_smtp_config(&req.params) { + Ok(c) => c, + Err(e) => { + return IpcResponse { + id: req.id.clone(), + success: false, + result: None, + error: Some(format!("Invalid config: {}", e)), + }; + } + }; + + // Parse optional rate limit config + let rate_config = req.params.get("rateLimits").and_then(|v| { + serde_json::from_value::(v.clone()).ok() + }); + + match mailer_smtp::server::start_server(config, state.callbacks.clone(), rate_config).await { + Ok((handle, event_rx)) => { + state.smtp_handle = Some(handle); + state.smtp_event_rx = Some(event_rx); + IpcResponse { + id: req.id.clone(), + success: true, + result: Some(serde_json::json!({"started": true})), + error: None, + } + } + Err(e) => IpcResponse { + id: req.id.clone(), + success: false, + result: None, + error: Some(format!("Failed to start SMTP server: {}", e)), + }, + } +} + +/// Handle stopSmtpServer IPC command. +async fn handle_stop_smtp_server(req: &IpcRequest, state: &mut ManagementState) -> IpcResponse { + if let Some(handle) = state.smtp_handle.take() { + handle.shutdown().await; + state.smtp_event_rx = None; + IpcResponse { + id: req.id.clone(), + success: true, + result: Some(serde_json::json!({"stopped": true})), + error: None, + } + } else { + IpcResponse { + id: req.id.clone(), + success: true, + result: Some(serde_json::json!({"stopped": true, "wasRunning": false})), + error: None, + } + } +} + +/// Handle emailProcessingResult IPC command — resolves a pending email callback. +fn handle_email_processing_result(req: &IpcRequest, state: &ManagementState) -> IpcResponse { + let correlation_id = req + .params + .get("correlationId") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + let result = EmailProcessingResult { + accepted: req.params.get("accepted").and_then(|v| v.as_bool()).unwrap_or(false), + smtp_code: req.params.get("smtpCode").and_then(|v| v.as_u64()).map(|v| v as u16), + smtp_message: req + .params + .get("smtpMessage") + .and_then(|v| v.as_str()) + .map(String::from), + }; + + if let Some((_, tx)) = state.callbacks.email.remove(correlation_id) { + let _ = tx.send(result); + IpcResponse { + id: req.id.clone(), + success: true, + result: Some(serde_json::json!({"resolved": true})), + error: None, + } + } else { + IpcResponse { + id: req.id.clone(), + success: false, + result: None, + error: Some(format!( + "No pending callback for correlationId: {}", + correlation_id + )), + } + } +} + +/// Handle authResult IPC command — resolves a pending auth callback. +fn handle_auth_result(req: &IpcRequest, state: &ManagementState) -> IpcResponse { + let correlation_id = req + .params + .get("correlationId") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + let result = AuthResult { + success: req.params.get("success").and_then(|v| v.as_bool()).unwrap_or(false), + message: req + .params + .get("message") + .and_then(|v| v.as_str()) + .map(String::from), + }; + + if let Some((_, tx)) = state.callbacks.auth.remove(correlation_id) { + let _ = tx.send(result); + IpcResponse { + id: req.id.clone(), + success: true, + result: Some(serde_json::json!({"resolved": true})), + error: None, + } + } else { + IpcResponse { + id: req.id.clone(), + success: false, + result: None, + error: Some(format!( + "No pending auth callback for correlationId: {}", + correlation_id + )), + } + } +} + +/// Parse SmtpServerConfig from IPC params JSON. +fn parse_smtp_config( + params: &serde_json::Value, +) -> Result { + let mut config = mailer_smtp::config::SmtpServerConfig::default(); + + if let Some(hostname) = params.get("hostname").and_then(|v| v.as_str()) { + config.hostname = hostname.to_string(); + } + + if let Some(ports) = params.get("ports").and_then(|v| v.as_array()) { + config.ports = ports + .iter() + .filter_map(|v| v.as_u64().map(|p| p as u16)) + .collect(); + } + + if let Some(secure_port) = params.get("securePort").and_then(|v| v.as_u64()) { + config.secure_port = Some(secure_port as u16); + } + + if let Some(cert) = params.get("tlsCertPem").and_then(|v| v.as_str()) { + config.tls_cert_pem = Some(cert.to_string()); + } + + if let Some(key) = params.get("tlsKeyPem").and_then(|v| v.as_str()) { + config.tls_key_pem = Some(key.to_string()); + } + + if let Some(size) = params.get("maxMessageSize").and_then(|v| v.as_u64()) { + config.max_message_size = size; + } + + if let Some(conns) = params.get("maxConnections").and_then(|v| v.as_u64()) { + config.max_connections = conns as u32; + } + + if let Some(rcpts) = params.get("maxRecipients").and_then(|v| v.as_u64()) { + config.max_recipients = rcpts as u32; + } + + if let Some(timeout) = params.get("connectionTimeoutSecs").and_then(|v| v.as_u64()) { + config.connection_timeout_secs = timeout; + } + + if let Some(timeout) = params.get("dataTimeoutSecs").and_then(|v| v.as_u64()) { + config.data_timeout_secs = timeout; + } + + if let Some(auth) = params.get("authEnabled").and_then(|v| v.as_bool()) { + config.auth_enabled = auth; + } + + if let Some(failures) = params.get("maxAuthFailures").and_then(|v| v.as_u64()) { + config.max_auth_failures = failures as u32; + } + + if let Some(timeout) = params.get("socketTimeoutSecs").and_then(|v| v.as_u64()) { + config.socket_timeout_secs = timeout; + } + + if let Some(timeout) = params.get("processingTimeoutSecs").and_then(|v| v.as_u64()) { + config.processing_timeout_secs = timeout; + } + + Ok(config) +} diff --git a/rust/crates/mailer-smtp/Cargo.toml b/rust/crates/mailer-smtp/Cargo.toml index 2e37751..9ad39c7 100644 --- a/rust/crates/mailer-smtp/Cargo.toml +++ b/rust/crates/mailer-smtp/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true [dependencies] mailer-core = { path = "../mailer-core" } +mailer-security = { path = "../mailer-security" } tokio.workspace = true tokio-rustls.workspace = true hickory-resolver.workspace = true @@ -14,3 +15,10 @@ thiserror.workspace = true tracing.workspace = true bytes.workspace = true serde.workspace = true +serde_json = "1" +regex = "1" +uuid = { version = "1", features = ["v4"] } +base64.workspace = true +rustls-pki-types.workspace = true +rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "std", "tls12"] } +rustls-pemfile = "2" diff --git a/rust/crates/mailer-smtp/src/command.rs b/rust/crates/mailer-smtp/src/command.rs new file mode 100644 index 0000000..9a533f5 --- /dev/null +++ b/rust/crates/mailer-smtp/src/command.rs @@ -0,0 +1,421 @@ +//! SMTP command parser. +//! +//! Parses raw SMTP command lines into structured `SmtpCommand` variants. + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// A parsed SMTP command. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum SmtpCommand { + /// EHLO with client hostname/IP + Ehlo(String), + /// HELO with client hostname/IP + Helo(String), + /// MAIL FROM with sender address and optional parameters (e.g. SIZE=12345) + MailFrom { + address: String, + params: HashMap>, + }, + /// RCPT TO with recipient address and optional parameters + RcptTo { + address: String, + params: HashMap>, + }, + /// DATA command — begin message body + Data, + /// RSET — reset current transaction + Rset, + /// NOOP — no operation + Noop, + /// QUIT — close connection + Quit, + /// STARTTLS — upgrade to TLS + StartTls, + /// AUTH with mechanism and optional initial response + Auth { + mechanism: AuthMechanism, + initial_response: Option, + }, + /// HELP with optional topic + Help(Option), + /// VRFY with address or username + Vrfy(String), + /// EXPN with mailing list name + Expn(String), +} + +/// Supported AUTH mechanisms. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum AuthMechanism { + Plain, + Login, +} + +/// Errors that can occur during command parsing. +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +pub enum ParseError { + #[error("empty command line")] + Empty, + #[error("unrecognized command: {0}")] + UnrecognizedCommand(String), + #[error("syntax error in parameters: {0}")] + SyntaxError(String), + #[error("missing required argument for {0}")] + MissingArgument(String), +} + +/// Parse a raw SMTP command line (without trailing CRLF) into a `SmtpCommand`. +pub fn parse_command(line: &str) -> Result { + let line = line.trim_end_matches('\r').trim_end_matches('\n'); + if line.is_empty() { + return Err(ParseError::Empty); + } + + // Split into verb and the rest + let (verb, rest) = split_first_word(line); + let verb_upper = verb.to_ascii_uppercase(); + + match verb_upper.as_str() { + "EHLO" => { + let hostname = rest.trim(); + if hostname.is_empty() { + return Err(ParseError::MissingArgument("EHLO".into())); + } + Ok(SmtpCommand::Ehlo(hostname.to_string())) + } + "HELO" => { + let hostname = rest.trim(); + if hostname.is_empty() { + return Err(ParseError::MissingArgument("HELO".into())); + } + Ok(SmtpCommand::Helo(hostname.to_string())) + } + "MAIL" => parse_mail_from(rest), + "RCPT" => parse_rcpt_to(rest), + "DATA" => Ok(SmtpCommand::Data), + "RSET" => Ok(SmtpCommand::Rset), + "NOOP" => Ok(SmtpCommand::Noop), + "QUIT" => Ok(SmtpCommand::Quit), + "STARTTLS" => Ok(SmtpCommand::StartTls), + "AUTH" => parse_auth(rest), + "HELP" => { + let topic = rest.trim(); + if topic.is_empty() { + Ok(SmtpCommand::Help(None)) + } else { + Ok(SmtpCommand::Help(Some(topic.to_string()))) + } + } + "VRFY" => { + let arg = rest.trim(); + if arg.is_empty() { + return Err(ParseError::MissingArgument("VRFY".into())); + } + Ok(SmtpCommand::Vrfy(arg.to_string())) + } + "EXPN" => { + let arg = rest.trim(); + if arg.is_empty() { + return Err(ParseError::MissingArgument("EXPN".into())); + } + Ok(SmtpCommand::Expn(arg.to_string())) + } + _ => Err(ParseError::UnrecognizedCommand(verb_upper)), + } +} + +/// Parse `FROM: [PARAM=VALUE ...]` after "MAIL". +fn parse_mail_from(rest: &str) -> Result { + // Expect "FROM:" prefix (case-insensitive, whitespace-flexible) + let rest = rest.trim_start(); + let rest_upper = rest.to_ascii_uppercase(); + if !rest_upper.starts_with("FROM") { + return Err(ParseError::SyntaxError( + "expected FROM after MAIL".into(), + )); + } + let rest = &rest[4..]; // skip "FROM" + let rest = rest.trim_start(); + if !rest.starts_with(':') { + return Err(ParseError::SyntaxError( + "expected colon after MAIL FROM".into(), + )); + } + let rest = &rest[1..]; // skip ':' + let rest = rest.trim_start(); + + parse_address_and_params(rest, "MAIL FROM").map(|(address, params)| SmtpCommand::MailFrom { + address, + params, + }) +} + +/// Parse `TO: [PARAM=VALUE ...]` after "RCPT". +fn parse_rcpt_to(rest: &str) -> Result { + let rest = rest.trim_start(); + let rest_upper = rest.to_ascii_uppercase(); + if !rest_upper.starts_with("TO") { + return Err(ParseError::SyntaxError("expected TO after RCPT".into())); + } + let rest = &rest[2..]; // skip "TO" + let rest = rest.trim_start(); + if !rest.starts_with(':') { + return Err(ParseError::SyntaxError( + "expected colon after RCPT TO".into(), + )); + } + let rest = &rest[1..]; // skip ':' + let rest = rest.trim_start(); + + parse_address_and_params(rest, "RCPT TO").map(|(address, params)| SmtpCommand::RcptTo { + address, + params, + }) +} + +/// Parse `
[PARAM=VALUE ...]` from the rest of a MAIL FROM or RCPT TO line. +fn parse_address_and_params( + input: &str, + context: &str, +) -> Result<(String, HashMap>), ParseError> { + if !input.starts_with('<') { + return Err(ParseError::SyntaxError(format!( + "expected '<' in {context}" + ))); + } + let close_bracket = input.find('>').ok_or_else(|| { + ParseError::SyntaxError(format!("missing '>' in {context}")) + })?; + let address = input[1..close_bracket].to_string(); + let remainder = &input[close_bracket + 1..]; + let params = parse_params(remainder)?; + Ok((address, params)) +} + +/// Parse SMTP extension parameters like `SIZE=12345 BODY=8BITMIME`. +fn parse_params(input: &str) -> Result>, ParseError> { + let mut params = HashMap::new(); + for token in input.split_whitespace() { + if let Some(eq_pos) = token.find('=') { + let key = token[..eq_pos].to_ascii_uppercase(); + let value = token[eq_pos + 1..].to_string(); + params.insert(key, Some(value)); + } else { + params.insert(token.to_ascii_uppercase(), None); + } + } + Ok(params) +} + +/// Parse AUTH command: `AUTH [initial-response]`. +fn parse_auth(rest: &str) -> Result { + let rest = rest.trim(); + if rest.is_empty() { + return Err(ParseError::MissingArgument("AUTH".into())); + } + let (mech_str, initial) = split_first_word(rest); + let mechanism = match mech_str.to_ascii_uppercase().as_str() { + "PLAIN" => AuthMechanism::Plain, + "LOGIN" => AuthMechanism::Login, + other => { + return Err(ParseError::SyntaxError(format!( + "unsupported AUTH mechanism: {other}" + ))); + } + }; + let initial_response = { + let s = initial.trim(); + if s.is_empty() { None } else { Some(s.to_string()) } + }; + Ok(SmtpCommand::Auth { + mechanism, + initial_response, + }) +} + +/// Split a string into the first whitespace-delimited word and the remainder. +fn split_first_word(s: &str) -> (&str, &str) { + match s.find(char::is_whitespace) { + Some(pos) => (&s[..pos], &s[pos + 1..]), + None => (s, ""), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ehlo() { + let cmd = parse_command("EHLO mail.example.com").unwrap(); + assert_eq!(cmd, SmtpCommand::Ehlo("mail.example.com".into())); + } + + #[test] + fn test_ehlo_case_insensitive() { + let cmd = parse_command("ehlo MAIL.EXAMPLE.COM").unwrap(); + assert_eq!(cmd, SmtpCommand::Ehlo("MAIL.EXAMPLE.COM".into())); + } + + #[test] + fn test_helo() { + let cmd = parse_command("HELO example.com").unwrap(); + assert_eq!(cmd, SmtpCommand::Helo("example.com".into())); + } + + #[test] + fn test_ehlo_missing_arg() { + let err = parse_command("EHLO").unwrap_err(); + assert!(matches!(err, ParseError::MissingArgument(_))); + } + + #[test] + fn test_mail_from() { + let cmd = parse_command("MAIL FROM:").unwrap(); + assert_eq!( + cmd, + SmtpCommand::MailFrom { + address: "sender@example.com".into(), + params: HashMap::new(), + } + ); + } + + #[test] + fn test_mail_from_with_params() { + let cmd = parse_command("MAIL FROM: SIZE=12345 BODY=8BITMIME").unwrap(); + if let SmtpCommand::MailFrom { address, params } = cmd { + assert_eq!(address, "sender@example.com"); + assert_eq!(params.get("SIZE"), Some(&Some("12345".into()))); + assert_eq!(params.get("BODY"), Some(&Some("8BITMIME".into()))); + } else { + panic!("expected MailFrom"); + } + } + + #[test] + fn test_mail_from_empty_address() { + let cmd = parse_command("MAIL FROM:<>").unwrap(); + assert_eq!( + cmd, + SmtpCommand::MailFrom { + address: "".into(), + params: HashMap::new(), + } + ); + } + + #[test] + fn test_mail_from_flexible_spacing() { + let cmd = parse_command("MAIL FROM: ").unwrap(); + if let SmtpCommand::MailFrom { address, .. } = cmd { + assert_eq!(address, "user@example.com"); + } else { + panic!("expected MailFrom"); + } + } + + #[test] + fn test_rcpt_to() { + let cmd = parse_command("RCPT TO:").unwrap(); + assert_eq!( + cmd, + SmtpCommand::RcptTo { + address: "recipient@example.com".into(), + params: HashMap::new(), + } + ); + } + + #[test] + fn test_data() { + assert_eq!(parse_command("DATA").unwrap(), SmtpCommand::Data); + } + + #[test] + fn test_rset() { + assert_eq!(parse_command("RSET").unwrap(), SmtpCommand::Rset); + } + + #[test] + fn test_noop() { + assert_eq!(parse_command("NOOP").unwrap(), SmtpCommand::Noop); + } + + #[test] + fn test_quit() { + assert_eq!(parse_command("QUIT").unwrap(), SmtpCommand::Quit); + } + + #[test] + fn test_starttls() { + assert_eq!(parse_command("STARTTLS").unwrap(), SmtpCommand::StartTls); + } + + #[test] + fn test_auth_plain() { + let cmd = parse_command("AUTH PLAIN dGVzdAB0ZXN0AHBhc3N3b3Jk").unwrap(); + assert_eq!( + cmd, + SmtpCommand::Auth { + mechanism: AuthMechanism::Plain, + initial_response: Some("dGVzdAB0ZXN0AHBhc3N3b3Jk".into()), + } + ); + } + + #[test] + fn test_auth_login_no_initial() { + let cmd = parse_command("AUTH LOGIN").unwrap(); + assert_eq!( + cmd, + SmtpCommand::Auth { + mechanism: AuthMechanism::Login, + initial_response: None, + } + ); + } + + #[test] + fn test_help() { + assert_eq!(parse_command("HELP").unwrap(), SmtpCommand::Help(None)); + assert_eq!( + parse_command("HELP MAIL").unwrap(), + SmtpCommand::Help(Some("MAIL".into())) + ); + } + + #[test] + fn test_vrfy() { + assert_eq!( + parse_command("VRFY user@example.com").unwrap(), + SmtpCommand::Vrfy("user@example.com".into()) + ); + } + + #[test] + fn test_expn() { + assert_eq!( + parse_command("EXPN staff").unwrap(), + SmtpCommand::Expn("staff".into()) + ); + } + + #[test] + fn test_empty() { + assert!(matches!(parse_command(""), Err(ParseError::Empty))); + } + + #[test] + fn test_unrecognized() { + let err = parse_command("FOOBAR test").unwrap_err(); + assert!(matches!(err, ParseError::UnrecognizedCommand(_))); + } + + #[test] + fn test_crlf_stripped() { + let cmd = parse_command("QUIT\r\n").unwrap(); + assert_eq!(cmd, SmtpCommand::Quit); + } +} diff --git a/rust/crates/mailer-smtp/src/config.rs b/rust/crates/mailer-smtp/src/config.rs new file mode 100644 index 0000000..75d58a1 --- /dev/null +++ b/rust/crates/mailer-smtp/src/config.rs @@ -0,0 +1,86 @@ +//! SMTP server configuration. + +use serde::{Deserialize, Serialize}; + +/// Configuration for an SMTP server instance. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SmtpServerConfig { + /// Server hostname for greeting and EHLO responses. + pub hostname: String, + /// Ports to listen on (e.g. [25, 587]). + pub ports: Vec, + /// Port for implicit TLS (e.g. 465). None = no implicit TLS port. + pub secure_port: Option, + /// TLS certificate chain in PEM format. + pub tls_cert_pem: Option, + /// TLS private key in PEM format. + pub tls_key_pem: Option, + /// Maximum message size in bytes. + pub max_message_size: u64, + /// Maximum number of concurrent connections. + pub max_connections: u32, + /// Maximum recipients per message. + pub max_recipients: u32, + /// Connection timeout in seconds. + pub connection_timeout_secs: u64, + /// Data phase timeout in seconds. + pub data_timeout_secs: u64, + /// Whether authentication is available. + pub auth_enabled: bool, + /// Maximum authentication failures before disconnect. + pub max_auth_failures: u32, + /// Socket timeout in seconds (idle timeout for the entire connection). + pub socket_timeout_secs: u64, + /// Timeout in seconds waiting for TS to respond to email processing. + pub processing_timeout_secs: u64, +} + +impl Default for SmtpServerConfig { + fn default() -> Self { + Self { + hostname: "mail.example.com".to_string(), + ports: vec![25], + secure_port: None, + tls_cert_pem: None, + tls_key_pem: None, + max_message_size: 10 * 1024 * 1024, // 10 MB + max_connections: 100, + max_recipients: 100, + connection_timeout_secs: 30, + data_timeout_secs: 60, + auth_enabled: false, + max_auth_failures: 3, + socket_timeout_secs: 300, + processing_timeout_secs: 30, + } + } +} + +impl SmtpServerConfig { + /// Check if TLS is configured. + pub fn has_tls(&self) -> bool { + self.tls_cert_pem.is_some() && self.tls_key_pem.is_some() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_defaults() { + let cfg = SmtpServerConfig::default(); + assert_eq!(cfg.max_message_size, 10 * 1024 * 1024); + assert_eq!(cfg.max_connections, 100); + assert!(!cfg.has_tls()); + } + + #[test] + fn test_has_tls() { + let mut cfg = SmtpServerConfig::default(); + cfg.tls_cert_pem = Some("cert".into()); + assert!(!cfg.has_tls()); // need both + cfg.tls_key_pem = Some("key".into()); + assert!(cfg.has_tls()); + } +} diff --git a/rust/crates/mailer-smtp/src/connection.rs b/rust/crates/mailer-smtp/src/connection.rs new file mode 100644 index 0000000..af9ec33 --- /dev/null +++ b/rust/crates/mailer-smtp/src/connection.rs @@ -0,0 +1,1023 @@ +//! Per-connection SMTP handler. +//! +//! Manages the read/write loop for a single SMTP connection. +//! Dispatches parsed commands, handles DATA mode, and manages +//! authentication flow. + +use crate::command::{parse_command, AuthMechanism, ParseError, SmtpCommand}; +use crate::config::SmtpServerConfig; +use crate::data::{DataAccumulator, DataAction}; +use crate::rate_limiter::RateLimiter; +use crate::response::{build_capabilities, SmtpResponse}; +use crate::session::{AuthState, SmtpSession}; +use crate::validation; + +use base64::Engine; +use base64::engine::general_purpose::STANDARD as BASE64; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::net::TcpStream; +use tokio::sync::{mpsc, oneshot}; +use tokio::time::{timeout, Duration}; +use tokio_rustls::server::TlsStream; +use tracing::{debug, info, warn}; + +/// Events emitted by a connection handler to the server. +#[derive(Debug, Serialize, Deserialize)] +pub enum ConnectionEvent { + /// A complete email has been received and needs processing. + EmailReceived { + correlation_id: String, + session_id: String, + mail_from: String, + rcpt_to: Vec, + /// Base64-encoded raw message for inline, or file path for large messages. + data: EmailData, + remote_addr: String, + client_hostname: Option, + secure: bool, + authenticated_user: Option, + /// In-process security results (DKIM, SPF, DMARC, content scan). + security_results: Option, + }, + /// An authentication request that needs TS validation. + AuthRequest { + correlation_id: String, + session_id: String, + username: String, + password: String, + remote_addr: String, + }, +} + +/// How email data is transported from Rust to TS. +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum EmailData { + /// Inline base64-encoded data (for messages <= 256KB). + #[serde(rename = "inline")] + Inline { base64: String }, + /// Path to a temp file containing the raw message (for large messages). + #[serde(rename = "file")] + File { path: String }, +} + +/// Result of TS processing an email. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EmailProcessingResult { + pub accepted: bool, + pub smtp_code: Option, + pub smtp_message: Option, +} + +/// Result of TS processing an auth request. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuthResult { + pub success: bool, + pub message: Option, +} + +/// Abstraction over plain and TLS streams. +pub enum SmtpStream { + Plain(BufReader), + Tls(BufReader>), +} + +impl SmtpStream { + /// Read a line from the stream (up to max_line_length bytes). + pub async fn read_line(&mut self, buf: &mut String, max_len: usize) -> std::io::Result { + match self { + SmtpStream::Plain(reader) => { + let result = reader.read_line(buf).await?; + if buf.len() > max_len { + buf.truncate(max_len); + } + Ok(result) + } + SmtpStream::Tls(reader) => { + let result = reader.read_line(buf).await?; + if buf.len() > max_len { + buf.truncate(max_len); + } + Ok(result) + } + } + } + + /// Read bytes from the stream into a buffer. + pub async fn read_chunk(&mut self, buf: &mut [u8]) -> std::io::Result { + use tokio::io::AsyncReadExt; + match self { + SmtpStream::Plain(reader) => reader.read(buf).await, + SmtpStream::Tls(reader) => reader.read(buf).await, + } + } + + /// Write bytes to the stream. + pub async fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> { + match self { + SmtpStream::Plain(reader) => reader.get_mut().write_all(buf).await, + SmtpStream::Tls(reader) => reader.get_mut().write_all(buf).await, + } + } + + /// Flush the write buffer. + pub async fn flush(&mut self) -> std::io::Result<()> { + match self { + SmtpStream::Plain(reader) => reader.get_mut().flush().await, + SmtpStream::Tls(reader) => reader.get_mut().flush().await, + } + } + + /// Unwrap to get the raw TcpStream for STARTTLS upgrade. + /// Only works on Plain streams. + pub fn into_tcp_stream(self) -> Option { + match self { + SmtpStream::Plain(reader) => Some(reader.into_inner()), + SmtpStream::Tls(_) => None, + } + } +} + +/// Handle a single SMTP connection. +/// +/// This is the main entry point spawned for each incoming connection. +pub async fn handle_connection( + mut stream: SmtpStream, + config: Arc, + rate_limiter: Arc, + event_tx: mpsc::Sender, + callback_register: Arc, + tls_acceptor: Option>, + remote_addr: String, + is_secure: bool, +) { + let mut session = SmtpSession::new(remote_addr.clone(), is_secure); + + // Check IP rate limit + if !rate_limiter.check_connection(&remote_addr) { + let resp = SmtpResponse::service_unavailable( + &config.hostname, + "Too many connections from your IP", + ); + let _ = stream.write_all(&resp.to_bytes()).await; + let _ = stream.flush().await; + info!( + session_id = %session.id, + remote_addr = %remote_addr, + "Connection rejected: rate limit exceeded" + ); + return; + } + + // Send greeting + let greeting = SmtpResponse::greeting(&config.hostname); + if stream.write_all(&greeting.to_bytes()).await.is_err() { + return; + } + if stream.flush().await.is_err() { + return; + } + + let socket_timeout = Duration::from_secs(config.socket_timeout_secs); + + loop { + let mut line = String::new(); + let read_result = timeout(socket_timeout, stream.read_line(&mut line, 4096)).await; + + match read_result { + Err(_) => { + // Timeout + let resp = SmtpResponse::service_unavailable( + &config.hostname, + "Connection timed out", + ); + let _ = stream.write_all(&resp.to_bytes()).await; + let _ = stream.flush().await; + debug!(session_id = %session.id, "Connection timed out"); + break; + } + Ok(Err(e)) => { + debug!(session_id = %session.id, error = %e, "Read error"); + break; + } + Ok(Ok(0)) => { + debug!(session_id = %session.id, "Client disconnected"); + break; + } + Ok(Ok(_)) => { + // Process command + let response = process_line( + &line, + &mut session, + &mut stream, + &config, + &rate_limiter, + &event_tx, + callback_register.as_ref(), + &tls_acceptor, + ) + .await; + + match response { + LineResult::Response(resp) => { + if stream.write_all(&resp.to_bytes()).await.is_err() { + break; + } + if stream.flush().await.is_err() { + break; + } + } + LineResult::Quit(resp) => { + let _ = stream.write_all(&resp.to_bytes()).await; + let _ = stream.flush().await; + break; + } + LineResult::StartTlsSignal => { + // Send 220 Ready response + let resp = SmtpResponse::new(220, "Ready to start TLS"); + if stream.write_all(&resp.to_bytes()).await.is_err() { + break; + } + if stream.flush().await.is_err() { + break; + } + // Extract TCP stream and upgrade + if let Some(tcp_stream) = stream.into_tcp_stream() { + if let Some(acceptor) = &tls_acceptor { + match acceptor.accept(tcp_stream).await { + Ok(tls_stream) => { + stream = SmtpStream::Tls(BufReader::new(tls_stream)); + session.secure = true; + // Client must re-EHLO after STARTTLS + session.state = crate::state::SmtpState::Connected; + session.client_hostname = None; + session.esmtp = false; + session.auth_state = AuthState::None; + session.envelope = Default::default(); + debug!(session_id = %session.id, "TLS upgrade successful"); + } + Err(e) => { + warn!(session_id = %session.id, error = %e, "TLS handshake failed"); + break; + } + } + } else { + break; + } + } else { + // Already TLS — shouldn't happen + break; + } + } + LineResult::NoResponse => {} + LineResult::Disconnect => { + break; + } + } + } + } + } + + info!( + session_id = %session.id, + remote_addr = %remote_addr, + messages = session.message_count, + "Connection closed" + ); +} + +/// Result of processing a single input line. +enum LineResult { + /// Send this response to the client. + Response(SmtpResponse), + /// Send this response then close the connection. + Quit(SmtpResponse), + /// Signal that STARTTLS should be performed (main loop sends 220 and upgrades). + StartTlsSignal, + /// No response needed (handled internally). + NoResponse, + /// Disconnect immediately. + Disconnect, +} + +/// Trait for registering and resolving correlation-ID callbacks. +pub trait CallbackRegistry: Send + Sync { + /// Register a callback for email processing and return a receiver. + fn register_email_callback( + &self, + correlation_id: &str, + ) -> oneshot::Receiver; + + /// Register a callback for auth and return a receiver. + fn register_auth_callback( + &self, + correlation_id: &str, + ) -> oneshot::Receiver; +} + +/// Process a single input line from the client. +async fn process_line( + line: &str, + session: &mut SmtpSession, + stream: &mut SmtpStream, + config: &SmtpServerConfig, + rate_limiter: &RateLimiter, + event_tx: &mpsc::Sender, + callback_registry: &dyn CallbackRegistry, + tls_acceptor: &Option>, +) -> LineResult { + // Handle AUTH intermediate states (waiting for username/password) + match &session.auth_state { + AuthState::WaitingForUsername => { + return handle_auth_username(line.trim(), session); + } + AuthState::WaitingForPassword { .. } => { + return handle_auth_password( + line.trim(), + session, + config, + rate_limiter, + event_tx, + callback_registry, + ) + .await; + } + _ => {} + } + + // Parse SMTP command + let cmd = match parse_command(line) { + Ok(cmd) => cmd, + Err(ParseError::Empty) => return LineResult::NoResponse, + Err(_) => { + if session.record_invalid_command() { + return LineResult::Quit(SmtpResponse::service_unavailable( + &config.hostname, + "Too many invalid commands", + )); + } + return LineResult::Response(SmtpResponse::syntax_error()); + } + }; + + match cmd { + SmtpCommand::Ehlo(hostname) => handle_ehlo(hostname, true, session, config), + SmtpCommand::Helo(hostname) => handle_ehlo(hostname, false, session, config), + + SmtpCommand::MailFrom { address, params } => { + handle_mail_from(address, params, session, config, rate_limiter) + } + + SmtpCommand::RcptTo { address, params } => { + handle_rcpt_to(address, params, session, config) + } + + SmtpCommand::Data => { + handle_data(session, stream, config, event_tx, callback_registry).await + } + + SmtpCommand::Rset => { + session.reset_transaction(); + LineResult::Response(SmtpResponse::ok("OK")) + } + + SmtpCommand::Noop => LineResult::Response(SmtpResponse::ok("OK")), + + SmtpCommand::Quit => { + LineResult::Quit(SmtpResponse::closing(&config.hostname)) + } + + SmtpCommand::StartTls => { + handle_starttls(session, config, tls_acceptor) + } + + SmtpCommand::Auth { + mechanism, + initial_response, + } => { + handle_auth( + mechanism, + initial_response, + session, + config, + rate_limiter, + event_tx, + callback_registry, + ) + .await + } + + SmtpCommand::Help(_) => { + LineResult::Response(SmtpResponse::new( + 214, + "EHLO HELO MAIL RCPT DATA RSET NOOP QUIT STARTTLS AUTH HELP VRFY", + )) + } + + SmtpCommand::Vrfy(_) => { + LineResult::Response(SmtpResponse::new(252, "Cannot VRFY user")) + } + + SmtpCommand::Expn(_) => { + LineResult::Response(SmtpResponse::not_implemented()) + } + } +} + +/// Handle EHLO/HELO command. +fn handle_ehlo( + hostname: String, + esmtp: bool, + session: &mut SmtpSession, + config: &SmtpServerConfig, +) -> LineResult { + if !validation::is_valid_ehlo_hostname(&hostname) { + return LineResult::Response(SmtpResponse::param_error( + "Invalid hostname", + )); + } + + session.reset_for_ehlo(hostname, esmtp); + + if esmtp { + let caps = build_capabilities( + config.max_message_size, + config.has_tls(), + session.secure, + config.auth_enabled, + ); + LineResult::Response(SmtpResponse::ehlo_response(&config.hostname, &caps)) + } else { + LineResult::Response(SmtpResponse::ok(format!( + "{} Hello", + config.hostname + ))) + } +} + +/// Handle MAIL FROM command. +fn handle_mail_from( + address: String, + params: std::collections::HashMap>, + session: &mut SmtpSession, + config: &SmtpServerConfig, + rate_limiter: &RateLimiter, +) -> LineResult { + if !session.state.can_mail_from() { + return LineResult::Response(SmtpResponse::bad_sequence( + "Send EHLO/HELO first", + )); + } + + if !validation::is_valid_smtp_address(&address) { + return LineResult::Response(SmtpResponse::param_error( + "Invalid sender address", + )); + } + + // Check SIZE param + if let Some(Some(size_str)) = params.get("SIZE") { + match validation::validate_size_param(size_str, config.max_message_size) { + Ok(_) => {} + Err(msg) => return LineResult::Response(SmtpResponse::new(552, msg)), + } + } + + // Rate limit check for sender + if !address.is_empty() && !rate_limiter.check_message(&address) { + return LineResult::Response(SmtpResponse::temp_failure( + "Too many messages from this sender, try again later", + )); + } + + session.envelope.mail_from = address; + session.envelope.declared_size = params + .get("SIZE") + .and_then(|v| v.as_ref()) + .and_then(|s| s.parse().ok()); + session.envelope.body_type = params + .get("BODY") + .and_then(|v| v.clone()); + + match session.state.transition_mail_from() { + Ok(new_state) => { + session.state = new_state; + LineResult::Response(SmtpResponse::ok("OK")) + } + Err(_) => LineResult::Response(SmtpResponse::bad_sequence( + "Bad sequence of commands", + )), + } +} + +/// Handle RCPT TO command. +fn handle_rcpt_to( + address: String, + _params: std::collections::HashMap>, + session: &mut SmtpSession, + config: &SmtpServerConfig, +) -> LineResult { + if !session.state.can_rcpt_to() { + return LineResult::Response(SmtpResponse::bad_sequence( + "Send MAIL FROM first", + )); + } + + if !validation::is_valid_smtp_address(&address) || address.is_empty() { + return LineResult::Response(SmtpResponse::param_error( + "Invalid recipient address", + )); + } + + if session.envelope.rcpt_to.len() >= config.max_recipients as usize { + return LineResult::Response(SmtpResponse::new( + 452, + "Too many recipients", + )); + } + + session.envelope.rcpt_to.push(address); + + match session.state.transition_rcpt_to() { + Ok(new_state) => { + session.state = new_state; + LineResult::Response(SmtpResponse::ok("OK")) + } + Err(_) => LineResult::Response(SmtpResponse::bad_sequence( + "Bad sequence of commands", + )), + } +} + +/// Handle DATA command: switch to data mode, accumulate, then emit event. +async fn handle_data( + session: &mut SmtpSession, + stream: &mut SmtpStream, + config: &SmtpServerConfig, + event_tx: &mpsc::Sender, + callback_registry: &dyn CallbackRegistry, +) -> LineResult { + if !session.state.can_data() { + return LineResult::Response(SmtpResponse::bad_sequence( + "Send RCPT TO first", + )); + } + + // Transition to DATA state + session.state = match session.state.transition_data() { + Ok(s) => s, + Err(_) => { + return LineResult::Response(SmtpResponse::bad_sequence( + "Bad sequence of commands", + )); + } + }; + + // Send 354 + let start_resp = SmtpResponse::start_data(); + if stream.write_all(&start_resp.to_bytes()).await.is_err() { + return LineResult::Disconnect; + } + if stream.flush().await.is_err() { + return LineResult::Disconnect; + } + + // Accumulate data + let mut accumulator = DataAccumulator::new(config.max_message_size); + let data_timeout = Duration::from_secs(config.data_timeout_secs); + let mut buf = [0u8; 8192]; + + loop { + let read_result = timeout(data_timeout, stream.read_chunk(&mut buf)).await; + match read_result { + Err(_) => { + // Data timeout + return LineResult::Quit(SmtpResponse::service_unavailable( + &config.hostname, + "Data timeout", + )); + } + Ok(Err(_)) => return LineResult::Disconnect, + Ok(Ok(0)) => return LineResult::Disconnect, + Ok(Ok(n)) => { + match accumulator.process_chunk(&buf[..n]) { + DataAction::Continue => continue, + DataAction::SizeExceeded => { + // Must still read until end-of-data to stay in sync + session.state = crate::state::SmtpState::Greeted; + session.envelope = Default::default(); + return LineResult::Response(SmtpResponse::size_exceeded( + config.max_message_size, + )); + } + DataAction::Complete => break, + } + } + } + } + + // Data complete — prepare for delivery + let raw_message = accumulator.into_message().unwrap_or_default(); + let correlation_id = uuid::Uuid::new_v4().to_string(); + + // Determine transport: inline base64 or temp file + let email_data = if raw_message.len() <= 256 * 1024 { + EmailData::Inline { + base64: BASE64.encode(&raw_message), + } + } else { + // Write to temp file + let tmp_path = format!("/tmp/mailer-smtp-{}.eml", &correlation_id); + match tokio::fs::write(&tmp_path, &raw_message).await { + Ok(_) => EmailData::File { path: tmp_path }, + Err(e) => { + warn!(error = %e, "Failed to write temp file for large email"); + // Fall back to inline + EmailData::Inline { + base64: BASE64.encode(&raw_message), + } + } + } + }; + + // Register callback before sending event + let rx = callback_registry.register_email_callback(&correlation_id); + + // Send event to TS + let event = ConnectionEvent::EmailReceived { + correlation_id: correlation_id.clone(), + session_id: session.id.clone(), + mail_from: session.envelope.mail_from.clone(), + rcpt_to: session.envelope.rcpt_to.clone(), + data: email_data, + remote_addr: session.remote_addr.clone(), + client_hostname: session.client_hostname.clone(), + secure: session.secure, + authenticated_user: session.authenticated_user().map(|s| s.to_string()), + security_results: None, // Will be populated by server.rs when in-process security is added + }; + + if event_tx.send(event).await.is_err() { + warn!("Failed to send emailReceived event"); + return LineResult::Response(SmtpResponse::local_error( + "Internal processing error", + )); + } + + // Wait for TS response with timeout + let processing_timeout = Duration::from_secs(config.processing_timeout_secs); + let result = match timeout(processing_timeout, rx).await { + Ok(Ok(result)) => result, + Ok(Err(_)) => { + warn!(correlation_id = %correlation_id, "Callback channel dropped"); + EmailProcessingResult { + accepted: false, + smtp_code: Some(451), + smtp_message: Some("Processing error".into()), + } + } + Err(_) => { + warn!(correlation_id = %correlation_id, "Processing timeout"); + EmailProcessingResult { + accepted: false, + smtp_code: Some(451), + smtp_message: Some("Processing timeout".into()), + } + } + }; + + // Reset transaction state + session.envelope = Default::default(); + let _ = session.state.transition_finished(); + session.state = crate::state::SmtpState::Finished; + session.record_message(); + + if result.accepted { + LineResult::Response(SmtpResponse::ok( + result.smtp_message.unwrap_or_else(|| "Message accepted".into()), + )) + } else { + let code = result.smtp_code.unwrap_or(550); + let msg = result + .smtp_message + .unwrap_or_else(|| "Message rejected".into()); + LineResult::Response(SmtpResponse::new(code, msg)) + } +} + +/// Handle STARTTLS command. +/// +/// Returns `StartTlsSignal` to indicate the main loop should send 220 and +/// perform the TLS upgrade. The main loop handles the stream swap. +fn handle_starttls( + session: &SmtpSession, + config: &SmtpServerConfig, + tls_acceptor: &Option>, +) -> LineResult { + if session.secure { + return LineResult::Response(SmtpResponse::bad_sequence( + "Already using TLS", + )); + } + + if !session.state.can_starttls() { + return LineResult::Response(SmtpResponse::bad_sequence( + "STARTTLS not allowed in current state", + )); + } + + if tls_acceptor.is_none() || !config.has_tls() { + return LineResult::Response(SmtpResponse::new( + 454, + "TLS not available", + )); + } + + // Signal the main loop to perform TLS upgrade. + // The main loop will: send 220, extract TCP stream, do TLS handshake. + LineResult::StartTlsSignal +} + +/// Handle AUTH command. +async fn handle_auth( + mechanism: AuthMechanism, + initial_response: Option, + session: &mut SmtpSession, + config: &SmtpServerConfig, + rate_limiter: &RateLimiter, + event_tx: &mpsc::Sender, + callback_registry: &dyn CallbackRegistry, +) -> LineResult { + if !config.auth_enabled { + return LineResult::Response(SmtpResponse::not_implemented()); + } + + if session.is_authenticated() { + return LineResult::Response(SmtpResponse::bad_sequence( + "Already authenticated", + )); + } + + if !session.state.can_auth() { + return LineResult::Response(SmtpResponse::bad_sequence( + "Send EHLO first", + )); + } + + match mechanism { + AuthMechanism::Plain => { + if let Some(response) = initial_response { + // Decode and validate immediately + return process_auth_plain( + &response, + session, + config, + rate_limiter, + event_tx, + callback_registry, + ) + .await; + } + // No initial response — send challenge for credentials + session.auth_state = AuthState::WaitingForUsername; + // For PLAIN, we use an empty challenge + LineResult::Response(SmtpResponse::auth_challenge("")) + } + AuthMechanism::Login => { + if let Some(response) = initial_response { + // The initial response is the username (base64) + match BASE64.decode(response.as_bytes()) { + Ok(decoded) => { + let username = String::from_utf8_lossy(&decoded).to_string(); + session.auth_state = AuthState::WaitingForPassword { username }; + // Send password prompt (base64 of "Password:") + LineResult::Response(SmtpResponse::auth_challenge( + &BASE64.encode(b"Password:"), + )) + } + Err(_) => LineResult::Response(SmtpResponse::param_error( + "Invalid base64 encoding", + )), + } + } else { + session.auth_state = AuthState::WaitingForUsername; + // Send username prompt (base64 of "Username:") + LineResult::Response(SmtpResponse::auth_challenge( + &BASE64.encode(b"Username:"), + )) + } + } + } +} + +/// Handle username input during LOGIN auth flow. +fn handle_auth_username(line: &str, session: &mut SmtpSession) -> LineResult { + // Cancel auth if client sends "*" + if line == "*" { + session.auth_state = AuthState::None; + return LineResult::Response(SmtpResponse::new(501, "Authentication cancelled")); + } + + match BASE64.decode(line.as_bytes()) { + Ok(decoded) => { + let username = String::from_utf8_lossy(&decoded).to_string(); + session.auth_state = AuthState::WaitingForPassword { username }; + LineResult::Response(SmtpResponse::auth_challenge( + &BASE64.encode(b"Password:"), + )) + } + Err(_) => { + session.auth_state = AuthState::None; + LineResult::Response(SmtpResponse::param_error( + "Invalid base64 encoding", + )) + } + } +} + +/// Handle password input during LOGIN auth flow. +async fn handle_auth_password( + line: &str, + session: &mut SmtpSession, + config: &SmtpServerConfig, + rate_limiter: &RateLimiter, + event_tx: &mpsc::Sender, + callback_registry: &dyn CallbackRegistry, +) -> LineResult { + // Cancel auth if client sends "*" + if line == "*" { + session.auth_state = AuthState::None; + return LineResult::Response(SmtpResponse::new(501, "Authentication cancelled")); + } + + let username = match &session.auth_state { + AuthState::WaitingForPassword { username } => username.clone(), + _ => { + session.auth_state = AuthState::None; + return LineResult::Response(SmtpResponse::bad_sequence("Unexpected auth state")); + } + }; + + let password = match BASE64.decode(line.as_bytes()) { + Ok(decoded) => String::from_utf8_lossy(&decoded).to_string(), + Err(_) => { + session.auth_state = AuthState::None; + return LineResult::Response(SmtpResponse::param_error( + "Invalid base64 encoding", + )); + } + }; + + validate_credentials( + &username, + &password, + session, + config, + rate_limiter, + event_tx, + callback_registry, + ) + .await +} + +/// Process AUTH PLAIN credentials (base64-encoded "\0username\0password"). +async fn process_auth_plain( + base64_data: &str, + session: &mut SmtpSession, + config: &SmtpServerConfig, + rate_limiter: &RateLimiter, + event_tx: &mpsc::Sender, + callback_registry: &dyn CallbackRegistry, +) -> LineResult { + let decoded = match BASE64.decode(base64_data.as_bytes()) { + Ok(d) => d, + Err(_) => { + return LineResult::Response(SmtpResponse::param_error( + "Invalid base64 encoding", + )); + } + }; + + // PLAIN format: \0username\0password + let parts: Vec<&[u8]> = decoded.splitn(3, |&b| b == 0).collect(); + if parts.len() < 3 { + return LineResult::Response(SmtpResponse::param_error( + "Invalid PLAIN auth format", + )); + } + + let username = String::from_utf8_lossy(parts[1]).to_string(); + let password = String::from_utf8_lossy(parts[2]).to_string(); + + validate_credentials( + &username, + &password, + session, + config, + rate_limiter, + event_tx, + callback_registry, + ) + .await +} + +/// Validate credentials by sending authRequest to TS and waiting for response. +async fn validate_credentials( + username: &str, + password: &str, + session: &mut SmtpSession, + config: &SmtpServerConfig, + rate_limiter: &RateLimiter, + event_tx: &mpsc::Sender, + callback_registry: &dyn CallbackRegistry, +) -> LineResult { + let correlation_id = uuid::Uuid::new_v4().to_string(); + + // Register callback before sending event + let rx = callback_registry.register_auth_callback(&correlation_id); + + let event = ConnectionEvent::AuthRequest { + correlation_id: correlation_id.clone(), + session_id: session.id.clone(), + username: username.to_string(), + password: password.to_string(), + remote_addr: session.remote_addr.clone(), + }; + + if event_tx.send(event).await.is_err() { + session.auth_state = AuthState::None; + return LineResult::Response(SmtpResponse::local_error( + "Internal processing error", + )); + } + + // Wait for TS response + let auth_timeout = Duration::from_secs(5); + let result = match timeout(auth_timeout, rx).await { + Ok(Ok(result)) => result, + Ok(Err(_)) => AuthResult { + success: false, + message: Some("Auth processing error".into()), + }, + Err(_) => AuthResult { + success: false, + message: Some("Auth timeout".into()), + }, + }; + + if result.success { + session.auth_state = AuthState::Authenticated { + username: username.to_string(), + }; + LineResult::Response(SmtpResponse::auth_success()) + } else { + session.auth_state = AuthState::None; + let exceeded = session.record_auth_failure(config.max_auth_failures); + if exceeded { + if !rate_limiter.check_auth_failure(&session.remote_addr) { + return LineResult::Quit(SmtpResponse::service_unavailable( + &config.hostname, + "Too many authentication failures", + )); + } + return LineResult::Quit(SmtpResponse::service_unavailable( + &config.hostname, + "Too many authentication failures", + )); + } + LineResult::Response(SmtpResponse::auth_failed()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_email_data_serialization() { + let data = EmailData::Inline { + base64: "dGVzdA==".into(), + }; + let json = serde_json::to_string(&data).unwrap(); + assert!(json.contains("inline")); + + let data = EmailData::File { + path: "/tmp/test.eml".into(), + }; + let json = serde_json::to_string(&data).unwrap(); + assert!(json.contains("file")); + } + + #[test] + fn test_processing_result_serialization() { + let result = EmailProcessingResult { + accepted: true, + smtp_code: Some(250), + smtp_message: Some("OK".into()), + }; + let json = serde_json::to_string(&result).unwrap(); + assert!(json.contains("accepted")); + } +} diff --git a/rust/crates/mailer-smtp/src/data.rs b/rust/crates/mailer-smtp/src/data.rs new file mode 100644 index 0000000..fe68313 --- /dev/null +++ b/rust/crates/mailer-smtp/src/data.rs @@ -0,0 +1,289 @@ +//! Email DATA phase processor. +//! +//! Handles dot-unstuffing, end-of-data detection, size enforcement, +//! and streaming accumulation of email data. + +/// Result of processing a chunk of DATA input. +#[derive(Debug, Clone, PartialEq)] +pub enum DataAction { + /// More data needed — continue accumulating. + Continue, + /// End-of-data detected. The complete message body is ready. + Complete, + /// Message size limit exceeded. + SizeExceeded, +} + +/// Streaming email data accumulator. +/// +/// Processes incoming bytes from the DATA phase, handling: +/// - CRLF line ending normalization +/// - Dot-unstuffing (RFC 5321 §4.5.2) +/// - End-of-data marker detection (`.`) +/// - Size enforcement +pub struct DataAccumulator { + /// Accumulated message bytes. + buffer: Vec, + /// Maximum allowed size in bytes. 0 = unlimited. + max_size: u64, + /// Whether we've detected end-of-data. + complete: bool, + /// Whether the current position is at the start of a line. + at_line_start: bool, + /// Partial state for cross-chunk boundary handling. + partial: PartialState, +} + +/// Tracks partial sequences that span chunk boundaries. +#[derive(Debug, Clone, Copy, PartialEq)] +enum PartialState { + /// No partial sequence. + None, + /// Saw `\r`, waiting for `\n`. + Cr, + /// At line start, saw `.`, waiting to determine dot-stuffing vs end-of-data. + Dot, + /// At line start, saw `.\r`, waiting for `\n` (end-of-data) or other. + DotCr, +} + +impl DataAccumulator { + /// Create a new accumulator with the given size limit. + pub fn new(max_size: u64) -> Self { + Self { + buffer: Vec::with_capacity(8192), + max_size, + complete: false, + at_line_start: true, // First byte is at start of first line + partial: PartialState::None, + } + } + + /// Process a chunk of incoming data. + /// + /// Returns the action to take: continue, complete, or size exceeded. + pub fn process_chunk(&mut self, chunk: &[u8]) -> DataAction { + if self.complete { + return DataAction::Complete; + } + + for &byte in chunk { + match self.partial { + PartialState::None => { + if self.at_line_start && byte == b'.' { + self.partial = PartialState::Dot; + } else if byte == b'\r' { + self.partial = PartialState::Cr; + } else { + self.buffer.push(byte); + self.at_line_start = false; + } + } + PartialState::Cr => { + if byte == b'\n' { + self.buffer.extend_from_slice(b"\r\n"); + self.at_line_start = true; + self.partial = PartialState::None; + } else { + // Bare CR — emit it and process current byte + self.buffer.push(b'\r'); + self.at_line_start = false; + self.partial = PartialState::None; + // Re-process current byte + if byte == b'\r' { + self.partial = PartialState::Cr; + } else { + self.buffer.push(byte); + } + } + } + PartialState::Dot => { + if byte == b'\r' { + self.partial = PartialState::DotCr; + } else if byte == b'.' { + // Dot-unstuffing: \r\n.. → \r\n. + // Emit one dot, consume the other + self.buffer.push(b'.'); + self.at_line_start = false; + self.partial = PartialState::None; + } else { + // Dot at line start but not stuffing or end-of-data + self.buffer.push(b'.'); + self.buffer.push(byte); + self.at_line_start = false; + self.partial = PartialState::None; + } + } + PartialState::DotCr => { + if byte == b'\n' { + // End-of-data: . + // Remove the trailing \r\n from the buffer + // (it was part of the terminator, not the message) + if self.buffer.ends_with(b"\r\n") { + let new_len = self.buffer.len() - 2; + self.buffer.truncate(new_len); + } + self.complete = true; + return DataAction::Complete; + } else { + // Not end-of-data — emit .\r and process current byte + self.buffer.push(b'.'); + self.buffer.push(b'\r'); + self.at_line_start = false; + self.partial = PartialState::None; + // Re-process current byte + if byte == b'\r' { + self.partial = PartialState::Cr; + } else { + self.buffer.push(byte); + } + } + } + } + + // Check size limit + if self.max_size > 0 && self.buffer.len() as u64 > self.max_size { + return DataAction::SizeExceeded; + } + } + + DataAction::Continue + } + + /// Consume the accumulator and return the complete message data. + /// + /// Returns `None` if end-of-data has not been detected. + pub fn into_message(self) -> Option> { + if !self.complete { + return None; + } + Some(self.buffer) + } + + /// Get a reference to the accumulated data so far. + pub fn data(&self) -> &[u8] { + &self.buffer + } + + /// Get the current accumulated size. + pub fn size(&self) -> usize { + self.buffer.len() + } + + /// Whether end-of-data has been detected. + pub fn is_complete(&self) -> bool { + self.complete + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_simple_message() { + let mut acc = DataAccumulator::new(0); + let data = b"Subject: Test\r\n\r\nHello world\r\n.\r\n"; + let action = acc.process_chunk(data); + assert_eq!(action, DataAction::Complete); + let msg = acc.into_message().unwrap(); + assert_eq!(msg, b"Subject: Test\r\n\r\nHello world"); + } + + #[test] + fn test_dot_unstuffing() { + let mut acc = DataAccumulator::new(0); + // A line starting with ".." should become "." + let data = b"Line 1\r\n..dot-stuffed\r\n.\r\n"; + let action = acc.process_chunk(data); + assert_eq!(action, DataAction::Complete); + let msg = acc.into_message().unwrap(); + assert_eq!(msg, b"Line 1\r\n.dot-stuffed"); + } + + #[test] + fn test_multiple_chunks() { + let mut acc = DataAccumulator::new(0); + assert_eq!(acc.process_chunk(b"Subject: Test\r\n"), DataAction::Continue); + assert_eq!(acc.process_chunk(b"\r\nBody line 1\r\n"), DataAction::Continue); + assert_eq!(acc.process_chunk(b"Body line 2\r\n.\r\n"), DataAction::Complete); + let msg = acc.into_message().unwrap(); + assert_eq!(msg, b"Subject: Test\r\n\r\nBody line 1\r\nBody line 2"); + } + + #[test] + fn test_end_of_data_spanning_chunks() { + let mut acc = DataAccumulator::new(0); + assert_eq!(acc.process_chunk(b"Body\r\n"), DataAction::Continue); + assert_eq!(acc.process_chunk(b".\r"), DataAction::Continue); + assert_eq!(acc.process_chunk(b"\n"), DataAction::Complete); + let msg = acc.into_message().unwrap(); + assert_eq!(msg, b"Body"); + } + + #[test] + fn test_size_limit() { + let mut acc = DataAccumulator::new(10); + let data = b"This is definitely more than 10 bytes\r\n.\r\n"; + let action = acc.process_chunk(data); + assert_eq!(action, DataAction::SizeExceeded); + } + + #[test] + fn test_not_complete() { + let mut acc = DataAccumulator::new(0); + acc.process_chunk(b"partial data"); + assert!(!acc.is_complete()); + assert!(acc.into_message().is_none()); + } + + #[test] + fn test_empty_message() { + let mut acc = DataAccumulator::new(0); + let action = acc.process_chunk(b".\r\n"); + assert_eq!(action, DataAction::Complete); + let msg = acc.into_message().unwrap(); + assert!(msg.is_empty()); + } + + #[test] + fn test_dot_not_at_line_start() { + let mut acc = DataAccumulator::new(0); + let data = b"Hello.World\r\n.\r\n"; + let action = acc.process_chunk(data); + assert_eq!(action, DataAction::Complete); + let msg = acc.into_message().unwrap(); + assert_eq!(msg, b"Hello.World"); + } + + #[test] + fn test_multiple_dots_in_line() { + let mut acc = DataAccumulator::new(0); + let data = b"...\r\n.\r\n"; + let action = acc.process_chunk(data); + assert_eq!(action, DataAction::Complete); + // First dot at line start is dot-unstuffed, leaving ".." + let msg = acc.into_message().unwrap(); + assert_eq!(msg, b".."); + } + + #[test] + fn test_crlf_dot_spanning_three_chunks() { + let mut acc = DataAccumulator::new(0); + assert_eq!(acc.process_chunk(b"Body\r"), DataAction::Continue); + assert_eq!(acc.process_chunk(b"\n."), DataAction::Continue); + assert_eq!(acc.process_chunk(b"\r\n"), DataAction::Complete); + let msg = acc.into_message().unwrap(); + assert_eq!(msg, b"Body"); + } + + #[test] + fn test_bare_cr() { + let mut acc = DataAccumulator::new(0); + let data = b"Hello\rWorld\r\n.\r\n"; + let action = acc.process_chunk(data); + assert_eq!(action, DataAction::Complete); + let msg = acc.into_message().unwrap(); + assert_eq!(msg, b"Hello\rWorld"); + } +} diff --git a/rust/crates/mailer-smtp/src/lib.rs b/rust/crates/mailer-smtp/src/lib.rs index 97b8e00..888580a 100644 --- a/rust/crates/mailer-smtp/src/lib.rs +++ b/rust/crates/mailer-smtp/src/lib.rs @@ -1,18 +1,39 @@ //! mailer-smtp: SMTP protocol engine (server + client). +//! +//! This crate provides the SMTP protocol implementation including: +//! - Command parsing (`command`) +//! - State machine (`state`) +//! - Response building (`response`) +//! - Email data accumulation (`data`) +//! - Per-connection session state (`session`) +//! - Address/input validation (`validation`) +//! - Server configuration (`config`) +//! - Rate limiting (`rate_limiter`) +//! - TCP/TLS server (`server`) +//! - Connection handling (`connection`) + +pub mod command; +pub mod config; +pub mod connection; +pub mod data; +pub mod rate_limiter; +pub mod response; +pub mod server; +pub mod session; +pub mod state; +pub mod validation; pub use mailer_core; -/// Placeholder for the SMTP server and client implementation. +// Re-export key types for convenience. +pub use command::{AuthMechanism, SmtpCommand}; +pub use config::SmtpServerConfig; +pub use data::{DataAccumulator, DataAction}; +pub use response::SmtpResponse; +pub use session::SmtpSession; +pub use state::SmtpState; + +/// Crate version. pub fn version() -> &'static str { env!("CARGO_PKG_VERSION") } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_version() { - assert!(!version().is_empty()); - } -} diff --git a/rust/crates/mailer-smtp/src/rate_limiter.rs b/rust/crates/mailer-smtp/src/rate_limiter.rs new file mode 100644 index 0000000..1bb7bad --- /dev/null +++ b/rust/crates/mailer-smtp/src/rate_limiter.rs @@ -0,0 +1,198 @@ +//! In-process SMTP rate limiter. +//! +//! Uses DashMap for lock-free concurrent access to rate counters. +//! Tracks connections per IP, messages per sender, and auth failures. + +use dashmap::DashMap; +use serde::{Deserialize, Serialize}; +use std::time::{Duration, Instant}; + +/// Rate limiter configuration. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RateLimitConfig { + /// Maximum connections per IP per window. + pub max_connections_per_ip: u32, + /// Maximum messages per sender per window. + pub max_messages_per_sender: u32, + /// Maximum auth failures per IP per window. + pub max_auth_failures_per_ip: u32, + /// Window duration in seconds. + pub window_secs: u64, +} + +impl Default for RateLimitConfig { + fn default() -> Self { + Self { + max_connections_per_ip: 50, + max_messages_per_sender: 100, + max_auth_failures_per_ip: 5, + window_secs: 60, + } + } +} + +/// A timestamped counter entry. +struct CounterEntry { + count: u32, + window_start: Instant, +} + +/// In-process rate limiter using DashMap. +pub struct RateLimiter { + config: RateLimitConfig, + window: Duration, + connections: DashMap, + messages: DashMap, + auth_failures: DashMap, +} + +impl RateLimiter { + /// Create a new rate limiter with the given configuration. + pub fn new(config: RateLimitConfig) -> Self { + let window = Duration::from_secs(config.window_secs); + Self { + config, + window, + connections: DashMap::new(), + messages: DashMap::new(), + auth_failures: DashMap::new(), + } + } + + /// Update the configuration at runtime. + pub fn update_config(&mut self, config: RateLimitConfig) { + self.window = Duration::from_secs(config.window_secs); + self.config = config; + } + + /// Check and record a new connection from an IP. + /// Returns `true` if the connection should be allowed. + pub fn check_connection(&self, ip: &str) -> bool { + self.increment_and_check( + &self.connections, + ip, + self.config.max_connections_per_ip, + ) + } + + /// Check and record a message from a sender. + /// Returns `true` if the message should be allowed. + pub fn check_message(&self, sender: &str) -> bool { + self.increment_and_check( + &self.messages, + sender, + self.config.max_messages_per_sender, + ) + } + + /// Check and record an auth failure from an IP. + /// Returns `true` if more attempts should be allowed. + pub fn check_auth_failure(&self, ip: &str) -> bool { + self.increment_and_check( + &self.auth_failures, + ip, + self.config.max_auth_failures_per_ip, + ) + } + + /// Increment a counter and check against the limit. + /// Returns `true` if within limits. + fn increment_and_check( + &self, + map: &DashMap, + key: &str, + limit: u32, + ) -> bool { + let now = Instant::now(); + let mut entry = map + .entry(key.to_string()) + .or_insert_with(|| CounterEntry { + count: 0, + window_start: now, + }); + + // Reset window if expired + if now.duration_since(entry.window_start) > self.window { + entry.count = 0; + entry.window_start = now; + } + + entry.count += 1; + entry.count <= limit + } + + /// Clean up expired entries. Call periodically. + pub fn cleanup(&self) { + let now = Instant::now(); + let window = self.window; + self.connections + .retain(|_, v| now.duration_since(v.window_start) <= window); + self.messages + .retain(|_, v| now.duration_since(v.window_start) <= window); + self.auth_failures + .retain(|_, v| now.duration_since(v.window_start) <= window); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_connection_limit() { + let limiter = RateLimiter::new(RateLimitConfig { + max_connections_per_ip: 3, + window_secs: 60, + ..Default::default() + }); + + assert!(limiter.check_connection("1.2.3.4")); + assert!(limiter.check_connection("1.2.3.4")); + assert!(limiter.check_connection("1.2.3.4")); + assert!(!limiter.check_connection("1.2.3.4")); // 4th = over limit + + // Different IP is independent + assert!(limiter.check_connection("5.6.7.8")); + } + + #[test] + fn test_message_limit() { + let limiter = RateLimiter::new(RateLimitConfig { + max_messages_per_sender: 2, + window_secs: 60, + ..Default::default() + }); + + assert!(limiter.check_message("sender@example.com")); + assert!(limiter.check_message("sender@example.com")); + assert!(!limiter.check_message("sender@example.com")); + } + + #[test] + fn test_auth_failure_limit() { + let limiter = RateLimiter::new(RateLimitConfig { + max_auth_failures_per_ip: 2, + window_secs: 60, + ..Default::default() + }); + + assert!(limiter.check_auth_failure("1.2.3.4")); + assert!(limiter.check_auth_failure("1.2.3.4")); + assert!(!limiter.check_auth_failure("1.2.3.4")); + } + + #[test] + fn test_cleanup() { + let limiter = RateLimiter::new(RateLimitConfig { + max_connections_per_ip: 1, + window_secs: 60, + ..Default::default() + }); + + limiter.check_connection("1.2.3.4"); + assert_eq!(limiter.connections.len(), 1); + + limiter.cleanup(); // entries not expired + assert_eq!(limiter.connections.len(), 1); + } +} diff --git a/rust/crates/mailer-smtp/src/response.rs b/rust/crates/mailer-smtp/src/response.rs new file mode 100644 index 0000000..502bfd8 --- /dev/null +++ b/rust/crates/mailer-smtp/src/response.rs @@ -0,0 +1,284 @@ +//! SMTP response builder. +//! +//! Constructs properly formatted SMTP response lines with status codes, +//! multiline support, and EHLO capability advertisement. + +use serde::{Deserialize, Serialize}; + +/// An SMTP response to send to the client. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SmtpResponse { + /// 3-digit SMTP status code. + pub code: u16, + /// Response lines (without the status code prefix). + pub lines: Vec, +} + +impl SmtpResponse { + /// Create a single-line response. + pub fn new(code: u16, message: impl Into) -> Self { + Self { + code, + lines: vec![message.into()], + } + } + + /// Create a multiline response. + pub fn multiline(code: u16, lines: Vec) -> Self { + Self { code, lines } + } + + /// Format the response as bytes ready to write to the socket. + /// + /// Multiline responses use `code-text` for intermediate lines + /// and `code text` for the final line (RFC 5321 §4.2). + pub fn to_bytes(&self) -> Vec { + let mut buf = Vec::new(); + if self.lines.is_empty() { + buf.extend_from_slice(format!("{} \r\n", self.code).as_bytes()); + } else if self.lines.len() == 1 { + buf.extend_from_slice( + format!("{} {}\r\n", self.code, self.lines[0]).as_bytes(), + ); + } else { + for (i, line) in self.lines.iter().enumerate() { + if i < self.lines.len() - 1 { + buf.extend_from_slice( + format!("{}-{}\r\n", self.code, line).as_bytes(), + ); + } else { + buf.extend_from_slice( + format!("{} {}\r\n", self.code, line).as_bytes(), + ); + } + } + } + buf + } + + // --- Common response constructors --- + + /// 220 Service ready greeting. + pub fn greeting(hostname: &str) -> Self { + Self::new(220, format!("{hostname} ESMTP Service Ready")) + } + + /// 221 Service closing. + pub fn closing(hostname: &str) -> Self { + Self::new(221, format!("{hostname} Service closing transmission channel")) + } + + /// 250 OK. + pub fn ok(message: impl Into) -> Self { + Self::new(250, message) + } + + /// EHLO response with capabilities. + pub fn ehlo_response(hostname: &str, capabilities: &[String]) -> Self { + let mut lines = Vec::with_capacity(capabilities.len() + 1); + lines.push(format!("{hostname} greets you")); + for cap in capabilities { + lines.push(cap.clone()); + } + Self::multiline(250, lines) + } + + /// 235 Authentication successful. + pub fn auth_success() -> Self { + Self::new(235, "2.7.0 Authentication successful") + } + + /// 334 Auth challenge (base64-encoded prompt). + pub fn auth_challenge(prompt: &str) -> Self { + Self::new(334, prompt) + } + + /// 354 Start mail input. + pub fn start_data() -> Self { + Self::new(354, "Start mail input; end with .") + } + + /// 421 Service not available. + pub fn service_unavailable(hostname: &str, reason: &str) -> Self { + Self::new(421, format!("{hostname} {reason}")) + } + + /// 450 Temporary failure. + pub fn temp_failure(message: impl Into) -> Self { + Self::new(450, message) + } + + /// 451 Local error. + pub fn local_error(message: impl Into) -> Self { + Self::new(451, message) + } + + /// 500 Syntax error. + pub fn syntax_error() -> Self { + Self::new(500, "Syntax error, command unrecognized") + } + + /// 501 Syntax error in parameters. + pub fn param_error(message: impl Into) -> Self { + Self::new(501, message) + } + + /// 502 Command not implemented. + pub fn not_implemented() -> Self { + Self::new(502, "Command not implemented") + } + + /// 503 Bad sequence. + pub fn bad_sequence(message: impl Into) -> Self { + Self::new(503, message) + } + + /// 530 Authentication required. + pub fn auth_required() -> Self { + Self::new(530, "5.7.0 Authentication required") + } + + /// 535 Authentication failed. + pub fn auth_failed() -> Self { + Self::new(535, "5.7.8 Authentication credentials invalid") + } + + /// 550 Mailbox unavailable. + pub fn mailbox_unavailable(message: impl Into) -> Self { + Self::new(550, message) + } + + /// 552 Message size exceeded. + pub fn size_exceeded(max_size: u64) -> Self { + Self::new( + 552, + format!("5.3.4 Message size exceeds maximum of {max_size} bytes"), + ) + } + + /// 554 Transaction failed. + pub fn transaction_failed(message: impl Into) -> Self { + Self::new(554, message) + } + + /// Check if this is a success response (2xx). + pub fn is_success(&self) -> bool { + self.code >= 200 && self.code < 300 + } + + /// Check if this is a temporary error (4xx). + pub fn is_temp_error(&self) -> bool { + self.code >= 400 && self.code < 500 + } + + /// Check if this is a permanent error (5xx). + pub fn is_perm_error(&self) -> bool { + self.code >= 500 && self.code < 600 + } +} + +/// Build the list of EHLO capabilities for the server. +pub fn build_capabilities( + max_size: u64, + tls_available: bool, + already_secure: bool, + auth_available: bool, +) -> Vec { + let mut caps = vec![ + format!("SIZE {max_size}"), + "8BITMIME".to_string(), + "PIPELINING".to_string(), + "ENHANCEDSTATUSCODES".to_string(), + "HELP".to_string(), + ]; + // Only advertise STARTTLS if TLS is available and not already using TLS + if tls_available && !already_secure { + caps.push("STARTTLS".to_string()); + } + if auth_available { + caps.push("AUTH PLAIN LOGIN".to_string()); + } + caps +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_single_line() { + let resp = SmtpResponse::new(250, "OK"); + assert_eq!(resp.to_bytes(), b"250 OK\r\n"); + } + + #[test] + fn test_multiline() { + let resp = SmtpResponse::multiline( + 250, + vec![ + "mail.example.com greets you".into(), + "SIZE 10485760".into(), + "STARTTLS".into(), + ], + ); + let expected = b"250-mail.example.com greets you\r\n250-SIZE 10485760\r\n250 STARTTLS\r\n"; + assert_eq!(resp.to_bytes(), expected.to_vec()); + } + + #[test] + fn test_greeting() { + let resp = SmtpResponse::greeting("mail.example.com"); + assert_eq!(resp.code, 220); + assert!(resp.lines[0].contains("mail.example.com")); + } + + #[test] + fn test_ehlo_response() { + let caps = vec!["SIZE 10485760".into(), "STARTTLS".into()]; + let resp = SmtpResponse::ehlo_response("mail.example.com", &caps); + assert_eq!(resp.code, 250); + assert_eq!(resp.lines.len(), 3); // hostname + 2 caps + } + + #[test] + fn test_status_checks() { + assert!(SmtpResponse::new(250, "OK").is_success()); + assert!(SmtpResponse::new(450, "Try later").is_temp_error()); + assert!(SmtpResponse::new(550, "No such user").is_perm_error()); + assert!(!SmtpResponse::new(250, "OK").is_temp_error()); + } + + #[test] + fn test_build_capabilities() { + let caps = build_capabilities(10485760, true, false, true); + assert!(caps.contains(&"SIZE 10485760".to_string())); + assert!(caps.contains(&"STARTTLS".to_string())); + assert!(caps.contains(&"AUTH PLAIN LOGIN".to_string())); + assert!(caps.contains(&"PIPELINING".to_string())); + } + + #[test] + fn test_build_capabilities_secure() { + // When already secure, STARTTLS should NOT be advertised + let caps = build_capabilities(10485760, true, true, false); + assert!(!caps.contains(&"STARTTLS".to_string())); + assert!(!caps.contains(&"AUTH PLAIN LOGIN".to_string())); + } + + #[test] + fn test_empty_response() { + let resp = SmtpResponse::multiline(250, vec![]); + assert_eq!(resp.to_bytes(), b"250 \r\n"); + } + + #[test] + fn test_common_responses() { + assert_eq!(SmtpResponse::start_data().code, 354); + assert_eq!(SmtpResponse::syntax_error().code, 500); + assert_eq!(SmtpResponse::not_implemented().code, 502); + assert_eq!(SmtpResponse::bad_sequence("test").code, 503); + assert_eq!(SmtpResponse::auth_required().code, 530); + assert_eq!(SmtpResponse::auth_failed().code, 535); + assert_eq!(SmtpResponse::auth_success().code, 235); + } +} diff --git a/rust/crates/mailer-smtp/src/server.rs b/rust/crates/mailer-smtp/src/server.rs new file mode 100644 index 0000000..d5d3d32 --- /dev/null +++ b/rust/crates/mailer-smtp/src/server.rs @@ -0,0 +1,308 @@ +//! SMTP TCP/TLS server. +//! +//! Listens on configured ports, accepts connections, and dispatches +//! them to per-connection handlers. + +use crate::config::SmtpServerConfig; +use crate::connection::{ + self, CallbackRegistry, ConnectionEvent, SmtpStream, +}; +use crate::rate_limiter::{RateLimitConfig, RateLimiter}; + +use rustls_pki_types::{CertificateDer, PrivateKeyDer}; +use std::io::BufReader; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use std::sync::Arc; +use tokio::io::BufReader as TokioBufReader; +use tokio::net::TcpListener; +use tokio::sync::mpsc; +use tracing::{error, info, warn}; + +/// Handle for a running SMTP server. +pub struct SmtpServerHandle { + /// Shutdown signal. + shutdown: Arc, + /// Join handles for the listener tasks. + handles: Vec>, + /// Active connection count. + pub active_connections: Arc, +} + +impl SmtpServerHandle { + /// Signal shutdown and wait for all listeners to stop. + pub async fn shutdown(self) { + self.shutdown.store(true, Ordering::SeqCst); + for handle in self.handles { + let _ = handle.await; + } + info!("SMTP server shut down"); + } + + /// Check if the server is running. + pub fn is_running(&self) -> bool { + !self.shutdown.load(Ordering::SeqCst) + } +} + +/// Start the SMTP server with the given configuration. +/// +/// Returns a handle that can be used to shut down the server, +/// and an event receiver for connection events (emailReceived, authRequest). +pub async fn start_server( + config: SmtpServerConfig, + callback_registry: Arc, + rate_limit_config: Option, +) -> Result<(SmtpServerHandle, mpsc::Receiver), Box> +{ + let config = Arc::new(config); + let shutdown = Arc::new(AtomicBool::new(false)); + let active_connections = Arc::new(AtomicU32::new(0)); + let rate_limiter = Arc::new(RateLimiter::new( + rate_limit_config.unwrap_or_default(), + )); + + let (event_tx, event_rx) = mpsc::channel::(1024); + + // Build TLS acceptor if configured + let tls_acceptor = if config.has_tls() { + Some(Arc::new(build_tls_acceptor(&config)?)) + } else { + None + }; + + let mut handles = Vec::new(); + + // Start listeners on each port + for &port in &config.ports { + let listener = TcpListener::bind(format!("0.0.0.0:{port}")).await?; + info!(port = port, "SMTP server listening (STARTTLS)"); + + let handle = tokio::spawn(accept_loop( + listener, + config.clone(), + shutdown.clone(), + active_connections.clone(), + rate_limiter.clone(), + event_tx.clone(), + callback_registry.clone(), + tls_acceptor.clone(), + false, // not implicit TLS + )); + handles.push(handle); + } + + // Start implicit TLS listener if configured + if let Some(secure_port) = config.secure_port { + if tls_acceptor.is_some() { + let listener = + TcpListener::bind(format!("0.0.0.0:{secure_port}")).await?; + info!(port = secure_port, "SMTP server listening (implicit TLS)"); + + let handle = tokio::spawn(accept_loop( + listener, + config.clone(), + shutdown.clone(), + active_connections.clone(), + rate_limiter.clone(), + event_tx.clone(), + callback_registry.clone(), + tls_acceptor.clone(), + true, // implicit TLS + )); + handles.push(handle); + } else { + warn!("Secure port configured but TLS certificates not provided"); + } + } + + // Spawn periodic rate limiter cleanup + { + let rate_limiter = rate_limiter.clone(); + let shutdown = shutdown.clone(); + tokio::spawn(async move { + let mut interval = + tokio::time::interval(tokio::time::Duration::from_secs(60)); + loop { + interval.tick().await; + if shutdown.load(Ordering::SeqCst) { + break; + } + rate_limiter.cleanup(); + } + }); + } + + Ok(( + SmtpServerHandle { + shutdown, + handles, + active_connections, + }, + event_rx, + )) +} + +/// Accept loop for a single listener. +async fn accept_loop( + listener: TcpListener, + config: Arc, + shutdown: Arc, + active_connections: Arc, + rate_limiter: Arc, + event_tx: mpsc::Sender, + callback_registry: Arc, + tls_acceptor: Option>, + implicit_tls: bool, +) { + loop { + if shutdown.load(Ordering::SeqCst) { + break; + } + + // Use a short timeout to check shutdown periodically + let accept_result = tokio::time::timeout( + tokio::time::Duration::from_secs(1), + listener.accept(), + ) + .await; + + let (tcp_stream, peer_addr) = match accept_result { + Ok(Ok((stream, addr))) => (stream, addr), + Ok(Err(e)) => { + error!(error = %e, "Accept error"); + continue; + } + Err(_) => continue, // timeout, check shutdown + }; + + // Check max connections + let current = active_connections.load(Ordering::SeqCst); + if current >= config.max_connections { + warn!( + current = current, + max = config.max_connections, + "Max connections reached, rejecting" + ); + drop(tcp_stream); + continue; + } + + let remote_addr = peer_addr.ip().to_string(); + let config = config.clone(); + let rate_limiter = rate_limiter.clone(); + let event_tx = event_tx.clone(); + let callback_registry = callback_registry.clone(); + let tls_acceptor = tls_acceptor.clone(); + let active_connections = active_connections.clone(); + + active_connections.fetch_add(1, Ordering::SeqCst); + + tokio::spawn(async move { + let stream = if implicit_tls { + // Implicit TLS: wrap immediately + if let Some(acceptor) = &tls_acceptor { + match acceptor.accept(tcp_stream).await { + Ok(tls_stream) => { + SmtpStream::Tls(TokioBufReader::new(tls_stream)) + } + Err(e) => { + warn!( + remote_addr = %remote_addr, + error = %e, + "Implicit TLS handshake failed" + ); + active_connections.fetch_sub(1, Ordering::SeqCst); + return; + } + } + } else { + active_connections.fetch_sub(1, Ordering::SeqCst); + return; + } + } else { + SmtpStream::Plain(TokioBufReader::new(tcp_stream)) + }; + + connection::handle_connection( + stream, + config, + rate_limiter, + event_tx, + callback_registry, + tls_acceptor, + remote_addr, + implicit_tls, + ) + .await; + + active_connections.fetch_sub(1, Ordering::SeqCst); + }); + } +} + +/// Build a TLS acceptor from PEM cert/key strings. +fn build_tls_acceptor( + config: &SmtpServerConfig, +) -> Result> { + let cert_pem = config + .tls_cert_pem + .as_ref() + .ok_or("TLS cert not configured")?; + let key_pem = config + .tls_key_pem + .as_ref() + .ok_or("TLS key not configured")?; + + // Parse certificates + let certs: Vec> = { + let mut reader = BufReader::new(cert_pem.as_bytes()); + rustls_pemfile::certs(&mut reader) + .collect::, _>>()? + }; + + if certs.is_empty() { + return Err("No certificates found in PEM".into()); + } + + // Parse private key + let key: PrivateKeyDer<'static> = { + let mut reader = BufReader::new(key_pem.as_bytes()); + // Try PKCS8 first, then RSA, then EC + let mut keys = Vec::new(); + for item in rustls_pemfile::read_all(&mut reader) { + match item? { + rustls_pemfile::Item::Pkcs8Key(key) => { + keys.push(PrivateKeyDer::Pkcs8(key)); + } + rustls_pemfile::Item::Pkcs1Key(key) => { + keys.push(PrivateKeyDer::Pkcs1(key)); + } + rustls_pemfile::Item::Sec1Key(key) => { + keys.push(PrivateKeyDer::Sec1(key)); + } + _ => {} + } + } + keys.into_iter() + .next() + .ok_or("No private key found in PEM")? + }; + + let tls_config = rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(certs, key)?; + + Ok(tokio_rustls::TlsAcceptor::from(Arc::new(tls_config))) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_server_config_defaults() { + let config = SmtpServerConfig::default(); + assert!(!config.has_tls()); + assert_eq!(config.ports, vec![25]); + } +} diff --git a/rust/crates/mailer-smtp/src/session.rs b/rust/crates/mailer-smtp/src/session.rs new file mode 100644 index 0000000..f1d7a22 --- /dev/null +++ b/rust/crates/mailer-smtp/src/session.rs @@ -0,0 +1,206 @@ +//! Per-connection SMTP session state. +//! +//! Tracks the envelope, authentication, TLS status, and counters +//! for a single SMTP connection. + +use crate::state::SmtpState; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +/// Envelope accumulator for the current mail transaction. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Envelope { + /// Sender address from MAIL FROM. + pub mail_from: String, + /// Recipient addresses from RCPT TO. + pub rcpt_to: Vec, + /// Declared message size from MAIL FROM SIZE= param (if any). + pub declared_size: Option, + /// BODY parameter (e.g. "8BITMIME"). + pub body_type: Option, +} + +/// Authentication state for the session. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AuthState { + /// Not authenticated and not in progress. + None, + /// Waiting for AUTH credentials (LOGIN flow step). + WaitingForUsername, + /// Have username, waiting for password. + WaitingForPassword { username: String }, + /// Successfully authenticated. + Authenticated { username: String }, +} + +impl Default for AuthState { + fn default() -> Self { + AuthState::None + } +} + +/// Per-connection session state. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SmtpSession { + /// Unique session identifier. + pub id: String, + /// Current protocol state. + pub state: SmtpState, + /// Client's EHLO/HELO hostname. + pub client_hostname: Option, + /// Whether the client used EHLO (vs HELO). + pub esmtp: bool, + /// Whether the connection is using TLS. + pub secure: bool, + /// Authentication state. + pub auth_state: AuthState, + /// Current transaction envelope. + pub envelope: Envelope, + /// Remote IP address. + pub remote_addr: String, + /// Number of messages sent in this session. + pub message_count: u32, + /// Number of failed auth attempts. + pub auth_failures: u32, + /// Number of invalid commands. + pub invalid_commands: u32, + /// Maximum allowed invalid commands before disconnect. + pub max_invalid_commands: u32, +} + +impl SmtpSession { + /// Create a new session for a connection. + pub fn new(remote_addr: String, secure: bool) -> Self { + Self { + id: Uuid::new_v4().to_string(), + state: SmtpState::Connected, + client_hostname: None, + esmtp: false, + secure, + auth_state: AuthState::None, + envelope: Envelope::default(), + remote_addr, + message_count: 0, + auth_failures: 0, + invalid_commands: 0, + max_invalid_commands: 20, + } + } + + /// Reset the current transaction (RSET), preserving connection state. + pub fn reset_transaction(&mut self) { + self.envelope = Envelope::default(); + if self.state != SmtpState::Connected { + self.state = SmtpState::Greeted; + } + } + + /// Reset session for a new EHLO (preserves counters and TLS). + pub fn reset_for_ehlo(&mut self, hostname: String, esmtp: bool) { + self.client_hostname = Some(hostname); + self.esmtp = esmtp; + self.envelope = Envelope::default(); + self.state = SmtpState::Greeted; + // Auth state is reset on new EHLO per RFC + self.auth_state = AuthState::None; + } + + /// Check if the client is authenticated. + pub fn is_authenticated(&self) -> bool { + matches!(self.auth_state, AuthState::Authenticated { .. }) + } + + /// Get the authenticated username, if any. + pub fn authenticated_user(&self) -> Option<&str> { + match &self.auth_state { + AuthState::Authenticated { username } => Some(username), + _ => None, + } + } + + /// Record a completed message delivery. + pub fn record_message(&mut self) { + self.message_count += 1; + } + + /// Record a failed auth attempt. Returns true if limit exceeded. + pub fn record_auth_failure(&mut self, max_failures: u32) -> bool { + self.auth_failures += 1; + self.auth_failures >= max_failures + } + + /// Record an invalid command. Returns true if limit exceeded. + pub fn record_invalid_command(&mut self) -> bool { + self.invalid_commands += 1; + self.invalid_commands >= self.max_invalid_commands + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new_session() { + let session = SmtpSession::new("127.0.0.1".into(), false); + assert_eq!(session.state, SmtpState::Connected); + assert!(!session.secure); + assert!(!session.is_authenticated()); + assert!(session.client_hostname.is_none()); + } + + #[test] + fn test_reset_transaction() { + let mut session = SmtpSession::new("127.0.0.1".into(), false); + session.state = SmtpState::RcptTo; + session.envelope.mail_from = "sender@example.com".into(); + session.envelope.rcpt_to.push("rcpt@example.com".into()); + + session.reset_transaction(); + + assert_eq!(session.state, SmtpState::Greeted); + assert!(session.envelope.mail_from.is_empty()); + assert!(session.envelope.rcpt_to.is_empty()); + } + + #[test] + fn test_reset_for_ehlo() { + let mut session = SmtpSession::new("127.0.0.1".into(), true); + session.auth_state = AuthState::Authenticated { + username: "user".into(), + }; + + session.reset_for_ehlo("mail.example.com".into(), true); + + assert_eq!(session.state, SmtpState::Greeted); + assert_eq!(session.client_hostname.as_deref(), Some("mail.example.com")); + assert!(session.esmtp); + assert!(!session.is_authenticated()); // Auth reset after EHLO + } + + #[test] + fn test_auth_failures() { + let mut session = SmtpSession::new("127.0.0.1".into(), false); + assert!(!session.record_auth_failure(3)); + assert!(!session.record_auth_failure(3)); + assert!(session.record_auth_failure(3)); // 3rd failure -> limit + } + + #[test] + fn test_invalid_commands() { + let mut session = SmtpSession::new("127.0.0.1".into(), false); + session.max_invalid_commands = 3; + assert!(!session.record_invalid_command()); + assert!(!session.record_invalid_command()); + assert!(session.record_invalid_command()); // 3rd -> limit + } + + #[test] + fn test_message_count() { + let mut session = SmtpSession::new("127.0.0.1".into(), false); + assert_eq!(session.message_count, 0); + session.record_message(); + session.record_message(); + assert_eq!(session.message_count, 2); + } +} diff --git a/rust/crates/mailer-smtp/src/state.rs b/rust/crates/mailer-smtp/src/state.rs new file mode 100644 index 0000000..1bb7221 --- /dev/null +++ b/rust/crates/mailer-smtp/src/state.rs @@ -0,0 +1,219 @@ +//! SMTP protocol state machine. +//! +//! Defines valid states and transitions for an SMTP session. + +use serde::{Deserialize, Serialize}; + +/// SMTP session states following RFC 5321. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum SmtpState { + /// Initial state — waiting for server greeting. + Connected, + /// After successful EHLO/HELO. + Greeted, + /// After MAIL FROM accepted. + MailFrom, + /// After at least one RCPT TO accepted. + RcptTo, + /// In DATA mode — accumulating message body. + Data, + /// Transaction completed — can start a new one or QUIT. + Finished, +} + +/// State transition errors. +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +pub enum TransitionError { + #[error("cannot {action} in state {state:?}")] + InvalidTransition { + state: SmtpState, + action: &'static str, + }, +} + +impl SmtpState { + /// Check whether EHLO/HELO is valid in the current state. + /// EHLO/HELO can be issued at any time to reset the session. + pub fn can_ehlo(&self) -> bool { + true + } + + /// Check whether MAIL FROM is valid in the current state. + pub fn can_mail_from(&self) -> bool { + matches!(self, SmtpState::Greeted | SmtpState::Finished) + } + + /// Check whether RCPT TO is valid in the current state. + pub fn can_rcpt_to(&self) -> bool { + matches!(self, SmtpState::MailFrom | SmtpState::RcptTo) + } + + /// Check whether DATA is valid in the current state. + pub fn can_data(&self) -> bool { + matches!(self, SmtpState::RcptTo) + } + + /// Check whether STARTTLS is valid in the current state. + /// Only before a transaction starts. + pub fn can_starttls(&self) -> bool { + matches!(self, SmtpState::Connected | SmtpState::Greeted | SmtpState::Finished) + } + + /// Check whether AUTH is valid in the current state. + /// Only after EHLO and before a transaction starts. + pub fn can_auth(&self) -> bool { + matches!(self, SmtpState::Greeted | SmtpState::Finished) + } + + /// Transition to Greeted state (after EHLO/HELO). + pub fn transition_ehlo(&self) -> Result { + // EHLO is always valid — it resets the session. + Ok(SmtpState::Greeted) + } + + /// Transition to MailFrom state (after MAIL FROM accepted). + pub fn transition_mail_from(&self) -> Result { + if self.can_mail_from() { + Ok(SmtpState::MailFrom) + } else { + Err(TransitionError::InvalidTransition { + state: *self, + action: "MAIL FROM", + }) + } + } + + /// Transition to RcptTo state (after RCPT TO accepted). + pub fn transition_rcpt_to(&self) -> Result { + if self.can_rcpt_to() { + Ok(SmtpState::RcptTo) + } else { + Err(TransitionError::InvalidTransition { + state: *self, + action: "RCPT TO", + }) + } + } + + /// Transition to Data state (after DATA command accepted). + pub fn transition_data(&self) -> Result { + if self.can_data() { + Ok(SmtpState::Data) + } else { + Err(TransitionError::InvalidTransition { + state: *self, + action: "DATA", + }) + } + } + + /// Transition to Finished state (after end-of-data). + pub fn transition_finished(&self) -> Result { + if *self == SmtpState::Data { + Ok(SmtpState::Finished) + } else { + Err(TransitionError::InvalidTransition { + state: *self, + action: "finish DATA", + }) + } + } + + /// Reset to Greeted state (after RSET command). + pub fn transition_rset(&self) -> Result { + match self { + SmtpState::Connected => Err(TransitionError::InvalidTransition { + state: *self, + action: "RSET", + }), + _ => Ok(SmtpState::Greeted), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_initial_state() { + let state = SmtpState::Connected; + assert!(!state.can_mail_from()); + assert!(!state.can_rcpt_to()); + assert!(!state.can_data()); + assert!(state.can_starttls()); + assert!(state.can_ehlo()); + } + + #[test] + fn test_ehlo_always_valid() { + for state in [ + SmtpState::Connected, + SmtpState::Greeted, + SmtpState::MailFrom, + SmtpState::RcptTo, + SmtpState::Data, + SmtpState::Finished, + ] { + assert!(state.can_ehlo()); + assert!(state.transition_ehlo().is_ok()); + } + } + + #[test] + fn test_normal_flow() { + let state = SmtpState::Connected; + let state = state.transition_ehlo().unwrap(); + assert_eq!(state, SmtpState::Greeted); + + let state = state.transition_mail_from().unwrap(); + assert_eq!(state, SmtpState::MailFrom); + + let state = state.transition_rcpt_to().unwrap(); + assert_eq!(state, SmtpState::RcptTo); + + // Multiple RCPT TO + let state = state.transition_rcpt_to().unwrap(); + assert_eq!(state, SmtpState::RcptTo); + + let state = state.transition_data().unwrap(); + assert_eq!(state, SmtpState::Data); + + let state = state.transition_finished().unwrap(); + assert_eq!(state, SmtpState::Finished); + + // New transaction + let state = state.transition_mail_from().unwrap(); + assert_eq!(state, SmtpState::MailFrom); + } + + #[test] + fn test_invalid_transitions() { + assert!(SmtpState::Connected.transition_mail_from().is_err()); + assert!(SmtpState::Connected.transition_rcpt_to().is_err()); + assert!(SmtpState::Connected.transition_data().is_err()); + assert!(SmtpState::Greeted.transition_rcpt_to().is_err()); + assert!(SmtpState::Greeted.transition_data().is_err()); + assert!(SmtpState::MailFrom.transition_data().is_err()); + } + + #[test] + fn test_rset() { + let state = SmtpState::RcptTo; + let state = state.transition_rset().unwrap(); + assert_eq!(state, SmtpState::Greeted); + + // RSET from Connected is invalid (no EHLO yet) + assert!(SmtpState::Connected.transition_rset().is_err()); + } + + #[test] + fn test_starttls_validity() { + assert!(SmtpState::Connected.can_starttls()); + assert!(SmtpState::Greeted.can_starttls()); + assert!(!SmtpState::MailFrom.can_starttls()); + assert!(!SmtpState::RcptTo.can_starttls()); + assert!(!SmtpState::Data.can_starttls()); + assert!(SmtpState::Finished.can_starttls()); + } +} diff --git a/rust/crates/mailer-smtp/src/validation.rs b/rust/crates/mailer-smtp/src/validation.rs new file mode 100644 index 0000000..7757c20 --- /dev/null +++ b/rust/crates/mailer-smtp/src/validation.rs @@ -0,0 +1,169 @@ +//! SMTP-level validation utilities. +//! +//! Address parsing, EHLO hostname validation, and header injection detection. + +use regex::Regex; +use std::sync::LazyLock; + +/// Regex for basic email address format validation. +static EMAIL_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"^[^\s@]+@[^\s@]+\.[^\s@]+$").unwrap() +}); + +/// Regex for valid EHLO hostname (domain name or IPv4/IPv6 literal). +/// Currently unused in favor of a more permissive check, but available +/// for strict validation if needed. +#[allow(dead_code)] +static EHLO_RE: LazyLock = LazyLock::new(|| { + // Permissive: domain names, IP literals [1.2.3.4], [IPv6:...], or bare words + Regex::new(r"^(?:\[(?:IPv6:)?[^\]]+\]|[a-zA-Z0-9](?:[a-zA-Z0-9\-\.]*[a-zA-Z0-9])?)$").unwrap() +}); + +/// Validate an email address for basic SMTP format. +/// +/// Returns `true` if the address has a valid-looking format. +/// Empty addresses (for bounce messages, MAIL FROM:<>) return `true`. +pub fn is_valid_smtp_address(address: &str) -> bool { + // Empty address is valid for MAIL FROM (bounce) + if address.is_empty() { + return true; + } + EMAIL_RE.is_match(address) +} + +/// Validate an EHLO/HELO hostname. +/// +/// Returns `true` if the hostname looks syntactically valid. +/// We are permissive because real-world SMTP clients send all kinds of values. +pub fn is_valid_ehlo_hostname(hostname: &str) -> bool { + if hostname.is_empty() { + return false; + } + // Be permissive — most SMTP servers accept anything non-empty. + // Only reject obviously malicious patterns. + if hostname.len() > 255 { + return false; + } + if contains_header_injection(hostname) { + return false; + } + // Must not contain null bytes + if hostname.contains('\0') { + return false; + } + true +} + +/// Check for SMTP header injection attempts. +/// +/// Returns `true` if the input contains characters that could be used +/// for header injection (bare CR/LF). +pub fn contains_header_injection(input: &str) -> bool { + input.contains('\r') || input.contains('\n') +} + +/// Validate the size parameter from MAIL FROM. +/// +/// Returns the parsed size if valid and within the max, or an error message. +pub fn validate_size_param(value: &str, max_size: u64) -> Result { + let size: u64 = value + .parse() + .map_err(|_| format!("invalid SIZE value: {value}"))?; + if size > max_size { + return Err(format!( + "message size {size} exceeds maximum {max_size}" + )); + } + Ok(size) +} + +/// Extract the domain part from an email address. +pub fn extract_domain(address: &str) -> Option<&str> { + if address.is_empty() { + return None; + } + address.rsplit_once('@').map(|(_, domain)| domain) +} + +/// Normalize an email address by lowercasing the domain part. +pub fn normalize_address(address: &str) -> String { + if address.is_empty() { + return String::new(); + } + match address.rsplit_once('@') { + Some((local, domain)) => format!("{local}@{}", domain.to_ascii_lowercase()), + None => address.to_string(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_valid_email() { + assert!(is_valid_smtp_address("user@example.com")); + assert!(is_valid_smtp_address("user+tag@sub.example.com")); + assert!(is_valid_smtp_address("a@b.c")); + } + + #[test] + fn test_empty_address_valid() { + assert!(is_valid_smtp_address("")); + } + + #[test] + fn test_invalid_email() { + assert!(!is_valid_smtp_address("no-at-sign")); + assert!(!is_valid_smtp_address("@no-local.com")); + assert!(!is_valid_smtp_address("user@")); + assert!(!is_valid_smtp_address("user@nodot")); + assert!(!is_valid_smtp_address("has space@example.com")); + } + + #[test] + fn test_valid_ehlo() { + assert!(is_valid_ehlo_hostname("mail.example.com")); + assert!(is_valid_ehlo_hostname("localhost")); + assert!(is_valid_ehlo_hostname("[127.0.0.1]")); + assert!(is_valid_ehlo_hostname("[IPv6:::1]")); + } + + #[test] + fn test_invalid_ehlo() { + assert!(!is_valid_ehlo_hostname("")); + assert!(!is_valid_ehlo_hostname("host\r\nname")); + assert!(!is_valid_ehlo_hostname(&"a".repeat(256))); + } + + #[test] + fn test_header_injection() { + assert!(contains_header_injection("test\r\nBcc: evil@evil.com")); + assert!(contains_header_injection("test\ninjection")); + assert!(contains_header_injection("test\rinjection")); + assert!(!contains_header_injection("normal text")); + } + + #[test] + fn test_size_param() { + assert_eq!(validate_size_param("12345", 1_000_000), Ok(12345)); + assert!(validate_size_param("99999999", 1_000).is_err()); + assert!(validate_size_param("notanumber", 1_000).is_err()); + } + + #[test] + fn test_extract_domain() { + assert_eq!(extract_domain("user@example.com"), Some("example.com")); + assert_eq!(extract_domain(""), None); + assert_eq!(extract_domain("nodomain"), None); + } + + #[test] + fn test_normalize_address() { + assert_eq!( + normalize_address("User@EXAMPLE.COM"), + "User@example.com" + ); + assert_eq!(normalize_address(""), ""); + } +} diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 1186d55..b5b0ecc 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartmta', - version: '2.1.0', + version: '2.2.0', description: 'A high-performance, enterprise-grade Mail Transfer Agent (MTA) built from scratch in TypeScript with Rust acceleration.' } diff --git a/ts/mail/routing/classes.unified.email.server.ts b/ts/mail/routing/classes.unified.email.server.ts index a1a643f..21a370d 100644 --- a/ts/mail/routing/classes.unified.email.server.ts +++ b/ts/mail/routing/classes.unified.email.server.ts @@ -10,6 +10,7 @@ import { import { DKIMCreator } from '../security/classes.dkimcreator.js'; import { IPReputationChecker } from '../../security/classes.ipreputationchecker.js'; import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js'; +import type { IEmailReceivedEvent, IAuthRequestEvent, IEmailData } from '../../security/classes.rustsecuritybridge.js'; // Deliverability types (IPWarmupManager and SenderReputationMonitor are optional external modules) interface IIPWarmupConfig { enabled?: boolean; @@ -396,134 +397,86 @@ export class UnifiedEmailServer extends EventEmitter { this.emit('started'); return; } - + // Ensure we have the necessary TLS options const hasTlsConfig = this.options.tls?.keyPath && this.options.tls?.certPath; - + // Prepare the certificate and key if available - let key: string | undefined; - let cert: string | undefined; - + let tlsCertPem: string | undefined; + let tlsKeyPem: string | undefined; + if (hasTlsConfig) { try { - key = plugins.fs.readFileSync(this.options.tls.keyPath!, 'utf8'); - cert = plugins.fs.readFileSync(this.options.tls.certPath!, 'utf8'); + tlsKeyPem = plugins.fs.readFileSync(this.options.tls.keyPath!, 'utf8'); + tlsCertPem = plugins.fs.readFileSync(this.options.tls.certPath!, 'utf8'); logger.log('info', 'TLS certificates loaded successfully'); } catch (error) { logger.log('warn', `Failed to load TLS certificates: ${error.message}`); } } - - // Create a SMTP server for each port - for (const port of this.options.ports as number[]) { - // Create a reference object to hold the MTA service during setup - const mtaRef = { - config: { - smtp: { - hostname: this.options.hostname - }, - security: { - checkIPReputation: false, - verifyDkim: true, - verifySpf: true, - verifyDmarc: true - } - }, - // Security verification delegated to the Rust bridge - dkimVerifier: { - verify: async (rawMessage: string) => { - try { - const results = await this.rustBridge.verifyDkim(rawMessage); - const first = results[0]; - return { isValid: first?.is_valid ?? false, domain: first?.domain ?? '' }; - } catch (err) { - logger.log('warn', `Rust DKIM verification failed: ${(err as Error).message}`); - return { isValid: false, domain: '' }; - } - } - }, - spfVerifier: { - verifyAndApply: async (session: any) => { - if (!session?.remoteAddress || session.remoteAddress === '127.0.0.1') { - return true; // localhost — skip SPF - } - try { - const result = await this.rustBridge.checkSpf({ - ip: session.remoteAddress, - heloDomain: session.clientHostname || '', - hostname: this.options.hostname, - mailFrom: session.envelope?.mailFrom?.address || session.mailFrom || '', - }); - return result.result === 'pass' || result.result === 'none' || result.result === 'neutral'; - } catch (err) { - logger.log('warn', `Rust SPF check failed: ${(err as Error).message}`); - return true; // Accept on error to avoid blocking mail - } - } - }, - dmarcVerifier: { - verify: async () => ({}), - applyPolicy: () => true - }, - processIncomingEmail: async (email: Email) => { - // Process email using the new route-based system - await this.processEmailByMode(email, { - id: 'session-' + Math.random().toString(36).substring(2), - state: SmtpState.FINISHED, - mailFrom: email.from, - rcptTo: email.to, - emailData: email.toRFC822String(), // Use the proper method to get the full email content - useTLS: false, - connectionEnded: true, - remoteAddress: '127.0.0.1', - clientHostname: '', - secure: false, - authenticated: false, - envelope: { - mailFrom: { address: email.from, args: {} }, - rcptTo: email.to.map(recipient => ({ address: recipient, args: {} })) - } - }); - - return true; - } - }; - - // Create server options - const serverOptions = { - port, - hostname: this.options.hostname, - key, - cert - }; - - // Create and start the SMTP server - const smtpServer = createSmtpServer(mtaRef as any, serverOptions); - this.servers.push(smtpServer); - - // Start the server - await new Promise((resolve, reject) => { - try { - // Leave this empty for now, smtpServer.start() is handled by the SMTPServer class internally - // The server is started when it's created - logger.log('info', `UnifiedEmailServer listening on port ${port}`); - - // Event handlers are managed internally by the SmtpServer class - // No need to access the private server property - - resolve(); - } catch (err) { - if ((err as any).code === 'EADDRINUSE') { - logger.log('error', `Port ${port} is already in use`); - reject(new Error(`Port ${port} is already in use`)); - } else { - logger.log('error', `Error starting server on port ${port}: ${err.message}`); - reject(err); - } - } - }); + + // --- Start Rust SMTP server --- + // Register event handlers for email reception and auth + this.rustBridge.onEmailReceived(async (data) => { + try { + await this.handleRustEmailReceived(data); + } catch (err) { + logger.log('error', `Error handling email from Rust SMTP: ${(err as Error).message}`); + // Send rejection back to Rust + await this.rustBridge.sendEmailProcessingResult({ + correlationId: data.correlationId, + accepted: false, + smtpCode: 451, + smtpMessage: 'Internal processing error', + }); + } + }); + + this.rustBridge.onAuthRequest(async (data) => { + try { + await this.handleRustAuthRequest(data); + } catch (err) { + logger.log('error', `Error handling auth from Rust SMTP: ${(err as Error).message}`); + await this.rustBridge.sendAuthResult({ + correlationId: data.correlationId, + success: false, + message: 'Internal auth error', + }); + } + }); + + // Determine which ports need STARTTLS and which need implicit TLS + const smtpPorts = (this.options.ports as number[]).filter(p => p !== 465); + const securePort = (this.options.ports as number[]).find(p => p === 465); + + const started = await this.rustBridge.startSmtpServer({ + hostname: this.options.hostname, + ports: smtpPorts, + securePort: securePort, + tlsCertPem, + tlsKeyPem, + maxMessageSize: this.options.maxMessageSize || 10 * 1024 * 1024, + maxConnections: this.options.maxConnections || this.options.maxClients || 100, + maxRecipients: 100, + connectionTimeoutSecs: this.options.connectionTimeout ? Math.floor(this.options.connectionTimeout / 1000) : 30, + dataTimeoutSecs: 60, + authEnabled: !!this.options.auth?.required || !!(this.options.auth?.users?.length), + maxAuthFailures: 3, + socketTimeoutSecs: this.options.socketTimeout ? Math.floor(this.options.socketTimeout / 1000) : 300, + processingTimeoutSecs: 30, + rateLimits: this.options.rateLimits ? { + maxConnectionsPerIp: this.options.rateLimits.global?.maxConnectionsPerIP || 50, + maxMessagesPerSender: this.options.rateLimits.global?.maxMessagesPerMinute || 100, + maxAuthFailuresPerIp: this.options.rateLimits.global?.maxAuthFailuresPerIP || 5, + windowSecs: 60, + } : undefined, + }); + + if (!started) { + throw new Error('Failed to start Rust SMTP server'); } - + + logger.log('info', `Rust SMTP server listening on ports: ${smtpPorts.join(', ')}${securePort ? ` + ${securePort} (TLS)` : ''}`); logger.log('info', 'UnifiedEmailServer started successfully'); this.emit('started'); } catch (error) { @@ -587,6 +540,14 @@ export class UnifiedEmailServer extends EventEmitter { logger.log('info', 'Stopping UnifiedEmailServer'); try { + // Stop the Rust SMTP server first + try { + await this.rustBridge.stopSmtpServer(); + logger.log('info', 'Rust SMTP server stopped'); + } catch (err) { + logger.log('warn', `Error stopping Rust SMTP server: ${(err as Error).message}`); + } + // Clear the servers array - servers will be garbage collected this.servers = []; @@ -623,11 +584,112 @@ export class UnifiedEmailServer extends EventEmitter { throw error; } } - - - - - + + // ----------------------------------------------------------------------- + // Rust SMTP server event handlers + // ----------------------------------------------------------------------- + + /** + * Handle an emailReceived event from the Rust SMTP server. + * Decodes the email data, processes it through the routing system, + * and sends back the result via the correlation-ID callback. + */ + private async handleRustEmailReceived(data: IEmailReceivedEvent): Promise { + const { correlationId, mailFrom, rcptTo, remoteAddr, clientHostname, secure, authenticatedUser } = data; + + logger.log('info', `Rust SMTP received email from=${mailFrom} to=${rcptTo.join(',')} remote=${remoteAddr}`); + + try { + // Decode the email data + let rawMessageBuffer: Buffer; + if (data.data.type === 'inline' && data.data.base64) { + rawMessageBuffer = Buffer.from(data.data.base64, 'base64'); + } else if (data.data.type === 'file' && data.data.path) { + rawMessageBuffer = plugins.fs.readFileSync(data.data.path); + // Clean up temp file + try { + plugins.fs.unlinkSync(data.data.path); + } catch { + // Ignore cleanup errors + } + } else { + throw new Error('Invalid email data transport'); + } + + // Build a session-like object for processEmailByMode + const session: IExtendedSmtpSession = { + id: data.sessionId || 'rust-' + Math.random().toString(36).substring(2), + state: SmtpState.FINISHED, + mailFrom: mailFrom, + rcptTo: rcptTo, + emailData: rawMessageBuffer.toString('utf8'), + useTLS: secure, + connectionEnded: false, + remoteAddress: remoteAddr, + clientHostname: clientHostname || '', + secure: secure, + authenticated: !!authenticatedUser, + envelope: { + mailFrom: { address: mailFrom, args: {} }, + rcptTo: rcptTo.map(addr => ({ address: addr, args: {} })), + }, + }; + + if (authenticatedUser) { + session.user = { username: authenticatedUser }; + } + + // Process the email through the routing system + await this.processEmailByMode(rawMessageBuffer, session); + + // Send acceptance back to Rust + await this.rustBridge.sendEmailProcessingResult({ + correlationId, + accepted: true, + smtpCode: 250, + smtpMessage: '2.0.0 Message accepted for delivery', + }); + } catch (err) { + logger.log('error', `Failed to process email from Rust SMTP: ${(err as Error).message}`); + await this.rustBridge.sendEmailProcessingResult({ + correlationId, + accepted: false, + smtpCode: 550, + smtpMessage: `5.0.0 Processing failed: ${(err as Error).message}`, + }); + } + } + + /** + * Handle an authRequest event from the Rust SMTP server. + * Validates credentials and sends back the result. + */ + private async handleRustAuthRequest(data: IAuthRequestEvent): Promise { + const { correlationId, username, password, remoteAddr } = data; + + logger.log('info', `Rust SMTP auth request for user=${username} from=${remoteAddr}`); + + // Check against configured users + const users = this.options.auth?.users || []; + const matched = users.find( + u => u.username === username && u.password === password + ); + + if (matched) { + await this.rustBridge.sendAuthResult({ + correlationId, + success: true, + }); + } else { + logger.log('warn', `Auth failed for user=${username} from=${remoteAddr}`); + await this.rustBridge.sendAuthResult({ + correlationId, + success: false, + message: 'Invalid credentials', + }); + } + } + /** * Verify inbound email security (DKIM/SPF/DMARC) using the Rust bridge. * Falls back gracefully if the bridge is not running. diff --git a/ts/security/classes.rustsecuritybridge.ts b/ts/security/classes.rustsecuritybridge.ts index 150d85e..8e5e18d 100644 --- a/ts/security/classes.rustsecuritybridge.ts +++ b/ts/security/classes.rustsecuritybridge.ts @@ -73,6 +73,60 @@ interface IVersionInfo { smtp: string; } +// --- SMTP Server types --- + +interface ISmtpServerConfig { + hostname: string; + ports: number[]; + securePort?: number; + tlsCertPem?: string; + tlsKeyPem?: string; + maxMessageSize?: number; + maxConnections?: number; + maxRecipients?: number; + connectionTimeoutSecs?: number; + dataTimeoutSecs?: number; + authEnabled?: boolean; + maxAuthFailures?: number; + socketTimeoutSecs?: number; + processingTimeoutSecs?: number; + rateLimits?: IRateLimitConfig; +} + +interface IRateLimitConfig { + maxConnectionsPerIp?: number; + maxMessagesPerSender?: number; + maxAuthFailuresPerIp?: number; + windowSecs?: number; +} + +interface IEmailData { + type: 'inline' | 'file'; + base64?: string; + path?: string; +} + +interface IEmailReceivedEvent { + correlationId: string; + sessionId: string; + mailFrom: string; + rcptTo: string[]; + data: IEmailData; + remoteAddr: string; + clientHostname: string | null; + secure: boolean; + authenticatedUser: string | null; + securityResults: any | null; +} + +interface IAuthRequestEvent { + correlationId: string; + sessionId: string; + username: string; + password: string; + remoteAddr: string; +} + /** * Type-safe command map for the mailer-bin IPC bridge. */ @@ -128,6 +182,35 @@ type TMailerCommands = { }; result: IEmailSecurityResult; }; + startSmtpServer: { + params: ISmtpServerConfig; + result: { started: boolean }; + }; + stopSmtpServer: { + params: Record; + result: { stopped: boolean; wasRunning?: boolean }; + }; + emailProcessingResult: { + params: { + correlationId: string; + accepted: boolean; + smtpCode?: number; + smtpMessage?: string; + }; + result: { resolved: boolean }; + }; + authResult: { + params: { + correlationId: string; + success: boolean; + message?: string; + }; + result: { resolved: boolean }; + }; + configureRateLimits: { + params: IRateLimitConfig; + result: { configured: boolean }; + }; }; // --------------------------------------------------------------------------- @@ -314,6 +397,85 @@ export class RustSecurityBridge { }): Promise { return this.bridge.sendCommand('verifyEmail', opts); } + + // ----------------------------------------------------------------------- + // SMTP Server lifecycle + // ----------------------------------------------------------------------- + + /** + * Start the Rust SMTP server. + * The server will listen on the configured ports and emit events for + * emailReceived and authRequest that must be handled by the caller. + */ + public async startSmtpServer(config: ISmtpServerConfig): Promise { + const result = await this.bridge.sendCommand('startSmtpServer', config); + return result?.started === true; + } + + /** Stop the Rust SMTP server. */ + public async stopSmtpServer(): Promise { + await this.bridge.sendCommand('stopSmtpServer', {} as any); + } + + /** + * Send the result of email processing back to the Rust SMTP server. + * This resolves a pending correlation-ID callback, allowing the Rust + * server to send the SMTP response to the client. + */ + public async sendEmailProcessingResult(opts: { + correlationId: string; + accepted: boolean; + smtpCode?: number; + smtpMessage?: string; + }): Promise { + await this.bridge.sendCommand('emailProcessingResult', opts); + } + + /** + * Send the result of authentication validation back to the Rust SMTP server. + */ + public async sendAuthResult(opts: { + correlationId: string; + success: boolean; + message?: string; + }): Promise { + await this.bridge.sendCommand('authResult', opts); + } + + /** Update rate limit configuration at runtime. */ + public async configureRateLimits(config: IRateLimitConfig): Promise { + await this.bridge.sendCommand('configureRateLimits', config); + } + + // ----------------------------------------------------------------------- + // Event registration — delegates to the underlying bridge EventEmitter + // ----------------------------------------------------------------------- + + /** + * Register a handler for emailReceived events from the Rust SMTP server. + * These events fire when a complete email has been received and needs processing. + */ + public onEmailReceived(handler: (data: IEmailReceivedEvent) => void): void { + this.bridge.on('management:emailReceived', handler); + } + + /** + * Register a handler for authRequest events from the Rust SMTP server. + * The handler must call sendAuthResult() with the correlationId. + */ + public onAuthRequest(handler: (data: IAuthRequestEvent) => void): void { + this.bridge.on('management:authRequest', handler); + } + + /** Remove an emailReceived event handler. */ + public offEmailReceived(handler: (data: IEmailReceivedEvent) => void): void { + this.bridge.off('management:emailReceived', handler); + } + + /** Remove an authRequest event handler. */ + public offAuthRequest(handler: (data: IAuthRequestEvent) => void): void { + this.bridge.off('management:authRequest', handler); + } } // Re-export interfaces for consumers @@ -327,4 +489,9 @@ export type { IContentScanResult, IReputationResult as IRustReputationResult, IVersionInfo, + ISmtpServerConfig, + IRateLimitConfig, + IEmailData, + IEmailReceivedEvent, + IAuthRequestEvent, };