kumo_dmarc/types/
results.rs

1use crate::types::identifier::Identifier;
2use crate::types::policy::Policy;
3use crate::types::policy_override::PolicyOverrideReason;
4use bstr::BString;
5use instant_xml::{FromXml, ToXml};
6use kumo_spf::SpfDisposition;
7use mailparsing::AuthenticationResult;
8use serde::{Deserialize, Serialize};
9use std::collections::BTreeMap;
10use std::fmt;
11use std::net::IpAddr;
12
13#[derive(Debug, Eq, FromXml, PartialEq, ToXml, Serialize, Deserialize, Clone, Copy)]
14#[xml(scalar, rename_all = "lowercase")]
15pub enum SpfScope {
16    Helo,
17    Mfrom,
18}
19
20#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone)]
21pub struct SpfAuthResult {
22    domain: BString,
23    scope: SpfScope,
24    result: SpfDisposition,
25}
26
27impl ToXml for SpfAuthResult {
28    fn serialize<W: fmt::Write + ?Sized>(
29        &self,
30        field: Option<instant_xml::Id<'_>>,
31        serializer: &mut instant_xml::Serializer<W>,
32    ) -> Result<(), instant_xml::Error> {
33        #[derive(ToXml)]
34        #[xml(rename = "spf")]
35        struct SpfAuth {
36            domain: String,
37            scope: SpfScope,
38            result: SpfDisposition,
39        }
40
41        SpfAuth {
42            domain: self.domain.to_string(),
43            scope: self.scope.clone(),
44            result: self.result.clone(),
45        }
46        .serialize(field, serializer)
47    }
48}
49
50impl From<AuthenticationResult> for SpfAuthResult {
51    fn from(value: AuthenticationResult) -> Self {
52        let d = value.props.get("header.d");
53
54        Self {
55            domain: d.cloned().unwrap_or_default(),
56            scope: SpfScope::Mfrom,
57            result: value.result.into(),
58        }
59    }
60}
61
62#[derive(Debug, Eq, PartialEq, ToXml, Serialize, Deserialize, Clone)]
63#[xml(rename = "auth_results")]
64pub struct AuthResults {
65    pub(crate) dkim: Vec<DkimAuthResult>,
66    pub(crate) spf: Vec<SpfAuthResult>,
67}
68
69#[derive(Debug, Eq, PartialEq, ToXml)]
70#[xml(rename = "record")]
71pub struct Results {
72    pub(crate) row: Row,
73    pub(crate) identifiers: Identifier,
74    pub(crate) auth_results: AuthResults,
75}
76
77#[derive(Debug, Eq, PartialEq, ToXml, Serialize, Deserialize, Clone, Copy)]
78#[xml(scalar, rename_all = "lowercase")]
79pub enum DkimResult {
80    None,
81    Pass,
82    Fail,
83    Policy,
84    Neutral,
85    TempError,
86    PermError,
87}
88
89impl From<String> for DkimResult {
90    fn from(value: String) -> Self {
91        match value.to_lowercase().as_str() {
92            "none" => DkimResult::None,
93            "pass" => DkimResult::Pass,
94            "fail" => DkimResult::Fail,
95            "policy" => DkimResult::Policy,
96            "neutral" => DkimResult::Neutral,
97            "temperror" => DkimResult::TempError,
98            "permerror" => DkimResult::PermError,
99            _ => DkimResult::None,
100        }
101    }
102}
103
104#[derive(Debug, Eq, PartialEq, ToXml, Serialize, Deserialize, Clone)]
105pub struct DkimAuthResult {
106    domain: String,
107    selector: Option<String>,
108    result: DkimResult,
109    human_result: Option<String>,
110}
111
112impl From<AuthenticationResult> for DkimAuthResult {
113    fn from(value: AuthenticationResult) -> Self {
114        let d = value.props.get("header.d");
115        let s = value.props.get("header.s");
116        Self {
117            domain: d.cloned().unwrap_or_default().to_string(),
118            selector: s.cloned().map(|x| x.to_string()),
119            result: value.result.clone().into(),
120            human_result: Some(value.result.to_string()),
121        }
122    }
123}
124
125#[derive(Debug, Eq, PartialEq, Clone, Copy, ToXml, Serialize, Deserialize)]
126#[xml(scalar, rename_all = "lowercase")]
127pub enum DmarcResult {
128    Pass,
129    Fail,
130}
131
132impl DmarcResult {
133    pub fn as_str(&self) -> &'static str {
134        match self {
135            Self::Pass => "pass",
136            Self::Fail => "fail",
137        }
138    }
139}
140
141impl fmt::Display for DmarcResult {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        write!(f, "{}", self.as_str())
144    }
145}
146
147// A coverage of both success and various failure modes
148#[derive(Debug, Eq, PartialEq, ToXml, Serialize, Clone, Copy)]
149#[xml(scalar)]
150pub enum Disposition {
151    Pass,
152    None,
153    Quarantine,
154    Reject,
155    TempError,
156    PermError,
157}
158
159impl ToString for Disposition {
160    fn to_string(&self) -> String {
161        match self {
162            Disposition::None => "None".to_string(),
163            Disposition::Pass => "Pass".to_string(),
164            Disposition::Quarantine => "Quarantine".to_string(),
165            Disposition::Reject => "Reject".to_string(),
166            Disposition::TempError => "TempError".to_string(),
167            Disposition::PermError => "PermError".to_string(),
168        }
169    }
170}
171
172impl Into<Disposition> for Policy {
173    fn into(self) -> Disposition {
174        match self {
175            Policy::None => Disposition::None,
176            Policy::Quarantine => Disposition::Quarantine,
177            Policy::Reject => Disposition::Reject,
178        }
179    }
180}
181
182// A synthetic type to bundle the result with a reason
183#[derive(Debug, Eq, PartialEq, Serialize)]
184pub struct DispositionWithContext {
185    pub result: Disposition,
186    pub context: String,
187    #[serde(default)]
188    pub props: BTreeMap<String, BString>,
189}
190
191#[derive(Debug, Eq, PartialEq, ToXml, Serialize, Deserialize, Clone)]
192#[xml(rename = "policy_evaluated")]
193pub struct PolicyEvaluated {
194    pub(crate) disposition: Policy,
195    pub(crate) dkim: DmarcResult,
196    pub(crate) spf: DmarcResult,
197    pub(crate) reason: Vec<PolicyOverrideReason>,
198}
199
200#[derive(Debug, Eq, PartialEq, ToXml)]
201#[xml(rename = "row")]
202pub struct Row {
203    pub(crate) source_ip: IpAddr,
204    pub(crate) count: u64,
205    pub(crate) policy_evaluated: PolicyEvaluated,
206}