2026-02-10 21:19:13 +00:00
|
|
|
//! mailer-bin: CLI and IPC binary for the @push.rocks/smartmta Rust crates.
|
feat(rust): implement mailer-core and mailer-security crates with CLI
Rust migration Phase 1 — implements real functionality in the previously
stubbed mailer-core and mailer-security crates (38 passing tests).
mailer-core: Email/EmailAddress/Attachment types, RFC 5322 MIME builder,
email format validation with scoring, bounce detection (14 types, 40+
regex patterns), DSN status parsing, retry delay calculation.
mailer-security: DKIM signing (RSA-SHA256) and verification, SPF checking,
DMARC verification with public suffix list, DNSBL IP reputation checking
(10 default servers, parallel queries), all powered by mail-auth 0.7.
mailer-bin: Full CLI with validate/bounce/check-ip/verify-email/dkim-sign
subcommands plus --management mode for smartrust JSON-over-stdin/stdout IPC.
2026-02-10 16:06:04 +00:00
|
|
|
//!
|
|
|
|
|
//! 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};
|
2026-02-10 22:00:44 +00:00
|
|
|
use dashmap::DashMap;
|
feat(rust): implement mailer-core and mailer-security crates with CLI
Rust migration Phase 1 — implements real functionality in the previously
stubbed mailer-core and mailer-security crates (38 passing tests).
mailer-core: Email/EmailAddress/Attachment types, RFC 5322 MIME builder,
email format validation with scoring, bounce detection (14 types, 40+
regex patterns), DSN status parsing, retry delay calculation.
mailer-security: DKIM signing (RSA-SHA256) and verification, SPF checking,
DMARC verification with public suffix list, DNSBL IP reputation checking
(10 default servers, parallel queries), all powered by mail-auth 0.7.
mailer-bin: Full CLI with validate/bounce/check-ip/verify-email/dkim-sign
subcommands plus --management mode for smartrust JSON-over-stdin/stdout IPC.
2026-02-10 16:06:04 +00:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use std::io::{self, BufRead, Write};
|
|
|
|
|
use std::net::IpAddr;
|
2026-02-10 22:00:44 +00:00
|
|
|
use std::sync::Arc;
|
|
|
|
|
use tokio::sync::oneshot;
|
|
|
|
|
|
|
|
|
|
use mailer_smtp::connection::{
|
|
|
|
|
AuthResult, CallbackRegistry, ConnectionEvent, EmailProcessingResult,
|
|
|
|
|
};
|
feat(rust): implement mailer-core and mailer-security crates with CLI
Rust migration Phase 1 — implements real functionality in the previously
stubbed mailer-core and mailer-security crates (38 passing tests).
mailer-core: Email/EmailAddress/Attachment types, RFC 5322 MIME builder,
email format validation with scoring, bounce detection (14 types, 40+
regex patterns), DSN status parsing, retry delay calculation.
mailer-security: DKIM signing (RSA-SHA256) and verification, SPF checking,
DMARC verification with public suffix list, DNSBL IP reputation checking
(10 default servers, parallel queries), all powered by mail-auth 0.7.
mailer-bin: Full CLI with validate/bounce/check-ip/verify-email/dkim-sign
subcommands plus --management mode for smartrust JSON-over-stdin/stdout IPC.
2026-02-10 16:06:04 +00:00
|
|
|
|
|
|
|
|
/// mailer-bin: Rust-powered email security tools
|
|
|
|
|
#[derive(Parser)]
|
|
|
|
|
#[command(name = "mailer-bin", version, about)]
|
|
|
|
|
struct Cli {
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
command: Option<Commands>,
|
|
|
|
|
|
|
|
|
|
/// 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<String>,
|
|
|
|
|
|
|
|
|
|
/// HELO domain for SPF check
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
helo: Option<String>,
|
|
|
|
|
|
|
|
|
|
/// Receiving server hostname
|
|
|
|
|
#[arg(long, default_value = "localhost")]
|
|
|
|
|
hostname: String,
|
|
|
|
|
|
|
|
|
|
/// MAIL FROM address for SPF check
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
mail_from: Option<String>,
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/// 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_json::Value>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
error: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
struct IpcEvent {
|
|
|
|
|
event: String,
|
|
|
|
|
data: serde_json::Value,
|
|
|
|
|
}
|
2026-02-10 15:31:31 +00:00
|
|
|
|
2026-02-10 22:00:44 +00:00
|
|
|
// --- Pending callbacks for correlation-ID based reverse calls ---
|
|
|
|
|
|
|
|
|
|
/// Stores oneshot senders for pending email processing and auth callbacks.
|
|
|
|
|
struct PendingCallbacks {
|
|
|
|
|
email: DashMap<String, oneshot::Sender<EmailProcessingResult>>,
|
|
|
|
|
auth: DashMap<String, oneshot::Sender<AuthResult>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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<EmailProcessingResult> {
|
|
|
|
|
let (tx, rx) = oneshot::channel();
|
|
|
|
|
self.email.insert(correlation_id.to_string(), tx);
|
|
|
|
|
rx
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn register_auth_callback(
|
|
|
|
|
&self,
|
|
|
|
|
correlation_id: &str,
|
|
|
|
|
) -> oneshot::Receiver<AuthResult> {
|
|
|
|
|
let (tx, rx) = oneshot::channel();
|
|
|
|
|
self.auth.insert(correlation_id.to_string(), tx);
|
|
|
|
|
rx
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 15:31:31 +00:00
|
|
|
fn main() {
|
feat(rust): implement mailer-core and mailer-security crates with CLI
Rust migration Phase 1 — implements real functionality in the previously
stubbed mailer-core and mailer-security crates (38 passing tests).
mailer-core: Email/EmailAddress/Attachment types, RFC 5322 MIME builder,
email format validation with scoring, bounce detection (14 types, 40+
regex patterns), DSN status parsing, retry delay calculation.
mailer-security: DKIM signing (RSA-SHA256) and verification, SPF checking,
DMARC verification with public suffix list, DNSBL IP reputation checking
(10 default servers, parallel queries), all powered by mail-auth 0.7.
mailer-bin: Full CLI with validate/bounce/check-ip/verify-email/dkim-sign
subcommands plus --management mode for smartrust JSON-over-stdin/stdout IPC.
2026-02-10 16:06:04 +00:00
|
|
|
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::<IpAddr>() {
|
|
|
|
|
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;
|
|
|
|
|
|
2026-02-10 22:00:44 +00:00
|
|
|
/// Shared state for the management mode.
|
|
|
|
|
struct ManagementState {
|
|
|
|
|
callbacks: Arc<PendingCallbacks>,
|
|
|
|
|
smtp_handle: Option<mailer_smtp::server::SmtpServerHandle>,
|
|
|
|
|
smtp_event_rx: Option<tokio::sync::mpsc::Receiver<ConnectionEvent>>,
|
2026-02-11 07:17:05 +00:00
|
|
|
smtp_client_manager: Arc<mailer_smtp::client::SmtpClientManager>,
|
2026-02-10 22:00:44 +00:00
|
|
|
}
|
|
|
|
|
|
feat(rust): implement mailer-core and mailer-security crates with CLI
Rust migration Phase 1 — implements real functionality in the previously
stubbed mailer-core and mailer-security crates (38 passing tests).
mailer-core: Email/EmailAddress/Attachment types, RFC 5322 MIME builder,
email format validation with scoring, bounce detection (14 types, 40+
regex patterns), DSN status parsing, retry delay calculation.
mailer-security: DKIM signing (RSA-SHA256) and verification, SPF checking,
DMARC verification with public suffix list, DNSBL IP reputation checking
(10 default servers, parallel queries), all powered by mail-auth 0.7.
mailer-bin: Full CLI with validate/bounce/check-ip/verify-email/dkim-sign
subcommands plus --management mode for smartrust JSON-over-stdin/stdout IPC.
2026-02-10 16:06:04 +00:00
|
|
|
/// Run in management/IPC mode for smartrust bridge.
|
2026-02-10 22:00:44 +00:00
|
|
|
///
|
|
|
|
|
/// This mode supports both request/response IPC (existing commands) and
|
|
|
|
|
/// long-running SMTP server with event-based callbacks.
|
feat(rust): implement mailer-core and mailer-security crates with CLI
Rust migration Phase 1 — implements real functionality in the previously
stubbed mailer-core and mailer-security crates (38 passing tests).
mailer-core: Email/EmailAddress/Attachment types, RFC 5322 MIME builder,
email format validation with scoring, bounce detection (14 types, 40+
regex patterns), DSN status parsing, retry delay calculation.
mailer-security: DKIM signing (RSA-SHA256) and verification, SPF checking,
DMARC verification with public suffix list, DNSBL IP reputation checking
(10 default servers, parallel queries), all powered by mail-auth 0.7.
mailer-bin: Full CLI with validate/bounce/check-ip/verify-email/dkim-sign
subcommands plus --management mode for smartrust JSON-over-stdin/stdout IPC.
2026-02-10 16:06:04 +00:00
|
|
|
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();
|
|
|
|
|
|
2026-02-10 22:00:44 +00:00
|
|
|
let callbacks = Arc::new(PendingCallbacks::new());
|
2026-02-11 07:17:05 +00:00
|
|
|
let smtp_client_manager = Arc::new(mailer_smtp::client::SmtpClientManager::new());
|
2026-02-10 22:00:44 +00:00
|
|
|
let mut state = ManagementState {
|
|
|
|
|
callbacks: callbacks.clone(),
|
|
|
|
|
smtp_handle: None,
|
|
|
|
|
smtp_event_rx: None,
|
2026-02-11 07:17:05 +00:00
|
|
|
smtp_client_manager: smtp_client_manager.clone(),
|
2026-02-10 22:00:44 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 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::<String>(256);
|
feat(rust): implement mailer-core and mailer-security crates with CLI
Rust migration Phase 1 — implements real functionality in the previously
stubbed mailer-core and mailer-security crates (38 passing tests).
mailer-core: Email/EmailAddress/Attachment types, RFC 5322 MIME builder,
email format validation with scoring, bounce detection (14 types, 40+
regex patterns), DSN status parsing, retry delay calculation.
mailer-security: DKIM signing (RSA-SHA256) and verification, SPF checking,
DMARC verification with public suffix list, DNSBL IP reputation checking
(10 default servers, parallel queries), all powered by mail-auth 0.7.
mailer-bin: Full CLI with validate/bounce/check-ip/verify-email/dkim-sign
subcommands plus --management mode for smartrust JSON-over-stdin/stdout IPC.
2026-02-10 16:06:04 +00:00
|
|
|
|
2026-02-10 22:00:44 +00:00
|
|
|
// 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,
|
|
|
|
|
}
|
feat(rust): implement mailer-core and mailer-security crates with CLI
Rust migration Phase 1 — implements real functionality in the previously
stubbed mailer-core and mailer-security crates (38 passing tests).
mailer-core: Email/EmailAddress/Attachment types, RFC 5322 MIME builder,
email format validation with scoring, bounce detection (14 types, 40+
regex patterns), DSN status parsing, retry delay calculation.
mailer-security: DKIM signing (RSA-SHA256) and verification, SPF checking,
DMARC verification with public suffix list, DNSBL IP reputation checking
(10 default servers, parallel queries), all powered by mail-auth 0.7.
mailer-bin: Full CLI with validate/bounce/check-ip/verify-email/dkim-sign
subcommands plus --management mode for smartrust JSON-over-stdin/stdout IPC.
2026-02-10 16:06:04 +00:00
|
|
|
}
|
2026-02-10 22:00:44 +00:00
|
|
|
});
|
feat(rust): implement mailer-core and mailer-security crates with CLI
Rust migration Phase 1 — implements real functionality in the previously
stubbed mailer-core and mailer-security crates (38 passing tests).
mailer-core: Email/EmailAddress/Attachment types, RFC 5322 MIME builder,
email format validation with scoring, bounce detection (14 types, 40+
regex patterns), DSN status parsing, retry delay calculation.
mailer-security: DKIM signing (RSA-SHA256) and verification, SPF checking,
DMARC verification with public suffix list, DNSBL IP reputation checking
(10 default servers, parallel queries), all powered by mail-auth 0.7.
mailer-bin: Full CLI with validate/bounce/check-ip/verify-email/dkim-sign
subcommands plus --management mode for smartrust JSON-over-stdin/stdout IPC.
2026-02-10 16:06:04 +00:00
|
|
|
|
2026-02-10 22:00:44 +00:00
|
|
|
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::<Option<ConnectionEvent>>().await
|
|
|
|
|
}
|
|
|
|
|
} => {
|
|
|
|
|
if let Some(event) = event {
|
|
|
|
|
handle_smtp_event(event);
|
|
|
|
|
}
|
|
|
|
|
}
|
feat(rust): implement mailer-core and mailer-security crates with CLI
Rust migration Phase 1 — implements real functionality in the previously
stubbed mailer-core and mailer-security crates (38 passing tests).
mailer-core: Email/EmailAddress/Attachment types, RFC 5322 MIME builder,
email format validation with scoring, bounce detection (14 types, 40+
regex patterns), DSN status parsing, retry delay calculation.
mailer-security: DKIM signing (RSA-SHA256) and verification, SPF checking,
DMARC verification with public suffix list, DNSBL IP reputation checking
(10 default servers, parallel queries), all powered by mail-auth 0.7.
mailer-bin: Full CLI with validate/bounce/check-ip/verify-email/dkim-sign
subcommands plus --management mode for smartrust JSON-over-stdin/stdout IPC.
2026-02-10 16:06:04 +00:00
|
|
|
}
|
2026-02-10 22:00:44 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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());
|
|
|
|
|
}
|
feat(rust): implement mailer-core and mailer-security crates with CLI
Rust migration Phase 1 — implements real functionality in the previously
stubbed mailer-core and mailer-security crates (38 passing tests).
mailer-core: Email/EmailAddress/Attachment types, RFC 5322 MIME builder,
email format validation with scoring, bounce detection (14 types, 40+
regex patterns), DSN status parsing, retry delay calculation.
mailer-security: DKIM signing (RSA-SHA256) and verification, SPF checking,
DMARC verification with public suffix list, DNSBL IP reputation checking
(10 default servers, parallel queries), all powered by mail-auth 0.7.
mailer-bin: Full CLI with validate/bounce/check-ip/verify-email/dkim-sign
subcommands plus --management mode for smartrust JSON-over-stdin/stdout IPC.
2026-02-10 16:06:04 +00:00
|
|
|
|
2026-02-10 22:00:44 +00:00
|
|
|
/// 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,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
feat(rust): implement mailer-core and mailer-security crates with CLI
Rust migration Phase 1 — implements real functionality in the previously
stubbed mailer-core and mailer-security crates (38 passing tests).
mailer-core: Email/EmailAddress/Attachment types, RFC 5322 MIME builder,
email format validation with scoring, bounce detection (14 types, 40+
regex patterns), DSN status parsing, retry delay calculation.
mailer-security: DKIM signing (RSA-SHA256) and verification, SPF checking,
DMARC verification with public suffix list, DNSBL IP reputation checking
(10 default servers, parallel queries), all powered by mail-auth 0.7.
mailer-bin: Full CLI with validate/bounce/check-ip/verify-email/dkim-sign
subcommands plus --management mode for smartrust JSON-over-stdin/stdout IPC.
2026-02-10 16:06:04 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 22:00:44 +00:00
|
|
|
async fn handle_ipc_request(req: &IpcRequest, state: &mut ManagementState) -> IpcResponse {
|
feat(rust): implement mailer-core and mailer-security crates with CLI
Rust migration Phase 1 — implements real functionality in the previously
stubbed mailer-core and mailer-security crates (38 passing tests).
mailer-core: Email/EmailAddress/Attachment types, RFC 5322 MIME builder,
email format validation with scoring, bounce detection (14 types, 40+
regex patterns), DSN status parsing, retry delay calculation.
mailer-security: DKIM signing (RSA-SHA256) and verification, SPF checking,
DMARC verification with public suffix list, DNSBL IP reputation checking
(10 default servers, parallel queries), all powered by mail-auth 0.7.
mailer-bin: Full CLI with validate/bounce/check-ip/verify-email/dkim-sign
subcommands plus --management mode for smartrust JSON-over-stdin/stdout IPC.
2026-02-10 16:06:04 +00:00
|
|
|
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::<IpAddr>() {
|
|
|
|
|
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()),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 16:25:55 +00:00
|
|
|
"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::<IpAddr>() {
|
|
|
|
|
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)),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 21:19:13 +00:00
|
|
|
"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<String> = 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,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat(rust): implement mailer-core and mailer-security crates with CLI
Rust migration Phase 1 — implements real functionality in the previously
stubbed mailer-core and mailer-security crates (38 passing tests).
mailer-core: Email/EmailAddress/Attachment types, RFC 5322 MIME builder,
email format validation with scoring, bounce detection (14 types, 40+
regex patterns), DSN status parsing, retry delay calculation.
mailer-security: DKIM signing (RSA-SHA256) and verification, SPF checking,
DMARC verification with public suffix list, DNSBL IP reputation checking
(10 default servers, parallel queries), all powered by mail-auth 0.7.
mailer-bin: Full CLI with validate/bounce/check-ip/verify-email/dkim-sign
subcommands plus --management mode for smartrust JSON-over-stdin/stdout IPC.
2026-02-10 16:06:04 +00:00
|
|
|
"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::<IpAddr>() {
|
|
|
|
|
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)),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 22:00:44 +00:00
|
|
|
// --- 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,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 07:17:05 +00:00
|
|
|
// --- 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)
|
|
|
|
|
}
|
|
|
|
|
|
feat(rust): implement mailer-core and mailer-security crates with CLI
Rust migration Phase 1 — implements real functionality in the previously
stubbed mailer-core and mailer-security crates (38 passing tests).
mailer-core: Email/EmailAddress/Attachment types, RFC 5322 MIME builder,
email format validation with scoring, bounce detection (14 types, 40+
regex patterns), DSN status parsing, retry delay calculation.
mailer-security: DKIM signing (RSA-SHA256) and verification, SPF checking,
DMARC verification with public suffix list, DNSBL IP reputation checking
(10 default servers, parallel queries), all powered by mail-auth 0.7.
mailer-bin: Full CLI with validate/bounce/check-ip/verify-email/dkim-sign
subcommands plus --management mode for smartrust JSON-over-stdin/stdout IPC.
2026-02-10 16:06:04 +00:00
|
|
|
_ => IpcResponse {
|
|
|
|
|
id: req.id.clone(),
|
|
|
|
|
success: false,
|
|
|
|
|
result: None,
|
|
|
|
|
error: Some(format!("Unknown method: {}", req.method)),
|
|
|
|
|
},
|
|
|
|
|
}
|
2026-02-10 15:31:31 +00:00
|
|
|
}
|
2026-02-10 22:00:44 +00:00
|
|
|
|
|
|
|
|
/// 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::<mailer_smtp::rate_limiter::RateLimitConfig>(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<mailer_smtp::config::SmtpServerConfig, String> {
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-02-11 07:17:05 +00:00
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// SMTP Client IPC handlers
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
/// Structured email to build a MIME message from.
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
#[serde(rename_all = "camelCase")]
|
|
|
|
|
struct OutboundEmail {
|
|
|
|
|
from: String,
|
|
|
|
|
to: Vec<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
cc: Vec<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
bcc: Vec<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
subject: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
text: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
html: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
headers: std::collections::HashMap<String, String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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<u8> {
|
|
|
|
|
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<String> {
|
|
|
|
|
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::<mailer_smtp::client::DkimSignConfig>(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<String> = 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,
|
|
|
|
|
}
|
|
|
|
|
}
|