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