Skip to main content

mail_auth/report/
mod.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7pub mod arf;
8pub mod dmarc;
9pub mod tlsrpt;
10use serde::{Deserialize, Serialize};
11use std::{borrow::Cow, net::IpAddr};
12
13#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
14#[cfg_attr(
15    feature = "rkyv",
16    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
17)]
18pub struct DateRange {
19    pub begin: u64,
20    pub end: u64,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
24#[cfg_attr(
25    feature = "rkyv",
26    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
27)]
28pub struct ReportMetadata {
29    pub org_name: String,
30    pub email: String,
31    pub extra_contact_info: Option<String>,
32    pub report_id: String,
33    pub date_range: DateRange,
34    pub error: Vec<String>,
35    #[serde(default)]
36    pub generator: Option<String>,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
40#[cfg_attr(
41    feature = "rkyv",
42    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
43)]
44pub enum Alignment {
45    Relaxed,
46    Strict,
47    #[default]
48    Unspecified,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
52#[cfg_attr(
53    feature = "rkyv",
54    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
55)]
56pub enum Disposition {
57    None,
58    Quarantine,
59    Reject,
60    #[default]
61    Unspecified,
62}
63
64#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
65#[cfg_attr(
66    feature = "rkyv",
67    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
68)]
69pub enum ActionDisposition {
70    None,
71    Pass,
72    Quarantine,
73    Reject,
74    #[default]
75    Unspecified,
76}
77
78#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
79#[cfg_attr(
80    feature = "rkyv",
81    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
82)]
83pub struct PolicyPublished {
84    pub domain: String,
85    pub version_published: Option<f32>,
86    pub adkim: Alignment,
87    pub aspf: Alignment,
88    pub p: Disposition,
89    pub sp: Disposition,
90    #[serde(default)]
91    pub np: Disposition,
92    pub testing: bool,
93    #[serde(default)]
94    pub discovery_method: Discovery,
95    pub fo: Option<String>,
96}
97
98impl Eq for PolicyPublished {}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
101#[cfg_attr(
102    feature = "rkyv",
103    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
104)]
105pub enum Discovery {
106    Psl,
107    Treewalk,
108    #[default]
109    Unspecified,
110}
111
112#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
113#[cfg_attr(
114    feature = "rkyv",
115    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
116)]
117pub enum DmarcResult {
118    Pass,
119    Fail,
120    #[default]
121    Unspecified,
122}
123
124#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
125#[cfg_attr(
126    feature = "rkyv",
127    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
128)]
129pub enum PolicyOverride {
130    LocalPolicy,
131    MailingList,
132    PolicyTestMode,
133    TrustedForwarder,
134    #[default]
135    Other,
136}
137
138#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
139#[cfg_attr(
140    feature = "rkyv",
141    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
142)]
143pub struct PolicyOverrideReason {
144    pub type_: PolicyOverride,
145    pub comment: Option<String>,
146}
147
148#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
149#[cfg_attr(
150    feature = "rkyv",
151    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
152)]
153pub struct PolicyEvaluated {
154    pub disposition: ActionDisposition,
155    pub dkim: DmarcResult,
156    pub spf: DmarcResult,
157    pub reason: Vec<PolicyOverrideReason>,
158}
159
160#[derive(Debug, Clone, Hash, Default, PartialEq, Eq, Serialize, Deserialize)]
161#[cfg_attr(
162    feature = "rkyv",
163    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
164)]
165pub struct Row {
166    pub source_ip: Option<IpAddr>,
167    pub count: u32,
168    pub policy_evaluated: PolicyEvaluated,
169}
170
171#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
172#[cfg_attr(
173    feature = "rkyv",
174    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
175)]
176pub struct Extension {
177    pub name: String,
178    pub definition: String,
179}
180
181#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
182#[cfg_attr(
183    feature = "rkyv",
184    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
185)]
186pub struct Identifier {
187    pub envelope_to: Option<String>,
188    pub envelope_from: String,
189    pub header_from: String,
190}
191
192#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
193#[cfg_attr(
194    feature = "rkyv",
195    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
196)]
197pub enum DkimResult {
198    #[default]
199    None,
200    Pass,
201    Fail,
202    Policy,
203    Neutral,
204    TempError,
205    PermError,
206}
207
208#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
209#[cfg_attr(
210    feature = "rkyv",
211    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
212)]
213pub struct DKIMAuthResult {
214    pub domain: String,
215    pub selector: String,
216    pub result: DkimResult,
217    pub human_result: Option<String>,
218}
219
220#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
221#[cfg_attr(
222    feature = "rkyv",
223    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
224)]
225pub enum SPFDomainScope {
226    Helo,
227    MailFrom,
228    #[default]
229    Unspecified,
230}
231
232#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
233#[cfg_attr(
234    feature = "rkyv",
235    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
236)]
237pub enum SpfResult {
238    #[default]
239    None,
240    Neutral,
241    Pass,
242    Fail,
243    SoftFail,
244    TempError,
245    PermError,
246}
247
248#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
249#[cfg_attr(
250    feature = "rkyv",
251    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
252)]
253pub struct SPFAuthResult {
254    pub domain: String,
255    pub scope: SPFDomainScope,
256    pub result: SpfResult,
257    pub human_result: Option<String>,
258}
259
260#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
261#[cfg_attr(
262    feature = "rkyv",
263    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
264)]
265pub struct AuthResult {
266    pub dkim: Vec<DKIMAuthResult>,
267    pub spf: Vec<SPFAuthResult>,
268}
269
270#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
271#[cfg_attr(
272    feature = "rkyv",
273    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
274)]
275pub struct Record {
276    pub row: Row,
277    pub identifiers: Identifier,
278    pub auth_results: AuthResult,
279    pub extensions: Vec<Extension>,
280}
281
282#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
283#[cfg_attr(
284    feature = "rkyv",
285    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
286)]
287pub struct Report {
288    pub version: f32,
289    pub report_metadata: ReportMetadata,
290    pub policy_published: PolicyPublished,
291    pub record: Vec<Record>,
292    pub extensions: Vec<Extension>,
293}
294
295impl Eq for Report {}
296
297#[derive(Debug, Clone, PartialEq, Eq)]
298pub enum Error {
299    MailParseError,
300    ReportParseError(String),
301    UncompressError(String),
302    NoReportsFound,
303}
304
305impl From<String> for Error {
306    fn from(err: String) -> Self {
307        Error::ReportParseError(err)
308    }
309}
310
311#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
312#[cfg_attr(
313    feature = "rkyv",
314    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
315)]
316pub struct Feedback<'x> {
317    pub feedback_type: FeedbackType,
318    pub arrival_date: Option<i64>,
319    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
320    pub authentication_results: Vec<Cow<'x, str>>,
321    pub incidents: u32,
322    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
323    pub original_envelope_id: Option<Cow<'x, str>>,
324    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
325    pub original_mail_from: Option<Cow<'x, str>>,
326    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
327    pub original_rcpt_to: Option<Cow<'x, str>>,
328    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
329    pub reported_domain: Vec<Cow<'x, str>>,
330    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
331    pub reported_uri: Vec<Cow<'x, str>>,
332    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
333    pub reporting_mta: Option<Cow<'x, str>>,
334    pub source_ip: Option<IpAddr>,
335    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
336    pub user_agent: Option<Cow<'x, str>>,
337    pub version: u32,
338    pub source_port: u32,
339
340    // Auth-Failure keys
341    pub auth_failure: AuthFailureType,
342    pub delivery_result: DeliveryResult,
343    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
344    pub dkim_adsp_dns: Option<Cow<'x, str>>,
345    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
346    pub dkim_canonicalized_body: Option<Cow<'x, str>>,
347    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
348    pub dkim_canonicalized_header: Option<Cow<'x, str>>,
349    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
350    pub dkim_domain: Option<Cow<'x, str>>,
351    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
352    pub dkim_identity: Option<Cow<'x, str>>,
353    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
354    pub dkim_selector: Option<Cow<'x, str>>,
355    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
356    pub dkim_selector_dns: Option<Cow<'x, str>>,
357    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
358    pub spf_dns: Option<Cow<'x, str>>,
359    pub identity_alignment: IdentityAlignment,
360
361    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
362    pub message: Option<Cow<'x, str>>,
363    #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
364    pub headers: Option<Cow<'x, str>>,
365}
366
367#[derive(Debug, Clone, PartialEq, Eq, Copy, Serialize, Deserialize, Default)]
368#[cfg_attr(
369    feature = "rkyv",
370    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
371)]
372pub enum AuthFailureType {
373    Adsp,
374    BodyHash,
375    Revoked,
376    Signature,
377    Spf,
378    Dmarc,
379    #[default]
380    Unspecified,
381}
382
383#[derive(Debug, Clone, PartialEq, Eq, Copy, Serialize, Deserialize, Default)]
384#[cfg_attr(
385    feature = "rkyv",
386    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
387)]
388pub enum IdentityAlignment {
389    None,
390    Spf,
391    Dkim,
392    DkimSpf,
393    #[default]
394    Unspecified,
395}
396
397#[derive(Debug, Clone, PartialEq, Eq, Copy, Serialize, Deserialize, Default)]
398#[cfg_attr(
399    feature = "rkyv",
400    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
401)]
402pub enum DeliveryResult {
403    Delivered,
404    Spam,
405    Policy,
406    Reject,
407    Other,
408    #[default]
409    Unspecified,
410}
411
412#[derive(Debug, Clone, PartialEq, Eq, Copy, Serialize, Deserialize, Default)]
413#[cfg_attr(
414    feature = "rkyv",
415    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
416)]
417pub enum FeedbackType {
418    Abuse,
419    AuthFailure,
420    Fraud,
421    NotSpam,
422    #[default]
423    Other,
424    Virus,
425}
426
427impl From<&crate::DkimResult> for AuthFailureType {
428    fn from(value: &crate::DkimResult) -> Self {
429        match value {
430            crate::DkimResult::Neutral(err)
431            | crate::DkimResult::Fail(err)
432            | crate::DkimResult::PermError(err)
433            | crate::DkimResult::TempError(err) => match err {
434                crate::Error::Dkim(crate::dkim::DkimError::FailedBodyHashMatch) => {
435                    AuthFailureType::BodyHash
436                }
437                #[cfg(feature = "arc")]
438                crate::Error::Arc(crate::arc::ArcError::FailedBodyHashMatch) => {
439                    AuthFailureType::BodyHash
440                }
441                crate::Error::Dkim(crate::dkim::DkimError::RevokedPublicKey) => {
442                    AuthFailureType::Revoked
443                }
444                _ => AuthFailureType::Signature,
445            },
446            crate::DkimResult::Pass | crate::DkimResult::None => AuthFailureType::Signature,
447        }
448    }
449}