use super::{RR_FAIL, RR_NEUTRAL_NONE, RR_SOFTFAIL, RR_TEMP_PERM_ERROR, SpfRecord};
use crate::Error;
use crate::sampling::is_within_pct;
use std::{fmt::Display, str::FromStr};
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum SpfResult {
Pass,
Fail,
SoftFail,
Neutral,
TempError,
PermError,
None,
}
#[derive(Debug, Eq, Clone)]
pub struct SpfOutput {
pub(crate) result: SpfResult,
pub(crate) domain: String,
pub(crate) report: Option<String>,
pub(crate) explanation: Option<String>,
pub(crate) identity: Option<SpfIdentity>,
}
impl PartialEq for SpfOutput {
fn eq(&self, other: &Self) -> bool {
self.result == other.result
&& self.domain == other.domain
&& self.report == other.report
&& self.explanation == other.explanation
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub(crate) enum SpfIdentity {
Helo,
MailFrom,
}
impl FromStr for SpfResult {
type Err = Error;
fn from_str(value: &str) -> Result<Self, Self::Err> {
hashify::map_ignore_case!(value.as_bytes(), SpfResult,
b"pass" => SpfResult::Pass,
b"fail" => SpfResult::Fail,
b"softfail" => SpfResult::SoftFail,
b"neutral" => SpfResult::Neutral,
b"temperror" => SpfResult::TempError,
b"permerror" => SpfResult::PermError,
b"none" => SpfResult::None,
)
.copied()
.ok_or(Error::Parse)
}
}
impl SpfOutput {
pub fn new(domain: String) -> Self {
SpfOutput {
result: SpfResult::None,
report: None,
explanation: None,
domain,
identity: None,
}
}
pub(crate) fn with_identity(mut self, identity: SpfIdentity) -> Self {
self.identity = Some(identity);
self
}
pub fn with_result(mut self, result: SpfResult) -> Self {
self.result = result;
self
}
pub fn with_report(mut self, spf: &SpfRecord) -> Self {
match &spf.ra {
Some(ra)
if is_within_pct(spf.rp)
&& match self.result {
SpfResult::Fail => (spf.rr & RR_FAIL) != 0,
SpfResult::SoftFail => (spf.rr & RR_SOFTFAIL) != 0,
SpfResult::Neutral | SpfResult::None => (spf.rr & RR_NEUTRAL_NONE) != 0,
SpfResult::TempError | SpfResult::PermError => {
(spf.rr & RR_TEMP_PERM_ERROR) != 0
}
SpfResult::Pass => false,
} =>
{
let ra = String::from_utf8_lossy(ra);
let mut report = String::with_capacity(ra.len() + self.domain.len() + 1);
report.push_str(ra.as_ref());
report.push('@');
report.push_str(&self.domain);
self.report = report.into();
}
_ => (),
}
self
}
pub fn with_explanation(mut self, explanation: String) -> Self {
self.explanation = explanation.into();
self
}
pub fn result(&self) -> SpfResult {
self.result
}
pub fn domain(&self) -> &str {
&self.domain
}
pub fn explanation(&self) -> Option<&str> {
self.explanation.as_deref()
}
pub fn report_address(&self) -> Option<&str> {
self.report.as_deref()
}
}
impl Default for SpfOutput {
fn default() -> Self {
Self {
result: SpfResult::None,
domain: Default::default(),
report: Default::default(),
explanation: Default::default(),
identity: None,
}
}
}
impl Display for SpfResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
SpfResult::Pass => "pass",
SpfResult::Fail => "fail",
SpfResult::SoftFail => "softfail",
SpfResult::Neutral => "neutral",
SpfResult::TempError => "temperror",
SpfResult::PermError => "permerror",
SpfResult::None => "none",
})
}
}