//! mailer-bin: CLI and IPC binary for the @push.rocks/smartmta Rust crates. //! //! Supports two modes: //! 1. **CLI mode** — traditional subcommands for testing and standalone use //! 2. **Management mode** (`--management`) — JSON-over-stdin/stdout IPC for //! 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)] #[command(name = "mailer-bin", version, about)] struct Cli { #[command(subcommand)] command: Option, /// Run in management/IPC mode (JSON-over-stdin/stdout for smartrust bridge) #[arg(long)] management: bool, } #[derive(Subcommand)] enum Commands { /// Print version information Version, /// Validate an email address Validate { /// The email address to validate email: String, }, /// Detect bounce type from an SMTP response Bounce { /// The SMTP response or diagnostic message message: String, }, /// Check IP reputation via DNSBL CheckIp { /// The IP address to check ip: String, }, /// Verify DKIM/SPF/DMARC for an email (reads raw message from stdin) VerifyEmail { /// Sender IP address for SPF check #[arg(long)] ip: Option, /// HELO domain for SPF check #[arg(long)] helo: Option, /// Receiving server hostname #[arg(long, default_value = "localhost")] hostname: String, /// MAIL FROM address for SPF check #[arg(long)] mail_from: Option, }, /// Sign an email with DKIM (reads raw message from stdin) DkimSign { /// Signing domain #[arg(long)] domain: String, /// DKIM selector #[arg(long, default_value = "mta")] selector: String, /// Path to RSA private key PEM file #[arg(long)] key: String, }, } // --- IPC types for smartrust bridge --- #[derive(Deserialize)] struct IpcRequest { id: String, method: String, params: serde_json::Value, } #[derive(Serialize)] struct IpcResponse { id: String, success: bool, #[serde(skip_serializing_if = "Option::is_none")] result: Option, #[serde(skip_serializing_if = "Option::is_none")] error: Option, } #[derive(Serialize)] struct IpcEvent { event: String, 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(); if cli.management { run_management_mode(); return; } match cli.command { Some(Commands::Version) | None => { println!( "mailer-bin v{} (core: {}, smtp: {}, security: {})", env!("CARGO_PKG_VERSION"), mailer_core::version(), mailer_smtp::version(), mailer_security::version(), ); } Some(Commands::Validate { email }) => { let result = mailer_core::validate_email(&email); println!( "{}", serde_json::to_string_pretty(&serde_json::json!({ "email": email, "valid": result.is_valid, "formatValid": result.format_valid, "score": result.score, "error": result.error_message, })) .unwrap() ); } Some(Commands::Bounce { message }) => { let detection = mailer_core::detect_bounce_type(Some(&message), None, None); println!( "{}", serde_json::to_string_pretty(&detection).unwrap() ); } Some(Commands::CheckIp { ip }) => { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { let ip_addr: IpAddr = match ip.parse() { Ok(addr) => addr, Err(e) => { eprintln!("Invalid IP address: {}", e); std::process::exit(1); } }; let resolver = hickory_resolver::TokioResolver::builder_tokio().map(|b| b.build()) .expect("Failed to create DNS resolver"); match mailer_security::check_reputation( ip_addr, mailer_security::DEFAULT_DNSBL_SERVERS, &resolver, ) .await { Ok(result) => { println!("{}", serde_json::to_string_pretty(&result).unwrap()); } Err(e) => { eprintln!("Error: {}", e); std::process::exit(1); } } }); } Some(Commands::VerifyEmail { ip, helo, hostname, mail_from, }) => { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { // Read raw message from stdin let mut raw_message = Vec::new(); io::stdin() .lock() .read_to_end(&mut raw_message) .expect("Failed to read from stdin"); let authenticator = mailer_security::default_authenticator() .expect("Failed to create authenticator"); // DKIM verification let dkim_results = mailer_security::verify_dkim(&raw_message, &authenticator) .await .unwrap_or_else(|e| { vec![mailer_security::DkimVerificationResult { is_valid: false, domain: None, selector: None, status: "error".to_string(), details: Some(e.to_string()), }] }); let mut output = serde_json::json!({ "dkim": dkim_results, }); // SPF verification (if IP provided) if let (Some(ip_str), Some(helo_domain), Some(sender)) = (&ip, &helo, &mail_from) { if let Ok(ip_addr) = ip_str.parse::() { match mailer_security::check_spf( ip_addr, helo_domain, &hostname, sender, &authenticator, ) .await { Ok(spf_result) => { output["spf"] = serde_json::to_value(&spf_result).unwrap(); } Err(e) => { output["spf"] = serde_json::json!({"error": e.to_string()}); } } } } println!("{}", serde_json::to_string_pretty(&output).unwrap()); }); } Some(Commands::DkimSign { domain, selector, key, }) => { // Read private key let key_pem = std::fs::read_to_string(&key).unwrap_or_else(|e| { eprintln!("Failed to read key file '{}': {}", key, e); std::process::exit(1); }); // Read raw message from stdin let mut raw_message = Vec::new(); io::stdin() .lock() .read_to_end(&mut raw_message) .expect("Failed to read from stdin"); match mailer_security::sign_dkim(&raw_message, &domain, &selector, &key_pem) { Ok(header) => { // Output signed message: DKIM header + original message print!("{}", header); io::stdout().write_all(&raw_message).unwrap(); } Err(e) => { eprintln!("DKIM signing failed: {}", e); std::process::exit(1); } } } } } use std::io::Read; /// Shared state for the management mode. struct ManagementState { callbacks: Arc, smtp_handle: Option, smtp_event_rx: Option>, smtp_client_manager: Arc, } /// 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 { event: "ready".to_string(), data: serde_json::json!({ "version": env!("CARGO_PKG_VERSION"), "core_version": mailer_core::version(), "security_version": mailer_security::version(), }), }; println!("{}", serde_json::to_string(&ready_event).unwrap()); io::stdout().flush().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap(); let callbacks = Arc::new(PendingCallbacks::new()); let smtp_client_manager = Arc::new(mailer_smtp::client::SmtpClientManager::new()); let mut state = ManagementState { callbacks: callbacks.clone(), smtp_handle: None, smtp_event_rx: None, smtp_client_manager: smtp_client_manager.clone(), }; // 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); // 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, } } }); 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, state: &mut ManagementState) -> IpcResponse { match req.method.as_str() { "ping" => IpcResponse { id: req.id.clone(), success: true, result: Some(serde_json::json!({"pong": true})), error: None, }, "version" => IpcResponse { id: req.id.clone(), success: true, result: Some(serde_json::json!({ "bin": env!("CARGO_PKG_VERSION"), "core": mailer_core::version(), "security": mailer_security::version(), "smtp": mailer_smtp::version(), })), error: None, }, "validateEmail" => { let email = req.params.get("email").and_then(|v| v.as_str()).unwrap_or(""); let result = mailer_core::validate_email(email); IpcResponse { id: req.id.clone(), success: true, result: Some(serde_json::json!({ "valid": result.is_valid, "formatValid": result.format_valid, "score": result.score, "error": result.error_message, })), error: None, } } "detectBounce" => { let smtp_response = req.params.get("smtpResponse").and_then(|v| v.as_str()); let diagnostic = req.params.get("diagnosticCode").and_then(|v| v.as_str()); let status = req.params.get("statusCode").and_then(|v| v.as_str()); let detection = mailer_core::detect_bounce_type(smtp_response, diagnostic, status); IpcResponse { id: req.id.clone(), success: true, result: Some(serde_json::to_value(&detection).unwrap()), error: None, } } "checkIpReputation" => { let ip_str = req.params.get("ip").and_then(|v| v.as_str()).unwrap_or(""); match ip_str.parse::() { Ok(ip_addr) => { let resolver = match hickory_resolver::TokioResolver::builder_tokio().map(|b| b.build()) { Ok(r) => r, Err(e) => { return IpcResponse { id: req.id.clone(), success: false, result: None, error: Some(format!("DNS resolver error: {}", e)), }; } }; match mailer_security::check_reputation( ip_addr, mailer_security::DEFAULT_DNSBL_SERVERS, &resolver, ) .await { Ok(result) => IpcResponse { id: req.id.clone(), success: true, result: Some(serde_json::to_value(&result).unwrap()), error: None, }, Err(e) => IpcResponse { id: req.id.clone(), success: false, result: None, error: Some(e.to_string()), }, } } Err(e) => IpcResponse { id: req.id.clone(), success: false, result: None, error: Some(format!("Invalid IP address: {}", e)), }, } } "verifyDkim" => { let raw_message = req .params .get("rawMessage") .and_then(|v| v.as_str()) .unwrap_or(""); let authenticator = match mailer_security::default_authenticator() { Ok(a) => a, Err(e) => { return IpcResponse { id: req.id.clone(), success: false, result: None, error: Some(format!("Authenticator error: {}", e)), }; } }; match mailer_security::verify_dkim(raw_message.as_bytes(), &authenticator).await { Ok(results) => IpcResponse { id: req.id.clone(), success: true, result: Some(serde_json::to_value(&results).unwrap()), error: None, }, Err(e) => IpcResponse { id: req.id.clone(), success: false, result: None, error: Some(e.to_string()), }, } } "signDkim" => { let raw_message = req .params .get("rawMessage") .and_then(|v| v.as_str()) .unwrap_or(""); let domain = req.params.get("domain").and_then(|v| v.as_str()).unwrap_or(""); let selector = req .params .get("selector") .and_then(|v| v.as_str()) .unwrap_or("mta"); let private_key = req .params .get("privateKey") .and_then(|v| v.as_str()) .unwrap_or(""); match mailer_security::sign_dkim(raw_message.as_bytes(), domain, selector, private_key) { Ok(header) => IpcResponse { id: req.id.clone(), success: true, result: Some(serde_json::json!({ "header": header, "signedMessage": format!("{}{}", header, raw_message), })), error: None, }, Err(e) => IpcResponse { id: req.id.clone(), success: false, result: None, error: Some(e.to_string()), }, } } "verifyEmail" => { let raw_message = req .params .get("rawMessage") .and_then(|v| v.as_str()) .unwrap_or(""); let ip_str = req.params.get("ip").and_then(|v| v.as_str()).unwrap_or(""); let helo = req .params .get("heloDomain") .and_then(|v| v.as_str()) .unwrap_or(""); let hostname = req .params .get("hostname") .and_then(|v| v.as_str()) .unwrap_or("localhost"); let mail_from = req .params .get("mailFrom") .and_then(|v| v.as_str()) .unwrap_or(""); match ip_str.parse::() { Ok(ip_addr) => { let authenticator = match mailer_security::default_authenticator() { Ok(a) => a, Err(e) => { return IpcResponse { id: req.id.clone(), success: false, result: None, error: Some(format!("Authenticator error: {}", e)), }; } }; match mailer_security::verify_email_security( raw_message.as_bytes(), ip_addr, helo, hostname, mail_from, &authenticator, ) .await { Ok(result) => IpcResponse { id: req.id.clone(), success: true, result: Some(serde_json::to_value(&result).unwrap()), error: None, }, Err(e) => IpcResponse { id: req.id.clone(), success: false, result: None, error: Some(e.to_string()), }, } } Err(e) => IpcResponse { id: req.id.clone(), success: false, result: None, error: Some(format!("Invalid IP address: {}", e)), }, } } "scanContent" => { let subject = req.params.get("subject").and_then(|v| v.as_str()); let text_body = req.params.get("textBody").and_then(|v| v.as_str()); let html_body = req.params.get("htmlBody").and_then(|v| v.as_str()); let attachment_names: Vec = req.params.get("attachmentNames") .and_then(|v| v.as_array()) .map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect()) .unwrap_or_default(); let result = mailer_security::content_scanner::scan_content( subject, text_body, html_body, &attachment_names ); IpcResponse { id: req.id.clone(), success: true, result: Some(serde_json::to_value(&result).unwrap()), error: None, } } "checkSpf" => { let ip_str = req.params.get("ip").and_then(|v| v.as_str()).unwrap_or(""); let helo = req .params .get("heloDomain") .and_then(|v| v.as_str()) .unwrap_or(""); let hostname = req .params .get("hostname") .and_then(|v| v.as_str()) .unwrap_or("localhost"); let mail_from = req .params .get("mailFrom") .and_then(|v| v.as_str()) .unwrap_or(""); match ip_str.parse::() { Ok(ip_addr) => { let authenticator = match mailer_security::default_authenticator() { Ok(a) => a, Err(e) => { return IpcResponse { id: req.id.clone(), success: false, result: None, error: Some(format!("Authenticator error: {}", e)), }; } }; match mailer_security::check_spf(ip_addr, helo, hostname, mail_from, &authenticator) .await { Ok(result) => IpcResponse { id: req.id.clone(), success: true, result: Some(serde_json::to_value(&result).unwrap()), error: None, }, Err(e) => IpcResponse { id: req.id.clone(), success: false, result: None, error: Some(e.to_string()), }, } } Err(e) => IpcResponse { id: req.id.clone(), success: false, result: None, error: Some(format!("Invalid IP address: {}", e)), }, } } // --- 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, } } // --- SMTP Client commands --- "sendEmail" => { handle_send_email(req, state).await } "sendRawEmail" => { handle_send_raw_email(req, state).await } "verifySmtpConnection" => { handle_verify_smtp_connection(req, state).await } "closeSmtpPool" => { handle_close_smtp_pool(req, state).await } "getSmtpPoolStatus" => { handle_get_smtp_pool_status(req, state) } _ => IpcResponse { id: req.id.clone(), success: false, result: None, error: Some(format!("Unknown method: {}", req.method)), }, } } /// 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) } // --------------------------------------------------------------------------- // SMTP Client IPC handlers // --------------------------------------------------------------------------- /// Structured email to build a MIME message from. #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct OutboundEmail { from: String, to: Vec, #[serde(default)] cc: Vec, #[serde(default)] bcc: Vec, #[serde(default)] subject: String, #[serde(default)] text: String, #[serde(default)] html: Option, #[serde(default)] headers: std::collections::HashMap, } impl OutboundEmail { /// Convert to `mailer_core::Email` for proper RFC 5322 MIME building. fn to_core_email(&self) -> mailer_core::Email { let mut email = mailer_core::Email::new(&self.from, &self.subject, &self.text); for addr in &self.to { email.add_to(addr); } for addr in &self.cc { email.add_cc(addr); } for addr in &self.bcc { email.add_bcc(addr); } if let Some(html) = &self.html { email.set_html(html); } for (key, value) in &self.headers { email.add_header(key, value); } email } /// Build an RFC 5322 compliant message using `mailer_core::build_rfc822`. fn to_rfc822(&self) -> Vec { let email = self.to_core_email(); match mailer_core::build_rfc822(&email) { Ok(msg) => msg.into_bytes(), Err(e) => { eprintln!("Failed to build RFC 822 message: {e}"); // Fallback: minimal message format!( "From: {}\r\nTo: {}\r\nSubject: {}\r\n\r\n{}", self.from, self.to.join(", "), self.subject, self.text ) .into_bytes() } } } /// Collect all recipients (to + cc + bcc). fn all_recipients(&self) -> Vec { let mut all = self.to.clone(); all.extend(self.cc.clone()); all.extend(self.bcc.clone()); all } } /// Handle sendEmail IPC command — build MIME, optional DKIM sign, send via pool. async fn handle_send_email(req: &IpcRequest, state: &ManagementState) -> IpcResponse { // Parse client config from params let config: mailer_smtp::client::SmtpClientConfig = match serde_json::from_value(req.params.clone()) { Ok(c) => c, Err(e) => { return IpcResponse { id: req.id.clone(), success: false, result: None, error: Some(format!("Invalid config: {}", e)), }; } }; // Parse the email let email: OutboundEmail = match req.params.get("email").and_then(|v| serde_json::from_value(v.clone()).ok()) { Some(e) => e, None => { return IpcResponse { id: req.id.clone(), success: false, result: None, error: Some("Missing or invalid 'email' field".into()), }; } }; // Build raw message let mut raw_message = email.to_rfc822(); // Optional DKIM signing if let Some(dkim_val) = req.params.get("dkim") { if let Ok(dkim_config) = serde_json::from_value::(dkim_val.clone()) { match mailer_security::sign_dkim( &raw_message, &dkim_config.domain, &dkim_config.selector, &dkim_config.private_key, ) { Ok(header) => { // Prepend DKIM header to the message let mut signed = header.into_bytes(); signed.extend_from_slice(&raw_message); raw_message = signed; } Err(e) => { // Log but don't fail — send unsigned eprintln!("DKIM signing failed: {}", e); } } } } let all_recipients = email.all_recipients(); let sender = &email.from; match state .smtp_client_manager .send_message(&config, sender, &all_recipients, &raw_message) .await { Ok(result) => IpcResponse { id: req.id.clone(), success: true, result: Some(serde_json::to_value(&result).unwrap()), error: None, }, Err(e) => IpcResponse { id: req.id.clone(), success: false, result: None, error: Some(serde_json::to_string(&serde_json::json!({ "message": e.to_string(), "errorType": e.error_type(), "retryable": e.is_retryable(), "smtpCode": e.smtp_code(), })) .unwrap()), }, } } /// Handle sendRawEmail IPC command — send a pre-formatted message. async fn handle_send_raw_email(req: &IpcRequest, state: &ManagementState) -> IpcResponse { // Parse client config from params let config: mailer_smtp::client::SmtpClientConfig = match serde_json::from_value(req.params.clone()) { Ok(c) => c, Err(e) => { return IpcResponse { id: req.id.clone(), success: false, result: None, error: Some(format!("Invalid config: {}", e)), }; } }; let envelope_from = req .params .get("envelopeFrom") .and_then(|v| v.as_str()) .unwrap_or(""); let envelope_to: Vec = req .params .get("envelopeTo") .and_then(|v| v.as_array()) .map(|a| { a.iter() .filter_map(|v| v.as_str().map(String::from)) .collect() }) .unwrap_or_default(); let raw_b64 = req .params .get("rawMessageBase64") .and_then(|v| v.as_str()) .unwrap_or(""); // Decode base64 message use base64::Engine; let raw_message = match base64::engine::general_purpose::STANDARD.decode(raw_b64) { Ok(data) => data, Err(e) => { return IpcResponse { id: req.id.clone(), success: false, result: None, error: Some(format!("Invalid base64 message: {}", e)), }; } }; match state .smtp_client_manager .send_message(&config, envelope_from, &envelope_to, &raw_message) .await { Ok(result) => IpcResponse { id: req.id.clone(), success: true, result: Some(serde_json::to_value(&result).unwrap()), error: None, }, Err(e) => IpcResponse { id: req.id.clone(), success: false, result: None, error: Some(serde_json::to_string(&serde_json::json!({ "message": e.to_string(), "errorType": e.error_type(), "retryable": e.is_retryable(), "smtpCode": e.smtp_code(), })) .unwrap()), }, } } /// Handle verifySmtpConnection IPC command. async fn handle_verify_smtp_connection( req: &IpcRequest, state: &ManagementState, ) -> IpcResponse { let config: mailer_smtp::client::SmtpClientConfig = match serde_json::from_value(req.params.clone()) { Ok(c) => c, Err(e) => { return IpcResponse { id: req.id.clone(), success: false, result: None, error: Some(format!("Invalid config: {}", e)), }; } }; match state.smtp_client_manager.verify_connection(&config).await { Ok(result) => IpcResponse { id: req.id.clone(), success: true, result: Some(serde_json::to_value(&result).unwrap()), error: None, }, Err(e) => IpcResponse { id: req.id.clone(), success: false, result: None, error: Some(e.to_string()), }, } } /// Handle closeSmtpPool IPC command. async fn handle_close_smtp_pool(req: &IpcRequest, state: &ManagementState) -> IpcResponse { if let Some(pool_key) = req.params.get("poolKey").and_then(|v| v.as_str()) { state.smtp_client_manager.close_pool(pool_key).await; } else { state.smtp_client_manager.close_all_pools().await; } IpcResponse { id: req.id.clone(), success: true, result: Some(serde_json::json!({"closed": true})), error: None, } } /// Handle getSmtpPoolStatus IPC command. fn handle_get_smtp_pool_status(req: &IpcRequest, state: &ManagementState) -> IpcResponse { let pools = state.smtp_client_manager.pool_status(); IpcResponse { id: req.id.clone(), success: true, result: Some(serde_json::json!({"pools": pools})), error: None, } }