//! 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 serde::{Deserialize, Serialize}; use std::io::{self, BufRead, Write}; use std::net::IpAddr; /// 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, } 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; /// Run in management/IPC mode for smartrust bridge. 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 stdin = io::stdin(); for line in stdin.lock().lines() { let line = match line { Ok(l) => l, Err(_) => break, }; if line.trim().is_empty() { continue; } 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; } }; let response = rt.block_on(handle_ipc_request(&req)); println!("{}", serde_json::to_string(&response).unwrap()); io::stdout().flush().unwrap(); } } async fn handle_ipc_request(req: &IpcRequest) -> 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)), }, } } _ => IpcResponse { id: req.id.clone(), success: false, result: None, error: Some(format!("Unknown method: {}", req.method)), }, } }