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#[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#[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}