pub mod cli_opts;
pub mod model;
pub mod read;
use crate::{
config::{
cli_opts::CliOptions,
model::{
DefinitiveHeloResults, EnhancedStatusCode, ExpExplainString, ExplainStringMod, Header,
HeaderType, LogDestination, LogLevel, ReasonExplainString, RejectResults, ReplyCode,
SkipSenders, Socket, SyslogFacility, TrustedNetworks,
},
read::ReadConfigError,
},
resolver::{DomainResolver, Resolver},
};
use log::{error, info, warn};
use once_cell::sync::Lazy;
use std::{
error::Error,
fmt::{self, Display, Formatter},
mem,
sync::{Arc, RwLock},
time::Duration,
};
use viaspf::{lookup::Lookup, record::ExplainString};
pub struct SessionConfig {
pub config: Config,
pub resolver: Resolver,
}
impl SessionConfig {
pub fn new(config: Config) -> Self {
let resolver = Resolver::Live(DomainResolver::new(config.timeout()));
Self { config, resolver }
}
pub fn with_mock_resolver(config: Config, resolver: Box<dyn Lookup>) -> Self {
let resolver = Resolver::Mock(Arc::new(resolver));
Self { config, resolver }
}
}
pub async fn reload(current_session_config: &RwLock<Arc<SessionConfig>>, opts: &CliOptions) {
let config_file = opts.config_file();
let config = match read::read_config(opts).await {
Ok(config) => config,
Err(e) => {
error!(
"failed to reload configuration from {}: {}",
config_file.display(),
read::focus_error(&e)
);
return;
}
};
let new_log_destination = config.log_destination();
let new_log_level = config.log_level();
let new_socket = config.socket().clone();
let new_syslog_facility = config.syslog_facility();
let old_session_config = {
let mut locked_session_config = current_session_config
.write()
.expect("could not get configuration write lock");
let resolver = match &locked_session_config.resolver {
Resolver::Live(_) => Resolver::Live(DomainResolver::new(config.timeout())),
Resolver::Mock(m) => Resolver::Mock(m.clone()),
};
let session_config = SessionConfig { config, resolver };
mem::replace(&mut *locked_session_config, Arc::new(session_config))
};
info!("configuration reloaded from {}", config_file.display());
if new_log_destination != old_session_config.config.log_destination() {
warn_changed_param("log_destination");
}
if new_log_level != old_session_config.config.log_level() {
warn_changed_param("log_level");
}
if new_socket != *old_session_config.config.socket() {
warn_changed_param("socket");
}
if new_syslog_facility != old_session_config.config.syslog_facility() {
warn_changed_param("syslog_facility");
}
}
fn warn_changed_param(name: &str) {
warn!("parameter \"{name}\" changed, restart needed");
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Config {
authserv_id: Option<String>,
definitive_helo_results: DefinitiveHeloResults,
delete_incoming_authentication_results: bool,
dry_run: bool,
fail_reply_code: ReplyCode,
fail_reply_text: ExplainString,
fail_reply_text_exp: ExpExplainString,
fail_status_code: EnhancedStatusCode,
header: Header,
hostname: Option<String>,
include_all_results: bool,
include_mailfrom_local_part: bool,
log_destination: LogDestination,
log_level: LogLevel,
max_lookups: usize,
max_void_lookups: usize,
permerror_reply_code: ReplyCode,
permerror_reply_text: ReasonExplainString,
permerror_status_code: EnhancedStatusCode,
reject_helo_results: RejectResults,
reject_results: RejectResults,
skip_senders: SkipSenders,
socket: Socket,
softfail_reply_code: ReplyCode,
softfail_reply_text: ExplainString,
softfail_status_code: EnhancedStatusCode,
syslog_facility: SyslogFacility,
temperror_reply_code: ReplyCode,
temperror_reply_text: ReasonExplainString,
temperror_status_code: EnhancedStatusCode,
timeout: Duration,
trust_authenticated_senders: bool,
trusted_networks: TrustedNetworks,
verify_helo: bool,
}
impl Config {
pub fn builder(socket: Socket) -> ConfigBuilder {
ConfigBuilder::new(socket)
}
pub fn authserv_id(&self) -> Option<&str> {
self.authserv_id.as_deref()
}
pub fn definitive_helo_results(&self) -> &DefinitiveHeloResults {
&self.definitive_helo_results
}
pub fn delete_incoming_authentication_results(&self) -> bool {
self.delete_incoming_authentication_results
}
pub fn dry_run(&self) -> bool {
self.dry_run
}
pub fn fail_reply_code(&self) -> &ReplyCode {
&self.fail_reply_code
}
pub fn fail_reply_text(&self) -> &ExplainString {
&self.fail_reply_text
}
pub fn fail_reply_text_exp(&self) -> &ExpExplainString {
&self.fail_reply_text_exp
}
pub fn fail_status_code(&self) -> &EnhancedStatusCode {
&self.fail_status_code
}
pub fn header(&self) -> &Header {
&self.header
}
pub fn hostname(&self) -> Option<&str> {
self.hostname.as_deref()
}
pub fn include_all_results(&self) -> bool {
self.include_all_results
}
pub fn include_mailfrom_local_part(&self) -> bool {
self.include_mailfrom_local_part
}
pub fn log_destination(&self) -> LogDestination {
self.log_destination
}
pub fn log_level(&self) -> LogLevel {
self.log_level
}
pub fn max_lookups(&self) -> usize {
self.max_lookups
}
pub fn max_void_lookups(&self) -> usize {
self.max_void_lookups
}
pub fn permerror_reply_code(&self) -> &ReplyCode {
&self.permerror_reply_code
}
pub fn permerror_reply_text(&self) -> &ReasonExplainString {
&self.permerror_reply_text
}
pub fn permerror_status_code(&self) -> &EnhancedStatusCode {
&self.permerror_status_code
}
pub fn reject_helo_results(&self) -> &RejectResults {
&self.reject_helo_results
}
pub fn reject_results(&self) -> &RejectResults {
&self.reject_results
}
pub fn skip_senders(&self) -> &SkipSenders {
&self.skip_senders
}
pub fn socket(&self) -> &Socket {
&self.socket
}
pub fn softfail_reply_code(&self) -> &ReplyCode {
&self.softfail_reply_code
}
pub fn softfail_reply_text(&self) -> &ExplainString {
&self.softfail_reply_text
}
pub fn softfail_status_code(&self) -> &EnhancedStatusCode {
&self.softfail_status_code
}
pub fn syslog_facility(&self) -> SyslogFacility {
self.syslog_facility
}
pub fn temperror_reply_code(&self) -> &ReplyCode {
&self.temperror_reply_code
}
pub fn temperror_reply_text(&self) -> &ReasonExplainString {
&self.temperror_reply_text
}
pub fn temperror_status_code(&self) -> &EnhancedStatusCode {
&self.temperror_status_code
}
pub fn timeout(&self) -> Duration {
self.timeout
}
pub fn trust_authenticated_senders(&self) -> bool {
self.trust_authenticated_senders
}
pub fn trusted_networks(&self) -> &TrustedNetworks {
&self.trusted_networks
}
pub fn verify_helo(&self) -> bool {
self.verify_helo
}
}
#[derive(Debug)]
pub enum ConfigError {
ReadConfig(ReadConfigError),
MissingMandatoryParam(String),
TypeConversion(String),
IncompatibleStatusCodes(ReplyCode, EnhancedStatusCode, String),
}
impl Error for ConfigError {}
impl Display for ConfigError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::ReadConfig(e) => write!(f, "failed to read configuration: {e}"),
Self::MissingMandatoryParam(s) => {
write!(f, "missing mandatory configuration parameter \"{s}\"")
}
Self::TypeConversion(s) => {
write!(f, "failed to convert value of configuration parameter \"{s}\"")
}
Self::IncompatibleStatusCodes(rc, esc, s) => {
write!(f, "incompatible reply status codes {rc} {esc} for result \"{s}\"")
}
}
}
}
static DEFAULT_ERROR_REPLY_CODE: Lazy<ReplyCode> = Lazy::new(|| "550".parse().unwrap());
static DEFAULT_TEMPERROR_REPLY_CODE: Lazy<ReplyCode> = Lazy::new(|| "451".parse().unwrap());
static DEFAULT_FAIL_STATUS_CODE: Lazy<EnhancedStatusCode> = Lazy::new(|| "5.7.23".parse().unwrap());
static DEFAULT_PERMERROR_STATUS_CODE: Lazy<EnhancedStatusCode> = Lazy::new(|| "5.7.24".parse().unwrap());
static DEFAULT_TEMPERROR_STATUS_CODE: Lazy<EnhancedStatusCode> = Lazy::new(|| "4.7.24".parse().unwrap());
static DEFAULT_FAIL_REPLY_TEXT: Lazy<ExplainString> =
Lazy::new(|| "SPF validation failed".parse().unwrap());
static DEFAULT_FAIL_REPLY_TEXT_EXP_PREFIX: Lazy<ExplainString> =
Lazy::new(|| "SPF validation failed: %{o} explains: ".parse().unwrap());
static DEFAULT_ERROR_REPLY_TEXT_PREFIX: Lazy<ExplainString> =
Lazy::new(|| "SPF validation error: ".parse().unwrap());
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConfigBuilder {
authserv_id: Option<String>,
definitive_helo_results: Option<DefinitiveHeloResults>,
delete_incoming_authentication_results: Option<bool>,
dry_run: Option<bool>,
fail_reply_code: Option<ReplyCode>,
fail_reply_text: Option<ExplainString>,
fail_reply_text_exp: Option<ExpExplainString>,
fail_status_code: Option<EnhancedStatusCode>,
header: Option<Header>,
hostname: Option<String>,
include_all_results: Option<bool>,
include_mailfrom_local_part: Option<bool>,
log_destination: Option<LogDestination>,
log_level: Option<LogLevel>,
max_lookups: Option<usize>,
max_void_lookups: Option<usize>,
permerror_reply_code: Option<ReplyCode>,
permerror_reply_text: Option<ReasonExplainString>,
permerror_status_code: Option<EnhancedStatusCode>,
reject_helo_results: Option<RejectResults>,
reject_results: Option<RejectResults>,
skip_senders: Option<SkipSenders>,
socket: Socket,
softfail_reply_code: Option<ReplyCode>,
softfail_reply_text: Option<ExplainString>,
softfail_status_code: Option<EnhancedStatusCode>,
syslog_facility: Option<SyslogFacility>,
temperror_reply_code: Option<ReplyCode>,
temperror_reply_text: Option<ReasonExplainString>,
temperror_status_code: Option<EnhancedStatusCode>,
timeout: Option<Duration>,
trust_authenticated_senders: Option<bool>,
trusted_networks: Option<TrustedNetworks>,
verify_helo: Option<bool>,
}
impl ConfigBuilder {
pub fn new(socket: Socket) -> Self {
Self {
authserv_id: Default::default(),
definitive_helo_results: Default::default(),
delete_incoming_authentication_results: Default::default(),
dry_run: Default::default(),
fail_reply_code: Default::default(),
fail_reply_text: Default::default(),
fail_reply_text_exp: Default::default(),
fail_status_code: Default::default(),
header: Default::default(),
hostname: Default::default(),
include_all_results: Default::default(),
include_mailfrom_local_part: Default::default(),
log_destination: Default::default(),
log_level: Default::default(),
max_lookups: Default::default(),
max_void_lookups: Default::default(),
permerror_reply_code: Default::default(),
permerror_reply_text: Default::default(),
permerror_status_code: Default::default(),
reject_helo_results: Default::default(),
reject_results: Default::default(),
skip_senders: Default::default(),
socket,
softfail_reply_code: Default::default(),
softfail_reply_text: Default::default(),
softfail_status_code: Default::default(),
syslog_facility: Default::default(),
temperror_reply_code: Default::default(),
temperror_reply_text: Default::default(),
temperror_status_code: Default::default(),
timeout: Default::default(),
trust_authenticated_senders: Default::default(),
trusted_networks: Default::default(),
verify_helo: Default::default(),
}
}
pub fn authserv_id<S: Into<String>>(mut self, value: S) -> Self {
self.authserv_id = Some(value.into());
self
}
pub fn definitive_helo_results<T: Into<DefinitiveHeloResults>>(mut self, value: T) -> Self {
self.definitive_helo_results = Some(value.into());
self
}
pub fn delete_incoming_authentication_results(mut self, value: bool) -> Self {
self.delete_incoming_authentication_results = Some(value);
self
}
pub fn dry_run(mut self, value: bool) -> Self {
self.dry_run = Some(value);
self
}
pub fn fail_reply_code(mut self, value: ReplyCode) -> Self {
self.fail_reply_code = Some(value);
self
}
pub fn fail_reply_text<S: Into<ExplainString>>(mut self, value: S) -> Self {
self.fail_reply_text = Some(value.into());
self
}
pub fn fail_reply_text_exp<S: Into<ExpExplainString>>(mut self, value: S) -> Self {
self.fail_reply_text_exp = Some(value.into());
self
}
pub fn fail_status_code(mut self, value: EnhancedStatusCode) -> Self {
self.fail_status_code = Some(value);
self
}
pub fn header<T: Into<Header>>(mut self, value: T) -> Self {
self.header = Some(value.into());
self
}
pub fn hostname<S: Into<String>>(mut self, value: S) -> Self {
self.hostname = Some(value.into());
self
}
pub fn include_all_results(mut self, value: bool) -> Self {
self.include_all_results = Some(value);
self
}
pub fn include_mailfrom_local_part(mut self, value: bool) -> Self {
self.include_mailfrom_local_part = Some(value);
self
}
pub fn log_destination(mut self, value: LogDestination) -> Self {
self.log_destination = Some(value);
self
}
pub fn log_level(mut self, value: LogLevel) -> Self {
self.log_level = Some(value);
self
}
pub fn max_lookups(mut self, value: usize) -> Self {
self.max_lookups = Some(value);
self
}
pub fn max_void_lookups(mut self, value: usize) -> Self {
self.max_void_lookups = Some(value);
self
}
pub fn permerror_reply_code(mut self, value: ReplyCode) -> Self {
self.permerror_reply_code = Some(value);
self
}
pub fn permerror_reply_text<S: Into<ReasonExplainString>>(mut self, value: S) -> Self {
self.permerror_reply_text = Some(value.into());
self
}
pub fn permerror_status_code(mut self, value: EnhancedStatusCode) -> Self {
self.permerror_status_code = Some(value);
self
}
pub fn reject_helo_results<T: Into<RejectResults>>(mut self, value: T) -> Self {
self.reject_helo_results = Some(value.into());
self
}
pub fn reject_results<T: Into<RejectResults>>(mut self, value: T) -> Self {
self.reject_results = Some(value.into());
self
}
pub fn skip_senders<S: Into<SkipSenders>>(mut self, value: S) -> Self {
self.skip_senders = Some(value.into());
self
}
pub fn softfail_reply_code(mut self, value: ReplyCode) -> Self {
self.softfail_reply_code = Some(value);
self
}
pub fn softfail_reply_text(mut self, value: ExplainString) -> Self {
self.softfail_reply_text = Some(value);
self
}
pub fn softfail_status_code(mut self, value: EnhancedStatusCode) -> Self {
self.softfail_status_code = Some(value);
self
}
pub fn syslog_facility(mut self, value: SyslogFacility) -> Self {
self.syslog_facility = Some(value);
self
}
pub fn temperror_reply_code(mut self, value: ReplyCode) -> Self {
self.temperror_reply_code = Some(value);
self
}
pub fn temperror_reply_text<S: Into<ReasonExplainString>>(mut self, value: S) -> Self {
self.temperror_reply_text = Some(value.into());
self
}
pub fn temperror_status_code(mut self, value: EnhancedStatusCode) -> Self {
self.temperror_status_code = Some(value);
self
}
pub fn timeout(mut self, value: Duration) -> Self {
self.timeout = Some(value);
self
}
pub fn trust_authenticated_senders(mut self, value: bool) -> Self {
self.trust_authenticated_senders = Some(value);
self
}
pub fn trusted_networks(mut self, value: TrustedNetworks) -> Self {
self.trusted_networks = Some(value);
self
}
pub fn verify_helo(mut self, value: bool) -> Self {
self.verify_helo = Some(value);
self
}
pub fn build(self) -> Result<Config, ConfigError> {
let max_lookups = self.max_lookups.unwrap_or(10);
let max_void_lookups = self.max_void_lookups.unwrap_or(2);
let timeout = self.timeout.unwrap_or_else(|| Duration::from_secs(20));
let trust_authenticated_senders = self.trust_authenticated_senders.unwrap_or(true);
let verify_helo = self.verify_helo.unwrap_or(true);
let header = self.header.unwrap_or_default();
let delete_incoming_authentication_results = self
.delete_incoming_authentication_results
.unwrap_or_else(|| {
header.iter().any(|&h| h == HeaderType::AuthenticationResults)
});
let reject_results = self.reject_results.unwrap_or_default();
let reject_helo_results = self
.reject_helo_results
.unwrap_or_else(|| reject_results.clone());
let (fail_reply_code, fail_status_code) = ensure_compatible(
self.fail_reply_code.unwrap_or_else(|| DEFAULT_ERROR_REPLY_CODE.clone()),
self.fail_status_code.unwrap_or_else(|| DEFAULT_FAIL_STATUS_CODE.clone()),
"fail",
)?;
let fail_reply_text = self
.fail_reply_text
.unwrap_or_else(|| DEFAULT_FAIL_REPLY_TEXT.clone());
let fail_reply_text_exp = self
.fail_reply_text_exp
.unwrap_or_else(|| ExpExplainString(ExplainStringMod::Decorate {
prefix: DEFAULT_FAIL_REPLY_TEXT_EXP_PREFIX.clone(),
suffix: Default::default(),
}));
let (softfail_reply_code, softfail_status_code) = ensure_compatible(
self.softfail_reply_code.unwrap_or_else(|| DEFAULT_ERROR_REPLY_CODE.clone()),
self.softfail_status_code.unwrap_or_else(|| DEFAULT_FAIL_STATUS_CODE.clone()),
"softfail",
)?;
let softfail_reply_text = self
.softfail_reply_text
.unwrap_or_else(|| DEFAULT_FAIL_REPLY_TEXT.clone());
let (temperror_reply_code, temperror_status_code) = ensure_compatible(
self.temperror_reply_code.unwrap_or_else(|| DEFAULT_TEMPERROR_REPLY_CODE.clone()),
self.temperror_status_code.unwrap_or_else(|| DEFAULT_TEMPERROR_STATUS_CODE.clone()),
"temperror",
)?;
let temperror_reply_text = self
.temperror_reply_text
.unwrap_or_else(|| ReasonExplainString(ExplainStringMod::Decorate {
prefix: DEFAULT_ERROR_REPLY_TEXT_PREFIX.clone(),
suffix: Default::default(),
}));
let (permerror_reply_code, permerror_status_code) = ensure_compatible(
self.permerror_reply_code.unwrap_or_else(|| DEFAULT_ERROR_REPLY_CODE.clone()),
self.permerror_status_code.unwrap_or_else(|| DEFAULT_PERMERROR_STATUS_CODE.clone()),
"permerror",
)?;
let permerror_reply_text = self
.permerror_reply_text
.unwrap_or_else(|| ReasonExplainString(ExplainStringMod::Decorate {
prefix: DEFAULT_ERROR_REPLY_TEXT_PREFIX.clone(),
suffix: Default::default(),
}));
Ok(Config {
authserv_id: self.authserv_id,
definitive_helo_results: self.definitive_helo_results.unwrap_or_default(),
delete_incoming_authentication_results,
dry_run: self.dry_run.unwrap_or_default(),
fail_reply_code,
fail_reply_text,
fail_reply_text_exp,
fail_status_code,
header,
hostname: self.hostname,
include_all_results: self.include_all_results.unwrap_or_default(),
include_mailfrom_local_part: self.include_mailfrom_local_part.unwrap_or_default(),
log_destination: self.log_destination.unwrap_or_default(),
log_level: self.log_level.unwrap_or_default(),
max_lookups,
max_void_lookups,
permerror_reply_code,
permerror_reply_text,
permerror_status_code,
reject_helo_results,
reject_results,
skip_senders: self.skip_senders.unwrap_or_default(),
socket: self.socket,
softfail_reply_code,
softfail_reply_text,
softfail_status_code,
syslog_facility: self.syslog_facility.unwrap_or_default(),
temperror_reply_code,
temperror_reply_text,
temperror_status_code,
timeout,
trust_authenticated_senders,
trusted_networks: self.trusted_networks.unwrap_or_default(),
verify_helo,
})
}
}
fn ensure_compatible(
reply_code: ReplyCode,
status_code: EnhancedStatusCode,
result_kind: &str,
) -> Result<(ReplyCode, EnhancedStatusCode), ConfigError> {
if status_code.is_compatible_with(&reply_code) {
Ok((reply_code, status_code))
} else {
Err(ConfigError::IncompatibleStatusCodes(
reply_code,
status_code,
result_kind.into(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn delete_incoming_authentication_results_default_ok() {
let config = Config::builder("unix:unused".parse().unwrap())
.header(HeaderType::ReceivedSpf)
.build()
.unwrap();
assert!(!config.delete_incoming_authentication_results());
let config = Config::builder("unix:unused".parse().unwrap())
.header(HeaderType::AuthenticationResults)
.build()
.unwrap();
assert!(config.delete_incoming_authentication_results());
}
}