spamassassin-milter 0.0.3

Milter for spam filtering with SpamAssassin
Documentation
use ipnet::IpNet;
use once_cell::sync::OnceCell;
use std::{collections::HashSet, net::IpAddr};

/// A builder for SpamAssassin Milter configuration objects.
#[derive(Clone, Debug)]
pub struct ConfigBuilder {
    has_trusted_networks: bool,
    trusted_networks: HashSet<IpNet>,
    auth_untrusted: bool,
    spamc_args: Vec<String>,
    max_message_size: usize,
    dry_run: bool,
    reject_spam: bool,
    preserve_headers: bool,
    preserve_body: bool,
    verbose: bool,
}

impl ConfigBuilder {
    pub fn set_has_trusted_networks(&mut self, value: bool) -> &mut Self {
        self.has_trusted_networks = value;
        self
    }

    pub fn add_trusted_network(&mut self, value: IpNet) -> &mut Self {
        self.has_trusted_networks = true;
        self.trusted_networks.insert(value);
        self
    }

    pub fn set_auth_untrusted(&mut self, value: bool) -> &mut Self {
        self.auth_untrusted = value;
        self
    }

    pub fn set_spamc_args(&mut self, value: Vec<String>) -> &mut Self {
        self.spamc_args = value;
        self
    }

    pub fn set_max_message_size(&mut self, value: usize) -> &mut Self {
        self.max_message_size = value;
        self
    }

    pub fn set_dry_run(&mut self, value: bool) -> &mut Self {
        self.dry_run = value;
        self
    }

    pub fn set_reject_spam(&mut self, value: bool) -> &mut Self {
        self.reject_spam = value;
        self
    }

    pub fn set_preserve_headers(&mut self, value: bool) -> &mut Self {
        self.preserve_headers = value;
        self
    }

    pub fn set_preserve_body(&mut self, value: bool) -> &mut Self {
        self.preserve_body = value;
        self
    }

    pub fn set_verbose(&mut self, value: bool) -> &mut Self {
        self.verbose = value;
        self
    }

    pub fn build(self) -> Config {
        Config {
            has_trusted_networks: self.has_trusted_networks,
            trusted_networks: self.trusted_networks,
            auth_untrusted: self.auth_untrusted,
            spamc_args: self.spamc_args,
            max_message_size: self.max_message_size,
            dry_run: self.dry_run,
            reject_spam: self.reject_spam,
            preserve_headers: self.preserve_headers,
            preserve_body: self.preserve_body,
            verbose: self.verbose,
        }
    }
}

impl Default for ConfigBuilder {
    fn default() -> Self {
        Self {
            has_trusted_networks: Default::default(),
            trusted_networks: Default::default(),
            auth_untrusted: Default::default(),
            spamc_args: Default::default(),
            max_message_size: 512_000,  // same as in `spamc`
            dry_run: Default::default(),
            reject_spam: Default::default(),
            preserve_headers: Default::default(),
            preserve_body: Default::default(),
            verbose: Default::default(),
        }
    }
}

/// A configuration object for SpamAssassin Milter.
#[derive(Clone, Debug)]
pub struct Config {
    has_trusted_networks: bool,
    trusted_networks: HashSet<IpNet>,
    auth_untrusted: bool,
    spamc_args: Vec<String>,
    max_message_size: usize,
    dry_run: bool,
    reject_spam: bool,
    preserve_headers: bool,
    preserve_body: bool,
    verbose: bool,
}

impl Config {
    pub fn builder() -> ConfigBuilder {
        Default::default()
    }

    pub fn has_trusted_networks(&self) -> bool {
        self.has_trusted_networks
    }

    pub fn is_in_trusted_networks(&self, ip: &IpAddr) -> bool {
        self.trusted_networks.iter().any(|n| n.contains(ip))
    }

    pub fn auth_untrusted(&self) -> bool {
        self.auth_untrusted
    }

    pub fn spamc_args(&self) -> &[String] {
        &self.spamc_args
    }

    pub fn max_message_size(&self) -> usize {
        self.max_message_size
    }

    pub fn dry_run(&self) -> bool {
        self.dry_run
    }

    pub fn reject_spam(&self) -> bool {
        self.reject_spam
    }

    pub fn preserve_headers(&self) -> bool {
        self.preserve_headers
    }

    pub fn preserve_body(&self) -> bool {
        self.preserve_body
    }

    pub fn verbose(&self) -> bool {
        self.verbose
    }
}

impl Default for Config {
    fn default() -> Self {
        ConfigBuilder::default().build()
    }
}

static CONFIG: OnceCell<Config> = OnceCell::new();

pub fn init(config: Config) {
    CONFIG.set(config).expect("configuration already initialized")
}

pub fn get() -> &'static Config {
    CONFIG.get().expect("configuration not initialized")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn trusted_networks_config() {
        let mut builder = Config::builder();
        builder.add_trusted_network("127.0.0.1/8".parse().unwrap());
        let config = builder.build();

        assert!(config.has_trusted_networks());
        assert!(config.is_in_trusted_networks(&"127.0.0.1".parse().unwrap()));
        assert!(!config.is_in_trusted_networks(&"10.1.0.1".parse().unwrap()));
    }
}