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