use async_trait::async_trait; use spf_milter::{CliOptionsBuilder, Config, LogDestination, LogLevel, Socket}; use std::{ env, io, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, path::PathBuf, time::Duration, }; use tokio::{ net::TcpListener, sync::{mpsc, oneshot}, task::JoinHandle, time, }; use viaspf::lookup::{Lookup, LookupError, LookupResult, Name}; pub fn configure_logging(opts: CliOptionsBuilder) -> CliOptionsBuilder { // Set this environment variable to test debug logging to syslog. if env::var("SPF_MILTER_TEST_LOG").is_ok() { opts.log_destination(LogDestination::Syslog) .log_level(LogLevel::Debug) } else { opts.log_destination(LogDestination::Stderr) } } #[derive(Default)] pub struct MockLookup { lookup_a: Option LookupResult> + Send + Sync + 'static>>, lookup_aaaa: Option LookupResult> + Send + Sync + 'static>>, lookup_mx: Option LookupResult> + Send + Sync + 'static>>, lookup_txt: Option LookupResult> + Send + Sync + 'static>>, lookup_ptr: Option LookupResult> + Send + Sync + 'static>>, } impl MockLookup { pub fn new() -> Self { Default::default() } pub fn with_lookup_a( mut self, value: impl Fn(&Name) -> LookupResult> + Send + Sync + 'static, ) -> Self { self.lookup_a = Some(Box::new(value)); self } pub fn with_lookup_aaaa( mut self, value: impl Fn(&Name) -> LookupResult> + Send + Sync + 'static, ) -> Self { self.lookup_aaaa = Some(Box::new(value)); self } pub fn with_lookup_mx( mut self, value: impl Fn(&Name) -> LookupResult> + Send + Sync + 'static, ) -> Self { self.lookup_mx = Some(Box::new(value)); self } pub fn with_lookup_txt( mut self, value: impl Fn(&Name) -> LookupResult> + Send + Sync + 'static, ) -> Self { self.lookup_txt = Some(Box::new(value)); self } pub fn with_lookup_ptr( mut self, value: impl Fn(IpAddr) -> LookupResult> + Send + Sync + 'static, ) -> Self { self.lookup_ptr = Some(Box::new(value)); self } } #[async_trait] impl Lookup for MockLookup { async fn lookup_a(&self, name: &Name) -> LookupResult> { self.lookup_a .as_ref() .map_or(Err(LookupError::NoRecords), |f| f(name)) } async fn lookup_aaaa(&self, name: &Name) -> LookupResult> { self.lookup_aaaa .as_ref() .map_or(Err(LookupError::NoRecords), |f| f(name)) } async fn lookup_mx(&self, name: &Name) -> LookupResult> { self.lookup_mx .as_ref() .map_or(Err(LookupError::NoRecords), |f| f(name)) } async fn lookup_txt(&self, name: &Name) -> LookupResult> { self.lookup_txt .as_ref() .map_or(Err(LookupError::NoRecords), |f| f(name)) } async fn lookup_ptr(&self, ip: IpAddr) -> LookupResult> { self.lookup_ptr .as_ref() .map_or(Err(LookupError::NoRecords), |f| f(ip)) } } pub fn to_config_file_name(file_name: &str) -> PathBuf { let mut path = PathBuf::from(file_name); path.set_extension("conf"); path } pub struct SpfMilter { milter_handle: JoinHandle>, reload: mpsc::Sender<()>, shutdown: oneshot::Sender<()>, addr: SocketAddr, } impl SpfMilter { pub async fn spawn(config: Config) -> io::Result { let listener = match config.socket() { Socket::Inet(addr) => TcpListener::bind(addr).await?, Socket::Unix(_) => unimplemented!(), }; let addr = listener.local_addr()?; let (reload_tx, reload_rx) = mpsc::channel(1); let (shutdown_tx, shutdown_rx) = oneshot::channel(); let milter = tokio::spawn(spf_milter::run(listener, config, reload_rx, shutdown_rx)); Ok(Self { milter_handle: milter, reload: reload_tx, shutdown: shutdown_tx, addr, }) } pub fn addr(&self) -> SocketAddr { self.addr } pub async fn reload_config(&self) { self.reload.send(()).await.unwrap(); // Because the reload task in SPF Milter is spawned, and the test code // has the role of ‘main thread’, we must sleep a while to give the // runtime enough pause to complete the slow, filesystem-accessing // configuration reloading operation. time::sleep(Duration::from_millis(100)).await; } pub async fn shutdown(self) -> io::Result<()> { let _ = self.shutdown.send(()); self.milter_handle.await? } }