mod auth;
mod callbacks;
mod config;
mod header;
mod resolver;
mod verify;
pub use crate::config::{
cli_opts::{CliOptions, CliOptionsBuilder},
model::{
LogDestination, LogLevel, ParseLogDestinationError, ParseLogLevelError, ParseSocketError,
ParseSyslogFacilityError, Socket, SyslogFacility,
},
};
use crate::config::{read, SessionConfig};
use indymilter::IntoListener;
use log::{error, info, LevelFilter, Log, Metadata, Record, SetLoggerError};
use std::{
future::Future,
io::{self, stderr, ErrorKind, Write},
sync::{Arc, RwLock},
};
use tokio::sync::mpsc;
use viaspf::lookup::Lookup;
pub const MILTER_NAME: &str = "SPF Milter";
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub struct Config {
cli_opts: CliOptions,
config: config::Config,
mock_resolver: Option<Box<dyn Lookup>>,
}
impl Config {
pub async fn read(opts: CliOptions) -> io::Result<Self> {
Self::read_internal(opts, None).await
}
pub async fn read_with_lookup(
opts: CliOptions,
lookup: impl Lookup + 'static,
) -> io::Result<Self> {
let mock_resolver = Box::new(lookup);
Self::read_internal(opts, Some(mock_resolver)).await
}
async fn read_internal(
opts: CliOptions,
mock_resolver: Option<Box<dyn Lookup>>,
) -> io::Result<Self> {
let config = read::read_config(&opts).await.map_err(|e| {
io::Error::new(
ErrorKind::Other,
format!(
"failed to load configuration from {}: {}",
opts.config_file().display(),
read::focus_error(&e)
),
)
})?;
Ok(Self {
cli_opts: opts,
config,
mock_resolver,
})
}
pub fn socket(&self) -> &Socket {
self.config.socket()
}
}
pub async fn run(
listener: impl IntoListener,
config: Config,
reload: mpsc::Receiver<()>,
shutdown: impl Future,
) -> io::Result<()> {
let Config { cli_opts, config, mock_resolver } = config;
match config.log_destination() {
LogDestination::Syslog => {
syslog::init_unix(config.syslog_facility().into(), config.log_level().into())
.map_err(|e| {
io::Error::new(
ErrorKind::Other,
format!("could not initialize syslog: {e}"),
)
})?;
}
LogDestination::Stderr => {
StderrLog::init(config.log_level()).map_err(|e| {
io::Error::new(
ErrorKind::Other,
format!("could not initialize stderr log: {e}"),
)
})?;
}
}
let session_config = match mock_resolver {
Some(resolver) => SessionConfig::with_mock_resolver(config, resolver),
None => SessionConfig::new(config),
};
let session_config = Arc::new(RwLock::new(Arc::new(session_config)));
spawn_reload_task(session_config.clone(), cli_opts, reload);
let callbacks = callbacks::make_callbacks(session_config);
let config = Default::default();
info!("{MILTER_NAME} {VERSION} starting");
let result = indymilter::run(listener, callbacks, config, shutdown).await;
match &result {
Ok(()) => info!("{MILTER_NAME} {VERSION} shut down"),
Err(e) => error!("{MILTER_NAME} {VERSION} terminated with error: {e}"),
}
result
}
fn spawn_reload_task(
session_config: Arc<RwLock<Arc<SessionConfig>>>,
opts: CliOptions,
mut reload: mpsc::Receiver<()>,
) {
tokio::spawn(async move {
while let Some(()) = reload.recv().await {
config::reload(&session_config, &opts).await;
}
});
}
struct StderrLog {
level: LevelFilter,
}
impl StderrLog {
fn init<L: Into<LevelFilter>>(level: L) -> Result<(), SetLoggerError> {
let level = level.into();
log::set_boxed_logger(Box::new(Self { level }))
.map(|_| log::set_max_level(level))
}
}
impl Log for StderrLog {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= self.level
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
let _ = writeln!(stderr(), "{}", record.args());
}
}
fn flush(&self) {}
}