use crate::{
config::{model::ExplainStringMod, Config},
resolver::Resolver,
};
use std::{
borrow::Cow,
fmt::{self, Display, Formatter},
net::IpAddr,
};
use viaspf::{record::ExplainString, DomainName, Sender, SpfResult, SpfResultCause};
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum Identity {
Helo(String),
MailFrom(String),
}
impl Identity {
pub fn name(&self) -> &'static str {
match self {
Self::Helo(_) => "helo",
Self::MailFrom(_) => "mailfrom",
}
}
}
impl AsRef<str> for Identity {
fn as_ref(&self) -> &str {
match self {
Self::Helo(s) | Self::MailFrom(s) => s,
}
}
}
impl Display for Identity {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.as_ref().fmt(f)
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct VerificationResult {
pub identity: Identity,
pub spf_result: SpfResult,
pub cause: Option<SpfResultCause>,
}
impl VerificationResult {
pub fn new(identity: Identity, spf_result: SpfResult, cause: Option<SpfResultCause>) -> Self {
Self {
identity,
spf_result,
cause,
}
}
}
pub fn prepare_mail_from_identity<'a>(
mut mail_from: &'a str,
helo_host: Option<&str>,
) -> Cow<'a, str> {
if let Some(s) = mail_from
.strip_prefix('<')
.and_then(|s| s.strip_suffix('>'))
{
mail_from = s;
}
if mail_from.is_empty() {
let helo_host = helo_host.unwrap_or("unknown");
format!("postmaster@{helo_host}").into()
} else {
mail_from.into()
}
}
enum Params {
Helo(Sender),
MailFrom(Sender, Option<DomainName>),
}
pub struct Verifier<'a> {
resolver: &'a Resolver,
config: viaspf::Config,
ip: IpAddr,
params: Option<Params>,
}
impl<'a> Verifier<'a> {
pub fn new(resolver: &'a Resolver, config: &Config, hostname: &str, ip: IpAddr) -> Self {
let config = to_verifier_config(config, hostname);
Self {
resolver,
config,
ip,
params: None,
}
}
pub async fn verify_helo(&mut self, helo_host: &str) -> Option<VerificationResult> {
let sender = Sender::from_domain(helo_host).ok()?;
let helo_domain = Some(sender.domain());
let config = &self.config;
let result = match self.resolver {
Resolver::Live(resolver) => {
viaspf::evaluate_sender(resolver, config, self.ip, &sender, helo_domain).await
}
Resolver::Mock(mock) => {
viaspf::evaluate_sender(mock.as_ref(), config, self.ip, &sender, helo_domain).await
}
};
self.params = Some(Params::Helo(sender));
let id = Identity::Helo(helo_host.into());
Some(VerificationResult::new(id, result.spf_result, result.cause))
}
pub async fn verify_mail_from(
&mut self,
mail_from: &str,
helo_host: Option<&str>,
) -> VerificationResult {
let id = Identity::MailFrom(mail_from.into());
let sender = match Sender::from_address(mail_from) {
Ok(sender) => sender,
Err(_) => return VerificationResult::new(id, SpfResult::None, None),
};
let helo_domain = helo_host.and_then(|s| s.parse().ok());
let config = &self.config;
let result = match self.resolver {
Resolver::Live(resolver) => {
viaspf::evaluate_sender(resolver, config, self.ip, &sender, helo_domain.as_ref())
.await
}
Resolver::Mock(mock) => {
viaspf::evaluate_sender(mock.as_ref(), config, self.ip, &sender, helo_domain.as_ref())
.await
}
};
self.params = Some(Params::MailFrom(sender, helo_domain));
VerificationResult::new(id, result.spf_result, result.cause)
}
pub async fn expand_explain_string(&self, exp: &ExplainString) -> String {
let params = self.params.as_ref().expect("verification not done");
let (sender, helo_domain) = match params {
Params::Helo(sender) => (sender, Some(sender.domain())),
Params::MailFrom(sender, helo_domain) => (sender, helo_domain.as_ref()),
};
let config = &self.config;
let result = match self.resolver {
Resolver::Live(resolver) => {
viaspf::expand_explain_string(resolver, config, exp, self.ip, sender, helo_domain)
.await
}
Resolver::Mock(mock) => {
viaspf::expand_explain_string(mock.as_ref(), config, exp, self.ip, sender, helo_domain)
.await
}
};
result.expansion
}
}
fn to_verifier_config(config: &Config, hostname: &str) -> viaspf::Config {
let mut builder = viaspf::Config::builder()
.hostname(hostname)
.timeout(config.timeout())
.max_lookups(config.max_lookups())
.max_void_lookups(config.max_void_lookups());
match config.fail_reply_text_exp().as_ref() {
ExplainStringMod::Substitute(explain_string) => {
let explain_string = explain_string.to_owned();
builder = builder.modify_exp_with(move |exp| {
exp.segments = explain_string.segments.clone();
});
}
ExplainStringMod::Decorate { prefix, suffix } => {
let (prefix, suffix) = (prefix.to_owned(), suffix.to_owned());
builder = builder.modify_exp_with(move |exp| {
exp.segments.splice(..0, prefix.segments.iter().cloned());
exp.segments.extend(suffix.segments.iter().cloned());
});
}
}
builder.build()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::model::Socket;
#[test]
fn to_verifier_config_exp_explain_string() {
let config = Config::builder(Socket::Inet("unused".into()))
.fail_reply_text_exp(ExplainStringMod::Decorate {
prefix: "<".parse().unwrap(),
suffix: ">".parse().unwrap(),
})
.build()
.unwrap();
let config = to_verifier_config(&config, "unused");
let f = config.modify_exp_fn().unwrap();
let mut explain_string = "denied".parse().unwrap();
f(&mut explain_string);
assert_eq!(
explain_string.to_string(),
"<denied>".parse::<ExplainString>().unwrap().to_string()
);
}
}